From 7feba7e49816485c43fb7bb46c2895317eb56c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Tue, 7 Nov 2017 13:07:11 +0100 Subject: [PATCH 001/282] basic setup for 360 screenshots --- apps/openmw/mwbase/world.hpp | 1 + apps/openmw/mwinput/inputmanagerimp.cpp | 33 +- apps/openmw/mwmechanics/character.cpp.orig | 2464 ++++++++++++++++++++ apps/openmw/mwmechanics/character.cpp.rej | 46 + apps/openmw/mwrender/renderingmanager.cpp | 5 + apps/openmw/mwrender/renderingmanager.hpp | 1 + apps/openmw/mwstate/statemanagerimp.cpp | 1 - apps/openmw/mwworld/worldimp.cpp | 5 + apps/openmw/mwworld/worldimp.hpp | 1 + 9 files changed, 2553 insertions(+), 4 deletions(-) create mode 100644 apps/openmw/mwmechanics/character.cpp.orig create mode 100644 apps/openmw/mwmechanics/character.cpp.rej diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 038535939..80a9e11a1 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -450,6 +450,7 @@ namespace MWBase /// \todo this does not belong here virtual void screenshot (osg::Image* image, int w, int h) = 0; + virtual void screenshot360 (osg::Image* image, int w) = 0; /// Find default position inside exterior cell specified by name /// \return false if exterior with given name not exists, true otherwise diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index a5bb93b6c..192eadbde 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1,6 +1,7 @@ #include "inputmanagerimp.hpp" #include +#include #include #include @@ -1014,10 +1015,36 @@ namespace MWInput void InputManager::screenshot() { - mScreenCaptureHandler->setFramesToCapture(1); - mScreenCaptureHandler->captureNextFrame(*mViewer); + // MOVE THIS ELSEWHERE LATER! + int screenshotW = 512; + osg::ref_ptr screenshot (new osg::Image); + MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(), screenshotW); - MWBase::Environment::get().getWindowManager()->messageBox ("Screenshot saved"); + osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg"); + + if (!readerwriter) + { + std::cerr << "Error: Unable to write screenshot, can't find a jpg ReaderWriter" << std::endl; + return; + } + + std::ofstream outfile; + outfile.open("test.jpg"); + + osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*screenshot, outfile); + + if (!result.success()) + { + outfile << "Error: Unable to write screenshot: " << result.message() << " code " << result.status() << std::endl; + return; + } + + outfile.close(); + +// mScreenCaptureHandler->setFramesToCapture(1); +// mScreenCaptureHandler->captureNextFrame(*mViewer); + +// MWBase::Environment::get().getWindowManager()->messageBox ("Screenshot saved"); } void InputManager::toggleInventory() diff --git a/apps/openmw/mwmechanics/character.cpp.orig b/apps/openmw/mwmechanics/character.cpp.orig new file mode 100644 index 000000000..f262850a4 --- /dev/null +++ b/apps/openmw/mwmechanics/character.cpp.orig @@ -0,0 +1,2464 @@ +/* + * OpenMW - The completely unofficial reimplementation of Morrowind + * + * This file (character.cpp) is part of the OpenMW package. + * + * OpenMW is distributed as free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * version 3, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 3 along with this program. If not, see + * http://www.gnu.org/licenses/ . + */ + +#include "character.hpp" + +#include + +#include + +#include + +#include + +#include "../mwrender/animation.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" +#include "../mwworld/esmstore.hpp" +#include "../mwworld/player.hpp" + +#include "movement.hpp" +#include "npcstats.hpp" +#include "creaturestats.hpp" +#include "security.hpp" +#include "actorutil.hpp" +#include "spellcasting.hpp" + +namespace +{ + +// Wraps a value to (-PI, PI] +void wrap(float& rad) +{ + if (rad>0) + rad = std::fmod(rad+osg::PI, 2.0f*osg::PI)-osg::PI; + else + rad = std::fmod(rad-osg::PI, 2.0f*osg::PI)+osg::PI; +} + +std::string toString(int num) +{ + std::ostringstream stream; + stream << num; + return stream.str(); +} + +std::string getBestAttack (const ESM::Weapon* weapon) +{ + int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; + int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; + int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; + if (slash == chop && slash == thrust) + return "slash"; + else if (thrust >= chop && thrust >= slash) + return "thrust"; + else if (slash >= chop && slash >= thrust) + return "slash"; + else + return "chop"; +} + +// Converts a movement Run state to its equivalent Walk state. +MWMechanics::CharacterState runStateToWalkState (MWMechanics::CharacterState state) +{ + using namespace MWMechanics; + CharacterState ret = state; + switch (state) + { + case CharState_RunForward: + ret = CharState_WalkForward; + break; + case CharState_RunBack: + ret = CharState_WalkBack; + break; + case CharState_RunLeft: + ret = CharState_WalkLeft; + break; + case CharState_RunRight: + ret = CharState_WalkRight; + break; + case CharState_SwimRunForward: + ret = CharState_SwimWalkForward; + break; + case CharState_SwimRunBack: + ret = CharState_SwimWalkBack; + break; + case CharState_SwimRunLeft: + ret = CharState_SwimWalkLeft; + break; + case CharState_SwimRunRight: + ret = CharState_SwimWalkRight; + break; + default: + break; + } + return ret; +} + +float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight) +{ + MWBase::World *world = MWBase::Environment::get().getWorld(); + const MWWorld::Store &store = world->getStore().get(); + + const float fallDistanceMin = store.find("fFallDamageDistanceMin")->getFloat(); + + if (fallHeight >= fallDistanceMin) + { + const float acrobaticsSkill = static_cast(ptr.getClass().getSkill(ptr, ESM::Skill::Acrobatics)); + const float jumpSpellBonus = ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Jump).getMagnitude(); + const float fallAcroBase = store.find("fFallAcroBase")->getFloat(); + const float fallAcroMult = store.find("fFallAcroMult")->getFloat(); + const float fallDistanceBase = store.find("fFallDistanceBase")->getFloat(); + const float fallDistanceMult = store.find("fFallDistanceMult")->getFloat(); + + float x = fallHeight - fallDistanceMin; + x -= (1.5f * acrobaticsSkill) + jumpSpellBonus; + x = std::max(0.0f, x); + + float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill); + x = fallDistanceBase + fallDistanceMult * x; + x *= a; + + return x; + } + return 0.f; +} + +} + +namespace MWMechanics +{ + +struct StateInfo { + CharacterState state; + const char groupname[32]; +}; + +static const StateInfo sMovementList[] = { + { CharState_WalkForward, "walkforward" }, + { CharState_WalkBack, "walkback" }, + { CharState_WalkLeft, "walkleft" }, + { CharState_WalkRight, "walkright" }, + + { CharState_SwimWalkForward, "swimwalkforward" }, + { CharState_SwimWalkBack, "swimwalkback" }, + { CharState_SwimWalkLeft, "swimwalkleft" }, + { CharState_SwimWalkRight, "swimwalkright" }, + + { CharState_RunForward, "runforward" }, + { CharState_RunBack, "runback" }, + { CharState_RunLeft, "runleft" }, + { CharState_RunRight, "runright" }, + + { CharState_SwimRunForward, "swimrunforward" }, + { CharState_SwimRunBack, "swimrunback" }, + { CharState_SwimRunLeft, "swimrunleft" }, + { CharState_SwimRunRight, "swimrunright" }, + + { CharState_SneakForward, "sneakforward" }, + { CharState_SneakBack, "sneakback" }, + { CharState_SneakLeft, "sneakleft" }, + { CharState_SneakRight, "sneakright" }, + + { CharState_Jump, "jump" }, + + { CharState_TurnLeft, "turnleft" }, + { CharState_TurnRight, "turnright" }, + { CharState_SwimTurnLeft, "swimturnleft" }, + { CharState_SwimTurnRight, "swimturnright" }, +}; +static const StateInfo *sMovementListEnd = &sMovementList[sizeof(sMovementList)/sizeof(sMovementList[0])]; + + +class FindCharState { + CharacterState state; + +public: + FindCharState(CharacterState _state) : state(_state) { } + + bool operator()(const StateInfo &info) const + { return info.state == state; } +}; + + +static const struct WeaponInfo { + WeaponType type; + const char shortgroup[16]; + const char longgroup[16]; +} sWeaponTypeList[] = { + { WeapType_HandToHand, "hh", "handtohand" }, + { WeapType_OneHand, "1h", "weapononehand" }, + { WeapType_TwoHand, "2c", "weapontwohand" }, + { WeapType_TwoWide, "2w", "weapontwowide" }, + { WeapType_BowAndArrow, "1h", "bowandarrow" }, + { WeapType_Crossbow, "crossbow", "crossbow" }, + { WeapType_Thrown, "1h", "throwweapon" }, + { WeapType_PickProbe, "1h", "pickprobe" }, + { WeapType_Spell, "spell", "spellcast" }, +}; +static const WeaponInfo *sWeaponTypeListEnd = &sWeaponTypeList[sizeof(sWeaponTypeList)/sizeof(sWeaponTypeList[0])]; + +class FindWeaponType { + WeaponType type; + +public: + FindWeaponType(WeaponType _type) : type(_type) { } + + bool operator()(const WeaponInfo &weap) const + { return weap.type == type; } +}; + +std::string CharacterController::chooseRandomGroup (const std::string& prefix, int* num) const +{ + int numAnims=0; + while (mAnimation->hasAnimation(prefix + toString(numAnims+1))) + ++numAnims; + + int roll = Misc::Rng::rollDice(numAnims) + 1; // [1, numAnims] + if (num) + *num = roll; + return prefix + toString(roll); +} + +void CharacterController::refreshHitRecoilAnims() +{ + bool recovery = mPtr.getClass().getCreatureStats(mPtr).getHitRecovery(); + bool knockdown = mPtr.getClass().getCreatureStats(mPtr).getKnockedDown(); + bool block = mPtr.getClass().getCreatureStats(mPtr).getBlock(); + bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); + if(mHitState == CharState_None) + { + if ((mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0 + || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0) + && mAnimation->hasAnimation("knockout")) + { + if (isSwimming && mAnimation->hasAnimation("swimknockout")) + { + mHitState = CharState_SwimKnockOut; + mCurrentHit = "swimknockout"; + } + else + { + mHitState = CharState_KnockOut; + mCurrentHit = "knockout"; + } + + mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul); + mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(true); + } + else if(knockdown && mAnimation->hasAnimation("knockdown")) + { + if (isSwimming && mAnimation->hasAnimation("swimknockdown")) + { + mHitState = CharState_SwimKnockDown; + mCurrentHit = "swimknockdown"; + } + else + { + mHitState = CharState_KnockDown; + mCurrentHit = "knockdown"; + } + + mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); + } + else if (recovery) + { + std::string anim = isSwimming ? chooseRandomGroup("swimhit") : chooseRandomGroup("hit"); + if (isSwimming && mAnimation->hasAnimation(anim)) + { + mHitState = CharState_SwimHit; + mCurrentHit = anim; + mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); + } + else + { + anim = chooseRandomGroup("hit"); + if (mAnimation->hasAnimation(anim)) + { + mHitState = CharState_Hit; + mCurrentHit = anim; + mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); + } + } + } + else if (block && mAnimation->hasAnimation("shield")) + { + mHitState = CharState_Block; + mCurrentHit = "shield"; + MWRender::Animation::AnimPriority priorityBlock (Priority_Hit); + priorityBlock[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block; + mAnimation->play(mCurrentHit, priorityBlock, MWRender::Animation::BlendMask_All, true, 1, "block start", "block stop", 0.0f, 0); + } + + // Cancel upper body animations + if (isKnockedOut() || isKnockedDown()) + { + if (mUpperBodyState > UpperCharState_WeapEquiped) + { + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperCharState_WeapEquiped; + } + else if (mUpperBodyState > UpperCharState_Nothing && mUpperBodyState < UpperCharState_WeapEquiped) + { + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperCharState_Nothing; + } + } + } + else if(!mAnimation->isPlaying(mCurrentHit)) + { + mCurrentHit.erase(); + if (knockdown) + mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(false); + if (recovery) + mPtr.getClass().getCreatureStats(mPtr).setHitRecovery(false); + if (block) + mPtr.getClass().getCreatureStats(mPtr).setBlock(false); + mHitState = CharState_None; + } + else if (isKnockedOut() && mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() > 0) + { + mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown; + mAnimation->disable(mCurrentHit); + mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "loop stop", "stop", 0.0f, 0); + } + if (mHitState != CharState_None) + mIdleState = CharState_None; +} + +void CharacterController::refreshJumpAnims(const WeaponInfo* weap, JumpingState jump, bool force) +{ + if(force || jump != mJumpState) + { + mIdleState = CharState_None; + bool startAtLoop = (jump == mJumpState); + mJumpState = jump; + + std::string jumpAnimName; + MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All; + if(mJumpState != JumpState_None) + { + jumpAnimName = "jump"; + if(weap != sWeaponTypeListEnd) + { + jumpAnimName += weap->shortgroup; + if(!mAnimation->hasAnimation(jumpAnimName)) + { + jumpmask = MWRender::Animation::BlendMask_LowerBody; + jumpAnimName = "jump"; + } + } + } + + if(mJumpState == JumpState_InAir) + { + mAnimation->disable(mCurrentJump); + mCurrentJump = jumpAnimName; + if (mAnimation->hasAnimation("jump")) + mAnimation->play(mCurrentJump, Priority_Jump, jumpmask, false, + 1.0f, (startAtLoop?"loop start":"start"), "stop", 0.0f, ~0ul); + } + else if (mJumpState == JumpState_Landing) + { + if (startAtLoop) + mAnimation->disable(mCurrentJump); + + if (mAnimation->hasAnimation("jump")) + mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, + 1.0f, "loop stop", "stop", 0.0f, 0); + } + else // JumpState_None + { + if (mCurrentJump.length() > 0) + { + mAnimation->disable(mCurrentJump); + mCurrentJump.clear(); + } + } + } +} + +void CharacterController::refreshMovementAnims(const WeaponInfo* weap, CharacterState movement, bool force) +{ + if(force || movement != mMovementState) + { + mMovementState = movement; + + if (movement != CharState_None) + mIdleState = CharState_None; + + std::string movementAnimName; + MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All; + const StateInfo *movestate = std::find_if(sMovementList, sMovementListEnd, FindCharState(mMovementState)); + if(movestate != sMovementListEnd) + { + movementAnimName = movestate->groupname; + if(weap != sWeaponTypeListEnd && movementAnimName.find("swim") == std::string::npos) + { + movementAnimName += weap->shortgroup; + if(!mAnimation->hasAnimation(movementAnimName)) + { + movemask = MWRender::Animation::BlendMask_LowerBody; + movementAnimName = movestate->groupname; + } + } + + if(!mAnimation->hasAnimation(movementAnimName)) + { + std::string::size_type swimpos = movementAnimName.find("swim"); + if(swimpos == std::string::npos) + { + std::string::size_type runpos = movementAnimName.find("run"); + if (runpos != std::string::npos) + { + movementAnimName.replace(runpos, runpos+3, "walk"); + if (!mAnimation->hasAnimation(movementAnimName)) + movementAnimName.clear(); + } + else + movementAnimName.clear(); + } + else + { + movementAnimName.erase(swimpos, 4); + if (weap != sWeaponTypeListEnd) + { + std::string weapMovementAnimName = movementAnimName + weap->shortgroup; + if(mAnimation->hasAnimation(weapMovementAnimName)) + movementAnimName = weapMovementAnimName; + else + movemask = MWRender::Animation::BlendMask_LowerBody; + } + + if (!mAnimation->hasAnimation(movementAnimName)) + movementAnimName.clear(); + } + } + } + + /* If we're playing the same animation, restart from the loop start instead of the + * beginning. */ + int mode = ((movementAnimName == mCurrentMovement) ? 2 : 1); + + mMovementAnimationControlled = true; + + mAnimation->disable(mCurrentMovement); + mCurrentMovement = movementAnimName; + if(!mCurrentMovement.empty()) + { + bool isrunning = mPtr.getClass().getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) + && !MWBase::Environment::get().getWorld()->isFlying(mPtr); + + // For non-flying creatures, MW uses the Walk animation to calculate the animation velocity + // even if we are running. This must be replicated, otherwise the observed speed would differ drastically. + std::string anim = mCurrentMovement; + mAdjustMovementAnimSpeed = true; + if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() + && !(mPtr.get()->mBase->mFlags & ESM::Creature::Flies)) + { + CharacterState walkState = runStateToWalkState(mMovementState); + const StateInfo *stateinfo = std::find_if(sMovementList, sMovementListEnd, FindCharState(walkState)); + anim = stateinfo->groupname; + + mMovementAnimSpeed = mAnimation->getVelocity(anim); + if (mMovementAnimSpeed <= 1.0f) + { + // Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward), + // then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist + // we will play without any scaling. + // Makes the speed attribute of most water creatures totally useless. + // And again, this can not be fixed without patching game data. + mAdjustMovementAnimSpeed = false; + mMovementAnimSpeed = 1.f; + } + } + else + { + mMovementAnimSpeed = mAnimation->getVelocity(anim); + + if (mMovementAnimSpeed <= 1.0f) + { + // The first person anims don't have any velocity to calculate a speed multiplier from. + // We use the third person velocities instead. + // FIXME: should be pulled from the actual animation, but it is not presently loaded. + mMovementAnimSpeed = (isrunning ? 222.857f : 154.064f); + mMovementAnimationControlled = false; + } + } + + mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, + 1.f, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul, true); + } + } +} + +void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterState idle, bool force) +{ + if(force || idle != mIdleState || + ((idle == mIdleState) && !mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty())) + { + mIdleState = idle; + size_t numLoops = ~0ul; + + std::string idleGroup; + MWRender::Animation::AnimPriority idlePriority (Priority_Default); + // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to + // "idle"+weapon or "idle". + if(mIdleState == CharState_IdleSwim && mAnimation->hasAnimation("idleswim")) + { + idleGroup = "idleswim"; + idlePriority = Priority_SwimIdle; + } + else if(mIdleState == CharState_IdleSneak && mAnimation->hasAnimation("idlesneak")) + { + idleGroup = "idlesneak"; + idlePriority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; + } + else if(mIdleState != CharState_None) + { + idleGroup = "idle"; + if(weap != sWeaponTypeListEnd) + { + idleGroup += weap->shortgroup; + if(!mAnimation->hasAnimation(idleGroup)) + idleGroup = "idle"; + + // play until the Loop Stop key 2 to 5 times, then play until the Stop key + // this replicates original engine behavior for the "Idle1h" 1st-person animation + numLoops = 1 + Misc::Rng::rollDice(4); + } + } + + mAnimation->disable(mCurrentIdle); + mCurrentIdle = idleGroup; + if(!mCurrentIdle.empty()) + mAnimation->play(mCurrentIdle, idlePriority, MWRender::Animation::BlendMask_All, false, + 1.0f, "start", "stop", 0.0f, numLoops, true); + } +} + +void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force) +{ + if (mPtr.getClass().isActor()) + refreshHitRecoilAnims(); + + const WeaponInfo *weap = std::find_if(sWeaponTypeList, sWeaponTypeListEnd, FindWeaponType(mWeaponType)); + if (!mPtr.getClass().isBipedal(mPtr)) + weap = sWeaponTypeListEnd; + + refreshJumpAnims(weap, jump, force); + refreshMovementAnims(weap, movement, force); + + // idle handled last as it can depend on the other states + // FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming update), + // the idle animation should be displayed + if ((mUpperBodyState != UpperCharState_Nothing + || (mMovementState != CharState_None && !isTurning()) + || mHitState != CharState_None) + && !mPtr.getClass().isBipedal(mPtr)) + idle = CharState_None; + + refreshIdleAnims(weap, idle, force); +} + + +void getWeaponGroup(WeaponType weaptype, std::string &group) +{ + const WeaponInfo *info = std::find_if(sWeaponTypeList, sWeaponTypeListEnd, FindWeaponType(weaptype)); + if(info != sWeaponTypeListEnd) + group = info->longgroup; + else + group.clear(); +} + + +MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype) +{ + if(stats.getDrawState() == DrawState_Spell) + { + *weaptype = WeapType_Spell; + return inv.end(); + } + + if(stats.getDrawState() == MWMechanics::DrawState_Weapon) + { + MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + if(weapon == inv.end()) + *weaptype = WeapType_HandToHand; + else + { + const std::string &type = weapon->getTypeName(); + if(type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) + *weaptype = WeapType_PickProbe; + else if(type == typeid(ESM::Weapon).name()) + { + MWWorld::LiveCellRef *ref = weapon->get(); + ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; + switch(weaponType) + { + case ESM::Weapon::ShortBladeOneHand: + case ESM::Weapon::LongBladeOneHand: + case ESM::Weapon::BluntOneHand: + case ESM::Weapon::AxeOneHand: + case ESM::Weapon::Arrow: + case ESM::Weapon::Bolt: + *weaptype = WeapType_OneHand; + break; + case ESM::Weapon::LongBladeTwoHand: + case ESM::Weapon::BluntTwoClose: + case ESM::Weapon::AxeTwoHand: + *weaptype = WeapType_TwoHand; + break; + case ESM::Weapon::BluntTwoWide: + case ESM::Weapon::SpearTwoWide: + *weaptype = WeapType_TwoWide; + break; + case ESM::Weapon::MarksmanBow: + *weaptype = WeapType_BowAndArrow; + break; + case ESM::Weapon::MarksmanCrossbow: + *weaptype = WeapType_Crossbow; + break; + case ESM::Weapon::MarksmanThrown: + *weaptype = WeapType_Thrown; + break; + } + } + } + + return weapon; + } + + return inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); +} + +void CharacterController::playDeath(float startpoint, CharacterState death) +{ + switch (death) + { + case CharState_SwimDeath: + mCurrentDeath = "swimdeath"; + break; + case CharState_SwimDeathKnockDown: + mCurrentDeath = "swimdeathknockdown"; + break; + case CharState_SwimDeathKnockOut: + mCurrentDeath = "swimdeathknockout"; + break; + case CharState_DeathKnockDown: + mCurrentDeath = "deathknockdown"; + break; + case CharState_DeathKnockOut: + mCurrentDeath = "deathknockout"; + break; + default: + mCurrentDeath = "death" + toString(death - CharState_Death1 + 1); + } + mDeathState = death; + + mPtr.getClass().getCreatureStats(mPtr).setDeathAnimation(mDeathState - CharState_Death1); + + // For dead actors, refreshCurrentAnims is no longer called, so we need to disable the movement state manually. + // Note that these animations wouldn't actually be visible (due to the Death animation's priority being higher). + // However, they could still trigger text keys, such as Hit events, or sounds. + mMovementState = CharState_None; + mAnimation->disable(mCurrentMovement); + mCurrentMovement = ""; + mUpperBodyState = UpperCharState_Nothing; + mAnimation->disable(mCurrentWeapon); + mCurrentWeapon = ""; + mHitState = CharState_None; + mAnimation->disable(mCurrentHit); + mCurrentHit = ""; + mIdleState = CharState_None; + mAnimation->disable(mCurrentIdle); + mCurrentIdle = ""; + mJumpState = JumpState_None; + mAnimation->disable(mCurrentJump); + mCurrentJump = ""; + mMovementAnimationControlled = true; + + mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All, + false, 1.0f, "start", "stop", startpoint, 0); +} + +CharacterState CharacterController::chooseRandomDeathState() const +{ + int selected=0; + chooseRandomGroup("death", &selected); + return static_cast(CharState_Death1 + (selected-1)); +} + +void CharacterController::playRandomDeath(float startpoint) +{ + if (mPtr == getPlayer()) + { + // The first-person animations do not include death, so we need to + // force-switch to third person before playing the death animation. + MWBase::Environment::get().getWorld()->useDeathCamera(); + } + + if(mHitState == CharState_SwimKnockDown && mAnimation->hasAnimation("swimdeathknockdown")) + { + mDeathState = CharState_SwimDeathKnockDown; + } + else if(mHitState == CharState_SwimKnockOut && mAnimation->hasAnimation("swimdeathknockout")) + { + mDeathState = CharState_SwimDeathKnockOut; + } + else if(MWBase::Environment::get().getWorld()->isSwimming(mPtr) && mAnimation->hasAnimation("swimdeath")) + { + mDeathState = CharState_SwimDeath; + } + else if (mHitState == CharState_KnockDown && mAnimation->hasAnimation("deathknockdown")) + { + mDeathState = CharState_DeathKnockDown; + } + else if (mHitState == CharState_KnockOut && mAnimation->hasAnimation("deathknockout")) + { + mDeathState = CharState_DeathKnockOut; + } + else + { + mDeathState = chooseRandomDeathState(); + } + playDeath(startpoint, mDeathState); +} + +CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim) + : mPtr(ptr) + , mAnimation(anim) + , mIdleState(CharState_None) + , mMovementState(CharState_None) + , mMovementAnimSpeed(0.f) + , mAdjustMovementAnimSpeed(false) + , mHasMovedInXY(false) + , mMovementAnimationControlled(true) + , mDeathState(CharState_None) + , mFloatToSurface(true) + , mHitState(CharState_None) + , mUpperBodyState(UpperCharState_Nothing) + , mJumpState(JumpState_None) + , mWeaponType(WeapType_None) + , mAttackStrength(0.f) + , mSkipAnim(false) + , mSecondsOfSwimming(0) + , mSecondsOfRunning(0) + , mTurnAnimationThreshold(0) + , mAttackingOrSpell(false) +{ + if(!mAnimation) + return; + + mAnimation->setTextKeyListener(this); + + const MWWorld::Class &cls = mPtr.getClass(); + if(cls.isActor()) + { + /* Accumulate along X/Y only for now, until we can figure out how we should + * handle knockout and death which moves the character down. */ + mAnimation->setAccumulation(osg::Vec3f(1.0f, 1.0f, 0.0f)); + + if (cls.hasInventoryStore(mPtr)) + { + getActiveWeapon(cls.getCreatureStats(mPtr), cls.getInventoryStore(mPtr), &mWeaponType); + if (mWeaponType != WeapType_None) + { + mUpperBodyState = UpperCharState_WeapEquiped; + getWeaponGroup(mWeaponType, mCurrentWeapon); + } + + if(mWeaponType != WeapType_None && mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand) + { + mAnimation->showWeapons(true); + mAnimation->setWeaponGroup(mCurrentWeapon); + } + + mAnimation->showCarriedLeft(updateCarriedLeftVisible(mWeaponType)); + } + + if(!cls.getCreatureStats(mPtr).isDead()) + mIdleState = CharState_Idle; + else + { + const MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); + if (cStats.isDeathAnimationFinished()) + { + // Set the death state, but don't play it yet + // We will play it in the first frame, but only if no script set the skipAnim flag + signed char deathanim = cStats.getDeathAnimation(); + if (deathanim == -1) + mDeathState = chooseRandomDeathState(); + else + mDeathState = static_cast(CharState_Death1 + deathanim); + + mFloatToSurface = false; + } + // else: nothing to do, will detect death in the next frame and start playing death animation + } + } + else + { + /* Don't accumulate with non-actors. */ + mAnimation->setAccumulation(osg::Vec3f(0.f, 0.f, 0.f)); + + mIdleState = CharState_Idle; + } + + + if(mDeathState == CharState_None) + refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); + + mAnimation->runAnimation(0.f); + + unpersistAnimationState(); +} + +CharacterController::~CharacterController() +{ + if (mAnimation) + { + persistAnimationState(); + mAnimation->setTextKeyListener(NULL); + } +} + +void split(const std::string &s, char delim, std::vector &elems) { + std::stringstream ss(s); + std::string item; + while (std::getline(ss, item, delim)) { + elems.push_back(item); + } +} + +void CharacterController::handleTextKey(const std::string &groupname, const std::multimap::const_iterator &key, const std::multimap &map) +{ + const std::string &evt = key->second; + + if(evt.compare(0, 7, "sound: ") == 0) + { + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->stopSound3D(mPtr, evt.substr(7)); + sndMgr->playSound3D(mPtr, evt.substr(7), 1.0f, 1.0f); + return; + } + if(evt.compare(0, 10, "soundgen: ") == 0) + { + std::string soundgen = evt.substr(10); + + // The event can optionally contain volume and pitch modifiers + float volume=1.f, pitch=1.f; + if (soundgen.find(" ") != std::string::npos) + { + std::vector tokens; + split(soundgen, ' ', tokens); + soundgen = tokens[0]; + if (tokens.size() >= 2) + { + std::stringstream stream; + stream << tokens[1]; + stream >> volume; + } + if (tokens.size() >= 3) + { + std::stringstream stream; + stream << tokens[2]; + stream >> pitch; + } + } + + std::string sound = mPtr.getClass().getSoundIdFromSndGen(mPtr, soundgen); + if(!sound.empty()) + { + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + if(evt.compare(10, evt.size()-10, "left") == 0 || evt.compare(10, evt.size()-10, "right") == 0 || evt.compare(10, evt.size()-10, "land") == 0) + { + // Don't make foot sounds local for the player, it makes sense to keep them + // positioned on the ground. + sndMgr->playSound3D(mPtr, sound, volume, pitch, MWSound::Type::Foot, + MWSound::PlayMode::NoPlayerLocal); + } + else + { + sndMgr->stopSound3D(mPtr, sound); + sndMgr->playSound3D(mPtr, sound, volume, pitch); + } + } + return; + } + + if(evt.compare(0, groupname.size(), groupname) != 0 || + evt.compare(groupname.size(), 2, ": ") != 0) + { + // Not ours, skip it + return; + } + size_t off = groupname.size()+2; + size_t len = evt.size() - off; + + if(evt.compare(off, len, "equip attach") == 0) + mAnimation->showWeapons(true); + else if(evt.compare(off, len, "unequip detach") == 0) + mAnimation->showWeapons(false); + else if(evt.compare(off, len, "chop hit") == 0) + mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); + else if(evt.compare(off, len, "slash hit") == 0) + mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); + else if(evt.compare(off, len, "thrust hit") == 0) + mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); + else if(evt.compare(off, len, "hit") == 0) + { + if (groupname == "attack1" || groupname == "swimattack1") + mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); + else if (groupname == "attack2" || groupname == "swimattack2") + mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); + else if (groupname == "attack3" || groupname == "swimattack3") + mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); + else + mPtr.getClass().hit(mPtr, mAttackStrength); + } + else if (!groupname.empty() + && (groupname.compare(0, groupname.size()-1, "attack") == 0 || groupname.compare(0, groupname.size()-1, "swimattack") == 0) + && evt.compare(off, len, "start") == 0) + { + std::multimap::const_iterator hitKey = key; + + // Not all animations have a hit key defined. If there is none, the hit happens with the start key. + bool hasHitKey = false; + while (hitKey != map.end()) + { + if (hitKey->second == groupname + ": hit") + { + hasHitKey = true; + break; + } + if (hitKey->second == groupname + ": stop") + break; + ++hitKey; + } + if (!hasHitKey) + { + if (groupname == "attack1" || groupname == "swimattack1") + mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); + else if (groupname == "attack2" || groupname == "swimattack2") + mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); + else if (groupname == "attack3" || groupname == "swimattack3") + mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); + } + } + else if (evt.compare(off, len, "shoot attach") == 0) + mAnimation->attachArrow(); + else if (evt.compare(off, len, "shoot release") == 0) + mAnimation->releaseArrow(mAttackStrength); + else if (evt.compare(off, len, "shoot follow attach") == 0) + mAnimation->attachArrow(); + + else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release" + // Make sure this key is actually for the RangeType we are casting. The flame atronach has + // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. + && evt.compare(off, len, mAttackType + " release") == 0) + { + MWBase::Environment::get().getWorld()->castSpell(mPtr); + } + + else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) + mPtr.getClass().block(mPtr); +} + +void CharacterController::updatePtr(const MWWorld::Ptr &ptr) +{ + mPtr = ptr; +} + +void CharacterController::updateIdleStormState(bool inwater) +{ + bool inStormDirection = false; + if (MWBase::Environment::get().getWorld()->isInStorm()) + { + osg::Vec3f stormDirection = MWBase::Environment::get().getWorld()->getStormDirection(); + osg::Vec3f characterDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); + inStormDirection = std::acos(stormDirection * characterDirection / (stormDirection.length() * characterDirection.length())) + > osg::DegreesToRadians(120.f); + } + if (inStormDirection && !inwater && mUpperBodyState == UpperCharState_Nothing && mAnimation->hasAnimation("idlestorm")) + { + float complete = 0; + mAnimation->getInfo("idlestorm", &complete); + + if (complete == 0) + mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::BlendMask_RightArm, false, + 1.0f, "start", "loop start", 0.0f, 0); + else if (complete == 1) + mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::BlendMask_RightArm, false, + 1.0f, "loop start", "loop stop", 0.0f, ~0ul); + } + else + { + if (mUpperBodyState == UpperCharState_Nothing) + { + if (mAnimation->isPlaying("idlestorm")) + { + if (mAnimation->getCurrentTime("idlestorm") < mAnimation->getTextKeyTime("idlestorm: loop stop")) + { + mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::BlendMask_RightArm, true, + 1.0f, "loop stop", "stop", 0.0f, 0); + } + } + } + else + mAnimation->disable("idlestorm"); + } +} + +bool CharacterController::updateCreatureState() +{ + const MWWorld::Class &cls = mPtr.getClass(); + CreatureStats &stats = cls.getCreatureStats(mPtr); + + WeaponType weapType = WeapType_None; + if(stats.getDrawState() == DrawState_Weapon) + weapType = WeapType_HandToHand; + else if (stats.getDrawState() == DrawState_Spell) + weapType = WeapType_Spell; + + if (weapType != mWeaponType) + { + mWeaponType = weapType; + if (mAnimation->isPlaying(mCurrentWeapon)) + mAnimation->disable(mCurrentWeapon); + } + + if(mAttackingOrSpell) + { + if(mUpperBodyState == UpperCharState_Nothing && mHitState == CharState_None) + { + MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); + + std::string startKey = "start"; + std::string stopKey = "stop"; + if (weapType == WeapType_Spell) + { + const std::string spellid = stats.getSpells().getSelectedSpell(); + if (!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) + { + MWMechanics::CastSpell cast(mPtr, NULL); + cast.playSpellCastingEffects(spellid); + + if (!mAnimation->hasAnimation("spellcast")) + MWBase::Environment::get().getWorld()->castSpell(mPtr); // No "release" text key to use, so cast immediately + else + { + const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); + const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); + + switch(effectentry.mRange) + { + case 0: mAttackType = "self"; break; + case 1: mAttackType = "touch"; break; + case 2: mAttackType = "target"; break; + } + + startKey = mAttackType + " " + startKey; + stopKey = mAttackType + " " + stopKey; + mCurrentWeapon = "spellcast"; + } + } + else + mCurrentWeapon = ""; + } + if (weapType != WeapType_Spell || !mAnimation->hasAnimation("spellcast")) // Not all creatures have a dedicated spellcast animation + { + bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); + int roll = Misc::Rng::rollDice(3); // [0, 2] + if (roll == 0) + mCurrentWeapon = isSwimming && mAnimation->hasAnimation("swimattack1") ? "swimattack1" : "attack1"; + else if (roll == 1) + mCurrentWeapon = isSwimming && mAnimation->hasAnimation("swimattack2") ? "swimattack2" : "attack2"; + else + mCurrentWeapon = isSwimming && mAnimation->hasAnimation("swimattack3") ? "swimattack3" : "attack3"; + } + + if (!mCurrentWeapon.empty()) + { + mAnimation->play(mCurrentWeapon, Priority_Weapon, + MWRender::Animation::BlendMask_All, true, + 1, startKey, stopKey, + 0.0f, 0); + mUpperBodyState = UpperCharState_StartToMinAttack; + + mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); + + if (weapType == WeapType_HandToHand) + playSwishSound(0.0f); + } + } + + mAttackingOrSpell = false; + } + + bool animPlaying = mAnimation->getInfo(mCurrentWeapon); + if (!animPlaying) + mUpperBodyState = UpperCharState_Nothing; + return false; +} + +bool CharacterController::updateCarriedLeftVisible(WeaponType weaptype) const +{ + // Shields/torches shouldn't be visible during any operation involving two hands + // There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop", + // but they are also present in weapon drawing animation. + switch (weaptype) + { + case WeapType_Spell: + case WeapType_BowAndArrow: + case WeapType_Crossbow: + case WeapType_HandToHand: + case WeapType_TwoHand: + case WeapType_TwoWide: + return false; + default: + return true; + } +} + +bool CharacterController::updateWeaponState() +{ + const MWWorld::Class &cls = mPtr.getClass(); + CreatureStats &stats = cls.getCreatureStats(mPtr); + WeaponType weaptype = WeapType_None; + if(stats.getDrawState() == DrawState_Weapon) + weaptype = WeapType_HandToHand; + else if (stats.getDrawState() == DrawState_Spell) + weaptype = WeapType_Spell; + + const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf(); + + std::string soundid; + if (mPtr.getClass().hasInventoryStore(mPtr)) + { + MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); + if(weapon != inv.end() && !(weaptype == WeapType_None && mWeaponType == WeapType_Spell)) + { + soundid = (weaptype == WeapType_None) ? + weapon->getClass().getDownSoundId(*weapon) : + weapon->getClass().getUpSoundId(*weapon); + } + } + + MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); + priorityWeapon[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + + bool forcestateupdate = false; + + // We should not play equipping animation and sound during weapon->weapon transition + bool isStillWeapon = weaptype > WeapType_HandToHand && weaptype < WeapType_Spell && + mWeaponType > WeapType_HandToHand && mWeaponType < WeapType_Spell; + + if(weaptype != mWeaponType && !isKnockedOut() && + !isKnockedDown() && !isRecovery()) + { + forcestateupdate = true; + + mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); + + std::string weapgroup; + if(weaptype == WeapType_None) + { + if ((!isWerewolf || mWeaponType != WeapType_Spell)) + { + getWeaponGroup(mWeaponType, weapgroup); + mAnimation->play(weapgroup, priorityWeapon, + MWRender::Animation::BlendMask_All, true, + 1.0f, "unequip start", "unequip stop", 0.0f, 0); + mUpperBodyState = UpperCharState_UnEquipingWeap; + } + } + else + { + getWeaponGroup(weaptype, weapgroup); + mAnimation->setWeaponGroup(weapgroup); + + if (!isStillWeapon) + { + mAnimation->showWeapons(false); + mAnimation->play(weapgroup, priorityWeapon, + MWRender::Animation::BlendMask_All, true, + 1.0f, "equip start", "equip stop", 0.0f, 0); + mUpperBodyState = UpperCharState_EquipingWeap; + } + + if(isWerewolf) + { + const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Sound *sound = store.get().searchRandom("WolfEquip"); + if(sound) + { + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); + } + } + } + + if(!soundid.empty() && !isStillWeapon) + { + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mPtr, soundid, 1.0f, 1.0f); + } + + mWeaponType = weaptype; + getWeaponGroup(mWeaponType, mCurrentWeapon); + } + + if(isWerewolf) + { + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + if(cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) + && mHasMovedInXY + && !MWBase::Environment::get().getWorld()->isSwimming(mPtr) + && mWeaponType == WeapType_None) + { + if(!sndMgr->getSoundPlaying(mPtr, "WolfRun")) + sndMgr->playSound3D(mPtr, "WolfRun", 1.0f, 1.0f, MWSound::Type::Sfx, + MWSound::PlayMode::Loop); + } + else + sndMgr->stopSound3D(mPtr, "WolfRun"); + } + + // Cancel attack if we no longer have ammunition + bool ammunition = true; + bool isWeapon = false; + float weapSpeed = 1.f; + if (mPtr.getClass().hasInventoryStore(mPtr)) + { + MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); + isWeapon = (weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()); + if(isWeapon) + weapSpeed = weapon->get()->mBase->mData.mSpeed; + + MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); + if (mWeaponType == WeapType_Crossbow) + ammunition = (ammo != inv.end() && ammo->get()->mBase->mData.mType == ESM::Weapon::Bolt); + else if (mWeaponType == WeapType_BowAndArrow) + ammunition = (ammo != inv.end() && ammo->get()->mBase->mData.mType == ESM::Weapon::Arrow); + if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped) + { + mAnimation->disable(mCurrentWeapon); + mUpperBodyState = UpperCharState_WeapEquiped; + } + } + + float complete; + bool animPlaying; + if(mAttackingOrSpell) + { + mIdleState = CharState_None; + if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block)) + { + MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); + mAttackStrength = 0; + if(mWeaponType == WeapType_Spell) + { + // Unset casting flag, otherwise pressing the mouse button down would + // continue casting every frame if there is no animation + mAttackingOrSpell = false; + if (mPtr == getPlayer()) + { + MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); + } + + const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + + // For the player, set the spell we want to cast + // This has to be done at the start of the casting animation, + // *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation) + if (mPtr == getPlayer()) + { + std::string selectedSpell = MWBase::Environment::get().getWindowManager()->getSelectedSpell(); + stats.getSpells().setSelectedSpell(selectedSpell); + } + std::string spellid = stats.getSpells().getSelectedSpell(); + + if(!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) + { + MWMechanics::CastSpell cast(mPtr, NULL); + cast.playSpellCastingEffects(spellid); + + const ESM::Spell *spell = store.get().find(spellid); + const ESM::ENAMstruct &lastEffect = spell->mEffects.mList.back(); + const ESM::MagicEffect *effect; + + effect = store.get().find(lastEffect.mEffectID); // use last effect of list for color of VFX_Hands + + const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); + + for (size_t iter = 0; iter < spell->mEffects.mList.size(); ++iter) // play hands vfx for each effect + { + if (mAnimation->getNode("Bip01 L Hand")) + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); + + if (mAnimation->getNode("Bip01 R Hand")) + mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); + } + + const ESM::ENAMstruct &firstEffect = spell->mEffects.mList.at(0); // first effect used for casting animation + + switch(firstEffect.mRange) + { + case 0: mAttackType = "self"; break; + case 1: mAttackType = "touch"; break; + case 2: mAttackType = "target"; break; + } + + mAnimation->play(mCurrentWeapon, priorityWeapon, + MWRender::Animation::BlendMask_All, true, + weapSpeed, mAttackType+" start", mAttackType+" stop", + 0.0f, 0); + mUpperBodyState = UpperCharState_CastingSpell; + } + if (mPtr.getClass().hasInventoryStore(mPtr)) + { + MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + if (inv.getSelectedEnchantItem() != inv.end()) + { + // Enchanted items cast immediately (no animation) + MWBase::Environment::get().getWorld()->castSpell(mPtr); + } + } + + } + else if(mWeaponType == WeapType_PickProbe) + { + MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + MWWorld::Ptr item = *weapon; + // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes. + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getFacedObject(); + std::string resultMessage, resultSound; + + if(!target.isEmpty()) + { + if(item.getTypeName() == typeid(ESM::Lockpick).name()) + Security(mPtr).pickLock(target, item, resultMessage, resultSound); + else if(item.getTypeName() == typeid(ESM::Probe).name()) + Security(mPtr).probeTrap(target, item, resultMessage, resultSound); + } + mAnimation->play(mCurrentWeapon, priorityWeapon, + MWRender::Animation::BlendMask_All, true, + 1.0f, "start", "stop", 0.0, 0); + mUpperBodyState = UpperCharState_FollowStartToFollowStop; + + if(!resultMessage.empty()) + MWBase::Environment::get().getWindowManager()->messageBox(resultMessage); + if(!resultSound.empty()) + MWBase::Environment::get().getSoundManager()->playSound3D(target, resultSound, + 1.0f, 1.0f); + } + else if (ammunition) + { + if(mWeaponType == WeapType_Crossbow || mWeaponType == WeapType_BowAndArrow || + mWeaponType == WeapType_Thrown) + mAttackType = "shoot"; + else + { + if(mPtr == getPlayer()) + { + if (isWeapon) + { + if (Settings::Manager::getBool("best attack", "Game")) + { + MWWorld::ConstContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + mAttackType = getBestAttack(weapon->get()->mBase); + } + else + setAttackTypeBasedOnMovement(); + } + else + setAttackTypeRandomly(mAttackType); + } + // else if (mPtr != getPlayer()) use mAttackType set by AiCombat + } + + mAnimation->play(mCurrentWeapon, priorityWeapon, + MWRender::Animation::BlendMask_All, false, + weapSpeed, mAttackType+" start", mAttackType+" min attack", + 0.0f, 0); + mUpperBodyState = UpperCharState_StartToMinAttack; + } + } + + animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); + if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown()) + mAttackStrength = complete; + } + else + { + animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); + if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown()) + { + float attackStrength = complete; + if (!mPtr.getClass().isNpc()) + { + // most creatures don't actually have an attack wind-up animation, so use a uniform random value + // (even some creatures that can use weapons don't have a wind-up animation either, e.g. Rieklings) + // Note: vanilla MW uses a random value for *all* non-player actors, but we probably don't need to go that far. + attackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); + } + + if(mWeaponType != WeapType_Crossbow && mWeaponType != WeapType_BowAndArrow) + { + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + + if(isWerewolf) + { + const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Sound *sound = store.get().searchRandom("WolfSwing"); + if(sound) + sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); + } + else + { + playSwishSound(attackStrength); + } + } + mAttackStrength = attackStrength; + + mAnimation->disable(mCurrentWeapon); + mAnimation->play(mCurrentWeapon, priorityWeapon, + MWRender::Animation::BlendMask_All, false, + weapSpeed, mAttackType+" max attack", mAttackType+" min hit", + 1.0f-complete, 0); + + complete = 0.f; + mUpperBodyState = UpperCharState_MaxAttackToMinHit; + } + else if (isKnockedDown()) + { + if (mUpperBodyState > UpperCharState_WeapEquiped) + mUpperBodyState = UpperCharState_WeapEquiped; + mAnimation->disable(mCurrentWeapon); + } + } + + mAnimation->setPitchFactor(0.f); + if (mWeaponType == WeapType_BowAndArrow || mWeaponType == WeapType_Thrown) + { + switch (mUpperBodyState) + { + case UpperCharState_StartToMinAttack: + mAnimation->setPitchFactor(complete); + break; + case UpperCharState_MinAttackToMaxAttack: + case UpperCharState_MaxAttackToMinHit: + case UpperCharState_MinHitToHit: + mAnimation->setPitchFactor(1.f); + break; + case UpperCharState_FollowStartToFollowStop: + if (animPlaying) + mAnimation->setPitchFactor(1.f-complete); + break; + default: + break; + } + } + else if (mWeaponType == WeapType_Crossbow) + { + switch (mUpperBodyState) + { + case UpperCharState_EquipingWeap: + mAnimation->setPitchFactor(complete); + break; + case UpperCharState_UnEquipingWeap: + mAnimation->setPitchFactor(1.f-complete); + break; + case UpperCharState_WeapEquiped: + case UpperCharState_StartToMinAttack: + case UpperCharState_MinAttackToMaxAttack: + case UpperCharState_MaxAttackToMinHit: + case UpperCharState_MinHitToHit: + case UpperCharState_FollowStartToFollowStop: + mAnimation->setPitchFactor(1.f); + break; + default: + break; + } + } + + if(!animPlaying) + { + if(mUpperBodyState == UpperCharState_EquipingWeap || + mUpperBodyState == UpperCharState_FollowStartToFollowStop || + mUpperBodyState == UpperCharState_CastingSpell) + { + if (ammunition && mWeaponType == WeapType_Crossbow) + mAnimation->attachArrow(); + + mUpperBodyState = UpperCharState_WeapEquiped; + } + else if(mUpperBodyState == UpperCharState_UnEquipingWeap) + mUpperBodyState = UpperCharState_Nothing; + } + else if(complete >= 1.0f) + { + std::string start, stop; + switch(mUpperBodyState) + { + case UpperCharState_StartToMinAttack: + start = mAttackType+" min attack"; + stop = mAttackType+" max attack"; + mUpperBodyState = UpperCharState_MinAttackToMaxAttack; + break; + case UpperCharState_MinAttackToMaxAttack: + //hack to avoid body pos desync when jumping/sneaking in 'max attack' state + if(!mAnimation->isPlaying(mCurrentWeapon)) + mAnimation->play(mCurrentWeapon, priorityWeapon, + MWRender::Animation::BlendMask_All, false, + 0, mAttackType+" min attack", mAttackType+" max attack", 0.999f, 0); + break; + case UpperCharState_MaxAttackToMinHit: + if(mAttackType == "shoot") + { + start = mAttackType+" min hit"; + stop = mAttackType+" release"; + } + else + { + start = mAttackType+" min hit"; + stop = mAttackType+" hit"; + } + mUpperBodyState = UpperCharState_MinHitToHit; + break; + case UpperCharState_MinHitToHit: + if(mAttackType == "shoot") + { + start = mAttackType+" follow start"; + stop = mAttackType+" follow stop"; + } + else + { + float str = mAttackStrength; + start = mAttackType+((str < 0.5f) ? " small follow start" + : (str < 1.0f) ? " medium follow start" + : " large follow start"); + stop = mAttackType+((str < 0.5f) ? " small follow stop" + : (str < 1.0f) ? " medium follow stop" + : " large follow stop"); + } + mUpperBodyState = UpperCharState_FollowStartToFollowStop; + break; + default: + break; + } + + if(!start.empty()) + { + mAnimation->disable(mCurrentWeapon); + if (mUpperBodyState == UpperCharState_FollowStartToFollowStop) + mAnimation->play(mCurrentWeapon, priorityWeapon, + MWRender::Animation::BlendMask_All, true, + weapSpeed, start, stop, 0.0f, 0); + else + mAnimation->play(mCurrentWeapon, priorityWeapon, + MWRender::Animation::BlendMask_All, false, + weapSpeed, start, stop, 0.0f, 0); + } + } + + if (mPtr.getClass().hasInventoryStore(mPtr)) + { + const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); + MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() + && updateCarriedLeftVisible(mWeaponType)) + + { + mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm, + false, 1.0f, "start", "stop", 0.0f, (~(size_t)0), true); + } + else if (mAnimation->isPlaying("torch")) + { + mAnimation->disable("torch"); + } + } + + mAnimation->setAccurateAiming(mUpperBodyState > UpperCharState_WeapEquiped); + + return forcestateupdate; +} + +void CharacterController::updateAnimQueue() +{ + if(mAnimQueue.size() > 1) + { + if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false) + { + mAnimation->disable(mAnimQueue.front().mGroup); + mAnimQueue.pop_front(); + + bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); + mAnimation->play(mAnimQueue.front().mGroup, Priority_Default, + MWRender::Animation::BlendMask_All, false, + 1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback); + } + } + + if(!mAnimQueue.empty()) + mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); +} + +void CharacterController::update(float duration) +{ + MWBase::World *world = MWBase::Environment::get().getWorld(); + const MWWorld::Class &cls = mPtr.getClass(); + osg::Vec3f movement(0.f, 0.f, 0.f); + float speed = 0.f; + + updateMagicEffects(); + + bool godmode = mPtr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); + + if(!cls.isActor()) + updateAnimQueue(); + else if(!cls.getCreatureStats(mPtr).isDead()) + { + bool onground = world->isOnGround(mPtr); + bool inwater = world->isSwimming(mPtr); + bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak); + bool flying = world->isFlying(mPtr); + // Can't run while flying (see speed formula in Npc/Creature::getSpeed) + bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying; + CreatureStats &stats = cls.getCreatureStats(mPtr); + + //Force Jump Logic + + bool isMoving = (std::abs(cls.getMovementSettings(mPtr).mPosition[0]) > .5 || std::abs(cls.getMovementSettings(mPtr).mPosition[1]) > .5); + if(!inwater && !flying) + { + //Force Jump + if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump)) + { + if(onground) + { + cls.getMovementSettings(mPtr).mPosition[2] = 1; + } + else + cls.getMovementSettings(mPtr).mPosition[2] = 0; + } + //Force Move Jump, only jump if they're otherwise moving + if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving) + { + + if(onground) + { + cls.getMovementSettings(mPtr).mPosition[2] = 1; + } + else + cls.getMovementSettings(mPtr).mPosition[2] = 0; + } + } + + osg::Vec3f vec(cls.getMovementSettings(mPtr).asVec3()); + vec.normalize(); + + if(mHitState != CharState_None && mJumpState == JumpState_None) + vec = osg::Vec3f(0.f, 0.f, 0.f); + osg::Vec3f rot = cls.getRotationVector(mPtr); + + speed = cls.getSpeed(mPtr); + + vec.x() *= speed; + vec.y() *= speed; + + CharacterState movestate = CharState_None; + CharacterState idlestate = CharState_SpecialIdle; + JumpingState jumpstate = JumpState_None; + + bool forcestateupdate = false; + + mHasMovedInXY = std::abs(vec.x())+std::abs(vec.y()) > 0.0f; + isrunning = isrunning && mHasMovedInXY; + + // advance athletics + if(mHasMovedInXY && mPtr == getPlayer()) + { + if(inwater) + { + mSecondsOfSwimming += duration; + while(mSecondsOfSwimming > 1) + { + cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 1); + mSecondsOfSwimming -= 1; + } + } + else if(isrunning && !sneak) + { + mSecondsOfRunning += duration; + while(mSecondsOfRunning > 1) + { + cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 0); + mSecondsOfRunning -= 1; + } + } + } + + // reduce fatigue + const MWWorld::Store &gmst = world->getStore().get(); + float fatigueLoss = 0; + static const float fFatigueRunBase = gmst.find("fFatigueRunBase")->getFloat(); + static const float fFatigueRunMult = gmst.find("fFatigueRunMult")->getFloat(); + static const float fFatigueSwimWalkBase = gmst.find("fFatigueSwimWalkBase")->getFloat(); + static const float fFatigueSwimRunBase = gmst.find("fFatigueSwimRunBase")->getFloat(); + static const float fFatigueSwimWalkMult = gmst.find("fFatigueSwimWalkMult")->getFloat(); + static const float fFatigueSwimRunMult = gmst.find("fFatigueSwimRunMult")->getFloat(); + static const float fFatigueSneakBase = gmst.find("fFatigueSneakBase")->getFloat(); + static const float fFatigueSneakMult = gmst.find("fFatigueSneakMult")->getFloat(); + + if (cls.getEncumbrance(mPtr) <= cls.getCapacity(mPtr)) + { + const float encumbrance = cls.getEncumbrance(mPtr) / cls.getCapacity(mPtr); + + if (sneak) + fatigueLoss = fFatigueSneakBase + encumbrance * fFatigueSneakMult; + else + { + if (inwater) + { + if (!isrunning) + fatigueLoss = fFatigueSwimWalkBase + encumbrance * fFatigueSwimWalkMult; + else + fatigueLoss = fFatigueSwimRunBase + encumbrance * fFatigueSwimRunMult; + } + else if (isrunning) + fatigueLoss = fFatigueRunBase + encumbrance * fFatigueRunMult; + } + } + fatigueLoss *= duration; + DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); + + if (!godmode) + { + fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss, fatigue.getCurrent() < 0); + cls.getCreatureStats(mPtr).setFatigue(fatigue); + } + + if(sneak || inwater || flying) + vec.z() = 0.0f; + + bool inJump = true; + if(!onground && !flying && !inwater) + { + // In the air (either getting up —ascending part of jump— or falling). + + forcestateupdate = (mJumpState != JumpState_InAir); + jumpstate = JumpState_InAir; + + static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->getFloat(); + static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->getFloat(); + float factor = fJumpMoveBase + fJumpMoveMult * mPtr.getClass().getSkill(mPtr, ESM::Skill::Acrobatics)/100.f; + factor = std::min(1.f, factor); + vec.x() *= factor; + vec.y() *= factor; + vec.z() = 0.0f; + } + else if(vec.z() > 0.0f && mJumpState == JumpState_None) + { + // Started a jump. + float z = cls.getJump(mPtr); + if (z > 0) + { + if(vec.x() == 0 && vec.y() == 0) + vec = osg::Vec3f(0.0f, 0.0f, z); + else + { + osg::Vec3f lat (vec.x(), vec.y(), 0.0f); + lat.normalize(); + vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * z * 0.707f; + } + + // advance acrobatics + if (mPtr == getPlayer()) + cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 0); + + // decrease fatigue + const float fatigueJumpBase = gmst.find("fFatigueJumpBase")->getFloat(); + const float fatigueJumpMult = gmst.find("fFatigueJumpMult")->getFloat(); + float normalizedEncumbrance = mPtr.getClass().getNormalizedEncumbrance(mPtr); + if (normalizedEncumbrance > 1) + normalizedEncumbrance = 1; + const float fatigueDecrease = fatigueJumpBase + normalizedEncumbrance * fatigueJumpMult; + + if (!godmode) + { + fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); + cls.getCreatureStats(mPtr).setFatigue(fatigue); + } + } + } + else if(mJumpState == JumpState_InAir && !inwater && !flying) + { + forcestateupdate = true; + jumpstate = JumpState_Landing; + vec.z() = 0.0f; + + float height = cls.getCreatureStats(mPtr).land(); + float healthLost = getFallDamage(mPtr, height); + + if (healthLost > 0.0f) + { + const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm(); + + // inflict fall damages + if (!godmode) + { + DynamicStat health = cls.getCreatureStats(mPtr).getHealth(); + float realHealthLost = static_cast(healthLost * (1.0f - 0.25f * fatigueTerm)); + health.setCurrent(health.getCurrent() - realHealthLost); + cls.getCreatureStats(mPtr).setHealth(health); + cls.onHit(mPtr, realHealthLost, true, MWWorld::Ptr(), MWWorld::Ptr(), osg::Vec3f(), true); + } + + const int acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics); + if (healthLost > (acrobaticsSkill * fatigueTerm)) + { + cls.getCreatureStats(mPtr).setKnockedDown(true); + } + else + { + // report acrobatics progression + if (mPtr == getPlayer()) + cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 1); + } + } + } + else + { + jumpstate = mAnimation->isPlaying(mCurrentJump) ? JumpState_Landing : JumpState_None; + + vec.z() = 0.0f; + + inJump = false; + + if(std::abs(vec.x()/2.0f) > std::abs(vec.y())) + { + if(vec.x() > 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight) + : (sneak ? CharState_SneakRight + : (isrunning ? CharState_RunRight : CharState_WalkRight))); + else if(vec.x() < 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft) + : (sneak ? CharState_SneakLeft + : (isrunning ? CharState_RunLeft : CharState_WalkLeft))); + } + else if(vec.y() != 0.0f) + { + if(vec.y() > 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward) + : (sneak ? CharState_SneakForward + : (isrunning ? CharState_RunForward : CharState_WalkForward))); + else if(vec.y() < 0.0f) + movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack) + : (sneak ? CharState_SneakBack + : (isrunning ? CharState_RunBack : CharState_WalkBack))); + } + else if(rot.z() != 0.0f && !sneak && !(mPtr == getPlayer() && MWBase::Environment::get().getWorld()->isFirstPerson())) + { + if(rot.z() > 0.0f) + { + movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; + mAnimation->disable(mCurrentJump); + } + else if(rot.z() < 0.0f) + { + movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; + mAnimation->disable(mCurrentJump); + } + } + } + + mTurnAnimationThreshold -= duration; + if (isTurning()) + mTurnAnimationThreshold = 0.05f; + else if (movestate == CharState_None && isTurning() + && mTurnAnimationThreshold > 0) + { + movestate = mMovementState; + } + + if(movestate != CharState_None && !isTurning()) + clearAnimQueue(); + + if(mAnimQueue.empty() || inwater || sneak) + { + idlestate = (inwater ? CharState_IdleSwim : (sneak && !inJump ? CharState_IdleSneak : CharState_Idle)); + } + else + updateAnimQueue(); + + if (!mSkipAnim) + { + // bipedal means hand-to-hand could be used (which is handled in updateWeaponState). an existing InventoryStore means an actual weapon could be used. + if(cls.isBipedal(mPtr) || cls.hasInventoryStore(mPtr)) + forcestateupdate = updateWeaponState() || forcestateupdate; + else + forcestateupdate = updateCreatureState() || forcestateupdate; + + refreshCurrentAnims(idlestate, movestate, jumpstate, forcestateupdate); + updateIdleStormState(inwater); + } + + if (inJump) + mMovementAnimationControlled = false; + + if (isTurning()) + { + if (duration > 0) + mAnimation->adjustSpeedMult(mCurrentMovement, std::min(1.5f, std::abs(rot.z()) / duration / static_cast(osg::PI))); + } + else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed) + { + float speedmult = speed / mMovementAnimSpeed; + mAnimation->adjustSpeedMult(mCurrentMovement, speedmult); + } + + if (!mSkipAnim) + { + if(!isKnockedDown() && !isKnockedOut()) + { + if (rot != osg::Vec3f()) + world->rotateObject(mPtr, rot.x(), rot.y(), rot.z(), true); + } + else //avoid z-rotating for knockdown + { + if (rot.x() != 0 && rot.y() != 0) + world->rotateObject(mPtr, rot.x(), rot.y(), 0.0f, true); + } + + if (!mMovementAnimationControlled) + world->queueMovement(mPtr, vec); + } + else + // We must always queue movement, even if there is none, to apply gravity. + world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); + + movement = vec; + cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0; + // Can't reset jump state (mPosition[2]) here; we don't know for sure whether the PhysicSystem will actually handle it in this frame + // due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled. + + if (!mSkipAnim) + updateHeadTracking(duration); + } + else if(cls.getCreatureStats(mPtr).isDead()) + { + // initial start of death animation for actors that started the game as dead + // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag + if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) + { + playDeath(1.f, mDeathState); + } + // We must always queue movement, even if there is none, to apply gravity. + world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); + } + + osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim ? 0.f : duration); + if(duration > 0.0f) + moved /= duration; + else + moved = osg::Vec3f(0.f, 0.f, 0.f); + + // Ensure we're moving in generally the right direction... + if(speed > 0.f) + { + float l = moved.length(); + + if((movement.x() < 0.0f && movement.x() < moved.x()*2.0f) || + (movement.x() > 0.0f && movement.x() > moved.x()*2.0f)) + moved.x() = movement.x(); + if((movement.y() < 0.0f && movement.y() < moved.y()*2.0f) || + (movement.y() > 0.0f && movement.y() > moved.y()*2.0f)) + moved.y() = movement.y(); + if((movement.z() < 0.0f && movement.z() < moved.z()*2.0f) || + (movement.z() > 0.0f && movement.z() > moved.z()*2.0f)) + moved.z() = movement.z(); + // but keep the original speed + float newLength = moved.length(); + if (newLength > 0) + moved *= (l / newLength); + } + + if (mSkipAnim) + mAnimation->updateEffects(duration); + + if (mFloatToSurface && cls.isActor() && cls.getCreatureStats(mPtr).isDead() && cls.canSwim(mPtr)) + moved.z() = 1.0; + + // Update movement + if(mMovementAnimationControlled && mPtr.getClass().isActor()) + world->queueMovement(mPtr, moved); + + mSkipAnim = false; + + mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); +} + +void CharacterController::persistAnimationState() +{ + ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); + + state.mScriptedAnims.clear(); + for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) + { + if (!iter->mPersist) + continue; + + ESM::AnimationState::ScriptedAnimation anim; + anim.mGroup = iter->mGroup; + + if (iter == mAnimQueue.begin()) + { + anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup); + float complete; + mAnimation->getInfo(anim.mGroup, &complete, NULL); + anim.mTime = complete; + } + else + { + anim.mLoopCount = iter->mLoopCount; + anim.mTime = 0.f; + } + + state.mScriptedAnims.push_back(anim); + } +} + +void CharacterController::unpersistAnimationState() +{ + const ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); + + if (!state.mScriptedAnims.empty()) + { + clearAnimQueue(); + for (ESM::AnimationState::ScriptedAnimations::const_iterator iter = state.mScriptedAnims.begin(); iter != state.mScriptedAnims.end(); ++iter) + { + AnimationQueueEntry entry; + entry.mGroup = iter->mGroup; + entry.mLoopCount = iter->mLoopCount; + entry.mPersist = true; + + mAnimQueue.push_back(entry); + } + + const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front(); + float complete = anim.mTime; + if (anim.mAbsolute) + { + float start = mAnimation->getTextKeyTime(anim.mGroup+": start"); + float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop"); + float time = std::max(start, std::min(stop, anim.mTime)); + complete = (time - start) / (stop - start); + } + + mAnimation->disable(mCurrentIdle); + mCurrentIdle.clear(); + mIdleState = CharState_SpecialIdle; + + bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); + mAnimation->play(anim.mGroup, + Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, + "start", "stop", complete, anim.mLoopCount, loopfallback); + } +} + +bool CharacterController::playGroup(const std::string &groupname, int mode, int count, bool persist) +{ + if(!mAnimation || !mAnimation->hasAnimation(groupname)) + return false; + + // If this animation is a looped animation (has a "loop start" key) that is already playing + // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count + // and remove any other animations that were queued. + // This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners correctly. + if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && + mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop start") >= 0 && + mAnimation->isPlaying(groupname)) + { + float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": loop stop"); + + if (endOfLoop < 0) // if no Loop Stop key was found, use the Stop key + endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": stop"); + + if (endOfLoop > 0 && (mAnimation->getCurrentTime(mAnimQueue.front().mGroup) < endOfLoop)) + { + mAnimQueue.resize(1); + return true; + } + } + + count = std::max(count, 1); + + AnimationQueueEntry entry; + entry.mGroup = groupname; + entry.mLoopCount = count-1; + entry.mPersist = persist; + + if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) + { + clearAnimQueue(); + mAnimQueue.push_back(entry); + + mAnimation->disable(mCurrentIdle); + mCurrentIdle.clear(); + + mIdleState = CharState_SpecialIdle; + bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0); + mAnimation->play(groupname, Priority_Default, + MWRender::Animation::BlendMask_All, false, 1.0f, + ((mode==2) ? "loop start" : "start"), "stop", 0.0f, count-1, loopfallback); + } + else if(mode == 0) + { + mAnimQueue.resize(1); + mAnimQueue.push_back(entry); + } + return true; +} + +void CharacterController::skipAnim() +{ + mSkipAnim = true; +} + +bool CharacterController::isAnimPlaying(const std::string &groupName) +{ + if(mAnimation == NULL) + return false; + return mAnimation->isPlaying(groupName); +} + + +void CharacterController::clearAnimQueue() +{ + if(!mAnimQueue.empty()) + mAnimation->disable(mAnimQueue.front().mGroup); + mAnimQueue.clear(); +} + +void CharacterController::forceStateUpdate() +{ + if(!mAnimation) + return; + clearAnimQueue(); + + refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); + if(mDeathState != CharState_None) + { + playRandomDeath(); + } + + mAnimation->runAnimation(0.f); +} + +CharacterController::KillResult CharacterController::kill() +{ + if (mDeathState == CharState_None) + { + playRandomDeath(); + + mAnimation->disable(mCurrentIdle); + + mIdleState = CharState_None; + mCurrentIdle.clear(); + return Result_DeathAnimStarted; + } + + MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); + if (isAnimPlaying(mCurrentDeath)) + return Result_DeathAnimPlaying; + if (!cStats.isDeathAnimationFinished()) + { + cStats.setDeathAnimationFinished(true); + return Result_DeathAnimJustFinished; + } + return Result_DeathAnimFinished; +} + +void CharacterController::resurrect() +{ + if(mDeathState == CharState_None) + return; + + if(mAnimation) + mAnimation->disable(mCurrentDeath); + mCurrentDeath.clear(); + mDeathState = CharState_None; +} + +void CharacterController::updateContinuousVfx() +{ + // Keeping track of when to stop a continuous VFX seems to be very difficult to do inside the spells code, + // as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here. + + // Stop any effects that are no longer active + std::vector effects; + mAnimation->getLoopingEffects(effects); + + for (std::vector::iterator it = effects.begin(); it != effects.end(); ++it) + { + if (mPtr.getClass().getCreatureStats(mPtr).isDead() + || mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(MWMechanics::EffectKey(*it)).getMagnitude() <= 0) + mAnimation->removeEffect(*it); + } +} + +void CharacterController::updateMagicEffects() +{ + if (!mPtr.getClass().isActor()) + return; + float alpha = 1.f; + if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).getModifier()) // Ignore base magnitude (see bug #3555). + { + if (mPtr == getPlayer()) + alpha = 0.4f; + else + alpha = 0.f; + } + float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); + if (chameleon) + { + alpha *= std::max(0.2f, (100.f - chameleon)/100.f); + } + mAnimation->setAlpha(alpha); + + bool vampire = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0.0f; + mAnimation->setVampire(vampire); + + float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude(); + mAnimation->setLightEffect(light); +} + +void CharacterController::setAttackTypeBasedOnMovement() +{ + float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition; + + if (move[1] && !move[0]) // forward-backward + mAttackType = "thrust"; + else if (move[0] && !move[1]) //sideway + mAttackType = "slash"; + else + mAttackType = "chop"; +} + +bool CharacterController::isAttackPrepairing() const +{ + return mUpperBodyState == UpperCharState_StartToMinAttack || + mUpperBodyState == UpperCharState_MinAttackToMaxAttack; +} + +bool CharacterController::isReadyToBlock() const +{ + return updateCarriedLeftVisible(mWeaponType); +} + +bool CharacterController::isKnockedDown() const +{ + return mHitState == CharState_KnockDown || + mHitState == CharState_SwimKnockDown; +} + +bool CharacterController::isKnockedOut() const +{ + return mHitState == CharState_KnockOut || + mHitState == CharState_SwimKnockOut; +} + +bool CharacterController::isTurning() const +{ + return mMovementState == CharState_TurnLeft || + mMovementState == CharState_TurnRight || + mMovementState == CharState_SwimTurnLeft || + mMovementState == CharState_SwimTurnRight; +} + +bool CharacterController::isRecovery() const +{ + return mHitState == CharState_Hit || + mHitState == CharState_SwimHit; +} + +bool CharacterController::isAttackingOrSpell() const +{ + return mUpperBodyState != UpperCharState_Nothing && + mUpperBodyState != UpperCharState_WeapEquiped; +} + +bool CharacterController::isSneaking() const +{ + return mIdleState == CharState_IdleSneak || + mMovementState == CharState_SneakForward || + mMovementState == CharState_SneakBack || + mMovementState == CharState_SneakLeft || + mMovementState == CharState_SneakRight; +} + +bool CharacterController::isRunning() const +{ + return mMovementState == CharState_RunForward || + mMovementState == CharState_RunBack || + mMovementState == CharState_RunLeft || + mMovementState == CharState_RunRight || + mMovementState == CharState_SwimRunForward || + mMovementState == CharState_SwimRunBack || + mMovementState == CharState_SwimRunLeft || + mMovementState == CharState_SwimRunRight; +} + +void CharacterController::setAttackingOrSpell(bool attackingOrSpell) +{ + mAttackingOrSpell = attackingOrSpell; +} + +void CharacterController::setAIAttackType(const std::string& attackType) +{ + mAttackType = attackType; +} + +void CharacterController::setAttackTypeRandomly(std::string& attackType) +{ + float random = Misc::Rng::rollProbability(); + if (random >= 2/3.f) + attackType = "thrust"; + else if (random >= 1/3.f) + attackType = "slash"; + else + attackType = "chop"; +} + +bool CharacterController::readyToPrepareAttack() const +{ + return (mHitState == CharState_None || mHitState == CharState_Block) + && mUpperBodyState <= UpperCharState_WeapEquiped; +} + +bool CharacterController::readyToStartAttack() const +{ + if (mHitState != CharState_None && mHitState != CharState_Block) + return false; + + if (mPtr.getClass().hasInventoryStore(mPtr) || mPtr.getClass().isBipedal(mPtr)) + return mUpperBodyState == UpperCharState_WeapEquiped; + else + return mUpperBodyState == UpperCharState_Nothing; +} + +float CharacterController::getAttackStrength() const +{ + return mAttackStrength; +} + +void CharacterController::setActive(bool active) +{ + mAnimation->setActive(active); +} + +void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr &target) +{ + mHeadTrackTarget = target; +} + +void CharacterController::playSwishSound(float attackStrength) +{ + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + + std::string sound = "Weapon Swish"; + if(attackStrength < 0.5f) + sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack + else if(attackStrength < 1.0f) + sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack + else + sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack +} + +void CharacterController::updateHeadTracking(float duration) +{ + const osg::Node* head = mAnimation->getNode("Bip01 Head"); + if (!head) + return; + + float zAngleRadians = 0.f; + float xAngleRadians = 0.f; + + if (!mHeadTrackTarget.isEmpty()) + { + osg::NodePathList nodepaths = head->getParentalNodePaths(); + if (nodepaths.empty()) + return; + osg::Matrixf mat = osg::computeLocalToWorld(nodepaths[0]); + osg::Vec3f headPos = mat.getTrans(); + + osg::Vec3f direction; + if (const MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget)) + { + const osg::Node* node = anim->getNode("Head"); + if (node == NULL) + node = anim->getNode("Bip01 Head"); + if (node != NULL) + { + nodepaths = node->getParentalNodePaths(); + if (!nodepaths.empty()) + direction = osg::computeLocalToWorld(nodepaths[0]).getTrans() - headPos; + } + else + // no head node to look at, fall back to look at center of collision box + direction = MWBase::Environment::get().getWorld()->aimToTarget(mPtr, mHeadTrackTarget); + } + direction.normalize(); + + if (!mPtr.getRefData().getBaseNode()) + return; + const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); + + zAngleRadians = std::atan2(direction.x(), direction.y()) - std::atan2(actorDirection.x(), actorDirection.y()); + xAngleRadians = -std::asin(direction.z()); + + wrap(zAngleRadians); + wrap(xAngleRadians); + + xAngleRadians = std::min(xAngleRadians, osg::DegreesToRadians(40.f)); + xAngleRadians = std::max(xAngleRadians, osg::DegreesToRadians(-40.f)); + zAngleRadians = std::min(zAngleRadians, osg::DegreesToRadians(30.f)); + zAngleRadians = std::max(zAngleRadians, osg::DegreesToRadians(-30.f)); + } + + float factor = duration*5; + factor = std::min(factor, 1.f); + xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * (-xAngleRadians); + zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * (-zAngleRadians); + + mAnimation->setHeadPitch(xAngleRadians); + mAnimation->setHeadYaw(zAngleRadians); +} + +} diff --git a/apps/openmw/mwmechanics/character.cpp.rej b/apps/openmw/mwmechanics/character.cpp.rej new file mode 100644 index 000000000..f04f72a90 --- /dev/null +++ b/apps/openmw/mwmechanics/character.cpp.rej @@ -0,0 +1,46 @@ +--- apps/openmw/mwmechanics/character.cpp ++++ apps/openmw/mwmechanics/character.cpp +@@ -372,29 +372,28 @@ void CharacterController::refreshJumpAnims(const WeaponInfo* weap, JumpingState + } + } + +- if(mJumpState == JumpState_InAir) ++ if (!mCurrentJump.empty()) + { + mAnimation->disable(mCurrentJump); +- mCurrentJump = jumpAnimName; +- if (mAnimation->hasAnimation("jump")) +- mAnimation->play(mCurrentJump, Priority_Jump, jumpmask, false, ++ mCurrentJump.clear(); ++ } ++ ++ if(mJumpState == JumpState_InAir) ++ { ++ if (mAnimation->hasAnimation(jumpAnimName)) ++ { ++ mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, + 1.0f, (startAtLoop?"loop start":"start"), "stop", 0.0f, ~0ul); ++ mCurrentJump = jumpAnimName; ++ } + } + else if (mJumpState == JumpState_Landing) + { +- if (startAtLoop) +- mAnimation->disable(mCurrentJump); +- +- if (mAnimation->hasAnimation("jump")) ++ if (mAnimation->hasAnimation(jumpAnimName)) ++ { + mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, + 1.0f, "loop stop", "stop", 0.0f, 0); +- } +- else // JumpState_None +- { +- if (mCurrentJump.length() > 0) +- { +- mAnimation->disable(mCurrentJump); +- mCurrentJump.clear(); ++ mCurrentJump = jumpAnimName; + } + } + } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 4fbcdc4d5..9d580a12a 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -620,6 +620,11 @@ namespace MWRender mutable bool mDone; }; + void RenderingManager::screenshot360(osg::Image* image, int w) + { + screenshot(image,w,w); + } + void RenderingManager::screenshot(osg::Image *image, int w, int h) { osg::ref_ptr rttCamera (new osg::Camera); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index f0087e43d..d0ac9a500 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -126,6 +126,7 @@ namespace MWRender /// Take a screenshot of w*h onto the given image, not including the GUI. void screenshot(osg::Image* image, int w, int h); + void screenshot360(osg::Image* image, int w); struct RayResult { diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index 14ee5adee..c643480a9 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -655,5 +655,4 @@ void MWState::StateManager::writeScreenshot(std::vector &imageData) const std::string data = ostream.str(); imageData = std::vector(data.begin(), data.end()); - } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 9c7fba9fa..f9a030a80 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2280,6 +2280,11 @@ namespace MWWorld { mRendering->screenshot(image, w, h); } + + void World::screenshot360 (osg::Image* image, int w) + { + mRendering->screenshot360(image, w); + } void World::activateDoor(const MWWorld::Ptr& door) { diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 7af7b2968..1882b88b9 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -560,6 +560,7 @@ namespace MWWorld /// \todo this does not belong here void screenshot (osg::Image* image, int w, int h) override; + void screenshot360 (osg::Image* image, int w) override; /// Find center of exterior cell above land surface /// \return false if exterior with given name not exists, true otherwise From ce55d7c2f584bd9d80cb362451755c01055e0f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Tue, 7 Nov 2017 15:02:01 +0100 Subject: [PATCH 002/282] basic cubemap rendering --- apps/openmw/mwrender/renderingmanager.cpp | 38 +++++++++++++++++++++-- apps/openmw/mwrender/renderingmanager.hpp | 2 +- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 9d580a12a..eaee3a803 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -16,6 +16,8 @@ #include #include +#include + #include #include @@ -622,10 +624,37 @@ namespace MWRender void RenderingManager::screenshot360(osg::Image* image, int w) { - screenshot(image,w,w); + osg::Vec3 directions[6] = { + osg::Vec3(0,0,-1), + osg::Vec3(-1,0,0), + osg::Vec3(0,0,1), + osg::Vec3(1,0,0), + osg::Vec3(0,1,0), + osg::Vec3(0,-1,0), + }; + + double fovBackup = mFieldOfView; + mFieldOfView = 90.0; // each side sees 90 degrees + + for (int i = 0; i < 6; i++) // for each cube side + { + osg::ref_ptr sideImage (new osg::Image); + screenshot(sideImage.get(),w,w,directions[i]); + + if (i == 0) + { + image->allocateImage(w * 6,w,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); + std::cout << image->s() << " " << image->t() << std::endl; + } + + osg::copyImage(sideImage.get(),0,0,0,sideImage->s(),sideImage->t(),sideImage->r(), + image,w * i,0,0); + } + + mFieldOfView = fovBackup; } - void RenderingManager::screenshot(osg::Image *image, int w, int h) + void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Vec3 direction) { osg::ref_ptr rttCamera (new osg::Camera); rttCamera->setNodeMask(Mask_RenderToTexture); @@ -634,7 +663,10 @@ namespace MWRender rttCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); rttCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); rttCamera->setProjectionMatrixAsPerspective(mFieldOfView, w/float(h), mNearClip, mViewDistance); - rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix()); + rttCamera->setViewMatrix( + mViewer->getCamera()->getViewMatrix() * osg::Matrixd::rotate(osg::Vec3(0,0,-1),direction) + ); + rttCamera->setViewport(0, 0, w, h); osg::ref_ptr texture (new osg::Texture2D); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index d0ac9a500..add7aae4d 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -125,7 +125,7 @@ namespace MWRender void setWaterHeight(float level); /// Take a screenshot of w*h onto the given image, not including the GUI. - void screenshot(osg::Image* image, int w, int h); + void screenshot(osg::Image* image, int w, int h, osg::Vec3 direction=osg::Vec3(0,0,-1)); void screenshot360(osg::Image* image, int w); struct RayResult From 5afe02505b6816e3f44c819047508047902b7bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Tue, 7 Nov 2017 16:12:31 +0100 Subject: [PATCH 003/282] hide player in first person 360 screenshot --- apps/openmw/mwinput/inputmanagerimp.cpp | 2 +- apps/openmw/mwrender/renderingmanager.cpp | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 192eadbde..61434521e 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1016,7 +1016,7 @@ namespace MWInput void InputManager::screenshot() { // MOVE THIS ELSEWHERE LATER! - int screenshotW = 512; + int screenshotW = 1024; osg::ref_ptr screenshot (new osg::Image); MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(), screenshotW); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index eaee3a803..4901bc176 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -636,21 +636,24 @@ namespace MWRender double fovBackup = mFieldOfView; mFieldOfView = 90.0; // each side sees 90 degrees + if (mCamera->isFirstPerson()) + mPlayerAnimation->getObjectRoot()->setNodeMask(0); + for (int i = 0; i < 6; i++) // for each cube side { osg::ref_ptr sideImage (new osg::Image); screenshot(sideImage.get(),w,w,directions[i]); if (i == 0) - { image->allocateImage(w * 6,w,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); - std::cout << image->s() << " " << image->t() << std::endl; - } osg::copyImage(sideImage.get(),0,0,0,sideImage->s(),sideImage->t(),sideImage->r(), image,w * i,0,0); } + if (mCamera->isFirstPerson()) + mPlayerAnimation->getObjectRoot()->setNodeMask(1); + mFieldOfView = fovBackup; } @@ -667,6 +670,8 @@ namespace MWRender mViewer->getCamera()->getViewMatrix() * osg::Matrixd::rotate(osg::Vec3(0,0,-1),direction) ); +// TODO: water reflections have to be transformed as well!!!!! + rttCamera->setViewport(0, 0, w, h); osg::ref_ptr texture (new osg::Texture2D); From 3be9e2ee9548ce4795fc3f9eb5149681d9f11f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Tue, 7 Nov 2017 19:47:36 +0100 Subject: [PATCH 004/282] make spherical screenshot class --- apps/openmw/mwrender/renderingmanager.cpp | 36 +++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 4901bc176..f25f28bab 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -622,8 +622,34 @@ namespace MWRender mutable bool mDone; }; + class SphericalScreenshot + { + public: + SphericalScreenshot(int size) + { + mSize = size; + + for (int i = 0; i < 6; ++i) + mImages.push_back(new osg::Image); + } + + osg::Image *getImage(int index) + { + return mImages[index].get(); + } + + protected: + std::vector> mImages; + int mSize; + }; + void RenderingManager::screenshot360(osg::Image* image, int w) { + int resultW = 1024; + int resultH = 768; + + SphericalScreenshot s(w); + osg::Vec3 directions[6] = { osg::Vec3(0,0,-1), osg::Vec3(-1,0,0), @@ -641,14 +667,14 @@ namespace MWRender for (int i = 0; i < 6; i++) // for each cube side { - osg::ref_ptr sideImage (new osg::Image); - screenshot(sideImage.get(),w,w,directions[i]); + osg::Image *sideImage = s.getImage(i); + screenshot(sideImage,w,w,directions[i]); if (i == 0) - image->allocateImage(w * 6,w,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); + //image->allocateImage(resultW,resultH,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); + image->allocateImage(6 * w,w,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); - osg::copyImage(sideImage.get(),0,0,0,sideImage->s(),sideImage->t(),sideImage->r(), - image,w * i,0,0); + osg::copyImage(sideImage,0,0,0,sideImage->s(),sideImage->t(),sideImage->r(),image,w * i,0,0); } if (mCamera->isFirstPerson()) From 4761a3d98b4d50dd041631490aa5c654f4c847a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Tue, 7 Nov 2017 22:13:05 +0100 Subject: [PATCH 005/282] dirty cylindrical projection --- apps/openmw/mwrender/renderingmanager.cpp | 85 +++++++++++++++++++++-- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index f25f28bab..f22a0778d 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -638,6 +638,80 @@ namespace MWRender return mImages[index].get(); } + void create(osg::Image *dest, int w, int h) + { + dest->allocateImage(w,h,mImages[0]->r(),mImages[0]->getPixelFormat(),mImages[0]->getDataType()); + + for (int j = 0; j < h; ++j) + for (int i = 0; i < w; ++i) + dest->setColor(getColorByDirection(cylindricalCoords(i / ((float) w), j / ((float) h))),i,j); + } + + osg::Vec3d cylindricalCoords(double x, double y) + { + osg::Vec3 result = osg::Vec3d(sin(x * 2 * osg::PI),cos(x * 2 * osg::PI),y * 2.0 - 1.0); + result.normalize(); + return result; + } + + osg::Vec4 getColorByDirection(osg::Vec3d d) + { + double x, y; + double ma; // see OpenGL 4.4 specification page 241 + int side; + + double ax, ay, az; + ax = d.x() > 0 ? d.x() : -d.x(); + ay = d.y() > 0 ? d.y() : -d.y(); + az = d.z() > 0 ? d.z() : -d.z(); + + + if (ax > ay) + { + if (ax > az) + { + side = d.x() > 0 ? 1 : 3; + ma = ax; + } + else + { + side = d.z() > 0 ? 5 : 4; + ma = az; + } + } + else + { + if (ay > az) + { + side = d.y() > 0 ? 0 : 2; + ma = ay; + } + else + { + side = d.z() > 0 ? 5 : 4; + ma = az; + } + } + + switch (side) + { + case 0: x = d.x(); y = d.z(); break; + case 1: x = -d.y(); y = d.z(); break; + case 2: x = -d.x(); y = d.z(); break; + case 3: x = d.y(); y = d.z(); break; + case 4: x = d.x(); y = d.y(); break; + case 5: x = d.x(); y = -d.y(); break; + default: break; + } + + x = 0.5 * (x / ma + 1); + y = 0.5 * (y / ma + 1); + + return mImages[side]->getColor( + std::min(std::max(int(x * mSize),0),mSize - 1), + std::min(std::max(int(y * mSize),0),mSize - 1)); //osg::Vec4(d.x(),d.y(),d.z(),1); + } + protected: std::vector> mImages; int mSize; @@ -645,9 +719,6 @@ namespace MWRender void RenderingManager::screenshot360(osg::Image* image, int w) { - int resultW = 1024; - int resultH = 768; - SphericalScreenshot s(w); osg::Vec3 directions[6] = { @@ -670,16 +741,18 @@ namespace MWRender osg::Image *sideImage = s.getImage(i); screenshot(sideImage,w,w,directions[i]); - if (i == 0) + // if (i == 0) //image->allocateImage(resultW,resultH,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); - image->allocateImage(6 * w,w,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); + //image->allocateImage(6 * w,w,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); - osg::copyImage(sideImage,0,0,0,sideImage->s(),sideImage->t(),sideImage->r(),image,w * i,0,0); + // osg::copyImage(sideImage,0,0,0,sideImage->s(),sideImage->t(),sideImage->r(),image,w * i,0,0); } if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(1); + s.create(image,1600,768); + mFieldOfView = fovBackup; } From 5f365181813c34505782a0efac62c940bf3814aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 8 Nov 2017 09:15:45 +0100 Subject: [PATCH 006/282] spherical mapping --- apps/openmw/mwrender/renderingmanager.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index f22a0778d..73b761ff2 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -644,7 +644,7 @@ namespace MWRender for (int j = 0; j < h; ++j) for (int i = 0; i < w; ++i) - dest->setColor(getColorByDirection(cylindricalCoords(i / ((float) w), j / ((float) h))),i,j); + dest->setColor(getColorByDirection(sphericalCoords(i / ((float) w), j / ((float) h))),i,j); } osg::Vec3d cylindricalCoords(double x, double y) @@ -654,6 +654,17 @@ namespace MWRender return result; } + osg::Vec3d sphericalCoords(double x, double y) + { + x = x * 2 * osg::PI; + y = (y - 0.5) * osg::PI; + + osg::Vec3 result = osg::Vec3(0.0,cos(y),sin(y)); + result = osg::Vec3(cos(x) * result.y(),sin(x) * result.y(),result.z()); + + return result; + } + osg::Vec4 getColorByDirection(osg::Vec3d d) { double x, y; From 5698d70806c52d7912e3d303851818ce3233c589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 8 Nov 2017 12:58:27 +0100 Subject: [PATCH 007/282] small planet mapping --- apps/openmw/mwrender/renderingmanager.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 73b761ff2..9e2773870 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -644,12 +644,12 @@ namespace MWRender for (int j = 0; j < h; ++j) for (int i = 0; i < w; ++i) - dest->setColor(getColorByDirection(sphericalCoords(i / ((float) w), j / ((float) h))),i,j); + dest->setColor(getColorByDirection(smallPlanetCoords(i / ((float) w), j / ((float) h))),i,j); } osg::Vec3d cylindricalCoords(double x, double y) { - osg::Vec3 result = osg::Vec3d(sin(x * 2 * osg::PI),cos(x * 2 * osg::PI),y * 2.0 - 1.0); + osg::Vec3 result = osg::Vec3d(cos(x * 2 * osg::PI),sin(x * 2 * osg::PI),y * 2.0 - 1.0); result.normalize(); return result; } @@ -665,6 +665,21 @@ namespace MWRender return result; } + osg::Vec3d smallPlanetCoords(double x, double y) + { + osg::Vec2d fromCenter = osg::Vec2d(x,y) - osg::Vec2d(0.5,0.5); + + double magnitude = fromCenter.length(); + + fromCenter.normalize(); + double dot = fromCenter * osg::Vec2d(0.0,1.0); + + x = x > 0.5 ? 0.5 - (dot + 1) / 4.0 : 0.5 + (dot + 1) / 4.0; + y = pow(std::min(1.0,magnitude / 0.5),0.5); + + return sphericalCoords(x,y); + } + osg::Vec4 getColorByDirection(osg::Vec3d d) { double x, y; @@ -720,7 +735,7 @@ namespace MWRender return mImages[side]->getColor( std::min(std::max(int(x * mSize),0),mSize - 1), - std::min(std::max(int(y * mSize),0),mSize - 1)); //osg::Vec4(d.x(),d.y(),d.z(),1); + std::min(std::max(int(y * mSize),0),mSize - 1)); } protected: @@ -762,7 +777,7 @@ namespace MWRender if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(1); - s.create(image,1600,768); + s.create(image,1600,1600); mFieldOfView = fovBackup; } From f60840754fcf7da27bb41640ee9c5ab3cb91cfb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 9 Nov 2017 14:44:42 +0100 Subject: [PATCH 008/282] disable water effects for spherical screenshots --- apps/openmw/mwrender/renderingmanager.cpp | 14 ++++------- apps/openmw/mwrender/water.cpp | 29 +++++++++++++++++++---- apps/openmw/mwrender/water.hpp | 1 + 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 9e2773870..b1f1888d0 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -644,7 +644,7 @@ namespace MWRender for (int j = 0; j < h; ++j) for (int i = 0; i < w; ++i) - dest->setColor(getColorByDirection(smallPlanetCoords(i / ((float) w), j / ((float) h))),i,j); + dest->setColor(getColorByDirection(sphericalCoords(i / ((float) w), j / ((float) h))),i,j); } osg::Vec3d cylindricalCoords(double x, double y) @@ -766,12 +766,6 @@ namespace MWRender { osg::Image *sideImage = s.getImage(i); screenshot(sideImage,w,w,directions[i]); - - // if (i == 0) - //image->allocateImage(resultW,resultH,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); - //image->allocateImage(6 * w,w,sideImage->r(),sideImage->getPixelFormat(),sideImage->getDataType()); - - // osg::copyImage(sideImage,0,0,0,sideImage->s(),sideImage->t(),sideImage->r(),image,w * i,0,0); } if (mCamera->isFirstPerson()) @@ -795,8 +789,6 @@ namespace MWRender mViewer->getCamera()->getViewMatrix() * osg::Matrixd::rotate(osg::Vec3(0,0,-1),direction) ); -// TODO: water reflections have to be transformed as well!!!!! - rttCamera->setViewport(0, 0, w, h); osg::ref_ptr texture (new osg::Texture2D); @@ -823,10 +815,14 @@ namespace MWRender // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() + mWater->setEffectsEnabled(false); + mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); + mWater->setEffectsEnabled(true); + callback->waitTillDone(); // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index c4dffb7a4..70e7b36ad 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -207,7 +207,6 @@ osg::ref_ptr readPngImage (const std::string& file) return result.getImage(); } - class Refraction : public osg::Camera { public: @@ -221,7 +220,7 @@ public: setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); setName("RefractionCamera"); - setCullMask(Mask_Effect|Mask_Scene|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting); + setupCullMask(true); setNodeMask(Mask_RenderToTexture); setViewport(0, 0, rttSize, rttSize); @@ -262,6 +261,12 @@ public: attach(osg::Camera::DEPTH_BUFFER, mRefractionDepthTexture); } + void setupCullMask(bool enabled) + { + setCullMask(!enabled ? 0 : + Mask_Effect|Mask_Scene|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting); + } + void setScene(osg::Node* scene) { if (mScene) @@ -304,9 +309,9 @@ public: setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); setName("ReflectionCamera"); - bool reflectActors = Settings::Manager::getBool("reflect actors", "Water"); + mReflectActors = Settings::Manager::getBool("reflect actors", "Water"); - setCullMask(Mask_Effect|Mask_Scene|Mask_Terrain|Mask_ParticleSystem|Mask_Sky|Mask_Player|Mask_Lighting|(reflectActors ? Mask_Actor : 0)); + setupCullMask(true); setNodeMask(Mask_RenderToTexture); unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); @@ -334,6 +339,12 @@ public: addChild(mClipCullNode); } + void setupCullMask(bool enabled) + { + setCullMask(!enabled ? 0 : + Mask_Effect|Mask_Scene|Mask_Terrain|Mask_ParticleSystem|Mask_Sky|Mask_Player|Mask_Lighting|(mReflectActors ? Mask_Actor : 0)); + } + void setWaterLevel(float waterLevel) { setViewMatrix(osg::Matrix::scale(1,1,-1) * osg::Matrix::translate(0,0,2 * waterLevel)); @@ -357,6 +368,7 @@ private: osg::ref_ptr mReflectionTexture; osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; + bool mReflectActors; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. @@ -697,4 +709,13 @@ void Water::clearRipples() mSimulation->clear(); } +void Water::setEffectsEnabled(bool enabled) +{ + if (mReflection) + mReflection->setupCullMask(enabled); + + if (mRefraction) + mRefraction->setupCullMask(enabled); +} + } diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index a4fd1ed36..ed6e40f1a 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -106,6 +106,7 @@ namespace MWRender void removeCell(const MWWorld::CellStore* store); ///< remove all emitters in this cell void clearRipples(); + void setEffectsEnabled(bool enabled); void changeCell(const MWWorld::CellStore* store); void setHeight(const float height); From 1b97a541f4e6ed5fafb41ff8213641c93e5c831e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 9 Nov 2017 16:06:29 +0100 Subject: [PATCH 009/282] make a new action for 360 screenshot --- apps/openmw/mwinput/inputmanagerimp.cpp | 22 ++++++++++++++++------ apps/openmw/mwinput/inputmanagerimp.hpp | 3 +++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 61434521e..5d1c236ca 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -247,6 +247,9 @@ namespace MWInput case A_Screenshot: screenshot(); break; + case A_Screenshot360: + screenshot360(); + break; case A_Inventory: toggleInventory (); break; @@ -1015,7 +1018,13 @@ namespace MWInput void InputManager::screenshot() { - // MOVE THIS ELSEWHERE LATER! + mScreenCaptureHandler->setFramesToCapture(1); + mScreenCaptureHandler->captureNextFrame(*mViewer); + MWBase::Environment::get().getWindowManager()->messageBox ("Screenshot saved"); + } + + void InputManager::screenshot360() + { int screenshotW = 1024; osg::ref_ptr screenshot (new osg::Image); MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(), screenshotW); @@ -1040,11 +1049,6 @@ namespace MWInput } outfile.close(); - -// mScreenCaptureHandler->setFramesToCapture(1); -// mScreenCaptureHandler->captureNextFrame(*mViewer); - -// MWBase::Environment::get().getWindowManager()->messageBox ("Screenshot saved"); } void InputManager::toggleInventory() @@ -1230,6 +1234,7 @@ namespace MWInput defaultKeyBindings[A_QuickKey9] = SDL_SCANCODE_9; defaultKeyBindings[A_QuickKey10] = SDL_SCANCODE_0; defaultKeyBindings[A_Screenshot] = SDL_SCANCODE_F12; + defaultKeyBindings[A_Screenshot360] = SDL_SCANCODE_F8; defaultKeyBindings[A_ToggleHUD] = SDL_SCANCODE_F11; defaultKeyBindings[A_ToggleDebug] = SDL_SCANCODE_F10; defaultKeyBindings[A_AlwaysRun] = SDL_SCANCODE_CAPSLOCK; @@ -1365,6 +1370,9 @@ namespace MWInput if (action == A_Screenshot) return "Screenshot"; + if (action == A_Screenshot360) + return "Screenshot 360"; + descriptions[A_Use] = "sUse"; descriptions[A_Activate] = "sActivate"; descriptions[A_MoveBackward] = "sBack"; @@ -1526,6 +1534,7 @@ namespace MWInput ret.push_back(A_QuickSave); ret.push_back(A_QuickLoad); ret.push_back(A_Screenshot); + ret.push_back(A_Screenshot360); ret.push_back(A_QuickKeysMenu); ret.push_back(A_QuickKey1); ret.push_back(A_QuickKey2); @@ -1557,6 +1566,7 @@ namespace MWInput ret.push_back(A_QuickSave); ret.push_back(A_QuickLoad); ret.push_back(A_Screenshot); + ret.push_back(A_Screenshot360); ret.push_back(A_QuickKeysMenu); ret.push_back(A_QuickKey1); ret.push_back(A_QuickKey2); diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index cba7fc743..07b43d0ac 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -227,6 +227,7 @@ namespace MWInput void toggleInventory(); void toggleConsole(); void screenshot(); + void screenshot360(); void toggleJournal(); void activate(); void toggleWalking(); @@ -257,6 +258,8 @@ namespace MWInput A_Screenshot, // Take a screenshot + A_Screenshot360, // Take a 360 degree screenshot + A_Inventory, // Toggle inventory screen A_Console, // Toggle console screen From d4fd08a63fc696e415957a15d9d06774654059fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 9 Nov 2017 16:49:46 +0100 Subject: [PATCH 010/282] save 360 screenshots in the configured directory --- apps/openmw/mwbase/world.hpp | 2 +- apps/openmw/mwinput/inputmanagerimp.cpp | 11 ++++++----- apps/openmw/mwrender/renderingmanager.cpp | 3 ++- apps/openmw/mwrender/renderingmanager.hpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 4 ++-- apps/openmw/mwworld/worldimp.hpp | 2 +- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 80a9e11a1..6d39229f0 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -450,7 +450,7 @@ namespace MWBase /// \todo this does not belong here virtual void screenshot (osg::Image* image, int w, int h) = 0; - virtual void screenshot360 (osg::Image* image, int w) = 0; + virtual void screenshot360 (osg::Image* image) = 0; /// Find default position inside exterior cell specified by name /// \return false if exterior with given name not exists, true otherwise diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 5d1c236ca..cd2e8408b 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1020,20 +1020,21 @@ namespace MWInput { mScreenCaptureHandler->setFramesToCapture(1); mScreenCaptureHandler->captureNextFrame(*mViewer); - MWBase::Environment::get().getWindowManager()->messageBox ("Screenshot saved"); + MWBase::Environment::get().getWindowManager()->messageBox("Screenshot saved"); } void InputManager::screenshot360() { - int screenshotW = 1024; osg::ref_ptr screenshot (new osg::Image); - MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(), screenshotW); + MWBase::Environment::get().getWorld()->screenshot360(screenshot.get()); +(*mScreenCaptureHandler->getCaptureOperation()) (*(screenshot.get()),0); +/* osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg"); if (!readerwriter) { - std::cerr << "Error: Unable to write screenshot, can't find a jpg ReaderWriter" << std::endl; + std::cerr << "Error: Unable to write 360 degree screenshot, can't find a jpg ReaderWriter" << std::endl; return; } @@ -1048,7 +1049,7 @@ namespace MWInput return; } - outfile.close(); + outfile.close(); */ } void InputManager::toggleInventory() diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index b1f1888d0..4ea602f3f 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -743,8 +743,9 @@ namespace MWRender int mSize; }; - void RenderingManager::screenshot360(osg::Image* image, int w) + void RenderingManager::screenshot360(osg::Image* image) { + int w = 1024; SphericalScreenshot s(w); osg::Vec3 directions[6] = { diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index add7aae4d..e6e2e1c0a 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -126,7 +126,7 @@ namespace MWRender /// Take a screenshot of w*h onto the given image, not including the GUI. void screenshot(osg::Image* image, int w, int h, osg::Vec3 direction=osg::Vec3(0,0,-1)); - void screenshot360(osg::Image* image, int w); + void screenshot360(osg::Image* image); struct RayResult { diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index f9a030a80..f51f9af3d 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2281,9 +2281,9 @@ namespace MWWorld mRendering->screenshot(image, w, h); } - void World::screenshot360 (osg::Image* image, int w) + void World::screenshot360 (osg::Image* image) { - mRendering->screenshot360(image, w); + mRendering->screenshot360(image); } void World::activateDoor(const MWWorld::Ptr& door) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 1882b88b9..a68982412 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -560,7 +560,7 @@ namespace MWWorld /// \todo this does not belong here void screenshot (osg::Image* image, int w, int h) override; - void screenshot360 (osg::Image* image, int w) override; + void screenshot360 (osg::Image* image) override; /// Find center of exterior cell above land surface /// \return false if exterior with given name not exists, true otherwise From 8f321140256d835adb0d0779eedef732b4cda1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 9 Nov 2017 18:26:27 +0100 Subject: [PATCH 011/282] segfault fix --- apps/openmw/engine.cpp | 9 ++-- apps/openmw/engine.hpp | 3 +- apps/openmw/mwinput/inputmanagerimp.cpp | 26 ++-------- apps/openmw/mwinput/inputmanagerimp.hpp | 4 +- apps/openmw/mwrender/renderingmanager.cpp | 62 +++++++++++++++-------- apps/openmw/mwrender/renderingmanager.hpp | 2 +- 6 files changed, 56 insertions(+), 50 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 3e5ab7ce6..499158fd4 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -484,7 +484,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) else gameControllerdb = ""; //if it doesn't exist, pass in an empty string - MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, keybinderUser, keybinderUserExists, gameControllerdb, mGrab); + MWInput::InputManager* input = new MWInput::InputManager (mWindow, mViewer, mScreenCaptureHandler, mScreenCaptureOperation, keybinderUser, keybinderUserExists, gameControllerdb, mGrab); mEnvironment.setInputManager (input); std::string myguiResources = (mResDir / "mygui").string(); @@ -641,8 +641,11 @@ void OMW::Engine::go() settingspath = loadSettings (settings); - mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(new WriteScreenshotToFileOperation(mCfgMgr.getUserDataPath().string(), - Settings::Manager::getString("screenshot format", "General"))); + mScreenCaptureOperation = new WriteScreenshotToFileOperation(mCfgMgr.getUserDataPath().string(), + Settings::Manager::getString("screenshot format", "General")); + + mScreenCaptureHandler = new osgViewer::ScreenCaptureHandler(mScreenCaptureOperation); + mViewer->addEventHandler(mScreenCaptureHandler); mEnvironment.setFrameRateLimit(Settings::Manager::getFloat("framerate limit", "Video")); diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index bf144bfed..3cbd4b6e7 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -7,7 +7,7 @@ #include #include - +#include #include "mwbase/environment.hpp" @@ -82,6 +82,7 @@ namespace OMW boost::filesystem::path mResDir; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; + osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation; std::string mCellName; std::vector mContentFiles; bool mSkipMenu; diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index cd2e8408b..e1c85744e 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -37,12 +37,14 @@ namespace MWInput SDL_Window* window, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler, + osgViewer::ScreenCaptureHandler::CaptureOperation *screenCaptureOperation, const std::string& userFile, bool userFileExists, const std::string& controllerBindingsFile, bool grab) : mWindow(window) , mWindowVisible(true) , mViewer(viewer) , mScreenCaptureHandler(screenCaptureHandler) + , mScreenCaptureOperation(screenCaptureOperation) , mJoystickLastUsed(false) , mPlayer(NULL) , mInputManager(NULL) @@ -1027,29 +1029,7 @@ namespace MWInput { osg::ref_ptr screenshot (new osg::Image); MWBase::Environment::get().getWorld()->screenshot360(screenshot.get()); -(*mScreenCaptureHandler->getCaptureOperation()) (*(screenshot.get()),0); - -/* - osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg"); - - if (!readerwriter) - { - std::cerr << "Error: Unable to write 360 degree screenshot, can't find a jpg ReaderWriter" << std::endl; - return; - } - - std::ofstream outfile; - outfile.open("test.jpg"); - - osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*screenshot, outfile); - - if (!result.success()) - { - outfile << "Error: Unable to write screenshot: " << result.message() << " code " << result.status() << std::endl; - return; - } - - outfile.close(); */ + (*mScreenCaptureOperation) (*(screenshot.get()),0); } void InputManager::toggleInventory() diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index 07b43d0ac..5d3e88eab 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -4,6 +4,7 @@ #include "../mwgui/mode.hpp" #include +#include #include #include @@ -14,7 +15,6 @@ #include "../mwbase/inputmanager.hpp" - namespace MWWorld { class Player; @@ -74,6 +74,7 @@ namespace MWInput SDL_Window* window, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler, + osgViewer::ScreenCaptureHandler::CaptureOperation *screenCaptureOperation, const std::string& userFile, bool userFileExists, const std::string& controllerBindingsFile, bool grab); @@ -158,6 +159,7 @@ namespace MWInput bool mWindowVisible; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; + osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation; bool mJoystickLastUsed; MWWorld::Player* mPlayer; diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 4ea602f3f..a08e7a0a2 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -625,6 +625,13 @@ namespace MWRender class SphericalScreenshot { public: + typedef enum + { + MAPPING_CYLINDRICAL = 0, + MAPPING_SPHERICAL, + MAPPING_SMALL_PLANET + } SphericalScreenshotMapping; + SphericalScreenshot(int size) { mSize = size; @@ -638,13 +645,26 @@ namespace MWRender return mImages[index].get(); } - void create(osg::Image *dest, int w, int h) + void create(osg::Image *dest, int w, int h, SphericalScreenshotMapping mapping) { dest->allocateImage(w,h,mImages[0]->r(),mImages[0]->getPixelFormat(),mImages[0]->getDataType()); for (int j = 0; j < h; ++j) for (int i = 0; i < w; ++i) - dest->setColor(getColorByDirection(sphericalCoords(i / ((float) w), j / ((float) h))),i,j); + { + osg::Vec3d coords; + osg::Vec2d normalizedXY = osg::Vec2d(i / ((float) w), j / ((float) h)); + + switch (mapping) + { + case MAPPING_CYLINDRICAL: coords = cylindricalCoords(normalizedXY.x(),normalizedXY.y()); break; + case MAPPING_SPHERICAL: coords = sphericalCoords(normalizedXY.x(),normalizedXY.y()); break; + case MAPPING_SMALL_PLANET: coords = smallPlanetCoords(normalizedXY.x(),normalizedXY.y()); break; + default: break; + } + + dest->setColor(getColorByDirection(coords),i,j); + } } osg::Vec3d cylindricalCoords(double x, double y) @@ -682,18 +702,18 @@ namespace MWRender osg::Vec4 getColorByDirection(osg::Vec3d d) { + // for details see OpenGL 4.4 specification page 241 + double x, y; - double ma; // see OpenGL 4.4 specification page 241 + double ma; int side; double ax, ay, az; - ax = d.x() > 0 ? d.x() : -d.x(); + ax = d.x() > 0 ? d.x() : -d.x(); // abs behaves weirdly for some reason ay = d.y() > 0 ? d.y() : -d.y(); az = d.z() > 0 ? d.z() : -d.z(); - if (ax > ay) - { if (ax > az) { side = d.x() > 0 ? 1 : 3; @@ -704,9 +724,7 @@ namespace MWRender side = d.z() > 0 ? 5 : 4; ma = az; } - } else - { if (ay > az) { side = d.y() > 0 ? 0 : 2; @@ -717,12 +735,11 @@ namespace MWRender side = d.z() > 0 ? 5 : 4; ma = az; } - } switch (side) { - case 0: x = d.x(); y = d.z(); break; - case 1: x = -d.y(); y = d.z(); break; + case 0: x = d.x(); y = d.z(); break; + case 1: x = -d.y(); y = d.z(); break; case 2: x = -d.x(); y = d.z(); break; case 3: x = d.y(); y = d.z(); break; case 4: x = d.x(); y = d.y(); break; @@ -745,8 +762,10 @@ namespace MWRender void RenderingManager::screenshot360(osg::Image* image) { - int w = 1024; - SphericalScreenshot s(w); + int cubeWidth = 1024; + int screenshotWidth = 1600; + int screenshotHeight = 1280; + SphericalScreenshot s(cubeWidth); osg::Vec3 directions[6] = { osg::Vec3(0,0,-1), @@ -766,18 +785,18 @@ namespace MWRender for (int i = 0; i < 6; i++) // for each cube side { osg::Image *sideImage = s.getImage(i); - screenshot(sideImage,w,w,directions[i]); + screenshot(sideImage,cubeWidth,cubeWidth,directions[i],true); } if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(1); - s.create(image,1600,1600); + s.create(image,screenshotWidth,screenshotHeight,SphericalScreenshot::MAPPING_SPHERICAL); mFieldOfView = fovBackup; } - void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Vec3 direction) + void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Vec3 direction, bool disableWaterEffects) { osg::ref_ptr rttCamera (new osg::Camera); rttCamera->setNodeMask(Mask_RenderToTexture); @@ -786,9 +805,7 @@ namespace MWRender rttCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); rttCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); rttCamera->setProjectionMatrixAsPerspective(mFieldOfView, w/float(h), mNearClip, mViewDistance); - rttCamera->setViewMatrix( - mViewer->getCamera()->getViewMatrix() * osg::Matrixd::rotate(osg::Vec3(0,0,-1),direction) - ); + rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * osg::Matrixd::rotate(osg::Vec3(0,0,-1),direction)); rttCamera->setViewport(0, 0, w, h); @@ -816,13 +833,16 @@ namespace MWRender // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() - mWater->setEffectsEnabled(false); + + if (disableWaterEffects) + mWater->setEffectsEnabled(false); mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); - mWater->setEffectsEnabled(true); + if (disableWaterEffects) + mWater->setEffectsEnabled(true); callback->waitTillDone(); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index e6e2e1c0a..cb0723397 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -125,7 +125,7 @@ namespace MWRender void setWaterHeight(float level); /// Take a screenshot of w*h onto the given image, not including the GUI. - void screenshot(osg::Image* image, int w, int h, osg::Vec3 direction=osg::Vec3(0,0,-1)); + void screenshot(osg::Image* image, int w, int h, osg::Vec3 direction=osg::Vec3(0,0,-1), bool disableWaterEffects=false); void screenshot360(osg::Image* image); struct RayResult From d763e9fe46be5189e4531e074be6a90309d6f77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 9 Nov 2017 20:25:29 +0100 Subject: [PATCH 012/282] add settings for spherical screenshots --- apps/openmw/mwinput/inputmanagerimp.cpp | 2 ++ apps/openmw/mwrender/renderingmanager.cpp | 15 +++++++++------ components/settings/settings.cpp | 12 ++++++++++++ components/settings/settings.hpp | 1 + 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index e1c85744e..9ea1f8e16 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1029,6 +1029,8 @@ namespace MWInput { osg::ref_ptr screenshot (new osg::Image); MWBase::Environment::get().getWorld()->screenshot360(screenshot.get()); + + // calling mScreenCaptureHandler->getCaptureOperation() here caused segfault for some reason (*mScreenCaptureOperation) (*(screenshot.get()),0); } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index a08e7a0a2..bac150459 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -627,8 +627,8 @@ namespace MWRender public: typedef enum { - MAPPING_CYLINDRICAL = 0, - MAPPING_SPHERICAL, + MAPPING_SPHERICAL = 0, + MAPPING_CYLINDRICAL, MAPPING_SMALL_PLANET } SphericalScreenshotMapping; @@ -762,9 +762,12 @@ namespace MWRender void RenderingManager::screenshot360(osg::Image* image) { - int cubeWidth = 1024; - int screenshotWidth = 1600; - int screenshotHeight = 1280; + int screenshotWidth = Settings::Manager::tryGetInt("s360 width","Video",mViewer->getCamera()->getViewport()->width()); + int screenshotHeight = Settings::Manager::tryGetInt("s360 height","Video",mViewer->getCamera()->getViewport()->height()); + SphericalScreenshot::SphericalScreenshotMapping mapping = static_cast( + Settings::Manager::tryGetInt("s360 mapping","Video",SphericalScreenshot::MAPPING_SPHERICAL)); + + int cubeWidth = screenshotWidth / 2; SphericalScreenshot s(cubeWidth); osg::Vec3 directions[6] = { @@ -791,7 +794,7 @@ namespace MWRender if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(1); - s.create(image,screenshotWidth,screenshotHeight,SphericalScreenshot::MAPPING_SPHERICAL); + s.create(image,screenshotWidth,mapping != SphericalScreenshot::MAPPING_SMALL_PLANET ? screenshotHeight : screenshotWidth,mapping); mFieldOfView = fovBackup; } diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index e93642ee2..4e250974a 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -401,6 +401,18 @@ int Manager::getInt (const std::string& setting, const std::string& category) return parseInt( getString(setting, category) ); } +int Manager::tryGetInt (const std::string &setting, const std::string& category, int defaultValue) +{ + try + { + return getInt(setting,category); + } + catch (std::runtime_error) + { + return defaultValue; + } +} + bool Manager::getBool (const std::string& setting, const std::string& category) { return parseBool( getString(setting, category) ); diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index 7adcb9b39..9e80c21a6 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -39,6 +39,7 @@ namespace Settings ///< returns the list of changed settings and then clears it static int getInt (const std::string& setting, const std::string& category); + static int tryGetInt (const std::string &setting, const std::string& category, int defaultValue); static float getFloat (const std::string& setting, const std::string& category); static std::string getString (const std::string& setting, const std::string& category); static bool getBool (const std::string& setting, const std::string& category); From e54c0a90fadda522a25fdc8fe5ce581ee75780a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 9 Nov 2017 21:14:02 +0100 Subject: [PATCH 013/282] fix mirrored spherical screenshots --- apps/openmw/mwrender/renderingmanager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index bac150459..c018c1707 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -669,14 +669,14 @@ namespace MWRender osg::Vec3d cylindricalCoords(double x, double y) { - osg::Vec3 result = osg::Vec3d(cos(x * 2 * osg::PI),sin(x * 2 * osg::PI),y * 2.0 - 1.0); + osg::Vec3 result = osg::Vec3d(cos(-1 * x * 2 * osg::PI),sin(-1 * x * 2 * osg::PI),y * 2.0 - 1.0); result.normalize(); return result; } osg::Vec3d sphericalCoords(double x, double y) { - x = x * 2 * osg::PI; + x = -1 * x * 2 * osg::PI; y = (y - 0.5) * osg::PI; osg::Vec3 result = osg::Vec3(0.0,cos(y),sin(y)); From 1b184d871646f6ec8954dca838c4ec4ee6194015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 9 Nov 2017 23:09:13 +0100 Subject: [PATCH 014/282] correct player mask --- apps/openmw/mwrender/renderingmanager.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index c018c1707..9c6eac3c0 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -782,6 +782,8 @@ namespace MWRender double fovBackup = mFieldOfView; mFieldOfView = 90.0; // each side sees 90 degrees + int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); + if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(0); @@ -791,11 +793,9 @@ namespace MWRender screenshot(sideImage,cubeWidth,cubeWidth,directions[i],true); } - if (mCamera->isFirstPerson()) - mPlayerAnimation->getObjectRoot()->setNodeMask(1); - s.create(image,screenshotWidth,mapping != SphericalScreenshot::MAPPING_SMALL_PLANET ? screenshotHeight : screenshotWidth,mapping); + mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); mFieldOfView = fovBackup; } From 43c49e2f3135f13297badb42d752c35ed4625a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 9 Nov 2017 23:12:23 +0100 Subject: [PATCH 015/282] delete accidentally commited files --- apps/openmw/mwmechanics/character.cpp.orig | 2464 -------------------- apps/openmw/mwmechanics/character.cpp.rej | 46 - 2 files changed, 2510 deletions(-) delete mode 100644 apps/openmw/mwmechanics/character.cpp.orig delete mode 100644 apps/openmw/mwmechanics/character.cpp.rej diff --git a/apps/openmw/mwmechanics/character.cpp.orig b/apps/openmw/mwmechanics/character.cpp.orig deleted file mode 100644 index f262850a4..000000000 --- a/apps/openmw/mwmechanics/character.cpp.orig +++ /dev/null @@ -1,2464 +0,0 @@ -/* - * OpenMW - The completely unofficial reimplementation of Morrowind - * - * This file (character.cpp) is part of the OpenMW package. - * - * OpenMW is distributed as free software: you can redistribute it - * and/or modify it under the terms of the GNU General Public License - * version 3, as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * version 3 along with this program. If not, see - * http://www.gnu.org/licenses/ . - */ - -#include "character.hpp" - -#include - -#include - -#include - -#include - -#include "../mwrender/animation.hpp" - -#include "../mwbase/environment.hpp" -#include "../mwbase/world.hpp" -#include "../mwbase/soundmanager.hpp" -#include "../mwbase/windowmanager.hpp" - -#include "../mwworld/class.hpp" -#include "../mwworld/inventorystore.hpp" -#include "../mwworld/esmstore.hpp" -#include "../mwworld/player.hpp" - -#include "movement.hpp" -#include "npcstats.hpp" -#include "creaturestats.hpp" -#include "security.hpp" -#include "actorutil.hpp" -#include "spellcasting.hpp" - -namespace -{ - -// Wraps a value to (-PI, PI] -void wrap(float& rad) -{ - if (rad>0) - rad = std::fmod(rad+osg::PI, 2.0f*osg::PI)-osg::PI; - else - rad = std::fmod(rad-osg::PI, 2.0f*osg::PI)+osg::PI; -} - -std::string toString(int num) -{ - std::ostringstream stream; - stream << num; - return stream.str(); -} - -std::string getBestAttack (const ESM::Weapon* weapon) -{ - int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; - int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; - int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; - if (slash == chop && slash == thrust) - return "slash"; - else if (thrust >= chop && thrust >= slash) - return "thrust"; - else if (slash >= chop && slash >= thrust) - return "slash"; - else - return "chop"; -} - -// Converts a movement Run state to its equivalent Walk state. -MWMechanics::CharacterState runStateToWalkState (MWMechanics::CharacterState state) -{ - using namespace MWMechanics; - CharacterState ret = state; - switch (state) - { - case CharState_RunForward: - ret = CharState_WalkForward; - break; - case CharState_RunBack: - ret = CharState_WalkBack; - break; - case CharState_RunLeft: - ret = CharState_WalkLeft; - break; - case CharState_RunRight: - ret = CharState_WalkRight; - break; - case CharState_SwimRunForward: - ret = CharState_SwimWalkForward; - break; - case CharState_SwimRunBack: - ret = CharState_SwimWalkBack; - break; - case CharState_SwimRunLeft: - ret = CharState_SwimWalkLeft; - break; - case CharState_SwimRunRight: - ret = CharState_SwimWalkRight; - break; - default: - break; - } - return ret; -} - -float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight) -{ - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Store &store = world->getStore().get(); - - const float fallDistanceMin = store.find("fFallDamageDistanceMin")->getFloat(); - - if (fallHeight >= fallDistanceMin) - { - const float acrobaticsSkill = static_cast(ptr.getClass().getSkill(ptr, ESM::Skill::Acrobatics)); - const float jumpSpellBonus = ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Jump).getMagnitude(); - const float fallAcroBase = store.find("fFallAcroBase")->getFloat(); - const float fallAcroMult = store.find("fFallAcroMult")->getFloat(); - const float fallDistanceBase = store.find("fFallDistanceBase")->getFloat(); - const float fallDistanceMult = store.find("fFallDistanceMult")->getFloat(); - - float x = fallHeight - fallDistanceMin; - x -= (1.5f * acrobaticsSkill) + jumpSpellBonus; - x = std::max(0.0f, x); - - float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill); - x = fallDistanceBase + fallDistanceMult * x; - x *= a; - - return x; - } - return 0.f; -} - -} - -namespace MWMechanics -{ - -struct StateInfo { - CharacterState state; - const char groupname[32]; -}; - -static const StateInfo sMovementList[] = { - { CharState_WalkForward, "walkforward" }, - { CharState_WalkBack, "walkback" }, - { CharState_WalkLeft, "walkleft" }, - { CharState_WalkRight, "walkright" }, - - { CharState_SwimWalkForward, "swimwalkforward" }, - { CharState_SwimWalkBack, "swimwalkback" }, - { CharState_SwimWalkLeft, "swimwalkleft" }, - { CharState_SwimWalkRight, "swimwalkright" }, - - { CharState_RunForward, "runforward" }, - { CharState_RunBack, "runback" }, - { CharState_RunLeft, "runleft" }, - { CharState_RunRight, "runright" }, - - { CharState_SwimRunForward, "swimrunforward" }, - { CharState_SwimRunBack, "swimrunback" }, - { CharState_SwimRunLeft, "swimrunleft" }, - { CharState_SwimRunRight, "swimrunright" }, - - { CharState_SneakForward, "sneakforward" }, - { CharState_SneakBack, "sneakback" }, - { CharState_SneakLeft, "sneakleft" }, - { CharState_SneakRight, "sneakright" }, - - { CharState_Jump, "jump" }, - - { CharState_TurnLeft, "turnleft" }, - { CharState_TurnRight, "turnright" }, - { CharState_SwimTurnLeft, "swimturnleft" }, - { CharState_SwimTurnRight, "swimturnright" }, -}; -static const StateInfo *sMovementListEnd = &sMovementList[sizeof(sMovementList)/sizeof(sMovementList[0])]; - - -class FindCharState { - CharacterState state; - -public: - FindCharState(CharacterState _state) : state(_state) { } - - bool operator()(const StateInfo &info) const - { return info.state == state; } -}; - - -static const struct WeaponInfo { - WeaponType type; - const char shortgroup[16]; - const char longgroup[16]; -} sWeaponTypeList[] = { - { WeapType_HandToHand, "hh", "handtohand" }, - { WeapType_OneHand, "1h", "weapononehand" }, - { WeapType_TwoHand, "2c", "weapontwohand" }, - { WeapType_TwoWide, "2w", "weapontwowide" }, - { WeapType_BowAndArrow, "1h", "bowandarrow" }, - { WeapType_Crossbow, "crossbow", "crossbow" }, - { WeapType_Thrown, "1h", "throwweapon" }, - { WeapType_PickProbe, "1h", "pickprobe" }, - { WeapType_Spell, "spell", "spellcast" }, -}; -static const WeaponInfo *sWeaponTypeListEnd = &sWeaponTypeList[sizeof(sWeaponTypeList)/sizeof(sWeaponTypeList[0])]; - -class FindWeaponType { - WeaponType type; - -public: - FindWeaponType(WeaponType _type) : type(_type) { } - - bool operator()(const WeaponInfo &weap) const - { return weap.type == type; } -}; - -std::string CharacterController::chooseRandomGroup (const std::string& prefix, int* num) const -{ - int numAnims=0; - while (mAnimation->hasAnimation(prefix + toString(numAnims+1))) - ++numAnims; - - int roll = Misc::Rng::rollDice(numAnims) + 1; // [1, numAnims] - if (num) - *num = roll; - return prefix + toString(roll); -} - -void CharacterController::refreshHitRecoilAnims() -{ - bool recovery = mPtr.getClass().getCreatureStats(mPtr).getHitRecovery(); - bool knockdown = mPtr.getClass().getCreatureStats(mPtr).getKnockedDown(); - bool block = mPtr.getClass().getCreatureStats(mPtr).getBlock(); - bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); - if(mHitState == CharState_None) - { - if ((mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() < 0 - || mPtr.getClass().getCreatureStats(mPtr).getFatigue().getBase() == 0) - && mAnimation->hasAnimation("knockout")) - { - if (isSwimming && mAnimation->hasAnimation("swimknockout")) - { - mHitState = CharState_SwimKnockOut; - mCurrentHit = "swimknockout"; - } - else - { - mHitState = CharState_KnockOut; - mCurrentHit = "knockout"; - } - - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul); - mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(true); - } - else if(knockdown && mAnimation->hasAnimation("knockdown")) - { - if (isSwimming && mAnimation->hasAnimation("swimknockdown")) - { - mHitState = CharState_SwimKnockDown; - mCurrentHit = "swimknockdown"; - } - else - { - mHitState = CharState_KnockDown; - mCurrentHit = "knockdown"; - } - - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - else if (recovery) - { - std::string anim = isSwimming ? chooseRandomGroup("swimhit") : chooseRandomGroup("hit"); - if (isSwimming && mAnimation->hasAnimation(anim)) - { - mHitState = CharState_SwimHit; - mCurrentHit = anim; - mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - else - { - anim = chooseRandomGroup("hit"); - if (mAnimation->hasAnimation(anim)) - { - mHitState = CharState_Hit; - mCurrentHit = anim; - mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0); - } - } - } - else if (block && mAnimation->hasAnimation("shield")) - { - mHitState = CharState_Block; - mCurrentHit = "shield"; - MWRender::Animation::AnimPriority priorityBlock (Priority_Hit); - priorityBlock[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block; - mAnimation->play(mCurrentHit, priorityBlock, MWRender::Animation::BlendMask_All, true, 1, "block start", "block stop", 0.0f, 0); - } - - // Cancel upper body animations - if (isKnockedOut() || isKnockedDown()) - { - if (mUpperBodyState > UpperCharState_WeapEquiped) - { - mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_WeapEquiped; - } - else if (mUpperBodyState > UpperCharState_Nothing && mUpperBodyState < UpperCharState_WeapEquiped) - { - mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_Nothing; - } - } - } - else if(!mAnimation->isPlaying(mCurrentHit)) - { - mCurrentHit.erase(); - if (knockdown) - mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(false); - if (recovery) - mPtr.getClass().getCreatureStats(mPtr).setHitRecovery(false); - if (block) - mPtr.getClass().getCreatureStats(mPtr).setBlock(false); - mHitState = CharState_None; - } - else if (isKnockedOut() && mPtr.getClass().getCreatureStats(mPtr).getFatigue().getCurrent() > 0) - { - mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown; - mAnimation->disable(mCurrentHit); - mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "loop stop", "stop", 0.0f, 0); - } - if (mHitState != CharState_None) - mIdleState = CharState_None; -} - -void CharacterController::refreshJumpAnims(const WeaponInfo* weap, JumpingState jump, bool force) -{ - if(force || jump != mJumpState) - { - mIdleState = CharState_None; - bool startAtLoop = (jump == mJumpState); - mJumpState = jump; - - std::string jumpAnimName; - MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All; - if(mJumpState != JumpState_None) - { - jumpAnimName = "jump"; - if(weap != sWeaponTypeListEnd) - { - jumpAnimName += weap->shortgroup; - if(!mAnimation->hasAnimation(jumpAnimName)) - { - jumpmask = MWRender::Animation::BlendMask_LowerBody; - jumpAnimName = "jump"; - } - } - } - - if(mJumpState == JumpState_InAir) - { - mAnimation->disable(mCurrentJump); - mCurrentJump = jumpAnimName; - if (mAnimation->hasAnimation("jump")) - mAnimation->play(mCurrentJump, Priority_Jump, jumpmask, false, - 1.0f, (startAtLoop?"loop start":"start"), "stop", 0.0f, ~0ul); - } - else if (mJumpState == JumpState_Landing) - { - if (startAtLoop) - mAnimation->disable(mCurrentJump); - - if (mAnimation->hasAnimation("jump")) - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, - 1.0f, "loop stop", "stop", 0.0f, 0); - } - else // JumpState_None - { - if (mCurrentJump.length() > 0) - { - mAnimation->disable(mCurrentJump); - mCurrentJump.clear(); - } - } - } -} - -void CharacterController::refreshMovementAnims(const WeaponInfo* weap, CharacterState movement, bool force) -{ - if(force || movement != mMovementState) - { - mMovementState = movement; - - if (movement != CharState_None) - mIdleState = CharState_None; - - std::string movementAnimName; - MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All; - const StateInfo *movestate = std::find_if(sMovementList, sMovementListEnd, FindCharState(mMovementState)); - if(movestate != sMovementListEnd) - { - movementAnimName = movestate->groupname; - if(weap != sWeaponTypeListEnd && movementAnimName.find("swim") == std::string::npos) - { - movementAnimName += weap->shortgroup; - if(!mAnimation->hasAnimation(movementAnimName)) - { - movemask = MWRender::Animation::BlendMask_LowerBody; - movementAnimName = movestate->groupname; - } - } - - if(!mAnimation->hasAnimation(movementAnimName)) - { - std::string::size_type swimpos = movementAnimName.find("swim"); - if(swimpos == std::string::npos) - { - std::string::size_type runpos = movementAnimName.find("run"); - if (runpos != std::string::npos) - { - movementAnimName.replace(runpos, runpos+3, "walk"); - if (!mAnimation->hasAnimation(movementAnimName)) - movementAnimName.clear(); - } - else - movementAnimName.clear(); - } - else - { - movementAnimName.erase(swimpos, 4); - if (weap != sWeaponTypeListEnd) - { - std::string weapMovementAnimName = movementAnimName + weap->shortgroup; - if(mAnimation->hasAnimation(weapMovementAnimName)) - movementAnimName = weapMovementAnimName; - else - movemask = MWRender::Animation::BlendMask_LowerBody; - } - - if (!mAnimation->hasAnimation(movementAnimName)) - movementAnimName.clear(); - } - } - } - - /* If we're playing the same animation, restart from the loop start instead of the - * beginning. */ - int mode = ((movementAnimName == mCurrentMovement) ? 2 : 1); - - mMovementAnimationControlled = true; - - mAnimation->disable(mCurrentMovement); - mCurrentMovement = movementAnimName; - if(!mCurrentMovement.empty()) - { - bool isrunning = mPtr.getClass().getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) - && !MWBase::Environment::get().getWorld()->isFlying(mPtr); - - // For non-flying creatures, MW uses the Walk animation to calculate the animation velocity - // even if we are running. This must be replicated, otherwise the observed speed would differ drastically. - std::string anim = mCurrentMovement; - mAdjustMovementAnimSpeed = true; - if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() - && !(mPtr.get()->mBase->mFlags & ESM::Creature::Flies)) - { - CharacterState walkState = runStateToWalkState(mMovementState); - const StateInfo *stateinfo = std::find_if(sMovementList, sMovementListEnd, FindCharState(walkState)); - anim = stateinfo->groupname; - - mMovementAnimSpeed = mAnimation->getVelocity(anim); - if (mMovementAnimSpeed <= 1.0f) - { - // Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward), - // then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist - // we will play without any scaling. - // Makes the speed attribute of most water creatures totally useless. - // And again, this can not be fixed without patching game data. - mAdjustMovementAnimSpeed = false; - mMovementAnimSpeed = 1.f; - } - } - else - { - mMovementAnimSpeed = mAnimation->getVelocity(anim); - - if (mMovementAnimSpeed <= 1.0f) - { - // The first person anims don't have any velocity to calculate a speed multiplier from. - // We use the third person velocities instead. - // FIXME: should be pulled from the actual animation, but it is not presently loaded. - mMovementAnimSpeed = (isrunning ? 222.857f : 154.064f); - mMovementAnimationControlled = false; - } - } - - mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, - 1.f, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul, true); - } - } -} - -void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterState idle, bool force) -{ - if(force || idle != mIdleState || - ((idle == mIdleState) && !mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty())) - { - mIdleState = idle; - size_t numLoops = ~0ul; - - std::string idleGroup; - MWRender::Animation::AnimPriority idlePriority (Priority_Default); - // Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to - // "idle"+weapon or "idle". - if(mIdleState == CharState_IdleSwim && mAnimation->hasAnimation("idleswim")) - { - idleGroup = "idleswim"; - idlePriority = Priority_SwimIdle; - } - else if(mIdleState == CharState_IdleSneak && mAnimation->hasAnimation("idlesneak")) - { - idleGroup = "idlesneak"; - idlePriority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; - } - else if(mIdleState != CharState_None) - { - idleGroup = "idle"; - if(weap != sWeaponTypeListEnd) - { - idleGroup += weap->shortgroup; - if(!mAnimation->hasAnimation(idleGroup)) - idleGroup = "idle"; - - // play until the Loop Stop key 2 to 5 times, then play until the Stop key - // this replicates original engine behavior for the "Idle1h" 1st-person animation - numLoops = 1 + Misc::Rng::rollDice(4); - } - } - - mAnimation->disable(mCurrentIdle); - mCurrentIdle = idleGroup; - if(!mCurrentIdle.empty()) - mAnimation->play(mCurrentIdle, idlePriority, MWRender::Animation::BlendMask_All, false, - 1.0f, "start", "stop", 0.0f, numLoops, true); - } -} - -void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force) -{ - if (mPtr.getClass().isActor()) - refreshHitRecoilAnims(); - - const WeaponInfo *weap = std::find_if(sWeaponTypeList, sWeaponTypeListEnd, FindWeaponType(mWeaponType)); - if (!mPtr.getClass().isBipedal(mPtr)) - weap = sWeaponTypeListEnd; - - refreshJumpAnims(weap, jump, force); - refreshMovementAnims(weap, movement, force); - - // idle handled last as it can depend on the other states - // FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming update), - // the idle animation should be displayed - if ((mUpperBodyState != UpperCharState_Nothing - || (mMovementState != CharState_None && !isTurning()) - || mHitState != CharState_None) - && !mPtr.getClass().isBipedal(mPtr)) - idle = CharState_None; - - refreshIdleAnims(weap, idle, force); -} - - -void getWeaponGroup(WeaponType weaptype, std::string &group) -{ - const WeaponInfo *info = std::find_if(sWeaponTypeList, sWeaponTypeListEnd, FindWeaponType(weaptype)); - if(info != sWeaponTypeListEnd) - group = info->longgroup; - else - group.clear(); -} - - -MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype) -{ - if(stats.getDrawState() == DrawState_Spell) - { - *weaptype = WeapType_Spell; - return inv.end(); - } - - if(stats.getDrawState() == MWMechanics::DrawState_Weapon) - { - MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - if(weapon == inv.end()) - *weaptype = WeapType_HandToHand; - else - { - const std::string &type = weapon->getTypeName(); - if(type == typeid(ESM::Lockpick).name() || type == typeid(ESM::Probe).name()) - *weaptype = WeapType_PickProbe; - else if(type == typeid(ESM::Weapon).name()) - { - MWWorld::LiveCellRef *ref = weapon->get(); - ESM::Weapon::Type weaponType = (ESM::Weapon::Type)ref->mBase->mData.mType; - switch(weaponType) - { - case ESM::Weapon::ShortBladeOneHand: - case ESM::Weapon::LongBladeOneHand: - case ESM::Weapon::BluntOneHand: - case ESM::Weapon::AxeOneHand: - case ESM::Weapon::Arrow: - case ESM::Weapon::Bolt: - *weaptype = WeapType_OneHand; - break; - case ESM::Weapon::LongBladeTwoHand: - case ESM::Weapon::BluntTwoClose: - case ESM::Weapon::AxeTwoHand: - *weaptype = WeapType_TwoHand; - break; - case ESM::Weapon::BluntTwoWide: - case ESM::Weapon::SpearTwoWide: - *weaptype = WeapType_TwoWide; - break; - case ESM::Weapon::MarksmanBow: - *weaptype = WeapType_BowAndArrow; - break; - case ESM::Weapon::MarksmanCrossbow: - *weaptype = WeapType_Crossbow; - break; - case ESM::Weapon::MarksmanThrown: - *weaptype = WeapType_Thrown; - break; - } - } - } - - return weapon; - } - - return inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); -} - -void CharacterController::playDeath(float startpoint, CharacterState death) -{ - switch (death) - { - case CharState_SwimDeath: - mCurrentDeath = "swimdeath"; - break; - case CharState_SwimDeathKnockDown: - mCurrentDeath = "swimdeathknockdown"; - break; - case CharState_SwimDeathKnockOut: - mCurrentDeath = "swimdeathknockout"; - break; - case CharState_DeathKnockDown: - mCurrentDeath = "deathknockdown"; - break; - case CharState_DeathKnockOut: - mCurrentDeath = "deathknockout"; - break; - default: - mCurrentDeath = "death" + toString(death - CharState_Death1 + 1); - } - mDeathState = death; - - mPtr.getClass().getCreatureStats(mPtr).setDeathAnimation(mDeathState - CharState_Death1); - - // For dead actors, refreshCurrentAnims is no longer called, so we need to disable the movement state manually. - // Note that these animations wouldn't actually be visible (due to the Death animation's priority being higher). - // However, they could still trigger text keys, such as Hit events, or sounds. - mMovementState = CharState_None; - mAnimation->disable(mCurrentMovement); - mCurrentMovement = ""; - mUpperBodyState = UpperCharState_Nothing; - mAnimation->disable(mCurrentWeapon); - mCurrentWeapon = ""; - mHitState = CharState_None; - mAnimation->disable(mCurrentHit); - mCurrentHit = ""; - mIdleState = CharState_None; - mAnimation->disable(mCurrentIdle); - mCurrentIdle = ""; - mJumpState = JumpState_None; - mAnimation->disable(mCurrentJump); - mCurrentJump = ""; - mMovementAnimationControlled = true; - - mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All, - false, 1.0f, "start", "stop", startpoint, 0); -} - -CharacterState CharacterController::chooseRandomDeathState() const -{ - int selected=0; - chooseRandomGroup("death", &selected); - return static_cast(CharState_Death1 + (selected-1)); -} - -void CharacterController::playRandomDeath(float startpoint) -{ - if (mPtr == getPlayer()) - { - // The first-person animations do not include death, so we need to - // force-switch to third person before playing the death animation. - MWBase::Environment::get().getWorld()->useDeathCamera(); - } - - if(mHitState == CharState_SwimKnockDown && mAnimation->hasAnimation("swimdeathknockdown")) - { - mDeathState = CharState_SwimDeathKnockDown; - } - else if(mHitState == CharState_SwimKnockOut && mAnimation->hasAnimation("swimdeathknockout")) - { - mDeathState = CharState_SwimDeathKnockOut; - } - else if(MWBase::Environment::get().getWorld()->isSwimming(mPtr) && mAnimation->hasAnimation("swimdeath")) - { - mDeathState = CharState_SwimDeath; - } - else if (mHitState == CharState_KnockDown && mAnimation->hasAnimation("deathknockdown")) - { - mDeathState = CharState_DeathKnockDown; - } - else if (mHitState == CharState_KnockOut && mAnimation->hasAnimation("deathknockout")) - { - mDeathState = CharState_DeathKnockOut; - } - else - { - mDeathState = chooseRandomDeathState(); - } - playDeath(startpoint, mDeathState); -} - -CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim) - : mPtr(ptr) - , mAnimation(anim) - , mIdleState(CharState_None) - , mMovementState(CharState_None) - , mMovementAnimSpeed(0.f) - , mAdjustMovementAnimSpeed(false) - , mHasMovedInXY(false) - , mMovementAnimationControlled(true) - , mDeathState(CharState_None) - , mFloatToSurface(true) - , mHitState(CharState_None) - , mUpperBodyState(UpperCharState_Nothing) - , mJumpState(JumpState_None) - , mWeaponType(WeapType_None) - , mAttackStrength(0.f) - , mSkipAnim(false) - , mSecondsOfSwimming(0) - , mSecondsOfRunning(0) - , mTurnAnimationThreshold(0) - , mAttackingOrSpell(false) -{ - if(!mAnimation) - return; - - mAnimation->setTextKeyListener(this); - - const MWWorld::Class &cls = mPtr.getClass(); - if(cls.isActor()) - { - /* Accumulate along X/Y only for now, until we can figure out how we should - * handle knockout and death which moves the character down. */ - mAnimation->setAccumulation(osg::Vec3f(1.0f, 1.0f, 0.0f)); - - if (cls.hasInventoryStore(mPtr)) - { - getActiveWeapon(cls.getCreatureStats(mPtr), cls.getInventoryStore(mPtr), &mWeaponType); - if (mWeaponType != WeapType_None) - { - mUpperBodyState = UpperCharState_WeapEquiped; - getWeaponGroup(mWeaponType, mCurrentWeapon); - } - - if(mWeaponType != WeapType_None && mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand) - { - mAnimation->showWeapons(true); - mAnimation->setWeaponGroup(mCurrentWeapon); - } - - mAnimation->showCarriedLeft(updateCarriedLeftVisible(mWeaponType)); - } - - if(!cls.getCreatureStats(mPtr).isDead()) - mIdleState = CharState_Idle; - else - { - const MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); - if (cStats.isDeathAnimationFinished()) - { - // Set the death state, but don't play it yet - // We will play it in the first frame, but only if no script set the skipAnim flag - signed char deathanim = cStats.getDeathAnimation(); - if (deathanim == -1) - mDeathState = chooseRandomDeathState(); - else - mDeathState = static_cast(CharState_Death1 + deathanim); - - mFloatToSurface = false; - } - // else: nothing to do, will detect death in the next frame and start playing death animation - } - } - else - { - /* Don't accumulate with non-actors. */ - mAnimation->setAccumulation(osg::Vec3f(0.f, 0.f, 0.f)); - - mIdleState = CharState_Idle; - } - - - if(mDeathState == CharState_None) - refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); - - mAnimation->runAnimation(0.f); - - unpersistAnimationState(); -} - -CharacterController::~CharacterController() -{ - if (mAnimation) - { - persistAnimationState(); - mAnimation->setTextKeyListener(NULL); - } -} - -void split(const std::string &s, char delim, std::vector &elems) { - std::stringstream ss(s); - std::string item; - while (std::getline(ss, item, delim)) { - elems.push_back(item); - } -} - -void CharacterController::handleTextKey(const std::string &groupname, const std::multimap::const_iterator &key, const std::multimap &map) -{ - const std::string &evt = key->second; - - if(evt.compare(0, 7, "sound: ") == 0) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->stopSound3D(mPtr, evt.substr(7)); - sndMgr->playSound3D(mPtr, evt.substr(7), 1.0f, 1.0f); - return; - } - if(evt.compare(0, 10, "soundgen: ") == 0) - { - std::string soundgen = evt.substr(10); - - // The event can optionally contain volume and pitch modifiers - float volume=1.f, pitch=1.f; - if (soundgen.find(" ") != std::string::npos) - { - std::vector tokens; - split(soundgen, ' ', tokens); - soundgen = tokens[0]; - if (tokens.size() >= 2) - { - std::stringstream stream; - stream << tokens[1]; - stream >> volume; - } - if (tokens.size() >= 3) - { - std::stringstream stream; - stream << tokens[2]; - stream >> pitch; - } - } - - std::string sound = mPtr.getClass().getSoundIdFromSndGen(mPtr, soundgen); - if(!sound.empty()) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(evt.compare(10, evt.size()-10, "left") == 0 || evt.compare(10, evt.size()-10, "right") == 0 || evt.compare(10, evt.size()-10, "land") == 0) - { - // Don't make foot sounds local for the player, it makes sense to keep them - // positioned on the ground. - sndMgr->playSound3D(mPtr, sound, volume, pitch, MWSound::Type::Foot, - MWSound::PlayMode::NoPlayerLocal); - } - else - { - sndMgr->stopSound3D(mPtr, sound); - sndMgr->playSound3D(mPtr, sound, volume, pitch); - } - } - return; - } - - if(evt.compare(0, groupname.size(), groupname) != 0 || - evt.compare(groupname.size(), 2, ": ") != 0) - { - // Not ours, skip it - return; - } - size_t off = groupname.size()+2; - size_t len = evt.size() - off; - - if(evt.compare(off, len, "equip attach") == 0) - mAnimation->showWeapons(true); - else if(evt.compare(off, len, "unequip detach") == 0) - mAnimation->showWeapons(false); - else if(evt.compare(off, len, "chop hit") == 0) - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); - else if(evt.compare(off, len, "slash hit") == 0) - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); - else if(evt.compare(off, len, "thrust hit") == 0) - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); - else if(evt.compare(off, len, "hit") == 0) - { - if (groupname == "attack1" || groupname == "swimattack1") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); - else if (groupname == "attack2" || groupname == "swimattack2") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); - else if (groupname == "attack3" || groupname == "swimattack3") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); - else - mPtr.getClass().hit(mPtr, mAttackStrength); - } - else if (!groupname.empty() - && (groupname.compare(0, groupname.size()-1, "attack") == 0 || groupname.compare(0, groupname.size()-1, "swimattack") == 0) - && evt.compare(off, len, "start") == 0) - { - std::multimap::const_iterator hitKey = key; - - // Not all animations have a hit key defined. If there is none, the hit happens with the start key. - bool hasHitKey = false; - while (hitKey != map.end()) - { - if (hitKey->second == groupname + ": hit") - { - hasHitKey = true; - break; - } - if (hitKey->second == groupname + ": stop") - break; - ++hitKey; - } - if (!hasHitKey) - { - if (groupname == "attack1" || groupname == "swimattack1") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop); - else if (groupname == "attack2" || groupname == "swimattack2") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash); - else if (groupname == "attack3" || groupname == "swimattack3") - mPtr.getClass().hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust); - } - } - else if (evt.compare(off, len, "shoot attach") == 0) - mAnimation->attachArrow(); - else if (evt.compare(off, len, "shoot release") == 0) - mAnimation->releaseArrow(mAttackStrength); - else if (evt.compare(off, len, "shoot follow attach") == 0) - mAnimation->attachArrow(); - - else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release" - // Make sure this key is actually for the RangeType we are casting. The flame atronach has - // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. - && evt.compare(off, len, mAttackType + " release") == 0) - { - MWBase::Environment::get().getWorld()->castSpell(mPtr); - } - - else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) - mPtr.getClass().block(mPtr); -} - -void CharacterController::updatePtr(const MWWorld::Ptr &ptr) -{ - mPtr = ptr; -} - -void CharacterController::updateIdleStormState(bool inwater) -{ - bool inStormDirection = false; - if (MWBase::Environment::get().getWorld()->isInStorm()) - { - osg::Vec3f stormDirection = MWBase::Environment::get().getWorld()->getStormDirection(); - osg::Vec3f characterDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); - inStormDirection = std::acos(stormDirection * characterDirection / (stormDirection.length() * characterDirection.length())) - > osg::DegreesToRadians(120.f); - } - if (inStormDirection && !inwater && mUpperBodyState == UpperCharState_Nothing && mAnimation->hasAnimation("idlestorm")) - { - float complete = 0; - mAnimation->getInfo("idlestorm", &complete); - - if (complete == 0) - mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::BlendMask_RightArm, false, - 1.0f, "start", "loop start", 0.0f, 0); - else if (complete == 1) - mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::BlendMask_RightArm, false, - 1.0f, "loop start", "loop stop", 0.0f, ~0ul); - } - else - { - if (mUpperBodyState == UpperCharState_Nothing) - { - if (mAnimation->isPlaying("idlestorm")) - { - if (mAnimation->getCurrentTime("idlestorm") < mAnimation->getTextKeyTime("idlestorm: loop stop")) - { - mAnimation->play("idlestorm", Priority_Storm, MWRender::Animation::BlendMask_RightArm, true, - 1.0f, "loop stop", "stop", 0.0f, 0); - } - } - } - else - mAnimation->disable("idlestorm"); - } -} - -bool CharacterController::updateCreatureState() -{ - const MWWorld::Class &cls = mPtr.getClass(); - CreatureStats &stats = cls.getCreatureStats(mPtr); - - WeaponType weapType = WeapType_None; - if(stats.getDrawState() == DrawState_Weapon) - weapType = WeapType_HandToHand; - else if (stats.getDrawState() == DrawState_Spell) - weapType = WeapType_Spell; - - if (weapType != mWeaponType) - { - mWeaponType = weapType; - if (mAnimation->isPlaying(mCurrentWeapon)) - mAnimation->disable(mCurrentWeapon); - } - - if(mAttackingOrSpell) - { - if(mUpperBodyState == UpperCharState_Nothing && mHitState == CharState_None) - { - MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); - - std::string startKey = "start"; - std::string stopKey = "stop"; - if (weapType == WeapType_Spell) - { - const std::string spellid = stats.getSpells().getSelectedSpell(); - if (!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) - { - MWMechanics::CastSpell cast(mPtr, NULL); - cast.playSpellCastingEffects(spellid); - - if (!mAnimation->hasAnimation("spellcast")) - MWBase::Environment::get().getWorld()->castSpell(mPtr); // No "release" text key to use, so cast immediately - else - { - const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); - const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0); - - switch(effectentry.mRange) - { - case 0: mAttackType = "self"; break; - case 1: mAttackType = "touch"; break; - case 2: mAttackType = "target"; break; - } - - startKey = mAttackType + " " + startKey; - stopKey = mAttackType + " " + stopKey; - mCurrentWeapon = "spellcast"; - } - } - else - mCurrentWeapon = ""; - } - if (weapType != WeapType_Spell || !mAnimation->hasAnimation("spellcast")) // Not all creatures have a dedicated spellcast animation - { - bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); - int roll = Misc::Rng::rollDice(3); // [0, 2] - if (roll == 0) - mCurrentWeapon = isSwimming && mAnimation->hasAnimation("swimattack1") ? "swimattack1" : "attack1"; - else if (roll == 1) - mCurrentWeapon = isSwimming && mAnimation->hasAnimation("swimattack2") ? "swimattack2" : "attack2"; - else - mCurrentWeapon = isSwimming && mAnimation->hasAnimation("swimattack3") ? "swimattack3" : "attack3"; - } - - if (!mCurrentWeapon.empty()) - { - mAnimation->play(mCurrentWeapon, Priority_Weapon, - MWRender::Animation::BlendMask_All, true, - 1, startKey, stopKey, - 0.0f, 0); - mUpperBodyState = UpperCharState_StartToMinAttack; - - mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); - - if (weapType == WeapType_HandToHand) - playSwishSound(0.0f); - } - } - - mAttackingOrSpell = false; - } - - bool animPlaying = mAnimation->getInfo(mCurrentWeapon); - if (!animPlaying) - mUpperBodyState = UpperCharState_Nothing; - return false; -} - -bool CharacterController::updateCarriedLeftVisible(WeaponType weaptype) const -{ - // Shields/torches shouldn't be visible during any operation involving two hands - // There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop", - // but they are also present in weapon drawing animation. - switch (weaptype) - { - case WeapType_Spell: - case WeapType_BowAndArrow: - case WeapType_Crossbow: - case WeapType_HandToHand: - case WeapType_TwoHand: - case WeapType_TwoWide: - return false; - default: - return true; - } -} - -bool CharacterController::updateWeaponState() -{ - const MWWorld::Class &cls = mPtr.getClass(); - CreatureStats &stats = cls.getCreatureStats(mPtr); - WeaponType weaptype = WeapType_None; - if(stats.getDrawState() == DrawState_Weapon) - weaptype = WeapType_HandToHand; - else if (stats.getDrawState() == DrawState_Spell) - weaptype = WeapType_Spell; - - const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf(); - - std::string soundid; - if (mPtr.getClass().hasInventoryStore(mPtr)) - { - MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); - if(weapon != inv.end() && !(weaptype == WeapType_None && mWeaponType == WeapType_Spell)) - { - soundid = (weaptype == WeapType_None) ? - weapon->getClass().getDownSoundId(*weapon) : - weapon->getClass().getUpSoundId(*weapon); - } - } - - MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); - priorityWeapon[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; - - bool forcestateupdate = false; - - // We should not play equipping animation and sound during weapon->weapon transition - bool isStillWeapon = weaptype > WeapType_HandToHand && weaptype < WeapType_Spell && - mWeaponType > WeapType_HandToHand && mWeaponType < WeapType_Spell; - - if(weaptype != mWeaponType && !isKnockedOut() && - !isKnockedDown() && !isRecovery()) - { - forcestateupdate = true; - - mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); - - std::string weapgroup; - if(weaptype == WeapType_None) - { - if ((!isWerewolf || mWeaponType != WeapType_Spell)) - { - getWeaponGroup(mWeaponType, weapgroup); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - 1.0f, "unequip start", "unequip stop", 0.0f, 0); - mUpperBodyState = UpperCharState_UnEquipingWeap; - } - } - else - { - getWeaponGroup(weaptype, weapgroup); - mAnimation->setWeaponGroup(weapgroup); - - if (!isStillWeapon) - { - mAnimation->showWeapons(false); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - 1.0f, "equip start", "equip stop", 0.0f, 0); - mUpperBodyState = UpperCharState_EquipingWeap; - } - - if(isWerewolf) - { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfEquip"); - if(sound) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); - } - } - } - - if(!soundid.empty() && !isStillWeapon) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mPtr, soundid, 1.0f, 1.0f); - } - - mWeaponType = weaptype; - getWeaponGroup(mWeaponType, mCurrentWeapon); - } - - if(isWerewolf) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - if(cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) - && mHasMovedInXY - && !MWBase::Environment::get().getWorld()->isSwimming(mPtr) - && mWeaponType == WeapType_None) - { - if(!sndMgr->getSoundPlaying(mPtr, "WolfRun")) - sndMgr->playSound3D(mPtr, "WolfRun", 1.0f, 1.0f, MWSound::Type::Sfx, - MWSound::PlayMode::Loop); - } - else - sndMgr->stopSound3D(mPtr, "WolfRun"); - } - - // Cancel attack if we no longer have ammunition - bool ammunition = true; - bool isWeapon = false; - float weapSpeed = 1.f; - if (mPtr.getClass().hasInventoryStore(mPtr)) - { - MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); - isWeapon = (weapon != inv.end() && weapon->getTypeName() == typeid(ESM::Weapon).name()); - if(isWeapon) - weapSpeed = weapon->get()->mBase->mData.mSpeed; - - MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); - if (mWeaponType == WeapType_Crossbow) - ammunition = (ammo != inv.end() && ammo->get()->mBase->mData.mType == ESM::Weapon::Bolt); - else if (mWeaponType == WeapType_BowAndArrow) - ammunition = (ammo != inv.end() && ammo->get()->mBase->mData.mType == ESM::Weapon::Arrow); - if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped) - { - mAnimation->disable(mCurrentWeapon); - mUpperBodyState = UpperCharState_WeapEquiped; - } - } - - float complete; - bool animPlaying; - if(mAttackingOrSpell) - { - mIdleState = CharState_None; - if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block)) - { - MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); - mAttackStrength = 0; - if(mWeaponType == WeapType_Spell) - { - // Unset casting flag, otherwise pressing the mouse button down would - // continue casting every frame if there is no animation - mAttackingOrSpell = false; - if (mPtr == getPlayer()) - { - MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false); - } - - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - - // For the player, set the spell we want to cast - // This has to be done at the start of the casting animation, - // *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation) - if (mPtr == getPlayer()) - { - std::string selectedSpell = MWBase::Environment::get().getWindowManager()->getSelectedSpell(); - stats.getSpells().setSelectedSpell(selectedSpell); - } - std::string spellid = stats.getSpells().getSelectedSpell(); - - if(!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) - { - MWMechanics::CastSpell cast(mPtr, NULL); - cast.playSpellCastingEffects(spellid); - - const ESM::Spell *spell = store.get().find(spellid); - const ESM::ENAMstruct &lastEffect = spell->mEffects.mList.back(); - const ESM::MagicEffect *effect; - - effect = store.get().find(lastEffect.mEffectID); // use last effect of list for color of VFX_Hands - - const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get().find ("VFX_Hands"); - - for (size_t iter = 0; iter < spell->mEffects.mList.size(); ++iter) // play hands vfx for each effect - { - if (mAnimation->getNode("Bip01 L Hand")) - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle); - - if (mAnimation->getNode("Bip01 R Hand")) - mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); - } - - const ESM::ENAMstruct &firstEffect = spell->mEffects.mList.at(0); // first effect used for casting animation - - switch(firstEffect.mRange) - { - case 0: mAttackType = "self"; break; - case 1: mAttackType = "touch"; break; - case 2: mAttackType = "target"; break; - } - - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - weapSpeed, mAttackType+" start", mAttackType+" stop", - 0.0f, 0); - mUpperBodyState = UpperCharState_CastingSpell; - } - if (mPtr.getClass().hasInventoryStore(mPtr)) - { - MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - if (inv.getSelectedEnchantItem() != inv.end()) - { - // Enchanted items cast immediately (no animation) - MWBase::Environment::get().getWorld()->castSpell(mPtr); - } - } - - } - else if(mWeaponType == WeapType_PickProbe) - { - MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - MWWorld::Ptr item = *weapon; - // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes. - MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getFacedObject(); - std::string resultMessage, resultSound; - - if(!target.isEmpty()) - { - if(item.getTypeName() == typeid(ESM::Lockpick).name()) - Security(mPtr).pickLock(target, item, resultMessage, resultSound); - else if(item.getTypeName() == typeid(ESM::Probe).name()) - Security(mPtr).probeTrap(target, item, resultMessage, resultSound); - } - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - 1.0f, "start", "stop", 0.0, 0); - mUpperBodyState = UpperCharState_FollowStartToFollowStop; - - if(!resultMessage.empty()) - MWBase::Environment::get().getWindowManager()->messageBox(resultMessage); - if(!resultSound.empty()) - MWBase::Environment::get().getSoundManager()->playSound3D(target, resultSound, - 1.0f, 1.0f); - } - else if (ammunition) - { - if(mWeaponType == WeapType_Crossbow || mWeaponType == WeapType_BowAndArrow || - mWeaponType == WeapType_Thrown) - mAttackType = "shoot"; - else - { - if(mPtr == getPlayer()) - { - if (isWeapon) - { - if (Settings::Manager::getBool("best attack", "Game")) - { - MWWorld::ConstContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - mAttackType = getBestAttack(weapon->get()->mBase); - } - else - setAttackTypeBasedOnMovement(); - } - else - setAttackTypeRandomly(mAttackType); - } - // else if (mPtr != getPlayer()) use mAttackType set by AiCombat - } - - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, false, - weapSpeed, mAttackType+" start", mAttackType+" min attack", - 0.0f, 0); - mUpperBodyState = UpperCharState_StartToMinAttack; - } - } - - animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); - if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown()) - mAttackStrength = complete; - } - else - { - animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); - if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown()) - { - float attackStrength = complete; - if (!mPtr.getClass().isNpc()) - { - // most creatures don't actually have an attack wind-up animation, so use a uniform random value - // (even some creatures that can use weapons don't have a wind-up animation either, e.g. Rieklings) - // Note: vanilla MW uses a random value for *all* non-player actors, but we probably don't need to go that far. - attackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability()); - } - - if(mWeaponType != WeapType_Crossbow && mWeaponType != WeapType_BowAndArrow) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - - if(isWerewolf) - { - const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - const ESM::Sound *sound = store.get().searchRandom("WolfSwing"); - if(sound) - sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); - } - else - { - playSwishSound(attackStrength); - } - } - mAttackStrength = attackStrength; - - mAnimation->disable(mCurrentWeapon); - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, false, - weapSpeed, mAttackType+" max attack", mAttackType+" min hit", - 1.0f-complete, 0); - - complete = 0.f; - mUpperBodyState = UpperCharState_MaxAttackToMinHit; - } - else if (isKnockedDown()) - { - if (mUpperBodyState > UpperCharState_WeapEquiped) - mUpperBodyState = UpperCharState_WeapEquiped; - mAnimation->disable(mCurrentWeapon); - } - } - - mAnimation->setPitchFactor(0.f); - if (mWeaponType == WeapType_BowAndArrow || mWeaponType == WeapType_Thrown) - { - switch (mUpperBodyState) - { - case UpperCharState_StartToMinAttack: - mAnimation->setPitchFactor(complete); - break; - case UpperCharState_MinAttackToMaxAttack: - case UpperCharState_MaxAttackToMinHit: - case UpperCharState_MinHitToHit: - mAnimation->setPitchFactor(1.f); - break; - case UpperCharState_FollowStartToFollowStop: - if (animPlaying) - mAnimation->setPitchFactor(1.f-complete); - break; - default: - break; - } - } - else if (mWeaponType == WeapType_Crossbow) - { - switch (mUpperBodyState) - { - case UpperCharState_EquipingWeap: - mAnimation->setPitchFactor(complete); - break; - case UpperCharState_UnEquipingWeap: - mAnimation->setPitchFactor(1.f-complete); - break; - case UpperCharState_WeapEquiped: - case UpperCharState_StartToMinAttack: - case UpperCharState_MinAttackToMaxAttack: - case UpperCharState_MaxAttackToMinHit: - case UpperCharState_MinHitToHit: - case UpperCharState_FollowStartToFollowStop: - mAnimation->setPitchFactor(1.f); - break; - default: - break; - } - } - - if(!animPlaying) - { - if(mUpperBodyState == UpperCharState_EquipingWeap || - mUpperBodyState == UpperCharState_FollowStartToFollowStop || - mUpperBodyState == UpperCharState_CastingSpell) - { - if (ammunition && mWeaponType == WeapType_Crossbow) - mAnimation->attachArrow(); - - mUpperBodyState = UpperCharState_WeapEquiped; - } - else if(mUpperBodyState == UpperCharState_UnEquipingWeap) - mUpperBodyState = UpperCharState_Nothing; - } - else if(complete >= 1.0f) - { - std::string start, stop; - switch(mUpperBodyState) - { - case UpperCharState_StartToMinAttack: - start = mAttackType+" min attack"; - stop = mAttackType+" max attack"; - mUpperBodyState = UpperCharState_MinAttackToMaxAttack; - break; - case UpperCharState_MinAttackToMaxAttack: - //hack to avoid body pos desync when jumping/sneaking in 'max attack' state - if(!mAnimation->isPlaying(mCurrentWeapon)) - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, false, - 0, mAttackType+" min attack", mAttackType+" max attack", 0.999f, 0); - break; - case UpperCharState_MaxAttackToMinHit: - if(mAttackType == "shoot") - { - start = mAttackType+" min hit"; - stop = mAttackType+" release"; - } - else - { - start = mAttackType+" min hit"; - stop = mAttackType+" hit"; - } - mUpperBodyState = UpperCharState_MinHitToHit; - break; - case UpperCharState_MinHitToHit: - if(mAttackType == "shoot") - { - start = mAttackType+" follow start"; - stop = mAttackType+" follow stop"; - } - else - { - float str = mAttackStrength; - start = mAttackType+((str < 0.5f) ? " small follow start" - : (str < 1.0f) ? " medium follow start" - : " large follow start"); - stop = mAttackType+((str < 0.5f) ? " small follow stop" - : (str < 1.0f) ? " medium follow stop" - : " large follow stop"); - } - mUpperBodyState = UpperCharState_FollowStartToFollowStop; - break; - default: - break; - } - - if(!start.empty()) - { - mAnimation->disable(mCurrentWeapon); - if (mUpperBodyState == UpperCharState_FollowStartToFollowStop) - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - weapSpeed, start, stop, 0.0f, 0); - else - mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, false, - weapSpeed, start, stop, 0.0f, 0); - } - } - - if (mPtr.getClass().hasInventoryStore(mPtr)) - { - const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); - if(torch != inv.end() && torch->getTypeName() == typeid(ESM::Light).name() - && updateCarriedLeftVisible(mWeaponType)) - - { - mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm, - false, 1.0f, "start", "stop", 0.0f, (~(size_t)0), true); - } - else if (mAnimation->isPlaying("torch")) - { - mAnimation->disable("torch"); - } - } - - mAnimation->setAccurateAiming(mUpperBodyState > UpperCharState_WeapEquiped); - - return forcestateupdate; -} - -void CharacterController::updateAnimQueue() -{ - if(mAnimQueue.size() > 1) - { - if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false) - { - mAnimation->disable(mAnimQueue.front().mGroup); - mAnimQueue.pop_front(); - - bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); - mAnimation->play(mAnimQueue.front().mGroup, Priority_Default, - MWRender::Animation::BlendMask_All, false, - 1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback); - } - } - - if(!mAnimQueue.empty()) - mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); -} - -void CharacterController::update(float duration) -{ - MWBase::World *world = MWBase::Environment::get().getWorld(); - const MWWorld::Class &cls = mPtr.getClass(); - osg::Vec3f movement(0.f, 0.f, 0.f); - float speed = 0.f; - - updateMagicEffects(); - - bool godmode = mPtr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - - if(!cls.isActor()) - updateAnimQueue(); - else if(!cls.getCreatureStats(mPtr).isDead()) - { - bool onground = world->isOnGround(mPtr); - bool inwater = world->isSwimming(mPtr); - bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak); - bool flying = world->isFlying(mPtr); - // Can't run while flying (see speed formula in Npc/Creature::getSpeed) - bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying; - CreatureStats &stats = cls.getCreatureStats(mPtr); - - //Force Jump Logic - - bool isMoving = (std::abs(cls.getMovementSettings(mPtr).mPosition[0]) > .5 || std::abs(cls.getMovementSettings(mPtr).mPosition[1]) > .5); - if(!inwater && !flying) - { - //Force Jump - if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump)) - { - if(onground) - { - cls.getMovementSettings(mPtr).mPosition[2] = 1; - } - else - cls.getMovementSettings(mPtr).mPosition[2] = 0; - } - //Force Move Jump, only jump if they're otherwise moving - if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving) - { - - if(onground) - { - cls.getMovementSettings(mPtr).mPosition[2] = 1; - } - else - cls.getMovementSettings(mPtr).mPosition[2] = 0; - } - } - - osg::Vec3f vec(cls.getMovementSettings(mPtr).asVec3()); - vec.normalize(); - - if(mHitState != CharState_None && mJumpState == JumpState_None) - vec = osg::Vec3f(0.f, 0.f, 0.f); - osg::Vec3f rot = cls.getRotationVector(mPtr); - - speed = cls.getSpeed(mPtr); - - vec.x() *= speed; - vec.y() *= speed; - - CharacterState movestate = CharState_None; - CharacterState idlestate = CharState_SpecialIdle; - JumpingState jumpstate = JumpState_None; - - bool forcestateupdate = false; - - mHasMovedInXY = std::abs(vec.x())+std::abs(vec.y()) > 0.0f; - isrunning = isrunning && mHasMovedInXY; - - // advance athletics - if(mHasMovedInXY && mPtr == getPlayer()) - { - if(inwater) - { - mSecondsOfSwimming += duration; - while(mSecondsOfSwimming > 1) - { - cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 1); - mSecondsOfSwimming -= 1; - } - } - else if(isrunning && !sneak) - { - mSecondsOfRunning += duration; - while(mSecondsOfRunning > 1) - { - cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 0); - mSecondsOfRunning -= 1; - } - } - } - - // reduce fatigue - const MWWorld::Store &gmst = world->getStore().get(); - float fatigueLoss = 0; - static const float fFatigueRunBase = gmst.find("fFatigueRunBase")->getFloat(); - static const float fFatigueRunMult = gmst.find("fFatigueRunMult")->getFloat(); - static const float fFatigueSwimWalkBase = gmst.find("fFatigueSwimWalkBase")->getFloat(); - static const float fFatigueSwimRunBase = gmst.find("fFatigueSwimRunBase")->getFloat(); - static const float fFatigueSwimWalkMult = gmst.find("fFatigueSwimWalkMult")->getFloat(); - static const float fFatigueSwimRunMult = gmst.find("fFatigueSwimRunMult")->getFloat(); - static const float fFatigueSneakBase = gmst.find("fFatigueSneakBase")->getFloat(); - static const float fFatigueSneakMult = gmst.find("fFatigueSneakMult")->getFloat(); - - if (cls.getEncumbrance(mPtr) <= cls.getCapacity(mPtr)) - { - const float encumbrance = cls.getEncumbrance(mPtr) / cls.getCapacity(mPtr); - - if (sneak) - fatigueLoss = fFatigueSneakBase + encumbrance * fFatigueSneakMult; - else - { - if (inwater) - { - if (!isrunning) - fatigueLoss = fFatigueSwimWalkBase + encumbrance * fFatigueSwimWalkMult; - else - fatigueLoss = fFatigueSwimRunBase + encumbrance * fFatigueSwimRunMult; - } - else if (isrunning) - fatigueLoss = fFatigueRunBase + encumbrance * fFatigueRunMult; - } - } - fatigueLoss *= duration; - DynamicStat fatigue = cls.getCreatureStats(mPtr).getFatigue(); - - if (!godmode) - { - fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss, fatigue.getCurrent() < 0); - cls.getCreatureStats(mPtr).setFatigue(fatigue); - } - - if(sneak || inwater || flying) - vec.z() = 0.0f; - - bool inJump = true; - if(!onground && !flying && !inwater) - { - // In the air (either getting up —ascending part of jump— or falling). - - forcestateupdate = (mJumpState != JumpState_InAir); - jumpstate = JumpState_InAir; - - static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->getFloat(); - static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->getFloat(); - float factor = fJumpMoveBase + fJumpMoveMult * mPtr.getClass().getSkill(mPtr, ESM::Skill::Acrobatics)/100.f; - factor = std::min(1.f, factor); - vec.x() *= factor; - vec.y() *= factor; - vec.z() = 0.0f; - } - else if(vec.z() > 0.0f && mJumpState == JumpState_None) - { - // Started a jump. - float z = cls.getJump(mPtr); - if (z > 0) - { - if(vec.x() == 0 && vec.y() == 0) - vec = osg::Vec3f(0.0f, 0.0f, z); - else - { - osg::Vec3f lat (vec.x(), vec.y(), 0.0f); - lat.normalize(); - vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * z * 0.707f; - } - - // advance acrobatics - if (mPtr == getPlayer()) - cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 0); - - // decrease fatigue - const float fatigueJumpBase = gmst.find("fFatigueJumpBase")->getFloat(); - const float fatigueJumpMult = gmst.find("fFatigueJumpMult")->getFloat(); - float normalizedEncumbrance = mPtr.getClass().getNormalizedEncumbrance(mPtr); - if (normalizedEncumbrance > 1) - normalizedEncumbrance = 1; - const float fatigueDecrease = fatigueJumpBase + normalizedEncumbrance * fatigueJumpMult; - - if (!godmode) - { - fatigue.setCurrent(fatigue.getCurrent() - fatigueDecrease); - cls.getCreatureStats(mPtr).setFatigue(fatigue); - } - } - } - else if(mJumpState == JumpState_InAir && !inwater && !flying) - { - forcestateupdate = true; - jumpstate = JumpState_Landing; - vec.z() = 0.0f; - - float height = cls.getCreatureStats(mPtr).land(); - float healthLost = getFallDamage(mPtr, height); - - if (healthLost > 0.0f) - { - const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm(); - - // inflict fall damages - if (!godmode) - { - DynamicStat health = cls.getCreatureStats(mPtr).getHealth(); - float realHealthLost = static_cast(healthLost * (1.0f - 0.25f * fatigueTerm)); - health.setCurrent(health.getCurrent() - realHealthLost); - cls.getCreatureStats(mPtr).setHealth(health); - cls.onHit(mPtr, realHealthLost, true, MWWorld::Ptr(), MWWorld::Ptr(), osg::Vec3f(), true); - } - - const int acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics); - if (healthLost > (acrobaticsSkill * fatigueTerm)) - { - cls.getCreatureStats(mPtr).setKnockedDown(true); - } - else - { - // report acrobatics progression - if (mPtr == getPlayer()) - cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 1); - } - } - } - else - { - jumpstate = mAnimation->isPlaying(mCurrentJump) ? JumpState_Landing : JumpState_None; - - vec.z() = 0.0f; - - inJump = false; - - if(std::abs(vec.x()/2.0f) > std::abs(vec.y())) - { - if(vec.x() > 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight) - : (sneak ? CharState_SneakRight - : (isrunning ? CharState_RunRight : CharState_WalkRight))); - else if(vec.x() < 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft) - : (sneak ? CharState_SneakLeft - : (isrunning ? CharState_RunLeft : CharState_WalkLeft))); - } - else if(vec.y() != 0.0f) - { - if(vec.y() > 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward) - : (sneak ? CharState_SneakForward - : (isrunning ? CharState_RunForward : CharState_WalkForward))); - else if(vec.y() < 0.0f) - movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack) - : (sneak ? CharState_SneakBack - : (isrunning ? CharState_RunBack : CharState_WalkBack))); - } - else if(rot.z() != 0.0f && !sneak && !(mPtr == getPlayer() && MWBase::Environment::get().getWorld()->isFirstPerson())) - { - if(rot.z() > 0.0f) - { - movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight; - mAnimation->disable(mCurrentJump); - } - else if(rot.z() < 0.0f) - { - movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft; - mAnimation->disable(mCurrentJump); - } - } - } - - mTurnAnimationThreshold -= duration; - if (isTurning()) - mTurnAnimationThreshold = 0.05f; - else if (movestate == CharState_None && isTurning() - && mTurnAnimationThreshold > 0) - { - movestate = mMovementState; - } - - if(movestate != CharState_None && !isTurning()) - clearAnimQueue(); - - if(mAnimQueue.empty() || inwater || sneak) - { - idlestate = (inwater ? CharState_IdleSwim : (sneak && !inJump ? CharState_IdleSneak : CharState_Idle)); - } - else - updateAnimQueue(); - - if (!mSkipAnim) - { - // bipedal means hand-to-hand could be used (which is handled in updateWeaponState). an existing InventoryStore means an actual weapon could be used. - if(cls.isBipedal(mPtr) || cls.hasInventoryStore(mPtr)) - forcestateupdate = updateWeaponState() || forcestateupdate; - else - forcestateupdate = updateCreatureState() || forcestateupdate; - - refreshCurrentAnims(idlestate, movestate, jumpstate, forcestateupdate); - updateIdleStormState(inwater); - } - - if (inJump) - mMovementAnimationControlled = false; - - if (isTurning()) - { - if (duration > 0) - mAnimation->adjustSpeedMult(mCurrentMovement, std::min(1.5f, std::abs(rot.z()) / duration / static_cast(osg::PI))); - } - else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed) - { - float speedmult = speed / mMovementAnimSpeed; - mAnimation->adjustSpeedMult(mCurrentMovement, speedmult); - } - - if (!mSkipAnim) - { - if(!isKnockedDown() && !isKnockedOut()) - { - if (rot != osg::Vec3f()) - world->rotateObject(mPtr, rot.x(), rot.y(), rot.z(), true); - } - else //avoid z-rotating for knockdown - { - if (rot.x() != 0 && rot.y() != 0) - world->rotateObject(mPtr, rot.x(), rot.y(), 0.0f, true); - } - - if (!mMovementAnimationControlled) - world->queueMovement(mPtr, vec); - } - else - // We must always queue movement, even if there is none, to apply gravity. - world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); - - movement = vec; - cls.getMovementSettings(mPtr).mPosition[0] = cls.getMovementSettings(mPtr).mPosition[1] = 0; - // Can't reset jump state (mPosition[2]) here; we don't know for sure whether the PhysicSystem will actually handle it in this frame - // due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled. - - if (!mSkipAnim) - updateHeadTracking(duration); - } - else if(cls.getCreatureStats(mPtr).isDead()) - { - // initial start of death animation for actors that started the game as dead - // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag - if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) - { - playDeath(1.f, mDeathState); - } - // We must always queue movement, even if there is none, to apply gravity. - world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); - } - - osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim ? 0.f : duration); - if(duration > 0.0f) - moved /= duration; - else - moved = osg::Vec3f(0.f, 0.f, 0.f); - - // Ensure we're moving in generally the right direction... - if(speed > 0.f) - { - float l = moved.length(); - - if((movement.x() < 0.0f && movement.x() < moved.x()*2.0f) || - (movement.x() > 0.0f && movement.x() > moved.x()*2.0f)) - moved.x() = movement.x(); - if((movement.y() < 0.0f && movement.y() < moved.y()*2.0f) || - (movement.y() > 0.0f && movement.y() > moved.y()*2.0f)) - moved.y() = movement.y(); - if((movement.z() < 0.0f && movement.z() < moved.z()*2.0f) || - (movement.z() > 0.0f && movement.z() > moved.z()*2.0f)) - moved.z() = movement.z(); - // but keep the original speed - float newLength = moved.length(); - if (newLength > 0) - moved *= (l / newLength); - } - - if (mSkipAnim) - mAnimation->updateEffects(duration); - - if (mFloatToSurface && cls.isActor() && cls.getCreatureStats(mPtr).isDead() && cls.canSwim(mPtr)) - moved.z() = 1.0; - - // Update movement - if(mMovementAnimationControlled && mPtr.getClass().isActor()) - world->queueMovement(mPtr, moved); - - mSkipAnim = false; - - mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); -} - -void CharacterController::persistAnimationState() -{ - ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); - - state.mScriptedAnims.clear(); - for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) - { - if (!iter->mPersist) - continue; - - ESM::AnimationState::ScriptedAnimation anim; - anim.mGroup = iter->mGroup; - - if (iter == mAnimQueue.begin()) - { - anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup); - float complete; - mAnimation->getInfo(anim.mGroup, &complete, NULL); - anim.mTime = complete; - } - else - { - anim.mLoopCount = iter->mLoopCount; - anim.mTime = 0.f; - } - - state.mScriptedAnims.push_back(anim); - } -} - -void CharacterController::unpersistAnimationState() -{ - const ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); - - if (!state.mScriptedAnims.empty()) - { - clearAnimQueue(); - for (ESM::AnimationState::ScriptedAnimations::const_iterator iter = state.mScriptedAnims.begin(); iter != state.mScriptedAnims.end(); ++iter) - { - AnimationQueueEntry entry; - entry.mGroup = iter->mGroup; - entry.mLoopCount = iter->mLoopCount; - entry.mPersist = true; - - mAnimQueue.push_back(entry); - } - - const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front(); - float complete = anim.mTime; - if (anim.mAbsolute) - { - float start = mAnimation->getTextKeyTime(anim.mGroup+": start"); - float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop"); - float time = std::max(start, std::min(stop, anim.mTime)); - complete = (time - start) / (stop - start); - } - - mAnimation->disable(mCurrentIdle); - mCurrentIdle.clear(); - mIdleState = CharState_SpecialIdle; - - bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); - mAnimation->play(anim.mGroup, - Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, - "start", "stop", complete, anim.mLoopCount, loopfallback); - } -} - -bool CharacterController::playGroup(const std::string &groupname, int mode, int count, bool persist) -{ - if(!mAnimation || !mAnimation->hasAnimation(groupname)) - return false; - - // If this animation is a looped animation (has a "loop start" key) that is already playing - // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count - // and remove any other animations that were queued. - // This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners correctly. - if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && - mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop start") >= 0 && - mAnimation->isPlaying(groupname)) - { - float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": loop stop"); - - if (endOfLoop < 0) // if no Loop Stop key was found, use the Stop key - endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": stop"); - - if (endOfLoop > 0 && (mAnimation->getCurrentTime(mAnimQueue.front().mGroup) < endOfLoop)) - { - mAnimQueue.resize(1); - return true; - } - } - - count = std::max(count, 1); - - AnimationQueueEntry entry; - entry.mGroup = groupname; - entry.mLoopCount = count-1; - entry.mPersist = persist; - - if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) - { - clearAnimQueue(); - mAnimQueue.push_back(entry); - - mAnimation->disable(mCurrentIdle); - mCurrentIdle.clear(); - - mIdleState = CharState_SpecialIdle; - bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0); - mAnimation->play(groupname, Priority_Default, - MWRender::Animation::BlendMask_All, false, 1.0f, - ((mode==2) ? "loop start" : "start"), "stop", 0.0f, count-1, loopfallback); - } - else if(mode == 0) - { - mAnimQueue.resize(1); - mAnimQueue.push_back(entry); - } - return true; -} - -void CharacterController::skipAnim() -{ - mSkipAnim = true; -} - -bool CharacterController::isAnimPlaying(const std::string &groupName) -{ - if(mAnimation == NULL) - return false; - return mAnimation->isPlaying(groupName); -} - - -void CharacterController::clearAnimQueue() -{ - if(!mAnimQueue.empty()) - mAnimation->disable(mAnimQueue.front().mGroup); - mAnimQueue.clear(); -} - -void CharacterController::forceStateUpdate() -{ - if(!mAnimation) - return; - clearAnimQueue(); - - refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); - if(mDeathState != CharState_None) - { - playRandomDeath(); - } - - mAnimation->runAnimation(0.f); -} - -CharacterController::KillResult CharacterController::kill() -{ - if (mDeathState == CharState_None) - { - playRandomDeath(); - - mAnimation->disable(mCurrentIdle); - - mIdleState = CharState_None; - mCurrentIdle.clear(); - return Result_DeathAnimStarted; - } - - MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr); - if (isAnimPlaying(mCurrentDeath)) - return Result_DeathAnimPlaying; - if (!cStats.isDeathAnimationFinished()) - { - cStats.setDeathAnimationFinished(true); - return Result_DeathAnimJustFinished; - } - return Result_DeathAnimFinished; -} - -void CharacterController::resurrect() -{ - if(mDeathState == CharState_None) - return; - - if(mAnimation) - mAnimation->disable(mCurrentDeath); - mCurrentDeath.clear(); - mDeathState = CharState_None; -} - -void CharacterController::updateContinuousVfx() -{ - // Keeping track of when to stop a continuous VFX seems to be very difficult to do inside the spells code, - // as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here. - - // Stop any effects that are no longer active - std::vector effects; - mAnimation->getLoopingEffects(effects); - - for (std::vector::iterator it = effects.begin(); it != effects.end(); ++it) - { - if (mPtr.getClass().getCreatureStats(mPtr).isDead() - || mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(MWMechanics::EffectKey(*it)).getMagnitude() <= 0) - mAnimation->removeEffect(*it); - } -} - -void CharacterController::updateMagicEffects() -{ - if (!mPtr.getClass().isActor()) - return; - float alpha = 1.f; - if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).getModifier()) // Ignore base magnitude (see bug #3555). - { - if (mPtr == getPlayer()) - alpha = 0.4f; - else - alpha = 0.f; - } - float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude(); - if (chameleon) - { - alpha *= std::max(0.2f, (100.f - chameleon)/100.f); - } - mAnimation->setAlpha(alpha); - - bool vampire = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0.0f; - mAnimation->setVampire(vampire); - - float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude(); - mAnimation->setLightEffect(light); -} - -void CharacterController::setAttackTypeBasedOnMovement() -{ - float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition; - - if (move[1] && !move[0]) // forward-backward - mAttackType = "thrust"; - else if (move[0] && !move[1]) //sideway - mAttackType = "slash"; - else - mAttackType = "chop"; -} - -bool CharacterController::isAttackPrepairing() const -{ - return mUpperBodyState == UpperCharState_StartToMinAttack || - mUpperBodyState == UpperCharState_MinAttackToMaxAttack; -} - -bool CharacterController::isReadyToBlock() const -{ - return updateCarriedLeftVisible(mWeaponType); -} - -bool CharacterController::isKnockedDown() const -{ - return mHitState == CharState_KnockDown || - mHitState == CharState_SwimKnockDown; -} - -bool CharacterController::isKnockedOut() const -{ - return mHitState == CharState_KnockOut || - mHitState == CharState_SwimKnockOut; -} - -bool CharacterController::isTurning() const -{ - return mMovementState == CharState_TurnLeft || - mMovementState == CharState_TurnRight || - mMovementState == CharState_SwimTurnLeft || - mMovementState == CharState_SwimTurnRight; -} - -bool CharacterController::isRecovery() const -{ - return mHitState == CharState_Hit || - mHitState == CharState_SwimHit; -} - -bool CharacterController::isAttackingOrSpell() const -{ - return mUpperBodyState != UpperCharState_Nothing && - mUpperBodyState != UpperCharState_WeapEquiped; -} - -bool CharacterController::isSneaking() const -{ - return mIdleState == CharState_IdleSneak || - mMovementState == CharState_SneakForward || - mMovementState == CharState_SneakBack || - mMovementState == CharState_SneakLeft || - mMovementState == CharState_SneakRight; -} - -bool CharacterController::isRunning() const -{ - return mMovementState == CharState_RunForward || - mMovementState == CharState_RunBack || - mMovementState == CharState_RunLeft || - mMovementState == CharState_RunRight || - mMovementState == CharState_SwimRunForward || - mMovementState == CharState_SwimRunBack || - mMovementState == CharState_SwimRunLeft || - mMovementState == CharState_SwimRunRight; -} - -void CharacterController::setAttackingOrSpell(bool attackingOrSpell) -{ - mAttackingOrSpell = attackingOrSpell; -} - -void CharacterController::setAIAttackType(const std::string& attackType) -{ - mAttackType = attackType; -} - -void CharacterController::setAttackTypeRandomly(std::string& attackType) -{ - float random = Misc::Rng::rollProbability(); - if (random >= 2/3.f) - attackType = "thrust"; - else if (random >= 1/3.f) - attackType = "slash"; - else - attackType = "chop"; -} - -bool CharacterController::readyToPrepareAttack() const -{ - return (mHitState == CharState_None || mHitState == CharState_Block) - && mUpperBodyState <= UpperCharState_WeapEquiped; -} - -bool CharacterController::readyToStartAttack() const -{ - if (mHitState != CharState_None && mHitState != CharState_Block) - return false; - - if (mPtr.getClass().hasInventoryStore(mPtr) || mPtr.getClass().isBipedal(mPtr)) - return mUpperBodyState == UpperCharState_WeapEquiped; - else - return mUpperBodyState == UpperCharState_Nothing; -} - -float CharacterController::getAttackStrength() const -{ - return mAttackStrength; -} - -void CharacterController::setActive(bool active) -{ - mAnimation->setActive(active); -} - -void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr &target) -{ - mHeadTrackTarget = target; -} - -void CharacterController::playSwishSound(float attackStrength) -{ - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - - std::string sound = "Weapon Swish"; - if(attackStrength < 0.5f) - sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack - else if(attackStrength < 1.0f) - sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack - else - sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack -} - -void CharacterController::updateHeadTracking(float duration) -{ - const osg::Node* head = mAnimation->getNode("Bip01 Head"); - if (!head) - return; - - float zAngleRadians = 0.f; - float xAngleRadians = 0.f; - - if (!mHeadTrackTarget.isEmpty()) - { - osg::NodePathList nodepaths = head->getParentalNodePaths(); - if (nodepaths.empty()) - return; - osg::Matrixf mat = osg::computeLocalToWorld(nodepaths[0]); - osg::Vec3f headPos = mat.getTrans(); - - osg::Vec3f direction; - if (const MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget)) - { - const osg::Node* node = anim->getNode("Head"); - if (node == NULL) - node = anim->getNode("Bip01 Head"); - if (node != NULL) - { - nodepaths = node->getParentalNodePaths(); - if (!nodepaths.empty()) - direction = osg::computeLocalToWorld(nodepaths[0]).getTrans() - headPos; - } - else - // no head node to look at, fall back to look at center of collision box - direction = MWBase::Environment::get().getWorld()->aimToTarget(mPtr, mHeadTrackTarget); - } - direction.normalize(); - - if (!mPtr.getRefData().getBaseNode()) - return; - const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); - - zAngleRadians = std::atan2(direction.x(), direction.y()) - std::atan2(actorDirection.x(), actorDirection.y()); - xAngleRadians = -std::asin(direction.z()); - - wrap(zAngleRadians); - wrap(xAngleRadians); - - xAngleRadians = std::min(xAngleRadians, osg::DegreesToRadians(40.f)); - xAngleRadians = std::max(xAngleRadians, osg::DegreesToRadians(-40.f)); - zAngleRadians = std::min(zAngleRadians, osg::DegreesToRadians(30.f)); - zAngleRadians = std::max(zAngleRadians, osg::DegreesToRadians(-30.f)); - } - - float factor = duration*5; - factor = std::min(factor, 1.f); - xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * (-xAngleRadians); - zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * (-zAngleRadians); - - mAnimation->setHeadPitch(xAngleRadians); - mAnimation->setHeadYaw(zAngleRadians); -} - -} diff --git a/apps/openmw/mwmechanics/character.cpp.rej b/apps/openmw/mwmechanics/character.cpp.rej deleted file mode 100644 index f04f72a90..000000000 --- a/apps/openmw/mwmechanics/character.cpp.rej +++ /dev/null @@ -1,46 +0,0 @@ ---- apps/openmw/mwmechanics/character.cpp -+++ apps/openmw/mwmechanics/character.cpp -@@ -372,29 +372,28 @@ void CharacterController::refreshJumpAnims(const WeaponInfo* weap, JumpingState - } - } - -- if(mJumpState == JumpState_InAir) -+ if (!mCurrentJump.empty()) - { - mAnimation->disable(mCurrentJump); -- mCurrentJump = jumpAnimName; -- if (mAnimation->hasAnimation("jump")) -- mAnimation->play(mCurrentJump, Priority_Jump, jumpmask, false, -+ mCurrentJump.clear(); -+ } -+ -+ if(mJumpState == JumpState_InAir) -+ { -+ if (mAnimation->hasAnimation(jumpAnimName)) -+ { -+ mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, - 1.0f, (startAtLoop?"loop start":"start"), "stop", 0.0f, ~0ul); -+ mCurrentJump = jumpAnimName; -+ } - } - else if (mJumpState == JumpState_Landing) - { -- if (startAtLoop) -- mAnimation->disable(mCurrentJump); -- -- if (mAnimation->hasAnimation("jump")) -+ if (mAnimation->hasAnimation(jumpAnimName)) -+ { - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, - 1.0f, "loop stop", "stop", 0.0f, 0); -- } -- else // JumpState_None -- { -- if (mCurrentJump.length() > 0) -- { -- mAnimation->disable(mCurrentJump); -- mCurrentJump.clear(); -+ mCurrentJump = jumpAnimName; - } - } - } From 5a07d135ae356837e64b050550ade27990bd9d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Fri, 10 Nov 2017 10:34:46 +0100 Subject: [PATCH 016/282] add settings for cubemap size --- apps/openmw/mwrender/renderingmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 9c6eac3c0..3416d1333 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -766,8 +766,8 @@ namespace MWRender int screenshotHeight = Settings::Manager::tryGetInt("s360 height","Video",mViewer->getCamera()->getViewport()->height()); SphericalScreenshot::SphericalScreenshotMapping mapping = static_cast( Settings::Manager::tryGetInt("s360 mapping","Video",SphericalScreenshot::MAPPING_SPHERICAL)); + int cubeWidth = Settings::Manager::tryGetInt("s360 cubemap size","Video",screenshotWidth / 2); - int cubeWidth = screenshotWidth / 2; SphericalScreenshot s(cubeWidth); osg::Vec3 directions[6] = { From 5baff05bace7598a653b18eb3b8bf2489c35761f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Fri, 10 Nov 2017 10:50:28 +0100 Subject: [PATCH 017/282] add cubemap mapping --- apps/openmw/mwrender/renderingmanager.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 3416d1333..a19af4822 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -629,7 +629,8 @@ namespace MWRender { MAPPING_SPHERICAL = 0, MAPPING_CYLINDRICAL, - MAPPING_SMALL_PLANET + MAPPING_SMALL_PLANET, + MAPPING_CUBEMAP } SphericalScreenshotMapping; SphericalScreenshot(int size) @@ -647,6 +648,16 @@ namespace MWRender void create(osg::Image *dest, int w, int h, SphericalScreenshotMapping mapping) { + if (mapping == MAPPING_CUBEMAP) + { + dest->allocateImage(mSize * 6,mSize,mImages[0]->r(),mImages[0]->getPixelFormat(),mImages[0]->getDataType()); + + for (int i = 0; i < 6; i++) + osg::copyImage(mImages[i].get(),0,0,0,mImages[i]->s(),mImages[i]->t(),mImages[i]->r(),dest,i * mSize,0,0); + + return; + } + dest->allocateImage(w,h,mImages[0]->r(),mImages[0]->getPixelFormat(),mImages[0]->getDataType()); for (int j = 0; j < h; ++j) @@ -768,6 +779,9 @@ namespace MWRender Settings::Manager::tryGetInt("s360 mapping","Video",SphericalScreenshot::MAPPING_SPHERICAL)); int cubeWidth = Settings::Manager::tryGetInt("s360 cubemap size","Video",screenshotWidth / 2); + if (mapping == SphericalScreenshot::MAPPING_CUBEMAP) + cubeWidth = screenshotWidth / 6; // the image will consist of 6 cube sides in a row + SphericalScreenshot s(cubeWidth); osg::Vec3 directions[6] = { From 9ab3a0c44b9068e477db9cb4fdfabcf1323b4b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Fri, 10 Nov 2017 11:16:25 +0100 Subject: [PATCH 018/282] set cubemap width differently --- apps/openmw/mwrender/renderingmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index a19af4822..dbf62a784 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -780,7 +780,7 @@ namespace MWRender int cubeWidth = Settings::Manager::tryGetInt("s360 cubemap size","Video",screenshotWidth / 2); if (mapping == SphericalScreenshot::MAPPING_CUBEMAP) - cubeWidth = screenshotWidth / 6; // the image will consist of 6 cube sides in a row + screenshotWidth = cubeWidth * 6; // the image will consist of 6 cube sides in a row SphericalScreenshot s(cubeWidth); From 497b33e4037ad9b296fc6bb757b2d9655735b9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Fri, 10 Nov 2017 14:28:09 +0100 Subject: [PATCH 019/282] small corrections --- apps/openmw/mwrender/renderingmanager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index dbf62a784..6b94c72f8 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -713,16 +713,16 @@ namespace MWRender osg::Vec4 getColorByDirection(osg::Vec3d d) { - // for details see OpenGL 4.4 specification page 241 + // for details see OpenGL 4.4 specification page 225 double x, y; double ma; int side; double ax, ay, az; - ax = d.x() > 0 ? d.x() : -d.x(); // abs behaves weirdly for some reason - ay = d.y() > 0 ? d.y() : -d.y(); - az = d.z() > 0 ? d.z() : -d.z(); + ax = fabs(d.x()); + ay = fabs(d.y()); + az = fabs(d.z()); if (ax > ay) if (ax > az) From 319ed2f9b86372fa604535cde1f1117e0fdd6793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Fri, 10 Nov 2017 15:23:44 +0100 Subject: [PATCH 020/282] disable 360 screenshots in vanity/preview mode --- apps/openmw/mwbase/world.hpp | 2 +- apps/openmw/mwinput/inputmanagerimp.cpp | 7 +++---- apps/openmw/mwrender/renderingmanager.cpp | 7 ++++++- apps/openmw/mwrender/renderingmanager.hpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 4 ++-- apps/openmw/mwworld/worldimp.hpp | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 6d39229f0..f91d1bc2a 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -450,7 +450,7 @@ namespace MWBase /// \todo this does not belong here virtual void screenshot (osg::Image* image, int w, int h) = 0; - virtual void screenshot360 (osg::Image* image) = 0; + virtual bool screenshot360 (osg::Image* image) = 0; /// Find default position inside exterior cell specified by name /// \return false if exterior with given name not exists, true otherwise diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 9ea1f8e16..bbd6feb66 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1028,10 +1028,9 @@ namespace MWInput void InputManager::screenshot360() { osg::ref_ptr screenshot (new osg::Image); - MWBase::Environment::get().getWorld()->screenshot360(screenshot.get()); - - // calling mScreenCaptureHandler->getCaptureOperation() here caused segfault for some reason - (*mScreenCaptureOperation) (*(screenshot.get()),0); + if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get())) + (*mScreenCaptureOperation) (*(screenshot.get()),0); + // calling mScreenCaptureHandler->getCaptureOperation() here caused segfault for some reason } void InputManager::toggleInventory() diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 6b94c72f8..aefc55c39 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -771,8 +771,11 @@ namespace MWRender int mSize; }; - void RenderingManager::screenshot360(osg::Image* image) + bool RenderingManager::screenshot360(osg::Image* image) { + if (mCamera->isVanityOrPreviewModeEnabled()) + return false; + int screenshotWidth = Settings::Manager::tryGetInt("s360 width","Video",mViewer->getCamera()->getViewport()->width()); int screenshotHeight = Settings::Manager::tryGetInt("s360 height","Video",mViewer->getCamera()->getViewport()->height()); SphericalScreenshot::SphericalScreenshotMapping mapping = static_cast( @@ -811,6 +814,8 @@ namespace MWRender mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); mFieldOfView = fovBackup; + + return true; } void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Vec3 direction, bool disableWaterEffects) diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index cb0723397..a4cb9bf8f 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -126,7 +126,7 @@ namespace MWRender /// Take a screenshot of w*h onto the given image, not including the GUI. void screenshot(osg::Image* image, int w, int h, osg::Vec3 direction=osg::Vec3(0,0,-1), bool disableWaterEffects=false); - void screenshot360(osg::Image* image); + bool screenshot360(osg::Image* image); struct RayResult { diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index f51f9af3d..0d0079677 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2281,9 +2281,9 @@ namespace MWWorld mRendering->screenshot(image, w, h); } - void World::screenshot360 (osg::Image* image) + bool World::screenshot360(osg::Image* image) { - mRendering->screenshot360(image); + return mRendering->screenshot360(image); } void World::activateDoor(const MWWorld::Ptr& door) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index a68982412..3a54d28ae 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -560,7 +560,7 @@ namespace MWWorld /// \todo this does not belong here void screenshot (osg::Image* image, int w, int h) override; - void screenshot360 (osg::Image* image) override; + bool screenshot360 (osg::Image* image) override; /// Find center of exterior cell above land surface /// \return false if exterior with given name not exists, true otherwise From 1f49612ca38aa8e7bc546d84e664644f5009645a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Fri, 10 Nov 2017 19:18:16 +0100 Subject: [PATCH 021/282] enable water effects for 360 screenshots --- apps/openmw/mwrender/renderingmanager.cpp | 14 ++++++-------- apps/openmw/mwrender/renderingmanager.hpp | 2 +- apps/openmw/mwrender/water.cpp | 10 ++++++++++ apps/openmw/mwrender/water.hpp | 4 ++++ 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index aefc55c39..ff54ab1dc 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -807,7 +807,7 @@ namespace MWRender for (int i = 0; i < 6; i++) // for each cube side { osg::Image *sideImage = s.getImage(i); - screenshot(sideImage,cubeWidth,cubeWidth,directions[i],true); + screenshot(sideImage,cubeWidth,cubeWidth,directions[i]); } s.create(image,screenshotWidth,mapping != SphericalScreenshot::MAPPING_SMALL_PLANET ? screenshotHeight : screenshotWidth,mapping); @@ -818,7 +818,7 @@ namespace MWRender return true; } - void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Vec3 direction, bool disableWaterEffects) + void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Vec3 direction) { osg::ref_ptr rttCamera (new osg::Camera); rttCamera->setNodeMask(Mask_RenderToTexture); @@ -844,6 +844,10 @@ namespace MWRender rttCamera->setUpdateCallback(new NoTraverseCallback); rttCamera->addChild(mSceneRoot); + + rttCamera->addChild(mWater->getReflectionCamera()); + rttCamera->addChild(mWater->getRefractionCamera()); + rttCamera->setCullMask(mViewer->getCamera()->getCullMask() & (~Mask_GUI)); mRootNode->addChild(rttCamera); @@ -856,16 +860,10 @@ namespace MWRender // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() - if (disableWaterEffects) - mWater->setEffectsEnabled(false); - mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); - if (disableWaterEffects) - mWater->setEffectsEnabled(true); - callback->waitTillDone(); // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index a4cb9bf8f..9e0394593 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -125,7 +125,7 @@ namespace MWRender void setWaterHeight(float level); /// Take a screenshot of w*h onto the given image, not including the GUI. - void screenshot(osg::Image* image, int w, int h, osg::Vec3 direction=osg::Vec3(0,0,-1), bool disableWaterEffects=false); + void screenshot(osg::Image* image, int w, int h, osg::Vec3 direction=osg::Vec3(0,0,-1)); bool screenshot360(osg::Image* image); struct RayResult diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 70e7b36ad..8b047eb84 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -476,6 +476,16 @@ void Water::updateWaterMaterial() updateVisible(); } +osg::Camera *Water::getReflectionCamera() +{ + return mReflection; +} + +osg::Camera *Water::getRefractionCamera() +{ + return mRefraction; +} + void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) { osg::ref_ptr stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water); diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index ed6e40f1a..6985ddca6 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -113,6 +114,9 @@ namespace MWRender void update(float dt); + osg::Camera *getReflectionCamera(); + osg::Camera *getRefractionCamera(); + void processChangedSettings(const Settings::CategorySettingVector& settings); osg::Uniform *getRainIntensityUniform(); From 525f8b4d8e4b0544ff4202519723fab65ad25565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Sat, 11 Nov 2017 13:51:42 +0100 Subject: [PATCH 022/282] get rid of special key for 360 screenshot --- apps/openmw/mwinput/inputmanagerimp.cpp | 43 ++++++++------ apps/openmw/mwinput/inputmanagerimp.hpp | 1 - apps/openmw/mwrender/renderingmanager.cpp | 71 ++++++++++++++++++----- 3 files changed, 82 insertions(+), 33 deletions(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index bbd6feb66..10288bbc0 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -249,9 +249,6 @@ namespace MWInput case A_Screenshot: screenshot(); break; - case A_Screenshot360: - screenshot360(); - break; case A_Inventory: toggleInventory (); break; @@ -1020,17 +1017,31 @@ namespace MWInput void InputManager::screenshot() { - mScreenCaptureHandler->setFramesToCapture(1); - mScreenCaptureHandler->captureNextFrame(*mViewer); - MWBase::Environment::get().getWindowManager()->messageBox("Screenshot saved"); - } + bool regularScreenshot = true; - void InputManager::screenshot360() - { - osg::ref_ptr screenshot (new osg::Image); - if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get())) - (*mScreenCaptureOperation) (*(screenshot.get()),0); - // calling mScreenCaptureHandler->getCaptureOperation() here caused segfault for some reason + try + { + // FIXME: the same string "screenshot type" is queried here AND in renderingmanager.cpp + std::string s = Settings::Manager::getString("screenshot type","Video"); + regularScreenshot = s.size() == 0; + } + catch (std::runtime_error) + { + } + + if (regularScreenshot) + { + mScreenCaptureHandler->setFramesToCapture(1); + mScreenCaptureHandler->captureNextFrame(*mViewer); + MWBase::Environment::get().getWindowManager()->messageBox("Screenshot saved"); + } + else + { + osg::ref_ptr screenshot (new osg::Image); + + if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get())) + (*mScreenCaptureOperation) (*(screenshot.get()),0); + } } void InputManager::toggleInventory() @@ -1216,7 +1227,6 @@ namespace MWInput defaultKeyBindings[A_QuickKey9] = SDL_SCANCODE_9; defaultKeyBindings[A_QuickKey10] = SDL_SCANCODE_0; defaultKeyBindings[A_Screenshot] = SDL_SCANCODE_F12; - defaultKeyBindings[A_Screenshot360] = SDL_SCANCODE_F8; defaultKeyBindings[A_ToggleHUD] = SDL_SCANCODE_F11; defaultKeyBindings[A_ToggleDebug] = SDL_SCANCODE_F10; defaultKeyBindings[A_AlwaysRun] = SDL_SCANCODE_CAPSLOCK; @@ -1352,9 +1362,6 @@ namespace MWInput if (action == A_Screenshot) return "Screenshot"; - if (action == A_Screenshot360) - return "Screenshot 360"; - descriptions[A_Use] = "sUse"; descriptions[A_Activate] = "sActivate"; descriptions[A_MoveBackward] = "sBack"; @@ -1516,7 +1523,6 @@ namespace MWInput ret.push_back(A_QuickSave); ret.push_back(A_QuickLoad); ret.push_back(A_Screenshot); - ret.push_back(A_Screenshot360); ret.push_back(A_QuickKeysMenu); ret.push_back(A_QuickKey1); ret.push_back(A_QuickKey2); @@ -1548,7 +1554,6 @@ namespace MWInput ret.push_back(A_QuickSave); ret.push_back(A_QuickLoad); ret.push_back(A_Screenshot); - ret.push_back(A_Screenshot360); ret.push_back(A_QuickKeysMenu); ret.push_back(A_QuickKey1); ret.push_back(A_QuickKey2); diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index 5d3e88eab..82b62e79b 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -229,7 +229,6 @@ namespace MWInput void toggleInventory(); void toggleConsole(); void screenshot(); - void screenshot360(); void toggleJournal(); void activate(); void toggleWalking(); diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index ff54ab1dc..682f35cc6 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -41,6 +41,8 @@ #include #include +#include + #include "../mwworld/cellstore.hpp" #include "sky.hpp" @@ -773,19 +775,62 @@ namespace MWRender bool RenderingManager::screenshot360(osg::Image* image) { - if (mCamera->isVanityOrPreviewModeEnabled()) + int screenshotW = mViewer->getCamera()->getViewport()->width(); + int screenshotH = mViewer->getCamera()->getViewport()->height(); + SphericalScreenshot::SphericalScreenshotMapping screenshotMapping = SphericalScreenshot::MAPPING_SPHERICAL; + int cubeSize = screenshotW / 2; + + try + { + std::string settingStr = Settings::Manager::getString("screenshot type","Video"); + std::vector settingArgs; + boost::algorithm::split(settingArgs,settingStr,boost::is_any_of(" ")); + + if (settingArgs.size() > 0) + { + std::string typeStrings[4] = {"spherical","cylindrical","planet","cubemap"}; + bool found = false; + + for (int i = 0; i < 4; ++i) + if (settingArgs[0].compare(typeStrings[i]) == 0) + { + screenshotMapping = (SphericalScreenshot::SphericalScreenshotMapping) i; + found = true; + break; + } + + if (!found) + { + std::cerr << "Wrong screenshot type: " << settingArgs[0] << "." << std::endl; + return false; + } + } + + if (settingArgs.size() > 1) + screenshotW = std::min(10000,std::atoi(settingArgs[1].c_str())); + + if (settingArgs.size() > 2) + screenshotH = std::min(10000,std::atoi(settingArgs[2].c_str())); + + if (settingArgs.size() > 3) + cubeSize = std::min(5000,std::atoi(settingArgs[3].c_str())); + } + catch (std::runtime_error) + { + std::cerr << "Wrong parameters for screenshot type." << std::endl; return false; + } - int screenshotWidth = Settings::Manager::tryGetInt("s360 width","Video",mViewer->getCamera()->getViewport()->width()); - int screenshotHeight = Settings::Manager::tryGetInt("s360 height","Video",mViewer->getCamera()->getViewport()->height()); - SphericalScreenshot::SphericalScreenshotMapping mapping = static_cast( - Settings::Manager::tryGetInt("s360 mapping","Video",SphericalScreenshot::MAPPING_SPHERICAL)); - int cubeWidth = Settings::Manager::tryGetInt("s360 cubemap size","Video",screenshotWidth / 2); + if (mCamera->isVanityOrPreviewModeEnabled()) + { + std::cerr << "Spherical screenshots are not allowed in preview mode." << std::endl; + return false; + } - if (mapping == SphericalScreenshot::MAPPING_CUBEMAP) - screenshotWidth = cubeWidth * 6; // the image will consist of 6 cube sides in a row + if (screenshotMapping == SphericalScreenshot::MAPPING_CUBEMAP) + screenshotW = cubeSize * 6; // the image will consist of 6 cube sides in a row - SphericalScreenshot s(cubeWidth); + SphericalScreenshot s(cubeSize); osg::Vec3 directions[6] = { osg::Vec3(0,0,-1), @@ -797,20 +842,20 @@ namespace MWRender }; double fovBackup = mFieldOfView; - mFieldOfView = 90.0; // each side sees 90 degrees + mFieldOfView = 90.0; // each cubemap side sees 90 degrees int maskBackup = mPlayerAnimation->getObjectRoot()->getNodeMask(); if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(0); - for (int i = 0; i < 6; i++) // for each cube side + for (int i = 0; i < 6; i++) // for each cubemap side { osg::Image *sideImage = s.getImage(i); - screenshot(sideImage,cubeWidth,cubeWidth,directions[i]); + screenshot(sideImage,cubeSize,cubeSize,directions[i]); } - s.create(image,screenshotWidth,mapping != SphericalScreenshot::MAPPING_SMALL_PLANET ? screenshotHeight : screenshotWidth,mapping); + s.create(image,screenshotW,screenshotMapping != SphericalScreenshot::MAPPING_SMALL_PLANET ? screenshotH : screenshotW,screenshotMapping); mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); mFieldOfView = fovBackup; From e804c4a011f1e6a1a1d14a39ddb4e48f981e3c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Sat, 11 Nov 2017 13:54:14 +0100 Subject: [PATCH 023/282] remove no longer used method --- components/settings/settings.cpp | 12 ------------ components/settings/settings.hpp | 1 - 2 files changed, 13 deletions(-) diff --git a/components/settings/settings.cpp b/components/settings/settings.cpp index 4e250974a..e93642ee2 100644 --- a/components/settings/settings.cpp +++ b/components/settings/settings.cpp @@ -401,18 +401,6 @@ int Manager::getInt (const std::string& setting, const std::string& category) return parseInt( getString(setting, category) ); } -int Manager::tryGetInt (const std::string &setting, const std::string& category, int defaultValue) -{ - try - { - return getInt(setting,category); - } - catch (std::runtime_error) - { - return defaultValue; - } -} - bool Manager::getBool (const std::string& setting, const std::string& category) { return parseBool( getString(setting, category) ); diff --git a/components/settings/settings.hpp b/components/settings/settings.hpp index 9e80c21a6..7adcb9b39 100644 --- a/components/settings/settings.hpp +++ b/components/settings/settings.hpp @@ -39,7 +39,6 @@ namespace Settings ///< returns the list of changed settings and then clears it static int getInt (const std::string& setting, const std::string& category); - static int tryGetInt (const std::string &setting, const std::string& category, int defaultValue); static float getFloat (const std::string& setting, const std::string& category); static std::string getString (const std::string& setting, const std::string& category); static bool getBool (const std::string& setting, const std::string& category); From d71d984cfae6b476eed0a404c0d8a97c81083af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Sat, 11 Nov 2017 14:14:24 +0100 Subject: [PATCH 024/282] more unused stuff cleanup --- apps/openmw/mwinput/inputmanagerimp.hpp | 2 -- apps/openmw/mwrender/water.cpp | 28 ++++--------------------- apps/openmw/mwrender/water.hpp | 1 - apps/openmw/mwstate/statemanagerimp.cpp | 1 + 4 files changed, 5 insertions(+), 27 deletions(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.hpp b/apps/openmw/mwinput/inputmanagerimp.hpp index 82b62e79b..bc62ef7dc 100644 --- a/apps/openmw/mwinput/inputmanagerimp.hpp +++ b/apps/openmw/mwinput/inputmanagerimp.hpp @@ -259,8 +259,6 @@ namespace MWInput A_Screenshot, // Take a screenshot - A_Screenshot360, // Take a 360 degree screenshot - A_Inventory, // Toggle inventory screen A_Console, // Toggle console screen diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index 8b047eb84..ee5b0d599 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -207,6 +207,7 @@ osg::ref_ptr readPngImage (const std::string& file) return result.getImage(); } + class Refraction : public osg::Camera { public: @@ -220,7 +221,7 @@ public: setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); setName("RefractionCamera"); - setupCullMask(true); + setCullMask(Mask_Effect|Mask_Scene|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting); setNodeMask(Mask_RenderToTexture); setViewport(0, 0, rttSize, rttSize); @@ -261,12 +262,6 @@ public: attach(osg::Camera::DEPTH_BUFFER, mRefractionDepthTexture); } - void setupCullMask(bool enabled) - { - setCullMask(!enabled ? 0 : - Mask_Effect|Mask_Scene|Mask_Terrain|Mask_Actor|Mask_ParticleSystem|Mask_Sky|Mask_Sun|Mask_Player|Mask_Lighting); - } - void setScene(osg::Node* scene) { if (mScene) @@ -309,9 +304,9 @@ public: setSmallFeatureCullingPixelSize(Settings::Manager::getInt("small feature culling pixel size", "Water")); setName("ReflectionCamera"); - mReflectActors = Settings::Manager::getBool("reflect actors", "Water"); + bool reflectActors = Settings::Manager::getBool("reflect actors", "Water"); - setupCullMask(true); + setCullMask(Mask_Effect|Mask_Scene|Mask_Terrain|Mask_ParticleSystem|Mask_Sky|Mask_Player|Mask_Lighting|(reflectActors ? Mask_Actor : 0)); setNodeMask(Mask_RenderToTexture); unsigned int rttSize = Settings::Manager::getInt("rtt size", "Water"); @@ -339,12 +334,6 @@ public: addChild(mClipCullNode); } - void setupCullMask(bool enabled) - { - setCullMask(!enabled ? 0 : - Mask_Effect|Mask_Scene|Mask_Terrain|Mask_ParticleSystem|Mask_Sky|Mask_Player|Mask_Lighting|(mReflectActors ? Mask_Actor : 0)); - } - void setWaterLevel(float waterLevel) { setViewMatrix(osg::Matrix::scale(1,1,-1) * osg::Matrix::translate(0,0,2 * waterLevel)); @@ -719,13 +708,4 @@ void Water::clearRipples() mSimulation->clear(); } -void Water::setEffectsEnabled(bool enabled) -{ - if (mReflection) - mReflection->setupCullMask(enabled); - - if (mRefraction) - mRefraction->setupCullMask(enabled); -} - } diff --git a/apps/openmw/mwrender/water.hpp b/apps/openmw/mwrender/water.hpp index 6985ddca6..e2413cfa0 100644 --- a/apps/openmw/mwrender/water.hpp +++ b/apps/openmw/mwrender/water.hpp @@ -107,7 +107,6 @@ namespace MWRender void removeCell(const MWWorld::CellStore* store); ///< remove all emitters in this cell void clearRipples(); - void setEffectsEnabled(bool enabled); void changeCell(const MWWorld::CellStore* store); void setHeight(const float height); diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index c643480a9..14ee5adee 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -655,4 +655,5 @@ void MWState::StateManager::writeScreenshot(std::vector &imageData) const std::string data = ostream.str(); imageData = std::vector(data.begin(), data.end()); + } From 4fc532d873f163c9d582c9d5354aa59baea7f2f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Sat, 11 Nov 2017 14:32:28 +0100 Subject: [PATCH 025/282] reference screenshot settings only from one place --- apps/openmw/mwbase/world.hpp | 2 +- apps/openmw/mwinput/inputmanagerimp.cpp | 9 ++-- apps/openmw/mwrender/renderingmanager.cpp | 59 ++++++++++------------- apps/openmw/mwrender/renderingmanager.hpp | 2 +- apps/openmw/mwrender/water.cpp | 1 - apps/openmw/mwworld/worldimp.cpp | 4 +- apps/openmw/mwworld/worldimp.hpp | 2 +- 7 files changed, 35 insertions(+), 44 deletions(-) diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index f91d1bc2a..f72f74c53 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -450,7 +450,7 @@ namespace MWBase /// \todo this does not belong here virtual void screenshot (osg::Image* image, int w, int h) = 0; - virtual bool screenshot360 (osg::Image* image) = 0; + virtual bool screenshot360 (osg::Image* image, std::string settingStr) = 0; /// Find default position inside exterior cell specified by name /// \return false if exterior with given name not exists, true otherwise diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 10288bbc0..0215e43df 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1019,11 +1019,12 @@ namespace MWInput { bool regularScreenshot = true; + std::string settingStr; + try { - // FIXME: the same string "screenshot type" is queried here AND in renderingmanager.cpp - std::string s = Settings::Manager::getString("screenshot type","Video"); - regularScreenshot = s.size() == 0; + settingStr = Settings::Manager::getString("screenshot type","Video"); + regularScreenshot = settingStr.size() == 0; } catch (std::runtime_error) { @@ -1039,7 +1040,7 @@ namespace MWInput { osg::ref_ptr screenshot (new osg::Image); - if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get())) + if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(),settingStr)) (*mScreenCaptureOperation) (*(screenshot.get()),0); } } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 682f35cc6..5fbc5d397 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -773,53 +773,44 @@ namespace MWRender int mSize; }; - bool RenderingManager::screenshot360(osg::Image* image) + bool RenderingManager::screenshot360(osg::Image* image, std::string settingStr) { int screenshotW = mViewer->getCamera()->getViewport()->width(); int screenshotH = mViewer->getCamera()->getViewport()->height(); SphericalScreenshot::SphericalScreenshotMapping screenshotMapping = SphericalScreenshot::MAPPING_SPHERICAL; int cubeSize = screenshotW / 2; - try - { - std::string settingStr = Settings::Manager::getString("screenshot type","Video"); - std::vector settingArgs; - boost::algorithm::split(settingArgs,settingStr,boost::is_any_of(" ")); - - if (settingArgs.size() > 0) - { - std::string typeStrings[4] = {"spherical","cylindrical","planet","cubemap"}; - bool found = false; + std::vector settingArgs; + boost::algorithm::split(settingArgs,settingStr,boost::is_any_of(" ")); - for (int i = 0; i < 4; ++i) - if (settingArgs[0].compare(typeStrings[i]) == 0) - { - screenshotMapping = (SphericalScreenshot::SphericalScreenshotMapping) i; - found = true; - break; - } + if (settingArgs.size() > 0) + { + std::string typeStrings[4] = {"spherical","cylindrical","planet","cubemap"}; + bool found = false; - if (!found) + for (int i = 0; i < 4; ++i) + if (settingArgs[0].compare(typeStrings[i]) == 0) { - std::cerr << "Wrong screenshot type: " << settingArgs[0] << "." << std::endl; - return false; + screenshotMapping = (SphericalScreenshot::SphericalScreenshotMapping) i; + found = true; + break; } + + if (!found) + { + std::cerr << "Wrong screenshot type: " << settingArgs[0] << "." << std::endl; + return false; } - - if (settingArgs.size() > 1) - screenshotW = std::min(10000,std::atoi(settingArgs[1].c_str())); + } + + if (settingArgs.size() > 1) + screenshotW = std::min(10000,std::atoi(settingArgs[1].c_str())); - if (settingArgs.size() > 2) - screenshotH = std::min(10000,std::atoi(settingArgs[2].c_str())); + if (settingArgs.size() > 2) + screenshotH = std::min(10000,std::atoi(settingArgs[2].c_str())); - if (settingArgs.size() > 3) - cubeSize = std::min(5000,std::atoi(settingArgs[3].c_str())); - } - catch (std::runtime_error) - { - std::cerr << "Wrong parameters for screenshot type." << std::endl; - return false; - } + if (settingArgs.size() > 3) + cubeSize = std::min(5000,std::atoi(settingArgs[3].c_str())); if (mCamera->isVanityOrPreviewModeEnabled()) { diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 9e0394593..01630c408 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -126,7 +126,7 @@ namespace MWRender /// Take a screenshot of w*h onto the given image, not including the GUI. void screenshot(osg::Image* image, int w, int h, osg::Vec3 direction=osg::Vec3(0,0,-1)); - bool screenshot360(osg::Image* image); + bool screenshot360(osg::Image* image, std::string settingStr); struct RayResult { diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index ee5b0d599..bfd671971 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -357,7 +357,6 @@ private: osg::ref_ptr mReflectionTexture; osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; - bool mReflectActors; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 0d0079677..1f8b01eac 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2281,9 +2281,9 @@ namespace MWWorld mRendering->screenshot(image, w, h); } - bool World::screenshot360(osg::Image* image) + bool World::screenshot360(osg::Image* image, std::string settingStr) { - return mRendering->screenshot360(image); + return mRendering->screenshot360(image,settingStr); } void World::activateDoor(const MWWorld::Ptr& door) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 3a54d28ae..e432e77d3 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -560,7 +560,7 @@ namespace MWWorld /// \todo this does not belong here void screenshot (osg::Image* image, int w, int h) override; - bool screenshot360 (osg::Image* image) override; + bool screenshot360 (osg::Image* image, std::string settingStr) override; /// Find center of exterior cell above land surface /// \return false if exterior with given name not exists, true otherwise From 511a5686dad417e8c5827a99dc29f85aa4487cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Sat, 11 Nov 2017 15:10:54 +0100 Subject: [PATCH 026/282] planet mapping adjustment --- apps/openmw/mwinput/inputmanagerimp.cpp | 1 + apps/openmw/mwrender/renderingmanager.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 0215e43df..cfae8e872 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1042,6 +1042,7 @@ namespace MWInput if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(),settingStr)) (*mScreenCaptureOperation) (*(screenshot.get()),0); + // mScreenCaptureHandler->getCaptureOperation() causes crash for some reason } } diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 5fbc5d397..b2d97e81d 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -654,7 +654,7 @@ namespace MWRender { dest->allocateImage(mSize * 6,mSize,mImages[0]->r(),mImages[0]->getPixelFormat(),mImages[0]->getDataType()); - for (int i = 0; i < 6; i++) + for (int i = 0; i < 6; ++i) osg::copyImage(mImages[i].get(),0,0,0,mImages[i]->s(),mImages[i]->t(),mImages[i]->r(),dest,i * mSize,0,0); return; @@ -778,7 +778,9 @@ namespace MWRender int screenshotW = mViewer->getCamera()->getViewport()->width(); int screenshotH = mViewer->getCamera()->getViewport()->height(); SphericalScreenshot::SphericalScreenshotMapping screenshotMapping = SphericalScreenshot::MAPPING_SPHERICAL; - int cubeSize = screenshotW / 2; + int cubeSize = screenshotMapping == SphericalScreenshot::SphericalScreenshotMapping::MAPPING_SMALL_PLANET ? + screenshotW: // planet mapping needs higher resolution + screenshotW / 2; std::vector settingArgs; boost::algorithm::split(settingArgs,settingStr,boost::is_any_of(" ")); From 1ab854446c4098bf34cee03d829428dcab34eef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Sat, 11 Nov 2017 17:21:01 +0100 Subject: [PATCH 027/282] remove unused include --- apps/openmw/mwinput/inputmanagerimp.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index cfae8e872..b491fb996 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1,7 +1,6 @@ #include "inputmanagerimp.hpp" #include -#include #include #include From 823218bb611adbbe07a1a4727a6f381cc7b896b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Tue, 14 Nov 2017 18:23:12 +0100 Subject: [PATCH 028/282] freeze screen during screenshot taking --- apps/openmw/mwrender/renderingmanager.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index b2d97e81d..a1b508f01 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -890,6 +890,13 @@ namespace MWRender mRootNode->addChild(rttCamera); + rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + GLbitfield maskBackup = mViewer->getCamera()->getClearMask(); + double clearDepthBackup = mViewer->getCamera()->getClearDepth(); + mViewer->getCamera()->setClearMask(GL_DEPTH_BUFFER_BIT); + mViewer->getCamera()->setClearDepth(0); + // The draw needs to complete before we can copy back our image. osg::ref_ptr callback (new NotifyDrawCompletedCallback); rttCamera->setFinalDrawCallback(callback); @@ -907,6 +914,9 @@ namespace MWRender // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); + mViewer->getCamera()->setClearMask(maskBackup); + mViewer->getCamera()->setClearDepth(clearDepthBackup); + rttCamera->removeChildren(0, rttCamera->getNumChildren()); mRootNode->removeChild(rttCamera); } From 1c3d45f641cc6e5b01b29b33ec6caf158cccde4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 15 Nov 2017 15:20:59 +0100 Subject: [PATCH 029/282] dirty 360 screenshot GPU setup --- apps/openmw/mwrender/renderingmanager.cpp | 101 ++++++++++++++++++---- apps/openmw/mwrender/renderingmanager.hpp | 3 + files/shaders/s360_fragment.glsl | 9 ++ files/shaders/s360_vertex.glsl | 9 ++ 4 files changed, 106 insertions(+), 16 deletions(-) create mode 100644 files/shaders/s360_fragment.glsl create mode 100644 files/shaders/s360_vertex.glsl diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index a1b508f01..878056955 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include @@ -24,6 +26,7 @@ #include #include #include +#include #include @@ -853,12 +856,93 @@ namespace MWRender mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); mFieldOfView = fovBackup; + +osg::ref_ptr cubeTexture (new osg::TextureCubeMap); + +for (int i = 0; i < 6; ++i) + cubeTexture->setImage(i,s.getImage(i)); + +osg::ref_ptr screenshotCamera (new osg::Camera); +osg::ref_ptr quad (new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0),2.0))); + +std::map defineMap; + +Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); +osg::ref_ptr fragmentShader (shaderMgr.getShader("s360_fragment.glsl",defineMap,osg::Shader::FRAGMENT)); +osg::ref_ptr vertexShader (shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); +osg::ref_ptr stateset = new osg::StateSet; + +osg::ref_ptr program (new osg::Program); +program->addShader(fragmentShader); +program->addShader(vertexShader); +stateset->setAttributeAndModes(program, osg::StateAttribute::ON); + +stateset->addUniform(new osg::Uniform("cubeMap",0)); +stateset->setTextureAttributeAndModes(0,cubeTexture,osg::StateAttribute::ON); + +quad->setStateSet(stateset); +quad->setUpdateCallback(NULL); + +screenshotCamera->addChild(quad); + +mRootNode->addChild(screenshotCamera); + +renderCameraToImage(screenshotCamera,image,1000,640); + + +screenshotCamera->removeChildren(0,screenshotCamera->getNumChildren()); +mRootNode->removeChild(screenshotCamera); + + return true; } + void RenderingManager::renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h) + { + camera->setNodeMask(Mask_RenderToTexture); + camera->attach(osg::Camera::COLOR_BUFFER, image); + camera->setRenderOrder(osg::Camera::PRE_RENDER); + camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); + camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT,osg::Camera::PIXEL_BUFFER_RTT); + + camera->setViewport(0, 0, w, h); + + osg::ref_ptr texture (new osg::Texture2D); + texture->setInternalFormat(GL_RGB); + texture->setTextureSize(w,h); + texture->setResizeNonPowerOfTwoHint(false); + texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); + texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); + camera->attach(osg::Camera::COLOR_BUFFER,texture); + + image->setDataType(GL_UNSIGNED_BYTE); + image->setPixelFormat(texture->getInternalFormat()); + + // The draw needs to complete before we can copy back our image. + osg::ref_ptr callback (new NotifyDrawCompletedCallback); + camera->setFinalDrawCallback(callback); + + // at the time this function is called we are in the middle of a frame, + // so out of order calls are necessary to get a correct frameNumber for the next frame. + // refer to the advance() and frame() order in Engine::go() + + mViewer->eventTraversal(); + mViewer->updateTraversal(); + mViewer->renderingTraversals(); + + callback->waitTillDone(); + + // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed + mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); + } + void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Vec3 direction) { osg::ref_ptr rttCamera (new osg::Camera); + + rttCamera->setProjectionMatrixAsPerspective(mFieldOfView, w/float(h), mNearClip, mViewDistance); + rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * osg::Matrixd::rotate(osg::Vec3(0,0,-1),direction)); + rttCamera->setNodeMask(Mask_RenderToTexture); rttCamera->attach(osg::Camera::COLOR_BUFFER, image); rttCamera->setRenderOrder(osg::Camera::PRE_RENDER); @@ -897,22 +981,7 @@ namespace MWRender mViewer->getCamera()->setClearMask(GL_DEPTH_BUFFER_BIT); mViewer->getCamera()->setClearDepth(0); - // The draw needs to complete before we can copy back our image. - osg::ref_ptr callback (new NotifyDrawCompletedCallback); - rttCamera->setFinalDrawCallback(callback); - - // at the time this function is called we are in the middle of a frame, - // so out of order calls are necessary to get a correct frameNumber for the next frame. - // refer to the advance() and frame() order in Engine::go() - - mViewer->eventTraversal(); - mViewer->updateTraversal(); - mViewer->renderingTraversals(); - - callback->waitTillDone(); - - // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed - mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); + renderCameraToImage(rttCamera.get(),image,w,h); mViewer->getCamera()->setClearMask(maskBackup); mViewer->getCamera()->setClearDepth(clearDepthBackup); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 01630c408..e6dfd3999 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -3,6 +3,7 @@ #include #include +#include #include @@ -214,6 +215,8 @@ namespace MWRender void reportStats() const; + void renderCameraToImage(osg::Camera *camera, osg::Image *image, int w, int h); + osg::ref_ptr getIntersectionVisitor(osgUtil::Intersector* intersector, bool ignorePlayer, bool ignoreActors); osg::ref_ptr mIntersectionVisitor; diff --git a/files/shaders/s360_fragment.glsl b/files/shaders/s360_fragment.glsl new file mode 100644 index 000000000..4bf91abb3 --- /dev/null +++ b/files/shaders/s360_fragment.glsl @@ -0,0 +1,9 @@ +#version 120 + +varying vec2 uv; +uniform samplerCube cubeMap; + +void main(void) +{ + gl_FragData[0] = textureCube(cubeMap,vec3(uv.x * 2.0 - 1.0,uv.y * 2.0 - 1.0,1)); +} diff --git a/files/shaders/s360_vertex.glsl b/files/shaders/s360_vertex.glsl new file mode 100644 index 000000000..84cecfdaf --- /dev/null +++ b/files/shaders/s360_vertex.glsl @@ -0,0 +1,9 @@ +#version 120 + +varying vec2 uv; + +void main(void) +{ + gl_Position = gl_Vertex; + uv = (gl_Vertex.xy + vec2(1.0)) / 2.0; +} From 226fb9c26bdbbcb7a406f3ca874319d0eb4a7119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 15 Nov 2017 16:07:01 +0100 Subject: [PATCH 030/282] render cubemaps in OGL coordinates --- apps/openmw/mwrender/renderingmanager.cpp | 85 ++++++++++++----------- apps/openmw/mwrender/renderingmanager.hpp | 2 +- files/shaders/s360_fragment.glsl | 14 +++- files/shaders/s360_vertex.glsl | 2 +- 4 files changed, 61 insertions(+), 42 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 878056955..bb7fbd5de 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -829,13 +829,20 @@ namespace MWRender SphericalScreenshot s(cubeSize); osg::Vec3 directions[6] = { - osg::Vec3(0,0,-1), - osg::Vec3(-1,0,0), - osg::Vec3(0,0,1), - osg::Vec3(1,0,0), - osg::Vec3(0,1,0), - osg::Vec3(0,-1,0), - }; + osg::Vec3(0,0,1), + osg::Vec3(0,0,-1), + osg::Vec3(-1,0,0), + osg::Vec3(1,0,0), + osg::Vec3(0,1,0), + osg::Vec3(0,-1,0)}; + + double rotations[] = { + -osg::PI / 2.0, + osg::PI / 2.0, + osg::PI, + 0, + osg::PI / 2.0, + osg::PI / 2.0}; double fovBackup = mFieldOfView; mFieldOfView = 90.0; // each cubemap side sees 90 degrees @@ -847,51 +854,55 @@ namespace MWRender for (int i = 0; i < 6; i++) // for each cubemap side { + osg::Matrixd transform = osg::Matrixd::rotate(osg::Vec3(0,0,-1),directions[i]) * osg::Matrixd::rotate(rotations[i],osg::Vec3(0,0,-1)); osg::Image *sideImage = s.getImage(i); - screenshot(sideImage,cubeSize,cubeSize,directions[i]); + screenshot(sideImage,cubeSize,cubeSize,transform); + sideImage->flipHorizontal(); } - s.create(image,screenshotW,screenshotMapping != SphericalScreenshot::MAPPING_SMALL_PLANET ? screenshotH : screenshotW,screenshotMapping); +// s.create(image,screenshotW,screenshotMapping != SphericalScreenshot::MAPPING_SMALL_PLANET ? screenshotH : screenshotW,screenshotMapping); mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); mFieldOfView = fovBackup; -osg::ref_ptr cubeTexture (new osg::TextureCubeMap); -for (int i = 0; i < 6; ++i) - cubeTexture->setImage(i,s.getImage(i)); -osg::ref_ptr screenshotCamera (new osg::Camera); -osg::ref_ptr quad (new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0),2.0))); + osg::ref_ptr cubeTexture (new osg::TextureCubeMap); -std::map defineMap; + for (int i = 0; i < 6; ++i) + cubeTexture->setImage(i,s.getImage(i)); -Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); -osg::ref_ptr fragmentShader (shaderMgr.getShader("s360_fragment.glsl",defineMap,osg::Shader::FRAGMENT)); -osg::ref_ptr vertexShader (shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); -osg::ref_ptr stateset = new osg::StateSet; + osg::ref_ptr screenshotCamera (new osg::Camera); + osg::ref_ptr quad (new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0),2.0))); -osg::ref_ptr program (new osg::Program); -program->addShader(fragmentShader); -program->addShader(vertexShader); -stateset->setAttributeAndModes(program, osg::StateAttribute::ON); + std::map defineMap; -stateset->addUniform(new osg::Uniform("cubeMap",0)); -stateset->setTextureAttributeAndModes(0,cubeTexture,osg::StateAttribute::ON); - -quad->setStateSet(stateset); -quad->setUpdateCallback(NULL); + Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); + osg::ref_ptr fragmentShader (shaderMgr.getShader("s360_fragment.glsl",defineMap,osg::Shader::FRAGMENT)); + osg::ref_ptr vertexShader (shaderMgr.getShader("s360_vertex.glsl", defineMap, osg::Shader::VERTEX)); + osg::ref_ptr stateset = new osg::StateSet; + + osg::ref_ptr program (new osg::Program); + program->addShader(fragmentShader); + program->addShader(vertexShader); + stateset->setAttributeAndModes(program, osg::StateAttribute::ON); + + stateset->addUniform(new osg::Uniform("cubeMap",0)); + stateset->setTextureAttributeAndModes(0,cubeTexture,osg::StateAttribute::ON); + + quad->setStateSet(stateset); + quad->setUpdateCallback(NULL); -screenshotCamera->addChild(quad); + screenshotCamera->addChild(quad); -mRootNode->addChild(screenshotCamera); + mRootNode->addChild(screenshotCamera); -renderCameraToImage(screenshotCamera,image,1000,640); + renderCameraToImage(screenshotCamera,image,1000,640); -screenshotCamera->removeChildren(0,screenshotCamera->getNumChildren()); -mRootNode->removeChild(screenshotCamera); + screenshotCamera->removeChildren(0,screenshotCamera->getNumChildren()); + mRootNode->removeChild(screenshotCamera); return true; @@ -936,20 +947,16 @@ mRootNode->removeChild(screenshotCamera); mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); } - void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Vec3 direction) + void RenderingManager::screenshot(osg::Image *image, int w, int h, osg::Matrixd cameraTransform) { osg::ref_ptr rttCamera (new osg::Camera); - - rttCamera->setProjectionMatrixAsPerspective(mFieldOfView, w/float(h), mNearClip, mViewDistance); - rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * osg::Matrixd::rotate(osg::Vec3(0,0,-1),direction)); - rttCamera->setNodeMask(Mask_RenderToTexture); rttCamera->attach(osg::Camera::COLOR_BUFFER, image); rttCamera->setRenderOrder(osg::Camera::PRE_RENDER); rttCamera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); rttCamera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); rttCamera->setProjectionMatrixAsPerspective(mFieldOfView, w/float(h), mNearClip, mViewDistance); - rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * osg::Matrixd::rotate(osg::Vec3(0,0,-1),direction)); + rttCamera->setViewMatrix(mViewer->getCamera()->getViewMatrix() * cameraTransform); rttCamera->setViewport(0, 0, w, h); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index e6dfd3999..8fd96cff9 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -126,7 +126,7 @@ namespace MWRender void setWaterHeight(float level); /// Take a screenshot of w*h onto the given image, not including the GUI. - void screenshot(osg::Image* image, int w, int h, osg::Vec3 direction=osg::Vec3(0,0,-1)); + void screenshot(osg::Image* image, int w, int h, osg::Matrixd cameraTransform=osg::Matrixd()); bool screenshot360(osg::Image* image, std::string settingStr); struct RayResult diff --git a/files/shaders/s360_fragment.glsl b/files/shaders/s360_fragment.glsl index 4bf91abb3..3ff8e93c1 100644 --- a/files/shaders/s360_fragment.glsl +++ b/files/shaders/s360_fragment.glsl @@ -3,7 +3,19 @@ varying vec2 uv; uniform samplerCube cubeMap; +#define PI 3.1415926535 + +vec3 cylindricalCoords(vec2 coords) +{ + return normalize(vec3(cos(-1 * coords.x * 2 * PI),sin(-1 * coords.x * 2 * PI),coords.y * 2.0 - 1.0)); +} + void main(void) { - gl_FragData[0] = textureCube(cubeMap,vec3(uv.x * 2.0 - 1.0,uv.y * 2.0 - 1.0,1)); + vec3 c; + c.x = uv.x * 2.0 - 1.0; + c.y = uv.y * 2.0 - 1.0; + c.z = 1.0; + + gl_FragData[0] = textureCube(cubeMap,vec3(cylindricalCoords(uv))); } diff --git a/files/shaders/s360_vertex.glsl b/files/shaders/s360_vertex.glsl index 84cecfdaf..b7fbf28a4 100644 --- a/files/shaders/s360_vertex.glsl +++ b/files/shaders/s360_vertex.glsl @@ -5,5 +5,5 @@ varying vec2 uv; void main(void) { gl_Position = gl_Vertex; - uv = (gl_Vertex.xy + vec2(1.0)) / 2.0; + uv = (gl_Vertex.xy * vec2(1.0,-1.0) + vec2(1.0)) / 2.0; } From af38d3a47d46a8d78f6f41564714026c7cdca6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 15 Nov 2017 17:01:16 +0100 Subject: [PATCH 031/282] get rid of sphericalscreenshot class --- apps/openmw/mwrender/renderingmanager.cpp | 203 ++++------------------ files/shaders/s360_fragment.glsl | 40 ++++- files/shaders/s360_vertex.glsl | 2 +- 3 files changed, 75 insertions(+), 170 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index bb7fbd5de..3b82b549d 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -627,161 +627,12 @@ namespace MWRender mutable bool mDone; }; - class SphericalScreenshot - { - public: - typedef enum - { - MAPPING_SPHERICAL = 0, - MAPPING_CYLINDRICAL, - MAPPING_SMALL_PLANET, - MAPPING_CUBEMAP - } SphericalScreenshotMapping; - - SphericalScreenshot(int size) - { - mSize = size; - - for (int i = 0; i < 6; ++i) - mImages.push_back(new osg::Image); - } - - osg::Image *getImage(int index) - { - return mImages[index].get(); - } - - void create(osg::Image *dest, int w, int h, SphericalScreenshotMapping mapping) - { - if (mapping == MAPPING_CUBEMAP) - { - dest->allocateImage(mSize * 6,mSize,mImages[0]->r(),mImages[0]->getPixelFormat(),mImages[0]->getDataType()); - - for (int i = 0; i < 6; ++i) - osg::copyImage(mImages[i].get(),0,0,0,mImages[i]->s(),mImages[i]->t(),mImages[i]->r(),dest,i * mSize,0,0); - - return; - } - - dest->allocateImage(w,h,mImages[0]->r(),mImages[0]->getPixelFormat(),mImages[0]->getDataType()); - - for (int j = 0; j < h; ++j) - for (int i = 0; i < w; ++i) - { - osg::Vec3d coords; - osg::Vec2d normalizedXY = osg::Vec2d(i / ((float) w), j / ((float) h)); - - switch (mapping) - { - case MAPPING_CYLINDRICAL: coords = cylindricalCoords(normalizedXY.x(),normalizedXY.y()); break; - case MAPPING_SPHERICAL: coords = sphericalCoords(normalizedXY.x(),normalizedXY.y()); break; - case MAPPING_SMALL_PLANET: coords = smallPlanetCoords(normalizedXY.x(),normalizedXY.y()); break; - default: break; - } - - dest->setColor(getColorByDirection(coords),i,j); - } - } - - osg::Vec3d cylindricalCoords(double x, double y) - { - osg::Vec3 result = osg::Vec3d(cos(-1 * x * 2 * osg::PI),sin(-1 * x * 2 * osg::PI),y * 2.0 - 1.0); - result.normalize(); - return result; - } - - osg::Vec3d sphericalCoords(double x, double y) - { - x = -1 * x * 2 * osg::PI; - y = (y - 0.5) * osg::PI; - - osg::Vec3 result = osg::Vec3(0.0,cos(y),sin(y)); - result = osg::Vec3(cos(x) * result.y(),sin(x) * result.y(),result.z()); - - return result; - } - - osg::Vec3d smallPlanetCoords(double x, double y) - { - osg::Vec2d fromCenter = osg::Vec2d(x,y) - osg::Vec2d(0.5,0.5); - - double magnitude = fromCenter.length(); - - fromCenter.normalize(); - double dot = fromCenter * osg::Vec2d(0.0,1.0); - - x = x > 0.5 ? 0.5 - (dot + 1) / 4.0 : 0.5 + (dot + 1) / 4.0; - y = pow(std::min(1.0,magnitude / 0.5),0.5); - - return sphericalCoords(x,y); - } - - osg::Vec4 getColorByDirection(osg::Vec3d d) - { - // for details see OpenGL 4.4 specification page 225 - - double x, y; - double ma; - int side; - - double ax, ay, az; - ax = fabs(d.x()); - ay = fabs(d.y()); - az = fabs(d.z()); - - if (ax > ay) - if (ax > az) - { - side = d.x() > 0 ? 1 : 3; - ma = ax; - } - else - { - side = d.z() > 0 ? 5 : 4; - ma = az; - } - else - if (ay > az) - { - side = d.y() > 0 ? 0 : 2; - ma = ay; - } - else - { - side = d.z() > 0 ? 5 : 4; - ma = az; - } - - switch (side) - { - case 0: x = d.x(); y = d.z(); break; - case 1: x = -d.y(); y = d.z(); break; - case 2: x = -d.x(); y = d.z(); break; - case 3: x = d.y(); y = d.z(); break; - case 4: x = d.x(); y = d.y(); break; - case 5: x = d.x(); y = -d.y(); break; - default: break; - } - - x = 0.5 * (x / ma + 1); - y = 0.5 * (y / ma + 1); - - return mImages[side]->getColor( - std::min(std::max(int(x * mSize),0),mSize - 1), - std::min(std::max(int(y * mSize),0),mSize - 1)); - } - - protected: - std::vector> mImages; - int mSize; - }; - bool RenderingManager::screenshot360(osg::Image* image, std::string settingStr) { int screenshotW = mViewer->getCamera()->getViewport()->width(); int screenshotH = mViewer->getCamera()->getViewport()->height(); - SphericalScreenshot::SphericalScreenshotMapping screenshotMapping = SphericalScreenshot::MAPPING_SPHERICAL; - int cubeSize = screenshotMapping == SphericalScreenshot::SphericalScreenshotMapping::MAPPING_SMALL_PLANET ? + int screenshotMapping = 0; + int cubeSize = screenshotMapping == 2 ? screenshotW: // planet mapping needs higher resolution screenshotW / 2; @@ -796,7 +647,7 @@ namespace MWRender for (int i = 0; i < 4; ++i) if (settingArgs[0].compare(typeStrings[i]) == 0) { - screenshotMapping = (SphericalScreenshot::SphericalScreenshotMapping) i; + screenshotMapping = i; found = true; break; } @@ -823,16 +674,23 @@ namespace MWRender return false; } - if (screenshotMapping == SphericalScreenshot::MAPPING_CUBEMAP) + bool rawCubemap = screenshotMapping == 3; + + if (rawCubemap) screenshotW = cubeSize * 6; // the image will consist of 6 cube sides in a row + else if (screenshotMapping == 2) + screenshotH = screenshotW; // use square resolution for planet mapping - SphericalScreenshot s(cubeSize); + std::vector> images; + + for (int i = 0; i < 6; ++i) + images.push_back(new osg::Image); osg::Vec3 directions[6] = { - osg::Vec3(0,0,1), + rawCubemap ? osg::Vec3(1,0,0) : osg::Vec3(0,0,1), osg::Vec3(0,0,-1), osg::Vec3(-1,0,0), - osg::Vec3(1,0,0), + rawCubemap ? osg::Vec3(0,0,1) : osg::Vec3(1,0,0), osg::Vec3(0,1,0), osg::Vec3(0,-1,0)}; @@ -854,24 +712,40 @@ namespace MWRender for (int i = 0; i < 6; i++) // for each cubemap side { - osg::Matrixd transform = osg::Matrixd::rotate(osg::Vec3(0,0,-1),directions[i]) * osg::Matrixd::rotate(rotations[i],osg::Vec3(0,0,-1)); - osg::Image *sideImage = s.getImage(i); + osg::Matrixd transform = osg::Matrixd::rotate(osg::Vec3(0,0,-1),directions[i]); + + if (!rawCubemap) + transform *= osg::Matrixd::rotate(rotations[i],osg::Vec3(0,0,-1)); + + osg::Image *sideImage = images[i].get(); screenshot(sideImage,cubeSize,cubeSize,transform); - sideImage->flipHorizontal(); - } -// s.create(image,screenshotW,screenshotMapping != SphericalScreenshot::MAPPING_SMALL_PLANET ? screenshotH : screenshotW,screenshotMapping); + if (!rawCubemap) + sideImage->flipHorizontal(); + } mPlayerAnimation->getObjectRoot()->setNodeMask(maskBackup); mFieldOfView = fovBackup; + if (rawCubemap) // for raw cubemap don't run on GPU, just merge the images + { + image->allocateImage(cubeSize * 6,cubeSize,images[0]->r(),images[0]->getPixelFormat(),images[0]->getDataType()); + for (int i = 0; i < 6; ++i) + osg::copyImage(images[i].get(),0,0,0,images[i]->s(),images[i]->t(),images[i]->r(),image,i * cubeSize,0,0); + return true; + } + + // run on GPU now: osg::ref_ptr cubeTexture (new osg::TextureCubeMap); + + cubeTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); + cubeTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); for (int i = 0; i < 6; ++i) - cubeTexture->setImage(i,s.getImage(i)); + cubeTexture->setImage(i,images[i].get()); osg::ref_ptr screenshotCamera (new osg::Camera); osg::ref_ptr quad (new osg::ShapeDrawable(new osg::Box(osg::Vec3(0,0,0),2.0))); @@ -889,6 +763,7 @@ namespace MWRender stateset->setAttributeAndModes(program, osg::StateAttribute::ON); stateset->addUniform(new osg::Uniform("cubeMap",0)); + stateset->addUniform(new osg::Uniform("mapping",screenshotMapping)); stateset->setTextureAttributeAndModes(0,cubeTexture,osg::StateAttribute::ON); quad->setStateSet(stateset); @@ -898,13 +773,11 @@ namespace MWRender mRootNode->addChild(screenshotCamera); - renderCameraToImage(screenshotCamera,image,1000,640); - + renderCameraToImage(screenshotCamera,image,screenshotW,screenshotH); screenshotCamera->removeChildren(0,screenshotCamera->getNumChildren()); mRootNode->removeChild(screenshotCamera); - return true; } diff --git a/files/shaders/s360_fragment.glsl b/files/shaders/s360_fragment.glsl index 3ff8e93c1..e2a6e2c99 100644 --- a/files/shaders/s360_fragment.glsl +++ b/files/shaders/s360_fragment.glsl @@ -2,20 +2,52 @@ varying vec2 uv; uniform samplerCube cubeMap; +uniform int mapping; #define PI 3.1415926535 +vec3 sphericalCoords(vec2 coords) +{ + coords.x = -1 * coords.x * 2 * PI; + coords.y = (coords.y - 0.5) * PI; + + vec3 result = vec3(0.0,cos(coords.y),sin(coords.y)); + result = vec3(cos(coords.x) * result.y,sin(coords.x) * result.y,result.z); + + return result; +} + vec3 cylindricalCoords(vec2 coords) { return normalize(vec3(cos(-1 * coords.x * 2 * PI),sin(-1 * coords.x * 2 * PI),coords.y * 2.0 - 1.0)); } +vec3 planetCoords(vec2 coords) +{ + vec2 fromCenter = coords - vec2(0.5,0.5); + + float magnitude = length(fromCenter); + + fromCenter = normalize(fromCenter); + + float dotProduct = dot(fromCenter,vec2(0.0,1.0)); + + coords.x = coords.x > 0.5 ? 0.5 - (dotProduct + 1.0) / 4.0 : 0.5 + (dotProduct + 1.0) / 4.0; + coords.y = max(0.0,1.0 - pow(magnitude / 0.5,0.5)); + return sphericalCoords(coords); +} + void main(void) { vec3 c; - c.x = uv.x * 2.0 - 1.0; - c.y = uv.y * 2.0 - 1.0; - c.z = 1.0; - gl_FragData[0] = textureCube(cubeMap,vec3(cylindricalCoords(uv))); + switch (mapping) + { + case 0: c = sphericalCoords(uv); break; + case 1: c = cylindricalCoords(uv); break; + case 2: c = planetCoords(uv); break; + default: c = sphericalCoords(uv); break; + } + + gl_FragData[0] = textureCube(cubeMap,c); } diff --git a/files/shaders/s360_vertex.glsl b/files/shaders/s360_vertex.glsl index b7fbf28a4..ad96620c3 100644 --- a/files/shaders/s360_vertex.glsl +++ b/files/shaders/s360_vertex.glsl @@ -5,5 +5,5 @@ varying vec2 uv; void main(void) { gl_Position = gl_Vertex; - uv = (gl_Vertex.xy * vec2(1.0,-1.0) + vec2(1.0)) / 2.0; + uv = (gl_Vertex.xy * vec2(1.0,-1.0) + vec2(1.0)) / 2; } From 1a4f351e3d9fafbb60c0baa1d0485376b55c65aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 16 Nov 2017 12:17:52 +0100 Subject: [PATCH 032/282] move camera freeze code --- apps/openmw/mwrender/renderingmanager.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 3b82b549d..c571d516f 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -806,6 +806,11 @@ namespace MWRender osg::ref_ptr callback (new NotifyDrawCompletedCallback); camera->setFinalDrawCallback(callback); + GLbitfield maskBackup = mViewer->getCamera()->getClearMask(); + double clearDepthBackup = mViewer->getCamera()->getClearDepth(); + mViewer->getCamera()->setClearMask(GL_DEPTH_BUFFER_BIT); // don't render the main camera + mViewer->getCamera()->setClearDepth(0); + // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() @@ -816,6 +821,9 @@ namespace MWRender callback->waitTillDone(); + mViewer->getCamera()->setClearMask(maskBackup); + mViewer->getCamera()->setClearDepth(clearDepthBackup); + // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); } @@ -856,16 +864,8 @@ namespace MWRender rttCamera->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - GLbitfield maskBackup = mViewer->getCamera()->getClearMask(); - double clearDepthBackup = mViewer->getCamera()->getClearDepth(); - mViewer->getCamera()->setClearMask(GL_DEPTH_BUFFER_BIT); - mViewer->getCamera()->setClearDepth(0); - renderCameraToImage(rttCamera.get(),image,w,h); - mViewer->getCamera()->setClearMask(maskBackup); - mViewer->getCamera()->setClearDepth(clearDepthBackup); - rttCamera->removeChildren(0, rttCamera->getNumChildren()); mRootNode->removeChild(rttCamera); } From 56c74fb96fcf59a43919d8f303ffb222817d4857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 16 Nov 2017 12:46:46 +0100 Subject: [PATCH 033/282] add scene switch node --- apps/openmw/mwrender/renderingmanager.cpp | 11 ++++++++--- apps/openmw/mwrender/renderingmanager.hpp | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index c571d516f..bc16a5ea2 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -205,7 +205,9 @@ namespace MWRender mSceneRoot = sceneRoot; sceneRoot->setStartLight(1); - mRootNode->addChild(sceneRoot); + mSceneSwitch = new osg::Switch; + mSceneSwitch->addChild(sceneRoot); + mRootNode->addChild(mSceneSwitch); mPathgrid.reset(new Pathgrid(mRootNode)); @@ -814,13 +816,16 @@ namespace MWRender // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() - + + mSceneSwitch->setAllChildrenOff(); // don't render the scene for main camera + mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); - callback->waitTillDone(); + mSceneSwitch->setAllChildrenOn(); + mViewer->getCamera()->setClearMask(maskBackup); mViewer->getCamera()->setClearDepth(clearDepthBackup); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 8fd96cff9..4a271228e 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -224,6 +224,7 @@ namespace MWRender osg::ref_ptr mViewer; osg::ref_ptr mRootNode; osg::ref_ptr mSceneRoot; + osg::ref_ptr mSceneSwitch; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mWorkQueue; From bccff768ffa8f890b4a7d335883730f9957187f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 16 Nov 2017 13:41:09 +0100 Subject: [PATCH 034/282] select closest power of 2 resolution for the cubemap --- apps/openmw/mwrender/renderingmanager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index bc16a5ea2..18d2dea2a 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -683,6 +683,9 @@ namespace MWRender else if (screenshotMapping == 2) screenshotH = screenshotW; // use square resolution for planet mapping + if (!rawCubemap) + cubeSize = pow(2,round(log2(cubeSize))); // select closest power of 2 for GPU + std::vector> images; for (int i = 0; i < 6; ++i) From 902862aa8bc79b98e373cfd715026a41e21bcdd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 16 Nov 2017 13:50:10 +0100 Subject: [PATCH 035/282] display confirming message for all screenshots --- apps/openmw/mwinput/inputmanagerimp.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index b491fb996..5aa98b6de 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1017,6 +1017,7 @@ namespace MWInput void InputManager::screenshot() { bool regularScreenshot = true; + bool screenshotTaken = false; std::string settingStr; @@ -1033,16 +1034,22 @@ namespace MWInput { mScreenCaptureHandler->setFramesToCapture(1); mScreenCaptureHandler->captureNextFrame(*mViewer); - MWBase::Environment::get().getWindowManager()->messageBox("Screenshot saved"); + screenshotTaken = true; } else { osg::ref_ptr screenshot (new osg::Image); if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(),settingStr)) + { (*mScreenCaptureOperation) (*(screenshot.get()),0); // mScreenCaptureHandler->getCaptureOperation() causes crash for some reason + screenshotTaken = true; + } } + + if (screenshotTaken) + MWBase::Environment::get().getWindowManager()->messageBox("Screenshot saved"); } void InputManager::toggleInventory() From 2b5f14754577c351e44cc8e18875b4abdb04c7a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 16 Nov 2017 20:06:34 +0100 Subject: [PATCH 036/282] allow non-power-of-2 sized cubemaps --- apps/openmw/mwrender/renderingmanager.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 18d2dea2a..92480f45b 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -683,9 +683,6 @@ namespace MWRender else if (screenshotMapping == 2) screenshotH = screenshotW; // use square resolution for planet mapping - if (!rawCubemap) - cubeSize = pow(2,round(log2(cubeSize))); // select closest power of 2 for GPU - std::vector> images; for (int i = 0; i < 6; ++i) @@ -745,6 +742,7 @@ namespace MWRender // run on GPU now: osg::ref_ptr cubeTexture (new osg::TextureCubeMap); + cubeTexture->setResizeNonPowerOfTwoHint(false); cubeTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); cubeTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); From 3ae53105679b9b5dba9cf84b3f37f043fb3c48e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 16 Nov 2017 22:08:33 +0100 Subject: [PATCH 037/282] use loading screen to freeze the screen --- apps/openmw/mwgui/loadingscreen.cpp | 14 +++++++++++--- apps/openmw/mwgui/loadingscreen.hpp | 4 +++- apps/openmw/mwrender/renderingmanager.cpp | 19 +++++-------------- .../loadinglistener/loadinglistener.hpp | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/apps/openmw/mwgui/loadingscreen.cpp b/apps/openmw/mwgui/loadingscreen.cpp index 4753c39f2..023a693c9 100644 --- a/apps/openmw/mwgui/loadingscreen.cpp +++ b/apps/openmw/mwgui/loadingscreen.cpp @@ -153,7 +153,7 @@ namespace MWGui virtual osg::BoundingSphere computeBound(const osg::Node&) const { return osg::BoundingSphere(); } }; - void LoadingScreen::loadingOn() + void LoadingScreen::loadingOn(bool visible) { mLoadingOnTime = mTimer.time_m(); // Early-out if already on @@ -170,7 +170,10 @@ namespace MWGui // We are already using node masks to avoid the scene from being updated/rendered, but node masks don't work for computeBound() mViewer->getSceneData()->setComputeBoundingSphereCallback(new DontComputeBoundCallback); - mShowWallpaper = (MWBase::Environment::get().getStateManager()->getState() + mVisible = visible; + mLoadingBox->setVisible(mVisible); + + mShowWallpaper = mVisible && (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame); setVisible(true); @@ -181,10 +184,15 @@ namespace MWGui } MWBase::Environment::get().getWindowManager()->pushGuiMode(mShowWallpaper ? GM_LoadingWallpaper : GM_Loading); + + if (!mVisible) + draw(); } void LoadingScreen::loadingOff() { + mLoadingBox->setVisible(true); // restore + if (mLastRenderTime < mLoadingOnTime) { // the loading was so fast that we didn't show loading screen at all @@ -307,7 +315,7 @@ namespace MWGui void LoadingScreen::draw() { - if (!needToDrawLoadingScreen()) + if (mVisible && !needToDrawLoadingScreen()) return; if (mShowWallpaper && mTimer.time_m() > mLastWallpaperChangeTime + 5000*1) diff --git a/apps/openmw/mwgui/loadingscreen.hpp b/apps/openmw/mwgui/loadingscreen.hpp index 8536972a3..bdd210d00 100644 --- a/apps/openmw/mwgui/loadingscreen.hpp +++ b/apps/openmw/mwgui/loadingscreen.hpp @@ -35,7 +35,7 @@ namespace MWGui /// Overridden from Loading::Listener, see the Loading::Listener documentation for usage details virtual void setLabel (const std::string& label, bool important); - virtual void loadingOn(); + virtual void loadingOn(bool visible=true); virtual void loadingOff(); virtual void setProgressRange (size_t range); virtual void setProgress (size_t value); @@ -63,6 +63,8 @@ namespace MWGui bool mImportantLabel; + bool mVisible; + size_t mProgress; bool mShowWallpaper; diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 92480f45b..037cf9403 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -47,6 +47,9 @@ #include #include "../mwworld/cellstore.hpp" +#include "../mwgui/loadingscreen.hpp" +#include "../mwbase/environment.hpp" +#include "../mwbase/windowmanager.hpp" #include "sky.hpp" #include "effectmanager.hpp" @@ -809,26 +812,14 @@ namespace MWRender osg::ref_ptr callback (new NotifyDrawCompletedCallback); camera->setFinalDrawCallback(callback); - GLbitfield maskBackup = mViewer->getCamera()->getClearMask(); - double clearDepthBackup = mViewer->getCamera()->getClearDepth(); - mViewer->getCamera()->setClearMask(GL_DEPTH_BUFFER_BIT); // don't render the main camera - mViewer->getCamera()->setClearDepth(0); - - // at the time this function is called we are in the middle of a frame, - // so out of order calls are necessary to get a correct frameNumber for the next frame. - // refer to the advance() and frame() order in Engine::go() - - mSceneSwitch->setAllChildrenOff(); // don't render the scene for main camera + MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOn(false); mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); callback->waitTillDone(); - mSceneSwitch->setAllChildrenOn(); - - mViewer->getCamera()->setClearMask(maskBackup); - mViewer->getCamera()->setClearDepth(clearDepthBackup); + MWBase::Environment::get().getWindowManager()->getLoadingScreen()->loadingOff(); // now that we've "used up" the current frame, get a fresh framenumber for the next frame() following after the screenshot is completed mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); diff --git a/components/loadinglistener/loadinglistener.hpp b/components/loadinglistener/loadinglistener.hpp index 1d48cce0b..6c7a3b090 100644 --- a/components/loadinglistener/loadinglistener.hpp +++ b/components/loadinglistener/loadinglistener.hpp @@ -20,7 +20,7 @@ namespace Loading /// @note To get the loading screen to actually update, you must call setProgress / increaseProgress periodically. /// @note It is best to use the ScopedLoad object instead of using loadingOn()/loadingOff() directly, /// so that the loading is exception safe. - virtual void loadingOn() {} + virtual void loadingOn(bool visible=true) {} virtual void loadingOff() {} /// Set the total range of progress (e.g. the number of objects to load). From fc507c66f7d289760134c204571978cda65ff343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 16 Nov 2017 22:17:00 +0100 Subject: [PATCH 038/282] remove no longer needed stuff --- apps/openmw/mwrender/renderingmanager.cpp | 4 +--- apps/openmw/mwrender/renderingmanager.hpp | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 037cf9403..fd352f694 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -208,9 +208,7 @@ namespace MWRender mSceneRoot = sceneRoot; sceneRoot->setStartLight(1); - mSceneSwitch = new osg::Switch; - mSceneSwitch->addChild(sceneRoot); - mRootNode->addChild(mSceneSwitch); + mRootNode->addChild(mSceneRoot); mPathgrid.reset(new Pathgrid(mRootNode)); diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 4a271228e..8fd96cff9 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -224,7 +224,6 @@ namespace MWRender osg::ref_ptr mViewer; osg::ref_ptr mRootNode; osg::ref_ptr mSceneRoot; - osg::ref_ptr mSceneSwitch; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mWorkQueue; From ddbf6c162fa47896047c3a1d4c53225e1a44d752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 16 Nov 2017 22:43:41 +0100 Subject: [PATCH 039/282] use linear filtering for the cubemap --- apps/openmw/mwrender/renderingmanager.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index fd352f694..55bf2f2c4 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -744,6 +744,9 @@ namespace MWRender osg::ref_ptr cubeTexture (new osg::TextureCubeMap); cubeTexture->setResizeNonPowerOfTwoHint(false); + + cubeTexture->setFilter(osg::Texture::MIN_FILTER,osg::Texture::LINEAR); + cubeTexture->setFilter(osg::Texture::MAG_FILTER,osg::Texture::LINEAR); cubeTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); cubeTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); From df61a302592fe40c28eb839a50861f311e8c21db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 16 Nov 2017 23:20:24 +0100 Subject: [PATCH 040/282] preincrement --- apps/openmw/mwrender/renderingmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 55bf2f2c4..7b734f6ce 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -713,7 +713,7 @@ namespace MWRender if (mCamera->isFirstPerson()) mPlayerAnimation->getObjectRoot()->setNodeMask(0); - for (int i = 0; i < 6; i++) // for each cubemap side + for (int i = 0; i < 6; ++i) // for each cubemap side { osg::Matrixd transform = osg::Matrixd::rotate(osg::Vec3(0,0,-1),directions[i]); From ea5e078526b8d8d26f50bf5f7a7cf70ac791f656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Fri, 1 Dec 2017 19:54:48 +0100 Subject: [PATCH 041/282] Update shaders in CMakeLists --- files/shaders/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/files/shaders/CMakeLists.txt b/files/shaders/CMakeLists.txt index 5833a592f..7baca78ef 100644 --- a/files/shaders/CMakeLists.txt +++ b/files/shaders/CMakeLists.txt @@ -16,6 +16,8 @@ set(SHADER_FILES terrain_fragment.glsl lighting.glsl parallax.glsl + s360_fragment.glsl + s360_vertex.glsl ) copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_SHADERS_ROOT} ${DDIRRELATIVE} "${SHADER_FILES}") From 390838e084c9fa2ab4a66151af3fb9dc512037b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Fri, 1 Dec 2017 21:03:29 +0100 Subject: [PATCH 042/282] Replace switch with ifs in shader --- files/shaders/s360_fragment.glsl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/files/shaders/s360_fragment.glsl b/files/shaders/s360_fragment.glsl index e2a6e2c99..f52e1478e 100644 --- a/files/shaders/s360_fragment.glsl +++ b/files/shaders/s360_fragment.glsl @@ -41,13 +41,12 @@ void main(void) { vec3 c; - switch (mapping) - { - case 0: c = sphericalCoords(uv); break; - case 1: c = cylindricalCoords(uv); break; - case 2: c = planetCoords(uv); break; - default: c = sphericalCoords(uv); break; - } + if (mapping == 0) + c = sphericalCoords(uv); + else if (mapping == 1) + c = cylindricalCoords(uv); + else + c = planetCoords(uv); gl_FragData[0] = textureCube(cubeMap,c); } From 37aa7612831864e3c24db9c4e3689ce0bb93ca27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Fri, 1 Dec 2017 21:47:26 +0100 Subject: [PATCH 043/282] Change spherical screenshot filtering to nearest --- apps/openmw/mwrender/renderingmanager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 7b734f6ce..3b9100126 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -745,8 +745,8 @@ namespace MWRender osg::ref_ptr cubeTexture (new osg::TextureCubeMap); cubeTexture->setResizeNonPowerOfTwoHint(false); - cubeTexture->setFilter(osg::Texture::MIN_FILTER,osg::Texture::LINEAR); - cubeTexture->setFilter(osg::Texture::MAG_FILTER,osg::Texture::LINEAR); + cubeTexture->setFilter(osg::Texture::MIN_FILTER,osg::Texture::NEAREST); + cubeTexture->setFilter(osg::Texture::MAG_FILTER,osg::Texture::NEAREST); cubeTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); cubeTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); From a142a67972f835d914c88a20528bf636630b2691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 6 Dec 2017 21:16:30 +0100 Subject: [PATCH 044/282] Add default settings for 360 screenshots --- apps/openmw/mwinput/inputmanagerimp.cpp | 2 +- files/settings-default.cfg | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 5aa98b6de..e01deb669 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1024,7 +1024,7 @@ namespace MWInput try { settingStr = Settings::Manager::getString("screenshot type","Video"); - regularScreenshot = settingStr.size() == 0; + regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0; } catch (std::runtime_error) { diff --git a/files/settings-default.cfg b/files/settings-default.cfg index aec667a9c..c0f9e85ce 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -359,6 +359,10 @@ contrast = 1.0 # Video gamma setting. (>0.0). No effect in Linux. gamma = 1.0 +# Type of screenshot to take (regular, cylindrical, spherical or planet), optionally followed by +# screenshot width, height and cubemap resolution in pixels. (e.g. spherical 1600 1000 1200) +screenshot type = regular + [Water] # Enable water shader with reflections and optionally refraction. From 10fe3342477816e99e7d9cd12e726cd09146f98d Mon Sep 17 00:00:00 2001 From: Florian Weber Date: Fri, 2 Mar 2018 15:05:00 +0100 Subject: [PATCH 045/282] add more precise float-spinbox and use it for rotations --- apps/opencs/model/world/columnbase.hpp | 1 + apps/opencs/model/world/columnimp.hpp | 2 +- apps/opencs/model/world/refidcollection.cpp | 6 +++--- apps/opencs/view/world/util.cpp | 9 +++++++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/opencs/model/world/columnbase.hpp b/apps/opencs/model/world/columnbase.hpp index d609a6253..df37afe60 100644 --- a/apps/opencs/model/world/columnbase.hpp +++ b/apps/opencs/model/world/columnbase.hpp @@ -88,6 +88,7 @@ namespace CSMWorld Display_UnsignedInteger8, Display_Integer, Display_Float, + Display_Double, Display_Var, Display_GmstVarType, Display_GlobalVarType, diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index f1025acb9..4ad447b0a 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -1371,7 +1371,7 @@ namespace CSMWorld RotColumn (ESM::Position ESXRecordT::* position, int index, bool door) : Column ( (door ? Columns::ColumnId_DoorPositionXRot : Columns::ColumnId_PositionXRot)+index, - ColumnBase::Display_Float), mPosition (position), mIndex (index) {} + ColumnBase::Display_Double), mPosition (position), mIndex (index) {} virtual QVariant get (const Record& record) const { diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index 44a6ce07d..daebc2dcb 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -184,11 +184,11 @@ CSMWorld::RefIdCollection::RefIdCollection() mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_PosZ, CSMWorld::ColumnBase::Display_Float)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_RotX, CSMWorld::ColumnBase::Display_Float)); + new RefIdColumn (Columns::ColumnId_RotX, CSMWorld::ColumnBase::Display_Double)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_RotY, CSMWorld::ColumnBase::Display_Float)); + new RefIdColumn (Columns::ColumnId_RotY, CSMWorld::ColumnBase::Display_Double)); mColumns.back().addColumn( - new RefIdColumn (Columns::ColumnId_RotZ, CSMWorld::ColumnBase::Display_Float)); + new RefIdColumn (Columns::ColumnId_RotZ, CSMWorld::ColumnBase::Display_Double)); // Nested table mColumns.push_back(RefIdColumn (Columns::ColumnId_AiPackageList, diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index efba1ea82..eab37e1bf 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -233,6 +233,15 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO return dsb; } + case CSMWorld::ColumnBase::Display_Double: + { + DialogueDoubleSpinBox *dsb = new DialogueDoubleSpinBox(parent); + dsb->setRange(-FLT_MAX, FLT_MAX); + dsb->setSingleStep(0.01f); + dsb->setDecimals(6); + return dsb; + } + case CSMWorld::ColumnBase::Display_LongString: { QPlainTextEdit *edit = new QPlainTextEdit(parent); From da74ca5ce0242cfd107a98d7934835aba178024f Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Mon, 5 Mar 2018 21:26:59 -0600 Subject: [PATCH 046/282] Add testing options to the Settings page --- apps/launcher/settingspage.cpp | 42 ++++ apps/launcher/settingspage.hpp | 1 + files/ui/settingspage.ui | 367 ++++++++++++++++++++------------- 3 files changed, 265 insertions(+), 145 deletions(-) diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 843b51391..906459c23 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -177,6 +177,28 @@ void Launcher::SettingsPage::on_browseButton_clicked() } } +void Launcher::SettingsPage::on_runScriptAfterStartupBrowseButton_clicked() +{ + QString scriptFile = QFileDialog::getOpenFileName( + this, + QObject::tr("Select script file"), + QDir::currentPath(), + QString(tr("Text file (*.txt)"))); + + + if (scriptFile.isEmpty()) + return; + + QFileInfo info(scriptFile); + + if (!info.exists() || !info.isReadable()) + return; + + const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); + + runScriptAfterStartupField->setText(path); +} + void Launcher::SettingsPage::wizardStarted() { mMain->hide(); // Hide the launcher @@ -260,6 +282,19 @@ void Launcher::SettingsPage::saveSettings() } else { mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); } + + // Testing + int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; + if (skipMenu != mGameSettings.value("skip-menu").toInt()) + mGameSettings.setValue("skip-menu", QString::number(skipMenu)); + + QString startCell = startDefaultCharacterAtField->text(); + if (startCell != mGameSettings.value("start")) { + mGameSettings.setValue("start", startCell); + } + QString scriptRun = runScriptAfterStartupField->text(); + if (scriptRun != mGameSettings.value("script-run")) + mGameSettings.setValue("script-run", scriptRun); } bool Launcher::SettingsPage::loadSettings() @@ -271,5 +306,12 @@ bool Launcher::SettingsPage::loadSettings() if (index != -1) languageComboBox->setCurrentIndex(index); + // Testing + if (mGameSettings.value("skip-menu").toInt() == 1) + skipMenuCheckBox->setCheckState(Qt::Checked); + + startDefaultCharacterAtField->setText(mGameSettings.value("start")); + runScriptAfterStartupField->setText(mGameSettings.value("script-run")); + return true; } diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index ccc2061dd..12539a0fc 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -38,6 +38,7 @@ namespace Launcher void on_wizardButton_clicked(); void on_importerButton_clicked(); void on_browseButton_clicked(); + void on_runScriptAfterStartupBrowseButton_clicked(); void wizardStarted(); void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui index f4f41f839..eed723adb 100644 --- a/files/ui/settingspage.ui +++ b/files/ui/settingspage.ui @@ -7,7 +7,7 @@ 0 0 514 - 397 + 532 @@ -15,153 +15,230 @@ - - - Morrowind Content Language + + + true - - - - - - 250 - 0 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + 0 + 0 + 473 + 510 + + + + + + + Morrowind Content Language + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 250 + 0 + + + + + + + + + + + Morrowind Installation Wizard + + + + + + Run &Installation Wizard + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Morrowind Settings Importer + + + + + + + + File to import settings from: + + + + + + + + + + Browse... + + + + + + + + + Import add-on and plugin selection (creates a new Content List) + + + true + + + + + + + + + Run &Settings Importer + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 4 + + + false + + + + + + + + + + Testing + + + + + + Skip menu and generate default character + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Start default character at + + + + + + + default cell + + + + + + + + + Run script after startup: + + + + + + + + + + + + Browse… + + + + + + + + + + - - - - Morrowind Installation Wizard - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Run &Installation Wizard - - - - - - - - - - Morrowind Settings Importer - - - - - - - - File to import settings from: - - - - - - - - - - Browse... - - - - - - - - - Import add-on and plugin selection (creates a new Content List) - - - true - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Run &Settings Importer - - - - - - - - - 4 - - - false - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - From 082e166faefd2daa149d3af49657c98bbb67db5f Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Mon, 5 Mar 2018 21:41:29 -0600 Subject: [PATCH 047/282] Making "start default character at" field enabled or disabled by the previous checkbox --- apps/launcher/settingspage.cpp | 55 ++++++++++++++++++++-------------- apps/launcher/settingspage.hpp | 4 ++- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 906459c23..ec784c9d3 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -177,28 +177,6 @@ void Launcher::SettingsPage::on_browseButton_clicked() } } -void Launcher::SettingsPage::on_runScriptAfterStartupBrowseButton_clicked() -{ - QString scriptFile = QFileDialog::getOpenFileName( - this, - QObject::tr("Select script file"), - QDir::currentPath(), - QString(tr("Text file (*.txt)"))); - - - if (scriptFile.isEmpty()) - return; - - QFileInfo info(scriptFile); - - if (!info.exists() || !info.isReadable()) - return; - - const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); - - runScriptAfterStartupField->setText(path); -} - void Launcher::SettingsPage::wizardStarted() { mMain->hide(); // Hide the launcher @@ -269,6 +247,33 @@ void Launcher::SettingsPage::updateOkButton(const QString &text) : mProfileDialog->setOkButtonEnabled(true); } +void Launcher::SettingsPage::on_skipMenuCheckBox_stateChanged(int state) { + startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked); + startDefaultCharacterAtField->setEnabled(state == Qt::Checked); +} + +void Launcher::SettingsPage::on_runScriptAfterStartupBrowseButton_clicked() +{ + QString scriptFile = QFileDialog::getOpenFileName( + this, + QObject::tr("Select script file"), + QDir::currentPath(), + QString(tr("Text file (*.txt)"))); + + + if (scriptFile.isEmpty()) + return; + + QFileInfo info(scriptFile); + + if (!info.exists() || !info.isReadable()) + return; + + const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); + + runScriptAfterStartupField->setText(path); +} + void Launcher::SettingsPage::saveSettings() { QString language(languageComboBox->currentText()); @@ -307,8 +312,12 @@ bool Launcher::SettingsPage::loadSettings() languageComboBox->setCurrentIndex(index); // Testing - if (mGameSettings.value("skip-menu").toInt() == 1) + bool skipMenu = mGameSettings.value("skip-menu").toInt() == Qt::Checked; + if (skipMenu) { skipMenuCheckBox->setCheckState(Qt::Checked); + } + startDefaultCharacterAtLabel->setEnabled(skipMenu); + startDefaultCharacterAtField->setEnabled(skipMenu); startDefaultCharacterAtField->setText(mGameSettings.value("start")); runScriptAfterStartupField->setText(mGameSettings.value("script-run")); diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index 12539a0fc..a9de974ff 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -38,7 +38,6 @@ namespace Launcher void on_wizardButton_clicked(); void on_importerButton_clicked(); void on_browseButton_clicked(); - void on_runScriptAfterStartupBrowseButton_clicked(); void wizardStarted(); void wizardFinished(int exitCode, QProcess::ExitStatus exitStatus); @@ -48,6 +47,9 @@ namespace Launcher void updateOkButton(const QString &text); + void on_skipMenuCheckBox_stateChanged(int state); + void on_runScriptAfterStartupBrowseButton_clicked(); + private: Process::ProcessInvoker *mWizardInvoker; From dcc262ed911fa4c0ba3db4e7f47e0d3cb22d80dd Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Mon, 5 Mar 2018 23:10:08 -0600 Subject: [PATCH 048/282] Fixing Skip Menu checkbox not working correctly --- apps/launcher/settingspage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index ec784c9d3..986aee048 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -312,7 +312,7 @@ bool Launcher::SettingsPage::loadSettings() languageComboBox->setCurrentIndex(index); // Testing - bool skipMenu = mGameSettings.value("skip-menu").toInt() == Qt::Checked; + bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1; if (skipMenu) { skipMenuCheckBox->setCheckState(Qt::Checked); } From 6931f6cadc14128df681c66ee269cee739aba31c Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Wed, 7 Mar 2018 18:37:43 -0600 Subject: [PATCH 049/282] Adding message indicating the purpose of the "Testing" block --- files/ui/settingspage.ui | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui index eed723adb..f1b833c60 100644 --- a/files/ui/settingspage.ui +++ b/files/ui/settingspage.ui @@ -23,9 +23,9 @@ 0 - 0 + -44 473 - 510 + 567 @@ -170,6 +170,23 @@ Testing + + + + These settings are intended for testing mods and may cause issues if used for normal gameplay. + + + true + + + + + + + Qt::Horizontal + + + From f07a12af733d6cabd3ce66cb49fedba474d7a8cd Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Thu, 8 Mar 2018 18:09:18 -0600 Subject: [PATCH 050/282] Changing label "and may cause issues" to "and will cause issues" --- files/ui/settingspage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui index f1b833c60..d0c77f4b7 100644 --- a/files/ui/settingspage.ui +++ b/files/ui/settingspage.ui @@ -173,7 +173,7 @@ - These settings are intended for testing mods and may cause issues if used for normal gameplay. + These settings are intended for testing mods and will cause issues if used for normal gameplay. true From d42791e26056e7dd98ab639f24b0627b2cff83a9 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Thu, 15 Mar 2018 22:11:54 -0500 Subject: [PATCH 051/282] Moving testing options to Advanced page --- apps/launcher/advancedpage.cpp | 58 ++++- apps/launcher/advancedpage.hpp | 9 +- apps/launcher/maindialog.cpp | 2 +- apps/launcher/settingspage.cpp | 51 ----- apps/launcher/settingspage.hpp | 3 - files/ui/advancedpage.ui | 99 ++++++++- files/ui/settingspage.ui | 384 +++++++++++++-------------------- 7 files changed, 300 insertions(+), 306 deletions(-) diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 0abefcc8f..bc3308da0 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -1,10 +1,14 @@ #include "advancedpage.hpp" -#include +#include +#include -Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent) +Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, + Config::GameSettings &gameSettings, + Settings::Manager &engineSettings, QWidget *parent) : QWidget(parent) , mCfgMgr(cfg) + , mGameSettings(gameSettings) , mEngineSettings(engineSettings) { setObjectName ("AdvancedPage"); @@ -13,8 +17,45 @@ Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, Settings: loadSettings(); } +void Launcher::AdvancedPage::on_skipMenuCheckBox_stateChanged(int state) { + startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked); + startDefaultCharacterAtField->setEnabled(state == Qt::Checked); +} + +void Launcher::AdvancedPage::on_runScriptAfterStartupBrowseButton_clicked() +{ + QString scriptFile = QFileDialog::getOpenFileName( + this, + QObject::tr("Select script file"), + QDir::currentPath(), + QString(tr("Text file (*.txt)"))); + + + if (scriptFile.isEmpty()) + return; + + QFileInfo info(scriptFile); + + if (!info.exists() || !info.isReadable()) + return; + + const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); + +} + bool Launcher::AdvancedPage::loadSettings() { + // Testing + bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1; + if (skipMenu) { + skipMenuCheckBox->setCheckState(Qt::Checked); + } + startDefaultCharacterAtLabel->setEnabled(skipMenu); + startDefaultCharacterAtField->setEnabled(skipMenu); + + startDefaultCharacterAtField->setText(mGameSettings.value("start")); + runScriptAfterStartupField->setText(mGameSettings.value("script-run")); + // Game Settings loadSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); loadSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); @@ -53,6 +94,19 @@ void Launcher::AdvancedPage::saveSettings() // Ensure we only set the new settings if they changed. This is to avoid cluttering the // user settings file (which by definition should only contain settings the user has touched) + // Testing + int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; + if (skipMenu != mGameSettings.value("skip-menu").toInt()) + mGameSettings.setValue("skip-menu", QString::number(skipMenu)); + + QString startCell = startDefaultCharacterAtField->text(); + if (startCell != mGameSettings.value("start")) { + mGameSettings.setValue("start", startCell); + } + QString scriptRun = runScriptAfterStartupField->text(); + if (scriptRun != mGameSettings.value("script-run")) + mGameSettings.setValue("script-run", scriptRun); + // Game Settings saveSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); saveSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp index a8361c98e..0a9957b9c 100644 --- a/apps/launcher/advancedpage.hpp +++ b/apps/launcher/advancedpage.hpp @@ -8,6 +8,7 @@ #include namespace Files { struct ConfigurationManager; } +namespace Config { class GameSettings; } namespace Launcher { @@ -16,13 +17,19 @@ namespace Launcher Q_OBJECT public: - AdvancedPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent = 0); + AdvancedPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, + Settings::Manager &engineSettings, QWidget *parent = 0); bool loadSettings(); void saveSettings(); + private slots: + void on_skipMenuCheckBox_stateChanged(int state); + void on_runScriptAfterStartupBrowseButton_clicked(); + private: Files::ConfigurationManager &mCfgMgr; + Config::GameSettings &mGameSettings; Settings::Manager &mEngineSettings; void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 1a210ccc5..356863d03 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -119,7 +119,7 @@ void Launcher::MainDialog::createPages() mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mGraphicsPage = new GraphicsPage(mCfgMgr, mEngineSettings, this); mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this); - mAdvancedPage = new AdvancedPage(mCfgMgr, mEngineSettings, this); + mAdvancedPage = new AdvancedPage(mCfgMgr, mGameSettings, mEngineSettings, this); // Set the combobox of the play page to imitate the combobox on the datafilespage mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); diff --git a/apps/launcher/settingspage.cpp b/apps/launcher/settingspage.cpp index 986aee048..843b51391 100644 --- a/apps/launcher/settingspage.cpp +++ b/apps/launcher/settingspage.cpp @@ -247,33 +247,6 @@ void Launcher::SettingsPage::updateOkButton(const QString &text) : mProfileDialog->setOkButtonEnabled(true); } -void Launcher::SettingsPage::on_skipMenuCheckBox_stateChanged(int state) { - startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked); - startDefaultCharacterAtField->setEnabled(state == Qt::Checked); -} - -void Launcher::SettingsPage::on_runScriptAfterStartupBrowseButton_clicked() -{ - QString scriptFile = QFileDialog::getOpenFileName( - this, - QObject::tr("Select script file"), - QDir::currentPath(), - QString(tr("Text file (*.txt)"))); - - - if (scriptFile.isEmpty()) - return; - - QFileInfo info(scriptFile); - - if (!info.exists() || !info.isReadable()) - return; - - const QString path(QDir::toNativeSeparators(info.absoluteFilePath())); - - runScriptAfterStartupField->setText(path); -} - void Launcher::SettingsPage::saveSettings() { QString language(languageComboBox->currentText()); @@ -287,19 +260,6 @@ void Launcher::SettingsPage::saveSettings() } else { mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252")); } - - // Testing - int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked; - if (skipMenu != mGameSettings.value("skip-menu").toInt()) - mGameSettings.setValue("skip-menu", QString::number(skipMenu)); - - QString startCell = startDefaultCharacterAtField->text(); - if (startCell != mGameSettings.value("start")) { - mGameSettings.setValue("start", startCell); - } - QString scriptRun = runScriptAfterStartupField->text(); - if (scriptRun != mGameSettings.value("script-run")) - mGameSettings.setValue("script-run", scriptRun); } bool Launcher::SettingsPage::loadSettings() @@ -311,16 +271,5 @@ bool Launcher::SettingsPage::loadSettings() if (index != -1) languageComboBox->setCurrentIndex(index); - // Testing - bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1; - if (skipMenu) { - skipMenuCheckBox->setCheckState(Qt::Checked); - } - startDefaultCharacterAtLabel->setEnabled(skipMenu); - startDefaultCharacterAtField->setEnabled(skipMenu); - - startDefaultCharacterAtField->setText(mGameSettings.value("start")); - runScriptAfterStartupField->setText(mGameSettings.value("script-run")); - return true; } diff --git a/apps/launcher/settingspage.hpp b/apps/launcher/settingspage.hpp index a9de974ff..ccc2061dd 100644 --- a/apps/launcher/settingspage.hpp +++ b/apps/launcher/settingspage.hpp @@ -47,9 +47,6 @@ namespace Launcher void updateOkButton(const QString &text); - void on_skipMenuCheckBox_stateChanged(int state); - void on_runScriptAfterStartupBrowseButton_clicked(); - private: Process::ProcessInvoker *mWizardInvoker; diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index 6832b86df..f2224ff13 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -11,13 +11,6 @@ - - - - <html><head/><body><p>This temporary page contains new settings that will be available in-game in a post-1.0 release of OpenMW.</p></body></html> - - - @@ -27,9 +20,9 @@ 0 - -187 + 0 630 - 510 + 746 @@ -266,6 +259,94 @@ + + + + Testing + + + + + + These settings are intended for testing mods and will cause issues if used for normal gameplay. + + + true + + + + + + + Qt::Horizontal + + + + + + + Skip menu and generate default character + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Start default character at + + + + + + + default cell + + + + + + + + + Run script after startup: + + + + + + + + + + + + Browse… + + + + + + + + diff --git a/files/ui/settingspage.ui b/files/ui/settingspage.ui index d0c77f4b7..f4f41f839 100644 --- a/files/ui/settingspage.ui +++ b/files/ui/settingspage.ui @@ -7,7 +7,7 @@ 0 0 514 - 532 + 397 @@ -15,247 +15,153 @@ - - - true + + + Morrowind Content Language - - - - 0 - -44 - 473 - 567 - - - - - - - Morrowind Content Language - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 250 - 0 - - - - - - - - - - - Morrowind Installation Wizard - - - - - - Run &Installation Wizard - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - Morrowind Settings Importer - - - - - - - - File to import settings from: - - - - - - - - - - Browse... - - - - - - - - - Import add-on and plugin selection (creates a new Content List) - - - true - - - - - - - - - Run &Settings Importer - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - 4 - - - false - - - - - - - - - - Testing - - - - - - These settings are intended for testing mods and will cause issues if used for normal gameplay. - - - true - - - - - - - Qt::Horizontal - - - - - - - Skip menu and generate default character - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - Start default character at - - - - - - - default cell - - - - - - - - - Run script after startup: - - - - - - - - - - - - Browse… - - - - - - - - - - + + + + + + 250 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Morrowind Installation Wizard + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Run &Installation Wizard + + + + + + + + + + Morrowind Settings Importer + + + + + + + + File to import settings from: + + + + + + + + + + Browse... + + + + + + + + + Import add-on and plugin selection (creates a new Content List) + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Run &Settings Importer + + + + + + + + + 4 + + + false + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + From fb27f34a32ed87ea757dd591af6526ede2604aa7 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sat, 7 Apr 2018 21:27:36 -0500 Subject: [PATCH 052/282] Add autocomplete to the "Start default character at" field --- apps/launcher/CMakeLists.txt | 3 ++ apps/launcher/advancedpage.cpp | 18 ++++++++++ apps/launcher/advancedpage.hpp | 6 ++++ apps/launcher/datafilespage.cpp | 11 ++++++ apps/launcher/datafilespage.hpp | 6 ++++ apps/launcher/maindialog.cpp | 3 ++ apps/launcher/utils/cellnameloader.cpp | 48 ++++++++++++++++++++++++++ apps/launcher/utils/cellnameloader.hpp | 41 ++++++++++++++++++++++ 8 files changed, 136 insertions(+) create mode 100644 apps/launcher/utils/cellnameloader.cpp create mode 100644 apps/launcher/utils/cellnameloader.hpp diff --git a/apps/launcher/CMakeLists.txt b/apps/launcher/CMakeLists.txt index aec8c2533..54d7c3ece 100644 --- a/apps/launcher/CMakeLists.txt +++ b/apps/launcher/CMakeLists.txt @@ -8,6 +8,7 @@ set(LAUNCHER settingspage.cpp advancedpage.cpp + utils/cellnameloader.cpp utils/profilescombobox.cpp utils/textinputdialog.cpp utils/lineedit.cpp @@ -24,6 +25,7 @@ set(LAUNCHER_HEADER settingspage.hpp advancedpage.hpp + utils/cellnameloader.hpp utils/profilescombobox.hpp utils/textinputdialog.hpp utils/lineedit.hpp @@ -39,6 +41,7 @@ set(LAUNCHER_HEADER_MOC settingspage.hpp advancedpage.hpp + utils/cellnameloader.hpp utils/textinputdialog.hpp utils/profilescombobox.hpp utils/lineedit.hpp diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index bc3308da0..bd077c12f 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -1,7 +1,12 @@ #include "advancedpage.hpp" #include +#include +#include #include +#include +#include +#include Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings, @@ -17,6 +22,19 @@ Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, loadSettings(); } +void Launcher::AdvancedPage::loadCellsForAutocomplete(QStringList filePaths) { + CellNameLoader cellNameLoader; + QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(filePaths)); + std::sort(cellNamesList.begin(), cellNamesList.end()); + + // Set up an auto-completer for the "Start default character at" field + auto *completer = new QCompleter(cellNamesList); + completer->setCompletionMode(QCompleter::PopupCompletion); + completer->setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); + startDefaultCharacterAtField->setCompleter(completer); + +} + void Launcher::AdvancedPage::on_skipMenuCheckBox_stateChanged(int state) { startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked); startDefaultCharacterAtField->setEnabled(state == Qt::Checked); diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp index 0a9957b9c..654214f29 100644 --- a/apps/launcher/advancedpage.hpp +++ b/apps/launcher/advancedpage.hpp @@ -23,6 +23,12 @@ namespace Launcher bool loadSettings(); void saveSettings(); + /** + * Load the cells associated with the given content files for use in autocomplete + * @param filePaths the file paths of the content files to be examined + */ + void loadCellsForAutocomplete(QStringList filePaths); + private slots: void on_skipMenuCheckBox_stateChanged(int state); void on_runScriptAfterStartupBrowseButton_clicked(); diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 0b0f8c75e..484c8c56b 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -142,6 +142,17 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile) mGameSettings.setContentList(fileNames); } +QStringList Launcher::DataFilesPage::selectedFilePaths() +{ + //retrieve the files selected for the profile + ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); + QStringList filePaths; + foreach(const ContentSelectorModel::EsmFile *item, items) { + filePaths.append(item->filePath()); + } + return filePaths; +} + void Launcher::DataFilesPage::removeProfile(const QString &profile) { mLauncherSettings.removeContentList(profile); diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index d25d20fc9..9955737d5 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -41,6 +41,12 @@ namespace Launcher void saveSettings(const QString &profile = ""); bool loadSettings(); + /** + * Returns the file paths of all selected content files + * @return the file paths of all selected content files + */ + QStringList selectedFilePaths(); + signals: void signalProfileChanged (int index); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 356863d03..9967cbddc 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -125,6 +125,9 @@ void Launcher::MainDialog::createPages() mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); mPlayPage->setProfilesIndex(mDataFilesPage->profilesIndex()); + // Load cells for the "Start default character at" field + mAdvancedPage->loadCellsForAutocomplete(mDataFilesPage->selectedFilePaths()); + // Add the pages to the stacked widget pagesWidget->addWidget(mPlayPage); pagesWidget->addWidget(mDataFilesPage); diff --git a/apps/launcher/utils/cellnameloader.cpp b/apps/launcher/utils/cellnameloader.cpp new file mode 100644 index 000000000..6d1ed2f49 --- /dev/null +++ b/apps/launcher/utils/cellnameloader.cpp @@ -0,0 +1,48 @@ +#include "cellnameloader.hpp" + +#include +#include + +QSet CellNameLoader::getCellNames(QStringList &contentPaths) +{ + QSet cellNames; + ESM::ESMReader esmReader; + + // Loop through all content files + for (auto &contentPath : contentPaths) { + esmReader.open(contentPath.toStdString()); + + // Loop through all records + while(esmReader.hasMoreRecs()) + { + ESM::NAME recordName = esmReader.getRecName(); + esmReader.getRecHeader(); + + if (isCellRecord(recordName)) { + QString cellName = getCellName(esmReader); + if (!cellName.isEmpty()) { + cellNames.insert(cellName); + } + } + + // Stop loading content for this record and continue to the next + esmReader.skipRecord(); + } + } + + return cellNames; +} + +bool CellNameLoader::isCellRecord(ESM::NAME &recordName) +{ + return recordName.intval == ESM::REC_CELL; +} + +QString CellNameLoader::getCellName(ESM::ESMReader &esmReader) +{ + ESM::Cell cell; + bool isDeleted = false; + cell.loadNameAndData(esmReader, isDeleted); + + return QString::fromStdString(cell.mName); +} \ No newline at end of file diff --git a/apps/launcher/utils/cellnameloader.hpp b/apps/launcher/utils/cellnameloader.hpp new file mode 100644 index 000000000..c58d09226 --- /dev/null +++ b/apps/launcher/utils/cellnameloader.hpp @@ -0,0 +1,41 @@ +#ifndef OPENMW_CELLNAMELOADER_H +#define OPENMW_CELLNAMELOADER_H + +#include +#include +#include + +#include + +namespace ESM {class ESMReader; struct Cell;} +namespace ContentSelectorView {class ContentSelector;} + +class CellNameLoader { + +public: + + /** + * Returns the names of all cells contained within the given content files + * @param contentPaths the file paths of each content file to be examined + * @return the names of all cells + */ + QSet getCellNames(QStringList &contentPaths); + +private: + /** + * Returns whether or not the given record is of type "Cell" + * @param name The name associated with the record + * @return whether or not the given record is of type "Cell" + */ + bool isCellRecord(ESM::NAME &name); + + /** + * Returns the name of the cell + * @param esmReader the reader currently pointed to a loaded cell + * @return the name of the cell + */ + QString getCellName(ESM::ESMReader &esmReader); +}; + + +#endif //OPENMW_CELLNAMELOADER_H From e32f38b939dbb8470a598d05a24f8bc310008a38 Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Wed, 21 Mar 2018 00:19:44 +0300 Subject: [PATCH 053/282] Allow jumping when you're stuck on a slope (fixes #4221) --- apps/openmw/mwphysics/physicssystem.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index f3c34bc4e..77e836789 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -308,6 +308,7 @@ namespace MWPhysics float swimlevel = waterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale); ActorTracer tracer; + osg::Vec3f inertia = physicActor->getInertialForce(); osg::Vec3f velocity; @@ -320,10 +321,11 @@ namespace MWPhysics { velocity = (osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * movement; - if (velocity.z() > 0.f && physicActor->getOnGround() && !physicActor->getOnSlope()) + if ((velocity.z() > 0.f && physicActor->getOnGround() && !physicActor->getOnSlope()) + || (velocity.z() > 0.f && velocity.z() + inertia.z() <= -velocity.z() && physicActor->getOnSlope())) inertia = velocity; - else if(!physicActor->getOnGround() || physicActor->getOnSlope()) - velocity = velocity + physicActor->getInertialForce(); + else if (!physicActor->getOnGround() || physicActor->getOnSlope()) + velocity = velocity + inertia; } // dead actors underwater will float to the surface, if the CharacterController tells us to do so From d58cce9c727dd6ddccea3bcee44338457615338f Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Tue, 22 May 2018 20:50:31 -0500 Subject: [PATCH 054/282] Adding WIP code to dynamically change the autocomplete fields --- apps/launcher/advancedpage.cpp | 5 +++++ apps/launcher/advancedpage.hpp | 3 +++ apps/launcher/datafilespage.cpp | 7 +++++++ apps/launcher/datafilespage.hpp | 3 +++ apps/launcher/maindialog.cpp | 1 + components/contentselector/view/contentselector.cpp | 13 +++++++++++++ components/contentselector/view/contentselector.hpp | 2 ++ 7 files changed, 34 insertions(+) diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index bd077c12f..383799e2a 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -162,4 +162,9 @@ void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::str bool cValue = checkbox->checkState(); if (cValue != mEngineSettings.getBool(setting, group)) mEngineSettings.setBool(setting, group, cValue); +} + +void Launcher::AdvancedPage::slotSelectedDataFilesChanged(QStringList selectedFiles) +{ + loadCellsForAutocomplete(selectedFiles); } \ No newline at end of file diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp index 654214f29..c40ae2da1 100644 --- a/apps/launcher/advancedpage.hpp +++ b/apps/launcher/advancedpage.hpp @@ -29,6 +29,9 @@ namespace Launcher */ void loadCellsForAutocomplete(QStringList filePaths); + public slots: + void slotSelectedDataFilesChanged(QStringList selectedFiles); + private slots: void on_skipMenuCheckBox_stateChanged(int state); void on_runScriptAfterStartupBrowseButton_clicked(); diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 484c8c56b..8d4d2422f 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -37,6 +37,8 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateOkButton(QString))); + connect(mSelector, SIGNAL(signalSelectedFilesChanged(QStringList)), + this, SLOT(slotSelectedFilesChanged(QStringList))); buildView(); loadSettings(); @@ -319,3 +321,8 @@ bool Launcher::DataFilesPage::showDeleteMessageBox (const QString &text) return (msgBox.clickedButton() == deleteButton); } + +void Launcher::DataFilesPage::slotSelectedFilesChanged(QStringList selectedFilesChanged) +{ + emit signalSelectedFilesChanged(selectedFilesChanged); +} \ No newline at end of file diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 9955737d5..fb9f9e04f 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -7,6 +7,7 @@ #include #include +#include class QSortFilterProxyModel; class QAbstractItemModel; @@ -49,6 +50,7 @@ namespace Launcher signals: void signalProfileChanged (int index); + void signalSelectedFilesChanged(QStringList selectedFiles); public slots: void slotProfileChanged (int index); @@ -58,6 +60,7 @@ namespace Launcher void slotProfileChangedByUser(const QString &previous, const QString ¤t); void slotProfileRenamed(const QString &previous, const QString ¤t); void slotProfileDeleted(const QString &item); + void slotSelectedFilesChanged (QStringList selectedFiles); void updateOkButton(const QString &text); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 9967cbddc..f3afb2732 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -142,6 +142,7 @@ void Launcher::MainDialog::createPages() connect(mPlayPage, SIGNAL(signalProfileChanged(int)), mDataFilesPage, SLOT(slotProfileChanged(int))); connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int))); + connect(mDataFilesPage, SIGNAL(signalSelectedFilesChanged(QStringList)), mAdvancedPage, SLOT(slotSelectedDataFilesChanged(QStringList))); } diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index f4da7e6ad..4798d160c 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -216,6 +216,8 @@ void ContentSelectorView::ContentSelector::slotShowContextMenu(const QPoint& pos { QPoint globalPos = ui.addonView->viewport()->mapToGlobal(pos); mContextMenu->exec(globalPos); + // TODO This is a temporary workaround to demonstrate that the selected files signal can be sent + emitSelectedFilesChanged(); } void ContentSelectorView::ContentSelector::setCheckStateForMultiSelectedItems(bool checked) @@ -240,3 +242,14 @@ void ContentSelectorView::ContentSelector::slotCheckMultiSelectedItems() { setCheckStateForMultiSelectedItems(true); } + +void ContentSelectorView::ContentSelector::emitSelectedFilesChanged() +{ + //retrieve the files selected for the profile + ContentSelectorModel::ContentFileList items = selectedFiles(); + QStringList filePaths; + foreach(const ContentSelectorModel::EsmFile *item, items) { + filePaths.append(item->filePath()); + } + emit signalSelectedFilesChanged(filePaths); +} \ No newline at end of file diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index 9f775d597..5015306ab 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -56,11 +56,13 @@ namespace ContentSelectorView void buildContextMenu(); void setGameFileSelected(int index, bool selected); void setCheckStateForMultiSelectedItems(bool checked); + void emitSelectedFilesChanged(); signals: void signalCurrentGamefileIndexChanged (int); void signalAddonDataChanged (const QModelIndex& topleft, const QModelIndex& bottomright); + void signalSelectedFilesChanged(QStringList selectedFiles); private slots: From c2fff61ccdc9c8397e7cae28f8c48bea5f6ab588 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sat, 26 May 2018 20:35:28 -0500 Subject: [PATCH 055/282] Changing so that data changes happen only after the addon is checked --- apps/launcher/datafilespage.cpp | 15 +++++++++++---- apps/launcher/datafilespage.hpp | 4 ++-- .../contentselector/view/contentselector.cpp | 13 ------------- .../contentselector/view/contentselector.hpp | 2 -- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 8d4d2422f..25a09ab1b 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -37,11 +37,14 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: connect(mProfileDialog->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(updateOkButton(QString))); - connect(mSelector, SIGNAL(signalSelectedFilesChanged(QStringList)), - this, SLOT(slotSelectedFilesChanged(QStringList))); buildView(); loadSettings(); + + // Connect signal and slot after the settings have been loaded. We only care about the user changing + // the addons and don't want to get signals of the system doing it during startup. + connect(mSelector, SIGNAL(signalAddonDataChanged(QModelIndex,QModelIndex)), + this, SLOT(slotAddonDataChanged())); } void Launcher::DataFilesPage::buildView() @@ -322,7 +325,11 @@ bool Launcher::DataFilesPage::showDeleteMessageBox (const QString &text) return (msgBox.clickedButton() == deleteButton); } -void Launcher::DataFilesPage::slotSelectedFilesChanged(QStringList selectedFilesChanged) +void Launcher::DataFilesPage::slotAddonDataChanged() { - emit signalSelectedFilesChanged(selectedFilesChanged); + QStringList selectedFiles = selectedFilePaths(); + if (previousSelectedFiles != selectedFiles) { + previousSelectedFiles = selectedFiles; + emit signalSelectedFilesChanged(selectedFiles); + } } \ No newline at end of file diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index fb9f9e04f..8893412be 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -60,7 +60,7 @@ namespace Launcher void slotProfileChangedByUser(const QString &previous, const QString ¤t); void slotProfileRenamed(const QString &previous, const QString ¤t); void slotProfileDeleted(const QString &item); - void slotSelectedFilesChanged (QStringList selectedFiles); + void slotAddonDataChanged (); void updateOkButton(const QString &text); @@ -81,7 +81,7 @@ namespace Launcher Config::LauncherSettings &mLauncherSettings; QString mPreviousProfile; - + QStringList previousSelectedFiles; QString mDataLocal; void setPluginsCheckstates(Qt::CheckState state); diff --git a/components/contentselector/view/contentselector.cpp b/components/contentselector/view/contentselector.cpp index 4798d160c..5e16064f4 100644 --- a/components/contentselector/view/contentselector.cpp +++ b/components/contentselector/view/contentselector.cpp @@ -216,8 +216,6 @@ void ContentSelectorView::ContentSelector::slotShowContextMenu(const QPoint& pos { QPoint globalPos = ui.addonView->viewport()->mapToGlobal(pos); mContextMenu->exec(globalPos); - // TODO This is a temporary workaround to demonstrate that the selected files signal can be sent - emitSelectedFilesChanged(); } void ContentSelectorView::ContentSelector::setCheckStateForMultiSelectedItems(bool checked) @@ -241,15 +239,4 @@ void ContentSelectorView::ContentSelector::slotUncheckMultiSelectedItems() void ContentSelectorView::ContentSelector::slotCheckMultiSelectedItems() { setCheckStateForMultiSelectedItems(true); -} - -void ContentSelectorView::ContentSelector::emitSelectedFilesChanged() -{ - //retrieve the files selected for the profile - ContentSelectorModel::ContentFileList items = selectedFiles(); - QStringList filePaths; - foreach(const ContentSelectorModel::EsmFile *item, items) { - filePaths.append(item->filePath()); - } - emit signalSelectedFilesChanged(filePaths); } \ No newline at end of file diff --git a/components/contentselector/view/contentselector.hpp b/components/contentselector/view/contentselector.hpp index 5015306ab..323f926ed 100644 --- a/components/contentselector/view/contentselector.hpp +++ b/components/contentselector/view/contentselector.hpp @@ -15,7 +15,6 @@ namespace ContentSelectorView Q_OBJECT QMenu *mContextMenu; - QStringList mFilePaths; protected: @@ -56,7 +55,6 @@ namespace ContentSelectorView void buildContextMenu(); void setGameFileSelected(int index, bool selected); void setCheckStateForMultiSelectedItems(bool checked); - void emitSelectedFilesChanged(); signals: void signalCurrentGamefileIndexChanged (int); From 78234e9468c2467a90e6a0bcf9101cebee76bb32 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sun, 27 May 2018 16:47:09 -0500 Subject: [PATCH 056/282] Moving autocomplete code to thread --- apps/launcher/advancedpage.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 383799e2a..bc0378314 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -166,5 +167,8 @@ void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::str void Launcher::AdvancedPage::slotSelectedDataFilesChanged(QStringList selectedFiles) { - loadCellsForAutocomplete(selectedFiles); + // Loading cells for core Morrowind + Expansions takes about 0.2 seconds, which is enough to cause a + // barely perceptible UI lag. Splitting into its own thread to alleviate that. + std::thread loadCellsThread(&AdvancedPage::loadCellsForAutocomplete, this, selectedFiles); + loadCellsThread.join(); } \ No newline at end of file From 26dfef797040e6e944495ab017c0c8c4e6770748 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sun, 27 May 2018 17:15:36 -0500 Subject: [PATCH 057/282] Changing where we are loading cells to prevent Qt access issue --- apps/launcher/advancedpage.cpp | 17 ++++------------- apps/launcher/advancedpage.hpp | 2 +- apps/launcher/datafilespage.cpp | 15 ++++++++++++++- apps/launcher/datafilespage.hpp | 1 + apps/launcher/maindialog.cpp | 2 +- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index bc0378314..3fe7c1f89 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -2,10 +2,8 @@ #include #include -#include #include #include -#include #include #include @@ -23,13 +21,9 @@ Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, loadSettings(); } -void Launcher::AdvancedPage::loadCellsForAutocomplete(QStringList filePaths) { - CellNameLoader cellNameLoader; - QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(filePaths)); - std::sort(cellNamesList.begin(), cellNamesList.end()); - +void Launcher::AdvancedPage::loadCellsForAutocomplete(QStringList cellNames) { // Set up an auto-completer for the "Start default character at" field - auto *completer = new QCompleter(cellNamesList); + auto *completer = new QCompleter(cellNames); completer->setCompletionMode(QCompleter::PopupCompletion); completer->setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); startDefaultCharacterAtField->setCompleter(completer); @@ -165,10 +159,7 @@ void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::str mEngineSettings.setBool(setting, group, cValue); } -void Launcher::AdvancedPage::slotSelectedDataFilesChanged(QStringList selectedFiles) +void Launcher::AdvancedPage::slotLoadedCellsChanged(QStringList cellNames) { - // Loading cells for core Morrowind + Expansions takes about 0.2 seconds, which is enough to cause a - // barely perceptible UI lag. Splitting into its own thread to alleviate that. - std::thread loadCellsThread(&AdvancedPage::loadCellsForAutocomplete, this, selectedFiles); - loadCellsThread.join(); + loadCellsForAutocomplete(cellNames); } \ No newline at end of file diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp index c40ae2da1..82b9c3b65 100644 --- a/apps/launcher/advancedpage.hpp +++ b/apps/launcher/advancedpage.hpp @@ -30,7 +30,7 @@ namespace Launcher void loadCellsForAutocomplete(QStringList filePaths); public slots: - void slotSelectedDataFilesChanged(QStringList selectedFiles); + void slotLoadedCellsChanged(QStringList cellNames); private slots: void on_skipMenuCheckBox_stateChanged(int state); diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 25a09ab1b..1f46c7156 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -7,7 +7,9 @@ #include #include #include +#include +#include #include #include @@ -330,6 +332,17 @@ void Launcher::DataFilesPage::slotAddonDataChanged() QStringList selectedFiles = selectedFilePaths(); if (previousSelectedFiles != selectedFiles) { previousSelectedFiles = selectedFiles; - emit signalSelectedFilesChanged(selectedFiles); + // Loading cells for core Morrowind + Expansions takes about 0.2 seconds, which is enough to cause a + // barely perceptible UI lag. Splitting into its own thread to alleviate that. + std::thread loadCellsThread(&DataFilesPage::reloadCells, this, selectedFiles); + loadCellsThread.join(); } +} + +void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles) +{ + CellNameLoader cellNameLoader; + QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(selectedFiles)); + std::sort(cellNamesList.begin(), cellNamesList.end()); + emit signalSelectedFilesChanged(cellNamesList); } \ No newline at end of file diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index 8893412be..d871eeee0 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -96,6 +96,7 @@ namespace Launcher void addProfile (const QString &profile, bool setAsCurrent); void checkForDefaultProfile(); void populateFileViews(const QString& contentModelName); + void reloadCells(QStringList selectedFiles); class PathIterator { diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index f3afb2732..79f3e5c67 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -142,7 +142,7 @@ void Launcher::MainDialog::createPages() connect(mPlayPage, SIGNAL(signalProfileChanged(int)), mDataFilesPage, SLOT(slotProfileChanged(int))); connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int))); - connect(mDataFilesPage, SIGNAL(signalSelectedFilesChanged(QStringList)), mAdvancedPage, SLOT(slotSelectedDataFilesChanged(QStringList))); + connect(mDataFilesPage, SIGNAL(signalSelectedFilesChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList))); } From e26c67582920709344036f54bb245086195408f7 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sat, 2 Jun 2018 17:29:35 -0500 Subject: [PATCH 058/282] Changing join to detach so that the thread will not block the UI --- apps/launcher/datafilespage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 1f46c7156..bcb2966d5 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -335,7 +335,7 @@ void Launcher::DataFilesPage::slotAddonDataChanged() // Loading cells for core Morrowind + Expansions takes about 0.2 seconds, which is enough to cause a // barely perceptible UI lag. Splitting into its own thread to alleviate that. std::thread loadCellsThread(&DataFilesPage::reloadCells, this, selectedFiles); - loadCellsThread.join(); + loadCellsThread.detach(); } } From 103a7ac62882d196b9fd12c9afa374f17a5e856f Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sun, 3 Jun 2018 16:32:12 -0500 Subject: [PATCH 059/282] Using a mutex lock to prevent race conditions --- apps/launcher/datafilespage.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index bcb2966d5..76ca893c4 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -339,8 +339,16 @@ void Launcher::DataFilesPage::slotAddonDataChanged() } } +// Mutex lock to run reloadCells synchronously. +std::mutex _reloadCellsMutex; + void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles) { + // Use a mutex lock so that we can prevent two threads from executing the rest of this code at the same time + // Based on https://stackoverflow.com/a/5429695/531762 + std::unique_lock lock(_reloadCellsMutex); + + // The following code will run only if there is not another thread currently running it CellNameLoader cellNameLoader; QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(selectedFiles)); std::sort(cellNamesList.begin(), cellNamesList.end()); From e282ece3d14bda73938cce38412a932d74826246 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sun, 3 Jun 2018 16:58:18 -0500 Subject: [PATCH 060/282] Fixing bug with autocomplete not loading correctly during startup --- apps/launcher/advancedpage.hpp | 11 +++++------ apps/launcher/datafilespage.cpp | 3 +++ apps/launcher/maindialog.cpp | 3 --- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/launcher/advancedpage.hpp b/apps/launcher/advancedpage.hpp index 82b9c3b65..59de3d319 100644 --- a/apps/launcher/advancedpage.hpp +++ b/apps/launcher/advancedpage.hpp @@ -23,12 +23,6 @@ namespace Launcher bool loadSettings(); void saveSettings(); - /** - * Load the cells associated with the given content files for use in autocomplete - * @param filePaths the file paths of the content files to be examined - */ - void loadCellsForAutocomplete(QStringList filePaths); - public slots: void slotLoadedCellsChanged(QStringList cellNames); @@ -41,6 +35,11 @@ namespace Launcher Config::GameSettings &mGameSettings; Settings::Manager &mEngineSettings; + /** + * Load the cells associated with the given content files for use in autocomplete + * @param filePaths the file paths of the content files to be examined + */ + void loadCellsForAutocomplete(QStringList filePaths); void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group); }; diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 76ca893c4..b3c8fb14a 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "utils/textinputdialog.hpp" #include "utils/profilescombobox.hpp" @@ -47,6 +48,8 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config: // the addons and don't want to get signals of the system doing it during startup. connect(mSelector, SIGNAL(signalAddonDataChanged(QModelIndex,QModelIndex)), this, SLOT(slotAddonDataChanged())); + // Call manually to indicate all changes to addon data during startup. + slotAddonDataChanged(); } void Launcher::DataFilesPage::buildView() diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 79f3e5c67..a70f6ff7f 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -125,9 +125,6 @@ void Launcher::MainDialog::createPages() mPlayPage->setProfilesModel(mDataFilesPage->profilesModel()); mPlayPage->setProfilesIndex(mDataFilesPage->profilesIndex()); - // Load cells for the "Start default character at" field - mAdvancedPage->loadCellsForAutocomplete(mDataFilesPage->selectedFilePaths()); - // Add the pages to the stacked widget pagesWidget->addWidget(mPlayPage); pagesWidget->addWidget(mDataFilesPage); From d46590934af75c265b6fd1894acbdc306b470c7d Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sun, 3 Jun 2018 16:59:27 -0500 Subject: [PATCH 061/282] Importing mutex --- apps/launcher/datafilespage.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index b3c8fb14a..6c23b3285 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include From ab433102a49057d8ae4eb7219674e75960313045 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 9 May 2018 10:31:53 +0400 Subject: [PATCH 062/282] Increase hit distance for player by halfExtents --- apps/openmw/mwworld/worldimp.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 13970ee13..61bed46f7 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1074,6 +1074,7 @@ namespace MWWorld osg::Quat rot = osg::Quat(posdata.rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(posdata.rot[2], osg::Vec3f(0,0,-1)); osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); + osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr); if (ptr == getPlayerPtr()) pos = getActorHeadTransform(ptr).getTrans(); // special cased for better aiming with the camera @@ -1082,11 +1083,12 @@ namespace MWWorld // general case, compatible with all types of different creatures // note: we intentionally do *not* use the collision box offset here, this is required to make // some flying creatures work that have their collision box offset in the air - osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr); pos.z() += halfExtents.z() * 2 * 0.75; - distance += halfExtents.y(); } + // the origin of hitbox is an actor's front, not center + distance += halfExtents.y(); + std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); if(result.first.isEmpty()) return std::make_pair(MWWorld::Ptr(), osg::Vec3f()); From f5dc9f01620306c147c1428a1bb39eee0c286acf Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 9 May 2018 10:53:53 +0400 Subject: [PATCH 063/282] Use hitbox cone only as fallback --- apps/openmw/mwphysics/physicssystem.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index f3c34bc4e..4830760af 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -846,6 +846,16 @@ namespace MWPhysics const osg::Quat &orient, float queryDistance, std::vector targets) { + // First of all, try to hit where you aim to + int hitmask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; + RayResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor, targets, 0xff, hitmask); + + if (result.mHit) + { + return std::make_pair(result.mHitObject, result.mHitPos); + } + + // Use cone shape as fallback const MWWorld::Store &store = MWBase::Environment::get().getWorld()->getStore().get(); btConeShape shape (osg::DegreesToRadians(store.find("fCombatAngleXY")->getFloat()/2.0f), queryDistance); From 9e5d577a71dab1fffec695121b400e9b4f675b93 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 9 May 2018 11:35:51 +0400 Subject: [PATCH 064/282] Aim from center of attacker to center of target --- apps/openmw/mwworld/worldimp.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 61bed46f7..586fc7816 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1083,7 +1083,7 @@ namespace MWWorld // general case, compatible with all types of different creatures // note: we intentionally do *not* use the collision box offset here, this is required to make // some flying creatures work that have their collision box offset in the air - pos.z() += halfExtents.z() * 2 * 0.75; + pos.z() += halfExtents.z(); } // the origin of hitbox is an actor's front, not center @@ -3468,7 +3468,7 @@ namespace MWWorld osg::Vec3f World::aimToTarget(const ConstPtr &actor, const MWWorld::ConstPtr& target) { osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3(); - weaponPos.z() += mPhysics->getHalfExtents(actor).z() * 2 * 0.75; + weaponPos.z() += mPhysics->getHalfExtents(actor).z(); osg::Vec3f targetPos = mPhysics->getCollisionObjectPosition(target); return (targetPos - weaponPos); } @@ -3477,7 +3477,7 @@ namespace MWWorld { osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3(); osg::Vec3f halfExtents = mPhysics->getHalfExtents(actor); - weaponPos.z() += halfExtents.z() * 2 * 0.75; + weaponPos.z() += halfExtents.z(); return mPhysics->getHitDistance(weaponPos, target) - halfExtents.y(); } From 4666a6a0abc221806fda56c85c5d5b910ef5f51d Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 9 May 2018 12:53:58 +0400 Subject: [PATCH 065/282] Use default hit formula as fallback --- apps/openmw/mwworld/worldimp.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 586fc7816..8423adedc 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1073,21 +1073,28 @@ namespace MWWorld osg::Quat rot = osg::Quat(posdata.rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(posdata.rot[2], osg::Vec3f(0,0,-1)); - osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr); + // the origin of hitbox is an actor's front, not center + distance += halfExtents.y(); + + // special cased for better aiming with the camera + // if we do not hit anything, will use the default approach as fallback if (ptr == getPlayerPtr()) - pos = getActorHeadTransform(ptr).getTrans(); // special cased for better aiming with the camera - else { - // general case, compatible with all types of different creatures - // note: we intentionally do *not* use the collision box offset here, this is required to make - // some flying creatures work that have their collision box offset in the air - pos.z() += halfExtents.z(); + osg::Vec3f pos = getActorHeadTransform(ptr).getTrans(); + + std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); + if(!result.first.isEmpty()) + return std::make_pair(result.first, result.second); } - // the origin of hitbox is an actor's front, not center - distance += halfExtents.y(); + osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); + + // general case, compatible with all types of different creatures + // note: we intentionally do *not* use the collision box offset here, this is required to make + // some flying creatures work that have their collision box offset in the air + pos.z() += halfExtents.z(); std::pair result = mPhysics->getHitContact(ptr, pos, rot, distance, targets); if(result.first.isEmpty()) From bde1d07d4eb756da90f31698aa154920fa102cd1 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 15 May 2018 18:57:36 +0400 Subject: [PATCH 066/282] Use hitboxes and focused object for touch spells (bug #3374) --- apps/openmw/mwphysics/physicssystem.cpp | 2 +- apps/openmw/mwworld/worldimp.cpp | 94 +++++++++++++------------ 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/apps/openmw/mwphysics/physicssystem.cpp b/apps/openmw/mwphysics/physicssystem.cpp index 4830760af..625ae5573 100644 --- a/apps/openmw/mwphysics/physicssystem.cpp +++ b/apps/openmw/mwphysics/physicssystem.cpp @@ -848,7 +848,7 @@ namespace MWPhysics { // First of all, try to hit where you aim to int hitmask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor; - RayResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor, targets, 0xff, hitmask); + RayResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor, targets, CollisionType_Actor, hitmask); if (result.mHit) { diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 8423adedc..cbd5b9340 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -2761,64 +2761,66 @@ namespace MWWorld { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); - // Get the target to use for "on touch" effects, using the facing direction from Head node - MWWorld::Ptr target; - float distance = getActivationDistancePlusTelekinesis(); - - osg::Vec3f hitPosition = actor.getRefData().getPosition().asVec3(); - osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); - - osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) - * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); - - osg::Vec3f direction = orient * osg::Vec3f(0,1,0); - osg::Vec3f dest = origin + direction * distance; - // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; if (!actor.isEmpty() && actor != MWMechanics::getPlayer()) actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targetActors); - // For actor targets, we want to use bounding boxes (physics raycast). - // This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise. - // For object targets, we want the detailed shapes (rendering raycast). - // If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf. + const float fCombatDistance = getStore().get().find("fCombatDistance")->getFloat(); - MWPhysics::PhysicsSystem::RayResult result1 = mPhysics->castRay(origin, dest, actor, targetActors, MWPhysics::CollisionType_Actor); - - MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); + osg::Vec3f hitPosition = actor.getRefData().getPosition().asVec3(); - float dist1 = std::numeric_limits::max(); - float dist2 = std::numeric_limits::max(); + // for player we can take faced object first + MWWorld::Ptr target; + if (actor == MWMechanics::getPlayer()) + target = getFacedObject(); - if (result1.mHit) - dist1 = (origin - result1.mHitPos).length(); - if (result2.mHit) - dist2 = (origin - result2.mHitPointWorld).length(); + // if the faced object can not be activated, do not use it + if (!target.isEmpty() && !target.getClass().canBeActivated(target)) + target = NULL; - if (result1.mHit) - { - target = result1.mHitObject; - hitPosition = result1.mHitPos; - if (dist1 > getMaxActivationDistance() && !target.isEmpty() && (target.getClass().isActor() || !target.getClass().canBeActivated(target))) - target = NULL; - } - else if (result2.mHit) + if (target.isEmpty()) { - target = result2.mHitObject; - hitPosition = result2.mHitPointWorld; - if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target)) - target = NULL; - } + // For actor targets, we want to use hit contact with bounding boxes. + // This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise. + // For object targets, we want the detailed shapes (rendering raycast). + // If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf. + std::pair result1 = getHitContact(actor, fCombatDistance, targetActors); - // When targeting an actor that is in combat with an "on touch" spell, - // compare against the minimum of activation distance and combat distance. + // Get the target to use for "on touch" effects, using the facing direction from Head node + osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); - if (!target.isEmpty() && target.getClass().isActor() && target.getClass().getCreatureStats (target).getAiSequence().isInCombat()) - { - distance = std::min (distance, getStore().get().find("fCombatDistance")->getFloat()); - if (distance < dist1) - target = NULL; + osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) + * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); + + osg::Vec3f direction = orient * osg::Vec3f(0,1,0); + float distance = getMaxActivationDistance(); + osg::Vec3f dest = origin + direction * distance; + + MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); + + float dist1 = std::numeric_limits::max(); + float dist2 = std::numeric_limits::max(); + + if (!result1.first.isEmpty() && result1.first.getClass().isActor()) + dist1 = (origin - result1.second).length(); + if (result2.mHit) + dist2 = (origin - result2.mHitPointWorld).length(); + + if (!result1.first.isEmpty() && result1.first.getClass().isActor()) + { + target = result1.first; + hitPosition = result1.second; + if (dist1 > getMaxActivationDistance()) + target = NULL; + } + else if (result2.mHit) + { + target = result2.mHitObject; + hitPosition = result2.mHitPointWorld; + if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target)) + target = NULL; + } } std::string selectedSpell = stats.getSpells().getSelectedSpell(); From 1b9edbe119a16bb132d75380b62e3f4e0d02597c Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 2 May 2018 14:13:49 +0400 Subject: [PATCH 067/282] Add unequip animation during stance switching (bug #4327) --- apps/openmw/mwmechanics/character.cpp | 93 +++++++++++++++++---------- apps/openmw/mwmechanics/character.hpp | 1 + 2 files changed, 61 insertions(+), 33 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 68dc17915..a0aad8b1b 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -749,6 +749,7 @@ void CharacterController::playRandomDeath(float startpoint) CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim) : mPtr(ptr) + , mWeapon(MWWorld::Ptr()) , mAnimation(anim) , mIdleState(CharState_None) , mMovementState(CharState_None) @@ -1156,17 +1157,26 @@ bool CharacterController::updateWeaponState() const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf(); - std::string soundid; + std::string upSoundId; + std::string downSoundId; if (mPtr.getClass().hasInventoryStore(mPtr)) { MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); - MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); - if(weapon != inv.end() && !(weaptype == WeapType_None && mWeaponType == WeapType_Spell)) - { - soundid = (weaptype == WeapType_None) ? - weapon->getClass().getDownSoundId(*weapon) : - weapon->getClass().getUpSoundId(*weapon); - } + MWWorld::ContainerStoreIterator weapon = getActiveWeapon(stats, inv, &weaptype); + if(stats.getDrawState() == DrawState_Spell) + weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + + if(weapon != inv.end() && mWeaponType != WeapType_HandToHand && weaptype > WeapType_HandToHand && weaptype < WeapType_Spell) + upSoundId = weapon->getClass().getUpSoundId(*weapon); + + if(weapon != inv.end() && mWeaponType > WeapType_HandToHand && mWeaponType < WeapType_Spell) + downSoundId = weapon->getClass().getDownSoundId(*weapon); + + // weapon->HtH switch: weapon is empty already, so we need to take sound from previous weapon + if(weapon == inv.end() && !mWeapon.isEmpty() && weaptype == WeapType_HandToHand && mWeaponType != WeapType_Spell) + downSoundId = mWeapon.getClass().getDownSoundId(mWeapon); + + mWeapon = weapon != inv.end() ? *weapon : MWWorld::Ptr(); } MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); @@ -1181,34 +1191,51 @@ bool CharacterController::updateWeaponState() if(weaptype != mWeaponType && !isKnockedOut() && !isKnockedDown() && !isRecovery()) { - forcestateupdate = true; - - mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); - std::string weapgroup; - if(weaptype == WeapType_None) + if ((!isWerewolf || mWeaponType != WeapType_Spell) + && mUpperBodyState != UpperCharState_UnEquipingWeap + && !isStillWeapon) { - if (!isWerewolf || mWeaponType != WeapType_Spell) + // Note: we do not disable unequipping animation automatically to avoid body desync + getWeaponGroup(mWeaponType, weapgroup); + mAnimation->play(weapgroup, priorityWeapon, + MWRender::Animation::BlendMask_All, false, + 1.0f, "unequip start", "unequip stop", 0.0f, 0); + mUpperBodyState = UpperCharState_UnEquipingWeap; + + if(!downSoundId.empty()) { - getWeaponGroup(mWeaponType, weapgroup); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - 1.0f, "unequip start", "unequip stop", 0.0f, 0); - mUpperBodyState = UpperCharState_UnEquipingWeap; + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mPtr, downSoundId, 1.0f, 1.0f); } } - else + + float complete; + bool animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); + if (!animPlaying || complete >= 1.0f) { + mUpperBodyState = UpperCharState_Nothing; + forcestateupdate = true; + mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); + getWeaponGroup(weaptype, weapgroup); mAnimation->setWeaponGroup(weapgroup); if (!isStillWeapon) { - mAnimation->showWeapons(false); - mAnimation->play(weapgroup, priorityWeapon, - MWRender::Animation::BlendMask_All, true, - 1.0f, "equip start", "equip stop", 0.0f, 0); - mUpperBodyState = UpperCharState_EquipingWeap; + if (weaptype == WeapType_None) + { + // Disable current weapon animation manually + mAnimation->disable(mCurrentWeapon); + } + else + { + mAnimation->showWeapons(false); + mAnimation->play(weapgroup, priorityWeapon, + MWRender::Animation::BlendMask_All, true, + 1.0f, "equip start", "equip stop", 0.0f, 0); + mUpperBodyState = UpperCharState_EquipingWeap; + } } if(isWerewolf) @@ -1221,16 +1248,16 @@ bool CharacterController::updateWeaponState() sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f); } } - } - if(!soundid.empty() && !isStillWeapon) - { - MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); - sndMgr->playSound3D(mPtr, soundid, 1.0f, 1.0f); - } + mWeaponType = weaptype; + getWeaponGroup(mWeaponType, mCurrentWeapon); - mWeaponType = weaptype; - getWeaponGroup(mWeaponType, mCurrentWeapon); + if(!upSoundId.empty() && !isStillWeapon) + { + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f); + } + } } if(isWerewolf) diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index cab51b82f..a172620b9 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -152,6 +152,7 @@ struct WeaponInfo; class CharacterController : public MWRender::Animation::TextKeyListener { MWWorld::Ptr mPtr; + MWWorld::Ptr mWeapon; MWRender::Animation *mAnimation; struct AnimationQueueEntry From 6eb531c6ace5411767ede50d4f1eec478a927533 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 7 Jun 2018 16:22:52 +0400 Subject: [PATCH 068/282] Add missing changelog entries --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbef559d..3b38b6bde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ 0.45.0 ------ + Bug #2835: Player able to slowly move when overencumbered Bug #4293: Faction members are not aware of faction ownerships in barter + Bug #4327: Missing animations during spell/weapon stance switching Bug #4426: RotateWorld behavior is incorrect Bug #4433: Guard behaviour is incorrect with Alarm = 0 From 05026b891ea52b06d056e080cfb4e264c71a9ce9 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 7 Jun 2018 20:42:18 +0400 Subject: [PATCH 069/282] Add changelog entries --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbef559d..30b2af930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ 0.45.0 ------ + Bug #3374: Touch spells not hitting kwama foragers + Bug #3591: Angled hit distance too low Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4426: RotateWorld behavior is incorrect Bug #4433: Guard behaviour is incorrect with Alarm = 0 From 1a354f88acace854cc7cbb6c04e0cf8c57cce4fc Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Thu, 7 Jun 2018 22:05:55 +0300 Subject: [PATCH 070/282] Make choices trigger goodbye if Goodbye is used (fixes #3897) --- CHANGELOG.md | 1 + apps/openmw/mwgui/dialogue.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbef559d..0fbc5092f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 0.45.0 ------ + Feature #3897: Have Goodbye give all choices the effects of Goodbye Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4426: RotateWorld behavior is incorrect Bug #4433: Guard behaviour is incorrect with Alarm = 0 diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 14bbe81ef..450799f29 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -635,6 +635,11 @@ namespace MWGui void DialogueWindow::onChoiceActivated(int id) { + if (mGoodbye) + { + onGoodbyeActivated(); + return; + } MWBase::Environment::get().getDialogueManager()->questionAnswered(id, mCallback.get()); updateTopics(); } From 7cafec9861eea5b44effdc3feb31ebdaec69e310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric?= Date: Thu, 7 Jun 2018 15:30:10 +0200 Subject: [PATCH 071/282] Add support for msvc with cmake version pre 3.9 (fixes #4429) --- cmake/OpenMWMacros.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/OpenMWMacros.cmake b/cmake/OpenMWMacros.cmake index fe2837a09..2fa86094f 100644 --- a/cmake/OpenMWMacros.cmake +++ b/cmake/OpenMWMacros.cmake @@ -172,7 +172,7 @@ macro (get_generator_is_multi_config VALUE) if (CMAKE_VERSION VERSION_GREATER 3.9 OR CMAKE_VERSION VERSION_EQUAL 3.9) get_cmake_property(${VALUE} GENERATOR_IS_MULTI_CONFIG) else (CMAKE_VERSION VERSION_GREATER 3.9 OR CMAKE_VERSION VERSION_EQUAL 3.9) - list(LENGTH "${CMAKE_CONFIGURATION_TYPES}" ${VALUE}) + list(LENGTH CMAKE_CONFIGURATION_TYPES ${VALUE}) endif (CMAKE_VERSION VERSION_GREATER 3.9 OR CMAKE_VERSION VERSION_EQUAL 3.9) endif (DEFINED generator_is_multi_config_var) endmacro (get_generator_is_multi_config) From 4a9b790dbea1023290e6e31a7ee69015661b385c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric?= Date: Fri, 8 Jun 2018 06:36:11 +0200 Subject: [PATCH 072/282] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbef559d..d423e1ca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ------ Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4426: RotateWorld behavior is incorrect + Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4433: Guard behaviour is incorrect with Alarm = 0 0.44.0 From b784c7873da469f457a1219bea30af772acf2dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric?= Date: Fri, 8 Jun 2018 06:36:43 +0200 Subject: [PATCH 073/282] Update authors --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 17f11730d..faa59670f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -37,6 +37,7 @@ Programmers Britt Mathis (galdor557) Capostrophic cc9cii + Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) Cory F. Cohen (cfcohen) From 53e888236636d544bdc15353546aa102672bd6ca Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Fri, 8 Jun 2018 11:44:49 +0200 Subject: [PATCH 074/282] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbef559d..d88fce1d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 0.45.0 ------ + Bug #4221: Characters get stuck in V-shaped terrain Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4426: RotateWorld behavior is incorrect Bug #4433: Guard behaviour is incorrect with Alarm = 0 From 2fada9487927be1932e0eb1a134ce69473b62a81 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 8 Jun 2018 15:44:35 +0400 Subject: [PATCH 075/282] Improve MRK NiStringExtraData handling (bug #4419) --- CHANGELOG.md | 1 + components/nifosg/nifloader.cpp | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d88fce1d0..08c81f60a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ------ Bug #4221: Characters get stuck in V-shaped terrain Bug #4293: Faction members are not aware of faction ownerships in barter + Bug #4419: MRK NiStringExtraData is handled incorrectly Bug #4426: RotateWorld behavior is incorrect Bug #4433: Guard behaviour is incorrect with Alarm = 0 diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 0add92f29..aada21fce 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -261,7 +261,7 @@ namespace NifOsg osg::ref_ptr textkeys (new TextKeyMapHolder); - osg::ref_ptr created = handleNode(nifNode, NULL, imageManager, std::vector(), 0, false, false, &textkeys->mTextKeys); + osg::ref_ptr created = handleNode(nifNode, NULL, imageManager, std::vector(), 0, false, false, false, &textkeys->mTextKeys); if (nif->getUseSkinning()) { @@ -463,7 +463,7 @@ namespace NifOsg } osg::ref_ptr handleNode(const Nif::Node* nifNode, osg::Group* parentNode, Resource::ImageManager* imageManager, - std::vector boundTextures, int animflags, bool skipMeshes, bool isAnimated, TextKeyMap* textKeys, osg::Node* rootNode=NULL) + std::vector boundTextures, int animflags, bool skipMeshes, bool hasMarkers, bool isAnimated, TextKeyMap* textKeys, osg::Node* rootNode=NULL) { if (rootNode != NULL && Misc::StringUtils::ciEqual(nifNode->name, "Bounding Box")) return NULL; @@ -510,7 +510,7 @@ namespace NifOsg if(sd->string == "MRK" && !Loader::getShowMarkers()) { // Marker objects. These meshes are only visible in the editor. - skipMeshes = true; + hasMarkers = true; } } } @@ -542,7 +542,7 @@ namespace NifOsg node->setNodeMask(0x1); } - if (skipMeshes && isAnimated) // make sure the empty node is not optimized away so the physicssystem can find it. + if ((skipMeshes || hasMarkers) && isAnimated) // make sure the empty node is not optimized away so the physicssystem can find it. { node->setDataVariance(osg::Object::DYNAMIC); } @@ -554,13 +554,18 @@ namespace NifOsg if (nifNode->recType == Nif::RC_NiTriShape && !skipMeshes) { const Nif::NiTriShape* triShape = static_cast(nifNode); - if (triShape->skin.empty()) - handleTriShape(triShape, node, composite, boundTextures, animflags); - else - handleSkinnedTriShape(triShape, node, composite, boundTextures, animflags); + const std::string nodeName = Misc::StringUtils::lowerCase(triShape->name); + static const std::string pattern = "tri editormarker"; + if (!hasMarkers || nodeName.compare(0, pattern.size(), pattern) != 0) + { + if (triShape->skin.empty()) + handleTriShape(triShape, node, composite, boundTextures, animflags); + else + handleSkinnedTriShape(triShape, node, composite, boundTextures, animflags); - if (!nifNode->controller.empty()) - handleMeshControllers(nifNode, node, composite, boundTextures, animflags); + if (!nifNode->controller.empty()) + handleMeshControllers(nifNode, node, composite, boundTextures, animflags); + } } if(nifNode->recType == Nif::RC_NiAutoNormalParticles || nifNode->recType == Nif::RC_NiRotatingParticles) @@ -598,7 +603,7 @@ namespace NifOsg for(size_t i = 0;i < children.length();++i) { if(!children[i].empty()) - handleNode(children[i].getPtr(), node, imageManager, boundTextures, animflags, skipMeshes, isAnimated, textKeys, rootNode); + handleNode(children[i].getPtr(), node, imageManager, boundTextures, animflags, skipMeshes, hasMarkers, isAnimated, textKeys, rootNode); } } From 11103211c5e6a875e8d18743df71875b3dff087a Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Fri, 8 Jun 2018 14:56:09 +0300 Subject: [PATCH 076/282] Make Goodbye and Choice choices mutually exclusive --- apps/openmw/mwdialogue/dialoguemanagerimp.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index de9ca83ca..da6e80e79 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -431,9 +431,11 @@ namespace MWDialogue void DialogueManager::addChoice (const std::string& text, int choice) { - mIsInChoice = true; - - mChoices.push_back(std::make_pair(text, choice)); + if (!mGoodbye) + { + mIsInChoice = true; + mChoices.push_back(std::make_pair(text, choice)); + } } const std::vector >& DialogueManager::getChoices() @@ -448,8 +450,8 @@ namespace MWDialogue void DialogueManager::goodbye() { - mIsInChoice = false; - mGoodbye = true; + if (!mIsInChoice) + mGoodbye = true; } void DialogueManager::persuade(int type, ResponseCallback* callback) From 0db702dfa78ca04423bafbe8d8243695206a9d10 Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Fri, 8 Jun 2018 15:05:00 +0300 Subject: [PATCH 077/282] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d88fce1d0..35d8ac4de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4426: RotateWorld behavior is incorrect Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Bug #4443: Goodbye option and dialogue choices are not mutually exclusive 0.44.0 ------ From fea34bd73f8302071d248f6f2fb199eb7afd3269 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 30 May 2018 08:54:43 +0400 Subject: [PATCH 078/282] Added support for per-group animation files --- CHANGELOG.md | 1 + apps/openmw/mwrender/animation.cpp | 41 +++++++++++++++++++ apps/openmw/mwrender/animation.hpp | 5 +++ .../reference/modding/settings/game.rst | 13 ++++++ files/settings-default.cfg | 3 ++ 5 files changed, 63 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d88fce1d0..ff3feec75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4426: RotateWorld behavior is incorrect Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Feature #4444: Per-group KF-animation files support 0.44.0 ------ diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 1bd839ead..3ccc06665 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -31,6 +31,8 @@ #include #include +#include + #include #include "../mwbase/environment.hpp" @@ -466,6 +468,8 @@ namespace MWRender mAnimationTimePtr[i].reset(new AnimationTime); mLightListCallback = new SceneUtil::LightListCallback; + + mUseAdditionalSources = Settings::Manager::getBool ("use additional anim sources", "Game"); } Animation::~Animation() @@ -536,6 +540,35 @@ namespace MWRender return mKeyframes->mTextKeys; } + void Animation::loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel) + { + const std::map& index = mResourceSystem->getVFS()->getIndex(); + + std::string animationPath = model; + if (animationPath.find("meshes") == 0) + { + animationPath.replace(0, 6, "animations"); + } + animationPath.replace(animationPath.size()-3, 3, "/"); + + mResourceSystem->getVFS()->normalizeFilename(animationPath); + + std::map::const_iterator found = index.lower_bound(animationPath); + while (found != index.end()) + { + const std::string& name = found->first; + if (name.size() >= animationPath.size() && name.substr(0, animationPath.size()) == animationPath) + { + size_t pos = name.find_last_of('.'); + if (pos != std::string::npos && name.compare(pos, name.size()-pos, ".kf") == 0) + addSingleAnimSource(name, baseModel); + } + else + break; + ++found; + } + } + void Animation::addAnimSource(const std::string &model, const std::string& baseModel) { std::string kfname = model; @@ -546,6 +579,14 @@ namespace MWRender else return; + addSingleAnimSource(kfname, baseModel); + + if (mUseAdditionalSources) + loadAllAnimationsInFolder(kfname, baseModel); + } + + void Animation::addSingleAnimSource(const std::string &kfname, const std::string& baseModel) + { if(!mResourceSystem->getVFS()->exists(kfname)) return; diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index cff98a4b7..43a626899 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -275,6 +275,8 @@ protected: osg::ref_ptr mLightListCallback; + bool mUseAdditionalSources; + const NodeMap& getNodeMap() const; /* Sets the appropriate animations on the bone groups based on priority. @@ -309,12 +311,15 @@ protected: */ void setObjectRoot(const std::string &model, bool forceskeleton, bool baseonly, bool isCreature); + void loadAllAnimationsInFolder(const std::string &model, const std::string &baseModel); + /** Adds the keyframe controllers in the specified model as a new animation source. * @note Later added animation sources have the highest priority when it comes to finding a particular animation. * @param model The file to add the keyframes for. Note that the .nif file extension will be replaced with .kf. * @param baseModel The filename of the mObjectRoot, only used for error messages. */ void addAnimSource(const std::string &model, const std::string& baseModel); + void addSingleAnimSource(const std::string &model, const std::string& baseModel); /** Adds an additional light to the given node using the specified ESM record. */ void addExtraLight(osg::ref_ptr parent, const ESM::Light *light); diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 7a9b89295..416f1bc1a 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -157,3 +157,16 @@ Otherwise they wait for the enemies or the player to do an attack first. Please note this setting has not been extensively tested and could have side effects with certain quests. This setting can only be configured by editing the settings configuration file. + +use additional anim sources +--------------------------- + +:Type: boolean +:Range: True/False +:Default: False + +Allow to load additional animation sources when enabled. +For example, if the main animation mesh has name Meshes/x.nif, an engine will load all KF-files from Animations/x folder and its child folders. +Can be useful if you want to use several animation replacers without merging them. +Attention: animations from AnimKit have own format and are not supposed to be directly loaded in-game! +This setting can only be configured by editing the settings configuration file. diff --git a/files/settings-default.cfg b/files/settings-default.cfg index b10b91eb1..09283bfc9 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -219,6 +219,9 @@ can loot during death animation = true # Makes the value of filled soul gems dependent only on soul magnitude (with formula from the Morrowind Code Patch) rebalance soul gem values = false +# Allow to load per-group KF-files from Animations folder +use additional anim sources = false + [General] # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). From de5a3eaae9d55c8f4046426c349253a844926c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric?= Date: Fri, 8 Jun 2018 17:08:44 +0200 Subject: [PATCH 079/282] Fix indentation issue: replace tab by spaces --- AUTHORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index faa59670f..b029140c7 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -37,7 +37,7 @@ Programmers Britt Mathis (galdor557) Capostrophic cc9cii - Cédric Mocquillon + Cédric Mocquillon Chris Boyce (slothlife) Chris Robinson (KittyCat) Cory F. Cohen (cfcohen) From fed10e87aa10bf82b6e193c4df8eda90e10c7ac9 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 21 Nov 2017 20:00:51 +0400 Subject: [PATCH 080/282] Store integer actor ID in AI packages (bug #4036) --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/aicombat.cpp | 7 +-- apps/openmw/mwmechanics/aicombat.hpp | 3 -- apps/openmw/mwmechanics/aiescort.cpp | 22 ++++---- apps/openmw/mwmechanics/aiescort.hpp | 6 +-- apps/openmw/mwmechanics/aifollow.cpp | 67 ++++++++++++------------ apps/openmw/mwmechanics/aifollow.hpp | 11 ++-- apps/openmw/mwmechanics/aipackage.cpp | 25 ++++++++- apps/openmw/mwmechanics/aipackage.hpp | 3 ++ apps/openmw/mwmechanics/aipursue.cpp | 4 +- apps/openmw/mwmechanics/aipursue.hpp | 4 -- apps/openmw/mwmechanics/spellcasting.cpp | 2 +- apps/openmw/mwmechanics/summoning.cpp | 2 +- components/esm/aisequence.cpp | 4 ++ components/esm/aisequence.hpp | 2 + components/esm/savedgame.cpp | 2 +- 16 files changed, 93 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc2a9acc1..62a011859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ 0.45.0 ------ Bug #2835: Player able to slowly move when overencumbered + Bug #4036: Weird behaviour of AI packages if package target has non-unique ID Bug #4221: Characters get stuck in V-shaped terrain Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4327: Missing animations during spell/weapon stance switching diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 83ebc67d9..06248fa10 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -103,9 +103,10 @@ namespace MWMechanics bool isFleeing(); }; - AiCombat::AiCombat(const MWWorld::Ptr& actor) : - mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()) - {} + AiCombat::AiCombat(const MWWorld::Ptr& actor) + { + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); + } AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat) { diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index e7d74248b..6e1f0c623 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -54,9 +54,6 @@ namespace MWMechanics virtual bool shouldCancelPreviousAi() const { return false; } private: - - int mTargetActorId; - /// Returns true if combat should end bool attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController); diff --git a/apps/openmw/mwmechanics/aiescort.cpp b/apps/openmw/mwmechanics/aiescort.cpp index 103ef32e1..a86d13d75 100644 --- a/apps/openmw/mwmechanics/aiescort.cpp +++ b/apps/openmw/mwmechanics/aiescort.cpp @@ -22,28 +22,32 @@ namespace MWMechanics { AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z) - : mActorId(actorId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) + : mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { + mTargetActorRefId = actorId; mMaxDist = 450; } - AiEscort::AiEscort(const std::string &actorId, const std::string &cellId,int duration, float x, float y, float z) - : mActorId(actorId), mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) + AiEscort::AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z) + : mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast(duration)) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { + mTargetActorRefId = actorId; mMaxDist = 450; } AiEscort::AiEscort(const ESM::AiSequence::AiEscort *escort) - : mActorId(escort->mTargetId), mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) + : mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ) , mMaxDist(450) , mRemainingDuration(escort->mRemainingDuration) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) { + mTargetActorRefId = escort->mTargetId; + mTargetActorId = escort->mTargetActorId; // mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration. // The exact value of mDuration only matters for repeating packages. if (mRemainingDuration > 0) // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. @@ -78,7 +82,7 @@ namespace MWMechanics actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false); - const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mActorId, false); + const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mTargetActorRefId, false); const float* const leaderPos = actor.getRefData().getPosition().pos; const float* const followerPos = follower.getRefData().getPosition().pos; double differenceBetween[3]; @@ -119,18 +123,14 @@ namespace MWMechanics return TypeIdEscort; } - MWWorld::Ptr AiEscort::getTarget() const - { - return MWBase::Environment::get().getWorld()->getPtr(mActorId, false); - } - void AiEscort::writeState(ESM::AiSequence::AiSequence &sequence) const { std::unique_ptr escort(new ESM::AiSequence::AiEscort()); escort->mData.mX = mX; escort->mData.mY = mY; escort->mData.mZ = mZ; - escort->mTargetId = mActorId; + escort->mTargetId = mTargetActorRefId; + escort->mTargetActorId = mTargetActorId; escort->mRemainingDuration = mRemainingDuration; escort->mCellId = mCellId; diff --git a/apps/openmw/mwmechanics/aiescort.hpp b/apps/openmw/mwmechanics/aiescort.hpp index 719582271..82dba960e 100644 --- a/apps/openmw/mwmechanics/aiescort.hpp +++ b/apps/openmw/mwmechanics/aiescort.hpp @@ -24,11 +24,11 @@ namespace MWMechanics /// Implementation of AiEscort /** The Actor will escort the specified actor to the world position x, y, z until they reach their position, or they run out of time \implement AiEscort **/ - AiEscort(const std::string &actorId,int duration, float x, float y, float z); + AiEscort(const std::string &actorId, int duration, float x, float y, float z); /// Implementation of AiEscortCell /** The Actor will escort the specified actor to the cell position x, y, z until they reach their position, or they run out of time \implement AiEscortCell **/ - AiEscort(const std::string &actorId,const std::string &cellId,int duration, float x, float y, float z); + AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z); AiEscort(const ESM::AiSequence::AiEscort* escort); @@ -38,7 +38,6 @@ namespace MWMechanics virtual int getTypeId() const; - MWWorld::Ptr getTarget() const; virtual bool sideWithTarget() const { return true; } void writeState(ESM::AiSequence::AiSequence &sequence) const; @@ -46,7 +45,6 @@ namespace MWMechanics void fastForward(const MWWorld::Ptr& actor, AiState& state); private: - std::string mActorId; std::string mCellId; float mX; float mY; diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index fd5f9c7fe..13de01f9a 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -35,32 +35,53 @@ struct AiFollowStorage : AiTemporaryBase int AiFollow::mFollowIndexCounter = 0; -AiFollow::AiFollow(const std::string &actorId,float duration, float x, float y, float z) +AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z) : mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mActorRefId(actorId), mActorId(-1), mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) +, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) { + mTargetActorRefId = actorId; } -AiFollow::AiFollow(const std::string &actorId,const std::string &cellId,float duration, float x, float y, float z) +AiFollow::AiFollow(const std::string &actorId, const std::string &cellId, float duration, float x, float y, float z) : mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) -, mActorRefId(actorId), mActorId(-1), mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) +, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) { + mTargetActorRefId = actorId; } -AiFollow::AiFollow(const std::string &actorId, bool commanded) +AiFollow::AiFollow(const MWWorld::Ptr& actor, float duration, float x, float y, float z) +: mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) +{ + mTargetActorRefId = actor.getCellRef().getRefId(); + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); +} + +AiFollow::AiFollow(const MWWorld::Ptr& actor, const std::string &cellId, float duration, float x, float y, float z) +: mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z) +, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++) +{ + mTargetActorRefId = actor.getCellRef().getRefId(); + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); +} + +AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded) : mAlwaysFollow(true), mCommanded(commanded), mDuration(0), mRemainingDuration(0), mX(0), mY(0), mZ(0) -, mActorRefId(actorId), mActorId(-1), mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) +, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++) { + mTargetActorRefId = actor.getCellRef().getRefId(); + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow) : mAlwaysFollow(follow->mAlwaysFollow), mCommanded(follow->mCommanded), mRemainingDuration(follow->mRemainingDuration) , mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ) - , mActorRefId(follow->mTargetId), mActorId(-1) , mCellId(follow->mCellId), mActive(follow->mActive), mFollowIndex(mFollowIndexCounter++) { -// mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration. -// The exact value of mDuration only matters for repeating packages. + mTargetActorRefId = follow->mTargetId; + mTargetActorId = follow->mTargetActorId; + // mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration. + // The exact value of mDuration only matters for repeating packages. if (mRemainingDuration > 0) // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves. mDuration = 1; else @@ -204,7 +225,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte std::string AiFollow::getFollowedActor() { - return mActorRefId; + return mTargetActorRefId; } AiFollow *MWMechanics::AiFollow::clone() const @@ -228,7 +249,8 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const follow->mData.mX = mX; follow->mData.mY = mY; follow->mData.mZ = mZ; - follow->mTargetId = mActorRefId; + follow->mTargetId = mTargetActorRefId; + follow->mTargetActorId = mTargetActorId; follow->mRemainingDuration = mRemainingDuration; follow->mCellId = mCellId; follow->mAlwaysFollow = mAlwaysFollow; @@ -241,29 +263,6 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const sequence.mPackages.push_back(package); } -MWWorld::Ptr AiFollow::getTarget() const -{ - if (mActorId == -2) - return MWWorld::Ptr(); - - if (mActorId == -1) - { - MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mActorRefId, false); - if (target.isEmpty()) - { - mActorId = -2; - return target; - } - else - mActorId = target.getClass().getCreatureStats(target).getActorId(); - } - - if (mActorId != -1) - return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId); - else - return MWWorld::Ptr(); -} - int AiFollow::getFollowIndex() const { return mFollowIndex; diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index 051a4a2ce..f0d43c9a7 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -25,16 +25,17 @@ namespace MWMechanics class AiFollow : public AiPackage { public: + AiFollow(const std::string &actorId, float duration, float x, float y, float z); + AiFollow(const std::string &actorId, const std::string &CellId, float duration, float x, float y, float z); /// Follow Actor for duration or until you arrive at a world position - AiFollow(const std::string &ActorId,float duration, float X, float Y, float Z); + AiFollow(const MWWorld::Ptr& actor, float duration, float X, float Y, float Z); /// Follow Actor for duration or until you arrive at a position in a cell - AiFollow(const std::string &ActorId,const std::string &CellId,float duration, float X, float Y, float Z); + AiFollow(const MWWorld::Ptr& actor, const std::string &CellId, float duration, float X, float Y, float Z); /// Follow Actor indefinitively - AiFollow(const std::string &ActorId, bool commanded=false); + AiFollow(const MWWorld::Ptr& actor, bool commanded=false); AiFollow(const ESM::AiSequence::AiFollow* follow); - MWWorld::Ptr getTarget() const; virtual bool sideWithTarget() const { return true; } virtual bool followTargetThroughDoors() const { return true; } virtual bool shouldCancelPreviousAi() const { return !mCommanded; } @@ -66,8 +67,6 @@ namespace MWMechanics float mX; float mY; float mZ; - std::string mActorRefId; - mutable int mActorId; std::string mCellId; bool mActive; // have we spotted the target? int mFollowIndex; diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 38f641b94..6a0f5b013 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -27,15 +27,36 @@ MWMechanics::AiPackage::~AiPackage() {} MWMechanics::AiPackage::AiPackage() : mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild + mTargetActorRefId(""), + mTargetActorId(-1), mRotateOnTheRunChecks(0), mIsShortcutting(false), - mShortcutProhibited(false), mShortcutFailPos() + mShortcutProhibited(false), + mShortcutFailPos() { } MWWorld::Ptr MWMechanics::AiPackage::getTarget() const { - return MWWorld::Ptr(); + if (mTargetActorId == -2) + return MWWorld::Ptr(); + + if (mTargetActorId == -1) + { + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false); + if (target.isEmpty()) + { + mTargetActorId = -2; + return target; + } + else + mTargetActorId = target.getClass().getCreatureStats(target).getActorId(); + } + + if (mTargetActorId != -1) + return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); + else + return MWWorld::Ptr(); } bool MWMechanics::AiPackage::sideWithTarget() const diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 06b4adf61..b4987e954 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -128,6 +128,9 @@ namespace MWMechanics float mTimer; + std::string mTargetActorRefId; + mutable int mTargetActorId; + osg::Vec3f mLastActorPos; short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility diff --git a/apps/openmw/mwmechanics/aipursue.cpp b/apps/openmw/mwmechanics/aipursue.cpp index fd8b5752a..f46594655 100644 --- a/apps/openmw/mwmechanics/aipursue.cpp +++ b/apps/openmw/mwmechanics/aipursue.cpp @@ -15,13 +15,13 @@ namespace MWMechanics { AiPursue::AiPursue(const MWWorld::Ptr& actor) - : mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()) { + mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } AiPursue::AiPursue(const ESM::AiSequence::AiPursue *pursue) - : mTargetActorId(pursue->mTargetActorId) { + mTargetActorId = pursue->mTargetActorId; } AiPursue *MWMechanics::AiPursue::clone() const diff --git a/apps/openmw/mwmechanics/aipursue.hpp b/apps/openmw/mwmechanics/aipursue.hpp index cb93e9636..455b0a2fd 100644 --- a/apps/openmw/mwmechanics/aipursue.hpp +++ b/apps/openmw/mwmechanics/aipursue.hpp @@ -40,10 +40,6 @@ namespace MWMechanics virtual bool canCancel() const { return false; } virtual bool shouldCancelPreviousAi() const { return false; } - - private: - - int mTargetActorId; // The actor to pursue }; } #endif diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index d71b4c8ba..f6d92726d 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -547,7 +547,7 @@ namespace MWMechanics || (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name())) && !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && magnitude >= target.getClass().getCreatureStats(target).getLevel()) { - MWMechanics::AiFollow package(caster.getCellRef().getRefId(), true); + MWMechanics::AiFollow package(caster, true); target.getClass().getCreatureStats(target).getAiSequence().stack(package, target); } diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index fad7bc96d..71c49f9df 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -59,7 +59,7 @@ namespace MWMechanics MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr()); // Make the summoned creature follow its master and help in fights - AiFollow package(mActor.getCellRef().getRefId()); + AiFollow package(mActor); summonedCreatureStats.getAiSequence().stack(package, ref.getPtr()); creatureActorId = summonedCreatureStats.getActorId(); diff --git a/components/esm/aisequence.cpp b/components/esm/aisequence.cpp index c39ef8269..90992bed3 100644 --- a/components/esm/aisequence.cpp +++ b/components/esm/aisequence.cpp @@ -46,6 +46,7 @@ namespace AiSequence { esm.getHNT (mData, "DATA"); mTargetId = esm.getHNString("TARG"); + esm.getHNOT (mTargetActorId, "TAID"); esm.getHNT (mRemainingDuration, "DURA"); mCellId = esm.getHNOString ("CELL"); } @@ -54,6 +55,7 @@ namespace AiSequence { esm.writeHNT ("DATA", mData); esm.writeHNString ("TARG", mTargetId); + esm.writeHNT ("TAID", mTargetActorId); esm.writeHNT ("DURA", mRemainingDuration); if (!mCellId.empty()) esm.writeHNString ("CELL", mCellId); @@ -63,6 +65,7 @@ namespace AiSequence { esm.getHNT (mData, "DATA"); mTargetId = esm.getHNString("TARG"); + esm.getHNOT (mTargetActorId, "TAID"); esm.getHNT (mRemainingDuration, "DURA"); mCellId = esm.getHNOString ("CELL"); esm.getHNT (mAlwaysFollow, "ALWY"); @@ -76,6 +79,7 @@ namespace AiSequence { esm.writeHNT ("DATA", mData); esm.writeHNString("TARG", mTargetId); + esm.writeHNT ("TAID", mTargetActorId); esm.writeHNT ("DURA", mRemainingDuration); if (!mCellId.empty()) esm.writeHNString ("CELL", mCellId); diff --git a/components/esm/aisequence.hpp b/components/esm/aisequence.hpp index 52446d38f..3c4cf2406 100644 --- a/components/esm/aisequence.hpp +++ b/components/esm/aisequence.hpp @@ -89,6 +89,7 @@ namespace ESM { AiEscortData mData; + int mTargetActorId; std::string mTargetId; std::string mCellId; float mRemainingDuration; @@ -101,6 +102,7 @@ namespace ESM { AiEscortData mData; + int mTargetActorId; std::string mTargetId; std::string mCellId; float mRemainingDuration; diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index 3220f496e..c96261c64 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm/savedgame.cpp @@ -5,7 +5,7 @@ #include "defs.hpp" unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 3; +int ESM::SavedGame::sCurrentFormat = 4; void ESM::SavedGame::load (ESMReader &esm) { From b7026df551e56181f87aa1929348b5b661c422ea Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Sun, 25 Mar 2018 13:05:42 +0300 Subject: [PATCH 081/282] Improve the offered price formula (Fixes #2222) --- .../openmw/mwmechanics/mechanicsmanagerimp.cpp | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 94317bbf2..0a3f8cad6 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -629,22 +629,12 @@ namespace MWMechanics float d = static_cast(std::min(sellerStats.getSkill(ESM::Skill::Mercantile).getModified(), 100)); float e = std::min(0.1f * sellerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); float f = std::min(0.2f * sellerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); - float pcTerm = (clampedDisposition - 50 + a + b + c) * playerStats.getFatigueTerm(); float npcTerm = (d + e + f) * sellerStats.getFatigueTerm(); - float buyTerm = 0.01f * (100 - 0.5f * (pcTerm - npcTerm)); - float sellTerm = 0.01f * (50 - 0.5f * (npcTerm - pcTerm)); - - float x; - if(buying) x = buyTerm; - else x = std::min(buyTerm, sellTerm); - int offerPrice; - if (x < 1) - offerPrice = int(x * basePrice); - else - offerPrice = basePrice + int((x - 1) * basePrice); - offerPrice = std::max(1, offerPrice); - return offerPrice; + float buyTerm = 0.01f * std::max(75.f, (100 - 0.5f * (pcTerm - npcTerm))); + float sellTerm = 0.01f * std::min(75.f, (50 - 0.5f * (npcTerm - pcTerm))); + int offerPrice = int(basePrice * (buying ? buyTerm : sellTerm)); + return std::max(1, offerPrice); } int MechanicsManager::countDeaths (const std::string& id) const From 24c1ee774477c44736f30881e4211d99693da115 Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Wed, 4 Apr 2018 15:34:34 +0300 Subject: [PATCH 082/282] Use relative stat difference for haggling --- apps/openmw/mwmechanics/trading.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/trading.cpp b/apps/openmw/mwmechanics/trading.cpp index ede0d2de7..eee5e6449 100644 --- a/apps/openmw/mwmechanics/trading.cpp +++ b/apps/openmw/mwmechanics/trading.cpp @@ -32,7 +32,6 @@ namespace MWMechanics // Is the player buying? bool buying = (merchantOffer < 0); - int a = std::abs(merchantOffer); int b = std::abs(playerOffer); int d = (buying) @@ -56,7 +55,7 @@ namespace MWMechanics float npcTerm = (d1 + e1 + f1) * merchantStats.getFatigueTerm(); float x = gmst.find("fBargainOfferMulti")->getFloat() * d + gmst.find("fBargainOfferBase")->getFloat() - + std::abs(int(pcTerm - npcTerm)); + + int(pcTerm - npcTerm); int roll = Misc::Rng::rollDice(100) + 1; From 01f12a6bd55ede0372265d368d6d1d30b2bbf159 Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Fri, 8 Jun 2018 19:01:48 +0300 Subject: [PATCH 083/282] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc2a9acc1..d1ec6e58e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ 0.45.0 ------ + Bug #2222: Fatigue's effect on selling price is backwards Bug #2835: Player able to slowly move when overencumbered Bug #4221: Characters get stuck in V-shaped terrain Bug #4293: Faction members are not aware of faction ownerships in barter From 99781ab70c11c25bb92c1fadb75b7173676328ae Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Fri, 8 Jun 2018 19:15:31 +0300 Subject: [PATCH 084/282] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fbc5092f..333ae6f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ 0.45.0 ------ - Feature #3897: Have Goodbye give all choices the effects of Goodbye + Bug #3897: Have Goodbye give all choices the effects of Goodbye Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4426: RotateWorld behavior is incorrect Bug #4433: Guard behaviour is incorrect with Alarm = 0 From b274931165c85b0ead18f4fc89c7e2ae6b83e036 Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Fri, 8 Jun 2018 19:18:53 +0300 Subject: [PATCH 085/282] Revert erroneous changes --- apps/openmw/mwdialogue/dialoguemanagerimp.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index da6e80e79..fb492ff3b 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -431,11 +431,8 @@ namespace MWDialogue void DialogueManager::addChoice (const std::string& text, int choice) { - if (!mGoodbye) - { - mIsInChoice = true; - mChoices.push_back(std::make_pair(text, choice)); - } + mIsInChoice = true; + mChoices.push_back(std::make_pair(text, choice)); } const std::vector >& DialogueManager::getChoices() @@ -450,8 +447,8 @@ namespace MWDialogue void DialogueManager::goodbye() { - if (!mIsInChoice) - mGoodbye = true; + mIsInChoice = false; + mGoodbye = true; } void DialogueManager::persuade(int type, ResponseCallback* callback) From 9d27eb197fceb00cf37b836bdfd649088f64635c Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 28 Nov 2017 12:05:05 +0400 Subject: [PATCH 086/282] AiWander: return to initial position only after combat --- apps/openmw/mwmechanics/aiwander.cpp | 68 +++++++------------ apps/openmw/mwmechanics/aiwander.hpp | 1 - .../mwmechanics/mechanicsmanagerimp.cpp | 18 ++++- 3 files changed, 41 insertions(+), 46 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 8199170dc..3ad45e2c3 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -59,7 +59,7 @@ namespace MWMechanics float mTargetAngleRadians; bool mTurnActorGivingGreetingToFacePlayer; float mReaction; // update some actions infrequently - + AiWander::GreetingState mSaidGreeting; int mGreetingTimer; @@ -70,7 +70,7 @@ namespace MWMechanics bool mIsWanderingManually; bool mCanWanderAlongPathGrid; - + unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors @@ -86,7 +86,7 @@ namespace MWMechanics float mDoorCheckDuration; int mStuckCount; - + AiWanderStorage(): mTargetAngleRadians(0), mTurnActorGivingGreetingToFacePlayer(false), @@ -111,7 +111,7 @@ namespace MWMechanics mIsWanderingManually = isManualWander; } }; - + AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): mDistance(distance), mDuration(duration), mRemainingDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat), mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)), mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0)) @@ -201,7 +201,18 @@ namespace MWMechanics stopWalking(actor, storage); currentCell = actor.getCell(); storage.mPopulateAvailableNodes = true; - mStoredInitialActorPosition = false; + } + + // Here we should reset an initial position, if a current cell was REALLY changed + // We do not store AiStorage in a savegame, so cellChange is not help us in this case + // TODO: find a more simple and fast solution, or do not store the mInitialActorPosition at all + if (mStoredInitialActorPosition) + { + int cx,cy; + MWBase::Environment::get().getWorld()->positionToIndex(mInitialActorPosition.x(),mInitialActorPosition.y(),cx,cy); + MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cx,cy); + if (cell != currentCell) + mStoredInitialActorPosition = false; } mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600); @@ -223,7 +234,7 @@ namespace MWMechanics if (mPathFinder.isPathConstructed()) storage.setState(Wander_Walking); } - + doPerFrameActionsForState(actor, duration, storage, pos); playIdleDialogueRandomly(actor); @@ -298,13 +309,6 @@ namespace MWMechanics if(mDistance && cellChange) mDistance = 0; - // For stationary NPCs, move back to the starting location if another AiPackage moved us elsewhere - if (mDistance == 0 && !cellChange - && (pos.asVec3() - mInitialActorPosition).length2() > (DESTINATION_TOLERANCE * DESTINATION_TOLERANCE)) - { - returnToStartLocation(actor, storage, pos); - } - // Allow interrupting a walking actor to trigger a greeting WanderState& wanderState = storage.mState; if ((wanderState == Wander_IdleNow) || (wanderState == Wander_Walking)) @@ -321,7 +325,7 @@ namespace MWMechanics { setPathToAnAllowedNode(actor, storage, pos); } - } + } } else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) { completeManualWalking(actor, storage); } @@ -330,8 +334,8 @@ namespace MWMechanics } bool AiWander::getRepeat() const - { - return mRepeat; + { + return mRepeat; } @@ -350,27 +354,6 @@ namespace MWMechanics return false; } - void AiWander::returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) - { - if (!mPathFinder.isPathConstructed()) - { - mDestination = mInitialActorPosition; - ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mDestination)); - - // actor position is already in world coordinates - ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); - - // don't take shortcuts for wandering - mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell())); - - if (mPathFinder.isPathConstructed()) - { - storage.setState(Wander_Walking); - mHasDestination = true; - } - } - } - /* * Commands actor to walk to a random location near original spawn location. */ @@ -497,7 +480,7 @@ namespace MWMechanics } } - void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, + void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage, ESM::Position& pos) { // Is there no destination or are we there yet? @@ -873,7 +856,7 @@ namespace MWMechanics state.moveIn(new AiWanderStorage()); - MWBase::Environment::get().getWorld()->moveObject(actor, static_cast(dest.mX), + MWBase::Environment::get().getWorld()->moveObject(actor, static_cast(dest.mX), static_cast(dest.mY), static_cast(dest.mZ)); actor.getClass().adjustPosition(actor, false); } @@ -914,7 +897,7 @@ namespace MWMechanics // get NPC's position in local (i.e. cell) coordinates osg::Vec3f npcPos(mInitialActorPosition); CoordinateConverter(cell).toLocal(npcPos); - + // Find closest pathgrid point int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, npcPos); @@ -945,7 +928,7 @@ namespace MWMechanics storage.mPopulateAvailableNodes = false; } - // When only one path grid point in wander distance, + // When only one path grid point in wander distance, // additional points for NPC to wander to are: // 1. NPC's initial location // 2. Partway along the path between the point and its connected points. @@ -969,7 +952,7 @@ namespace MWMechanics delta.normalize(); int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE); - + // must not travel longer than distance between waypoints or NPC goes past waypoint distance = std::min(distance, static_cast(length)); delta *= distance; @@ -1041,4 +1024,3 @@ namespace MWMechanics init(); } } - diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 6266a7708..3f69d107d 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -85,7 +85,6 @@ namespace MWMechanics bool reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos, float duration); bool isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage); - void returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos); void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance); bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination); bool destinationThroughGround(const osg::Vec3f& startPoint, const osg::Vec3f& destination); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 94317bbf2..bddcf83d6 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -22,6 +22,7 @@ #include "aicombat.hpp" #include "aipursue.hpp" +#include "aitravel.hpp" #include "spellcasting.hpp" #include "autocalcspell.hpp" #include "npcstats.hpp" @@ -1598,9 +1599,22 @@ namespace MWMechanics void MechanicsManager::startCombat(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target) { - if (ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(target)) + MWMechanics::AiSequence& aiSequence = ptr.getClass().getCreatureStats(ptr).getAiSequence(); + + if (aiSequence.isInCombat(target)) return; - ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(MWMechanics::AiCombat(target), ptr); + + // we should return a wandering actor back after combat + // TODO: only for stationary wander? + if (!aiSequence.isInCombat() && aiSequence.getLastRunTypeId() == MWMechanics::AiPackage::TypeIdWander) + { + osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); + + MWMechanics::AiTravel travelPackage(pos.x(), pos.y(), pos.z()); + aiSequence.stack(travelPackage, ptr); + } + + aiSequence.stack(MWMechanics::AiCombat(target), ptr); if (target == getPlayer()) { // if guard starts combat with player, guards pursuing player should do the same From 18ff097e4a2ab38bb3d46e3164e26fa89de8317e Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 28 Nov 2017 12:06:09 +0400 Subject: [PATCH 087/282] Add the parameter to AiSequence::stack() to control ability to cancel other AI packages --- apps/openmw/mwmechanics/aisequence.cpp | 4 ++-- apps/openmw/mwmechanics/aisequence.hpp | 2 +- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 2a2d598f5..b38111f7b 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -298,7 +298,7 @@ void AiSequence::clear() mPackages.clear(); } -void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor) +void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther) { if (actor == getPlayer()) throw std::runtime_error("Can't add AI packages to player"); @@ -308,7 +308,7 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor) stopCombat(); // remove previous packages if required - if (package.shouldCancelPreviousAi()) + if (cancelOther && package.shouldCancelPreviousAi()) { for(std::list::iterator it = mPackages.begin(); it != mPackages.end();) { diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index 41d204ee2..d725409de 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -115,7 +115,7 @@ namespace MWMechanics ///< Add \a package to the front of the sequence /** Suspends current package @param actor The actor that owns this AiSequence **/ - void stack (const AiPackage& package, const MWWorld::Ptr& actor); + void stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther=true); /// Return the current active package. /** If there is no active package, it will throw an exception **/ diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index bddcf83d6..d032ed632 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1611,7 +1611,7 @@ namespace MWMechanics osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); MWMechanics::AiTravel travelPackage(pos.x(), pos.y(), pos.z()); - aiSequence.stack(travelPackage, ptr); + aiSequence.stack(travelPackage, ptr, false); } aiSequence.stack(MWMechanics::AiCombat(target), ptr); From 81f29d8dcd131bc123fbed5858f49fa049c060d9 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 1 Dec 2017 18:53:31 +0400 Subject: [PATCH 088/282] AiWander: resume moving to destination after combat --- apps/openmw/mwmechanics/aipackage.hpp | 3 +++ apps/openmw/mwmechanics/aiwander.cpp | 15 ++++++++++++--- apps/openmw/mwmechanics/aiwander.hpp | 6 ++++-- .../openmw/mwmechanics/mechanicsmanagerimp.cpp | 18 ++++++++++++++---- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 06b4adf61..d9a7fa386 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -79,6 +79,9 @@ namespace MWMechanics /// Get the target actor the AI is targeted at (not applicable to all AI packages, default return empty Ptr) virtual MWWorld::Ptr getTarget() const; + /// Get the destination point of the AI package (not applicable to all AI packages, default return (0, 0, 0)) + virtual osg::Vec3f getDestination(const MWWorld::Ptr& actor) const { return osg::Vec3f(0, 0, 0); }; + /// Return true if having this AiPackage makes the actor side with the target in fights (default false) virtual bool sideWithTarget() const; diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 3ad45e2c3..2e832dc3f 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -335,9 +335,18 @@ namespace MWMechanics bool AiWander::getRepeat() const { - return mRepeat; + return mRepeat; } + osg::Vec3f AiWander::getDestination(const MWWorld::Ptr& actor) const + { + if (mHasDestination) + return mDestination; + + const ESM::Pathgrid::Point currentPosition = actor.getRefData().getPosition().pos; + const osg::Vec3f currentPositionVec3f = osg::Vec3f(currentPosition.mX, currentPosition.mY, currentPosition.mZ); + return currentPositionVec3f; + } bool AiWander::isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage) { @@ -346,8 +355,8 @@ namespace MWMechanics // End package if duration is complete if (mRemainingDuration <= 0) { - stopWalking(actor, storage); - return true; + stopWalking(actor, storage); + return true; } } // if get here, not yet completed diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 3f69d107d..d96d93165 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -47,9 +47,11 @@ namespace MWMechanics virtual void writeState(ESM::AiSequence::AiSequence &sequence) const; virtual void fastForward(const MWWorld::Ptr& actor, AiState& state); - + bool getRepeat() const; - + + osg::Vec3f getDestination(const MWWorld::Ptr& actor) const; + enum GreetingState { Greet_None, Greet_InProgress, diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index d032ed632..55aace8e7 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1605,12 +1605,22 @@ namespace MWMechanics return; // we should return a wandering actor back after combat - // TODO: only for stationary wander? - if (!aiSequence.isInCombat() && aiSequence.getLastRunTypeId() == MWMechanics::AiPackage::TypeIdWander) + // the same thing for actors without AI packages + if (!aiSequence.isInCombat() && aiSequence.getTypeId() <= MWMechanics::AiPackage::TypeIdWander) { - osg::Vec3f pos = ptr.getRefData().getPosition().asVec3(); + int typeId = aiSequence.getTypeId(); + osg::Vec3f dest; + if (typeId == MWMechanics::AiPackage::TypeIdNone) + { + dest = ptr.getRefData().getPosition().asVec3(); + } + else if (typeId == MWMechanics::AiPackage::TypeIdWander) + { + AiPackage* activePackage = aiSequence.getActivePackage(); + dest = activePackage->getDestination(ptr); + } - MWMechanics::AiTravel travelPackage(pos.x(), pos.y(), pos.z()); + MWMechanics::AiTravel travelPackage(dest.x(), dest.y(), dest.z()); aiSequence.stack(travelPackage, ptr, false); } From 5105c676422fa2664d4605717a04d9aea500e157 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 1 Dec 2017 19:58:42 +0400 Subject: [PATCH 089/282] Add mHidden field to AiTravel --- apps/openmw/mwmechanics/aipackage.hpp | 3 ++- apps/openmw/mwmechanics/aitravel.cpp | 8 ++++---- apps/openmw/mwmechanics/aitravel.hpp | 4 +++- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index d9a7fa386..829bbe898 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -48,7 +48,8 @@ namespace MWMechanics TypeIdPursue = 6, TypeIdAvoidDoor = 7, TypeIdFace = 8, - TypeIdBreathe = 9 + TypeIdBreathe = 9, + TypeIdInternalTravel = 10 }; ///Default constructor diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index 36b96101f..ea14407ca 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -28,15 +28,14 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2) namespace MWMechanics { - AiTravel::AiTravel(float x, float y, float z) - : mX(x),mY(y),mZ(z) + AiTravel::AiTravel(float x, float y, float z, bool hidden) + : mX(x),mY(y),mZ(z),mHidden(hidden) { } AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ) { - } AiTravel *MWMechanics::AiTravel::clone() const @@ -64,7 +63,8 @@ namespace MWMechanics int AiTravel::getTypeId() const { - return TypeIdTravel; + // TODO: store mHidden in the savegame? + return mHidden ? TypeIdInternalTravel : TypeIdTravel; } void AiTravel::fastForward(const MWWorld::Ptr& actor, AiState& state) diff --git a/apps/openmw/mwmechanics/aitravel.hpp b/apps/openmw/mwmechanics/aitravel.hpp index 8c75bded1..c297771d0 100644 --- a/apps/openmw/mwmechanics/aitravel.hpp +++ b/apps/openmw/mwmechanics/aitravel.hpp @@ -20,7 +20,7 @@ namespace MWMechanics { public: /// Default constructor - AiTravel(float x, float y, float z); + AiTravel(float x, float y, float z, bool hidden = false); AiTravel(const ESM::AiSequence::AiTravel* travel); /// Simulates the passing of time @@ -38,6 +38,8 @@ namespace MWMechanics float mX; float mY; float mZ; + + bool mHidden; }; } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 55aace8e7..dc3695239 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1620,7 +1620,7 @@ namespace MWMechanics dest = activePackage->getDestination(ptr); } - MWMechanics::AiTravel travelPackage(dest.x(), dest.y(), dest.z()); + MWMechanics::AiTravel travelPackage(dest.x(), dest.y(), dest.z(), true); aiSequence.stack(travelPackage, ptr, false); } From 57d686131e89ec4f18ab97bcab7fa7443d7df20e Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 1 Dec 2017 20:01:15 +0400 Subject: [PATCH 090/282] Remove redundant condition --- apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index dc3695239..fbf989d05 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1606,7 +1606,7 @@ namespace MWMechanics // we should return a wandering actor back after combat // the same thing for actors without AI packages - if (!aiSequence.isInCombat() && aiSequence.getTypeId() <= MWMechanics::AiPackage::TypeIdWander) + if (aiSequence.getTypeId() <= MWMechanics::AiPackage::TypeIdWander) { int typeId = aiSequence.getTypeId(); osg::Vec3f dest; From 2f5beb885371aa2daed3b999b78cda24ec3abcfc Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 1 Dec 2017 20:38:10 +0400 Subject: [PATCH 091/282] Remove unnecessary hack --- apps/openmw/mwmechanics/aiwander.cpp | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 2e832dc3f..ee680159e 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -201,18 +201,7 @@ namespace MWMechanics stopWalking(actor, storage); currentCell = actor.getCell(); storage.mPopulateAvailableNodes = true; - } - - // Here we should reset an initial position, if a current cell was REALLY changed - // We do not store AiStorage in a savegame, so cellChange is not help us in this case - // TODO: find a more simple and fast solution, or do not store the mInitialActorPosition at all - if (mStoredInitialActorPosition) - { - int cx,cy; - MWBase::Environment::get().getWorld()->positionToIndex(mInitialActorPosition.x(),mInitialActorPosition.y(),cx,cy); - MWWorld::CellStore* cell = MWBase::Environment::get().getWorld()->getExterior(cx,cy); - if (cell != currentCell) - mStoredInitialActorPosition = false; + mStoredInitialActorPosition = false; } mRemainingDuration -= ((duration*MWBase::Environment::get().getWorld()->getTimeScaleFactor()) / 3600); From 3a0ee78d2b300301a16e4739cd11de14195f9d0d Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 25 May 2018 19:31:31 +0400 Subject: [PATCH 092/282] AiTravel: store mHidden flag in savegame --- apps/openmw/mwmechanics/aisequence.cpp | 3 ++- apps/openmw/mwmechanics/aitravel.cpp | 4 ++-- components/esm/aisequence.cpp | 2 ++ components/esm/aisequence.hpp | 1 + components/esm/savedgame.cpp | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index b38111f7b..2c48eacf8 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -184,7 +184,8 @@ bool isActualAiPackage(int packageTypeId) && packageTypeId != AiPackage::TypeIdPursue && packageTypeId != AiPackage::TypeIdAvoidDoor && packageTypeId != AiPackage::TypeIdFace - && packageTypeId != AiPackage::TypeIdBreathe); + && packageTypeId != AiPackage::TypeIdBreathe + && packageTypeId != AiPackage::TypeIdInternalTravel); } void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) diff --git a/apps/openmw/mwmechanics/aitravel.cpp b/apps/openmw/mwmechanics/aitravel.cpp index ea14407ca..72e6ced19 100644 --- a/apps/openmw/mwmechanics/aitravel.cpp +++ b/apps/openmw/mwmechanics/aitravel.cpp @@ -34,7 +34,7 @@ namespace MWMechanics } AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) - : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ) + : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(travel->mHidden) { } @@ -63,7 +63,6 @@ namespace MWMechanics int AiTravel::getTypeId() const { - // TODO: store mHidden in the savegame? return mHidden ? TypeIdInternalTravel : TypeIdTravel; } @@ -83,6 +82,7 @@ namespace MWMechanics travel->mData.mX = mX; travel->mData.mY = mY; travel->mData.mZ = mZ; + travel->mHidden = mHidden; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Travel; diff --git a/components/esm/aisequence.cpp b/components/esm/aisequence.cpp index c39ef8269..196c1754f 100644 --- a/components/esm/aisequence.cpp +++ b/components/esm/aisequence.cpp @@ -35,11 +35,13 @@ namespace AiSequence void AiTravel::load(ESMReader &esm) { esm.getHNT (mData, "DATA"); + esm.getHNOT (mHidden, "HIDD"); } void AiTravel::save(ESMWriter &esm) const { esm.writeHNT ("DATA", mData); + esm.writeHNT ("HIDD", mHidden); } void AiEscort::load(ESMReader &esm) diff --git a/components/esm/aisequence.hpp b/components/esm/aisequence.hpp index 52446d38f..d4315062b 100644 --- a/components/esm/aisequence.hpp +++ b/components/esm/aisequence.hpp @@ -80,6 +80,7 @@ namespace ESM struct AiTravel : AiPackage { AiTravelData mData; + bool mHidden; void load(ESMReader &esm); void save(ESMWriter &esm) const; diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index 3220f496e..c96261c64 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm/savedgame.cpp @@ -5,7 +5,7 @@ #include "defs.hpp" unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 3; +int ESM::SavedGame::sCurrentFormat = 4; void ESM::SavedGame::load (ESMReader &esm) { From 3d0631cfcc01fdb5032ac3df2be00a1beadd1e6a Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 25 May 2018 20:07:08 +0400 Subject: [PATCH 093/282] Store last AI package in savegame --- apps/openmw/mwmechanics/aisequence.cpp | 6 +++++- components/esm/aisequence.cpp | 4 ++++ components/esm/aisequence.hpp | 6 +++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 2c48eacf8..51c82f6ed 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -393,6 +393,8 @@ void AiSequence::writeState(ESM::AiSequence::AiSequence &sequence) const { (*iter)->writeState(sequence); } + + sequence.mLastAiPackage = mLastAiPackage; } void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) @@ -404,7 +406,7 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) int count = 0; for (std::vector::const_iterator it = sequence.mPackages.begin(); it != sequence.mPackages.end(); ++it) - { + { if (isActualAiPackage(it->mType)) count++; } @@ -463,6 +465,8 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) mPackages.push_back(package.release()); } + + mLastAiPackage = sequence.mLastAiPackage; } void AiSequence::fastForward(const MWWorld::Ptr& actor, AiState& state) diff --git a/components/esm/aisequence.cpp b/components/esm/aisequence.cpp index 196c1754f..43712d9ea 100644 --- a/components/esm/aisequence.cpp +++ b/components/esm/aisequence.cpp @@ -156,6 +156,8 @@ namespace AiSequence break; } } + + esm.writeHNT ("LAST", mLastAiPackage); } void AiSequence::load(ESMReader &esm) @@ -223,6 +225,8 @@ namespace AiSequence return; } } + + esm.getHNOT (mLastAiPackage, "LAST"); } } } diff --git a/components/esm/aisequence.hpp b/components/esm/aisequence.hpp index d4315062b..0cbde1b8e 100644 --- a/components/esm/aisequence.hpp +++ b/components/esm/aisequence.hpp @@ -148,10 +148,14 @@ namespace ESM struct AiSequence { - AiSequence() {} + AiSequence() + { + mLastAiPackage = -1; + } ~AiSequence(); std::vector mPackages; + int mLastAiPackage; void load (ESMReader &esm); void save (ESMWriter &esm) const; From 74a2cbe6960ce178fb426f86430532c561cb0da7 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 1 Jun 2018 09:30:44 +0400 Subject: [PATCH 094/282] AI: return back after pursuit --- apps/openmw/mwmechanics/aisequence.cpp | 23 +++++++++++++++++++ .../mwmechanics/mechanicsmanagerimp.cpp | 20 ---------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 51c82f6ed..84ddf8fdf 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -308,6 +308,29 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo if (isActualAiPackage(package.getTypeId())) stopCombat(); + // we should return a wandering actor back after combat or pursuit + // the same thing for actors without AI packages + int currentTypeId = getTypeId(); + int newTypeId = package.getTypeId(); + if (currentTypeId <= MWMechanics::AiPackage::TypeIdWander + && (newTypeId <= MWMechanics::AiPackage::TypeIdCombat + || newTypeId == MWMechanics::AiPackage::TypeIdPursue)) + { + osg::Vec3f dest; + if (currentTypeId == MWMechanics::AiPackage::TypeIdWander) + { + AiPackage* activePackage = getActivePackage(); + dest = activePackage->getDestination(actor); + } + else + { + dest = actor.getRefData().getPosition().asVec3(); + } + + MWMechanics::AiTravel travelPackage(dest.x(), dest.y(), dest.z(), true); + stack(travelPackage, actor, false); + } + // remove previous packages if required if (cancelOther && package.shouldCancelPreviousAi()) { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index fbf989d05..89e1bef06 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1604,26 +1604,6 @@ namespace MWMechanics if (aiSequence.isInCombat(target)) return; - // we should return a wandering actor back after combat - // the same thing for actors without AI packages - if (aiSequence.getTypeId() <= MWMechanics::AiPackage::TypeIdWander) - { - int typeId = aiSequence.getTypeId(); - osg::Vec3f dest; - if (typeId == MWMechanics::AiPackage::TypeIdNone) - { - dest = ptr.getRefData().getPosition().asVec3(); - } - else if (typeId == MWMechanics::AiPackage::TypeIdWander) - { - AiPackage* activePackage = aiSequence.getActivePackage(); - dest = activePackage->getDestination(ptr); - } - - MWMechanics::AiTravel travelPackage(dest.x(), dest.y(), dest.z(), true); - aiSequence.stack(travelPackage, ptr, false); - } - aiSequence.stack(MWMechanics::AiCombat(target), ptr); if (target == getPlayer()) { From 6ed27732995c2a87f2e62c4966c24a729a60b158 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 1 Jun 2018 12:05:10 +0400 Subject: [PATCH 095/282] Do not stack return packages --- apps/openmw/mwmechanics/aisequence.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 84ddf8fdf..85afbc453 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -308,11 +308,13 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo if (isActualAiPackage(package.getTypeId())) stopCombat(); - // we should return a wandering actor back after combat or pursuit - // the same thing for actors without AI packages + // We should return a wandering actor back after combat or pursuit. + // The same thing for actors without AI packages. + // Also there is no point to stack return packages. int currentTypeId = getTypeId(); int newTypeId = package.getTypeId(); if (currentTypeId <= MWMechanics::AiPackage::TypeIdWander + && !hasPackage(MWMechanics::AiPackage::TypeIdInternalTravel) && (newTypeId <= MWMechanics::AiPackage::TypeIdCombat || newTypeId == MWMechanics::AiPackage::TypeIdPursue)) { From 2e6cf2a4149ae87ca932833c70f594725475c5a1 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 8 Jun 2018 20:39:59 +0400 Subject: [PATCH 096/282] Add changelog entries --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb72c7cac..82410813e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ 0.45.0 ------ + Bug #2835: Player able to slowly move when overencumbered + Bug #3997: Almalexia doesn't pace Bug #4221: Characters get stuck in V-shaped terrain + Bug #4251: Stationary NPCs do not return to their position after combat Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4327: Missing animations during spell/weapon stance switching Bug #4426: RotateWorld behavior is incorrect Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 + Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 Bug #4443: Goodbye option and dialogue choices are not mutually exclusive Feature #4444: Per-group KF-animation files support From 6277f5511c4346a4038dda8401edf241b8a88b51 Mon Sep 17 00:00:00 2001 From: wareya Date: Fri, 8 Jun 2018 17:52:46 -0400 Subject: [PATCH 097/282] fix #3876 and #3993 --- components/esmterrain/storage.cpp | 34 ++++++++++++++++++++++++------- components/terrain/material.cpp | 3 +++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index dc144d119..a1b4856e1 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -430,15 +430,23 @@ namespace ESMTerrain // Second iteration - create and fill in the blend maps const int blendmapSize = (realTextureSize-1) * chunkSize + 1; + // We need to upscale the blendmap 2x with nearest neighbor sampling to look like Vanilla + const int imageScaleFactor = 2; + const int blendmapImageSize = blendmapSize * imageScaleFactor; + + + const bool largeImage = true; for (int i=0; i image (new osg::Image); - image->allocateImage(blendmapSize, blendmapSize, 1, format, GL_UNSIGNED_BYTE); + if(!largeImage) + image->allocateImage(blendmapSize, blendmapSize, 1, format, GL_UNSIGNED_BYTE); + else + image->allocateImage(blendmapImageSize, blendmapImageSize, 1, format, GL_UNSIGNED_BYTE); unsigned char* pData = image->data(); - for (int y=0; ysecond; int blendIndex = (pack ? static_cast(std::floor((layerIndex - 1) / 4.f)) : layerIndex - 1); int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0; - - if (blendIndex == i) - pData[(blendmapSize - y - 1)*blendmapSize*channels + x*channels + channel] = 255; + + int alpha = (blendIndex == i) ? 255 : 0; + + if(!largeImage) + pData[((blendmapSize - y - 1)*blendmapSize + x)*channels + channel] = alpha; else - pData[(blendmapSize - y - 1)*blendmapSize*channels + x*channels + channel] = 0; + { + int realY = (blendmapSize - y - 1)*imageScaleFactor; + int realX = x*imageScaleFactor; + if(true) + pData[((realY+0)*blendmapImageSize + realX + 0)*channels + channel] = alpha; + if(realY+1 < blendmapImageSize) + pData[((realY+1)*blendmapImageSize + realX + 0)*channels + channel] = alpha; + if(realX+1 < blendmapImageSize) + pData[((realY+0)*blendmapImageSize + realX + 1)*channels + channel] = alpha; + if(realY+1 < blendmapImageSize && realX+1 < blendmapImageSize) + pData[((realY+1)*blendmapImageSize + realX + 1)*channels + channel] = alpha; + } } } - blendmaps.push_back(image); } } diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index 640f2932b..56ace0e5a 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -25,6 +25,9 @@ namespace Terrain matrix.preMultTranslate(osg::Vec3f(0.5f, 0.5f, 0.f)); matrix.preMultScale(osg::Vec3f(scale, scale, 1.f)); matrix.preMultTranslate(osg::Vec3f(-0.5f, -0.5f, 0.f)); + // We need to nudge the blendmap to look like vanilla. + // This causes visible seams unless the blendmap's resolution is doubled, but Vanilla also doubles the blendmap, apparently. + matrix.preMultTranslate(osg::Vec3f(1.0f/blendmapScale/4.0f, 1.0f/blendmapScale/4.0f, 0.f)); texMat = new osg::TexMat(matrix); From 62c4eb8d6abbb4b3ebf5af951801dc0ce17bbc1f Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Fri, 8 Jun 2018 19:16:24 -0500 Subject: [PATCH 098/282] Explicitly flagging loaded cells changed as queued --- apps/launcher/maindialog.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index a70f6ff7f..90197b157 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -139,7 +139,8 @@ void Launcher::MainDialog::createPages() connect(mPlayPage, SIGNAL(signalProfileChanged(int)), mDataFilesPage, SLOT(slotProfileChanged(int))); connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int))); - connect(mDataFilesPage, SIGNAL(signalSelectedFilesChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList))); + // Using Qt::QueuedConnection because signal is emitted in a subthread and slot is in the main thread + connect(mDataFilesPage, SIGNAL(signalSelectedFilesChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList)), Qt::QueuedConnection); } From dfa99685658318a27fa79e59f7060aa8fbebb68c Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Fri, 8 Jun 2018 19:18:23 -0500 Subject: [PATCH 099/282] Renaming Launcher::DataFilesPage::signalSelectedFilesChanged to signalLoadedCellsChanged --- apps/launcher/datafilespage.cpp | 2 +- apps/launcher/datafilespage.hpp | 2 +- apps/launcher/maindialog.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/launcher/datafilespage.cpp b/apps/launcher/datafilespage.cpp index 6c23b3285..7b703a924 100644 --- a/apps/launcher/datafilespage.cpp +++ b/apps/launcher/datafilespage.cpp @@ -356,5 +356,5 @@ void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles) CellNameLoader cellNameLoader; QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(selectedFiles)); std::sort(cellNamesList.begin(), cellNamesList.end()); - emit signalSelectedFilesChanged(cellNamesList); + emit signalLoadedCellsChanged(cellNamesList); } \ No newline at end of file diff --git a/apps/launcher/datafilespage.hpp b/apps/launcher/datafilespage.hpp index d871eeee0..2cbace38e 100644 --- a/apps/launcher/datafilespage.hpp +++ b/apps/launcher/datafilespage.hpp @@ -50,7 +50,7 @@ namespace Launcher signals: void signalProfileChanged (int index); - void signalSelectedFilesChanged(QStringList selectedFiles); + void signalLoadedCellsChanged(QStringList selectedFiles); public slots: void slotProfileChanged (int index); diff --git a/apps/launcher/maindialog.cpp b/apps/launcher/maindialog.cpp index 90197b157..27fa2d903 100644 --- a/apps/launcher/maindialog.cpp +++ b/apps/launcher/maindialog.cpp @@ -140,7 +140,7 @@ void Launcher::MainDialog::createPages() connect(mPlayPage, SIGNAL(signalProfileChanged(int)), mDataFilesPage, SLOT(slotProfileChanged(int))); connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int))); // Using Qt::QueuedConnection because signal is emitted in a subthread and slot is in the main thread - connect(mDataFilesPage, SIGNAL(signalSelectedFilesChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList)), Qt::QueuedConnection); + connect(mDataFilesPage, SIGNAL(signalLoadedCellsChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList)), Qt::QueuedConnection); } From 4ba361fea6d1ecef2e13e2bad2267c6644df5e6a Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 10 Apr 2018 07:06:44 +0400 Subject: [PATCH 100/282] Unhardcode sunset and sunrise settings (bug #1990) --- apps/openmw/mwworld/weather.cpp | 119 ++++++++++++++++++++++---------- apps/openmw/mwworld/weather.hpp | 43 +++++++++++- 2 files changed, 123 insertions(+), 39 deletions(-) diff --git a/apps/openmw/mwworld/weather.cpp b/apps/openmw/mwworld/weather.cpp index 155e4340a..d12e633be 100644 --- a/apps/openmw/mwworld/weather.cpp +++ b/apps/openmw/mwworld/weather.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" @@ -43,49 +42,67 @@ namespace } template -T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings) const +T TimeOfDayInterpolator::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const { - // TODO: use pre/post sunset/sunrise time values in [Weather] section + WeatherSetting setting = timeSettings.getSetting(prefix); + float preSunriseTime = setting.mPreSunriseTime; + float postSunriseTime = setting.mPostSunriseTime; + float preSunsetTime = setting.mPreSunsetTime; + float postSunsetTime = setting.mPostSunsetTime; // night - if (gameHour <= timeSettings.mNightEnd || gameHour >= timeSettings.mNightStart + 1) + if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime) return mNightValue; // sunrise - else if (gameHour >= timeSettings.mNightEnd && gameHour <= timeSettings.mDayStart + 1) + else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime) { - if (gameHour <= timeSettings.mSunriseTime) + float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime; + float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f; + + if (gameHour <= middle) { // fade in - float advance = timeSettings.mSunriseTime - gameHour; - float factor = advance / 0.5f; + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; return lerp(mSunriseValue, mNightValue, factor); } else { // fade out - float advance = gameHour - timeSettings.mSunriseTime; - float factor = advance / 3.f; + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; return lerp(mSunriseValue, mDayValue, factor); } } // day - else if (gameHour >= timeSettings.mDayStart + 1 && gameHour <= timeSettings.mDayEnd - 1) + else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime) return mDayValue; // sunset - else if (gameHour >= timeSettings.mDayEnd - 1 && gameHour <= timeSettings.mNightStart + 1) + else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime) { - if (gameHour <= timeSettings.mDayEnd + 1) + float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime; + float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f; + + if (gameHour <= middle) { // fade in - float advance = (timeSettings.mDayEnd + 1) - gameHour; - float factor = (advance / 2); + float advance = middle - gameHour; + float factor = 0.f; + if (duration > 0) + factor = advance / duration * 2; return lerp(mSunsetValue, mDayValue, factor); } else { // fade out - float advance = gameHour - (timeSettings.mDayEnd + 1); - float factor = advance / 2.f; + float advance = gameHour - middle; + float factor = 1.f; + if (duration > 0) + factor = advance / duration * 2; return lerp(mSunsetValue, mNightValue, factor); } } @@ -539,10 +556,28 @@ WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, const Fall , mPlayingSoundID() { mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration; - mTimeSettings.mNightEnd = mSunriseTime - 0.5f; + mTimeSettings.mNightEnd = mSunriseTime; mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration; mTimeSettings.mDayEnd = mSunsetTime; - mTimeSettings.mSunriseTime = mSunriseTime; + + mTimeSettings.addSetting(fallback, "Sky"); + mTimeSettings.addSetting(fallback, "Ambient"); + mTimeSettings.addSetting(fallback, "Fog"); + mTimeSettings.addSetting(fallback, "Sun"); + + // Morrowind handles stars settings differently for other ones + mTimeSettings.mStarsPostSunsetStart = fallback.getFallbackFloat("Weather_Stars_Post-Sunset_Start"); + mTimeSettings.mStarsPreSunriseFinish = fallback.getFallbackFloat("Weather_Stars_Pre-Sunrise_Finish"); + mTimeSettings.mStarsFadingDuration = fallback.getFallbackFloat("Weather_Stars_Fading_Duration"); + + WeatherSetting starSetting = { + mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish, + mTimeSettings.mStarsPostSunsetStart, + mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart + }; + + mTimeSettings.mSunriseTransitions["Stars"] = starSetting; mWeatherSettings.reserve(10); // These distant land fog factor and offset values are the defaults MGE XE provides. Should be @@ -698,10 +733,13 @@ void WeatherManager::update(float duration, bool paused, const TimeStamp& time, const float nightDuration = 24.f - dayDuration; double theta; - if ( !is_night ) { + if ( !is_night ) + { theta = static_cast(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration; - } else { - theta = static_cast(osg::PI) * (1.f - (adjustedHour - adjustedNightStart) / nightDuration); + } + else + { + theta = static_cast(osg::PI) + static_cast(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration; } osg::Vec3f final( @@ -711,7 +749,7 @@ void WeatherManager::update(float duration, bool paused, const TimeStamp& time, mRendering.setSunDirection( final * -1 ); } - float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings); + float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog"); float peakHour = mSunriseTime + (mSunsetTime - mSunriseTime) / 2; if (time.getHour() < mSunriseTime || time.getHour() > mSunsetTime) @@ -784,7 +822,7 @@ unsigned int WeatherManager::getWeatherID() const bool WeatherManager::useTorches(float hour) const { - bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart - 1; + bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart; return isDark && !mPrecipitation; } @@ -1060,20 +1098,25 @@ inline void WeatherManager::calculateResult(const int weatherID, const float gam mResult.mParticleEffect = current.mParticleEffect; mResult.mRainEffect = current.mRainEffect; - mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart - 1); + mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration); - mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings); + mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog"); + mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient"); + mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun"); + mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky"); + mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars"); mResult.mDLFogFactor = current.mDL.FogFactor; mResult.mDLFogOffset = current.mDL.FogOffset; - mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings); - mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings); - mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings); - mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings); - mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings); - if (gameHour >= mSunsetTime - mSunPreSunsetTime) + WeatherSetting setting = mTimeSettings.getSetting("Sun"); + float preSunsetTime = setting.mPreSunsetTime; + + if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime) { - float factor = (gameHour - (mSunsetTime - mSunPreSunsetTime)) / mSunPreSunsetTime; + float factor = 1.f; + if (preSunsetTime > 0) + factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime; factor = std::min(1.f, factor); mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor); // The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because @@ -1087,15 +1130,17 @@ inline void WeatherManager::calculateResult(const int weatherID, const float gam else mResult.mSunDiscColor = osg::Vec4f(1,1,1,1); - if (gameHour >= mSunsetTime) + if (gameHour >= mTimeSettings.mDayEnd) { - float fade = std::min(1.f, (gameHour - mSunsetTime) / 2.f); + // sunset + float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd)); fade = fade*fade; mResult.mSunDiscColor.a() = 1.f - fade; } - else if (gameHour >= mSunriseTime && gameHour <= mSunriseTime + 1) + else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f) { - mResult.mSunDiscColor.a() = gameHour - mSunriseTime; + // sunrise + mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd; } else mResult.mSunDiscColor.a() = 1; diff --git a/apps/openmw/mwworld/weather.hpp b/apps/openmw/mwworld/weather.hpp index 4be0d3e20..cf6868356 100644 --- a/apps/openmw/mwworld/weather.hpp +++ b/apps/openmw/mwworld/weather.hpp @@ -7,6 +7,8 @@ #include +#include + #include "../mwbase/soundmanager.hpp" #include "../mwrender/sky.hpp" @@ -38,6 +40,13 @@ namespace MWWorld { class TimeStamp; + struct WeatherSetting + { + float mPreSunriseTime; + float mPostSunriseTime; + float mPreSunsetTime; + float mPostSunsetTime; + }; struct TimeOfDaySettings { @@ -45,7 +54,37 @@ namespace MWWorld float mNightEnd; float mDayStart; float mDayEnd; - float mSunriseTime; + + std::map mSunriseTransitions; + + float mStarsPostSunsetStart; + float mStarsPreSunriseFinish; + float mStarsFadingDuration; + + WeatherSetting getSetting(const std::string& type) const + { + std::map::const_iterator it = mSunriseTransitions.find(type); + if (it != mSunriseTransitions.end()) + { + return it->second; + } + else + { + return { 1.f, 1.f, 1.f, 1.f }; + } + } + + void addSetting(const Fallback::Map& fallback, const std::string& type) + { + WeatherSetting setting = { + fallback.getFallbackFloat("Weather_" + type + "_Pre-Sunrise_Time"), + fallback.getFallbackFloat("Weather_" + type + "_Post-Sunrise_Time"), + fallback.getFallbackFloat("Weather_" + type + "_Pre-Sunset_Time"), + fallback.getFallbackFloat("Weather_" + type + "_Post-Sunset_Time") + }; + + mSunriseTransitions[type] = setting; + } }; /// Interpolates between 4 data points (sunrise, day, sunset, night) based on the time of day. @@ -59,7 +98,7 @@ namespace MWWorld { } - T getValue (const float gameHour, const TimeOfDaySettings& timeSettings) const; + T getValue (const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const; private: T mSunriseValue, mDayValue, mSunsetValue, mNightValue; From 5ead6353ac38cd3df0a09287034ee69da8bee554 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 7 Jun 2018 16:44:46 +0400 Subject: [PATCH 101/282] Add missing changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd1acff2a..f36636571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ 0.45.0 ------ + Bug #1990: Sunrise/sunset not set correct Bug #2835: Player able to slowly move when overencumbered Bug #3374: Touch spells not hitting kwama foragers Bug #3591: Angled hit distance too low From ae87e0d3fcbd1ba6348f51fcbb05a693741deda3 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sat, 9 Jun 2018 15:34:08 +0400 Subject: [PATCH 102/282] Do not reset mUpperBodyState for weapon->weapon switch (regression #4446) --- apps/openmw/mwmechanics/character.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index a0aad8b1b..07e5fa7d6 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1214,7 +1214,6 @@ bool CharacterController::updateWeaponState() bool animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); if (!animPlaying || complete >= 1.0f) { - mUpperBodyState = UpperCharState_Nothing; forcestateupdate = true; mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype)); From fba0c155df01481b8f6bf534b9c20c5ebeb903c4 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sat, 9 Jun 2018 17:07:38 +0400 Subject: [PATCH 103/282] Fix assertion fail related to NiLookAtController --- components/nif/controller.cpp | 4 ++-- components/nif/controller.hpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/nif/controller.cpp b/components/nif/controller.cpp index ddfa02a09..49f591b47 100644 --- a/components/nif/controller.cpp +++ b/components/nif/controller.cpp @@ -104,13 +104,13 @@ namespace Nif void NiLookAtController::read(NIFStream *nif) { Controller::read(nif); - data.read(nif); + target.read(nif); } void NiLookAtController::post(NIFFile *nif) { Controller::post(nif); - data.post(nif); + target.post(nif); } void NiPathController::read(NIFStream *nif) diff --git a/components/nif/controller.hpp b/components/nif/controller.hpp index ce8bff041..f22d10622 100644 --- a/components/nif/controller.hpp +++ b/components/nif/controller.hpp @@ -102,7 +102,7 @@ public: class NiLookAtController : public Controller { public: - NiKeyframeDataPtr data; + NodePtr target; void read(NIFStream *nif); void post(NIFFile *nif); From 8f45b0d53a3c398df88d83681c24a25863b910ba Mon Sep 17 00:00:00 2001 From: wareya Date: Sat, 9 Jun 2018 10:11:43 -0400 Subject: [PATCH 104/282] remove unnecessary conditions --- components/esmterrain/storage.cpp | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index a1b4856e1..fd04d90b9 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -434,19 +434,14 @@ namespace ESMTerrain const int imageScaleFactor = 2; const int blendmapImageSize = blendmapSize * imageScaleFactor; - - const bool largeImage = true; - for (int i=0; i image (new osg::Image); - if(!largeImage) - image->allocateImage(blendmapSize, blendmapSize, 1, format, GL_UNSIGNED_BYTE); - else - image->allocateImage(blendmapImageSize, blendmapImageSize, 1, format, GL_UNSIGNED_BYTE); + image->allocateImage(blendmapImageSize, blendmapImageSize, 1, format, GL_UNSIGNED_BYTE); unsigned char* pData = image->data(); + for (int y=0; y Date: Sat, 9 Jun 2018 10:31:51 -0400 Subject: [PATCH 105/282] remove indentation from blank lines --- components/esmterrain/storage.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index fd04d90b9..dadc64f57 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -433,7 +433,7 @@ namespace ESMTerrain // We need to upscale the blendmap 2x with nearest neighbor sampling to look like Vanilla const int imageScaleFactor = 2; const int blendmapImageSize = blendmapSize * imageScaleFactor; - + for (int i=0; i image (new osg::Image); image->allocateImage(blendmapImageSize, blendmapImageSize, 1, format, GL_UNSIGNED_BYTE); unsigned char* pData = image->data(); - + for (int y=0; ysecond; int blendIndex = (pack ? static_cast(std::floor((layerIndex - 1) / 4.f)) : layerIndex - 1); int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0; - + int alpha = (blendIndex == i) ? 255 : 0; - + int realY = (blendmapSize - y - 1)*imageScaleFactor; int realX = x*imageScaleFactor; - + pData[((realY+0)*blendmapImageSize + realX + 0)*channels + channel] = alpha; pData[((realY+1)*blendmapImageSize + realX + 0)*channels + channel] = alpha; pData[((realY+0)*blendmapImageSize + realX + 1)*channels + channel] = alpha; From a9ca528fb8a6afdb2d88cbde023e84a862e74004 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sat, 9 Jun 2018 19:42:24 -0500 Subject: [PATCH 106/282] Adding version number of macOS build of OpenMW --- files/mac/openmw-Info.plist.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/files/mac/openmw-Info.plist.in b/files/mac/openmw-Info.plist.in index b4a8af480..7583b45ad 100644 --- a/files/mac/openmw-Info.plist.in +++ b/files/mac/openmw-Info.plist.in @@ -18,6 +18,8 @@ APPL CFBundleSignature ???? + CFBundleShortVersionString + ${OPENMW_VERSION} CFBundleVersion ${OPENMW_VERSION} CSResourcesFileMapped From dbc87e7c7dc9d7adebda34d0458b972a682038ec Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sun, 10 Jun 2018 11:28:22 +0200 Subject: [PATCH 107/282] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd1acff2a..a3101056e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Bug #3897: Have Goodbye give all choices the effects of Goodbye Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID + Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully Bug #4221: Characters get stuck in V-shaped terrain Bug #4251: Stationary NPCs do not return to their position after combat Bug #4293: Faction members are not aware of faction ownerships in barter From 0375bedab229358b8fe04c29cd16d382623f4370 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 23 Nov 2017 19:57:36 +0400 Subject: [PATCH 108/282] Equip previous item after a bound item expires (bug #2326) --- apps/openmw/mwmechanics/actors.cpp | 106 +++++++++++++++++++++++------ apps/openmw/mwmechanics/actors.hpp | 4 ++ 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 7990373a7..10e9b7afc 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -50,27 +50,36 @@ bool isConscious(const MWWorld::Ptr& ptr) return !stats.isDead() && !stats.getKnockedDown(); } -void adjustBoundItem (const std::string& item, bool bound, const MWWorld::Ptr& actor) +int getBoundItemSlot (const std::string& itemId) { - if (bound) + static std::map boundItemsMap; + if (boundItemsMap.empty()) { - if (actor.getClass().getContainerStore(actor).count(item) == 0) - { - MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); - MWWorld::Ptr newPtr = *store.MWWorld::ContainerStore::add(item, 1, actor); - MWWorld::ActionEquip action(newPtr); - action.execute(actor); - MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - // change draw state only if the item is in player's right hand - if (actor == MWMechanics::getPlayer() - && rightHand != store.end() && newPtr == *rightHand) - { - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); - } - } + std::string boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundBootsID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Boots; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundCuirassID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Cuirass; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundLeftGauntletID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_LeftGauntlet; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundRightGauntletID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_RightGauntlet; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundHelmID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Helmet; + + boundId = MWBase::Environment::get().getWorld()->getStore().get().find("sMagicBoundShieldID")->getString(); + boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_CarriedLeft; } - else - actor.getClass().getInventoryStore(actor).remove(item, 1, actor, true); + + int slot = MWWorld::InventoryStore::Slot_CarriedRight; + std::map::iterator it = boundItemsMap.find(itemId); + if (it != boundItemsMap.end()) + slot = it->second; + + return slot; } class CheckActorCommanded : public MWMechanics::EffectSourceVisitor @@ -139,7 +148,6 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float namespace MWMechanics { - const float aiProcessingDistance = 7168; const float sqrAiProcessingDistance = aiProcessingDistance*aiProcessingDistance; @@ -227,6 +235,65 @@ namespace MWMechanics } }; + void Actors::adjustBoundItem (const std::string& itemId, bool bound, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + + if (bound) + { + if (actor.getClass().getContainerStore(actor).count(itemId) != 0) + return; + + int slot = getBoundItemSlot(itemId); + + MWWorld::Ptr prevItem = *store.getSlot(slot); + + MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); + MWWorld::ActionEquip action(boundPtr); + action.execute(actor); + + if (actor != MWMechanics::getPlayer()) + return; + + MWWorld::Ptr newItem = *store.getSlot(slot); + + if (newItem.isEmpty() || boundPtr != newItem) + return; + + // change draw state only if the item is in player's right hand + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + + mPreviousItems[slot] = std::make_pair(itemId, prevItem.isEmpty() ? "" : prevItem.getCellRef().getRefId()); + } + else + { + store.remove(itemId, 1, actor, true); + + if (actor != MWMechanics::getPlayer()) + return; + + int slot = getBoundItemSlot(itemId); + + std::pair prevItem = mPreviousItems[slot]; + + if (prevItem.first != itemId) + return; + + MWWorld::Ptr ptr = MWWorld::Ptr(); + if (prevItem.second != "") + ptr = store.search (prevItem.second); + + mPreviousItems.erase(slot); + + if (ptr.isEmpty()) + return; + + MWWorld::ActionEquip action(ptr); + action.execute(actor); + } + } + void Actors::updateActor (const MWWorld::Ptr& ptr, float duration) { // magic effects @@ -1875,6 +1942,7 @@ namespace MWMechanics } mActors.clear(); mDeathCount.clear(); + mPreviousItems.clear(); } void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 15f2d3dc8..f0e157db1 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -25,6 +25,10 @@ namespace MWMechanics class Actors { std::map mDeathCount; + typedef std::map> PreviousItems; + PreviousItems mPreviousItems; + + void adjustBoundItem (const std::string& itemId, bool bound, const MWWorld::Ptr& actor); void updateNpc(const MWWorld::Ptr &ptr, float duration); From 9b72a6ac69ddd4b282ff3c9ed46ca4d8b7cde44c Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 27 Nov 2017 13:00:14 +0400 Subject: [PATCH 109/282] Use the MWWorld::Ptr() instead of string ID --- apps/openmw/mwmechanics/actors.cpp | 23 +++++++++++++++-------- apps/openmw/mwmechanics/actors.hpp | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 10e9b7afc..76c5744b3 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -264,7 +264,7 @@ namespace MWMechanics if (slot == MWWorld::InventoryStore::Slot_CarriedRight) MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); - mPreviousItems[slot] = std::make_pair(itemId, prevItem.isEmpty() ? "" : prevItem.getCellRef().getRefId()); + mPreviousItems[slot] = std::make_pair(itemId, prevItem); } else { @@ -275,21 +275,28 @@ namespace MWMechanics int slot = getBoundItemSlot(itemId); - std::pair prevItem = mPreviousItems[slot]; + std::pair prevItem = mPreviousItems[slot]; if (prevItem.first != itemId) return; - MWWorld::Ptr ptr = MWWorld::Ptr(); - if (prevItem.second != "") - ptr = store.search (prevItem.second); - mPreviousItems.erase(slot); - if (ptr.isEmpty()) + if (prevItem.second.isEmpty()) + return; + + // check if the item is still in the player's inventory + MWWorld::ContainerStoreIterator it = store.begin(); + for (; it != store.end(); ++it) + { + if (*it == prevItem.second) + break; + } + + if (it == store.end()) return; - MWWorld::ActionEquip action(ptr); + MWWorld::ActionEquip action(prevItem.second); action.execute(actor); } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index f0e157db1..40e9e1d21 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -25,7 +25,7 @@ namespace MWMechanics class Actors { std::map mDeathCount; - typedef std::map> PreviousItems; + typedef std::map> PreviousItems; PreviousItems mPreviousItems; void adjustBoundItem (const std::string& itemId, bool bound, const MWWorld::Ptr& actor); From d1b1cb748d94cda070c5383973329613620097d0 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 24 Dec 2017 16:26:43 +0400 Subject: [PATCH 110/282] Reequip previous item only if the expired bound item was equipped --- apps/openmw/mwmechanics/actors.cpp | 41 +++++++++++++++--------------- apps/openmw/mwmechanics/actors.hpp | 2 +- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 76c5744b3..4019067e3 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -238,15 +238,14 @@ namespace MWMechanics void Actors::adjustBoundItem (const std::string& itemId, bool bound, const MWWorld::Ptr& actor) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + int slot = getBoundItemSlot(itemId); if (bound) { if (actor.getClass().getContainerStore(actor).count(itemId) != 0) return; - int slot = getBoundItemSlot(itemId); - - MWWorld::Ptr prevItem = *store.getSlot(slot); + MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); MWWorld::ActionEquip action(boundPtr); @@ -264,39 +263,40 @@ namespace MWMechanics if (slot == MWWorld::InventoryStore::Slot_CarriedRight) MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); - mPreviousItems[slot] = std::make_pair(itemId, prevItem); + if (prevItem != store.end()) + mPreviousItems[itemId] = *prevItem; } else { + MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); + + bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual((*currentItem).getCellRef().getRefId(), itemId); + store.remove(itemId, 1, actor, true); if (actor != MWMechanics::getPlayer()) return; - int slot = getBoundItemSlot(itemId); + MWWorld::Ptr prevItem = mPreviousItems[itemId]; - std::pair prevItem = mPreviousItems[slot]; + mPreviousItems.erase(itemId); - if (prevItem.first != itemId) - return; - - mPreviousItems.erase(slot); - - if (prevItem.second.isEmpty()) + if (prevItem.isEmpty()) return; // check if the item is still in the player's inventory MWWorld::ContainerStoreIterator it = store.begin(); for (; it != store.end(); ++it) { - if (*it == prevItem.second) + if (*it == prevItem) break; } - if (it == store.end()) + // we should equip previous item only if expired bound item was equipped. + if (it == store.end() || !wasEquipped) return; - MWWorld::ActionEquip action(prevItem.second); + MWWorld::ActionEquip action(prevItem); action.execute(actor); } } @@ -830,9 +830,15 @@ namespace MWMechanics float magnitude = effects.get(it->first).getMagnitude(); if (found != (magnitude > 0)) { + if (magnitude > 0) + creatureStats.mBoundItems.insert(it->first); + else + creatureStats.mBoundItems.erase(it->first); + std::string itemGmst = it->second; std::string item = MWBase::Environment::get().getWorld()->getStore().get().find( itemGmst)->getString(); + if (it->first == ESM::MagicEffect::BoundGloves) { item = MWBase::Environment::get().getWorld()->getStore().get().find( @@ -844,11 +850,6 @@ namespace MWMechanics } else adjustBoundItem(item, magnitude > 0, ptr); - - if (magnitude > 0) - creatureStats.mBoundItems.insert(it->first); - else - creatureStats.mBoundItems.erase(it->first); } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 40e9e1d21..e696abf01 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -25,7 +25,7 @@ namespace MWMechanics class Actors { std::map mDeathCount; - typedef std::map> PreviousItems; + typedef std::map PreviousItems; PreviousItems mPreviousItems; void adjustBoundItem (const std::string& itemId, bool bound, const MWWorld::Ptr& actor); From 4de9d9fa778402eff3de8c9e8d56b44132aaca80 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sun, 24 Dec 2017 17:18:16 +0400 Subject: [PATCH 111/282] Split adjustBoundItem() --- apps/openmw/mwmechanics/actors.cpp | 96 +++++++++++++++--------------- apps/openmw/mwmechanics/actors.hpp | 3 +- 2 files changed, 49 insertions(+), 50 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 4019067e3..d1e74e576 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -235,70 +235,71 @@ namespace MWMechanics } }; - void Actors::adjustBoundItem (const std::string& itemId, bool bound, const MWWorld::Ptr& actor) + void Actors::addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); int slot = getBoundItemSlot(itemId); - if (bound) - { - if (actor.getClass().getContainerStore(actor).count(itemId) != 0) - return; + if (actor.getClass().getContainerStore(actor).count(itemId) != 0) + return; - MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); + MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot); - MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); - MWWorld::ActionEquip action(boundPtr); - action.execute(actor); + MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor); + MWWorld::ActionEquip action(boundPtr); + action.execute(actor); - if (actor != MWMechanics::getPlayer()) - return; + if (actor != MWMechanics::getPlayer()) + return; - MWWorld::Ptr newItem = *store.getSlot(slot); + MWWorld::Ptr newItem = *store.getSlot(slot); - if (newItem.isEmpty() || boundPtr != newItem) - return; + if (newItem.isEmpty() || boundPtr != newItem) + return; - // change draw state only if the item is in player's right hand - if (slot == MWWorld::InventoryStore::Slot_CarriedRight) - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + // change draw state only if the item is in player's right hand + if (slot == MWWorld::InventoryStore::Slot_CarriedRight) + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); - if (prevItem != store.end()) - mPreviousItems[itemId] = *prevItem; - } - else - { - MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); + if (prevItem != store.end()) + mPreviousItems[itemId] = *prevItem; + } - bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual((*currentItem).getCellRef().getRefId(), itemId); + void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) + { + MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); + int slot = getBoundItemSlot(itemId); - store.remove(itemId, 1, actor, true); + MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); - if (actor != MWMechanics::getPlayer()) - return; + bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual((*currentItem).getCellRef().getRefId(), itemId); - MWWorld::Ptr prevItem = mPreviousItems[itemId]; + store.remove(itemId, 1, actor, true); - mPreviousItems.erase(itemId); + if (actor != MWMechanics::getPlayer()) + return; - if (prevItem.isEmpty()) - return; + MWWorld::Ptr prevItem = mPreviousItems[itemId]; - // check if the item is still in the player's inventory - MWWorld::ContainerStoreIterator it = store.begin(); - for (; it != store.end(); ++it) - { - if (*it == prevItem) - break; - } + mPreviousItems.erase(itemId); - // we should equip previous item only if expired bound item was equipped. - if (it == store.end() || !wasEquipped) - return; + if (prevItem.isEmpty()) + return; - MWWorld::ActionEquip action(prevItem); - action.execute(actor); + // check if the item is still in the player's inventory + MWWorld::ContainerStoreIterator it = store.begin(); + for (; it != store.end(); ++it) + { + if (*it == prevItem) + break; } + + // we should equip previous item only if expired bound item was equipped. + if (it == store.end() || !wasEquipped) + return; + + MWWorld::ActionEquip action(prevItem); + action.execute(actor); } void Actors::updateActor (const MWWorld::Ptr& ptr, float duration) @@ -839,17 +840,14 @@ namespace MWMechanics std::string item = MWBase::Environment::get().getWorld()->getStore().get().find( itemGmst)->getString(); + magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); + if (it->first == ESM::MagicEffect::BoundGloves) { - item = MWBase::Environment::get().getWorld()->getStore().get().find( - "sMagicBoundLeftGauntletID")->getString(); - adjustBoundItem(item, magnitude > 0, ptr); item = MWBase::Environment::get().getWorld()->getStore().get().find( "sMagicBoundRightGauntletID")->getString(); - adjustBoundItem(item, magnitude > 0, ptr); + magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr); } - else - adjustBoundItem(item, magnitude > 0, ptr); } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index e696abf01..b3e1f95db 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -28,7 +28,8 @@ namespace MWMechanics typedef std::map PreviousItems; PreviousItems mPreviousItems; - void adjustBoundItem (const std::string& itemId, bool bound, const MWWorld::Ptr& actor); + void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); + void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); void updateNpc(const MWWorld::Ptr &ptr, float duration); From f977c6876ff391cc4624483647bf3197791482c5 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 23 Feb 2018 14:48:40 +0400 Subject: [PATCH 112/282] Bound items: store item ID instead of pointer --- apps/openmw/mwmechanics/actors.cpp | 28 ++++++++++++++++++---------- apps/openmw/mwmechanics/actors.hpp | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index d1e74e576..8a13b492f 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -262,7 +262,7 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); if (prevItem != store.end()) - mPreviousItems[itemId] = *prevItem; + mPreviousItems[itemId] = (*prevItem).getCellRef().getRefId(); } void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) @@ -279,26 +279,34 @@ namespace MWMechanics if (actor != MWMechanics::getPlayer()) return; - MWWorld::Ptr prevItem = mPreviousItems[itemId]; + std::string prevItemId = mPreviousItems[itemId]; mPreviousItems.erase(itemId); - if (prevItem.isEmpty()) + if (prevItemId.empty()) return; - // check if the item is still in the player's inventory - MWWorld::ContainerStoreIterator it = store.begin(); - for (; it != store.end(); ++it) + // Find the item by id + MWWorld::Ptr item; + for (MWWorld::ContainerStoreIterator iter = store.begin(); iter != store.end(); ++iter) { - if (*it == prevItem) - break; + if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), prevItemId)) + { + if (item.isEmpty() || + // Prefer the stack with the lowest remaining uses + !item.getClass().hasItemHealth(*iter) || + iter->getClass().getItemHealth(*iter) < item.getClass().getItemHealth(item)) + { + item = *iter; + } + } } // we should equip previous item only if expired bound item was equipped. - if (it == store.end() || !wasEquipped) + if (item.isEmpty() || !wasEquipped) return; - MWWorld::ActionEquip action(prevItem); + MWWorld::ActionEquip action(item); action.execute(actor); } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index b3e1f95db..90b646f0e 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -25,7 +25,7 @@ namespace MWMechanics class Actors { std::map mDeathCount; - typedef std::map PreviousItems; + typedef std::map PreviousItems; PreviousItems mPreviousItems; void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); From 9fd2d57b8663dee2df8ca739f15d7b39b769b3a6 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 9 Mar 2018 08:56:04 +0400 Subject: [PATCH 113/282] Move previous items to player --- apps/openmw/mwmechanics/actors.cpp | 14 ++++++++------ apps/openmw/mwmechanics/actors.hpp | 3 --- apps/openmw/mwworld/player.cpp | 16 ++++++++++++++++ apps/openmw/mwworld/player.hpp | 9 +++++++++ 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 8a13b492f..6549c169b 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -262,7 +262,10 @@ namespace MWMechanics MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); if (prevItem != store.end()) - mPreviousItems[itemId] = (*prevItem).getCellRef().getRefId(); + { + MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); + player->setPreviousItem(itemId, prevItem->getCellRef().getRefId()); + } } void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) @@ -272,16 +275,16 @@ namespace MWMechanics MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot); - bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual((*currentItem).getCellRef().getRefId(), itemId); + bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId); store.remove(itemId, 1, actor, true); if (actor != MWMechanics::getPlayer()) return; - std::string prevItemId = mPreviousItems[itemId]; - - mPreviousItems.erase(itemId); + MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); + std::string prevItemId = player->getPreviousItem(itemId); + player->erasePreviousItem(itemId); if (prevItemId.empty()) return; @@ -1956,7 +1959,6 @@ namespace MWMechanics } mActors.clear(); mDeathCount.clear(); - mPreviousItems.clear(); } void Actors::updateMagicEffects(const MWWorld::Ptr &ptr) diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 90b646f0e..0de1f4d6c 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include "../mwbase/world.hpp" @@ -25,8 +24,6 @@ namespace MWMechanics class Actors { std::map mDeathCount; - typedef std::map PreviousItems; - PreviousItems mPreviousItems; void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor); diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 34c5f713d..193663098 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -287,6 +287,7 @@ namespace MWWorld mAttackingOrSpell = false; mCurrentCrimeId = -1; mPaidCrimeId = -1; + mPreviousItems.clear(); mLastKnownExteriorPosition = osg::Vec3f(0,0,0); for (int i=0; i + #include "../mwworld/refdata.hpp" #include "../mwworld/livecellref.hpp" @@ -46,6 +48,9 @@ namespace MWWorld int mCurrentCrimeId; // the id assigned witnesses int mPaidCrimeId; // the last id paid off (0 bounty) + typedef std::map PreviousItems; // previous equipped items, needed for bound spells + PreviousItems mPreviousItems; + // Saved stats prior to becoming a werewolf MWMechanics::SkillValue mSaveSkills[ESM::Skill::Length]; MWMechanics::AttributeValue mSaveAttributes[ESM::Attribute::Length]; @@ -120,6 +125,10 @@ namespace MWWorld int getNewCrimeId(); // get new id for witnesses void recordCrimeId(); // record the paid crime id when bounty is 0 int getCrimeId() const; // get the last paid crime id + + void setPreviousItem(const std::string& boundItemId, const std::string& previousItemId); + std::string getPreviousItem(const std::string& boundItemId); + void erasePreviousItem(const std::string& boundItemId); }; } #endif From acd3cba5fae441602389bd2dbef4579e8e70a842 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 9 Mar 2018 10:20:17 +0400 Subject: [PATCH 114/282] Store previous items in the savegame --- CHANGELOG.md | 1 + apps/openmw/mwworld/player.cpp | 4 ++++ components/esm/player.cpp | 18 ++++++++++++++++++ components/esm/player.hpp | 3 +++ components/esm/savedgame.cpp | 2 +- 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3101056e..a19b012c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ 0.45.0 ------ + Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped Bug #2835: Player able to slowly move when overencumbered Bug #3374: Touch spells not hitting kwama foragers Bug #3591: Angled hit distance too low diff --git a/apps/openmw/mwworld/player.cpp b/apps/openmw/mwworld/player.cpp index 193663098..5439447fd 100644 --- a/apps/openmw/mwworld/player.cpp +++ b/apps/openmw/mwworld/player.cpp @@ -342,6 +342,8 @@ namespace MWWorld for (int i=0; ifirst); + esm.writeHNString ("PREV", it->second); + } + for (int i=0; i mSaveAttributes[ESM::Attribute::Length]; StatState mSaveSkills[ESM::Skill::Length]; + typedef std::map PreviousItems; // previous equipped items, needed for bound spells + PreviousItems mPreviousItems; + void load (ESMReader &esm); void save (ESMWriter &esm) const; }; diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index c96261c64..ea9fef4fb 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm/savedgame.cpp @@ -5,7 +5,7 @@ #include "defs.hpp" unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 4; +int ESM::SavedGame::sCurrentFormat = 5; void ESM::SavedGame::load (ESMReader &esm) { From ab03d238bbacb0ae3ef967d005e6bc375f99ed3b Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sun, 10 Jun 2018 07:43:38 -0500 Subject: [PATCH 115/282] Adding Feature #4345 Implemented as part of #1623, but not added. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3101056e..8c8a7eb90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Feature #4345: Add equivalents for the command line commands to Launcher Feature #4444: Per-group KF-animation files support 0.44.0 From d43766d3c9936c3875c1f2bcd0d88c68558dc9f6 Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Sun, 10 Jun 2018 17:47:05 +0300 Subject: [PATCH 116/282] Make WakeUpPC interrupt waiting if it was supposed to be (fixes #3629) --- CHANGELOG.md | 1 + apps/openmw/mwgui/waitdialog.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3101056e..5a22b9d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Bug #2835: Player able to slowly move when overencumbered Bug #3374: Touch spells not hitting kwama foragers Bug #3591: Angled hit distance too low + Bug #3629: DB assassin attack never triggers creature spawning Bug #3897: Have Goodbye give all choices the effects of Goodbye Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID diff --git a/apps/openmw/mwgui/waitdialog.cpp b/apps/openmw/mwgui/waitdialog.cpp index 61febf315..52575e25c 100644 --- a/apps/openmw/mwgui/waitdialog.cpp +++ b/apps/openmw/mwgui/waitdialog.cpp @@ -310,8 +310,10 @@ namespace MWGui void WaitDialog::wakeUp () { mSleeping = false; - mTimeAdvancer.stop(); - stopWaiting(); + if (mInterruptAt != -1) + onWaitingInterrupted(); + else + stopWaiting(); } } From 66a46ff03cf997197dcbfd9e7494de716cee58c4 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 11 Jun 2018 11:22:56 +0400 Subject: [PATCH 117/282] Do not show any book text after last
tag. --- CHANGELOG.md | 1 + apps/openmw/mwgui/formatting.cpp | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3101056e..356be37a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully + Bug #4215: OpenMW shows book text after last
tag Bug #4221: Characters get stuck in V-shaped terrain Bug #4251: Stationary NPCs do not return to their position after combat Bug #4293: Faction members are not aware of faction ownerships in barter diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index a9319048e..16568e2f3 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -31,6 +31,14 @@ namespace MWGui boost::algorithm::replace_all(mText, "\r", ""); + // vanilla game does not show any text after last
tag. + const std::string lowerText = Misc::StringUtils::lowerCase(mText); + int index = lowerText.rfind("
"); + if (index == -1) + mText = ""; + else + mText = mText.substr(0, index+4); + registerTag("br", Event_BrTag); registerTag("p", Event_PTag); registerTag("img", Event_ImgTag); From e234dd2a36b1a66eb407b967b003f50b0cdce959 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 11 Jun 2018 17:18:51 +0400 Subject: [PATCH 118/282] Do not interrupt scripted animations --- apps/openmw/mwmechanics/character.cpp | 34 ++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 07e5fa7d6..71d02d59a 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -561,6 +561,14 @@ void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterStat void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force) { + // If the current animation is persistent, do not touch it + if (!mAnimQueue.empty()) + { + AnimationQueueEntry& first = mAnimQueue.front(); + if (first.mPersist) + return; + } + if (mPtr.getClass().isActor()) refreshHitRecoilAnims(); @@ -2135,6 +2143,14 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int if(!mAnimation || !mAnimation->hasAnimation(groupname)) return false; + // We should not interrupt persistent animations by non-persistent ones + if (!mAnimQueue.empty()) + { + AnimationQueueEntry& first = mAnimQueue.front(); + if (first.mPersist && !persist) + return false; + } + // If this animation is a looped animation (has a "loop start" key) that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count // and remove any other animations that were queued. @@ -2199,9 +2215,21 @@ bool CharacterController::isAnimPlaying(const std::string &groupName) void CharacterController::clearAnimQueue() { - if(!mAnimQueue.empty()) - mAnimation->disable(mAnimQueue.front().mGroup); - mAnimQueue.clear(); + // Do not interrupt scripted animations + if (!mAnimQueue.empty()) + { + AnimationQueueEntry& first = mAnimQueue.front(); + if (!first.mPersist) + mAnimation->disable(mAnimQueue.front().mGroup); + } + + for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) + { + if (!it->mPersist) + it = mAnimQueue.erase(it); + else + ++it; + } } void CharacterController::forceStateUpdate() From 6099735c608e90b33a6bc2ab6ad74fd464b08099 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 11 Jun 2018 17:52:58 +0400 Subject: [PATCH 119/282] Early out only when scripted animation is playing --- apps/openmw/mwmechanics/character.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 71d02d59a..795675fb3 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -565,7 +565,7 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat if (!mAnimQueue.empty()) { AnimationQueueEntry& first = mAnimQueue.front(); - if (first.mPersist) + if (first.mPersist && isAnimPlaying(first.mGroup)) return; } From d0619cfb3500a43bcf6019a0f2e7c206a360f447 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 11 Jun 2018 18:52:20 +0400 Subject: [PATCH 120/282] Play death animation for non-persisting actors with 0 health (bug #4291) --- apps/openmw/mwmechanics/character.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 795675fb3..daab8cdeb 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2023,7 +2023,11 @@ void CharacterController::update(float duration) // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) { - playDeath(1.f, mDeathState); + // Fast-forward death animation to end for persisting corpses + if (cls.isPersistent(mPtr)) + playDeath(1.f, mDeathState); + else + playDeath(0.f, mDeathState); } // We must always queue movement, even if there is none, to apply gravity. world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); @@ -2239,6 +2243,7 @@ void CharacterController::forceStateUpdate() clearAnimQueue(); refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); + if(mDeathState != CharState_None) { playRandomDeath(); From a42c663fd70b0b44289fe7a00481d517204133d5 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 11 Jun 2018 18:53:25 +0400 Subject: [PATCH 121/282] Do not interrupt scripted animations by death animation (bug #4286) --- apps/openmw/mwmechanics/character.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index daab8cdeb..d0ffe742a 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -728,6 +728,14 @@ void CharacterController::playRandomDeath(float startpoint) MWBase::Environment::get().getWorld()->useDeathCamera(); } + // Do not interrupt scripted animation by death + if (!mAnimQueue.empty()) + { + AnimationQueueEntry& first = mAnimQueue.front(); + if (first.mPersist && isAnimPlaying(first.mGroup)) + return; + } + if(mHitState == CharState_SwimKnockDown && mAnimation->hasAnimation("swimdeathknockdown")) { mDeathState = CharState_SwimDeathKnockDown; From 977a27ecb76d9fe3da1194c6dc73b52cb5b82d45 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 11 Jun 2018 22:29:32 +0400 Subject: [PATCH 122/282] Do not clear corpses until end of death animation (bug #4307) --- apps/openmw/mwclass/creature.cpp | 5 ++++- apps/openmw/mwclass/npc.cpp | 5 ++++- apps/openmw/mwmechanics/character.cpp | 7 ++----- apps/openmw/mwworld/cellstore.cpp | 7 ++++++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 2e16b13aa..27a20a0f5 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -136,7 +136,7 @@ namespace MWClass data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); if (data->mCreatureStats.isDead()) - data->mCreatureStats.setDeathAnimationFinished(true); + data->mCreatureStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr)); // spells for (std::vector::const_iterator iter (ref->mBase->mSpells.mList.begin()); @@ -814,6 +814,9 @@ namespace MWClass if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) return; + if (!creatureStats.isDeathAnimationFinished()) + return; + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->getFloat(); diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 8e8b5c3ad..172e4cc8f 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -353,7 +353,7 @@ namespace MWClass data->mNpcStats.setNeedRecalcDynamicStats(true); } if (data->mNpcStats.isDead()) - data->mNpcStats.setDeathAnimationFinished(true); + data->mNpcStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr)); // race powers const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(ref->mBase->mRace); @@ -1351,6 +1351,9 @@ namespace MWClass if (ptr.getRefData().getCount() > 0 && !creatureStats.isDead()) return; + if (!creatureStats.isDeathAnimationFinished()) + return; + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); static const float fCorpseRespawnDelay = gmst.find("fCorpseRespawnDelay")->getFloat(); static const float fCorpseClearDelay = gmst.find("fCorpseClearDelay")->getFloat(); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index d0ffe742a..fd7abb571 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2029,13 +2029,10 @@ void CharacterController::update(float duration) { // initial start of death animation for actors that started the game as dead // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag - if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) + if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty() && cls.isPersistent(mPtr)) { // Fast-forward death animation to end for persisting corpses - if (cls.isPersistent(mPtr)) - playDeath(1.f, mDeathState); - else - playDeath(0.f, mDeathState); + playDeath(1.f, mDeathState); } // We must always queue movement, even if there is none, to apply gravity. world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); diff --git a/apps/openmw/mwworld/cellstore.cpp b/apps/openmw/mwworld/cellstore.cpp index 1b6495c11..fc3c2e245 100644 --- a/apps/openmw/mwworld/cellstore.cpp +++ b/apps/openmw/mwworld/cellstore.cpp @@ -945,8 +945,13 @@ namespace MWWorld { const MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); static const float fCorpseClearDelay = MWBase::Environment::get().getWorld()->getStore().get().find("fCorpseClearDelay")->getFloat(); - if (creatureStats.isDead() && !ptr.getClass().isPersistent(ptr) && creatureStats.getTimeOfDeath() + fCorpseClearDelay <= MWBase::Environment::get().getWorld()->getTimeStamp()) + if (creatureStats.isDead() && + creatureStats.isDeathAnimationFinished() && + !ptr.getClass().isPersistent(ptr) && + creatureStats.getTimeOfDeath() + fCorpseClearDelay <= MWBase::Environment::get().getWorld()->getTimeStamp()) + { MWBase::Environment::get().getWorld()->deleteObject(ptr); + } } void CellStore::respawn() From 427be928d0501fd055706502e3746aa390908c9f Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 11 Jun 2018 23:17:54 +0400 Subject: [PATCH 123/282] Do not update animation state for dead actors --- apps/openmw/mwmechanics/character.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index fd7abb571..eb89fe785 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -845,8 +845,8 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim mIdleState = CharState_Idle; } - - if(mDeathState == CharState_None) + // Do not update animation status for dead actors + if(mDeathState == CharState_None && !cls.getCreatureStats(mPtr).isDead()) refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); mAnimation->runAnimation(0.f); From 0d3f535590f82b0f0b71657c8d9440b6e8dfb07d Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 08:57:24 +0400 Subject: [PATCH 124/282] Warn about mod conflicts --- docs/source/reference/modding/settings/game.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 416f1bc1a..308a29546 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -77,6 +77,7 @@ This is how original Morrowind behaves. If this setting is false, player has to wait until end of death animation in all cases. This case is more safe, but makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. +Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation. This setting can only be configured by editing the settings configuration file. From 5fba1c599b761fd9e81ee2d0d3f898be49bc8abf Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 05:52:19 +0000 Subject: [PATCH 125/282] Update README.md with an important distinction. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc600fdbe..7d92879e2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ OpenMW [![Build Status](https://api.travis-ci.org/OpenMW/openmw.svg)](https://travis-ci.org/OpenMW/openmw) [![Build status](https://ci.appveyor.com/api/projects/status/github/openmw/openmw?svg=true)](https://ci.appveyor.com/project/psi29a/openmw) [![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740) -OpenMW is a recreation of the engine for the popular role-playing game Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work. +OpenMW is a open-source game engine that supports playing Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work. OpenMW also comes with OpenMW-CS, a replacement for Morrowind's TES Construction Set. From ebaa6fb5a212817d3052b738998f5f4d19e6bb5b Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 09:55:43 +0400 Subject: [PATCH 126/282] Play death scream only once --- apps/openmw/mwmechanics/character.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index eb89fe785..8439a4e4c 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -728,14 +728,6 @@ void CharacterController::playRandomDeath(float startpoint) MWBase::Environment::get().getWorld()->useDeathCamera(); } - // Do not interrupt scripted animation by death - if (!mAnimQueue.empty()) - { - AnimationQueueEntry& first = mAnimQueue.front(); - if (first.mPersist && isAnimPlaying(first.mGroup)) - return; - } - if(mHitState == CharState_SwimKnockDown && mAnimation->hasAnimation("swimdeathknockdown")) { mDeathState = CharState_SwimDeathKnockDown; @@ -760,6 +752,15 @@ void CharacterController::playRandomDeath(float startpoint) { mDeathState = chooseRandomDeathState(); } + + // Do not interrupt scripted animation by death + if (!mAnimQueue.empty()) + { + AnimationQueueEntry& first = mAnimQueue.front(); + if (first.mPersist && isAnimPlaying(first.mGroup)) + return; + } + playDeath(startpoint, mDeathState); } From b0a140e714272ac3d44bcb40198dc4c490aa120c Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 10:00:38 +0400 Subject: [PATCH 127/282] Disable actor collision only after end of death animation --- apps/openmw/mwclass/actor.cpp | 2 +- apps/openmw/mwclass/creature.cpp | 1 + apps/openmw/mwclass/npc.cpp | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwclass/actor.cpp b/apps/openmw/mwclass/actor.cpp index 17af4725e..73a4d37d7 100644 --- a/apps/openmw/mwclass/actor.cpp +++ b/apps/openmw/mwclass/actor.cpp @@ -31,7 +31,7 @@ namespace MWClass if (!model.empty()) { physics.addActor(ptr, model); - if (getCreatureStats(ptr).isDead()) + if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); } } diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 27a20a0f5..a07a5c893 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -135,6 +135,7 @@ namespace MWClass data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Flee, ref->mBase->mAiData.mFlee); data->mCreatureStats.setAiSetting (MWMechanics::CreatureStats::AI_Alarm, ref->mBase->mAiData.mAlarm); + // Persistent actors with 0 health do not play death animation if (data->mCreatureStats.isDead()) data->mCreatureStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr)); diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 172e4cc8f..92e25baee 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -352,6 +352,8 @@ namespace MWClass data->mNpcStats.setNeedRecalcDynamicStats(true); } + + // Persistent actors with 0 health do not play death animation if (data->mNpcStats.isDead()) data->mNpcStats.setDeathAnimationFinished(ptr.getClass().isPersistent(ptr)); From e3812f40753f0969557cf93361cb7ba4d27eaeed Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 11:27:18 +0400 Subject: [PATCH 128/282] Check creature stats only for actors --- apps/openmw/mwmechanics/character.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 8439a4e4c..332cd225c 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -847,7 +847,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim } // Do not update animation status for dead actors - if(mDeathState == CharState_None && !cls.getCreatureStats(mPtr).isDead()) + if(mDeathState == CharState_None && (!cls.isActor() || !cls.getCreatureStats(mPtr).isDead())) refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); mAnimation->runAnimation(0.f); From 0c926552502759ece98bdf14e76b7d01b6d7aaf7 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 11:51:54 +0400 Subject: [PATCH 129/282] Avoid code duplication in character manager --- apps/openmw/mwmechanics/character.cpp | 44 ++++++++++++--------------- apps/openmw/mwmechanics/character.hpp | 2 ++ 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 332cd225c..6eaaa3def 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -562,12 +562,8 @@ void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterStat void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force) { // If the current animation is persistent, do not touch it - if (!mAnimQueue.empty()) - { - AnimationQueueEntry& first = mAnimQueue.front(); - if (first.mPersist && isAnimPlaying(first.mGroup)) - return; - } + if (isPersistentAnimPlaying()) + return; if (mPtr.getClass().isActor()) refreshHitRecoilAnims(); @@ -754,12 +750,8 @@ void CharacterController::playRandomDeath(float startpoint) } // Do not interrupt scripted animation by death - if (!mAnimQueue.empty()) - { - AnimationQueueEntry& first = mAnimQueue.front(); - if (first.mPersist && isAnimPlaying(first.mGroup)) - return; - } + if (isPersistentAnimPlaying()) + return; playDeath(startpoint, mDeathState); } @@ -2154,12 +2146,8 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int return false; // We should not interrupt persistent animations by non-persistent ones - if (!mAnimQueue.empty()) - { - AnimationQueueEntry& first = mAnimQueue.front(); - if (first.mPersist && !persist) - return false; - } + if (isPersistentAnimPlaying() && !persist) + return false; // If this animation is a looped animation (has a "loop start" key) that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count @@ -2215,6 +2203,17 @@ void CharacterController::skipAnim() mSkipAnim = true; } +bool CharacterController::isPersistentAnimPlaying() +{ + if (!mAnimQueue.empty()) + { + AnimationQueueEntry& first = mAnimQueue.front(); + return first.mPersist && isAnimPlaying(first.mGroup); + } + + return false; +} + bool CharacterController::isAnimPlaying(const std::string &groupName) { if(mAnimation == NULL) @@ -2222,16 +2221,11 @@ bool CharacterController::isAnimPlaying(const std::string &groupName) return mAnimation->isPlaying(groupName); } - void CharacterController::clearAnimQueue() { // Do not interrupt scripted animations - if (!mAnimQueue.empty()) - { - AnimationQueueEntry& first = mAnimQueue.front(); - if (!first.mPersist) - mAnimation->disable(mAnimQueue.front().mGroup); - } + if (!isPersistentAnimPlaying() && !mAnimQueue.empty()) + mAnimation->disable(mAnimQueue.front().mGroup); for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) { diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index a172620b9..74910b3c3 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -221,6 +221,8 @@ class CharacterController : public MWRender::Animation::TextKeyListener bool updateCreatureState(); void updateIdleStormState(bool inwater); + bool isPersistentAnimPlaying(); + void updateAnimQueue(); void updateHeadTracking(float duration); From 7502a7dc4d299cdacbf46275ad2c820e71c0e125 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 07:52:53 +0000 Subject: [PATCH 130/282] what does this give us from a CI perspective? --- .gitlab-ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..90525e5a9 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,26 @@ +# use the official gcc image, based on debian +# can use verions as well, like gcc:5.2 +# see https://hub.docker.com/_/gcc/ +image: gcc + +build: + stage: build + # instead of calling g++ directly you can also use some build toolkit like make + # install the necessary build tools when needed + # before_script: + - apt update && apt -y install build-essential cmake git + script: + - whois + # artifacts: + # paths: + # - mybinary + # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time + # cache: + # paths: + # - "*.o" + +# run tests using the binary built before +test: + stage: test + script: + - whois From 04dc74a1d6898dbd26961cfb13127117b74cc0fc Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 08:05:30 +0000 Subject: [PATCH 131/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 90525e5a9..3b0dca5fe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,12 +8,12 @@ build: # instead of calling g++ directly you can also use some build toolkit like make # install the necessary build tools when needed # before_script: - - apt update && apt -y install build-essential cmake git + # - apt update && apt -y install make autoconf script: - whois - # artifacts: - # paths: - # - mybinary +# artifacts: +# paths: +# - mybinary # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time # cache: # paths: From 8714f48ce7b1bed792e6cd582710a1ad3da72fbd Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 08:22:44 +0000 Subject: [PATCH 132/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3b0dca5fe..394f07136 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,7 @@ build: # before_script: # - apt update && apt -y install make autoconf script: - - whois + - ls /etc # artifacts: # paths: # - mybinary @@ -23,4 +23,4 @@ build: test: stage: test script: - - whois + - ls From d986354d53af6a22162257eebfb146f46237adc1 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 08:30:18 +0000 Subject: [PATCH 133/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 394f07136..b614160e9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,7 +10,13 @@ build: # before_script: # - apt update && apt -y install make autoconf script: + - ls ./ - ls /etc + - ls /bin + - ls /usr/bin + - cat /etc/lsb-release + - apt-get install -y cmake + - mkdir build; cd build; cmake ../openmw # artifacts: # paths: # - mybinary From de1cad86abb974e0d88c33d030cf518624204737 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 08:37:26 +0000 Subject: [PATCH 134/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b614160e9..f8df83a5f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,9 +14,8 @@ build: - ls /etc - ls /bin - ls /usr/bin - - cat /etc/lsb-release - apt-get install -y cmake - - mkdir build; cd build; cmake ../openmw + - mkdir build; cd build; cmake ../ # artifacts: # paths: # - mybinary From 8c4731728c97822af846ae8e3322e1b77bdd568b Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 08:41:07 +0000 Subject: [PATCH 135/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f8df83a5f..7d1adb5ab 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,8 @@ build: - ls /etc - ls /bin - ls /usr/bin + - apt-get update + - apt-cache search cmake - apt-get install -y cmake - mkdir build; cd build; cmake ../ # artifacts: From 0e441d48ac78f11993df4fc630a258131a8eddc2 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 12:55:28 +0400 Subject: [PATCH 136/282] Give scripted animations highest priority (bug #4286) --- apps/openmw/mwmechanics/character.cpp | 6 +++++- apps/openmw/mwmechanics/character.hpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 6eaaa3def..9737ec355 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1308,6 +1308,10 @@ bool CharacterController::updateWeaponState() } } + // Combat for actors with persistent animations obviously will be buggy + if (isPersistentAnimPlaying()) + return forcestateupdate; + float complete; bool animPlaying; if(mAttackingOrSpell) @@ -2186,7 +2190,7 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int mIdleState = CharState_SpecialIdle; bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0); - mAnimation->play(groupname, Priority_Default, + mAnimation->play(groupname, persist ? Priority_Persistent : Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, ((mode==2) ? "loop start" : "start"), "stop", 0.0f, count-1, loopfallback); } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 74910b3c3..791732c79 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -39,8 +39,8 @@ enum Priority { Priority_Knockdown, Priority_Torch, Priority_Storm, - Priority_Death, + Priority_Persistent, Num_Priorities }; From 20d8a424d6a5dd32aa373321a8f21aeba454f0ab Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 09:09:37 +0000 Subject: [PATCH 137/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7d1adb5ab..262c8147c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,8 +16,14 @@ build: - ls /usr/bin - apt-get update - apt-cache search cmake - - apt-get install -y cmake + - apt-get install -y cmake + - apt-get install -y libunshield-dev libtinyxml-dev + - apt-get install -y libboost-filesystem-dev libboost-program-options-dev libboost-system-dev + - apt-get install -y libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev + - apt-get install -y libsdl2-dev libqt4-dev libopenal-dev + - apt-get install -y libbullet-dev libopenscenegraph-3.4-dev libmygui-dev - mkdir build; cd build; cmake ../ + - make -j4 # artifacts: # paths: # - mybinary From a89441e879d0bf31a7f1d9502a367371d5bd0b6d Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 09:26:10 +0000 Subject: [PATCH 138/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 262c8147c..28fdec103 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,11 +17,13 @@ build: - apt-get update - apt-cache search cmake - apt-get install -y cmake - - apt-get install -y libunshield-dev libtinyxml-dev + - apt-get install -y - apt-get install -y libboost-filesystem-dev libboost-program-options-dev libboost-system-dev - apt-get install -y libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev - - apt-get install -y libsdl2-dev libqt4-dev libopenal-dev - - apt-get install -y libbullet-dev libopenscenegraph-3.4-dev libmygui-dev + - apt-get install -y libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libmygui-dev libunshield-dev libtinyxml-dev + - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb + - dpkg -i *.deb - mkdir build; cd build; cmake ../ - make -j4 # artifacts: From e5dff83e38583a968f95217dd8c4fc183ef8c7ec Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 09:26:42 +0000 Subject: [PATCH 139/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 28fdec103..67481a71f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,6 @@ build: - apt-get update - apt-cache search cmake - apt-get install -y cmake - - apt-get install -y - apt-get install -y libboost-filesystem-dev libboost-program-options-dev libboost-system-dev - apt-get install -y libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev - apt-get install -y libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libmygui-dev libunshield-dev libtinyxml-dev From 25bb7c18266093f67bbadba73152f5a9a856a5a3 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 14:04:03 +0400 Subject: [PATCH 140/282] Make 'PlayGroup idle' to cancel scripted animations --- apps/openmw/mwmechanics/character.cpp | 21 +++++++++++++-------- apps/openmw/mwmechanics/character.hpp | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 9737ec355..5443bee81 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2182,23 +2182,28 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) { - clearAnimQueue(); - mAnimQueue.push_back(entry); + clearAnimQueue(persist); mAnimation->disable(mCurrentIdle); mCurrentIdle.clear(); mIdleState = CharState_SpecialIdle; bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0); - mAnimation->play(groupname, persist ? Priority_Persistent : Priority_Default, + mAnimation->play(groupname, persist && groupname != "idle" ? Priority_Persistent : Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, ((mode==2) ? "loop start" : "start"), "stop", 0.0f, count-1, loopfallback); } else { mAnimQueue.resize(1); - mAnimQueue.push_back(entry); } + + // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing + if (groupname == "idle") + entry.mPersist = false; + + mAnimQueue.push_back(entry); + return true; } @@ -2225,15 +2230,15 @@ bool CharacterController::isAnimPlaying(const std::string &groupName) return mAnimation->isPlaying(groupName); } -void CharacterController::clearAnimQueue() +void CharacterController::clearAnimQueue(bool clearPersistAnims) { - // Do not interrupt scripted animations - if (!isPersistentAnimPlaying() && !mAnimQueue.empty()) + // Do not interrupt scripted animations, if we want to keep them + if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty()) mAnimation->disable(mAnimQueue.front().mGroup); for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) { - if (!it->mPersist) + if (clearPersistAnims || !it->mPersist) it = mAnimQueue.erase(it); else ++it; diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 791732c79..381cf71a5 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -215,7 +215,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener void refreshMovementAnims(const WeaponInfo* weap, CharacterState movement, bool force=false); void refreshIdleAnims(const WeaponInfo* weap, CharacterState idle, bool force=false); - void clearAnimQueue(); + void clearAnimQueue(bool clearPersistAnims = false); bool updateWeaponState(); bool updateCreatureState(); From bc0eb3349bf037ca599da1db2f21a15d17dc747c Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 10:33:35 +0000 Subject: [PATCH 141/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 67481a71f..b372e6dbf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,24 +10,22 @@ build: # before_script: # - apt update && apt -y install make autoconf script: - - ls ./ - - ls /etc - - ls /bin - - ls /usr/bin - apt-get update - - apt-cache search cmake - - apt-get install -y cmake - - apt-get install -y libboost-filesystem-dev libboost-program-options-dev libboost-system-dev + - apt-get install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev - apt-get install -y libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev - - apt-get install -y libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libmygui-dev libunshield-dev libtinyxml-dev + - apt-get install -y libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libunshield-dev libtinyxml-dev + - apt-get install -y libmygui-dev libullet-dev # to be updated to latest below because stretch is too old - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - dpkg -i *.deb - mkdir build; cd build; cmake ../ - make -j4 -# artifacts: -# paths: -# - mybinary + artifacts: + paths: + - build # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time # cache: # paths: From e3832cd2e2cbb3b46d3c71db14cf54289acf4ad4 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 11:24:27 +0000 Subject: [PATCH 142/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b372e6dbf..f6e599a09 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,7 +14,7 @@ build: - apt-get install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev - apt-get install -y libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev - apt-get install -y libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libunshield-dev libtinyxml-dev - - apt-get install -y libmygui-dev libullet-dev # to be updated to latest below because stretch is too old + - apt-get install -y libmygui-dev libbullet-dev # to be updated to latest below because stretch is too old - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb From dddceba8f2d2911f4340094307e8e27536001e12 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 11:32:37 +0000 Subject: [PATCH 143/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f6e599a09..be9986e6d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,10 +17,11 @@ build: - apt-get install -y libmygui-dev libbullet-dev # to be updated to latest below because stretch is too old - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb - - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb +# - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - - dpkg -i *.deb + - dpkg -i *.deb --ignore-depends - mkdir build; cd build; cmake ../ - make -j4 artifacts: From a166534226dd34d7ca2e5da8a9ee805c8e9b02a7 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 11:41:19 +0000 Subject: [PATCH 144/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index be9986e6d..fed43f495 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ build: - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - - dpkg -i *.deb --ignore-depends + - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb - mkdir build; cd build; cmake ../ - make -j4 artifacts: From f299be8158b22dbe5d272b45707b9511c8f8ba41 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 16:07:36 +0400 Subject: [PATCH 145/282] Play scripted animations even if SkipAnim is used --- apps/openmw/mwmechanics/character.cpp | 3 ++- apps/openmw/mwrender/animation.cpp | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 5443bee81..fbdb19d5b 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2035,7 +2035,8 @@ void CharacterController::update(float duration) world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); } - osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim ? 0.f : duration); + bool isPersist = isPersistentAnimPlaying(); + osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isPersist ? 0.f : duration); if(duration > 0.0f) moved /= duration; else diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 3ccc06665..d96b9f809 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1089,11 +1089,28 @@ namespace MWRender osg::Vec3f Animation::runAnimation(float duration) { + // If we have scripted animations, play only them + bool hasScriptedAnims = false; + for (AnimStateMap::iterator stateiter = mStates.begin(); stateiter != mStates.end(); stateiter++) + { + if (stateiter->second.mPriority.contains(int(MWMechanics::Priority_Persistent)) && stateiter->second.mPlaying) + { + hasScriptedAnims = true; + break; + } + } + osg::Vec3f movement(0.f, 0.f, 0.f); AnimStateMap::iterator stateiter = mStates.begin(); while(stateiter != mStates.end()) { AnimState &state = stateiter->second; + if (hasScriptedAnims && !state.mPriority.contains(int(MWMechanics::Priority_Persistent))) + { + ++stateiter; + continue; + } + const NifOsg::TextKeyMap &textkeys = state.mSource->getTextKeys(); NifOsg::TextKeyMap::const_iterator textkey(textkeys.upper_bound(state.getTime())); From bce6d79ad37bb309918f63d48f1f520329fb540a Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 16:19:28 +0400 Subject: [PATCH 146/282] Add changelog entries, related to animations --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd1acff2a..6ca79a664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,13 +3,17 @@ Bug #2835: Player able to slowly move when overencumbered Bug #3374: Touch spells not hitting kwama foragers + Bug #3486: [Mod] NPC Commands does not work Bug #3591: Angled hit distance too low Bug #3897: Have Goodbye give all choices the effects of Goodbye Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID Bug #4221: Characters get stuck in V-shaped terrain Bug #4251: Stationary NPCs do not return to their position after combat + Bug #4286: Scripted animations can be interrupted + Bug #4291: Non-persistent actors that started the game as dead do not play death animations Bug #4293: Faction members are not aware of faction ownerships in barter + Bug #4307: World cleanup should remove dead bodies only if death animation is finished Bug #4327: Missing animations during spell/weapon stance switching Bug #4419: MRK NiStringExtraData is handled incorrectly Bug #4426: RotateWorld behavior is incorrect From 1c736ea06448ee9660668fc31e1ca4f92d593550 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 12:46:19 +0000 Subject: [PATCH 147/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fed43f495..ac0fb1175 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,10 +5,6 @@ image: gcc build: stage: build - # instead of calling g++ directly you can also use some build toolkit like make - # install the necessary build tools when needed - # before_script: - # - apt update && apt -y install make autoconf script: - apt-get update - apt-get install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev @@ -17,23 +13,23 @@ build: - apt-get install -y libmygui-dev libbullet-dev # to be updated to latest below because stretch is too old - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb -# - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.ogreplatform0debian1v5_3.2.2+dfsg-1_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb - mkdir build; cd build; cmake ../ - make -j4 + - DESTDIR=artifacts make install artifacts: paths: - - build + - build/artifacts/ # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time - # cache: - # paths: - # - "*.o" + cache: + paths: + - "*.o" # run tests using the binary built before -test: - stage: test - script: - - ls +#test: +# stage: test +# script: +# - ls From 816a1733dcf6adc8edf17ed5ebd9c1d16daed7e6 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 12 Jun 2018 15:29:28 +0200 Subject: [PATCH 148/282] Allow comma after Begin and End script instruction (Fixes #4451) --- components/compiler/fileparser.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/compiler/fileparser.cpp b/components/compiler/fileparser.cpp index 52a9a63f1..c9e205b8a 100644 --- a/components/compiler/fileparser.cpp +++ b/components/compiler/fileparser.cpp @@ -119,6 +119,11 @@ namespace Compiler return false; } } + else if (code==Scanner::S_comma && (mState==NameState || mState==EndNameState)) + { + // ignoring comma (for now) + return true; + } return Parser::parseSpecial (code, loc, scanner); } From 296ad8424e998cb0215fc2e62a99c3a1bdd6f377 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 12 Jun 2018 17:06:09 +0200 Subject: [PATCH 149/282] updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e199d0827..346d2c53c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ Bug #3374: Touch spells not hitting kwama foragers Bug #3591: Angled hit distance too low Bug #3629: DB assassin attack never triggers creature spawning + Bug #3876: Landscape texture painting is misaligned Bug #3897: Have Goodbye give all choices the effects of Goodbye + Bug #3993: Terrain texture blending map is not upscaled Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully From 565922f9ad910473e9a8430bc860171187978782 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 12 Jun 2018 17:52:16 +0200 Subject: [PATCH 150/282] naked expressions beginning with the member operator were allowed erroneously outside of the console (Fixes issue #2971) --- components/compiler/lineparser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/compiler/lineparser.cpp b/components/compiler/lineparser.cpp index c7f82a3d0..2d551348d 100644 --- a/components/compiler/lineparser.cpp +++ b/components/compiler/lineparser.cpp @@ -512,7 +512,7 @@ namespace Compiler return true; } - if (code==Scanner::S_member && mState==PotentialExplicitState) + if (code==Scanner::S_member && mState==PotentialExplicitState && mAllowExpression) { mState = MemberState; parseExpression (scanner, loc); From 1c8a20a54afc3958e3e622a331fe798c45e9f43d Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Tue, 12 Jun 2018 19:33:41 +0300 Subject: [PATCH 151/282] Set ok button focus in settings window by default (fixes #4368) --- CHANGELOG.md | 1 + apps/openmw/mwgui/settingswindow.cpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 346d2c53c..f407a156e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Bug #4251: Stationary NPCs do not return to their position after combat Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4327: Missing animations during spell/weapon stance switching + Bug #4368: Settings window ok button doesn't have key focus by default Bug #4419: MRK NiStringExtraData is handled incorrectly Bug #4426: RotateWorld behavior is incorrect Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 diff --git a/apps/openmw/mwgui/settingswindow.cpp b/apps/openmw/mwgui/settingswindow.cpp index 9bf6e4385..677ddefb3 100644 --- a/apps/openmw/mwgui/settingswindow.cpp +++ b/apps/openmw/mwgui/settingswindow.cpp @@ -562,8 +562,9 @@ namespace MWGui void SettingsWindow::onOpen() { - updateControlsBox (); + updateControlsBox(); resetScrollbars(); + MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } void SettingsWindow::onWindowResize(MyGUI::Window *_sender) From 6114cff8422ee2e2dfc5b53852452a7be55bfe86 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 12 Jun 2018 19:14:32 +0200 Subject: [PATCH 152/282] updated credits file --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index b029140c7..b13953824 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -63,6 +63,7 @@ Programmers Evgeniy Mineev (sandstranger) Federico Guerra (FedeWar) Fil Krynicki (filkry) + Florian Weber (Florianjw) Gašper Sedej gugus/gus Hallfaer Tuilinn From 9c45cc7e48897171fc27c219bd1e7d9533b81ff9 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 12 Jun 2018 22:05:00 +0400 Subject: [PATCH 153/282] Use player reference instead of pointer --- apps/openmw/mwmechanics/actors.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 6549c169b..de394c446 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -257,15 +257,14 @@ namespace MWMechanics if (newItem.isEmpty() || boundPtr != newItem) return; + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + // change draw state only if the item is in player's right hand if (slot == MWWorld::InventoryStore::Slot_CarriedRight) - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + player.setDrawState(MWMechanics::DrawState_Weapon); if (prevItem != store.end()) - { - MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); - player->setPreviousItem(itemId, prevItem->getCellRef().getRefId()); - } + player.setPreviousItem(itemId, prevItem->getCellRef().getRefId()); } void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor) @@ -282,9 +281,9 @@ namespace MWMechanics if (actor != MWMechanics::getPlayer()) return; - MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); - std::string prevItemId = player->getPreviousItem(itemId); - player->erasePreviousItem(itemId); + MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); + std::string prevItemId = player.getPreviousItem(itemId); + player.erasePreviousItem(itemId); if (prevItemId.empty()) return; From 224b94c0ce02349fbdfc99300f0350ad37b9581e Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Sat, 9 Jun 2018 10:55:34 +0200 Subject: [PATCH 154/282] Decompress cursors using SDL software renderer on Mac or if OSG >= 3.5.8 --- CHANGELOG.md | 1 + components/sdlutil/sdlcursormanager.cpp | 83 ++++++++++++++++++++----- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb72c7cac..6210f3f11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Bug #4433: Guard behaviour is incorrect with Alarm = 0 Bug #4443: Goodbye option and dialogue choices are not mutually exclusive Feature #4444: Per-group KF-animation files support + Bug #4424: [macOS] Cursor is either empty or garbage when compiled against macOS 10.13 SDK 0.44.0 ------ diff --git a/components/sdlutil/sdlcursormanager.cpp b/components/sdlutil/sdlcursormanager.cpp index 65aa2106f..368d8e502 100644 --- a/components/sdlutil/sdlcursormanager.cpp +++ b/components/sdlutil/sdlcursormanager.cpp @@ -7,11 +7,14 @@ #include #include +#include +#include #include #include #include #include +#include #include #include "imagetosurface.hpp" @@ -22,6 +25,13 @@ USE_GRAPHICSWINDOW() #endif +#if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 8) || defined(__APPLE__) +#define OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION 1 +#else +#define OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION 0 +#endif + +#if !OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION namespace { @@ -132,17 +142,6 @@ namespace osg::ref_ptr geom; -#if defined(__APPLE__) - // Extra flip needed on OS X systems due to a driver bug - const char* envval = getenv("OPENMW_CURSOR_WORKAROUND"); - bool workaround = !envval || envval == std::string("1"); - std::string vendorString = (const char*)glGetString(GL_VENDOR); - if (!envval) - workaround = vendorString.find("Intel") != std::string::npos || vendorString.find("ATI") != std::string::npos || vendorString.find("AMD") != std::string::npos; - if (workaround) - geom = osg::createTexturedQuadGeometry(osg::Vec3(-1,1,0), osg::Vec3(2,0,0), osg::Vec3(0,-2,0)); - else -#endif geom = osg::createTexturedQuadGeometry(osg::Vec3(-1,-1,0), osg::Vec3(2,0,0), osg::Vec3(0,2,0)); geom->drawImplementation(renderInfo); @@ -157,6 +156,7 @@ namespace } } +#endif namespace SDLUtil { @@ -220,13 +220,63 @@ namespace SDLUtil #endif } - void SDLCursorManager::_createCursorFromResource(const std::string& name, int rotDegrees, osg::Image* image, Uint8 hotspot_x, Uint8 hotspot_y) +#if OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION + typedef std::unique_ptr SDLSurfacePtr; + + SDLSurfacePtr decompress(osg::Image* source, int rotDegrees) { - osg::ref_ptr decompressed; + int width = source->s(); + int height = source->t(); + bool useAlpha = source->isImageTranslucent(); + + osg::ref_ptr decompressedImage = new osg::Image; + decompressedImage->setFileName(source->getFileName()); + decompressedImage->allocateImage(width, height, 1, useAlpha ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE); + for (int s=0; ssetColor(source->getColor(s,t,0), s,t,0); + + Uint32 redMask = 0x000000ff; + Uint32 greenMask = 0x0000ff00; + Uint32 blueMask = 0x00ff0000; + Uint32 alphaMask = useAlpha ? 0xff000000 : 0; + + SDL_Surface *cursorSurface = SDL_CreateRGBSurfaceFrom(decompressedImage->data(), + width, + height, + decompressedImage->getPixelSizeInBits(), + decompressedImage->getRowSizeInBytes(), + redMask, + greenMask, + blueMask, + alphaMask); + + SDL_Surface *targetSurface = SDL_CreateRGBSurface(0, width, height, 32, redMask, greenMask, blueMask, alphaMask); + SDL_Renderer *renderer = SDL_CreateSoftwareRenderer(targetSurface); + + SDL_RenderClear(renderer); + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); + SDL_Texture *cursorTexture = SDL_CreateTextureFromSurface(renderer, cursorSurface); + + SDL_RenderCopyEx(renderer, cursorTexture, NULL, NULL, -rotDegrees, NULL, SDL_FLIP_VERTICAL); + + SDL_DestroyTexture(cursorTexture); + SDL_FreeSurface(cursorSurface); + SDL_DestroyRenderer(renderer); + + return SDLSurfacePtr(targetSurface, SDL_FreeSurface); + } +#endif + + void SDLCursorManager::_createCursorFromResource(const std::string& name, int rotDegrees, osg::Image* image, Uint8 hotspot_x, Uint8 hotspot_y) + { if (mCursorMap.find(name) != mCursorMap.end()) return; +#if !OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION + osg::ref_ptr decompressed; + try { decompressed = decompress(image, static_cast(rotDegrees)); } catch (std::exception& e) { @@ -239,10 +289,15 @@ namespace SDLUtil //set the cursor and store it for later SDL_Cursor* curs = SDL_CreateColorCursor(surf, hotspot_x, hotspot_y); - mCursorMap.insert(CursorMap::value_type(std::string(name), curs)); //clean up SDL_FreeSurface(surf); +#else + auto surf = decompress(image, rotDegrees); + //set the cursor and store it for later + SDL_Cursor* curs = SDL_CreateColorCursor(surf.get(), hotspot_x, hotspot_y); +#endif + mCursorMap.insert(CursorMap::value_type(std::string(name), curs)); } } From cc396f4dfdace36c9efd12526d52eefb5392a762 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 19:06:59 +0000 Subject: [PATCH 155/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ac0fb1175..5f2cbf912 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,7 @@ image: gcc build: stage: build script: + - nproc - apt-get update - apt-get install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev - apt-get install -y libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev @@ -18,7 +19,7 @@ build: - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb - mkdir build; cd build; cmake ../ - - make -j4 + - make -j3 - DESTDIR=artifacts make install artifacts: paths: @@ -29,7 +30,7 @@ build: - "*.o" # run tests using the binary built before -#test: -# stage: test -# script: -# - ls +test: + stage: test + script: + - true From 3c933ebaad915b6726e55aff10b228df4fce9b34 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 12 Jun 2018 19:10:15 +0000 Subject: [PATCH 156/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5f2cbf912..2a4607a38 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -33,4 +33,4 @@ build: test: stage: test script: - - true + - ls From db8aaa74d6352168c74e39848d8294b8ae5cd590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 13 Jun 2018 01:48:31 +0200 Subject: [PATCH 157/282] Start cell border debug drawing --- apps/openmw/mwbase/world.hpp | 1 + apps/openmw/mwrender/renderingmanager.cpp | 13 +++- apps/openmw/mwrender/renderingmanager.hpp | 3 + apps/openmw/mwscript/docs/vmformat.txt | 3 +- apps/openmw/mwscript/miscextensions.cpp | 15 ++++ apps/openmw/mwworld/worldimp.cpp | 5 ++ apps/openmw/mwworld/worldimp.hpp | 1 + components/CMakeLists.txt | 2 +- components/compiler/extensions0.cpp | 2 + components/compiler/opcodes.hpp | 1 + components/terrain/cellborder.cpp | 91 +++++++++++++++++++++++ components/terrain/cellborder.hpp | 36 +++++++++ components/terrain/quadtreeworld.cpp | 4 +- components/terrain/quadtreeworld.hpp | 2 +- components/terrain/terraingrid.cpp | 10 ++- components/terrain/terraingrid.hpp | 7 +- components/terrain/world.cpp | 31 +++++++- components/terrain/world.hpp | 20 +++-- 18 files changed, 226 insertions(+), 21 deletions(-) create mode 100644 components/terrain/cellborder.cpp create mode 100644 components/terrain/cellborder.hpp diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 038535939..ef3063d83 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -120,6 +120,7 @@ namespace MWBase virtual bool toggleWater() = 0; virtual bool toggleWorld() = 0; + virtual bool toggleBorders() = 0; virtual void adjustSky() = 0; diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 4fbcdc4d5..6e5b06038 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -181,6 +181,7 @@ namespace MWRender , mNightEyeFactor(0.f) , mFieldOfViewOverride(0.f) , mFieldOfViewOverridden(false) + , mBorders(false) { resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); resourceSystem->getSceneManager()->setShaderPath(resourcePath + "/shaders"); @@ -219,9 +220,10 @@ namespace MWRender Settings::Manager::getBool("auto use terrain specular maps", "Shaders")); if (distantTerrain) - mTerrain.reset(new Terrain::QuadTreeWorld(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile)); + mTerrain.reset(new Terrain::QuadTreeWorld(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile, Mask_Debug)); else - mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile)); + mTerrain.reset(new Terrain::TerrainGrid(sceneRoot, mRootNode, mResourceSystem, mTerrainStorage, Mask_Terrain, Mask_PreCompile, Mask_Debug)); + mTerrain->setDefaultViewer(mViewer->getCamera()); mCamera.reset(new Camera(mViewer->getCamera())); @@ -441,6 +443,13 @@ namespace MWRender { mSky->setEnabled(enabled); } + + bool RenderingManager::toggleBorders() + { + mBorders = !mBorders; + mTerrain->setBordersVisible(mBorders); + return mBorders; + } bool RenderingManager::toggleRenderMode(RenderMode mode) { diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index f0087e43d..56839adce 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -205,6 +205,8 @@ namespace MWRender LandManager* getLandManager() const; + bool toggleBorders(); + private: void updateProjectionMatrix(); void updateTextureFiltering(); @@ -257,6 +259,7 @@ namespace MWRender bool mFieldOfViewOverridden; float mFieldOfView; float mFirstPersonFieldOfView; + bool mBorders; void operator = (const RenderingManager&); RenderingManager(const RenderingManager&); diff --git a/apps/openmw/mwscript/docs/vmformat.txt b/apps/openmw/mwscript/docs/vmformat.txt index e999097db..fed780be7 100644 --- a/apps/openmw/mwscript/docs/vmformat.txt +++ b/apps/openmw/mwscript/docs/vmformat.txt @@ -454,5 +454,6 @@ op 0x2000303: Fixme, explicit op 0x2000304: Show op 0x2000305: Show, explicit op 0x2000306: OnActivate, explicit +op 0x2000307: ToggleBorders, tb -opcodes 0x2000307-0x3ffffff unused +opcodes 0x2000308-0x3ffffff unused diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index 1067b5536..632d9e97c 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -254,6 +254,20 @@ namespace MWScript } }; + class OpToggleBorders : public Interpreter::Opcode0 + { + public: + + virtual void execute (Interpreter::Runtime& runtime) + { + bool enabled = + MWBase::Environment::get().getWorld()->toggleBorders(); + + runtime.getContext().report (enabled ? + "Border Rendering -> On" : "Border Rendering -> Off"); + } + }; + class OpTogglePathgrid : public Interpreter::Opcode0 { public: @@ -1374,6 +1388,7 @@ namespace MWScript interpreter.installSegment5 (Compiler::Misc::opcodeRemoveFromLevItem, new OpRemoveFromLevItem); interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraph, new OpShowSceneGraph); interpreter.installSegment3 (Compiler::Misc::opcodeShowSceneGraphExplicit, new OpShowSceneGraph); + interpreter.installSegment5 (Compiler::Misc::opcodeToggleBorders, new OpToggleBorders); } } } diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 9c7fba9fa..af30b07d1 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1919,6 +1919,11 @@ namespace MWWorld return mRendering->toggleRenderMode(MWRender::Render_Scene); } + bool World::toggleBorders() + { + return mRendering->toggleBorders(); + } + void World::PCDropped (const Ptr& item) { std::string script = item.getClass().getScript(item); diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 7af7b2968..dc1ff4588 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -217,6 +217,7 @@ namespace MWWorld bool toggleWater() override; bool toggleWorld() override; + bool toggleBorders() override; void adjustSky() override; diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index a0b426a16..d74f11fc2 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -120,7 +120,7 @@ add_component_dir (translation ) add_component_dir (terrain - storage world buffercache defs terraingrid material terraindrawable texturemanager chunkmanager compositemaprenderer quadtreeworld quadtreenode viewdata + storage world buffercache defs terraingrid material terraindrawable texturemanager chunkmanager compositemaprenderer quadtreeworld quadtreenode viewdata cellborder ) add_component_dir (loadinglistener diff --git a/components/compiler/extensions0.cpp b/components/compiler/extensions0.cpp index cd5bf7ef7..7638d0f78 100644 --- a/components/compiler/extensions0.cpp +++ b/components/compiler/extensions0.cpp @@ -318,6 +318,8 @@ namespace Compiler extensions.registerInstruction ("removefromlevcreature", "ccl", opcodeRemoveFromLevCreature); extensions.registerInstruction ("addtolevitem", "ccl", opcodeAddToLevItem); extensions.registerInstruction ("removefromlevitem", "ccl", opcodeRemoveFromLevItem); + extensions.registerInstruction ("tb", "", opcodeToggleBorders); + extensions.registerInstruction ("toggleborders", "", opcodeToggleBorders); } } diff --git a/components/compiler/opcodes.hpp b/components/compiler/opcodes.hpp index 6a6552467..aef92b311 100644 --- a/components/compiler/opcodes.hpp +++ b/components/compiler/opcodes.hpp @@ -295,6 +295,7 @@ namespace Compiler const int opcodeRemoveFromLevItem = 0x20002fe; const int opcodeShowSceneGraph = 0x2002f; const int opcodeShowSceneGraphExplicit = 0x20030; + const int opcodeToggleBorders = 0x2000307; } namespace Sky diff --git a/components/terrain/cellborder.cpp b/components/terrain/cellborder.cpp new file mode 100644 index 000000000..b74e2e9be --- /dev/null +++ b/components/terrain/cellborder.cpp @@ -0,0 +1,91 @@ +#include "cellborder.hpp" + +#include +#include +#include + +#include "world.hpp" + +namespace MWRender +{ + +CellBorder::CellBorder(Terrain::World *world, osg::Group *root): + mWorld(world), + mRoot(root), + mBorderRoot(0) +{ +} + +void CellBorder::createCellBorderGeometry(int x, int y) +{ + const int cellSize = 8192; + const int borderSegments = 40; + const float offset = 10.0; + + osg::Vec3 cellCorner = osg::Vec3(x * cellSize,y * cellSize,0); + + osg::ref_ptr vertices = new osg::Vec3Array; + osg::ref_ptr colors = new osg::Vec4Array; + osg::ref_ptr normals = new osg::Vec3Array; + + normals->push_back(osg::Vec3(0.0f,-1.0f, 0.0f)); + + float borderStep = cellSize / ((float) borderSegments); + + for (int i = 0; i <= 2 * borderSegments; ++i) + { + osg::Vec3f pos = i < borderSegments ? + osg::Vec3(i * borderStep,0.0f,0.0f) : + osg::Vec3(cellSize,(i - borderSegments) * borderStep,0.0f); + + pos += cellCorner; + pos += osg::Vec3f(0,0,mWorld->getHeightAt(pos) + offset); + + vertices->push_back(pos); + + osg::Vec4f col = i % 2 == 0 ? + osg::Vec4f(0,0,0,1) : + osg::Vec4f(1,1,0,1); + + colors->push_back(col); + } + + osg::ref_ptr border = new osg::Geometry; + border->setVertexArray(vertices.get()); + border->setNormalArray(normals.get()); + border->setNormalBinding(osg::Geometry::BIND_OVERALL); + border->setColorArray(colors.get()); + border->setColorBinding(osg::Geometry::BIND_PER_VERTEX); + + border->addPrimitiveSet(new osg::DrawArrays(GL_LINE_STRIP,0,vertices->size())); + + osg::ref_ptr borderGeode = new osg::Geode; + borderGeode->addDrawable(border.get()); + + osg::StateSet *stateSet = borderGeode->getOrCreateStateSet(); + + osg::PolygonMode* polygonmode = new osg::PolygonMode; + polygonmode->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE); + stateSet->setAttributeAndModes(polygonmode,osg::StateAttribute::ON); + + mRoot->addChild(borderGeode); + + mBorderRoot = borderGeode; + + mCellBorderNodes[std::make_pair(x,y)] = borderGeode; +} + +void CellBorder::destroyCellBorderGeometry(int x, int y) +{ + CellGrid::iterator it = mCellBorderNodes.find(std::make_pair(x,y)); + + if (it == mCellBorderNodes.end()) + return; + + osg::ref_ptr borderNode = it->second; + mBorderRoot->removeChild(borderNode); + + mCellBorderNodes.erase(it); +} + +} diff --git a/components/terrain/cellborder.hpp b/components/terrain/cellborder.hpp new file mode 100644 index 000000000..6144f8c32 --- /dev/null +++ b/components/terrain/cellborder.hpp @@ -0,0 +1,36 @@ +#ifndef GAME_RENDER_CELLBORDER +#define GAME_RENDER_CELLBORDER + +#include +#include + +namespace Terrain +{ + class World; +} + +namespace MWRender +{ + /** + * @Brief Handles the debug cell borders. + */ + class CellBorder + { + public: + typedef std::map, osg::ref_ptr > CellGrid; + + CellBorder(Terrain::World *world, osg::Group *root); + + void createCellBorderGeometry(int x, int y); + void destroyCellBorderGeometry(int x, int y); + + protected: + Terrain::World *mWorld; + osg::Group *mRoot; + osg::Group *mBorderRoot; + + CellGrid mCellBorderNodes; + }; +} + +#endif diff --git a/components/terrain/quadtreeworld.cpp b/components/terrain/quadtreeworld.cpp index f31064805..f9bc7bb60 100644 --- a/components/terrain/quadtreeworld.cpp +++ b/components/terrain/quadtreeworld.cpp @@ -235,8 +235,8 @@ private: osg::ref_ptr mRootNode; }; -QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, int nodeMask, int preCompileMask) - : World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask) +QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resource::ResourceSystem *resourceSystem, Storage *storage, int nodeMask, int preCompileMask, int borderMask) + : World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mViewDataMap(new ViewDataMap) , mQuadTreeBuilt(false) { diff --git a/components/terrain/quadtreeworld.hpp b/components/terrain/quadtreeworld.hpp index ef33f158e..c166a9cb1 100644 --- a/components/terrain/quadtreeworld.hpp +++ b/components/terrain/quadtreeworld.hpp @@ -19,7 +19,7 @@ namespace Terrain class QuadTreeWorld : public Terrain::World { public: - QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0); + QuadTreeWorld(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0, int borderMask=0); ~QuadTreeWorld(); void accept(osg::NodeVisitor& nv); diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp index 466cddddc..f5ab6f64b 100644 --- a/components/terrain/terraingrid.cpp +++ b/components/terrain/terraingrid.cpp @@ -17,8 +17,8 @@ public: virtual void reset(unsigned int frame) {} }; -TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask) - : Terrain::World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask) +TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) + : Terrain::World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) , mNumSplits(4) { } @@ -75,6 +75,8 @@ void TerrainGrid::loadCell(int x, int y) if (!terrainNode) return; // no terrain defined + Terrain::World::loadCell(x,y); + mTerrainRoot->addChild(terrainNode); mGrid[std::make_pair(x,y)] = terrainNode; @@ -82,7 +84,9 @@ void TerrainGrid::loadCell(int x, int y) void TerrainGrid::unloadCell(int x, int y) { - Grid::iterator it = mGrid.find(std::make_pair(x,y)); + World::unloadCell(x,y); + + MWRender::CellBorder::CellGrid::iterator it = mGrid.find(std::make_pair(x,y)); if (it == mGrid.end()) return; diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp index 189fe7f63..f21dd39d3 100644 --- a/components/terrain/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -7,6 +7,8 @@ #include "world.hpp" +#include "cellborder.hpp" + namespace Terrain { @@ -14,7 +16,7 @@ namespace Terrain class TerrainGrid : public Terrain::World { public: - TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0); + TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask=~0, int borderMask=0); ~TerrainGrid(); virtual void cacheCell(View* view, int x, int y); @@ -33,8 +35,7 @@ namespace Terrain // split each ESM::Cell into mNumSplits*mNumSplits terrain chunks unsigned int mNumSplits; - typedef std::map, osg::ref_ptr > Grid; - Grid mGrid; + MWRender::CellBorder::CellGrid mGrid; }; } diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 335bb496b..c08c02dc5 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -14,7 +14,7 @@ namespace Terrain { -World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask) +World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) : mStorage(storage) , mParent(parent) , mResourceSystem(resourceSystem) @@ -28,6 +28,12 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mTerrainRoot->setName("Terrain Root"); + mBorderRoot = new osg::Switch; + mBorderRoot->setName("Border Root"); + mBorderRoot->setNodeMask(borderMask); + + mTerrainRoot->addChild(mBorderRoot); + osg::ref_ptr compositeCam = new osg::Camera; compositeCam->setRenderOrder(osg::Camera::PRE_RENDER, -1); compositeCam->setProjectionMatrix(osg::Matrix::identity()); @@ -39,7 +45,6 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst compileRoot->addChild(compositeCam); - mCompositeMapRenderer = new CompositeMapRenderer; compositeCam->addChild(mCompositeMapRenderer); @@ -48,8 +53,30 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mTextureManager.reset(new TextureManager(mResourceSystem->getSceneManager())); mChunkManager.reset(new ChunkManager(mStorage, mResourceSystem->getSceneManager(), mTextureManager.get(), mCompositeMapRenderer)); + mCellBorder.reset(new MWRender::CellBorder(this,mTerrainRoot.get())); + mResourceSystem->addResourceManager(mChunkManager.get()); mResourceSystem->addResourceManager(mTextureManager.get()); + + setBordersVisible(false); +} + +void World::setBordersVisible(bool visible) +{ + if (visible) + mBorderRoot->setAllChildrenOn(); + else + mBorderRoot->setAllChildrenOff(); +} + +void World::loadCell(int x, int y) +{ + mCellBorder->createCellBorderGeometry(x,y); +} + +void World::unloadCell(int x, int y) +{ + mCellBorder->destroyCellBorderGeometry(x,y); } World::~World() diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index e1c3828fc..0d043c035 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -4,11 +4,14 @@ #include #include #include +#include #include #include "defs.hpp" +#include "cellborder.hpp" + namespace osg { class Group; @@ -54,7 +57,7 @@ namespace Terrain /// @param storage Storage instance to get terrain data from (heights, normals, colors, textures..) /// @param nodeMask mask for the terrain root /// @param preCompileMask mask for pre compiling textures - World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask); + World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask); virtual ~World(); /// Apply the scene manager's texture filtering settings to all cached textures. @@ -73,16 +76,16 @@ namespace Terrain /// Load the cell into the scene graph. /// @note Not thread safe. - /// @note May be ignored by derived implementations that don't organize the terrain into cells. - virtual void loadCell(int x, int y) {} + virtual void loadCell(int x, int y); /// Remove the cell from the scene graph. /// @note Not thread safe. - /// @note May be ignored by derived implementations that don't organize the terrain into cells. - virtual void unloadCell(int x, int y) {} + virtual void unloadCell(int x, int y); virtual void enable(bool enabled) {} + void setBordersVisible(bool visible); + /// Create a View to use with preload feature. The caller is responsible for deleting the view. /// @note Thread safe. virtual View* createView() { return NULL; } @@ -98,10 +101,14 @@ namespace Terrain Storage* getStorage() { return mStorage; } protected: + void createCellBorderGeometry(int x, int y); + void destroyCellBorderGeometry(int x, int y); + Storage* mStorage; osg::ref_ptr mParent; osg::ref_ptr mTerrainRoot; + osg::ref_ptr mBorderRoot; osg::ref_ptr mCompositeMapCamera; osg::ref_ptr mCompositeMapRenderer; @@ -110,8 +117,9 @@ namespace Terrain std::unique_ptr mTextureManager; std::unique_ptr mChunkManager; - }; + std::unique_ptr mCellBorder; + }; } #endif From 94f695cffc8307c53c94e314b9380e3b8b27ab06 Mon Sep 17 00:00:00 2001 From: wareya Date: Tue, 12 Jun 2018 21:04:27 -0400 Subject: [PATCH 158/282] Fix #4452 and remove dead code --- components/esmterrain/storage.cpp | 22 ++++++++------------ components/esmterrain/storage.hpp | 2 -- components/terrain/material.cpp | 34 ++++++++++++++++++++++++++++--- components/terrain/storage.hpp | 2 -- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index dadc64f57..52850fd74 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -361,6 +361,11 @@ namespace ESMTerrain std::string Storage::getTextureName(UniqueTextureId id) { + // Goes under used terrain blend transitions + static const std::string baseTexture = "textures\\tx_black_01.dds"; + if (id.first == -1) + return baseTexture; + static const std::string defaultTexture = "textures\\_land_default.dds"; if (id.first == 0) return defaultTexture; // Not sure if the default texture really is hardcoded? @@ -396,11 +401,9 @@ namespace ESMTerrain // Save the used texture indices so we know the total number of textures // and number of required blend maps std::set textureIndices; - // Due to the way the blending works, the base layer will always shine through in between - // blend transitions (eg halfway between two texels, both blend values will be 0.5, so 25% of base layer visible). - // To get a consistent look, we need to make sure to use the same base layer in all cells. - // So we're always adding _land_default.dds as the base layer here, even if it's not referenced in this cell. - textureIndices.insert(std::make_pair(0,0)); + // Due to the way the blending works, the base layer will bleed between texture transitions so we want it to be a black texture + // The subsequent passes are added instead of blended, so this gives the correct result + textureIndices.insert(std::make_pair(-1,0)); // -1 goes to tx_black_01 LandCache cache; @@ -618,15 +621,6 @@ namespace ESMTerrain return info; } - Terrain::LayerInfo Storage::getDefaultLayer() - { - Terrain::LayerInfo info; - info.mDiffuseMap = "textures\\_land_default.dds"; - info.mParallax = false; - info.mSpecular = false; - return info; - } - float Storage::getCellWorldSize() { return static_cast(ESM::Land::REAL_SIZE); diff --git a/components/esmterrain/storage.hpp b/components/esmterrain/storage.hpp index 0bb24a4ab..f3300f748 100644 --- a/components/esmterrain/storage.hpp +++ b/components/esmterrain/storage.hpp @@ -94,8 +94,6 @@ namespace ESMTerrain virtual float getHeightAt (const osg::Vec3f& worldPos); - virtual Terrain::LayerInfo getDefaultLayer(); - /// Get the transformation factor for mapping cell units to world units. virtual float getCellWorldSize(); diff --git a/components/terrain/material.cpp b/components/terrain/material.cpp index 56ace0e5a..722df9507 100644 --- a/components/terrain/material.cpp +++ b/components/terrain/material.cpp @@ -2,11 +2,13 @@ #include +#include #include #include #include #include #include +#include #include @@ -59,24 +61,52 @@ namespace Terrain } return depth; } + osg::ref_ptr getLequalDepth() + { + static osg::ref_ptr depth; + if (!depth) + { + depth = new osg::Depth; + depth->setFunction(osg::Depth::LEQUAL); + } + return depth; + } std::vector > createPasses(bool useShaders, bool forcePerPixelLighting, bool clampLighting, Shader::ShaderManager* shaderManager, const std::vector &layers, const std::vector > &blendmaps, int blendmapScale, float layerTileSize) { std::vector > passes; - bool firstLayer = true; unsigned int blendmapIndex = 0; unsigned int passIndex = 0; for (std::vector::const_iterator it = layers.begin(); it != layers.end(); ++it) { + bool firstLayer = (it == layers.begin()); + osg::ref_ptr stateset (new osg::StateSet); if (!firstLayer) { + static osg::ref_ptr blendFunc; + if (!blendFunc) + { + blendFunc= new osg::BlendFunc(); + blendFunc->setFunction(osg::BlendFunc::SRC_ALPHA, osg::BlendFunc::ONE); + } stateset->setMode(GL_BLEND, osg::StateAttribute::ON); + stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON); + stateset->setAttributeAndModes(getEqualDepth(), osg::StateAttribute::ON); } + // disable fog if we're the first layer of several - supposed to be completely black + if (firstLayer && blendmaps.size() > 0) + { + osg::ref_ptr fog (new osg::Fog); + fog->setStart(10000000); + fog->setEnd(10000000); + stateset->setAttributeAndModes(fog, osg::StateAttribute::OFF|osg::StateAttribute::OVERRIDE); + stateset->setAttributeAndModes(getLequalDepth(), osg::StateAttribute::ON); + } int texunit = 0; @@ -158,8 +188,6 @@ namespace Terrain stateset->setTextureAttributeAndModes(texunit, getLayerTexMat(layerTileSize), osg::StateAttribute::ON); } - firstLayer = false; - stateset->setRenderBinDetails(passIndex++, "RenderBin"); passes.push_back(stateset); diff --git a/components/terrain/storage.hpp b/components/terrain/storage.hpp index a4a8bc9fd..ebac1148c 100644 --- a/components/terrain/storage.hpp +++ b/components/terrain/storage.hpp @@ -72,8 +72,6 @@ namespace Terrain virtual float getHeightAt (const osg::Vec3f& worldPos) = 0; - virtual LayerInfo getDefaultLayer() = 0; - /// Get the transformation factor for mapping cell units to world units. virtual float getCellWorldSize() = 0; From bd4badc1531b3f7939267e18e81382b9621da21a Mon Sep 17 00:00:00 2001 From: wareya Date: Tue, 12 Jun 2018 21:05:12 -0400 Subject: [PATCH 159/282] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f407a156e..fd9325d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Bug #4452: Default terrain texture bleeds through texture transitions Feature #4345: Add equivalents for the command line commands to Launcher Feature #4444: Per-group KF-animation files support From 058cfb553ce2a20a0150793796b4b5c32527eb9b Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Tue, 12 Jun 2018 22:18:06 -0500 Subject: [PATCH 160/282] Adding CFBundleIdentifier to OpenMW's Info.plist file for Macs --- files/mac/openmw-Info.plist.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/files/mac/openmw-Info.plist.in b/files/mac/openmw-Info.plist.in index 7583b45ad..20dc36afa 100644 --- a/files/mac/openmw-Info.plist.in +++ b/files/mac/openmw-Info.plist.in @@ -8,6 +8,8 @@ English CFBundleExecutable openmw-launcher + CFBundleIdentifier + org.openmw.openmw CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString From b37f3251263ec86d2c8c1622989616d1c674d64e Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Tue, 12 Jun 2018 22:20:16 -0500 Subject: [PATCH 161/282] #4324/Updating Changelog.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f407a156e..95a947d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts Feature #4345: Add equivalents for the command line commands to Launcher Feature #4444: Per-group KF-animation files support From 2e2be76e3ff700a3348d6420571b650a9e2cdd40 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 13 Jun 2018 08:20:31 +0000 Subject: [PATCH 162/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2a4607a38..18ae73124 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,23 +3,29 @@ # see https://hub.docker.com/_/gcc/ image: gcc +cache: + key: apt-cache + paths: + - apt-cache/ + +before_script: + - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR + - apt-get update -yq + - apt-get -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libunshield-dev libtinyxml-dev +# - apt-get install -y libmygui-dev libbullet-dev # to be updated to latest below because stretch is too old + - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb + - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb + build: stage: build script: - nproc - - apt-get update - - apt-get install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev - - apt-get install -y libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev - - apt-get install -y libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libunshield-dev libtinyxml-dev - - apt-get install -y libmygui-dev libbullet-dev # to be updated to latest below because stretch is too old - - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb - - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb - - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb - - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb - - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb - - mkdir build; cd build; cmake ../ - - make -j3 + - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ + - make -j2 - DESTDIR=artifacts make install artifacts: paths: From dcfbd554bbd1ac264a31f4c5afdf2261d0e3f579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 13 Jun 2018 11:12:46 +0200 Subject: [PATCH 163/282] Remove try catch block --- apps/openmw/mwinput/inputmanagerimp.cpp | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 2a101e58f..1e8a12e2c 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1041,14 +1041,8 @@ namespace MWInput std::string settingStr; - try - { - settingStr = Settings::Manager::getString("screenshot type","Video"); - regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0; - } - catch (std::runtime_error) - { - } + settingStr = Settings::Manager::getString("screenshot type","Video"); + regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0; if (regularScreenshot) { From a49649c313ce267318d203a0687afe53ac37d70b Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 13 Jun 2018 09:29:30 +0000 Subject: [PATCH 164/282] Try to get it to run and build on my docker instance. --- .gitlab-ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 18ae73124..961965763 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,11 @@ # see https://hub.docker.com/_/gcc/ image: gcc +job: + tags: + - openmw + - debian + cache: key: apt-cache paths: @@ -25,7 +30,7 @@ build: script: - nproc - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ - - make -j2 + - make -j4 - DESTDIR=artifacts make install artifacts: paths: From 559754fa7683dd33084013e977689fdc7dbf06dc Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 13 Jun 2018 09:31:39 +0000 Subject: [PATCH 165/282] try this dance again --- .gitlab-ci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 961965763..2c47affd6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,11 +3,6 @@ # see https://hub.docker.com/_/gcc/ image: gcc -job: - tags: - - openmw - - debian - cache: key: apt-cache paths: From da37585a8e708fd974d99ffd3f01a55ae16fac1f Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 13 Jun 2018 10:15:07 +0000 Subject: [PATCH 166/282] Update .gitlab-ci.yml so that we only build with -j2 --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2c47affd6..18ae73124 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ build: script: - nproc - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ - - make -j4 + - make -j2 - DESTDIR=artifacts make install artifacts: paths: From 3f4d5598a524790b33eaace4be7d5b20335817d0 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 13 Jun 2018 11:55:33 +0000 Subject: [PATCH 167/282] Update README.md to be more generic about OpenMW --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7d92879e2..a6d0cacd5 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ OpenMW [![Build Status](https://api.travis-ci.org/OpenMW/openmw.svg)](https://travis-ci.org/OpenMW/openmw) [![Build status](https://ci.appveyor.com/api/projects/status/github/openmw/openmw?svg=true)](https://ci.appveyor.com/project/psi29a/openmw) [![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740) -OpenMW is a open-source game engine that supports playing Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work. +OpenMW is an open-source game engine that supports playing Morrowind by Bethesda Softworks. You need to own the game for OpenMW to play Morrowind. -OpenMW also comes with OpenMW-CS, a replacement for Morrowind's TES Construction Set. +OpenMW also comes with OpenMW-CS, a replacement for Bethesda's Construction Set. * Version: 0.44.0 * License: GPLv3 (see [LICENSE](https://github.com/OpenMW/openmw/blob/master/LICENSE) for more information) From 032768a505e041a627dd455d1973c1b99f32f95d Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 13 Jun 2018 12:38:03 +0000 Subject: [PATCH 168/282] try to use as many cores as possible --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 18ae73124..74bc74ca8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -23,9 +23,9 @@ before_script: build: stage: build script: - - nproc + - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ - - make -j2 + - make -j$cores_to_use - DESTDIR=artifacts make install artifacts: paths: From 7d2394273e3a85739a2bc4834937e6c21a633cf3 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 13 Jun 2018 14:55:03 +0200 Subject: [PATCH 169/282] added statusbar to search window (Fixes #3276) --- CHANGELOG.md | 1 + apps/opencs/view/tools/searchsubview.cpp | 22 ++++++++++++++++++++++ apps/opencs/view/tools/searchsubview.hpp | 10 ++++++++++ 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc66bc61..8a436b4ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts Feature #4345: Add equivalents for the command line commands to Launcher Feature #4444: Per-group KF-animation files support diff --git a/apps/opencs/view/tools/searchsubview.cpp b/apps/opencs/view/tools/searchsubview.cpp index 493defa5a..b50e78227 100644 --- a/apps/opencs/view/tools/searchsubview.cpp +++ b/apps/opencs/view/tools/searchsubview.cpp @@ -8,6 +8,9 @@ #include "../../model/world/idtablebase.hpp" #include "../../model/prefs/state.hpp" +#include "../world/tablebottombox.hpp" +#include "../world/creator.hpp" + #include "reporttable.hpp" #include "searchbox.hpp" @@ -73,6 +76,9 @@ CSVTools::SearchSubView::SearchSubView (const CSMWorld::UniversalId& id, CSMDoc: layout->addWidget (mTable = new ReportTable (document, id, true), 2); + layout->addWidget (mBottom = + new CSVWorld::TableBottomBox (CSVWorld::NullCreatorFactory(), document, id, this), 0); + QWidget *widget = new QWidget; widget->setLayout (layout); @@ -93,6 +99,12 @@ CSVTools::SearchSubView::SearchSubView (const CSMWorld::UniversalId& id, CSMDoc: this, SLOT (startSearch (const CSMTools::Search&))); connect (&mSearchBox, SIGNAL (replaceAll()), this, SLOT (replaceAllRequest())); + + connect (document.getReport (id), SIGNAL (rowsRemoved (const QModelIndex&, int, int)), + this, SLOT (tableSizeUpdate())); + + connect (document.getReport (id), SIGNAL (rowsInserted (const QModelIndex&, int, int)), + this, SLOT (tableSizeUpdate())); } void CSVTools::SearchSubView::setEditLock (bool locked) @@ -101,6 +113,11 @@ void CSVTools::SearchSubView::setEditLock (bool locked) mSearchBox.setEditLock (locked); } +void CSVTools::SearchSubView::setStatusBar (bool show) +{ + mBottom->setStatusBar(show); +} + void CSVTools::SearchSubView::stateChanged (int state, CSMDoc::Document *document) { mSearchBox.setSearchMode (!(state & CSMDoc::State_Searching)); @@ -126,3 +143,8 @@ void CSVTools::SearchSubView::replaceAllRequest() { replace (false); } + +void CSVTools::SearchSubView::tableSizeUpdate() +{ + mBottom->tableSizeChanged (mDocument.getReport (getUniversalId())->rowCount(), 0, 0); +} diff --git a/apps/opencs/view/tools/searchsubview.hpp b/apps/opencs/view/tools/searchsubview.hpp index ac0a5a762..d22367722 100644 --- a/apps/opencs/view/tools/searchsubview.hpp +++ b/apps/opencs/view/tools/searchsubview.hpp @@ -15,6 +15,11 @@ namespace CSMDoc class Document; } +namespace CSVWorld +{ + class TableBottomBox; +} + namespace CSVTools { class ReportTable; @@ -28,6 +33,7 @@ namespace CSVTools CSMDoc::Document& mDocument; CSMTools::Search mSearch; bool mLocked; + CSVWorld::TableBottomBox *mBottom; private: @@ -43,6 +49,8 @@ namespace CSVTools virtual void setEditLock (bool locked); + virtual void setStatusBar (bool show); + private slots: void stateChanged (int state, CSMDoc::Document *document); @@ -52,6 +60,8 @@ namespace CSVTools void replaceRequest(); void replaceAllRequest(); + + void tableSizeUpdate(); }; } From 9bd940e1535667faa87b1c2971c2672c9b10e894 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 13 Jun 2018 13:06:37 +0000 Subject: [PATCH 170/282] Update README.md to indicate that our gitlab pipeline is building --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6d0cacd5..9af9ef976 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ OpenMW ====== -[![Build Status](https://api.travis-ci.org/OpenMW/openmw.svg)](https://travis-ci.org/OpenMW/openmw) [![Build status](https://ci.appveyor.com/api/projects/status/github/openmw/openmw?svg=true)](https://ci.appveyor.com/project/psi29a/openmw) [![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740) +[![Build Status](https://api.travis-ci.org/OpenMW/openmw.svg)](https://travis-ci.org/OpenMW/openmw) [![Build status](https://ci.appveyor.com/api/projects/status/github/openmw/openmw?svg=true)](https://ci.appveyor.com/project/psi29a/openmw) [![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740) [![pipeline status](https://gitlab.com/OpenMW/openmw/badges/master/pipeline.svg)](https://gitlab.com/OpenMW/openmw/commits/master) OpenMW is an open-source game engine that supports playing Morrowind by Bethesda Softworks. You need to own the game for OpenMW to play Morrowind. From 81b78a82e822941f35e73e531c89c975d1241536 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 1 Dec 2017 10:07:02 +0400 Subject: [PATCH 171/282] AI: try to open doors every AI_REACTION_TIME seconds (bug #4454) --- CHANGELOG.md | 1 + apps/openmw/mwclass/container.cpp | 15 +++--- apps/openmw/mwclass/door.cpp | 19 +++---- apps/openmw/mwmechanics/aipackage.cpp | 69 +++++++++++++------------- apps/openmw/mwmechanics/aipackage.hpp | 1 + apps/openmw/mwmechanics/obstacle.cpp | 21 +++++--- apps/openmw/mwmechanics/obstacle.hpp | 2 +- apps/openmw/mwworld/containerstore.cpp | 2 +- 8 files changed, 66 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc66bc61..90a198bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Bug #4454: AI opens doors too slow Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts Feature #4345: Add equivalents for the command line commands to Launcher Feature #4444: Per-group KF-animation files support diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index b6a46cff8..1d51a7830 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -139,24 +139,21 @@ namespace MWClass const std::string trapActivationSound = "Disarm Trap Fail"; MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr(); - const MWWorld::InventoryStore& invStore = player.getClass().getInventoryStore(player); + MWWorld::InventoryStore& invStore = player.getClass().getInventoryStore(player); bool isLocked = ptr.getCellRef().getLockLevel() > 0; bool isTrapped = !ptr.getCellRef().getTrap().empty(); bool hasKey = false; std::string keyName; - // make key id lowercase - std::string keyId = ptr.getCellRef().getKey(); - Misc::StringUtils::lowerCaseInPlace(keyId); - for (MWWorld::ConstContainerStoreIterator it = invStore.cbegin(); it != invStore.cend(); ++it) + const std::string keyId = ptr.getCellRef().getKey(); + if (!keyId.empty()) { - std::string refId = it->getCellRef().getRefId(); - Misc::StringUtils::lowerCaseInPlace(refId); - if (refId == keyId) + MWWorld::Ptr keyPtr = invStore.search(keyId); + if (!keyPtr.isEmpty()) { hasKey = true; - keyName = it->getClass().getName(*it); + keyName = keyPtr.getClass().getName(keyPtr); } } diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index 903ec4958..eba87a47b 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -114,7 +114,7 @@ namespace MWClass const std::string lockedSound = "LockedDoor"; const std::string trapActivationSound = "Disarm Trap Fail"; - const MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); + MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); bool isLocked = ptr.getCellRef().getLockLevel() > 0; bool isTrapped = !ptr.getCellRef().getTrap().empty(); @@ -135,21 +135,14 @@ namespace MWClass animation->addSpellCastGlow(effect, 1); // 1 second glow to match the time taken for a door opening or closing } - // make key id lowercase - std::string keyId = ptr.getCellRef().getKey(); + const std::string keyId = ptr.getCellRef().getKey(); if (!keyId.empty()) { - Misc::StringUtils::lowerCaseInPlace(keyId); - for (MWWorld::ConstContainerStoreIterator it = invStore.cbegin(); it != invStore.cend(); ++it) + MWWorld::Ptr keyPtr = invStore.search(keyId); + if (!keyPtr.isEmpty()) { - std::string refId = it->getCellRef().getRefId(); - Misc::StringUtils::lowerCaseInPlace(refId); - if (refId == keyId) - { - hasKey = true; - keyName = it->getClass().getName(*it); - break; - } + hasKey = true; + keyName = keyPtr.getClass().getName(keyPtr); } } diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 6a0f5b013..e6cca0523 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -120,6 +120,9 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr if (!isDestReached && mTimer > AI_REACTION_TIME) { + if (actor.getClass().isBipedal(actor)) + openDoors(actor); + bool wasShortcutting = mIsShortcutting; bool destInLOS = false; @@ -209,41 +212,10 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur // first check if obstacle is a door static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); - MWWorld::Ptr door = getNearbyDoor(actor, distance); - if (door != MWWorld::Ptr() && actor.getClass().isBipedal(actor)) + const MWWorld::Ptr door = getNearbyDoor(actor, distance); + if (!door.isEmpty() && actor.getClass().isBipedal(actor)) { - // note: AiWander currently does not open doors - if (getTypeId() != TypeIdWander && !door.getCellRef().getTeleport() && door.getClass().getDoorState(door) == 0) - { - if ((door.getCellRef().getTrap().empty() && door.getCellRef().getLockLevel() <= 0 )) - { - MWBase::Environment::get().getWorld()->activate(door, actor); - return; - } - - std::string keyId = door.getCellRef().getKey(); - if (keyId.empty()) - return; - - bool hasKey = false; - const MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); - - // make key id lowercase - Misc::StringUtils::lowerCaseInPlace(keyId); - for (MWWorld::ConstContainerStoreIterator it = invStore.cbegin(); it != invStore.cend(); ++it) - { - std::string refId = it->getCellRef().getRefId(); - Misc::StringUtils::lowerCaseInPlace(refId); - if (refId == keyId) - { - hasKey = true; - break; - } - } - - if (hasKey) - MWBase::Environment::get().getWorld()->activate(door, actor); - } + openDoors(actor); } else { @@ -251,6 +223,35 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur } } +void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor) +{ + static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); + + const MWWorld::Ptr door = getNearbyDoor(actor, distance); + if (door == MWWorld::Ptr()) + return; + + // note: AiWander currently does not open doors + if (getTypeId() != TypeIdWander && !door.getCellRef().getTeleport() && door.getClass().getDoorState(door) == 0) + { + if ((door.getCellRef().getTrap().empty() && door.getCellRef().getLockLevel() <= 0 )) + { + MWBase::Environment::get().getWorld()->activate(door, actor); + return; + } + + const std::string keyId = door.getCellRef().getKey(); + if (keyId.empty()) + return; + + MWWorld::ContainerStore &invStore = actor.getClass().getContainerStore(actor); + MWWorld::Ptr keyPtr = invStore.search(keyId); + + if (!keyPtr.isEmpty()) + MWBase::Environment::get().getWorld()->activate(door, actor); + } +} + const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const MWWorld::CellStore *cell) { const ESM::CellId& id = cell->getCell()->getCellId(); diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 7e8f905ad..2b685accc 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -123,6 +123,7 @@ namespace MWMechanics virtual bool doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest, const MWWorld::CellStore* currentCell); void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos); + void openDoors(const MWWorld::Ptr& actor); const PathgridGraph& getPathGridGraph(const MWWorld::CellStore* cell); diff --git a/apps/openmw/mwmechanics/obstacle.cpp b/apps/openmw/mwmechanics/obstacle.cpp index 3c6f14bfd..0635a5520 100644 --- a/apps/openmw/mwmechanics/obstacle.cpp +++ b/apps/openmw/mwmechanics/obstacle.cpp @@ -26,13 +26,13 @@ namespace MWMechanics bool proximityToDoor(const MWWorld::Ptr& actor, float minDist) { - if(getNearbyDoor(actor, minDist)!=MWWorld::Ptr()) - return true; - else + if(getNearbyDoor(actor, minDist).isEmpty()) return false; + else + return true; } - MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist) + const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist) { MWWorld::CellStore *cell = actor.getCell(); @@ -50,6 +50,16 @@ namespace MWMechanics const MWWorld::LiveCellRef& ref = *it; osg::Vec3f doorPos(ref.mData.getPosition().asVec3()); + + // FIXME: cast + const MWWorld::Ptr doorPtr = MWWorld::Ptr(&const_cast &>(ref), actor.getCell()); + + int doorState = doorPtr.getClass().getDoorState(doorPtr); + float doorRot = ref.mData.getPosition().rot[2] - doorPtr.getCellRef().getPosition().rot[2]; + + if (doorState != 0 || doorRot != 0) + continue; // the door is already opened/opening + doorPos.z() = 0; float angle = std::acos(actorDir * (doorPos - pos) / (actorDir.length() * (doorPos - pos).length())); @@ -62,8 +72,7 @@ namespace MWMechanics if ((pos - doorPos).length2() > minDist*minDist) continue; - // FIXME cast - return MWWorld::Ptr(&const_cast &>(ref), actor.getCell()); // found, stop searching + return doorPtr; // found, stop searching } return MWWorld::Ptr(); // none found diff --git a/apps/openmw/mwmechanics/obstacle.hpp b/apps/openmw/mwmechanics/obstacle.hpp index f71207346..6a84e0ef9 100644 --- a/apps/openmw/mwmechanics/obstacle.hpp +++ b/apps/openmw/mwmechanics/obstacle.hpp @@ -17,7 +17,7 @@ namespace MWMechanics /// Returns door pointer within range. No guarantee is given as to which one /** \return Pointer to the door, or NULL if none exists **/ - MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); + const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); class ObstacleCheck { diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index dd5d7a853..657d59c59 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -47,7 +47,7 @@ namespace for (typename MWWorld::CellRefList::List::iterator iter (list.mList.begin()); iter!=list.mList.end(); ++iter) { - if (Misc::StringUtils::ciEqual(iter->mBase->mId, id2)) + if (Misc::StringUtils::ciEqual(iter->mBase->mId, id2) && iter->mData.getCount()) { MWWorld::Ptr ptr (&*iter, 0); ptr.setContainerStore (store); From 7ca56ccd291868c4ad11e7c1210252393d935221 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 13 Jun 2018 15:48:24 +0200 Subject: [PATCH 172/282] set search status bar to 'no results' message when search yields no results --- apps/opencs/model/doc/document.cpp | 7 ++++--- apps/opencs/model/doc/document.hpp | 4 +++- apps/opencs/view/tools/searchsubview.cpp | 13 +++++++++++++ apps/opencs/view/tools/searchsubview.hpp | 2 ++ apps/opencs/view/world/tablebottombox.cpp | 16 ++++++++++++++++ apps/opencs/view/world/tablebottombox.hpp | 3 +++ 6 files changed, 41 insertions(+), 4 deletions(-) diff --git a/apps/opencs/model/doc/document.cpp b/apps/opencs/model/doc/document.cpp index 7a825ba39..e45d13aa9 100644 --- a/apps/opencs/model/doc/document.cpp +++ b/apps/opencs/model/doc/document.cpp @@ -320,12 +320,13 @@ CSMDoc::Document::Document (const Files::ConfigurationManager& configuration, connect (&mUndoStack, SIGNAL (cleanChanged (bool)), this, SLOT (modificationStateChanged (bool))); connect (&mTools, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); - connect (&mTools, SIGNAL (done (int, bool)), this, SLOT (operationDone (int, bool))); + connect (&mTools, SIGNAL (done (int, bool)), this, SIGNAL (operationDone (int, bool))); + connect (&mTools, SIGNAL (done (int, bool)), this, SLOT (operationDone2 (int, bool))); connect (&mTools, SIGNAL (mergeDone (CSMDoc::Document*)), this, SIGNAL (mergeDone (CSMDoc::Document*))); connect (&mSaving, SIGNAL (progress (int, int, int)), this, SLOT (progress (int, int, int))); - connect (&mSaving, SIGNAL (done (int, bool)), this, SLOT (operationDone (int, bool))); + connect (&mSaving, SIGNAL (done (int, bool)), this, SLOT (operationDone2 (int, bool))); connect ( &mSaving, SIGNAL (reportMessage (const CSMDoc::Message&, int)), @@ -437,7 +438,7 @@ void CSMDoc::Document::reportMessage (const CSMDoc::Message& message, int type) std::cout << message.mMessage << std::endl; } -void CSMDoc::Document::operationDone (int type, bool failed) +void CSMDoc::Document::operationDone2 (int type, bool failed) { if (type==CSMDoc::State_Saving && !failed) mDirty = false; diff --git a/apps/opencs/model/doc/document.hpp b/apps/opencs/model/doc/document.hpp index d31fd5aca..4c442428e 100644 --- a/apps/opencs/model/doc/document.hpp +++ b/apps/opencs/model/doc/document.hpp @@ -168,13 +168,15 @@ namespace CSMDoc /// document. This signal must be handled to avoid a leak. void mergeDone (CSMDoc::Document *document); + void operationDone (int type, bool failed); + private slots: void modificationStateChanged (bool clean); void reportMessage (const CSMDoc::Message& message, int type); - void operationDone (int type, bool failed); + void operationDone2 (int type, bool failed); void runStateChanged(); diff --git a/apps/opencs/view/tools/searchsubview.cpp b/apps/opencs/view/tools/searchsubview.cpp index b50e78227..9bada22af 100644 --- a/apps/opencs/view/tools/searchsubview.cpp +++ b/apps/opencs/view/tools/searchsubview.cpp @@ -3,6 +3,7 @@ #include #include "../../model/doc/document.hpp" +#include "../../model/doc/state.hpp" #include "../../model/tools/search.hpp" #include "../../model/tools/reportmodel.hpp" #include "../../model/world/idtablebase.hpp" @@ -105,6 +106,9 @@ CSVTools::SearchSubView::SearchSubView (const CSMWorld::UniversalId& id, CSMDoc: connect (document.getReport (id), SIGNAL (rowsInserted (const QModelIndex&, int, int)), this, SLOT (tableSizeUpdate())); + + connect (&document, SIGNAL (operationDone (int, bool)), + this, SLOT (operationDone (int, bool))); } void CSVTools::SearchSubView::setEditLock (bool locked) @@ -148,3 +152,12 @@ void CSVTools::SearchSubView::tableSizeUpdate() { mBottom->tableSizeChanged (mDocument.getReport (getUniversalId())->rowCount(), 0, 0); } + +void CSVTools::SearchSubView::operationDone (int type, bool failed) +{ + if (type==CSMDoc::State_Searching && !failed && + !mDocument.getReport (getUniversalId())->rowCount()) + { + mBottom->setStatusMessage ("No Results"); + } +} diff --git a/apps/opencs/view/tools/searchsubview.hpp b/apps/opencs/view/tools/searchsubview.hpp index d22367722..c0f3eac84 100644 --- a/apps/opencs/view/tools/searchsubview.hpp +++ b/apps/opencs/view/tools/searchsubview.hpp @@ -62,6 +62,8 @@ namespace CSVTools void replaceAllRequest(); void tableSizeUpdate(); + + void operationDone (int type, bool failed); }; } diff --git a/apps/opencs/view/world/tablebottombox.cpp b/apps/opencs/view/world/tablebottombox.cpp index cfde5c694..f6b060a8f 100644 --- a/apps/opencs/view/world/tablebottombox.cpp +++ b/apps/opencs/view/world/tablebottombox.cpp @@ -28,6 +28,12 @@ void CSVWorld::TableBottomBox::updateStatus() { if (mShowStatusBar) { + if (!mStatusMessage.isEmpty()) + { + mStatus->setText (mStatusMessage); + return; + } + static const char *sLabels[4] = { "record", "deleted", "touched", "selected" }; static const char *sLabelsPlural[4] = { "records", "deleted", "touched", "selected" }; @@ -178,10 +184,17 @@ void CSVWorld::TableBottomBox::currentWidgetChanged(int /*index*/) updateSize(); } +void CSVWorld::TableBottomBox::setStatusMessage (const QString& message) +{ + mStatusMessage = message; + updateStatus(); +} + void CSVWorld::TableBottomBox::selectionSizeChanged (int size) { if (mStatusCount[3]!=size) { + mStatusMessage = ""; mStatusCount[3] = size; updateStatus(); } @@ -210,7 +223,10 @@ void CSVWorld::TableBottomBox::tableSizeChanged (int size, int deleted, int modi } if (changed) + { + mStatusMessage = ""; updateStatus(); + } } void CSVWorld::TableBottomBox::positionChanged (int row, int column) diff --git a/apps/opencs/view/world/tablebottombox.hpp b/apps/opencs/view/world/tablebottombox.hpp index 5402c466e..baa68087b 100644 --- a/apps/opencs/view/world/tablebottombox.hpp +++ b/apps/opencs/view/world/tablebottombox.hpp @@ -39,6 +39,7 @@ namespace CSVWorld bool mHasPosition; int mRow; int mColumn; + QString mStatusMessage; private: @@ -73,6 +74,8 @@ namespace CSVWorld /// /// \note The BotomBox does not partake in the deletion of records. + void setStatusMessage (const QString& message); + signals: void requestFocus (const std::string& id); From 61c968d5507670f9e3fce874e8abd48f0c819a85 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 13 Jun 2018 12:56:58 +0400 Subject: [PATCH 173/282] Ignore broken items when search for replacement (bug #4453) --- apps/openmw/mwgui/quickkeysmenu.cpp | 88 ++++++++++++++++---------- apps/openmw/mwgui/quickkeysmenu.hpp | 1 + apps/openmw/mwmechanics/actors.cpp | 18 +----- apps/openmw/mwworld/containerstore.cpp | 24 +++++++ apps/openmw/mwworld/containerstore.hpp | 3 + 5 files changed, 85 insertions(+), 49 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 2ce9d04e5..08192625f 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -81,6 +81,47 @@ namespace MWGui delete mMagicSelectionDialog; } + void QuickKeysMenu::onOpen() + { + WindowBase::onOpen(); + + MWWorld::Ptr player = MWMechanics::getPlayer(); + MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); + + // Check if quick keys are still valid + for (int i=0; i<10; ++i) + { + ItemWidget* button = mQuickKeyButtons[i]; + int type = mAssigned[i]; + + switch (type) + { + case Type_Unassigned: + case Type_HandToHand: + case Type_Magic: + break; + case Type_Item: + case Type_MagicItem: + { + MWWorld::Ptr item = *button->getUserData(); + // Make sure the item is available and is not broken + if (item.getRefData().getCount() < 1 || + (item.getClass().hasItemHealth(item) && + item.getClass().getItemHealth(item) <= 0)) + { + // Try searching for a compatible replacement + std::string id = item.getCellRef().getRefId(); + + item = store.findReplacement(id); + button->setUserData(MWWorld::Ptr(item)); + break; + } + } + } + } + + } + void QuickKeysMenu::unassign(ItemWidget* key, int index) { key->clearUserStrings(); @@ -122,12 +163,10 @@ namespace MWGui assert(index != -1); mSelectedIndex = index; - { - // open assign dialog - if (!mAssignDialog) - mAssignDialog = new QuickKeysMenuAssign(this); - mAssignDialog->setVisible (true); - } + // open assign dialog + if (!mAssignDialog) + mAssignDialog = new QuickKeysMenuAssign(this); + mAssignDialog->setVisible (true); } void QuickKeysMenu::onOkButtonClicked (MyGUI::Widget *sender) @@ -296,21 +335,16 @@ namespace MWGui if (type == Type_Item || type == Type_MagicItem) { MWWorld::Ptr item = *button->getUserData(); - // make sure the item is available - if (item.getRefData ().getCount() < 1) + // Make sure the item is available and is not broken + if (item.getRefData().getCount() < 1 || + (item.getClass().hasItemHealth(item) && + item.getClass().getItemHealth(item) <= 0)) { // Try searching for a compatible replacement std::string id = item.getCellRef().getRefId(); - for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) - { - if (Misc::StringUtils::ciEqual(it->getCellRef().getRefId(), id)) - { - item = *it; - button->setUserData(MWWorld::Ptr(item)); - break; - } - } + item = store.findReplacement(id); + button->setUserData(MWWorld::Ptr(item)); if (item.getRefData().getCount() < 1) { @@ -498,6 +532,9 @@ namespace MWGui ESM::QuickKeys keys; keys.load(reader); + MWWorld::Ptr player = MWMechanics::getPlayer(); + MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); + int i=0; for (std::vector::const_iterator it = keys.mKeys.begin(); it != keys.mKeys.end(); ++it) { @@ -519,22 +556,7 @@ namespace MWGui case Type_MagicItem: { // Find the item by id - MWWorld::Ptr player = MWMechanics::getPlayer(); - MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); - MWWorld::Ptr item; - for (MWWorld::ContainerStoreIterator iter = store.begin(); iter != store.end(); ++iter) - { - if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), id)) - { - if (item.isEmpty() || - // Prefer the stack with the lowest remaining uses - !item.getClass().hasItemHealth(*iter) || - iter->getClass().getItemHealth(*iter) < item.getClass().getItemHealth(item)) - { - item = *iter; - } - } - } + MWWorld::Ptr item = store.findReplacement(id); if (item.isEmpty()) unassign(button, i); diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index 0070aa55b..b5bc60b19 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -34,6 +34,7 @@ namespace MWGui void onAssignMagicItem (MWWorld::Ptr item); void onAssignMagic (const std::string& spellId); void onAssignMagicCancel (); + void onOpen(); void activateQuickKey(int index); void updateActivatedQuickKey(); diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index de394c446..f1bc6907c 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -288,23 +288,9 @@ namespace MWMechanics if (prevItemId.empty()) return; - // Find the item by id - MWWorld::Ptr item; - for (MWWorld::ContainerStoreIterator iter = store.begin(); iter != store.end(); ++iter) - { - if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), prevItemId)) - { - if (item.isEmpty() || - // Prefer the stack with the lowest remaining uses - !item.getClass().hasItemHealth(*iter) || - iter->getClass().getItemHealth(*iter) < item.getClass().getItemHealth(item)) - { - item = *iter; - } - } - } - + // Find previous item (or its replacement) by id. // we should equip previous item only if expired bound item was equipped. + MWWorld::Ptr item = store.findReplacement(prevItemId); if (item.isEmpty() || !wasEquipped) return; diff --git a/apps/openmw/mwworld/containerstore.cpp b/apps/openmw/mwworld/containerstore.cpp index dd5d7a853..c92d51701 100644 --- a/apps/openmw/mwworld/containerstore.cpp +++ b/apps/openmw/mwworld/containerstore.cpp @@ -675,6 +675,30 @@ int MWWorld::ContainerStore::getType (const ConstPtr& ptr) "Object '" + ptr.getCellRef().getRefId() + "' of type " + ptr.getTypeName() + " can not be placed into a container"); } +MWWorld::Ptr MWWorld::ContainerStore::findReplacement(const std::string& id) +{ + MWWorld::Ptr item; + int itemHealth = 1; + for (MWWorld::ContainerStoreIterator iter = begin(); iter != end(); ++iter) + { + int iterHealth = iter->getClass().hasItemHealth(*iter) ? iter->getClass().getItemHealth(*iter) : 1; + if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), id)) + { + // Prefer the stack with the lowest remaining uses + // Try to get item with zero durability only if there are no other items found + if (item.isEmpty() || + (iterHealth > 0 && iterHealth < itemHealth) || + (itemHealth <= 0 && iterHealth > 0)) + { + item = *iter; + itemHealth = iterHealth; + } + } + } + + return item; +} + MWWorld::Ptr MWWorld::ContainerStore::search (const std::string& id) { { diff --git a/apps/openmw/mwworld/containerstore.hpp b/apps/openmw/mwworld/containerstore.hpp index dbb82cbda..b67eb6552 100644 --- a/apps/openmw/mwworld/containerstore.hpp +++ b/apps/openmw/mwworld/containerstore.hpp @@ -198,6 +198,9 @@ namespace MWWorld ///< This function throws an exception, if ptr does not point to an object, that can be /// put into a container. + Ptr findReplacement(const std::string& id); + ///< Returns replacement for object with given id. Prefer used items (with low durability left). + Ptr search (const std::string& id); virtual void writeState (ESM::InventoryState& state) const; From 48d74a8781b8ba1f50fce9bc0eeb1cf09c34caa6 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 13 Jun 2018 15:34:13 +0000 Subject: [PATCH 174/282] Disable testing for now, not yet necessary. --- .gitlab-ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 74bc74ca8..9f5442ae4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,8 +35,8 @@ build: paths: - "*.o" -# run tests using the binary built before -test: - stage: test - script: - - ls +# TODO: run tests using the binary built before +#test: +# stage: test +# script: +# - ls From e814843cdbf82d501f9fb64caa8a91d34f9adf30 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 13 Jun 2018 22:28:34 +0400 Subject: [PATCH 175/282] Add missing changelog entries --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7871998a3..dfea001f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Bug #2222: Fatigue's effect on selling price is backwards Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped Bug #2835: Player able to slowly move when overencumbered + Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #3374: Touch spells not hitting kwama foragers Bug #3591: Angled hit distance too low Bug #3629: DB assassin attack never triggers creature spawning @@ -20,11 +21,14 @@ Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4327: Missing animations during spell/weapon stance switching Bug #4368: Settings window ok button doesn't have key focus by default + Bug #4393: NPCs walk back to where they were after using ResetActors Bug #4419: MRK NiStringExtraData is handled incorrectly Bug #4426: RotateWorld behavior is incorrect Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Bug #4451: Script fails to compile when using "Begin, [ScriptName]" syntax + Bug #4453: Quick keys behaviour is invalid for equipment Bug #4454: AI opens doors too slow Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts From 5387e3ba8f5475a030cd98948441d39dbfd7f58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 13 Jun 2018 21:15:22 +0200 Subject: [PATCH 176/282] Update CHANGELOG --- CHANGELOG.md | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2a5472e..3a59aca8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,140 @@ +0.45.0 +------ + + Bug #1990: Sunrise/sunset not set correct + Bug #2222: Fatigue's effect on selling price is backwards + Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped + Bug #2835: Player able to slowly move when overencumbered + Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y + Bug #3374: Touch spells not hitting kwama foragers + Bug #3591: Angled hit distance too low + Bug #3629: DB assassin attack never triggers creature spawning + Bug #3876: Landscape texture painting is misaligned + Bug #3897: Have Goodbye give all choices the effects of Goodbye + Bug #3993: Terrain texture blending map is not upscaled + Bug #3997: Almalexia doesn't pace + Bug #4036: Weird behaviour of AI packages if package target has non-unique ID + Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully + Bug #4215: OpenMW shows book text after last
tag + Bug #4221: Characters get stuck in V-shaped terrain + Bug #4251: Stationary NPCs do not return to their position after combat + Bug #4293: Faction members are not aware of faction ownerships in barter + Bug #4327: Missing animations during spell/weapon stance switching + Bug #4368: Settings window ok button doesn't have key focus by default + Bug #4393: NPCs walk back to where they were after using ResetActors + Bug #4419: MRK NiStringExtraData is handled incorrectly + Bug #4426: RotateWorld behavior is incorrect + Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 + Bug #4432: Guards behaviour is incorrect if they do not have AI packages + Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Bug #4451: Script fails to compile when using "Begin, [ScriptName]" syntax + Bug #4453: Quick keys behaviour is invalid for equipment + Bug #4454: AI opens doors too slow + Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results + Feature #4222: 360° screenshots + Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts + Feature #4345: Add equivalents for the command line commands to Launcher + Feature #4444: Per-group KF-animation files support + +0.44.0 +------ + + Bug #1428: Daedra summoning scripts aren't executed when the item is taken through the inventory + Bug #1987: Some glyphs are not supported + Bug #2254: Magic related visual effects are not rendered when loading a saved game + Bug #2485: Journal alphabetical index doesn't match "Morrowind content language" setting + Bug #2703: OnPCHitMe is not handled correctly + Bug #2829: Incorrect order for content list consisting of a game file and an esp without dependencies + Bug #2841: "Total eclipse" happens if weather settings are not defined. + Bug #2897: Editor: Rename "Original creature" field + Bug #3278: Editor: Unchecking "Auto Calc" flag changes certain values + Bug #3343: Editor: ID sorting is case-sensitive in certain tables + Bug #3557: Resource priority confusion when using the local data path as installation root + Bug #3587: Pathgrid and Flying Creatures wrong behaviour – abotWhereAreAllBirdsGoing + Bug #3603: SetPos should not skip weather transitions + Bug #3618: Myar Aranath total conversion can't be started due to capital-case extension of the master file + Bug #3638: Fast forwarding can move NPC inside objects + Bug #3664: Combat music does not start in dialogue + Bug #3696: Newlines are accompanied by empty rectangle glyph in dialogs + Bug #3708: Controllers broken on macOS + Bug #3726: Items with suppressed activation can be picked up via the inventory menu + Bug #3783: [Mod] Abot's Silt Striders 1.16 - silt strider "falls" to ground and glides on floor during travel + Bug #3863: Can be forced to not resist arrest if you cast Calm Humanoid on aggroed death warrant guards + Bug #3884: Incorrect enemy behavior when exhausted + Bug #3926: Installation Wizard places Morrowind.esm after Tribunal/Bloodmoon if it has a later file creation date + Bug #4061: Scripts error on special token included in name + Bug #4111: Crash when mouse over soulgem with a now-missing soul + Bug #4122: Swim animation should not be interrupted during underwater attack + Bug #4134: Battle music behaves different than vanilla + Bug #4135: Reflecting an absorb spell different from vanilla + Bug #4136: Enchanted weapons without "ignore normal weapons" flag don't bypass creature "ignore normal weapons" effect + Bug #4143: Antialiasing produces graphical artifacts when used with shader lighting + Bug #4159: NPCs' base skeleton files should not be optimized + Bug #4177: Jumping/landing animation interference/flickering + Bug #4179: NPCs do not face target + Bug #4180: Weapon switch sound playing even though no weapon is switched + Bug #4184: Guards can initiate dialogue even though you are far above them + Bug #4190: Enchanted clothes changes visibility with Chameleon on equip/unequip + Bug #4191: "screenshot saved" message also appears in the screenshot image + Bug #4192: Archers in OpenMW have shorter attack range than archers in Morrowind + Bug #4210: Some dialogue topics are not highlighted on first encounter + Bug #4211: FPS drops after minimizing the game during rainy weather + Bug #4216: Thrown weapon projectile doesn't rotate + Bug #4223: Displayed spell casting chance must be 0 if player doesn't have enough magicka to cast it + Bug #4225: Double "Activate" key presses with Mouse and Gamepad. + Bug #4226: The current player's class should be default value in the class select menu + Bug #4229: Tribunal/Bloodmoon summoned creatures fight other summons + Bug #4233: W and A keys override S and D Keys + Bug #4235: Wireframe mode affects local map + Bug #4239: Quick load from container screen causes crash + Bug #4242: Crime greetings display in Journal + Bug #4245: Merchant NPCs sell ingredients growing on potted plants they own + Bug #4246: Take armor condition into account when calcuting armor rating + Bug #4250: Jumping is not as fluid as it was pre-0.43.0 + Bug #4252: "Error in frame: FFmpeg exception: Failed to allocate input stream" message spam if OpenMW encounter non-music file in the Music folder + Bug #4261: Magic effects from eaten ingredients always have 1 sec duration + Bug #4263: Arrow position is incorrect in 3rd person view during attack for beast races + Bug #4264: Player in god mode can be affected by some negative spell effects + Bug #4269: Crash when hovering the faction section and the 'sAnd' GMST is missing (as in MW 1.0) + Bug #4272: Root note transformations are discarded again + Bug #4279: Sometimes cells are not marked as explored on the map + Bug #4298: Problem with MessageBox and chargen menu interaction order + Bug #4301: Optimizer breaks LOD nodes + Bug #4308: PlaceAtMe doesn't inherit scale of calling object + Bug #4309: Only harmful effects with resistance effect set are resistable + Bug #4313: Non-humanoid creatures are capable of opening doors + Bug #4314: Rainy weather slows down the game when changing from indoors/outdoors + Bug #4319: Collisions for certain meshes are incorrectly ignored + Bug #4320: Using mouse 1 to move forward causes selection dialogues to jump selections forward. + Bug #4322: NPC disposition: negative faction reaction modifier doesn't take PC rank into account + Bug #4328: Ownership by dead actors is not cleared from picked items + Bug #4334: Torch and shield usage inconsistent with original game + Bug #4336: Wizard: Incorrect Morrowind assets path autodetection + Bug #4343: Error message for coc and starting cell shouldn't imply that it only works for interior cells + Bug #4346: Count formatting does not work well with very high numbers + Bug #4351: Using AddSoulgem fills all soul gems of the specified type + Bug #4391: No visual indication is provided when an unavailable spell fails to be chosen via a quick key + Bug #4392: Inventory filter breaks after loading a game + Bug #4405: No default terrain in empty cells when distant terrain is enabled + Bug #4410: [Mod] Arktwend: OpenMW does not use default marker definitions + Bug #4412: openmw-iniimporter ignores data paths from config + Bug #4413: Moving with 0 strength uses all of your fatigue + Bug #4420: Camera flickering when I open up and close menus while sneaking + Bug #4435: Item health is considered a signed integer + Bug #4441: Adding items to currently disabled weapon-wielding creatures crashes the game + Feature #1786: Round up encumbrance value in the encumbrance bar + Feature #2694: Editor: rename "model" column to make its purpose clear + Feature #3870: Editor: Terrain Texture Brush Button + Feature #3872: Editor: Edit functions in terrain texture editing mode + Feature #4054: Launcher: Create menu for settings.cfg options + Feature #4064: Option for fast travel services to charge for the first companion + Feature #4142: Implement fWereWolfHealth GMST + Feature #4174: Multiple quicksaves + Feature #4407: Support NiLookAtController + Feature #4423: Rebalance soul gem values + Task #4015: Use AppVeyor build artifact features to make continuous builds available + Editor: New (and more complete) icon set + 0.43.0 ------ From 7178ee3a6e94b1001042a8bbbc292e023709f3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Wed, 13 Jun 2018 21:34:43 +0200 Subject: [PATCH 177/282] Add FIXME comment --- apps/openmw/mwinput/inputmanagerimp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 1e8a12e2c..c440de455 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1057,7 +1057,7 @@ namespace MWInput if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(),settingStr)) { (*mScreenCaptureOperation) (*(screenshot.get()),0); - // mScreenCaptureHandler->getCaptureOperation() causes crash for some reason + // FIXME: mScreenCaptureHandler->getCaptureOperation() causes crash for some reason screenshotTaken = true; } } From 1fd5ad3e56ce9f3690569b95b3287a0d19be6bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 14 Jun 2018 01:01:22 +0200 Subject: [PATCH 178/282] Use REAL_SIZE constant --- components/terrain/cellborder.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/terrain/cellborder.cpp b/components/terrain/cellborder.cpp index b74e2e9be..31a162af7 100644 --- a/components/terrain/cellborder.cpp +++ b/components/terrain/cellborder.cpp @@ -5,6 +5,7 @@ #include #include "world.hpp" +#include "../esm/loadland.hpp" namespace MWRender { @@ -18,7 +19,7 @@ CellBorder::CellBorder(Terrain::World *world, osg::Group *root): void CellBorder::createCellBorderGeometry(int x, int y) { - const int cellSize = 8192; + const int cellSize = ESM::Land::REAL_SIZE; const int borderSegments = 40; const float offset = 10.0; From 1b8d500c07892eda46219c63ebd324f38e9cd2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 14 Jun 2018 12:01:09 +0200 Subject: [PATCH 179/282] Make tb command work again --- components/terrain/cellborder.cpp | 13 ++++++++----- components/terrain/cellborder.hpp | 6 +++++- components/terrain/terraingrid.cpp | 24 ++++++++++++++++++++---- components/terrain/terraingrid.hpp | 6 +++++- components/terrain/world.cpp | 28 ---------------------------- components/terrain/world.hpp | 15 +++------------ 6 files changed, 41 insertions(+), 51 deletions(-) diff --git a/components/terrain/cellborder.cpp b/components/terrain/cellborder.cpp index 31a162af7..f9af60b75 100644 --- a/components/terrain/cellborder.cpp +++ b/components/terrain/cellborder.cpp @@ -12,8 +12,7 @@ namespace MWRender CellBorder::CellBorder(Terrain::World *world, osg::Group *root): mWorld(world), - mRoot(root), - mBorderRoot(0) + mRoot(root) { } @@ -71,8 +70,6 @@ void CellBorder::createCellBorderGeometry(int x, int y) mRoot->addChild(borderGeode); - mBorderRoot = borderGeode; - mCellBorderNodes[std::make_pair(x,y)] = borderGeode; } @@ -84,9 +81,15 @@ void CellBorder::destroyCellBorderGeometry(int x, int y) return; osg::ref_ptr borderNode = it->second; - mBorderRoot->removeChild(borderNode); + mRoot->removeChild(borderNode); mCellBorderNodes.erase(it); } +void CellBorder::destroyCellBorderGeometry() +{ + for (CellGrid::iterator it = mCellBorderNodes.begin(); it != mCellBorderNodes.end(); ++it) + destroyCellBorderGeometry(it->first.first,it->first.second); +} + } diff --git a/components/terrain/cellborder.hpp b/components/terrain/cellborder.hpp index 6144f8c32..a505aec9c 100644 --- a/components/terrain/cellborder.hpp +++ b/components/terrain/cellborder.hpp @@ -24,10 +24,14 @@ namespace MWRender void createCellBorderGeometry(int x, int y); void destroyCellBorderGeometry(int x, int y); + /** + Destroys the geometry for all borders. + */ + void destroyCellBorderGeometry(); + protected: Terrain::World *mWorld; osg::Group *mRoot; - osg::Group *mBorderRoot; CellGrid mCellBorderNodes; }; diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp index f5ab6f64b..edd7de5da 100644 --- a/components/terrain/terraingrid.cpp +++ b/components/terrain/terraingrid.cpp @@ -19,8 +19,9 @@ public: TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) : Terrain::World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) - , mNumSplits(4) + , mNumSplits(4), mBorderVisible(false) { + mCellBorder.reset(new MWRender::CellBorder(this,mTerrainRoot.get())); } TerrainGrid::~TerrainGrid() @@ -65,6 +66,19 @@ osg::ref_ptr TerrainGrid::buildTerrain (osg::Group* parent, float chu } } +void TerrainGrid::setBordersVisible(bool visible) +{ + mBorderVisible = visible; + + if (visible) + { + for (MWRender::CellBorder::CellGrid::iterator it = mGrid.begin(); it != mGrid.end(); ++it) + mCellBorder->createCellBorderGeometry(it->first.first,it->first.second); + } + else + mCellBorder->destroyCellBorderGeometry(); +} + void TerrainGrid::loadCell(int x, int y) { if (mGrid.find(std::make_pair(x, y)) != mGrid.end()) @@ -75,7 +89,8 @@ void TerrainGrid::loadCell(int x, int y) if (!terrainNode) return; // no terrain defined - Terrain::World::loadCell(x,y); + if (mBorderVisible) + mCellBorder->createCellBorderGeometry(x,y); mTerrainRoot->addChild(terrainNode); @@ -84,12 +99,13 @@ void TerrainGrid::loadCell(int x, int y) void TerrainGrid::unloadCell(int x, int y) { - World::unloadCell(x,y); - MWRender::CellBorder::CellGrid::iterator it = mGrid.find(std::make_pair(x,y)); if (it == mGrid.end()) return; + if (mBorderVisible) + mCellBorder->destroyCellBorderGeometry(x,y); + osg::ref_ptr terrainNode = it->second; mTerrainRoot->removeChild(terrainNode); diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp index f21dd39d3..164f09bd6 100644 --- a/components/terrain/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -29,6 +29,7 @@ namespace Terrain View* createView(); + virtual void setBordersVisible(bool visible) override; private: osg::ref_ptr buildTerrain (osg::Group* parent, float chunkSize, const osg::Vec2f& chunkCenter); @@ -36,8 +37,11 @@ namespace Terrain unsigned int mNumSplits; MWRender::CellBorder::CellGrid mGrid; - }; + std::unique_ptr mCellBorder; + + bool mBorderVisible; + }; } #endif diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index d871d141b..b88e3f157 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -28,12 +28,6 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mTerrainRoot->setName("Terrain Root"); - mBorderRoot = new osg::Switch; - mBorderRoot->setName("Border Root"); - mBorderRoot->setNodeMask(borderMask); - - mTerrainRoot->addChild(mBorderRoot); - osg::ref_ptr compositeCam = new osg::Camera; compositeCam->setRenderOrder(osg::Camera::PRE_RENDER, -1); compositeCam->setProjectionMatrix(osg::Matrix::identity()); @@ -53,30 +47,8 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mTextureManager.reset(new TextureManager(mResourceSystem->getSceneManager())); mChunkManager.reset(new ChunkManager(mStorage, mResourceSystem->getSceneManager(), mTextureManager.get(), mCompositeMapRenderer)); - mCellBorder.reset(new MWRender::CellBorder(this,mTerrainRoot.get())); - mResourceSystem->addResourceManager(mChunkManager.get()); mResourceSystem->addResourceManager(mTextureManager.get()); - - setBordersVisible(false); -} - -void World::setBordersVisible(bool visible) -{ - if (visible) - mBorderRoot->setAllChildrenOn(); - else - mBorderRoot->setAllChildrenOff(); -} - -void World::loadCell(int x, int y) -{ - mCellBorder->createCellBorderGeometry(x,y); -} - -void World::unloadCell(int x, int y) -{ - mCellBorder->destroyCellBorderGeometry(x,y); } World::~World() diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index b847d08ab..77aa1ee28 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -4,14 +4,11 @@ #include #include #include -#include #include #include "defs.hpp" -#include "cellborder.hpp" - namespace osg { class Group; @@ -79,15 +76,15 @@ namespace Terrain /// Load the cell into the scene graph. /// @note Not thread safe. - virtual void loadCell(int x, int y); + virtual void loadCell(int x, int y) {} /// Remove the cell from the scene graph. /// @note Not thread safe. - virtual void unloadCell(int x, int y); + virtual void unloadCell(int x, int y) {} virtual void enable(bool enabled) {} - void setBordersVisible(bool visible); + virtual void setBordersVisible(bool visible) {} /// Create a View to use with preload feature. The caller is responsible for deleting the view. /// @note Thread safe. @@ -104,14 +101,10 @@ namespace Terrain Storage* getStorage() { return mStorage; } protected: - void createCellBorderGeometry(int x, int y); - void destroyCellBorderGeometry(int x, int y); - Storage* mStorage; osg::ref_ptr mParent; osg::ref_ptr mTerrainRoot; - osg::ref_ptr mBorderRoot; osg::ref_ptr mCompositeMapCamera; osg::ref_ptr mCompositeMapRenderer; @@ -120,8 +113,6 @@ namespace Terrain std::unique_ptr mTextureManager; std::unique_ptr mChunkManager; - - std::unique_ptr mCellBorder; }; } From f18d57429e434505e3e428255cc46e2c6d245ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 14 Jun 2018 12:27:22 +0200 Subject: [PATCH 180/282] Move cell border management to World --- components/terrain/terraingrid.cpp | 9 +++------ components/terrain/terraingrid.hpp | 6 ------ components/terrain/world.cpp | 22 ++++++++++++++++++++++ components/terrain/world.hpp | 11 ++++++++--- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp index edd7de5da..6329accb2 100644 --- a/components/terrain/terraingrid.cpp +++ b/components/terrain/terraingrid.cpp @@ -19,9 +19,8 @@ public: TerrainGrid::TerrainGrid(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSystem* resourceSystem, Storage* storage, int nodeMask, int preCompileMask, int borderMask) : Terrain::World(parent, compileRoot, resourceSystem, storage, nodeMask, preCompileMask, borderMask) - , mNumSplits(4), mBorderVisible(false) + , mNumSplits(4) { - mCellBorder.reset(new MWRender::CellBorder(this,mTerrainRoot.get())); } TerrainGrid::~TerrainGrid() @@ -89,8 +88,7 @@ void TerrainGrid::loadCell(int x, int y) if (!terrainNode) return; // no terrain defined - if (mBorderVisible) - mCellBorder->createCellBorderGeometry(x,y); + TerrainGrid::World::loadCell(x,y); mTerrainRoot->addChild(terrainNode); @@ -103,8 +101,7 @@ void TerrainGrid::unloadCell(int x, int y) if (it == mGrid.end()) return; - if (mBorderVisible) - mCellBorder->destroyCellBorderGeometry(x,y); + Terrain::World::unloadCell(x,y); osg::ref_ptr terrainNode = it->second; mTerrainRoot->removeChild(terrainNode); diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp index 164f09bd6..0537dce42 100644 --- a/components/terrain/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -7,8 +7,6 @@ #include "world.hpp" -#include "cellborder.hpp" - namespace Terrain { @@ -37,10 +35,6 @@ namespace Terrain unsigned int mNumSplits; MWRender::CellBorder::CellGrid mGrid; - - std::unique_ptr mCellBorder; - - bool mBorderVisible; }; } diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index b88e3f157..578131cc5 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -18,6 +18,7 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst : mStorage(storage) , mParent(parent) , mResourceSystem(resourceSystem) + , mBorderVisible(false) { mTerrainRoot = new osg::Group; mTerrainRoot->setNodeMask(nodeMask); @@ -46,6 +47,7 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mTextureManager.reset(new TextureManager(mResourceSystem->getSceneManager())); mChunkManager.reset(new ChunkManager(mStorage, mResourceSystem->getSceneManager(), mTextureManager.get(), mCompositeMapRenderer)); + mCellBorder.reset(new MWRender::CellBorder(this,mTerrainRoot.get())); mResourceSystem->addResourceManager(mChunkManager.get()); mResourceSystem->addResourceManager(mTextureManager.get()); @@ -64,6 +66,26 @@ World::~World() delete mStorage; } +void World::setBordersVisible(bool visible) +{ + mBorderVisible = visible; + + if (!visible) + mCellBorder->destroyCellBorderGeometry(); +} + +void World::loadCell(int x, int y) +{ + if (mBorderVisible) + mCellBorder->createCellBorderGeometry(x,y); +} + +void World::unloadCell(int x, int y) +{ + if (mBorderVisible) + mCellBorder->destroyCellBorderGeometry(x,y); +} + void World::setTargetFrameRate(float rate) { mCompositeMapRenderer->setTargetFrameRate(rate); diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index 77aa1ee28..5ea0559df 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -8,6 +8,7 @@ #include #include "defs.hpp" +#include "cellborder.hpp" namespace osg { @@ -76,15 +77,15 @@ namespace Terrain /// Load the cell into the scene graph. /// @note Not thread safe. - virtual void loadCell(int x, int y) {} + virtual void loadCell(int x, int y); /// Remove the cell from the scene graph. /// @note Not thread safe. - virtual void unloadCell(int x, int y) {} + virtual void unloadCell(int x, int y); virtual void enable(bool enabled) {} - virtual void setBordersVisible(bool visible) {} + virtual void setBordersVisible(bool visible); /// Create a View to use with preload feature. The caller is responsible for deleting the view. /// @note Thread safe. @@ -113,6 +114,10 @@ namespace Terrain std::unique_ptr mTextureManager; std::unique_ptr mChunkManager; + + std::unique_ptr mCellBorder; + + bool mBorderVisible; }; } From 414e6caafe9c8626a18d1b42975c454892e92572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 14 Jun 2018 13:14:38 +0200 Subject: [PATCH 181/282] Make tb work with distant terrain --- components/terrain/terraingrid.cpp | 13 ------------- components/terrain/terraingrid.hpp | 1 - components/terrain/world.cpp | 11 ++++++++++- components/terrain/world.hpp | 5 ++++- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/components/terrain/terraingrid.cpp b/components/terrain/terraingrid.cpp index 6329accb2..74f683774 100644 --- a/components/terrain/terraingrid.cpp +++ b/components/terrain/terraingrid.cpp @@ -65,19 +65,6 @@ osg::ref_ptr TerrainGrid::buildTerrain (osg::Group* parent, float chu } } -void TerrainGrid::setBordersVisible(bool visible) -{ - mBorderVisible = visible; - - if (visible) - { - for (MWRender::CellBorder::CellGrid::iterator it = mGrid.begin(); it != mGrid.end(); ++it) - mCellBorder->createCellBorderGeometry(it->first.first,it->first.second); - } - else - mCellBorder->destroyCellBorderGeometry(); -} - void TerrainGrid::loadCell(int x, int y) { if (mGrid.find(std::make_pair(x, y)) != mGrid.end()) diff --git a/components/terrain/terraingrid.hpp b/components/terrain/terraingrid.hpp index 0537dce42..87e3b432c 100644 --- a/components/terrain/terraingrid.hpp +++ b/components/terrain/terraingrid.hpp @@ -27,7 +27,6 @@ namespace Terrain View* createView(); - virtual void setBordersVisible(bool visible) override; private: osg::ref_ptr buildTerrain (osg::Group* parent, float chunkSize, const osg::Vec2f& chunkCenter); diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index 578131cc5..f0f628561 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -70,7 +70,12 @@ void World::setBordersVisible(bool visible) { mBorderVisible = visible; - if (!visible) + if (visible) + { + for (std::set>::iterator it = mLoadedCells.begin(); it != mLoadedCells.end(); ++it) + mCellBorder->createCellBorderGeometry(it->first,it->second); + } + else mCellBorder->destroyCellBorderGeometry(); } @@ -78,12 +83,16 @@ void World::loadCell(int x, int y) { if (mBorderVisible) mCellBorder->createCellBorderGeometry(x,y); + + mLoadedCells.insert(std::pair(x,y)); } void World::unloadCell(int x, int y) { if (mBorderVisible) mCellBorder->destroyCellBorderGeometry(x,y); + + mLoadedCells.erase(std::pair(x,y)); } void World::setTargetFrameRate(float rate) diff --git a/components/terrain/world.hpp b/components/terrain/world.hpp index 5ea0559df..ae71693bd 100644 --- a/components/terrain/world.hpp +++ b/components/terrain/world.hpp @@ -6,6 +6,7 @@ #include #include +#include #include "defs.hpp" #include "cellborder.hpp" @@ -117,7 +118,9 @@ namespace Terrain std::unique_ptr mCellBorder; - bool mBorderVisible; + bool mBorderVisible; + + std::set> mLoadedCells; }; } From ab8de9fa14e0302f097c181196ff0db205406dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 14 Jun 2018 13:18:37 +0200 Subject: [PATCH 182/282] Set node mask to cell borders --- components/terrain/cellborder.cpp | 7 +++++-- components/terrain/cellborder.hpp | 3 ++- components/terrain/world.cpp | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/components/terrain/cellborder.cpp b/components/terrain/cellborder.cpp index f9af60b75..d9e6d52fc 100644 --- a/components/terrain/cellborder.cpp +++ b/components/terrain/cellborder.cpp @@ -10,9 +10,10 @@ namespace MWRender { -CellBorder::CellBorder(Terrain::World *world, osg::Group *root): +CellBorder::CellBorder(Terrain::World *world, osg::Group *root, int borderMask): mWorld(world), - mRoot(root) + mRoot(root), + mBorderMask(borderMask) { } @@ -68,6 +69,8 @@ void CellBorder::createCellBorderGeometry(int x, int y) polygonmode->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE); stateSet->setAttributeAndModes(polygonmode,osg::StateAttribute::ON); + borderGeode->setNodeMask(mBorderMask); + mRoot->addChild(borderGeode); mCellBorderNodes[std::make_pair(x,y)] = borderGeode; diff --git a/components/terrain/cellborder.hpp b/components/terrain/cellborder.hpp index a505aec9c..0cbe7b899 100644 --- a/components/terrain/cellborder.hpp +++ b/components/terrain/cellborder.hpp @@ -19,7 +19,7 @@ namespace MWRender public: typedef std::map, osg::ref_ptr > CellGrid; - CellBorder(Terrain::World *world, osg::Group *root); + CellBorder(Terrain::World *world, osg::Group *root, int borderMask); void createCellBorderGeometry(int x, int y); void destroyCellBorderGeometry(int x, int y); @@ -34,6 +34,7 @@ namespace MWRender osg::Group *mRoot; CellGrid mCellBorderNodes; + int mBorderMask; }; } diff --git a/components/terrain/world.cpp b/components/terrain/world.cpp index f0f628561..cc81dbef8 100644 --- a/components/terrain/world.cpp +++ b/components/terrain/world.cpp @@ -47,7 +47,7 @@ World::World(osg::Group* parent, osg::Group* compileRoot, Resource::ResourceSyst mTextureManager.reset(new TextureManager(mResourceSystem->getSceneManager())); mChunkManager.reset(new ChunkManager(mStorage, mResourceSystem->getSceneManager(), mTextureManager.get(), mCompositeMapRenderer)); - mCellBorder.reset(new MWRender::CellBorder(this,mTerrainRoot.get())); + mCellBorder.reset(new MWRender::CellBorder(this,mTerrainRoot.get(),borderMask)); mResourceSystem->addResourceManager(mChunkManager.get()); mResourceSystem->addResourceManager(mTextureManager.get()); From 24078d4a72343d388b39438f1afdfeb19a43e741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 14 Jun 2018 13:22:12 +0200 Subject: [PATCH 183/282] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f407a156e..4c7930f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 + Feature #4256: Implement ToggleBorders (TB) console command Feature #4345: Add equivalents for the command line commands to Launcher Feature #4444: Per-group KF-animation files support From 34f8eca7bd11271aad5fc5925128101f369eb39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20=C4=8C=C3=AD=C5=BE?= Date: Thu, 14 Jun 2018 13:43:32 +0200 Subject: [PATCH 184/282] Fix indent --- components/terrain/cellborder.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/terrain/cellborder.hpp b/components/terrain/cellborder.hpp index 0cbe7b899..530ea31ca 100644 --- a/components/terrain/cellborder.hpp +++ b/components/terrain/cellborder.hpp @@ -6,7 +6,7 @@ namespace Terrain { - class World; + class World; } namespace MWRender From 49ba00a3ec5a9b8fd9702080d2e4c53b6605d4a3 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sat, 9 Jun 2018 20:47:17 +0400 Subject: [PATCH 185/282] Add NPC validation to esmstore (bug #2772) --- CHANGELOG.md | 1 + apps/openmw/mwworld/esmstore.cpp | 58 +++++++++++++++++++++++++++++++- apps/openmw/mwworld/esmstore.hpp | 5 ++- apps/openmw/mwworld/worldimp.cpp | 2 +- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a59aca8e..205adc1dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Bug #1990: Sunrise/sunset not set correct Bug #2222: Fatigue's effect on selling price is backwards Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped + Bug #2772: Non-existing class or faction freezes the game Bug #2835: Player able to slowly move when overencumbered Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #3374: Touch spells not hitting kwama foragers diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 2fe9ebcad..ac5608b8e 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -120,7 +120,7 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) } } -void ESMStore::setUp() +void ESMStore::setUp(bool validateRecords) { mIds.clear(); @@ -142,6 +142,62 @@ void ESMStore::setUp() mAttributes.setUp(); mDialogs.setUp(); mStatics.setUp(); + + if (validateRecords) + validate(); +} + +void ESMStore::validate() +{ + // Cache first class from store - we will use it if current class is not found + std::string defaultCls = ""; + Store::iterator it = mClasses.begin(); + if (it != mClasses.end()) + defaultCls = it->mId; + else + throw std::runtime_error("List of NPC classes is empty!"); + + // Validate NPCs for non-existing class and faction. + // We will replace invalid entries by fixed ones + std::vector entitiesToReplace; + for (ESM::NPC npc : mNpcs) + { + bool changed = false; + + const std::string npcFaction = npc.mFaction; + if (!npcFaction.empty()) + { + const ESM::Faction *fact = mFactions.search(npcFaction); + if (!fact) + { + std::cerr << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent faction '" << npc.mFaction << "', ignoring it." << std::endl; + npc.mFaction = ""; + npc.mNpdt.mRank = -1; + changed = true; + } + } + + std::string npcClass = npc.mClass; + if (!npcClass.empty()) + { + const ESM::Class *cls = mClasses.search(npcClass); + if (!cls) + { + std::cerr << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent class '" << npc.mClass << "', using '" << defaultCls << "' class as replacement." << std::endl; + npc.mClass = defaultCls; + changed = true; + } + } + + if (changed) + entitiesToReplace.push_back(npc); + } + + for (const ESM::NPC &npc : entitiesToReplace) + { + mNpcs.eraseStatic(npc.mId); + mNpcs.insertStatic(npc); + } } int ESMStore::countSavedGameRecords() const diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index a14f6368e..f4d792118 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -74,6 +74,9 @@ namespace MWWorld unsigned int mDynamicCount; + /// Validate entries in store after setup + void validate(); + public: /// \todo replace with SharedIterator typedef std::map::const_iterator iterator; @@ -228,7 +231,7 @@ namespace MWWorld // This method must be called once, after loading all master/plugin files. This can only be done // from the outside, so it must be public. - void setUp(); + void setUp(bool validateRecords = false); int countSavedGameRecords() const; diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 000bdfa1a..3d7ecb0e3 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -184,7 +184,7 @@ namespace MWWorld fillGlobalVariables(); - mStore.setUp(); + mStore.setUp(true); mStore.movePlayerRecord(); mSwimHeightScale = mStore.get().find("fSwimHeightScale")->getFloat(); From 2854f6ca839d4d77b9bb335762911bd692c4974d Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 14 Jun 2018 21:32:40 +0400 Subject: [PATCH 186/282] Handle exception if we try to play non-music file (bug #4416) --- CHANGELOG.md | 1 + apps/openmw/mwsound/ffmpeg_decoder.cpp | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a59aca8e..22b589ed1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Bug #4327: Missing animations during spell/weapon stance switching Bug #4368: Settings window ok button doesn't have key focus by default Bug #4393: NPCs walk back to where they were after using ResetActors + Bug #4416: Handle exception if we try to play non-music file Bug #4419: MRK NiStringExtraData is handled incorrectly Bug #4426: RotateWorld behavior is incorrect Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 diff --git a/apps/openmw/mwsound/ffmpeg_decoder.cpp b/apps/openmw/mwsound/ffmpeg_decoder.cpp index e2d54876f..f458c0a97 100644 --- a/apps/openmw/mwsound/ffmpeg_decoder.cpp +++ b/apps/openmw/mwsound/ffmpeg_decoder.cpp @@ -251,21 +251,24 @@ void FFmpeg_Decoder::open(const std::string &fname) if(mOutputChannelLayout == 0) mOutputChannelLayout = av_get_default_channel_layout((*mStream)->codec->channels); } - catch(...) { + catch(...) + { if(mStream) avcodec_close((*mStream)->codec); mStream = NULL; - if (mFormatCtx->pb->buffer != NULL) + if (mFormatCtx != NULL) { - av_free(mFormatCtx->pb->buffer); - mFormatCtx->pb->buffer = NULL; - } - av_free(mFormatCtx->pb); - mFormatCtx->pb = NULL; + if (mFormatCtx->pb->buffer != NULL) + { + av_free(mFormatCtx->pb->buffer); + mFormatCtx->pb->buffer = NULL; + } + av_free(mFormatCtx->pb); + mFormatCtx->pb = NULL; - avformat_close_input(&mFormatCtx); - throw; + avformat_close_input(&mFormatCtx); + } } } From 1abf749f034d72bab77e6fb2d2aff559bc4c6246 Mon Sep 17 00:00:00 2001 From: Capostrophic Date: Thu, 14 Jun 2018 22:08:53 +0300 Subject: [PATCH 187/282] Remove screenshot taken message --- apps/openmw/mwinput/inputmanagerimp.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/openmw/mwinput/inputmanagerimp.cpp b/apps/openmw/mwinput/inputmanagerimp.cpp index 9fe9026ca..242058c5f 100644 --- a/apps/openmw/mwinput/inputmanagerimp.cpp +++ b/apps/openmw/mwinput/inputmanagerimp.cpp @@ -1036,7 +1036,6 @@ namespace MWInput void InputManager::screenshot() { bool regularScreenshot = true; - bool screenshotTaken = false; std::string settingStr; @@ -1047,7 +1046,6 @@ namespace MWInput { mScreenCaptureHandler->setFramesToCapture(1); mScreenCaptureHandler->captureNextFrame(*mViewer); - screenshotTaken = true; } else { @@ -1057,12 +1055,8 @@ namespace MWInput { (*mScreenCaptureOperation) (*(screenshot.get()),0); // FIXME: mScreenCaptureHandler->getCaptureOperation() causes crash for some reason - screenshotTaken = true; } } - - if (screenshotTaken) - MWBase::Environment::get().getWindowManager()->messageBox("Screenshot saved"); } void InputManager::toggleInventory() From 7615e78e521c1cd9f2f864ae57ddb786568758a8 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Thu, 14 Jun 2018 19:39:24 -0500 Subject: [PATCH 188/282] Move SDL initialization from main.cpp to graphicspage.cpp --- apps/launcher/graphicspage.cpp | 24 ++++++++++++++++++++++++ apps/launcher/graphicspage.hpp | 5 +++++ apps/launcher/main.cpp | 18 ++---------------- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index d3dbfa559..998888369 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -11,6 +11,7 @@ #define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ #endif // MAC_OS_X_VERSION_MIN_REQUIRED +#include #include #include @@ -46,8 +47,28 @@ Launcher::GraphicsPage::GraphicsPage(Files::ConfigurationManager &cfg, Settings: } +bool Launcher::GraphicsPage::connectToSdl() { + SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); + SDL_SetMainReady(); + // Required for determining screen resolution and such on the Graphics tab + if (SDL_Init(SDL_INIT_VIDEO) != 0) + { + return false; + } + signal(SIGINT, SIG_DFL); // We don't want to use the SDL event loop in the launcher, + // so reset SIGINT which SDL wants to redirect to an SDL_Quit event. + + return true; +} + bool Launcher::GraphicsPage::setupSDL() { + bool sdlConnectSuccessful = connectToSdl(); + if (!sdlConnectSuccessful) + { + return false; + } + int displays = SDL_GetNumVideoDisplays(); if (displays < 0) @@ -67,6 +88,9 @@ bool Launcher::GraphicsPage::setupSDL() screenComboBox->addItem(QString(tr("Screen ")) + QString::number(i + 1)); } + // Disconnect from SDL processes + SDL_Quit(); + return true; } diff --git a/apps/launcher/graphicspage.hpp b/apps/launcher/graphicspage.hpp index e6eb53a3b..0354e5202 100644 --- a/apps/launcher/graphicspage.hpp +++ b/apps/launcher/graphicspage.hpp @@ -37,6 +37,11 @@ namespace Launcher QStringList getAvailableResolutions(int screen); QRect getMaximumResolution(); + /** + * Connect to the SDL so that we can use it to determine graphics + * @return whether or not connecting to SDL is successful + */ + bool connectToSdl(); bool setupSDL(); }; } diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index 96cadc8a7..dd2353606 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -12,24 +12,12 @@ #define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ #endif // MAC_OS_X_VERSION_MIN_REQUIRED -#include - #include "maindialog.hpp" int main(int argc, char *argv[]) { try { - SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); - SDL_SetMainReady(); - if (SDL_Init(SDL_INIT_VIDEO) != 0) - { - qDebug() << "SDL_Init failed: " << QString::fromUtf8(SDL_GetError()); - return 0; - } - signal(SIGINT, SIG_DFL); // We don't want to use the SDL event loop in the launcher, - // so reset SIGINT which SDL wants to redirect to an SDL_Quit event. - QApplication app(argc, argv); // Now we make sure the current dir is set to application path @@ -46,13 +34,11 @@ int main(int argc, char *argv[]) if (result == Launcher::FirstRunDialogResultContinue) mainWin.show(); - int returnValue = app.exec(); - SDL_Quit(); - return returnValue; + return app.exec(); } catch (std::exception& e) { std::cerr << "ERROR: " << e.what() << std::endl; return 0; } -} +} \ No newline at end of file From e51bfb46c6b0ccffbe4ddae56408aa3159f8097a Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Thu, 14 Jun 2018 19:42:40 -0500 Subject: [PATCH 189/282] Adding Changelog records --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a59aca8e..c373ab325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,14 @@ Bug #2222: Fatigue's effect on selling price is backwards Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped Bug #2835: Player able to slowly move when overencumbered + Bug #2862: [macOS] Can't quit launcher using Command-Q or OpenMW->Quit Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #3374: Touch spells not hitting kwama foragers Bug #3591: Angled hit distance too low Bug #3629: DB assassin attack never triggers creature spawning Bug #3876: Landscape texture painting is misaligned Bug #3897: Have Goodbye give all choices the effects of Goodbye + Bug #3911: [macOS] Typing in the "Content List name" dialog box produces double characters Bug #3993: Terrain texture blending map is not upscaled Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID From 506d615acc1c8683665d6e3ba39a77d92203eef3 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Thu, 14 Jun 2018 22:16:35 -0500 Subject: [PATCH 190/282] Moving csignal import from main to graphicspage --- apps/launcher/graphicspage.cpp | 1 + apps/launcher/main.cpp | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/launcher/graphicspage.cpp b/apps/launcher/graphicspage.cpp index 998888369..34442fe71 100644 --- a/apps/launcher/graphicspage.cpp +++ b/apps/launcher/graphicspage.cpp @@ -1,6 +1,7 @@ #include "graphicspage.hpp" #include +#include #include #include #include diff --git a/apps/launcher/main.cpp b/apps/launcher/main.cpp index dd2353606..866ae2aa9 100644 --- a/apps/launcher/main.cpp +++ b/apps/launcher/main.cpp @@ -1,5 +1,4 @@ #include -#include #include #include From 9c3da411307b9f7f18db454d9499ec4a58fa48e7 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Fri, 15 Jun 2018 14:03:43 +0400 Subject: [PATCH 191/282] Add murder bounty when a player follower commits murder (bug #2852) --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/actors.cpp | 25 +++++++++++-------- .../mwmechanics/mechanicsmanagerimp.cpp | 18 ++++++++++--- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a59aca8e..d32f48103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Bug #2222: Fatigue's effect on selling price is backwards Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped Bug #2835: Player able to slowly move when overencumbered + Bug #2852: No murder bounty when a player follower commits murder Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #3374: Touch spells not hitting kwama foragers Bug #3591: Angled hit distance too low diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index f1bc6907c..9f73b79cc 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -760,11 +760,14 @@ namespace MWMechanics { // The actor was killed by a magic effect. Figure out if the player was responsible for it. const ActiveSpells& spells = creatureStats.getActiveSpells(); - bool killedByPlayer = false; MWWorld::Ptr player = getPlayer(); + std::set playerFollowers; + getActorsSidingWith(player, playerFollowers); + for (ActiveSpells::TIterator it = spells.begin(); it != spells.end(); ++it) { const ActiveSpells::ActiveSpellParams& spell = it->second; + MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); for (std::vector::const_iterator effectIt = spell.mEffects.begin(); effectIt != spell.mEffects.end(); ++effectIt) { @@ -782,17 +785,19 @@ namespace MWMechanics isDamageEffect = true; } - MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.mCasterActorId); - if (isDamageEffect && caster == player) - killedByPlayer = true; + if (isDamageEffect) + { + if (caster == player || playerFollowers.find(caster) != playerFollowers.end()) + { + if (caster.getClass().getNpcStats(caster).isWerewolf()) + caster.getClass().getNpcStats(caster).addWerewolfKill(); + + MWBase::Environment::get().getMechanicsManager()->actorKilled(ptr, player); + break; + } + } } } - if (killedByPlayer) - { - MWBase::Environment::get().getMechanicsManager()->actorKilled(ptr, player); - if (player.getClass().getNpcStats(player).isWerewolf()) - player.getClass().getNpcStats(player).addWerewolfKill(); - } } // TODO: dirty flag for magic effects to avoid some unnecessary work below? diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 80b9ace86..1fc98a327 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -1495,7 +1495,7 @@ namespace MWMechanics void MechanicsManager::actorKilled(const MWWorld::Ptr &victim, const MWWorld::Ptr &attacker) { - if (attacker.isEmpty() || attacker != getPlayer()) + if (attacker.isEmpty() || victim.isEmpty()) return; if (victim == attacker) @@ -1505,13 +1505,23 @@ namespace MWMechanics return; // TODO: implement animal rights const MWMechanics::NpcStats& victimStats = victim.getClass().getNpcStats(victim); + if (victimStats.getCrimeId() == -1) + return; + + // For now we report only about crimes of player and player's followers + const MWWorld::Ptr &player = getPlayer(); + if (attacker != player) + { + std::set playerFollowers; + getActorsSidingWith(player, playerFollowers); + if (playerFollowers.find(attacker) == playerFollowers.end()) + return; + } // Simple check for who attacked first: if the player attacked first, a crimeId should be set // Doesn't handle possible edge case where no one reported the assault, but in such a case, // for bystanders it is not possible to tell who attacked first, anyway. - if (victimStats.getCrimeId() != -1) - commitCrime(attacker, victim, MWBase::MechanicsManager::OT_Murder); - + commitCrime(player, victim, MWBase::MechanicsManager::OT_Murder); } bool MechanicsManager::awarenessCheck(const MWWorld::Ptr &ptr, const MWWorld::Ptr &observer) From 359f87ab9f4ad628b231705bcfd7b5f76c30071f Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Sat, 16 Jun 2018 12:12:32 +0200 Subject: [PATCH 192/282] Change imageToSurface to return a unique_ptr to avoid manual surface cleanup --- apps/openmw/engine.cpp | 5 ++--- components/sdlutil/imagetosurface.cpp | 4 ++-- components/sdlutil/imagetosurface.hpp | 6 ++++-- components/sdlutil/sdlcursormanager.cpp | 17 +++++------------ 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 78e368cfc..65ea181fb 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -428,9 +428,8 @@ void OMW::Engine::setWindowIcon() else { osg::ref_ptr image = result.getImage(); - SDL_Surface* surface = SDLUtil::imageToSurface(image, true); - SDL_SetWindowIcon(mWindow, surface); - SDL_FreeSurface(surface); + auto surface = SDLUtil::imageToSurface(image, true); + SDL_SetWindowIcon(mWindow, surface.get()); } } diff --git a/components/sdlutil/imagetosurface.cpp b/components/sdlutil/imagetosurface.cpp index 6313c0a8f..6e68c0f45 100644 --- a/components/sdlutil/imagetosurface.cpp +++ b/components/sdlutil/imagetosurface.cpp @@ -6,7 +6,7 @@ namespace SDLUtil { -SDL_Surface* imageToSurface(osg::Image *image, bool flip) +SurfaceUniquePtr imageToSurface(osg::Image *image, bool flip) { int width = image->s(); int height = image->t(); @@ -22,7 +22,7 @@ SDL_Surface* imageToSurface(osg::Image *image, bool flip) static_cast(clr.g() * 255), static_cast(clr.b() * 255), static_cast(clr.a() * 255)); } - return surface; + return SurfaceUniquePtr(surface, SDL_FreeSurface); } } diff --git a/components/sdlutil/imagetosurface.hpp b/components/sdlutil/imagetosurface.hpp index ad0457433..9ce56b909 100644 --- a/components/sdlutil/imagetosurface.hpp +++ b/components/sdlutil/imagetosurface.hpp @@ -1,6 +1,8 @@ #ifndef OPENMW_COMPONENTS_SDLUTIL_IMAGETOSURFACE_H #define OPENMW_COMPONENTS_SDLUTIL_IMAGETOSURFACE_H +#include + struct SDL_Surface; namespace osg @@ -10,10 +12,10 @@ namespace osg namespace SDLUtil { + typedef std::unique_ptr SurfaceUniquePtr; /// Convert an osg::Image to an SDL_Surface. - /// @note The returned surface must be freed using SDL_FreeSurface. - SDL_Surface* imageToSurface(osg::Image* image, bool flip=false); + SurfaceUniquePtr imageToSurface(osg::Image* image, bool flip=false); } diff --git a/components/sdlutil/sdlcursormanager.cpp b/components/sdlutil/sdlcursormanager.cpp index 368d8e502..bf265dc7e 100644 --- a/components/sdlutil/sdlcursormanager.cpp +++ b/components/sdlutil/sdlcursormanager.cpp @@ -221,9 +221,7 @@ namespace SDLUtil } #if OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION - typedef std::unique_ptr SDLSurfacePtr; - - SDLSurfacePtr decompress(osg::Image* source, int rotDegrees) + SurfaceUniquePtr decompress(osg::Image* source, int rotDegrees) { int width = source->s(); int height = source->t(); @@ -265,7 +263,7 @@ namespace SDLUtil SDL_FreeSurface(cursorSurface); SDL_DestroyRenderer(renderer); - return SDLSurfacePtr(targetSurface, SDL_FreeSurface); + return SurfaceUniquePtr(targetSurface, SDL_FreeSurface); } #endif @@ -285,18 +283,13 @@ namespace SDLUtil return; } - SDL_Surface* surf = SDLUtil::imageToSurface(decompressed, true); - - //set the cursor and store it for later - SDL_Cursor* curs = SDL_CreateColorCursor(surf, hotspot_x, hotspot_y); - - //clean up - SDL_FreeSurface(surf); + auto surf = SDLUtil::imageToSurface(decompressed, true); #else auto surf = decompress(image, rotDegrees); +#endif //set the cursor and store it for later SDL_Cursor* curs = SDL_CreateColorCursor(surf.get(), hotspot_x, hotspot_y); -#endif + mCursorMap.insert(CursorMap::value_type(std::string(name), curs)); } From 2a65aaf5abf30d0e3db94ef51d96d478cfc41603 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sat, 16 Jun 2018 14:21:28 +0400 Subject: [PATCH 193/282] Forbid actors to use teleporting doors (bug #2562) --- CHANGELOG.md | 1 + apps/openmw/mwclass/door.cpp | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e1feb27..f5b48d6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Bug #1990: Sunrise/sunset not set correct Bug #2222: Fatigue's effect on selling price is backwards Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped + Bug #2562: Forcing AI to activate a teleport door sometimes causes a crash Bug #2835: Player able to slowly move when overencumbered Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #3374: Touch spells not hitting kwama foragers diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index eba87a47b..6846d3280 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -121,14 +121,23 @@ namespace MWClass bool hasKey = false; std::string keyName; + // FIXME: If NPC activate teleporting door, it can lead to crash due to iterator invalidation in the Actors update. + // Make such activation a no-op for now, how in vanilla game. + if (actor != MWMechanics::getPlayer() && ptr.getCellRef().getTeleport()) + { + std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); + action->setSound(lockedSound); + return action; + } + // make door glow if player activates it with telekinesis - if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr() && - MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > + if (actor == MWMechanics::getPlayer() && + MWBase::Environment::get().getWorld()->getDistanceToFacedObject() > MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); - const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); int index = ESM::MagicEffect::effectStringToId("sEffectTelekinesis"); const ESM::MagicEffect *effect = store.get().find(index); From 75d79e98b9d91b3f251c5dbfbf4c5a3589292cc2 Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Sat, 16 Jun 2018 12:38:16 +0200 Subject: [PATCH 194/282] Force software decompression if OPENMW_DECOMPRESS_TEXTURES is set --- components/sdlutil/sdlcursormanager.cpp | 140 ++++++++++++------------ 1 file changed, 68 insertions(+), 72 deletions(-) diff --git a/components/sdlutil/sdlcursormanager.cpp b/components/sdlutil/sdlcursormanager.cpp index bf265dc7e..1747c9b94 100644 --- a/components/sdlutil/sdlcursormanager.cpp +++ b/components/sdlutil/sdlcursormanager.cpp @@ -25,15 +25,14 @@ USE_GRAPHICSWINDOW() #endif -#if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 8) || defined(__APPLE__) -#define OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION 1 -#else -#define OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION 0 -#endif - -#if !OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION -namespace +namespace CursorDecompression { + // macOS builds use the OSG fork that includes DXTC commit + #if OSG_VERSION_GREATER_OR_EQUAL(3, 5, 8) || defined(__APPLE__) + static const bool DXTCSupported = true; + #else + static const bool DXTCSupported = false; + #endif class MyGraphicsContext { public: @@ -90,10 +89,8 @@ namespace osg::ref_ptr _gc; }; - osg::ref_ptr decompress (osg::ref_ptr source, float rotDegrees) + SDLUtil::SurfaceUniquePtr hardwareDecompress (osg::ref_ptr source, float rotDegrees) { - // TODO: use software decompression once S3TC patent expires - int width = source->s(); int height = source->t(); @@ -152,11 +149,55 @@ namespace source->releaseGLObjects(); texture->releaseGLObjects(); - return resultImage; + return SDLUtil::imageToSurface(resultImage, true); + } + + SDLUtil::SurfaceUniquePtr softwareDecompress (osg::ref_ptr source, float rotDegrees) + { + int width = source->s(); + int height = source->t(); + bool useAlpha = source->isImageTranslucent(); + + osg::ref_ptr decompressedImage = new osg::Image; + decompressedImage->setFileName(source->getFileName()); + decompressedImage->allocateImage(width, height, 1, useAlpha ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE); + for (int s=0; ssetColor(source->getColor(s,t,0), s,t,0); + + Uint32 redMask = 0x000000ff; + Uint32 greenMask = 0x0000ff00; + Uint32 blueMask = 0x00ff0000; + Uint32 alphaMask = useAlpha ? 0xff000000 : 0; + + SDL_Surface *cursorSurface = SDL_CreateRGBSurfaceFrom(decompressedImage->data(), + width, + height, + decompressedImage->getPixelSizeInBits(), + decompressedImage->getRowSizeInBytes(), + redMask, + greenMask, + blueMask, + alphaMask); + + SDL_Surface *targetSurface = SDL_CreateRGBSurface(0, width, height, 32, redMask, greenMask, blueMask, alphaMask); + SDL_Renderer *renderer = SDL_CreateSoftwareRenderer(targetSurface); + + SDL_RenderClear(renderer); + + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); + SDL_Texture *cursorTexture = SDL_CreateTextureFromSurface(renderer, cursorSurface); + + SDL_RenderCopyEx(renderer, cursorTexture, NULL, NULL, -rotDegrees, NULL, SDL_FLIP_VERTICAL); + + SDL_DestroyTexture(cursorTexture); + SDL_FreeSurface(cursorSurface); + SDL_DestroyRenderer(renderer); + + return SDLUtil::SurfaceUniquePtr(targetSurface, SDL_FreeSurface); } } -#endif namespace SDLUtil { @@ -220,77 +261,32 @@ namespace SDLUtil #endif } -#if OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION - SurfaceUniquePtr decompress(osg::Image* source, int rotDegrees) - { - int width = source->s(); - int height = source->t(); - bool useAlpha = source->isImageTranslucent(); - - osg::ref_ptr decompressedImage = new osg::Image; - decompressedImage->setFileName(source->getFileName()); - decompressedImage->allocateImage(width, height, 1, useAlpha ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE); - for (int s=0; ssetColor(source->getColor(s,t,0), s,t,0); - - Uint32 redMask = 0x000000ff; - Uint32 greenMask = 0x0000ff00; - Uint32 blueMask = 0x00ff0000; - Uint32 alphaMask = useAlpha ? 0xff000000 : 0; - - SDL_Surface *cursorSurface = SDL_CreateRGBSurfaceFrom(decompressedImage->data(), - width, - height, - decompressedImage->getPixelSizeInBits(), - decompressedImage->getRowSizeInBytes(), - redMask, - greenMask, - blueMask, - alphaMask); - - SDL_Surface *targetSurface = SDL_CreateRGBSurface(0, width, height, 32, redMask, greenMask, blueMask, alphaMask); - SDL_Renderer *renderer = SDL_CreateSoftwareRenderer(targetSurface); - - SDL_RenderClear(renderer); - - SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); - SDL_Texture *cursorTexture = SDL_CreateTextureFromSurface(renderer, cursorSurface); - - SDL_RenderCopyEx(renderer, cursorTexture, NULL, NULL, -rotDegrees, NULL, SDL_FLIP_VERTICAL); - - SDL_DestroyTexture(cursorTexture); - SDL_FreeSurface(cursorSurface); - SDL_DestroyRenderer(renderer); - - return SurfaceUniquePtr(targetSurface, SDL_FreeSurface); - } -#endif - void SDLCursorManager::_createCursorFromResource(const std::string& name, int rotDegrees, osg::Image* image, Uint8 hotspot_x, Uint8 hotspot_y) { if (mCursorMap.find(name) != mCursorMap.end()) return; -#if !OPENMW_USE_SOFTWARE_CURSOR_DECOMPRESSION - osg::ref_ptr decompressed; + static bool forceSoftwareDecompression = (getenv("OPENMW_DECOMPRESS_TEXTURES") != 0); + + SurfaceUniquePtr (*decompressionFunction)(osg::ref_ptr, float); + if (forceSoftwareDecompression || CursorDecompression::DXTCSupported) { + decompressionFunction = CursorDecompression::softwareDecompress; + } else { + decompressionFunction = CursorDecompression::hardwareDecompress; + } try { - decompressed = decompress(image, static_cast(rotDegrees)); + auto surface = decompressionFunction(image, static_cast(rotDegrees)); + + //set the cursor and store it for later + SDL_Cursor* curs = SDL_CreateColorCursor(surface.get(), hotspot_x, hotspot_y); + + mCursorMap.insert(CursorMap::value_type(std::string(name), curs)); } catch (std::exception& e) { std::cerr << e.what() << std::endl; std::cerr <<"Using default cursor."< Date: Sat, 16 Jun 2018 17:34:49 +0400 Subject: [PATCH 195/282] Ignore lights without CanCarry flags when NPC selects torch (bug #4457) --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/actors.cpp | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e1feb27..f598aaa3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Bug #4451: Script fails to compile when using "Begin, [ScriptName]" syntax Bug #4453: Quick keys behaviour is invalid for equipment Bug #4454: AI opens doors too slow + Bug #4457: Item without CanCarry flag prevents shield autoequipping in dark areas Feature #4256: Implement ToggleBorders (TB) console command Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index f1bc6907c..664c41a87 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -984,7 +984,8 @@ namespace MWMechanics MWWorld::ContainerStoreIterator torch = inventoryStore.end(); for (MWWorld::ContainerStoreIterator it = inventoryStore.begin(); it != inventoryStore.end(); ++it) { - if (it->getTypeName() == typeid(ESM::Light).name()) + if (it->getTypeName() == typeid(ESM::Light).name() && + it->getClass().canBeEquipped(*it, ptr).first) { torch = it; break; @@ -1005,8 +1006,7 @@ namespace MWMechanics heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); // If we have a torch and can equip it, then equip it now. - if (heldIter == inventoryStore.end() - && torch->getClass().canBeEquipped(*torch, ptr).first == 1) + if (heldIter == inventoryStore.end()) { inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, torch, ptr); } From f3f74876643c099db8b41a27fb3e7cd8bedaed96 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Sat, 16 Jun 2018 22:11:10 +0400 Subject: [PATCH 196/282] Fix arguments parsing for AiWander console command (bug #4458) --- CHANGELOG.md | 1 + apps/openmw/mwscript/aiextensions.cpp | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e1feb27..273669163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Bug #4451: Script fails to compile when using "Begin, [ScriptName]" syntax Bug #4453: Quick keys behaviour is invalid for equipment Bug #4454: AI opens doors too slow + Bug #4458: AiWander console command handles idle chances incorrectly Feature #4256: Implement ToggleBorders (TB) console command Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp index c98e0fc5a..c51a55b50 100644 --- a/apps/openmw/mwscript/aiextensions.cpp +++ b/apps/openmw/mwscript/aiextensions.cpp @@ -193,10 +193,18 @@ namespace MWScript Interpreter::Type_Integer time = static_cast(runtime[0].mFloat); runtime.pop(); + // Chance for Idle is unused + if (arg0) + { + --arg0; + runtime.pop(); + } + std::vector idleList; bool repeat = false; - for(int i=1; i < 10 && arg0; ++i) + // Chances for Idle2-Idle9 + for(int i=2; i<=9 && arg0; ++i) { if(!repeat) repeat = true; From c5f5984c098083b62ce3e9d6ba9e6e63b84829f1 Mon Sep 17 00:00:00 2001 From: Ryan Tucker Date: Sat, 16 Jun 2018 14:44:49 -0700 Subject: [PATCH 197/282] Started migrating installation guide from the wiki. Fixed a bunch of errors that popped up on my local build. --- docs/source/manuals/index.rst | 1 + docs/source/manuals/installation/index.rst | 11 ++++ .../installation/install-game-files.rst | 5 ++ .../manuals/installation/install-openmw.rst | 63 +++++++++++++++++++ .../manuals/openmw-cs}/record-filters.rst | 2 +- .../manuals/openmw-cs}/record-types.rst | 0 .../manuals/openmw-cs}/tables.rst | 0 docs/source/manuals/openmw-cs/tour.rst | 2 +- .../reference/modding/settings/cells.rst | 2 +- .../reference/modding/settings/saves.rst | 2 +- 10 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 docs/source/manuals/installation/index.rst create mode 100644 docs/source/manuals/installation/install-game-files.rst create mode 100644 docs/source/manuals/installation/install-openmw.rst rename docs/{cs-manual/source => source/manuals/openmw-cs}/record-filters.rst (99%) rename docs/{cs-manual/source => source/manuals/openmw-cs}/record-types.rst (100%) rename docs/{cs-manual/source => source/manuals/openmw-cs}/tables.rst (100%) diff --git a/docs/source/manuals/index.rst b/docs/source/manuals/index.rst index 2bf1fff5e..e6f0cbef2 100644 --- a/docs/source/manuals/index.rst +++ b/docs/source/manuals/index.rst @@ -5,3 +5,4 @@ User Manuals :maxdepth: 2 openmw-cs/index + installation/index diff --git a/docs/source/manuals/installation/index.rst b/docs/source/manuals/installation/index.rst new file mode 100644 index 000000000..f22e77449 --- /dev/null +++ b/docs/source/manuals/installation/index.rst @@ -0,0 +1,11 @@ +Installation Guide +################## + +In order to use OpenMW, you must install both the engine and the game files for a compatible game. + +.. toctree:: + :maxdepth: 2 + + install-openmw + install-game-files + diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst new file mode 100644 index 000000000..11c746f44 --- /dev/null +++ b/docs/source/manuals/installation/install-game-files.rst @@ -0,0 +1,5 @@ +================== +Install Game Files +================== + +Coming Soon... \ No newline at end of file diff --git a/docs/source/manuals/installation/install-openmw.rst b/docs/source/manuals/installation/install-openmw.rst new file mode 100644 index 000000000..fab00ab9b --- /dev/null +++ b/docs/source/manuals/installation/install-openmw.rst @@ -0,0 +1,63 @@ +============== +Install OpenMW +============== + +The (easier) Binary Way +======================= + +If you're not sure what any of the different methods mean, you should probably stick to this one. +Simply download the latest version for your operating system from +`github.com/OpenMW/openmw/releases `_ +and run the install package once downloaded. It's now installed! + + .. note:: + There is no need to uninstall previous versions + as OpenMW automatically installs into a separate directory for each new version. + Your saves and configuration are compatible and accessible between versions. + +The (bleeding edge) Source Way +============================== + +Visit the `Development Environment Setup `_ +section of the Wiki for detailed instructions on how to build the engine. + +The Ubuntu Way +============== + +A `Launchpad PPA `_ is available. +Add it and install OpenMW:: + + $ sudo add-apt-repository ppa:openmw/openmw + $ sudo apt-get update + $ sudo apt-get install openmw openmw-launcher + +.. note:: + OpenMW-CS must be installed separately by typing:: + + $ sudo apt-get install openmw-cs + +The Arch Linux Way +================== + +The binary package is available in the official [community] Repositories. +To install, simply run the following as root (or in sudo):: + + # pacman -S openmw + +The Void Linux Way +================== + +The binary package is available in the official Repository +To install simply run the following as root (or in sudo):: + + # xbps-install openmw + +The Debian Way +============== + +OpenMW is available from the unstable (sid) repository of Debian contrib +and can be easily installed if you are using testing or unstable. +However, it depends on several packages which are not in stable, +so it is not possible to install OpenMW in Wheezy without creating a FrankenDebian. +This is not recommended or supported. + diff --git a/docs/cs-manual/source/record-filters.rst b/docs/source/manuals/openmw-cs/record-filters.rst similarity index 99% rename from docs/cs-manual/source/record-filters.rst rename to docs/source/manuals/openmw-cs/record-filters.rst index 3379f557f..a579d8dd8 100644 --- a/docs/cs-manual/source/record-filters.rst +++ b/docs/source/manuals/openmw-cs/record-filters.rst @@ -144,7 +144,7 @@ Writing expressions The syntax for expressions is as follows: -.. code-block:: +.. code:: () diff --git a/docs/cs-manual/source/record-types.rst b/docs/source/manuals/openmw-cs/record-types.rst similarity index 100% rename from docs/cs-manual/source/record-types.rst rename to docs/source/manuals/openmw-cs/record-types.rst diff --git a/docs/cs-manual/source/tables.rst b/docs/source/manuals/openmw-cs/tables.rst similarity index 100% rename from docs/cs-manual/source/tables.rst rename to docs/source/manuals/openmw-cs/tables.rst diff --git a/docs/source/manuals/openmw-cs/tour.rst b/docs/source/manuals/openmw-cs/tour.rst index 83b7aae27..e45b6164f 100644 --- a/docs/source/manuals/openmw-cs/tour.rst +++ b/docs/source/manuals/openmw-cs/tour.rst @@ -294,7 +294,7 @@ Increase the Count to 1. Save the addon, then test to ensure it works - e.g. start a new game and lockpick the chest. Placing in plain sight -===================== +====================== Let's hide the Ring of Night vision in the cabin of the [Ancient Shipwreck] (https://en.uesp.net/wiki/Morrowind:Ancient_Shipwreck), a derelict vessel diff --git a/docs/source/reference/modding/settings/cells.rst b/docs/source/reference/modding/settings/cells.rst index 171ddb7c8..43a984ca7 100644 --- a/docs/source/reference/modding/settings/cells.rst +++ b/docs/source/reference/modding/settings/cells.rst @@ -188,7 +188,7 @@ target framerate Affects the time to be set aside each frame for graphics preloading operations. The game will distribute the preloading over several frames so as to not go under the specified framerate. For best results, set this value to the monitor's refresh rate. If you still experience stutters on turning around, you can try a lower value, although the framerate during loading will suffer a bit in that case. pointers cache size ------------------- +------------------- :Type: integer :Range: >0 diff --git a/docs/source/reference/modding/settings/saves.rst b/docs/source/reference/modding/settings/saves.rst index fdb25fb79..5add36c0c 100644 --- a/docs/source/reference/modding/settings/saves.rst +++ b/docs/source/reference/modding/settings/saves.rst @@ -34,7 +34,7 @@ for each saved game in the Load menu. Currently, the counter includes time spent This setting can only be configured by editing the settings configuration file. max quicksaves ----------- +-------------- :Type: integer :Range: >0 From 7e23e6586babdd9a7f85f1131cadfa2320cdfeeb Mon Sep 17 00:00:00 2001 From: Ryan Tucker Date: Sat, 16 Jun 2018 17:10:35 -0700 Subject: [PATCH 198/282] Put together most of the informal guide on contributing to documentation. --- docs/source/reference/documentationHowTo.rst | 109 +++++++++++++++++++ docs/source/reference/index.rst | 3 +- 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 docs/source/reference/documentationHowTo.rst diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/reference/documentationHowTo.rst new file mode 100644 index 000000000..0a39aa978 --- /dev/null +++ b/docs/source/reference/documentationHowTo.rst @@ -0,0 +1,109 @@ +####################################### +So you want to help with documentation? +####################################### + +Or a beginner's guide to writing docs without having to deal with more techie stuff than you have to. +##################################################################################################### + +Intro +===== + +The premise of this guide is that you would like to help out the OpenMW project beyond play-testing for bugs and such, buuuuut you're like me and don't really know how to code. This has the rather pesky side effect of you not really knowing about all the tools like GitHub and such. While many of these tools are super handy and great to know how to use, not everyone has the actual need and desire to learn the ins and outs of them. Since we would like as much help fleshing out the user documentation as possible, I wrote this guide to lower the barrier of entry into contributing to the project. + +HOWEVER, as much as I will try to guide you through all the tedious setup and day-to-day stuff, you will eventually have to learn to write using ReST (reStructuredText) formatting. Since you're probably like me when I started helping and don't know wtf ReST is, it's an incredibly simple language that is easy to read in plain text form that can then be converted automatically into different types of documents like PDFs and html for webpages. + +So here's what you're gonna be learning how to set up: + +1. `GitHub`_ +2. `PyCharm`_ +3. `Sphinx`_ +4. `Sample PR`_ + +GitHub +====== + +GitHub is the website the OpenMW project is hosted on. It utilizes Git, which is a version control system, meaning it helps us all collaborate on the project without interfering with each others' work. The commands are a little annoying because there is a certain amount of undescriptive jargon, but for the most part, what you need to know is very simple and I'll walk you through it. There are three main parts that you should know: + +1. The OpenMW repository +2. Your online repository +3. Your local repository + +The master OpenMW respository is where all of our work comes together and where the most current version of the source code resides. A repository, also called repo, is a directory or the main folder that holds a project. You will need to create your own account on GitHub so you can *fork* the OpenMW repository. Forking is just when you clone a project into a repository on your own account so you can make changes however you like without accidentally messing up the original project. Now, you could add and edit files on GitHub.com directly through your online repository, however it's much easier to work on them on your own computer in your local repository. Local just refers to the fact that it's physically stored on your computer's hard drive. Here are the easy steps for doing all this: + +1. Go to GitHub.com and sign up for a free account. +2. Navigate to the master OpenMW repo at: https://github.com/OpenMW/openmw +3. In the upper right corner, click on the button that says "Fork". This should take you to the newly created fork in your own account ``/openmw``. + +Now you have an online repository that is the exact copy of the OpenMW master. To set up your local repository, we're going to use PyCharm. + +If you want more info I recommend reading this guide: http://readwrite.com/2013/09/30/understanding-github-a-journey-for-beginners-part-1/ + +PyCharm +======= + +PyCharm is what's known as an IDE, which stands for integrated development environment. All this means is that it's for writing code and has a bunch of built-in features that make it easier to do so. In this case, PyCharm is made for the language Python, which is what Sphinx is written in. We won't actually be touching any of the Python, but some of the built-in features are extremely useful. Let's start setting it up: + +1. Go to https://www.jetbrains.com/pycharm/download/ +2. Select your OS, then download the free Community version. +3. Locate and install. +4. Run the program and let it load. +5. Now we're going to connect it to our GitHub account and let it create the local repository by itself. In the welcome menu, go to the bottom right where it says configure and select Settings/Preferences. +6. Click Version Control and select GitHub. +7. Click Create API Token and enter your GitHub username and password in the dialogue box, then click Login. +8. This should allow PyCharm to automatically connect to GitHub, but go ahead and click Test just to be sure. +9. Click Apply and OK to save the settings. +10. Back in the welcome window, click "Check out from version control" and select GitHub. + + .. note:: + After this step, it should log in to your GitHub. If not, you probably messed up the Token creation. If you're on Mac, you may come across and error complaining about XCode and admin priviledges. If this happens, open Terminal and type: ``sudo xcodebuild -license`` Read through the license and agree. This should fix the error and allow you to log in. + +11. In Git Repository URL, select your OpenMW repository and click Clone + +Congrats! You now have the OpenMW sourcecode on your computer and you can begin making changes and contributing. If you're reading this guide though, you probably won't have any idea how to do that, so let's go through setting up Sphinx, then I'll go through it. + +Sphinx +====== + +So far I've mentioned ReST (reStructuredText) a couple times, but what is it, and what is Sphinx? The most basic explanation is that ReST is the markup language (like HTML is the markup language for webpages) and Sphinx is the program that goes through and builds the actual document so you can read it in a more visually pleasing way. For a much more detailed explanation, I recommend: https://coderwall.com/p/vemncg/what-is-the-difference-rest-docutils-sphinx-readthedocs + +This will be the most technical section as we have to use the command prompt or terminal to install Python and Sphinx. I had intended to give you a universal explanation on how to install both, but it would drastically increase the length of this guide. The tutorial on the Sphinx website is really just going to be better than anything I write here, so please refer to their guide here: http://www.sphinx-doc.org/en/stable/install.html + +Hopefully you now have Python and Sphinx installed. ... + +Now you should have everything installed and running so you can collaborate on documentation properly. Let's go through a few more brief GitHub basics. There are really only 4 things you will be using regularly: + +1. Rebase +2. Commit +3. Push +4. Pull request (PR) + +Rebasing means you're taking all changes in one branch and applying them directly on top of another branch. This is slightly different than a merge which compares the two branches and makes another state combining the two. The difference is slight, but we use the rebase because it keeps the history cleaner. You will always rebase your local repository from the OpenMW master repository. This ensures you have all the most up to date changes before working on stuff so there is less chance of conflicts that need to be resolved when your branch is merged back into the master. A commit is basically just stating which files you want to mark as ready to be "pushed" to your online repository. A push is just copying those "committed" changes to your online repo. (Commit and push can be combined in one step in PyCharm, so yay) Once you've pushed all the changes you need to contribute something to the project, you will then submit a pull request, so called because you are *requesting* that the project maintainers "pull" and merge the changes you've made into the project master repository. One of the project maintainers will probably ask you to make some corrections or clarifications. Go back and repeat this process to make those changes, and repeat until they're good enough to get merged. + +So to go over all that again. You rebase *every* time you start working on something to ensure you're working on the most updated version (I do literally every time I open PyCharm). Then make your edits. You commit and push from your local repo to your online repo. Then you submit a pull request and people can review your changes before they get merged into the project master! Or in list form: + +1. Rebase local repo from OpenMW master +2. Make your edits +3. Commit and push your local edits to your online repo +4. Go online and submit a pull request +5. Repeat steps 1-4 until someone approves and merges your PR + +Preview Documentation +********************* + +You will probably find it helpful to be able to preview any documentation you've made. I often forget necessary syntax and this allows me to double check my work before submitting a PR. Luckily, PyCharm has a handy built-in feature that allows you to easily generate the docs. + +1. In the top right corner of the PyCharm window, select the drop-down menu and select `Edit Configurations`. +2. In the `Run/Debug Configurations` dialogue, click the green plus button in the top left and select `Python Docs > Sphinx Tasks`. +3. Under the Configuration tab, make sure the following are filled out: + :Name: + :Command: html + :Input: + :Output: +4. Click `Apply`, then `OK`. + +Now in order to generate the documentation on your computer to preview them, just click the green play button in the top right, next to the drop down menu with the name you chose above selected. Sphinx will run and you can view the resulting documentation wherever you chose Output to be, above. The window that Sphinx runs in will also show any errors that occur during the build in red, which should help you find typos and missing/incorrect syntax. + +Sample PR +========= + +Coming soon... \ No newline at end of file diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index 500936059..3f2e2f560 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -4,4 +4,5 @@ Reference Material .. toctree:: :maxdepth: 2 - modding/index \ No newline at end of file + modding/index + documentationHowTo \ No newline at end of file From 3cc6da1db2aaa0c584091527fd0decc26f0715fb Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sun, 17 Jun 2018 10:13:03 +0200 Subject: [PATCH 199/282] Update door.cpp typo fix --- apps/openmw/mwclass/door.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index 6846d3280..a6aacfa12 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -122,7 +122,7 @@ namespace MWClass std::string keyName; // FIXME: If NPC activate teleporting door, it can lead to crash due to iterator invalidation in the Actors update. - // Make such activation a no-op for now, how in vanilla game. + // Make such activation a no-op for now, like how it is in the vanilla game. if (actor != MWMechanics::getPlayer() && ptr.getCellRef().getTeleport()) { std::shared_ptr action(new MWWorld::FailedAction(std::string(), ptr)); From 8376c8c68e39bab3f63faf9f2c3ada344d4bd62d Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Mon, 18 Jun 2018 03:23:07 +0300 Subject: [PATCH 200/282] Allow partial matches in NotCell condition (fixes #4459) --- apps/openmw/mwdialogue/filter.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp index e8757e4a3..15020898f 100644 --- a/apps/openmw/mwdialogue/filter.cpp +++ b/apps/openmw/mwdialogue/filter.cpp @@ -491,10 +491,11 @@ bool MWDialogue::Filter::getSelectStructBoolean (const SelectWrapper& select) co return !Misc::StringUtils::ciEqual(mActor.get()->mBase->mRace, select.getName()); case SelectWrapper::Function_NotCell: - - return !Misc::StringUtils::ciEqual(MWBase::Environment::get().getWorld()->getCellName(mActor.getCell()) - , select.getName()); - + { + const std::string& actorCell = MWBase::Environment::get().getWorld()->getCellName(mActor.getCell()); + return !(actorCell.length() >= select.getName().length() + && Misc::StringUtils::ciEqual(actorCell.substr(0, select.getName().length()), select.getName())); + } case SelectWrapper::Function_SameGender: return (player.get()->mBase->mFlags & ESM::NPC::Female)== From 0cf2f6452b1a3dace0db5a98398afee971a3428d Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Mon, 18 Jun 2018 03:27:12 +0300 Subject: [PATCH 201/282] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe894e7fb..8e97435c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Bug #4454: AI opens doors too slow Bug #4457: Item without CanCarry flag prevents shield autoequipping in dark areas Bug #4458: AiWander console command handles idle chances incorrectly + Bug #4459: NotCell dialogue condition doesn't support partial matches Feature #4256: Implement ToggleBorders (TB) console command Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots From 1c7d5c68c506c13211c4f0c505efa57b4f5a00c9 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Mon, 18 Jun 2018 09:07:34 +0000 Subject: [PATCH 202/282] Update CHANGELOG.md, technically solved by moving from redmine over to gitlab. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe894e7fb..cd9140e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully + Bug #4125: OpenMW logo cropped on bugtracker Bug #4215: OpenMW shows book text after last
tag Bug #4221: Characters get stuck in V-shaped terrain Bug #4251: Stationary NPCs do not return to their position after combat From fe86e7ffc7dd3b9363f9ca2d6c28950dac6960ad Mon Sep 17 00:00:00 2001 From: rhtucker Date: Mon, 18 Jun 2018 12:49:48 -0700 Subject: [PATCH 203/282] Fixed inconsistent use of "since" --- docs/source/reference/documentationHowTo.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/reference/documentationHowTo.rst index 0a39aa978..2d8b4fae2 100644 --- a/docs/source/reference/documentationHowTo.rst +++ b/docs/source/reference/documentationHowTo.rst @@ -10,7 +10,7 @@ Intro The premise of this guide is that you would like to help out the OpenMW project beyond play-testing for bugs and such, buuuuut you're like me and don't really know how to code. This has the rather pesky side effect of you not really knowing about all the tools like GitHub and such. While many of these tools are super handy and great to know how to use, not everyone has the actual need and desire to learn the ins and outs of them. Since we would like as much help fleshing out the user documentation as possible, I wrote this guide to lower the barrier of entry into contributing to the project. -HOWEVER, as much as I will try to guide you through all the tedious setup and day-to-day stuff, you will eventually have to learn to write using ReST (reStructuredText) formatting. Since you're probably like me when I started helping and don't know wtf ReST is, it's an incredibly simple language that is easy to read in plain text form that can then be converted automatically into different types of documents like PDFs and html for webpages. +*However*, as much as I will try to guide you through all the tedious setup and day-to-day stuff, you will eventually have to learn to write using ReST (reStructuredText) formatting. Since you're probably like me when I started helping and don't know wtf ReST is, never fear. It's an incredibly simple language that is easy to read in plain text form that can then be converted automatically into different types of documents like PDFs and html for webpages. So here's what you're gonna be learning how to set up: @@ -36,7 +36,7 @@ The master OpenMW respository is where all of our work comes together and where Now you have an online repository that is the exact copy of the OpenMW master. To set up your local repository, we're going to use PyCharm. -If you want more info I recommend reading this guide: http://readwrite.com/2013/09/30/understanding-github-a-journey-for-beginners-part-1/ +If you want more info I recommend reading this guide: https://readwrite.com/2013/09/30/understanding-github-a-journey-for-beginners-part-1/ PyCharm ======= @@ -66,7 +66,7 @@ Sphinx So far I've mentioned ReST (reStructuredText) a couple times, but what is it, and what is Sphinx? The most basic explanation is that ReST is the markup language (like HTML is the markup language for webpages) and Sphinx is the program that goes through and builds the actual document so you can read it in a more visually pleasing way. For a much more detailed explanation, I recommend: https://coderwall.com/p/vemncg/what-is-the-difference-rest-docutils-sphinx-readthedocs -This will be the most technical section as we have to use the command prompt or terminal to install Python and Sphinx. I had intended to give you a universal explanation on how to install both, but it would drastically increase the length of this guide. The tutorial on the Sphinx website is really just going to be better than anything I write here, so please refer to their guide here: http://www.sphinx-doc.org/en/stable/install.html +This will be the most technical section as we have to use the command prompt or terminal to install Python and Sphinx. I had intended to give you a universal explanation on how to install both, but it would drastically increase the length of this guide. The tutorial on the Sphinx website is really just going to be better than anything I write here, so please refer to their guide here: https://www.sphinx-doc.org/en/stable/install.html Hopefully you now have Python and Sphinx installed. ... @@ -106,4 +106,4 @@ Now in order to generate the documentation on your computer to preview them, jus Sample PR ========= -Coming soon... \ No newline at end of file +Coming soon... From 5fd3ec103545cd9efb6150c2f06062fbc5c546c8 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Tue, 19 Jun 2018 14:17:33 +0400 Subject: [PATCH 204/282] Implement unlockable locks with 'lock 0' console command --- CHANGELOG.md | 1 + apps/openmw/mwclass/container.cpp | 12 +++++++----- apps/openmw/mwclass/door.cpp | 14 ++++++++------ apps/openmw/mwmechanics/mechanicsmanagerimp.cpp | 7 ++++++- apps/openmw/mwmechanics/security.cpp | 4 +++- components/esm/cellref.cpp | 3 +++ components/esm/cellref.hpp | 2 ++ 7 files changed, 30 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2033d3f08..af8c813db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Bug #4419: MRK NiStringExtraData is handled incorrectly Bug #4426: RotateWorld behavior is incorrect Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 + Bug #4431: "Lock 0" console command is a no-op Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 Bug #4451: Script fails to compile when using "Begin, [ScriptName]" syntax diff --git a/apps/openmw/mwclass/container.cpp b/apps/openmw/mwclass/container.cpp index 1d51a7830..efe460b00 100644 --- a/apps/openmw/mwclass/container.cpp +++ b/apps/openmw/mwclass/container.cpp @@ -239,7 +239,8 @@ namespace MWClass info.caption = ref->mBase->mName; std::string text; - if (ptr.getCellRef().getLockLevel() > 0) + int lockLevel = ptr.getCellRef().getLockLevel(); + if (lockLevel > 0 && lockLevel != ESM::UnbreakableLock) text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(ptr.getCellRef().getLockLevel()); else if (ptr.getCellRef().getLockLevel() < 0) text += "\n#{sUnlocked}"; @@ -271,15 +272,16 @@ namespace MWClass void Container::lock (const MWWorld::Ptr& ptr, int lockLevel) const { - if(lockLevel!=0) - ptr.getCellRef().setLockLevel(abs(lockLevel)); //Changes lock to locklevel, in positive + if(lockLevel != 0) + ptr.getCellRef().setLockLevel(abs(lockLevel)); //Changes lock to locklevel, if positive else - ptr.getCellRef().setLockLevel(abs(ptr.getCellRef().getLockLevel())); //No locklevel given, just flip the original one + ptr.getCellRef().setLockLevel(ESM::UnbreakableLock); // If zero, set to max lock level } void Container::unlock (const MWWorld::Ptr& ptr) const { - ptr.getCellRef().setLockLevel(-abs(ptr.getCellRef().getLockLevel())); //Makes lockLevel negative + int lockLevel = ptr.getCellRef().getLockLevel(); + ptr.getCellRef().setLockLevel(-abs(lockLevel)); //Makes lockLevel negative } bool Container::canLock(const MWWorld::ConstPtr &ptr) const diff --git a/apps/openmw/mwclass/door.cpp b/apps/openmw/mwclass/door.cpp index a6aacfa12..d738974dd 100644 --- a/apps/openmw/mwclass/door.cpp +++ b/apps/openmw/mwclass/door.cpp @@ -193,7 +193,7 @@ namespace MWClass std::shared_ptr action(new MWWorld::ActionTeleport (ptr.getCellRef().getDestCell(), ptr.getCellRef().getDoorDest(), true)); action->setSound(openSound); return action; - } + } } else { @@ -240,15 +240,16 @@ namespace MWClass void Door::lock (const MWWorld::Ptr& ptr, int lockLevel) const { - if(lockLevel!=0) - ptr.getCellRef().setLockLevel(abs(lockLevel)); //Changes lock to locklevel, in positive + if(lockLevel != 0) + ptr.getCellRef().setLockLevel(abs(lockLevel)); //Changes lock to locklevel, if positive else - ptr.getCellRef().setLockLevel(abs(ptr.getCellRef().getLockLevel())); //No locklevel given, just flip the original one + ptr.getCellRef().setLockLevel(ESM::UnbreakableLock); // If zero, set to max lock level } void Door::unlock (const MWWorld::Ptr& ptr) const { - ptr.getCellRef().setLockLevel(-abs(ptr.getCellRef().getLockLevel())); //Makes lockLevel negative + int lockLevel = ptr.getCellRef().getLockLevel(); + ptr.getCellRef().setLockLevel(-abs(lockLevel)); //Makes lockLevel negative } bool Door::canLock(const MWWorld::ConstPtr &ptr) const @@ -300,7 +301,8 @@ namespace MWClass text += "\n" + getDestination(*ref); } - if (ptr.getCellRef().getLockLevel() > 0) + int lockLevel = ptr.getCellRef().getLockLevel(); + if (lockLevel > 0 && lockLevel != ESM::UnbreakableLock) text += "\n#{sLockLevel}: " + MWGui::ToolTips::toString(ptr.getCellRef().getLockLevel()); else if (ptr.getCellRef().getLockLevel() < 0) text += "\n#{sUnlocked}"; diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index 1fc98a327..c2f7a664c 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -884,8 +884,13 @@ namespace MWMechanics const MWWorld::CellRef& cellref = target.getCellRef(); // there is no harm to use unlocked doors - if (target.getClass().isDoor() && cellref.getLockLevel() <= 0 && ptr.getCellRef().getTrap().empty()) + int lockLevel = cellref.getLockLevel(); + if (target.getClass().isDoor() && + (lockLevel <= 0 || lockLevel == ESM::UnbreakableLock) && + ptr.getCellRef().getTrap().empty()) + { return true; + } // TODO: implement a better check to check if target is owned bed if (target.getClass().isActivator() && target.getClass().getScript(target).compare(0, 3, "Bed") != 0) diff --git a/apps/openmw/mwmechanics/security.cpp b/apps/openmw/mwmechanics/security.cpp index 97878db87..3e38bc521 100644 --- a/apps/openmw/mwmechanics/security.cpp +++ b/apps/openmw/mwmechanics/security.cpp @@ -31,7 +31,9 @@ namespace MWMechanics void Security::pickLock(const MWWorld::Ptr &lock, const MWWorld::Ptr &lockpick, std::string& resultMessage, std::string& resultSound) { - if (!(lock.getCellRef().getLockLevel() > 0) || !lock.getClass().canLock(lock)) //If it's unlocked back out immediately + if (lock.getCellRef().getLockLevel() <= 0 || + lock.getCellRef().getLockLevel() == ESM::UnbreakableLock || + !lock.getClass().canLock(lock)) //If it's unlocked or can not be unlocked back out immediately return; int lockStrength = lock.getCellRef().getLockLevel(); diff --git a/components/esm/cellref.cpp b/components/esm/cellref.cpp index f35149586..1f7c2b964 100644 --- a/components/esm/cellref.cpp +++ b/components/esm/cellref.cpp @@ -126,6 +126,9 @@ void ESM::CellRef::loadData(ESMReader &esm, bool &isDeleted) break; } } + + if (mLockLevel == 0 && !mKey.empty()) + mLockLevel = UnbreakableLock; } void ESM::CellRef::save (ESMWriter &esm, bool wideRefNum, bool inInventory, bool isDeleted) const diff --git a/components/esm/cellref.hpp b/components/esm/cellref.hpp index f14617531..a0b9fdd58 100644 --- a/components/esm/cellref.hpp +++ b/components/esm/cellref.hpp @@ -1,6 +1,7 @@ #ifndef OPENMW_ESM_CELLREF_H #define OPENMW_ESM_CELLREF_H +#include #include #include "defs.hpp" @@ -10,6 +11,7 @@ namespace ESM class ESMWriter; class ESMReader; + const int UnbreakableLock = std::numeric_limits::max(); struct RefNum { From 0c4fa55f1657d9f0d270051832484b76981dbaf4 Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Tue, 19 Jun 2018 17:03:43 +0300 Subject: [PATCH 205/282] Make Open spells casted by anything trigger player crime event (fixes #4461) --- apps/openmw/mwmechanics/spellcasting.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index f6d92726d..0fa13fc07 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -665,8 +665,9 @@ namespace MWMechanics if (target.getCellRef().getLockLevel() > 0) { MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); - if (!caster.isEmpty() && caster.getClass().isActor()) - MWBase::Environment::get().getMechanicsManager()->objectOpened(caster, target); + if (!caster.isEmpty()) + MWBase::Environment::get().getMechanicsManager()->objectOpened(getPlayer(), target); + // Use the player instead of the caster for vanilla crime compatibility if (caster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicOpenSuccess}"); From 0731d79c091ab056c148ea5dbb17d5c97c17e7af Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Tue, 19 Jun 2018 17:06:31 +0300 Subject: [PATCH 206/282] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc150f344..aa680a09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Bug #4457: Item without CanCarry flag prevents shield autoequipping in dark areas Bug #4458: AiWander console command handles idle chances incorrectly Bug #4459: NotCell dialogue condition doesn't support partial matches + Bug #4461: "Open" spell from non-player caster isn't a crime Feature #4256: Implement ToggleBorders (TB) console command Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots From 0d70cb9473abd68583db9cbc51cc6562ce349858 Mon Sep 17 00:00:00 2001 From: Adam Bowen Date: Tue, 19 Jun 2018 15:45:04 +0000 Subject: [PATCH 207/282] Re-link bugtracker links to Gitlab * Link "1.0" issues to the "1.0" tag * Link "report a bug" to the issues page * Link "known issues" to the "Bug" tag --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9af9ef976..333eedff1 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Font Licenses: Current Status -------------- -The main quests in Morrowind, Tribunal and Bloodmoon are all completable. Some issues with side quests are to be expected (but rare). Check the [bug tracker](https://bugs.openmw.org/) for a list of issues we need to resolve before the "1.0" release. Even before the "1.0" release however, OpenMW boasts some new [features](https://wiki.openmw.org/index.php?title=Features), such as improved graphics and user interfaces. +The main quests in Morrowind, Tribunal and Bloodmoon are all completable. Some issues with side quests are to be expected (but rare). Check the [bug tracker](https://gitlab.com/OpenMW/openmw/issues?label_name%5B%5D=1.0) for a list of issues we need to resolve before the "1.0" release. Even before the "1.0" release however, OpenMW boasts some new [features](https://wiki.openmw.org/index.php?title=Features), such as improved graphics and user interfaces. Pre-existing modifications created for the original Morrowind engine can be hit-and-miss. The OpenMW script compiler performs more thorough error-checking than Morrowind does, meaning that a mod created for Morrowind may not necessarily run in OpenMW. Some mods also rely on quirky behaviour or engine bugs in order to work. We are considering such compatibility issues on a case-by-case basis - in some cases adding a workaround to OpenMW may be feasible, in other cases fixing the mod will be the only option. If you know of any mods that work or don't work, feel free to add them to the [Mod status](https://wiki.openmw.org/index.php?title=Mod_status) wiki page. @@ -30,8 +30,8 @@ Getting Started * [Build from source](https://wiki.openmw.org/index.php?title=Development_Environment_Setup) * [Testing the game](https://wiki.openmw.org/index.php?title=Testing) * [How to contribute](https://wiki.openmw.org/index.php?title=Contribution_Wanted) -* [Report a bug](https://bugs.openmw.org/projects/openmw) - read the [guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) before submitting your first bug! -* [Known issues](https://bugs.openmw.org/projects/openmw/issues?utf8=%E2%9C%93&set_filter=1&f%5B%5D=status_id&op%5Bstatus_id%5D=%3D&v%5Bstatus_id%5D%5B%5D=7&f%5B%5D=tracker_id&op%5Btracker_id%5D=%3D&v%5Btracker_id%5D%5B%5D=1&f%5B%5D=&c%5B%5D=project&c%5B%5D=tracker&c%5B%5D=status&c%5B%5D=priority&c%5B%5D=subject&c%5B%5D=assigned_to&c%5B%5D=updated_on&group_by=tracker) +* [Report a bug](https://gitlab.com/OpenMW/openmw/issues) - read the [guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) before submitting your first bug! +* [Known issues](https://gitlab.com/OpenMW/openmw/issues?label_name%5B%5D=Bug) The data path ------------- From 6c23caadd7bf37bee5e80c5d0e1f8f323697144e Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Tue, 19 Jun 2018 20:33:30 +0300 Subject: [PATCH 208/282] Fix crash when a target in a different cell is (un)locked --- apps/openmw/mwmechanics/spellcasting.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 0fa13fc07..f1997e8d7 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -644,8 +644,9 @@ namespace MWMechanics { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - animation->addSpellCastGlow(magiceffect); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); + if (animation) + animation->addSpellCastGlow(magiceffect); if (target.getCellRef().getLockLevel() < magnitude) //If the door is not already locked to a higher value, lock it to spell magnitude { if (caster == getPlayer()) @@ -658,15 +659,16 @@ namespace MWMechanics { const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const ESM::MagicEffect *magiceffect = store.get().find(effectId); - MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); - animation->addSpellCastGlow(magiceffect); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); + if (animation) + animation->addSpellCastGlow(magiceffect); if (target.getCellRef().getLockLevel() <= magnitude) { if (target.getCellRef().getLockLevel() > 0) { MWBase::Environment::get().getSoundManager()->playSound3D(target, "Open Lock", 1.f, 1.f); if (!caster.isEmpty()) - MWBase::Environment::get().getMechanicsManager()->objectOpened(getPlayer(), target); + MWBase::Environment::get().getMechanicsManager()->objectOpened(getPlayer(), target); // Use the player instead of the caster for vanilla crime compatibility if (caster == getPlayer()) From 24ddb66af9493e1edde00684280bcbbf7089b992 Mon Sep 17 00:00:00 2001 From: Xenkhan <36826384+Xenkhan@users.noreply.github.com> Date: Tue, 19 Jun 2018 15:03:30 -0500 Subject: [PATCH 209/282] Retrieve SDL window settings instead of using magic numbers --- apps/openmw/engine.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 5052a13cc..a9f239914 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -383,18 +383,17 @@ void OMW::Engine::createWindow(Settings::Manager& settings) osg::ref_ptr traits = new osg::GraphicsContext::Traits; SDL_GetWindowPosition(mWindow, &traits->x, &traits->y); SDL_GetWindowSize(mWindow, &traits->width, &traits->height); + SDL_GL_GetAttribute(SDL_GL_RED_SIZE, reinterpret_cast(&traits->red)); + SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, reinterpret_cast(&traits->green)); + SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, reinterpret_cast(&traits->blue)); + SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, reinterpret_cast(&traits->depth)); + SDL_GL_GetAttribute(SDL_GL_STENCIL_SIZE, reinterpret_cast(&traits->stencil)); + SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, reinterpret_cast(&traits->doubleBuffer)); traits->windowName = SDL_GetWindowTitle(mWindow); traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); - // FIXME: Some way to get these settings back from the SDL window? - traits->red = 8; - traits->green = 8; - traits->blue = 8; - traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel - traits->depth = 24; - traits->stencil = 8; traits->vsync = vsync; - traits->doubleBuffer = true; + traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel traits->inheritedWindowData = new SDLUtil::GraphicsWindowSDL2::WindowData(mWindow); osg::ref_ptr graphicsWindow = new SDLUtil::GraphicsWindowSDL2(traits); From 9d61d76e928d146f9c1ad76ab6152fdcda58c711 Mon Sep 17 00:00:00 2001 From: Atahualpa Date: Wed, 20 Jun 2018 00:20:03 +0200 Subject: [PATCH 210/282] Adds the option to ignore "Base" records when running the verifier. (fixes #4466) Adds a boolean setting to the user preferences. This setting is locally saved to all OpenMW-CS check stages. When a verification is done, the setting is updated on setup for each check stage. If set to true, the boolean value is then used to skip the verification process for every base record - minus some special cases where, e.g., counters are to be set first. Related issue: - Fixes #4466: Editor: Add option to ignore base records when running verifier (https://gitlab.com/OpenMW/openmw/issues/4466) Tests: The changes were successfully tested in OpenMW-CS by creating faulty "Base" and "Modified" records for every record type (if possible) and, then, running the verifier with and without the option respectively. --- CHANGELOG.md | 3 +- apps/opencs/model/prefs/state.cpp | 1 + apps/opencs/model/tools/birthsigncheck.cpp | 11 ++- apps/opencs/model/tools/birthsigncheck.hpp | 1 + apps/opencs/model/tools/bodypartcheck.cpp | 11 ++- apps/opencs/model/tools/bodypartcheck.hpp | 1 + apps/opencs/model/tools/classcheck.cpp | 11 ++- apps/opencs/model/tools/classcheck.hpp | 1 + apps/opencs/model/tools/factioncheck.cpp | 11 ++- apps/opencs/model/tools/factioncheck.hpp | 1 + apps/opencs/model/tools/gmstcheck.cpp | 11 ++- apps/opencs/model/tools/gmstcheck.hpp | 1 + apps/opencs/model/tools/journalcheck.cpp | 15 +++- apps/opencs/model/tools/journalcheck.hpp | 1 + apps/opencs/model/tools/magiceffectcheck.cpp | 16 +++- apps/opencs/model/tools/magiceffectcheck.hpp | 1 + apps/opencs/model/tools/pathgridcheck.cpp | 11 ++- apps/opencs/model/tools/pathgridcheck.hpp | 1 + apps/opencs/model/tools/racecheck.cpp | 20 +++-- apps/opencs/model/tools/racecheck.hpp | 1 + .../opencs/model/tools/referenceablecheck.cpp | 78 ++++++++++++------- .../opencs/model/tools/referenceablecheck.hpp | 1 + apps/opencs/model/tools/referencecheck.cpp | 8 +- apps/opencs/model/tools/referencecheck.hpp | 1 + apps/opencs/model/tools/regioncheck.cpp | 11 ++- apps/opencs/model/tools/regioncheck.hpp | 1 + apps/opencs/model/tools/scriptcheck.cpp | 16 +++- apps/opencs/model/tools/scriptcheck.hpp | 1 + apps/opencs/model/tools/skillcheck.cpp | 11 ++- apps/opencs/model/tools/skillcheck.hpp | 1 + apps/opencs/model/tools/soundcheck.cpp | 11 ++- apps/opencs/model/tools/soundcheck.hpp | 1 + apps/opencs/model/tools/soundgencheck.cpp | 14 +++- apps/opencs/model/tools/soundgencheck.hpp | 1 + apps/opencs/model/tools/spellcheck.cpp | 11 ++- apps/opencs/model/tools/spellcheck.hpp | 1 + apps/opencs/model/tools/startscriptcheck.cpp | 11 ++- apps/opencs/model/tools/startscriptcheck.hpp | 1 + apps/opencs/model/tools/topicinfocheck.cpp | 11 ++- apps/opencs/model/tools/topicinfocheck.hpp | 2 + apps/opencs/model/world/record.cpp | 5 ++ apps/opencs/model/world/record.hpp | 2 + 42 files changed, 260 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc150f344..33ba3e461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,12 +44,13 @@ Bug #4457: Item without CanCarry flag prevents shield autoequipping in dark areas Bug #4458: AiWander console command handles idle chances incorrectly Bug #4459: NotCell dialogue condition doesn't support partial matches - Feature #4256: Implement ToggleBorders (TB) console command Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots + Feature #4256: Implement ToggleBorders (TB) console command Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts Feature #4345: Add equivalents for the command line commands to Launcher Feature #4444: Per-group KF-animation files support + Feature #4466: (OpenMW-CS) Add option to ignore "Base" records when running verifier 0.44.0 ------ diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index 6f64da72e..e1236a0e4 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -123,6 +123,7 @@ void CSMPrefs::State::declare() declareEnum ("double-s", "Shift Double Click", actionRemove).addValues (reportValues); declareEnum ("double-c", "Control Double Click", actionEditAndRemove).addValues (reportValues); declareEnum ("double-sc", "Shift Control Double Click", actionNone).addValues (reportValues); + declareBool("ignore-base-records", "Ignore base records in verifier", false); declareCategory ("Search & Replace"); declareInt ("char-before", "Characters before search string", 10). diff --git a/apps/opencs/model/tools/birthsigncheck.cpp b/apps/opencs/model/tools/birthsigncheck.cpp index 9898352f1..5fe2479cd 100644 --- a/apps/opencs/model/tools/birthsigncheck.cpp +++ b/apps/opencs/model/tools/birthsigncheck.cpp @@ -5,14 +5,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::BirthsignCheckStage::BirthsignCheckStage (const CSMWorld::IdCollection& birthsigns) : mBirthsigns (birthsigns) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::BirthsignCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mBirthsigns.getSize(); } @@ -20,7 +26,8 @@ void CSMTools::BirthsignCheckStage::perform (int stage, CSMDoc::Messages& messag { const CSMWorld::Record& record = mBirthsigns.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const ESM::BirthSign& birthsign = record.get(); diff --git a/apps/opencs/model/tools/birthsigncheck.hpp b/apps/opencs/model/tools/birthsigncheck.hpp index 16d4c666f..a8a7a2c14 100644 --- a/apps/opencs/model/tools/birthsigncheck.hpp +++ b/apps/opencs/model/tools/birthsigncheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class BirthsignCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mBirthsigns; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/bodypartcheck.cpp b/apps/opencs/model/tools/bodypartcheck.cpp index 68a09485f..8d19ef489 100644 --- a/apps/opencs/model/tools/bodypartcheck.cpp +++ b/apps/opencs/model/tools/bodypartcheck.cpp @@ -1,5 +1,7 @@ #include "bodypartcheck.hpp" +#include "../prefs/state.hpp" + CSMTools::BodyPartCheckStage::BodyPartCheckStage( const CSMWorld::IdCollection &bodyParts, const CSMWorld::Resources &meshes, @@ -7,10 +9,14 @@ CSMTools::BodyPartCheckStage::BodyPartCheckStage( mBodyParts(bodyParts), mMeshes(meshes), mRaces(races) -{ } +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::BodyPartCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mBodyParts.getSize(); } @@ -18,7 +24,8 @@ void CSMTools::BodyPartCheckStage::perform (int stage, CSMDoc::Messages &message { const CSMWorld::Record &record = mBodyParts.getRecord(stage); - if ( record.isDeleted() ) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const ESM::BodyPart &bodyPart = record.get(); diff --git a/apps/opencs/model/tools/bodypartcheck.hpp b/apps/opencs/model/tools/bodypartcheck.hpp index dbab5f5c6..5c8ae2929 100644 --- a/apps/opencs/model/tools/bodypartcheck.hpp +++ b/apps/opencs/model/tools/bodypartcheck.hpp @@ -17,6 +17,7 @@ namespace CSMTools const CSMWorld::IdCollection &mBodyParts; const CSMWorld::Resources &mMeshes; const CSMWorld::IdCollection &mRaces; + bool mIgnoreBaseRecords; public: BodyPartCheckStage( diff --git a/apps/opencs/model/tools/classcheck.cpp b/apps/opencs/model/tools/classcheck.cpp index 79cb704bf..82007c860 100644 --- a/apps/opencs/model/tools/classcheck.cpp +++ b/apps/opencs/model/tools/classcheck.cpp @@ -6,14 +6,20 @@ #include #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::ClassCheckStage::ClassCheckStage (const CSMWorld::IdCollection& classes) : mClasses (classes) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::ClassCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mClasses.getSize(); } @@ -21,7 +27,8 @@ void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mClasses.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const ESM::Class& class_ = record.get(); diff --git a/apps/opencs/model/tools/classcheck.hpp b/apps/opencs/model/tools/classcheck.hpp index b76da3f13..ba0a07047 100644 --- a/apps/opencs/model/tools/classcheck.hpp +++ b/apps/opencs/model/tools/classcheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class ClassCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mClasses; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/factioncheck.cpp b/apps/opencs/model/tools/factioncheck.cpp index 621b28070..2cf88fc0e 100644 --- a/apps/opencs/model/tools/factioncheck.cpp +++ b/apps/opencs/model/tools/factioncheck.cpp @@ -6,14 +6,20 @@ #include #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::FactionCheckStage::FactionCheckStage (const CSMWorld::IdCollection& factions) : mFactions (factions) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::FactionCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mFactions.getSize(); } @@ -21,7 +27,8 @@ void CSMTools::FactionCheckStage::perform (int stage, CSMDoc::Messages& messages { const CSMWorld::Record& record = mFactions.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const ESM::Faction& faction = record.get(); diff --git a/apps/opencs/model/tools/factioncheck.hpp b/apps/opencs/model/tools/factioncheck.hpp index 321a4d6d8..b26d19717 100644 --- a/apps/opencs/model/tools/factioncheck.hpp +++ b/apps/opencs/model/tools/factioncheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class FactionCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mFactions; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/gmstcheck.cpp b/apps/opencs/model/tools/gmstcheck.cpp index 0c32c0056..e3b4777bf 100644 --- a/apps/opencs/model/tools/gmstcheck.cpp +++ b/apps/opencs/model/tools/gmstcheck.cpp @@ -2,14 +2,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/defaultgmsts.hpp" CSMTools::GmstCheckStage::GmstCheckStage(const CSMWorld::IdCollection& gameSettings) : mGameSettings(gameSettings) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::GmstCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mGameSettings.getSize(); } @@ -17,7 +23,8 @@ void CSMTools::GmstCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mGameSettings.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const ESM::GameSetting& gmst = record.get(); diff --git a/apps/opencs/model/tools/gmstcheck.hpp b/apps/opencs/model/tools/gmstcheck.hpp index 0d4f7f204..27bd61317 100644 --- a/apps/opencs/model/tools/gmstcheck.hpp +++ b/apps/opencs/model/tools/gmstcheck.hpp @@ -25,6 +25,7 @@ namespace CSMTools private: const CSMWorld::IdCollection& mGameSettings; + bool mIgnoreBaseRecords; std::string varTypeToString(ESM::VarType); diff --git a/apps/opencs/model/tools/journalcheck.cpp b/apps/opencs/model/tools/journalcheck.cpp index bdd14ddf0..a565f3786 100644 --- a/apps/opencs/model/tools/journalcheck.cpp +++ b/apps/opencs/model/tools/journalcheck.cpp @@ -3,13 +3,19 @@ #include #include +#include "../prefs/state.hpp" + CSMTools::JournalCheckStage::JournalCheckStage(const CSMWorld::IdCollection &journals, const CSMWorld::InfoCollection& journalInfos) : mJournals(journals), mJournalInfos(journalInfos) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::JournalCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mJournals.getSize(); } @@ -17,7 +23,8 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record &journalRecord = mJournals.getRecord(stage); - if (journalRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && journalRecord.isBaseOnly()) || journalRecord.isDeleted()) return; const ESM::Dialogue &journal = journalRecord.get(); @@ -43,6 +50,10 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) statusNamedCount += 1; } + // Skip "Base" records (setting!) + if (mIgnoreBaseRecords && infoRecord.isBaseOnly()) + continue; + if (journalInfo.mResponse.empty()) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_JournalInfo, journalInfo.mId); diff --git a/apps/opencs/model/tools/journalcheck.hpp b/apps/opencs/model/tools/journalcheck.hpp index c9f619698..661edcaef 100644 --- a/apps/opencs/model/tools/journalcheck.hpp +++ b/apps/opencs/model/tools/journalcheck.hpp @@ -28,6 +28,7 @@ namespace CSMTools const CSMWorld::IdCollection& mJournals; const CSMWorld::InfoCollection& mJournalInfos; + bool mIgnoreBaseRecords; }; } diff --git a/apps/opencs/model/tools/magiceffectcheck.cpp b/apps/opencs/model/tools/magiceffectcheck.cpp index ab8b3b68b..f9e83aa23 100644 --- a/apps/opencs/model/tools/magiceffectcheck.cpp +++ b/apps/opencs/model/tools/magiceffectcheck.cpp @@ -2,6 +2,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/resources.hpp" #include "../world/data.hpp" @@ -77,16 +79,26 @@ CSMTools::MagicEffectCheckStage::MagicEffectCheckStage(const CSMWorld::IdCollect mReferenceables(referenceables), mIcons(icons), mTextures(textures) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::MagicEffectCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mMagicEffects.getSize(); } void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages &messages) { - ESM::MagicEffect effect = mMagicEffects.getRecord(stage).get(); + const CSMWorld::Record &record = mMagicEffects.getRecord(stage); + + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + return; + + ESM::MagicEffect effect = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_MagicEffect, effect.mId); if (effect.mData.mBaseCost < 0.0f) diff --git a/apps/opencs/model/tools/magiceffectcheck.hpp b/apps/opencs/model/tools/magiceffectcheck.hpp index 0ad6760d3..28a406283 100644 --- a/apps/opencs/model/tools/magiceffectcheck.hpp +++ b/apps/opencs/model/tools/magiceffectcheck.hpp @@ -24,6 +24,7 @@ namespace CSMTools const CSMWorld::RefIdCollection &mReferenceables; const CSMWorld::Resources &mIcons; const CSMWorld::Resources &mTextures; + bool mIgnoreBaseRecords; private: bool isTextureExists(const std::string &texture, bool isIcon) const; diff --git a/apps/opencs/model/tools/pathgridcheck.cpp b/apps/opencs/model/tools/pathgridcheck.cpp index be4d37792..53293fa79 100644 --- a/apps/opencs/model/tools/pathgridcheck.cpp +++ b/apps/opencs/model/tools/pathgridcheck.cpp @@ -3,6 +3,8 @@ #include #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" #include "../world/idcollection.hpp" #include "../world/subcellcollection.hpp" @@ -10,10 +12,14 @@ CSMTools::PathgridCheckStage::PathgridCheckStage (const CSMWorld::SubCellCollection& pathgrids) : mPathgrids (pathgrids) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::PathgridCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mPathgrids.getSize(); } @@ -21,7 +27,8 @@ void CSMTools::PathgridCheckStage::perform (int stage, CSMDoc::Messages& message { const CSMWorld::Record& record = mPathgrids.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const CSMWorld::Pathgrid& pathgrid = record.get(); diff --git a/apps/opencs/model/tools/pathgridcheck.hpp b/apps/opencs/model/tools/pathgridcheck.hpp index f45b5bc93..3e2fdd0ab 100644 --- a/apps/opencs/model/tools/pathgridcheck.hpp +++ b/apps/opencs/model/tools/pathgridcheck.hpp @@ -25,6 +25,7 @@ namespace CSMTools { const CSMWorld::SubCellCollection >& mPathgrids; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/racecheck.cpp b/apps/opencs/model/tools/racecheck.cpp index b30088620..c7fd83224 100644 --- a/apps/opencs/model/tools/racecheck.cpp +++ b/apps/opencs/model/tools/racecheck.cpp @@ -4,6 +4,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& messages) @@ -15,6 +17,14 @@ void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& me const ESM::Race& race = record.get(); + // Consider mPlayable flag even when "Base" records are ignored + if (race.mData.mFlags & 0x1) + mPlayable = true; + + // Skip "Base" records (setting!) + if (mIgnoreBaseRecords && record.isBaseOnly()) + return; + CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Race, race.mId); // test for empty name and description @@ -38,10 +48,6 @@ void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& me if (race.mData.mWeight.mFemale<0) messages.push_back (std::make_pair (id, "female " + race.mId + " has negative weight")); - // remember playable flag - if (race.mData.mFlags & 0x1) - mPlayable = true; - /// \todo check data members that can't be edited in the table view } @@ -55,11 +61,15 @@ void CSMTools::RaceCheckStage::performFinal (CSMDoc::Messages& messages) CSMTools::RaceCheckStage::RaceCheckStage (const CSMWorld::IdCollection& races) : mRaces (races), mPlayable (false) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::RaceCheckStage::setup() { mPlayable = false; + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mRaces.getSize()+1; } diff --git a/apps/opencs/model/tools/racecheck.hpp b/apps/opencs/model/tools/racecheck.hpp index 3e67b7577..55c283611 100644 --- a/apps/opencs/model/tools/racecheck.hpp +++ b/apps/opencs/model/tools/racecheck.hpp @@ -14,6 +14,7 @@ namespace CSMTools { const CSMWorld::IdCollection& mRaces; bool mPlayable; + bool mIgnoreBaseRecords; void performPerRecord (int stage, CSMDoc::Messages& messages); diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index 1e86dfe37..0c9a32b5c 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -2,6 +2,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/record.hpp" #include "../world/universalid.hpp" @@ -18,6 +20,7 @@ CSMTools::ReferenceableCheckStage::ReferenceableCheckStage( mScripts(scripts), mPlayerPresent(false) { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); } void CSMTools::ReferenceableCheckStage::perform (int stage, CSMDoc::Messages& messages) @@ -228,6 +231,8 @@ void CSMTools::ReferenceableCheckStage::perform (int stage, CSMDoc::Messages& me int CSMTools::ReferenceableCheckStage::setup() { mPlayerPresent = false; + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mReferencables.getSize() + 1; } @@ -238,7 +243,8 @@ void CSMTools::ReferenceableCheckStage::bookCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Book& book = (dynamic_cast& >(baseRecord)).get(); @@ -257,7 +263,8 @@ void CSMTools::ReferenceableCheckStage::activatorCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Activator& activator = (dynamic_cast& >(baseRecord)).get(); @@ -278,7 +285,8 @@ void CSMTools::ReferenceableCheckStage::potionCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Potion& potion = (dynamic_cast& >(baseRecord)).get(); @@ -299,7 +307,8 @@ void CSMTools::ReferenceableCheckStage::apparatusCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Apparatus& apparatus = (dynamic_cast& >(baseRecord)).get(); @@ -320,7 +329,8 @@ void CSMTools::ReferenceableCheckStage::armorCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Armor& armor = (dynamic_cast& >(baseRecord)).get(); @@ -347,7 +357,8 @@ void CSMTools::ReferenceableCheckStage::clothingCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Clothing& clothing = (dynamic_cast& >(baseRecord)).get(); @@ -365,7 +376,8 @@ void CSMTools::ReferenceableCheckStage::containerCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Container& container = (dynamic_cast& >(baseRecord)).get(); @@ -397,7 +409,8 @@ void CSMTools::ReferenceableCheckStage::creatureCheck ( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Creature& creature = (dynamic_cast&>(baseRecord)).get(); @@ -473,7 +486,8 @@ void CSMTools::ReferenceableCheckStage::doorCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Door& door = (dynamic_cast&>(baseRecord)).get(); @@ -497,7 +511,8 @@ void CSMTools::ReferenceableCheckStage::ingredientCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Ingredient& ingredient = (dynamic_cast& >(baseRecord)).get(); @@ -516,10 +531,9 @@ void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) - { + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; - } const ESM::CreatureLevList& CreatureLevList = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_CreatureLevelledList, CreatureLevList.mId); //CreatureLevList but Type_CreatureLevelledList :/ @@ -534,10 +548,9 @@ void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) - { + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; - } const ESM::ItemLevList& ItemLevList = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_ItemLevelledList, ItemLevList.mId); @@ -551,7 +564,8 @@ void CSMTools::ReferenceableCheckStage::lightCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Light& light = (dynamic_cast& >(baseRecord)).get(); @@ -574,7 +588,8 @@ void CSMTools::ReferenceableCheckStage::lockpickCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Lockpick& lockpick = (dynamic_cast& >(baseRecord)).get(); @@ -595,7 +610,8 @@ void CSMTools::ReferenceableCheckStage::miscCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Miscellaneous& miscellaneous = (dynamic_cast& >(baseRecord)).get(); @@ -619,6 +635,14 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( const ESM::NPC& npc = (dynamic_cast& >(baseRecord)).get(); CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Npc, npc.mId); + //Detect if player is present + if (Misc::StringUtils::ciEqual(npc.mId, "player")) //Happy now, scrawl? + mPlayerPresent = true; + + // Skip "Base" records (setting!) + if (mIgnoreBaseRecords && baseRecord.isBaseOnly()) + return; + short level(npc.mNpdt.mLevel); char disposition(npc.mNpdt.mDisposition); char reputation(npc.mNpdt.mReputation); @@ -626,10 +650,6 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( //Don't know what unknown is for int gold(npc.mNpdt.mGold); - //Detect if player is present - if (Misc::StringUtils::ciEqual(npc.mId, "player")) //Happy now, scrawl? - mPlayerPresent = true; - if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) //12 = autocalculated { if ((npc.mFlags & ESM::NPC::Autocalc) == 0) //0x0010 = autocalculated flag @@ -728,7 +748,8 @@ void CSMTools::ReferenceableCheckStage::weaponCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Weapon& weapon = (dynamic_cast& >(baseRecord)).get(); @@ -808,7 +829,8 @@ void CSMTools::ReferenceableCheckStage::probeCheck( { const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Probe& probe = (dynamic_cast& >(baseRecord)).get(); @@ -827,7 +849,8 @@ void CSMTools::ReferenceableCheckStage::repairCheck ( { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Repair& repair = (dynamic_cast& >(baseRecord)).get(); @@ -846,7 +869,8 @@ void CSMTools::ReferenceableCheckStage::staticCheck ( { const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); - if (baseRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) return; const ESM::Static& staticElement = (dynamic_cast& >(baseRecord)).get(); diff --git a/apps/opencs/model/tools/referenceablecheck.hpp b/apps/opencs/model/tools/referenceablecheck.hpp index 4356e50b2..f9341bd9c 100644 --- a/apps/opencs/model/tools/referenceablecheck.hpp +++ b/apps/opencs/model/tools/referenceablecheck.hpp @@ -82,6 +82,7 @@ namespace CSMTools const CSMWorld::IdCollection& mFactions; const CSMWorld::IdCollection& mScripts; bool mPlayerPresent; + bool mIgnoreBaseRecords; }; } #endif // REFERENCEABLECHECKSTAGE_H diff --git a/apps/opencs/model/tools/referencecheck.cpp b/apps/opencs/model/tools/referencecheck.cpp index 7f247741c..347a8a399 100644 --- a/apps/opencs/model/tools/referencecheck.cpp +++ b/apps/opencs/model/tools/referencecheck.cpp @@ -1,5 +1,7 @@ #include "referencecheck.hpp" +#include "../prefs/state.hpp" + CSMTools::ReferenceCheckStage::ReferenceCheckStage( const CSMWorld::RefCollection& references, const CSMWorld::RefIdCollection& referencables, @@ -12,13 +14,15 @@ CSMTools::ReferenceCheckStage::ReferenceCheckStage( mCells(cells), mFactions(factions) { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); } void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &messages) { const CSMWorld::Record& record = mReferences.getRecord(stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const CSMWorld::CellRef& cellRef = record.get(); @@ -100,5 +104,7 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &message int CSMTools::ReferenceCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mReferences.getSize(); } diff --git a/apps/opencs/model/tools/referencecheck.hpp b/apps/opencs/model/tools/referencecheck.hpp index 70ef02916..5e25924f3 100644 --- a/apps/opencs/model/tools/referencecheck.hpp +++ b/apps/opencs/model/tools/referencecheck.hpp @@ -23,6 +23,7 @@ namespace CSMTools const CSMWorld::RefIdData& mDataSet; const CSMWorld::IdCollection& mCells; const CSMWorld::IdCollection& mFactions; + bool mIgnoreBaseRecords; }; } diff --git a/apps/opencs/model/tools/regioncheck.cpp b/apps/opencs/model/tools/regioncheck.cpp index 734861080..45b311414 100644 --- a/apps/opencs/model/tools/regioncheck.cpp +++ b/apps/opencs/model/tools/regioncheck.cpp @@ -5,14 +5,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::RegionCheckStage::RegionCheckStage (const CSMWorld::IdCollection& regions) : mRegions (regions) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::RegionCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mRegions.getSize(); } @@ -20,7 +26,8 @@ void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRegions.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const ESM::Region& region = record.get(); diff --git a/apps/opencs/model/tools/regioncheck.hpp b/apps/opencs/model/tools/regioncheck.hpp index 8ba32e137..4c12727f0 100644 --- a/apps/opencs/model/tools/regioncheck.hpp +++ b/apps/opencs/model/tools/regioncheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class RegionCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mRegions; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index 268aea379..bf5d48f65 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -60,6 +60,8 @@ CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMDoc::Document& document) Compiler::registerExtensions (mExtensions); mContext.setExtensions (&mExtensions); + + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); } int CSMTools::ScriptCheckStage::setup() @@ -78,17 +80,25 @@ int CSMTools::ScriptCheckStage::setup() mId.clear(); Compiler::ErrorHandler::reset(); + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mDocument.getData().getScripts().getSize(); } void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) { + const CSMWorld::Record &record = mDocument.getData().getScripts().getRecord(stage); + mId = mDocument.getData().getScripts().getId (stage); if (mDocument.isBlacklisted ( CSMWorld::UniversalId (CSMWorld::UniversalId::Type_Script, mId))) return; + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + return; + mMessages = &messages; switch (mWarningMode) @@ -100,10 +110,8 @@ void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) try { - const CSMWorld::Data& data = mDocument.getData(); - - mFile = data.getScripts().getRecord (stage).get().mId; - std::istringstream input (data.getScripts().getRecord (stage).get().mScriptText); + mFile = record.get().mId; + std::istringstream input (record.get().mScriptText); Compiler::Scanner scanner (*this, input, mContext.getExtensions()); diff --git a/apps/opencs/model/tools/scriptcheck.hpp b/apps/opencs/model/tools/scriptcheck.hpp index f58215800..8f4ac9763 100644 --- a/apps/opencs/model/tools/scriptcheck.hpp +++ b/apps/opencs/model/tools/scriptcheck.hpp @@ -32,6 +32,7 @@ namespace CSMTools std::string mFile; CSMDoc::Messages *mMessages; WarningMode mWarningMode; + bool mIgnoreBaseRecords; CSMDoc::Message::Severity getSeverity (Type type); diff --git a/apps/opencs/model/tools/skillcheck.cpp b/apps/opencs/model/tools/skillcheck.cpp index 77ba8d4a2..2214ec11a 100644 --- a/apps/opencs/model/tools/skillcheck.cpp +++ b/apps/opencs/model/tools/skillcheck.cpp @@ -4,14 +4,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::SkillCheckStage::SkillCheckStage (const CSMWorld::IdCollection& skills) : mSkills (skills) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::SkillCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mSkills.getSize(); } @@ -19,7 +25,8 @@ void CSMTools::SkillCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSkills.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const ESM::Skill& skill = record.get(); diff --git a/apps/opencs/model/tools/skillcheck.hpp b/apps/opencs/model/tools/skillcheck.hpp index 93b06fe71..edd6b79a0 100644 --- a/apps/opencs/model/tools/skillcheck.hpp +++ b/apps/opencs/model/tools/skillcheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class SkillCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mSkills; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/soundcheck.cpp b/apps/opencs/model/tools/soundcheck.cpp index 3dbd3ef11..b21341333 100644 --- a/apps/opencs/model/tools/soundcheck.cpp +++ b/apps/opencs/model/tools/soundcheck.cpp @@ -4,14 +4,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::SoundCheckStage::SoundCheckStage (const CSMWorld::IdCollection& sounds) : mSounds (sounds) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::SoundCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mSounds.getSize(); } @@ -19,7 +25,8 @@ void CSMTools::SoundCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSounds.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const ESM::Sound& sound = record.get(); diff --git a/apps/opencs/model/tools/soundcheck.hpp b/apps/opencs/model/tools/soundcheck.hpp index 52f2d3714..d6fff5263 100644 --- a/apps/opencs/model/tools/soundcheck.hpp +++ b/apps/opencs/model/tools/soundcheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class SoundCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mSounds; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/soundgencheck.cpp b/apps/opencs/model/tools/soundgencheck.cpp index a36c494a1..acd245016 100644 --- a/apps/opencs/model/tools/soundgencheck.cpp +++ b/apps/opencs/model/tools/soundgencheck.cpp @@ -2,6 +2,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/refiddata.hpp" #include "../world/universalid.hpp" @@ -11,20 +13,24 @@ CSMTools::SoundGenCheckStage::SoundGenCheckStage(const CSMWorld::IdCollection &record = mSoundGens.getRecord(stage); - if (record.isDeleted()) - { + + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; - } const ESM::SoundGenerator& soundGen = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_SoundGen, soundGen.mId); diff --git a/apps/opencs/model/tools/soundgencheck.hpp b/apps/opencs/model/tools/soundgencheck.hpp index 91b08f979..19388cb91 100644 --- a/apps/opencs/model/tools/soundgencheck.hpp +++ b/apps/opencs/model/tools/soundgencheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools const CSMWorld::IdCollection &mSoundGens; const CSMWorld::IdCollection &mSounds; const CSMWorld::RefIdCollection &mReferenceables; + bool mIgnoreBaseRecords; public: SoundGenCheckStage(const CSMWorld::IdCollection &soundGens, diff --git a/apps/opencs/model/tools/spellcheck.cpp b/apps/opencs/model/tools/spellcheck.cpp index 91aed37ed..6fb38138b 100644 --- a/apps/opencs/model/tools/spellcheck.cpp +++ b/apps/opencs/model/tools/spellcheck.cpp @@ -5,14 +5,20 @@ #include +#include "../prefs/state.hpp" + #include "../world/universalid.hpp" CSMTools::SpellCheckStage::SpellCheckStage (const CSMWorld::IdCollection& spells) : mSpells (spells) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::SpellCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mSpells.getSize(); } @@ -20,7 +26,8 @@ void CSMTools::SpellCheckStage::perform (int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSpells.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; const ESM::Spell& spell = record.get(); diff --git a/apps/opencs/model/tools/spellcheck.hpp b/apps/opencs/model/tools/spellcheck.hpp index 9c3ea8885..03513adc3 100644 --- a/apps/opencs/model/tools/spellcheck.hpp +++ b/apps/opencs/model/tools/spellcheck.hpp @@ -13,6 +13,7 @@ namespace CSMTools class SpellCheckStage : public CSMDoc::Stage { const CSMWorld::IdCollection& mSpells; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/startscriptcheck.cpp b/apps/opencs/model/tools/startscriptcheck.cpp index 220751797..64c1c9209 100644 --- a/apps/opencs/model/tools/startscriptcheck.cpp +++ b/apps/opencs/model/tools/startscriptcheck.cpp @@ -1,18 +1,23 @@ #include "startscriptcheck.hpp" +#include "../prefs/state.hpp" + #include CSMTools::StartScriptCheckStage::StartScriptCheckStage ( const CSMWorld::IdCollection& startScripts, const CSMWorld::IdCollection& scripts) : mStartScripts (startScripts), mScripts (scripts) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} void CSMTools::StartScriptCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mStartScripts.getRecord (stage); - if (record.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) return; std::string scriptId = record.get().mId; @@ -26,5 +31,7 @@ void CSMTools::StartScriptCheckStage::perform(int stage, CSMDoc::Messages& messa int CSMTools::StartScriptCheckStage::setup() { + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mStartScripts.getSize(); } diff --git a/apps/opencs/model/tools/startscriptcheck.hpp b/apps/opencs/model/tools/startscriptcheck.hpp index cb82cbae7..a7d70ee5a 100644 --- a/apps/opencs/model/tools/startscriptcheck.hpp +++ b/apps/opencs/model/tools/startscriptcheck.hpp @@ -14,6 +14,7 @@ namespace CSMTools { const CSMWorld::IdCollection& mStartScripts; const CSMWorld::IdCollection& mScripts; + bool mIgnoreBaseRecords; public: diff --git a/apps/opencs/model/tools/topicinfocheck.cpp b/apps/opencs/model/tools/topicinfocheck.cpp index 05f02c763..69ebec96b 100644 --- a/apps/opencs/model/tools/topicinfocheck.cpp +++ b/apps/opencs/model/tools/topicinfocheck.cpp @@ -2,6 +2,8 @@ #include +#include "../prefs/state.hpp" + #include "../world/infoselectwrapper.hpp" CSMTools::TopicInfoCheckStage::TopicInfoCheckStage( @@ -29,7 +31,9 @@ CSMTools::TopicInfoCheckStage::TopicInfoCheckStage( mTopics(topics), mReferencables(referencables), mSoundFiles(soundFiles) -{} +{ + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); +} int CSMTools::TopicInfoCheckStage::setup() { @@ -67,6 +71,8 @@ int CSMTools::TopicInfoCheckStage::setup() } } + mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + return mTopicInfos.getSize(); } @@ -74,7 +80,8 @@ void CSMTools::TopicInfoCheckStage::perform(int stage, CSMDoc::Messages& message { const CSMWorld::Record& infoRecord = mTopicInfos.getRecord(stage); - if (infoRecord.isDeleted()) + // Skip "Base" records (setting!) and "Deleted" records + if ((mIgnoreBaseRecords && infoRecord.isBaseOnly()) || infoRecord.isDeleted()) return; const CSMWorld::Info& topicInfo = infoRecord.get(); diff --git a/apps/opencs/model/tools/topicinfocheck.hpp b/apps/opencs/model/tools/topicinfocheck.hpp index 510901dac..dbd5fe1c5 100644 --- a/apps/opencs/model/tools/topicinfocheck.hpp +++ b/apps/opencs/model/tools/topicinfocheck.hpp @@ -65,6 +65,8 @@ namespace CSMTools std::set mCellNames; + bool mIgnoreBaseRecords; + // These return false when not successful and write an error bool verifyActor(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); bool verifyCell(const std::string& name, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); diff --git a/apps/opencs/model/world/record.cpp b/apps/opencs/model/world/record.cpp index f13a36afc..3291b0462 100644 --- a/apps/opencs/model/world/record.cpp +++ b/apps/opencs/model/world/record.cpp @@ -2,6 +2,11 @@ CSMWorld::RecordBase::~RecordBase() {} +bool CSMWorld::RecordBase::isBaseOnly() const +{ + return mState == State_BaseOnly; +} + bool CSMWorld::RecordBase::isDeleted() const { return mState==State_Deleted || mState==State_Erased; diff --git a/apps/opencs/model/world/record.hpp b/apps/opencs/model/world/record.hpp index 3362f9f96..0468bf8e7 100644 --- a/apps/opencs/model/world/record.hpp +++ b/apps/opencs/model/world/record.hpp @@ -27,6 +27,8 @@ namespace CSMWorld virtual void assign (const RecordBase& record) = 0; ///< Will throw an exception if the types don't match. + bool isBaseOnly() const; + bool isDeleted() const; bool isErased() const; From c9756cee4cbb346cd2259471972038581fa71bbb Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 20 Jun 2018 12:37:58 +0400 Subject: [PATCH 211/282] Fast-forward death animation to end if death animation was finished earlier (regression #4468) --- apps/openmw/mwmechanics/character.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index fbdb19d5b..e2df546cd 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2026,10 +2026,11 @@ void CharacterController::update(float duration) { // initial start of death animation for actors that started the game as dead // not done in constructor since we need to give scripts a chance to set the mSkipAnim flag - if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty() && cls.isPersistent(mPtr)) + if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty()) { - // Fast-forward death animation to end for persisting corpses - playDeath(1.f, mDeathState); + // Fast-forward death animation to end for persisting corpses or corpses after end of death animation + if (cls.isPersistent(mPtr) || cls.getCreatureStats(mPtr).isDeathAnimationFinished()) + playDeath(1.f, mDeathState); } // We must always queue movement, even if there is none, to apply gravity. world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f)); From c0fc615cd6472c87854354089700a2719b42702b Mon Sep 17 00:00:00 2001 From: Atahualpa Date: Wed, 20 Jun 2018 11:29:38 +0200 Subject: [PATCH 212/282] Adds the option to ignore "Base" records when running the verifier. (fixes #4466) Improves previous commit: 1. Initialise mIgnoreBase boolean member with FALSE. 2. Remove isBaseOnly() function and replace with direct use of Record member. --- apps/opencs/model/tools/birthsigncheck.cpp | 4 +- apps/opencs/model/tools/bodypartcheck.cpp | 4 +- apps/opencs/model/tools/classcheck.cpp | 4 +- apps/opencs/model/tools/factioncheck.cpp | 4 +- apps/opencs/model/tools/gmstcheck.cpp | 4 +- apps/opencs/model/tools/journalcheck.cpp | 6 +-- apps/opencs/model/tools/magiceffectcheck.cpp | 4 +- apps/opencs/model/tools/pathgridcheck.cpp | 4 +- apps/opencs/model/tools/racecheck.cpp | 4 +- .../opencs/model/tools/referenceablecheck.cpp | 42 +++++++++---------- apps/opencs/model/tools/referencecheck.cpp | 4 +- apps/opencs/model/tools/regioncheck.cpp | 4 +- apps/opencs/model/tools/scriptcheck.cpp | 4 +- apps/opencs/model/tools/skillcheck.cpp | 4 +- apps/opencs/model/tools/soundcheck.cpp | 4 +- apps/opencs/model/tools/soundgencheck.cpp | 4 +- apps/opencs/model/tools/spellcheck.cpp | 4 +- apps/opencs/model/tools/startscriptcheck.cpp | 4 +- apps/opencs/model/tools/topicinfocheck.cpp | 4 +- apps/opencs/model/world/record.cpp | 7 +--- apps/opencs/model/world/record.hpp | 4 +- 21 files changed, 60 insertions(+), 67 deletions(-) diff --git a/apps/opencs/model/tools/birthsigncheck.cpp b/apps/opencs/model/tools/birthsigncheck.cpp index 5fe2479cd..fc2989307 100644 --- a/apps/opencs/model/tools/birthsigncheck.cpp +++ b/apps/opencs/model/tools/birthsigncheck.cpp @@ -12,7 +12,7 @@ CSMTools::BirthsignCheckStage::BirthsignCheckStage (const CSMWorld::IdCollection& birthsigns) : mBirthsigns (birthsigns) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::BirthsignCheckStage::setup() @@ -27,7 +27,7 @@ void CSMTools::BirthsignCheckStage::perform (int stage, CSMDoc::Messages& messag const CSMWorld::Record& record = mBirthsigns.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::BirthSign& birthsign = record.get(); diff --git a/apps/opencs/model/tools/bodypartcheck.cpp b/apps/opencs/model/tools/bodypartcheck.cpp index 8d19ef489..b5bd78f6c 100644 --- a/apps/opencs/model/tools/bodypartcheck.cpp +++ b/apps/opencs/model/tools/bodypartcheck.cpp @@ -10,7 +10,7 @@ CSMTools::BodyPartCheckStage::BodyPartCheckStage( mMeshes(meshes), mRaces(races) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::BodyPartCheckStage::setup() @@ -25,7 +25,7 @@ void CSMTools::BodyPartCheckStage::perform (int stage, CSMDoc::Messages &message const CSMWorld::Record &record = mBodyParts.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::BodyPart &bodyPart = record.get(); diff --git a/apps/opencs/model/tools/classcheck.cpp b/apps/opencs/model/tools/classcheck.cpp index 82007c860..89923a398 100644 --- a/apps/opencs/model/tools/classcheck.cpp +++ b/apps/opencs/model/tools/classcheck.cpp @@ -13,7 +13,7 @@ CSMTools::ClassCheckStage::ClassCheckStage (const CSMWorld::IdCollection& classes) : mClasses (classes) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::ClassCheckStage::setup() @@ -28,7 +28,7 @@ void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages) const CSMWorld::Record& record = mClasses.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Class& class_ = record.get(); diff --git a/apps/opencs/model/tools/factioncheck.cpp b/apps/opencs/model/tools/factioncheck.cpp index 2cf88fc0e..39073db5f 100644 --- a/apps/opencs/model/tools/factioncheck.cpp +++ b/apps/opencs/model/tools/factioncheck.cpp @@ -13,7 +13,7 @@ CSMTools::FactionCheckStage::FactionCheckStage (const CSMWorld::IdCollection& factions) : mFactions (factions) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::FactionCheckStage::setup() @@ -28,7 +28,7 @@ void CSMTools::FactionCheckStage::perform (int stage, CSMDoc::Messages& messages const CSMWorld::Record& record = mFactions.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Faction& faction = record.get(); diff --git a/apps/opencs/model/tools/gmstcheck.cpp b/apps/opencs/model/tools/gmstcheck.cpp index e3b4777bf..7cd13e5c2 100644 --- a/apps/opencs/model/tools/gmstcheck.cpp +++ b/apps/opencs/model/tools/gmstcheck.cpp @@ -9,7 +9,7 @@ CSMTools::GmstCheckStage::GmstCheckStage(const CSMWorld::IdCollection& gameSettings) : mGameSettings(gameSettings) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::GmstCheckStage::setup() @@ -24,7 +24,7 @@ void CSMTools::GmstCheckStage::perform(int stage, CSMDoc::Messages& messages) const CSMWorld::Record& record = mGameSettings.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::GameSetting& gmst = record.get(); diff --git a/apps/opencs/model/tools/journalcheck.cpp b/apps/opencs/model/tools/journalcheck.cpp index a565f3786..4a7ab7d66 100644 --- a/apps/opencs/model/tools/journalcheck.cpp +++ b/apps/opencs/model/tools/journalcheck.cpp @@ -9,7 +9,7 @@ CSMTools::JournalCheckStage::JournalCheckStage(const CSMWorld::IdCollection &journalRecord = mJournals.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && journalRecord.isBaseOnly()) || journalRecord.isDeleted()) + if ((mIgnoreBaseRecords && journalRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || journalRecord.isDeleted()) return; const ESM::Dialogue &journal = journalRecord.get(); @@ -51,7 +51,7 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) } // Skip "Base" records (setting!) - if (mIgnoreBaseRecords && infoRecord.isBaseOnly()) + if (mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) continue; if (journalInfo.mResponse.empty()) diff --git a/apps/opencs/model/tools/magiceffectcheck.cpp b/apps/opencs/model/tools/magiceffectcheck.cpp index f9e83aa23..531bd9e1d 100644 --- a/apps/opencs/model/tools/magiceffectcheck.cpp +++ b/apps/opencs/model/tools/magiceffectcheck.cpp @@ -80,7 +80,7 @@ CSMTools::MagicEffectCheckStage::MagicEffectCheckStage(const CSMWorld::IdCollect mIcons(icons), mTextures(textures) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::MagicEffectCheckStage::setup() @@ -95,7 +95,7 @@ void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages &messa const CSMWorld::Record &record = mMagicEffects.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; ESM::MagicEffect effect = record.get(); diff --git a/apps/opencs/model/tools/pathgridcheck.cpp b/apps/opencs/model/tools/pathgridcheck.cpp index 53293fa79..6427bb119 100644 --- a/apps/opencs/model/tools/pathgridcheck.cpp +++ b/apps/opencs/model/tools/pathgridcheck.cpp @@ -13,7 +13,7 @@ CSMTools::PathgridCheckStage::PathgridCheckStage (const CSMWorld::SubCellCollection& pathgrids) : mPathgrids (pathgrids) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::PathgridCheckStage::setup() @@ -28,7 +28,7 @@ void CSMTools::PathgridCheckStage::perform (int stage, CSMDoc::Messages& message const CSMWorld::Record& record = mPathgrids.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const CSMWorld::Pathgrid& pathgrid = record.get(); diff --git a/apps/opencs/model/tools/racecheck.cpp b/apps/opencs/model/tools/racecheck.cpp index c7fd83224..38abfef18 100644 --- a/apps/opencs/model/tools/racecheck.cpp +++ b/apps/opencs/model/tools/racecheck.cpp @@ -22,7 +22,7 @@ void CSMTools::RaceCheckStage::performPerRecord (int stage, CSMDoc::Messages& me mPlayable = true; // Skip "Base" records (setting!) - if (mIgnoreBaseRecords && record.isBaseOnly()) + if (mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) return; CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Race, race.mId); @@ -62,7 +62,7 @@ void CSMTools::RaceCheckStage::performFinal (CSMDoc::Messages& messages) CSMTools::RaceCheckStage::RaceCheckStage (const CSMWorld::IdCollection& races) : mRaces (races), mPlayable (false) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::RaceCheckStage::setup() diff --git a/apps/opencs/model/tools/referenceablecheck.cpp b/apps/opencs/model/tools/referenceablecheck.cpp index 0c9a32b5c..3e8dc1188 100644 --- a/apps/opencs/model/tools/referenceablecheck.cpp +++ b/apps/opencs/model/tools/referenceablecheck.cpp @@ -20,7 +20,7 @@ CSMTools::ReferenceableCheckStage::ReferenceableCheckStage( mScripts(scripts), mPlayerPresent(false) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } void CSMTools::ReferenceableCheckStage::perform (int stage, CSMDoc::Messages& messages) @@ -244,7 +244,7 @@ void CSMTools::ReferenceableCheckStage::bookCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Book& book = (dynamic_cast& >(baseRecord)).get(); @@ -264,7 +264,7 @@ void CSMTools::ReferenceableCheckStage::activatorCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Activator& activator = (dynamic_cast& >(baseRecord)).get(); @@ -286,7 +286,7 @@ void CSMTools::ReferenceableCheckStage::potionCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Potion& potion = (dynamic_cast& >(baseRecord)).get(); @@ -308,7 +308,7 @@ void CSMTools::ReferenceableCheckStage::apparatusCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Apparatus& apparatus = (dynamic_cast& >(baseRecord)).get(); @@ -330,7 +330,7 @@ void CSMTools::ReferenceableCheckStage::armorCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Armor& armor = (dynamic_cast& >(baseRecord)).get(); @@ -358,7 +358,7 @@ void CSMTools::ReferenceableCheckStage::clothingCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Clothing& clothing = (dynamic_cast& >(baseRecord)).get(); @@ -377,7 +377,7 @@ void CSMTools::ReferenceableCheckStage::containerCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Container& container = (dynamic_cast& >(baseRecord)).get(); @@ -410,7 +410,7 @@ void CSMTools::ReferenceableCheckStage::creatureCheck ( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Creature& creature = (dynamic_cast&>(baseRecord)).get(); @@ -487,7 +487,7 @@ void CSMTools::ReferenceableCheckStage::doorCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Door& door = (dynamic_cast&>(baseRecord)).get(); @@ -512,7 +512,7 @@ void CSMTools::ReferenceableCheckStage::ingredientCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Ingredient& ingredient = (dynamic_cast& >(baseRecord)).get(); @@ -532,7 +532,7 @@ void CSMTools::ReferenceableCheckStage::creaturesLevListCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::CreatureLevList& CreatureLevList = (dynamic_cast& >(baseRecord)).get(); @@ -549,7 +549,7 @@ void CSMTools::ReferenceableCheckStage::itemLevelledListCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::ItemLevList& ItemLevList = (dynamic_cast& >(baseRecord)).get(); @@ -565,7 +565,7 @@ void CSMTools::ReferenceableCheckStage::lightCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Light& light = (dynamic_cast& >(baseRecord)).get(); @@ -589,7 +589,7 @@ void CSMTools::ReferenceableCheckStage::lockpickCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Lockpick& lockpick = (dynamic_cast& >(baseRecord)).get(); @@ -611,7 +611,7 @@ void CSMTools::ReferenceableCheckStage::miscCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Miscellaneous& miscellaneous = (dynamic_cast& >(baseRecord)).get(); @@ -640,7 +640,7 @@ void CSMTools::ReferenceableCheckStage::npcCheck ( mPlayerPresent = true; // Skip "Base" records (setting!) - if (mIgnoreBaseRecords && baseRecord.isBaseOnly()) + if (mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) return; short level(npc.mNpdt.mLevel); @@ -749,7 +749,7 @@ void CSMTools::ReferenceableCheckStage::weaponCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Weapon& weapon = (dynamic_cast& >(baseRecord)).get(); @@ -830,7 +830,7 @@ void CSMTools::ReferenceableCheckStage::probeCheck( const CSMWorld::RecordBase& baseRecord = records.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Probe& probe = (dynamic_cast& >(baseRecord)).get(); @@ -850,7 +850,7 @@ void CSMTools::ReferenceableCheckStage::repairCheck ( const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Repair& repair = (dynamic_cast& >(baseRecord)).get(); @@ -870,7 +870,7 @@ void CSMTools::ReferenceableCheckStage::staticCheck ( const CSMWorld::RecordBase& baseRecord = records.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && baseRecord.isBaseOnly()) || baseRecord.isDeleted()) + if ((mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || baseRecord.isDeleted()) return; const ESM::Static& staticElement = (dynamic_cast& >(baseRecord)).get(); diff --git a/apps/opencs/model/tools/referencecheck.cpp b/apps/opencs/model/tools/referencecheck.cpp index 347a8a399..447238be4 100644 --- a/apps/opencs/model/tools/referencecheck.cpp +++ b/apps/opencs/model/tools/referencecheck.cpp @@ -14,7 +14,7 @@ CSMTools::ReferenceCheckStage::ReferenceCheckStage( mCells(cells), mFactions(factions) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &messages) @@ -22,7 +22,7 @@ void CSMTools::ReferenceCheckStage::perform(int stage, CSMDoc::Messages &message const CSMWorld::Record& record = mReferences.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const CSMWorld::CellRef& cellRef = record.get(); diff --git a/apps/opencs/model/tools/regioncheck.cpp b/apps/opencs/model/tools/regioncheck.cpp index 45b311414..f21253090 100644 --- a/apps/opencs/model/tools/regioncheck.cpp +++ b/apps/opencs/model/tools/regioncheck.cpp @@ -12,7 +12,7 @@ CSMTools::RegionCheckStage::RegionCheckStage (const CSMWorld::IdCollection& regions) : mRegions (regions) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::RegionCheckStage::setup() @@ -27,7 +27,7 @@ void CSMTools::RegionCheckStage::perform (int stage, CSMDoc::Messages& messages) const CSMWorld::Record& record = mRegions.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Region& region = record.get(); diff --git a/apps/opencs/model/tools/scriptcheck.cpp b/apps/opencs/model/tools/scriptcheck.cpp index bf5d48f65..d3c6221cd 100644 --- a/apps/opencs/model/tools/scriptcheck.cpp +++ b/apps/opencs/model/tools/scriptcheck.cpp @@ -61,7 +61,7 @@ CSMTools::ScriptCheckStage::ScriptCheckStage (const CSMDoc::Document& document) Compiler::registerExtensions (mExtensions); mContext.setExtensions (&mExtensions); - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::ScriptCheckStage::setup() @@ -96,7 +96,7 @@ void CSMTools::ScriptCheckStage::perform (int stage, CSMDoc::Messages& messages) return; // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; mMessages = &messages; diff --git a/apps/opencs/model/tools/skillcheck.cpp b/apps/opencs/model/tools/skillcheck.cpp index 2214ec11a..b34d18e2a 100644 --- a/apps/opencs/model/tools/skillcheck.cpp +++ b/apps/opencs/model/tools/skillcheck.cpp @@ -11,7 +11,7 @@ CSMTools::SkillCheckStage::SkillCheckStage (const CSMWorld::IdCollection& skills) : mSkills (skills) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::SkillCheckStage::setup() @@ -26,7 +26,7 @@ void CSMTools::SkillCheckStage::perform (int stage, CSMDoc::Messages& messages) const CSMWorld::Record& record = mSkills.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Skill& skill = record.get(); diff --git a/apps/opencs/model/tools/soundcheck.cpp b/apps/opencs/model/tools/soundcheck.cpp index b21341333..b84453b5c 100644 --- a/apps/opencs/model/tools/soundcheck.cpp +++ b/apps/opencs/model/tools/soundcheck.cpp @@ -11,7 +11,7 @@ CSMTools::SoundCheckStage::SoundCheckStage (const CSMWorld::IdCollection& sounds) : mSounds (sounds) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::SoundCheckStage::setup() @@ -26,7 +26,7 @@ void CSMTools::SoundCheckStage::perform (int stage, CSMDoc::Messages& messages) const CSMWorld::Record& record = mSounds.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Sound& sound = record.get(); diff --git a/apps/opencs/model/tools/soundgencheck.cpp b/apps/opencs/model/tools/soundgencheck.cpp index acd245016..3692259ce 100644 --- a/apps/opencs/model/tools/soundgencheck.cpp +++ b/apps/opencs/model/tools/soundgencheck.cpp @@ -14,7 +14,7 @@ CSMTools::SoundGenCheckStage::SoundGenCheckStage(const CSMWorld::IdCollection &record = mSoundGens.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::SoundGenerator& soundGen = record.get(); diff --git a/apps/opencs/model/tools/spellcheck.cpp b/apps/opencs/model/tools/spellcheck.cpp index 6fb38138b..3e59f0d9a 100644 --- a/apps/opencs/model/tools/spellcheck.cpp +++ b/apps/opencs/model/tools/spellcheck.cpp @@ -12,7 +12,7 @@ CSMTools::SpellCheckStage::SpellCheckStage (const CSMWorld::IdCollection& spells) : mSpells (spells) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::SpellCheckStage::setup() @@ -27,7 +27,7 @@ void CSMTools::SpellCheckStage::perform (int stage, CSMDoc::Messages& messages) const CSMWorld::Record& record = mSpells.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Spell& spell = record.get(); diff --git a/apps/opencs/model/tools/startscriptcheck.cpp b/apps/opencs/model/tools/startscriptcheck.cpp index 64c1c9209..b1d92380b 100644 --- a/apps/opencs/model/tools/startscriptcheck.cpp +++ b/apps/opencs/model/tools/startscriptcheck.cpp @@ -9,7 +9,7 @@ CSMTools::StartScriptCheckStage::StartScriptCheckStage ( const CSMWorld::IdCollection& scripts) : mStartScripts (startScripts), mScripts (scripts) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } void CSMTools::StartScriptCheckStage::perform(int stage, CSMDoc::Messages& messages) @@ -17,7 +17,7 @@ void CSMTools::StartScriptCheckStage::perform(int stage, CSMDoc::Messages& messa const CSMWorld::Record& record = mStartScripts.getRecord (stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && record.isBaseOnly()) || record.isDeleted()) + if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; std::string scriptId = record.get().mId; diff --git a/apps/opencs/model/tools/topicinfocheck.cpp b/apps/opencs/model/tools/topicinfocheck.cpp index 69ebec96b..ac1f596ae 100644 --- a/apps/opencs/model/tools/topicinfocheck.cpp +++ b/apps/opencs/model/tools/topicinfocheck.cpp @@ -32,7 +32,7 @@ CSMTools::TopicInfoCheckStage::TopicInfoCheckStage( mReferencables(referencables), mSoundFiles(soundFiles) { - mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); + mIgnoreBaseRecords = false; } int CSMTools::TopicInfoCheckStage::setup() @@ -81,7 +81,7 @@ void CSMTools::TopicInfoCheckStage::perform(int stage, CSMDoc::Messages& message const CSMWorld::Record& infoRecord = mTopicInfos.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records - if ((mIgnoreBaseRecords && infoRecord.isBaseOnly()) || infoRecord.isDeleted()) + if ((mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || infoRecord.isDeleted()) return; const CSMWorld::Info& topicInfo = infoRecord.get(); diff --git a/apps/opencs/model/world/record.cpp b/apps/opencs/model/world/record.cpp index 3291b0462..da1651f2b 100644 --- a/apps/opencs/model/world/record.cpp +++ b/apps/opencs/model/world/record.cpp @@ -2,11 +2,6 @@ CSMWorld::RecordBase::~RecordBase() {} -bool CSMWorld::RecordBase::isBaseOnly() const -{ - return mState == State_BaseOnly; -} - bool CSMWorld::RecordBase::isDeleted() const { return mState==State_Deleted || mState==State_Erased; @@ -22,4 +17,4 @@ bool CSMWorld::RecordBase::isErased() const bool CSMWorld::RecordBase::isModified() const { return mState==State_Modified || mState==State_ModifiedOnly; -} +} \ No newline at end of file diff --git a/apps/opencs/model/world/record.hpp b/apps/opencs/model/world/record.hpp index 0468bf8e7..0313f2e41 100644 --- a/apps/opencs/model/world/record.hpp +++ b/apps/opencs/model/world/record.hpp @@ -27,8 +27,6 @@ namespace CSMWorld virtual void assign (const RecordBase& record) = 0; ///< Will throw an exception if the types don't match. - bool isBaseOnly() const; - bool isDeleted() const; bool isErased() const; @@ -158,4 +156,4 @@ namespace CSMWorld } } -#endif +#endif \ No newline at end of file From c195144b17d1e9c55de0bc90eeff116b8c4292ba Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 20 Jun 2018 14:24:32 +0400 Subject: [PATCH 213/282] Take transformation from first node with given name in file (bug #4469) --- CHANGELOG.md | 1 + apps/openmw/mwrender/animation.cpp | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa680a09e..558a94cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Bug #4458: AiWander console command handles idle chances incorrectly Bug #4459: NotCell dialogue condition doesn't support partial matches Bug #4461: "Open" spell from non-player caster isn't a crime + Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal Feature #4256: Implement ToggleBorders (TB) console command Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index d96b9f809..0d740b2f6 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -100,7 +100,13 @@ namespace void apply(osg::MatrixTransform& trans) { - mMap[Misc::StringUtils::lowerCase(trans.getName())] = &trans; + // Take transformation for first found node in file + const std::string nodeName = Misc::StringUtils::lowerCase(trans.getName()); + if (mMap.find(nodeName) == mMap.end()) + { + mMap[nodeName] = &trans; + } + traverse(trans); } From 65ec58a669cc0eb511edd84bb30e82a24a13838c Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Wed, 20 Jun 2018 15:43:55 +0200 Subject: [PATCH 214/282] Move the changelog entry to 0.44.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6210f3f11..471df4f53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ Bug #4433: Guard behaviour is incorrect with Alarm = 0 Bug #4443: Goodbye option and dialogue choices are not mutually exclusive Feature #4444: Per-group KF-animation files support - Bug #4424: [macOS] Cursor is either empty or garbage when compiled against macOS 10.13 SDK 0.44.0 ------ @@ -109,6 +108,7 @@ Feature #4423: Rebalance soul gem values Task #4015: Use AppVeyor build artifact features to make continuous builds available Editor: New (and more complete) icon set + Bug #4424: [macOS] Cursor is either empty or garbage when compiled against macOS 10.13 SDK 0.43.0 ------ From 4e121a74347e570517ca226121c1aab129962f63 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 20 Jun 2018 16:17:47 +0200 Subject: [PATCH 215/282] updated credits file --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index b13953824..415b87b4e 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -173,6 +173,7 @@ Programmers Documentation ------------- + Adam Bowen (adamnbowen) Alejandro Sanchez (HiPhish) Bodillium Bret Curtis (psi29a) From 9cf815505bcad7203a966f95bb8a0059fee5e4f3 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Wed, 20 Jun 2018 16:21:23 +0200 Subject: [PATCH 216/282] formatting --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 471df4f53..789aea001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ Bug #4412: openmw-iniimporter ignores data paths from config Bug #4413: Moving with 0 strength uses all of your fatigue Bug #4420: Camera flickering when I open up and close menus while sneaking + Bug #4424: [macOS] Cursor is either empty or garbage when compiled against macOS 10.13 SDK Bug #4435: Item health is considered a signed integer Bug #4441: Adding items to currently disabled weapon-wielding creatures crashes the game Feature #1786: Round up encumbrance value in the encumbrance bar @@ -108,7 +109,6 @@ Feature #4423: Rebalance soul gem values Task #4015: Use AppVeyor build artifact features to make continuous builds available Editor: New (and more complete) icon set - Bug #4424: [macOS] Cursor is either empty or garbage when compiled against macOS 10.13 SDK 0.43.0 ------ From 90ccf5b42baedac1fd2bbd9a4265737063e959a3 Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Wed, 20 Jun 2018 19:59:55 +0200 Subject: [PATCH 217/282] [macOS, CI] Build using macOS 10.13 SDK --- .travis.yml | 2 +- CI/before_install.osx.sh | 2 +- CI/before_script.osx.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 02cc7cf20..7d2e6a795 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ os: - linux - osx -osx_image: xcode8.3 +osx_image: xcode9.4 language: cpp sudo: required dist: trusty diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 86e8fb81b..47f4021cf 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -6,5 +6,5 @@ brew outdated cmake || brew upgrade cmake brew outdated pkgconfig || brew upgrade pkgconfig brew install $macos_qt_formula -curl https://downloads.openmw.org/osx/dependencies/openmw-deps-c40905f.zip -o ~/openmw-deps.zip +curl https://downloads.openmw.org/osx/dependencies/openmw-deps-100d2e0.zip -o ~/openmw-deps.zip unzip ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index 806b545e3..2d1cf8729 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -11,7 +11,7 @@ cd build cmake \ -D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH" \ -D CMAKE_OSX_DEPLOYMENT_TARGET="10.9" \ --D CMAKE_OSX_SYSROOT="macosx10.12" \ +-D CMAKE_OSX_SYSROOT="macosx10.13" \ -D CMAKE_BUILD_TYPE=Release \ -D OPENMW_OSX_DEPLOYMENT=TRUE \ -D DESIRED_QT_VERSION=5 \ From 5a4817c147dd38ddd01082fb5c9c63d1ce1756fa Mon Sep 17 00:00:00 2001 From: Xenkhan <36826384+Xenkhan@users.noreply.github.com> Date: Wed, 20 Jun 2018 13:33:59 -0500 Subject: [PATCH 218/282] Get rid of reinterpret_cast<> --- apps/openmw/engine.cpp | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index a9f239914..78aaab5ea 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -379,16 +379,30 @@ void OMW::Engine::createWindow(Settings::Manager& settings) } setWindowIcon(); - + osg::ref_ptr traits = new osg::GraphicsContext::Traits; + int redSize; + int greenSize; + int blueSize; + int depthSize; + int stencilSize; + int doubleBuffer; + + SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &redSize); + SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &greenSize); + SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &blueSize); + SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &depthSize); + SDL_GL_GetAttribute(SDL_GL_STENCIL_SIZE, &stencilSize); + SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, &doubleBuffer); + SDL_GetWindowPosition(mWindow, &traits->x, &traits->y); SDL_GetWindowSize(mWindow, &traits->width, &traits->height); - SDL_GL_GetAttribute(SDL_GL_RED_SIZE, reinterpret_cast(&traits->red)); - SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, reinterpret_cast(&traits->green)); - SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, reinterpret_cast(&traits->blue)); - SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, reinterpret_cast(&traits->depth)); - SDL_GL_GetAttribute(SDL_GL_STENCIL_SIZE, reinterpret_cast(&traits->stencil)); - SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, reinterpret_cast(&traits->doubleBuffer)); + traits->red = redSize; + traits->green = greenSize; + traits->blue = blueSize; + traits->depth = depthSize; + traits->stencil = stencilSize; + traits->doubleBuffer = doubleBuffer; traits->windowName = SDL_GetWindowTitle(mWindow); traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); From 405a0caf29cda87449b37675772f05166c969f37 Mon Sep 17 00:00:00 2001 From: Xenkhan <36826384+Xenkhan@users.noreply.github.com> Date: Wed, 20 Jun 2018 13:56:00 -0500 Subject: [PATCH 219/282] Remove unneeded whitespace --- apps/openmw/engine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 78aaab5ea..c06a3cbd7 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -379,7 +379,7 @@ void OMW::Engine::createWindow(Settings::Manager& settings) } setWindowIcon(); - + osg::ref_ptr traits = new osg::GraphicsContext::Traits; int redSize; int greenSize; From d1736ad0c8f10cea05baec60bede218e4fd1f6b0 Mon Sep 17 00:00:00 2001 From: AnyOldName3 Date: Thu, 21 Jun 2018 14:23:30 +0000 Subject: [PATCH 220/282] Update .editorconfig to include GLSL --- .editorconfig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 25cc3fffc..307f5e58f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,4 +8,9 @@ insert_final_newline = true [*.hpp] indent_style = space indent_size = 4 -insert_final_newline = true \ No newline at end of file +insert_final_newline = true + +[*.glsl] +indent_style = space +indent_size = 4 +insert_final_newline = false \ No newline at end of file From f0acc645442819e1cc51d84eada83e1a6d3e40b0 Mon Sep 17 00:00:00 2001 From: Xenkhan <36826384+Xenkhan@users.noreply.github.com> Date: Thu, 21 Jun 2018 15:04:04 -0500 Subject: [PATCH 221/282] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc150f344..630948a0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Bug #4457: Item without CanCarry flag prevents shield autoequipping in dark areas Bug #4458: AiWander console command handles idle chances incorrectly Bug #4459: NotCell dialogue condition doesn't support partial matches + Bug #4471: Retrieve SDL window settings instead of using magic numbers Feature #4256: Implement ToggleBorders (TB) console command Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots From dcba3a105860bff0f6ab484502c7cb351327d51b Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Fri, 22 Jun 2018 15:13:09 +0000 Subject: [PATCH 223/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 78 +++++++++++++++++++++++++------------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9f5442ae4..3cfb429c1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,42 +1,42 @@ -# use the official gcc image, based on debian -# can use verions as well, like gcc:5.2 -# see https://hub.docker.com/_/gcc/ -image: gcc - -cache: - key: apt-cache - paths: - - apt-cache/ - -before_script: - - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR - - apt-get update -yq - - apt-get -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libunshield-dev libtinyxml-dev -# - apt-get install -y libmygui-dev libbullet-dev # to be updated to latest below because stretch is too old - - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb - - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb - - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb - - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb - - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb - -build: - stage: build - script: - - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi - - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ - - make -j$cores_to_use - - DESTDIR=artifacts make install - artifacts: - paths: - - build/artifacts/ - # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time +linux-stable: + image: gcc + cache: + key: apt-cache paths: - - "*.o" + - apt-cache/ + + before_script: + - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR + - apt-get update -yq + - apt-get -o dir::cache::archives="$APT_CACHE_DIR" install -y cmake libboost-filesystem-dev libboost-program-options-dev libboost-system-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libsdl2-dev libqt4-dev libopenal-dev libopenscenegraph-3.4-dev libunshield-dev libtinyxml-dev + # - apt-get install -y libmygui-dev libbullet-dev # to be updated to latest below because stretch is too old + - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet-dev_2.87+dfsg-2_amd64.deb -o libbullet-dev_2.87+dfsg-2_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/b/bullet/libbullet2.87_2.87+dfsg-2_amd64.deb -o libbullet2.87_2.87+dfsg-2_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb -o libmygui.openglplatform0debian1v5_3.2.2+dfsg-1_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb + - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb + - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb + + build: + stage: build + script: + - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi + - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ + - make -j$cores_to_use + - DESTDIR=artifacts make install + artifacts: + paths: + - build/artifacts/ +darwin: + build: + stage: build + script: + - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi + - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ + - make -j$cores_to_use + - DESTDIR=artifacts make install + artifacts: + paths: + - build/artifacts/ -# TODO: run tests using the binary built before -#test: -# stage: test -# script: -# - ls From 601b69b36c293a65eb4ca50494d5579656097a3b Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Fri, 22 Jun 2018 15:14:09 +0000 Subject: [PATCH 224/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3cfb429c1..7e156275f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,6 @@ +stages: + - build + linux-stable: image: gcc From 88555c04635165114e3f6f251663eeb4f0d29b45 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Fri, 22 Jun 2018 15:20:38 +0000 Subject: [PATCH 225/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7e156275f..80a6c4cdb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,25 +21,23 @@ linux-stable: - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb - build: - stage: build - script: - - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi - - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ - - make -j$cores_to_use - - DESTDIR=artifacts make install - artifacts: - paths: - - build/artifacts/ -darwin: - build: - stage: build - script: - - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi - - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ - - make -j$cores_to_use - - DESTDIR=artifacts make install - artifacts: - paths: - - build/artifacts/ + stage: build + script: + - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi + - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ + - make -j$cores_to_use + - DESTDIR=artifacts make install + artifacts: + paths: + - build/artifacts/ +darwin: + stage: build + script: + - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi + - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ + - make -j$cores_to_use + - DESTDIR=artifacts make install + artifacts: + paths: + - build/artifacts/ \ No newline at end of file From 393ec9994475ed75178d1bc6ddd3e9a90dfb0f2d Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Fri, 22 Jun 2018 15:29:27 +0000 Subject: [PATCH 226/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 80a6c4cdb..a87fcae0a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,10 @@ stages: - build linux-stable: + tags: + - debian + - linux + image: gcc cache: @@ -32,6 +36,9 @@ linux-stable: paths: - build/artifacts/ darwin: + tags: + - xcode_7-2 + - osx_10-11 stage: build script: - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi From c2826ca8786af19eedb0e521d1bc18f19816ad73 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Fri, 22 Jun 2018 16:41:20 +0000 Subject: [PATCH 227/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a87fcae0a..bf206df21 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: linux-stable: tags: - - debian + - docker - linux image: gcc @@ -37,8 +37,8 @@ linux-stable: - build/artifacts/ darwin: tags: - - xcode_7-2 - - osx_10-11 + - macos + - macos_10_12 stage: build script: - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi From 40a9d8ac06ff51a7e0900f121df29d5d8af9c59c Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sat, 23 Jun 2018 07:19:05 +0000 Subject: [PATCH 228/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bf206df21..807738c31 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,7 @@ linux-stable: darwin: tags: - macos - - macos_10_12 + - xcode stage: build script: - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi From da4c55d5adf756ca7cc49a4a80a214f7347105bb Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Sat, 23 Jun 2018 17:51:32 +1000 Subject: [PATCH 229/282] prevent segfalt in QuickKeysMenu when item has been removed from player inventory added a MWWorld::ContainerStore to hold item copies which are then used to find real items with findReplacement(). (storing the RefId could be a better solution but would probably leave tooltips broken...) --- apps/openmw/mwgui/quickkeysmenu.cpp | 147 ++++++++++++++-------------- apps/openmw/mwgui/quickkeysmenu.hpp | 3 + 2 files changed, 78 insertions(+), 72 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 08192625f..2271d2582 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -124,6 +124,13 @@ namespace MWGui void QuickKeysMenu::unassign(ItemWidget* key, int index) { + // cleanup refrance ItemContainer + if( mAssigned[index] == Type_Item || mAssigned[index] == Type_MagicItem) + { + MWWorld::Ptr refItem = *key->getUserData(); + mRefItemContainer.remove(refItem.getCellRef().getRefId(), 1, MWMechanics::getPlayer()); + } + key->clearUserStrings(); key->setItem(MWWorld::Ptr()); while (key->getChildCount()) // Destroy number label @@ -221,9 +228,11 @@ namespace MWGui mAssigned[mSelectedIndex] = Type_Item; - button->setItem(item, ItemWidget::Barter); + MWWorld::Ptr itemCopy = *mRefItemContainer.add(item, 1, MWMechanics::getPlayer()); + + button->setItem(itemCopy, ItemWidget::Barter); button->setUserString ("ToolTipType", "ItemPtr"); - button->setUserData(MWWorld::Ptr(item)); + button->setUserData(itemCopy); if (mItemSelectionDialog) mItemSelectionDialog->setVisible(false); @@ -334,29 +343,78 @@ namespace MWGui if (type == Type_Item || type == Type_MagicItem) { - MWWorld::Ptr item = *button->getUserData(); - // Make sure the item is available and is not broken - if (item.getRefData().getCount() < 1 || - (item.getClass().hasItemHealth(item) && - item.getClass().getItemHealth(item) <= 0)) + MWWorld::Ptr refItem = *button->getUserData(); + MWWorld::Ptr item = store.findReplacement(refItem.getCellRef().getRefId()); + + // check the item is available and not broken + if (!item || item.getRefData().getCount() < 1 || + (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) + { + if (!item || item.getRefData().getCount() < 1) + { + // item not in plater inventory found + MWBase::Environment::get().getWindowManager()->messageBox( + "#{sQuickMenu5} " + refItem.getClass().getName(refItem)); + + return; + } + } + + if (type == Type_Item) { - // Try searching for a compatible replacement - std::string id = item.getCellRef().getRefId(); + bool isWeapon = item.getTypeName() == typeid(ESM::Weapon).name(); + bool isTool = item.getTypeName() == typeid(ESM::Probe).name() || + item.getTypeName() == typeid(ESM::Lockpick).name(); - item = store.findReplacement(id); - button->setUserData(MWWorld::Ptr(item)); + // delay weapon switching if player is busy + if (isDelayNeeded && (isWeapon || isTool)) + { + mActivatedIndex = index; + return; + } - if (item.getRefData().getCount() < 1) + // disable weapon switching if player is dead or paralyzed + if (isReturnNeeded && (isWeapon || isTool)) { - // No replacement was found - MWBase::Environment::get().getWindowManager ()->messageBox ( - "#{sQuickMenu5} " + item.getClass().getName(item)); return; } + + MWBase::Environment::get().getWindowManager()->useItem(item); + MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); + // change draw state only if the item is in player's right hand + if (rightHand != store.end() && item == *rightHand) + { + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); + } } - } + else if (type == Type_MagicItem) + { + // retrieve ContainerStoreIterator to the item + MWWorld::ContainerStoreIterator it = store.begin(); + for (; it != store.end(); ++it) + { + if (*it == item) + { + break; + } + } + assert(it != store.end()); + + // equip, if it can be equipped + if (!item.getClass().getEquipmentSlots(item).first.empty()) + { + MWBase::Environment::get().getWindowManager()->useItem(item); + + // make sure that item was successfully equipped + if (!store.isEquipped(item)) + return; + } - if (type == Type_Magic) + store.setSelectedEnchantItem(it); + MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); + } + } + else if (type == Type_Magic) { std::string spellId = button->getUserString("Spell"); @@ -374,61 +432,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); } - else if (type == Type_Item) - { - MWWorld::Ptr item = *button->getUserData(); - bool isWeapon = item.getTypeName() == typeid(ESM::Weapon).name(); - bool isTool = item.getTypeName() == typeid(ESM::Probe).name() || item.getTypeName() == typeid(ESM::Lockpick).name(); - - // delay weapon switching if player is busy - if (isDelayNeeded && (isWeapon || isTool)) - { - mActivatedIndex = index; - return; - } - - // disable weapon switching if player is dead or paralyzed - if (isReturnNeeded && (isWeapon || isTool)) - { - return; - } - - MWBase::Environment::get().getWindowManager()->useItem(item); - MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); - // change draw state only if the item is in player's right hand - if (rightHand != store.end() && item == *rightHand) - { - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); - } - } - else if (type == Type_MagicItem) - { - MWWorld::Ptr item = *button->getUserData(); - - // retrieve ContainerStoreIterator to the item - MWWorld::ContainerStoreIterator it = store.begin(); - for (; it != store.end(); ++it) - { - if (*it == item) - { - break; - } - } - assert(it != store.end()); - - // equip, if it can be equipped - if (!item.getClass().getEquipmentSlots(item).first.empty()) - { - MWBase::Environment::get().getWindowManager()->useItem(item); - - // make sure that item was successfully equipped - if (!store.isEquipped(item)) - return; - } - - store.setSelectedEnchantItem(it); - MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); - } else if (type == Type_HandToHand) { store.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, player); diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index b5bc60b19..5e2305df8 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -2,6 +2,7 @@ #define MWGUI_QUICKKEYS_H #include "../mwworld/ptr.hpp" +#include "../mwworld/containerstore.hpp" #include "windowbase.hpp" @@ -58,6 +59,8 @@ namespace MWGui MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; + MWWorld::ContainerStore mRefItemContainer; + std::vector mQuickKeyButtons; std::vector mAssigned; From 35b0546737f3dfa7f08bd3202a054648da0f35ae Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Sat, 23 Jun 2018 14:21:21 +0300 Subject: [PATCH 230/282] Consider

tag when discarding post-EOL tag text (regression #4473) --- apps/openmw/mwgui/formatting.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index 16568e2f3..edcb94eed 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -31,13 +31,16 @@ namespace MWGui boost::algorithm::replace_all(mText, "\r", ""); - // vanilla game does not show any text after last
tag. + // vanilla game does not show any text after the last EOL tag. const std::string lowerText = Misc::StringUtils::lowerCase(mText); - int index = lowerText.rfind("
"); - if (index == -1) + int brIndex = lowerText.rfind("
"); + int pIndex = lowerText.rfind("

"); + if (brIndex == pIndex) mText = ""; + else if (brIndex > pIndex) + mText = mText.substr(0, brIndex+4); else - mText = mText.substr(0, index+4); + mText = mText.substr(0, pIndex+3); registerTag("br", Event_BrTag); registerTag("p", Event_PTag); From afbe8f21616531f9664b9963f18f2c2735bde919 Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Sat, 23 Jun 2018 14:29:15 +0300 Subject: [PATCH 231/282] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 782a817e8..aef35f5a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ Bug #4036: Weird behaviour of AI packages if package target has non-unique ID Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully Bug #4125: OpenMW logo cropped on bugtracker - Bug #4215: OpenMW shows book text after last
tag + Bug #4215: OpenMW shows book text after last EOL tag Bug #4221: Characters get stuck in V-shaped terrain Bug #4251: Stationary NPCs do not return to their position after combat Bug #4286: Scripted animations can be interrupted From 55de1c1a721edf3c14dd8ff836c336292585ec45 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sat, 23 Jun 2018 13:22:20 +0000 Subject: [PATCH 232/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 807738c31..3ae419c1d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ stages: - build -linux-stable: +Debian: tags: - docker - linux @@ -35,11 +35,12 @@ linux-stable: artifacts: paths: - build/artifacts/ -darwin: +MacOS: tags: - macos - xcode stage: build + allow_failure: true script: - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ From f1158e8129d947117acee2fac4f906c0faf37eda Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sat, 23 Jun 2018 13:50:58 +0000 Subject: [PATCH 233/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3ae419c1d..690ef8ef3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,10 +41,10 @@ MacOS: - xcode stage: build allow_failure: true - script: - - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi - - mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../ - - make -j$cores_to_use + script: + - CI/before_install.osx.sh + - CI/before_script.osx.sh + - make -j2 - DESTDIR=artifacts make install artifacts: paths: From ecafcefae9efaab61d21f429f10be598ec178e21 Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Sun, 24 Jun 2018 14:39:48 +0300 Subject: [PATCH 234/282] Fall back to regular head when getVampireHead fails --- apps/openmw/mwrender/npcanimation.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 08e376f08..baf6cb822 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -416,6 +416,7 @@ void NpcAnimation::updateNpcBase() const ESM::Race *race = store.get().find(mNpc->mRace); bool isWerewolf = (mNpcType == Type_Werewolf); bool isVampire = (mNpcType == Type_Vampire); + bool isFemale = !mNpc->isMale(); if (isWerewolf) { @@ -425,8 +426,9 @@ void NpcAnimation::updateNpcBase() else { mHeadModel = ""; - if (isVampire) // FIXME: fall back to regular head when getVampireHead fails? - mHeadModel = getVampireHead(mNpc->mRace, mNpc->mFlags & ESM::NPC::Female); + const std::string& vampireHead = getVampireHead(mNpc->mRace, isFemale); + if (isVampire && !vampireHead.empty()) + mHeadModel = vampireHead; else if (!mNpc->mHead.empty()) { const ESM::BodyPart* bp = store.get().search(mNpc->mHead); @@ -448,7 +450,6 @@ void NpcAnimation::updateNpcBase() } bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; - bool isFemale = !mNpc->isMale(); std::string smodel; if (mViewMode != VM_FirstPerson) From d6a170f8483fd7cecc71b8cf12fb69207d1e457a Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Sun, 24 Jun 2018 14:58:54 +0300 Subject: [PATCH 235/282] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 782a817e8..7276593a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Bug #4461: "Open" spell from non-player caster isn't a crime Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal Bug #4471: Retrieve SDL window settings instead of using magic numbers + Bug #4474: No fallback when getVampireHead fails Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots Feature #4256: Implement ToggleBorders (TB) console command From 87f367ec117db627c8fc6d2f72861151a356ff96 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sun, 24 Jun 2018 17:52:57 +0000 Subject: [PATCH 236/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 690ef8ef3..fc00ba728 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,14 +5,11 @@ Debian: tags: - docker - linux - image: gcc - cache: key: apt-cache paths: - apt-cache/ - before_script: - export APT_CACHE_DIR=`pwd`/apt-cache && mkdir -pv $APT_CACHE_DIR - apt-get update -yq @@ -24,8 +21,6 @@ Debian: - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb -o libmyguiengine3debian1v5_3.2.2+dfsg-1_amd64.deb - curl http://ftp.us.debian.org/debian/pool/main/m/mygui/libmygui-dev_3.2.2+dfsg-1_amd64.deb -o libmygui-dev_3.2.2+dfsg-1_amd64.deb - dpkg --ignore-depends=libmygui.ogreplatform0debian1v5 -i *.deb - - stage: build script: - cores_to_use=$((`nproc`-2)); if (( $cores_to_use < 1 )); then cores_to_use=1; fi @@ -42,6 +37,7 @@ MacOS: stage: build allow_failure: true script: + - macos_qt_formula=qt - CI/before_install.osx.sh - CI/before_script.osx.sh - make -j2 From d008cd0c46c1ff8e78164aafa0ae883862897b17 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Sun, 24 Jun 2018 21:26:53 +0000 Subject: [PATCH 237/282] Update .gitlab-ci.yml --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fc00ba728..83a6ab63f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,8 +37,8 @@ MacOS: stage: build allow_failure: true script: - - macos_qt_formula=qt - - CI/before_install.osx.sh + #- macos_qt_formula=qt + #- CI/before_install.osx.sh - CI/before_script.osx.sh - make -j2 - DESTDIR=artifacts make install From 359e748c28671700cd6ed98eef096928f54f9491 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 25 Jun 2018 09:35:42 +0400 Subject: [PATCH 238/282] Initialize some missing fields in constructors --- apps/openmw/mwsound/openal_output.cpp | 2 +- components/esm/loadland.hpp | 7 ++++++- components/esm/loadpgrd.hpp | 5 ++++- components/esm/objectstate.hpp | 4 +++- components/esmterrain/storage.cpp | 2 ++ components/terrain/quadtreenode.cpp | 1 + components/widgets/windowcaption.cpp | 1 + 7 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwsound/openal_output.cpp b/apps/openmw/mwsound/openal_output.cpp index 31d46ce31..f4587130e 100644 --- a/apps/openmw/mwsound/openal_output.cpp +++ b/apps/openmw/mwsound/openal_output.cpp @@ -387,7 +387,7 @@ private: OpenAL_SoundStream::OpenAL_SoundStream(ALuint src, DecoderPtr decoder) : mSource(src), mCurrentBufIdx(0), mFormat(AL_NONE), mSampleRate(0) , mBufferSize(0), mFrameSize(0), mSilence(0), mDecoder(std::move(decoder)) - , mLoudnessAnalyzer(nullptr) + , mLoudnessAnalyzer(nullptr), mIsFinished(true) { mBuffers.fill(0); } diff --git a/components/esm/loadland.hpp b/components/esm/loadland.hpp index 2163c30fc..cccb472de 100644 --- a/components/esm/loadland.hpp +++ b/components/esm/loadland.hpp @@ -83,7 +83,12 @@ struct Land struct LandData { LandData() - : mDataLoaded(0) + : mHeightOffset(0) + , mMinHeight(0) + , mMaxHeight(0) + , mUnk1(0) + , mUnk2(0) + , mDataLoaded(0) { } diff --git a/components/esm/loadpgrd.hpp b/components/esm/loadpgrd.hpp index d1003eb86..4e74c9a24 100644 --- a/components/esm/loadpgrd.hpp +++ b/components/esm/loadpgrd.hpp @@ -36,7 +36,10 @@ struct Pathgrid Point& operator=(const float[3]); Point(const float[3]); Point(); - Point(int x, int y, int z) : mX(x), mY(y), mZ(z) {} + Point(int x, int y, int z) + : mX(x), mY(y), mZ(z) + , mAutogenerated(0), mConnectionNum(0), mUnknown(0) + {} }; // 16 bytes struct Edge // path grid edge diff --git a/components/esm/objectstate.hpp b/components/esm/objectstate.hpp index b8eb138eb..d14c04b64 100644 --- a/components/esm/objectstate.hpp +++ b/components/esm/objectstate.hpp @@ -34,7 +34,9 @@ namespace ESM ESM::AnimationState mAnimationState; - ObjectState() : mHasCustomState(true), mVersion(0) + ObjectState() + : mHasLocals(0), mEnabled(0), mCount(0) + , mFlags(0), mHasCustomState(true), mVersion(0) {} /// @note Does not load the CellRef ID, it should already be loaded before calling this method diff --git a/components/esmterrain/storage.cpp b/components/esmterrain/storage.cpp index 52850fd74..ff3123c5b 100644 --- a/components/esmterrain/storage.cpp +++ b/components/esmterrain/storage.cpp @@ -24,6 +24,8 @@ namespace ESMTerrain }; LandObject::LandObject() + : mLand(nullptr) + , mLoadFlags(0) { } diff --git a/components/terrain/quadtreenode.cpp b/components/terrain/quadtreenode.cpp index 0394adea7..f4fc8df89 100644 --- a/components/terrain/quadtreenode.cpp +++ b/components/terrain/quadtreenode.cpp @@ -61,6 +61,7 @@ QuadTreeNode::QuadTreeNode(QuadTreeNode* parent, ChildDirection direction, float , mValidBounds(false) , mSize(size) , mCenter(center) + , mViewDataMap(nullptr) { for (unsigned int i=0; i<4; ++i) mNeighbours[i] = 0; diff --git a/components/widgets/windowcaption.cpp b/components/widgets/windowcaption.cpp index bcb0a7c12..1c8fb5608 100644 --- a/components/widgets/windowcaption.cpp +++ b/components/widgets/windowcaption.cpp @@ -8,6 +8,7 @@ namespace Gui WindowCaption::WindowCaption() : mLeft(NULL) , mRight(NULL) + , mClient(NULL) { } From 441463327c23d0a49961e403f29e81ebb19b2ee1 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 25 Jun 2018 09:57:40 +0400 Subject: [PATCH 239/282] Validate map size --- apps/openmw/mwgui/hud.cpp | 4 +++- apps/openmw/mwgui/mapwindow.cpp | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/hud.cpp b/apps/openmw/mwgui/hud.cpp index 23eb499de..1841303f2 100644 --- a/apps/openmw/mwgui/hud.cpp +++ b/apps/openmw/mwgui/hud.cpp @@ -158,7 +158,9 @@ namespace MWGui getWidget(mCrosshair, "Crosshair"); - LocalMapBase::init(mMinimap, mCompass, Settings::Manager::getInt("local map hud widget size", "Map"), Settings::Manager::getInt("local map cell distance", "Map")); + int mapSize = std::max(1, Settings::Manager::getInt("local map hud widget size", "Map")); + int cellDistance = std::max(1, Settings::Manager::getInt("local map cell distance", "Map")); + LocalMapBase::init(mMinimap, mCompass, mapSize, cellDistance); mMainWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onWorldClicked); mMainWidget->eventMouseMove += MyGUI::newDelegate(this, &HUD::onWorldMouseOver); diff --git a/apps/openmw/mwgui/mapwindow.cpp b/apps/openmw/mwgui/mapwindow.cpp index d3c1ec292..c1ff9510d 100644 --- a/apps/openmw/mwgui/mapwindow.cpp +++ b/apps/openmw/mwgui/mapwindow.cpp @@ -679,7 +679,9 @@ namespace MWGui mEventBoxLocal->eventMouseButtonPressed += MyGUI::newDelegate(this, &MapWindow::onDragStart); mEventBoxLocal->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &MapWindow::onMapDoubleClicked); - LocalMapBase::init(mLocalMap, mPlayerArrowLocal, Settings::Manager::getInt("local map widget size", "Map"), Settings::Manager::getInt("local map cell distance", "Map")); + int mapSize = std::max(1, Settings::Manager::getInt("local map widget size", "Map")); + int cellDistance = std::max(1, Settings::Manager::getInt("local map cell distance", "Map")); + LocalMapBase::init(mLocalMap, mPlayerArrowLocal, mapSize, cellDistance); mGlobalMap->setVisible(mGlobal); mLocalMap->setVisible(!mGlobal); From 46c6abcf54f8e969e784089c0bd8ec99e9e58afa Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Mon, 25 Jun 2018 16:02:28 +1000 Subject: [PATCH 240/282] add string vectors for name/id in QuickKeysMenu for item lookups --- apps/openmw/mwgui/quickkeysmenu.cpp | 41 ++++++++++++++++------------- apps/openmw/mwgui/quickkeysmenu.hpp | 2 ++ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 2271d2582..7d3e2cbc7 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -60,6 +60,9 @@ namespace MWGui mAssigned.push_back(Type_Unassigned); + mAssignedId.push_back(std::string("")); + mAssignedName.push_back(std::string("")); + unassign(button, i); } } @@ -131,6 +134,9 @@ namespace MWGui mRefItemContainer.remove(refItem.getCellRef().getRefId(), 1, MWMechanics::getPlayer()); } + mAssignedName[index] = ""; + mAssignedId[index] = ""; + key->clearUserStrings(); key->setItem(MWWorld::Ptr()); while (key->getChildCount()) // Destroy number label @@ -227,12 +233,14 @@ namespace MWGui MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); mAssigned[mSelectedIndex] = Type_Item; + mAssignedId[mSelectedIndex] = item.getCellRef().getRefId(); + mAssignedName[mSelectedIndex] = item.getClass().getName(item); - MWWorld::Ptr itemCopy = *mRefItemContainer.add(item, 1, MWMechanics::getPlayer()); + MWWorld::Ptr refItem = *mRefItemContainer.add(item, 1, MWMechanics::getPlayer()); - button->setItem(itemCopy, ItemWidget::Barter); + button->setItem(refItem, ItemWidget::Barter); button->setUserString ("ToolTipType", "ItemPtr"); - button->setUserData(itemCopy); + button->setUserData(item); if (mItemSelectionDialog) mItemSelectionDialog->setVisible(false); @@ -343,18 +351,26 @@ namespace MWGui if (type == Type_Item || type == Type_MagicItem) { - MWWorld::Ptr refItem = *button->getUserData(); - MWWorld::Ptr item = store.findReplacement(refItem.getCellRef().getRefId()); + MWWorld::Ptr item = *button->getUserData(); + + MWWorld::ContainerStoreIterator it = store.begin(); + for (; it != store.end(); ++it) + { + if (*it == item) + break; + } + if (it == store.end()) + item = NULL; // check the item is available and not broken if (!item || item.getRefData().getCount() < 1 || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { + item = store.findReplacement(mAssignedId[index-1]); if (!item || item.getRefData().getCount() < 1) { - // item not in plater inventory found MWBase::Environment::get().getWindowManager()->messageBox( - "#{sQuickMenu5} " + refItem.getClass().getName(refItem)); + "#{sQuickMenu5} " + mAssignedName[index-1]); return; } @@ -389,17 +405,6 @@ namespace MWGui } else if (type == Type_MagicItem) { - // retrieve ContainerStoreIterator to the item - MWWorld::ContainerStoreIterator it = store.begin(); - for (; it != store.end(); ++it) - { - if (*it == item) - { - break; - } - } - assert(it != store.end()); - // equip, if it can be equipped if (!item.getClass().getEquipmentSlots(item).first.empty()) { diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index 5e2305df8..8df0ae239 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -60,6 +60,8 @@ namespace MWGui MyGUI::Button* mOkButton; MWWorld::ContainerStore mRefItemContainer; + std::vector mAssignedId; + std::vector mAssignedName; std::vector mQuickKeyButtons; std::vector mAssigned; From 7dff8d8fe28c6d3d95f9d3c90070ad757c7c84b3 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 25 Jun 2018 10:26:58 +0400 Subject: [PATCH 241/282] Check cell for null --- apps/openmw/mwworld/worldimp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 83d27f6d8..9de4da1ee 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1179,8 +1179,8 @@ namespace MWWorld } else { - bool currCellActive = mWorldScene->isCellActive(*currCell); - bool newCellActive = mWorldScene->isCellActive(*newCell); + bool currCellActive = currCell && mWorldScene->isCellActive(*currCell); + bool newCellActive = newCell && mWorldScene->isCellActive(*newCell); if (!currCellActive && newCellActive) { newPtr = currCell->moveTo(ptr, newCell); From 97d8cc0efe4df7d618d31b666e3d97d555ffa9be Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 25 Jun 2018 11:21:00 +0400 Subject: [PATCH 242/282] Check if the local was not found, just for sure --- apps/openmw/mwscript/locals.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwscript/locals.cpp b/apps/openmw/mwscript/locals.cpp index 9dd9d338e..64f3058eb 100644 --- a/apps/openmw/mwscript/locals.cpp +++ b/apps/openmw/mwscript/locals.cpp @@ -240,6 +240,10 @@ namespace MWScript char type = declarations.getType (iter->first); int index2 = declarations.getIndex (iter->first); + // silently ignore locals that don't exist anymore + if (type == ' ' || index2 == -1) + continue; + try { switch (type) @@ -247,8 +251,6 @@ namespace MWScript case 's': mShorts.at (index2) = iter->second.getInteger(); break; case 'l': mLongs.at (index2) = iter->second.getInteger(); break; case 'f': mFloats.at (index2) = iter->second.getFloat(); break; - - // silently ignore locals that don't exist anymore } } catch (...) From 186ec8c50fa205ae731031934589091d3f96c3b0 Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Tue, 26 Jun 2018 13:35:04 +1000 Subject: [PATCH 243/282] rm ContainerStore/refItem --- apps/openmw/mwgui/quickkeysmenu.cpp | 11 +---------- apps/openmw/mwgui/quickkeysmenu.hpp | 6 ++---- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 7d3e2cbc7..e19df2bbb 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -127,13 +127,6 @@ namespace MWGui void QuickKeysMenu::unassign(ItemWidget* key, int index) { - // cleanup refrance ItemContainer - if( mAssigned[index] == Type_Item || mAssigned[index] == Type_MagicItem) - { - MWWorld::Ptr refItem = *key->getUserData(); - mRefItemContainer.remove(refItem.getCellRef().getRefId(), 1, MWMechanics::getPlayer()); - } - mAssignedName[index] = ""; mAssignedId[index] = ""; @@ -236,9 +229,7 @@ namespace MWGui mAssignedId[mSelectedIndex] = item.getCellRef().getRefId(); mAssignedName[mSelectedIndex] = item.getClass().getName(item); - MWWorld::Ptr refItem = *mRefItemContainer.add(item, 1, MWMechanics::getPlayer()); - - button->setItem(refItem, ItemWidget::Barter); + button->setItem(item, ItemWidget::Barter); button->setUserString ("ToolTipType", "ItemPtr"); button->setUserData(item); diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index 8df0ae239..29506ab58 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -59,12 +59,10 @@ namespace MWGui MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; - MWWorld::ContainerStore mRefItemContainer; - std::vector mAssignedId; - std::vector mAssignedName; - std::vector mQuickKeyButtons; std::vector mAssigned; + std::vector mAssignedId; + std::vector mAssignedName; QuickKeysMenuAssign* mAssignDialog; ItemSelectionDialog* mItemSelectionDialog; From 43c9fd4cec3e9c060ca524a048bf63ba8f0f1021 Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Tue, 26 Jun 2018 13:41:53 +1000 Subject: [PATCH 244/282] check MWWorld::Ptr != NULL for MWGui ItemPtr tooltips --- apps/openmw/mwgui/tooltips.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/openmw/mwgui/tooltips.cpp b/apps/openmw/mwgui/tooltips.cpp index b2991a034..20814aac5 100644 --- a/apps/openmw/mwgui/tooltips.cpp +++ b/apps/openmw/mwgui/tooltips.cpp @@ -191,6 +191,9 @@ namespace MWGui else if (type == "ItemPtr") { mFocusObject = *focus->getUserData(); + if (!mFocusObject) + return; + tooltipSize = getToolTipViaPtr(mFocusObject.getRefData().getCount(), false, checkOwned()); } else if (type == "ItemModelIndex") From ae4cb0c3ee279ce05aaeba6d5c9a827d0c02a0b3 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 13:50:45 +0000 Subject: [PATCH 245/282] no need for macos_qt_formula since on osx there is no more qt4, just qt5 --- .travis.yml | 1 - CI/before_install.osx.sh | 2 +- CI/before_script.osx.sh | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7d2e6a795..c6facef2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,6 @@ env: # The next declaration is the encrypted COVERITY_SCAN_TOKEN, created # via the "travis encrypt" command using the project repo's public key - secure: "jybGzAdUbqt9vWR/GEnRd96BgAi/7Zd1+2HK68j/i/8+/1YH2XxLOy4Jv/DUBhBlJIkxs/Xv8dRcUlFOclZDHX1d/9Qnsqd3oUVkD7k1y7cTOWy9TBQaE/v/kZo3LpzA3xPwwthrb0BvqIbOfIELi5fS5s8ba85WFRg3AX70wWE=" - - macos_qt_formula=qt addons: apt: sources: diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 47f4021cf..48e2ef6ba 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -4,7 +4,7 @@ brew update brew outdated cmake || brew upgrade cmake brew outdated pkgconfig || brew upgrade pkgconfig -brew install $macos_qt_formula +brew install qt curl https://downloads.openmw.org/osx/dependencies/openmw-deps-100d2e0.zip -o ~/openmw-deps.zip unzip ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null diff --git a/CI/before_script.osx.sh b/CI/before_script.osx.sh index 2d1cf8729..8ee01b652 100755 --- a/CI/before_script.osx.sh +++ b/CI/before_script.osx.sh @@ -4,7 +4,7 @@ export CXX=clang++ export CC=clang DEPENDENCIES_ROOT="/private/tmp/openmw-deps/openmw-deps" -QT_PATH=`brew --prefix $macos_qt_formula` +QT_PATH=`brew --prefix qt` mkdir build cd build From 308d78e3dafab71ee8ba173e8ed7fb35a7a41e2f Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 13:54:37 +0000 Subject: [PATCH 246/282] only download via curl if file on server is different than what is currently cached on VM. --- .gitlab-ci.yml | 2 +- CI/before_install.osx.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 83a6ab63f..97070136f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -38,7 +38,7 @@ MacOS: allow_failure: true script: #- macos_qt_formula=qt - #- CI/before_install.osx.sh + - CI/before_install.osx.sh - CI/before_script.osx.sh - make -j2 - DESTDIR=artifacts make install diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index 48e2ef6ba..c4793d1cf 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -6,5 +6,5 @@ brew outdated cmake || brew upgrade cmake brew outdated pkgconfig || brew upgrade pkgconfig brew install qt -curl https://downloads.openmw.org/osx/dependencies/openmw-deps-100d2e0.zip -o ~/openmw-deps.zip +curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-100d2e0.zip -o ~/openmw-deps.zip unzip ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null From 9e2c6d9fe1cb142f9e72f1f2b9a800d44e98f8ce Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 13:57:13 +0000 Subject: [PATCH 247/282] actually step into the build directory and build! --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 97070136f..8d3ebccbe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,7 +40,7 @@ MacOS: #- macos_qt_formula=qt - CI/before_install.osx.sh - CI/before_script.osx.sh - - make -j2 + - cd build; make -j2 - DESTDIR=artifacts make install artifacts: paths: From e068ee353372676279cf65708e861d5733007f44 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 14:01:10 +0000 Subject: [PATCH 248/282] tell unzip to overwrite all files --- CI/before_install.osx.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CI/before_install.osx.sh b/CI/before_install.osx.sh index c4793d1cf..2ab996b10 100755 --- a/CI/before_install.osx.sh +++ b/CI/before_install.osx.sh @@ -7,4 +7,4 @@ brew outdated pkgconfig || brew upgrade pkgconfig brew install qt curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-100d2e0.zip -o ~/openmw-deps.zip -unzip ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null +unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null From 06eb9539bd219a41664e90bd7d5c95103eed0c04 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 14:51:55 +0000 Subject: [PATCH 249/282] Let make install do it's thing, it's more reasonable on macos and let's only archive that. --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8d3ebccbe..9ca719efe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,7 +41,7 @@ MacOS: - CI/before_install.osx.sh - CI/before_script.osx.sh - cd build; make -j2 - - DESTDIR=artifacts make install + - make install artifacts: paths: - - build/artifacts/ \ No newline at end of file + - /usr/local/OpenMW.app \ No newline at end of file From c1aee49b763f51657ae742967f42ae13fddf323b Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 15:09:51 +0000 Subject: [PATCH 250/282] clean up after our previous build. --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9ca719efe..986e2d2aa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,11 +37,11 @@ MacOS: stage: build allow_failure: true script: - #- macos_qt_formula=qt + - sudo rm -fr /usr/local/Openmw.app # clean up after previous build - CI/before_install.osx.sh - CI/before_script.osx.sh - cd build; make -j2 - - make install + - sudo make install artifacts: paths: - /usr/local/OpenMW.app \ No newline at end of file From dd60b8f1791edb309584378e63a71238902aef72 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 15:18:18 +0000 Subject: [PATCH 251/282] only delete files inside OpenMW.app dir --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 986e2d2aa..6ffe17a70 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,11 +37,11 @@ MacOS: stage: build allow_failure: true script: - - sudo rm -fr /usr/local/Openmw.app # clean up after previous build + - rm -fr /usr/local/Openmw.app/* # clean up after previous build - CI/before_install.osx.sh - CI/before_script.osx.sh - cd build; make -j2 - - sudo make install + - make install artifacts: paths: - /usr/local/OpenMW.app \ No newline at end of file From cfcd9c3fc63e384a919add2c136294fe63577c90 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 15:36:53 +0000 Subject: [PATCH 252/282] What happens when we don't make install? --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6ffe17a70..14348f5c2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,7 +41,7 @@ MacOS: - CI/before_install.osx.sh - CI/before_script.osx.sh - cd build; make -j2 - - make install + # - make install artifacts: paths: - /usr/local/OpenMW.app \ No newline at end of file From 9e7731259dc10e7e5d477c0950cd61698fa5847a Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 15:40:06 +0000 Subject: [PATCH 253/282] what if we try make -j2 package ? --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 14348f5c2..1d7502bb8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -40,7 +40,7 @@ MacOS: - rm -fr /usr/local/Openmw.app/* # clean up after previous build - CI/before_install.osx.sh - CI/before_script.osx.sh - - cd build; make -j2 + - cd build; make -j2 package # - make install artifacts: paths: From 4d6ca2c387c1e41ee8f226f33fe2eabfb2b03706 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Tue, 26 Jun 2018 17:45:20 +0000 Subject: [PATCH 254/282] preserve dmg artifact --- .gitlab-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1d7502bb8..d815c7d77 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,11 +37,10 @@ MacOS: stage: build allow_failure: true script: - - rm -fr /usr/local/Openmw.app/* # clean up after previous build + - rm -fr build/* # remove anything in the build directory - CI/before_install.osx.sh - CI/before_script.osx.sh - cd build; make -j2 package - # - make install artifacts: paths: - - /usr/local/OpenMW.app \ No newline at end of file + - build/OpenMW-*.dmg \ No newline at end of file From 3c7ab976c381d661b3ef6a9162da7e1d8c7bc0f7 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Wed, 27 Jun 2018 08:22:45 +0400 Subject: [PATCH 255/282] Ignore movement from scripted animations (bug #4475) --- CHANGELOG.md | 1 + apps/openmw/mwmechanics/character.cpp | 2 +- apps/openmw/mwrender/animation.cpp | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8379b6bea..39af121aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal Bug #4471: Retrieve SDL window settings instead of using magic numbers Bug #4474: No fallback when getVampireHead fails + Bug #4475: Scripted animations should not cause movement Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots Feature #4256: Implement ToggleBorders (TB) console command diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index e2df546cd..c049d7933 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2141,7 +2141,7 @@ void CharacterController::unpersistAnimationState() bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0); mAnimation->play(anim.mGroup, - Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f, + Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", complete, anim.mLoopCount, loopfallback); } } diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index 0d740b2f6..ff31e1cfb 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -1190,6 +1190,10 @@ namespace MWRender mHeadController->setRotate(osg::Quat(mHeadPitchRadians, osg::Vec3f(1,0,0)) * osg::Quat(mHeadYawRadians, osg::Vec3f(0,0,1))); } + // Scripted animations should not cause movement + if (hasScriptedAnims) + return osg::Vec3f(0, 0, 0); + return movement; } From 2afcea3870c718fcf1088383ee21e0a9db3c3453 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 27 Jun 2018 11:59:40 +0200 Subject: [PATCH 256/282] added post-1.0 design document --- docs/openmw-stage1.md | 2377 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2377 insertions(+) create mode 100644 docs/openmw-stage1.md diff --git a/docs/openmw-stage1.md b/docs/openmw-stage1.md new file mode 100644 index 000000000..42fe8a2aa --- /dev/null +++ b/docs/openmw-stage1.md @@ -0,0 +1,2377 @@ +# Preamble + +This document outlines the development of OpenMW after the 1.0 release. + +For the first couple of feature releases after 1.0 we should focus on giving mod developers additional tools so they can produce interesting mods with features not supported in vanilla Morrowind. If we look at the use of Morrowind (e.g. on Twitch) we see that people still overwhelmingly make use of the vanilla engine instead of OpenMW. We want to make OpenMW the default choice when playing Morrowind-content. The way to achieve this goal are interesting and desirable mods that require OpenMW. + +Now is not the time for sweeping architectural changes. With 1.0 we acquired an enormous advantage over vanilla Morrowind (full source access) and now we should cash in on that advantage by pushing out large numbers of features that are directly visible to the user. + +At the same time we want to stay true to our origins. While we want to broaden the scope of OpenMW and make it into a more general purpose engine (in the sense that it is used for more than just playing Morrowind), it should still be a Morrowindesque engine. The general direction should be to widen and to generalise. We aim to evolve, not to revolutionise. + +Our goal here is to make OpenMW into a general purpose engine, but a general purpose 1st/3rd person real-time RPG engine. We do not attempt to support other genres or other flavours of RPG. + +The development of OpenMW will hopefully continue for a long time and we can not reasonable hope to sketch out its entire future development in a single design document. Therefore this document should be seen as stage 1 of the post 1.0 development only. It may last us 6 months or a year or several years, depending on how much development activity we can achieve. But eventually there will be a stage 2 design document. + +# Definitions + +## Instances & Objects + +The terminology regarding instances and objects is somewhat confusing since there are several variants of it that are incompatible with each other. Throughout this document we will use the following variant: + +* Instance: The sub-records placed within a cell record. Mostly called references in the vanilla Morrowind terminology. In C++ OOP we would rather call this an object or an instance. +* Object: Records like doors, activators, containers and so on. Mostly called IDs in the vanilla Morrowind terminology. In C++ OOP we would rather call this a class. + +In a few cases the term "object" will be used in the classical OOP sense. Any such mentioning will be accompanied by a statement about the different use. + +## Deprecation + +This document declares some features and functions as deprecated. This usage is somewhat different from the usual meaning in software development. Usually something being deprecated not only means that its use is discouraged but also that it may be removed in later versions. In OpenMW we can not ever do the later because that would break compatibility with older content. + +Instead a feature being deprecated shall mean the following: + +* We discourage the use of the feature (and may reinforce this position by non-intrusive warning messages at some point) +* We will not extend the feature to work with other enhancements of OpenMW. + +A hypothetical example: We deprecate the MessageBox instruction. If we introduce new variable types, the MessageBox text formatting will not be extended to cover these. + +## Script Instructions and Script Functions + +We define a script instruction as a statement that does not have any return value and therefore can't be used as part of an expression. We define script functions as a function that does have a return value. Sometimes the term script instruction is used to mean both instructions and functions (since we do not have a term that covers both). + +We also use the term script function for user defined scripts that are used as functions. + +The meaning should be clear from context. + +# Organisation & Procedures + +This document describes the core enhancements for the next couple of feature releases after 1.0. This does not mean that these releases are limited to what is described here. We have a whole feature request forum full of ideas and many issues on the bug-tracker. But we need to be careful here about what we add and what not. + +With 1.0 we have a clear goal in front of us. After 1.0 things are less clear. We need to consider that any feature we add now and that influences the file formats will stay forever. We can not ever remove features of this kind because that would break compatibility with older content files. Even with features that don't influence the file format we should consider carefully so the engine does not end up as a bloated mess. + +## Versioning + +We continue with the semantic versioning scheme, meaning the next feature releases will be 1.1.0, 1.2.0, 1.3.0 and so on. + +We will only jump to 2.0.0 if a fundamental change is made (e.g. multiplayer). Otherwise we will continue, even if that means we end up with version 1.314159.0 eventually. + +OpenMW and OpenMW-CS will be kept in sync after 1.0. + +**Important**: If OpenMW reaches 1.0 before OpenMW-CS, we need to wait for OpenMW-CS to catch up before we continue with feature development. + +## Bug Fix Releases + +We may decide to have regular bug fix releases in parallel to the feature release development, i.e. while we work on 1.2.0 there could be 1.1.1, 1.1.2, 1.1.3 and so on. If OpenMW is adopted more widely and the development of feature releases takes a longer time this approach might be beneficial to the end users. We would then continue to develop on the release branch of the feature release which then becomes a maintenance branch. This could either happen via (i) merging bug fix commits into the maintenance branch and then periodically merge the maintenance branch into master or (ii) merge bug fix commits both into the maintenance branch and master. + +However this approach means additional work and would be of limited usefulness if we have very quick feature releases. We should hear the people involved in the release process on this topic before we make a decision, because they would be the ones having to do most of the additional work. + +## Roadmap + +We will continue with the revised roadmap scheme. This means we add a new milestone (openmw-stage1). After we are finished discussing this design document and have made necessary adjustments we will cut it up into individual tasks and add them to openmw-stage1. This milestone will then take the role of the current openmw-1.0 and openmw-cs-1.0 milestones. +Confirmed bug reports also go into openmw-stage1. Other issues (feature requests and tasks) only after we have approved them for near future development. +We will most like not have a separate openmw-stage1 for the editor, since for the bulk of the changes (The Grand Dehardcoding) most tasks we will require changes to both OpenMW and the editor. + +# Content File Format & Namespaces + +We will continue with the same general structure of the content file format. All changes will be specific to individual record types. + +We already have a new version counter in the omwaddon and omwgame file formats. We will continue to increase this counter with every new feature release that requires additions or modifications to the file format. + +The ability to read files of older formats must be maintained. The ability of the editor to write older file formats is optional and unless there is strong demand and enough manpower we will most like not support this feature. + +## Records + +Records from older file formats are adjusted and converted during the load process (both in OpenMW and OpenMW-CS). The data structures in OpenMW and OpenMW-CS will always match the most up to date version. + +The following cases of changes to the record format have been identified: + +* New optional records: No action on load required since the older formats won't have this kind of record. +* New mandatory records: Records are build from cfg values or/and GMST values. Typically such a record will merge several values from these sources. We create these records only if the omwgame file is of a version prior to the introduction of this record. We need to modify the record when omwaddon files provide their own record for the value sources (i.e. GMST records). +* Existing records with new optional fields: No action on load required. We already support this. +* Existing records with new mandatory fields: Same as with new mandatory records. +* Existing record that needs to be fully converted into a different record type: Conversion needs to be performed on load. +* Existing record that is switched over from integer-based to ID-based indexing (this is a special case of the case above) +* Existing records is split into two records: Split needs to be performed on load. + +There have been some concerns regarding the efficiency of string based IDs (vs integer). While there hasn't been an evidence indicating that this is actually a problem in OpenMW, it is important to point out that the move from integer to string does not mean that such a problem could not be addressed. + +We already have plans to introduce a new string type for IDs, which deals with the case-issue (current std::string based implementation is error-prone and results in some very verbose code). + +If we are careful when designing this new string class it will be easy to swap out the implementation from a string based one to an integer based one, where the actual strings are stored in a hash-table (similar to the FName class in Unreal Engine 4). + +If we allow older format content files to depend on newer format content files the loading process will become more complicated (see the example given in the "New mandatory records" case). We could avoid these complications by forbidding content files from depending on content files with newer format. This limitation however would have a noticeable usability impact. This issue needs to be discussed further. + +All new records will be identified by a unique string ID. We will not add any new record types that are indexed by integer values (e.g. skills) or other schemes. Descriptions of new record type in this document will usually leave out the ID field. + +## Namespaces + +Within the editor we already have limited pre-1.0 support for namespaces (using C++ syntax). We will continue to expand on that. Namespaces are case-insensitive. + +Namespaces will serve as a tool to avoid naming conflicts between content files and to organise records within a content file. + +We encourage a clean use of namespaces, meaning there will be no equivalent of 'using namespace'. + +We will not enforce the use of a single namespace (not accounting for sub-namespaces) per content file. But we will strongly encourage this approach. + +There are four special namespaces: + +* sys: This is equivalent to std in C++. In general only we are allowed to add new IDs to it (e.g. new GMSTs). +* project: Records within this namespace are saved in a local project file, but may also be presented to OpenMW when starting it through OpenMW-CS. +* session: Records with this namespace exist only for the duration of a single editing sessions and are currently not presented to OpenMW. +* default: Previously called sys::default (see section about cells and worldspaces) + +To encourage at least some degree of stylistic uniformity we will use only all lower case names for namespaces in all official documentation. + +Having a record with the same ID as a namespace is allowed (see section about cells & worldspaces for an example). + +# Meta-Data & File Management + +## Identifying Meta-Data + +Identifying meta-data specifies the identity of the content file in a formal way that can be utilised by various tools. + +There are four parts to this meta data: + +* Name: A unique name that identifies the content file across all versions and all languages +* Version: A unique version identifier. A uniform versioning scheme is required here to avoid unnecessary complexity in the tools. Most likely we will use the same semantic scheme as with OpenMW itself. +* Language: see section about localisation +* Multiplayer: see subsection about multiplayer in misc section + +There are two possible ways to encode this meta data: + +1. In a new record or sub-record +2. In the file name + +The record option is more flexible and allows for extensions at a later point with the least amount of disruption. It also avoids unnecessarily cryptic file names. On the other hand we need unique file names anyway and storing the information in the file name avoids redundancy. This needs to be discussed further. + +There is a large number of possible uses for this kind of meta data. Here are some examples: + +* Simplifying the content file selection within the launcher: All entries for the same content file can be grouped into a single row. We can then add two columns for version and language that contain a combo box with the available options. That should reduce the amount of clutter significantly and also should help to avoid user errors because it is now impossible to select two versions of the same file at once. +* We can add a language setting to the launcher and make it automatically select the preferred language for content files (where available). +* Tools for porting content files from one version of a dependency to another will benefit from being able to automatically identify that two different dependencies are actually just different versions of the same file. + +## Non-Identifying Meta-Data + +### License + +We provide the user with a reasonable list of license choices (including a custom one where the user can provide his own license text). When the content file is packaged by the editor, a copy of the license text is included automatically. For newly created content files this field should be set to a sensible default (definitely not custom). Furthermore the merge tool can check for possible license conflicts. + +This enhancement will hopefully cut down on the license wild-growth within the Morrowind modding community. If we want to get fancy with this feature we could also add a tool that helps the user to decide on a license. + +### List of Authors + +Currently we have a single text field for the authors. This is suboptimal for community projects. With the current solution the best a community can do is to insert 'The x Team' and then maintain an external list. + +A better solution is to replace this field with a list of names that can be automatically merged (including checking for duplicates) when merging content files. +The editor can be configured with a user name, which is automatically inserted when creating a new content file or modifying an existing content file. + +## Resources Files + +### Problem + +Currently all resources files of all installed content files are dumped into a single directory. This poses several problems: + +* A content file that comes with resources files that override other resources files can not be simple switched off by disabling it in the launcher. +* Without external record keeping there is no way of knowing which resources files to include when packaging a content file for release. This is a particularly big problem if we want to automate this process. +* Automated tools for checking for missing resources won't be able to look at the resources for the currently selected stack of content files only, which means some problems may remain undetected. + +### Data Paths + +Some people have started to put different content files into separate directories, each with its own data path. This is not a solution. + +The selection of data path is a separate stage from the selection of content files. More precisely the available selection of content files depends on the selection of data paths. This results in a two stage content selection mechanism, which is undesirable from a usability perspective. + +### Solution + +The solution is to group resources files within the data directory structure. + +The first proposed solution (putting all resources files into a single archive with the same name as the content file) received a lot of criticism during forum discussions. + +As an alternative we could create a single content directory (per content file) within the data directory. This content directory would then contain the content file and the usual directory structure (Icons, Meshes, Music, ...). + +We may offer one of these solutions or both. This needs to be discussed further. + +In the forum the following flaw in the archive solution has been brought up (applies equally to the directory solution): + +In some cases one may want to use only the resources from a content file without the actual content file and license reason may stop the repackaging in a form that allows this kind of usage within the new system. + +There is an easy fix for this issue: We add a flag to the dependency list in each content file. If this flag is set for a dependency, only the resources are used and the actual content file is ignored. + +### Importing & Backwards-Compatibility + +Most of the current content exists in a form that is not compatible with the new system. + +In most cases this can easily be resolved by adding an importer tool to the launcher, that takes the content and copies it into the desired organisation scheme (archive or directory). + +For other content (preexisting data directories and Morrowind, since Morrowind and its add-ons are installed into the same directory) we need to maintain a level of compatibility with the old approach, that can be switched on and off as needed. + +### Resources Packages + +During the discussion about the new content file organisation scheme (omwgame, omwaddon) there was some criticism about the scheme not allowing plugins with no dependencies. + +This is not a problem, because such plugins are generally useless. A plugin without any dependency can not modify or reference any record. That was not such a large issue with vanilla Morrowind, because in this case the plugin could have still have depended on things like attributes or dynamic stats. However in OpenMW these will all turn into records and therefore can not be accessed from within an omwaddon file without dependencies. This will only get more extreme as the de-hardcoding progresses further. An omwaddon file without dependencies can do literally nothing, which makes it useless by definition. + +But there is one exception. Some members of the mod community have provided resources packages (meshes and similar) that can be used by other content developers. Under our new resources scheme these would have to be accompanied by an omwaddon file. This is not a good solution because of two reasons: + +* a omwgame file may need to depend on such a resource pack (currently impossible) +* a resource pack can be used by omwaddon files that depend on any omwgame file (also currently impossible) + +Therefore we will introduce resources packs as a third kind of content type. Resources packs do not have dependencies and they can be placed in any position in the load order (provided the load order fulfils the dependency requirements of the content files depending on the resources pack). This will require some adjustments to the content file selector GUI. + +Note that resources from a resources pack can still override resources from other resources packs and other content files earlier in the load order. Therefore the load order is highly relevant for resources packs too. + +A resources pack consist of an omwres content file and the associated resources files. If we choose the file-name variant for identifying meta-data and the archive implementation path (vs sub-directory), we could theoretically skip the omwres content file and just make the resources archive the omwres file instead. However this is not a good idea, because that would leave us no way to carry along author and license information. + +## Packaging + +The editor will be enhanced with a packaging tool that can take a content file and its associated resources files and package them into a form suitable for distribution. + +The question now is what is a suitable form. For Linux the first option that comes to mind are the respective software package management systems. This of course does not help Windows and Mac users. While there is the obvious and very reasonable answer ("Get a real operating system!") this seems unnecessarily harsh to Mac users and is overall not helpful. + +Therefore we will develop our own cross-platform package format for OpenMW content. The exact scope still needs to be determined, but it could be as simple as an archive that holds the content file and the associated resources files. Giving this archive type a unique file extension (e.g. omwpack) would then allow the launcher to pick up opening requests (i.e. double click on the file) and install the content (optionally including a check for dependencies). + +# Scripting + +Note: Extensions to the scripting language in form of new instructions and functions are distributed over this entire document. In some cases features may require additional functions that return the value of certain fields of certain records. These functions are usually not listed explicitly and are left as an exercise to the reader. + +## Language Version + +The version of the language used in a script is determined by a single integer number which is stored in a new sub-record within the script record. Note that this is different from earlier concepts which used a more complicated version identifier and stored it within the actual script. + +Scripts that are missing this sub-record are considered version 0 (legacy). New scripts (when created in OpenMW-CS) default to the most up-to-date version. The version can be selected from a combo box in the script sub-view. We may add a user setting that hides the combo box UI if the version is the most up to date one. + +The version number is incremented whenever an addition to the scripting language is made (at most once per feature release). A new feature release is not required to increment the version number, if no feature changes have been made to the language in this release. + +From version 1 on all workarounds for bad scripts will be disabled. This should not break anything for well written scripts. In general moving to a new language version should usually at most require minor fixes. + +Since old scripts will remain with the old language version (unless there is a need to modify them and use newer features), this scheme will not break compatibility for existing content. + +## Error Handling + +We will continue with the existing runtime error handling scheme (meaning stopping the script execution on a runtime error). Introducing exception handling instead would be overkill. + +We will do our best to avoid crashes as result of broken script. That means (among others) that we need to put an artificial (configurable) limit on function call depth to avoid crashes and lockups from endless script recursion. We may also decide to put a similar limit on loops. + +## Variable Types + +The types *long* and *float* remain unchanged. + +We will flag *short* as deprecated. Currently *short* has no advantage over *long* (it uses the same amount of memory in OpenMW). For anything but a very specific kind of bit cutting the *short* type has no use at all. +If we ever decide that we do want a shorter integer type we can always de-deprecate *short*. + +We will introduce several new types: + +### Strings + +The name of the new string type is **String** and string literals are marked by placing them in quotation marks. Example: + +```` +String a +Set a to "Some Text" +```` + +Comparison operators for strings are provided in the usual way. + +Since we discontinue bad script workarounds most ambiguousness regarding string literals should have been removed already (we will not accept local variable names and instructions within quotation marks anymore). Only a single problem case remains: + +If a function requires an ID, is it a literal ID or is it the name of a string variable that contains an ID? Example: + +```` +String Fargoth +Set Fargoth to "player" +Fargoth -> AddItem "Gold_001", 100 +```` + +We can not make a rule that forbids the use of local variable names that are also IDs, because that would allow random content files to break scripts in other, unrelated content files. + +Therefore the solution here is to simply give the local variable precedence over IDs. If the script author wanted to give money to Fargoth he should not have created a local variable with this name. + +Note that global variables are not an issue here, because global variables are IDs and IDs need to be unique (with certain exceptions that are not relevant here). + +To remove even the last remaining potential problems with ambiguity we will also introduce a new string literal that can only be a literal and never a string variable name. These literals are marked by a L prefix. Utilising this feature the code block above could be rewritten to the following if the script author absolutely insists on having a local variable named Fargoth: + +```` +String Fargoth +Set Fargoth to "player" +L"Fargoth" -> AddItem "Gold_001", 100 +```` + +### Instance-References + +The name of the new reference type is **Ref**. It can reference an instance in the world or in a container or be a null reference. + +Since we currently have no way to reference an instance persistently the use of the ref type is limited for the time being (see section about variable scope for further details). + +References can be used in any place that would otherwise allow an ID that stand for an existing reference. The rules for strings regarding ambiguousness apply to references in the same way. + +A reference will implicitly cast to 0 (a null reference) or 1 (not a null reference) when used in a numeric expression. + +Note: A reference pointing into the contents of a container points to a stack of items and not a single item. + +We introduce the following keywords: + +* Self: a value of type Ref (the instance the script is currently running on, only available in local scripts and dialogue scripts) +* NullRef: a literal of type Ref (null reference) +* GetId (r): return ID of reference as a string value +* GetContainer (r): returns the reference of the container r is in (or a null-reference if r is not in a container) +* GetCell (r): returns the ID string of the cell r is in (either directly or via a container) +* SearchActive (id, container=0): return a reference with the given ID within the active cells. If container!=0, also check in containers; return a null-reference if no reference is found +* SearchIn (id, id2): return a reference with the given ID within something (id2) that can contain references; this can be a cell, a worldspace, a container, a creature or a NPC. If id2 represents an instance, a ref variable can be given instead; return a null-reference if no reference is found + +### Lists + +A single type list available for the following types: + +* Long +* Float +* String +* Ref + +We will not support mixed type list because that would require a major change to the type system. Internally and functionally a list will work like an array. The type names for lists are: + +* LongList +* FloatList +* StringList +* RefList + +List literals are given as a comma separated list of values in square brackets. Empty list literals are allowed. In the context of lists we generally allow implicit promotion of integer to float types. + +We support the following list operations: + +* l1[i]: Return member i (indexing begins at 0) +* GetSize(l1): Returns the number of elements in l1 as an *Integer* +* l1 + l2: Returns a concatenated list consisting of the members of l1 followed by the members of l2 +* GetSubset (l1, l2): Return the common subset of the elements of l1 and the elements of l2 +* HasElement (l1, e): Return 0 (if e is not element of l1) or 1 (otherwise) +* GetIndex (l1, e): Return index of first occurrence of element e in l1 (or -1 if none) +* Minimum (l1): Return the minimum of the elements in l1 (only works with numerical types) +* Maximum (l1): Return the minimum of the elements in l1 (only works with numerical types) + +and the following list instructions: + +* Set l1[i] To e: Set list element i of list l1 to value e (indexing begins at 0) +* Sort (l1, cf=""): Sort list elements into ascending order (if cf is an empty string) or via comparison function script cf +* Filter (l1, ff): Filter list l1 by filter function ff (only keep elements that do not return 0) +* Reverse (l1): Reverse order of list elements +* Append (l1, e): Append element e to list l1 +* Append (l1, l2): Append elements of list l2 to list l1 +* Remove (l1, e): Remove all elements equal to e from list l1 +* Remove (l1, l2): Remove from list l1 all elements which are also in list l2 +* InsertAt (l1, e, i): Insert element at position i into list l1 (replacing element i on position upwards and extending the size of the list by one) +* RemoveAt (l1, i): Remove element at position i from list l1 (moving elements with an index higher than i down by one and reducing the size of the list by one) +* Resize (l1, n): Set size of l1 to n. If list is extended additional elements are default initialised. + +Variable names used in the lists above: + +* l1 and l2 are lists/list literals of the same element type +* i is an integer value that functions as list index +* n is an integer value +* e is a variable/literal of the element type of l1 and l2 +* cf: ID of a comparison function script that takes two element type arguments and returns an integer (-1, 0, 1) +* ff: Id of filter function script that takes one element type arguments and returns an integer (0, 1) + +### List Aliases + +We define three more type aliases: + +* Vector3: A float list of 3 members, used for location or rotation +* Vector4: A float list of 4 members (location and zrot) +* Vector6: A float list of 6 members (location and rotation) + +These aliases are not separate types. + +In the specification of additional functions and instructions values of these types are specified as v3, v4 and v6 respectively. Passing in shorter lists is allowed and will set the missing elements to 0. + +Note that all rotations are specified in degrees. + +## Variable Scope + +All variable types will be available at all scopes with the exception of reference variables. Reference variables will be limited to true local scope (see below), because all other scopes require writing the variable to a saved game file and we currently don't have a general method to serialise instance references. This is a task for stage 2 or more likely OpenMW 2.0. + +The global and the local scope remain unchanged. We will add two more scopes: + +### True Local Variables + +What the Morrowind scripting language describes as local variables dose not match the concept of local variables in other languages. In case of local scripts Morrowind local variables function as member variables. In case of global scripts Morrowind local variables function as static variables. + +There is a need for true local variables: + +* Less pollution of local dialogue variable names +* Less state that needs to be saved +* Avoidance of potential scripting errors when the state of a local variable is maintained across multiple script executions and the script does not re-initialise the variable at the beginning of a new run + +We will call this scope **true local variable** to distinguish it from old local variables. + +Declaration of true local variables will look like this: + +```` +Local Long a = 1 +Local Long b +```` + +If no default value is given the variable is default initialised (value 0, empty string, empty list, null reference). + +### Record Variables + +A problem (especially relevant for large projects with complex functionality) is that there is no place other then global variables to store additional state that is not specific to an instance. A global function with multiple variables can be used instead but that is just a workaround. + +Furthermore there is currently no way to add new state to other objects (object in the OOP sense, not in the Morrowind sense) other then instances. + +Therefore we introduce record variables which are a generalisation of old local variables in local scripts. + +The respective records will be enhanced by an optional list of subrecords that can declare record variables and their default values. + +Obvious candidates for this feature are: + +* Objects +* Cells +* Regions +* Worldspaces +* Factions + +Note: Object record variables are essentially the same as old local variables declared in a local script, but declared in the object record instead of the script. + +We will introduce the following record variable functions: + +* Has{Type} (vn): Return 1 if record/reference has a long/float/string/list record variable of name vn, 0 otherwise. +* Get{Type} (vn): Return value of record/reference variable vn. Note that we don't use the x.y syntax here, because we need to explicitly specify the type to allow for proper compile time error checking for reference variables. + +and the following record variable instruction: + +* Set{Type} (vn, val): Set variable vn to value val. It is an error, if this variable does not exist. + +With all functions and instructions the record/reference is specified via the -> operator in the usual way. When using object records (via ID) the first instance found is used instead of the object record. + +Variable names used in the lists above: + +* id: string +* vn: string +* ref: reference +* val: value according to {Type} + +and finally {Type} standing for Long, Float, String, LongList, FloatList or StringList + +We can fold the access of local variables in global scripts into the new record variable system, even though global scripts do not have explicit record variables. The new functions for record variable access make the old x.y syntax obsolete and therefore we declare it as deprecated. + +## Control Structures + +We will add a for loop that works on lists. To avoid unnecessary complexity in the language and to encourage the use of lists we will not have an index based for loop (these can easily be simulated via while). + +Example: + +```` +For x In l + do something with x +EndFor +```` + +We will add the break and continue keywords both for for and while loops. + +We may add a switch-case construct, but this is most likely a stage 2 task since there are many different ways to do switch-case and no obviously superior solution. This will most likely require extended discussion and design work. + +## General Additions + +The vanilla scripting language is missing a few pieces of basic functionality. Therefore we need to add the following kinds of instructions: + +* Trigonometric functions (Cos, Sin, Tan, Acos, Asin, Atan, Atan2, DegToRad, RadToDeg) +* Logical boolean function (And, Or, Not, Xor) +* Other mathematical functions (Abs, Floor, Ceil, Clamp, Lerp, Sign) + +## Object Management + +We will add a function that moves an instance into the world. This feature (especially the move function) preludes a concept of persistent instance identity, which is most likely a stage 2 or OpenMW 2.0 feature. For now these functions are required to deal with state attached to the instance in the form of record variables/old local variables. + +* MoveObjectToWorld (target, v6): Move instance from current location to world location. +* CloneObjectToWorld (target, v6): Add a copy of instance to world location. + +(in all cases reference specification for objects to be moved/cloned via -> operator in the usual way) + +Moving or cloning out of a container will only move or clone a single item, not the whole stack. + +The target value represents the target cell or worldspace (see section "References to cells in data structures" for details) + +We will add replacement functions for object placement related functions without declaring the original functions deprecated. + +Instructions: + +* SetRotation (v3): Set instance's rotation; characters and NPCs ignore first two elements (replaces SetAngle) +* SetPosition (v3): Set instance's position (replaces SetPos) +* Teleport (target, v6, strict=0): Teleport instance to location specified by target and v6 (if the instance is a character or NPC the xrot and yrot values are ignored). If strict!=0, no corrections are made to the specified location. Otherwise the location may be adjusted for safety. (replaces Position and PositionCell) +* Spawn (objectId, target, v6, strict=0): Same as teleport, but spawns a new object (replaces PlaceItem and PlaceItemCell) + +Functions: + +* GetRotation: Returns instance's rotation as a Vector3 (replaces GetAngle) +* GetPosition: Returns instance's position as a Vector3 (replaces GetPos) + +## Object-Type Specific Additions + +We will add functions to modify and query other non-item-specific state that is already present in the cellref class and therefore already written to saved game files. + +Instructions: + +* SetTrap (trapId): Set trap ID +* SetKey (trapId): Set key ID +* SetTeleport (target, v4): Set teleport location (does not affect teleport flag) + +Functions: + +* GetTrap: Return trap ID +* GetKey: Return key ID +* GetTeleportTarget: Return teleport target +* GetTeleportPosition: Returns teleport location and zrot as Vector4 + +## Object-Types + +We introduce compile time constants for object types (Weapons, Activators and so on). These have integer values. The constants must be named consistently in such a way that name collisions are unlikely (e.g. TypeWeapon, TypeActivator). + +We add a function that returns the type of an object or instance: + +* GetObjectType + +## Queries + +A query returns a list of instances within a certain area. Along with the description of the area the query takes a list of object types (t) as an additional argument. + +* QuerySphere t, v3, r +* QueryBox t, v3_tl, v3_br (this is an axis-aligned box) +* QueryCylinder t, v3, r, height (the cylinder is aligned along the Z-axis, v3 specifies the middle point of the bottom circle) + +## Text Formatting + +The vanilla scripting language provides text formatting in a single place (MessageBox instruction). This is insufficient, because: + +1. We will need text formatting in many places, independently of message boxes +2. The MessageBox text formatting sucks + +We will deprecate MessageBox. As a replacement we will introduce new instructions for creating message boxes (see following sub-section and GUI section) without a formatting features and also separate text formatting functions. + +* Str(v, p = -1): Returns a string containing the string representation of the numeric value v. p specifies the number of digits after the decimal point. If p==-1 a suitable value depending on the type of v will be chosen. +* LFormat (s, a): Returns a string equal to the string s with all occurrences of %x replaced with the respective element indexed x from the string list a. +* Join (l, s): Return a string consisting of the members of the string list l separated by the string s + +## Multiple Choice Message Boxes + +We add two new functions for showing multiple choice message boxes: + +* MakeChoice t, sl, f +* MakeBranchChoice t, sl, fl + +Arguments are as follows: + +* t: The message box text +* sl: A list of strings, defining the available options +* f: The ID of a script function that is called when an option is selected. The function takes one long argument (the option index, starting at 0). +* fl: A list of script function names, same length as sl. Functions do not take any arguments. Empty strings in the list are allowed, which makes OpenMW ignore the respective choice. + +## Functions + +We extend scripts to become callable functions with argument passing and a return type. + +### Declaration + +The syntax of the begin statement is extended in the following way: + +```` +Begin { ( { -> return-type } ) } +```` + +( {} denoting optional parts ) + +Argument lists are allowed to be empty. Argument lists are comma-separated. Elements in argument list are type name pairs. Elements can be given default values in a C++ typical fashion. + +Arguments and return values are for all intents and purposes true local variable. Therefore the ref type is available. + +### Calling + +A function can be called by its name followed by parenthesis. Arguments are listed inside the parenthesis separated by comma. + +We may at some point add the use of named arguments for function calling (comparable to Python), but this is most likely a stage 2 feature. + +If the function has a return type the function is evaluated as an expressions with a type corresponding to the return type. + +Calling a function pauses the execution of the calling script, executes the called script and then resumes the execution of the calling script. This is different from the StartScript/StopScript instructions. The StartScript instruction is not modified (i.e. no argument passing). + +A function call must be performed via a function name literal. The function name can not be given as a string variable, since this would make it impossible to check for the correctness of the function signature at compile time. + +Local and global scripts must not have any arguments without default values. + +## Script Hooks + +We introduce two new record type (hook, hooked script). The hooked script is a generalisation of the concept of a startup script. We may decide to fold the startup script record type into the hook record type. + +### Hooked Scripts + +A hooked script record binds a script to a hook record. More than one hooked script record per hook record is allowed. + +The ID of a hook record is build from the hook record ID followed by a $ followed by the script name. + +Example: + +```` +Startup$KillFargoth +```` + +By building hook IDs in this way we allow content files to delete hooked script records in dependencies without the need for an additional ID that identifies individual hook records. + +If more than one script is attached to a hook the order in which the scripts are executed is implementation defined at this point and scripts must not depend on a specific order. + +### Hooks + +A hook record declares a new hook. Each hook record contains a list of argument types with optional default values (again comparable to C++ function calling). + +There will be no return values (problematic since there can be multiple scripts per hook). + +We provide system hooks within the namespace sys that are called in specific situations (see the section about de-hardcoding for some examples). Content developers may also provide their own hooks (user hooks). +System hooks are not added to content files, since they can not be modified by content developers anyway. We will instead inject the system hook records on content file load. + +System hooks are triggered by the engine. System hooks and user hooks can be triggered explicitly with a new script instruction. + +## Script Slots + +We define the term script slot as an optional sub-record in an object record that contains the name of a script. The default script slot for most object types is the "Run-Once-Per-Frame" slot, a.k.a. local script. + +The default slot does not take any function arguments, but other slots can. The function arguments of a script attached to a slot need to match the hard-coded argument list of the slot. + +Note that if to scripts attached to the same object both define a (non-true) local variable with the same name, there will be only one variable. It is an error if the type of these variables don't match. + +### Additional Slots + +We add slots for OnX type keywords to object types where applicable The relevant keywords are: + +* OnActivate +* OnDeath +* OnKnockout +* OnMurder +* OnPCAdd +* OnPCDrop +* OnPcEquip +* OnHitMe +* OnPCRepair +* OnPCSoulGemUse +* OnRepair + +### Custom Slots + +We may allow the addition of custom slots (defined by the content developer), though this is an advanced feature more likely to be implemented in stage 2. Further details need to be explored. + +## Namespaces + +The namespace of a script is determined by its ID. For example a script with the ID foo::Bar would be placed in the namespace foo. + +IDs in a script refer to the local namespace by default, meaning the ID a in the script b::c would refer to b::a if such an ID exists or to ::a otherwise. + +## Other Script Instructions + +This is a collection of script instructions that fit nowhere else: + +* GetPcTarget: Return the reference of the instance the player is currently looking at (crosshair). Can be a NullRef. +* GetMultiplayer: Always returns 0 +* GetPlayers: Returns a list of reference to all player controlled instances. This list contains a single reference to the instance of object Player. + +## Other Script Hooks + +This is a collection of script hooks that fit nowhere else: + +* sys::NewGameStarted: Executed when a new game is started. + +# Cells, Worldspaces & Areas + +## Interior vs Exterior + +The distinction between Interior and Exterior cells stopped making sense with the release of the Tribunal add-on. Therefore we should drop this terminology and replace it with Unpaged Cells (Interior) and Paged Cells (Exterior). + +## Paged Cells + +Paged cells remain essentially unchanged from old exterior cells, the only major difference being the ID and the worldspace. + +Currently there is no uniform ID scheme for cells. Interior cells are named via an ID, exterior cells are named via a cell index (x, y). This needs to be changed, since we need a more consistent way to address cells. + +The editor already gives exterior cells an ID (currently in the form of #x,y). The new scheme (relevant at least for the editor and scripting) will be "worldspace::x,y" (the cell will have the ID "x,y" in the namespace "worldspace"). We may also consider to replace the two integer coordinates in the cell record with the string ID. The performance impact of this change should be insignificant. + +The default exterior worldspace will be called default (previously called sys::default). Therefore the old exterior cell 0, 0 will be called "default:0,0". We are moving default out of sys to avoid creating an exception to the rule that content files are not allowed to create records in sys. + +## Unpaged Cells + +Unpaged cells take the place of old interior cells. The only difference between old interior cells and unpaged cells is the worldspace. + +By default each unpaged cell has an associated worldspace record with the same ID. Furthermore the unpaged cell record is enhanced by an optional sub-record that specifies an alternative worldspace (if present). + +Most of the cell configuration data is moved out of the cell record and into the worldspace record. + +By moving out the worldspace into a separate record we can unify worldspace data between interior and exterior cells. We can also associate multiple interior cells with the same worldspace or interior cells with an exterior worldspace. This should reduce duplicated data significantly. + +## Worldspace Records + +Worldspace records are shared by paged and unpaged cells (i.e. there are no interior and exterior worldspaces). + +Worldspace records contain the following fields: + +* Water: Default Water level, Water ID (see de-hardcoding). Note: Script instructions that modify the water level will now modify the water level for a worldspace instead of a cell. +* Ambient Light: 4 values +* Sky: Sky ID (analogous to Water ID, most likely not part of stage 1). Note: A worldspace can not have both an ambient light and a sky sub-record. +* Terrain: No data (might be better implemented as a flag). Presence indicate that worldspace has terrain. Ignored by unpaged cells. + +All fields are optional. If a field is missing the respective cell/worldspace element is not present. + +## References to cells in data structures + +Vanilla references to cells are generally based on the ID of the cell. There are no fields in vanilla Morrowind data structures that reference individual exterior cells. + +We keep these reference fields, but change their meaning: + +1. Check if it is the ID of an unpaged cell. If yes, use this cell +2. Check if it is a worldspace. If yes, use paged cells in this worldspace. +3. Otherwise, error. + +## Scripts + +### Water Level + +The existing script instructions that deal with water level can be safely extended to the new system. We will add an optional argument at the end of the argument list of each instruction (GetWaterLevel, ModWaterLevel and SetWaterLevel) that specifies the worldspace the instruction is action on. If the argument is missing the instruction effects the current worldspace instead. + +Note that this is different from vanilla Morrowind in regards to use in exterior cells. + +### New Instructions + +We add the following script instructions: + +* EnumerateActiveCells: Returns a list of strings, containing the IDs of all cells currently active. Paged cells are listed individually. +* GetWorldspace (c): Returns the ID of the worldspace the cell with ID c is in +* GetWater (w): Returns the ID of the water used in the worldspace with ID w +* GetSky (w): Returns the ID of the sky used in the worldspace with ID w + +### Script hooks + +We add four new script hooks: + +* RegionEntered +* RegionExited +* WorldspaceEntered +* WorldspaceExited + +All four hooks take the ID of the region or worldspace as arguments and are executed when the player enters or exists a region or worldspace. + +## Areas + +The ability to designate a section of a worldspace with a specific ID that can be checked or referenced has many uses. A few examples: + +* Give a NPC certain dialogue topics only in specific places +* Limit wandering NPCs to an area +* Add special/magical effects to a location + +Currently our ability to do that is largely limited to cells. This is a problem because of two reasons: + +* The fixed-size square nature of nature of cells makes areas unreasonably inflexible. +* We can't have overlapping areas. + +### Record + +We introduce a new record (Area) that consists of the following fields: + +* Worldspace ID +* Polygon (list of 2D coordinates), defining a surface on the x-y-plane +* Min-Height: Z-coordinate at which the area starts +* Max-Height: Z-coordinate at which the area ends +* Enter script ID (string, optional): Script function that is called when the player enters the area +* Exit script ID (string, optional): Script function that is called when the player exits the area (must also be called in case of teleportation) +* Inside script ID (string, optional): Script function that is called while the player is in the area. +* Inside script delta (float, optional): Minimum time between two executions of the inside script function. +* Composite (integer, optional): If this flag is set the area will be ignored by OpenMW except as a part of a joined area. Composite areas are not accessible by script instructions and do not run any scripts of their own. + +All script functions take the ID of the area as a parameter. + +Whenever a cell or a worldspace is referenced for checking or defining locations the ID of an area can be used instead. + +### Script Functions + +We add the following script functions: + +* GetAreas (v3): Returns a string list containing the IDs of all areas v3 is inside of +* InArea (id): Returns 1 or 0 depending on if the instance is in area id or not + +## Joined Areas + +A joined area is an area that consists of multiple un-joined areas. Joined areas can contain both composite and non-composite areas. A joined area can stretch across multiple worldspaces. Scripts do not distinguish between areas and joined areas. + +### Record + +We introduce a new record (JoinedArea) that consists of the following fields: + +* Enter script ID (string, optional): Script function that is called when the player enters the area +* Exit script ID (string, optional): Script function that is called when the player exits the area (must also be called in case of teleportation) +* Inside script ID (string, optional): Script function that is called while the player is in the area. +* Inside script delta (float, optional): Minimum time between two executions of the inside script function. + +# Item-Interactions & -Management + +## Deletion + +We will add an instruction to delete instances via a reference variable. Current solutions are insufficient because they can not target specific items (when in a container) and require different approaches depending on if an instance is in a container or in the world. Self-deletion must be safe. For the sake of consistency We extend this instruction to work on non-item objects too. + +* Delete (reference specification for instance to be deleted via -> operator in the usual way) + +## Container + +We will add a function that returns the contents of a container as a list of references (the term container includes here creatures and NPCs). + +* GetContents (reference specification for container via -> operator in the usual way) + +We will add a function that moves/clones an item instance into a container (actual container or NPC). This feature (especially the move function) preludes a concept of persistent instance identity, which is most likely a stage 2 or OpenMW 2.0 feature. For now these functions are required to deal with state attached to the instance in the form of record variables/old local variables. + +* MoveItemToContainer (ref/id, count=1): Move item from current location to container specified by ref/id +* CloneItemToContainer (ref/id, count=1): Add a copy of item to container specified by ref/id + +(in all cases reference specification for item to be moved/cloned work via -> operator in the usual way) + +The count argument is ignored when the original item is not in a container. + +## Other Item-Related Instructions + +* SetItemHealth (health): Set item current health +* SetItemCharge (charge): Set item current charge +* SetItemOwner (owner): Set item owner ID +* SetItemSoul (soul): Set soul ID (soul gems only) +* SetItemFaction (faction): Set item faction ID +* SetItemFactionRank (rank): Set item faction rank + +Functions: + +* IsItem: Return 1 if reference is an item, 0 otherwise +* GetItemHealth: Return item current health +* GetItemMaxHealth: Return item max health +* GetItemCharge: Return item current charge +* GetItemMaxCharge: Return item max charge +* GetItemOwner: Return item owner ID +* GetSoulItem: Return soul ID (soul gems only) +* GetItemFaction: Return item faction ID +* GetItemFactionRank: Return item faction rank + +## Item Tags + +Currently there is no customisable way of categorising items. To compensate for this shortcoming we introduce items tags. + +An item tag is a string that is attached to an object record. An object record can have multiple tags. + +Objects also have implicit item tags that are determined by their type (e.g. every weapon object has a tag weapon even without the object record explicit containing this tag. We may introduce other kinds of implicit tags (e.g. weapon types, enchanted items). + +The de-hardcoding introduces additional item tags. + +Item tags are immutable at runtime and can be queried via script instructions: + +* GetItemTags (Id): Returns a string list of tags for the object Id. As usual instead of an ID a reference variable can be used. +* HasitemTag (Id, Tag): Return 1 or 0 depending on Id has the tag Tag. + +Using these instructions on non-item objects return an empty list and 0 respectively. + +Item tags can be used to organise container windows (see GUI section). + +A few examples of tags that content developers may come up with: + +* quest: A quest item +* vendor: An item that has no other use than to sell it to a vendor + +We may suggest some default tags in the documentation (or even the editor) to encourage more consistent tag use by content developers. + +## Interactions + +We enhance the way how the player interacts with the world, especially via items. + +### Methods of Interactions + +There are three methods of interaction: + +* The player presses the attack button while holding an interaction-item in his hand and targeting an object in the world. This feature exists in vanilla MW only within the security skill. We generalise these kinds of interactions (see Held Items in the De-Hardcoding section). We also allow this feature for weapons held in the main hand. If an interaction is possible the interaction takes the place of the attack. If the interaction is ignored or refused the attack proceeds. +* The player drags an item from a container onto on instance in the world. This kind of interaction does not exist in vanilla MW. +* The player drags an item from a container onto another item in another (or the same) container. This kind of interaction does not exist in vanilla MW. + +### Interaction Subrecords + +All item object record types are enhanced by a new optional sub-record that holds the name of the ItemItemInteraction script. + +All object record types (including items) are enhanced by a new optional sub-record that holds the name of the ItemObjectInteraction script. + +### Interaction-Chain + +Custom interactions are handled by running through a sequence of scripts. Each script can either ignore the interactions or accept it or refuse it. + +If the action is accepted or refused the chain stops. If the action is refused or the chain runs to its end without any script accepting it the player will be notified via a sound-effect. We may either re-purpose an existing sound-effect or acquire a new one. + +We add two new script instructions: + +* AcceptInteraction +* RefuseInteraction + +Using any of these instructions outside of an interaction chain is an error. We use these instructions instead of return values, because a part of the interaction chain works via hooks which do no provide return values. + +All interaction chain functions share the following signature: + +* ref to Item A/Item +* ref to Item B/Object +* ref to actor performing the interaction (player for now, we may extend that later) +* integer: 0 interaction was initiated via drag & drop, 1 interaction was initiated via attack button + +### Item-Item-Chain: + +* Hook sys::PreItemItemInteraction +* ItemItemInteraction Script of object A +* ItemItemInteraction Script of object B +* Hook sys::ItemItemInteraction + +### Item-Object-Chain: + +If the object is also an item the Item-Item-Chain is used instead. + +* Hook sys::PreItemObjectInteraction +* ItemObjectInteraction Script of item +* ItemObjectInteraction Script of object +* Hook sys::ItemObjectInteraction + +# De-Hardcoding + +This section describes the first batch of de-hardcoding tasks. This is the core part of stage 1 (*The Grand De-Hardcoding*). We are aiming mostly for low hanging but highly profitable fruits here. More complex de-hardcoding (especially tasks that require more extensive script support) will follow in stage 2. + +## GMSTs + +Many GMSTs will be integrated into other records, which will effectively remove the GMST. We may consider actually removing the GMST record during the load process. + +We add three new types of GMSTs: + +* IntegerList +* FloatList +* StringList + +We may consider allowing content files to create new GMSTs outside of the sys namespace. This would make the GMST record type more consistent with other record types. To make this addition useful we need to add script functions to read GMSTs: + +* Keyword: Get{Type}Gmst() + +{Type} can be Integer, Float, String, Integerlist, FloatList or StringList. + +## Fallback Values + +The openmw.cfg file contains a set of fallback values. These were extracted from the Morrowind.ini file. As the name indicates we consider these values as a fallback for legacy format content files only. Our goal is to move all these values to content file format. In some cases we also may decide to declare a fallback value obsolete and not use it at all. + +All usable values should be migrated to new GMST records, unless they are already covered by other parts of the de-hardcoding. + +## General Scripting Enhancements + +We introduce new script functions: + +* Enumerate{x}: {x} is either Skills, Races, Classes, DynamicStats, Attributes, WeaponTypes, ArmorTypes, Specialisations, MagicSchools, Factions or Birthsigns. Returns a string list of the IDs of all records of the respective type. + +## Dynamic Stats + +We will unify dynamic stats and make them configurable. For stage 1 we will not allow the deletion of existing dynamic stats (they are used in too many parts of the engine). But we will allow new dynamic stats. + +We add a new record type for dynamic stats. Records for Health (sys::Health), Fatigue (sys::Fatigue) and Magicka (sys::Magicka) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). + +A dynamic stats record contains the following information: + +* Name (taken from GMSTs for default stats) +* Tooltip text (taken from GMSTs for default stats) +* Colour +* (optional) PrevID: ID of stat the engine will try to sort in this stats after (similarly to info records) +* (optional) NextID: ID of stat the engine will try to sort in this stats before (similarly to info records) +* (optional) Vital-flag (player dies if stat with vital flag drops to 0) +* (optional) ID of level-up function: Takes the reference of the character as argument and returns an integer value representing the max stat change from the level-up. +* (optional) ID of time passed function: Takes the reference of the character and the duration as arguments. Returns a float that is used to modify the current stat value. Not used when waiting or sleeping. +* (optional) ID of wait function: Takes the reference of the character, the duration and a sleep flag (integer) as arguments. Returns a float that is used to modify the current stat value. +* Default value: If an actor does not have this stat (possible if the stat was defined in an addon and not in the base-game) this value is used for the maximum value of the stat. + +Scripts for default stats are injected as necessary. Some of these scripts require access to GMST values. We need to figure out how to implement this, either grab the value from the GMST when adding the script or adding a script function to read GMST values. The later is easier but will not allow us to remove the redundant GMST record easily. + +The currently existing Get, Set and Mod instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the stat ID as an argument. We need separate instructions to deal with base, modified and current values. + +## Attributes + +We will unify attributes and make them configurable. For stage 1 we will not allow the deletion of existing attributes (they are used in too many parts of the engine). But we will allow new attributes. + +We add a new record type for attributes. Records for the existing attributes (sys::NameOfAttribute) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). + +A attribute record contains the following information: + +* Name (taken from GMSTs for default attributes) +* Tooltip text (taken from GMSTs for default attributes) +* (optional) PrevID: ID of attribute the engine will try to sort in this attribute after (similarly to info records) +* (optional) NextID: ID of attribute the engine will try to sort in this attribute before (similarly to info records) +* Default value: If an actor does not have this attribute (possible if the attribute was defined in an addon and not in the base-game) this value is used for the attribute. + +Note that all records that reference attributes need to have the respective fields be changed from integers to strings. + +The currently existing Get, Set and Mod instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the attribute ID as an argument. We need separate instructions to deal with base, modified and current values. + +Additionally we will add a new GMST sys::ClassAttributes of type integer. This GMST specifies the number of favoured attributes that a class has. The value defaults to 2. + +## Weapon Types + +We will unify weapon types and make them configurable. + +We add a new record type for weapon types. Records for the existing weapon types (sys::NameOfType) will be created by the engine when loading older omwgame files. + +A weapon type record contains the following information: + +* Name (taken from GMSTs for default types) +* HandlingType (how the weapon is held and what animations to play when attacking) +* Name of HitTestScript (see Combat subsection) + +For stage 1 the weapon type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However there are already possible uses (for example a type of "ancient blades" that require a separate skill). + +We add another script function that returns the weapon type ID of an instance or an object ID: GetWeapopnTypeId. Note that GetWeaponType is already taken by Tribunal and we can not repurpose this name without breaking compatibility. + +## Armour Types + +In vanilla Morrowind armour types exist only implicitly. There is no record and no option to select armour types. Armour types are determined by weight only. + +We will keep implicit armour types as an option, but also add armour types explicitly as a new type of record. + +Records for the existing types (sys::LightArmorType, sys::MediumArmorType, sys::HeavyArmorType) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). + +A armour type record contains the following information: + +* Name (taken from GMSTs for default types) +* (optional) Minimum weight + +Additional armour object records are extended by the optional field "armor type". + +If an armour object does not have this field its type is determined by the weight by the following algorithm: + +* Consider all armour types with a minimum weight field +* Exclude types that have a minimum weight larger than the weight of the object +* Pick the type with the largest minimum weight + +For stage 1 the armour type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However there are already possible uses (for example going Oblivion by killing medium armour or adding a new type of armour "ancient armor" that requires a separate skill). + +We add another script function that returns the armour type ID of an instance or an object ID: GetArmorTypeId. + +## Specialisation + +We will unify specialisations and make them configurable. + +We add a new record type for specialisations. Records for Combat (sys::CombatSpec), Stealth (sys::StealthSpec) and Magic (sys::MagicSpec) will be created by the engine when loading older omwgame files. + +A specialisation record contains the following information: + +* Name (taken from GMSTs for default types) + +We add a new script instruction GetSpecialization id, that returns the specialisation of a class or a skill with the given ID. + +## Magic Schools + +We will unify magic schools and make them configurable. + +We add a new record type for magic schools. Records for the existing magic schools will be created by the engine when loading older omwgame files. + +A magic school record contains the following information: + +* Name (taken from GMSTs for default schools) +* Specialisation (sys::MagicSpec for default schools) +* Resource (sys::Magicka for default schools) + +A use for the specialisation field would be (for example) dividing magic up into two categories "divine magic" and "arcane magic" and have characters specialise accordingly. + +## Skills + +Skills are a complex topic and we won't achieve full de-hardcoding in stage 1 and most like there will never be a 100% complete de-hardcoding. But there is still a lot we can do with skills. + +Currently skills exist as indexed records. We need to move these over to ID-based records. The exiting indices are translated to IDs of the form sys::NameOfSkill. + +We add a new sub-record type (Function Sub-Record, see below). Each skill can have zero, one or multiple function sub-records. + +The following skills are too tightly bound into the engine to allow their easy removal: + +* Armorer +* Enchanting +* Alchemy +* Acrobatics +* Block +* Sneak +* Athletics +* Hand-to-hand +* Unarmored +* Mercantile +* Speechcraft + +Therefore we will (for now) forbid the deletion of the respective skill records. All other default skills can be deleted. + +### Scripts Instructions + +The currently existing Get, Set and Mod instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the skill ID as an argument. We need separate instructions to deal with base and modified values. + +We also introduce new script instructions: + +* UseSkill id, v: Progress skill id by a value of v (float). +* UseValueSkill Id, i: Progress skill id by the value given by use value field with index i (0, 1, 2 or 3) +* Get{x}SkillLevel s_id: Return the skill level of a function sub-record with TargetId s_id (string). {x} is either Armor, Weapon or Magic. See next sub-section for details. + +Note that we do not de-hardcode the fixed number of use value fields. The usefulness of these fields is most likely limited for new skills. The UseSkill instruction, which bypasses the use value fields should be sufficient in most cases. We only add the UseValueSkill instructions to better support script interactions with default skills. + +### Function Sub-Records + +A function sub-record describes how a skill interacts with hardcoded engine functions. A function record consists of a function ID (we use an index here, because the list of functions will definitely not be extensible by content files) and additional arguments. + +For stage one we require only one additional argument: + +* TargetId (single string value) + +For stage 1 we introduce three function IDs: Armour skill, weapon skill, magic skill. + +We do not forbid non-unique function sub-records; meaning two skills may have identical function sub-records (e.g. two skills governing light armor). If there is more than one skill with the same function sub-record and the skill level for this function needs to be considered, we use the maximum over all relevant skill levels. + +### Armour Skills + +We make armour skills fully customisable by introducing a function ID for armour types. Any skill that has a function sub-record with this ID is an armour skill. For default armour skills loaded from older omwgame files we will inject function sub-records during the loading process. + +The TargetId of a armour function sub-record indicates the armour type that the skill is governing. + +The default skills are: Heavy Armor, Medium Armor, Light Armor. + +### Weapon Skills + +We make weapon skills fully customisable by introducing a function ID for weapon types. Any skill that has a function sub-record with this ID is a weapon skill. For default weapon skills loaded from older omwgame files we will inject function sub-records during the loading process. + +The TargetId of a weapon function sub-record indicates the weapon type that the skill is governing. + +The default skills are: Spear, Axe, Blunt Weapon, Long Blade, Marksman, Short Blade + +### Magic Skills + +We make magic skills fully customisable by introducing a function ID for magic schools. Any skill that has a function sub-record with this ID is a magic skill. For default magic skills loaded from older omwgame files we will inject function sub-records during the loading process. + +The TargetId of a magic school function sub-record indicates the magic school that the skill is governing. + +The default skills are: Illusion, Conjuration, Alteration, Destruction, Mysticism, Restoration + +## Weather + +We make weather types customisable by moving from hard-coded index based weather effects to ID based weather types stored in content files. To this end we introduce a new record type (Weather). When loading older omwgame files we will inject weather records for the ten default weather types. + +A weather record is made up from sub-records, each describing a weather effect. A weather record can have any number of weather effect sub-records. + +There are three types of effect sub-records: + +* Magic effect: A magic effect that is applied to all actors within active cells while the respective weather type is active. +* Sky: Modifications to the visuals of the sky (e.g clouds) +* Particles: Particle effects (e.g. rain) +* Event: Events that can happen with a certain probability while the weather type is active (e.g. lightning). Events happen at random locations. + +Magic effect sub-records contain the ID of the respective magic effect. All other sub-records contain an integer ID specifying the effect and (if necessary) additional parameters. + +All effects described in these sub-records affect active cells only. + +### Sky + +TODO all existing sky effects and possibly new ones; bugger scrawl to fill in the details + +### Particles + +TODO all existing particle effects and possibly new ones; bugger scrawl to fill in the details + +### Event + +We will add the following event IDs: + +* 0: Scripted Event: Additional data consists of the name of the script to be run when the event is triggered. The script takes the following arguments: worldspace (string), location (Vector3), weather ID (string) +* 1: Lightning + +In addition to the event ID and event ID specific data event sub-records have an event trigger block. + +### Event, Trigger + +* Chance: The chance of the event happening (floating number in the range between 0 and 1) per unit of time. We may want to make the unit of time globally configurable with a new GMST. +* Min-Distance (optional): The minimum distance from the player at which the effect can take place (0 by default). +* Max-Distance (optional): The maximum distance from the player at which the effect can take place (infinite by default): + +### Scripts + +We need to introduce new script instructions regarding to weather, since the existing instructions are based on fixed number weather types or integer indexed weather types. These instructions can not be salvaged and we therefore declare them deprecated. + +New script instructions: + +* SetWeather (region ID, weather ID): Replaces change weather. Set current weather for a region. +* GetWeather (region ID): Replaces GetCurrentWeather. Returns current weather for a region. +* SetRegionWeather (region ID, weather ID, chance): Replaces ModRegion. Modify a single entry in the weather table of a region. Chance is of type long. We relax the requirement that the sum of the chance values need to be 100. +* UpdateRegionWeather (region ID): Forces a new roll on the weather table of a region. + +## Water + +We currently have only a single water type that is hard-coded. We name this water type sys::Water and inject a suitable record of the new type (see below) during loading of older omwgame files. + +We generalise the concept of water to liquid by introducing a new record type (Liquid). Liquid records can be used both for different looking water type (more swampy water, water with different colouration) but also for completely different liquid types (e.g. Lava). + +To support the water type sys::water we also need to add a new magic effect (sys::SuffocationEffect). + +Liquid records are referenced both in worldspace records (see Cells & Worldspaces section) and in body of liquid records (see Misc section). + +A liquid record consists of the following fields: + +* Effects: IDs of zero or more magic effects that applied to actors while in the liquid. +* Submerged Effects: IDs of zero or more magic effects that are applied to actors while completely submerged in the liquid. +* Liquid type ID: Integer, hard-coded +* Additional parameter, specific to liquid type + +We need to support at least one liquid type (ID 0: Water) from the start. Other types can be added. + +TODO check with scrawl about other ways to handle visuals for liquid types that requires less hard-coding. Shaders? + +## Magic Effects + +Currently we have a fixed number of magic effects that are referenced by an integer index. + +We move over the magic effect records to a string ID based scheme. + +We also introduce a new effect (sys::SuffocationEffect) by injecting a record when loading from older omwgame files. This effect triggers the normal procedure for running out of air. Note that this will require creating a new effect icon. + +Effect records can be deleted without restrictions. A content developer can add new effect records, again without restrictions. + +We add a field to the effect record that contains the effect's name, currently stored in a GMST. + +We also add an optional field that contains the ID of a script function that is called when the magic effect takes effect. The function takes two arguments: + +* The reference of the effects source. This would be a spellcaster in case of a spell or an item in case of an enchantment. +* A list of target references. + +Furthermore we add a second optional field that contains the ID of a script function that is called when the magic effect ends (only relevant for non-instant effects). The function signature is the same. + +For the existing effects we will inject scripts that match the previously hard-coded effects when loading from an older omwgame file. This will require the addition of a significant number of new script instructions that are all trivial, since they will just call existing engine functions. + +It is important to generalise as much as possible when creating these new script functions. For example we won't have a DivineIntervention script instruction. Instead we add a GetClosestMarker function and use existing functions for querying position and cell and then use the new Teleport instruction. + +## Input + +We allow the addition of customisable input functions that can be bound to keyboard buttons in the usual way and can trigger the execution of scripts. Existing input bindings are not affected by this. + +To this end we introduce a new record type (Input): + +* Label +* Tooltip (string, optional) +* Default key (integer) +* Key down event script (string, optional): A script that is executed when the key is pressed. +* Key up event script (string, optional): A script that is executed when the key is released. +* Key pressed event script (string, optional): A script that is executed periodically while the key is down. +* Key pressed delta (float): Minimum time between two executions of the key pressed event script. Defaults to a reasonable value in the absence of this field. + +## Held Items + +We merge the probe and lockpick object types into a new object type: Held Item. The security skill related functions are handled via the ItemObjectInteraction script subrecord. When loading older omwgame files a suitable script in the sys namespace is injected for probing and lockpicking each. When transforming probe and lockpick records into Held Item records a ItemObjectInteraction subrecord referencing the respective script is injected. + +If we enhance the animation system we may need to add additional subrecords that specify the pose for holding the item or the animation for using it. + +## Pickpocketing + +We add a new GMST (sys::ReversePickpocketing) of type integer that indicates if reverse pickpocketing is allowed (value other than 0) or not (value 0). + +We move the function for checking if pickpocketing succeeded from the C++ code to a new script function (sys::PickpocketTest). We inject this function when loading older omwgame files. + +The function takes the following arguments: + +* ref to thief actor +* ref to victim actor +* ref to item to be stolen +* reverse flag: integer, 0 for regular pickpocketing, 1 for reverse + +The function returns 1 if the pickpocket attempt succeeded and 0 if it failed. + +Items with the nopick tag are excluded from pickpocketing. + +## Combat + +De-hardcoding combat is a monumental task that we can not hope to complete within the time frame of stage 1. We will de-hardcode some parts that do not depend on large scale improvements to animation and can be handled with the scripting improvements in stage 1. Any further enhancements are left for stage 2 or more likely OpenMW 2.0. + +### Hits + +We move the hit test check for C++ code to script functions. The following functions will be used: + +* For melee weapons the HitTest script function defined in the respective WeaponType record; arguments: ref to weapon, ref to target +* For ranged weapons the HitTest script function defined in the respective WeaponType record; arguments: ref to weapon, ref to target, ref to ammunition +* For unarmed attacks the function sys::UnarmedHitTest; arguments: ref to weapon, ref to target + +All functions return an integer: 0 (no hit), a value other than 0 (hit) + +sys::UnarmedHitTest and default functions for armed hit tests (sys::MeleeHitTest, sys::RangedHitTest) are injected when loading older omwgame files. + +### Script Enhancements + +We add a new script function: + +* GetTargetRef: Returns the current target of an actor. Returns a NullRef if the actor is not in combat. Also returns a NullRef when used on an instance that is not an actor. + +We add two new script instructions: + +* DeclareWar l: Similar to StartCombat, but specifies a whole group of enemies. l is a list of references. +* DeclarePeace l: Ends the hostilities resulting from previous combat towards the actors listed in the reference list l. We may need to add a grace period after this instruction during attacks do not cause hostility again, so we can handle non-instant attacks. + +### Script Hooks + +We add three new script hooks. + +sys::Kill is executed whenever an actor is killed. Scripts for this hook take the following arguments: + +* Ref to target +* Ref to actor who killed the target (may be a null ref) + +sys::CombatStarted and sys::CombatEnded are executed when the player enters or exits combat, respectively. + +## Music + +### Playlists + +We add a new record type: PlayList + +The playlist record has the following fields: + +* Probability: Float [0.0-1.0] +* Music Directory (optional): String +* Title (can appear more than once): String + +The titles on the playlist are all the titles in the music directory and the titles listed explicitly. + +When playing a playlist first the probability is considered to randomly decide if a track is played or not. Then a track from the list is chosen. + +If a playlist is played in loop mode the process above is repeated whenever: + +* The current track ends +* If no track is playing at reasonably chosen time intervals + +### Background and Event Music + +We distinguish between two types of music: + +* Background Music +* Event Music + +Background music is music that (potentially) runs all the time, unless explicitly stopped. Background music is always looping. + +Event music is played only on certain events. Event music can be played looped or only a single time. In the former case event music needs to be stopped explicitly. Once event music ends background music is resumed exactly where it was interrupted. + +Background and event music persists through save-load-cycles. + +### Script Enhancements + +We add several new script instructions: + +* PlayBackgroundMusic (t) +* PlaylistBackgroundMusic (pl) +* StopBackgroundMusic +* PlayEventMusic (t, loop=0, force=0) +* PlaylistEventMusic (pl, loop=0, force=0) +* StopEventMusic + +Arguments: + +* t (string): a single title (specified by its location in the data directory) +* pl (string): a playlist (specified by its ID) +* loop (integer): looping or not +* force (integer): Terminate any other running event music first. If this flag is 0 and there is already event music playing the new event music is ignored. Event music provided by content developers will usually not set this flag. + +Note: An attempt to play a track or playlist that is currently playing is ignored. In this case the track or playlist does not restart. + +### Transition of Existing System + +We completely replace the old music system with a new script-based one. The StreamMusic script instruction is flagged as deprecated. + +New records are injected when loading from an older omwgame file. + +We add two playlists (sys::ExplorePlaylist and sys::CombatPlaylist) which are pointed to the usual directories and play with a probability of 1.0. + +We add a GMST (sys::DefaultBackgroundMusic) with a default value of "sys::ExplorePlaylist". Whenever starting a new game or loading a game with no background music running the playlist specified in this GMST is played as background music. If the GMST holds an empty string no background music is played by default. + +We add a script to the sys::CombatStarted and sys::CombatEnded hook each. The former plays sys::CombatPlaylist looped and forced. The later stops event music. + +### Location-Based Background Music + +We add an optional string sub-record to the records listed below. This field contains a single track or a playlist, that determine the background music for the respective location. + +* Worldspace +* Region +* Area +* Cell + +More specific entries (towards the bottom of the list) take precedence over more general entries. Global background music (controlled via script instructions) has the lowest priority. + +## Character Creation + +The process of character creation currently allows for very little customisation. We redo the character creation process with the following goals in mind: + +* Allow more control over how the individual steps are chained together +* Allow additional steps (both future hardcoded enhancements like additional visual customisation and custom steps entirely defined in content files) +* Allow replacement of existing steps with alternative methods + +Currently the character creation process runs through these steps: + +1. Choose Name +2. Choose Race and appearance +3. Choose a method for class selection and perform class selection (3 methods) +4. Choose birthsign +5. Review + +### Chaining + +We add two new GMSTs to control how the steps of character creation are chained together: + +* sys::NewCharBack: Integer; allow going back to previous steps (not being able to go back also implies that there is no going forward) +* sys::NewCharSteps: String-List: List of scripts associated with the steps of the character creation process. The scripts are listed ordered according to the sequence they are executed in. + +Each character creation step has a an associated script that is called when the step is reached (either via a EnableX script instruction or via the player going back or forth). + +### Script Instructions + +We add several new script instructions. + +* CharacterCreationRun (n): Opens the respective GUI/window +* SetPlayerName (name) +* SetPlayerRace (id) +* SetPlayerFace (id) +* SetPlayerHair (id) +* SetPlayerSex (i): 0 or 1 +* SetPlayerClass (id) +* SetPlayerBirthsign (id) +* CharacterCreationBack: Go back to previous step +* CharacterCreationNext: Go to next step (ignored if the next step had not been accessed before) + +n is an integer argument with one of several hard-coded integer values: + +* 0: Name GUI +* 1: Race GUI +* 2: Class selection via picking an existing class +* 3: Class selection via creating a class +* 4: Birthsign GUI +* 5: Review GUI + +### Transition of Existing System + +When loading older omwgame files we inject two GMSTs (sys::NewCharBack and sys::NewCharSteps) as well as several scripts. + +For each step we add a new script that consists of only a single line: + +```` +CharacterCreationRun n +```` + +where n is the appropriate index. + +Only steps #3 will be handled differently than described above. + +The class selection method GUI can be implemented entirely with a message box. Therefore we scrap the hard-coded method selection window entirely. + +Likewise the character selection method of answering a set of quests can be implemented via a set of message boxes. Therefore we scrap this window too. + +## Level-Up + +The vanilla (character-)level-up system allows almost no modification. We keep the existing hard-coded system, but make it more configurable, and we also allow content developers to completely replace the existing system. + +### State Un-Hiding + +Checks for a level up are based on the skill progression since the last level up. In vanilla this state is not accessible to content developers or players. + +We address this issue by turning these hidden variables into record variables attached to the player character. The variables have the type long, a name matching the ID of the skill and are created on the fly whenever the engine detects a skill level up. Note that regular script instructions that change skill level do not count towards level ups. + +We also add another record variable (LevelUpCount) that counts the number of major and minor skill level-ups. + +By binding these stats to a character instance instead of using global stats we are also making preparations for optional future level up mechanics for NPCs (companions or otherwise). This is most likely a task for stage 2 though. + +### Trainers + +Some NPCs are flagged as skill trainers. These trainers can train the player in their three best skills. Skill levels gained via training count towards level-up. We make the following enhancements to this mechanism: + +* New GMST (sys::SkillTrainCount, integer): Replaces the hard-coded value of 3 skills. +* New script (sys::SkillTrainingTest): Tests if NPC can train player in a skill (returns 0 or 1). This test is performed after the faction standing test and after the skill list is trimmed down via sys::SkillTrainCount. The function takes the following arguments: ref to player, ref to NPC, ID of skill +* New script (sys::SkillTrainingApplied): Executed after training has been completed. Arguments are the same as with sys::SkillTrainingTest. + +When loading from an old omwgame file GMST and scripts are injected. sys::SkillTrainCount defaults to 3, sys::SkillTrainingTest performing the usual tests on skill level and attributes and sys::SkillTrainingApplied defaults to an empty script. + +### Level-Up Test + +The level-up test is performed when either the player rests (by default via T key) or when the ShowRestMenu function is called. + +The test is performed by calling the function sys::LevelUpTest, which returns either 0 or 1. If 0 is returned no further action is taken. If a value other than 0 is returned the usual level-up procedure continues. Note that this function is allowed to initiate a custom level-up procedure and return 0. + +The function takes two arguments: + +* A ref to the player +* An integer that indicates what started the level-up test (-1 for resting) + +We enhance the ShowRestMenu instruction with an optional long argument (defaults to 0) that is passed to the sys::LevelUpTest function as second argument. + +When loading from an older omwgame file we inject a sys::LevelUpTest script that matches the hardcoded version of the level-up test. + +### Level-Up Procedure + +We keep the level-up procedure but replace several hardcoded values with GMSTs. + +* Number of major skills: sys::MajorSkillCount (default: 5) +* Number of minor skills: sys::MinorSkillCount (default: 5) +* Number of attributes to increase during level-up: sys::AttributeLevelUp (default: 3) + +We add a new function (sys::AttributeIncrease) that determines the number of points an attribute can be increased. The function takes a reference to the player and the ID of the attribute as argument and returns the number of attribute points. As usual we inject a matching function that matches the hardcoded implementation. + +We add localisation records (see section about localisation) of the form default::LevelUpMessageX, where X is a positive integer number. When loading older omwgame files these records are injected based on level up fallback values. We use the default namespace instead of sys, because content developers need to be able to add their own records. + +We add a script (sys::LevelUpFinished) that is called after the level-up procedure has been completed. The script takes a reference to the player character as its only argument. + +The default implementation (injected when loading from an older omwgame file) resets the skill record variables and reduces the LevelUpCount variable by 10. + +### Level-Up Image + +The level up dialogue contains an image that depends on how the skill increase is distributed among specialisations. The function that decides about this image is very simple. However since specialisations won't be hard-coded anymore we can not simply pass these values into the function without first establishing more involved script support than planned for stage 1. + +This is actually not a problem since we can simply track the increase values in record variables, which may also prove useful for other purposes. + +With this modification the C++ function (MWGui::LevelupDialog::getLevelupClassImage) can be directly translated into a script function (sys::LevelUpImage). + +Note that this change does not allow for easy addition of new classes to the scheme. This is not a feature present in vanilla MW either. We may decide to investigate other options than simply translating the C++ function into a script function. + +## Trading + +### Pricing + +The algorithm for trading (implemented in MWMechnics::Trading::haggle) can be translated straightforward into a script function once all stage 1 script enhancements are in place. + +### Restocking Money + +Currently the frequency of money restocking is controlled by a the GMST fBarterGoldResetDelay. Money is always reset to the original value. + +We make all of these aspects configurable by adding a new field to all actor record types (NPC, creature) that contains a script ID. + +The script takes a reference to the actor as its single argument. + +When loading older omwgame files we inject a script (sys::UpdateGold) and inject this ID into all actor records that are flagged for merchant services. We also add a record variable (last restock). + +The script checks the current time against the last restock variable and the GMST and resets the gold if necessary. + +### Restocking Items + +Currently items are restocked after every trade. Items in the merchant's inventory and in all containers owned by the merchant are restocked to their original count. + +This process is complicated by the existence of levelled item lists, which makes it too complex to re-implement in a script. + +We will make several changes to increase the flexibility of item restocking. + +First we move the re-fill operation from after a trade is completed to before a trade is completed. This should not affect existing content. + +We add three more scripts (optional, specified via new string fields in the actor's record) that hook into the re-fill process. + +1. Check if a re-fill should be performed; called at the beginning of every trade cycle. Arguments: ref to actor, integer (1 if first trade since talking to the merchant, 0 otherwise). Returns an integer (0 no re-fill required, otherwise re-fill is required). If script is not present a re-fill is performed). +2. Called for every item that the re-fill algorithm flags for removal. Arguments: ref to actor, ref to item. Returns an integer (0 if item should be kept, otherwise item is removed). If script is not present the removal takes place. +3. Called at the end of re-fill procedure. This gives content authors the opportunity to make other modifications. Arguments: ref to actor. No return value. + +## Fast-Travel + +Morrowind offers a wide selection of fast travel options. Each of these fall into one of three categories: + +1. Spells and teleport items +2. Travel to fixed locations via paying a travel NPC (boat, slit strider, gondola, guild guide) +3. The propylon chamber system + +With vanilla Morrowind and other stage 1 changes #1 and #3 are already sufficiently configurable. #2 is not. + +In stage 1 we will improve the flexibility of the travel network system and also its usability both for content creators and for players. + +### Travel Network Record + +We introduce a new record type (Travel Network) that consists of the following fields: + +* Colour (integer) +* ID of Cost-Script (string): Takes three arguments (ref to travelling character, ID of source location record, ID of destination location record) and returns a long (number of coins). This is the value before bartering and followers are taken into consideration. +* ID of TravelTime-Script (string): Takes three arguments (ref to travelling character, ID of source location record, ID of destination location record) and returns a float (duration in hours) +* ID of TravelCompleted-Script (string, optional): Takes three arguments (ref to travelling character, ID of source location record, ID of destination location record). Executed when a character has finished travelling. +* Auto-Knowledge (integer, flag): Is the travel network fully known from the beginning of the game (!=0) or do locations need to be discovered to be usable (0). +* Visible (integer, flag): Is the travel network visible on the map? + +### Travel Location Record + +We introduce a new record type (Travel Location) that consists of the following fields: + +* Travel Network ID +* Location worldspace (String) +* Location position and orientation (Vector4) +* Parent worldspace (String, optional) +* Parent position (Vector3, optional) +* List of IDs for travel locations that can be reached from this travel location + +By formalising travel locations in this way we make it easier to modify travel locations later on (currently a modder would have to go through all travel NPCs of the same network, if he wanted to move a travel location) and to build visualisation features (see below). + +### Travel Sub-records in NPC records + +We keep the existing sub-record structures for travel since there is no automated way of transiting them to the new scheme. This task will be left to mod developers. + +NPCs supporting the new scheme will have a single field that specifies the ID of the associated travel location record. + +### Transition of Existing System + +When loading older omwgame files we inject suitable travel network records. Unfortunately there is no procedural method to distinguish between boat, slit strider and gondola, so these will all have to be grouped together into an sys::TravelOthers network. It will be up to mod developers to untangle these. +The travel network records will be created with the visible flag set to 0, because old-style travel data isn't available at load time and therefore visualisation can not work. + +Travel locations can not be automatically transitioned to the new system. This task is left to mod developers. + +### Block List + +The layout of a travel network is determined by the data in the content files and can not be changed during the game. However we do offer several methods to configure a network during the game. + +* Location Block List: Individual locations can be dynamically blocked, either for incoming or outgoing connections or both. +* Connection Block List: Connections between two locations can be blocked dynamically. This blocking is directed, meaning blocking the connection from A to B does not automatically block the connection from B to A. + +### Visualisation + +We offer visualisation somewhat similar to [this](http://en.uesp.net/wiki/Morrowind:Transport). + +Each travel network has a colour associated with it that will be used to mark connections. If a travel location has a parent section, the parent section will be used instead to determine the location (e.g. exterior location for guild guides inside a building). + +Only networks flagged as visible are drawn. We may add a GUI element that allows showing/hiding of visible networks either as a group or individually. + +### Script Enhancements + +We introduce several new script functions: + +* GetTravelLocationWorldspace (id): Returns String +* GetTravelLocation (id): Returns Vector4 +* GetTravelParentLocationWorldspace (id): Returns String +* GetTravelParentLocation (id): Returns Vector3 +* GetTravelLocationList (id, ignore_blocked=0): Returns a list containing the IDs of all locations of the travel network id +* GetTravelDestinationList (id, ignore_blocked=0): Return list of IDs of destination travel locations for the travel location id + +We also introduce several new script instructions: + +* SetTravelVisibility (id, visibility): Sets the visibility (flag) of a travel network (id) +* RevealTravelLocation (id): Flag a travel location as known +* BlockTravelConnection (source-id, destination-id, blocked): Block or unblock a connection between two locations +* BlockTravelLocation (id, incoming-blocked, outgoing-blocked): Block a travel location. + +# NPCs & AI + +## Navmeshes + +The current system of waypoints is limited. It is unlikely that we can improve it much. This makes pathfinding one of the few cases were we seriously need to consider completely abandoning an existing system in favour of a new one (navmeshes in this case). + +## Waypoints + +Waypoints can still be useful, but in a different way. Content developers will often designate a specific location for a NPC to stand at or walk to. Currently the easiest method to achieve this is to place an instance at the location, note down the coordinates and then enter them into a script or a field in a record. This is not an efficient approach and also causes maintainability issues, when a location that is used in multiple parts of a content file needs to be moved. + +We introduce a new record type (Waypoint) with the following fields: + +* Position (Vector3) +* Worldspace/Cell Id (String; cell takes precedence over worldspace) + +We also add a new script function: + +* GetWaypointPosition (id): Returns a Vector3 + +Waypoints are not part of the cell data structures because they need to be accessed independently of cells and we don't want to search through (potentially) every single cell every time a waypoint is referenced. + +## NPC Schedules + +NPC schedules are an advanced topic and as such would be a better fit for stage 2. However schedules are also a very iconic feature with a large impact on the playing experience. Therefore we will consider them for stage 1. The following is a preliminary proposal that is in need of more design work. + +### Active and Passive Cells + +We run schedules only in active cells. But we also need to consider passive cells. + +Here are a few examples. For simplicity lets consider a one-dimensional cell grid with the usual pattern of one cell around of the player cell being active. + +For this example let the player be in cell 0. Now consider NPC A, who is in cell 2 (not active). He has a schedule that takes him to cell 1 sometimes. As a player we would expect to see him from the distance then. + +Another example with the same setup as above: While A is moving within cell 2 towards cell 1, the player relocates to cell 2. He would to see A make his way towards cell 1. Moreover if the player moves back to cell 0 and then returns to cell 2, he would expect to see A having made some progress. + +Conclusion: We must track all actors who's schedule can take them into active cells. + +It is not clear yet how to achieve this goal best. In the most simple implementation we could just estimate the time it takes to perform a task in a schedule and spawn the actor into a active cells accordingly. This would however not cover the second example. + +Alternatively we could invent a semi-active cell state and apply it to all cells that contain NPCs who's schedule can touch the active cells. This semi-active state would not run any scripts, nor would it render or run animations. And it may use a simplified/limited form of physics. This approach would cover all cases. However it has the potential for a severe performance impact. + +Again alternatively, we may offer both implementation and switch between them base on settings/available processing power. + +### Actor Tracking + +Independently from the chosen implementation we need to track all actors who's schedules are relevant to the active cells. This is a problem because with the default data structures we would at least need to scan every single cell on startup for relevant actors. The performance impact of such an approach is most likely not acceptable. + +Therefore information about which actor's schedule is relevant to which cell needs to be stored in a separate data structure that exists outside of the cell records. + +### Schedule Record + +We add a new record type (Schedule). A schedule consists of a list of task sub-records. + +Each task sub-record consists of: + +* A time of the day range +* An AI package +* Ignore flag (see next section) + +The time table of a schedule is allowed to have holes (e.g schedule 1 ending at 10:00 and schedule 2 beginning at 10:15). + +Task may also overlap partially, but no two task may start at the same time. + +When picking a task from the schedule all task which contain the current time are considered. Of these the task with the earliest start time is chosen. + +A new task is picked only when the current one has expired. + +### Managing Actor Schedules + +Schedules can be assigned to actors either in the actor record or with a new script instruction. + +When explicit switching from one schedule to another, tasks with the ignore flag are ignored. This feature is intended for transitory tasks, that are pointless if the previous task hasn't been performed. + +We add script instructions that allow for querying the current schedule of an actor. We also add a script instruction to prematurely end a task. + +## AI Packages + +To accommodate the NPC schedule feature and other enhancements to the AI system properly we need to redo the AI package system. We will deprecate the old packages. + +This is an early draft of the new system and will almost certainly require more design work before we can cut it up into individual tasks. + +### Idle + +Idle activities are currently bound to the Wander package. This is suboptimal, both because other packages could benefit from it and because storing the idle function in a package instead of the NPC makes it hard to give the NPC a characteristic idle pattern. + +We add a list of idle chances for each available idle animation to the NPC record. Ideally this list should be extensible, but first we need to improve the animation system so that the number of idle animations isn't hard-coded anymore. This may be a stage 2 task. + +Each package contains two fields related to idling behaviour: + +* Chance: A value between 0 and 1 that specifies the chance (per reasonable unit of time) of an idle animation to be executed. +* Dialogue Chance: A value between 0 and 1 that specifies the chance of an idle dialogue if an idle animation is executed. + +We may decide to use a scale of 0 to 100 instead of 0 to 1 to increase usability for less mathematical minded content developers. + +The old wander package keeps its old idle list. When active it disables the new idle system. + +If a NPC does not have any active packages the idle function is still active. In this case Chance and Dialogue Chance are determined by two new GMSTs, which default to 0 when loading older omwgame files. + +### Package Roles + +We categorise package into four roles: + +* Legacy: Old packages, generated either via script instructions or via AI package list in actor record. Removed once it runs out (if not flagged for repeat). Deprecated. +* Schedule: Inserted by the schedule system. Can not be directly manipulated via script instructions. +* Manual: New packages, generated via new script instructions. +* Situation: Inserted via other game mechanics (e.g. combat, breath). Can not be directly manipulated via script instructions. + +Packages of a type lower in the list take precedence over packages higher in the list. For the roles schedule and situation there can be only one package per actor at a time. + +We add new script instructions to query the package state of an actor and to insert and remove manual packages. + +### Duration + +Each package has two fields that describe its duration: + +* Duration itself: Package will self-delete once the current time is larger than the start time plus the duration) +* Repeat flag: Package adds itself again at the end of the end of the duration + +Packages lacking these fields persist indefinitely unless explicitly removed. + +Some legacy packages already have these fields. Schedule packages will not have these fields since the removal is controlled entirely by the schedule system. + +### Updates Packages + +We translate the packages AiFollow, AiActivate and AiTravel into the new format. + +AiFollow and AiTravel specify a location by given a set of coordinates and an optional cell ID. We will replace part of the package with a different format. + +There are two possible locations sub-records of which each package may only contain one: + +* A set of coordinates and a worldspace/cell ID (not optional) +* The ID of a (new type) waypoint (no worldspace ID required, since the waypoint includes that) + +The other old-style packages require more extensive modifications. + +### AiEscort + +We can largely keep AiEscort with the following modifications: + +* We use the new format to describe the target location (see above) +* Instead of a single location the package lists a sequence of locations, which will allow content developers to make actors escort the player along certain paths and around dangerous areas without resorting to multiple AI packages. + +### AiWander + +Instead of a location and distance based AI wander, we add a new package that limits the wandering to a single area. + +There shouldn't be much need for this package, since AiPatrol and AiVisit offer better alternatives. But we offer it anyway to cover cases where neither of the new packages is seen as useful. + +### AiPatrol + +This is a new package with no direct equivalent in the old system. The package contains the following fields: + +* A list of locations (in the new format) +* A loop flag + +The actor will walk from one location to the next. Once he reached the last location he will either continue with the first location (if the loop flag is set) or walk down the list of locations in opposite direction and then start over (if the loop flag is not set). + +The obvious use case for this package is a guard patrolling a perimeter or a place. + +### AiVisit + +This is a new package with no direct equivalent in the old system. The package contains a list where each list item contains the following fields: + +* Location +* Duration (float) +* Chance (float) + +An actor with this package will randomly choose a location from the list according to the chance field (locations with higher chance are picked more often), then move to this location and stay there until the duration has expired and then continue from the top. + +This package is intended as an alternative to the old AiWander. Hopefully (by giving the NPCs actual places to go to instead of just wandering around aimlessly) it can achieve NPC behaviour that is less silly looking and at least gives the appearance of making sense. + +# Localisation + +## Problems + +There are two fundamental problems with the way localisation is handled in vanilla MW: + +* Localised strings are used as internal identifiers that are referenced by other parts of the content +* Localised strings are intermixed with other records + +This results in multiple issues: + +* When localising the developer must hunt down all references to the localised strings and change them manually (cell names and topics). +* When updating a localisation to a new version of the content file the translator has to manually pierce together his old translation and the new content file. +* Changing a string that is used as an internal reference breaks the connection to the referenced records in relation to the unlocalised file, which in turn makes it impossible to utilise automated tools that help for the localisation process. +* Content files of mismatching languages generally can't be combined in a content file list. Mixing languages is generally not desirable for playing, but may sometimes be unavoidable. The ability to mix languages is highly desirable for developers (e.g. debugging). + +## Encoding + +We already use UTF-8 internally. For post-1.0 versions of content file formats we will also use UTF-8 exclusively. + +## Localisation Content Files + +Localisation will be contained in separate content files (omwlang) that are to be loaded on top of the content file to be localised. + +The launcher will treat a content file and its localisation file as a single entity (see section about Identifying Meta-Data). + +We may add a launcher language setting the determines the default language chosen by the launcher. This needs to be a separate option from the legacy encoding setting, which is currently also called language setting. + +## Localisation Records + +A common localisation scheme is to use the English text as a key. This way non-localised text does not need to bother with the localisation mechanism at all. + +This is not a good idea, both in general and particularly in our case: + +* The assumption that English is the base language fails in certain areas of the MW community. +* Text does not always translate 1-to-1 between languages. A popular example is the term Free Software. The free has two meanings: free as in free speech and free as in free beer. But in other languages (e.g. German) there are separate words for these two meanings. By using the word free as a key in multiple places with different meanings it would be impossible to correctly localise the text to German. +* The original text might change through an update (fixed spelling mistake is an obvious use-case here). If the text is used as key that breaks the connection between the text in the original content file and the updates content file and therefore forces the translator to figure out what is going on and then make manual corrections. + +Instead we use an ID based scheme. We add a new type of record (localisation) that contains as a single field the localised text. + +In legacy content files user visible text is contained in other records. On loading of these files we move this text out of these records and into separate localisation files. The ID of the localisation record will be generated from the ID of the other record with a suitable suffix (e.g. $name). + +By factoring out all user visible text into a separate record type we make it possible for the editor to present the text to the translator in a single table. + +We will allow the editing of the localisation record within the UI (table or dialogue) of the main record, so that for a content developer who is not involved in localisation the workflow is not being made more complicated. + +## Fallback-Strings + +The openmw.cfg file contains a collection of fallback strings that were taken from Morrowind.ini. No localisation mechanism is provided (other than swapping out Morrowind.ini files). + +OpenMW uses these strings only when loading older content files. However "older" is a less specific term here than throughout the rest of this document. As OpenMW progresses through feature releases we will continue to add new user visible strings (e.g. labels for new user settings). Therefore we will need a fully functional localisation system for fallback strings alongside the primary record-based localisation system. + +We will allow an optional language-code prefix (lp) in the key: + +```` +fallback-lp-somekey=Some Text +```` + +When looking up fallback strings the preferred language is chosen by the default language setting in the launcher (we may decide to provide a separate setting for fallback strings if this doesn't result in an overly complicated settings UI). + +If a fallback string with this language-code is available it will be used. If no such fallback string is available, the default fallback string (without language-code prefix) will be used exist. The default fallback string must exist for all fallback strings with language codes. Otherwise the cfg file must be considered defective. + +We add an option to the importer tool to import strings only, which then will be merged (with a user-specified language-code prefix) into an existing cfg file. + +Non-legacy fallback strings (added after 1.0) will be added to the global cfg file instead. We will accept localised versions of these strings provided by the community. Since these strings are not part of vanilla MW, there are no legal issues with including these strings in the official repository. + +## Cell Names + +The handling of cell names in vanilla MW is one of the major localisation problems that need to be addressed post 1.0. Cell names are used both as the text that is presented to the user and as the internal identifier by which cells are referenced. This is further complicated by a system that allows the use of partial names. + +We turn the name field (the ID) into an ID-only field. The actual user visible name is moved into an optional localisation record. + +We allow referencing of cells by both ID and name. If the name is used we emit an unobtrusive warning message. + +This change does not fix the problem of already existing localisations that have modified the names of cells. See the section about localisation mapping below for a workaround. + +## Topics + +The situation with dialogue topics is similar to the cell situation and we will apply the same fix, though with a minor modification. + +When referencing a topic by the localised text (instead of the ID) from within a dialogue text (this includes journal entries) we do not emit a warning message. Requiring the use of IDs within the dialogue text would be too much of a burden to the content developers. + +## Scripts + +Vanilla scripts can contain user visible string literals. This interferes with localisation in an unacceptable way. + +User visible string literals within scripts exist in three forms: + +1. MessageBox strings +2. Topics (in AddTopic instruction) +3. Cell Names (in various location related instructions) + +There is nothing we can do to enforce a solution for #1 without causing unreasonable complications for script authors. But we can at least offer options. + +* We add a variant of the new LFormat script function (called just Format) which takes as its first parameter the ID of a localisation record instead of a string literal. +* We add a script function called Text which takes the ID of a localisation record as argument and returns the matching string. + +Issues #2 and #3 can be enforced at least partially. We declare the use of user visible strings (instead of IDs) deprecated. For all new script instructions using the user visible string instead of the ID results in an error. Old instructions produce a warning instead. + +## Localisation Mapping + +The new localisation scheme leaves existing localisations un-addressed. Localisations of Morrowind.esm break the connection of cell names and topics in relation to the un-localised Morrowind.esm (English). + +We provide a workaround for this problem by introducing localisation mapping. Localisation mapping is a language-specific list of ID/user visible text pairs, provided as a text file. A similar scheme exists for the Russian localisation. We may decide to simple extend the existing implementation of this feature in OpenMW to provide localisation mapping. + +Regarding the localisation mapping files two important points need to be considered: + +* The files can not be generated automatically. Theoretically we could develop a tool that checks for cell and topic records in two Morrowind.esm files that match in fields other than ID and other user visible text. However this would still require manual checking and seems overall a lot of work for little gain. +* We should not generate and distribute these files within the OpenMW project because of copyright reasons. This task would be left to the modding community. + +# GUI + +## Argument against general GUI customisation + +Under general GUI customisation we understand a fully featured GUI framework that allows content developers to build their own GUI. This is an argument against such an approach. + +What are the use cases for a full-scale GUI framework? They seem to fall into two general categories: + +1. Alternatives for existing GUI elements (e.g. new inventory system, new load/save GUI, new alchemy GUI) +2. GUI for a completely new game feature that does not exist in vanilla Morrowind + +We have handled some instances of case #1 in the OpenMW engine itself already. These seem to do their job and have been well received. It makes sense to continue with this approach and keep this kind of GUI enhancements in the core project instead of handing it over to mod developers. The needs of total conversions in this regard can be met with a skinning system and additional configuration options (see below). + +The second case is a little different. If we want enable mod developers to add new skills in the same style as for example alchemy we need a full GUI framework. The question is do we want to encourage this kind of content? Morrowind and also OpenMW are already very heavy on the GUI. A feature set that encourages a lighter use of GUI elements (in favour of more world interactivity instead) could result in overall better content quality. + +At a smaller scale the need for interactive GUI features can be fulfilled by individual GUI elements (see below) that can be used from a regular script, similar to multiple choice message boxes. + +Note that by going down this route we do not block the addition of a full-scale GUI framework at a later time, if we should decide that we need one after all. All the features described here are useful in general and can be translated into a full-scale solution. + +## Skinning + +Under the term skinning we understand changes to the look of a GUI without affecting the functionality. Vanilla Morrowind already provides a certain level of skinning-capacity though textures and Morrowind.ini settings. We are going to expand on these and clean up the skinning process. After 1.0 all skinning should be done exclusively through content files and associated resources files. Skinning does not belong into ini/cfg files. + +### Windows + +In general we do not need to provide skinning options for individual windows. Instead we aim for a single set of options that are applied to all windows. This will ensure consistency throughout the GUI. + +This is a preliminary list of elements that require customisation: + +* Window border/furniture textures and sizes +* Text (Font, Colour, Size) +* Window background +* Other Widgets (e.g. Frames) + +### HUD + +Morrowind (and OpenMW 1.0) has only 4 HUD elements: + +* Mini-map +* Active effect list +* Dynamic stats bars with enemy health bar and selected weapon and spell display +* Notifications (non-interactive message boxes) + +For all these elements we need the ability to customise the the size and the position (Notifications are a special case here, see below). But we also need to give attention to individual HUD elements (see subsections below). + +### Mini-Map + +At the very least we should make the width and the height of the mini-map configurable independently from each other. But we also should consider allowing other shapes (maybe defined via a texture). + +### Active effect list + +We need to make the orientation of the effect list configurable (left, right, down, up). + +### Dynamic Stats + +Dynamic stats are more complicated since they merge three elements: + +* Dynamic stats +* Active weapon and spell +* Enemy health bar + +The first two go together well enough, but we should provide an option to decouple the enemy health bar and turn it into its own HUD elements. + +We also need to consider that after 1.0 we won't always have three dynamic stats. There could be more or (in stage 2) less. The HUD element must be build in a way that can adjust its layout accordingly without manual layouting. + +### Notifications + +Currently we have a single hard-coded notification area (bottom centre of screen), where up to a fixed number of text paragraphs can be displayed in a message box style GUI element. + +We introduce a new type of record (NotificationArea) with the following fields: + +* Position (top/centre/bottom, left/centre/right) +* Scale +* Maximum number of messages +* Duration factor (scales how long a message stays on screen, depending on the length of the message) +* Message box layout (size, textures) +* Text (horizontal alignment, size) +* Text-Wrap (on or off) +* Default style (see below) + +When loading older omwgame files we inject a NotificationaArea record (sys::Messages) that matches the old hard-coded notification area. + +We add a new script instructions that replaces the non-interactive variant of the old MessageBox instruction. + +* Message text, style = "", area = "sys::Messages"): If style is an empty string, the default style for the notification area is used. + +### Notification styles + +We introduce a new type of record (NotificationStyle) with the following fields: + +* Colour +* Font (including options for bold and italic) + +When loading older omwgame files we inject a NotificationStyle record (sys::DefaultMessageStyle) that matches the old hard-coded notification area. + +## New Input-Elements + +We add a couple of new input elements that are each represented as a separate window: + +* Item selection: Takes a list of references and returns the list and an index. List is presented as a horizontal icon view of the items. +* List selection: Takes a list of strings and returns the list and an index. List is presented as a vertical list of text items. +* Numerical input: Takes a range, a precision value and a default value and returns a float. +* Text input: Takes a default value and returns a string. + +Additionally each window has a titlebar, an okay button and and optional cancel button. + +For each element we add a new script instruction that brings up the respective element. These instructions take the following arguments: + +* The input values listed above. +* The ID of a script function that is called when the okay button is pressed +* (optional) The ID of a script function that is pressed when the cancel button is pressed. If this argument is missing (or the ID string is empty) no cancel button is shown. +* Window title text + +## Party HUD Element + +We add an optional HUD element that shows the status of companions. This HUD element can be switched on/off and configured in the user settings. + +For multiplayer we can also show the status of other players. There need to be visual distinction between companions and other players. + +## Container Windows + +The vanilla Morrowind container windows (inventory, NPC inventory on loot/trade, actual container) has a very poor usability. + +Main issues are: + +* Icons are not distinguishable enough. Users need to regularly utilise tooltips to identify an item which defeats the purpose of the icon. This problems is caused both by the large number of different items in Morrowind and by the small icon size which does not leave much room for visual differentiation. +* The available tools (item category tabs) are insufficient to manage a large inventory. + +We need to improve upon this issue, but we also need to stay close to the original look and feel. We will make three improvements in a way that is as unobtrusive as possible: + +### View Modes + +We offer three view modes similar to classical file browser: + +* Small Icon Grid: The vanilla Morrowind mode. If no small icon is available, we use a down-scaled version of the large icon. +* Large Icon Grid: Like the vanilla Morrowind mode, but with larger icons (at least twice, but probably three times the size). We add a new field to all item type records that contains name of the large icon texture. If no large icon is available an up-scaled version of the small icon is used. +* Table: One line per item (or item-stack). The columns shown are configurable in the user-settings. + +The view mode is selected by three buttons that could be placed in the same row as the category tabs. + +### Search + +We add a search box (either fixed if we can make space for it or as a pop-up). If the search box is not empty only those items are shown those names contain the search string. + +### Categories + +We make item categories configurable via content files by introducing a new record (ItemCategory) with the following fields: + +* Category name +* Item filter + +The item filter will be based on item tags (see section Item-Interactions & -Management). These need to be boolean expressions (e.g. tag1 and tag2 or tag3 or not tag4) since a single tag won't allow for enough customisation. Since this is an issue sensitive to scaling (people have large inventories) we should aim for an implementation that is simple and fast. + +When loading older omwgame files we inject ItemCategory records for the vanilla categories. + +We may explore an alternative GUI layout consisting of a vertical bar (maybe on the right side of the window) that contains an icon for each category instead of text. + +The current horizontal text-based category selector is problematic for two reasons: + +* Since we hand over the creation of categories to mod developers we can expect a large number of them and therefore scaling becomes an issue. Horizontally arranged text items scale terribly because text items also primarily extend horizontally. There are workarounds for this problem but these are either bad or add alternative interface (e.g. a pulldown menu listing all tabs) which isn't a great solution either. Under no circumstances will we use the workaround that adds two arrow buttons left and right of the tab bar to change the visible section of the tab bar. This is an anti-pattern and everyone who has ever committed the horrendous crime of implementing this GUI-atrocity deserves to end up in usability hell. +* In table mode we already have a bar at the top of the window. Two bars on the same window border give a cluttered, unclean appearance. + +If we do use an alternative layout we need to add an icon field to the ItemCategory record and we also need to consider, if we drop the vanilla one completely or let the user choose. Unless there is strong opposition (this is a significant change from Vanilla after all) we should choose the former option. If we opt for a less radical change we still should switch from text to icons to counter the scaling problem, even if we stick with the horizontal layout. + +## Character State + +Currently there is no way to attach additional status information to items or the player character and show these in the appropriate GUI elements. + +With record variables we already have the means to store additional stats. We only need a way to indicate to OpenMW that we want to display these stats. + +To this end we add a new record type (CustomStat) with the following fields: + +* Type (item or character) +* Label +* Tooltip (optional) +* Name of the record variable + +Item stats will be shown in the item tooltip. Character stats will be shown on the right side of the character stats window. + +We fold bounty value and reputation into this system and inject suitable CustomStat records when loading older omwgame files. + +We will also fold skill progression since level up (the number of skill gains for major and minor skills) into this system but without proving a CustomStat record (since this stat isn't displayed in Vanilla). This task is left to content developers (see below). + +### Skill Increases + +The number of skill increases is an important value for the levelling process. Unfortunately these values are not presented to the player in Vanilla. + +We add a new GMST (sys::TrackSkills) of type integer. When loading older omwgame files we inject this record with a default value of 0. + +If the GMST is not 0 we show skill progression in two places: + +* The tooltip of the skill (number of skill levels gains) +* The tooltip of the attribute (total number of level gains for all skills associated with this attribute) + +# Graphics + +TODO bugger scrawl until he agrees to fill in this section + +Random collection of ideas that may be feasible or not: + +idea? simple text on instance via a new object type, changeable both during content development and during gameplay via script (e.g. sign post) + +idea? celestial de-hardcoding (number of moons, number of suns, custom objects, configurable movement across the sky) + +idea? animation de-hardcoding/allow additional animations; possible use-cases: + +* more idle animations +* sitting animations +* item handling animations (take a drink from a mug, hold a fishing rod, carry a box) + +# Editor + +## Scripting + +We will use Python as a scripting language for OpenMW-CS. Adding a scripting language is a major task and for stage 1 we will most likely be limited to the basics. More advanced support can be added later. + +Sandboxing isn't an issue here, since if we want editor extensions to be useful they can't be sandboxed anyway. + +### API + +We expose the following API: + +* Read-Access to all records +* Read-Access to the content file list +* Read-Access to the resources lists +* Command issuing + +### Operation Scripts + +We let scripts to run as operations (exclusively for now). In the context of OpenMW-CS the term operation means the following: + +* An operation runs in a separate thread and has read only access to the content data +* Multiple operations can run concurrently, but only one operation of each type (e.g. saving, verifier, python script) at a time +* While at least one operation is running the content data can not be modified +* The operation can be terminated by the user at any time +* The operation reports back to the main thread via messages (shown in a table like with the verifier) and progress state (represented as progress bar) + +A script operation may issue commands. These commands are queued up and then executed in the main thread once the operation has finished. + +The operations script defines the GUI element it is triggered from; either through an API or an associated resource (depending on how we decide to manage Python script in OpenMW-CS). + +For stage 1 we allow operation scripts to add menu items to the main menu and to context menus. We will not add a tools main menu item under which scripts are grouped, because this is akin to a misc category, the most useless category of all. We may consider allowing operation scripts to create their own top-level main menu items. + +## Debugging + +We will expand on the use of OpenMW-CS as a debugging tool. To support this feature we require an easy to use IPC method (preferably platform-independent). Performance is of less importance since we are unlikely to send large amounts of data between OpenMW and OpenMW-CS. + +The potential for debugging tools is almost unlimited. But for stage 1 we most likely will only implement some basic tools. Two easily implementable tools with great usability value are listed below. + +Note that all debugging tools will require that OpenMW is started from OpenMW-CS. + +### Marking + +We add a new keyboard-shortcut to OpenMW (only available when started from OpenMW-CS). When used OpenMW will look at the instance under the mouse pointer (or the crosshair if the mouse pointer is hidden). If this instance is part of a content file (i.e. not created during play) OpenMW will send the cell and RefID of the instance to OpenMW-CS. OpenMW-CS inserts the instance info into a table. + +This table functions in a similar way to the verifier table; allowing the content developer to jump directly to records that have been identified as being in need of attention. + +### Script Error Reporting + +OpenMW currently reports script errors and warnings to the console only. When started from OpenMW-CS OpenMW will also send these errors back to OpenMW-CS which will present it in a verifier-like table to the user. + +## Namespaces + +We will encourage the use of a single main namespace per content file (the content developer is free to add sub-namespaces within this namespace). To this end we will add a namespace field to the content file header. + +When adding new IDs the namespace from the header will be inserted into the input field as a default. + +We also add function that produces a warning if the user tries to create an ID outside this namespace and outside the special namespaces project, session and default. This function can be disabled via the user-settings. If the content file doesn't have a main namespace (the default for all older content files) this function is inactive. + +## Help + +OpenMW-CS currently has very little in-application help. Improving this will be an ongoing process. For stage 1 we will focus on scripting. + +We will add a keyboard-shortcut and a context menu item to the script editor. When activated while the cursor is on a keyword we show a help text that explains the syntax and the function. + +Ideally we would want to use the same help text within the application and in our documentation. A way to handle this needs to be determined. + +The location where we show the help text needs to be determined. For script editor views we could re-use the widget that displays errors. Or we could add a separate subview type for help text. We may decide to add a help main menu item from which the help system can be navigated. + +## Spellchecker + +We will add a spellchecker by utilising an existing spellcheck solution. All names (e.g. items, cells, races) will be implicitly added to the list of known words. + +## Porting Tools + +Under the term porting we understand the process of moving a content file from one version of a dependency to another version of the same dependency. + +At the simplest level this requires only changing the dependency information but we will add additional functions that help with the process. + +For the porting of localisation content files (i.e. a new version of a content file is released and the localisation needs to adapt to it) we will add the following functions: + +* OpenMW-CS will look up all localisation records that have been added in the new version and present these to the user in a list +* OpenMW-CS will look up all localisation records that have been modified in the new version and present these to the user in a list that shows both the old and the new value + +# Misc + +This is a random collection of other enhancements that don't fit anywhere else. + +## Content User Settings + +Currently there is no clean way for content files to provide their own user settings. + +To compensate for this shortcoming we introduce a new record type (UserSetting) and a script hook and we rearrange the user settings window. + +### UserSettings Record + +We add a new record type that describes user settings options. This record type offers several kinds of user settings but does not specify the layout of the GUI elements that are used to represent the settings. We strictly separate the data and the representation (skinning). + +User settings data is stored in GMSTs. + +A user settings record consists of the following fields: + +* ID of the GMST used to store the record +* Category ID +* Label (string) +* Tooltip (string, optional) +* Type of setting (integer, hard-coded) +* Additional data + +The following user setting types are available: + +* bool (type 0): GMST is an integer; GUI is a checkbox; additional data: default value +* numeric, integer (type 1): GMST is an integer; GUI is a spinbox; additional data: upper bound, lower bound, default value +* numeric, float (type 2): GMST is a float; GUI is a spinbox; additional data; upper bound, lower bound, precision, default value +* list (type 3) GMST is an integer; GUI is a combobox; additional data; list of strings for combobox text, default value +* slider (type 4): GMST is a float; GUI is a slider; additional data; upper bound, lower bound, default value + +### Categories + +User settings categories are represented as separate pages/tabs in the GUI. We specify categories via category IDs (strings). Currently there does not appear to be a need for a separate user settings category record, since this record would have no data. + +Content files can create new categories (simply by referencing them in a user settings record) or add to existing categories (including the vanilla ones). We should consider to add a couple of additional default categories (including localised labels, but empty and invisible until populated by content files) to help content developers organise their settings in a coherent way. + +The categories in vanilla MW are General, Audio, Controls and Graphics. Content files can add settings to these too. But we should consider reorganisation. General is not a good category. We should consider splitting it up. We should also consider moving the keybindings into their own category. A separate keybindings category would have to be the only exception to the rule that content files are allowed to add settings to pre-existing categories. + +### Hook + +We add a new hook (sys::SettingsChanged) that is triggered whenever a change to the user settings is mode. The hook function takes a list of strings as argument, containing the IDs of user settings that have been changed. + +### Settings Window + +The current design of the user settings window in OpenMW is not scalable (it can not accommodate more settings without getting significantly worse). Therefore a complete redesign is required. We should aim for something that resembles the OpenMW-CS user settings window. + +This also provides an opportunity to add a GUI to many cfg file only settings that got added prior to 1.0. But we should consider if some of these wouldn't be better suited for bare GMSTs without user setting. + +## Launcher Skinning + +Currently our launcher has a generic look with some Morrowind branding. This is not desirable for total conversions. + +To compensate for this shortcoming we introduce launcher skinning. The launcher skinning is determined by the currently selected omwgame file. We add an new, optional record type that is allowed only in omwgame files. + +The exact scope of this new record remains to be determined, but icons and backgrounds are obvious candidates. + +## Body of Liquid + +Currently we at most have a single body of liquid in each cell that covers the entire cell. This makes it impossible to create additional lakes that are not at sea level. + +To compensate for this shortcoming we introduce a new object type: LiquidBody. + +Actors within a LiquidBody instance behave in the same way as actors in the regular cell water (drowning, swimming). + +A LiquidBody object record has the following fields: + +* Liquid ID (see de-hardcoding section, water sub-section) +* Height (float) +* Shape + +Shape is a closed bezier-curve that can be edited in the editor. This shape defines a surface (the liquid surface). The depths of liquid body is defined by the height value. + +We can imply that only the surface of the LiquidShape instance is exposed. The other sides do not require rendering. + +## Dagoth'Ur Un-Fix + +We introduced a fix for a defect in Morrowind.esm by blocking remote access to instances of the object dagoth_ur_1 via mod instructions for dynamic stats. We will now bind this fix to a new integer GMST in the sys namespace. This will allow content developers to disable this fix in their mods (hopefully after given poor old Dagoth'Ur a bit more health). + +## Comment Subrecords + +We add a new optional sub-record to all top-level records. This subrecord contains a string that can be used for writing down notes and additional documentation by content developers. + +OpenMW does not need to read these records. If we find that comment subrecords add too much bloat to the record data structures we may decide to skip loading them in OpenMW and even keep them out of the data structures used by OpenMW. + +OpenMW-CS will add the comment sub-record as a regular data field. We will provide an option to show the comments as tooltips in various places. + +Note that the vanilla esm format already contains a similar field in the land texture record. This field needs to be folded into the new command sub-record. + +## No Pause Mode + +We add four new GMSTs of type integer (default value 1): + +* sys::StatusPause +* sys::DialogPause +* sys::ReadingPause +* sys::CraftingPause + +These decide if the game is paused when in status mode (right-click), dialogue mode, journal mode or any of the crafting modes (e.g. alchemy). To handle sys::DialogPause==0 properly we ignore dialogues initiated by NPCs or via script, if the player is already in a dialogue. + +## Instance Persistency + +We add a new optional field to all object records and instance subrecords, a single enum-like integer. + +The value in the object record is considered by OpenMW-CS when creating new instances of that object only (it functions as a default value). OpenMW considers the value in the object record when spawning a new instance during gameplay. + +The possible values of the new field are as follows: + +* Persistent: Instance can be changed and is included in the save file (this matches the vanilla/OpenMW 1.0 behaviour). +* Transient: Instance can be changed, but is not stored in the save file +* TransientNoSync: Same as Transient; in a multiplayer environment the instance does not need to be synchronised across multiple clients +* Immutable: Instance can not be changed and is not stored in the save file. An attempt to manipulate the instance via a script results in an error. Attempting to spawn an immutable instance into the world also results in an error. + +If not present the field defaults to Persistent, except for static objects and instances for which it defaults to Immutable. + +The DontSaveObject script instructions (ignored in OpenMW 1.0) can be used to switch a persistent instance to transient state. This function is meant primarily for (marginally) improved backwards compatibility. We do not encourage the use of DontSaveObject for new content. + +Immutable statics are a marginal departure from vanilla Morrowind, since statics can be manipulated to some degree. However since that never worked properly in the first place it is not very likely that we break anything and even in case of breakage a quick and simple fix to the content is available. Combined with the large benefits of this feature (drastically reduced save game file size, for one thing) taking the risk of introducing an incompatibility seems worthwhile. + +## Multi-Player + +OpenMW currently does not support multi-player and is unlikely to do so in the near or intermediate future. But we still can make improvements that aid the multi-player project alongside OpenMW and especially content developers that wish to improve the multi-player experience of their content. + +Our goal for multi-player (both in regards to the separate project and if/when multi-player gets integrated into OpenMW) is for content to be usable both in single and in multi-player. This goal isn't fully achievable without special attention from content developers. No matter how we approach multi-player some assumption made by content developers for single-player will be broken, which will then result in some broken content. The best we can hope for is to minimise the breakage. + +To this end we will introduce features that will allow content developers to include support for multi-player; mostly in the form of script instructions. These feature must be designed in a way that does not interfere with single-player. Several examples can be found throughout this document. + +In the same vein we also provide optimisation options for multi-player, i.e. ways to flag state as not requiring synchronisation across server and clients. + +### Content Files + +There will always be content files that do not take multi-player into consideration (Morrowind.esm being the obvious case, but some or most current content developers may fall into the same category). This can easily be addressed by putting another content file with fixes on top of the content file (provided the original file had a suitable license). + +We will mark up these content files in a similar way as we mark localisations, so that the launcher can automatically match them to their respective main content file and automatically enable them when running multi-player. + +### Scripting + +Scripting is the biggest issue in regards to multi-player. The current implementation of the multi-player project executes scripts client-side. This is unlikely to work. Anything related to random numbers is an obvious source for (potentially) game-breaking de-syncs. If we dig deeper we will probably find more sources of problems. + +On the other hand scripts that contain the ID player can not be executed server-side, because there would be no way to know which player is meant. + +The changes required to address these issues are substantial. This is not a task for near future post-1.0 development or even intermediate future development. + +What we can do now is to help content developers create their content multi-player ready, so that when multi-player becomes available in OpenMW their content will work out of the box (minus the unavoidable bugs that come from not being able to test content). + +### Player + +We can have the script engine detect if a script contains the ID player and if that is the case let it execute on the client instead of the server. However that is only a workaround and it is not reliable. Obvious cases where this could fail are: + +* Function calls where one of the involved scripts including the ID player and the other doesn't +* The ID player being used not explicitly but via an ID variable. + +It would still make sense to implement this workaround as a fallback. + +For a proper solution to the problem we need to engineer the scripting language in a specific way and provide guidance to content developers how to use it in a multi-player compatible style. + +In particular we need to provide features that allow for the avoidance of the ID literal *Player*. + +In some cases the solution is obvious. For example the script slot for OnActivate can pass the activating actor as an argument to the script. Such a script may be able to operate in a more generalised way that does not require an explicit reference to the player. If the script needs to distinguish between player actors and non-player actors it can check the received actor reference against the list of player references. + +There are also many script functions that imply the player (e.g. EnableBirthMenu). These must be changed to work on an actor instead. + +This is a complex issue that will require more design and deliberation before we can act on it. From 8cda355af6c90cddfcd87f7693a9e230a420d146 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 27 Jun 2018 12:14:34 +0200 Subject: [PATCH 257/282] last minute changes to design doc --- docs/openmw-stage1.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/openmw-stage1.md b/docs/openmw-stage1.md index 42fe8a2aa..b082759bf 100644 --- a/docs/openmw-stage1.md +++ b/docs/openmw-stage1.md @@ -231,6 +231,8 @@ Therefore we will develop our own cross-platform package format for OpenMW conte # Scripting +*Important: The following is a draft for a scripting language titles oldscript+ in the forum discussions. We will not go down this implementation route. But this section is still relevant because newscript has to cover all functionality listed here.* + Note: Extensions to the scripting language in form of new instructions and functions are distributed over this entire document. In some cases features may require additional functions that return the value of certain fields of certain records. These functions are usually not listed explicitly and are left as an exercise to the reader. ## Language Version @@ -1159,11 +1161,11 @@ All effects described in these sub-records affect active cells only. ### Sky -TODO all existing sky effects and possibly new ones; bugger scrawl to fill in the details +TODO all existing sky effects and possibly new ones ### Particles -TODO all existing particle effects and possibly new ones; bugger scrawl to fill in the details +TODO all existing particle effects and possibly new ones ### Event @@ -1210,7 +1212,7 @@ A liquid record consists of the following fields: We need to support at least one liquid type (ID 0: Water) from the start. Other types can be added. -TODO check with scrawl about other ways to handle visuals for liquid types that requires less hard-coding. Shaders? +TODO check about other ways to handle visuals for liquid types that requires less hard-coding. Shaders? ## Magic Effects @@ -2112,7 +2114,7 @@ If the GMST is not 0 we show skill progression in two places: # Graphics -TODO bugger scrawl until he agrees to fill in this section +TODO Random collection of ideas that may be feasible or not: From 8bc6c853963ac2b837f3f7a869885a115c1e2423 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 27 Jun 2018 12:24:21 +0200 Subject: [PATCH 258/282] last minute changes (this time for real; forgot to merge in the last update from the private repo) --- docs/openmw-stage1.md | 1046 +++++++++++++++++++++-------------------- 1 file changed, 538 insertions(+), 508 deletions(-) diff --git a/docs/openmw-stage1.md b/docs/openmw-stage1.md index b082759bf..9458b66bb 100644 --- a/docs/openmw-stage1.md +++ b/docs/openmw-stage1.md @@ -10,7 +10,7 @@ At the same time we want to stay true to our origins. While we want to broaden t Our goal here is to make OpenMW into a general purpose engine, but a general purpose 1st/3rd person real-time RPG engine. We do not attempt to support other genres or other flavours of RPG. -The development of OpenMW will hopefully continue for a long time and we can not reasonable hope to sketch out its entire future development in a single design document. Therefore this document should be seen as stage 1 of the post 1.0 development only. It may last us 6 months or a year or several years, depending on how much development activity we can achieve. But eventually there will be a stage 2 design document. +The development of OpenMW will hopefully continue for a long time and we can not reasonably hope to sketch out its entire future development in a single design document. Therefore this document should be seen as stage 1 of the post 1.0 development only. It may last us 6 months or a year or several years, depending on how much development activity we can achieve. But eventually there will be a stage 2 design document. # Definitions @@ -68,7 +68,7 @@ However this approach means additional work and would be of limited usefulness i We will continue with the revised roadmap scheme. This means we add a new milestone (openmw-stage1). After we are finished discussing this design document and have made necessary adjustments we will cut it up into individual tasks and add them to openmw-stage1. This milestone will then take the role of the current openmw-1.0 and openmw-cs-1.0 milestones. Confirmed bug reports also go into openmw-stage1. Other issues (feature requests and tasks) only after we have approved them for near future development. -We will most like not have a separate openmw-stage1 for the editor, since for the bulk of the changes (The Grand Dehardcoding) most tasks we will require changes to both OpenMW and the editor. +We will most like not have a separate openmw-stage1 for the editor, since for the bulk of the changes (The Grand De-hardcoding) most tasks we will require changes to both OpenMW and the editor. # Content File Format & Namespaces @@ -92,7 +92,7 @@ The following cases of changes to the record format have been identified: * Existing record that is switched over from integer-based to ID-based indexing (this is a special case of the case above) * Existing records is split into two records: Split needs to be performed on load. -There have been some concerns regarding the efficiency of string based IDs (vs integer). While there hasn't been an evidence indicating that this is actually a problem in OpenMW, it is important to point out that the move from integer to string does not mean that such a problem could not be addressed. +There have been some concerns regarding the efficiency of string based IDs (vs. integer). While there hasn't been an evidence indicating that this is actually a problem in OpenMW, it is important to point out that the move from integer to string does not mean that such a problem could not be addressed. We already have plans to introduce a new string type for IDs, which deals with the case-issue (current std::string based implementation is error-prone and results in some very verbose code). @@ -202,13 +202,13 @@ Most of the current content exists in a form that is not compatible with the new In most cases this can easily be resolved by adding an importer tool to the launcher, that takes the content and copies it into the desired organisation scheme (archive or directory). -For other content (preexisting data directories and Morrowind, since Morrowind and its add-ons are installed into the same directory) we need to maintain a level of compatibility with the old approach, that can be switched on and off as needed. +For other content (pre-existing data directories and Morrowind, since Morrowind and its add-ons are installed into the same directory) we need to maintain a level of compatibility with the old approach, that can be switched on and off as needed. ### Resources Packages -During the discussion about the new content file organisation scheme (omwgame, omwaddon) there was some criticism about the scheme not allowing plugins with no dependencies. +During the discussion about the new content file organisation scheme (omwgame, omwaddon) there was some criticism about the scheme not allowing plug-ins with no dependencies. -This is not a problem, because such plugins are generally useless. A plugin without any dependency can not modify or reference any record. That was not such a large issue with vanilla Morrowind, because in this case the plugin could have still have depended on things like attributes or dynamic stats. However in OpenMW these will all turn into records and therefore can not be accessed from within an omwaddon file without dependencies. This will only get more extreme as the de-hardcoding progresses further. An omwaddon file without dependencies can do literally nothing, which makes it useless by definition. +This is not a problem, because such plug-ins are generally useless. A plug-in without any dependency can not modify or reference any record. That was not such a large issue with vanilla Morrowind, because in this case the plug-in could have still have depended on things like attributes or dynamic stats. However in OpenMW these will all turn into records and therefore can not be accessed from within an omwaddon file without dependencies. This will only get more extreme as the de-hardcoding progresses further. An omwaddon file without dependencies can do literally nothing, which makes it useless by definition. But there is one exception. Some members of the mod community have provided resources packages (meshes and similar) that can be used by other content developers. Under our new resources scheme these would have to be accompanied by an omwaddon file. This is not a good solution because of two reasons: @@ -231,40 +231,38 @@ Therefore we will develop our own cross-platform package format for OpenMW conte # Scripting -*Important: The following is a draft for a scripting language titles oldscript+ in the forum discussions. We will not go down this implementation route. But this section is still relevant because newscript has to cover all functionality listed here.* - -Note: Extensions to the scripting language in form of new instructions and functions are distributed over this entire document. In some cases features may require additional functions that return the value of certain fields of certain records. These functions are usually not listed explicitly and are left as an exercise to the reader. +**Note**: Extensions to the scripting language in form of new instructions and functions are distributed over this entire document. In some cases, features may require additional functions that return the value of certain fields of certain records. These functions are usually not listed explicitly and are left as an exercise to the reader. ## Language Version -The version of the language used in a script is determined by a single integer number which is stored in a new sub-record within the script record. Note that this is different from earlier concepts which used a more complicated version identifier and stored it within the actual script. +The version of the language used in a script is determined by a single integer number which is stored in a new subrecord within the script record. Note that this is different from earlier concepts, which used a more complicated version identifier and stored it within the actual script. -Scripts that are missing this sub-record are considered version 0 (legacy). New scripts (when created in OpenMW-CS) default to the most up-to-date version. The version can be selected from a combo box in the script sub-view. We may add a user setting that hides the combo box UI if the version is the most up to date one. +Scripts that are missing this subrecord are considered version 0 (legacy). New scripts (when created in OpenMW-CS) default to the most up-to-date version. The version can be selected from a combo box in the script subview. We may add a user setting that hides the combo box UI if the version is the most up to date one. The version number is incremented whenever an addition to the scripting language is made (at most once per feature release). A new feature release is not required to increment the version number, if no feature changes have been made to the language in this release. -From version 1 on all workarounds for bad scripts will be disabled. This should not break anything for well written scripts. In general moving to a new language version should usually at most require minor fixes. +From version 1 on, all workarounds for bad scripts will be disabled. This should not break anything for well-written scripts. In general, moving to a new language version should usually at most require minor fixes. Since old scripts will remain with the old language version (unless there is a need to modify them and use newer features), this scheme will not break compatibility for existing content. ## Error Handling -We will continue with the existing runtime error handling scheme (meaning stopping the script execution on a runtime error). Introducing exception handling instead would be overkill. +We will continue with the existing runtime error handling scheme (i.e., stopping the script execution on a runtime error). Introducing exception handling instead would be overkill. -We will do our best to avoid crashes as result of broken script. That means (among others) that we need to put an artificial (configurable) limit on function call depth to avoid crashes and lockups from endless script recursion. We may also decide to put a similar limit on loops. +We will do our best to avoid crashes as a result of broken scripts. That means (among others) that we need to put an artificial (configurable) limit on function call depth to avoid crashes and lock-ups from endless script recursion. We may also decide to put a similar limit on loops. ## Variable Types -The types *long* and *float* remain unchanged. +The types `long` and `float` remain unchanged. -We will flag *short* as deprecated. Currently *short* has no advantage over *long* (it uses the same amount of memory in OpenMW). For anything but a very specific kind of bit cutting the *short* type has no use at all. -If we ever decide that we do want a shorter integer type we can always de-deprecate *short*. +We will flag `short` as deprecated. Currently `short` has no advantage over `long` (it uses the same amount of memory in OpenMW). For anything but a very specific kind of bit-cutting the `short` type has no use at all. +If we ever decide that we do want a shorter integer type we can always de-deprecate `short`. We will introduce several new types: ### Strings -The name of the new string type is **String** and string literals are marked by placing them in quotation marks. Example: +The name of the new string type is `String`, and string literals are marked by placing them in quotation marks. Example: ```` String a @@ -273,7 +271,7 @@ Set a to "Some Text" Comparison operators for strings are provided in the usual way. -Since we discontinue bad script workarounds most ambiguousness regarding string literals should have been removed already (we will not accept local variable names and instructions within quotation marks anymore). Only a single problem case remains: +Because we are discontinuing bad script workarounds, most ambiguity regarding string literals should have been removed already (we will not accept local variable names and instructions within quotation marks any more). Only a single problem case remains: If a function requires an ID, is it a literal ID or is it the name of a string variable that contains an ID? Example: @@ -283,13 +281,13 @@ Set Fargoth to "player" Fargoth -> AddItem "Gold_001", 100 ```` -We can not make a rule that forbids the use of local variable names that are also IDs, because that would allow random content files to break scripts in other, unrelated content files. +We cannot make a rule that forbids the use of local variable names that are also IDs, because that would allow random content files to break scripts in other, unrelated content files. -Therefore the solution here is to simply give the local variable precedence over IDs. If the script author wanted to give money to Fargoth he should not have created a local variable with this name. +Therefore, the solution here is to simply give the local variable precedence over the ID. If the script author intended to give money to `Fargoth`, he should not have created a local variable with the same name. Note that global variables are not an issue here, because global variables are IDs and IDs need to be unique (with certain exceptions that are not relevant here). -To remove even the last remaining potential problems with ambiguity we will also introduce a new string literal that can only be a literal and never a string variable name. These literals are marked by a L prefix. Utilising this feature the code block above could be rewritten to the following if the script author absolutely insists on having a local variable named Fargoth: +To remove even the last remaining potential problems with ambiguity, we will also introduce a new string literal that can only be a literal and never a string variable name. These literals are preceded by an `L` character. Utilising this feature, the aforementioned code block could be rewritten as is shown below, if the script author absolutely insists on having a local variable named `Fargoth`: ```` String Fargoth @@ -297,93 +295,93 @@ Set Fargoth to "player" L"Fargoth" -> AddItem "Gold_001", 100 ```` -### Instance-References +### Instance References -The name of the new reference type is **Ref**. It can reference an instance in the world or in a container or be a null reference. +The name of the new reference type is `Ref`. It can reference an instance in the world or in a container or be a null reference. -Since we currently have no way to reference an instance persistently the use of the ref type is limited for the time being (see section about variable scope for further details). +Since we currently have no way to reference an instance persistently, the use of the `Ref` type is limited for the time being (see section *Variable Scope* for further details). -References can be used in any place that would otherwise allow an ID that stand for an existing reference. The rules for strings regarding ambiguousness apply to references in the same way. +References can be used in any place that would otherwise allow an ID that stand for an existing reference. The rules for strings regarding ambiguity apply to references in the same way. A reference will implicitly cast to 0 (a null reference) or 1 (not a null reference) when used in a numeric expression. -Note: A reference pointing into the contents of a container points to a stack of items and not a single item. +**Note**: A reference pointing to the contents of a container points to a stack of items and not a single item. We introduce the following keywords: -* Self: a value of type Ref (the instance the script is currently running on, only available in local scripts and dialogue scripts) -* NullRef: a literal of type Ref (null reference) -* GetId (r): return ID of reference as a string value -* GetContainer (r): returns the reference of the container r is in (or a null-reference if r is not in a container) -* GetCell (r): returns the ID string of the cell r is in (either directly or via a container) -* SearchActive (id, container=0): return a reference with the given ID within the active cells. If container!=0, also check in containers; return a null-reference if no reference is found -* SearchIn (id, id2): return a reference with the given ID within something (id2) that can contain references; this can be a cell, a worldspace, a container, a creature or a NPC. If id2 represents an instance, a ref variable can be given instead; return a null-reference if no reference is found +* `Self`: A value of type `Ref` (the instance the script is currently running on, only available in local scripts and dialogue scripts) +* `NullRef`: A literal of type `Ref` (null reference) +* `GetId(r)`: Returns the ID of the reference `r` as a string value +* ``GetContainer(r)`: Returns the reference of the container `r` is in (or a null reference if `r` is not in a container) +* `GetCell(r)`: Returns the ID string of the cell `r` is in (either directly or via a container) +* `SearchActive(id, container = 0)`: Returns a reference with the given ID within the active cells. If `container != 0`, also check in containers; returns a null reference if no reference is found +* `SearchIn(id, id2)`: Returns a reference with the given ID within something (`id2`) that can contain references; this can be a cell, a worldspace, a container, a creature or an NPC. If `id2` represents an instance, a `Ref` variable can be given instead; returns a null reference if no reference is found ### Lists -A single type list available for the following types: +A single-type list will be available for the following types: -* Long -* Float -* String -* Ref +* `long` +* `float` +* `String` +* `Ref` -We will not support mixed type list because that would require a major change to the type system. Internally and functionally a list will work like an array. The type names for lists are: +We will not support mixed-type lists because that would require a major change to the type system. Internally and functionally, a list will work like an array. The type names for lists are: -* LongList -* FloatList -* StringList -* RefList +* `LongList` +* `FloatList` +* `StringList` +* `RefList` -List literals are given as a comma separated list of values in square brackets. Empty list literals are allowed. In the context of lists we generally allow implicit promotion of integer to float types. +List literals are given as a comma-separated list of values in square brackets. Empty list literals are allowed. In the context of lists, we generally allow implicit promotion of integer to float types. We support the following list operations: -* l1[i]: Return member i (indexing begins at 0) -* GetSize(l1): Returns the number of elements in l1 as an *Integer* -* l1 + l2: Returns a concatenated list consisting of the members of l1 followed by the members of l2 -* GetSubset (l1, l2): Return the common subset of the elements of l1 and the elements of l2 -* HasElement (l1, e): Return 0 (if e is not element of l1) or 1 (otherwise) -* GetIndex (l1, e): Return index of first occurrence of element e in l1 (or -1 if none) -* Minimum (l1): Return the minimum of the elements in l1 (only works with numerical types) -* Maximum (l1): Return the minimum of the elements in l1 (only works with numerical types) +* `l1[i]`: Returns member `i` (indexing begins at 0) +* `GetSize(l1)`: Returns the number of elements in `l1` as an integer +* `l1 + l2`: Returns a concatenated list consisting of the members of `l1` followed by the members of `l2` +* `GetSubset(l1, l2)`: Returns the common subset of the elements of `l1` and `l2` +* `HasElement(l1, e)`: Returns 0 (if `e` is not element of `l1`) or 1 (otherwise) +* `GetIndex(l1, e)`: Returns the index of the first occurrence of `e` in `l1` (or -1 if none) +* `Minimum(l1)`: Returns the minimum of the elements in `l1` (only works with numerical types) +* `Maximum(l1)`: Returns the maximum of the elements in `l1` (only works with numerical types) and the following list instructions: -* Set l1[i] To e: Set list element i of list l1 to value e (indexing begins at 0) -* Sort (l1, cf=""): Sort list elements into ascending order (if cf is an empty string) or via comparison function script cf -* Filter (l1, ff): Filter list l1 by filter function ff (only keep elements that do not return 0) -* Reverse (l1): Reverse order of list elements -* Append (l1, e): Append element e to list l1 -* Append (l1, l2): Append elements of list l2 to list l1 -* Remove (l1, e): Remove all elements equal to e from list l1 -* Remove (l1, l2): Remove from list l1 all elements which are also in list l2 -* InsertAt (l1, e, i): Insert element at position i into list l1 (replacing element i on position upwards and extending the size of the list by one) -* RemoveAt (l1, i): Remove element at position i from list l1 (moving elements with an index higher than i down by one and reducing the size of the list by one) -* Resize (l1, n): Set size of l1 to n. If list is extended additional elements are default initialised. +* `Set l1[i] To e`: Sets list element `i` of list `l1` to value `e` (indexing begins at 0) +* `Sort(l1, cf="")`: Sorts list elements into ascending order (if `cf` is an empty string) or via comparison function script `cf` +* `Filter(l1, ff)`: Filters list `l1` by filter function `ff` (only keeps elements that do not return 0) +* `Reverse(l1)`: Reverses order of list elements of list `l1` +* `Append(l1, e)`: Appends element `e` to list `l1` +* `Append(l1, l2)`: Appends elements of list `l2` to list `l1` +* `Remove(l1, e)`: Removes all elements equal to `e` from list `l1` +* `Remove(l1, l2)`: Removes all elements from list `l1` which are also in list `l2` +* `InsertAt(l1, e, i)`: Inserts element `e` into list `l1` at position `i` (moving the element at index `i` one position upwards and extending the size of the list by one) +* `RemoveAt(l1, i)`: Removes the element at position `i` from list `l1` (moving elements with an index higher than `i` down by one and reducing the size of the list by one) +* `Resize(l1, n)`: Set size of list `l1` to `n`; if the list is extended, additional elements are initialised with default values Variable names used in the lists above: -* l1 and l2 are lists/list literals of the same element type -* i is an integer value that functions as list index -* n is an integer value -* e is a variable/literal of the element type of l1 and l2 -* cf: ID of a comparison function script that takes two element type arguments and returns an integer (-1, 0, 1) -* ff: Id of filter function script that takes one element type arguments and returns an integer (0, 1) +* `l1` and `l2` are lists / list literals of the same element type +* `i` is an integer value and is used as list index +* `n` is an integer value +* `e` is a variable/literal of the element type of `l1` and `l2` +* `cf` is the ID of a comparison function script that takes two element-type arguments and returns an integer (-1, 0, 1) +* `ff` is the ID of a filter function script that takes one element-type argument and returns an integer (0, 1) ### List Aliases We define three more type aliases: -* Vector3: A float list of 3 members, used for location or rotation -* Vector4: A float list of 4 members (location and zrot) -* Vector6: A float list of 6 members (location and rotation) +* `Vector3`: A `FloatList` of 3 members, used for location or rotation +* `Vector4`: A `FloatList` of 4 members (location and rotation around z-axis) +* `Vector6`: A `FloatList` of 6 members (location and rotation) These aliases are not separate types. -In the specification of additional functions and instructions values of these types are specified as v3, v4 and v6 respectively. Passing in shorter lists is allowed and will set the missing elements to 0. +In the specification of additional functions and instructions, values of these types are specified as `v3`, `v4`, and `v6` respectively. Passing shorter lists is allowed and will set the missing elements to 0. -Note that all rotations are specified in degrees. +**Note**: All rotations are specified in degrees. ## Variable Scope @@ -393,32 +391,32 @@ The global and the local scope remain unchanged. We will add two more scopes: ### True Local Variables -What the Morrowind scripting language describes as local variables dose not match the concept of local variables in other languages. In case of local scripts Morrowind local variables function as member variables. In case of global scripts Morrowind local variables function as static variables. +What the Morrowind scripting language describes as local variables does not match the concept of local variables in other languages. In case of local scripts, Morrowind local variables function as member variables. In case of global scripts, Morrowind local variables function as static variables. -There is a need for true local variables: +There are several reasons for the introduction of true local variables: * Less pollution of local dialogue variable names * Less state that needs to be saved -* Avoidance of potential scripting errors when the state of a local variable is maintained across multiple script executions and the script does not re-initialise the variable at the beginning of a new run +* Prevention of potential scripting errors when the state of a local variable is maintained across multiple script executions and the script does not re-initialise the variable at the beginning of a new run We will call this scope **true local variable** to distinguish it from old local variables. Declaration of true local variables will look like this: ```` -Local Long a = 1 -Local Long b +Local long a = 1 +Local long b ```` -If no default value is given the variable is default initialised (value 0, empty string, empty list, null reference). +If no default value is given, the variable is default-initialised (value 0, empty string, empty list, null reference). ### Record Variables -A problem (especially relevant for large projects with complex functionality) is that there is no place other then global variables to store additional state that is not specific to an instance. A global function with multiple variables can be used instead but that is just a workaround. +A common problem (especially relevant for large projects with complex functionality) is that there is no place other than global variables to store additional state that is not specific to an instance. A global function with multiple variables can be used instead, but that is just a workaround. -Furthermore there is currently no way to add new state to other objects (object in the OOP sense, not in the Morrowind sense) other then instances. +Furthermore, there is currently no way to add new state to other objects (object in the OOP sense, not in the Morrowind sense) other than instances. -Therefore we introduce record variables which are a generalisation of old local variables in local scripts. +Therefore, we introduce record variables, which are a generalisation of old local variables in local scripts. The respective records will be enhanced by an optional list of subrecords that can declare record variables and their default values. @@ -430,33 +428,33 @@ Obvious candidates for this feature are: * Worldspaces * Factions -Note: Object record variables are essentially the same as old local variables declared in a local script, but declared in the object record instead of the script. +**Note**: Object record variables are essentially the same as old local variables declared in a local script - only that they are declared in an object record instead. We will introduce the following record variable functions: -* Has{Type} (vn): Return 1 if record/reference has a long/float/string/list record variable of name vn, 0 otherwise. -* Get{Type} (vn): Return value of record/reference variable vn. Note that we don't use the x.y syntax here, because we need to explicitly specify the type to allow for proper compile time error checking for reference variables. +* `Has{Type}(vn)`: Returns 1 if record/reference has a long/float/string/list record variable of name `vn`, 0 otherwise +* `Get{Type}(vn)`: Returns the value of record/reference variable `vn`. Note that we don't use the x.y syntax here, because we need to explicitly specify the type to allow proper compile-time error checking for reference variables. -and the following record variable instruction: +and the following record variable instructions: -* Set{Type} (vn, val): Set variable vn to value val. It is an error, if this variable does not exist. +* `Set{Type}(vn, val)`: Sets variable `vn` to value `val`. It is considered an error if this variable does not exist. -With all functions and instructions the record/reference is specified via the -> operator in the usual way. When using object records (via ID) the first instance found is used instead of the object record. +Within all functions and instructions, the record/reference is specified via the `->` operator in the usual way. When using object records (via ID), the first instance found is used instead of the object record. Variable names used in the lists above: -* id: string -* vn: string -* ref: reference -* val: value according to {Type} +* `id` is a `String` +* `vn` is a `String` +* `ref`is a reference of type `Ref` +* `val` is a value according to `{Type}` -and finally {Type} standing for Long, Float, String, LongList, FloatList or StringList +Finally, `{Type}` stands for `long`, `float`, `String`, `LongList`, `FloatList`, or `StringList`. -We can fold the access of local variables in global scripts into the new record variable system, even though global scripts do not have explicit record variables. The new functions for record variable access make the old x.y syntax obsolete and therefore we declare it as deprecated. +We can fold the access of local variables in global scripts into the new record variable system, even though global scripts do not have explicit record variables. The new functions for record variable access make the old x.y syntax obsolete and, therefore, we declare it as deprecated. ## Control Structures -We will add a for loop that works on lists. To avoid unnecessary complexity in the language and to encourage the use of lists we will not have an index based for loop (these can easily be simulated via while). +We will add a `For` loop that works on lists. To avoid unnecessary complexity in the language and to encourage the use of lists, we will not have an index-based `For` loop (these can easily be simulated via `While`). Example: @@ -466,104 +464,104 @@ For x In l EndFor ```` -We will add the break and continue keywords both for for and while loops. +We will add the `break` and `continue` keywords both for `For` and `While` loops. -We may add a switch-case construct, but this is most likely a stage 2 task since there are many different ways to do switch-case and no obviously superior solution. This will most likely require extended discussion and design work. +We may add a `switch-case` construct, but this is most likely a stage-2 task since there are many different ways to do `switch-case` and no obviously superior solution. This will most likely require extended discussion and design work. ## General Additions -The vanilla scripting language is missing a few pieces of basic functionality. Therefore we need to add the following kinds of instructions: +The vanilla scripting language is missing a few pieces of basic functionality. Therefore, we need to add the following kinds of instructions: -* Trigonometric functions (Cos, Sin, Tan, Acos, Asin, Atan, Atan2, DegToRad, RadToDeg) -* Logical boolean function (And, Or, Not, Xor) -* Other mathematical functions (Abs, Floor, Ceil, Clamp, Lerp, Sign) +* Trigonometric functions (`Cos`, `Sin`, `Tan`, `Acos`, `Asin`, `Atan`, `Atan2`, `DegToRad`, `RadToDeg`) +* Logical boolean function (`And`, `Or`, `Not`, `Xor`) +* Other mathematical functions (`Abs`, `Floor`, `Ceil`, `Clamp`, `Lerp`, `Sign`) ## Object Management -We will add a function that moves an instance into the world. This feature (especially the move function) preludes a concept of persistent instance identity, which is most likely a stage 2 or OpenMW 2.0 feature. For now these functions are required to deal with state attached to the instance in the form of record variables/old local variables. +We will add a function that moves an instance into the world. This feature (especially the `move` function) preludes a concept of persistent instance identity, which is most likely a stage-2 or OpenMW-2.0 feature. For now, these functions are required to deal with state attached to the instance in the form of record variables or old local variables respectively. -* MoveObjectToWorld (target, v6): Move instance from current location to world location. -* CloneObjectToWorld (target, v6): Add a copy of instance to world location. +* `MoveObjectToWorld(target, v6)`: Moves the instance from current location to world location `target` at coordinates `v6` +* `CloneObjectToWorld(target, v6)`: Adds a copy of the instance `target` to world location `target` at coordinates `v6` -(in all cases reference specification for objects to be moved/cloned via -> operator in the usual way) +**Note**: In all cases, reference specification for objects to be moved/cloned can be done via the `->` operator in the usual way. -Moving or cloning out of a container will only move or clone a single item, not the whole stack. +**Note**: Moving or cloning out of a container will only move or clone a single item, not the whole stack. -The target value represents the target cell or worldspace (see section "References to cells in data structures" for details) +The `target` value represents the target cell or worldspace (see section *References to Cells in Data Structures* for details) -We will add replacement functions for object placement related functions without declaring the original functions deprecated. +We will add replacement functions for object-placement-related functions without declaring the original functions deprecated. Instructions: -* SetRotation (v3): Set instance's rotation; characters and NPCs ignore first two elements (replaces SetAngle) -* SetPosition (v3): Set instance's position (replaces SetPos) -* Teleport (target, v6, strict=0): Teleport instance to location specified by target and v6 (if the instance is a character or NPC the xrot and yrot values are ignored). If strict!=0, no corrections are made to the specified location. Otherwise the location may be adjusted for safety. (replaces Position and PositionCell) -* Spawn (objectId, target, v6, strict=0): Same as teleport, but spawns a new object (replaces PlaceItem and PlaceItemCell) +* `SetRotation(v3)`: Sets the instance's rotation; actors ignore the first two elements (replaces `SetAngle`) +* `SetPosition(v3)`: Sets the instance's position (replaces `SetPos`) +* `Teleport(target, v6, strict = 0)`: Teleports the instance to location specified by `target` and `v6`; actors ignore the first two elements; if `strict != 0`, no corrections are made to the specified location, otherwise the location may be adjusted for safety (replaces `Position` and `PositionCell`) +* `Spawn(objectId, target, v6, strict = 0)`: Same as `Teleport`, but spawns a new object `objectId` (replaces `PlaceItem` and `PlaceItemCell`) Functions: -* GetRotation: Returns instance's rotation as a Vector3 (replaces GetAngle) -* GetPosition: Returns instance's position as a Vector3 (replaces GetPos) +* `GetRotation`: Returns the instance's rotation as a `Vector3` (replaces `GetAngle`) +* `GetPosition`: Returns the instance's position as a `Vector3` (replaces `GetPos`) -## Object-Type Specific Additions +## Object-Type-Specific Additions We will add functions to modify and query other non-item-specific state that is already present in the cellref class and therefore already written to saved game files. Instructions: -* SetTrap (trapId): Set trap ID -* SetKey (trapId): Set key ID -* SetTeleport (target, v4): Set teleport location (does not affect teleport flag) +* `SetTrap(trapId)`: Sets the instance's trap ID to `trapId` +* `SetKey(trapId)`: Sets the instance's key ID to `keyId` +* `SetTeleport(target, v4)`: Sets the instance's teleport location to the location specified by `target` and `v4` (does not affect teleport flag) Functions: -* GetTrap: Return trap ID -* GetKey: Return key ID -* GetTeleportTarget: Return teleport target -* GetTeleportPosition: Returns teleport location and zrot as Vector4 +* `GetTrap`: Return the instance's trap ID +* `GetKey`: Return the instance's key ID +* `GetTeleportTarget`: Return the instance's teleport target +* `GetTeleportPosition`: Returns the instance's teleport location and zrot as `Vector4` ## Object-Types -We introduce compile time constants for object types (Weapons, Activators and so on). These have integer values. The constants must be named consistently in such a way that name collisions are unlikely (e.g. TypeWeapon, TypeActivator). +We introduce compile-time constants for object types (Weapons, Activators and so on). These have integer values. The constants must be named consistently in such a way that name collisions are unlikely (e.g. `TypeWeapon`, `TypeActivator`). We add a function that returns the type of an object or instance: -* GetObjectType +* `GetObjectType` ## Queries -A query returns a list of instances within a certain area. Along with the description of the area the query takes a list of object types (t) as an additional argument. +A query returns a list of instances within a certain area. Along with the description of the area, the query takes a list of object types (`t`) as an additional argument. -* QuerySphere t, v3, r -* QueryBox t, v3_tl, v3_br (this is an axis-aligned box) -* QueryCylinder t, v3, r, height (the cylinder is aligned along the Z-axis, v3 specifies the middle point of the bottom circle) +* `QuerySphere t, v3, r` +* `QueryBox t, v3_tl, v3_br` (this is an axis-aligned box) +* `QueryCylinder t, v3, r, height` (the cylinder is aligned along the z-axis, `v3` specifies the middle point of the bottom circle) ## Text Formatting -The vanilla scripting language provides text formatting in a single place (MessageBox instruction). This is insufficient, because: +The vanilla scripting language provides text formatting in a single place (`MessageBox` instruction). This is insufficient: -1. We will need text formatting in many places, independently of message boxes -2. The MessageBox text formatting sucks +1. We will need text formatting in many places, independently of message boxes. +2. The `MessageBox` text formatting sucks. -We will deprecate MessageBox. As a replacement we will introduce new instructions for creating message boxes (see following sub-section and GUI section) without a formatting features and also separate text formatting functions. +We will deprecate `MessageBox`. As a replacement, we will introduce new instructions for creating message boxes (see following subsection and *GUI* section) without a formatting feature and separate text formatting functions. -* Str(v, p = -1): Returns a string containing the string representation of the numeric value v. p specifies the number of digits after the decimal point. If p==-1 a suitable value depending on the type of v will be chosen. -* LFormat (s, a): Returns a string equal to the string s with all occurrences of %x replaced with the respective element indexed x from the string list a. -* Join (l, s): Return a string consisting of the members of the string list l separated by the string s +* `Str(v, p = -1)`: Returns a `String` containing the string representation of the numeric value `v`. `p` specifies the number of digits after the decimal point. If `p == -1`, a suitable value depending on the type of `v` will be chosen +* `LFormat(s, a)`: Returns a `String` equal to the `String` `s` with all occurrences of %x replaced with the respective element indexed x from the string list `a` +* `Join(l, s)`: Returns a `String` consisting of the members of the `StringList` `l` separated by the `String` `s` -## Multiple Choice Message Boxes +## Multiple-Choice Message Boxes -We add two new functions for showing multiple choice message boxes: +We add two new functions for showing multiple-choice message boxes: -* MakeChoice t, sl, f -* MakeBranchChoice t, sl, fl +* `MakeChoice t, sl, f` +* `MakeBranchChoice t, sl, fl` Arguments are as follows: -* t: The message box text -* sl: A list of strings, defining the available options -* f: The ID of a script function that is called when an option is selected. The function takes one long argument (the option index, starting at 0). -* fl: A list of script function names, same length as sl. Functions do not take any arguments. Empty strings in the list are allowed, which makes OpenMW ignore the respective choice. +* `t` is the message box's text +* `sl` is a list of strings, defining the available options +* `f` is the ID of a script function that is called when an option is selected; the function takes one `long` argument (the option index, starting at 0) +* `fl` is a list of script function names with the same length as `sl`; the functions do not take any arguments; empty strings in the list are allowed, which makes OpenMW ignore the respective choice ## Functions @@ -577,35 +575,35 @@ The syntax of the begin statement is extended in the following way: Begin { ( { -> return-type } ) } ```` -( {} denoting optional parts ) +**Note**: `{}` denotes optional parts. -Argument lists are allowed to be empty. Argument lists are comma-separated. Elements in argument list are type name pairs. Elements can be given default values in a C++ typical fashion. +Argument lists are allowed to be empty and are comma-separated. Elements in argument lists are type-name pairs. Elements can be given default values in a C++ fashion. -Arguments and return values are for all intents and purposes true local variable. Therefore the ref type is available. +Arguments and return values are for all intents and purposes true local variables. Therefore, the `Ref` type is available. ### Calling -A function can be called by its name followed by parenthesis. Arguments are listed inside the parenthesis separated by comma. +A function can be called by its name followed by parentheses. Arguments are listed inside the parentheses separated by commas. -We may at some point add the use of named arguments for function calling (comparable to Python), but this is most likely a stage 2 feature. +We may at some point add the use of named arguments for function calling (comparable to Python), but this is most likely a stage-2 feature. -If the function has a return type the function is evaluated as an expressions with a type corresponding to the return type. +If the function has a return type, the function is evaluated as an expression with a type corresponding to the return type. -Calling a function pauses the execution of the calling script, executes the called script and then resumes the execution of the calling script. This is different from the StartScript/StopScript instructions. The StartScript instruction is not modified (i.e. no argument passing). +Calling a function pauses the execution of the calling script, executes the called script and then resumes the execution of the calling script. This is different from the `StartScript` and `StopScript` instructions. The `StartScript` instruction is not modified (i.e., no argument passing). -A function call must be performed via a function name literal. The function name can not be given as a string variable, since this would make it impossible to check for the correctness of the function signature at compile time. +A function call must be performed via a function name literal. The function name cannot be given as a string variable, since this would make it impossible to check for the correctness of the function signature at compile time. Local and global scripts must not have any arguments without default values. ## Script Hooks -We introduce two new record type (hook, hooked script). The hooked script is a generalisation of the concept of a startup script. We may decide to fold the startup script record type into the hook record type. +We introduce two new record types (hook, hooked script). The hooked script is a generalisation of the concept of a start-up script. We may decide to fold the start-up script record type into the hook record type. ### Hooked Scripts A hooked script record binds a script to a hook record. More than one hooked script record per hook record is allowed. -The ID of a hook record is build from the hook record ID followed by a $ followed by the script name. +The ID of a hook record is built from the hook record ID followed by a `$` followed by the script name. Example: @@ -613,9 +611,9 @@ Example: Startup$KillFargoth ```` -By building hook IDs in this way we allow content files to delete hooked script records in dependencies without the need for an additional ID that identifies individual hook records. +By building hook IDs in this way, we allow content files to delete hooked script records in dependencies without the need for an additional ID that identifies individual hook records. -If more than one script is attached to a hook the order in which the scripts are executed is implementation defined at this point and scripts must not depend on a specific order. +**Note**: If more than one script is attached to a hook, the order in which the scripts are executed is, at this point, implementation-defined and, thus, scripts must not depend on a specific order. ### Hooks @@ -623,34 +621,34 @@ A hook record declares a new hook. Each hook record contains a list of argument There will be no return values (problematic since there can be multiple scripts per hook). -We provide system hooks within the namespace sys that are called in specific situations (see the section about de-hardcoding for some examples). Content developers may also provide their own hooks (user hooks). -System hooks are not added to content files, since they can not be modified by content developers anyway. We will instead inject the system hook records on content file load. +We provide system hooks within the namespace `sys` that are called in specific situations (see the section about de-hardcoding for some examples). Content developers may also provide their own hooks (user hooks). +System hooks are not added to content files, since they cannot be modified by content developers anyway. We will instead inject the system hook records on content-file load. System hooks are triggered by the engine. System hooks and user hooks can be triggered explicitly with a new script instruction. ## Script Slots -We define the term script slot as an optional sub-record in an object record that contains the name of a script. The default script slot for most object types is the "Run-Once-Per-Frame" slot, a.k.a. local script. +We define the term script slot as an optional subrecord in an object record that contains the name of a script. The default script slot for most object types is the "Run-Once-Per-Frame" slot, a.k.a. local script. -The default slot does not take any function arguments, but other slots can. The function arguments of a script attached to a slot need to match the hard-coded argument list of the slot. +The default slot does not take any function arguments, but other slots can. The function arguments of a script attached to a slot need to match the hardcoded argument list of the slot. -Note that if to scripts attached to the same object both define a (non-true) local variable with the same name, there will be only one variable. It is an error if the type of these variables don't match. +**Note**: If two scripts attached to the same object both define a (non-true) local variable with the same name, there will be only one variable. It is an error if the type of these variables don't match. ### Additional Slots -We add slots for OnX type keywords to object types where applicable The relevant keywords are: +We add slots for `OnX`-type keywords to object types where applicable. The relevant keywords are: -* OnActivate -* OnDeath -* OnKnockout -* OnMurder -* OnPCAdd -* OnPCDrop -* OnPcEquip -* OnHitMe -* OnPCRepair -* OnPCSoulGemUse -* OnRepair +* `OnActivate` +* `OnDeath` +* `OnKnockout` +* `OnMurder` +* `OnPCAdd` +* `OnPCDrop` +* `OnPcEquip` +* `OnHitMe` +* `OnPCRepair` +* `OnPCSoulGemUse` +* `OnRepair` ### Custom Slots @@ -658,221 +656,221 @@ We may allow the addition of custom slots (defined by the content developer), th ## Namespaces -The namespace of a script is determined by its ID. For example a script with the ID foo::Bar would be placed in the namespace foo. +The namespace of a script is determined by its ID. For example a script with the ID `foo::Bar` would be placed in the namespace `foo`. -IDs in a script refer to the local namespace by default, meaning the ID a in the script b::c would refer to b::a if such an ID exists or to ::a otherwise. +IDs in a script refer to the local namespace by default, meaning the ID `a` in the script `b::c` would refer to `b::a` if such an ID exists, or to `::a` otherwise. ## Other Script Instructions This is a collection of script instructions that fit nowhere else: -* GetPcTarget: Return the reference of the instance the player is currently looking at (crosshair). Can be a NullRef. -* GetMultiplayer: Always returns 0 -* GetPlayers: Returns a list of reference to all player controlled instances. This list contains a single reference to the instance of object Player. +* `GetPcTarget`: Returns the reference of the instance the player is currently looking at (crosshair); can be a null reference +* `GetMultiplayer`: Always returns 0 +* `GetPlayers`: Returns a list of references to all player-controlled instances; this list contains a single reference to the instance of object `Player` ## Other Script Hooks This is a collection of script hooks that fit nowhere else: -* sys::NewGameStarted: Executed when a new game is started. +* `sys::NewGameStarted`: Executed when a new game is started # Cells, Worldspaces & Areas -## Interior vs Exterior +## Interior vs. Exterior -The distinction between Interior and Exterior cells stopped making sense with the release of the Tribunal add-on. Therefore we should drop this terminology and replace it with Unpaged Cells (Interior) and Paged Cells (Exterior). +The distinction between interior and exterior cells stopped making sense with the release of the Tribunal add-on. Therefore, we should drop this terminology and replace it with **Unpaged Cells** (interior) and **Paged Cells** (exterior). -## Paged Cells +## Unpaged Cells -Paged cells remain essentially unchanged from old exterior cells, the only major difference being the ID and the worldspace. +Unpaged cells take the place of old interior cells. The only difference between old interior cells and unpaged cells is the worldspace. -Currently there is no uniform ID scheme for cells. Interior cells are named via an ID, exterior cells are named via a cell index (x, y). This needs to be changed, since we need a more consistent way to address cells. +By default, each unpaged cell has an associated worldspace record with the same ID. Furthermore, the unpaged-cell record is enhanced by an optional subrecord that specifies an alternative worldspace (if present). -The editor already gives exterior cells an ID (currently in the form of #x,y). The new scheme (relevant at least for the editor and scripting) will be "worldspace::x,y" (the cell will have the ID "x,y" in the namespace "worldspace"). We may also consider to replace the two integer coordinates in the cell record with the string ID. The performance impact of this change should be insignificant. +Most of the cell configuration data is moved out of the cell record and into the worldspace record. -The default exterior worldspace will be called default (previously called sys::default). Therefore the old exterior cell 0, 0 will be called "default:0,0". We are moving default out of sys to avoid creating an exception to the rule that content files are not allowed to create records in sys. +By moving out the worldspace into a separate record, we can unify worldspace data between interior and exterior cells. We can also associate multiple interior cells with the same worldspace or interior cells with an exterior worldspace. This should reduce duplicated data significantly. -## Unpaged Cells +## Paged Cells -Unpaged cells take the place of old interior cells. The only difference between old interior cells and unpaged cells is the worldspace. +Paged cells remain essentially unchanged from old exterior cells, the only major difference being the ID and the worldspace. -By default each unpaged cell has an associated worldspace record with the same ID. Furthermore the unpaged cell record is enhanced by an optional sub-record that specifies an alternative worldspace (if present). +Currently, there is no uniform ID scheme for cells. Interior cells are named via an ID, exterior cells are named via a cell index (x, y). This needs to be changed, since we need a more consistent way to address cells. -Most of the cell configuration data is moved out of the cell record and into the worldspace record. +The editor already gives exterior cells an ID (currently in the form of `#x,y`). The new scheme (relevant at least for the editor and scripting) will be `worldspace::x,y` (the cell will have the ID `x,y` in the namespace `worldspace`). We may also consider to replace the two integer coordinates in the cell record with the string ID. The performance impact of this change should be insignificant. -By moving out the worldspace into a separate record we can unify worldspace data between interior and exterior cells. We can also associate multiple interior cells with the same worldspace or interior cells with an exterior worldspace. This should reduce duplicated data significantly. +The default exterior worldspace will be called `default` (previously called `sys::default`). Therefore, the old exterior cell 0, 0 will be called `default:0,0`. We are moving `default` out of `sys` to avoid creating an exception to the rule that content files are not allowed to create records in `sys`. ## Worldspace Records -Worldspace records are shared by paged and unpaged cells (i.e. there are no interior and exterior worldspaces). +Worldspace records are shared by paged and unpaged cells (i.e., there are no interior and exterior worldspaces). Worldspace records contain the following fields: -* Water: Default Water level, Water ID (see de-hardcoding). Note: Script instructions that modify the water level will now modify the water level for a worldspace instead of a cell. +* Water: Default Water Level, Water ID (see de-hardcoding). **Note**: Script instructions that modify the water level will now modify the water level of a worldspace instead the one of a cell. * Ambient Light: 4 values -* Sky: Sky ID (analogous to Water ID, most likely not part of stage 1). Note: A worldspace can not have both an ambient light and a sky sub-record. -* Terrain: No data (might be better implemented as a flag). Presence indicate that worldspace has terrain. Ignored by unpaged cells. +* Sky: Sky ID (analogous to Water ID, most likely not part of stage 1). **Note**: A worldspace cannot have both an ambient-light and a sky subrecord. +* Terrain: No data (might be better implemented as a flag); presence indicates that worldspace has terrain; ignored by unpaged cells -All fields are optional. If a field is missing the respective cell/worldspace element is not present. +All fields are optional. If a field is missing, the respective cell/worldspace element is not present. -## References to cells in data structures +## References to Cells in Data Structures -Vanilla references to cells are generally based on the ID of the cell. There are no fields in vanilla Morrowind data structures that reference individual exterior cells. +Vanilla references to cells are generally based on the ID of the cell. There are no fields in vanilla Morrowind's data structures that reference individual exterior cells. We keep these reference fields, but change their meaning: -1. Check if it is the ID of an unpaged cell. If yes, use this cell -2. Check if it is a worldspace. If yes, use paged cells in this worldspace. +1. Check whether it is the ID of an unpaged cell. If yes, use this cell. +2. Check whether it is a worldspace. If yes, use the paged cells in this worldspace. 3. Otherwise, error. ## Scripts ### Water Level -The existing script instructions that deal with water level can be safely extended to the new system. We will add an optional argument at the end of the argument list of each instruction (GetWaterLevel, ModWaterLevel and SetWaterLevel) that specifies the worldspace the instruction is action on. If the argument is missing the instruction effects the current worldspace instead. +The existing script instructions that deal with water level can be safely extended to the new system. We will add an optional argument at the end of the argument list of each instruction (`GetWaterLevel`, `ModWaterLevel`, and `SetWaterLevel`) which specifies the worldspace the instruction is acting on. If the argument is missing, the instruction affects the current worldspace instead. -Note that this is different from vanilla Morrowind in regards to use in exterior cells. +**Note**: This behaviour is different from vanilla Morrowind in regards to exterior cells. ### New Instructions We add the following script instructions: -* EnumerateActiveCells: Returns a list of strings, containing the IDs of all cells currently active. Paged cells are listed individually. -* GetWorldspace (c): Returns the ID of the worldspace the cell with ID c is in -* GetWater (w): Returns the ID of the water used in the worldspace with ID w -* GetSky (w): Returns the ID of the sky used in the worldspace with ID w +* `EnumerateActiveCells`: Returns a `StringList` containing the IDs of all cells currently active; paged cells are listed individually +* `GetWorldspace(c)`: Returns the ID of the worldspace the cell with the ID `c` is in +* `GetWater(w)`: Returns the ID of the water used in the worldspace with the ID `w` +* `GetSky(w)`: Returns the ID of the sky used in the worldspace with the ID `w` ### Script hooks We add four new script hooks: -* RegionEntered -* RegionExited -* WorldspaceEntered -* WorldspaceExited +* `RegionEntered` +* `RegionExited` +* `WorldspaceEntered` +* `WorldspaceExited` -All four hooks take the ID of the region or worldspace as arguments and are executed when the player enters or exists a region or worldspace. +All four hooks take the ID of the region or worldspace as arguments and are executed when the player enters or exits a region or a worldspace. ## Areas The ability to designate a section of a worldspace with a specific ID that can be checked or referenced has many uses. A few examples: -* Give a NPC certain dialogue topics only in specific places +* Give an NPC certain dialogue topics only in specific places * Limit wandering NPCs to an area * Add special/magical effects to a location -Currently our ability to do that is largely limited to cells. This is a problem because of two reasons: +Currently, our ability to do that is largely limited to cells. This is a problem because of two reasons: -* The fixed-size square nature of nature of cells makes areas unreasonably inflexible. +* The fixed-size, square nature of cells makes areas unreasonably inflexible. * We can't have overlapping areas. ### Record -We introduce a new record (Area) that consists of the following fields: +We introduce a new record ("Area") that consists of the following fields: * Worldspace ID -* Polygon (list of 2D coordinates), defining a surface on the x-y-plane -* Min-Height: Z-coordinate at which the area starts -* Max-Height: Z-coordinate at which the area ends +* Polygon: A list of 2D coordinates defining a surface on the xy-plane +* Min-Height: z-coordinate at which the area starts +* Max-Height: z-coordinate at which the area ends * Enter script ID (string, optional): Script function that is called when the player enters the area * Exit script ID (string, optional): Script function that is called when the player exits the area (must also be called in case of teleportation) * Inside script ID (string, optional): Script function that is called while the player is in the area. -* Inside script delta (float, optional): Minimum time between two executions of the inside script function. -* Composite (integer, optional): If this flag is set the area will be ignored by OpenMW except as a part of a joined area. Composite areas are not accessible by script instructions and do not run any scripts of their own. +* Inside script delta (float, optional): Minimum time between two executions of the inside-script function. +* Composite (integer, optional): If this flag is set, the area will be ignored by OpenMW except as a part of a joined area. **Note**: Composite areas are not accessible by script instructions and do not run any scripts of their own. All script functions take the ID of the area as a parameter. -Whenever a cell or a worldspace is referenced for checking or defining locations the ID of an area can be used instead. +Whenever a cell or a worldspace is referenced to check or define locations, the ID of an area can be used instead. ### Script Functions We add the following script functions: -* GetAreas (v3): Returns a string list containing the IDs of all areas v3 is inside of -* InArea (id): Returns 1 or 0 depending on if the instance is in area id or not +* `GetAreas(v3)`: Returns a `StringList` containing the IDs of all areas `v3` is in +* `InArea(id)`: Returns 1 (instance is in area `id`) or 0 (otherwise) ## Joined Areas -A joined area is an area that consists of multiple un-joined areas. Joined areas can contain both composite and non-composite areas. A joined area can stretch across multiple worldspaces. Scripts do not distinguish between areas and joined areas. +A joined area is an area that consists of multiple unjoined areas. Joined areas can contain both composite and non-composite areas. A joined area can stretch across multiple worldspaces. Scripts do not distinguish between areas and joined areas. ### Record -We introduce a new record (JoinedArea) that consists of the following fields: +We introduce a new record ("Joined Area") that consists of the following fields: * Enter script ID (string, optional): Script function that is called when the player enters the area * Exit script ID (string, optional): Script function that is called when the player exits the area (must also be called in case of teleportation) * Inside script ID (string, optional): Script function that is called while the player is in the area. -* Inside script delta (float, optional): Minimum time between two executions of the inside script function. +* Inside script delta (float, optional): Minimum time between two executions of the inside-script function. -# Item-Interactions & -Management +# Item Interaction & Item Management ## Deletion -We will add an instruction to delete instances via a reference variable. Current solutions are insufficient because they can not target specific items (when in a container) and require different approaches depending on if an instance is in a container or in the world. Self-deletion must be safe. For the sake of consistency We extend this instruction to work on non-item objects too. +We will add an instruction to delete instances via a reference variable. Current solutions are insufficient because they can not target specific items (when in a container) and require different approaches depending on whether an instance is in a container or in the world. Self-deletion must be safe. For the sake of consistency, we extend this instruction to work on non-item objects too. -* Delete (reference specification for instance to be deleted via -> operator in the usual way) +* `Delete` (reference specification for instance to be deleted via the `->` operator in the usual way) ## Container -We will add a function that returns the contents of a container as a list of references (the term container includes here creatures and NPCs). +We will add a function that returns the contents of a container as a list of references. **Note**: In this case, the term "container" also includes creatures and NPCs. -* GetContents (reference specification for container via -> operator in the usual way) +* `GetContents` (reference specification for container via the `->` operator in the usual way) -We will add a function that moves/clones an item instance into a container (actual container or NPC). This feature (especially the move function) preludes a concept of persistent instance identity, which is most likely a stage 2 or OpenMW 2.0 feature. For now these functions are required to deal with state attached to the instance in the form of record variables/old local variables. +We will add a function that moves/clones an item instance into a container (actual container or actor). This feature (especially the `move` function) preludes a concept of persistent instance identity, which is most likely a stage-2 or OpenMW-2.0 feature. For now, these functions are required to deal with state attached to the instance in the form of record variables / old local variables. (In all cases, the reference specification for the item to be moved/cloned works via the `->` operator in the usual way.) -* MoveItemToContainer (ref/id, count=1): Move item from current location to container specified by ref/id -* CloneItemToContainer (ref/id, count=1): Add a copy of item to container specified by ref/id +* `MoveItemToContainer(ref/id, count = 1)`: Move item from current location to container specified by `ref/id` +* `CloneItemToContainer(ref/id, count = 1)`: Add a copy of item to container specified by `ref/id` -(in all cases reference specification for item to be moved/cloned work via -> operator in the usual way) - -The count argument is ignored when the original item is not in a container. +The `count` argument is ignored when the original item is not in a container. ## Other Item-Related Instructions -* SetItemHealth (health): Set item current health -* SetItemCharge (charge): Set item current charge -* SetItemOwner (owner): Set item owner ID -* SetItemSoul (soul): Set soul ID (soul gems only) -* SetItemFaction (faction): Set item faction ID -* SetItemFactionRank (rank): Set item faction rank +Instructions: + +* `SetItemHealth(health)`: Sets item's current health +* `SetItemCharge(charge)`: Sets item's current charge +* `SetItemOwner(owner)`: Sets item's owner ID +* `SetItemSoul(soul)`: Sets item's soul ID (soul gems only) +* `SetItemFaction(faction)`: Sets item's faction ID +* `SetItemFactionRank(rank)`: Sets item's faction rank Functions: -* IsItem: Return 1 if reference is an item, 0 otherwise -* GetItemHealth: Return item current health -* GetItemMaxHealth: Return item max health -* GetItemCharge: Return item current charge -* GetItemMaxCharge: Return item max charge -* GetItemOwner: Return item owner ID -* GetSoulItem: Return soul ID (soul gems only) -* GetItemFaction: Return item faction ID -* GetItemFactionRank: Return item faction rank +* `IsItem`: Returns 1 if reference is an item, 0 otherwise +* `GetItemHealth`: Returns item's current health +* `GetItemMaxHealth`: Returns item's maximum health +* `GetItemCharge`: Returns item's current charge +* `GetItemMaxCharge`: Returns item's maximum charge +* `GetItemOwner`: Returns item's owner ID +* `GetSoulItem`: Returns item's soul ID (soul gems only) +* `GetItemFaction`: Returns item's faction ID +* `GetItemFactionRank`: Returns item's faction rank ## Item Tags -Currently there is no customisable way of categorising items. To compensate for this shortcoming we introduce items tags. +Currently, there is no customisable way of categorising items. To compensate for this shortcoming, we introduce item tags. An item tag is a string that is attached to an object record. An object record can have multiple tags. -Objects also have implicit item tags that are determined by their type (e.g. every weapon object has a tag weapon even without the object record explicit containing this tag. We may introduce other kinds of implicit tags (e.g. weapon types, enchanted items). +Objects also have implicit item tags that are determined by their type (e.g., every weapon object has a tag "Weapon" even without the object record explicitly containing this tag). We may introduce other kinds of implicit tags (e.g. weapon types, enchanted items). The de-hardcoding introduces additional item tags. Item tags are immutable at runtime and can be queried via script instructions: -* GetItemTags (Id): Returns a string list of tags for the object Id. As usual instead of an ID a reference variable can be used. -* HasitemTag (Id, Tag): Return 1 or 0 depending on Id has the tag Tag. +* `GetItemTags(id)`: Returns a `StringList` of tags for the object `id`; as usual, instead of an ID a reference variable can be used +* `HasitemTag(id, tag)`: Returns 1 or 0 depending on `id` having the tag `tag` -Using these instructions on non-item objects return an empty list and 0 respectively. +Using these instructions on non-item objects returns an empty list or 0 respectively. -Item tags can be used to organise container windows (see GUI section). +Item tags can be used to organise container windows (see *GUI* section). A few examples of tags that content developers may come up with: -* quest: A quest item -* vendor: An item that has no other use than to sell it to a vendor +* "Quest": A quest item +* "Vendor": An item that has no other use than to sell it to a vendor -We may suggest some default tags in the documentation (or even the editor) to encourage more consistent tag use by content developers. +We may suggest some default tags in the documentation (or even the editor itself) to encourage more consistent tag use by content developers. ## Interactions @@ -882,435 +880,467 @@ We enhance the way how the player interacts with the world, especially via items There are three methods of interaction: -* The player presses the attack button while holding an interaction-item in his hand and targeting an object in the world. This feature exists in vanilla MW only within the security skill. We generalise these kinds of interactions (see Held Items in the De-Hardcoding section). We also allow this feature for weapons held in the main hand. If an interaction is possible the interaction takes the place of the attack. If the interaction is ignored or refused the attack proceeds. -* The player drags an item from a container onto on instance in the world. This kind of interaction does not exist in vanilla MW. -* The player drags an item from a container onto another item in another (or the same) container. This kind of interaction does not exist in vanilla MW. +* The player presses the attack button while holding an interaction item in his hand and targeting an object in the world. This feature exists in vanilla Morrowind only within the Security skill. We generalise this kind of interaction (see *Held Items* in the *De-Hardcoding* section). We also allow this feature for weapons held in the main hand: If an interaction is possible, the interaction takes the place of the attack. If the interaction is ignored or refused, the attack proceeds. +* The player drags an item from a container onto an instance in the world. This kind of interaction does not exist in vanilla Morrowind. +* The player drags an item from a container onto another item in another (or the same) container. This kind of interaction does not exist in vanilla Morrowind. ### Interaction Subrecords -All item object record types are enhanced by a new optional sub-record that holds the name of the ItemItemInteraction script. +All item-object record types are enhanced by a new optional subrecord that holds the name of the ItemItemInteraction script. -All object record types (including items) are enhanced by a new optional sub-record that holds the name of the ItemObjectInteraction script. +All object record types (including items) are enhanced by a new optional subrecord that holds the name of the ItemObjectInteraction script. -### Interaction-Chain +### Interaction Chain -Custom interactions are handled by running through a sequence of scripts. Each script can either ignore the interactions or accept it or refuse it. +Custom interactions are handled by running through a sequence of scripts. Each script can either ignore the interaction, accept it, or refuse it. -If the action is accepted or refused the chain stops. If the action is refused or the chain runs to its end without any script accepting it the player will be notified via a sound-effect. We may either re-purpose an existing sound-effect or acquire a new one. +If the action is accepted or refused, the chain stops. If the action is refused or the chain runs to its end without any script accepting it, the player will be notified via a sound effect. We may either re-purpose an existing sound effect or acquire a new one. We add two new script instructions: -* AcceptInteraction -* RefuseInteraction +* `AcceptInteraction` +* `RefuseInteraction` -Using any of these instructions outside of an interaction chain is an error. We use these instructions instead of return values, because a part of the interaction chain works via hooks which do no provide return values. +Using any of these instructions outside of an interaction chain is an error. We use these instructions instead of return values, because a part of the interaction chain works via hooks which do not provide return values. All interaction chain functions share the following signature: -* ref to Item A/Item -* ref to Item B/Object -* ref to actor performing the interaction (player for now, we may extend that later) -* integer: 0 interaction was initiated via drag & drop, 1 interaction was initiated via attack button +* reference to item A +* reference to item/object B +* reference to actor performing the interaction (player for now, we may extend that later) +* integer: 0 if interaction was initiated via drag & drop, 1 if interaction was initiated via attack button -### Item-Item-Chain: +### Item-Item Chain: -* Hook sys::PreItemItemInteraction -* ItemItemInteraction Script of object A -* ItemItemInteraction Script of object B -* Hook sys::ItemItemInteraction +* Hook `sys::PreItemItemInteraction` +* `ItemItemInteraction script of item A +* `ItemItemInteraction script of item B +* Hook `sys::ItemItemInteraction` -### Item-Object-Chain: +### Item-Object Chain: If the object is also an item the Item-Item-Chain is used instead. -* Hook sys::PreItemObjectInteraction -* ItemObjectInteraction Script of item -* ItemObjectInteraction Script of object -* Hook sys::ItemObjectInteraction +* Hook `sys::PreItemObjectInteraction` +* `ItemObjectInteraction` script of item A +* `ItemObjectInteraction` script of object B +* Hook `sys::ItemObjectInteraction` # De-Hardcoding -This section describes the first batch of de-hardcoding tasks. This is the core part of stage 1 (*The Grand De-Hardcoding*). We are aiming mostly for low hanging but highly profitable fruits here. More complex de-hardcoding (especially tasks that require more extensive script support) will follow in stage 2. +This section describes the first batch of de-hardcoding tasks. This is the core part of stage 1 (*The Grand De-Hardcoding*). We are aiming mostly for low-hanging, but highly profitable fruits here. More complex de-hardcoding (especially tasks that require more extensive script support) will follow in stage 2. ## GMSTs Many GMSTs will be integrated into other records, which will effectively remove the GMST. We may consider actually removing the GMST record during the load process. -We add three new types of GMSTs: +We will add three new types of GMSTs: -* IntegerList -* FloatList -* StringList +* `IntegerList` +* `FloatList` +* `StringList` -We may consider allowing content files to create new GMSTs outside of the sys namespace. This would make the GMST record type more consistent with other record types. To make this addition useful we need to add script functions to read GMSTs: +We may consider allowing content files to create new GMSTs outside of the `sys` namespace. This would make the GMST record type more consistent with other record types. To make this addition useful, we need to add script functions to read GMSTs: -* Keyword: Get{Type}Gmst() +* `Get{Type}Gmst()` -{Type} can be Integer, Float, String, Integerlist, FloatList or StringList. +`{Type}` can be of type `long`, `float`, `String`, `Longlist`, `FloatList`, or `StringList`. ## Fallback Values -The openmw.cfg file contains a set of fallback values. These were extracted from the Morrowind.ini file. As the name indicates we consider these values as a fallback for legacy format content files only. Our goal is to move all these values to content file format. In some cases we also may decide to declare a fallback value obsolete and not use it at all. +The *openmw.cfg* file contains a set of fallback values. These were extracted from the *Morrowind.ini* file. As the name indicates, we consider these values as a fallback for legacy-format content files only. Our goal is to move all these values to content file format. In some cases, we may also decide to declare a fallback value obsolete and not use it at all. All usable values should be migrated to new GMST records, unless they are already covered by other parts of the de-hardcoding. ## General Scripting Enhancements -We introduce new script functions: +We will introduce new script functions: -* Enumerate{x}: {x} is either Skills, Races, Classes, DynamicStats, Attributes, WeaponTypes, ArmorTypes, Specialisations, MagicSchools, Factions or Birthsigns. Returns a string list of the IDs of all records of the respective type. +* `Enumerate{x}`: Returns a `StringList` of the IDs of all records of the respective type; `{x}` is either "Skills", "Races", "Classes", "DynamicStats", "Attributes", "WeaponTypes", "ArmorTypes", "Specialisations", "MagicSchools", "Factions" or "Birthsigns" ## Dynamic Stats -We will unify dynamic stats and make them configurable. For stage 1 we will not allow the deletion of existing dynamic stats (they are used in too many parts of the engine). But we will allow new dynamic stats. +We will unify dynamic stats and make them configurable. For stage 1, we will not allow the deletion of existing dynamic stats (they are used in too many parts of the engine). But we will allow new dynamic stats. -We add a new record type for dynamic stats. Records for Health (sys::Health), Fatigue (sys::Fatigue) and Magicka (sys::Magicka) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). +We add a new record type for dynamic stats. Records for Health (`sys::Health`), Fatigue (`sys::Fatigue`), and Magicka (`sys::Magicka`) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). -A dynamic stats record contains the following information: +A dynamic-stat record contains the following information: * Name (taken from GMSTs for default stats) -* Tooltip text (taken from GMSTs for default stats) +* Tooltip Text (taken from GMSTs for default stats) * Colour -* (optional) PrevID: ID of stat the engine will try to sort in this stats after (similarly to info records) -* (optional) NextID: ID of stat the engine will try to sort in this stats before (similarly to info records) -* (optional) Vital-flag (player dies if stat with vital flag drops to 0) -* (optional) ID of level-up function: Takes the reference of the character as argument and returns an integer value representing the max stat change from the level-up. -* (optional) ID of time passed function: Takes the reference of the character and the duration as arguments. Returns a float that is used to modify the current stat value. Not used when waiting or sleeping. -* (optional) ID of wait function: Takes the reference of the character, the duration and a sleep flag (integer) as arguments. Returns a float that is used to modify the current stat value. -* Default value: If an actor does not have this stat (possible if the stat was defined in an addon and not in the base-game) this value is used for the maximum value of the stat. +* (optional) PrevID: ID of the stat the engine will try to subsequently sort this stat in (similar to info records) +* (optional) NextID: ID of the stat the engine will try to antecedently sort this stat in (similar to info records) +* (optional) Vital: Flag which specifies whether the player dies if the stat drops to 0 +* (optional) Level-Up Function: ID of the stat's level-up function, which takes the reference of the character as argument and returns an `long` value representing the maximum stat change achievable with a level-up +* (optional) Time-Passed Function: ID of the stat's time-passed functionm, which takes the reference of the character and the duration as arguments and returns a `float` that is used to modify the current stat value (the function is not used when waiting or sleeping) +* (optional) Wait Function: ID of the stat's wait function, which takes the reference of the character, the duration, and a sleep flag (integer) as arguments and returns a `float` that is used to modify the current stat value +* Default Value: Value that is used as the maximum value of the stat if an actor does not have this stat (possible if the stat was defined in an add-on and not in the base game) -Scripts for default stats are injected as necessary. Some of these scripts require access to GMST values. We need to figure out how to implement this, either grab the value from the GMST when adding the script or adding a script function to read GMST values. The later is easier but will not allow us to remove the redundant GMST record easily. +Scripts for default stats are injected as necessary. Some of these scripts require access to GMST values. We need to figure out how to implement this: either grab the value from the GMST when adding the script or adding a script function to read GMST values. The latter is easier but will not allow us to remove the redundant GMST records easily. -The currently existing Get, Set and Mod instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the stat ID as an argument. We need separate instructions to deal with base, modified and current values. +The currently existing `Get`, `Set`, and `Mod` instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the stat ID as an argument. We need separate instructions to deal with base, modified and current values. ## Attributes We will unify attributes and make them configurable. For stage 1 we will not allow the deletion of existing attributes (they are used in too many parts of the engine). But we will allow new attributes. -We add a new record type for attributes. Records for the existing attributes (sys::NameOfAttribute) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). +We add a new record type for attributes. Records for the existing attributes (`sys::NameOfAttribute`) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). -A attribute record contains the following information: +An attribute record contains the following information: * Name (taken from GMSTs for default attributes) -* Tooltip text (taken from GMSTs for default attributes) -* (optional) PrevID: ID of attribute the engine will try to sort in this attribute after (similarly to info records) -* (optional) NextID: ID of attribute the engine will try to sort in this attribute before (similarly to info records) -* Default value: If an actor does not have this attribute (possible if the attribute was defined in an addon and not in the base-game) this value is used for the attribute. +* Tooltip Text (taken from GMSTs for default attributes) +* (optional) PrevID: ID of the attribute the engine will try to subsequently sort this attribute in (similar to info records) +* (optional) NextID: ID of the attribute the engine will try to antecedently sort this attribute in (similar to info records) +* Default Value: Value that is used as the maximum value of the attribute if an actor does not have this attribute (possible if the attribute was defined in an add-on and not in the base game) -Note that all records that reference attributes need to have the respective fields be changed from integers to strings. +**Note**: All records that reference attributes need the respective fields to be changed from integers to strings. -The currently existing Get, Set and Mod instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the attribute ID as an argument. We need separate instructions to deal with base, modified and current values. +The currently existing `Get`, `Set`, and `Mod` instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the attribute ID as an argument. We need separate instructions to deal with base, modified and current values. -Additionally we will add a new GMST sys::ClassAttributes of type integer. This GMST specifies the number of favoured attributes that a class has. The value defaults to 2. +Additionally, we will add a new GMST `sys::ClassAttributes` of type `long`. This GMST specifies the number of favoured attributes that a class has. The value defaults to 2. ## Weapon Types We will unify weapon types and make them configurable. -We add a new record type for weapon types. Records for the existing weapon types (sys::NameOfType) will be created by the engine when loading older omwgame files. +We add a new record type for weapon types. Records for the existing weapon types (`sys::NameOfType`) will be created by the engine when loading older omwgame files. A weapon type record contains the following information: * Name (taken from GMSTs for default types) -* HandlingType (how the weapon is held and what animations to play when attacking) -* Name of HitTestScript (see Combat subsection) +* Handling Type: Defines how the weapon is held and what animations play when attacking +* Hit Test Script: Name of the hit test script used (see *Combat* subsection) + +For stage 1, the weapon type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However, there are already possible uses, e.g., a weapon type "Ancient Blades" which requires a separate skill. + +We add a new script function: -For stage 1 the weapon type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However there are already possible uses (for example a type of "ancient blades" that require a separate skill). +* `GetWeaponTypeId id`: Returns the weapon type ID of an instance or an object with the ID `id` -We add another script function that returns the weapon type ID of an instance or an object ID: GetWeapopnTypeId. Note that GetWeaponType is already taken by Tribunal and we can not repurpose this name without breaking compatibility. +**Note**: `GetWeaponType` is already taken by Tribunal and we can not repurpose this name without breaking compatibility. ## Armour Types -In vanilla Morrowind armour types exist only implicitly. There is no record and no option to select armour types. Armour types are determined by weight only. +In vanilla Morrowind, armour types exist only implicitly. There is no record and no option to select armour types. Armour types are determined by weight only. -We will keep implicit armour types as an option, but also add armour types explicitly as a new type of record. +We will keep implicit armour types as an option, but we'll also add armour types explicitly as a new type of record. -Records for the existing types (sys::LightArmorType, sys::MediumArmorType, sys::HeavyArmorType) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). +Records for the existing types (`sys::LightArmorType`, `sys::MediumArmorType`, `sys::HeavyArmorType`) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). -A armour type record contains the following information: +An armour type record contains the following information: * Name (taken from GMSTs for default types) * (optional) Minimum weight -Additional armour object records are extended by the optional field "armor type". +Additionally, armour object records are extended by the optional field "Armour type". + +If an armour object has no armour type specified, its type is determined by its weight according to the following algorithm: + +1. Consider all armour types with a minimum weight field. +2. Exclude types that have a minimum weight larger than the weight of the object. +3. Pick the type with the largest minimum weight. -If an armour object does not have this field its type is determined by the weight by the following algorithm: +For stage 1, the armour type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However, there are already possible uses, e.g., imitating TES IV: Oblivion by eliminating medium armour or adding a new armour type such as "Ancient Armour" which requires a separate skill. -* Consider all armour types with a minimum weight field -* Exclude types that have a minimum weight larger than the weight of the object -* Pick the type with the largest minimum weight +We add another script function: -For stage 1 the armour type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However there are already possible uses (for example going Oblivion by killing medium armour or adding a new type of armour "ancient armor" that requires a separate skill). +* `GetArmorTypeId id`: Returns the armour type ID of an instance or an object with the ID `id` -We add another script function that returns the armour type ID of an instance or an object ID: GetArmorTypeId. +**Note**: For the sake of consistency, we've adopted the naming scheme introduced for weapon types. ## Specialisation We will unify specialisations and make them configurable. -We add a new record type for specialisations. Records for Combat (sys::CombatSpec), Stealth (sys::StealthSpec) and Magic (sys::MagicSpec) will be created by the engine when loading older omwgame files. +We add a new record type for specialisations. Records for Combat (`sys::CombatSpec`), Stealth (`sys::StealthSpec`), and Magic (`sys::MagicSpec`) will be created by the engine when loading older omwgame files. A specialisation record contains the following information: * Name (taken from GMSTs for default types) -We add a new script instruction GetSpecialization id, that returns the specialisation of a class or a skill with the given ID. +We add a new script instruction: + +* `GetSpecialization id`: Returns the specialisation of a class or a skill with the given ID `id` ## Magic Schools We will unify magic schools and make them configurable. -We add a new record type for magic schools. Records for the existing magic schools will be created by the engine when loading older omwgame files. +We add a new record type for magic schools. Records for the existing magic schools (`sys::AlterationMagicSchool`, `sys::ConjurationMagicSchool`, etc.) will be created by the engine when loading older omwgame files. A magic school record contains the following information: * Name (taken from GMSTs for default schools) -* Specialisation (sys::MagicSpec for default schools) -* Resource (sys::Magicka for default schools) +* Specialisation (`sys::MagicSpec` for default schools) +* Resource (`sys::Magicka` for default schools) -A use for the specialisation field would be (for example) dividing magic up into two categories "divine magic" and "arcane magic" and have characters specialise accordingly. +A use for the specialisation field would be, e.g., dividing magic into the two categories "Divine Magic" and "Arcane Magic" and having characters specialise accordingly. ## Skills -Skills are a complex topic and we won't achieve full de-hardcoding in stage 1 and most like there will never be a 100% complete de-hardcoding. But there is still a lot we can do with skills. +Skills are a complex topic and we won't achieve full de-hardcoding in stage 1 - and most likely there will never be a 100% complete de-hardcoding. But there is still a lot we can do with skills. -Currently skills exist as indexed records. We need to move these over to ID-based records. The exiting indices are translated to IDs of the form sys::NameOfSkill. +Currently, skills exist as indexed records. We need to move these over to ID-based records. The exiting indices are translated to IDs of the form `sys::NameOfSkill`. -We add a new sub-record type (Function Sub-Record, see below). Each skill can have zero, one or multiple function sub-records. +We add a new subrecord type (see subsection *Function Subrecords* below). Each skill can have zero, one, or multiple function subrecords. The following skills are too tightly bound into the engine to allow their easy removal: -* Armorer -* Enchanting -* Alchemy * Acrobatics -* Block -* Sneak +* Alchemy +* Armorer * Athletics +* Block +* Enchant * Hand-to-hand -* Unarmored * Mercantile +* Sneak * Speechcraft +* Unarmored -Therefore we will (for now) forbid the deletion of the respective skill records. All other default skills can be deleted. +Therefore, we will (for now) forbid the deletion of the respective skill records. All other default skills can be deleted. -### Scripts Instructions +### Script Instructions -The currently existing Get, Set and Mod instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the skill ID as an argument. We need separate instructions to deal with base and modified values. +The currently existing `Get`, `Set`, and `Mod` instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the skill ID as an argument. We need separate instructions to deal with base and modified values. We also introduce new script instructions: -* UseSkill id, v: Progress skill id by a value of v (float). -* UseValueSkill Id, i: Progress skill id by the value given by use value field with index i (0, 1, 2 or 3) -* Get{x}SkillLevel s_id: Return the skill level of a function sub-record with TargetId s_id (string). {x} is either Armor, Weapon or Magic. See next sub-section for details. +* `UseSkill id, v`: Progresses the skill `id` by a value of `v` (`float`). +* `UseValueSkill Id, i`: Progresses the skill `id` by the value given by the use-value field with index `i` (0, 1, 2 or 3) +* `Get{x}SkillLevel s_id`: Returns the skill level of a function subrecord with `TargetId` `s_id` (`String`). `{x}` is either "Armor", "Weapon", or "Magic"; see the next subsection for details. -Note that we do not de-hardcode the fixed number of use value fields. The usefulness of these fields is most likely limited for new skills. The UseSkill instruction, which bypasses the use value fields should be sufficient in most cases. We only add the UseValueSkill instructions to better support script interactions with default skills. +**Note**: We do not de-hardcode the fixed number of use-value fields. The usefulness of these fields for new skills is most likely limited. The `UseSkill` instruction, which bypasses the use-value fields, should be sufficient in most cases. We only add the `UseValueSkill` instructions to better support script interactions with default skills. -### Function Sub-Records +### Function Subrecords -A function sub-record describes how a skill interacts with hardcoded engine functions. A function record consists of a function ID (we use an index here, because the list of functions will definitely not be extensible by content files) and additional arguments. +A function subrecord describes how a skill interacts with hardcoded engine functions. A function record consists of a function ID (we use an index here, because the list of functions will definitely not be extensible by content files) and additional arguments. -For stage one we require only one additional argument: +For stage 1, we require only one additional argument: -* TargetId (single string value) +* `TargetId` (single `String` value) -For stage 1 we introduce three function IDs: Armour skill, weapon skill, magic skill. +Stage 1 will introduce three function IDs: -We do not forbid non-unique function sub-records; meaning two skills may have identical function sub-records (e.g. two skills governing light armor). If there is more than one skill with the same function sub-record and the skill level for this function needs to be considered, we use the maximum over all relevant skill levels. +* Armour Skill +* Weapon Skill +* Magic Skill + +We do not forbid non-unique function subrecords, i.e., two skills may have identical function subrecords, e.g., two skills governing light armour. If there is more than one skill with the same function subrecord and the skill level for this function needs to be considered, we use the maximum of all relevant skill levels. ### Armour Skills -We make armour skills fully customisable by introducing a function ID for armour types. Any skill that has a function sub-record with this ID is an armour skill. For default armour skills loaded from older omwgame files we will inject function sub-records during the loading process. +We make armour skills fully customisable by introducing a function ID for armour types. Any skill that has a function subrecord with this ID is an armour skill. For default armour skills loaded from older omwgame files, we will inject function subrecords during the loading process. + +The `TargetId` of an armour function subrecord indicates the armour type that the skill is governing. -The TargetId of a armour function sub-record indicates the armour type that the skill is governing. +The default skills are: -The default skills are: Heavy Armor, Medium Armor, Light Armor. +* "Heavy Armor" +* "Light Armor" +* "Medium Armor" ### Weapon Skills -We make weapon skills fully customisable by introducing a function ID for weapon types. Any skill that has a function sub-record with this ID is a weapon skill. For default weapon skills loaded from older omwgame files we will inject function sub-records during the loading process. +We make weapon skills fully customisable by introducing a function ID for weapon types. Any skill that has a function subrecord with this ID is a weapon skill. For default weapon skills loaded from older omwgame files, we will inject function subrecords during the loading process. + +The `TargetId` of a weapon function subrecord indicates the weapon type that the skill is governing. -The TargetId of a weapon function sub-record indicates the weapon type that the skill is governing. +The default skills are: -The default skills are: Spear, Axe, Blunt Weapon, Long Blade, Marksman, Short Blade +* "Axe" +* "Blunt Weapon" +* "Long Blade" +* "Marksman" +* "Short Blade" +* "Spear" ### Magic Skills -We make magic skills fully customisable by introducing a function ID for magic schools. Any skill that has a function sub-record with this ID is a magic skill. For default magic skills loaded from older omwgame files we will inject function sub-records during the loading process. +We make magic skills fully customisable by introducing a function ID for magic schools. Any skill that has a function subrecord with this ID is a magic skill. For default magic skills loaded from older omwgame files, we will inject function subrecords during the loading process. -The TargetId of a magic school function sub-record indicates the magic school that the skill is governing. +The `TargetId` of a magic school function subrecord indicates the magic school that the skill is governing. -The default skills are: Illusion, Conjuration, Alteration, Destruction, Mysticism, Restoration +The default skills are: + +* "Alteration" +* "Conjuration" +* "Destruction" +* "Illusion" +* "Mysticism" +* "Restoration" ## Weather -We make weather types customisable by moving from hard-coded index based weather effects to ID based weather types stored in content files. To this end we introduce a new record type (Weather). When loading older omwgame files we will inject weather records for the ten default weather types. +We make weather types customisable by moving from hardcoded, index-based weather effects to ID-based weather types stored in content files. To achieve this, we introduce a new record type ("Weather"). When loading older omwgame files, we will inject weather records for the ten default weather types (`sys::WeatherAsh`, `sys::WeatherBlight`, etc.). -A weather record is made up from sub-records, each describing a weather effect. A weather record can have any number of weather effect sub-records. +A weather record is made up of subrecords, each describing a weather effect. A weather record can have any number of weather effect subrecords. -There are three types of effect sub-records: +There are four types of weather effect subrecords: -* Magic effect: A magic effect that is applied to all actors within active cells while the respective weather type is active. -* Sky: Modifications to the visuals of the sky (e.g clouds) -* Particles: Particle effects (e.g. rain) -* Event: Events that can happen with a certain probability while the weather type is active (e.g. lightning). Events happen at random locations. +* "Magic Effect": A magic effect that is applied to all actors within active cells while the respective weather type is active +* "Sky": Modifications to the visuals of the sky (e.g, clouds) +* "Particles": Particle effects (e.g., rain) +* "Event": Events that can happen with a certain probability while the weather type is active (e.g., lightning); events happen at random locations -Magic effect sub-records contain the ID of the respective magic effect. All other sub-records contain an integer ID specifying the effect and (if necessary) additional parameters. +Magic effect subrecords contain the ID of the respective magic effect. All other subrecords contain an integer ID specifying the effect and (if necessary) additional parameters. -All effects described in these sub-records affect active cells only. +All effects described in these subrecords affect active cells only. ### Sky -TODO all existing sky effects and possibly new ones +TODO: +- Should cover all existing sky effects and possibly new ones ### Particles -TODO all existing particle effects and possibly new ones +TODO: +- Should cover all existing particle effects and possibly new ones ### Event We will add the following event IDs: -* 0: Scripted Event: Additional data consists of the name of the script to be run when the event is triggered. The script takes the following arguments: worldspace (string), location (Vector3), weather ID (string) -* 1: Lightning - -In addition to the event ID and event ID specific data event sub-records have an event trigger block. +* "0": Scripted event; additional data consists of the name of the script to be run when the event is triggered; the script takes the following arguments: worldspace (`String`), location (`Vector3`), weather ID (`String`) +* "1": Lightning -### Event, Trigger +In addition to the event ID and event-ID-specific data, event subrecords have an "Event Trigger" block: -* Chance: The chance of the event happening (floating number in the range between 0 and 1) per unit of time. We may want to make the unit of time globally configurable with a new GMST. -* Min-Distance (optional): The minimum distance from the player at which the effect can take place (0 by default). -* Max-Distance (optional): The maximum distance from the player at which the effect can take place (infinite by default): +* Chance: Floating number in the range between 0 and 1; the chance per unit of time that the event happens (we may want to make the unit of time globally configurable with a new GMST) +* (optional) Min-Distance: The minimum distance from the player at which the effect can take place (0 by default) +* (optional) Max-Distance: The maximum distance from the player at which the effect can take place (infinite by default) ### Scripts -We need to introduce new script instructions regarding to weather, since the existing instructions are based on fixed number weather types or integer indexed weather types. These instructions can not be salvaged and we therefore declare them deprecated. +We need to introduce new script instructions regarding the weather, since the existing instructions are based on a fixed number of weather types or integer-indexed weather types. These instructions can not be salvaged and we therefore declare them deprecated. New script instructions: -* SetWeather (region ID, weather ID): Replaces change weather. Set current weather for a region. -* GetWeather (region ID): Replaces GetCurrentWeather. Returns current weather for a region. -* SetRegionWeather (region ID, weather ID, chance): Replaces ModRegion. Modify a single entry in the weather table of a region. Chance is of type long. We relax the requirement that the sum of the chance values need to be 100. -* UpdateRegionWeather (region ID): Forces a new roll on the weather table of a region. +* `SetWeather(regionId, weatherId)`: Sets the current weather for the region with the ID `regionId` to `weatherId` (replaces `ChangeWeather`) +* `GetWeather(regionId)`: Returns the current weather ID for the region with the ID `regionId` (replaces `GetCurrentWeather`) +* `SetRegionWeather(regionId, weatherId, chance)`: Modifies the entry `weatherId` in the weather table of the region with the ID `regionId`; `chance` is of type `long`; we relax the requirement that the sum of the chance values needs to be 100 (replaces `ModRegion`) +* `UpdateRegionWeather(regionId)`: Forces a new roll on the weather table of the region with the ID `regionId` ## Water -We currently have only a single water type that is hard-coded. We name this water type sys::Water and inject a suitable record of the new type (see below) during loading of older omwgame files. +We currently have but a single water type that is hardcoded. We name this water type `sys::Water` and inject a suitable record of the new type (see below) during loading of older omwgame files. -We generalise the concept of water to liquid by introducing a new record type (Liquid). Liquid records can be used both for different looking water type (more swampy water, water with different colouration) but also for completely different liquid types (e.g. Lava). +We generalise the concept of water to the concept of liquid by introducing a new record type ("Liquid"). Liquid records can be used both for different-looking water types (e.g., more swampy water, water with different colouration) but also for completely different liquid types (e.g. lava). -To support the water type sys::water we also need to add a new magic effect (sys::SuffocationEffect). +**Note**: Morrowind only uses one liquid type, which we refer to as "water". (While lava does exist in the game, it is handled with activators, rather than as a liquid type) -Liquid records are referenced both in worldspace records (see Cells & Worldspaces section) and in body of liquid records (see Misc section). +To support the water type `sys::Water`, we also need to add a new magic effect (`sys::SuffocationEffect`). + +Liquid records are referenced both in worldspace records (see *Cells, Worldspaces & Areas* section) and in the body of liquid records (see *Misc* section). A liquid record consists of the following fields: -* Effects: IDs of zero or more magic effects that applied to actors while in the liquid. -* Submerged Effects: IDs of zero or more magic effects that are applied to actors while completely submerged in the liquid. -* Liquid type ID: Integer, hard-coded -* Additional parameter, specific to liquid type +* Effects: IDs of zero or more magic effects which are applied to actors while in the liquid +* Submerged Effects: IDs of zero or more magic effects that are applied to actors while completely submerged in the liquid +* Liquid Type ID: `long`, hardcoded +* Additional parameters, specific to liquid type We need to support at least one liquid type (ID 0: Water) from the start. Other types can be added. -TODO check about other ways to handle visuals for liquid types that requires less hard-coding. Shaders? +TODO: +- Are there other ways to handle visuals for liquid types that require less hardcoding? +- Use of shaders? ## Magic Effects -Currently we have a fixed number of magic effects that are referenced by an integer index. - -We move over the magic effect records to a string ID based scheme. +Currently, we have a fixed number of magic effects that are referenced by an integer index. -We also introduce a new effect (sys::SuffocationEffect) by injecting a record when loading from older omwgame files. This effect triggers the normal procedure for running out of air. Note that this will require creating a new effect icon. +We move over to effect records with a string-ID-based scheme. -Effect records can be deleted without restrictions. A content developer can add new effect records, again without restrictions. +We also introduce a new effect (`sys::SuffocationEffect`) by injecting a record when loading from older omwgame files. This effect triggers the normal procedure for running out of air. -We add a field to the effect record that contains the effect's name, currently stored in a GMST. +**Note**: This will require creating a new effect icon, which will be stored internally along with other resources that are part of the game engine. -We also add an optional field that contains the ID of a script function that is called when the magic effect takes effect. The function takes two arguments: +Effect records can be deleted without restrictions. A content developer can add new effect records - again without restrictions. -* The reference of the effects source. This would be a spellcaster in case of a spell or an item in case of an enchantment. -* A list of target references. +An effect record consists of the following fields: -Furthermore we add a second optional field that contains the ID of a script function that is called when the magic effect ends (only relevant for non-instant effects). The function signature is the same. +* Name (currently stored in a GMST) +* (optional) Effect Function: Contains the ID of a script function that is called when the magic effect takes effect; the function takes two arguments: the reference of the effects source (this would be a spellcaster in case of a spell or an item in case of an enchantment) and a list of target references +* (optional) Wear-Off Function: Contains the ID of a script function that is called when the magic effect ends (only relevant for non-instant effects); the function takes the same two arguments as the "Effect Function" field -For the existing effects we will inject scripts that match the previously hard-coded effects when loading from an older omwgame file. This will require the addition of a significant number of new script instructions that are all trivial, since they will just call existing engine functions. +For the existing effects, we will inject scripts that match the previously hardcoded effects when loading from an older omwgame file. This will require the addition of a significant number of new script instructions that are all trivial, since they will just call existing engine functions. -It is important to generalise as much as possible when creating these new script functions. For example we won't have a DivineIntervention script instruction. Instead we add a GetClosestMarker function and use existing functions for querying position and cell and then use the new Teleport instruction. +It is important to generalise as much as possible when creating these new script functions, e.g., we won't have a `DivineIntervention` script instruction. Instead we add a `GetClosestMarker` function, use existing functions for querying position and cell and, then, use the new `Teleport` instruction. ## Input We allow the addition of customisable input functions that can be bound to keyboard buttons in the usual way and can trigger the execution of scripts. Existing input bindings are not affected by this. -To this end we introduce a new record type (Input): +To this end, we introduce a new record type ("Input"): * Label -* Tooltip (string, optional) -* Default key (integer) -* Key down event script (string, optional): A script that is executed when the key is pressed. -* Key up event script (string, optional): A script that is executed when the key is released. -* Key pressed event script (string, optional): A script that is executed periodically while the key is down. -* Key pressed delta (float): Minimum time between two executions of the key pressed event script. Defaults to a reasonable value in the absence of this field. +* (optional) Tooltip: `String` value +* Default Key: `long` value +* (optional) Key-Press Event Script: Contains the ID of a script that is executed when the key is pressed +* (optional) Key-Release Event Script: Contains the ID of a script that is executed when the key is released +* (optional) Key-Hold Event Script: Contains the ID of a script that is executed when the key is pressed and held +* Key-Hold Delta: `float` value describing the minimum time between two executions of the "Key-Hold Event Script"; defaults to a reasonable value ## Held Items -We merge the probe and lockpick object types into a new object type: Held Item. The security skill related functions are handled via the ItemObjectInteraction script subrecord. When loading older omwgame files a suitable script in the sys namespace is injected for probing and lockpicking each. When transforming probe and lockpick records into Held Item records a ItemObjectInteraction subrecord referencing the respective script is injected. +We merge the probe and lockpick object types into a new object type: "Held Item". The Security-skill-related functions are handled via the `ItemObjectInteraction` script subrecord. When loading older omwgame files, a suitable script in the `sys` namespace is injected for probing and lock picking each. When transforming probe and lockpick records into held-item records, an `ItemObjectInteraction` subrecord referencing the respective script is injected. -If we enhance the animation system we may need to add additional subrecords that specify the pose for holding the item or the animation for using it. +**Note**: If we enhance the animation system, we may need to add additional subrecords that specify the pose for holding the item or the animation for using it. ## Pickpocketing -We add a new GMST (sys::ReversePickpocketing) of type integer that indicates if reverse pickpocketing is allowed (value other than 0) or not (value 0). +We add a new GMST (`sys::ReversePickpocketing`) of type `long` that indicates if reverse pickpocketing is allowed (value other than 0) or not (value 0). -We move the function for checking if pickpocketing succeeded from the C++ code to a new script function (sys::PickpocketTest). We inject this function when loading older omwgame files. +We move the function for checking if pickpocketing succeeded from the C++ code to a new script function (`sys::PickpocketTest`). We inject this function when loading older omwgame files. The function takes the following arguments: -* ref to thief actor -* ref to victim actor -* ref to item to be stolen -* reverse flag: integer, 0 for regular pickpocketing, 1 for reverse +* thiefId: Reference to the thief (actor) +* victimId: Reference to the victim (actor) +* stolenItemId: Reference to the item to be stolen +* reversePickpocket: `long` flag; set to 0 for regular pickpocketing, set to 1 for reverse pickpocketing The function returns 1 if the pickpocket attempt succeeded and 0 if it failed. -Items with the nopick tag are excluded from pickpocketing. +Items with the `noPick` tag are excluded from pickpocketing. ## Combat -De-hardcoding combat is a monumental task that we can not hope to complete within the time frame of stage 1. We will de-hardcode some parts that do not depend on large scale improvements to animation and can be handled with the scripting improvements in stage 1. Any further enhancements are left for stage 2 or more likely OpenMW 2.0. +De-hardcoding combat is a monumental task that we can not hope to complete within the time frame of stage 1. We will de-hardcode some parts that do not depend on large-scale improvements to animation and can be handled with the scripting improvements in stage 1. Any further enhancements are left for stage 2 or, more likely, OpenMW 2.0. ### Hits -We move the hit test check for C++ code to script functions. The following functions will be used: +We move the hit test check from C++ code to script functions. The following functions will be used: -* For melee weapons the HitTest script function defined in the respective WeaponType record; arguments: ref to weapon, ref to target -* For ranged weapons the HitTest script function defined in the respective WeaponType record; arguments: ref to weapon, ref to target, ref to ammunition -* For unarmed attacks the function sys::UnarmedHitTest; arguments: ref to weapon, ref to target +* `(weaponId, targetId)`: Function for melee weapons defined in the corresponding WeaponType record; `weaponId` refers to the weapon used, `targetId` refers to the ID of the target +* `(weaponId, targetId, ammunitionId)`: Function for ramged weapons defined in the corresponding WeaponType record; `weaponId` refers to the weapon used and `targetId` refers to the ID of the target, and `ammunitionId` refers to the ammunition used by the ranged weapon +* `sys::UnarmedHitTest(weaponId, targetId)`: Default function for unarmed attacks; `weaponId` refers to the weapon used and `targetId` refers to the ID of the target -All functions return an integer: 0 (no hit), a value other than 0 (hit) +All functions return 0 if the attack misses and a value other than 0 on a successful hit. -sys::UnarmedHitTest and default functions for armed hit tests (sys::MeleeHitTest, sys::RangedHitTest) are injected when loading older omwgame files. +`sys::UnarmedHitTest` and default functions for armed-hit tests (`sys::MeleeHitTest` and `sys::RangedHitTest`) are injected when loading older omwgame files. ### Script Enhancements We add a new script function: -* GetTargetRef: Returns the current target of an actor. Returns a NullRef if the actor is not in combat. Also returns a NullRef when used on an instance that is not an actor. +* `GetTargetRef`: Returns the current target of an actor; returns a null reference if the actor is not in combat or if it is used on an instance that is not an actor We add two new script instructions: -* DeclareWar l: Similar to StartCombat, but specifies a whole group of enemies. l is a list of references. -* DeclarePeace l: Ends the hostilities resulting from previous combat towards the actors listed in the reference list l. We may need to add a grace period after this instruction during attacks do not cause hostility again, so we can handle non-instant attacks. +* `DeclareWar l`: Similar to `StartCombat`, but specifies a whole group of enemies; `l` is a list of references +* `DeclarePeace l`: Ends the hostilities resulting from previous combat towards the actors listed in the reference list `l`. **Note**: We may need to add a grace period after this instruction during which attacks do not cause hostility again, so we can handle non-instant attacks. ### Script Hooks -We add three new script hooks. - -sys::Kill is executed whenever an actor is killed. Scripts for this hook take the following arguments: - -* Ref to target -* Ref to actor who killed the target (may be a null ref) +We add three new script hooks: -sys::CombatStarted and sys::CombatEnded are executed when the player enters or exits combat, respectively. +* `sys::Kill`: Executed whenever an actor is killed; scripts for this hook take the following two arguments: reference to the target and reference to the actor who killed the target (may be a null reference) +* `sys::CombatStarted`: Executed when the player enters combat +* `sys::CombatEnded`: Executed when the player exits combat ## Music @@ -1326,12 +1356,12 @@ The playlist record has the following fields: The titles on the playlist are all the titles in the music directory and the titles listed explicitly. -When playing a playlist first the probability is considered to randomly decide if a track is played or not. Then a track from the list is chosen. +When playing a playlist, first the probability is considered to randomly decide if a track is played or not. Then a track from the list is chosen. If a playlist is played in loop mode the process above is repeated whenever: * The current track ends -* If no track is playing at reasonably chosen time intervals +* No track is playing at reasonably chosen time intervals ### Background and Event Music @@ -1376,11 +1406,11 @@ We add two playlists (sys::ExplorePlaylist and sys::CombatPlaylist) which are po We add a GMST (sys::DefaultBackgroundMusic) with a default value of "sys::ExplorePlaylist". Whenever starting a new game or loading a game with no background music running the playlist specified in this GMST is played as background music. If the GMST holds an empty string no background music is played by default. -We add a script to the sys::CombatStarted and sys::CombatEnded hook each. The former plays sys::CombatPlaylist looped and forced. The later stops event music. +We add a script to the sys::CombatStarted and sys::CombatEnded hook each. The former plays sys::CombatPlaylist looped and forced. The latter stops event music. ### Location-Based Background Music -We add an optional string sub-record to the records listed below. This field contains a single track or a playlist, that determine the background music for the respective location. +We add an optional string sub-record to the records listed below. This field contains a single track or a playlist that determine the background music for the respective location. * Worldspace * Region @@ -1399,8 +1429,8 @@ The process of character creation currently allows for very little customisation Currently the character creation process runs through these steps: -1. Choose Name -2. Choose Race and appearance +1. Choose name +2. Choose race and appearance 3. Choose a method for class selection and perform class selection (3 methods) 4. Choose birthsign 5. Review @@ -1429,7 +1459,7 @@ We add several new script instructions. * CharacterCreationBack: Go back to previous step * CharacterCreationNext: Go to next step (ignored if the next step had not been accessed before) -n is an integer argument with one of several hard-coded integer values: +n is an integer argument with one of several hardcoded integer values: * 0: Name GUI * 1: Race GUI @@ -1460,11 +1490,11 @@ Likewise the character selection method of answering a set of quests can be impl The vanilla (character-)level-up system allows almost no modification. We keep the existing hard-coded system, but make it more configurable, and we also allow content developers to completely replace the existing system. -### State Un-Hiding +### State Unveiling Checks for a level up are based on the skill progression since the last level up. In vanilla this state is not accessible to content developers or players. -We address this issue by turning these hidden variables into record variables attached to the player character. The variables have the type long, a name matching the ID of the skill and are created on the fly whenever the engine detects a skill level up. Note that regular script instructions that change skill level do not count towards level ups. +We address this issue by turning these hidden variables into record variables attached to the player character. The variables have the type long and a name matching the ID of the skill, and are created on the fly whenever the engine detects a skill level up. Note that regular script instructions that change skill level do not count towards level ups. We also add another record variable (LevelUpCount) that counts the number of major and minor skill level-ups. @@ -1478,7 +1508,7 @@ Some NPCs are flagged as skill trainers. These trainers can train the player in * New script (sys::SkillTrainingTest): Tests if NPC can train player in a skill (returns 0 or 1). This test is performed after the faction standing test and after the skill list is trimmed down via sys::SkillTrainCount. The function takes the following arguments: ref to player, ref to NPC, ID of skill * New script (sys::SkillTrainingApplied): Executed after training has been completed. Arguments are the same as with sys::SkillTrainingTest. -When loading from an old omwgame file GMST and scripts are injected. sys::SkillTrainCount defaults to 3, sys::SkillTrainingTest performing the usual tests on skill level and attributes and sys::SkillTrainingApplied defaults to an empty script. +When loading from an old omwgame file, GMST and scripts are injected. sys::SkillTrainCount defaults to 3, sys::SkillTrainingTest performs the usual tests on skill level and attributes and sys::SkillTrainingApplied defaults to an empty script. ### Level-Up Test @@ -1513,7 +1543,7 @@ The default implementation (injected when loading from an older omwgame file) re ### Level-Up Image -The level up dialogue contains an image that depends on how the skill increase is distributed among specialisations. The function that decides about this image is very simple. However since specialisations won't be hard-coded anymore we can not simply pass these values into the function without first establishing more involved script support than planned for stage 1. +The level up dialogue contains an image that depends on how the skill increase is distributed among specialisations. The function that decides about this image is very simple. However, since specialisations won't be hardcoded any more, we can not simply pass these values into the function without first establishing more involved script support than planned for stage 1. This is actually not a problem since we can simply track the increase values in record variables, which may also prove useful for other purposes. @@ -1547,13 +1577,13 @@ This process is complicated by the existence of levelled item lists, which makes We will make several changes to increase the flexibility of item restocking. -First we move the re-fill operation from after a trade is completed to before a trade is completed. This should not affect existing content. +First we move the refill operation from after a trade is completed to before a trade is completed. This should not affect existing content. -We add three more scripts (optional, specified via new string fields in the actor's record) that hook into the re-fill process. +We add three more scripts (optional, specified via new string fields in the actor's record) that hook into the refill process. -1. Check if a re-fill should be performed; called at the beginning of every trade cycle. Arguments: ref to actor, integer (1 if first trade since talking to the merchant, 0 otherwise). Returns an integer (0 no re-fill required, otherwise re-fill is required). If script is not present a re-fill is performed). -2. Called for every item that the re-fill algorithm flags for removal. Arguments: ref to actor, ref to item. Returns an integer (0 if item should be kept, otherwise item is removed). If script is not present the removal takes place. -3. Called at the end of re-fill procedure. This gives content authors the opportunity to make other modifications. Arguments: ref to actor. No return value. +1. Check if a refill should be performed; called at the beginning of every trade cycle. Arguments: ref to actor, integer (1 if first trade since talking to the merchant, 0 otherwise). Returns an integer (0 no refill required, otherwise refill is required). If this script is not present, a refill is performed). +2. Called for every item that the refill algorithm flags for removal. Arguments: ref to actor, ref to item. Returns an integer (0 if item should be kept, otherwise item is removed). If this script is not present, the removal takes place. +3. Called at the end of refill procedure. This gives content authors the opportunity to make other modifications. Arguments: ref to actor. No return value. ## Fast-Travel @@ -1666,23 +1696,23 @@ NPC schedules are an advanced topic and as such would be a better fit for stage We run schedules only in active cells. But we also need to consider passive cells. -Here are a few examples. For simplicity lets consider a one-dimensional cell grid with the usual pattern of one cell around of the player cell being active. +Here are a few examples. For simplicity let's consider a one-dimensional cell grid with the usual pattern of one cell around of the player cell being active. For this example let the player be in cell 0. Now consider NPC A, who is in cell 2 (not active). He has a schedule that takes him to cell 1 sometimes. As a player we would expect to see him from the distance then. Another example with the same setup as above: While A is moving within cell 2 towards cell 1, the player relocates to cell 2. He would to see A make his way towards cell 1. Moreover if the player moves back to cell 0 and then returns to cell 2, he would expect to see A having made some progress. -Conclusion: We must track all actors who's schedule can take them into active cells. +Conclusion: We must track all actors whose schedules can take them into active cells. -It is not clear yet how to achieve this goal best. In the most simple implementation we could just estimate the time it takes to perform a task in a schedule and spawn the actor into a active cells accordingly. This would however not cover the second example. +It is not clear yet how to achieve this goal best. In the most simple implementation we could just estimate the time it takes to perform a task in a schedule and spawn the actor into active cells accordingly. This would however not cover the second example. -Alternatively we could invent a semi-active cell state and apply it to all cells that contain NPCs who's schedule can touch the active cells. This semi-active state would not run any scripts, nor would it render or run animations. And it may use a simplified/limited form of physics. This approach would cover all cases. However it has the potential for a severe performance impact. +Alternatively we could invent a semi-active cell state and apply it to all cells that contain NPCs whose schedules can touch the active cells. This semi-active state would not run any scripts, nor would it render or run animations, and it may use a simplified/limited form of physics. This approach would cover all cases. However, it has the potential for a severe performance impact. -Again alternatively, we may offer both implementation and switch between them base on settings/available processing power. +Again alternatively, we may offer both implementations and switch between them base on settings/available processing power. ### Actor Tracking -Independently from the chosen implementation we need to track all actors who's schedules are relevant to the active cells. This is a problem because with the default data structures we would at least need to scan every single cell on startup for relevant actors. The performance impact of such an approach is most likely not acceptable. +Independently from the chosen implementation, we need to track all actors whose schedules are relevant to the active cells. This is a problem because with the default data structures we would at least need to scan every single cell on start-up for relevant actors. The performance impact of such an approach is most likely not acceptable. Therefore information about which actor's schedule is relevant to which cell needs to be stored in a separate data structure that exists outside of the cell records. @@ -1698,9 +1728,9 @@ Each task sub-record consists of: The time table of a schedule is allowed to have holes (e.g schedule 1 ending at 10:00 and schedule 2 beginning at 10:15). -Task may also overlap partially, but no two task may start at the same time. +Tasks may also overlap partially, but no two tasks may start at the same time. -When picking a task from the schedule all task which contain the current time are considered. Of these the task with the earliest start time is chosen. +When picking a task from the schedule, all tasks which contain the current time are considered. Of these the task with the earliest start time is chosen. A new task is picked only when the current one has expired. @@ -1708,7 +1738,7 @@ A new task is picked only when the current one has expired. Schedules can be assigned to actors either in the actor record or with a new script instruction. -When explicit switching from one schedule to another, tasks with the ignore flag are ignored. This feature is intended for transitory tasks, that are pointless if the previous task hasn't been performed. +When explicitly switching from one schedule to another, tasks with the ignore flag are ignored. This feature is intended for transitory tasks, that are pointless if the previous task hasn't been performed. We add script instructions that allow for querying the current schedule of an actor. We also add a script instruction to prematurely end a task. @@ -1722,14 +1752,14 @@ This is an early draft of the new system and will almost certainly require more Idle activities are currently bound to the Wander package. This is suboptimal, both because other packages could benefit from it and because storing the idle function in a package instead of the NPC makes it hard to give the NPC a characteristic idle pattern. -We add a list of idle chances for each available idle animation to the NPC record. Ideally this list should be extensible, but first we need to improve the animation system so that the number of idle animations isn't hard-coded anymore. This may be a stage 2 task. +We add a list of idle chances for each available idle animation to the NPC record. Ideally this list should be extensible, but first we need to improve the animation system so that the number of idle animations isn't hard-coded any more. This may be a stage 2 task. Each package contains two fields related to idling behaviour: * Chance: A value between 0 and 1 that specifies the chance (per reasonable unit of time) of an idle animation to be executed. * Dialogue Chance: A value between 0 and 1 that specifies the chance of an idle dialogue if an idle animation is executed. -We may decide to use a scale of 0 to 100 instead of 0 to 1 to increase usability for less mathematical minded content developers. +We may decide to use a scale of 0 to 100 instead of 0 to 1 to increase usability for less mathematically minded content developers. The old wander package keeps its old idle list. When active it disables the new idle system. @@ -1739,7 +1769,7 @@ If a NPC does not have any active packages the idle function is still active. In We categorise package into four roles: -* Legacy: Old packages, generated either via script instructions or via AI package list in actor record. Removed once it runs out (if not flagged for repeat). Deprecated. +* Legacy: Old packages, generated either via script instructions or via AI package list in the actor record. Removed once they run out (if not flagged for repeat). Deprecated. * Schedule: Inserted by the schedule system. Can not be directly manipulated via script instructions. * Manual: New packages, generated via new script instructions. * Situation: Inserted via other game mechanics (e.g. combat, breath). Can not be directly manipulated via script instructions. @@ -1752,7 +1782,7 @@ We add new script instructions to query the package state of an actor and to ins Each package has two fields that describe its duration: -* Duration itself: Package will self-delete once the current time is larger than the start time plus the duration) +* Duration itself: Package will self-delete once the current time is larger than the start time plus the duration * Repeat flag: Package adds itself again at the end of the end of the duration Packages lacking these fields persist indefinitely unless explicitly removed. @@ -1763,7 +1793,7 @@ Some legacy packages already have these fields. Schedule packages will not have We translate the packages AiFollow, AiActivate and AiTravel into the new format. -AiFollow and AiTravel specify a location by given a set of coordinates and an optional cell ID. We will replace part of the package with a different format. +AiFollow and AiTravel specify a location by being given a set of coordinates and an optional cell ID. We will replace part of the package with a different format. There are two possible locations sub-records of which each package may only contain one: @@ -1792,7 +1822,7 @@ This is a new package with no direct equivalent in the old system. The package c * A list of locations (in the new format) * A loop flag -The actor will walk from one location to the next. Once he reached the last location he will either continue with the first location (if the loop flag is set) or walk down the list of locations in opposite direction and then start over (if the loop flag is not set). +The actor will walk from one location to the next. Once he has reached the last location he will either continue with the first location (if the loop flag is set) or walk down the list of locations in opposite direction and then start over (if the loop flag is not set). The obvious use case for this package is a guard patrolling a perimeter or a place. @@ -1821,7 +1851,7 @@ This results in multiple issues: * When localising the developer must hunt down all references to the localised strings and change them manually (cell names and topics). * When updating a localisation to a new version of the content file the translator has to manually pierce together his old translation and the new content file. -* Changing a string that is used as an internal reference breaks the connection to the referenced records in relation to the unlocalised file, which in turn makes it impossible to utilise automated tools that help for the localisation process. +* Changing a string that is used as an internal reference breaks the connection to the referenced records in relation to the non-localised file, which in turn makes it impossible to utilise automated tools that help for the localisation process. * Content files of mismatching languages generally can't be combined in a content file list. Mixing languages is generally not desirable for playing, but may sometimes be unavoidable. The ability to mix languages is highly desirable for developers (e.g. debugging). ## Encoding @@ -1834,7 +1864,7 @@ Localisation will be contained in separate content files (omwlang) that are to b The launcher will treat a content file and its localisation file as a single entity (see section about Identifying Meta-Data). -We may add a launcher language setting the determines the default language chosen by the launcher. This needs to be a separate option from the legacy encoding setting, which is currently also called language setting. +We may add a launcher language setting that determines the default language chosen by the launcher. This needs to be a separate option from the legacy encoding setting, which is currently also called language setting. ## Localisation Records @@ -1868,7 +1898,7 @@ fallback-lp-somekey=Some Text When looking up fallback strings the preferred language is chosen by the default language setting in the launcher (we may decide to provide a separate setting for fallback strings if this doesn't result in an overly complicated settings UI). -If a fallback string with this language-code is available it will be used. If no such fallback string is available, the default fallback string (without language-code prefix) will be used exist. The default fallback string must exist for all fallback strings with language codes. Otherwise the cfg file must be considered defective. +If a fallback string with this language-code is available it will be used. If no such fallback string is available, the default fallback string (without language-code prefix) will be used. The default fallback string must exist for all fallback strings with language codes. Otherwise the cfg file must be considered defective. We add an option to the importer tool to import strings only, which then will be merged (with a user-specified language-code prefix) into an existing cfg file. @@ -1880,7 +1910,7 @@ The handling of cell names in vanilla MW is one of the major localisation proble We turn the name field (the ID) into an ID-only field. The actual user visible name is moved into an optional localisation record. -We allow referencing of cells by both ID and name. If the name is used we emit an unobtrusive warning message. +We allow referencing of cells by both ID and name. If the name is used, we emit an unobtrusive warning message. This change does not fix the problem of already existing localisations that have modified the names of cells. See the section about localisation mapping below for a workaround. @@ -1909,7 +1939,7 @@ Issues #2 and #3 can be enforced at least partially. We declare the use of user ## Localisation Mapping -The new localisation scheme leaves existing localisations un-addressed. Localisations of Morrowind.esm break the connection of cell names and topics in relation to the un-localised Morrowind.esm (English). +The new localisation scheme leaves existing localisations unaddressed. Localisations of Morrowind.esm break the connection of cell names and topics in relation to the un-localised Morrowind.esm (English). We provide a workaround for this problem by introducing localisation mapping. Localisation mapping is a language-specific list of ID/user visible text pairs, provided as a text file. A similar scheme exists for the Russian localisation. We may decide to simple extend the existing implementation of this feature in OpenMW to provide localisation mapping. @@ -1939,7 +1969,7 @@ Note that by going down this route we do not block the addition of a full-scale ## Skinning -Under the term skinning we understand changes to the look of a GUI without affecting the functionality. Vanilla Morrowind already provides a certain level of skinning-capacity though textures and Morrowind.ini settings. We are going to expand on these and clean up the skinning process. After 1.0 all skinning should be done exclusively through content files and associated resources files. Skinning does not belong into ini/cfg files. +Under the term skinning we understand changes to the look of a GUI without affecting the functionality. Vanilla Morrowind already provides a certain level of skinning-capacity though textures and Morrowind.ini settings. We are going to expand on these and clean up the skinning process. After 1.0 all skinning should be done exclusively through content files and associated resources files. Skinning does not belong in ini/cfg files. ### Windows @@ -1981,7 +2011,7 @@ Dynamic stats are more complicated since they merge three elements: The first two go together well enough, but we should provide an option to decouple the enemy health bar and turn it into its own HUD elements. -We also need to consider that after 1.0 we won't always have three dynamic stats. There could be more or (in stage 2) less. The HUD element must be build in a way that can adjust its layout accordingly without manual layouting. +We also need to consider that after 1.0 we won't always have three dynamic stats. There could be more or (in stage 2) less. The HUD element must be built in a way that it can adjust its layout accordingly without manual layouting. ### Notifications @@ -2022,7 +2052,7 @@ We add a couple of new input elements that are each represented as a separate wi * Numerical input: Takes a range, a precision value and a default value and returns a float. * Text input: Takes a default value and returns a string. -Additionally each window has a titlebar, an okay button and and optional cancel button. +Additionally each window has a title bar, an okay button and and optional cancel button. For each element we add a new script instruction that brings up the respective element. These instructions take the following arguments: @@ -2077,10 +2107,10 @@ We may explore an alternative GUI layout consisting of a vertical bar (maybe on The current horizontal text-based category selector is problematic for two reasons: -* Since we hand over the creation of categories to mod developers we can expect a large number of them and therefore scaling becomes an issue. Horizontally arranged text items scale terribly because text items also primarily extend horizontally. There are workarounds for this problem but these are either bad or add alternative interface (e.g. a pulldown menu listing all tabs) which isn't a great solution either. Under no circumstances will we use the workaround that adds two arrow buttons left and right of the tab bar to change the visible section of the tab bar. This is an anti-pattern and everyone who has ever committed the horrendous crime of implementing this GUI-atrocity deserves to end up in usability hell. +* Since we hand over the creation of categories to mod developers we can expect a large number of them and therefore scaling becomes an issue. Horizontally arranged text items scale terribly because text items also primarily extend horizontally. There are workarounds for this problem but these are either bad or add alternative interface (e.g. a pull-down menu listing all tabs) which isn't a great solution either. Under no circumstances will we use the workaround that adds two arrow buttons left and right of the tab bar to change the visible section of the tab bar. This is an anti-pattern and everyone who has ever committed the horrendous crime of implementing this GUI-atrocity deserves to end up in usability hell. * In table mode we already have a bar at the top of the window. Two bars on the same window border give a cluttered, unclean appearance. -If we do use an alternative layout we need to add an icon field to the ItemCategory record and we also need to consider, if we drop the vanilla one completely or let the user choose. Unless there is strong opposition (this is a significant change from Vanilla after all) we should choose the former option. If we opt for a less radical change we still should switch from text to icons to counter the scaling problem, even if we stick with the horizontal layout. +If we do use an alternative layout, we need to add an icon field to the ItemCategory record and we also need to consider whether we drop the vanilla one completely or let the user choose. Unless there is strong opposition (this is a significant change from Vanilla after all) we should choose the former option. If we opt for a less radical change we still should switch from text to icons to counter the scaling problem, even if we stick with the horizontal layout. ## Character State @@ -2151,7 +2181,7 @@ We let scripts to run as operations (exclusively for now). In the context of Ope * An operation runs in a separate thread and has read only access to the content data * Multiple operations can run concurrently, but only one operation of each type (e.g. saving, verifier, python script) at a time -* While at least one operation is running the content data can not be modified +* While at least one operation is running, the content data can not be modified * The operation can be terminated by the user at any time * The operation reports back to the main thread via messages (shown in a table like with the verifier) and progress state (represented as progress bar) @@ -2171,7 +2201,7 @@ Note that all debugging tools will require that OpenMW is started from OpenMW-CS ### Marking -We add a new keyboard-shortcut to OpenMW (only available when started from OpenMW-CS). When used OpenMW will look at the instance under the mouse pointer (or the crosshair if the mouse pointer is hidden). If this instance is part of a content file (i.e. not created during play) OpenMW will send the cell and RefID of the instance to OpenMW-CS. OpenMW-CS inserts the instance info into a table. +We add a new keyboard shortcut to OpenMW (only available when started from OpenMW-CS). When used OpenMW will look at the instance under the mouse pointer (or the crosshair if the mouse pointer is hidden). If this instance is part of a content file (i.e. not created during play) OpenMW will send the cell and RefID of the instance to OpenMW-CS. OpenMW-CS inserts the instance info into a table. This table functions in a similar way to the verifier table; allowing the content developer to jump directly to records that have been identified as being in need of attention. @@ -2191,15 +2221,15 @@ We also add function that produces a warning if the user tries to create an ID o OpenMW-CS currently has very little in-application help. Improving this will be an ongoing process. For stage 1 we will focus on scripting. -We will add a keyboard-shortcut and a context menu item to the script editor. When activated while the cursor is on a keyword we show a help text that explains the syntax and the function. +We will add a keyboard shortcut and a context menu item to the script editor. When activated while the cursor is on a keyword we show a help text that explains the syntax and the function. Ideally we would want to use the same help text within the application and in our documentation. A way to handle this needs to be determined. -The location where we show the help text needs to be determined. For script editor views we could re-use the widget that displays errors. Or we could add a separate subview type for help text. We may decide to add a help main menu item from which the help system can be navigated. +The location where we show the help text needs to be determined. For script editor views we could reuse the widget that displays errors. Or we could add a separate subview type for help text. We may decide to add a help main menu item from which the help system can be navigated. -## Spellchecker +## Spell Checker -We will add a spellchecker by utilising an existing spellcheck solution. All names (e.g. items, cells, races) will be implicitly added to the list of known words. +We will add a spell checker by utilising an existing spell check solution. All names (e.g. items, cells, races) will be implicitly added to the list of known words. ## Porting Tools @@ -2239,19 +2269,19 @@ A user settings record consists of the following fields: The following user setting types are available: -* bool (type 0): GMST is an integer; GUI is a checkbox; additional data: default value -* numeric, integer (type 1): GMST is an integer; GUI is a spinbox; additional data: upper bound, lower bound, default value -* numeric, float (type 2): GMST is a float; GUI is a spinbox; additional data; upper bound, lower bound, precision, default value -* list (type 3) GMST is an integer; GUI is a combobox; additional data; list of strings for combobox text, default value +* bool (type 0): GMST is an integer; GUI is a check box; additional data: default value +* numeric, integer (type 1): GMST is an integer; GUI is a spin box; additional data: upper bound, lower bound, default value +* numeric, float (type 2): GMST is a float; GUI is a spin box; additional data; upper bound, lower bound, precision, default value +* list (type 3) GMST is an integer; GUI is a combo box; additional data; list of strings for combo box text, default value * slider (type 4): GMST is a float; GUI is a slider; additional data; upper bound, lower bound, default value ### Categories User settings categories are represented as separate pages/tabs in the GUI. We specify categories via category IDs (strings). Currently there does not appear to be a need for a separate user settings category record, since this record would have no data. -Content files can create new categories (simply by referencing them in a user settings record) or add to existing categories (including the vanilla ones). We should consider to add a couple of additional default categories (including localised labels, but empty and invisible until populated by content files) to help content developers organise their settings in a coherent way. +Content files can create new categories (simply by referencing them in a user settings record) or add to existing categories (including the vanilla ones). We should consider adding a couple of additional default categories (including localised labels, but empty and invisible until populated by content files) to help content developers organise their settings in a coherent way. -The categories in vanilla MW are General, Audio, Controls and Graphics. Content files can add settings to these too. But we should consider reorganisation. General is not a good category. We should consider splitting it up. We should also consider moving the keybindings into their own category. A separate keybindings category would have to be the only exception to the rule that content files are allowed to add settings to pre-existing categories. +The categories in vanilla MW are General, Audio, Controls and Graphics. Content files can add settings to these too, but we should consider reorganisation. General is not a good category. We should consider splitting it up. We should also consider moving the key bindings into their own category. A separate key bindings category would have to be the only exception to the rule that content files are allowed to add settings to pre-existing categories. ### Hook @@ -2285,13 +2315,13 @@ A LiquidBody object record has the following fields: * Height (float) * Shape -Shape is a closed bezier-curve that can be edited in the editor. This shape defines a surface (the liquid surface). The depths of liquid body is defined by the height value. +Shape is a closed Bézier curve that can be edited in the editor. This shape defines a surface (the liquid surface). The depths of liquid body is defined by the height value. We can imply that only the surface of the LiquidShape instance is exposed. The other sides do not require rendering. -## Dagoth'Ur Un-Fix +## Dagoth Ur Fix Un-Fix -We introduced a fix for a defect in Morrowind.esm by blocking remote access to instances of the object dagoth_ur_1 via mod instructions for dynamic stats. We will now bind this fix to a new integer GMST in the sys namespace. This will allow content developers to disable this fix in their mods (hopefully after given poor old Dagoth'Ur a bit more health). +We introduced a fix for a defect in Morrowind.esm by blocking remote access to instances of the object dagoth_ur_1 via mod instructions for dynamic stats. We will now bind this fix to a new integer GMST in the sys namespace. This will allow content developers to disable this fix in their mods (hopefully after giving poor old Dagoth Ur a bit more health). ## Comment Subrecords @@ -2314,7 +2344,7 @@ We add four new GMSTs of type integer (default value 1): These decide if the game is paused when in status mode (right-click), dialogue mode, journal mode or any of the crafting modes (e.g. alchemy). To handle sys::DialogPause==0 properly we ignore dialogues initiated by NPCs or via script, if the player is already in a dialogue. -## Instance Persistency +## Instance Persistence We add a new optional field to all object records and instance subrecords, a single enum-like integer. From 5ee731d86f942e597ad6976e368d8fb9c0af6236 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 27 Jun 2018 12:33:23 +0200 Subject: [PATCH 259/282] updated roadmap section --- docs/openmw-stage1.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/openmw-stage1.md b/docs/openmw-stage1.md index 9458b66bb..4707a4435 100644 --- a/docs/openmw-stage1.md +++ b/docs/openmw-stage1.md @@ -66,9 +66,9 @@ However this approach means additional work and would be of limited usefulness i ## Roadmap -We will continue with the revised roadmap scheme. This means we add a new milestone (openmw-stage1). After we are finished discussing this design document and have made necessary adjustments we will cut it up into individual tasks and add them to openmw-stage1. This milestone will then take the role of the current openmw-1.0 and openmw-cs-1.0 milestones. -Confirmed bug reports also go into openmw-stage1. Other issues (feature requests and tasks) only after we have approved them for near future development. -We will most like not have a separate openmw-stage1 for the editor, since for the bulk of the changes (The Grand De-hardcoding) most tasks we will require changes to both OpenMW and the editor. +We will continue with the revised roadmap scheme. This means we add a new label (stage1). After we are finished discussing this design document and have made necessary adjustments we will cut it up into individual tasks and add them to the tracker with the stage1 label. This label will then take the role of the current 1.0 label. +Confirmed bug reports also get tagged with stage1. Other issues (feature requests and tasks) only after we have approved them for near future development. +We will most likely not have a separate stage1 label for the editor, since for the bulk of the changes (The Grand De-hardcoding) most tasks we will require changes to both OpenMW and the editor. # Content File Format & Namespaces From 520e65f82205480a6286d6c51129c0715ec13149 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Wed, 27 Jun 2018 13:32:01 +0200 Subject: [PATCH 260/282] last minute updates from zini --- CONTRIBUTING.md | 2 +- docs/openmw-stage1.md | 1050 +++++++++++++++++++++-------------------- 2 files changed, 542 insertions(+), 510 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4805bff3b..73ec986b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ How to contribute to OpenMW Not sure what to do with all your free time? Pick out a task from here: -https://bugs.openmw.org/ +https://gitlab.com/OpenMW/openmw/issues Currently, we are focused on completing the MW game experience and general polishing. Features out of this scope may be approved in some cases, but you should probably start a discussion first. diff --git a/docs/openmw-stage1.md b/docs/openmw-stage1.md index 42fe8a2aa..4707a4435 100644 --- a/docs/openmw-stage1.md +++ b/docs/openmw-stage1.md @@ -10,7 +10,7 @@ At the same time we want to stay true to our origins. While we want to broaden t Our goal here is to make OpenMW into a general purpose engine, but a general purpose 1st/3rd person real-time RPG engine. We do not attempt to support other genres or other flavours of RPG. -The development of OpenMW will hopefully continue for a long time and we can not reasonable hope to sketch out its entire future development in a single design document. Therefore this document should be seen as stage 1 of the post 1.0 development only. It may last us 6 months or a year or several years, depending on how much development activity we can achieve. But eventually there will be a stage 2 design document. +The development of OpenMW will hopefully continue for a long time and we can not reasonably hope to sketch out its entire future development in a single design document. Therefore this document should be seen as stage 1 of the post 1.0 development only. It may last us 6 months or a year or several years, depending on how much development activity we can achieve. But eventually there will be a stage 2 design document. # Definitions @@ -66,9 +66,9 @@ However this approach means additional work and would be of limited usefulness i ## Roadmap -We will continue with the revised roadmap scheme. This means we add a new milestone (openmw-stage1). After we are finished discussing this design document and have made necessary adjustments we will cut it up into individual tasks and add them to openmw-stage1. This milestone will then take the role of the current openmw-1.0 and openmw-cs-1.0 milestones. -Confirmed bug reports also go into openmw-stage1. Other issues (feature requests and tasks) only after we have approved them for near future development. -We will most like not have a separate openmw-stage1 for the editor, since for the bulk of the changes (The Grand Dehardcoding) most tasks we will require changes to both OpenMW and the editor. +We will continue with the revised roadmap scheme. This means we add a new label (stage1). After we are finished discussing this design document and have made necessary adjustments we will cut it up into individual tasks and add them to the tracker with the stage1 label. This label will then take the role of the current 1.0 label. +Confirmed bug reports also get tagged with stage1. Other issues (feature requests and tasks) only after we have approved them for near future development. +We will most likely not have a separate stage1 label for the editor, since for the bulk of the changes (The Grand De-hardcoding) most tasks we will require changes to both OpenMW and the editor. # Content File Format & Namespaces @@ -92,7 +92,7 @@ The following cases of changes to the record format have been identified: * Existing record that is switched over from integer-based to ID-based indexing (this is a special case of the case above) * Existing records is split into two records: Split needs to be performed on load. -There have been some concerns regarding the efficiency of string based IDs (vs integer). While there hasn't been an evidence indicating that this is actually a problem in OpenMW, it is important to point out that the move from integer to string does not mean that such a problem could not be addressed. +There have been some concerns regarding the efficiency of string based IDs (vs. integer). While there hasn't been an evidence indicating that this is actually a problem in OpenMW, it is important to point out that the move from integer to string does not mean that such a problem could not be addressed. We already have plans to introduce a new string type for IDs, which deals with the case-issue (current std::string based implementation is error-prone and results in some very verbose code). @@ -202,13 +202,13 @@ Most of the current content exists in a form that is not compatible with the new In most cases this can easily be resolved by adding an importer tool to the launcher, that takes the content and copies it into the desired organisation scheme (archive or directory). -For other content (preexisting data directories and Morrowind, since Morrowind and its add-ons are installed into the same directory) we need to maintain a level of compatibility with the old approach, that can be switched on and off as needed. +For other content (pre-existing data directories and Morrowind, since Morrowind and its add-ons are installed into the same directory) we need to maintain a level of compatibility with the old approach, that can be switched on and off as needed. ### Resources Packages -During the discussion about the new content file organisation scheme (omwgame, omwaddon) there was some criticism about the scheme not allowing plugins with no dependencies. +During the discussion about the new content file organisation scheme (omwgame, omwaddon) there was some criticism about the scheme not allowing plug-ins with no dependencies. -This is not a problem, because such plugins are generally useless. A plugin without any dependency can not modify or reference any record. That was not such a large issue with vanilla Morrowind, because in this case the plugin could have still have depended on things like attributes or dynamic stats. However in OpenMW these will all turn into records and therefore can not be accessed from within an omwaddon file without dependencies. This will only get more extreme as the de-hardcoding progresses further. An omwaddon file without dependencies can do literally nothing, which makes it useless by definition. +This is not a problem, because such plug-ins are generally useless. A plug-in without any dependency can not modify or reference any record. That was not such a large issue with vanilla Morrowind, because in this case the plug-in could have still have depended on things like attributes or dynamic stats. However in OpenMW these will all turn into records and therefore can not be accessed from within an omwaddon file without dependencies. This will only get more extreme as the de-hardcoding progresses further. An omwaddon file without dependencies can do literally nothing, which makes it useless by definition. But there is one exception. Some members of the mod community have provided resources packages (meshes and similar) that can be used by other content developers. Under our new resources scheme these would have to be accompanied by an omwaddon file. This is not a good solution because of two reasons: @@ -231,38 +231,38 @@ Therefore we will develop our own cross-platform package format for OpenMW conte # Scripting -Note: Extensions to the scripting language in form of new instructions and functions are distributed over this entire document. In some cases features may require additional functions that return the value of certain fields of certain records. These functions are usually not listed explicitly and are left as an exercise to the reader. +**Note**: Extensions to the scripting language in form of new instructions and functions are distributed over this entire document. In some cases, features may require additional functions that return the value of certain fields of certain records. These functions are usually not listed explicitly and are left as an exercise to the reader. ## Language Version -The version of the language used in a script is determined by a single integer number which is stored in a new sub-record within the script record. Note that this is different from earlier concepts which used a more complicated version identifier and stored it within the actual script. +The version of the language used in a script is determined by a single integer number which is stored in a new subrecord within the script record. Note that this is different from earlier concepts, which used a more complicated version identifier and stored it within the actual script. -Scripts that are missing this sub-record are considered version 0 (legacy). New scripts (when created in OpenMW-CS) default to the most up-to-date version. The version can be selected from a combo box in the script sub-view. We may add a user setting that hides the combo box UI if the version is the most up to date one. +Scripts that are missing this subrecord are considered version 0 (legacy). New scripts (when created in OpenMW-CS) default to the most up-to-date version. The version can be selected from a combo box in the script subview. We may add a user setting that hides the combo box UI if the version is the most up to date one. The version number is incremented whenever an addition to the scripting language is made (at most once per feature release). A new feature release is not required to increment the version number, if no feature changes have been made to the language in this release. -From version 1 on all workarounds for bad scripts will be disabled. This should not break anything for well written scripts. In general moving to a new language version should usually at most require minor fixes. +From version 1 on, all workarounds for bad scripts will be disabled. This should not break anything for well-written scripts. In general, moving to a new language version should usually at most require minor fixes. Since old scripts will remain with the old language version (unless there is a need to modify them and use newer features), this scheme will not break compatibility for existing content. ## Error Handling -We will continue with the existing runtime error handling scheme (meaning stopping the script execution on a runtime error). Introducing exception handling instead would be overkill. +We will continue with the existing runtime error handling scheme (i.e., stopping the script execution on a runtime error). Introducing exception handling instead would be overkill. -We will do our best to avoid crashes as result of broken script. That means (among others) that we need to put an artificial (configurable) limit on function call depth to avoid crashes and lockups from endless script recursion. We may also decide to put a similar limit on loops. +We will do our best to avoid crashes as a result of broken scripts. That means (among others) that we need to put an artificial (configurable) limit on function call depth to avoid crashes and lock-ups from endless script recursion. We may also decide to put a similar limit on loops. ## Variable Types -The types *long* and *float* remain unchanged. +The types `long` and `float` remain unchanged. -We will flag *short* as deprecated. Currently *short* has no advantage over *long* (it uses the same amount of memory in OpenMW). For anything but a very specific kind of bit cutting the *short* type has no use at all. -If we ever decide that we do want a shorter integer type we can always de-deprecate *short*. +We will flag `short` as deprecated. Currently `short` has no advantage over `long` (it uses the same amount of memory in OpenMW). For anything but a very specific kind of bit-cutting the `short` type has no use at all. +If we ever decide that we do want a shorter integer type we can always de-deprecate `short`. We will introduce several new types: ### Strings -The name of the new string type is **String** and string literals are marked by placing them in quotation marks. Example: +The name of the new string type is `String`, and string literals are marked by placing them in quotation marks. Example: ```` String a @@ -271,7 +271,7 @@ Set a to "Some Text" Comparison operators for strings are provided in the usual way. -Since we discontinue bad script workarounds most ambiguousness regarding string literals should have been removed already (we will not accept local variable names and instructions within quotation marks anymore). Only a single problem case remains: +Because we are discontinuing bad script workarounds, most ambiguity regarding string literals should have been removed already (we will not accept local variable names and instructions within quotation marks any more). Only a single problem case remains: If a function requires an ID, is it a literal ID or is it the name of a string variable that contains an ID? Example: @@ -281,13 +281,13 @@ Set Fargoth to "player" Fargoth -> AddItem "Gold_001", 100 ```` -We can not make a rule that forbids the use of local variable names that are also IDs, because that would allow random content files to break scripts in other, unrelated content files. +We cannot make a rule that forbids the use of local variable names that are also IDs, because that would allow random content files to break scripts in other, unrelated content files. -Therefore the solution here is to simply give the local variable precedence over IDs. If the script author wanted to give money to Fargoth he should not have created a local variable with this name. +Therefore, the solution here is to simply give the local variable precedence over the ID. If the script author intended to give money to `Fargoth`, he should not have created a local variable with the same name. Note that global variables are not an issue here, because global variables are IDs and IDs need to be unique (with certain exceptions that are not relevant here). -To remove even the last remaining potential problems with ambiguity we will also introduce a new string literal that can only be a literal and never a string variable name. These literals are marked by a L prefix. Utilising this feature the code block above could be rewritten to the following if the script author absolutely insists on having a local variable named Fargoth: +To remove even the last remaining potential problems with ambiguity, we will also introduce a new string literal that can only be a literal and never a string variable name. These literals are preceded by an `L` character. Utilising this feature, the aforementioned code block could be rewritten as is shown below, if the script author absolutely insists on having a local variable named `Fargoth`: ```` String Fargoth @@ -295,93 +295,93 @@ Set Fargoth to "player" L"Fargoth" -> AddItem "Gold_001", 100 ```` -### Instance-References +### Instance References -The name of the new reference type is **Ref**. It can reference an instance in the world or in a container or be a null reference. +The name of the new reference type is `Ref`. It can reference an instance in the world or in a container or be a null reference. -Since we currently have no way to reference an instance persistently the use of the ref type is limited for the time being (see section about variable scope for further details). +Since we currently have no way to reference an instance persistently, the use of the `Ref` type is limited for the time being (see section *Variable Scope* for further details). -References can be used in any place that would otherwise allow an ID that stand for an existing reference. The rules for strings regarding ambiguousness apply to references in the same way. +References can be used in any place that would otherwise allow an ID that stand for an existing reference. The rules for strings regarding ambiguity apply to references in the same way. A reference will implicitly cast to 0 (a null reference) or 1 (not a null reference) when used in a numeric expression. -Note: A reference pointing into the contents of a container points to a stack of items and not a single item. +**Note**: A reference pointing to the contents of a container points to a stack of items and not a single item. We introduce the following keywords: -* Self: a value of type Ref (the instance the script is currently running on, only available in local scripts and dialogue scripts) -* NullRef: a literal of type Ref (null reference) -* GetId (r): return ID of reference as a string value -* GetContainer (r): returns the reference of the container r is in (or a null-reference if r is not in a container) -* GetCell (r): returns the ID string of the cell r is in (either directly or via a container) -* SearchActive (id, container=0): return a reference with the given ID within the active cells. If container!=0, also check in containers; return a null-reference if no reference is found -* SearchIn (id, id2): return a reference with the given ID within something (id2) that can contain references; this can be a cell, a worldspace, a container, a creature or a NPC. If id2 represents an instance, a ref variable can be given instead; return a null-reference if no reference is found +* `Self`: A value of type `Ref` (the instance the script is currently running on, only available in local scripts and dialogue scripts) +* `NullRef`: A literal of type `Ref` (null reference) +* `GetId(r)`: Returns the ID of the reference `r` as a string value +* ``GetContainer(r)`: Returns the reference of the container `r` is in (or a null reference if `r` is not in a container) +* `GetCell(r)`: Returns the ID string of the cell `r` is in (either directly or via a container) +* `SearchActive(id, container = 0)`: Returns a reference with the given ID within the active cells. If `container != 0`, also check in containers; returns a null reference if no reference is found +* `SearchIn(id, id2)`: Returns a reference with the given ID within something (`id2`) that can contain references; this can be a cell, a worldspace, a container, a creature or an NPC. If `id2` represents an instance, a `Ref` variable can be given instead; returns a null reference if no reference is found ### Lists -A single type list available for the following types: +A single-type list will be available for the following types: -* Long -* Float -* String -* Ref +* `long` +* `float` +* `String` +* `Ref` -We will not support mixed type list because that would require a major change to the type system. Internally and functionally a list will work like an array. The type names for lists are: +We will not support mixed-type lists because that would require a major change to the type system. Internally and functionally, a list will work like an array. The type names for lists are: -* LongList -* FloatList -* StringList -* RefList +* `LongList` +* `FloatList` +* `StringList` +* `RefList` -List literals are given as a comma separated list of values in square brackets. Empty list literals are allowed. In the context of lists we generally allow implicit promotion of integer to float types. +List literals are given as a comma-separated list of values in square brackets. Empty list literals are allowed. In the context of lists, we generally allow implicit promotion of integer to float types. We support the following list operations: -* l1[i]: Return member i (indexing begins at 0) -* GetSize(l1): Returns the number of elements in l1 as an *Integer* -* l1 + l2: Returns a concatenated list consisting of the members of l1 followed by the members of l2 -* GetSubset (l1, l2): Return the common subset of the elements of l1 and the elements of l2 -* HasElement (l1, e): Return 0 (if e is not element of l1) or 1 (otherwise) -* GetIndex (l1, e): Return index of first occurrence of element e in l1 (or -1 if none) -* Minimum (l1): Return the minimum of the elements in l1 (only works with numerical types) -* Maximum (l1): Return the minimum of the elements in l1 (only works with numerical types) +* `l1[i]`: Returns member `i` (indexing begins at 0) +* `GetSize(l1)`: Returns the number of elements in `l1` as an integer +* `l1 + l2`: Returns a concatenated list consisting of the members of `l1` followed by the members of `l2` +* `GetSubset(l1, l2)`: Returns the common subset of the elements of `l1` and `l2` +* `HasElement(l1, e)`: Returns 0 (if `e` is not element of `l1`) or 1 (otherwise) +* `GetIndex(l1, e)`: Returns the index of the first occurrence of `e` in `l1` (or -1 if none) +* `Minimum(l1)`: Returns the minimum of the elements in `l1` (only works with numerical types) +* `Maximum(l1)`: Returns the maximum of the elements in `l1` (only works with numerical types) and the following list instructions: -* Set l1[i] To e: Set list element i of list l1 to value e (indexing begins at 0) -* Sort (l1, cf=""): Sort list elements into ascending order (if cf is an empty string) or via comparison function script cf -* Filter (l1, ff): Filter list l1 by filter function ff (only keep elements that do not return 0) -* Reverse (l1): Reverse order of list elements -* Append (l1, e): Append element e to list l1 -* Append (l1, l2): Append elements of list l2 to list l1 -* Remove (l1, e): Remove all elements equal to e from list l1 -* Remove (l1, l2): Remove from list l1 all elements which are also in list l2 -* InsertAt (l1, e, i): Insert element at position i into list l1 (replacing element i on position upwards and extending the size of the list by one) -* RemoveAt (l1, i): Remove element at position i from list l1 (moving elements with an index higher than i down by one and reducing the size of the list by one) -* Resize (l1, n): Set size of l1 to n. If list is extended additional elements are default initialised. +* `Set l1[i] To e`: Sets list element `i` of list `l1` to value `e` (indexing begins at 0) +* `Sort(l1, cf="")`: Sorts list elements into ascending order (if `cf` is an empty string) or via comparison function script `cf` +* `Filter(l1, ff)`: Filters list `l1` by filter function `ff` (only keeps elements that do not return 0) +* `Reverse(l1)`: Reverses order of list elements of list `l1` +* `Append(l1, e)`: Appends element `e` to list `l1` +* `Append(l1, l2)`: Appends elements of list `l2` to list `l1` +* `Remove(l1, e)`: Removes all elements equal to `e` from list `l1` +* `Remove(l1, l2)`: Removes all elements from list `l1` which are also in list `l2` +* `InsertAt(l1, e, i)`: Inserts element `e` into list `l1` at position `i` (moving the element at index `i` one position upwards and extending the size of the list by one) +* `RemoveAt(l1, i)`: Removes the element at position `i` from list `l1` (moving elements with an index higher than `i` down by one and reducing the size of the list by one) +* `Resize(l1, n)`: Set size of list `l1` to `n`; if the list is extended, additional elements are initialised with default values Variable names used in the lists above: -* l1 and l2 are lists/list literals of the same element type -* i is an integer value that functions as list index -* n is an integer value -* e is a variable/literal of the element type of l1 and l2 -* cf: ID of a comparison function script that takes two element type arguments and returns an integer (-1, 0, 1) -* ff: Id of filter function script that takes one element type arguments and returns an integer (0, 1) +* `l1` and `l2` are lists / list literals of the same element type +* `i` is an integer value and is used as list index +* `n` is an integer value +* `e` is a variable/literal of the element type of `l1` and `l2` +* `cf` is the ID of a comparison function script that takes two element-type arguments and returns an integer (-1, 0, 1) +* `ff` is the ID of a filter function script that takes one element-type argument and returns an integer (0, 1) ### List Aliases We define three more type aliases: -* Vector3: A float list of 3 members, used for location or rotation -* Vector4: A float list of 4 members (location and zrot) -* Vector6: A float list of 6 members (location and rotation) +* `Vector3`: A `FloatList` of 3 members, used for location or rotation +* `Vector4`: A `FloatList` of 4 members (location and rotation around z-axis) +* `Vector6`: A `FloatList` of 6 members (location and rotation) These aliases are not separate types. -In the specification of additional functions and instructions values of these types are specified as v3, v4 and v6 respectively. Passing in shorter lists is allowed and will set the missing elements to 0. +In the specification of additional functions and instructions, values of these types are specified as `v3`, `v4`, and `v6` respectively. Passing shorter lists is allowed and will set the missing elements to 0. -Note that all rotations are specified in degrees. +**Note**: All rotations are specified in degrees. ## Variable Scope @@ -391,32 +391,32 @@ The global and the local scope remain unchanged. We will add two more scopes: ### True Local Variables -What the Morrowind scripting language describes as local variables dose not match the concept of local variables in other languages. In case of local scripts Morrowind local variables function as member variables. In case of global scripts Morrowind local variables function as static variables. +What the Morrowind scripting language describes as local variables does not match the concept of local variables in other languages. In case of local scripts, Morrowind local variables function as member variables. In case of global scripts, Morrowind local variables function as static variables. -There is a need for true local variables: +There are several reasons for the introduction of true local variables: * Less pollution of local dialogue variable names * Less state that needs to be saved -* Avoidance of potential scripting errors when the state of a local variable is maintained across multiple script executions and the script does not re-initialise the variable at the beginning of a new run +* Prevention of potential scripting errors when the state of a local variable is maintained across multiple script executions and the script does not re-initialise the variable at the beginning of a new run We will call this scope **true local variable** to distinguish it from old local variables. Declaration of true local variables will look like this: ```` -Local Long a = 1 -Local Long b +Local long a = 1 +Local long b ```` -If no default value is given the variable is default initialised (value 0, empty string, empty list, null reference). +If no default value is given, the variable is default-initialised (value 0, empty string, empty list, null reference). ### Record Variables -A problem (especially relevant for large projects with complex functionality) is that there is no place other then global variables to store additional state that is not specific to an instance. A global function with multiple variables can be used instead but that is just a workaround. +A common problem (especially relevant for large projects with complex functionality) is that there is no place other than global variables to store additional state that is not specific to an instance. A global function with multiple variables can be used instead, but that is just a workaround. -Furthermore there is currently no way to add new state to other objects (object in the OOP sense, not in the Morrowind sense) other then instances. +Furthermore, there is currently no way to add new state to other objects (object in the OOP sense, not in the Morrowind sense) other than instances. -Therefore we introduce record variables which are a generalisation of old local variables in local scripts. +Therefore, we introduce record variables, which are a generalisation of old local variables in local scripts. The respective records will be enhanced by an optional list of subrecords that can declare record variables and their default values. @@ -428,33 +428,33 @@ Obvious candidates for this feature are: * Worldspaces * Factions -Note: Object record variables are essentially the same as old local variables declared in a local script, but declared in the object record instead of the script. +**Note**: Object record variables are essentially the same as old local variables declared in a local script - only that they are declared in an object record instead. We will introduce the following record variable functions: -* Has{Type} (vn): Return 1 if record/reference has a long/float/string/list record variable of name vn, 0 otherwise. -* Get{Type} (vn): Return value of record/reference variable vn. Note that we don't use the x.y syntax here, because we need to explicitly specify the type to allow for proper compile time error checking for reference variables. +* `Has{Type}(vn)`: Returns 1 if record/reference has a long/float/string/list record variable of name `vn`, 0 otherwise +* `Get{Type}(vn)`: Returns the value of record/reference variable `vn`. Note that we don't use the x.y syntax here, because we need to explicitly specify the type to allow proper compile-time error checking for reference variables. -and the following record variable instruction: +and the following record variable instructions: -* Set{Type} (vn, val): Set variable vn to value val. It is an error, if this variable does not exist. +* `Set{Type}(vn, val)`: Sets variable `vn` to value `val`. It is considered an error if this variable does not exist. -With all functions and instructions the record/reference is specified via the -> operator in the usual way. When using object records (via ID) the first instance found is used instead of the object record. +Within all functions and instructions, the record/reference is specified via the `->` operator in the usual way. When using object records (via ID), the first instance found is used instead of the object record. Variable names used in the lists above: -* id: string -* vn: string -* ref: reference -* val: value according to {Type} +* `id` is a `String` +* `vn` is a `String` +* `ref`is a reference of type `Ref` +* `val` is a value according to `{Type}` -and finally {Type} standing for Long, Float, String, LongList, FloatList or StringList +Finally, `{Type}` stands for `long`, `float`, `String`, `LongList`, `FloatList`, or `StringList`. -We can fold the access of local variables in global scripts into the new record variable system, even though global scripts do not have explicit record variables. The new functions for record variable access make the old x.y syntax obsolete and therefore we declare it as deprecated. +We can fold the access of local variables in global scripts into the new record variable system, even though global scripts do not have explicit record variables. The new functions for record variable access make the old x.y syntax obsolete and, therefore, we declare it as deprecated. ## Control Structures -We will add a for loop that works on lists. To avoid unnecessary complexity in the language and to encourage the use of lists we will not have an index based for loop (these can easily be simulated via while). +We will add a `For` loop that works on lists. To avoid unnecessary complexity in the language and to encourage the use of lists, we will not have an index-based `For` loop (these can easily be simulated via `While`). Example: @@ -464,104 +464,104 @@ For x In l EndFor ```` -We will add the break and continue keywords both for for and while loops. +We will add the `break` and `continue` keywords both for `For` and `While` loops. -We may add a switch-case construct, but this is most likely a stage 2 task since there are many different ways to do switch-case and no obviously superior solution. This will most likely require extended discussion and design work. +We may add a `switch-case` construct, but this is most likely a stage-2 task since there are many different ways to do `switch-case` and no obviously superior solution. This will most likely require extended discussion and design work. ## General Additions -The vanilla scripting language is missing a few pieces of basic functionality. Therefore we need to add the following kinds of instructions: +The vanilla scripting language is missing a few pieces of basic functionality. Therefore, we need to add the following kinds of instructions: -* Trigonometric functions (Cos, Sin, Tan, Acos, Asin, Atan, Atan2, DegToRad, RadToDeg) -* Logical boolean function (And, Or, Not, Xor) -* Other mathematical functions (Abs, Floor, Ceil, Clamp, Lerp, Sign) +* Trigonometric functions (`Cos`, `Sin`, `Tan`, `Acos`, `Asin`, `Atan`, `Atan2`, `DegToRad`, `RadToDeg`) +* Logical boolean function (`And`, `Or`, `Not`, `Xor`) +* Other mathematical functions (`Abs`, `Floor`, `Ceil`, `Clamp`, `Lerp`, `Sign`) ## Object Management -We will add a function that moves an instance into the world. This feature (especially the move function) preludes a concept of persistent instance identity, which is most likely a stage 2 or OpenMW 2.0 feature. For now these functions are required to deal with state attached to the instance in the form of record variables/old local variables. +We will add a function that moves an instance into the world. This feature (especially the `move` function) preludes a concept of persistent instance identity, which is most likely a stage-2 or OpenMW-2.0 feature. For now, these functions are required to deal with state attached to the instance in the form of record variables or old local variables respectively. -* MoveObjectToWorld (target, v6): Move instance from current location to world location. -* CloneObjectToWorld (target, v6): Add a copy of instance to world location. +* `MoveObjectToWorld(target, v6)`: Moves the instance from current location to world location `target` at coordinates `v6` +* `CloneObjectToWorld(target, v6)`: Adds a copy of the instance `target` to world location `target` at coordinates `v6` -(in all cases reference specification for objects to be moved/cloned via -> operator in the usual way) +**Note**: In all cases, reference specification for objects to be moved/cloned can be done via the `->` operator in the usual way. -Moving or cloning out of a container will only move or clone a single item, not the whole stack. +**Note**: Moving or cloning out of a container will only move or clone a single item, not the whole stack. -The target value represents the target cell or worldspace (see section "References to cells in data structures" for details) +The `target` value represents the target cell or worldspace (see section *References to Cells in Data Structures* for details) -We will add replacement functions for object placement related functions without declaring the original functions deprecated. +We will add replacement functions for object-placement-related functions without declaring the original functions deprecated. Instructions: -* SetRotation (v3): Set instance's rotation; characters and NPCs ignore first two elements (replaces SetAngle) -* SetPosition (v3): Set instance's position (replaces SetPos) -* Teleport (target, v6, strict=0): Teleport instance to location specified by target and v6 (if the instance is a character or NPC the xrot and yrot values are ignored). If strict!=0, no corrections are made to the specified location. Otherwise the location may be adjusted for safety. (replaces Position and PositionCell) -* Spawn (objectId, target, v6, strict=0): Same as teleport, but spawns a new object (replaces PlaceItem and PlaceItemCell) +* `SetRotation(v3)`: Sets the instance's rotation; actors ignore the first two elements (replaces `SetAngle`) +* `SetPosition(v3)`: Sets the instance's position (replaces `SetPos`) +* `Teleport(target, v6, strict = 0)`: Teleports the instance to location specified by `target` and `v6`; actors ignore the first two elements; if `strict != 0`, no corrections are made to the specified location, otherwise the location may be adjusted for safety (replaces `Position` and `PositionCell`) +* `Spawn(objectId, target, v6, strict = 0)`: Same as `Teleport`, but spawns a new object `objectId` (replaces `PlaceItem` and `PlaceItemCell`) Functions: -* GetRotation: Returns instance's rotation as a Vector3 (replaces GetAngle) -* GetPosition: Returns instance's position as a Vector3 (replaces GetPos) +* `GetRotation`: Returns the instance's rotation as a `Vector3` (replaces `GetAngle`) +* `GetPosition`: Returns the instance's position as a `Vector3` (replaces `GetPos`) -## Object-Type Specific Additions +## Object-Type-Specific Additions We will add functions to modify and query other non-item-specific state that is already present in the cellref class and therefore already written to saved game files. Instructions: -* SetTrap (trapId): Set trap ID -* SetKey (trapId): Set key ID -* SetTeleport (target, v4): Set teleport location (does not affect teleport flag) +* `SetTrap(trapId)`: Sets the instance's trap ID to `trapId` +* `SetKey(trapId)`: Sets the instance's key ID to `keyId` +* `SetTeleport(target, v4)`: Sets the instance's teleport location to the location specified by `target` and `v4` (does not affect teleport flag) Functions: -* GetTrap: Return trap ID -* GetKey: Return key ID -* GetTeleportTarget: Return teleport target -* GetTeleportPosition: Returns teleport location and zrot as Vector4 +* `GetTrap`: Return the instance's trap ID +* `GetKey`: Return the instance's key ID +* `GetTeleportTarget`: Return the instance's teleport target +* `GetTeleportPosition`: Returns the instance's teleport location and zrot as `Vector4` ## Object-Types -We introduce compile time constants for object types (Weapons, Activators and so on). These have integer values. The constants must be named consistently in such a way that name collisions are unlikely (e.g. TypeWeapon, TypeActivator). +We introduce compile-time constants for object types (Weapons, Activators and so on). These have integer values. The constants must be named consistently in such a way that name collisions are unlikely (e.g. `TypeWeapon`, `TypeActivator`). We add a function that returns the type of an object or instance: -* GetObjectType +* `GetObjectType` ## Queries -A query returns a list of instances within a certain area. Along with the description of the area the query takes a list of object types (t) as an additional argument. +A query returns a list of instances within a certain area. Along with the description of the area, the query takes a list of object types (`t`) as an additional argument. -* QuerySphere t, v3, r -* QueryBox t, v3_tl, v3_br (this is an axis-aligned box) -* QueryCylinder t, v3, r, height (the cylinder is aligned along the Z-axis, v3 specifies the middle point of the bottom circle) +* `QuerySphere t, v3, r` +* `QueryBox t, v3_tl, v3_br` (this is an axis-aligned box) +* `QueryCylinder t, v3, r, height` (the cylinder is aligned along the z-axis, `v3` specifies the middle point of the bottom circle) ## Text Formatting -The vanilla scripting language provides text formatting in a single place (MessageBox instruction). This is insufficient, because: +The vanilla scripting language provides text formatting in a single place (`MessageBox` instruction). This is insufficient: -1. We will need text formatting in many places, independently of message boxes -2. The MessageBox text formatting sucks +1. We will need text formatting in many places, independently of message boxes. +2. The `MessageBox` text formatting sucks. -We will deprecate MessageBox. As a replacement we will introduce new instructions for creating message boxes (see following sub-section and GUI section) without a formatting features and also separate text formatting functions. +We will deprecate `MessageBox`. As a replacement, we will introduce new instructions for creating message boxes (see following subsection and *GUI* section) without a formatting feature and separate text formatting functions. -* Str(v, p = -1): Returns a string containing the string representation of the numeric value v. p specifies the number of digits after the decimal point. If p==-1 a suitable value depending on the type of v will be chosen. -* LFormat (s, a): Returns a string equal to the string s with all occurrences of %x replaced with the respective element indexed x from the string list a. -* Join (l, s): Return a string consisting of the members of the string list l separated by the string s +* `Str(v, p = -1)`: Returns a `String` containing the string representation of the numeric value `v`. `p` specifies the number of digits after the decimal point. If `p == -1`, a suitable value depending on the type of `v` will be chosen +* `LFormat(s, a)`: Returns a `String` equal to the `String` `s` with all occurrences of %x replaced with the respective element indexed x from the string list `a` +* `Join(l, s)`: Returns a `String` consisting of the members of the `StringList` `l` separated by the `String` `s` -## Multiple Choice Message Boxes +## Multiple-Choice Message Boxes -We add two new functions for showing multiple choice message boxes: +We add two new functions for showing multiple-choice message boxes: -* MakeChoice t, sl, f -* MakeBranchChoice t, sl, fl +* `MakeChoice t, sl, f` +* `MakeBranchChoice t, sl, fl` Arguments are as follows: -* t: The message box text -* sl: A list of strings, defining the available options -* f: The ID of a script function that is called when an option is selected. The function takes one long argument (the option index, starting at 0). -* fl: A list of script function names, same length as sl. Functions do not take any arguments. Empty strings in the list are allowed, which makes OpenMW ignore the respective choice. +* `t` is the message box's text +* `sl` is a list of strings, defining the available options +* `f` is the ID of a script function that is called when an option is selected; the function takes one `long` argument (the option index, starting at 0) +* `fl` is a list of script function names with the same length as `sl`; the functions do not take any arguments; empty strings in the list are allowed, which makes OpenMW ignore the respective choice ## Functions @@ -575,35 +575,35 @@ The syntax of the begin statement is extended in the following way: Begin { ( { -> return-type } ) } ```` -( {} denoting optional parts ) +**Note**: `{}` denotes optional parts. -Argument lists are allowed to be empty. Argument lists are comma-separated. Elements in argument list are type name pairs. Elements can be given default values in a C++ typical fashion. +Argument lists are allowed to be empty and are comma-separated. Elements in argument lists are type-name pairs. Elements can be given default values in a C++ fashion. -Arguments and return values are for all intents and purposes true local variable. Therefore the ref type is available. +Arguments and return values are for all intents and purposes true local variables. Therefore, the `Ref` type is available. ### Calling -A function can be called by its name followed by parenthesis. Arguments are listed inside the parenthesis separated by comma. +A function can be called by its name followed by parentheses. Arguments are listed inside the parentheses separated by commas. -We may at some point add the use of named arguments for function calling (comparable to Python), but this is most likely a stage 2 feature. +We may at some point add the use of named arguments for function calling (comparable to Python), but this is most likely a stage-2 feature. -If the function has a return type the function is evaluated as an expressions with a type corresponding to the return type. +If the function has a return type, the function is evaluated as an expression with a type corresponding to the return type. -Calling a function pauses the execution of the calling script, executes the called script and then resumes the execution of the calling script. This is different from the StartScript/StopScript instructions. The StartScript instruction is not modified (i.e. no argument passing). +Calling a function pauses the execution of the calling script, executes the called script and then resumes the execution of the calling script. This is different from the `StartScript` and `StopScript` instructions. The `StartScript` instruction is not modified (i.e., no argument passing). -A function call must be performed via a function name literal. The function name can not be given as a string variable, since this would make it impossible to check for the correctness of the function signature at compile time. +A function call must be performed via a function name literal. The function name cannot be given as a string variable, since this would make it impossible to check for the correctness of the function signature at compile time. Local and global scripts must not have any arguments without default values. ## Script Hooks -We introduce two new record type (hook, hooked script). The hooked script is a generalisation of the concept of a startup script. We may decide to fold the startup script record type into the hook record type. +We introduce two new record types (hook, hooked script). The hooked script is a generalisation of the concept of a start-up script. We may decide to fold the start-up script record type into the hook record type. ### Hooked Scripts A hooked script record binds a script to a hook record. More than one hooked script record per hook record is allowed. -The ID of a hook record is build from the hook record ID followed by a $ followed by the script name. +The ID of a hook record is built from the hook record ID followed by a `$` followed by the script name. Example: @@ -611,9 +611,9 @@ Example: Startup$KillFargoth ```` -By building hook IDs in this way we allow content files to delete hooked script records in dependencies without the need for an additional ID that identifies individual hook records. +By building hook IDs in this way, we allow content files to delete hooked script records in dependencies without the need for an additional ID that identifies individual hook records. -If more than one script is attached to a hook the order in which the scripts are executed is implementation defined at this point and scripts must not depend on a specific order. +**Note**: If more than one script is attached to a hook, the order in which the scripts are executed is, at this point, implementation-defined and, thus, scripts must not depend on a specific order. ### Hooks @@ -621,34 +621,34 @@ A hook record declares a new hook. Each hook record contains a list of argument There will be no return values (problematic since there can be multiple scripts per hook). -We provide system hooks within the namespace sys that are called in specific situations (see the section about de-hardcoding for some examples). Content developers may also provide their own hooks (user hooks). -System hooks are not added to content files, since they can not be modified by content developers anyway. We will instead inject the system hook records on content file load. +We provide system hooks within the namespace `sys` that are called in specific situations (see the section about de-hardcoding for some examples). Content developers may also provide their own hooks (user hooks). +System hooks are not added to content files, since they cannot be modified by content developers anyway. We will instead inject the system hook records on content-file load. System hooks are triggered by the engine. System hooks and user hooks can be triggered explicitly with a new script instruction. ## Script Slots -We define the term script slot as an optional sub-record in an object record that contains the name of a script. The default script slot for most object types is the "Run-Once-Per-Frame" slot, a.k.a. local script. +We define the term script slot as an optional subrecord in an object record that contains the name of a script. The default script slot for most object types is the "Run-Once-Per-Frame" slot, a.k.a. local script. -The default slot does not take any function arguments, but other slots can. The function arguments of a script attached to a slot need to match the hard-coded argument list of the slot. +The default slot does not take any function arguments, but other slots can. The function arguments of a script attached to a slot need to match the hardcoded argument list of the slot. -Note that if to scripts attached to the same object both define a (non-true) local variable with the same name, there will be only one variable. It is an error if the type of these variables don't match. +**Note**: If two scripts attached to the same object both define a (non-true) local variable with the same name, there will be only one variable. It is an error if the type of these variables don't match. ### Additional Slots -We add slots for OnX type keywords to object types where applicable The relevant keywords are: +We add slots for `OnX`-type keywords to object types where applicable. The relevant keywords are: -* OnActivate -* OnDeath -* OnKnockout -* OnMurder -* OnPCAdd -* OnPCDrop -* OnPcEquip -* OnHitMe -* OnPCRepair -* OnPCSoulGemUse -* OnRepair +* `OnActivate` +* `OnDeath` +* `OnKnockout` +* `OnMurder` +* `OnPCAdd` +* `OnPCDrop` +* `OnPcEquip` +* `OnHitMe` +* `OnPCRepair` +* `OnPCSoulGemUse` +* `OnRepair` ### Custom Slots @@ -656,221 +656,221 @@ We may allow the addition of custom slots (defined by the content developer), th ## Namespaces -The namespace of a script is determined by its ID. For example a script with the ID foo::Bar would be placed in the namespace foo. +The namespace of a script is determined by its ID. For example a script with the ID `foo::Bar` would be placed in the namespace `foo`. -IDs in a script refer to the local namespace by default, meaning the ID a in the script b::c would refer to b::a if such an ID exists or to ::a otherwise. +IDs in a script refer to the local namespace by default, meaning the ID `a` in the script `b::c` would refer to `b::a` if such an ID exists, or to `::a` otherwise. ## Other Script Instructions This is a collection of script instructions that fit nowhere else: -* GetPcTarget: Return the reference of the instance the player is currently looking at (crosshair). Can be a NullRef. -* GetMultiplayer: Always returns 0 -* GetPlayers: Returns a list of reference to all player controlled instances. This list contains a single reference to the instance of object Player. +* `GetPcTarget`: Returns the reference of the instance the player is currently looking at (crosshair); can be a null reference +* `GetMultiplayer`: Always returns 0 +* `GetPlayers`: Returns a list of references to all player-controlled instances; this list contains a single reference to the instance of object `Player` ## Other Script Hooks This is a collection of script hooks that fit nowhere else: -* sys::NewGameStarted: Executed when a new game is started. +* `sys::NewGameStarted`: Executed when a new game is started # Cells, Worldspaces & Areas -## Interior vs Exterior +## Interior vs. Exterior -The distinction between Interior and Exterior cells stopped making sense with the release of the Tribunal add-on. Therefore we should drop this terminology and replace it with Unpaged Cells (Interior) and Paged Cells (Exterior). +The distinction between interior and exterior cells stopped making sense with the release of the Tribunal add-on. Therefore, we should drop this terminology and replace it with **Unpaged Cells** (interior) and **Paged Cells** (exterior). -## Paged Cells +## Unpaged Cells -Paged cells remain essentially unchanged from old exterior cells, the only major difference being the ID and the worldspace. +Unpaged cells take the place of old interior cells. The only difference between old interior cells and unpaged cells is the worldspace. -Currently there is no uniform ID scheme for cells. Interior cells are named via an ID, exterior cells are named via a cell index (x, y). This needs to be changed, since we need a more consistent way to address cells. +By default, each unpaged cell has an associated worldspace record with the same ID. Furthermore, the unpaged-cell record is enhanced by an optional subrecord that specifies an alternative worldspace (if present). -The editor already gives exterior cells an ID (currently in the form of #x,y). The new scheme (relevant at least for the editor and scripting) will be "worldspace::x,y" (the cell will have the ID "x,y" in the namespace "worldspace"). We may also consider to replace the two integer coordinates in the cell record with the string ID. The performance impact of this change should be insignificant. +Most of the cell configuration data is moved out of the cell record and into the worldspace record. -The default exterior worldspace will be called default (previously called sys::default). Therefore the old exterior cell 0, 0 will be called "default:0,0". We are moving default out of sys to avoid creating an exception to the rule that content files are not allowed to create records in sys. +By moving out the worldspace into a separate record, we can unify worldspace data between interior and exterior cells. We can also associate multiple interior cells with the same worldspace or interior cells with an exterior worldspace. This should reduce duplicated data significantly. -## Unpaged Cells +## Paged Cells -Unpaged cells take the place of old interior cells. The only difference between old interior cells and unpaged cells is the worldspace. +Paged cells remain essentially unchanged from old exterior cells, the only major difference being the ID and the worldspace. -By default each unpaged cell has an associated worldspace record with the same ID. Furthermore the unpaged cell record is enhanced by an optional sub-record that specifies an alternative worldspace (if present). +Currently, there is no uniform ID scheme for cells. Interior cells are named via an ID, exterior cells are named via a cell index (x, y). This needs to be changed, since we need a more consistent way to address cells. -Most of the cell configuration data is moved out of the cell record and into the worldspace record. +The editor already gives exterior cells an ID (currently in the form of `#x,y`). The new scheme (relevant at least for the editor and scripting) will be `worldspace::x,y` (the cell will have the ID `x,y` in the namespace `worldspace`). We may also consider to replace the two integer coordinates in the cell record with the string ID. The performance impact of this change should be insignificant. -By moving out the worldspace into a separate record we can unify worldspace data between interior and exterior cells. We can also associate multiple interior cells with the same worldspace or interior cells with an exterior worldspace. This should reduce duplicated data significantly. +The default exterior worldspace will be called `default` (previously called `sys::default`). Therefore, the old exterior cell 0, 0 will be called `default:0,0`. We are moving `default` out of `sys` to avoid creating an exception to the rule that content files are not allowed to create records in `sys`. ## Worldspace Records -Worldspace records are shared by paged and unpaged cells (i.e. there are no interior and exterior worldspaces). +Worldspace records are shared by paged and unpaged cells (i.e., there are no interior and exterior worldspaces). Worldspace records contain the following fields: -* Water: Default Water level, Water ID (see de-hardcoding). Note: Script instructions that modify the water level will now modify the water level for a worldspace instead of a cell. +* Water: Default Water Level, Water ID (see de-hardcoding). **Note**: Script instructions that modify the water level will now modify the water level of a worldspace instead the one of a cell. * Ambient Light: 4 values -* Sky: Sky ID (analogous to Water ID, most likely not part of stage 1). Note: A worldspace can not have both an ambient light and a sky sub-record. -* Terrain: No data (might be better implemented as a flag). Presence indicate that worldspace has terrain. Ignored by unpaged cells. +* Sky: Sky ID (analogous to Water ID, most likely not part of stage 1). **Note**: A worldspace cannot have both an ambient-light and a sky subrecord. +* Terrain: No data (might be better implemented as a flag); presence indicates that worldspace has terrain; ignored by unpaged cells -All fields are optional. If a field is missing the respective cell/worldspace element is not present. +All fields are optional. If a field is missing, the respective cell/worldspace element is not present. -## References to cells in data structures +## References to Cells in Data Structures -Vanilla references to cells are generally based on the ID of the cell. There are no fields in vanilla Morrowind data structures that reference individual exterior cells. +Vanilla references to cells are generally based on the ID of the cell. There are no fields in vanilla Morrowind's data structures that reference individual exterior cells. We keep these reference fields, but change their meaning: -1. Check if it is the ID of an unpaged cell. If yes, use this cell -2. Check if it is a worldspace. If yes, use paged cells in this worldspace. +1. Check whether it is the ID of an unpaged cell. If yes, use this cell. +2. Check whether it is a worldspace. If yes, use the paged cells in this worldspace. 3. Otherwise, error. ## Scripts ### Water Level -The existing script instructions that deal with water level can be safely extended to the new system. We will add an optional argument at the end of the argument list of each instruction (GetWaterLevel, ModWaterLevel and SetWaterLevel) that specifies the worldspace the instruction is action on. If the argument is missing the instruction effects the current worldspace instead. +The existing script instructions that deal with water level can be safely extended to the new system. We will add an optional argument at the end of the argument list of each instruction (`GetWaterLevel`, `ModWaterLevel`, and `SetWaterLevel`) which specifies the worldspace the instruction is acting on. If the argument is missing, the instruction affects the current worldspace instead. -Note that this is different from vanilla Morrowind in regards to use in exterior cells. +**Note**: This behaviour is different from vanilla Morrowind in regards to exterior cells. ### New Instructions We add the following script instructions: -* EnumerateActiveCells: Returns a list of strings, containing the IDs of all cells currently active. Paged cells are listed individually. -* GetWorldspace (c): Returns the ID of the worldspace the cell with ID c is in -* GetWater (w): Returns the ID of the water used in the worldspace with ID w -* GetSky (w): Returns the ID of the sky used in the worldspace with ID w +* `EnumerateActiveCells`: Returns a `StringList` containing the IDs of all cells currently active; paged cells are listed individually +* `GetWorldspace(c)`: Returns the ID of the worldspace the cell with the ID `c` is in +* `GetWater(w)`: Returns the ID of the water used in the worldspace with the ID `w` +* `GetSky(w)`: Returns the ID of the sky used in the worldspace with the ID `w` ### Script hooks We add four new script hooks: -* RegionEntered -* RegionExited -* WorldspaceEntered -* WorldspaceExited +* `RegionEntered` +* `RegionExited` +* `WorldspaceEntered` +* `WorldspaceExited` -All four hooks take the ID of the region or worldspace as arguments and are executed when the player enters or exists a region or worldspace. +All four hooks take the ID of the region or worldspace as arguments and are executed when the player enters or exits a region or a worldspace. ## Areas The ability to designate a section of a worldspace with a specific ID that can be checked or referenced has many uses. A few examples: -* Give a NPC certain dialogue topics only in specific places +* Give an NPC certain dialogue topics only in specific places * Limit wandering NPCs to an area * Add special/magical effects to a location -Currently our ability to do that is largely limited to cells. This is a problem because of two reasons: +Currently, our ability to do that is largely limited to cells. This is a problem because of two reasons: -* The fixed-size square nature of nature of cells makes areas unreasonably inflexible. +* The fixed-size, square nature of cells makes areas unreasonably inflexible. * We can't have overlapping areas. ### Record -We introduce a new record (Area) that consists of the following fields: +We introduce a new record ("Area") that consists of the following fields: * Worldspace ID -* Polygon (list of 2D coordinates), defining a surface on the x-y-plane -* Min-Height: Z-coordinate at which the area starts -* Max-Height: Z-coordinate at which the area ends +* Polygon: A list of 2D coordinates defining a surface on the xy-plane +* Min-Height: z-coordinate at which the area starts +* Max-Height: z-coordinate at which the area ends * Enter script ID (string, optional): Script function that is called when the player enters the area * Exit script ID (string, optional): Script function that is called when the player exits the area (must also be called in case of teleportation) * Inside script ID (string, optional): Script function that is called while the player is in the area. -* Inside script delta (float, optional): Minimum time between two executions of the inside script function. -* Composite (integer, optional): If this flag is set the area will be ignored by OpenMW except as a part of a joined area. Composite areas are not accessible by script instructions and do not run any scripts of their own. +* Inside script delta (float, optional): Minimum time between two executions of the inside-script function. +* Composite (integer, optional): If this flag is set, the area will be ignored by OpenMW except as a part of a joined area. **Note**: Composite areas are not accessible by script instructions and do not run any scripts of their own. All script functions take the ID of the area as a parameter. -Whenever a cell or a worldspace is referenced for checking or defining locations the ID of an area can be used instead. +Whenever a cell or a worldspace is referenced to check or define locations, the ID of an area can be used instead. ### Script Functions We add the following script functions: -* GetAreas (v3): Returns a string list containing the IDs of all areas v3 is inside of -* InArea (id): Returns 1 or 0 depending on if the instance is in area id or not +* `GetAreas(v3)`: Returns a `StringList` containing the IDs of all areas `v3` is in +* `InArea(id)`: Returns 1 (instance is in area `id`) or 0 (otherwise) ## Joined Areas -A joined area is an area that consists of multiple un-joined areas. Joined areas can contain both composite and non-composite areas. A joined area can stretch across multiple worldspaces. Scripts do not distinguish between areas and joined areas. +A joined area is an area that consists of multiple unjoined areas. Joined areas can contain both composite and non-composite areas. A joined area can stretch across multiple worldspaces. Scripts do not distinguish between areas and joined areas. ### Record -We introduce a new record (JoinedArea) that consists of the following fields: +We introduce a new record ("Joined Area") that consists of the following fields: * Enter script ID (string, optional): Script function that is called when the player enters the area * Exit script ID (string, optional): Script function that is called when the player exits the area (must also be called in case of teleportation) * Inside script ID (string, optional): Script function that is called while the player is in the area. -* Inside script delta (float, optional): Minimum time between two executions of the inside script function. +* Inside script delta (float, optional): Minimum time between two executions of the inside-script function. -# Item-Interactions & -Management +# Item Interaction & Item Management ## Deletion -We will add an instruction to delete instances via a reference variable. Current solutions are insufficient because they can not target specific items (when in a container) and require different approaches depending on if an instance is in a container or in the world. Self-deletion must be safe. For the sake of consistency We extend this instruction to work on non-item objects too. +We will add an instruction to delete instances via a reference variable. Current solutions are insufficient because they can not target specific items (when in a container) and require different approaches depending on whether an instance is in a container or in the world. Self-deletion must be safe. For the sake of consistency, we extend this instruction to work on non-item objects too. -* Delete (reference specification for instance to be deleted via -> operator in the usual way) +* `Delete` (reference specification for instance to be deleted via the `->` operator in the usual way) ## Container -We will add a function that returns the contents of a container as a list of references (the term container includes here creatures and NPCs). +We will add a function that returns the contents of a container as a list of references. **Note**: In this case, the term "container" also includes creatures and NPCs. -* GetContents (reference specification for container via -> operator in the usual way) +* `GetContents` (reference specification for container via the `->` operator in the usual way) -We will add a function that moves/clones an item instance into a container (actual container or NPC). This feature (especially the move function) preludes a concept of persistent instance identity, which is most likely a stage 2 or OpenMW 2.0 feature. For now these functions are required to deal with state attached to the instance in the form of record variables/old local variables. +We will add a function that moves/clones an item instance into a container (actual container or actor). This feature (especially the `move` function) preludes a concept of persistent instance identity, which is most likely a stage-2 or OpenMW-2.0 feature. For now, these functions are required to deal with state attached to the instance in the form of record variables / old local variables. (In all cases, the reference specification for the item to be moved/cloned works via the `->` operator in the usual way.) -* MoveItemToContainer (ref/id, count=1): Move item from current location to container specified by ref/id -* CloneItemToContainer (ref/id, count=1): Add a copy of item to container specified by ref/id +* `MoveItemToContainer(ref/id, count = 1)`: Move item from current location to container specified by `ref/id` +* `CloneItemToContainer(ref/id, count = 1)`: Add a copy of item to container specified by `ref/id` -(in all cases reference specification for item to be moved/cloned work via -> operator in the usual way) - -The count argument is ignored when the original item is not in a container. +The `count` argument is ignored when the original item is not in a container. ## Other Item-Related Instructions -* SetItemHealth (health): Set item current health -* SetItemCharge (charge): Set item current charge -* SetItemOwner (owner): Set item owner ID -* SetItemSoul (soul): Set soul ID (soul gems only) -* SetItemFaction (faction): Set item faction ID -* SetItemFactionRank (rank): Set item faction rank +Instructions: + +* `SetItemHealth(health)`: Sets item's current health +* `SetItemCharge(charge)`: Sets item's current charge +* `SetItemOwner(owner)`: Sets item's owner ID +* `SetItemSoul(soul)`: Sets item's soul ID (soul gems only) +* `SetItemFaction(faction)`: Sets item's faction ID +* `SetItemFactionRank(rank)`: Sets item's faction rank Functions: -* IsItem: Return 1 if reference is an item, 0 otherwise -* GetItemHealth: Return item current health -* GetItemMaxHealth: Return item max health -* GetItemCharge: Return item current charge -* GetItemMaxCharge: Return item max charge -* GetItemOwner: Return item owner ID -* GetSoulItem: Return soul ID (soul gems only) -* GetItemFaction: Return item faction ID -* GetItemFactionRank: Return item faction rank +* `IsItem`: Returns 1 if reference is an item, 0 otherwise +* `GetItemHealth`: Returns item's current health +* `GetItemMaxHealth`: Returns item's maximum health +* `GetItemCharge`: Returns item's current charge +* `GetItemMaxCharge`: Returns item's maximum charge +* `GetItemOwner`: Returns item's owner ID +* `GetSoulItem`: Returns item's soul ID (soul gems only) +* `GetItemFaction`: Returns item's faction ID +* `GetItemFactionRank`: Returns item's faction rank ## Item Tags -Currently there is no customisable way of categorising items. To compensate for this shortcoming we introduce items tags. +Currently, there is no customisable way of categorising items. To compensate for this shortcoming, we introduce item tags. An item tag is a string that is attached to an object record. An object record can have multiple tags. -Objects also have implicit item tags that are determined by their type (e.g. every weapon object has a tag weapon even without the object record explicit containing this tag. We may introduce other kinds of implicit tags (e.g. weapon types, enchanted items). +Objects also have implicit item tags that are determined by their type (e.g., every weapon object has a tag "Weapon" even without the object record explicitly containing this tag). We may introduce other kinds of implicit tags (e.g. weapon types, enchanted items). The de-hardcoding introduces additional item tags. Item tags are immutable at runtime and can be queried via script instructions: -* GetItemTags (Id): Returns a string list of tags for the object Id. As usual instead of an ID a reference variable can be used. -* HasitemTag (Id, Tag): Return 1 or 0 depending on Id has the tag Tag. +* `GetItemTags(id)`: Returns a `StringList` of tags for the object `id`; as usual, instead of an ID a reference variable can be used +* `HasitemTag(id, tag)`: Returns 1 or 0 depending on `id` having the tag `tag` -Using these instructions on non-item objects return an empty list and 0 respectively. +Using these instructions on non-item objects returns an empty list or 0 respectively. -Item tags can be used to organise container windows (see GUI section). +Item tags can be used to organise container windows (see *GUI* section). A few examples of tags that content developers may come up with: -* quest: A quest item -* vendor: An item that has no other use than to sell it to a vendor +* "Quest": A quest item +* "Vendor": An item that has no other use than to sell it to a vendor -We may suggest some default tags in the documentation (or even the editor) to encourage more consistent tag use by content developers. +We may suggest some default tags in the documentation (or even the editor itself) to encourage more consistent tag use by content developers. ## Interactions @@ -880,435 +880,467 @@ We enhance the way how the player interacts with the world, especially via items There are three methods of interaction: -* The player presses the attack button while holding an interaction-item in his hand and targeting an object in the world. This feature exists in vanilla MW only within the security skill. We generalise these kinds of interactions (see Held Items in the De-Hardcoding section). We also allow this feature for weapons held in the main hand. If an interaction is possible the interaction takes the place of the attack. If the interaction is ignored or refused the attack proceeds. -* The player drags an item from a container onto on instance in the world. This kind of interaction does not exist in vanilla MW. -* The player drags an item from a container onto another item in another (or the same) container. This kind of interaction does not exist in vanilla MW. +* The player presses the attack button while holding an interaction item in his hand and targeting an object in the world. This feature exists in vanilla Morrowind only within the Security skill. We generalise this kind of interaction (see *Held Items* in the *De-Hardcoding* section). We also allow this feature for weapons held in the main hand: If an interaction is possible, the interaction takes the place of the attack. If the interaction is ignored or refused, the attack proceeds. +* The player drags an item from a container onto an instance in the world. This kind of interaction does not exist in vanilla Morrowind. +* The player drags an item from a container onto another item in another (or the same) container. This kind of interaction does not exist in vanilla Morrowind. ### Interaction Subrecords -All item object record types are enhanced by a new optional sub-record that holds the name of the ItemItemInteraction script. +All item-object record types are enhanced by a new optional subrecord that holds the name of the ItemItemInteraction script. -All object record types (including items) are enhanced by a new optional sub-record that holds the name of the ItemObjectInteraction script. +All object record types (including items) are enhanced by a new optional subrecord that holds the name of the ItemObjectInteraction script. -### Interaction-Chain +### Interaction Chain -Custom interactions are handled by running through a sequence of scripts. Each script can either ignore the interactions or accept it or refuse it. +Custom interactions are handled by running through a sequence of scripts. Each script can either ignore the interaction, accept it, or refuse it. -If the action is accepted or refused the chain stops. If the action is refused or the chain runs to its end without any script accepting it the player will be notified via a sound-effect. We may either re-purpose an existing sound-effect or acquire a new one. +If the action is accepted or refused, the chain stops. If the action is refused or the chain runs to its end without any script accepting it, the player will be notified via a sound effect. We may either re-purpose an existing sound effect or acquire a new one. We add two new script instructions: -* AcceptInteraction -* RefuseInteraction +* `AcceptInteraction` +* `RefuseInteraction` -Using any of these instructions outside of an interaction chain is an error. We use these instructions instead of return values, because a part of the interaction chain works via hooks which do no provide return values. +Using any of these instructions outside of an interaction chain is an error. We use these instructions instead of return values, because a part of the interaction chain works via hooks which do not provide return values. All interaction chain functions share the following signature: -* ref to Item A/Item -* ref to Item B/Object -* ref to actor performing the interaction (player for now, we may extend that later) -* integer: 0 interaction was initiated via drag & drop, 1 interaction was initiated via attack button +* reference to item A +* reference to item/object B +* reference to actor performing the interaction (player for now, we may extend that later) +* integer: 0 if interaction was initiated via drag & drop, 1 if interaction was initiated via attack button -### Item-Item-Chain: +### Item-Item Chain: -* Hook sys::PreItemItemInteraction -* ItemItemInteraction Script of object A -* ItemItemInteraction Script of object B -* Hook sys::ItemItemInteraction +* Hook `sys::PreItemItemInteraction` +* `ItemItemInteraction script of item A +* `ItemItemInteraction script of item B +* Hook `sys::ItemItemInteraction` -### Item-Object-Chain: +### Item-Object Chain: If the object is also an item the Item-Item-Chain is used instead. -* Hook sys::PreItemObjectInteraction -* ItemObjectInteraction Script of item -* ItemObjectInteraction Script of object -* Hook sys::ItemObjectInteraction +* Hook `sys::PreItemObjectInteraction` +* `ItemObjectInteraction` script of item A +* `ItemObjectInteraction` script of object B +* Hook `sys::ItemObjectInteraction` # De-Hardcoding -This section describes the first batch of de-hardcoding tasks. This is the core part of stage 1 (*The Grand De-Hardcoding*). We are aiming mostly for low hanging but highly profitable fruits here. More complex de-hardcoding (especially tasks that require more extensive script support) will follow in stage 2. +This section describes the first batch of de-hardcoding tasks. This is the core part of stage 1 (*The Grand De-Hardcoding*). We are aiming mostly for low-hanging, but highly profitable fruits here. More complex de-hardcoding (especially tasks that require more extensive script support) will follow in stage 2. ## GMSTs Many GMSTs will be integrated into other records, which will effectively remove the GMST. We may consider actually removing the GMST record during the load process. -We add three new types of GMSTs: +We will add three new types of GMSTs: -* IntegerList -* FloatList -* StringList +* `IntegerList` +* `FloatList` +* `StringList` -We may consider allowing content files to create new GMSTs outside of the sys namespace. This would make the GMST record type more consistent with other record types. To make this addition useful we need to add script functions to read GMSTs: +We may consider allowing content files to create new GMSTs outside of the `sys` namespace. This would make the GMST record type more consistent with other record types. To make this addition useful, we need to add script functions to read GMSTs: -* Keyword: Get{Type}Gmst() +* `Get{Type}Gmst()` -{Type} can be Integer, Float, String, Integerlist, FloatList or StringList. +`{Type}` can be of type `long`, `float`, `String`, `Longlist`, `FloatList`, or `StringList`. ## Fallback Values -The openmw.cfg file contains a set of fallback values. These were extracted from the Morrowind.ini file. As the name indicates we consider these values as a fallback for legacy format content files only. Our goal is to move all these values to content file format. In some cases we also may decide to declare a fallback value obsolete and not use it at all. +The *openmw.cfg* file contains a set of fallback values. These were extracted from the *Morrowind.ini* file. As the name indicates, we consider these values as a fallback for legacy-format content files only. Our goal is to move all these values to content file format. In some cases, we may also decide to declare a fallback value obsolete and not use it at all. All usable values should be migrated to new GMST records, unless they are already covered by other parts of the de-hardcoding. ## General Scripting Enhancements -We introduce new script functions: +We will introduce new script functions: -* Enumerate{x}: {x} is either Skills, Races, Classes, DynamicStats, Attributes, WeaponTypes, ArmorTypes, Specialisations, MagicSchools, Factions or Birthsigns. Returns a string list of the IDs of all records of the respective type. +* `Enumerate{x}`: Returns a `StringList` of the IDs of all records of the respective type; `{x}` is either "Skills", "Races", "Classes", "DynamicStats", "Attributes", "WeaponTypes", "ArmorTypes", "Specialisations", "MagicSchools", "Factions" or "Birthsigns" ## Dynamic Stats -We will unify dynamic stats and make them configurable. For stage 1 we will not allow the deletion of existing dynamic stats (they are used in too many parts of the engine). But we will allow new dynamic stats. +We will unify dynamic stats and make them configurable. For stage 1, we will not allow the deletion of existing dynamic stats (they are used in too many parts of the engine). But we will allow new dynamic stats. -We add a new record type for dynamic stats. Records for Health (sys::Health), Fatigue (sys::Fatigue) and Magicka (sys::Magicka) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). +We add a new record type for dynamic stats. Records for Health (`sys::Health`), Fatigue (`sys::Fatigue`), and Magicka (`sys::Magicka`) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). -A dynamic stats record contains the following information: +A dynamic-stat record contains the following information: * Name (taken from GMSTs for default stats) -* Tooltip text (taken from GMSTs for default stats) +* Tooltip Text (taken from GMSTs for default stats) * Colour -* (optional) PrevID: ID of stat the engine will try to sort in this stats after (similarly to info records) -* (optional) NextID: ID of stat the engine will try to sort in this stats before (similarly to info records) -* (optional) Vital-flag (player dies if stat with vital flag drops to 0) -* (optional) ID of level-up function: Takes the reference of the character as argument and returns an integer value representing the max stat change from the level-up. -* (optional) ID of time passed function: Takes the reference of the character and the duration as arguments. Returns a float that is used to modify the current stat value. Not used when waiting or sleeping. -* (optional) ID of wait function: Takes the reference of the character, the duration and a sleep flag (integer) as arguments. Returns a float that is used to modify the current stat value. -* Default value: If an actor does not have this stat (possible if the stat was defined in an addon and not in the base-game) this value is used for the maximum value of the stat. +* (optional) PrevID: ID of the stat the engine will try to subsequently sort this stat in (similar to info records) +* (optional) NextID: ID of the stat the engine will try to antecedently sort this stat in (similar to info records) +* (optional) Vital: Flag which specifies whether the player dies if the stat drops to 0 +* (optional) Level-Up Function: ID of the stat's level-up function, which takes the reference of the character as argument and returns an `long` value representing the maximum stat change achievable with a level-up +* (optional) Time-Passed Function: ID of the stat's time-passed functionm, which takes the reference of the character and the duration as arguments and returns a `float` that is used to modify the current stat value (the function is not used when waiting or sleeping) +* (optional) Wait Function: ID of the stat's wait function, which takes the reference of the character, the duration, and a sleep flag (integer) as arguments and returns a `float` that is used to modify the current stat value +* Default Value: Value that is used as the maximum value of the stat if an actor does not have this stat (possible if the stat was defined in an add-on and not in the base game) -Scripts for default stats are injected as necessary. Some of these scripts require access to GMST values. We need to figure out how to implement this, either grab the value from the GMST when adding the script or adding a script function to read GMST values. The later is easier but will not allow us to remove the redundant GMST record easily. +Scripts for default stats are injected as necessary. Some of these scripts require access to GMST values. We need to figure out how to implement this: either grab the value from the GMST when adding the script or adding a script function to read GMST values. The latter is easier but will not allow us to remove the redundant GMST records easily. -The currently existing Get, Set and Mod instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the stat ID as an argument. We need separate instructions to deal with base, modified and current values. +The currently existing `Get`, `Set`, and `Mod` instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the stat ID as an argument. We need separate instructions to deal with base, modified and current values. ## Attributes We will unify attributes and make them configurable. For stage 1 we will not allow the deletion of existing attributes (they are used in too many parts of the engine). But we will allow new attributes. -We add a new record type for attributes. Records for the existing attributes (sys::NameOfAttribute) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). +We add a new record type for attributes. Records for the existing attributes (`sys::NameOfAttribute`) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). -A attribute record contains the following information: +An attribute record contains the following information: * Name (taken from GMSTs for default attributes) -* Tooltip text (taken from GMSTs for default attributes) -* (optional) PrevID: ID of attribute the engine will try to sort in this attribute after (similarly to info records) -* (optional) NextID: ID of attribute the engine will try to sort in this attribute before (similarly to info records) -* Default value: If an actor does not have this attribute (possible if the attribute was defined in an addon and not in the base-game) this value is used for the attribute. +* Tooltip Text (taken from GMSTs for default attributes) +* (optional) PrevID: ID of the attribute the engine will try to subsequently sort this attribute in (similar to info records) +* (optional) NextID: ID of the attribute the engine will try to antecedently sort this attribute in (similar to info records) +* Default Value: Value that is used as the maximum value of the attribute if an actor does not have this attribute (possible if the attribute was defined in an add-on and not in the base game) -Note that all records that reference attributes need to have the respective fields be changed from integers to strings. +**Note**: All records that reference attributes need the respective fields to be changed from integers to strings. -The currently existing Get, Set and Mod instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the attribute ID as an argument. We need separate instructions to deal with base, modified and current values. +The currently existing `Get`, `Set`, and `Mod` instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the attribute ID as an argument. We need separate instructions to deal with base, modified and current values. -Additionally we will add a new GMST sys::ClassAttributes of type integer. This GMST specifies the number of favoured attributes that a class has. The value defaults to 2. +Additionally, we will add a new GMST `sys::ClassAttributes` of type `long`. This GMST specifies the number of favoured attributes that a class has. The value defaults to 2. ## Weapon Types We will unify weapon types and make them configurable. -We add a new record type for weapon types. Records for the existing weapon types (sys::NameOfType) will be created by the engine when loading older omwgame files. +We add a new record type for weapon types. Records for the existing weapon types (`sys::NameOfType`) will be created by the engine when loading older omwgame files. A weapon type record contains the following information: * Name (taken from GMSTs for default types) -* HandlingType (how the weapon is held and what animations to play when attacking) -* Name of HitTestScript (see Combat subsection) +* Handling Type: Defines how the weapon is held and what animations play when attacking +* Hit Test Script: Name of the hit test script used (see *Combat* subsection) + +For stage 1, the weapon type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However, there are already possible uses, e.g., a weapon type "Ancient Blades" which requires a separate skill. + +We add a new script function: -For stage 1 the weapon type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However there are already possible uses (for example a type of "ancient blades" that require a separate skill). +* `GetWeaponTypeId id`: Returns the weapon type ID of an instance or an object with the ID `id` -We add another script function that returns the weapon type ID of an instance or an object ID: GetWeapopnTypeId. Note that GetWeaponType is already taken by Tribunal and we can not repurpose this name without breaking compatibility. +**Note**: `GetWeaponType` is already taken by Tribunal and we can not repurpose this name without breaking compatibility. ## Armour Types -In vanilla Morrowind armour types exist only implicitly. There is no record and no option to select armour types. Armour types are determined by weight only. +In vanilla Morrowind, armour types exist only implicitly. There is no record and no option to select armour types. Armour types are determined by weight only. -We will keep implicit armour types as an option, but also add armour types explicitly as a new type of record. +We will keep implicit armour types as an option, but we'll also add armour types explicitly as a new type of record. -Records for the existing types (sys::LightArmorType, sys::MediumArmorType, sys::HeavyArmorType) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). +Records for the existing types (`sys::LightArmorType`, `sys::MediumArmorType`, `sys::HeavyArmorType`) will be created by the engine when loading older omwgame files (these records are mandatory for newer omwgame files). -A armour type record contains the following information: +An armour type record contains the following information: * Name (taken from GMSTs for default types) * (optional) Minimum weight -Additional armour object records are extended by the optional field "armor type". +Additionally, armour object records are extended by the optional field "Armour type". + +If an armour object has no armour type specified, its type is determined by its weight according to the following algorithm: + +1. Consider all armour types with a minimum weight field. +2. Exclude types that have a minimum weight larger than the weight of the object. +3. Pick the type with the largest minimum weight. -If an armour object does not have this field its type is determined by the weight by the following algorithm: +For stage 1, the armour type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However, there are already possible uses, e.g., imitating TES IV: Oblivion by eliminating medium armour or adding a new armour type such as "Ancient Armour" which requires a separate skill. -* Consider all armour types with a minimum weight field -* Exclude types that have a minimum weight larger than the weight of the object -* Pick the type with the largest minimum weight +We add another script function: -For stage 1 the armour type record is still very basic. Its purpose is mostly to allow better de-hardcoding of skills. We may expand on this in the future. However there are already possible uses (for example going Oblivion by killing medium armour or adding a new type of armour "ancient armor" that requires a separate skill). +* `GetArmorTypeId id`: Returns the armour type ID of an instance or an object with the ID `id` -We add another script function that returns the armour type ID of an instance or an object ID: GetArmorTypeId. +**Note**: For the sake of consistency, we've adopted the naming scheme introduced for weapon types. ## Specialisation We will unify specialisations and make them configurable. -We add a new record type for specialisations. Records for Combat (sys::CombatSpec), Stealth (sys::StealthSpec) and Magic (sys::MagicSpec) will be created by the engine when loading older omwgame files. +We add a new record type for specialisations. Records for Combat (`sys::CombatSpec`), Stealth (`sys::StealthSpec`), and Magic (`sys::MagicSpec`) will be created by the engine when loading older omwgame files. A specialisation record contains the following information: * Name (taken from GMSTs for default types) -We add a new script instruction GetSpecialization id, that returns the specialisation of a class or a skill with the given ID. +We add a new script instruction: + +* `GetSpecialization id`: Returns the specialisation of a class or a skill with the given ID `id` ## Magic Schools We will unify magic schools and make them configurable. -We add a new record type for magic schools. Records for the existing magic schools will be created by the engine when loading older omwgame files. +We add a new record type for magic schools. Records for the existing magic schools (`sys::AlterationMagicSchool`, `sys::ConjurationMagicSchool`, etc.) will be created by the engine when loading older omwgame files. A magic school record contains the following information: * Name (taken from GMSTs for default schools) -* Specialisation (sys::MagicSpec for default schools) -* Resource (sys::Magicka for default schools) +* Specialisation (`sys::MagicSpec` for default schools) +* Resource (`sys::Magicka` for default schools) -A use for the specialisation field would be (for example) dividing magic up into two categories "divine magic" and "arcane magic" and have characters specialise accordingly. +A use for the specialisation field would be, e.g., dividing magic into the two categories "Divine Magic" and "Arcane Magic" and having characters specialise accordingly. ## Skills -Skills are a complex topic and we won't achieve full de-hardcoding in stage 1 and most like there will never be a 100% complete de-hardcoding. But there is still a lot we can do with skills. +Skills are a complex topic and we won't achieve full de-hardcoding in stage 1 - and most likely there will never be a 100% complete de-hardcoding. But there is still a lot we can do with skills. -Currently skills exist as indexed records. We need to move these over to ID-based records. The exiting indices are translated to IDs of the form sys::NameOfSkill. +Currently, skills exist as indexed records. We need to move these over to ID-based records. The exiting indices are translated to IDs of the form `sys::NameOfSkill`. -We add a new sub-record type (Function Sub-Record, see below). Each skill can have zero, one or multiple function sub-records. +We add a new subrecord type (see subsection *Function Subrecords* below). Each skill can have zero, one, or multiple function subrecords. The following skills are too tightly bound into the engine to allow their easy removal: -* Armorer -* Enchanting -* Alchemy * Acrobatics -* Block -* Sneak +* Alchemy +* Armorer * Athletics +* Block +* Enchant * Hand-to-hand -* Unarmored * Mercantile +* Sneak * Speechcraft +* Unarmored -Therefore we will (for now) forbid the deletion of the respective skill records. All other default skills can be deleted. +Therefore, we will (for now) forbid the deletion of the respective skill records. All other default skills can be deleted. -### Scripts Instructions +### Script Instructions -The currently existing Get, Set and Mod instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the skill ID as an argument. We need separate instructions to deal with base and modified values. +The currently existing `Get`, `Set`, and `Mod` instructions are flagged as deprecated and replaced by a single instruction for each operation that takes the skill ID as an argument. We need separate instructions to deal with base and modified values. We also introduce new script instructions: -* UseSkill id, v: Progress skill id by a value of v (float). -* UseValueSkill Id, i: Progress skill id by the value given by use value field with index i (0, 1, 2 or 3) -* Get{x}SkillLevel s_id: Return the skill level of a function sub-record with TargetId s_id (string). {x} is either Armor, Weapon or Magic. See next sub-section for details. +* `UseSkill id, v`: Progresses the skill `id` by a value of `v` (`float`). +* `UseValueSkill Id, i`: Progresses the skill `id` by the value given by the use-value field with index `i` (0, 1, 2 or 3) +* `Get{x}SkillLevel s_id`: Returns the skill level of a function subrecord with `TargetId` `s_id` (`String`). `{x}` is either "Armor", "Weapon", or "Magic"; see the next subsection for details. -Note that we do not de-hardcode the fixed number of use value fields. The usefulness of these fields is most likely limited for new skills. The UseSkill instruction, which bypasses the use value fields should be sufficient in most cases. We only add the UseValueSkill instructions to better support script interactions with default skills. +**Note**: We do not de-hardcode the fixed number of use-value fields. The usefulness of these fields for new skills is most likely limited. The `UseSkill` instruction, which bypasses the use-value fields, should be sufficient in most cases. We only add the `UseValueSkill` instructions to better support script interactions with default skills. -### Function Sub-Records +### Function Subrecords -A function sub-record describes how a skill interacts with hardcoded engine functions. A function record consists of a function ID (we use an index here, because the list of functions will definitely not be extensible by content files) and additional arguments. +A function subrecord describes how a skill interacts with hardcoded engine functions. A function record consists of a function ID (we use an index here, because the list of functions will definitely not be extensible by content files) and additional arguments. -For stage one we require only one additional argument: +For stage 1, we require only one additional argument: -* TargetId (single string value) +* `TargetId` (single `String` value) -For stage 1 we introduce three function IDs: Armour skill, weapon skill, magic skill. +Stage 1 will introduce three function IDs: -We do not forbid non-unique function sub-records; meaning two skills may have identical function sub-records (e.g. two skills governing light armor). If there is more than one skill with the same function sub-record and the skill level for this function needs to be considered, we use the maximum over all relevant skill levels. +* Armour Skill +* Weapon Skill +* Magic Skill + +We do not forbid non-unique function subrecords, i.e., two skills may have identical function subrecords, e.g., two skills governing light armour. If there is more than one skill with the same function subrecord and the skill level for this function needs to be considered, we use the maximum of all relevant skill levels. ### Armour Skills -We make armour skills fully customisable by introducing a function ID for armour types. Any skill that has a function sub-record with this ID is an armour skill. For default armour skills loaded from older omwgame files we will inject function sub-records during the loading process. +We make armour skills fully customisable by introducing a function ID for armour types. Any skill that has a function subrecord with this ID is an armour skill. For default armour skills loaded from older omwgame files, we will inject function subrecords during the loading process. + +The `TargetId` of an armour function subrecord indicates the armour type that the skill is governing. -The TargetId of a armour function sub-record indicates the armour type that the skill is governing. +The default skills are: -The default skills are: Heavy Armor, Medium Armor, Light Armor. +* "Heavy Armor" +* "Light Armor" +* "Medium Armor" ### Weapon Skills -We make weapon skills fully customisable by introducing a function ID for weapon types. Any skill that has a function sub-record with this ID is a weapon skill. For default weapon skills loaded from older omwgame files we will inject function sub-records during the loading process. +We make weapon skills fully customisable by introducing a function ID for weapon types. Any skill that has a function subrecord with this ID is a weapon skill. For default weapon skills loaded from older omwgame files, we will inject function subrecords during the loading process. + +The `TargetId` of a weapon function subrecord indicates the weapon type that the skill is governing. -The TargetId of a weapon function sub-record indicates the weapon type that the skill is governing. +The default skills are: -The default skills are: Spear, Axe, Blunt Weapon, Long Blade, Marksman, Short Blade +* "Axe" +* "Blunt Weapon" +* "Long Blade" +* "Marksman" +* "Short Blade" +* "Spear" ### Magic Skills -We make magic skills fully customisable by introducing a function ID for magic schools. Any skill that has a function sub-record with this ID is a magic skill. For default magic skills loaded from older omwgame files we will inject function sub-records during the loading process. +We make magic skills fully customisable by introducing a function ID for magic schools. Any skill that has a function subrecord with this ID is a magic skill. For default magic skills loaded from older omwgame files, we will inject function subrecords during the loading process. -The TargetId of a magic school function sub-record indicates the magic school that the skill is governing. +The `TargetId` of a magic school function subrecord indicates the magic school that the skill is governing. -The default skills are: Illusion, Conjuration, Alteration, Destruction, Mysticism, Restoration +The default skills are: + +* "Alteration" +* "Conjuration" +* "Destruction" +* "Illusion" +* "Mysticism" +* "Restoration" ## Weather -We make weather types customisable by moving from hard-coded index based weather effects to ID based weather types stored in content files. To this end we introduce a new record type (Weather). When loading older omwgame files we will inject weather records for the ten default weather types. +We make weather types customisable by moving from hardcoded, index-based weather effects to ID-based weather types stored in content files. To achieve this, we introduce a new record type ("Weather"). When loading older omwgame files, we will inject weather records for the ten default weather types (`sys::WeatherAsh`, `sys::WeatherBlight`, etc.). -A weather record is made up from sub-records, each describing a weather effect. A weather record can have any number of weather effect sub-records. +A weather record is made up of subrecords, each describing a weather effect. A weather record can have any number of weather effect subrecords. -There are three types of effect sub-records: +There are four types of weather effect subrecords: -* Magic effect: A magic effect that is applied to all actors within active cells while the respective weather type is active. -* Sky: Modifications to the visuals of the sky (e.g clouds) -* Particles: Particle effects (e.g. rain) -* Event: Events that can happen with a certain probability while the weather type is active (e.g. lightning). Events happen at random locations. +* "Magic Effect": A magic effect that is applied to all actors within active cells while the respective weather type is active +* "Sky": Modifications to the visuals of the sky (e.g, clouds) +* "Particles": Particle effects (e.g., rain) +* "Event": Events that can happen with a certain probability while the weather type is active (e.g., lightning); events happen at random locations -Magic effect sub-records contain the ID of the respective magic effect. All other sub-records contain an integer ID specifying the effect and (if necessary) additional parameters. +Magic effect subrecords contain the ID of the respective magic effect. All other subrecords contain an integer ID specifying the effect and (if necessary) additional parameters. -All effects described in these sub-records affect active cells only. +All effects described in these subrecords affect active cells only. ### Sky -TODO all existing sky effects and possibly new ones; bugger scrawl to fill in the details +TODO: +- Should cover all existing sky effects and possibly new ones ### Particles -TODO all existing particle effects and possibly new ones; bugger scrawl to fill in the details +TODO: +- Should cover all existing particle effects and possibly new ones ### Event We will add the following event IDs: -* 0: Scripted Event: Additional data consists of the name of the script to be run when the event is triggered. The script takes the following arguments: worldspace (string), location (Vector3), weather ID (string) -* 1: Lightning - -In addition to the event ID and event ID specific data event sub-records have an event trigger block. +* "0": Scripted event; additional data consists of the name of the script to be run when the event is triggered; the script takes the following arguments: worldspace (`String`), location (`Vector3`), weather ID (`String`) +* "1": Lightning -### Event, Trigger +In addition to the event ID and event-ID-specific data, event subrecords have an "Event Trigger" block: -* Chance: The chance of the event happening (floating number in the range between 0 and 1) per unit of time. We may want to make the unit of time globally configurable with a new GMST. -* Min-Distance (optional): The minimum distance from the player at which the effect can take place (0 by default). -* Max-Distance (optional): The maximum distance from the player at which the effect can take place (infinite by default): +* Chance: Floating number in the range between 0 and 1; the chance per unit of time that the event happens (we may want to make the unit of time globally configurable with a new GMST) +* (optional) Min-Distance: The minimum distance from the player at which the effect can take place (0 by default) +* (optional) Max-Distance: The maximum distance from the player at which the effect can take place (infinite by default) ### Scripts -We need to introduce new script instructions regarding to weather, since the existing instructions are based on fixed number weather types or integer indexed weather types. These instructions can not be salvaged and we therefore declare them deprecated. +We need to introduce new script instructions regarding the weather, since the existing instructions are based on a fixed number of weather types or integer-indexed weather types. These instructions can not be salvaged and we therefore declare them deprecated. New script instructions: -* SetWeather (region ID, weather ID): Replaces change weather. Set current weather for a region. -* GetWeather (region ID): Replaces GetCurrentWeather. Returns current weather for a region. -* SetRegionWeather (region ID, weather ID, chance): Replaces ModRegion. Modify a single entry in the weather table of a region. Chance is of type long. We relax the requirement that the sum of the chance values need to be 100. -* UpdateRegionWeather (region ID): Forces a new roll on the weather table of a region. +* `SetWeather(regionId, weatherId)`: Sets the current weather for the region with the ID `regionId` to `weatherId` (replaces `ChangeWeather`) +* `GetWeather(regionId)`: Returns the current weather ID for the region with the ID `regionId` (replaces `GetCurrentWeather`) +* `SetRegionWeather(regionId, weatherId, chance)`: Modifies the entry `weatherId` in the weather table of the region with the ID `regionId`; `chance` is of type `long`; we relax the requirement that the sum of the chance values needs to be 100 (replaces `ModRegion`) +* `UpdateRegionWeather(regionId)`: Forces a new roll on the weather table of the region with the ID `regionId` ## Water -We currently have only a single water type that is hard-coded. We name this water type sys::Water and inject a suitable record of the new type (see below) during loading of older omwgame files. +We currently have but a single water type that is hardcoded. We name this water type `sys::Water` and inject a suitable record of the new type (see below) during loading of older omwgame files. -We generalise the concept of water to liquid by introducing a new record type (Liquid). Liquid records can be used both for different looking water type (more swampy water, water with different colouration) but also for completely different liquid types (e.g. Lava). +We generalise the concept of water to the concept of liquid by introducing a new record type ("Liquid"). Liquid records can be used both for different-looking water types (e.g., more swampy water, water with different colouration) but also for completely different liquid types (e.g. lava). -To support the water type sys::water we also need to add a new magic effect (sys::SuffocationEffect). +**Note**: Morrowind only uses one liquid type, which we refer to as "water". (While lava does exist in the game, it is handled with activators, rather than as a liquid type) -Liquid records are referenced both in worldspace records (see Cells & Worldspaces section) and in body of liquid records (see Misc section). +To support the water type `sys::Water`, we also need to add a new magic effect (`sys::SuffocationEffect`). + +Liquid records are referenced both in worldspace records (see *Cells, Worldspaces & Areas* section) and in the body of liquid records (see *Misc* section). A liquid record consists of the following fields: -* Effects: IDs of zero or more magic effects that applied to actors while in the liquid. -* Submerged Effects: IDs of zero or more magic effects that are applied to actors while completely submerged in the liquid. -* Liquid type ID: Integer, hard-coded -* Additional parameter, specific to liquid type +* Effects: IDs of zero or more magic effects which are applied to actors while in the liquid +* Submerged Effects: IDs of zero or more magic effects that are applied to actors while completely submerged in the liquid +* Liquid Type ID: `long`, hardcoded +* Additional parameters, specific to liquid type We need to support at least one liquid type (ID 0: Water) from the start. Other types can be added. -TODO check with scrawl about other ways to handle visuals for liquid types that requires less hard-coding. Shaders? +TODO: +- Are there other ways to handle visuals for liquid types that require less hardcoding? +- Use of shaders? ## Magic Effects -Currently we have a fixed number of magic effects that are referenced by an integer index. - -We move over the magic effect records to a string ID based scheme. +Currently, we have a fixed number of magic effects that are referenced by an integer index. -We also introduce a new effect (sys::SuffocationEffect) by injecting a record when loading from older omwgame files. This effect triggers the normal procedure for running out of air. Note that this will require creating a new effect icon. +We move over to effect records with a string-ID-based scheme. -Effect records can be deleted without restrictions. A content developer can add new effect records, again without restrictions. +We also introduce a new effect (`sys::SuffocationEffect`) by injecting a record when loading from older omwgame files. This effect triggers the normal procedure for running out of air. -We add a field to the effect record that contains the effect's name, currently stored in a GMST. +**Note**: This will require creating a new effect icon, which will be stored internally along with other resources that are part of the game engine. -We also add an optional field that contains the ID of a script function that is called when the magic effect takes effect. The function takes two arguments: +Effect records can be deleted without restrictions. A content developer can add new effect records - again without restrictions. -* The reference of the effects source. This would be a spellcaster in case of a spell or an item in case of an enchantment. -* A list of target references. +An effect record consists of the following fields: -Furthermore we add a second optional field that contains the ID of a script function that is called when the magic effect ends (only relevant for non-instant effects). The function signature is the same. +* Name (currently stored in a GMST) +* (optional) Effect Function: Contains the ID of a script function that is called when the magic effect takes effect; the function takes two arguments: the reference of the effects source (this would be a spellcaster in case of a spell or an item in case of an enchantment) and a list of target references +* (optional) Wear-Off Function: Contains the ID of a script function that is called when the magic effect ends (only relevant for non-instant effects); the function takes the same two arguments as the "Effect Function" field -For the existing effects we will inject scripts that match the previously hard-coded effects when loading from an older omwgame file. This will require the addition of a significant number of new script instructions that are all trivial, since they will just call existing engine functions. +For the existing effects, we will inject scripts that match the previously hardcoded effects when loading from an older omwgame file. This will require the addition of a significant number of new script instructions that are all trivial, since they will just call existing engine functions. -It is important to generalise as much as possible when creating these new script functions. For example we won't have a DivineIntervention script instruction. Instead we add a GetClosestMarker function and use existing functions for querying position and cell and then use the new Teleport instruction. +It is important to generalise as much as possible when creating these new script functions, e.g., we won't have a `DivineIntervention` script instruction. Instead we add a `GetClosestMarker` function, use existing functions for querying position and cell and, then, use the new `Teleport` instruction. ## Input We allow the addition of customisable input functions that can be bound to keyboard buttons in the usual way and can trigger the execution of scripts. Existing input bindings are not affected by this. -To this end we introduce a new record type (Input): +To this end, we introduce a new record type ("Input"): * Label -* Tooltip (string, optional) -* Default key (integer) -* Key down event script (string, optional): A script that is executed when the key is pressed. -* Key up event script (string, optional): A script that is executed when the key is released. -* Key pressed event script (string, optional): A script that is executed periodically while the key is down. -* Key pressed delta (float): Minimum time between two executions of the key pressed event script. Defaults to a reasonable value in the absence of this field. +* (optional) Tooltip: `String` value +* Default Key: `long` value +* (optional) Key-Press Event Script: Contains the ID of a script that is executed when the key is pressed +* (optional) Key-Release Event Script: Contains the ID of a script that is executed when the key is released +* (optional) Key-Hold Event Script: Contains the ID of a script that is executed when the key is pressed and held +* Key-Hold Delta: `float` value describing the minimum time between two executions of the "Key-Hold Event Script"; defaults to a reasonable value ## Held Items -We merge the probe and lockpick object types into a new object type: Held Item. The security skill related functions are handled via the ItemObjectInteraction script subrecord. When loading older omwgame files a suitable script in the sys namespace is injected for probing and lockpicking each. When transforming probe and lockpick records into Held Item records a ItemObjectInteraction subrecord referencing the respective script is injected. +We merge the probe and lockpick object types into a new object type: "Held Item". The Security-skill-related functions are handled via the `ItemObjectInteraction` script subrecord. When loading older omwgame files, a suitable script in the `sys` namespace is injected for probing and lock picking each. When transforming probe and lockpick records into held-item records, an `ItemObjectInteraction` subrecord referencing the respective script is injected. -If we enhance the animation system we may need to add additional subrecords that specify the pose for holding the item or the animation for using it. +**Note**: If we enhance the animation system, we may need to add additional subrecords that specify the pose for holding the item or the animation for using it. ## Pickpocketing -We add a new GMST (sys::ReversePickpocketing) of type integer that indicates if reverse pickpocketing is allowed (value other than 0) or not (value 0). +We add a new GMST (`sys::ReversePickpocketing`) of type `long` that indicates if reverse pickpocketing is allowed (value other than 0) or not (value 0). -We move the function for checking if pickpocketing succeeded from the C++ code to a new script function (sys::PickpocketTest). We inject this function when loading older omwgame files. +We move the function for checking if pickpocketing succeeded from the C++ code to a new script function (`sys::PickpocketTest`). We inject this function when loading older omwgame files. The function takes the following arguments: -* ref to thief actor -* ref to victim actor -* ref to item to be stolen -* reverse flag: integer, 0 for regular pickpocketing, 1 for reverse +* thiefId: Reference to the thief (actor) +* victimId: Reference to the victim (actor) +* stolenItemId: Reference to the item to be stolen +* reversePickpocket: `long` flag; set to 0 for regular pickpocketing, set to 1 for reverse pickpocketing The function returns 1 if the pickpocket attempt succeeded and 0 if it failed. -Items with the nopick tag are excluded from pickpocketing. +Items with the `noPick` tag are excluded from pickpocketing. ## Combat -De-hardcoding combat is a monumental task that we can not hope to complete within the time frame of stage 1. We will de-hardcode some parts that do not depend on large scale improvements to animation and can be handled with the scripting improvements in stage 1. Any further enhancements are left for stage 2 or more likely OpenMW 2.0. +De-hardcoding combat is a monumental task that we can not hope to complete within the time frame of stage 1. We will de-hardcode some parts that do not depend on large-scale improvements to animation and can be handled with the scripting improvements in stage 1. Any further enhancements are left for stage 2 or, more likely, OpenMW 2.0. ### Hits -We move the hit test check for C++ code to script functions. The following functions will be used: +We move the hit test check from C++ code to script functions. The following functions will be used: -* For melee weapons the HitTest script function defined in the respective WeaponType record; arguments: ref to weapon, ref to target -* For ranged weapons the HitTest script function defined in the respective WeaponType record; arguments: ref to weapon, ref to target, ref to ammunition -* For unarmed attacks the function sys::UnarmedHitTest; arguments: ref to weapon, ref to target +* `(weaponId, targetId)`: Function for melee weapons defined in the corresponding WeaponType record; `weaponId` refers to the weapon used, `targetId` refers to the ID of the target +* `(weaponId, targetId, ammunitionId)`: Function for ramged weapons defined in the corresponding WeaponType record; `weaponId` refers to the weapon used and `targetId` refers to the ID of the target, and `ammunitionId` refers to the ammunition used by the ranged weapon +* `sys::UnarmedHitTest(weaponId, targetId)`: Default function for unarmed attacks; `weaponId` refers to the weapon used and `targetId` refers to the ID of the target -All functions return an integer: 0 (no hit), a value other than 0 (hit) +All functions return 0 if the attack misses and a value other than 0 on a successful hit. -sys::UnarmedHitTest and default functions for armed hit tests (sys::MeleeHitTest, sys::RangedHitTest) are injected when loading older omwgame files. +`sys::UnarmedHitTest` and default functions for armed-hit tests (`sys::MeleeHitTest` and `sys::RangedHitTest`) are injected when loading older omwgame files. ### Script Enhancements We add a new script function: -* GetTargetRef: Returns the current target of an actor. Returns a NullRef if the actor is not in combat. Also returns a NullRef when used on an instance that is not an actor. +* `GetTargetRef`: Returns the current target of an actor; returns a null reference if the actor is not in combat or if it is used on an instance that is not an actor We add two new script instructions: -* DeclareWar l: Similar to StartCombat, but specifies a whole group of enemies. l is a list of references. -* DeclarePeace l: Ends the hostilities resulting from previous combat towards the actors listed in the reference list l. We may need to add a grace period after this instruction during attacks do not cause hostility again, so we can handle non-instant attacks. +* `DeclareWar l`: Similar to `StartCombat`, but specifies a whole group of enemies; `l` is a list of references +* `DeclarePeace l`: Ends the hostilities resulting from previous combat towards the actors listed in the reference list `l`. **Note**: We may need to add a grace period after this instruction during which attacks do not cause hostility again, so we can handle non-instant attacks. ### Script Hooks -We add three new script hooks. - -sys::Kill is executed whenever an actor is killed. Scripts for this hook take the following arguments: - -* Ref to target -* Ref to actor who killed the target (may be a null ref) +We add three new script hooks: -sys::CombatStarted and sys::CombatEnded are executed when the player enters or exits combat, respectively. +* `sys::Kill`: Executed whenever an actor is killed; scripts for this hook take the following two arguments: reference to the target and reference to the actor who killed the target (may be a null reference) +* `sys::CombatStarted`: Executed when the player enters combat +* `sys::CombatEnded`: Executed when the player exits combat ## Music @@ -1324,12 +1356,12 @@ The playlist record has the following fields: The titles on the playlist are all the titles in the music directory and the titles listed explicitly. -When playing a playlist first the probability is considered to randomly decide if a track is played or not. Then a track from the list is chosen. +When playing a playlist, first the probability is considered to randomly decide if a track is played or not. Then a track from the list is chosen. If a playlist is played in loop mode the process above is repeated whenever: * The current track ends -* If no track is playing at reasonably chosen time intervals +* No track is playing at reasonably chosen time intervals ### Background and Event Music @@ -1374,11 +1406,11 @@ We add two playlists (sys::ExplorePlaylist and sys::CombatPlaylist) which are po We add a GMST (sys::DefaultBackgroundMusic) with a default value of "sys::ExplorePlaylist". Whenever starting a new game or loading a game with no background music running the playlist specified in this GMST is played as background music. If the GMST holds an empty string no background music is played by default. -We add a script to the sys::CombatStarted and sys::CombatEnded hook each. The former plays sys::CombatPlaylist looped and forced. The later stops event music. +We add a script to the sys::CombatStarted and sys::CombatEnded hook each. The former plays sys::CombatPlaylist looped and forced. The latter stops event music. ### Location-Based Background Music -We add an optional string sub-record to the records listed below. This field contains a single track or a playlist, that determine the background music for the respective location. +We add an optional string sub-record to the records listed below. This field contains a single track or a playlist that determine the background music for the respective location. * Worldspace * Region @@ -1397,8 +1429,8 @@ The process of character creation currently allows for very little customisation Currently the character creation process runs through these steps: -1. Choose Name -2. Choose Race and appearance +1. Choose name +2. Choose race and appearance 3. Choose a method for class selection and perform class selection (3 methods) 4. Choose birthsign 5. Review @@ -1427,7 +1459,7 @@ We add several new script instructions. * CharacterCreationBack: Go back to previous step * CharacterCreationNext: Go to next step (ignored if the next step had not been accessed before) -n is an integer argument with one of several hard-coded integer values: +n is an integer argument with one of several hardcoded integer values: * 0: Name GUI * 1: Race GUI @@ -1458,11 +1490,11 @@ Likewise the character selection method of answering a set of quests can be impl The vanilla (character-)level-up system allows almost no modification. We keep the existing hard-coded system, but make it more configurable, and we also allow content developers to completely replace the existing system. -### State Un-Hiding +### State Unveiling Checks for a level up are based on the skill progression since the last level up. In vanilla this state is not accessible to content developers or players. -We address this issue by turning these hidden variables into record variables attached to the player character. The variables have the type long, a name matching the ID of the skill and are created on the fly whenever the engine detects a skill level up. Note that regular script instructions that change skill level do not count towards level ups. +We address this issue by turning these hidden variables into record variables attached to the player character. The variables have the type long and a name matching the ID of the skill, and are created on the fly whenever the engine detects a skill level up. Note that regular script instructions that change skill level do not count towards level ups. We also add another record variable (LevelUpCount) that counts the number of major and minor skill level-ups. @@ -1476,7 +1508,7 @@ Some NPCs are flagged as skill trainers. These trainers can train the player in * New script (sys::SkillTrainingTest): Tests if NPC can train player in a skill (returns 0 or 1). This test is performed after the faction standing test and after the skill list is trimmed down via sys::SkillTrainCount. The function takes the following arguments: ref to player, ref to NPC, ID of skill * New script (sys::SkillTrainingApplied): Executed after training has been completed. Arguments are the same as with sys::SkillTrainingTest. -When loading from an old omwgame file GMST and scripts are injected. sys::SkillTrainCount defaults to 3, sys::SkillTrainingTest performing the usual tests on skill level and attributes and sys::SkillTrainingApplied defaults to an empty script. +When loading from an old omwgame file, GMST and scripts are injected. sys::SkillTrainCount defaults to 3, sys::SkillTrainingTest performs the usual tests on skill level and attributes and sys::SkillTrainingApplied defaults to an empty script. ### Level-Up Test @@ -1511,7 +1543,7 @@ The default implementation (injected when loading from an older omwgame file) re ### Level-Up Image -The level up dialogue contains an image that depends on how the skill increase is distributed among specialisations. The function that decides about this image is very simple. However since specialisations won't be hard-coded anymore we can not simply pass these values into the function without first establishing more involved script support than planned for stage 1. +The level up dialogue contains an image that depends on how the skill increase is distributed among specialisations. The function that decides about this image is very simple. However, since specialisations won't be hardcoded any more, we can not simply pass these values into the function without first establishing more involved script support than planned for stage 1. This is actually not a problem since we can simply track the increase values in record variables, which may also prove useful for other purposes. @@ -1545,13 +1577,13 @@ This process is complicated by the existence of levelled item lists, which makes We will make several changes to increase the flexibility of item restocking. -First we move the re-fill operation from after a trade is completed to before a trade is completed. This should not affect existing content. +First we move the refill operation from after a trade is completed to before a trade is completed. This should not affect existing content. -We add three more scripts (optional, specified via new string fields in the actor's record) that hook into the re-fill process. +We add three more scripts (optional, specified via new string fields in the actor's record) that hook into the refill process. -1. Check if a re-fill should be performed; called at the beginning of every trade cycle. Arguments: ref to actor, integer (1 if first trade since talking to the merchant, 0 otherwise). Returns an integer (0 no re-fill required, otherwise re-fill is required). If script is not present a re-fill is performed). -2. Called for every item that the re-fill algorithm flags for removal. Arguments: ref to actor, ref to item. Returns an integer (0 if item should be kept, otherwise item is removed). If script is not present the removal takes place. -3. Called at the end of re-fill procedure. This gives content authors the opportunity to make other modifications. Arguments: ref to actor. No return value. +1. Check if a refill should be performed; called at the beginning of every trade cycle. Arguments: ref to actor, integer (1 if first trade since talking to the merchant, 0 otherwise). Returns an integer (0 no refill required, otherwise refill is required). If this script is not present, a refill is performed). +2. Called for every item that the refill algorithm flags for removal. Arguments: ref to actor, ref to item. Returns an integer (0 if item should be kept, otherwise item is removed). If this script is not present, the removal takes place. +3. Called at the end of refill procedure. This gives content authors the opportunity to make other modifications. Arguments: ref to actor. No return value. ## Fast-Travel @@ -1664,23 +1696,23 @@ NPC schedules are an advanced topic and as such would be a better fit for stage We run schedules only in active cells. But we also need to consider passive cells. -Here are a few examples. For simplicity lets consider a one-dimensional cell grid with the usual pattern of one cell around of the player cell being active. +Here are a few examples. For simplicity let's consider a one-dimensional cell grid with the usual pattern of one cell around of the player cell being active. For this example let the player be in cell 0. Now consider NPC A, who is in cell 2 (not active). He has a schedule that takes him to cell 1 sometimes. As a player we would expect to see him from the distance then. Another example with the same setup as above: While A is moving within cell 2 towards cell 1, the player relocates to cell 2. He would to see A make his way towards cell 1. Moreover if the player moves back to cell 0 and then returns to cell 2, he would expect to see A having made some progress. -Conclusion: We must track all actors who's schedule can take them into active cells. +Conclusion: We must track all actors whose schedules can take them into active cells. -It is not clear yet how to achieve this goal best. In the most simple implementation we could just estimate the time it takes to perform a task in a schedule and spawn the actor into a active cells accordingly. This would however not cover the second example. +It is not clear yet how to achieve this goal best. In the most simple implementation we could just estimate the time it takes to perform a task in a schedule and spawn the actor into active cells accordingly. This would however not cover the second example. -Alternatively we could invent a semi-active cell state and apply it to all cells that contain NPCs who's schedule can touch the active cells. This semi-active state would not run any scripts, nor would it render or run animations. And it may use a simplified/limited form of physics. This approach would cover all cases. However it has the potential for a severe performance impact. +Alternatively we could invent a semi-active cell state and apply it to all cells that contain NPCs whose schedules can touch the active cells. This semi-active state would not run any scripts, nor would it render or run animations, and it may use a simplified/limited form of physics. This approach would cover all cases. However, it has the potential for a severe performance impact. -Again alternatively, we may offer both implementation and switch between them base on settings/available processing power. +Again alternatively, we may offer both implementations and switch between them base on settings/available processing power. ### Actor Tracking -Independently from the chosen implementation we need to track all actors who's schedules are relevant to the active cells. This is a problem because with the default data structures we would at least need to scan every single cell on startup for relevant actors. The performance impact of such an approach is most likely not acceptable. +Independently from the chosen implementation, we need to track all actors whose schedules are relevant to the active cells. This is a problem because with the default data structures we would at least need to scan every single cell on start-up for relevant actors. The performance impact of such an approach is most likely not acceptable. Therefore information about which actor's schedule is relevant to which cell needs to be stored in a separate data structure that exists outside of the cell records. @@ -1696,9 +1728,9 @@ Each task sub-record consists of: The time table of a schedule is allowed to have holes (e.g schedule 1 ending at 10:00 and schedule 2 beginning at 10:15). -Task may also overlap partially, but no two task may start at the same time. +Tasks may also overlap partially, but no two tasks may start at the same time. -When picking a task from the schedule all task which contain the current time are considered. Of these the task with the earliest start time is chosen. +When picking a task from the schedule, all tasks which contain the current time are considered. Of these the task with the earliest start time is chosen. A new task is picked only when the current one has expired. @@ -1706,7 +1738,7 @@ A new task is picked only when the current one has expired. Schedules can be assigned to actors either in the actor record or with a new script instruction. -When explicit switching from one schedule to another, tasks with the ignore flag are ignored. This feature is intended for transitory tasks, that are pointless if the previous task hasn't been performed. +When explicitly switching from one schedule to another, tasks with the ignore flag are ignored. This feature is intended for transitory tasks, that are pointless if the previous task hasn't been performed. We add script instructions that allow for querying the current schedule of an actor. We also add a script instruction to prematurely end a task. @@ -1720,14 +1752,14 @@ This is an early draft of the new system and will almost certainly require more Idle activities are currently bound to the Wander package. This is suboptimal, both because other packages could benefit from it and because storing the idle function in a package instead of the NPC makes it hard to give the NPC a characteristic idle pattern. -We add a list of idle chances for each available idle animation to the NPC record. Ideally this list should be extensible, but first we need to improve the animation system so that the number of idle animations isn't hard-coded anymore. This may be a stage 2 task. +We add a list of idle chances for each available idle animation to the NPC record. Ideally this list should be extensible, but first we need to improve the animation system so that the number of idle animations isn't hard-coded any more. This may be a stage 2 task. Each package contains two fields related to idling behaviour: * Chance: A value between 0 and 1 that specifies the chance (per reasonable unit of time) of an idle animation to be executed. * Dialogue Chance: A value between 0 and 1 that specifies the chance of an idle dialogue if an idle animation is executed. -We may decide to use a scale of 0 to 100 instead of 0 to 1 to increase usability for less mathematical minded content developers. +We may decide to use a scale of 0 to 100 instead of 0 to 1 to increase usability for less mathematically minded content developers. The old wander package keeps its old idle list. When active it disables the new idle system. @@ -1737,7 +1769,7 @@ If a NPC does not have any active packages the idle function is still active. In We categorise package into four roles: -* Legacy: Old packages, generated either via script instructions or via AI package list in actor record. Removed once it runs out (if not flagged for repeat). Deprecated. +* Legacy: Old packages, generated either via script instructions or via AI package list in the actor record. Removed once they run out (if not flagged for repeat). Deprecated. * Schedule: Inserted by the schedule system. Can not be directly manipulated via script instructions. * Manual: New packages, generated via new script instructions. * Situation: Inserted via other game mechanics (e.g. combat, breath). Can not be directly manipulated via script instructions. @@ -1750,7 +1782,7 @@ We add new script instructions to query the package state of an actor and to ins Each package has two fields that describe its duration: -* Duration itself: Package will self-delete once the current time is larger than the start time plus the duration) +* Duration itself: Package will self-delete once the current time is larger than the start time plus the duration * Repeat flag: Package adds itself again at the end of the end of the duration Packages lacking these fields persist indefinitely unless explicitly removed. @@ -1761,7 +1793,7 @@ Some legacy packages already have these fields. Schedule packages will not have We translate the packages AiFollow, AiActivate and AiTravel into the new format. -AiFollow and AiTravel specify a location by given a set of coordinates and an optional cell ID. We will replace part of the package with a different format. +AiFollow and AiTravel specify a location by being given a set of coordinates and an optional cell ID. We will replace part of the package with a different format. There are two possible locations sub-records of which each package may only contain one: @@ -1790,7 +1822,7 @@ This is a new package with no direct equivalent in the old system. The package c * A list of locations (in the new format) * A loop flag -The actor will walk from one location to the next. Once he reached the last location he will either continue with the first location (if the loop flag is set) or walk down the list of locations in opposite direction and then start over (if the loop flag is not set). +The actor will walk from one location to the next. Once he has reached the last location he will either continue with the first location (if the loop flag is set) or walk down the list of locations in opposite direction and then start over (if the loop flag is not set). The obvious use case for this package is a guard patrolling a perimeter or a place. @@ -1819,7 +1851,7 @@ This results in multiple issues: * When localising the developer must hunt down all references to the localised strings and change them manually (cell names and topics). * When updating a localisation to a new version of the content file the translator has to manually pierce together his old translation and the new content file. -* Changing a string that is used as an internal reference breaks the connection to the referenced records in relation to the unlocalised file, which in turn makes it impossible to utilise automated tools that help for the localisation process. +* Changing a string that is used as an internal reference breaks the connection to the referenced records in relation to the non-localised file, which in turn makes it impossible to utilise automated tools that help for the localisation process. * Content files of mismatching languages generally can't be combined in a content file list. Mixing languages is generally not desirable for playing, but may sometimes be unavoidable. The ability to mix languages is highly desirable for developers (e.g. debugging). ## Encoding @@ -1832,7 +1864,7 @@ Localisation will be contained in separate content files (omwlang) that are to b The launcher will treat a content file and its localisation file as a single entity (see section about Identifying Meta-Data). -We may add a launcher language setting the determines the default language chosen by the launcher. This needs to be a separate option from the legacy encoding setting, which is currently also called language setting. +We may add a launcher language setting that determines the default language chosen by the launcher. This needs to be a separate option from the legacy encoding setting, which is currently also called language setting. ## Localisation Records @@ -1866,7 +1898,7 @@ fallback-lp-somekey=Some Text When looking up fallback strings the preferred language is chosen by the default language setting in the launcher (we may decide to provide a separate setting for fallback strings if this doesn't result in an overly complicated settings UI). -If a fallback string with this language-code is available it will be used. If no such fallback string is available, the default fallback string (without language-code prefix) will be used exist. The default fallback string must exist for all fallback strings with language codes. Otherwise the cfg file must be considered defective. +If a fallback string with this language-code is available it will be used. If no such fallback string is available, the default fallback string (without language-code prefix) will be used. The default fallback string must exist for all fallback strings with language codes. Otherwise the cfg file must be considered defective. We add an option to the importer tool to import strings only, which then will be merged (with a user-specified language-code prefix) into an existing cfg file. @@ -1878,7 +1910,7 @@ The handling of cell names in vanilla MW is one of the major localisation proble We turn the name field (the ID) into an ID-only field. The actual user visible name is moved into an optional localisation record. -We allow referencing of cells by both ID and name. If the name is used we emit an unobtrusive warning message. +We allow referencing of cells by both ID and name. If the name is used, we emit an unobtrusive warning message. This change does not fix the problem of already existing localisations that have modified the names of cells. See the section about localisation mapping below for a workaround. @@ -1907,7 +1939,7 @@ Issues #2 and #3 can be enforced at least partially. We declare the use of user ## Localisation Mapping -The new localisation scheme leaves existing localisations un-addressed. Localisations of Morrowind.esm break the connection of cell names and topics in relation to the un-localised Morrowind.esm (English). +The new localisation scheme leaves existing localisations unaddressed. Localisations of Morrowind.esm break the connection of cell names and topics in relation to the un-localised Morrowind.esm (English). We provide a workaround for this problem by introducing localisation mapping. Localisation mapping is a language-specific list of ID/user visible text pairs, provided as a text file. A similar scheme exists for the Russian localisation. We may decide to simple extend the existing implementation of this feature in OpenMW to provide localisation mapping. @@ -1937,7 +1969,7 @@ Note that by going down this route we do not block the addition of a full-scale ## Skinning -Under the term skinning we understand changes to the look of a GUI without affecting the functionality. Vanilla Morrowind already provides a certain level of skinning-capacity though textures and Morrowind.ini settings. We are going to expand on these and clean up the skinning process. After 1.0 all skinning should be done exclusively through content files and associated resources files. Skinning does not belong into ini/cfg files. +Under the term skinning we understand changes to the look of a GUI without affecting the functionality. Vanilla Morrowind already provides a certain level of skinning-capacity though textures and Morrowind.ini settings. We are going to expand on these and clean up the skinning process. After 1.0 all skinning should be done exclusively through content files and associated resources files. Skinning does not belong in ini/cfg files. ### Windows @@ -1979,7 +2011,7 @@ Dynamic stats are more complicated since they merge three elements: The first two go together well enough, but we should provide an option to decouple the enemy health bar and turn it into its own HUD elements. -We also need to consider that after 1.0 we won't always have three dynamic stats. There could be more or (in stage 2) less. The HUD element must be build in a way that can adjust its layout accordingly without manual layouting. +We also need to consider that after 1.0 we won't always have three dynamic stats. There could be more or (in stage 2) less. The HUD element must be built in a way that it can adjust its layout accordingly without manual layouting. ### Notifications @@ -2020,7 +2052,7 @@ We add a couple of new input elements that are each represented as a separate wi * Numerical input: Takes a range, a precision value and a default value and returns a float. * Text input: Takes a default value and returns a string. -Additionally each window has a titlebar, an okay button and and optional cancel button. +Additionally each window has a title bar, an okay button and and optional cancel button. For each element we add a new script instruction that brings up the respective element. These instructions take the following arguments: @@ -2075,10 +2107,10 @@ We may explore an alternative GUI layout consisting of a vertical bar (maybe on The current horizontal text-based category selector is problematic for two reasons: -* Since we hand over the creation of categories to mod developers we can expect a large number of them and therefore scaling becomes an issue. Horizontally arranged text items scale terribly because text items also primarily extend horizontally. There are workarounds for this problem but these are either bad or add alternative interface (e.g. a pulldown menu listing all tabs) which isn't a great solution either. Under no circumstances will we use the workaround that adds two arrow buttons left and right of the tab bar to change the visible section of the tab bar. This is an anti-pattern and everyone who has ever committed the horrendous crime of implementing this GUI-atrocity deserves to end up in usability hell. +* Since we hand over the creation of categories to mod developers we can expect a large number of them and therefore scaling becomes an issue. Horizontally arranged text items scale terribly because text items also primarily extend horizontally. There are workarounds for this problem but these are either bad or add alternative interface (e.g. a pull-down menu listing all tabs) which isn't a great solution either. Under no circumstances will we use the workaround that adds two arrow buttons left and right of the tab bar to change the visible section of the tab bar. This is an anti-pattern and everyone who has ever committed the horrendous crime of implementing this GUI-atrocity deserves to end up in usability hell. * In table mode we already have a bar at the top of the window. Two bars on the same window border give a cluttered, unclean appearance. -If we do use an alternative layout we need to add an icon field to the ItemCategory record and we also need to consider, if we drop the vanilla one completely or let the user choose. Unless there is strong opposition (this is a significant change from Vanilla after all) we should choose the former option. If we opt for a less radical change we still should switch from text to icons to counter the scaling problem, even if we stick with the horizontal layout. +If we do use an alternative layout, we need to add an icon field to the ItemCategory record and we also need to consider whether we drop the vanilla one completely or let the user choose. Unless there is strong opposition (this is a significant change from Vanilla after all) we should choose the former option. If we opt for a less radical change we still should switch from text to icons to counter the scaling problem, even if we stick with the horizontal layout. ## Character State @@ -2112,7 +2144,7 @@ If the GMST is not 0 we show skill progression in two places: # Graphics -TODO bugger scrawl until he agrees to fill in this section +TODO Random collection of ideas that may be feasible or not: @@ -2149,7 +2181,7 @@ We let scripts to run as operations (exclusively for now). In the context of Ope * An operation runs in a separate thread and has read only access to the content data * Multiple operations can run concurrently, but only one operation of each type (e.g. saving, verifier, python script) at a time -* While at least one operation is running the content data can not be modified +* While at least one operation is running, the content data can not be modified * The operation can be terminated by the user at any time * The operation reports back to the main thread via messages (shown in a table like with the verifier) and progress state (represented as progress bar) @@ -2169,7 +2201,7 @@ Note that all debugging tools will require that OpenMW is started from OpenMW-CS ### Marking -We add a new keyboard-shortcut to OpenMW (only available when started from OpenMW-CS). When used OpenMW will look at the instance under the mouse pointer (or the crosshair if the mouse pointer is hidden). If this instance is part of a content file (i.e. not created during play) OpenMW will send the cell and RefID of the instance to OpenMW-CS. OpenMW-CS inserts the instance info into a table. +We add a new keyboard shortcut to OpenMW (only available when started from OpenMW-CS). When used OpenMW will look at the instance under the mouse pointer (or the crosshair if the mouse pointer is hidden). If this instance is part of a content file (i.e. not created during play) OpenMW will send the cell and RefID of the instance to OpenMW-CS. OpenMW-CS inserts the instance info into a table. This table functions in a similar way to the verifier table; allowing the content developer to jump directly to records that have been identified as being in need of attention. @@ -2189,15 +2221,15 @@ We also add function that produces a warning if the user tries to create an ID o OpenMW-CS currently has very little in-application help. Improving this will be an ongoing process. For stage 1 we will focus on scripting. -We will add a keyboard-shortcut and a context menu item to the script editor. When activated while the cursor is on a keyword we show a help text that explains the syntax and the function. +We will add a keyboard shortcut and a context menu item to the script editor. When activated while the cursor is on a keyword we show a help text that explains the syntax and the function. Ideally we would want to use the same help text within the application and in our documentation. A way to handle this needs to be determined. -The location where we show the help text needs to be determined. For script editor views we could re-use the widget that displays errors. Or we could add a separate subview type for help text. We may decide to add a help main menu item from which the help system can be navigated. +The location where we show the help text needs to be determined. For script editor views we could reuse the widget that displays errors. Or we could add a separate subview type for help text. We may decide to add a help main menu item from which the help system can be navigated. -## Spellchecker +## Spell Checker -We will add a spellchecker by utilising an existing spellcheck solution. All names (e.g. items, cells, races) will be implicitly added to the list of known words. +We will add a spell checker by utilising an existing spell check solution. All names (e.g. items, cells, races) will be implicitly added to the list of known words. ## Porting Tools @@ -2237,19 +2269,19 @@ A user settings record consists of the following fields: The following user setting types are available: -* bool (type 0): GMST is an integer; GUI is a checkbox; additional data: default value -* numeric, integer (type 1): GMST is an integer; GUI is a spinbox; additional data: upper bound, lower bound, default value -* numeric, float (type 2): GMST is a float; GUI is a spinbox; additional data; upper bound, lower bound, precision, default value -* list (type 3) GMST is an integer; GUI is a combobox; additional data; list of strings for combobox text, default value +* bool (type 0): GMST is an integer; GUI is a check box; additional data: default value +* numeric, integer (type 1): GMST is an integer; GUI is a spin box; additional data: upper bound, lower bound, default value +* numeric, float (type 2): GMST is a float; GUI is a spin box; additional data; upper bound, lower bound, precision, default value +* list (type 3) GMST is an integer; GUI is a combo box; additional data; list of strings for combo box text, default value * slider (type 4): GMST is a float; GUI is a slider; additional data; upper bound, lower bound, default value ### Categories User settings categories are represented as separate pages/tabs in the GUI. We specify categories via category IDs (strings). Currently there does not appear to be a need for a separate user settings category record, since this record would have no data. -Content files can create new categories (simply by referencing them in a user settings record) or add to existing categories (including the vanilla ones). We should consider to add a couple of additional default categories (including localised labels, but empty and invisible until populated by content files) to help content developers organise their settings in a coherent way. +Content files can create new categories (simply by referencing them in a user settings record) or add to existing categories (including the vanilla ones). We should consider adding a couple of additional default categories (including localised labels, but empty and invisible until populated by content files) to help content developers organise their settings in a coherent way. -The categories in vanilla MW are General, Audio, Controls and Graphics. Content files can add settings to these too. But we should consider reorganisation. General is not a good category. We should consider splitting it up. We should also consider moving the keybindings into their own category. A separate keybindings category would have to be the only exception to the rule that content files are allowed to add settings to pre-existing categories. +The categories in vanilla MW are General, Audio, Controls and Graphics. Content files can add settings to these too, but we should consider reorganisation. General is not a good category. We should consider splitting it up. We should also consider moving the key bindings into their own category. A separate key bindings category would have to be the only exception to the rule that content files are allowed to add settings to pre-existing categories. ### Hook @@ -2283,13 +2315,13 @@ A LiquidBody object record has the following fields: * Height (float) * Shape -Shape is a closed bezier-curve that can be edited in the editor. This shape defines a surface (the liquid surface). The depths of liquid body is defined by the height value. +Shape is a closed Bézier curve that can be edited in the editor. This shape defines a surface (the liquid surface). The depths of liquid body is defined by the height value. We can imply that only the surface of the LiquidShape instance is exposed. The other sides do not require rendering. -## Dagoth'Ur Un-Fix +## Dagoth Ur Fix Un-Fix -We introduced a fix for a defect in Morrowind.esm by blocking remote access to instances of the object dagoth_ur_1 via mod instructions for dynamic stats. We will now bind this fix to a new integer GMST in the sys namespace. This will allow content developers to disable this fix in their mods (hopefully after given poor old Dagoth'Ur a bit more health). +We introduced a fix for a defect in Morrowind.esm by blocking remote access to instances of the object dagoth_ur_1 via mod instructions for dynamic stats. We will now bind this fix to a new integer GMST in the sys namespace. This will allow content developers to disable this fix in their mods (hopefully after giving poor old Dagoth Ur a bit more health). ## Comment Subrecords @@ -2312,7 +2344,7 @@ We add four new GMSTs of type integer (default value 1): These decide if the game is paused when in status mode (right-click), dialogue mode, journal mode or any of the crafting modes (e.g. alchemy). To handle sys::DialogPause==0 properly we ignore dialogues initiated by NPCs or via script, if the player is already in a dialogue. -## Instance Persistency +## Instance Persistence We add a new optional field to all object records and instance subrecords, a single enum-like integer. From 9c78364c45679a2846081f833507935cc4af7fe1 Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Wed, 27 Jun 2018 22:19:09 +0200 Subject: [PATCH 261/282] Revert "Merge pull request #1771 from Xenkhan/master" This reverts commit 9667dd051c8f1794cd01605df9c88d10fe0cd514, reversing changes made to f52e06fc19b0d6cf05ad79ca17f29e5835720cc6. --- CHANGELOG.md | 1 - apps/openmw/engine.cpp | 29 ++++++++--------------------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39af121aa..c1181b615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,6 @@ Bug #4459: NotCell dialogue condition doesn't support partial matches Bug #4461: "Open" spell from non-player caster isn't a crime Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal - Bug #4471: Retrieve SDL window settings instead of using magic numbers Bug #4474: No fallback when getVampireHead fails Bug #4475: Scripted animations should not cause movement Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 8c3c9494c..72dfaa0e4 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -381,33 +381,20 @@ void OMW::Engine::createWindow(Settings::Manager& settings) setWindowIcon(); osg::ref_ptr traits = new osg::GraphicsContext::Traits; - int redSize; - int greenSize; - int blueSize; - int depthSize; - int stencilSize; - int doubleBuffer; - - SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &redSize); - SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &greenSize); - SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &blueSize); - SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &depthSize); - SDL_GL_GetAttribute(SDL_GL_STENCIL_SIZE, &stencilSize); - SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, &doubleBuffer); - SDL_GetWindowPosition(mWindow, &traits->x, &traits->y); SDL_GetWindowSize(mWindow, &traits->width, &traits->height); - traits->red = redSize; - traits->green = greenSize; - traits->blue = blueSize; - traits->depth = depthSize; - traits->stencil = stencilSize; - traits->doubleBuffer = doubleBuffer; traits->windowName = SDL_GetWindowTitle(mWindow); traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); - traits->vsync = vsync; + // FIXME: Some way to get these settings back from the SDL window? + traits->red = 8; + traits->green = 8; + traits->blue = 8; traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel + traits->depth = 24; + traits->stencil = 8; + traits->vsync = vsync; + traits->doubleBuffer = true; traits->inheritedWindowData = new SDLUtil::GraphicsWindowSDL2::WindowData(mWindow); osg::ref_ptr graphicsWindow = new SDLUtil::GraphicsWindowSDL2(traits); From 5fcb09112767905d560f15808bc9e2e6d85083f3 Mon Sep 17 00:00:00 2001 From: Nikolay Kasyanov Date: Wed, 27 Jun 2018 22:22:01 +0200 Subject: [PATCH 262/282] Replace FIXME with a detailed explanation of the issue --- apps/openmw/engine.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 72dfaa0e4..93153da87 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -386,7 +386,11 @@ void OMW::Engine::createWindow(Settings::Manager& settings) traits->windowName = SDL_GetWindowTitle(mWindow); traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); - // FIXME: Some way to get these settings back from the SDL window? + // We tried to get rid of the hardcoding but failed: https://github.com/OpenMW/openmw/pull/1771 + // Here goes kcat's quote: + // It's ultimately a chicken and egg problem, and the reason why the code is like it was in the first place. + // It needs a context to get the current attributes, but it needs the attributes to set up the context. + // So it just specifies the same values that were given to SDL in the hopes that it's good enough to what the window eventually gets. traits->red = 8; traits->green = 8; traits->blue = 8; From 335e2c5897d73ea866807f427c5b362e5867c03a Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Thu, 28 Jun 2018 13:27:08 +1000 Subject: [PATCH 263/282] add keyData struct + general cleanup --- apps/openmw/mwgui/quickkeysmenu.cpp | 181 +++++++++++++--------------- apps/openmw/mwgui/quickkeysmenu.hpp | 20 +-- 2 files changed, 99 insertions(+), 102 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index e19df2bbb..819decfc1 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -43,27 +43,21 @@ namespace MWGui getWidget(mInstructionLabel, "InstructionLabel"); mMainWidget->setSize(mMainWidget->getWidth(), - mMainWidget->getHeight() + (mInstructionLabel->getTextSize().height - mInstructionLabel->getHeight())); + mMainWidget->getHeight() + + (mInstructionLabel->getTextSize().height - mInstructionLabel->getHeight())); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onOkButtonClicked); center(); + mKey = std::vector(10); for (int i = 0; i < 10; ++i) { - ItemWidget* button; - getWidget(button, "QuickKey" + MyGUI::utility::toString(i+1)); + mKey[i].index = i; + getWidget(mKey[i].button, "QuickKey" + MyGUI::utility::toString(i+1)); + mKey[i].button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); - button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); - - mQuickKeyButtons.push_back(button); - - mAssigned.push_back(Type_Unassigned); - - mAssignedId.push_back(std::string("")); - mAssignedName.push_back(std::string("")); - - unassign(button, i); + unassign(&mKey[i]); } } @@ -73,7 +67,7 @@ namespace MWGui for (int i=0; i<10; ++i) { - unassign(mQuickKeyButtons[i], i); + unassign(&mKey[i]); } } @@ -94,10 +88,7 @@ namespace MWGui // Check if quick keys are still valid for (int i=0; i<10; ++i) { - ItemWidget* button = mQuickKeyButtons[i]; - int type = mAssigned[i]; - - switch (type) + switch (mKey[i].type) { case Type_Unassigned: case Type_HandToHand: @@ -106,7 +97,7 @@ namespace MWGui case Type_Item: case Type_MagicItem: { - MWWorld::Ptr item = *button->getUserData(); + MWWorld::Ptr item = *mKey[i].button->getUserData(); // Make sure the item is available and is not broken if (item.getRefData().getCount() < 1 || (item.getClass().hasItemHealth(item) && @@ -116,51 +107,53 @@ namespace MWGui std::string id = item.getCellRef().getRefId(); item = store.findReplacement(id); - button->setUserData(MWWorld::Ptr(item)); + mKey[i].button->setUserData(MWWorld::Ptr(item)); break; } } } } - } - void QuickKeysMenu::unassign(ItemWidget* key, int index) + void QuickKeysMenu::unassign(struct keyData* key) { - mAssignedName[index] = ""; - mAssignedId[index] = ""; + key->button->clearUserStrings(); + key->button->setItem(MWWorld::Ptr()); - key->clearUserStrings(); - key->setItem(MWWorld::Ptr()); - while (key->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(key->getChildAt(0)); + while(key->button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(key->button->getChildAt(0)); - if (index == 9) + if (key->index == 9) { - mAssigned[index] = Type_HandToHand; + key->type = Type_HandToHand; - MyGUI::ImageBox* image = key->createWidget("ImageBox", + MyGUI::ImageBox* image = key->button->createWidget("ImageBox", MyGUI::IntCoord(14, 13, 32, 32), MyGUI::Align::Default); + image->setImageTexture("icons\\k\\stealth_handtohand.dds"); image->setNeedMouseFocus(false); } else { - mAssigned[index] = Type_Unassigned; + key->type = Type_Unassigned; + key->id = ""; + key->name = ""; - MyGUI::TextBox* textBox = key->createWidgetReal("SandText", MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); - textBox->setTextAlign (MyGUI::Align::Center); - textBox->setCaption (MyGUI::utility::toString(index+1)); - textBox->setNeedMouseFocus (false); + MyGUI::TextBox* textBox = key->button->createWidgetReal("SandText", + MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); + + textBox->setTextAlign(MyGUI::Align::Center); + textBox->setCaption(MyGUI::utility::toString(key->index + 1)); + textBox->setNeedMouseFocus(false); } } void QuickKeysMenu::onQuickKeyButtonClicked(MyGUI::Widget* sender) { int index = -1; - for (int i = 0; i < 10; ++i) + for(int i = 0; i < 10; ++i) { - if (sender == mQuickKeyButtons[i] || sender->getParent () == mQuickKeyButtons[i]) + if(sender == mKey[i].button || sender->getParent() == mKey[i].button) { index = i; break; @@ -170,9 +163,10 @@ namespace MWGui mSelectedIndex = index; // open assign dialog - if (!mAssignDialog) + if(!mAssignDialog) mAssignDialog = new QuickKeysMenuAssign(this); - mAssignDialog->setVisible (true); + + mAssignDialog->setVisible(true); } void QuickKeysMenu::onOkButtonClicked (MyGUI::Widget *sender) @@ -180,7 +174,6 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_QuickKeysMenu); } - void QuickKeysMenu::onItemButtonClicked(MyGUI::Widget* sender) { if (!mItemSelectionDialog ) @@ -198,42 +191,42 @@ namespace MWGui void QuickKeysMenu::onMagicButtonClicked(MyGUI::Widget* sender) { - if (!mMagicSelectionDialog ) + if(!mMagicSelectionDialog) { mMagicSelectionDialog = new MagicSelectionDialog(this); } mMagicSelectionDialog->setVisible(true); - mAssignDialog->setVisible (false); + mAssignDialog->setVisible(false); } void QuickKeysMenu::onUnassignButtonClicked(MyGUI::Widget* sender) { - unassign(mQuickKeyButtons[mSelectedIndex], mSelectedIndex); - mAssignDialog->setVisible (false); + unassign(&mKey[mSelectedIndex]); + mAssignDialog->setVisible(false); } void QuickKeysMenu::onCancelButtonClicked(MyGUI::Widget* sender) { - mAssignDialog->setVisible (false); + mAssignDialog->setVisible(false); } void QuickKeysMenu::onAssignItem(MWWorld::Ptr item) { - assert (mSelectedIndex >= 0); - ItemWidget* button = mQuickKeyButtons[mSelectedIndex]; - while (button->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); + assert(mSelectedIndex >= 0); - mAssigned[mSelectedIndex] = Type_Item; - mAssignedId[mSelectedIndex] = item.getCellRef().getRefId(); - mAssignedName[mSelectedIndex] = item.getClass().getName(item); + while(mKey[mSelectedIndex].button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(mKey[mSelectedIndex].button->getChildAt(0)); - button->setItem(item, ItemWidget::Barter); - button->setUserString ("ToolTipType", "ItemPtr"); - button->setUserData(item); + mKey[mSelectedIndex].type = Type_Item; + mKey[mSelectedIndex].id = item.getCellRef().getRefId(); + mKey[mSelectedIndex].name = item.getClass().getName(item); - if (mItemSelectionDialog) + mKey[mSelectedIndex].button->setItem(item, ItemWidget::Barter); + mKey[mSelectedIndex].button->setUserString("ToolTipType", "ItemPtr"); + mKey[mSelectedIndex].button->setUserData(item); + + if(mItemSelectionDialog) mItemSelectionDialog->setVisible(false); } @@ -242,37 +235,37 @@ namespace MWGui mItemSelectionDialog->setVisible(false); } - void QuickKeysMenu::onAssignMagicItem (MWWorld::Ptr item) + void QuickKeysMenu::onAssignMagicItem(MWWorld::Ptr item) { - assert (mSelectedIndex >= 0); - ItemWidget* button = mQuickKeyButtons[mSelectedIndex]; - while (button->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); + assert(mSelectedIndex >= 0); - mAssigned[mSelectedIndex] = Type_MagicItem; + while(mKey[mSelectedIndex].button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(mKey[mSelectedIndex].button->getChildAt(0)); - button->setFrame("textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); - button->setIcon(item); + mKey[mSelectedIndex].type = Type_MagicItem; - button->setUserString ("ToolTipType", "ItemPtr"); - button->setUserData(MWWorld::Ptr(item)); + mKey[mSelectedIndex].button->setFrame("textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); + mKey[mSelectedIndex].button->setIcon(item); - if (mMagicSelectionDialog) + mKey[mSelectedIndex].button->setUserString ("ToolTipType", "ItemPtr"); + mKey[mSelectedIndex].button->setUserData(MWWorld::Ptr(item)); + + if(mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); } - void QuickKeysMenu::onAssignMagic (const std::string& spellId) + void QuickKeysMenu::onAssignMagic(const std::string& spellId) { - assert (mSelectedIndex >= 0); - ItemWidget* button = mQuickKeyButtons[mSelectedIndex]; - while (button->getChildCount()) // Destroy number label + assert(mSelectedIndex >= 0); + ItemWidget* button = mKey[mSelectedIndex].button; + while(mKey[mSelectedIndex].button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); - mAssigned[mSelectedIndex] = Type_Magic; + mKey[mSelectedIndex].type = Type_Magic; - button->setItem(MWWorld::Ptr()); - button->setUserString ("ToolTipType", "Spell"); - button->setUserString ("Spell", spellId); + mKey[mSelectedIndex].button->setItem(MWWorld::Ptr()); + mKey[mSelectedIndex].button->setUserString("ToolTipType", "Spell"); + mKey[mSelectedIndex].button->setUserString("Spell", spellId); const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); @@ -311,10 +304,9 @@ namespace MWGui void QuickKeysMenu::activateQuickKey(int index) { - assert (index-1 >= 0); - ItemWidget* button = mQuickKeyButtons[index-1]; - - QuickKeyType type = mAssigned[index-1]; + assert(index-1 >= 0); + ItemWidget* button = mKey[index-1].button; + QuickKeyType type = mKey[index-1].type; MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); @@ -351,17 +343,17 @@ namespace MWGui break; } if (it == store.end()) - item = NULL; + item = nullptr; // check the item is available and not broken if (!item || item.getRefData().getCount() < 1 || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { - item = store.findReplacement(mAssignedId[index-1]); + item = store.findReplacement(mKey[index-1].id); if (!item || item.getRefData().getCount() < 1) { MWBase::Environment::get().getWindowManager()->messageBox( - "#{sQuickMenu5} " + mAssignedName[index-1]); + "#{sQuickMenu5} " + mKey[index-1].name); return; } @@ -490,9 +482,9 @@ namespace MWGui for (int i=0; i<10; ++i) { - ItemWidget* button = mQuickKeyButtons[i]; + ItemWidget* button = mKey[i].button; - int type = mAssigned[i]; + int type = mKey[i].type; ESM::QuickKeys::QuickKey key; key.mType = type; @@ -525,7 +517,7 @@ namespace MWGui void QuickKeysMenu::readRecord(ESM::ESMReader &reader, uint32_t type) { - if (type != ESM::REC_KEYS) + if(type != ESM::REC_KEYS) return; ESM::QuickKeys keys; @@ -535,20 +527,19 @@ namespace MWGui MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); int i=0; - for (std::vector::const_iterator it = keys.mKeys.begin(); it != keys.mKeys.end(); ++it) + for(std::vector::const_iterator it = keys.mKeys.begin(); it != keys.mKeys.end(); ++it) { - if (i >= 10) + if(i >= 10) return; mSelectedIndex = i; int keyType = it->mType; std::string id = it->mId; - ItemWidget* button = mQuickKeyButtons[i]; switch (keyType) { case Type_Magic: - if (MWBase::Environment::get().getWorld()->getStore().get().search(id)) + if(MWBase::Environment::get().getWorld()->getStore().get().search(id)) onAssignMagic(id); break; case Type_Item: @@ -557,13 +548,13 @@ namespace MWGui // Find the item by id MWWorld::Ptr item = store.findReplacement(id); - if (item.isEmpty()) - unassign(button, i); + if(item.isEmpty()) + unassign(&mKey[i]); else { - if (keyType == Type_Item) + if(keyType == Type_Item) onAssignItem(item); - else if (keyType == Type_MagicItem) + else if(keyType == Type_MagicItem) onAssignMagicItem(item); } @@ -571,7 +562,7 @@ namespace MWGui } case Type_Unassigned: case Type_HandToHand: - unassign(button, i); + unassign(&mKey[i]); break; } diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index 29506ab58..f11673f5f 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -2,7 +2,6 @@ #define MWGUI_QUICKKEYS_H #include "../mwworld/ptr.hpp" -#include "../mwworld/containerstore.hpp" #include "windowbase.hpp" @@ -56,14 +55,21 @@ namespace MWGui private: + + struct keyData { + int index; + ItemWidget* button; + QuickKeysMenu::QuickKeyType type; + std::string id; + std::string name; + keyData(): index(-1), button(nullptr), type(Type_Unassigned), id(""), name("") {} + }; + + std::vector mKey; + MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; - std::vector mQuickKeyButtons; - std::vector mAssigned; - std::vector mAssignedId; - std::vector mAssignedName; - QuickKeysMenuAssign* mAssignDialog; ItemSelectionDialog* mItemSelectionDialog; MagicSelectionDialog* mMagicSelectionDialog; @@ -74,7 +80,7 @@ namespace MWGui void onQuickKeyButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); - void unassign(ItemWidget* key, int index); + void unassign(struct keyData* key); }; class QuickKeysMenuAssign : public WindowModal From 80a3f0a0d40a7f39a8be6337119286a3d64d683a Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Thu, 28 Jun 2018 17:02:25 +1000 Subject: [PATCH 264/282] switch mSelectedIndex/mActivatedIndex int to mSelected/mActivated keyData pointers --- apps/openmw/mwgui/quickkeysmenu.cpp | 174 ++++++++++++++-------------- apps/openmw/mwgui/quickkeysmenu.hpp | 5 +- 2 files changed, 90 insertions(+), 89 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 819decfc1..b2bf9df0e 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -33,11 +33,13 @@ namespace MWGui QuickKeysMenu::QuickKeysMenu() : WindowBase("openmw_quickkeys_menu.layout") + , mKey(std::vector(10)) + , mSelected(nullptr) + , mActivated(nullptr) , mAssignDialog(0) , mItemSelectionDialog(0) , mMagicSelectionDialog(0) - , mSelectedIndex(-1) - , mActivatedIndex(-1) + { getWidget(mOkButton, "OKButton"); getWidget(mInstructionLabel, "InstructionLabel"); @@ -49,8 +51,6 @@ namespace MWGui mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onOkButtonClicked); center(); - mKey = std::vector(10); - for (int i = 0; i < 10; ++i) { mKey[i].index = i; @@ -63,7 +63,7 @@ namespace MWGui void QuickKeysMenu::clear() { - mActivatedIndex = -1; + mActivated = nullptr; for (int i=0; i<10; ++i) { @@ -120,7 +120,7 @@ namespace MWGui key->button->clearUserStrings(); key->button->setItem(MWWorld::Ptr()); - while(key->button->getChildCount()) // Destroy number label + while (key->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(key->button->getChildAt(0)); if (key->index == 9) @@ -151,19 +151,19 @@ namespace MWGui void QuickKeysMenu::onQuickKeyButtonClicked(MyGUI::Widget* sender) { int index = -1; - for(int i = 0; i < 10; ++i) + for (int i = 0; i < 10; ++i) { - if(sender == mKey[i].button || sender->getParent() == mKey[i].button) + if (sender == mKey[i].button || sender->getParent() == mKey[i].button) { index = i; break; } } assert(index != -1); - mSelectedIndex = index; + mSelected = &mKey[index]; // open assign dialog - if(!mAssignDialog) + if (!mAssignDialog) mAssignDialog = new QuickKeysMenuAssign(this); mAssignDialog->setVisible(true); @@ -176,7 +176,7 @@ namespace MWGui void QuickKeysMenu::onItemButtonClicked(MyGUI::Widget* sender) { - if (!mItemSelectionDialog ) + if (!mItemSelectionDialog) { mItemSelectionDialog = new ItemSelectionDialog("#{sQuickMenu6}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &QuickKeysMenu::onAssignItem); @@ -186,12 +186,12 @@ namespace MWGui mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyUsableItems); - mAssignDialog->setVisible (false); + mAssignDialog->setVisible(false); } void QuickKeysMenu::onMagicButtonClicked(MyGUI::Widget* sender) { - if(!mMagicSelectionDialog) + if (!mMagicSelectionDialog) { mMagicSelectionDialog = new MagicSelectionDialog(this); } @@ -202,7 +202,7 @@ namespace MWGui void QuickKeysMenu::onUnassignButtonClicked(MyGUI::Widget* sender) { - unassign(&mKey[mSelectedIndex]); + unassign(mSelected); mAssignDialog->setVisible(false); } @@ -213,20 +213,20 @@ namespace MWGui void QuickKeysMenu::onAssignItem(MWWorld::Ptr item) { - assert(mSelectedIndex >= 0); + assert(mSelected); - while(mKey[mSelectedIndex].button->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(mKey[mSelectedIndex].button->getChildAt(0)); + while (mSelected->button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); - mKey[mSelectedIndex].type = Type_Item; - mKey[mSelectedIndex].id = item.getCellRef().getRefId(); - mKey[mSelectedIndex].name = item.getClass().getName(item); + mSelected->type = Type_Item; + mSelected->id = item.getCellRef().getRefId(); + mSelected->name = item.getClass().getName(item); - mKey[mSelectedIndex].button->setItem(item, ItemWidget::Barter); - mKey[mSelectedIndex].button->setUserString("ToolTipType", "ItemPtr"); - mKey[mSelectedIndex].button->setUserData(item); + mSelected->button->setItem(item, ItemWidget::Barter); + mSelected->button->setUserString("ToolTipType", "ItemPtr"); + mSelected->button->setUserData(item); - if(mItemSelectionDialog) + if (mItemSelectionDialog) mItemSelectionDialog->setVisible(false); } @@ -237,38 +237,37 @@ namespace MWGui void QuickKeysMenu::onAssignMagicItem(MWWorld::Ptr item) { - assert(mSelectedIndex >= 0); + assert(mSelected); - while(mKey[mSelectedIndex].button->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(mKey[mSelectedIndex].button->getChildAt(0)); + while (mSelected->button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); - mKey[mSelectedIndex].type = Type_MagicItem; + mSelected->type = Type_MagicItem; - mKey[mSelectedIndex].button->setFrame("textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); - mKey[mSelectedIndex].button->setIcon(item); + mSelected->button->setFrame("textures\\menu_icon_select_magic_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); + mSelected->button->setIcon(item); - mKey[mSelectedIndex].button->setUserString ("ToolTipType", "ItemPtr"); - mKey[mSelectedIndex].button->setUserData(MWWorld::Ptr(item)); + mSelected->button->setUserString("ToolTipType", "ItemPtr"); + mSelected->button->setUserData(MWWorld::Ptr(item)); - if(mMagicSelectionDialog) + if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); } void QuickKeysMenu::onAssignMagic(const std::string& spellId) { - assert(mSelectedIndex >= 0); - ItemWidget* button = mKey[mSelectedIndex].button; - while(mKey[mSelectedIndex].button->getChildCount()) // Destroy number label - MyGUI::Gui::getInstance().destroyWidget(button->getChildAt(0)); + assert(mSelected); + while (mSelected->button->getChildCount()) // Destroy number label + MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); - mKey[mSelectedIndex].type = Type_Magic; + mSelected->type = Type_Magic; + mSelected->id = spellId; - mKey[mSelectedIndex].button->setItem(MWWorld::Ptr()); - mKey[mSelectedIndex].button->setUserString("ToolTipType", "Spell"); - mKey[mSelectedIndex].button->setUserString("Spell", spellId); + mSelected->button->setItem(MWWorld::Ptr()); + mSelected->button->setUserString("ToolTipType", "Spell"); + mSelected->button->setUserString("Spell", spellId); - const MWWorld::ESMStore &esmStore = - MWBase::Environment::get().getWorld()->getStore(); + const MWWorld::ESMStore &esmStore = MWBase::Environment::get().getWorld()->getStore(); // use the icon of the first effect const ESM::Spell* spell = esmStore.get().find(spellId); @@ -281,14 +280,14 @@ namespace MWGui path.insert(slashPos+1, "b_"); path = MWBase::Environment::get().getWindowManager()->correctIconPath(path); - button->setFrame("textures\\menu_icon_select_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); - button->setIcon(path); + mSelected->button->setFrame("textures\\menu_icon_select_magic.dds", MyGUI::IntCoord(2, 2, 40, 40)); + mSelected->button->setIcon(path); if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); } - void QuickKeysMenu::onAssignMagicCancel () + void QuickKeysMenu::onAssignMagicCancel() { mMagicSelectionDialog->setVisible(false); } @@ -296,17 +295,16 @@ namespace MWGui void QuickKeysMenu::updateActivatedQuickKey() { // there is no delayed action, nothing to do. - if (mActivatedIndex < 0) + if (!mActivated) return; - activateQuickKey(mActivatedIndex); + activateQuickKey(mActivated->index); } void QuickKeysMenu::activateQuickKey(int index) { - assert(index-1 >= 0); - ItemWidget* button = mKey[index-1].button; - QuickKeyType type = mKey[index-1].type; + assert(index > 0); + struct keyData *key = &mKey[index-1]; MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); @@ -319,22 +317,23 @@ namespace MWGui || playerStats.getHitRecovery(); bool isReturnNeeded = playerStats.isParalyzed() || playerStats.isDead(); - if (isReturnNeeded && type != Type_Item) + + if (isReturnNeeded && key->type != Type_Item) { return; } - - if (isDelayNeeded && type != Type_Item) + else if(isDelayNeeded && key->type != Type_Item) { - mActivatedIndex = index; + mActivated = key; return; } else - mActivatedIndex = -1; + mActivated = nullptr; + - if (type == Type_Item || type == Type_MagicItem) + if (key->type == Type_Item || key->type == Type_MagicItem) { - MWWorld::Ptr item = *button->getUserData(); + MWWorld::Ptr item = *key->button->getUserData(); MWWorld::ContainerStoreIterator it = store.begin(); for (; it != store.end(); ++it) @@ -347,19 +346,20 @@ namespace MWGui // check the item is available and not broken if (!item || item.getRefData().getCount() < 1 || - (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) + (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { - item = store.findReplacement(mKey[index-1].id); + item = store.findReplacement(key->id); + if (!item || item.getRefData().getCount() < 1) { MWBase::Environment::get().getWindowManager()->messageBox( - "#{sQuickMenu5} " + mKey[index-1].name); + "#{sQuickMenu5} " + key->name); return; } } - if (type == Type_Item) + if (key->type == Type_Item) { bool isWeapon = item.getTypeName() == typeid(ESM::Weapon).name(); bool isTool = item.getTypeName() == typeid(ESM::Probe).name() || @@ -368,12 +368,11 @@ namespace MWGui // delay weapon switching if player is busy if (isDelayNeeded && (isWeapon || isTool)) { - mActivatedIndex = index; + mActivated = key; return; } - // disable weapon switching if player is dead or paralyzed - if (isReturnNeeded && (isWeapon || isTool)) + else if (isReturnNeeded && (isWeapon || isTool)) { return; } @@ -386,7 +385,7 @@ namespace MWGui MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); } } - else if (type == Type_MagicItem) + else if (key->type == Type_MagicItem) { // equip, if it can be equipped if (!item.getClass().getEquipmentSlots(item).first.empty()) @@ -402,25 +401,28 @@ namespace MWGui MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); } } - else if (type == Type_Magic) + else if (key->type == Type_Magic) { - std::string spellId = button->getUserString("Spell"); + std::string spellId = key->id; // Make sure the player still has this spell MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); + if (!spells.hasSpell(spellId)) { - const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellId); - MWBase::Environment::get().getWindowManager()->messageBox ( - "#{sQuickMenu5} " + spell->mName); + const ESM::Spell* spell = + MWBase::Environment::get().getWorld()->getStore().get().find(spellId); + MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + spell->mName); return; } + store.setSelectedEnchantItem(store.end()); - MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); + MWBase::Environment::get().getWindowManager() + ->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Spell); } - else if (type == Type_HandToHand) + else if (key->type == Type_HandToHand) { store.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight, player); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon); @@ -517,7 +519,7 @@ namespace MWGui void QuickKeysMenu::readRecord(ESM::ESMReader &reader, uint32_t type) { - if(type != ESM::REC_KEYS) + if (type != ESM::REC_KEYS) return; ESM::QuickKeys keys; @@ -527,34 +529,34 @@ namespace MWGui MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); int i=0; - for(std::vector::const_iterator it = keys.mKeys.begin(); it != keys.mKeys.end(); ++it) + for (std::vector::const_iterator it = keys.mKeys.begin(); it != keys.mKeys.end(); ++it) { - if(i >= 10) + if (i >= 10) return; - mSelectedIndex = i; - int keyType = it->mType; - std::string id = it->mId; + mSelected = &mKey[i]; + mSelected->type = (QuickKeysMenu::QuickKeyType) it->mType; + mSelected->id = it->mId; - switch (keyType) + switch (mSelected->type) { case Type_Magic: - if(MWBase::Environment::get().getWorld()->getStore().get().search(id)) - onAssignMagic(id); + if (MWBase::Environment::get().getWorld()->getStore().get().search(mSelected->id)) + onAssignMagic(mSelected->id); break; case Type_Item: case Type_MagicItem: { // Find the item by id - MWWorld::Ptr item = store.findReplacement(id); + MWWorld::Ptr item = store.findReplacement(mSelected->id); - if(item.isEmpty()) + if (item.isEmpty()) unassign(&mKey[i]); else { - if(keyType == Type_Item) + if (mSelected->type == Type_Item) onAssignItem(item); - else if(keyType == Type_MagicItem) + else if (mSelected->type == Type_MagicItem) onAssignMagicItem(item); } diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index f11673f5f..56394d660 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -66,6 +66,8 @@ namespace MWGui }; std::vector mKey; + struct keyData* mSelected; + struct keyData* mActivated; MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; @@ -74,9 +76,6 @@ namespace MWGui ItemSelectionDialog* mItemSelectionDialog; MagicSelectionDialog* mMagicSelectionDialog; - int mSelectedIndex; - int mActivatedIndex; - void onQuickKeyButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); From 5455490ad2630d68b25a465abcbae2c1c8161cc5 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 28 Jun 2018 11:12:48 +0400 Subject: [PATCH 265/282] Avoid fall-through in spell selection --- apps/openmw/mwmechanics/spellpriority.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/spellpriority.cpp b/apps/openmw/mwmechanics/spellpriority.cpp index 7aef54007..7bedb1e37 100644 --- a/apps/openmw/mwmechanics/spellpriority.cpp +++ b/apps/openmw/mwmechanics/spellpriority.cpp @@ -328,7 +328,10 @@ namespace MWMechanics if (race->mData.mFlags & ESM::Race::Beast) return 0.f; } - // Intended fall-through + else + return 0.f; + + break; // Creatures can not wear armor case ESM::MagicEffect::BoundCuirass: case ESM::MagicEffect::BoundGloves: From d9a1de0ec7e97bb7dee093ec8c881f85886d5406 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Thu, 28 Jun 2018 11:13:32 +0400 Subject: [PATCH 266/282] Do not use deprecated function --- apps/opencs/view/world/enumdelegate.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index e582e3356..4bd40b830 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -110,7 +110,11 @@ void CSVWorld::EnumDelegate::paint (QPainter *painter, const QStyleOptionViewIte int valueIndex = getValueIndex(index); if (valueIndex != -1) { +#if QT_VERSION >= QT_VERSION_CHECK(5,7,0) + QStyleOptionViewItem itemOption(option); +#else QStyleOptionViewItemV4 itemOption(option); +#endif itemOption.text = mValues.at(valueIndex).second; QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &itemOption, painter); } From 24d5fb09da679e2f62737678ad21668b10fa49a9 Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Thu, 28 Jun 2018 22:55:02 +1000 Subject: [PATCH 267/282] fix crash on simultaneous key presses --- apps/openmw/mwgui/quickkeysmenu.cpp | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index b2bf9df0e..4c5a7bf16 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -303,7 +303,8 @@ namespace MWGui void QuickKeysMenu::activateQuickKey(int index) { - assert(index > 0); + assert(index >= 1 && index <= 9); + struct keyData *key = &mKey[index-1]; MWWorld::Ptr player = MWMechanics::getPlayer(); @@ -318,15 +319,12 @@ namespace MWGui bool isReturnNeeded = playerStats.isParalyzed() || playerStats.isDead(); - if (isReturnNeeded && key->type != Type_Item) - { + if (isReturnNeeded) return; - } - else if(isDelayNeeded && key->type != Type_Item) - { + + else if (isDelayNeeded) mActivated = key; - return; - } + else mActivated = nullptr; @@ -367,12 +365,6 @@ namespace MWGui // delay weapon switching if player is busy if (isDelayNeeded && (isWeapon || isTool)) - { - mActivated = key; - return; - } - // disable weapon switching if player is dead or paralyzed - else if (isReturnNeeded && (isWeapon || isTool)) { return; } From ed71656ea6efcbdaadadb08f4315f457d8544ee9 Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Fri, 29 Jun 2018 01:58:57 +1000 Subject: [PATCH 268/282] fix updateActivatedQuickKey() crash keyboard numbers don't start at zero... --- apps/openmw/mwgui/quickkeysmenu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 4c5a7bf16..2899dfb77 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -53,7 +53,7 @@ namespace MWGui for (int i = 0; i < 10; ++i) { - mKey[i].index = i; + mKey[i].index = i+1; getWidget(mKey[i].button, "QuickKey" + MyGUI::utility::toString(i+1)); mKey[i].button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); @@ -143,7 +143,7 @@ namespace MWGui MyGUI::FloatCoord(0,0,1,1), MyGUI::Align::Default); textBox->setTextAlign(MyGUI::Align::Center); - textBox->setCaption(MyGUI::utility::toString(key->index + 1)); + textBox->setCaption(MyGUI::utility::toString(key->index)); textBox->setNeedMouseFocus(false); } } From 72f6b1a6939cd9a220a5262f34b4b3d7224da59d Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Thu, 28 Jun 2018 20:13:18 -0500 Subject: [PATCH 269/282] Separating "Game" Advanced Settings into "Game Mechanics" and "User Interface" --- CHANGELOG.md | 1 + apps/launcher/advancedpage.cpp | 37 +++--- files/ui/advancedpage.ui | 209 +++++++++++++++++---------------- 3 files changed, 129 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1181b615..89fe8eac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal Bug #4474: No fallback when getVampireHead fails Bug #4475: Scripted animations should not cause movement + Bug #4479: "Game" category on Advanced page is getting too long Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots Feature #4256: Implement ToggleBorders (TB) console command diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index 2b2d7b448..b0a35f0a5 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -73,18 +73,8 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); loadSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); loadSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game"); - loadSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); - loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); - loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); - loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); loadSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); - // Expected values are (0, 1, 2, 3) - int showOwnedIndex = mEngineSettings.getInt("show owned", "Game"); - // Match the index with the option. Will default to 0 if invalid. - if (showOwnedIndex >= 0 && showOwnedIndex <= 3) - showOwnedComboBox->setCurrentIndex(showOwnedIndex); - // Input Settings loadSettingBool(allowThirdPersonZoomCheckBox, "allow third person zoom", "Input"); loadSettingBool(grabCursorCheckBox, "grab cursor", "Input"); @@ -94,6 +84,16 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(timePlayedCheckbox, "timeplayed", "Saves"); maximumQuicksavesComboBox->setValue(mEngineSettings.getInt("max quicksaves", "Saves")); + // User Interface Settings + loadSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); + loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); + loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); + loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); + int showOwnedIndex = mEngineSettings.getInt("show owned", "Game"); + // Match the index with the option (only 0, 1, 2, or 3 are valid). Will default to 0 if invalid. + if (showOwnedIndex >= 0 && showOwnedIndex <= 3) + showOwnedComboBox->setCurrentIndex(showOwnedIndex); + // Other Settings QString screenshotFormatString = QString::fromStdString(mEngineSettings.getString("screenshot format", "General")).toUpper(); if (screenshotFormatComboBox->findText(screenshotFormatString) == -1) @@ -125,16 +125,8 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game"); saveSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game"); saveSettingBool(preventMerchantEquippingCheckBox, "prevent merchant equipping", "Game"); - saveSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); - saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); - saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); - saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); saveSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game"); - int showOwnedCurrentIndex = showOwnedComboBox->currentIndex(); - if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game")) - mEngineSettings.setInt("show owned", "Game", showOwnedCurrentIndex); - // Input Settings saveSettingBool(allowThirdPersonZoomCheckBox, "allow third person zoom", "Input"); saveSettingBool(grabCursorCheckBox, "grab cursor", "Input"); @@ -147,6 +139,15 @@ void Launcher::AdvancedPage::saveSettings() mEngineSettings.setInt("max quicksaves", "Saves", maximumQuicksaves); } + // User Interface Settings + saveSettingBool(showEffectDurationCheckBox, "show effect duration", "Game"); + saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game"); + saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game"); + saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game"); + int showOwnedCurrentIndex = showOwnedComboBox->currentIndex(); + if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game")) + mEngineSettings.setInt("show owned", "Game", showOwnedCurrentIndex); + // Other Settings std::string screenshotFormatString = screenshotFormatComboBox->currentText().toLower().toStdString(); if (screenshotFormatString != mEngineSettings.getString("screenshot format", "General")) diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index 7a01ce41d..0e43654f2 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -22,14 +22,14 @@ 0 0 630 - 746 + 791 - Game + Game Mechanics @@ -62,46 +62,6 @@ - - - - <html><head/><body><p>Show the remaining duration of magic effects and lights if this setting is true. The remaining duration is displayed in the tooltip by hovering over the magical effect. </p><p>The default value is false.</p></body></html> - - - Show effect duration - - - - - - - <html><head/><body><p>Whether or not the chance of success will be displayed in the enchanting menu.</p><p>The default value is false.</p></body></html> - - - Show enchant chance - - - - - - - <html><head/><body><p>If this setting is true, melee weapons reach and speed will be showed on item tooltip.</p><p>The default value is false.</p></body></html> - - - Show melee info - - - - - - - <html><head/><body><p>If this setting is true, damage bonus of arrows and bolts will be showed on item tooltip.</p><p>The default value is false.</p></body></html> - - - Show projectile damage - - - @@ -112,64 +72,6 @@ - - - - <html><head/><body><p>Enable visual clues for items owned by NPCs when the crosshair is on the object.</p><p>The default value is Off.</p></body></html> - - - - -1 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Show owned: - - - - - - - 1 - - - - Off - - - - - Tool Tip Only - - - - - Crosshair Only - - - - - Tool Tip and Crosshair - - - - - - - @@ -357,6 +259,113 @@ + + + + User Interface + + + + + + <html><head/><body><p>Show the remaining duration of magic effects and lights if this setting is true. The remaining duration is displayed in the tooltip by hovering over the magical effect. </p><p>The default value is false.</p></body></html> + + + Show effect duration + + + + + + + <html><head/><body><p>Whether or not the chance of success will be displayed in the enchanting menu.</p><p>The default value is false.</p></body></html> + + + Show enchant chance + + + + + + + <html><head/><body><p>If this setting is true, melee weapons reach and speed will be showed on item tooltip.</p><p>The default value is false.</p></body></html> + + + Show melee info + + + + + + + <html><head/><body><p>If this setting is true, damage bonus of arrows and bolts will be showed on item tooltip.</p><p>The default value is false.</p></body></html> + + + Show projectile damage + + + + + + + <html><head/><body><p>Enable visual clues for items owned by NPCs when the crosshair is on the object.</p><p>The default value is Off.</p></body></html> + + + + -1 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Show owned: + + + + + + + 1 + + + + Off + + + + + Tool Tip Only + + + + + Crosshair Only + + + + + Tool Tip and Crosshair + + + + + + + + + + From 2722ca50fb2cddb5a8518205ec08ba2126dace57 Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Fri, 29 Jun 2018 23:32:05 +1000 Subject: [PATCH 270/282] fix QuickKeysMenu crash on reopening window after item drop + pickup [see: !11#note_85086570] --- apps/openmw/mwgui/quickkeysmenu.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 2899dfb77..47bc88c8b 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -99,15 +99,16 @@ namespace MWGui { MWWorld::Ptr item = *mKey[i].button->getUserData(); // Make sure the item is available and is not broken - if (item.getRefData().getCount() < 1 || + if (!item || item.getRefData().getCount() < 1 || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { // Try searching for a compatible replacement - std::string id = item.getCellRef().getRefId(); + item = store.findReplacement(mKey[i].id); + + if (item) + mKey[i].button->setUserData(MWWorld::Ptr(item)); - item = store.findReplacement(id); - mKey[i].button->setUserData(MWWorld::Ptr(item)); break; } } From 596be205c1179fb363f04950d435406e522982ea Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Fri, 29 Jun 2018 23:43:51 +1000 Subject: [PATCH 271/282] cleanup unnecessary struct keywords... --- apps/openmw/mwgui/quickkeysmenu.cpp | 6 +++--- apps/openmw/mwgui/quickkeysmenu.hpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 47bc88c8b..4bfbd4dac 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -33,7 +33,7 @@ namespace MWGui QuickKeysMenu::QuickKeysMenu() : WindowBase("openmw_quickkeys_menu.layout") - , mKey(std::vector(10)) + , mKey(std::vector(10)) , mSelected(nullptr) , mActivated(nullptr) , mAssignDialog(0) @@ -116,7 +116,7 @@ namespace MWGui } } - void QuickKeysMenu::unassign(struct keyData* key) + void QuickKeysMenu::unassign(keyData* key) { key->button->clearUserStrings(); key->button->setItem(MWWorld::Ptr()); @@ -306,7 +306,7 @@ namespace MWGui { assert(index >= 1 && index <= 9); - struct keyData *key = &mKey[index-1]; + keyData *key = &mKey[index-1]; MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index 56394d660..6d2ab8494 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -65,7 +65,7 @@ namespace MWGui keyData(): index(-1), button(nullptr), type(Type_Unassigned), id(""), name("") {} }; - std::vector mKey; + std::vector mKey; struct keyData* mSelected; struct keyData* mActivated; From bccba24c40dd3887896d931aa907957eda7b0a01 Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Fri, 29 Jun 2018 19:57:09 +0300 Subject: [PATCH 272/282] Make unarmed creature attacks not affect armor condition (fixes #2455) --- CHANGELOG.md | 1 + apps/openmw/mwclass/npc.cpp | 22 ++++++++++++---------- apps/openmw/mwmechanics/combat.cpp | 16 +++++++++------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89fe8eac4..14d644c83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Bug #1990: Sunrise/sunset not set correct Bug #2222: Fatigue's effect on selling price is backwards Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped + Bug #2455: Creatures attacks degrade armor Bug #2562: Forcing AI to activate a teleport door sometimes causes a crash Bug #2772: Non-existing class or faction freezes the game Bug #2835: Player able to slowly move when overencumbered diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 92e25baee..2cf2185ba 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -773,22 +773,24 @@ namespace MWClass float x = damage / (damage + getArmorRating(ptr)); damage *= std::max(gmst.fCombatArmorMinMult->getFloat(), x); int damageDiff = static_cast(unmitigatedDamage - damage); - if (damage < 1) - damage = 1; + damage = std::max(1.f, damage); + damageDiff = std::max(1, damageDiff); MWWorld::InventoryStore &inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); if(!armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name()) { - int armorhealth = armor.getClass().getItemHealth(armor); - armorhealth -= std::min(std::max(1, damageDiff), - armorhealth); - armor.getCellRef().setCharge(armorhealth); - - // Armor broken? unequip it - if (armorhealth == 0) - armor = *inv.unequipItem(armor, ptr); + if (!(object.isEmpty() && !attacker.getClass().isNpc())) // Unarmed creature attacks don't affect armor condition + { + int armorhealth = armor.getClass().getItemHealth(armor); + armorhealth -= std::min(damageDiff, armorhealth); + armor.getCellRef().setCharge(armorhealth); + + // Armor broken? unequip it + if (armorhealth == 0) + armor = *inv.unequipItem(armor, ptr); + } if (ptr == MWMechanics::getPlayer()) skillUsageSucceeded(ptr, armor.getClass().getEquipmentSkill(armor), 0); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index ac34c658b..6b45a513b 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -115,14 +115,16 @@ namespace MWMechanics if (Misc::Rng::roll0to99() < x) { - // Reduce shield durability by incoming damage - int shieldhealth = shield->getClass().getItemHealth(*shield); - - shieldhealth -= std::min(shieldhealth, int(damage)); - shield->getCellRef().setCharge(shieldhealth); - if (shieldhealth == 0) - inv.unequipItem(*shield, blocker); + if (!(weapon.isEmpty() && !attacker.getClass().isNpc())) // Unarmed creature attacks don't affect armor condition + { + // Reduce shield durability by incoming damage + int shieldhealth = shield->getClass().getItemHealth(*shield); + shieldhealth -= std::min(shieldhealth, int(damage)); + shield->getCellRef().setCharge(shieldhealth); + if (shieldhealth == 0) + inv.unequipItem(*shield, blocker); + } // Reduce blocker fatigue const float fFatigueBlockBase = gmst.find("fFatigueBlockBase")->getFloat(); const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->getFloat(); From 6e9c08083d583707ec25c35f8eace1ba1df7a214 Mon Sep 17 00:00:00 2001 From: Capostrophic <21265616+Capostrophic@users.noreply.github.com> Date: Fri, 29 Jun 2018 20:35:45 +0300 Subject: [PATCH 273/282] Add missing empty attacker checks --- apps/openmw/mwclass/npc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 2cf2185ba..0becb18df 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -781,7 +781,7 @@ namespace MWClass MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); if(!armor.isEmpty() && armor.getTypeName() == typeid(ESM::Armor).name()) { - if (!(object.isEmpty() && !attacker.getClass().isNpc())) // Unarmed creature attacks don't affect armor condition + if (attacker.isEmpty() || (!attacker.isEmpty() && !(object.isEmpty() && !attacker.getClass().isNpc()))) // Unarmed creature attacks don't affect armor condition { int armorhealth = armor.getClass().getItemHealth(armor); armorhealth -= std::min(damageDiff, armorhealth); From 09c9bd34c3e438ad2a47be6be098e5e75b46fb5b Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Sat, 30 Jun 2018 12:43:50 +1000 Subject: [PATCH 274/282] cleanup more unnecessary struct keywords... --- apps/openmw/mwgui/quickkeysmenu.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.hpp b/apps/openmw/mwgui/quickkeysmenu.hpp index 6d2ab8494..431e847cb 100644 --- a/apps/openmw/mwgui/quickkeysmenu.hpp +++ b/apps/openmw/mwgui/quickkeysmenu.hpp @@ -66,8 +66,8 @@ namespace MWGui }; std::vector mKey; - struct keyData* mSelected; - struct keyData* mActivated; + keyData* mSelected; + keyData* mActivated; MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; @@ -79,7 +79,7 @@ namespace MWGui void onQuickKeyButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); - void unassign(struct keyData* key); + void unassign(keyData* key); }; class QuickKeysMenuAssign : public WindowModal From 362798bd908c41a9140b1490b70fc064c519b0b2 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 30 Jun 2018 10:20:12 +0200 Subject: [PATCH 275/282] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d644c83..55442586b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Bug #4474: No fallback when getVampireHead fails Bug #4475: Scripted animations should not cause movement Bug #4479: "Game" category on Advanced page is getting too long + Bug #4480: Segfalt in QuickKeysMenu when item no longer in inventory Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #4222: 360° screenshots Feature #4256: Implement ToggleBorders (TB) console command From 3660d55adffef8c051f0b11426e6fbb0e1eba980 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Sat, 30 Jun 2018 10:21:10 +0200 Subject: [PATCH 276/282] updated credits file --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 415b87b4e..90d25f0b6 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -63,6 +63,7 @@ Programmers Evgeniy Mineev (sandstranger) Federico Guerra (FedeWar) Fil Krynicki (filkry) + Finbar Crago (finbar-crago) Florian Weber (Florianjw) Gašper Sedej gugus/gus From 5d9035c6b224f7d23af731699bf3214571ed6ebf Mon Sep 17 00:00:00 2001 From: Finbar Crago Date: Sun, 1 Jul 2018 13:46:23 +1000 Subject: [PATCH 277/282] [Fixes #4482] Missing HandToHand on key quick key 0 (introduced in MR !11 for issue #4480) because apparently i can't count to ten... --- apps/openmw/mwgui/quickkeysmenu.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/openmw/mwgui/quickkeysmenu.cpp b/apps/openmw/mwgui/quickkeysmenu.cpp index 4bfbd4dac..badf50213 100644 --- a/apps/openmw/mwgui/quickkeysmenu.cpp +++ b/apps/openmw/mwgui/quickkeysmenu.cpp @@ -124,7 +124,7 @@ namespace MWGui while (key->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(key->button->getChildAt(0)); - if (key->index == 9) + if (key->index == 10) { key->type = Type_HandToHand; @@ -163,6 +163,10 @@ namespace MWGui assert(index != -1); mSelected = &mKey[index]; + // prevent reallocation of zero key from Type_HandToHand + if(mSelected->index == 10) + return; + // open assign dialog if (!mAssignDialog) mAssignDialog = new QuickKeysMenuAssign(this); @@ -304,7 +308,7 @@ namespace MWGui void QuickKeysMenu::activateQuickKey(int index) { - assert(index >= 1 && index <= 9); + assert(index >= 1 && index <= 10); keyData *key = &mKey[index-1]; From 7cbc4eeb492260a7c9f1aa703234d4cc9e742e02 Mon Sep 17 00:00:00 2001 From: Thunderforge Date: Sun, 1 Jul 2018 19:17:50 -0500 Subject: [PATCH 278/282] Adding missing override keywords Prevents compiler warnings such as this: ``` /Users/Will/CLionProjects/OpenMW/apps/openmw/mwgui/windowbase.hpp:65:22: warning: 'onOpen' overrides a member function but is not marked 'override' [-Winconsistent-missing-override] virtual void onOpen(); ^ /Users/Will/CLionProjects/OpenMW/apps/openmw/mwgui/windowbase.hpp:38:22: note: overridden virtual function is here virtual void onOpen() {} ^ ``` --- apps/openmw/mwgui/messagebox.hpp | 2 +- apps/openmw/mwgui/windowbase.hpp | 8 ++++---- apps/openmw/mwworld/worldimp.hpp | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index e0bcbe667..156a17e67 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -81,7 +81,7 @@ namespace MWGui MyGUI::Widget* getDefaultKeyFocus() override; - virtual bool exit() { return false; } + virtual bool exit() override { return false; } bool mMarkedToDelete; diff --git a/apps/openmw/mwgui/windowbase.hpp b/apps/openmw/mwgui/windowbase.hpp index 56901c95a..fde1f2ac9 100644 --- a/apps/openmw/mwgui/windowbase.hpp +++ b/apps/openmw/mwgui/windowbase.hpp @@ -56,15 +56,15 @@ namespace MWGui /* - * "Modal" windows cause the rest of the interface to be unaccessible while they are visible + * "Modal" windows cause the rest of the interface to be inaccessible while they are visible */ class WindowModal : public WindowBase { public: WindowModal(const std::string& parLayout); - virtual void onOpen(); - virtual void onClose(); - virtual bool exit() {return true;} + virtual void onOpen() override; + virtual void onClose() override; + virtual bool exit() override {return true;} }; /// A window that cannot be the target of a drag&drop action. diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 29bc4692c..015a8b31b 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -608,7 +608,7 @@ namespace MWWorld void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength) override; - void applyLoopingParticles(const MWWorld::Ptr& ptr); + void applyLoopingParticles(const MWWorld::Ptr& ptr) override; const std::vector& getContentFiles() const override; void breakInvisibility (const MWWorld::Ptr& actor) override; From f4330cf057fa79ce1375401b31311939db316c85 Mon Sep 17 00:00:00 2001 From: Andrei Kortunov Date: Mon, 2 Jul 2018 11:50:59 +0400 Subject: [PATCH 279/282] Editor: limit FPS in 3D preview windows (feature #3641) --- CHANGELOG.md | 1 + apps/opencs/model/prefs/state.cpp | 3 +++ apps/opencs/view/render/scenewidget.cpp | 13 +++++++++++++ 3 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39af121aa..ed21863ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Bug #4474: No fallback when getVampireHead fails Bug #4475: Scripted animations should not cause movement Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results + Feature #3641: Editor: Limit FPS in 3d preview window Feature #4222: 360° screenshots Feature #4256: Implement ToggleBorders (TB) console command Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts diff --git a/apps/opencs/model/prefs/state.cpp b/apps/opencs/model/prefs/state.cpp index e1236a0e4..a704fb825 100644 --- a/apps/opencs/model/prefs/state.cpp +++ b/apps/opencs/model/prefs/state.cpp @@ -201,6 +201,9 @@ void CSMPrefs::State::declare() declareDouble ("rotate-factor", "Free rotation factor", 0.007).setPrecision(4).setRange(0.0001, 0.1); declareCategory ("Rendering"); + declareInt ("framerate-limit", "FPS limit", 60). + setTooltip("Framerate limit in 3D preview windows. Zero value means \"unlimited\"."). + setRange(0, 10000); declareInt ("camera-fov", "Camera FOV", 90).setRange(10, 170); declareBool ("camera-ortho", "Orthographic projection for camera", false); declareInt ("camera-ortho-size", "Orthographic projection size parameter", 100). diff --git a/apps/opencs/view/render/scenewidget.cpp b/apps/opencs/view/render/scenewidget.cpp index f24a9de50..0d1780d57 100644 --- a/apps/opencs/view/render/scenewidget.cpp +++ b/apps/opencs/view/render/scenewidget.cpp @@ -151,6 +151,9 @@ CompositeViewer::CompositeViewer() connect( &mTimer, SIGNAL(timeout()), this, SLOT(update()) ); mTimer.start( 10 ); + + int frameRateLimit = CSMPrefs::get()["Rendering"]["framerate-limit"].toInt(); + setRunMaxFrameRate(frameRateLimit); } CompositeViewer &CompositeViewer::get() @@ -168,6 +171,12 @@ void CompositeViewer::update() mSimulationTime += dt; frame(mSimulationTime); + + double minFrameTime = _runMaxFrameRate > 0.0 ? 1.0 / _runMaxFrameRate : 0.0; + if (dt < minFrameTime) + { + OpenThreads::Thread::microSleep(1000*1000*(minFrameTime-dt)); + } } // --------------------------------------------------- @@ -376,6 +385,10 @@ void SceneWidget::settingChanged (const CSMPrefs::Setting *setting) { mOrbitCamControl->setConstRoll(setting->isTrue()); } + else if (*setting=="Rendering/framerate-limit") + { + CompositeViewer::get().setRunMaxFrameRate(setting->toInt()); + } else if (*setting=="Rendering/camera-fov" || *setting=="Rendering/camera-ortho" || *setting=="Rendering/camera-ortho-size") From e9cc697b60522011b53c9816bb942db6305cc869 Mon Sep 17 00:00:00 2001 From: Doc West Date: Tue, 3 Jul 2018 00:52:23 +0200 Subject: [PATCH 280/282] Sort EnumDelegate values by name --- apps/opencs/view/world/enumdelegate.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/opencs/view/world/enumdelegate.cpp b/apps/opencs/view/world/enumdelegate.cpp index 4bd40b830..c303b71ac 100644 --- a/apps/opencs/view/world/enumdelegate.cpp +++ b/apps/opencs/view/world/enumdelegate.cpp @@ -175,5 +175,16 @@ CSVWorld::CommandDelegate *CSVWorld::EnumDelegateFactory::makeDelegate ( void CSVWorld::EnumDelegateFactory::add (int value, const QString& name) { - mValues.push_back (std::make_pair (value, name)); + auto pair = std::make_pair (value, name); + + for (auto it=mValues.begin(); it!=mValues.end(); ++it) + { + if (it->second > name) + { + mValues.insert(it, pair); + return; + } + } + + mValues.push_back(std::make_pair (value, name)); } From 2a367a0c35aa8ce0072f76e995eeea4af22abf46 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 3 Jul 2018 09:09:05 +0200 Subject: [PATCH 281/282] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55442586b..ef8fe436d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ Feature #4256: Implement ToggleBorders (TB) console command Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts Feature #4345: Add equivalents for the command line commands to Launcher + Feature #4404: Editor: All EnumDelegate fields should have their items sorted alphabetically Feature #4444: Per-group KF-animation files support Feature #4466: Editor: Add option to ignore "Base" records when running verifier From 47d0321366824ec2cefb793c6b9380cb5b738589 Mon Sep 17 00:00:00 2001 From: Marc Zinnschlag Date: Tue, 3 Jul 2018 09:09:48 +0200 Subject: [PATCH 282/282] updated credits file --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index 90d25f0b6..f1e6e58ee 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -22,6 +22,7 @@ Programmers alexanderkjall Alexander Nadeau (wareya) Alexander Olofsson (Ace) + Alex S (docwest) Allofich Andrei Kortunov (akortunov) AnyOldName3