Merge branch 'master' into pathfinder_detour

pull/541/head
elsid 6 years ago
commit 49d81241db

@ -8,6 +8,7 @@
Bug #2256: Landing sound not playing when jumping immediately after landing
Bug #2274: Thin platform clips through player character instead of lifting
Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped
Bug #2446: Restore Attribute/Skill should allow restoring drained attributes
Bug #2455: Creatures attacks degrade armor
Bug #2562: Forcing AI to activate a teleport door sometimes causes a crash
Bug #2626: Resurrecting the player does not resume the game
@ -33,6 +34,7 @@
Bug #3788: GetPCInJail and GetPCTraveling do not work as in vanilla
Bug #3836: Script fails to compile when command argument contains "\n"
Bug #3876: Landscape texture painting is misaligned
Bug #3890: Magic light source attenuation is inaccurate
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 #3920: RemoveSpellEffects doesn't remove constant effects
@ -45,6 +47,7 @@
Bug #4110: Fixed undo / redo menu text losing the assigned shortcuts
Bug #4125: OpenMW logo cropped on bugtracker
Bug #4215: OpenMW shows book text after last EOL tag
Bug #4217: Fixme implementation differs from Morrowind's
Bug #4221: Characters get stuck in V-shaped terrain
Bug #4230: AiTravel package issues break some Tribunal quests
Bug #4231: Infected rats from the "Crimson Plague" quest rendered unconscious by change in Drain Fatigue functionality
@ -54,6 +57,7 @@
Bug #4274: Pre-0.43 death animations are not forward-compatible with 0.43+
Bug #4286: Scripted animations can be interrupted
Bug #4291: Non-persistent actors that started the game as dead do not play death animations
Bug #4292: CenterOnCell implementation differs from vanilla
Bug #4293: Faction members are not aware of faction ownerships in barter
Bug #4304: "Follow" not working as a second AI package
Bug #4307: World cleanup should remove dead bodies only if death animation is finished
@ -64,7 +68,7 @@
Bug #4368: Settings window ok button doesn't have key focus by default
Bug #4378: On-self absorb spells restore stats
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 #4416: Non-music files crash the game when they are tried to be played
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
@ -80,6 +84,7 @@
Bug #4459: NotCell dialogue condition doesn't support partial matches
Bug #4460: Script function "Equip" doesn't bypass beast restrictions
Bug #4461: "Open" spell from non-player caster isn't a crime
Bug #4463: %g format doesn't return more digits
Bug #4464: OpenMW keeps AiState cached storages even after we cancel AI packages
Bug #4467: Content selector: cyrillic characters are decoded incorrectly in plugin descriptions
Bug #4469: Abot Silt Striders Model turn 90 degrees on horizontal
@ -130,10 +135,12 @@
Bug #4633: Sneaking stance affects speed even if the actor is not able to crouch
Bug #4641: GetPCJumping is handled incorrectly
Bug #4644: %Name should be available for all actors, not just for NPCs
Bug #4646: Weapon force-equipment messes up ongoing attack animations
Bug #4648: Hud thinks that throwing weapons have condition
Bug #4649: Levelup fully restores health
Bug #4653: Length of non-ASCII strings is handled incorrectly in ESM reader
Bug #4654: Editor: UpdateVisitor does not initialize skeletons for animated objects
Bug #4656: Combat AI: back up behaviour is incorrect
Bug #4668: Editor: Light source color is displayed as an integer
Bug #4669: ToggleCollision should trace the player down after collision being enabled
Bug #4671: knownEffect functions should use modified Alchemy skill
@ -141,11 +148,16 @@
Bug #4674: Journal can be opened when settings window is open
Bug #4677: Crash in ESM reader when NPC record has DNAM record without DODT one
Bug #4678: Crash in ESP parser when SCVR has no variable names
Bug #4684: Spell Absorption is additive
Bug #4685: Missing sound causes an exception inside Say command
Bug #4689: Default creature soundgen entries are not used
Bug #4691: Loading bar for cell should be moved up when text is still active at bottom of screen
Feature #912: Editor: Add missing icons to UniversalId tables
Feature #1221: Editor: Creature/NPC rendering
Feature #1617: Editor: Enchantment effect record verifier
Feature #1645: Casting effects from objects
Feature #2606: Editor: Implemented (optional) case sensitive global search
Feature #2787: Use the autogenerated collision box, if the creature mesh has no predefined one
Feature #2847: Content selector: allow to copy the path to a file by using the context menu
Feature #3083: Play animation when NPC is casting spell via script
Feature #3103: Provide option for disposition to get increased by successful trade
@ -155,6 +167,7 @@
Feature #4012: Editor: Write a log file if OpenCS crashes
Feature #4222: 360° screenshots
Feature #4256: Implement ToggleBorders (TB) console command
Feature #4285: Support soundgen calls for activators
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
@ -174,6 +187,8 @@
Feature #4632: AI priority: utilize vanilla AI GMSTs for priority rating
Feature #4636: Use sTo GMST in spellmaking menu
Feature #4642: Batching potion creation
Feature #4647: Cull actors outside of AI processing range
Feature #4682: Use the collision box from basic creature mesh if the X one have no collisions
Task #2490: Don't open command prompt window on Release-mode builds automatically
Task #4545: Enable is_pod string test
Task #4605: Optimize skinning

@ -6,5 +6,5 @@ brew outdated cmake || brew upgrade cmake
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
curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-4eec887.zip -o ~/openmw-deps.zip
unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null

@ -7,6 +7,7 @@
#include <components/debug/debuglog.hpp>
#include <components/fallback/validate.hpp>
#include <components/misc/rng.hpp>
#include <components/nifosg/nifloader.hpp>
#include "model/doc/document.hpp"
@ -355,6 +356,8 @@ int CS::Editor::run()
if (mLocal.empty())
return 1;
Misc::Rng::init();
mStartup.show();
QApplication::setQuitOnLastWindowClosed (true);

@ -233,6 +233,10 @@ namespace MWBase
virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) = 0;
virtual void processChangedSettings (const std::set< std::pair<std::string, std::string> >& settings) = 0;
virtual float getActorsProcessingRange() const = 0;
/// Check if the target actor was detected by an observer
/// If the observer is a non-NPC, check all actors in AI processing distance as observers
virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) = 0;

@ -285,6 +285,8 @@ namespace MWBase
virtual void setEnemy (const MWWorld::Ptr& enemy) = 0;
virtual int getMessagesCount() const = 0;
virtual const Translation::Storage& getTranslationDataStorage() const = 0;
/// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this.

@ -268,8 +268,8 @@ namespace MWBase
///< Adjust position after load to be on ground. Must be called after model load.
/// @param force do this even if the ptr is flying
virtual void fixPosition (const MWWorld::Ptr& actor) = 0;
///< Attempt to fix position so that the Ptr is no longer inside collision geometry.
virtual void fixPosition () = 0;
///< Attempt to fix position so that the player is not stuck inside the geometry.
/// @note No-op for items in containers. Use ContainerStore::removeItem instead.
virtual void deleteObject (const MWWorld::Ptr& ptr) = 0;
@ -303,9 +303,14 @@ namespace MWBase
///< Queues movement for \a ptr (in local space), to be applied in the next call to
/// doPhysics.
virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2, bool ignoreDoors=false) = 0;
virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2, int mask) = 0;
///< cast a Ray and return true if there is an object in the ray path.
virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) = 0;
virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled) = 0;
virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 0;
virtual bool toggleCollisionMode() = 0;
///< Toggle collision mode for player. If disabled player object should ignore
/// collisions and gravity.

@ -1,6 +1,7 @@
#include "activator.hpp"
#include <components/esm/loadacti.hpp>
#include <components/misc/rng.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
@ -134,4 +135,74 @@ namespace MWClass
return MWWorld::Ptr(cell.insert(ref), &cell);
}
std::string Activator::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const
{
const std::string model = getModel(ptr); // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
std::string creatureId;
for (const ESM::Creature &iter : store.get<ESM::Creature>())
{
if (!iter.mModel.empty() && Misc::StringUtils::ciEqual(model, "meshes\\" + iter.mModel))
{
creatureId = !iter.mOriginal.empty() ? iter.mOriginal : iter.mId;
break;
}
}
int type = getSndGenTypeFromName(name);
std::vector<const ESM::SoundGenerator*> fallbacksounds;
if (!creatureId.empty())
{
std::vector<const ESM::SoundGenerator*> sounds;
for (auto sound = store.get<ESM::SoundGenerator>().begin(); sound != store.get<ESM::SoundGenerator>().end(); ++sound)
{
if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(creatureId, sound->mCreature)))
sounds.push_back(&*sound);
if (type == sound->mType && sound->mCreature.empty())
fallbacksounds.push_back(&*sound);
}
if (!sounds.empty())
return sounds[Misc::Rng::rollDice(sounds.size())]->mSound;
if (!fallbacksounds.empty())
return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound;
}
else
{
// The activator doesn't have a corresponding creature ID, but we can try to use the defaults
for (auto sound = store.get<ESM::SoundGenerator>().begin(); sound != store.get<ESM::SoundGenerator>().end(); ++sound)
if (type == sound->mType && sound->mCreature.empty())
fallbacksounds.push_back(&*sound);
if (!fallbacksounds.empty())
return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound;
}
return std::string();
}
int Activator::getSndGenTypeFromName(const std::string &name)
{
if (name == "left")
return ESM::SoundGenerator::LeftFoot;
if (name == "right")
return ESM::SoundGenerator::RightFoot;
if (name == "swimleft")
return ESM::SoundGenerator::SwimLeft;
if (name == "swimright")
return ESM::SoundGenerator::SwimRight;
if (name == "moan")
return ESM::SoundGenerator::Moan;
if (name == "roar")
return ESM::SoundGenerator::Roar;
if (name == "scream")
return ESM::SoundGenerator::Scream;
if (name == "land")
return ESM::SoundGenerator::Land;
throw std::runtime_error(std::string("Unexpected soundgen type: ")+name);
}
}

@ -10,6 +10,8 @@ namespace MWClass
virtual MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const;
static int getSndGenTypeFromName(const std::string &name);
public:
virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const;
@ -44,6 +46,8 @@ namespace MWClass
///< Whether or not to use animated variant of model (default false)
virtual bool isActivator() const;
virtual std::string getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const;
};
}

@ -297,7 +297,7 @@ namespace MWClass
{
const MWWorld::InventoryStore& invStore = npc.getClass().getInventoryStore(npc);
if (ptr.getCellRef().getCharge() == 0)
if (getItemHealth(ptr) == 0)
return std::make_pair(0, "#{sInventoryMessage1}");
// slots that this item can be equipped in

@ -632,25 +632,27 @@ namespace MWClass
if(type >= 0)
{
std::vector<const ESM::SoundGenerator*> sounds;
std::vector<const ESM::SoundGenerator*> fallbacksounds;
MWWorld::LiveCellRef<ESM::Creature>* ref = ptr.get<ESM::Creature>();
const std::string& ourId = (ref->mBase->mOriginal.empty()) ? ptr.getCellRef().getRefId() : ref->mBase->mOriginal;
MWWorld::Store<ESM::SoundGenerator>::iterator sound = store.begin();
while(sound != store.end())
while (sound != store.end())
{
if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(ourId, sound->mCreature)))
sounds.push_back(&*sound);
if (type == sound->mType && sound->mCreature.empty())
fallbacksounds.push_back(&*sound);
++sound;
}
if(!sounds.empty())
if (!sounds.empty())
return sounds[Misc::Rng::rollDice(sounds.size())]->mSound;
if (!fallbacksounds.empty())
return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound;
}
if (type == ESM::SoundGenerator::Land)
return "Body Fall Large";
return "";
}
@ -688,9 +690,9 @@ namespace MWClass
MWBase::World *world = MWBase::Environment::get().getWorld();
osg::Vec3f pos(ptr.getRefData().getPosition().asVec3());
if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr))
return 2;
return ESM::SoundGenerator::SwimLeft;
if(world->isOnGround(ptr))
return 0;
return ESM::SoundGenerator::LeftFoot;
return -1;
}
if(name == "right")
@ -698,23 +700,23 @@ namespace MWClass
MWBase::World *world = MWBase::Environment::get().getWorld();
osg::Vec3f pos(ptr.getRefData().getPosition().asVec3());
if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr))
return 3;
return ESM::SoundGenerator::SwimRight;
if(world->isOnGround(ptr))
return 1;
return ESM::SoundGenerator::RightFoot;
return -1;
}
if(name == "swimleft")
return 2;
return ESM::SoundGenerator::SwimLeft;
if(name == "swimright")
return 3;
return ESM::SoundGenerator::SwimRight;
if(name == "moan")
return 4;
return ESM::SoundGenerator::Moan;
if(name == "roar")
return 5;
return ESM::SoundGenerator::Roar;
if(name == "scream")
return 6;
return ESM::SoundGenerator::Scream;
if(name == "land")
return 7;
return ESM::SoundGenerator::Land;
throw std::runtime_error(std::string("Unexpected soundgen type: ")+name);
}

@ -1148,9 +1148,7 @@ namespace MWClass
const bool hasHealth = it->getClass().hasItemHealth(*it);
if (hasHealth)
{
int armorHealth = it->getClass().getItemHealth(*it);
int armorMaxHealth = it->getClass().getItemMaxHealth(*it);
ratings[i] *= (float(armorHealth) / armorMaxHealth);
ratings[i] *= it->getClass().getItemNormalizedHealth(*it);
}
}
}
@ -1242,15 +1240,9 @@ namespace MWClass
return "";
}
// Morrowind ignores land soundgen for NPCs
if(name == "land")
{
MWBase::World *world = MWBase::Environment::get().getWorld();
osg::Vec3f pos(ptr.getRefData().getPosition().asVec3());
if (world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr))
return "DefaultLandWater";
return "DefaultLand";
}
return "";
if(name == "swimleft")
return "Swim Left";
if(name == "swimright")

@ -383,7 +383,7 @@ namespace MWClass
std::pair<int, std::string> Weapon::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const
{
if (hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0)
if (hasItemHealth(ptr) && getItemHealth(ptr) == 0)
return std::make_pair(0, "#{sInventoryMessage1}");
// Do not allow equip weapons from inventory during attack

@ -96,7 +96,7 @@ namespace MWGui
Log(Debug::Warning) << "Warning: no splash screens found!";
}
void LoadingScreen::setLabel(const std::string &label, bool important)
void LoadingScreen::setLabel(const std::string &label, bool important, bool center)
{
mImportantLabel = important;
@ -105,7 +105,11 @@ namespace MWGui
MyGUI::IntSize size(mLoadingText->getTextSize().width+padding, mLoadingBox->getHeight());
size.width = std::max(300, size.width);
mLoadingBox->setSize(size);
mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mLoadingBox->getTop());
if (center)
mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight()/2 - mLoadingBox->getHeight()/2);
else
mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight() - mLoadingBox->getHeight() - 8);
}
void LoadingScreen::setVisible(bool visible)

@ -34,7 +34,7 @@ namespace MWGui
virtual ~LoadingScreen();
/// Overridden from Loading::Listener, see the Loading::Listener documentation for usage details
virtual void setLabel (const std::string& label, bool important);
virtual void setLabel (const std::string& label, bool important, bool center);
virtual void loadingOn(bool visible=true);
virtual void loadingOff();
virtual void setProgressRange (size_t range);

@ -51,7 +51,7 @@ void MerchantRepair::setPtr(const MWWorld::Ptr &actor)
{
int maxDurability = iter->getClass().getItemMaxHealth(*iter);
int durability = iter->getClass().getItemHealth(*iter);
if (maxDurability == durability)
if (maxDurability == durability || maxDurability == 0)
continue;
int basePrice = iter->getClass().getValue(*iter);

@ -35,6 +35,11 @@ namespace MWGui
}
}
int MessageBoxManager::getMessagesCount()
{
return mMessageBoxes.size();
}
void MessageBoxManager::clear()
{
if (mInterMessageBoxe)

@ -28,6 +28,8 @@ namespace MWGui
bool createInteractiveMessageBox (const std::string& message, const std::vector<std::string>& buttons);
bool isInteractiveMessageBox ();
int getMessagesCount();
const InteractiveMessageBox* getInteractiveMessageBox() const { return mInterMessageBoxe; }
/// Remove all message boxes

@ -20,6 +20,7 @@
#include "../mwbase/world.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/inputmanager.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "confirmationdialog.hpp"
@ -437,6 +438,7 @@ namespace MWGui
MWBase::Environment::get().getSoundManager()->processChangedSettings(changed);
MWBase::Environment::get().getWindowManager()->processChangedSettings(changed);
MWBase::Environment::get().getInputManager()->processChangedSettings(changed);
MWBase::Environment::get().getMechanicsManager()->processChangedSettings(changed);
}
void SettingsWindow::onKeyboardSwitchClicked(MyGUI::Widget* _sender)

@ -91,8 +91,7 @@ namespace
if (ench->mData.mType == ESM::Enchantment::ConstantEffect)
leftChargePercent = 101;
else
leftChargePercent = (left.mBase.getCellRef().getEnchantmentCharge() == -1) ? 100
: static_cast<int>(left.mBase.getCellRef().getEnchantmentCharge() / static_cast<float>(ench->mData.mCharge) * 100);
leftChargePercent = static_cast<int>(left.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100);
}
}
@ -104,8 +103,7 @@ namespace
if (ench->mData.mType == ESM::Enchantment::ConstantEffect)
rightChargePercent = 101;
else
rightChargePercent = (right.mBase.getCellRef().getEnchantmentCharge() == -1) ? 100
: static_cast<int>(right.mBase.getCellRef().getEnchantmentCharge() / static_cast<float>(ench->mData.mCharge) * 100);
rightChargePercent = static_cast<int>(right.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100);
}
}

@ -35,8 +35,7 @@ namespace
float price = static_cast<float>(item.getClass().getValue(item));
if (item.getClass().hasItemHealth(item))
{
price *= item.getClass().getItemHealth(item);
price /= item.getClass().getItemMaxHealth(item);
price *= item.getClass().getItemNormalizedHealth(item);
}
return static_cast<int>(price * count);
}

@ -1369,8 +1369,7 @@ namespace MWGui
const ESM::Enchantment* ench = mStore->get<ESM::Enchantment>()
.find(item.getClass().getEnchantment(item));
int chargePercent = (item.getCellRef().getEnchantmentCharge() == -1) ? 100
: static_cast<int>(item.getCellRef().getEnchantmentCharge() / static_cast<float>(ench->mData.mCharge) * 100);
int chargePercent = static_cast<int>(item.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100);
mHud->setSelectedEnchantItem(item, chargePercent);
mSpellWindow->setTitle(item.getClass().getName(item));
}
@ -1386,7 +1385,7 @@ namespace MWGui
int durabilityPercent = 100;
if (item.getClass().hasItemHealth(item))
{
durabilityPercent = static_cast<int>(item.getClass().getItemHealth(item) / static_cast<float>(item.getClass().getItemMaxHealth(item)) * 100);
durabilityPercent = static_cast<int>(item.getClass().getItemNormalizedHealth(item) * 100);
}
mHud->setSelectedWeapon(item, durabilityPercent);
mInventoryWindow->setTitle(item.getClass().getName(item));
@ -1671,6 +1670,15 @@ namespace MWGui
mHud->setEnemy(enemy);
}
int WindowManager::getMessagesCount() const
{
int count = 0;
if (mMessageBoxManager)
count = mMessageBoxManager->getMessagesCount();
return count;
}
Loading::Listener* WindowManager::getLoadingScreen()
{
return mLoadingScreen;

@ -316,6 +316,8 @@ namespace MWGui
virtual void setEnemy (const MWWorld::Ptr& enemy);
virtual int getMessagesCount() const;
virtual const Translation::Storage& getTranslationDataStorage() const;
void onSoulgemDialogButtonPressed (int button);

@ -147,9 +147,6 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float
namespace MWMechanics
{
const float aiProcessingDistance = 7168;
const float sqrAiProcessingDistance = aiProcessingDistance*aiProcessingDistance;
class SoulTrap : public MWMechanics::EffectSourceVisitor
{
MWWorld::Ptr mCreature;
@ -364,7 +361,8 @@ namespace MWMechanics
const ESM::Position& actor1Pos = actor1.getRefData().getPosition();
const ESM::Position& actor2Pos = actor2.getRefData().getPosition();
float sqrDist = (actor1Pos.asVec3() - actor2Pos.asVec3()).length2();
if (sqrDist > sqrAiProcessingDistance)
if (sqrDist > mActorsProcessingRange*mActorsProcessingRange)
return;
// No combat for totally static creatures
@ -1130,8 +1128,11 @@ namespace MWMechanics
}
}
Actors::Actors() {
Actors::Actors()
{
mTimerDisposeSummonsCorpses = 0.2f; // We should add a delay between summoned creature death and its corpse despawning
updateProcessingRange();
}
Actors::~Actors()
@ -1139,6 +1140,23 @@ namespace MWMechanics
clear();
}
float Actors::getProcessingRange() const
{
return mActorsProcessingRange;
}
void Actors::updateProcessingRange()
{
// We have to cap it since using high values (larger than 7168) will make some quests harder or impossible to complete (bug #1876)
static const float maxProcessingRange = 7168.f;
static const float minProcessingRange = maxProcessingRange / 2.f;
float actorsProcessingRange = Settings::Manager::getFloat("actors processing range", "Game");
actorsProcessingRange = std::min(actorsProcessingRange, maxProcessingRange);
actorsProcessingRange = std::max(actorsProcessingRange, minProcessingRange);
mActorsProcessingRange = actorsProcessingRange;
}
void Actors::addActor (const MWWorld::Ptr& ptr, bool updateImmediately)
{
removeActor(ptr);
@ -1184,7 +1202,7 @@ namespace MWMechanics
// Otherwise check if any actor in AI processing range sees the target actor
std::vector<MWWorld::Ptr> actors;
osg::Vec3f position (actor.getRefData().getPosition().asVec3());
getObjectsInRange(position, aiProcessingDistance, actors);
getObjectsInRange(position, mActorsProcessingRange, actors);
for(std::vector<MWWorld::Ptr>::iterator it = actors.begin(); it != actors.end(); ++it)
{
if (*it == actor)
@ -1242,7 +1260,7 @@ namespace MWMechanics
{
if (iter->first == player) continue;
bool inProcessingRange = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() <= sqrAiProcessingDistance;
bool inProcessingRange = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2() <= mActorsProcessingRange*mActorsProcessingRange;
if (inProcessingRange)
{
MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first);
@ -1288,7 +1306,8 @@ namespace MWMechanics
if (timerUpdateEquippedLight >= updateEquippedLightInterval) timerUpdateEquippedLight = 0;
// show torches only when there are darkness and no precipitations
bool showTorches = MWBase::Environment::get().getWorld()->useTorches();
MWBase::World* world = MWBase::Environment::get().getWorld();
bool showTorches = world->useTorches();
MWWorld::Ptr player = getPlayer();
const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3();
@ -1301,7 +1320,7 @@ namespace MWMechanics
int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId();
if (attackedByPlayerId != -1)
{
const MWWorld::Ptr playerHitAttemptActor = MWBase::Environment::get().getWorld()->searchPtrViaActorId(attackedByPlayerId);
const MWWorld::Ptr playerHitAttemptActor = world->searchPtrViaActorId(attackedByPlayerId);
if (!playerHitAttemptActor.isInCell())
player.getClass().getCreatureStats(player).setHitAttemptActorId(-1);
@ -1314,14 +1333,11 @@ namespace MWMechanics
CharacterController* ctrl = iter->second->getCharacterController();
float distSqr = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2();
// AI processing is only done within distance of 7168 units to the player. Note the "AI distance" slider doesn't affect this
// (it only does some throttling for targets beyond the "AI distance", so doesn't give any guarantees as to whether AI will be enabled or not)
// This distance could be made configurable later, but the setting must be marked with a big warning:
// using higher values will make a quest in Bloodmoon harder or impossible to complete (bug #1876)
bool inProcessingRange = distSqr <= sqrAiProcessingDistance;
// AI processing is only done within given distance to the player.
bool inProcessingRange = distSqr <= mActorsProcessingRange*mActorsProcessingRange;
if (isPlayer)
ctrl->setAttackingOrSpell(MWBase::Environment::get().getWorld()->getPlayer().getAttackingOrSpell());
ctrl->setAttackingOrSpell(world->getPlayer().getAttackingOrSpell());
// If dead or no longer in combat, no longer store any actors who attempted to hit us. Also remove for the player.
if (iter->first != player && (iter->first.getClass().getCreatureStats(iter->first).isDead()
@ -1335,10 +1351,10 @@ namespace MWMechanics
if (!iter->first.getClass().getCreatureStats(iter->first).isDead())
{
bool cellChanged = MWBase::Environment::get().getWorld()->hasCellChanged();
bool cellChanged = world->hasCellChanged();
MWWorld::Ptr actor = iter->first; // make a copy of the map key to avoid it being invalidated when the player teleports
updateActor(actor, duration);
if (!cellChanged && MWBase::Environment::get().getWorld()->hasCellChanged())
if (!cellChanged && world->hasCellChanged())
{
return; // for now abort update of the old cell when cell changes by teleportation magic effect
// a better solution might be to apply cell changes at the end of the frame
@ -1363,7 +1379,7 @@ namespace MWMechanics
MWWorld::Ptr headTrackTarget;
MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first);
bool firstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson();
bool firstPersonPlayer = isPlayer && world->isFirstPerson();
// 1. Unconsious actor can not track target
// 2. Actors in combat and pursue mode do not bother to headtrack
@ -1423,27 +1439,25 @@ namespace MWMechanics
CharacterController* playerCharacter = nullptr;
for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter)
{
const float animationDistance = aiProcessingDistance + 400; // Slightly larger than AI distance so there is time to switch back to the idle animation.
const float distSqr = (playerPos - iter->first.getRefData().getPosition().asVec3()).length2();
const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length();
bool isPlayer = iter->first == player;
bool inAnimationRange = isPlayer || (animationDistance == 0 || distSqr <= animationDistance*animationDistance);
bool inRange = isPlayer || dist <= mActorsProcessingRange;
int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower)
if (isPlayer)
activeFlag = 2;
int active = inAnimationRange ? activeFlag : 0;
bool canFly = iter->first.getClass().canFly(iter->first);
if (canFly)
{
// Keep animating flying creatures so they don't just hover in-air
inAnimationRange = true;
active = std::max(1, active);
}
int active = inRange ? activeFlag : 0;
CharacterController* ctrl = iter->second->getCharacterController();
ctrl->setActive(active);
if (!inAnimationRange)
if (!inRange)
{
iter->first.getRefData().getBaseNode()->setNodeMask(0);
world->setActorCollisionMode(iter->first, false);
continue;
}
else if (!isPlayer)
iter->first.getRefData().getBaseNode()->setNodeMask(1<<3);
if (iter->first.getClass().getCreatureStats(iter->first).isParalyzed())
ctrl->skipAnim();
@ -1455,11 +1469,28 @@ namespace MWMechanics
playerCharacter = ctrl;
continue;
}
world->setActorCollisionMode(iter->first, true);
ctrl->update(duration);
// Fade away actors on large distance (>90% of actor's processing distance)
float visibilityRatio = 1.0;
float fadeStartDistance = mActorsProcessingRange*0.9f;
float fadeEndDistance = mActorsProcessingRange;
float fadeRatio = (dist - fadeStartDistance)/(fadeEndDistance - fadeStartDistance);
if (fadeRatio > 0)
visibilityRatio -= std::max(0.f, fadeRatio);
visibilityRatio = std::min(1.f, visibilityRatio);
ctrl->setVisibility(visibilityRatio);
}
if (playerCharacter)
{
playerCharacter->update(duration);
playerCharacter->setVisibility(1.f);
}
for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter)
{
@ -1671,7 +1702,7 @@ namespace MWMechanics
restoreDynamicStats(iter->first, sleep);
if ((!iter->first.getRefData().getBaseNode()) ||
(playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > sqrAiProcessingDistance)
(playerPos - iter->first.getRefData().getPosition().asVec3()).length2() > mActorsProcessingRange*mActorsProcessingRange)
continue;
adjustMagicEffects (iter->first);
@ -1915,7 +1946,7 @@ namespace MWMechanics
std::list<MWWorld::Ptr> list;
std::vector<MWWorld::Ptr> neighbors;
osg::Vec3f position (actor.getRefData().getPosition().asVec3());
getObjectsInRange(position, aiProcessingDistance, neighbors);
getObjectsInRange(position, mActorsProcessingRange, neighbors);
for(auto neighbor = neighbors.begin(); neighbor != neighbors.end(); ++neighbor)
{
if (*neighbor == actor)
@ -1936,7 +1967,7 @@ namespace MWMechanics
std::list<MWWorld::Ptr> list;
std::vector<MWWorld::Ptr> neighbors;
osg::Vec3f position (actor.getRefData().getPosition().asVec3());
getObjectsInRange(position, aiProcessingDistance, neighbors);
getObjectsInRange(position, mActorsProcessingRange, neighbors);
std::set<MWWorld::Ptr> followers;
getActorsFollowing(actor, followers);

@ -65,6 +65,9 @@ namespace MWMechanics
/// paused we may want to do it manually (after equipping permanent enchantment)
void updateMagicEffects (const MWWorld::Ptr& ptr);
void updateProcessingRange();
float getProcessingRange() const;
void addActor (const MWWorld::Ptr& ptr, bool updateImmediately=false);
///< Register an actor for stats management
///
@ -168,6 +171,7 @@ namespace MWMechanics
private:
PtrActorMap mActors;
float mTimerDisposeSummonsCorpses;
float mActorsProcessingRange;
};
}

@ -4,6 +4,10 @@
#include <components/esm/aisequence.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
#include "../mwphysics/collisiontype.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
@ -456,7 +460,48 @@ namespace MWMechanics
mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
mCombatMove = true;
}
else if (isDistantCombat)
{
// Backing up behaviour
// Actor backs up slightly further away than opponent's weapon range
// (in vanilla - only as far as oponent's weapon range),
// or not at all if opponent is using a ranged weapon
if (targetUsesRanged || distToTarget > rangeAttackOfTarget*1.5) // Don't back up if the target is wielding ranged weapon
return;
// actor should not back up into water
if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.5f))
return;
int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Door;
// Actor can not back up if there is no free space behind
// Currently we take the 35% of actor's height from the ground as vector height.
// This approach allows us to detect small obstacles (e.g. crates) and curved walls.
osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor);
osg::Vec3f pos = actor.getRefData().getPosition().asVec3();
osg::Vec3f source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z());
osg::Vec3f fallbackDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,-1,0);
osg::Vec3f destination = source + fallbackDirection * (halfExtents.y() + 16);
bool isObstacleDetected = MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask);
if (isObstacleDetected)
return;
// Check if there is nothing behind - probably actor is near cliff.
// A current approach: cast ray 1.5-yard ray down in 1.5 yard behind actor from 35% of actor's height.
// If we did not hit anything, there is a cliff behind actor.
source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z()) + fallbackDirection * (halfExtents.y() + 96);
destination = source - osg::Vec3f(0, 0, 0.75f * halfExtents.z() + 96);
bool isCliffDetected = !MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask);
if (isCliffDetected)
return;
mMovement.mPosition[1] = -1;
}
// dodge movements (for NPCs and bipedal creatures)
// Note: do not use for ranged combat yet since in couple with back up behaviour can move actor out of cliff
else if (actor.getClass().isBipedal(actor))
{
// apply sideway movement (kind of dodging) with some probability
@ -468,20 +513,6 @@ namespace MWMechanics
mCombatMove = true;
}
}
// Backing up behaviour
// Actor backs up slightly further away than opponent's weapon range
// (in vanilla - only as far as oponent's weapon range),
// or not at all if opponent is using a ranged weapon
if (isDistantCombat)
{
// actor should not back up into water
if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.5f))
return;
if (!targetUsesRanged && distToTarget <= rangeAttackOfTarget*1.5) // Don't back up if the target is wielding ranged weapon
mMovement.mPosition[1] = -1;
}
}
void AiCombatStorage::updateCombatMove(float duration)

@ -110,7 +110,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
}
/// Stops the actor when it gets too close to a unloaded cell
//... At current time, this test is unnecessary. AI shuts down when actor is more than 7168
//... At current time, this test is unnecessary. AI shuts down when actor is more than "actors processing range" setting value
//... units from player, and exterior cells are 8192 units long and wide.
//... But AI processing distance may increase in the future.
if (isNearInactiveCell(position))
@ -356,7 +356,7 @@ bool MWMechanics::AiPackage::isNearInactiveCell(osg::Vec3f position)
// currently assumes 3 x 3 grid for exterior cells, with player at center cell.
// ToDo: (Maybe) use "exterior cell load distance" setting to get count of actual active cells
// While AI Process distance is 7168, AI shuts down actors before they reach edges of 3 x 3 grid.
// AI shuts down actors before they reach edges of 3 x 3 grid.
const float distanceFromEdge = 200.0;
float minThreshold = (-1.0f * ESM::Land::REAL_SIZE) + distanceFromEdge;
float maxThreshold = (2.0f * ESM::Land::REAL_SIZE) - distanceFromEdge;

@ -3,8 +3,9 @@
#include <components/esm/aisequence.hpp>
#include <components/esm/loadcell.hpp>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/cellstore.hpp"
@ -21,7 +22,8 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2)
// Maximum travel distance for vanilla compatibility.
// Was likely meant to prevent NPCs walking into non-loaded exterior cells, but for some reason is used in interior cells as well.
// We can make this configurable at some point, but the default *must* be the below value. Anything else will break shoddily-written content (*cough* MW *cough*) in bizarre ways.
return (pos1 - pos2).length2() <= 7168*7168;
bool aiDistance = MWBase::Environment::get().getMechanicsManager()->getActorsProcessingRange();
return (pos1 - pos2).length2() <= aiDistance*aiDistance;
}
}

@ -30,6 +30,7 @@
#include "../mwrender/animation.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/windowmanager.hpp"
@ -979,17 +980,13 @@ void CharacterController::handleTextKey(const std::string &groupname, const std:
}
}
if (soundgen == "land") // Morrowind ignores land soundgen for some reason
return;
std::string sound = mPtr.getClass().getSoundIdFromSndGen(mPtr, soundgen);
if(!sound.empty())
{
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
if(soundgen == "left" || soundgen == "right")
// NB: landing sound is not played for NPCs here
if(soundgen == "left" || soundgen == "right" || soundgen == "land")
{
// 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);
}
@ -1282,6 +1279,18 @@ bool CharacterController::updateWeaponState(CharacterState& idle)
bool isStillWeapon = weaptype > WeapType_HandToHand && weaptype < WeapType_Spell &&
mWeaponType > WeapType_HandToHand && mWeaponType < WeapType_Spell;
// If the current weapon type was changed in the middle of attack (e.g. by Equip console command or when bound spell expires),
// we should force actor to the "weapon equipped" state, interrupt attack and update animations.
if (isStillWeapon && mWeaponType != weaptype && mUpperBodyState > UpperCharState_WeapEquiped)
{
forcestateupdate = true;
mUpperBodyState = UpperCharState_WeapEquiped;
mAttackingOrSpell = false;
mAnimation->disable(mCurrentWeapon);
if (mPtr == getPlayer())
MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false);
}
if(!isKnockedOut() && !isKnockedDown() && !isRecovery())
{
std::string weapgroup;
@ -1841,7 +1850,7 @@ void CharacterController::updateAnimQueue()
mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1);
}
void CharacterController::update(float duration)
void CharacterController::update(float duration, bool animationOnly)
{
MWBase::World *world = MWBase::Environment::get().getWorld();
const MWWorld::Class &cls = mPtr.getClass();
@ -2071,11 +2080,17 @@ void CharacterController::update(float duration)
}
}
// Play landing sound
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
std::string sound = cls.getSoundIdFromSndGen(mPtr, "land");
if (!sound.empty())
// Play landing sound for NPCs
if (mPtr.getClass().isNpc())
{
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
std::string sound = "DefaultLand";
osg::Vec3f pos(mPtr.getRefData().getPosition().asVec3());
if (world->isUnderwater(mPtr.getCell(), pos) || world->isWalkingOnWater(mPtr))
sound = "DefaultLandWater";
sndMgr->playSound3D(mPtr, sound, 1.f, 1.f, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal);
}
}
else
{
@ -2221,10 +2236,10 @@ void CharacterController::update(float duration)
world->rotateObject(mPtr, rot.x(), rot.y(), 0.0f, true);
}
if (!mMovementAnimationControlled)
if (!animationOnly && !mMovementAnimationControlled)
world->queueMovement(mPtr, vec);
}
else
else if (!animationOnly)
// We must always queue movement, even if there is none, to apply gravity.
world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
@ -2247,7 +2262,8 @@ void CharacterController::update(float duration)
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));
if (!animationOnly)
world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
}
bool isPersist = isPersistentAnimPlaying();
@ -2281,7 +2297,7 @@ void CharacterController::update(float duration)
moved.z() = 1.0;
// Update movement
if(mMovementAnimationControlled && mPtr.getClass().isActor())
if(!animationOnly && mMovementAnimationControlled && mPtr.getClass().isActor())
world->queueMovement(mPtr, moved);
mSkipAnim = false;
@ -2530,20 +2546,6 @@ 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);
@ -2552,6 +2554,32 @@ void CharacterController::updateMagicEffects()
mAnimation->setLightEffect(light);
}
void CharacterController::setVisibility(float visibility)
{
// We should take actor's invisibility in account
if (mPtr.getClass().isActor())
{
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);
}
visibility = std::min(visibility, alpha);
}
// TODO: implement a dithering shader rather than just change object transparency.
mAnimation->setAlpha(visibility);
}
void CharacterController::setAttackTypeBasedOnMovement()
{
float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition;

@ -257,7 +257,7 @@ public:
void updatePtr(const MWWorld::Ptr &ptr);
void update(float duration);
void update(float duration, bool animationOnly=false);
void persistAnimationState();
void unpersistAnimationState();
@ -292,6 +292,7 @@ public:
bool isTurning() const;
bool isAttackingOrSpell() const;
void setVisibility(float visibility);
void setAttackingOrSpell(bool attackingOrSpell);
void castSpell(const std::string spellId, bool manualSpell=false);
void setAIAttackType(const std::string& attackType);

@ -371,11 +371,9 @@ namespace MWMechanics
return;
const bool weaphashealth = weapon.getClass().hasItemHealth(weapon);
if(weaphashealth)
if (weaphashealth)
{
int weaphealth = weapon.getClass().getItemHealth(weapon);
int weapmaxhealth = weapon.getClass().getItemMaxHealth(weapon);
damage *= (float(weaphealth) / weapmaxhealth);
damage *= weapon.getClass().getItemNormalizedHealth(weapon);
}
static const float fDamageStrengthBase = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()

@ -17,6 +17,7 @@
#include "../mwworld/ptr.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/dialoguemanager.hpp"
@ -431,6 +432,29 @@ namespace MWMechanics
mObjects.update(duration, paused);
}
void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector &changed)
{
for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it)
{
if (it->first == "Game" && it->second == "actors processing range")
{
int state = MWBase::Environment::get().getStateManager()->getState();
if (state != MWBase::StateManager::State_Running)
continue;
mActors.updateProcessingRange();
// Update mechanics for new processing range immediately
update(0.f, false);
}
}
}
float MechanicsManager::getActorsProcessingRange() const
{
return mActors.getProcessingRange();
}
bool MechanicsManager::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer)
{
return mActors.isActorDetected(actor, observer);

@ -1,6 +1,8 @@
#ifndef GAME_MWMECHANICS_MECHANICSMANAGERIMP_H
#define GAME_MWMECHANICS_MECHANICSMANAGERIMP_H
#include <components/settings/settings.hpp>
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwworld/ptr.hpp"
@ -206,6 +208,10 @@ namespace MWMechanics
virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false);
void processChangedSettings(const Settings::CategorySettingVector& settings) override;
virtual float getActorsProcessingRange() const;
/// Check if the target actor was detected by an observer
/// If the observer is a non-NPC, check all actors in AI processing distance as observers
virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer);

@ -11,6 +11,8 @@
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwphysics/collisiontype.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
@ -196,8 +198,9 @@ namespace MWMechanics
converter.toWorld(temp);
// Add Z offset since path node can overlap with other objects.
// Also ignore doors in raytesting.
const int mask = MWPhysics::CollisionType_World;
bool isPathClear = !MWBase::Environment::get().getWorld()->castRay(
startPoint.x(), startPoint.y(), startPoint.z() + 16, temp.mX, temp.mY, temp.mZ + 16, true);
startPoint.x(), startPoint.y(), startPoint.z() + 16, temp.mX, temp.mY, temp.mZ + 16, mask);
if (isPathClear)
path.pop_front();
}

@ -324,6 +324,34 @@ namespace MWMechanics
return true;
}
class GetAbsorptionProbability : public MWMechanics::EffectSourceVisitor
{
public:
float mProbability;
GetAbsorptionProbability(const MWWorld::Ptr& actor)
: mProbability(0.f){}
virtual void visit (MWMechanics::EffectKey key,
const std::string& sourceName, const std::string& sourceId, int casterActorId,
float magnitude, float remainingTime = -1, float totalTime = -1)
{
if (key.mId == ESM::MagicEffect::SpellAbsorption)
{
if (mProbability == 0.f)
mProbability = magnitude / 100;
else
{
// If there are different sources of SpellAbsorption effect, multiply failing probability for all effects.
// Real absorption probability will be the (1 - total fail chance) in this case.
float failProbability = 1.f - mProbability;
failProbability *= 1.f - magnitude / 100;
mProbability = 1.f - failProbability;
}
}
}
};
CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool manualSpell)
: mCaster(caster)
, mTarget(target)
@ -444,22 +472,31 @@ namespace MWMechanics
MWBase::Environment::get().getWindowManager()->setEnemy(target);
// Try absorbing if it's a spell
// NOTE: Vanilla does this once per spell absorption effect source instead of adding the % from all sources together, not sure
// if that is worth replicating.
// Unlike Reflect, this is done once per spell absorption effect source
bool absorbed = false;
if (spell && caster != target && target.getClass().isActor())
{
float absorb = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude();
absorbed = (Misc::Rng::roll0to99() < absorb);
if (absorbed)
CreatureStats& stats = target.getClass().getCreatureStats(target);
if (stats.getMagicEffects().get(ESM::MagicEffect::SpellAbsorption).getMagnitude() > 0.f)
{
const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_Absorb");
MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect(
"meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, "");
// Magicka is increased by cost of spell
DynamicStat<float> magicka = target.getClass().getCreatureStats(target).getMagicka();
magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost);
target.getClass().getCreatureStats(target).setMagicka(magicka);
GetAbsorptionProbability check(target);
stats.getActiveSpells().visitEffectSources(check);
stats.getSpells().visitEffectSources(check);
if (target.getClass().hasInventoryStore(target))
target.getClass().getInventoryStore(target).visitEffectSources(check);
int absorb = check.mProbability * 100;
absorbed = (Misc::Rng::roll0to99() < absorb);
if (absorbed)
{
const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_Absorb");
MWBase::Environment::get().getWorld()->getAnimation(target)->addEffect(
"meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, "");
// Magicka is increased by cost of spell
DynamicStat<float> magicka = stats.getMagicka();
magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost);
stats.setMagicka(magicka);
}
}
}

@ -6,6 +6,7 @@
#include <components/sceneutil/positionattitudetransform.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/debug/debuglog.hpp>
#include "../mwworld/class.hpp"
@ -28,6 +29,31 @@ Actor::Actor(const MWWorld::Ptr& ptr, osg::ref_ptr<const Resource::BulletShape>
mHalfExtents = shape->mCollisionBoxHalfExtents;
mMeshTranslation = shape->mCollisionBoxTranslate;
// We can not create actor without collisions - he will fall through the ground.
// In this case we should autogenerate collision box based on mesh shape
// (NPCs have bodyparts and use a different approach)
if (!ptr.getClass().isNpc() && mHalfExtents.length2() == 0.f)
{
const Resource::BulletShape* collisionShape = shape.get();
if (collisionShape && collisionShape->mCollisionShape)
{
btTransform transform;
transform.setIdentity();
btVector3 min;
btVector3 max;
collisionShape->mCollisionShape->getAabb(transform, min, max);
mHalfExtents.x() = (max[0] - min[0])/2.f;
mHalfExtents.y() = (max[1] - min[1])/2.f;
mHalfExtents.z() = (max[2] - min[2])/2.f;
mMeshTranslation = osg::Vec3f(0.f, 0.f, mHalfExtents.z());
}
if (mHalfExtents.length2() == 0.f)
Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" << ptr.getCellRef().getRefId() << "\".";
}
// Use capsule shape only if base is square (nonuniform scaling apparently doesn't work on it)
if (std::abs(mHalfExtents.x()-mHalfExtents.y())<mHalfExtents.x()*0.05 && mHalfExtents.z() >= mHalfExtents.x())
{

@ -1187,6 +1187,16 @@ namespace MWPhysics
if (!shape)
return;
// Try to get shape from basic model as fallback for creatures
if (!ptr.getClass().isNpc() && shape->mCollisionBoxHalfExtents.length2() == 0)
{
const std::string fallbackModel = ptr.getClass().getModel(ptr);
if (fallbackModel != mesh)
{
shape = mShapeManager->getShape(fallbackModel);
}
}
Actor* actor = new Actor(ptr, shape, mCollisionWorld);
mActors.insert(std::make_pair(ptr, actor));
}
@ -1206,6 +1216,33 @@ namespace MWPhysics
return false;
}
void PhysicsSystem::setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled)
{
ActorMap::iterator found = mActors.find(ptr);
if (found != mActors.end())
{
bool cmode = found->second->getCollisionMode();
if (cmode == enabled)
return;
cmode = enabled;
found->second->enableCollisionMode(cmode);
found->second->enableCollisionBody(cmode);
}
}
bool PhysicsSystem::isActorCollisionEnabled(const MWWorld::Ptr& ptr)
{
ActorMap::iterator found = mActors.find(ptr);
if (found != mActors.end())
{
bool cmode = found->second->getCollisionMode();
return cmode;
}
return false;
}
void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr &ptr, const osg::Vec3f &movement)
{
PtrVelocityList::iterator iter = mMovementQueue.begin();

@ -92,6 +92,8 @@ namespace MWPhysics
const HeightField* getHeightField(int x, int y) const;
bool toggleCollisionMode();
bool isActorCollisionEnabled(const MWWorld::Ptr& ptr);
void setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled);
void stepSimulation(float dt);
void debugDraw();

@ -15,13 +15,13 @@
#include <components/debug/debuglog.hpp>
#include <components/nifosg/nifloader.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/resource/keyframemanager.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/misc/constants.hpp>
#include <components/nifosg/nifloader.hpp> // KeyframeHolder
#include <components/nifosg/controller.hpp>
@ -190,12 +190,6 @@ namespace
{
}
RemoveFinishedCallbackVisitor(int effectId)
: RemoveVisitor()
, mHasMagicEffects(false)
{
}
virtual void apply(osg::Node &node)
{
traverse(node);
@ -228,9 +222,6 @@ namespace
virtual void apply(osg::Geometry&)
{
}
private:
int mEffectId;
};
class RemoveCallbackVisitor : public RemoveVisitor
@ -1748,21 +1739,31 @@ namespace MWRender
if (alpha != 1.f)
{
osg::StateSet* stateset (new osg::StateSet);
// If we have an existing material for alpha transparency, just override alpha level
osg::StateSet* stateset = mObjectRoot->getOrCreateStateSet();
osg::Material* material = static_cast<osg::Material*>(stateset->getAttribute(osg::StateAttribute::MATERIAL));
if (material)
{
material->setAlpha(osg::Material::FRONT_AND_BACK, alpha);
}
else
{
osg::StateSet* stateset (new osg::StateSet);
osg::BlendFunc* blendfunc (new osg::BlendFunc);
stateset->setAttributeAndModes(blendfunc, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
osg::BlendFunc* blendfunc (new osg::BlendFunc);
stateset->setAttributeAndModes(blendfunc, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
// FIXME: overriding diffuse/ambient/emissive colors
osg::Material* material (new osg::Material);
material->setColorMode(osg::Material::OFF);
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,alpha));
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));
stateset->setAttributeAndModes(material, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
// FIXME: overriding diffuse/ambient/emissive colors
material = new osg::Material;
material->setColorMode(osg::Material::OFF);
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,alpha));
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));
stateset->setAttributeAndModes(material, osg::StateAttribute::ON|osg::StateAttribute::OVERRIDE);
mObjectRoot->setStateSet(stateset);
mObjectRoot->setStateSet(stateset);
mResourceSystem->getSceneManager()->recreateShaders(mObjectRoot);
mResourceSystem->getSceneManager()->recreateShaders(mObjectRoot);
}
}
else
{
@ -1798,9 +1799,12 @@ namespace MWRender
}
else
{
effect += 3;
float radius = effect * 66.f;
float linearAttenuation = 0.5f / effect;
// TODO: use global attenuation settings
// 1 pt of Light magnitude corresponds to 1 foot of radius
float radius = effect * std::ceil(Constants::UnitsPerFoot);
const float linearValue = 3.f; // Currently hardcoded: unmodified Morrowind attenuation settings
float linearAttenuation = linearValue / radius;
if (!mGlowLight || linearAttenuation != mGlowLight->getLight(0)->getLinearAttenuation())
{
@ -1822,7 +1826,8 @@ namespace MWRender
mGlowLight->setLight(light);
}
mGlowLight->setRadius(radius);
// Make the obvious cut-off a bit less obvious
mGlowLight->setRadius(radius * 3);
}
}

@ -738,8 +738,7 @@ namespace MWScript
virtual void execute (Interpreter::Runtime& runtime)
{
const MWWorld::Ptr ptr = MWMechanics::getPlayer();
MWBase::Environment::get().getWorld()->fixPosition(ptr);
MWBase::Environment::get().getWorld()->fixPosition();
}
};

@ -245,20 +245,30 @@ namespace MWSound
DecoderPtr SoundManager::loadVoice(const std::string &voicefile)
{
DecoderPtr decoder = getDecoder();
// Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
if(mVFS->exists(voicefile))
decoder->open(voicefile);
else
try
{
DecoderPtr decoder = getDecoder();
// Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
if(mVFS->exists(voicefile))
decoder->open(voicefile);
else
{
std::string file = voicefile;
std::string::size_type pos = file.rfind('.');
if(pos != std::string::npos)
file = file.substr(0, pos)+".mp3";
decoder->open(file);
}
return decoder;
}
catch(std::exception &e)
{
std::string file = voicefile;
std::string::size_type pos = file.rfind('.');
if(pos != std::string::npos)
file = file.substr(0, pos)+".mp3";
decoder->open(file);
Log(Debug::Error) << "Failed to load audio from " << voicefile << ": " << e.what();
}
return decoder;
return nullptr;
}
Sound *SoundManager::getSoundRef()
@ -471,6 +481,8 @@ namespace MWSound
mVFS->normalizeFilename(voicefile);
DecoderPtr decoder = loadVoice(voicefile);
if (!decoder)
return;
MWBase::World *world = MWBase::Environment::get().getWorld();
const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans();
@ -503,6 +515,8 @@ namespace MWSound
mVFS->normalizeFilename(voicefile);
DecoderPtr decoder = loadVoice(voicefile);
if (!decoder)
return;
stopSay(MWWorld::ConstPtr());
Stream *sound = playVoice(decoder, osg::Vec3f(), true);

@ -117,7 +117,7 @@ namespace MWSound
Sound_Buffer *lookupSound(const std::string &soundId) const;
Sound_Buffer *loadSound(const std::string &soundId);
// returns a decoder to start streaming
// returns a decoder to start streaming, or nullptr if the sound was not found
DecoderPtr loadVoice(const std::string &voicefile);
Sound *getSoundRef();

@ -265,9 +265,10 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot
writer.save (stream);
Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen();
int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount();
// Using only Cells for progress information, since they typically have the largest records by far
listener.setProgressRange(MWBase::Environment::get().getWorld()->countSavedGameCells());
listener.setLabel("#{sNotifyMessage4}", true);
listener.setLabel("#{sNotifyMessage4}", true, messagesCount > 0);
Loading::ScopedLoad load(&listener);
@ -389,9 +390,10 @@ void MWState::StateManager::loadGame (const Character *character, const std::str
std::map<int, int> contentFileMap = buildContentFileIndexMap (reader);
Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen();
int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount();
listener.setProgressRange(100);
listener.setLabel("#{sLoadingMessage14}");
listener.setLabel("#{sLoadingMessage14}", false, messagesCount > 0);
Loading::ScopedLoad load(&listener);

@ -70,6 +70,22 @@ namespace MWWorld
return mCellRef.mEnchantmentCharge;
}
float CellRef::getNormalizedEnchantmentCharge(int maxCharge) const
{
if (maxCharge == 0)
{
return 0;
}
else if (mCellRef.mEnchantmentCharge == -1)
{
return 1;
}
else
{
return mCellRef.mEnchantmentCharge / static_cast<float>(maxCharge);
}
}
void CellRef::setEnchantmentCharge(float charge)
{
if (charge != mCellRef.mEnchantmentCharge)

@ -56,6 +56,9 @@ namespace MWWorld
// Remaining enchantment charge. This could be -1 if the charge was not touched yet (i.e. full).
float getEnchantmentCharge() const;
// Remaining enchantment charge rescaled to the supplied maximum charge (such as one of the enchantment).
float getNormalizedEnchantmentCharge(int maxCharge) const;
void setEnchantmentCharge(float charge);
// For weapon or armor, this is the remaining item health.

@ -83,6 +83,18 @@ namespace MWWorld
return ptr.getCellRef().getCharge();
}
float Class::getItemNormalizedHealth (const ConstPtr& ptr) const
{
if (getItemMaxHealth(ptr) == 0)
{
return 0.f;
}
else
{
return getItemHealth(ptr) / static_cast<float>(getItemMaxHealth(ptr));
}
}
int Class::getItemMaxHealth (const ConstPtr& ptr) const
{
throw std::runtime_error ("class does not have item health");

@ -112,6 +112,9 @@ namespace MWWorld
virtual int getItemHealth (const ConstPtr& ptr) const;
///< Return current item health or throw an exception if class does not have item health
virtual float getItemNormalizedHealth (const ConstPtr& ptr) const;
///< Return current item health re-scaled to maximum health
virtual int getItemMaxHealth (const ConstPtr& ptr) const;
///< Return item max health or throw an exception, if class does not have item health
/// (default implementation: throw an exception)

@ -509,8 +509,9 @@ namespace MWWorld
Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
Loading::ScopedLoad load(loadingListener);
int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount();
std::string loadingExteriorText = "#{sLoadingMessage3}";
loadingListener->setLabel(loadingExteriorText);
loadingListener->setLabel(loadingExteriorText, false, messagesCount > 0);
CellStoreCollection::iterator active = mActiveCells.begin();
while (active!=mActiveCells.end())
@ -695,8 +696,9 @@ namespace MWWorld
MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5);
Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
int messagesCount = MWBase::Environment::get().getWindowManager()->getMessagesCount();
std::string loadingInteriorText = "#{sLoadingMessage2}";
loadingListener->setLabel(loadingInteriorText);
loadingListener->setLabel(loadingInteriorText, false, messagesCount > 0);
Loading::ScopedLoad load(loadingListener);
if(!loadcell)

@ -1394,8 +1394,9 @@ namespace MWWorld
moveObject(ptr, ptr.getCell(), pos.x(), pos.y(), pos.z());
}
void World::fixPosition(const Ptr &actor)
void World::fixPosition()
{
const MWWorld::Ptr actor = getPlayerPtr();
const float distance = 128.f;
ESM::Position esmPos = actor.getRefData().getPosition();
osg::Quat orientation(esmPos.rot[2], osg::Vec3f(0,0,-1));
@ -1425,7 +1426,7 @@ namespace MWWorld
esmPos.pos[0] = traced.x();
esmPos.pos[1] = traced.y();
esmPos.pos[2] = traced.z();
MWWorld::ActionTeleport("", esmPos, false).execute(actor);
MWWorld::ActionTeleport(actor.getCell()->isExterior() ? "" : actor.getCell()->getCell()->mName, esmPos, false).execute(actor);
}
}
@ -1574,15 +1575,18 @@ namespace MWWorld
return mNavigator->updateObject(DetourNavigator::ObjectId(object), shapes, object->getCollisionObject()->getWorldTransform());
}
bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2, bool ignoreDoors)
bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2)
{
int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_Door;
bool result = castRay(x1, y1, z1, x2, y2, z2, mask);
return result;
}
bool World::castRay (float x1, float y1, float z1, float x2, float y2, float z2, int mask)
{
osg::Vec3f a(x1,y1,z1);
osg::Vec3f b(x2,y2,z2);
int mask = MWPhysics::CollisionType_World;
if (!ignoreDoors)
mask |= MWPhysics::CollisionType_Door;
MWPhysics::PhysicsSystem::RayResult result = mPhysics->castRay(a, b, MWWorld::Ptr(), std::vector<MWWorld::Ptr>(), mask);
return result.mHit;
}
@ -1648,6 +1652,16 @@ namespace MWWorld
}
}
void World::setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled)
{
mPhysics->setActorCollisionMode(ptr, enabled);
}
bool World::isActorCollisionEnabled(const MWWorld::Ptr& ptr)
{
return mPhysics->isActorCollisionEnabled(ptr);
}
bool World::toggleCollisionMode()
{
if (mPhysics->toggleCollisionMode())
@ -3631,7 +3645,13 @@ namespace MWWorld
MWBase::Environment::get().getMechanicsManager()->getObjectsInRange(
origin, feetToGameUnits(static_cast<float>(effectIt->mArea)), objects);
for (std::vector<MWWorld::Ptr>::iterator affected = objects.begin(); affected != objects.end(); ++affected)
{
// Ignore actors without collisions here, otherwise it will be possible to hit actors outside processing range.
if (affected->getClass().isActor() && !isActorCollisionEnabled(*affected))
continue;
toApply[*affected].push_back(*effectIt);
}
}
// Now apply the appropriate effects to each actor in range

@ -301,8 +301,8 @@ namespace MWWorld
///< Adjust position after load to be on ground. Must be called after model load.
/// @param force do this even if the ptr is flying
void fixPosition (const Ptr& actor) override;
///< Attempt to fix position so that the Ptr is no longer inside collision geometry.
void fixPosition () override;
///< Attempt to fix position so that the player is not stuck inside the geometry.
void enable (const Ptr& ptr) override;
@ -412,9 +412,14 @@ namespace MWWorld
///< Queues movement for \a ptr (in local space), to be applied in the next call to
/// doPhysics.
bool castRay (float x1, float y1, float z1, float x2, float y2, float z2, bool ignoreDoors=false) override;
bool castRay (float x1, float y1, float z1, float x2, float y2, float z2, int mask) override;
///< cast a Ray and return true if there is an object in the ray path.
bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) override;
void setActorCollisionMode(const Ptr& ptr, bool enabled) override;
bool isActorCollisionEnabled(const Ptr& ptr) override;
bool toggleCollisionMode() override;
///< Toggle collision mode for player. If disabled player object should ignore
/// collisions and gravity.

@ -8,12 +8,13 @@ namespace Debug
{
enum Level
{
NoLevel = 0,
Error = 1,
Warning = 2,
Info = 3,
Verbose = 4,
Marker = Verbose
Marker = Verbose,
NoLevel = 5 // Do not filter messages in this case
};
extern Level CurrentDebugLevel;
@ -30,6 +31,11 @@ public:
mLock(sLock),
mLevel(level)
{
// If the app has no logging system enabled, log level is not specified.
// Show all messages without marker - we just use the plain cout in this case.
if (Debug::CurrentDebugLevel == Debug::NoLevel)
return;
if (mLevel <= Debug::CurrentDebugLevel)
std::cout << static_cast<unsigned char>(mLevel);
}

@ -14,7 +14,7 @@ namespace Loading
/// @note "non-important" labels may not show on screen if the loading process went so fast
/// that the implementation decided not to show a loading screen at all. "important" labels
/// will show in a separate message-box if the loading screen was not shown.
virtual void setLabel (const std::string& label, bool important=false) {}
virtual void setLabel (const std::string& label, bool important=false, bool center=false) {}
/// Start a loading sequence. Must call loadingOff() when done.
/// @note To get the loading screen to actually update, you must call setProgress / increaseProgress periodically.

@ -6,6 +6,7 @@
#include <osg/Geometry>
#include <components/debug/debuglog.hpp>
#include <components/misc/rng.hpp>
#include <components/nif/controlled.hpp>
#include <components/nif/nifkey.hpp>
#include <components/nif/data.hpp>
@ -81,17 +82,17 @@ ParticleShooter::ParticleShooter(const ParticleShooter &copy, const osg::CopyOp
void ParticleShooter::shoot(osgParticle::Particle *particle) const
{
float hdir = mHorizontalDir + mHorizontalAngle * (2.f * (std::rand() / static_cast<double>(RAND_MAX)) - 1.f);
float vdir = mVerticalDir + mVerticalAngle * (2.f * (std::rand() / static_cast<double>(RAND_MAX)) - 1.f);
float hdir = mHorizontalDir + mHorizontalAngle * (2.f * Misc::Rng::rollClosedProbability() - 1.f);
float vdir = mVerticalDir + mVerticalAngle * (2.f * Misc::Rng::rollClosedProbability() - 1.f);
osg::Vec3f dir = (osg::Quat(vdir, osg::Vec3f(0,1,0)) * osg::Quat(hdir, osg::Vec3f(0,0,1)))
* osg::Vec3f(0,0,1);
float vel = mMinSpeed + (mMaxSpeed - mMinSpeed) * std::rand() / static_cast<float>(RAND_MAX);
float vel = mMinSpeed + (mMaxSpeed - mMinSpeed) * Misc::Rng::rollClosedProbability();
particle->setVelocity(dir * vel);
// Not supposed to set this here, but there doesn't seem to be a better way of doing it
particle->setLifeTime(mLifetime + mLifetimeRandom * std::rand() / static_cast<float>(RAND_MAX));
particle->setLifeTime(mLifetime + mLifetimeRandom * Misc::Rng::rollClosedProbability());
}
GrowFadeAffector::GrowFadeAffector(float growTime, float fadeTime)
@ -277,7 +278,8 @@ void Emitter::emitParticles(double dt)
if (!mTargets.empty())
{
int randomRecIndex = mTargets[(std::rand() / (static_cast<double>(RAND_MAX)+1.0)) * mTargets.size()];
int randomIndex = Misc::Rng::rollClosedProbability() * (mTargets.size() - 1);
int randomRecIndex = mTargets[randomIndex];
// we could use a map here for faster lookup
FindGroupByRecIndex visitor(randomRecIndex);

@ -27,8 +27,7 @@ namespace Resource
btCollisionShape* mCollisionShape;
btCollisionShape* mAvoidCollisionShape;
// Used for actors. Note, ideally actors would use a separate loader - as it is
// we have to keep a redundant copy of the actor model around in mCollisionShape, which isn't used.
// Used for actors. mCollisionShape is used for actors only when we need to autogenerate collision box for creatures.
// For now, use one file <-> one resource for simplicity.
osg::Vec3f mCollisionBoxHalfExtents;
osg::Vec3f mCollisionBoxTranslate;

@ -169,8 +169,8 @@ def setup(app):
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static',
'manuals/openmw-cs/_static'
html_static_path = [
'_static'
]
# Add any extra paths that contain custom files (such as robots.txt or

@ -8,9 +8,19 @@ Or a beginner's guide to writing docs without having to deal with more techie st
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, 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.
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, 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.
Baby Steps
==========
@ -25,9 +35,11 @@ Choose Repository and Files in the menu on the left, then docs and source in the
Dont overlook the tutorial-style-guide.txt there for some tips to get you started.
Open whichever file you want to tackle probably within the manuals or reference directories.
Theres also a dropdown box to the right of edit, at the top of the left menu, which offers options such as new file or directory, or upload file, with “+” to close that dropdown box.
Theres also a dropdown box to the right of edit, at the top of the left menu,
which offers options such as new file or directory, or upload file, with “+” to close that dropdown box.
Click on "Edit" towards top right which will reveal the underlying version, rather than the version displayed to normal reaaders. Use "Write" and "Preview" to switch between the two views.
Click on "Edit" towards top right which will reveal the underlying version,
rather than the version displayed to normal readers. Use "Write" and "Preview" to switch between the two views.
When you have made the appropriate changes, and checked them in Preview mode, click the Green "Commit changes" button at the bottom.
This should add a branch, with a default name such as patch-1, to your own repository, and add a Merge Request to the main OpenMW Project.
@ -48,13 +60,24 @@ So here's what you're gonna be learning how to set up:
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:
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:
The master OpenMW repository 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
@ -67,7 +90,11 @@ If you want more info I recommend reading this guide: https://readwrite.com/2013
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:
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.
@ -81,31 +108,61 @@ PyCharm is what's known as an IDE, which stands for integrated development envir
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.
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.
Congrats! You now have the OpenMW source code 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
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: https://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. ...
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:
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:
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
@ -116,7 +173,9 @@ So to go over all that again. You rebase *every* time you start working on somet
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.
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`.
@ -127,16 +186,21 @@ You will probably find it helpful to be able to preview any documentation you've
:Output: <wherever you want the build files to be>
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.
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.
GitLab integration in PyCharm
=============================
As most of the hosting of OpenMW has moved to Gitlab, we should encourage the use of GitLab, though GitHub will continue to be supported.
As most of the hosting of OpenMW has moved to Gitlab, we should encourage the use of GitLab,
though GitHub will continue to be supported.
Add a couple of plugins to Pycharm - see general instructions at https://www.jetbrains.com/help/pycharm/installing-updating-and-uninstalling-repository-plugins.html
Add a couple of plugins to PyCharm - see general instructions at https://www.jetbrains.com/help/pycharm/installing-updating-and-uninstalling-repository-plugins.html
For Linux/Windows - (Macos is a little different)
For Linux/Windows - (MacOS is a little different)
1. File/Settings/Plugins
2. Browse Repositories

@ -4,15 +4,19 @@ Modding OpenMW vs Morrowind
A brief overview of the differences between the two engines.
============================================================
OpenMW is designed to be able to use all the normal Morrowind mod files such as ESM/ESP plugins, texture replacers, mesh replacers, etc.
OpenMW is designed to be able to use all the normal Morrowind mod files such as ESM/ESP plugins, texture replacers,
mesh replacers, etc.
.. warning::
All external programs and libraries that depend on ``morrowind.exe`` cannot function with OpenMW. This means you should assume mods dependent on Morrowind Graphics Extender, Morrowind Code Patch, Morrowind Script Extender, etc, will *not* work correctly, nor will the tools themselves.
All external programs and libraries that depend on ``morrowind.exe`` cannot function with OpenMW.
This means you should assume mods dependent on Morrowind Graphics Extender, Morrowind Code Patch,
Morrowind Script Extender, etc, will *not* work correctly, nor will the tools themselves.
Multiple Data Folders
---------------------
The largest difference between OpenMW and Morrowind in terms of data structure is OpenMW's support of multiple data folders. This has many advantages, especially when it comes to uninstalling mods and preventing unintentional overwrites of files.
The largest difference between OpenMW and Morrowind in terms of data structure is OpenMW's support of multiple data folders.
This has many advantages, especially when it comes to uninstalling mods and preventing unintentional overwrites of files.
.. warning::
Most mods can still be installed into the root OpenMW data folder, but this is not recommended.
@ -34,25 +38,33 @@ To uninstall these mods simply delete that mod's respective ``data=`` entry.
The mods are loaded in the order of these entries, with the top being overwritten by mods added towards the bottom.
.. note::
Mods that depend on ESM/ESP plugins can be rearranged within the OpenMW Launcher, but mesh/texture replacer mods can only be reordered by moving their ``data=`` entry.
Mods that depend on ESM/ESP plugins can be rearranged within the OpenMW Launcher,
but mesh/texture replacer mods can only be reordered by moving their ``data=`` entry.
OpenMW Launcher
---------------
The launcher included with OpenMW is similar to the original Morrowind Launcher. Go to the Data Files tab to enable and disable plugins. You can also drag list items to modify the load order. Content lists can be created at the bottom by clicking the New Content List button, creating a list name, then setting up a new modlist. This is helpful for different player profiles and testing out different load orders.
The launcher included with OpenMW is similar to the original Morrowind Launcher.
Go to the Data Files tab to enable and disable plugins. You can also drag list items to modify the load order.
Content lists can be created at the bottom by clicking the New Content List button, creating a list name,
then setting up a new modlist. This is helpful for different player profiles and testing out different load orders.
.. TODO use a substitution image for the New Content List button.
Settings.cfg
------------
The ``settings.cfg`` file is essentially the same as the INI files for Morrowind. It is located in the same directory as ``openmw.cfg``. This is where many video, audio, GUI, input, etc. settings can be modified. Some are available in-game, but many are only available in this configuration file. Please see https://wiki.openmw.org/index.php?title=Settings for the complete listing.
The ``settings.cfg`` file is essentially the same as the INI files for Morrowind.
It is located in the same directory as ``openmw.cfg``. This is where many video, audio, GUI, input, etc.
settings can be modified. Some are available in-game, but many are only available in this configuration file.
Please see https://wiki.openmw.org/index.php?title=Settings for the complete listing.
.. TODO Create a proper ReST document tree for all the settings rather than Wiki.
Open Source Resources Support
-----------------------------
While OpenMW supports all of the original files that Morrowind supported, we've expanded support to many open source file formats. These are summarized below:
While OpenMW supports all of the original files that Morrowind supported,
we've expanded support to many open source file formats. These are summarized below:
<this will be a table of the type of file, the morrowind supported file, and the OpenMW supported file formats>

@ -4,14 +4,20 @@ Fonts
Morrowind .fnt fonts
--------------------
Morrowind uses a custom ``.fnt`` file format. It is not compatible with the Windows Font File ``.fnt`` format, nor compatible with ``.fnt`` formats from any other Bethesda games. To our knowledge, the format is undocumented and no tools for viewing or editing these fonts exist.
Morrowind uses a custom ``.fnt`` file format. It is not compatible with the Windows Font File ``.fnt`` format,
nor compatible with ``.fnt`` formats from any other Bethesda games. To our knowledge,
the format is undocumented and no tools for viewing or editing these fonts exist.
OpenMW can load this format and convert it on the fly into something usable (see font loader `source code <https://github.com/OpenMW/openmw/blob/master/components/fontloader/fontloader.cpp#L210>`_). In OpenMW 0.32, an --export-fonts command line option was added to write the converted font (a PNG image and an XML file describing the position of each glyph in the image) to the current directory.
OpenMW can load this format and convert it on the fly into something usable
(see font loader `source code <https://github.com/OpenMW/openmw/blob/master/components/fontloader/fontloader.cpp#L210>`_).
In OpenMW 0.32, an --export-fonts command line option was added to write the converted font
(a PNG image and an XML file describing the position of each glyph in the image) to the current directory.
TrueType fonts
--------------
Unlike vanilla Morrowind, OpenMW directly supports TrueType (``.ttf``) fonts. This is the recommended way to create new fonts.
Unlike vanilla Morrowind, OpenMW directly supports TrueType (``.ttf``) fonts.
This is the recommended way to create new fonts.
- To replace the primary "Magic Cards" font:
@ -74,4 +80,7 @@ Unlike vanilla Morrowind, OpenMW directly supports TrueType (``.ttf``) fonts. Th
Bitmap fonts
------------
Morrowind ``.fnt`` files are essentially a bitmap font, but using them is discouraged because of no Unicode support. MyGUI has its own format for bitmap fonts. An example can be seen by using the --export-fonts command line option (see above), which converts Morrowind ``.fnt`` to a MyGUI bitmap font. This is the recommended format to use if you wish to edit Morrowind's bitmap font or create a new bitmap font.
Morrowind ``.fnt`` files are essentially a bitmap font, but using them is discouraged because of no Unicode support.
MyGUI has its own format for bitmap fonts. An example can be seen by using the --export-fonts command line option (see above),
which converts Morrowind ``.fnt`` to a MyGUI bitmap font.
This is the recommended format to use if you wish to edit Morrowind's bitmap font or create a new bitmap font.

@ -1,4 +1,10 @@
Foreword
########
OpenMW is a complete game engine built to be content agnostic. The majority of this guide is applicable to any non-Morrowind project using its engine. That being said, it was designed with the extensive modding community of Morrowind in mind. Therefore, if you are already familiar with modding in Morrowind, you will likely be able to start modding in OpenMW with little to no instruction. We do recommend you at least refer to :doc:`differences` to find out about what's different between OpenMW and the original Morrowind engine. For everyone else, or just a good refresher, read on!
OpenMW is a complete game engine built to be content agnostic.
The majority of this guide is applicable to any non-Morrowind project using its engine.
That being said, it was designed with the extensive modding community of Morrowind in mind.
Therefore, if you are already familiar with modding in Morrowind,
you will likely be able to start modding in OpenMW with little to no instruction.
We do recommend you at least refer to :doc:`differences` to find out about what's different between OpenMW and the
original Morrowind engine. For everyone else, or just a good refresher, read on!

@ -21,16 +21,20 @@ Install
#. If your mod contains resources in a ``.bsa`` file, go to near the top of the file, locate the entries like ''fallback-archive=Morrowind.bsa'' and create a new line underneath and type: ``fallback-archive=<name of your bsa>.bsa''``.
.. note::
Some text editors, such as TextEdit on Mac, will autocorrect your double quotes to typographical "curly" quotes instead of leaving them as the proper neutral vertical quotes ``""``.
Some text editors, such as TextEdit on Mac, will auto-correct your double quotes to typographical "curly"
quotes instead of leaving them as the proper neutral vertical quotes ``""``.
#. Save your ``openmw.cfg`` file.
You have now installed your mod. Any simple replacer mods that only contain resource files such as meshes or textures will now automatically be loaded in the order of their ``data=*`` entry. This is important to note because replacer mods that replace the same resource will overwrite previous ones as you go down the list.
You have now installed your mod. Any simple replacer mods that only contain resource files such as meshes or
textures will now automatically be loaded in the order of their ``data=*`` entry.
This is important to note because replacer mods that replace the same resource will overwrite previous ones as you go down the list.
Enable
------
Any mods that have plugin files must be enabled to work. Master game files and plugin files can only be enabled if they have been properly installed within a *data folder* as described above.
Any mods that have plugin files must be enabled to work.
Master game files and plugin files can only be enabled if they have been properly installed within a *data folder* as described above.
#. Open the OpenMW Launcher.
#. Click on the Data Files tab.

@ -8,7 +8,8 @@ crosshair
:Range: True/False
:Default: True
This setting determines whether the crosshair or reticle is displayed. Enabling the crosshair provides more immediate feedback about which object is currently the focus of actions.
This setting determines whether the crosshair or reticle is displayed.
Enabling the crosshair provides more immediate feedback about which object is currently the focus of actions.
Some players perceive that disabling the crosshair provides a more immersive experience.
Another common use is to disable the crosshair for screen shots.

@ -11,7 +11,10 @@ exterior cell load distance
This setting determines the number of exterior cells adjacent to the character that will be loaded for rendering.
.. Warning::
Values greater than 1 will significantly affect the frame rate and loading times. This setting is mainly intended for making screenshots of scenic vistas and not for real-time gameplay. Loading more cells can break certain scripts or quests in the game that expect cells to not be loaded until the player is there. These limitations will be addressed in a future version with a separate technique for rendering distant cells.
Values greater than 1 will significantly affect the frame rate and loading times.
This setting is mainly intended for making screenshots of scenic vistas and not for real-time gameplay.
Loading more cells can break certain scripts or quests in the game that expect cells to not be loaded until the player is there.
These limitations will be addressed in a future version with a separate technique for rendering distant cells.
This setting interacts with viewing distance and field of view settings.
@ -163,11 +166,13 @@ prediction time
:Range: >=0
:Default: 1
The amount of time (in seconds) in the future to predict the player position for. This predicted position is used to preload any cells and/or distant terrain required at that position.
The amount of time (in seconds) in the future to predict the player position for.
This predicted position is used to preload any cells and/or distant terrain required at that position.
This setting will only have an effect if 'preload enabled' is set or the 'distant terrain' in the Terrain section is set.
Increasing this setting from its default may help if your computer/hard disk is too slow to preload in time and you see loading screens and/or lag spikes.
Increasing this setting from its default may help if your computer/hard disk is too slow to preload in time and you see
loading screens and/or lag spikes.
cache expiry delay
------------------
@ -185,7 +190,10 @@ target framerate
:Range: >0
:Default: 60
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.
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
-------------------
@ -194,4 +202,6 @@ pointers cache size
:Range: >0
:Default: 40
The count of object pointers that will be saved for a faster search by object ID. This is a temporary setting that can be used to mitigate scripting performance issues with certain game files. If your profiler (press F3 twice) displays a large overhead for the Scripting section, try increasing this setting.
The count of object pointers that will be saved for a faster search by object ID.
This is a temporary setting that can be used to mitigate scripting performance issues with certain game files.
If your profiler (press F3 twice) displays a large overhead for the Scripting section, try increasing this setting.

@ -9,11 +9,11 @@ show owned
:Default: 0
Enable visual clues for items owned by NPCs when the crosshair is on the object.
If the setting is 0, no clues are provided which is the default Morrowind behavior.
If the setting is 0, no clues are provided which is the default Morrowind behaviour.
If the setting is 1, the background of the tool tip for the object is highlighted
in the color specified by the color background owned setting in the GUI Settings Section.
If the setting is 2, the crosshair is the color of the color crosshair owned setting in the GUI Settings section.
If the setting is 3, both the tool tip background and the crosshair are colored.
in the colour specified by the colour background owned setting in the GUI Settings Section.
If the setting is 2, the crosshair is the colour of the colour crosshair owned setting in the GUI Settings section.
If the setting is 3, both the tool tip background and the crosshair are coloured.
The crosshair is not visible if crosshair is false.
This setting can be configured in Advanced tab of the launcher.
@ -71,12 +71,14 @@ can loot during death animation
:Range: True/False
:Default: True
If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat.
However disposing corpses during death animation is not recommended - death counter may not be incremented, and this behaviour can break quests.
If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation,
if they are not in combat. However disposing corpses during death animation is not recommended -
death counter may not be incremented, and this behaviour can break quests.
This is how 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.
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 be toggled in Advanced tab of the launcher.
@ -97,8 +99,21 @@ and values above 500 will result in the player inflicting no damage.
This setting can be controlled in game with the Difficulty slider in the Prefs panel of the Options menu.
actors processing range
-----------------------
:Type: integer
:Range: 3584 to 7168
:Default: 7168
This setting allows to specify a distance from player in game units, in which OpenMW updates actor's state.
Actor state update includes AI, animations, and physics processing.
Actors near that border start softly fade out instead of just appearing/disapperaing.
This setting can be controlled in game with the "Actors processing range slider" in the Prefs panel of the Options menu.
classic reflected absorb spells behavior
-----------------------------------------
----------------------------------------
:Type: boolean
:Range: True/False
@ -166,10 +181,11 @@ use additional anim sources
: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!
Allow the engine to load additional animation sources when enabled.
For example, if the main animation mesh has name Meshes/x.nif,
the engine will load all KF-files from Animations/x folder and its child folders.
This can be useful if you want to use several animation replacers without merging them.
Attention: animations from AnimKit have their own format and are not supposed to be directly loaded in-game!
This setting can only be configured by editing the settings configuration file.
barter disposition change is permanent
@ -179,7 +195,8 @@ barter disposition change is permanent
:Range: True/False
:Default: False
If this setting is true, disposition change of merchants caused by trading will be permanent and won't be discarded upon exiting dialogue with them.
This imitates the option Morrowind Code Patch offers.
If this setting is true,
disposition change of merchants caused by trading will be permanent and won't be discarded upon exiting dialogue with them.
This imitates the option that Morrowind Code Patch offers.
This setting can be toggled in Advanced tab of the launcher.

@ -8,10 +8,26 @@ If you are familiar with ``.ini`` tweaks in Morrowind or the other games, this w
All settings described in this section are changed in ``settings.cfg``, located in your OpenMW user directory.
See :doc:`../paths` for this location.
Changing Settings
#################
#. Once you have located your ``settings.cfg`` file, open it in a plain text editor.
#. Find the setting(s) you wish to change in the following pages.
#. If the setting is not already in ``settings.cfg``,
add it by copy and pasting the name exactly as written in this guide.
#. Set the value of the setting by typing ``= <value>`` after the setting on the same line,
using an appropriate value in place of ``<value>``.
#. If this is the first setting from it's category that you're adding,
be sure to add the heading in square brackets ``[]`` above it using just the setting type,
i.e. without the word "Settings".
For example, to delay tooltips popping up by 1 second, add the line ``tooltip delay = 1.0``.
Then to the line above, type ``[GUI]``, as the tooltip delay setting comes from the "GUI Settings" section.
Although this guide attempts to be comprehensive and up to date,
you will always be able to find the full list of settings available and their default values in ``settings-default.cfg``
in your main OpenMW installation directory.
The ranges I have included with each setting are the physically possible ranges, not recommendations.
The ranges included with each setting are the physically possible ranges, not recommendations.
.. warning::
As the title suggests, these are advanced settings.

@ -8,7 +8,8 @@ global
:Range: True/False
:Default: False
If this value is true, the map window will display the world map, otherwise the local map. The setting updates automatically when pressing the local/world map switch button on the map window.
If this value is true, the map window will display the world map, otherwise the local map.
The setting updates automatically when pressing the local/world map switch button on the map window.
global map cell size
--------------------
@ -110,4 +111,6 @@ local map cell distance
:Range: >= 1
:Default: 1
Similar to "exterior cell load distance" in the Cells section, controls how many cells are rendered on the local map. Values higher than the default may result in longer loading times. Please note that only loaded cells can be rendered, so this setting must be lower or equal to "exterior cell load distance" to work properly.
Similar to "exterior cell load distance" in the Cells section, controls how many cells are rendered on the local map.
Values higher than the default may result in longer loading times. Please note that only loaded cells can be rendered,
so this setting must be lower or equal to "exterior cell load distance" to work properly.

@ -29,7 +29,8 @@ timeplayed
:Default: False
This setting determines whether the amount of the time the player has spent playing will be displayed
for each saved game in the Load menu. Currently, the counter includes time spent in menus, including the pause menu, but does not include time spent with the game window minimized.
for each saved game in the Load menu. Currently, the counter includes time spent in menus, including the pause menu,
but does not include time spent with the game window minimized.
This setting can only be configured by editing the settings configuration file.
@ -40,6 +41,8 @@ max quicksaves
:Range: >0
:Default: 1
This setting determines how many quicksave and autosave slots you can have at a time. If greater than 1, quicksaves will be sequentially created each time you quicksave. Once the maximum number of quicksaves has been reached, the oldest quicksave will be recycled the next time you perform a quicksave.
This setting determines how many quicksave and autosave slots you can have at a time. If greater than 1,
quicksaves will be sequentially created each time you quicksave. Once the maximum number of quicksaves has been reached,
the oldest quicksave will be recycled the next time you perform a quicksave.
This setting can only be configured by editing the settings configuration file.

@ -1,5 +1,5 @@
Shader Settings
###############
Shaders Settings
################
force shaders
-------------
@ -39,7 +39,8 @@ Only affects objects that render with shaders (see 'force shaders' option).
Always affects terrain.
Leaving this option at its default makes the lighting compatible with Morrowind's fixed-function method,
but the lighting may appear dull and there might be color shifts. Setting this option to 'false' results in more dynamic lighting.
but the lighting may appear dull and there might be colour shifts.
Setting this option to 'false' results in more dynamic lighting.
auto use object normal maps
---------------------------
@ -83,7 +84,7 @@ auto use terrain specular maps
:Default: False
If a file with pattern 'terrain specular map pattern' exists, use that file as a 'diffuse specular' map.
The texture must contain the layer color in the RGB channel (as usual), and a specular multiplier in the alpha channel.
The texture must contain the layer colour in the RGB channel (as usual), and a specular multiplier in the alpha channel.
normal map pattern
------------------

@ -67,7 +67,7 @@ voice volume
:Range: 0.0 (silent) to 1.0 (maximum volume)
:Default: 0.8
This setting controls the volume for spoken dialog from NPCs.
This setting controls the volume for spoken dialogue from NPCs.
This setting can be changed in game using the Voice slider from the Audio panel of the Options menu.
@ -122,7 +122,7 @@ hrtf
This setting specifies which HRTF profile to use when HRTF is enabled. Blank means use the default.
This setting has no effect if HRTF is not enabled based on the hrtf enable setting.
Allowed values for this field are enumerated in openmw.log file is an HRTF enabled ausio system is installed.
Allowed values for this field are enumerated in openmw.log file is an HRTF enabled audio system is installed.
The default value is empty, which uses the default profile.
This setting can only be configured by editing the settings configuration file.

@ -12,7 +12,8 @@ Controls whether the engine will use paging and LOD algorithms to load the terra
Otherwise, only the terrain of the surrounding cells is loaded.
.. note::
When enabling distant terrain, make sure the 'viewing distance' in the camera section is set to a larger value so that you can actually see the additional terrain.
When enabling distant terrain, make sure the 'viewing distance' in the camera section is set to a larger value so
that you can actually see the additional terrain.
To avoid frame drops as the player moves around, nearby terrain pages are always preloaded in the background,
regardless of the preloading settings in the 'Cells' section,

@ -170,7 +170,8 @@ contrast
This setting controls the contrast correction for all video in the game.
This setting can only be configured by editing the settings configuration file. It has been reported to not work on some Linux systems.
This setting can only be configured by editing the settings configuration file.
It has been reported to not work on some Linux systems.
gamma
-----
@ -183,4 +184,5 @@ This setting controls the gamma correction for all video in the game.
Gamma is an exponent that makes colors brighter if greater than 1.0 and darker if less than 1.0.
This setting can be changed in the Detail tab of the Video panel of the Options menu.
It has been reported to not work on some Linux systems, and therefore the in-game setting in the Options menu has been disabled on Linux systems.
It has been reported to not work on some Linux systems,
and therefore the in-game setting in the Options menu has been disabled on Linux systems.

@ -25,7 +25,8 @@ Hand editing the configuration file might result in some fine tuning for alignme
but the settings will be overwritten if a window is moved.
.. note::
To scale the windows, making the widgets proportionally larger, see the scaling factor setting in the GUI section instead.
To scale the windows, making the widgets proportionally larger,
see the scaling factor setting in the GUI section instead.
:Type: boolean
:Range: True/False
@ -210,7 +211,7 @@ dialogue
w = 0.45
The dialog window, for talking with NPCs.
The dialogue window, for talking with NPCs.
Activated by clicking on a NPC.
alchemy

@ -20,29 +20,43 @@ General introduction to normal map conversion
:Authors: Joakim (Lysol) Berg
:Updated: 2016-11-11
This page has general information and tutorials on how normal mapping works in OpenMW and how you can make mods using the old fake normal mapping technique (such as `Netch Bump mapped`_ and `Hlaalu Bump mapped`_, and maybe the most (in)famous one to give shiny rocks in OpenMW, the mod `On the Rocks`_!, featured in MGSO and Morrowind Rebirth) work in OpenMW.
This page has general information and tutorials on how normal mapping works in OpenMW and how you can make mods using
the old fake normal mapping technique (such as `Netch Bump mapped`_ and `Hlaalu Bump mapped`_, and maybe the most
(in)famous one to give shiny rocks in OpenMW, the mod `On the Rocks`_!, featured in MGSO and Morrowind Rebirth) work in OpenMW.
*Note:* The conversion made in the `Converting Apel's Various Things - Sacks`_-part of this tutorial require the use of the application NifSkope_.
*Another note:* I will use the terms bump mapping and normal mapping simultaneously. Normal mapping is one form of bump mapping. In other words, normal mapping is bump mapping, but bump mapping isn't necessarily normal mapping. There are several techniques for bump mapping, and normal mapping is the most common one today.
*Another note:* I will use the terms bump mapping and normal mapping simultaneously.
Normal mapping is one form of bump mapping. In other words, normal mapping is bump mapping,
but bump mapping isn't necessarily normal mapping.
There are several techniques for bump mapping, and normal mapping is the most common one today.
So let's get on with it.
Normal Mapping in OpenMW
************************
Normal mapping in OpenMW works in a very simple way: The engine just looks for a texture with a *_n.dds* suffix, and you're done.
Normal mapping in OpenMW works in a very simple way: The engine just looks for a texture with a *_n.dds* suffix,
and you're done.
So to expand on this a bit, let's take a look at how a model seeks for textures.
Let us assume we have the model *example.nif*. In this model file, there should be a tag (NiSourceTexture) that states what texture it should use and where to find it. Typically, it will point to something like *exampletexture_01.dds*. This texture is supposed to be located directly in the Textures folder since it does not state anything else. If the model is a custom made one, modders tend to group their textures in separate folders, just to easily keep track of them. It might be something like *./Textures/moddername/exampletexture_02.dds*.
Let us assume we have the model *example.nif*. In this model file,
there should be a tag (NiSourceTexture) that states what texture it should use and where to find it. Typically,
it will point to something like *exampletexture_01.dds*. This texture is supposed to be located directly in the
Textures folder since it does not state anything else. If the model is a custom made one, modders tend to group
their textures in separate folders, just to easily keep track of them.
It might be something like *./Textures/moddername/exampletexture_02.dds*.
When OpenMW finally adds normal mapping, it simply takes the NiSourceTexture file path, e.g., *exampletexture_01.dds*, and looks for a *exampletexture_01_n.dds*. If it can't find this file, no normal mapping is added. If it *does* find this file, the model will use this texture as a normal map. Simple.
When OpenMW finally adds normal mapping, it simply takes the NiSourceTexture file path, e.g.,
*exampletexture_01.dds*, and looks for a *exampletexture_01_n.dds*. If it can't find this file, no normal mapping is added.
If it *does* find this file, the model will use this texture as a normal map. Simple.
Activating normal mapping shaders in OpenMW
*******************************************
Before normal (and specular and parallax) maps will show up in OpenMW, you'll need to activate them in the settings.cfg_-file. Add these rows where it would make sense:
Before normal (and specular and parallax) maps will show up in OpenMW, you'll need to activate them in the
settings.cfg_-file. Add these rows where it would make sense:
::
@ -72,13 +86,26 @@ Normal mapping in Morrowind with Morrowind Code Patch
**Conversion difficulty:**
*Varies. Sometimes quick and easy, sometimes time-consuming and hard.*
You might have bumped (pun intended) on a few bump-mapped texture packs for Morrowind that require the Morrowind Code Patch (MCP). You might even be thinking: Why doesn't OpenMW just support these instead of reinventing the wheel? I know it sounds strange, but it will make sense. Here's how MCP handles normal maps:
You might have bumped (pun intended) on a few bump-mapped texture packs for Morrowind that require the
Morrowind Code Patch (MCP). You might even be thinking: Why doesn't OpenMW just support these instead of reinventing
the wheel? I know it sounds strange, but it will make sense. Here's how MCP handles normal maps:
Morrowind does not recognize normal maps (they weren't really a "thing" yet in 2002), so even if you have a normal map, Morrowind will not load and display it. MCP has a clever way to solve this issue, by using something Morrowind *does* support, namely environment maps. You could add a tag for an environment map and then add a normal map as the environment map, but you'd end up with a shiny ugly model in the game. MCP solves this by turning down the brightness of the environment maps, making the model look *kind of* as if it had a normal map applied to it. I say kind of because it does not really look as good as normal mapping usually does. It was a hacky way to do it, but it was the only way at the time, and therefore the best way.
Morrowind does not recognize normal maps (they weren't really a "thing" yet in 2002), so even if you have a normal map,
Morrowind will not load and display it. MCP has a clever way to solve this issue, by using something Morrowind *does* support,
namely environment maps. You could add a tag for an environment map and then add a normal map as the environment map,
but you'd end up with a shiny ugly model in the game. MCP solves this by turning down the brightness of the environment maps,
making the model look *kind of* as if it had a normal map applied to it.
I say kind of because it does not really look as good as normal mapping usually does. It was a hacky way to do it,
but it was the only way at the time, and therefore the best way.
The biggest problem with this is not that it doesn't look as good as it could no, the biggest problem in my opinion is that it requires you to state the file paths for your normal map textures *in the models*! For buildings, which often use several textures for one single model file, it could take *ages* to do this, and you had to do it for dozens of model files too. You also had to ship your texture pack with model files, making your mod bigger in file size.
The biggest problem with this is not that it doesn't look as good as it could no,
the biggest problem in my opinion is that it requires you to state the file paths for your normal map textures *in the models*!
For buildings, which often use several textures for one single model file, it could take *ages* to do this,
and you had to do it for dozens of model files too. You also had to ship your texture pack with model files,
making your mod bigger in file size.
These are basically the reasons why OpenMW does not support fake bump maps like MCP does. It is just a really bad way to enhance your models, all the more when you have the possibility to do it in a better way.
These are basically the reasons why OpenMW does not support fake bump maps like MCP does.
It is just a really bad way to enhance your models, all the more when you have the possibility to do it in a better way.
Normal mapping in Morrowind with MGE XE
***************************************
@ -86,9 +113,18 @@ Normal mapping in Morrowind with MGE XE
**Conversion difficulty:**
*Easy*
The most recent feature on this topic is that the Morrowind Graphics Extender (MGE) finally started to support real normal mapping in an experimental version available here: `MGE XE`_ (you can't use MGE with OpenMW!). Not only this but it also adds full support for physically based rendering (PBR), making it one step ahead of OpenMW in terms of texturing techniques. However, OpenMW will probably have this feature in the future too and let's hope that OpenMW and MGE will handle PBR in a similar fashion in the future so that mods can be used for both MGE and OpenMW without any hassle.
The most recent feature on this topic is that the Morrowind Graphics Extender (MGE) finally started to support real
normal mapping in an experimental version available here: `MGE XE`_ (you can't use MGE with OpenMW!).
Not only this but it also adds full support for physically based rendering (PBR),
making it one step ahead of OpenMW in terms of texturing techniques. However,
OpenMW will probably have this feature in the future too and let's hope that OpenMW and MGE will handle PBR in a
similar fashion in the future so that mods can be used for both MGE and OpenMW without any hassle.
I haven't researched that much on the MGE variant yet but it does support real implementation of normal mapping, making it really easy to convert mods made for MGE into OpenMW (I'm only talking about the normal map textures though). There's some kind of text file if I understood it correctly that MGE uses to find the normal map. OpenMW does not need this, you just have to make sure the normal map has the same name as the diffuse texture but with the correct suffix after.
I haven't researched that much on the MGE variant yet but it does support real implementation of normal mapping,
making it really easy to convert mods made for MGE into OpenMW (I'm only talking about the normal map textures though).
There's some kind of text file if I understood it correctly that MGE uses to find the normal map.
OpenMW does not need this, you just have to make sure the normal map has the same name as the diffuse texture but with
the correct suffix after.
Now, on to the tutorials.
@ -99,14 +135,21 @@ Converting PeterBitt's Scamp Replacer
:Authors: Joakim (Lysol) Berg
:Updated: 2016-11-11
So, let's say you've found out that PeterBitt_ makes awesome models and textures featuring physically based rendering (PBR) and normal maps. Let's say that you tried to run his `PBR Scamp Replacer`_ in OpenMW and that you were greatly disappointed when the normal map didn't seem to work. Lastly, let's say you came here, looking for some answers. Am I right? Great. Because you've come to the right place!
So, let's say you've found out that PeterBitt_ makes awesome models and textures featuring physically based rendering
(PBR) and normal maps. Let's say that you tried to run his `PBR Scamp Replacer`_ in OpenMW and that you were greatly
disappointed when the normal map didn't seem to work. Lastly, let's say you came here, looking for some answers.
Am I right? Great. Because you've come to the right place!
*A quick note before we begin*: Please note that you can only use the normal map texture and not the rest of the materials, since PBR isn't implemented in OpenMW yet. Sometimes PBR textures can look dull without all of the texture files, so have that in mind.
*A quick note before we begin*: Please note that you can only use the normal map texture and not the rest of the materials,
since PBR isn't implemented in OpenMW yet. Sometimes PBR textures can look dull without all of the texture files,
so have that in mind.
Tutorial - MGE
**************
In this tutorial, I will use PeterBitt's `PBR Scamp Replacer`_ as an example, but any mod featuring PBR that requires the PBR version of MGE will do, provided it also includes a normal map (which it probably does).
In this tutorial, I will use PeterBitt's `PBR Scamp Replacer`_ as an example,
but any mod featuring PBR that requires the PBR version of MGE will do,
provided it also includes a normal map (which it probably does).
So, follow these steps:
@ -120,11 +163,14 @@ So, follow these steps:
#. Rename your newly extracted file (``tx_Scamp_normals.dds``) to ``tx_Scamp_n.dds`` (which is exactly the same name as the diffuse texture file, except for the added *_n* suffix before the filename extention).
#. You're actually done!
So as you might notice, converting these mods is very simple and takes just a couple of minutes. It's more or less just a matter of renaming and moving a few files.
So as you might notice, converting these mods is very simple and takes just a couple of minutes.
It's more or less just a matter of renaming and moving a few files.
I totally recommend you to also try this on PeterBitt's Nix Hound replacer and Flash3113's various replacers. It should be the same principle to get those to work.
I totally recommend you to also try this on PeterBitt's Nix Hound replacer and Flash3113's various replacers.
It should be the same principle to get those to work.
And let's hope that some one implements PBR shaders to OpenMW too, so that we can use all the material files of these mods in the future.
And let's hope that some one implements PBR shaders to OpenMW too,
so that we can use all the material files of these mods in the future.
Converting Lougian's Hlaalu Bump mapped
---------------------------------------
@ -133,12 +179,17 @@ Converting Lougian's Hlaalu Bump mapped
:Authors: Joakim (Lysol) Berg
:Updated: 2016-11-11
Converting textures made for the Morrowind Code Patch (MCP) fake bump mapping can be really easy or a real pain, depending on a few circumstances. In this tutorial, we will look at a very easy, although in some cases a bit time-consuming, example.
Converting textures made for the Morrowind Code Patch (MCP) fake bump mapping can be really easy or a real pain,
depending on a few circumstances. In this tutorial, we will look at a very easy,
although in some cases a bit time-consuming, example.
Tutorial - MCP, Part 1
**********************
We will be converting a quite popular texture replacer of the Hlaalu architecture, namely Lougian's `Hlaalu Bump mapped`_. Since this is just a texture pack and not a model replacer, we can convert the mod in a few minutes by just renaming a few dozen files and by *not* extracting the included model (``.nif``) files when installing the mod.
We will be converting a quite popular texture replacer of the Hlaalu architecture, namely Lougian's `Hlaalu Bump mapped`_.
Since this is just a texture pack and not a model replacer,
we can convert the mod in a few minutes by just renaming a few dozen files and by *not* extracting the included model
(``.nif``) files when installing the mod.
#. Download Lougian's `Hlaalu Bump mapped`_.
#. Install the mod by extracting the ``./Textures`` folder to a data folder the way you usually install mods (**Pro tip**: Install using OpenMW's `Multiple data folders`_ function!).
@ -149,7 +200,8 @@ We will be converting a quite popular texture replacer of the Hlaalu architectur
- As a nice bonus to this tutorial, this pack actually included one specularity texture too. We should use it of course. It's the one called "``tx_glass_amber_02_reflection.dds``". For OpenMW to recognize this file and use it as a specular map, you need to change the *_reflection.dds* part to *_spec.dds*, resulting in the name ``tx_glass_amber_01_spec.dds``.
#. That should be it. Really simple, but I do know that it takes a few minutes to rename all those files.
Now if the mod you want to change includes custom made models it gets a bit more complicated I'm afraid. But that is for the next tutorial.
Now if the mod you want to change includes custom made models it gets a bit more complicated I'm afraid.
But that is for the next tutorial.
Converting Apel's Various Things - Sacks
----------------------------------------
@ -158,12 +210,16 @@ Converting Apel's Various Things - Sacks
:Authors: Joakim (Lysol) Berg
:Updated: 2016-11-09
In part one of this tutorial, we converted a mod that only included modified Morrowind model (``.nif``) files so that the normal maps could be loaded in Morrowind with MCP. We ignored those model files since they are not needed with OpenMW. In this tutorial however, we will convert a mod that includes new, custom made models. In other words, we cannot just ignore those files this time.
In part one of this tutorial, we converted a mod that only included modified Morrowind model (``.nif``)
files so that the normal maps could be loaded in Morrowind with MCP.
We ignored those model files since they are not needed with OpenMW. In this tutorial however,
we will convert a mod that includes new, custom made models. In other words, we cannot just ignore those files this time.
Tutorial - MCP, Part 2
**********************
The sacks included in Apel's `Various Things - Sacks`_ come in two versions Without bump mapping, and with bump mapping. Since we want the glory of normal mapping in our OpenMW setup, we will go with the bump-mapped version.
The sacks included in Apel's `Various Things - Sacks`_ come in two versions Without bump mapping, and with bump mapping.
Since we want the glory of normal mapping in our OpenMW setup, we will go with the bump-mapped version.
#. Start by downloading Apel's `Various Things - Sacks`_ from Nexus.
#. Once downloaded, install it the way you'd normally install your mods (**Pro tip**: Install using OpenMW's `Multiple data folders`_ function!).
@ -181,7 +237,9 @@ The sacks included in Apel's `Various Things - Sacks`_ come in two versions
#. OpenMW detects normal maps if they have the same name as the base diffuse texture, but with a *_n.dds* suffix. In this mod, the normal maps has a suffix of *_nm.dds*. Change all the files that ends with *_nm.dds* to instead end with *_n.dds*.
#. Finally, `we are done`_!
Since these models have one or two textures applied to them, the fix was not that time-consuming. It gets worse when you have to fix a model that uses loads of textures. The principle is the same, it just requires more manual work which is annoying and takes time.
Since these models have one or two textures applied to them, the fix was not that time-consuming.
It gets worse when you have to fix a model that uses loads of textures. The principle is the same,
it just requires more manual work which is annoying and takes time.
.. _`Netch Bump mapped`: https://www.nexusmods.com/morrowind/mods/42851/?
.. _`Hlaalu Bump mapped`: https://www.nexusmods.com/morrowind/mods/42396/?

@ -19,10 +19,16 @@ OpenMW automatically uses shaders for objects with these mapping techniques.
Normal Mapping
##############
To plug in a normal map, you name the normal map as the diffuse texture but with a specified suffix after. OpenMW will then recognise the file and load it as a normal map, provided you have set up your settings file correctly. See the section `Automatic use`_ further down below for detailed information.
To plug in a normal map, you name the normal map as the diffuse texture but with a specified suffix after.
OpenMW will then recognise the file and load it as a normal map, provided you have set up your settings file correctly.
See the section `Automatic use`_ further down below for detailed information.
.. note::
While the original Morrowind engine does support the loading of a BumpTexture slot in the NIF, it will not display it as a normal map. Morrowind Code Patch (MCP) added a way to hack normal maps into the engine by first enabling the engine to load the BumpTexture slot as an environment map and then turn down the brightness of the environment map. This will imitate how a real normal map shader would display a normal map, but it will not look exactly the same.
While the original Morrowind engine does support the loading of a BumpTexture slot in the NIF,
it will not display it as a normal map. Morrowind Code Patch (MCP)
added a way to hack normal maps into the engine by first enabling the engine to load the BumpTexture slot as an
environment map and then turn down the brightness of the environment map.
This will imitate how a real normal map shader would display a normal map, but it will not look exactly the same.
OpenMW uses standard normal mapping, which achieves much better results.
Unfortunately, this difference can result in incompatibilities.
Some mods

@ -1046,58 +1046,35 @@ bool TiXmlDocument::LoadFile( FILE* file, TiXmlEncoding encoding )
return false;
}
const char* lastPos = buf;
const char* p = buf;
const char* p = buf; // the read head
char* q = buf; // the write head
const char CR = 0x0d;
const char LF = 0x0a;
buf[length] = 0;
while( *p ) {
assert( p < (buf+length) );
if ( *p == 0xa ) {
// Newline character. No special rules for this. Append all the characters
// since the last string, and include the newline.
data.append( lastPos, (p-lastPos+1) ); // append, include the newline
++p; // move past the newline
lastPos = p; // and point to the new buffer (may be 0)
assert( p <= (buf+length) );
}
else if ( *p == 0xd ) {
// Carriage return. Append what we have so far, then
// handle moving forward in the buffer.
if ( (p-lastPos) > 0 ) {
data.append( lastPos, p-lastPos ); // do not add the CR
}
data += (char)0xa; // a proper newline
if ( *(p+1) == 0xa ) {
// Carriage return - new line sequence
p += 2;
lastPos = p;
assert( p <= (buf+length) );
}
else {
// it was followed by something else...that is presumably characters again.
++p;
lastPos = p;
assert( p <= (buf+length) );
assert( q <= (buf+length) );
assert( q <= p );
if ( *p == CR ) {
*q++ = LF;
p++;
if ( *p == LF ) { // check for CR+LF (and skip LF)
p++;
}
}
else {
++p;
*q++ = *p++;
}
}
// Handle any left over characters.
if ( p-lastPos ) {
data.append( lastPos, p-lastPos );
}
delete [] buf;
buf = 0;
assert( q <= (buf+length) );
*q = 0;
Parse( data.c_str(), 0, encoding );
Parse( buf, 0, encoding );
if ( Error() )
return false;
else
return true;
delete [] buf;
return !Error();
}

@ -73,7 +73,32 @@
<Property key="TextAlign" value="Right"/>
</Widget>
</Widget>
<Widget type="HBox" skin="" position="4 200 260 24">
<Widget type="Widget" skin="" position="4 184 352 54" align="Left Top HStretch">
<Widget type="TextBox" skin="NormalText" position="0 0 352 16" align="Left Top" name="ActorProcessingText">
<Property key="Caption" value="Actors Processing Range"/>
</Widget>
<Widget type="MWScrollBar" skin="MW_HScroll" position="0 20 352 14" align="Left Top HStretch">
<Property key="Range" value="3584"/>
<Property key="Page" value="300"/>
<UserString key="SettingType" value="Slider"/>
<UserString key="SettingCategory" value="Game"/>
<UserString key="SettingName" value="actors processing range"/>
<UserString key="SettingValueType" value="Float"/>
<UserString key="SettingMin" value="3584"/>
<UserString key="SettingMax" value="7168"/>
<UserString key="SettingLabelWidget" value="ActorProcessingText"/>
<UserString key="SettingLabelCaption" value="Actors Processing Range"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 38 352 16" align="Left Top">
<Property key="Caption" value="#{sLow}"/>
<Property key="TextAlign" value="Left"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 38 352 16" align="Right Top">
<Property key="Caption" value="#{sHigh}"/>
<Property key="TextAlign" value="Right"/>
</Widget>
</Widget>
<Widget type="HBox" skin="" position="4 250 260 24">
<Widget type="AutoSizedButton" skin="MW_Button" position="0 0 24 24" align="Left Top">
<UserString key="SettingCategory" value="Saves"/>
<UserString key="SettingName" value="autosave"/>
@ -83,7 +108,7 @@
<Property key="Caption" value="#{sQuick_Save}"/>
</Widget>
</Widget>
<Widget type="HBox" skin="" position="4 230 260 24">
<Widget type="HBox" skin="" position="4 280 260 24">
<Widget type="AutoSizedButton" skin="MW_Button" position="0 0 24 24" align="Left Top">
<UserString key="SettingCategory" value="Game"/>
<UserString key="SettingName" value="best attack"/>
@ -93,7 +118,7 @@
<Property key="Caption" value="#{sBestAttack}"/>
</Widget>
</Widget>
<Widget type="HBox" skin="" position="4 260 260 24">
<Widget type="HBox" skin="" position="4 310 260 24">
<Widget type="AutoSizedButton" skin="MW_Button" position="0 0 24 24" align="Left Top">
<UserString key="SettingCategory" value="GUI"/>
<UserString key="SettingName" value="subtitles"/>
@ -103,7 +128,7 @@
<Property key="Caption" value="#{sSubtitles}"/>
</Widget>
</Widget>
<Widget type="HBox" skin="" position="4 290 260 24">
<Widget type="HBox" skin="" position="4 340 260 24">
<Widget type="AutoSizedButton" skin="MW_Button" position="0 0 24 24" align="Left Top">
<UserString key="SettingCategory" value="HUD"/>
<UserString key="SettingName" value="crosshair"/>
@ -199,7 +224,7 @@
</Widget>
</Widget>
<Widget type="TextBox" skin="NormalText" position="4 254 336 18" align="Left Bottom">
<Property key="Caption" value="Camera sensitivity"/>
<Property key="Caption" value="Camera Sensitivity"/>
</Widget>
<Widget type="MWScrollBar" skin="MW_HScroll" position="4 278 336 18" align="HStretch Bottom">
<Property key="Range" value="10000"/>

@ -94,6 +94,7 @@
<file alias="edit-preview">record-preview.png</file>
<file alias="edit-clone">record-clone.png</file>
<file alias="edit-add">record-add.png</file>
<file alias="edit-undo">record-revert.png</file>
<file alias="resources-icon">resources-icon.png</file>
<file alias="resources-mesh">resources-mesh.png</file>
<file alias="resources-music">resources-music.png</file>

@ -200,6 +200,9 @@ best attack = false
# Difficulty. Expressed as damage dealt and received. (e.g. -100 to 100).
difficulty = 0
# The maximum range of actor AI, animations and physics updates.
actors processing range = 7168
# Make reflected Absorb spells have no practical effect, like in Morrowind.
classic reflected absorb spells behavior = true

Loading…
Cancel
Save