1
0
Fork 1
mirror of https://github.com/TES3MP/openmw-tes3mp.git synced 2025-01-16 00:19:56 +00:00

Merge branch 'master' into openxr_vr

This commit is contained in:
Mads Buvik Sandvei 2020-01-26 20:43:46 +01:00
commit 3984e2030a
74 changed files with 1200 additions and 405 deletions

View file

@ -28,6 +28,7 @@
Bug #4262: Rain settings are hardcoded
Bug #4270: Closing doors while they are obstructed desyncs closing sfx
Bug #4276: Resizing character window differs from vanilla
Bug #4284: ForceSneak behaviour is inconsistent if the target has AiWander package
Bug #4329: Removed birthsign abilities are restored after reloading the save
Bug #4341: Error message about missing GDB is too vague
Bug #4383: Bow model obscures crosshair when arrow is drawn
@ -41,6 +42,7 @@
Bug #4600: Crash when no sound output is available or --no-sound is used.
Bug #4639: Black screen after completing first mages guild mission + training
Bug #4650: Focus is lost after pressing ESC in confirmation dialog inside savegame dialog
Bug #4680: Heap corruption on faulty esp
Bug #4701: PrisonMarker record is not hardcoded like other markers
Bug #4703: Editor: it's possible to preview levelled list records
Bug #4705: Editor: unable to open exterior cell views from Instances table
@ -133,6 +135,7 @@
Bug #5063: Shape named "Tri Shadow" in creature mesh is visible if it isn't hidden
Bug #5067: Ranged attacks on unaware opponents ("critical hits") differ from the vanilla engine
Bug #5069: Blocking creatures' attacks doesn't degrade shields
Bug #5073: NPCs open doors in front of them even if they don't have to
Bug #5074: Paralyzed actors greet the player
Bug #5075: Enchanting cast style can be changed if there's no object
Bug #5078: DisablePlayerLooking is broken
@ -178,6 +181,7 @@
Bug #5209: Spellcasting ignores race height
Bug #5210: AiActivate allows actors to open dialogue and inventory windows
Bug #5211: Screen fades in if the first loaded save is in interior cell
Bug #5212: AiTravel does not work for actors outside of AI processing range
Bug #5213: SameFaction script function is broken
Bug #5218: Crash when disabling ToggleBorders
Bug #5220: GetLOS crashes when actor isn't loaded
@ -185,16 +189,25 @@
Bug #5223: Bow replacement during attack animation removes attached arrow
Bug #5226: Reputation should be capped
Bug #5229: Crash if mesh controller node has no data node
Bug #5239: OpenMW-CS does not support non-ASCII characters in path names
Bug #5241: On-self absorb spells cannot be detected
Bug #5242: ExplodeSpell behavior differs from Cast behavior
Bug #5249: Wandering NPCs start walking too soon after they hello
Bug #5250: Creatures display shield ground mesh instead of shield body part
Bug #5255: "GetTarget, player" doesn't return 1 during NPC hello
Feature #1774: Handle AvoidNode
Feature #2229: Improve pathfinding AI
Feature #3025: Analogue gamepad movement controls
Feature #3442: Default values for fallbacks from ini file
Feature #3517: Multiple projectiles enchantment
Feature #3610: Option to invert X axis
Feature #3871: Editor: Terrain Selection
Feature #3893: Implicit target for "set" function in console
Feature #3980: In-game option to disable controller
Feature #3999: Shift + Double Click should maximize/restore menu size
Feature #4001: Toggle sneak controller shortcut
Feature #4068: OpenMW-CS: Add a button to reset key bindings to defaults
Feature #4129: Beta Comment to File
Feature #4209: Editor: Faction rank sub-table
Feature #4255: Handle broken RepairedOnMe script function
Feature #4316: Implement RaiseRank/LowerRank functions properly
@ -235,6 +248,7 @@
Feature #5147: Show spell magicka cost in spell buying window
Feature #5170: Editor: Land shape editing, land selection
Feature #5193: Weapon sheathing
Feature #5219: Impelement TestCells console command
Feature #5224: Handle NiKeyframeController for NiTriShape
Task #4686: Upgrade media decoder to a more current FFmpeg API
Task #4695: Optimize Distant Terrain memory consumption

View file

@ -2,6 +2,7 @@
#include <iomanip>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
#include <osgDB/ReadFile>
@ -349,7 +350,7 @@ namespace ESSImport
writer.setFormat (ESM::SavedGame::sCurrentFormat);
std::ofstream stream(mOutFile.c_str(), std::ios::binary);
boost::filesystem::ofstream stream(boost::filesystem::path(mOutFile), std::ios::out | std::ios::binary);
// all unused
writer.setVersion(0);
writer.setType(0);

View file

@ -375,7 +375,12 @@ void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles)
// The following code will run only if there is not another thread currently running it
CellNameLoader cellNameLoader;
#if QT_VERSION >= QT_VERSION_CHECK(5,14,0)
QSet<QString> set = cellNameLoader.getCellNames(selectedFiles);
QStringList cellNamesList(set.begin(), set.end());
#else
QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(selectedFiles));
#endif
std::sort(cellNamesList.begin(), cellNamesList.end());
emit signalLoadedCellsChanged(cellNamesList);
}

View file

@ -1,7 +1,6 @@
#include "document.hpp"
#include <cassert>
#include <fstream>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
@ -289,19 +288,22 @@ CSMDoc::Document::Document (const Files::ConfigurationManager& configuration,
if (mNew || !boost::filesystem::exists (mProjectPath))
{
boost::filesystem::path customFiltersPath (configuration.getUserDataPath());
customFiltersPath /= "defaultfilters";
boost::filesystem::path filtersPath (configuration.getUserDataPath() / "defaultfilters");
std::ofstream destination (mProjectPath.string().c_str(), std::ios::binary);
boost::filesystem::ofstream destination(mProjectPath, std::ios::out | std::ios::binary);
if (!destination.is_open())
throw std::runtime_error("Can not create project file: " + mProjectPath.string());
destination.exceptions(std::ios::failbit | std::ios::badbit);
if (boost::filesystem::exists (customFiltersPath))
{
destination << std::ifstream(customFiltersPath.string().c_str(), std::ios::binary).rdbuf();
}
else
{
destination << std::ifstream(std::string(mResDir.string() + "/defaultfilters").c_str(), std::ios::binary).rdbuf();
}
if (!boost::filesystem::exists (filtersPath))
filtersPath = mResDir / "defaultfilters";
boost::filesystem::ifstream source(filtersPath, std::ios::in | std::ios::binary);
if (!source.is_open())
throw std::runtime_error("Can not read filters file: " + filtersPath.string());
source.exceptions(std::ios::failbit | std::ios::badbit);
destination << source.rdbuf();
}
if (mNew)

View file

@ -4,11 +4,13 @@
#include <QComboBox>
#include <QGridLayout>
#include <QPushButton>
#include <QStackedLayout>
#include <QVBoxLayout>
#include "../../model/prefs/setting.hpp"
#include "../../model/prefs/category.hpp"
#include "../../model/prefs/state.hpp"
namespace CSVPrefs
{
@ -29,8 +31,18 @@ namespace CSVPrefs
mPageSelector = new QComboBox();
connect(mPageSelector, SIGNAL(currentIndexChanged(int)), mStackedLayout, SLOT(setCurrentIndex(int)));
QFrame* lineSeparator = new QFrame(topWidget);
lineSeparator->setFrameShape(QFrame::HLine);
lineSeparator->setFrameShadow(QFrame::Sunken);
// Reset key bindings button
QPushButton* resetButton = new QPushButton ("Reset to Defaults", topWidget);
connect(resetButton, SIGNAL(clicked()), this, SLOT(resetKeyBindings()));
topLayout->addWidget(mPageSelector);
topLayout->addWidget(stackedWidget);
topLayout->addWidget(lineSeparator);
topLayout->addWidget(resetButton);
topLayout->setSizeConstraint(QLayout::SetMinAndMaxSize);
// Add each option
@ -85,4 +97,9 @@ namespace CSVPrefs
}
}
}
void KeyBindingPage::resetKeyBindings()
{
CSMPrefs::State::get().resetCategory("Key Bindings");
}
}

View file

@ -29,6 +29,10 @@ namespace CSVPrefs
QStackedLayout* mStackedLayout;
QGridLayout* mPageLayout;
QComboBox* mPageSelector;
private slots:
void resetKeyBindings();
};
}

View file

@ -118,6 +118,9 @@ namespace MWBase
virtual MWWorld::CellStore *getCell (const ESM::CellId& id) = 0;
virtual void testExteriorCells() = 0;
virtual void testInteriorCells() = 0;
virtual void useDeathCamera() = 0;
virtual void setWaterHeight(const float height) = 0;

View file

@ -400,17 +400,7 @@ namespace MWGui
struct tm* timeinfo;
timeinfo = localtime(&time);
// Use system/environment locale settings for datetime formatting
char* oldLctime = setlocale(LC_TIME, nullptr);
setlocale(LC_TIME, "");
const int size=1024;
char buffer[size];
if (std::strftime(buffer, size, "%x %X", timeinfo) > 0)
text << buffer << "\n";
// reset
setlocale(LC_TIME, oldLctime);
text << std::put_time(timeinfo, "%Y.%m.%d %T") << "\n";
text << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n";
text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCell << "}\n";

View file

@ -7,8 +7,6 @@
#include <MyGUI_Gui.h>
#include <MyGUI_TabControl.h>
#include <boost/algorithm/string.hpp>
#include <SDL_video.h>
#include <iomanip>
@ -45,10 +43,10 @@ namespace
void parseResolution (int &x, int &y, const std::string& str)
{
std::vector<std::string> split;
boost::algorithm::split (split, str, boost::is_any_of("@(x"));
Misc::StringUtils::split (str, split, "@(x");
assert (split.size() >= 2);
boost::trim(split[0]);
boost::trim(split[1]);
Misc::StringUtils::trim(split[0]);
Misc::StringUtils::trim(split[1]);
x = MyGUI::utility::parseInt (split[0]);
y = MyGUI::utility::parseInt (split[1]);
}

View file

@ -1601,6 +1601,8 @@ namespace MWInput
return "Zoom In";
else if (action == A_ZoomOut)
return "Zoom Out";
else if (action == A_ToggleHUD)
return "Toggle HUD";
descriptions[A_Use] = "sUse";
descriptions[A_Activate] = "sActivate";
@ -1779,6 +1781,7 @@ namespace MWInput
ret.push_back(A_Console);
ret.push_back(A_QuickSave);
ret.push_back(A_QuickLoad);
ret.push_back(A_ToggleHUD);
ret.push_back(A_Screenshot);
ret.push_back(A_QuickKeysMenu);
ret.push_back(A_QuickKey1);
@ -1812,6 +1815,7 @@ namespace MWInput
ret.push_back(A_Rest);
ret.push_back(A_QuickSave);
ret.push_back(A_QuickLoad);
ret.push_back(A_ToggleHUD);
ret.push_back(A_Screenshot);
ret.push_back(A_QuickKeysMenu);
ret.push_back(A_QuickKey1);
@ -1852,7 +1856,7 @@ namespace MWInput
}
// Disallow binding reserved keys
if (key == SDL_SCANCODE_F3 || key == SDL_SCANCODE_F4 || key == SDL_SCANCODE_F10 || key == SDL_SCANCODE_F11)
if (key == SDL_SCANCODE_F3 || key == SDL_SCANCODE_F4 || key == SDL_SCANCODE_F10)
return;
#ifndef __APPLE__

View file

@ -142,8 +142,9 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float
namespace MWMechanics
{
static const int GREETING_SHOULD_START = 4; //how many updates should pass before NPC can greet player
static const int GREETING_SHOULD_END = 10;
static const int GREETING_SHOULD_START = 4; // how many updates should pass before NPC can greet player
static const int GREETING_SHOULD_END = 20; // how many updates should pass before NPC stops turning to player
static const int GREETING_COOLDOWN = 40; // how many updates should pass before NPC can continue movement
static const float DECELERATE_DISTANCE = 512.f;
class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor
@ -519,9 +520,10 @@ namespace MWMechanics
{
greetingTimer++;
turnActorToFacePlayer(actor, dir);
if (greetingTimer <= GREETING_SHOULD_END || MWBase::Environment::get().getSoundManager()->sayActive(actor))
turnActorToFacePlayer(actor, dir);
if (greetingTimer >= GREETING_SHOULD_END)
if (greetingTimer >= GREETING_COOLDOWN)
{
greetingState = Greet_Done;
greetingTimer = 0;
@ -1695,6 +1697,11 @@ namespace MWMechanics
}
}
}
else if (aiActive && iter->first != player && isConscious(iter->first))
{
CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first);
stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true);
}
if(iter->first.getClass().isNpc())
{
@ -1722,7 +1729,15 @@ namespace MWMechanics
{
const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length();
bool isPlayer = iter->first == player;
bool inRange = isPlayer || dist <= mActorsProcessingRange;
CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first);
// Actors with active AI should be able to move.
bool alwaysActive = false;
if (!isPlayer && isConscious(iter->first) && !stats.isParalyzed())
{
MWMechanics::AiSequence& seq = stats.getAiSequence();
alwaysActive = !seq.isEmpty() && seq.getActivePackage()->alwaysActive();
}
bool inRange = isPlayer || dist <= mActorsProcessingRange || alwaysActive;
int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower)
if (isPlayer)
activeFlag = 2;

View file

@ -197,7 +197,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
zTurn(actor, mPathFinder.getZAngleToNext(position.x(), position.y()));
smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0);
mObstacleCheck.update(actor, duration);
const auto destination = mPathFinder.getPath().empty() ? dest : mPathFinder.getPath().front();
mObstacleCheck.update(actor, destination, duration);
// handle obstacles on the way
evadeObstacles(actor);

View file

@ -104,6 +104,9 @@ namespace MWMechanics
virtual osg::Vec3f getDestination() { return osg::Vec3f(0, 0, 0); }
// Return true if any loaded actor with this AI package must be active.
virtual bool alwaysActive() const { return false; }
/// Reset pathfinding state
void reset();

View file

@ -198,7 +198,7 @@ bool isActualAiPackage(int packageTypeId)
packageTypeId <= AiPackage::TypeIdActivate);
}
void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration)
void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange)
{
if(actor != getPlayer())
{
@ -209,6 +209,9 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac
}
MWMechanics::AiPackage* package = mPackages.front();
if (!package->alwaysActive() && outOfRange)
return;
int packageTypeId = package->getTypeId();
// workaround ai packages not being handled as in the vanilla engine
if (isActualAiPackage(packageTypeId))

View file

@ -110,7 +110,7 @@ namespace MWMechanics
void stopPursuit();
/// Execute current package, switching if needed.
void execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration);
void execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange=false);
/// Simulate the passing of time using the currently active AI package
void fastForward(const MWWorld::Ptr &actor);

View file

@ -34,6 +34,8 @@ namespace MWMechanics
virtual bool useVariableSpeed() const { return true;}
virtual bool alwaysActive() const { return true; }
virtual osg::Vec3f getDestination() { return osg::Vec3f(mX, mY, mZ); }
private:

View file

@ -3,6 +3,7 @@
#include <components/debug/debuglog.hpp>
#include <components/misc/rng.hpp>
#include <components/esm/aisequence.hpp>
#include <components/detournavigator/navigator.hpp>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
@ -52,6 +53,14 @@ namespace MWMechanics
return 1;
return COUNT_BEFORE_RESET;
}
osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance)
{
const float randomDirection = Misc::Rng::rollClosedProbability() * 2.0f * osg::PI;
osg::Matrixf rotation;
rotation.makeRotate(randomDirection, osg::Vec3f(0.0, 0.0, 1.0));
return position + osg::Vec3f(distance, 0.0, 0.0) * rotation;
}
}
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat):
@ -310,14 +319,24 @@ namespace MWMechanics
std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here
const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor);
const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor);
const auto world = MWBase::Environment::get().getWorld();
const auto halfExtents = world->getPathfindingHalfExtents(actor);
const auto navigator = world->getNavigator();
const auto navigatorFlags = getNavigatorFlags(actor);
do {
// Determine a random location within radius of original position
const float wanderRadius = (0.2f + Misc::Rng::rollClosedProbability() * 0.8f) * wanderDistance;
const float randomDirection = Misc::Rng::rollClosedProbability() * 2.0f * osg::PI;
const float destinationX = mInitialActorPosition.x() + wanderRadius * std::cos(randomDirection);
const float destinationY = mInitialActorPosition.y() + wanderRadius * std::sin(randomDirection);
const float destinationZ = mInitialActorPosition.z();
mDestination = osg::Vec3f(destinationX, destinationY, destinationZ);
if (!isWaterCreature && !isFlyingCreature)
{
// findRandomPointAroundCircle uses wanderDistance as limit for random and not as exact distance
if (const auto destination = navigator->findRandomPointAroundCircle(halfExtents, currentPosition, wanderDistance, navigatorFlags))
mDestination = *destination;
else
mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius);
}
else
mDestination = getRandomPointAround(mInitialActorPosition, wanderRadius);
// Check if land creature will walk onto water or if water creature will swim onto land
if (!isWaterCreature && destinationIsAtWater(actor, mDestination))
@ -327,15 +346,9 @@ namespace MWMechanics
continue;
if (isWaterCreature || isFlyingCreature)
{
mPathFinder.buildStraightPath(mDestination);
}
else
{
const osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getPathfindingHalfExtents(actor);
mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, halfExtents,
getNavigatorFlags(actor));
}
mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, halfExtents, navigatorFlags);
if (mPathFinder.isPathConstructed())
{

View file

@ -2249,7 +2249,7 @@ void CharacterController::update(float duration, bool animationOnly)
if(movestate != CharState_None && !isTurning())
clearAnimQueue();
if(mAnimQueue.empty() || inwater || sneak)
if(mAnimQueue.empty() || inwater || (sneak && mIdleState != CharState_SpecialIdle))
{
if (inwater)
idlestate = CharState_IdleSwim;

View file

@ -1,6 +1,7 @@
#include "enchanting.hpp"
#include <components/misc/rng.hpp>
#include <components/settings/settings.hpp>
#include "../mwworld/manualref.hpp"
#include "../mwworld/class.hpp"
@ -62,7 +63,6 @@ namespace MWMechanics
const MWWorld::Ptr& player = getPlayer();
MWWorld::ContainerStore& store = player.getClass().getContainerStore(player);
ESM::Enchantment enchantment;
enchantment.mData.mCharge = getGemCharge();
enchantment.mData.mAutocalc = 0;
enchantment.mData.mType = mCastStyle;
enchantment.mData.mCost = getBaseCastCost();
@ -81,19 +81,26 @@ namespace MWMechanics
mEnchanter.getClass().skillUsageSucceeded (mEnchanter, ESM::Skill::Enchant, 2);
}
if(mCastStyle==ESM::Enchantment::ConstantEffect)
{
enchantment.mData.mCharge=0;
}
enchantment.mEffects = mEffectList;
int count = getEnchantItemsCount();
if(mCastStyle==ESM::Enchantment::ConstantEffect)
enchantment.mData.mCharge = 0;
else
enchantment.mData.mCharge = getGemCharge() / count;
// Try to find a dynamic enchantment with the same stats, create a new one if not found.
const ESM::Enchantment* enchantmentPtr = getRecord(enchantment);
if (enchantmentPtr == nullptr)
enchantmentPtr = MWBase::Environment::get().getWorld()->createRecord (enchantment);
// Apply the enchantment
const ESM::Enchantment *enchantmentPtr = MWBase::Environment::get().getWorld()->createRecord (enchantment);
std::string newItemId = mOldItemPtr.getClass().applyEnchantment(mOldItemPtr, enchantmentPtr->mId, getGemCharge(), mNewItemName);
// Add the new item to player inventory and remove the old one
store.remove(mOldItemPtr, 1, player);
store.add(newItemId, 1, player);
store.remove(mOldItemPtr, count, player);
store.add(newItemId, count, player);
if(!mSelfEnchanting)
payForEnchantment();
@ -123,20 +130,22 @@ namespace MWMechanics
}
else if (mWeaponType != -1)
{ // Weapon
ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass;
switch(mCastStyle)
{
case ESM::Enchantment::WhenStrikes:
mCastStyle = ESM::Enchantment::WhenUsed;
if (weapclass == ESM::WeaponType::Melee || weapclass == ESM::WeaponType::Ranged)
mCastStyle = ESM::Enchantment::WhenUsed;
return;
case ESM::Enchantment::WhenUsed:
if (powerfulSoul)
if (powerfulSoul && weapclass != ESM::WeaponType::Ammo && weapclass != ESM::WeaponType::Thrown)
mCastStyle = ESM::Enchantment::ConstantEffect;
else if (getWeaponType(mWeaponType)->mWeaponClass != ESM::WeaponType::Ranged)
else if (weapclass != ESM::WeaponType::Ranged)
mCastStyle = ESM::Enchantment::WhenStrikes;
return;
default: // takes care of Constant effect too
mCastStyle = ESM::Enchantment::WhenUsed;
if (getWeaponType(mWeaponType)->mWeaponClass != ESM::WeaponType::Ranged)
if (weapclass != ESM::WeaponType::Ranged)
mCastStyle = ESM::Enchantment::WhenStrikes;
return;
}
@ -200,6 +209,53 @@ namespace MWMechanics
return enchantmentCost;
}
const ESM::Enchantment* Enchanting::getRecord(const ESM::Enchantment& toFind) const
{
const MWWorld::Store<ESM::Enchantment>& enchantments = MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>();
MWWorld::Store<ESM::Enchantment>::iterator iter (enchantments.begin());
iter += (enchantments.getSize() - enchantments.getDynamicSize());
for (; iter != enchantments.end(); ++iter)
{
if (iter->mEffects.mList.size() != toFind.mEffects.mList.size())
continue;
if (iter->mData.mAutocalc != toFind.mData.mAutocalc
|| iter->mData.mType != toFind.mData.mType
|| iter->mData.mCost != toFind.mData.mCost
|| iter->mData.mCharge != toFind.mData.mCharge)
continue;
// Don't choose an ID that came from the content files, would have unintended side effects
if (!enchantments.isDynamic(iter->mId))
continue;
bool mismatch = false;
for (int i=0; i<static_cast<int> (iter->mEffects.mList.size()); ++i)
{
const ESM::ENAMstruct& first = iter->mEffects.mList[i];
const ESM::ENAMstruct& second = toFind.mEffects.mList[i];
if (first.mEffectID!=second.mEffectID ||
first.mArea!=second.mArea ||
first.mRange!=second.mRange ||
first.mSkill!=second.mSkill ||
first.mAttribute!=second.mAttribute ||
first.mMagnMin!=second.mMagnMin ||
first.mMagnMax!=second.mMagnMax ||
first.mDuration!=second.mDuration)
{
mismatch = true;
break;
}
}
if (!mismatch)
return &(*iter);
}
return nullptr;
}
int Enchanting::getBaseCastCost() const
{
@ -224,6 +280,7 @@ namespace MWMechanics
float priceMultipler = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find ("fEnchantmentValueMult")->mValue.getFloat();
int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mEnchanter, static_cast<int>(getEnchantPoints() * priceMultipler), true);
price *= getEnchantItemsCount() * getTypeMultiplier();
return price;
}
@ -282,13 +339,45 @@ namespace MWMechanics
const float fEnchantmentChanceMult = gmst.find("fEnchantmentChanceMult")->mValue.getFloat();
const float fEnchantmentConstantChanceMult = gmst.find("fEnchantmentConstantChanceMult")->mValue.getFloat();
float x = (a - getEnchantPoints()*fEnchantmentChanceMult + 0.2f * b + 0.1f * c) * stats.getFatigueTerm();
float x = (a - getEnchantPoints() * fEnchantmentChanceMult * getTypeMultiplier() * getEnchantItemsCount() + 0.2f * b + 0.1f * c) * stats.getFatigueTerm();
if (mCastStyle == ESM::Enchantment::ConstantEffect)
x *= fEnchantmentConstantChanceMult;
return static_cast<int>(x);
}
int Enchanting::getEnchantItemsCount() const
{
int count = 1;
float enchantPoints = getEnchantPoints();
if (mWeaponType != -1 && enchantPoints > 0)
{
ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass;
if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo)
{
static const float multiplier = std::max(0.f, std::min(1.0f, Settings::Manager::getFloat("projectiles enchant multiplier", "Game")));
MWWorld::Ptr player = getPlayer();
int itemsInInventoryCount = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId());
count = std::min(itemsInInventoryCount, std::max(1, int(getGemCharge() * multiplier / enchantPoints)));
}
}
return count;
}
float Enchanting::getTypeMultiplier() const
{
static const bool useMultiplier = Settings::Manager::getFloat("projectiles enchant multiplier", "Game") > 0;
if (useMultiplier && mWeaponType != -1 && getEnchantPoints() > 0)
{
ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass;
if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo)
return 0.125f;
}
return 1.f;
}
void Enchanting::payForEnchantment() const
{
const MWWorld::Ptr& player = getPlayer();

View file

@ -4,6 +4,7 @@
#include <string>
#include <components/esm/effectlist.hpp>
#include <components/esm/loadench.hpp>
#include "../mwworld/ptr.hpp"
@ -25,6 +26,8 @@ namespace MWMechanics
std::string mObjectType;
int mWeaponType;
const ESM::Enchantment* getRecord(const ESM::Enchantment& newEnchantment) const;
public:
Enchanting();
void setEnchanter(const MWWorld::Ptr& enchanter);
@ -45,6 +48,8 @@ namespace MWMechanics
int getMaxEnchantValue() const;
int getGemCharge() const;
int getEnchantChance() const;
int getEnchantItemsCount() const;
float getTypeMultiplier() const;
bool soulEmpty() const; //Return true if empty
bool itemEmpty() const; //Return true if empty
void payForEnchantment() const;

View file

@ -77,89 +77,94 @@ namespace MWMechanics
}
ObstacleCheck::ObstacleCheck()
: mWalkState(State_Norm)
, mStuckDuration(0)
, mEvadeDuration(0)
, mDistSameSpot(-1) // avoid calculating it each time
: mWalkState(WalkState::Initial)
, mStateDuration(0)
, mEvadeDirectionIndex(0)
{
}
void ObstacleCheck::clear()
{
mWalkState = State_Norm;
mStuckDuration = 0;
mEvadeDuration = 0;
mWalkState = WalkState::Initial;
}
bool ObstacleCheck::isEvading() const
{
return mWalkState == State_Evade;
return mWalkState == WalkState::Evade;
}
/*
* input - actor, duration (time since last check)
* output - true if evasive action needs to be taken
*
* Walking state transitions (player greeting check not shown):
* Walking state transitions (player greeting check not shown):
*
* MoveNow <------------------------------------+
* | d|
* | |
* +-> State_Norm <---> State_CheckStuck --> State_Evade
* ^ ^ | f ^ | t ^ | |
* | | | | | | | |
* | +---+ +---+ +---+ | u
* | any < t < u |
* +--------------------------------------------+
* Initial ----> Norm <--------> CheckStuck -------> Evade ---+
* ^ ^ | f ^ | t ^ | |
* | | | | | | | |
* | +-+ +---+ +---+ | u
* | any < t < u |
* +---------------------------------------------+
*
* f = one reaction time
* d = proximity to a closed door
* t = how long before considered stuck
* u = how long to move sideways
*
*/
void ObstacleCheck::update(const MWWorld::Ptr& actor, float duration)
void ObstacleCheck::update(const MWWorld::Ptr& actor, const osg::Vec3f& destination, float duration)
{
const osg::Vec3f pos = actor.getRefData().getPosition().asVec3();
const auto position = actor.getRefData().getPosition().asVec3();
if (mDistSameSpot == -1)
mDistSameSpot = DIST_SAME_SPOT * actor.getClass().getSpeed(actor);
const float distSameSpot = mDistSameSpot * duration;
const bool samePosition = (pos - mPrev).length2() < distSameSpot * distSameSpot;
mPrev = pos;
if (mWalkState != State_Evade)
if (mWalkState == WalkState::Initial)
{
if(!samePosition)
mWalkState = WalkState::Norm;
mStateDuration = 0;
mPrev = position;
return;
}
if (mWalkState != WalkState::Evade)
{
const float distSameSpot = DIST_SAME_SPOT * actor.getClass().getSpeed(actor) * duration;
const float prevDistance = (destination - mPrev).length();
const float currentDistance = (destination - position).length();
const float movedDistance = prevDistance - currentDistance;
mPrev = position;
if (movedDistance >= distSameSpot)
{
mWalkState = State_Norm;
mStuckDuration = 0;
mEvadeDuration = 0;
mWalkState = WalkState::Norm;
mStateDuration = 0;
return;
}
mWalkState = State_CheckStuck;
mStuckDuration += duration;
// consider stuck only if position unchanges for a period
if(mStuckDuration < DURATION_SAME_SPOT)
return; // still checking, note duration added to timer
else
if (mWalkState == WalkState::Norm)
{
mStuckDuration = 0;
mWalkState = State_Evade;
chooseEvasionDirection();
mWalkState = WalkState::CheckStuck;
mStateDuration = duration;
return;
}
mStateDuration += duration;
if (mStateDuration < DURATION_SAME_SPOT)
{
return;
}
mWalkState = WalkState::Evade;
mStateDuration = 0;
chooseEvasionDirection();
return;
}
mEvadeDuration += duration;
if(mEvadeDuration >= DURATION_TO_EVADE)
mStateDuration += duration;
if(mStateDuration >= DURATION_TO_EVADE)
{
// tried to evade, assume all is ok and start again
mWalkState = State_Norm;
mEvadeDuration = 0;
mWalkState = WalkState::Norm;
mStateDuration = 0;
mPrev = position;
}
}

View file

@ -32,30 +32,27 @@ namespace MWMechanics
bool isEvading() const;
// Updates internal state, call each frame for moving actor
void update(const MWWorld::Ptr& actor, float duration);
void update(const MWWorld::Ptr& actor, const osg::Vec3f& destination, float duration);
// change direction to try to fix "stuck" actor
void takeEvasiveAction(MWMechanics::Movement& actorMovement) const;
private:
// for checking if we're stuck
osg::Vec3f mPrev;
// directions to try moving in when get stuck
static const float evadeDirections[NUM_EVADE_DIRECTIONS][2];
enum WalkState
enum class WalkState
{
State_Norm,
State_CheckStuck,
State_Evade
Initial,
Norm,
CheckStuck,
Evade
};
WalkState mWalkState;
float mStuckDuration; // accumulate time here while in same spot
float mEvadeDuration;
float mDistSameSpot; // take account of actor's speed
float mStateDuration;
int mEvadeDirectionIndex;
void chooseEvasionDirection();

View file

@ -447,9 +447,9 @@ namespace MWMechanics
if (!checkEffectTarget(effectIt->mEffectID, target, caster, castByPlayer))
continue;
// caster needs to be an actor that's not the target for linked effects (e.g. Absorb)
// caster needs to be an actor for linked effects (e.g. Absorb)
if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked
&& (caster.isEmpty() || !caster.getClass().isActor() || caster == target))
&& (caster.isEmpty() || !caster.getClass().isActor()))
continue;
// If player is healing someone, show the target's HP bar
@ -552,6 +552,15 @@ namespace MWMechanics
effect.mArg = MWMechanics::EffectKey(*effectIt).mArg;
effect.mMagnitude = magnitude;
// Avoid applying absorb effects if the caster is the target
// We still need the spell to be added
if (caster == target
&& effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute
&& effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill)
{
effect.mMagnitude = 0;
}
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
if (hasDuration && effectIt->mDuration == 0)
{
@ -600,21 +609,19 @@ namespace MWMechanics
// magnitude, since we're transferring stats from the target to the caster
if (!caster.isEmpty() && caster != target && caster.getClass().isActor())
{
for (int i=0; i<5; ++i)
if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute &&
effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill)
{
if (effectIt->mEffectID == ESM::MagicEffect::AbsorbAttribute+i)
{
std::vector<ActiveSpells::ActiveEffect> absorbEffects;
ActiveSpells::ActiveEffect effect_ = effect;
effect_.mMagnitude *= -1;
absorbEffects.push_back(effect_);
if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game"))
target.getClass().getCreatureStats(target).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, caster.getClass().getCreatureStats(caster).getActorId());
else
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId());
}
std::vector<ActiveSpells::ActiveEffect> absorbEffects;
ActiveSpells::ActiveEffect effect_ = effect;
effect_.mMagnitude *= -1;
absorbEffects.push_back(effect_);
if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game"))
target.getClass().getCreatureStats(target).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, caster.getClass().getCreatureStats(caster).getActorId());
else
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId());
}
}
}

View file

@ -27,6 +27,7 @@
#include "../mwworld/ptr.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/weapontype.hpp"
@ -87,6 +88,31 @@ PartHolderPtr ActorAnimation::attachMesh(const std::string& model, const std::st
std::string ActorAnimation::getShieldMesh(MWWorld::ConstPtr shield) const
{
std::string mesh = shield.getClass().getModel(shield);
const ESM::Armor *armor = shield.get<ESM::Armor>()->mBase;
const std::vector<ESM::PartReference>& bodyparts = armor->mParts.mParts;
if (!bodyparts.empty())
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
const MWWorld::Store<ESM::BodyPart> &partStore = store.get<ESM::BodyPart>();
// Try to get shield model from bodyparts first, with ground model as fallback
for (const auto& part : bodyparts)
{
// Assume all creatures use the male mesh.
if (part.mPart != ESM::PRT_Shield || part.mMale.empty())
continue;
const ESM::BodyPart *bodypart = partStore.search(part.mMale);
if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty())
{
mesh = "meshes\\" + bodypart->mModel;
break;
}
}
}
if (mesh.empty())
return mesh;
std::string holsteredName = mesh;
holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif");
if(mResourceSystem->getVFS()->exists(holsteredName))

View file

@ -13,11 +13,13 @@
#include <components/misc/stringops.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwmechanics/weapontype.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
namespace MWRender
{
@ -114,6 +116,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot)
MWWorld::ConstPtr item = *it;
std::string bonename;
std::string itemModel = item.getClass().getModel(item);
if (slot == MWWorld::InventoryStore::Slot_CarriedRight)
{
if(item.getTypeName() == typeid(ESM::Weapon).name())
@ -132,11 +135,30 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot)
bonename = "Weapon Bone";
}
else
{
bonename = "Shield Bone";
if (item.getTypeName() == typeid(ESM::Armor).name())
{
// Shield body part model should be used if possible.
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
for (const auto& part : item.get<ESM::Armor>()->mBase->mParts.mParts)
{
// Assume all creatures use the male mesh.
if (part.mPart != ESM::PRT_Shield || part.mMale.empty())
continue;
const ESM::BodyPart *bodypart = store.get<ESM::BodyPart>().search(part.mMale);
if (bodypart && bodypart->mData.mType == ESM::BodyPart::MT_Armor && !bodypart->mModel.empty())
{
itemModel = "meshes\\" + bodypart->mModel;
break;
}
}
}
}
try
{
osg::ref_ptr<osg::Node> node = mResourceSystem->getSceneManager()->getInstance(item.getClass().getModel(item));
osg::ref_ptr<osg::Node> node = mResourceSystem->getSceneManager()->getInstance(itemModel);
const NodeMap& nodeMap = getNodeMap();
NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename));

View file

@ -518,14 +518,14 @@ std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const
{
std::string mesh = shield.getClass().getModel(shield);
const ESM::Armor *armor = shield.get<ESM::Armor>()->mBase;
std::vector<ESM::PartReference> bodyparts = armor->mParts.mParts;
const std::vector<ESM::PartReference>& bodyparts = armor->mParts.mParts;
if (!bodyparts.empty())
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
const MWWorld::Store<ESM::BodyPart> &partStore = store.get<ESM::BodyPart>();
// For NPCs try to get shield model from bodyparts first, with ground model as fallback
for (auto & part : bodyparts)
// Try to get shield model from bodyparts first, with ground model as fallback
for (const auto& part : bodyparts)
{
if (part.mPart != ESM::PRT_Shield)
continue;
@ -538,16 +538,21 @@ std::string NpcAnimation::getShieldMesh(MWWorld::ConstPtr shield) const
if (!bodypartName.empty())
{
const ESM::BodyPart *bodypart = 0;
bodypart = partStore.search(bodypartName);
const ESM::BodyPart *bodypart = partStore.search(bodypartName);
if (bodypart == nullptr || bodypart->mData.mType != ESM::BodyPart::MT_Armor)
return "";
return std::string();
else if (!bodypart->mModel.empty())
{
mesh = "meshes\\" + bodypart->mModel;
break;
}
}
}
}
if (mesh.empty())
return std::string();
std::string holsteredName = mesh;
holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif");
if(mResourceSystem->getVFS()->exists(holsteredName))

View file

@ -15,7 +15,6 @@
#include <osg/TextureCubeMap>
#include <osgUtil/LineSegmentIntersector>
#include <osgUtil/IncrementalCompileOperation>
#include <osg/ImageUtils>
@ -23,6 +22,8 @@
#include <components/debug/debuglog.hpp>
#include <components/misc/stringops.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/resource/scenemanager.hpp>
@ -48,8 +49,6 @@
#include <components/detournavigator/navigator.hpp>
#include <boost/algorithm/string.hpp>
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
#include "../mwgui/loadingscreen.hpp"
@ -391,6 +390,11 @@ namespace MWRender
mWorkQueue = nullptr;
}
osgUtil::IncrementalCompileOperation* RenderingManager::getIncrementalCompileOperation()
{
return mViewer->getIncrementalCompileOperation();
}
MWRender::Objects& RenderingManager::getObjects()
{
return *mObjects.get();
@ -763,7 +767,7 @@ namespace MWRender
int screenshotMapping = 0;
std::vector<std::string> settingArgs;
boost::algorithm::split(settingArgs,settingStr,boost::is_any_of(" "));
Misc::StringUtils::split(settingStr, settingArgs);
if (settingArgs.size() > 0)
{

View file

@ -7,6 +7,8 @@
#include <components/settings/settings.hpp>
#include <osgUtil/IncrementalCompileOperation>
#include "objects.hpp"
#include "renderinginterface.hpp"
@ -89,6 +91,8 @@ namespace MWRender
const std::string& resourcePath, DetourNavigator::Navigator& navigator);
~RenderingManager();
osgUtil::IncrementalCompileOperation* getIncrementalCompileOperation();
MWRender::Objects& getObjects();
Resource::ResourceSystem* getResourceSystem();

View file

@ -25,6 +25,7 @@
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/soundmanager.hpp"
#include "interpretercontext.hpp"
#include "ref.hpp"
@ -435,6 +436,14 @@ namespace MWScript
if (!targetPtr.isEmpty() && targetPtr.getCellRef().getRefId() == testedTargetId)
targetsAreEqual = true;
}
else
{
bool turningToPlayer = creatureStats.isTurningToPlayer();
bool greeting = creatureStats.getGreetingState() == MWMechanics::Greet_InProgress;
bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor);
if (turningToPlayer || (greeting && sayActive))
targetsAreEqual = (testedTargetId == "player"); // Currently the player ID is hardcoded
}
runtime.push(int(targetsAreEqual));
}
};

View file

@ -10,11 +10,13 @@
#include <components/interpreter/runtime.hpp>
#include <components/interpreter/opcodes.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/player.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwworld/actionteleport.hpp"
#include "../mwworld/cellstore.hpp"
#include "../mwbase/environment.hpp"
#include "../mwworld/player.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwmechanics/actorutil.hpp"
@ -34,6 +36,52 @@ namespace MWScript
}
};
class OpTestCells : public Interpreter::Opcode0
{
public:
virtual void execute (Interpreter::Runtime& runtime)
{
if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame)
{
runtime.getContext().report("Use TestCells from the main menu, when there is no active game session.");
return;
}
bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode();
if (wasConsole)
MWBase::Environment::get().getWindowManager()->toggleConsole();
MWBase::Environment::get().getWorld()->testExteriorCells();
if (wasConsole)
MWBase::Environment::get().getWindowManager()->toggleConsole();
}
};
class OpTestInteriorCells : public Interpreter::Opcode0
{
public:
virtual void execute (Interpreter::Runtime& runtime)
{
if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame)
{
runtime.getContext().report("Use TestInteriorCells from the main menu, when there is no active game session.");
return;
}
bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode();
if (wasConsole)
MWBase::Environment::get().getWindowManager()->toggleConsole();
MWBase::Environment::get().getWorld()->testInteriorCells();
if (wasConsole)
MWBase::Environment::get().getWindowManager()->toggleConsole();
}
};
class OpCOC : public Interpreter::Opcode0
{
public:
@ -204,6 +252,8 @@ namespace MWScript
void installOpcodes (Interpreter::Interpreter& interpreter)
{
interpreter.installSegment5 (Compiler::Cell::opcodeCellChanged, new OpCellChanged);
interpreter.installSegment5 (Compiler::Cell::opcodeTestCells, new OpTestCells);
interpreter.installSegment5 (Compiler::Cell::opcodeTestInteriorCells, new OpTestInteriorCells);
interpreter.installSegment5 (Compiler::Cell::opcodeCOC, new OpCOC);
interpreter.installSegment5 (Compiler::Cell::opcodeCOE, new OpCOE);
interpreter.installSegment5 (Compiler::Cell::opcodeGetInterior, new OpGetInterior);

View file

@ -461,5 +461,7 @@ op 0x200030a: SetNavMeshNumber
op 0x200030b: Journal, explicit
op 0x200030c: RepairedOnMe
op 0x200030d: RepairedOnMe, explicit
op 0x200030e: TestCells
op 0x200030f: TestInteriorCells
opcodes 0x200030c-0x3ffffff unused
opcodes 0x2000310-0x3ffffff unused

View file

@ -1,10 +1,13 @@
#include "miscextensions.hpp"
#include <cstdlib>
#include <iomanip>
#include <components/compiler/opcodes.hpp>
#include <components/compiler/locals.hpp>
#include <components/debug/debuglog.hpp>
#include <components/interpreter/interpreter.hpp>
#include <components/interpreter/runtime.hpp>
#include <components/interpreter/opcodes.hpp>
@ -1106,18 +1109,9 @@ namespace MWScript
return;
}
if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power)
{
runtime.getContext().report("spellcasting failed: you can only cast spells and powers.");
return;
}
if (ptr == MWMechanics::getPlayer())
{
MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr);
store.setSelectedEnchantItem(store.end());
MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, ptr)));
MWBase::Environment::get().getWindowManager()->updateSpellWindow();
MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId);
return;
}
@ -1125,7 +1119,6 @@ namespace MWScript
{
MWMechanics::AiCast castPackage(targetId, spellId, true);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr);
return;
}
@ -1149,9 +1142,29 @@ namespace MWScript
{
MWWorld::Ptr ptr = R()(runtime);
std::string spell = runtime.getStringLiteral (runtime[0].mInteger);
std::string spellId = runtime.getStringLiteral (runtime[0].mInteger);
runtime.pop();
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search(spellId);
if (!spell)
{
runtime.getContext().report("spellcasting failed: cannot find spell \""+spellId+"\"");
return;
}
if (ptr == MWMechanics::getPlayer())
{
MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId);
return;
}
if (ptr.getClass().isActor())
{
MWMechanics::AiCast castPackage(ptr.getCellRef().getRefId(), spellId, true);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr);
return;
}
MWMechanics::CastSpell cast(ptr, ptr, false, true);
cast.mHitPosition = ptr.getRefData().getPosition().asVec3();
cast.mAlwaysSucceed = true;
@ -1222,6 +1235,11 @@ namespace MWScript
std::stringstream msg;
msg << "Report time: ";
std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
msg << std::put_time(std::gmtime(&currentTime), "%Y.%m.%d %T UTC") << std::endl;
msg << "Content file: ";
if (!ptr.getCellRef().hasContentFile())
@ -1263,6 +1281,8 @@ namespace MWScript
--arg0;
}
Log(Debug::Warning) << "\n" << msg.str();
runtime.getContext().report(msg.str());
}
};

View file

@ -504,14 +504,10 @@ void MWWorld::InventoryStore::autoEquipShield(const MWWorld::Ptr& actor, TSlots&
continue;
if (iter->getClass().canBeEquipped(*iter, actor).first != 1)
continue;
if (iter->getClass().getItemHealth(*iter) <= 0)
continue;
std::pair<std::vector<int>, bool> shieldSlots =
iter->getClass().getEquipmentSlots(*iter);
if (shieldSlots.first.empty())
continue;
int slot = shieldSlots.first[0];
const ContainerStoreIterator& shield = mSlots[slot];
const ContainerStoreIterator& shield = slots_[slot];
if (shield != end()
&& shield.getType() == Type_Armor && shield->get<ESM::Armor>()->mBase->mData.mType == ESM::Armor::Shield)
{

View file

@ -11,6 +11,7 @@
#include <components/esm/loadbsgn.hpp>
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
@ -19,6 +20,7 @@
#include "../mwmechanics/movement.hpp"
#include "../mwmechanics/npcstats.hpp"
#include "../mwmechanics/spellcasting.hpp"
#include "class.hpp"
#include "ptr.hpp"
@ -492,4 +494,14 @@ namespace MWWorld
{
mPreviousItems.erase(boundItemId);
}
void Player::setSelectedSpell(const std::string& spellId)
{
Ptr player = getPlayer();
InventoryStore& store = player.getClass().getInventoryStore(player);
store.setSelectedEnchantItem(store.end());
int castChance = int(MWMechanics::getSpellSuccessChance(spellId, player));
MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, castChance);
MWBase::Environment::get().getWindowManager()->updateSpellWindow();
}
}

View file

@ -135,6 +135,8 @@ namespace MWWorld
void setPreviousItem(const std::string& boundItemId, const std::string& previousItemId);
std::string getPreviousItem(const std::string& boundItemId);
void erasePreviousItem(const std::string& boundItemId);
void setSelectedSpell(const std::string& spellId);
};
}
#endif

View file

@ -12,6 +12,7 @@
#include <components/resource/resourcesystem.hpp>
#include <components/resource/scenemanager.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/sceneutil/unrefqueue.hpp>
#include <components/detournavigator/navigator.hpp>
#include <components/detournavigator/debug.hpp>
#include <components/misc/convert.hpp>
@ -22,6 +23,8 @@
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwrender/renderingmanager.hpp"
#include "../mwrender/landmanager.hpp"
@ -205,10 +208,11 @@ namespace
{
MWWorld::CellStore& mCell;
Loading::Listener& mLoadingListener;
bool mTest;
std::vector<MWWorld::Ptr> mToInsert;
InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener);
InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool test);
bool operator() (const MWWorld::Ptr& ptr);
@ -216,8 +220,8 @@ namespace
void insert(AddObject&& addObject);
};
InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener)
: mCell (cell), mLoadingListener (loadingListener)
InsertVisitor::InsertVisitor (MWWorld::CellStore& cell, Loading::Listener& loadingListener, bool test)
: mCell (cell), mLoadingListener (loadingListener), mTest(test)
{}
bool InsertVisitor::operator() (const MWWorld::Ptr& ptr)
@ -246,7 +250,8 @@ namespace
}
}
mLoadingListener.increaseProgress (1);
if (!mTest)
mLoadingListener.increaseProgress (1);
}
}
@ -317,9 +322,10 @@ namespace MWWorld
mPreloader->updateCache(mRendering.getReferenceTime());
}
void Scene::unloadCell (CellStoreCollection::iterator iter)
void Scene::unloadCell (CellStoreCollection::iterator iter, bool test)
{
Log(Debug::Info) << "Unloading cell " << (*iter)->getCell()->getDescription();
if (!test)
Log(Debug::Info) << "Unloading cell " << (*iter)->getCell()->getDescription();
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
ListAndResetObjectsVisitor visitor;
@ -373,13 +379,16 @@ namespace MWWorld
mActiveCells.erase(*iter);
}
void Scene::loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn)
void Scene::loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test)
{
std::pair<CellStoreCollection::iterator, bool> result = mActiveCells.insert(cell);
if(result.second)
{
Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription();
if (test)
Log(Debug::Info) << "Testing cell " << cell->getCell()->getDescription();
else
Log(Debug::Info) << "Loading cell " << cell->getCell()->getDescription();
float verts = ESM::Land::LAND_SIZE;
float worldsize = ESM::Land::REAL_SIZE;
@ -390,7 +399,7 @@ namespace MWWorld
const int cellY = cell->getCell()->getGridY();
// Load terrain physics first...
if (cell->getCell()->isExterior())
if (!test && cell->getCell()->isExterior())
{
osg::ref_ptr<const ESMTerrain::LandObject> land = mRendering.getLandManager()->getLand(cellX, cellY);
const ESM::Land::LandData* data = land ? land->getData(ESM::Land::DATA_VHGT) : 0;
@ -418,38 +427,44 @@ namespace MWWorld
cell->respawn();
// ... then references. This is important for adjustPosition to work correctly.
insertCell (*cell, loadingListener);
insertCell (*cell, loadingListener, test);
mRendering.addCell(cell);
MWBase::Environment::get().getWindowManager()->addCell(cell);
bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior();
float waterLevel = cell->getWaterLevel();
mRendering.setWaterEnabled(waterEnabled);
if (waterEnabled)
if (!test)
{
mPhysics->enableWater(waterLevel);
mRendering.setWaterHeight(waterLevel);
if (cell->getCell()->isExterior())
MWBase::Environment::get().getWindowManager()->addCell(cell);
bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior();
float waterLevel = cell->getWaterLevel();
mRendering.setWaterEnabled(waterEnabled);
if (waterEnabled)
{
if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE,
cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform());
mPhysics->enableWater(waterLevel);
mRendering.setWaterHeight(waterLevel);
if (cell->getCell()->isExterior())
{
if (const auto heightField = mPhysics->getHeightField(cellX, cellY))
navigator->addWater(osg::Vec2i(cellX, cellY), ESM::Land::REAL_SIZE,
cell->getWaterLevel(), heightField->getCollisionObject()->getWorldTransform());
}
else
{
navigator->addWater(osg::Vec2i(cellX, cellY), std::numeric_limits<int>::max(),
cell->getWaterLevel(), btTransform::getIdentity());
}
}
else
mPhysics->disableWater();
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx))
{
navigator->addWater(osg::Vec2i(cellX, cellY), std::numeric_limits<int>::max(),
cell->getWaterLevel(), btTransform::getIdentity());
mRendering.configureAmbient(cell->getCell());
}
}
else
mPhysics->disableWater();
const auto player = MWBase::Environment::get().getWorld()->getPlayerPtr();
navigator->update(player.getRefData().getPosition().asVec3());
if (!cell->isExterior() && !(cell->getCell()->mData.mFlags & ESM::Cell::QuasiEx))
mRendering.configureAmbient(cell->getCell());
}
mPreloader->notifyLoaded(cell);
@ -594,6 +609,101 @@ namespace MWWorld
mCellChanged = true;
}
void Scene::testExteriorCells()
{
// Note: temporary disable ICO to decrease memory usage
mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(nullptr);
mRendering.getResourceSystem()->setExpiryDelay(1.f);
const MWWorld::Store<ESM::Cell> &cells = MWBase::Environment::get().getWorld()->getStore().get<ESM::Cell>();
Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
Loading::ScopedLoad load(loadingListener);
loadingListener->setProgressRange(cells.getExtSize());
MWWorld::Store<ESM::Cell>::iterator it = cells.extBegin();
int i = 1;
for (; it != cells.extEnd(); ++it)
{
loadingListener->setLabel("Testing exterior cells ("+std::to_string(i)+"/"+std::to_string(cells.getExtSize())+")...");
CellStoreCollection::iterator iter = mActiveCells.begin();
CellStore *cell = MWBase::Environment::get().getWorld()->getExterior(it->mData.mX, it->mData.mY);
loadCell (cell, loadingListener, false, true);
iter = mActiveCells.begin();
while (iter != mActiveCells.end())
{
if (it->isExterior() && it->mData.mX == (*iter)->getCell()->getGridX() &&
it->mData.mY == (*iter)->getCell()->getGridY())
{
unloadCell(iter, true);
break;
}
++iter;
}
mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime());
mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue());
loadingListener->increaseProgress (1);
i++;
}
mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(mRendering.getIncrementalCompileOperation());
mRendering.getResourceSystem()->setExpiryDelay(Settings::Manager::getFloat("cache expiry delay", "Cells"));
}
void Scene::testInteriorCells()
{
// Note: temporary disable ICO to decrease memory usage
mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(nullptr);
mRendering.getResourceSystem()->setExpiryDelay(1.f);
const MWWorld::Store<ESM::Cell> &cells = MWBase::Environment::get().getWorld()->getStore().get<ESM::Cell>();
Loading::Listener* loadingListener = MWBase::Environment::get().getWindowManager()->getLoadingScreen();
Loading::ScopedLoad load(loadingListener);
loadingListener->setProgressRange(cells.getIntSize());
int i = 1;
MWWorld::Store<ESM::Cell>::iterator it = cells.intBegin();
for (; it != cells.intEnd(); ++it)
{
loadingListener->setLabel("Testing interior cells ("+std::to_string(i)+"/"+std::to_string(cells.getIntSize())+")...");
CellStore *cell = MWBase::Environment::get().getWorld()->getInterior(it->mName);
loadCell (cell, loadingListener, false, true);
CellStoreCollection::iterator iter = mActiveCells.begin();
while (iter != mActiveCells.end())
{
assert (!(*iter)->getCell()->isExterior());
if (it->mName == (*iter)->getCell()->mName)
{
unloadCell(iter, true);
break;
}
++iter;
}
mRendering.getResourceSystem()->updateCache(mRendering.getReferenceTime());
mRendering.getUnrefQueue()->flush(mRendering.getWorkQueue());
loadingListener->increaseProgress (1);
i++;
}
mRendering.getResourceSystem()->getSceneManager()->setIncrementalCompileOperation(mRendering.getIncrementalCompileOperation());
mRendering.getResourceSystem()->setExpiryDelay(Settings::Manager::getFloat("cache expiry delay", "Cells"));
}
void Scene::changePlayerCell(CellStore *cell, const ESM::Position &pos, bool adjustPlayerPos)
{
mCurrentCell = cell;
@ -759,9 +869,9 @@ namespace MWWorld
mCellChanged = false;
}
void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener)
void Scene::insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test)
{
InsertVisitor insertVisitor (cell, *loadingListener);
InsertVisitor insertVisitor (cell, *loadingListener, test);
cell.forEach (insertVisitor);
insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mRendering); });
insertVisitor.insert([&] (const MWWorld::Ptr& ptr) { addObject(ptr, *mPhysics, mNavigator); });

View file

@ -85,7 +85,7 @@ namespace MWWorld
osg::Vec3f mLastPlayerPos;
void insertCell (CellStore &cell, Loading::Listener* loadingListener);
void insertCell (CellStore &cell, Loading::Listener* loadingListener, bool test = false);
// Load and unload cells as necessary to create a cell grid with "X" and "Y" in the center
void changeCellGrid (int playerCellX, int playerCellY, bool changeEvent = true);
@ -107,9 +107,9 @@ namespace MWWorld
void preloadCell(MWWorld::CellStore* cell, bool preloadSurrounding=false);
void preloadTerrain(const osg::Vec3f& pos);
void unloadCell (CellStoreCollection::iterator iter);
void unloadCell (CellStoreCollection::iterator iter, bool test = false);
void loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn);
void loadCell (CellStore *cell, Loading::Listener* loadingListener, bool respawn, bool test = false);
void playerMoved (const osg::Vec3f& pos);
@ -151,6 +151,9 @@ namespace MWWorld
Ptr searchPtrViaActorId (int actorId);
void preload(const std::string& mesh, bool useAnim=false);
void testExteriorCells();
void testInteriorCells();
};
}

View file

@ -792,6 +792,14 @@ namespace MWWorld
{
return mSharedInt.size() + mSharedExt.size();
}
size_t Store<ESM::Cell>::getExtSize() const
{
return mSharedExt.size();
}
size_t Store<ESM::Cell>::getIntSize() const
{
return mSharedInt.size();
}
void Store<ESM::Cell>::listIdentifier(std::vector<std::string> &list) const
{
list.reserve(list.size() + mSharedInt.size());

View file

@ -106,6 +106,11 @@ namespace MWWorld
return iter;
}
SharedIterator &operator+=(int advance) {
mIter += advance;
return *this;
}
SharedIterator &operator--() {
--mIter;
return *this;
@ -314,6 +319,8 @@ namespace MWWorld
const ESM::Cell *searchExtByRegion(const std::string &id) const;
size_t getSize() const;
size_t getExtSize() const;
size_t getIntSize() const;
void listIdentifier(std::vector<std::string> &list) const;

View file

@ -586,6 +586,16 @@ namespace MWWorld
return getInterior (id.mWorldspace);
}
void World::testExteriorCells()
{
mWorldScene->testExteriorCells();
}
void World::testInteriorCells()
{
mWorldScene->testInteriorCells();
}
void World::useDeathCamera()
{
if(mRendering->getCamera()->isVanityOrPreviewModeEnabled() )

View file

@ -223,6 +223,9 @@ namespace MWWorld
CellStore *getCell (const ESM::CellId& id) override;
void testExteriorCells() override;
void testInteriorCells() override;
//switch to POV before showing player's death animation
void useDeathCamera() override;

View file

@ -2,11 +2,14 @@
#include <components/detournavigator/navigatorimpl.hpp>
#include <components/detournavigator/exceptions.hpp>
#include <components/misc/rng.hpp>
#include <BulletCollision/CollisionShapes/btHeightfieldTerrainShape.h>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionShapes/btCompoundShape.h>
#include <boost/optional/optional_io.hpp>
#include <gtest/gtest.h>
#include <iterator>
@ -655,4 +658,32 @@ namespace
osg::Vec3f(215, -215, 1.87718021869659423828125),
})) << mPath;
}
TEST_F(DetourNavigatorNavigatorTest, update_then_find_random_point_around_circle_should_return_position)
{
const std::array<btScalar, 5 * 5> heightfieldData {{
0, 0, 0, 0, 0,
0, -25, -25, -25, -25,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
0, -25, -100, -100, -100,
}};
btHeightfieldTerrainShape shape(5, 5, heightfieldData.data(), 1, 0, 0, 2, PHY_FLOAT, false);
shape.setLocalScaling(btVector3(128, 128, 1));
mNavigator->addAgent(mAgentHalfExtents);
mNavigator->addObject(ObjectId(&shape), shape, btTransform::getIdentity());
mNavigator->update(mPlayerPosition);
mNavigator->wait();
Misc::Rng::init(42);
const auto result = mNavigator->findRandomPointAroundCircle(mAgentHalfExtents, mStart, 100.0, Flag_walk);
ASSERT_EQ(result, boost::optional<osg::Vec3f>(osg::Vec3f(-209.95985412597656, 129.89768981933594, -0.26253718137741089)));
const auto distance = (*result - mStart).length();
EXPECT_EQ(distance, 85.260780334472656) << distance;
}
}

View file

@ -266,9 +266,13 @@ namespace
MOCK_CONST_METHOD0(numRecords, std::size_t ());
MOCK_CONST_METHOD1(getRoot, Nif::Record* (std::size_t));
MOCK_CONST_METHOD0(numRoots, std::size_t ());
MOCK_CONST_METHOD1(getString, std::string (std::size_t));
MOCK_METHOD1(setUseSkinning, void (bool));
MOCK_CONST_METHOD0(getUseSkinning, bool ());
MOCK_CONST_METHOD0(getFilename, std::string ());
MOCK_CONST_METHOD0(getVersion, unsigned int ());
MOCK_CONST_METHOD0(getUserVersion, unsigned int ());
MOCK_CONST_METHOD0(getBethVersion, unsigned int ());
};
struct RecordMock : Nif::Record

View file

@ -172,6 +172,8 @@ add_component_dir(detournavigator
recastmeshobject
navmeshtilescache
settings
navigator
findrandompointaroundcircle
)
set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui
@ -217,7 +219,6 @@ add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR})
target_link_libraries(components
${Boost_SYSTEM_LIBRARY}
${Boost_FILESYSTEM_LIBRARY}
${Boost_THREAD_LIBRARY}
${Boost_PROGRAM_OPTIONS_LIBRARY}
${Boost_IOSTREAMS_LIBRARY}
${OSG_LIBRARIES}

View file

@ -89,6 +89,8 @@ namespace Compiler
void registerExtensions (Extensions& extensions)
{
extensions.registerFunction ("cellchanged", 'l', "", opcodeCellChanged);
extensions.registerInstruction("testcells", "", opcodeTestCells);
extensions.registerInstruction("testinteriorcells", "", opcodeTestInteriorCells);
extensions.registerInstruction ("coc", "S", opcodeCOC);
extensions.registerInstruction ("centeroncell", "S", opcodeCOC);
extensions.registerInstruction ("coe", "ll", opcodeCOE);

View file

@ -75,6 +75,8 @@ namespace Compiler
namespace Cell
{
const int opcodeCellChanged = 0x2000000;
const int opcodeTestCells = 0x200030e;
const int opcodeTestInteriorCells = 0x200030f;
const int opcodeCOC = 0x2000026;
const int opcodeCOE = 0x2000226;
const int opcodeGetInterior = 0x2000131;

View file

@ -4,14 +4,15 @@
#include <DetourNavMesh.h>
#include <fstream>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>
namespace DetourNavigator
{
void writeToFile(const RecastMesh& recastMesh, const std::string& pathPrefix, const std::string& revision)
{
const auto path = pathPrefix + "recastmesh" + revision + ".obj";
std::ofstream file(path);
boost::filesystem::ofstream file(boost::filesystem::path(path), std::ios::out);
if (!file.is_open())
throw NavigatorException("Open file failed: " + path);
file.exceptions(std::ios::failbit | std::ios::badbit);
@ -64,7 +65,7 @@ namespace DetourNavigator
};
const auto path = pathPrefix + "all_tiles_navmesh" + revision + ".bin";
std::ofstream file(path, std::ios::binary);
boost::filesystem::ofstream file(boost::filesystem::path(path), std::ios::out | std::ios::binary);
if (!file.is_open())
throw NavigatorException("Open file failed: " + path);
file.exceptions(std::ios::failbit | std::ios::badbit);

View file

@ -0,0 +1,45 @@
#include "findrandompointaroundcircle.hpp"
#include "settings.hpp"
#include "findsmoothpath.hpp"
#include <components/misc/rng.hpp>
#include <DetourCommon.h>
#include <DetourNavMesh.h>
#include <DetourNavMeshQuery.h>
namespace DetourNavigator
{
boost::optional<osg::Vec3f> findRandomPointAroundCircle(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents,
const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const Settings& settings)
{
dtNavMeshQuery navMeshQuery;
initNavMeshQuery(navMeshQuery, navMesh, settings.mMaxNavMeshQueryNodes);
dtQueryFilter queryFilter;
queryFilter.setIncludeFlags(includeFlags);
dtPolyRef startRef = 0;
osg::Vec3f startPolygonPosition;
for (int i = 0; i < 3; ++i)
{
const auto status = navMeshQuery.findNearestPoly(start.ptr(), (halfExtents * (1 << i)).ptr(), &queryFilter,
&startRef, startPolygonPosition.ptr());
if (!dtStatusFailed(status) && startRef != 0)
break;
}
if (startRef == 0)
return boost::optional<osg::Vec3f>();
dtPolyRef resultRef = 0;
osg::Vec3f resultPosition;
navMeshQuery.findRandomPointAroundCircle(startRef, start.ptr(), maxRadius, &queryFilter,
&Misc::Rng::rollProbability, &resultRef, resultPosition.ptr());
if (resultRef == 0)
return boost::optional<osg::Vec3f>();
return boost::optional<osg::Vec3f>(resultPosition);
}
}

View file

@ -0,0 +1,20 @@
#ifndef OPENMW_COMPONENTS_DETOURNAVIGATOR_FINDRANDOMPOINTAROUNDCIRCLE_H
#define OPENMW_COMPONENTS_DETOURNAVIGATOR_FINDRANDOMPOINTAROUNDCIRCLE_H
#include "flags.hpp"
#include <boost/optional.hpp>
#include <osg/Vec3f>
class dtNavMesh;
namespace DetourNavigator
{
struct Settings;
boost::optional<osg::Vec3f> findRandomPointAroundCircle(const dtNavMesh& navMesh, const osg::Vec3f& halfExtents,
const osg::Vec3f& start, const float maxRadius, const Flags includeFlags, const Settings& settings);
}
#endif

View file

@ -0,0 +1,20 @@
#include "findrandompointaroundcircle.hpp"
#include "navigator.hpp"
namespace DetourNavigator
{
boost::optional<osg::Vec3f> Navigator::findRandomPointAroundCircle(const osg::Vec3f& agentHalfExtents,
const osg::Vec3f& start, const float maxRadius, const Flags includeFlags) const
{
const auto navMesh = getNavMesh(agentHalfExtents);
if (!navMesh)
return boost::optional<osg::Vec3f>();
const auto settings = getSettings();
const auto result = DetourNavigator::findRandomPointAroundCircle(navMesh->lockConst()->getImpl(),
toNavMeshCoordinates(settings, agentHalfExtents), toNavMeshCoordinates(settings, start),
toNavMeshCoordinates(settings, maxRadius), includeFlags, settings);
if (!result)
return boost::optional<osg::Vec3f>();
return boost::optional<osg::Vec3f>(fromNavMeshCoordinates(settings, *result));
}
}

View file

@ -157,7 +157,6 @@ namespace DetourNavigator
* @param out the beginning of the destination range.
* @return Output iterator to the element in the destination range, one past the last element of found path.
* Equal to out if no path is found.
* @throws InvalidArgument if there is no navmesh for given agentHalfExtents.
*/
template <class OutputIterator>
OutputIterator findPath(const osg::Vec3f& agentHalfExtents, const float stepSize, const osg::Vec3f& start,
@ -194,6 +193,17 @@ namespace DetourNavigator
virtual const Settings& getSettings() const = 0;
virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0;
/**
* @brief findRandomPointAroundCircle returns random location on navmesh within the reach of specified location.
* @param agentHalfExtents allows to find navmesh for given actor.
* @param start path from given point.
* @param maxRadius limit maximum distance from start.
* @param includeFlags setup allowed surfaces for actor to walk.
* @return not empty optional with position if point is found and empty optional if point is not found.
*/
boost::optional<osg::Vec3f> findRandomPointAroundCircle(const osg::Vec3f& agentHalfExtents,
const osg::Vec3f& start, const float maxRadius, const Flags includeFlags) const;
};
}

View file

@ -20,7 +20,7 @@ namespace ESM
int left = esm.getSubSize();
if (left < s)
esm.fail("SCVR string list is smaller than specified");
esm.getExact(&tmp[0], s);
esm.getExact(tmp.data(), s);
if (left > s)
esm.skip(left-s); // skip the leftover junk
@ -29,37 +29,47 @@ namespace ESM
// The tmp buffer is a null-byte separated string list, we
// just have to pick out one string at a time.
char* str = &tmp[0];
char* str = tmp.data();
if (!str && mVarNames.size() > 0)
{
Log(Debug::Warning) << "SCVR with no variable names";
return;
}
// Support '\r' terminated strings like vanilla. See Bug #1324.
std::replace(tmp.begin(), tmp.end(), '\r', '\0');
// Avoid heap corruption
if (!tmp.empty() && tmp[tmp.size()-1] != '\0')
{
tmp.emplace_back('\0');
std::stringstream ss;
ss << "Malformed string table";
ss << "\n File: " << esm.getName();
ss << "\n Record: " << esm.getContext().recName.toString();
ss << "\n Subrecord: " << "SCVR";
ss << "\n Offset: 0x" << std::hex << esm.getFileOffset();
Log(Debug::Verbose) << ss.str();
}
for (size_t i = 0; i < mVarNames.size(); i++)
{
// Support '\r' terminated strings like vanilla. See Bug #1324.
char *termsym = strchr(str, '\r');
if(termsym) *termsym = '\0';
mVarNames[i] = std::string(str);
str += mVarNames[i].size() + 1;
if (str - &tmp[0] > s)
if (static_cast<size_t>(str - tmp.data()) > tmp.size())
{
// Apparently SCVR subrecord is not used and variable names are
// determined on the fly from the script text. Therefore don't throw
// an exeption, just log an error and continue.
// SCVR subrecord is unused and variable names are determined
// from the script source, so an overflow is not fatal.
std::stringstream ss;
ss << "String table overflow";
ss << "\n File: " << esm.getName();
ss << "\n Record: " << esm.getContext().recName.toString();
ss << "\n Subrecord: " << "SCVR";
ss << "\n Offset: 0x" << std::hex << esm.getFileOffset();
Log(Debug::Verbose) << ss.str();
// Get rid of empty strings in the list.
mVarNames.resize(i+1);
break;
}
}
}

View file

@ -7,10 +7,9 @@
#include <osg/Image>
#include <osg/Plane>
#include <boost/algorithm/string.hpp>
#include <components/debug/debuglog.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/misc/stringops.hpp>
#include <components/vfs/manager.hpp>
namespace ESMTerrain
@ -564,7 +563,7 @@ namespace ESMTerrain
if (mAutoUseNormalMaps)
{
std::string texture_ = texture;
boost::replace_last(texture_, ".", mNormalHeightMapPattern + ".");
Misc::StringUtils::replaceLast(texture_, ".", mNormalHeightMapPattern + ".");
if (mVFS->exists(texture_))
{
info.mNormalMap = texture_;
@ -573,7 +572,7 @@ namespace ESMTerrain
else
{
texture_ = texture;
boost::replace_last(texture_, ".", mNormalMapPattern + ".");
Misc::StringUtils::replaceLast(texture_, ".", mNormalMapPattern + ".");
if (mVFS->exists(texture_))
info.mNormalMap = texture_;
}
@ -582,7 +581,7 @@ namespace ESMTerrain
if (mAutoUseSpecularMaps)
{
std::string texture_ = texture;
boost::replace_last(texture_, ".", mSpecularMapPattern + ".");
Misc::StringUtils::replaceLast(texture_, ".", mSpecularMapPattern + ".");
if (mVFS->exists(texture_))
{
info.mDiffuseMap = texture_;

View file

@ -261,7 +261,7 @@ void LowLevelFile::open (char const * filename)
if (handle == INVALID_HANDLE_VALUE)
{
std::ostringstream os;
os << "Failed to open '" << filename << "' for reading.";
os << "Failed to open '" << filename << "' for reading: " << GetLastError();
throw std::runtime_error (os.str ());
}

View file

@ -1,6 +1,7 @@
#ifndef MISC_STRINGOPS_H
#define MISC_STRINGOPS_H
#include <cctype>
#include <string>
#include <algorithm>
@ -245,6 +246,58 @@ public:
{
return format(fmt.c_str(), args ...);
}
static inline void trim(std::string &s)
{
// left trim
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch)
{
return !std::isspace(ch);
}));
// right trim
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch)
{
return !std::isspace(ch);
}).base(), s.end());
}
template <class Container>
static inline void split(const std::string& str, Container& cont, const std::string& delims = " ")
{
std::size_t current, previous = 0;
current = str.find_first_of(delims);
while (current != std::string::npos)
{
cont.push_back(str.substr(previous, current - previous));
previous = current + 1;
current = str.find_first_of(delims, previous);
}
cont.push_back(str.substr(previous, current - previous));
}
// TODO: use the std::string_view once we will use the C++17.
// It should allow us to avoid data copying while we still will support both string and literal arguments.
static inline void replaceAll(std::string& data, std::string toSearch, std::string replaceStr)
{
size_t pos = data.find(toSearch);
while( pos != std::string::npos)
{
data.replace(pos, toSearch.size(), replaceStr);
pos = data.find(toSearch, pos + replaceStr.size());
}
}
static inline void replaceLast(std::string& str, std::string substr, std::string with)
{
size_t pos = str.rfind(substr);
if (pos == std::string::npos)
return;
str.replace(pos, substr.size(), with);
}
};
}

View file

@ -9,7 +9,7 @@ namespace Nif
{
Named::read(nif);
external = !!nif->getChar();
external = nif->getChar() != 0;
if(external)
filename = nif->getString();
else

View file

@ -35,16 +35,16 @@ void ShapeData::read(NIFStream *nif)
{
int verts = nif->getUShort();
if(nif->getInt())
if (nif->getBoolean())
nif->getVector3s(vertices, verts);
if(nif->getInt())
if (nif->getBoolean())
nif->getVector3s(normals, verts);
center = nif->getVector3();
radius = nif->getFloat();
if(nif->getInt())
if (nif->getBoolean())
nif->getVector4s(colors, verts);
// Only the first 6 bits are used as a count. I think the rest are
@ -120,7 +120,7 @@ void NiAutoNormalParticlesData::read(NIFStream *nif)
particleRadius = nif->getFloat();
activeCount = nif->getUShort();
if(nif->getInt())
if (nif->getBoolean())
{
// Particle sizes
nif->getFloats(sizes, vertices.size());
@ -131,7 +131,7 @@ void NiRotatingParticlesData::read(NIFStream *nif)
{
NiAutoNormalParticlesData::read(nif);
if(nif->getInt())
if (nif->getBoolean())
{
// Rotation quaternions.
nif->getQuaternions(rotations, vertices.size());
@ -176,7 +176,7 @@ void NiPixelData::read(NIFStream *nif)
numberOfMipmaps = nif->getUInt();
// Bytes per pixel, should be bpp * 8
// Bytes per pixel, should be bpp / 8
/* int bytes = */ nif->getUInt();
for(unsigned int i=0; i<numberOfMipmaps; i++)
@ -228,10 +228,8 @@ void NiSkinData::read(NIFStream *nif)
nif->getInt(); // -1
bones.resize(boneNum);
for(int i=0;i<boneNum;i++)
for (BoneInfo &bi : bones)
{
BoneInfo &bi = bones[i];
bi.trafo.rotation = nif->getMatrix3();
bi.trafo.pos = nif->getVector3();
bi.trafo.scale = nif->getFloat();
@ -267,7 +265,7 @@ void NiKeyframeData::read(NIFStream *nif)
{
mRotations = std::make_shared<QuaternionKeyMap>();
mRotations->read(nif);
if(mRotations->mInterpolationType == Vector3KeyMap::sXYZInterpolation)
if(mRotations->mInterpolationType == InterpolationType_XYZ)
{
//Chomp unused float
nif->getFloat();

View file

@ -9,9 +9,7 @@ namespace Nif
/// Open a NIF stream. The name is used for error messages.
NIFFile::NIFFile(Files::IStreamPtr stream, const std::string &name)
: ver(0)
, filename(name)
, mUseSkinning(false)
: filename(name)
{
parse(stream);
}
@ -139,27 +137,18 @@ void NIFFile::parse(Files::IStreamPtr stream)
// Check the header string
std::string head = nif.getVersionString();
if(head.compare(0, 22, "NetImmerse File Format") != 0)
fail("Invalid NIF header: " + head);
fail("Invalid NIF header: " + head);
// Get BCD version
ver = nif.getUInt();
// 4.0.0.0 is an older, practically identical version of the format.
// It's not used by Morrowind assets but Morrowind supports it.
if(ver != 0x04000000 && ver != VER_MW)
if(ver != VER_4_0_0_0 && ver != VER_MW)
fail("Unsupported NIF version: " + printVersion(ver));
// Number of records
size_t recNum = nif.getInt();
records.resize(recNum);
/* The format for 10.0.1.0 seems to be a bit different. After the
header, it contains the number of records, r (int), just like
4.0.0.2, but following that it contains a short x, followed by x
strings. Then again by r shorts, one for each record, giving
which of the above strings to use to identify the record. After
this follows two ints (zero?) and then the record data. However
we do not support or plan to support other versions yet.
*/
for(size_t i = 0;i < recNum;i++)
{
Record *r = nullptr;

View file

@ -26,21 +26,27 @@ struct File
virtual size_t numRoots() const = 0;
virtual std::string getString(size_t index) const = 0;
virtual void setUseSkinning(bool skinning) = 0;
virtual bool getUseSkinning() const = 0;
virtual std::string getFilename() const = 0;
virtual unsigned int getVersion() const = 0;
virtual unsigned int getUserVersion() const = 0;
virtual unsigned int getBethVersion() const = 0;
};
class NIFFile final : public File
{
enum NIFVersion {
VER_MW = 0x04000002 // Morrowind NIFs
};
/// Nif file version
unsigned int ver;
/// File version, user version, Bethesda version
unsigned int ver = 0;
unsigned int userVer = 0;
unsigned int bethVer = 0;
/// File name, used for error messages and opening the file
std::string filename;
@ -51,7 +57,10 @@ class NIFFile final : public File
/// Root list. This is a select portion of the pointers from records
std::vector<Record*> roots;
bool mUseSkinning;
/// String table
std::vector<std::string> strings;
bool mUseSkinning = false;
/// Parse the file
void parse(Files::IStreamPtr stream);
@ -66,6 +75,34 @@ class NIFFile final : public File
void operator = (NIFFile const &);
public:
enum NIFVersion
{
// Feature-relevant
VER_4_1_0_0 = 0x04010000, // 1-byte booleans (previously 4-byte)
VER_5_0_0_1 = 0x05000001, // Optimized record type listings
VER_5_0_0_6 = 0x05000006, // Record groups
VER_10_0_1_8 = 0x0A000108, // The last version without user version
VER_20_1_0_1 = 0x14010001, // String tables
VER_20_2_0_5 = 0x14020005, // Record sizes
// Game-relevant
VER_4_0_0_0 = 0x04000000, // Freedom Force NIFs, supported by Morrowind
VER_MW = 0x04000002, // 4.0.0.2. Morrowind and Freedom Force NIFs
VER_4_2_1_0 = 0x04020100, // Used in Civ4 and Dark Age of Camelot
VER_CI = 0x04020200, // 4.2.2.0. Main Culpa Innata NIF version, also used in Civ4
VER_ZT2 = 0x0A000100, // 10.0.1.0. Main Zoo Tycoon 2 NIF version, also used in Oblivion and Civ4
VER_OB_OLD = 0x0A000102, // 10.0.1.2. Main older Oblivion NIF version
VER_GAMEBRYO = 0x0A010000, // 10.1.0.0. Lots of games use it. The first version that has Gamebryo File Format header.
VER_10_2_0_0 = 0x0A020000, // Lots of games use this version as well.
VER_CIV4 = 0x14000004, // 20.0.0.4. Main Civilization IV NIF version.
VER_OB = 0x14000005, // 20.0.0.5. Main Oblivion NIF version
VER_BGS = 0x14020007 // 20.2.0.7. Main Fallout 3/4/76/New Vegas and Skyrim/SkyrimSE NIF version.
};
enum BethVersion
{
BETHVER_FO3 = 34, // Fallout 3
BETHVER_FO4 = 130 // Fallout 4
};
/// Used if file parsing fails
void fail(const std::string &msg) const
{
@ -101,6 +138,12 @@ public:
/// Number of roots
size_t numRoots() const override { return roots.size(); }
/// Get a given string from the file's string table
std::string getString(size_t index) const override
{
return strings.at(index);
}
/// Set whether there is skinning contained in this NIF file.
/// @note This is just a hint for users of the NIF file and has no effect on the loading procedure.
void setUseSkinning(bool skinning) override;
@ -109,8 +152,17 @@ public:
/// Get the name of the file
std::string getFilename() const override { return filename; }
/// Get the version of the NIF format used
unsigned int getVersion() const override { return ver; }
/// Get the user version of the NIF format used
unsigned int getUserVersion() const override { return userVer; }
/// Get the Bethesda version of the NIF format used
unsigned int getBethVersion() const override { return bethVer; }
};
typedef std::shared_ptr<const Nif::NIFFile> NIFFilePtr;
using NIFFilePtr = std::shared_ptr<const Nif::NIFFile>;

View file

@ -13,6 +13,16 @@
namespace Nif
{
enum InterpolationType
{
InterpolationType_Unknown = 0,
InterpolationType_Linear = 1,
InterpolationType_Quadratic = 2,
InterpolationType_TBC = 3,
InterpolationType_XYZ = 4,
InterpolationType_Constant = 5
};
template<typename T>
struct KeyT {
T mValue;
@ -26,34 +36,27 @@ struct KeyT {
float mContinuity; // Only for TBC interpolation
*/
};
typedef KeyT<float> FloatKey;
typedef KeyT<osg::Vec3f> Vector3Key;
typedef KeyT<osg::Vec4f> Vector4Key;
typedef KeyT<osg::Quat> QuaternionKey;
using FloatKey = KeyT<float>;
using Vector3Key = KeyT<osg::Vec3f>;
using Vector4Key = KeyT<osg::Vec4f>;
using QuaternionKey = KeyT<osg::Quat>;
template<typename T, T (NIFStream::*getValue)()>
struct KeyMapT {
typedef std::map< float, KeyT<T> > MapType;
using MapType = std::map<float, KeyT<T>>;
typedef T ValueType;
typedef KeyT<T> KeyType;
using ValueType = T;
using KeyType = KeyT<T>;
static const unsigned int sLinearInterpolation = 1;
static const unsigned int sQuadraticInterpolation = 2;
static const unsigned int sTBCInterpolation = 3;
static const unsigned int sXYZInterpolation = 4;
unsigned int mInterpolationType;
unsigned int mInterpolationType = InterpolationType_Linear;
MapType mKeys;
KeyMapT() : mInterpolationType(sLinearInterpolation) {}
//Read in a KeyGroup (see http://niftools.sourceforge.net/doc/nif/NiKeyframeData.html)
void read(NIFStream *nif, bool force=false)
{
assert(nif);
mInterpolationType = 0;
mInterpolationType = InterpolationType_Unknown;
size_t count = nif->getUInt();
if(count == 0 && !force)
@ -66,7 +69,8 @@ struct KeyMapT {
KeyT<T> key;
NIFStream &nifReference = *nif;
if(mInterpolationType == sLinearInterpolation)
if (mInterpolationType == InterpolationType_Linear
|| mInterpolationType == InterpolationType_Constant)
{
for(size_t i = 0;i < count;i++)
{
@ -75,7 +79,7 @@ struct KeyMapT {
mKeys[time] = key;
}
}
else if(mInterpolationType == sQuadraticInterpolation)
else if (mInterpolationType == InterpolationType_Quadratic)
{
for(size_t i = 0;i < count;i++)
{
@ -84,7 +88,7 @@ struct KeyMapT {
mKeys[time] = key;
}
}
else if(mInterpolationType == sTBCInterpolation)
else if (mInterpolationType == InterpolationType_TBC)
{
for(size_t i = 0;i < count;i++)
{
@ -94,11 +98,11 @@ struct KeyMapT {
}
}
//XYZ keys aren't actually read here.
//data.hpp sees that the last type read was sXYZInterpolation and:
//data.hpp sees that the last type read was InterpolationType_XYZ and:
// Eats a floating point number, then
// Re-runs the read function 3 more times.
// When it does that it's reading in a bunch of sLinearInterpolation keys, not sXYZInterpolation.
else if(mInterpolationType == sXYZInterpolation)
// When it does that it's reading in a bunch of InterpolationType_Linear keys, not InterpolationType_XYZ.
else if(mInterpolationType == InterpolationType_XYZ)
{
//Don't try to read XYZ keys into the wrong part
if ( count != 1 )
@ -109,7 +113,7 @@ struct KeyMapT {
nif->file->fail(error.str());
}
}
else if (0 == mInterpolationType)
else if (mInterpolationType == InterpolationType_Unknown)
{
if (count != 0)
nif->file->fail("Interpolation type 0 doesn't work with keys");
@ -149,15 +153,17 @@ private:
/*key.mContinuity = */nif.getFloat();
}
};
typedef KeyMapT<float,&NIFStream::getFloat> FloatKeyMap;
typedef KeyMapT<osg::Vec3f,&NIFStream::getVector3> Vector3KeyMap;
typedef KeyMapT<osg::Vec4f,&NIFStream::getVector4> Vector4KeyMap;
typedef KeyMapT<osg::Quat,&NIFStream::getQuaternion> QuaternionKeyMap;
using FloatKeyMap = KeyMapT<float,&NIFStream::getFloat>;
using Vector3KeyMap = KeyMapT<osg::Vec3f,&NIFStream::getVector3>;
using Vector4KeyMap = KeyMapT<osg::Vec4f,&NIFStream::getVector4>;
using QuaternionKeyMap = KeyMapT<osg::Quat,&NIFStream::getQuaternion>;
using ByteKeyMap = KeyMapT<char,&NIFStream::getChar>;
typedef std::shared_ptr<FloatKeyMap> FloatKeyMapPtr;
typedef std::shared_ptr<Vector3KeyMap> Vector3KeyMapPtr;
typedef std::shared_ptr<Vector4KeyMap> Vector4KeyMapPtr;
typedef std::shared_ptr<QuaternionKeyMap> QuaternionKeyMapPtr;
using FloatKeyMapPtr = std::shared_ptr<FloatKeyMap>;
using Vector3KeyMapPtr = std::shared_ptr<Vector3KeyMap>;
using Vector4KeyMapPtr = std::shared_ptr<Vector4KeyMap>;
using QuaternionKeyMapPtr = std::shared_ptr<QuaternionKeyMap>;
using ByteKeyMapPtr = std::shared_ptr<ByteKeyMap>;
} // Namespace
#endif //#ifndef OPENMW_COMPONENTS_NIF_NIFKEY_HPP

View file

@ -24,4 +24,21 @@ namespace Nif
t.scale = getFloat();
return t;
}
///Currently specific for 4.0.0.2 and earlier
bool NIFStream::getBoolean()
{
return getInt() != 0;
}
///Read in a string, either from the string table using the index (currently absent) or from the stream using the specified length
std::string NIFStream::getString()
{
return getSizedString();
}
// Convenience utility functions: get the versions of the currently read file
unsigned int NIFStream::getVersion() { return file->getVersion(); }
unsigned int NIFStream::getUserVersion() { return file->getBethVersion(); }
unsigned int NIFStream::getBethVersion() { return file->getBethVersion(); }
}

View file

@ -155,8 +155,16 @@ public:
Transformation getTrafo();
bool getBoolean();
std::string getString();
unsigned int getVersion();
unsigned int getUserVersion();
unsigned int getBethVersion();
///Read in a string of the given length
std::string getString(size_t length)
std::string getSizedString(size_t length)
{
std::vector<char> str(length + 1, 0);
@ -165,11 +173,19 @@ public:
return str.data();
}
///Read in a string of the length specified in the file
std::string getString()
std::string getSizedString()
{
size_t size = readLittleEndianType<uint32_t,uint32_t>(inp);
return getString(size);
return getSizedString(size);
}
///Specific to Bethesda headers, uses a byte for length
std::string getExportString()
{
size_t size = static_cast<size_t>(readLittleEndianType<uint8_t,uint8_t>(inp));
return getSizedString(size);
}
///This is special since the version string doesn't start with a number, and ends with "\n"
std::string getVersionString()
{
@ -190,6 +206,18 @@ public:
readLittleEndianDynamicBufferOfType<float,uint32_t>(inp, vec.data(), size);
}
void getInts(std::vector<int> &vec, size_t size)
{
vec.resize(size);
readLittleEndianDynamicBufferOfType<int,int>(inp, vec.data(), size);
}
void getUInts(std::vector<unsigned int> &vec, size_t size)
{
vec.resize(size);
readLittleEndianDynamicBufferOfType<unsigned int,unsigned int>(inp, vec.data(), size);
}
void getVector2s(std::vector<osg::Vec2f> &vec, size_t size)
{
vec.resize(size);
@ -217,6 +245,20 @@ public:
for (size_t i = 0;i < quat.size();i++)
quat[i] = getQuaternion();
}
void getStrings(std::vector<std::string> &vec, size_t size)
{
vec.resize(size);
for (size_t i = 0; i < vec.size(); i++)
vec[i] = getString();
}
/// We need to use this when the string table isn't actually initialized.
void getSizedStrings(std::vector<std::string> &vec, size_t size)
{
vec.resize(size);
for (size_t i = 0; i < vec.size(); i++)
vec[i] = getSizedString();
}
};
}

View file

@ -24,7 +24,7 @@ class Node : public Named
{
public:
// Node flags. Interpretation depends somewhat on the type of node.
int flags;
unsigned int flags;
Transformation trafo;
osg::Vec3f velocity; // Unused? Might be a run-time game state
PropertyList props;
@ -44,7 +44,7 @@ public:
velocity = nif->getVector3();
props.read(nif);
hasBounds = !!nif->getInt();
hasBounds = nif->getBoolean();
if(hasBounds)
{
nif->getInt(); // always 1

View file

@ -14,7 +14,7 @@ void Property::read(NIFStream *nif)
void NiTexturingProperty::Texture::read(NIFStream *nif)
{
inUse = !!nif->getInt();
inUse = nif->getBoolean();
if(!inUse) return;
texture.read(nif);

View file

@ -167,6 +167,7 @@ using NiParticleModifierPtr = RecordPtrT<NiParticleModifier>;
using NodeList = RecordListT<Node>;
using PropertyList = RecordListT<Property>;
using ExtraList = RecordListT<Extra>;
using NiSourceTextureList = RecordListT<NiSourceTexture>;
} // Namespace

View file

@ -35,17 +35,33 @@ namespace NifOsg
{
// interpolation of keyframes
template <typename MapT, typename InterpolationFunc>
template <typename MapT>
class ValueInterpolator
{
public:
typedef typename MapT::ValueType ValueT;
ValueInterpolator()
: mDefaultVal(ValueT())
typename MapT::MapType::const_iterator retrieveKey(float time) const
{
// retrieve the current position in the map, optimized for the most common case
// where time moves linearly along the keyframe track
if (mLastHighKey != mKeys->mKeys.end())
{
if (time > mLastHighKey->first)
{
// try if we're there by incrementing one
++mLastLowKey;
++mLastHighKey;
}
if (mLastHighKey != mKeys->mKeys.end() && time >= mLastLowKey->first && time <= mLastHighKey->first)
return mLastHighKey;
}
return mKeys->mKeys.lower_bound(time);
}
public:
using ValueT = typename MapT::ValueType;
ValueInterpolator() = default;
ValueInterpolator(std::shared_ptr<const MapT> keys, ValueT defaultVal = ValueT())
: mKeys(keys)
, mDefaultVal(defaultVal)
@ -67,44 +83,21 @@ namespace NifOsg
if(time <= keys.begin()->first)
return keys.begin()->second.mValue;
// retrieve the current position in the map, optimized for the most common case
// where time moves linearly along the keyframe track
typename MapT::MapType::const_iterator it = mLastHighKey;
if (mLastHighKey != keys.end())
{
if (time > mLastHighKey->first)
{
// try if we're there by incrementing one
++mLastLowKey;
++mLastHighKey;
it = mLastHighKey;
}
if (mLastHighKey == keys.end() || (time < mLastLowKey->first || time > mLastHighKey->first))
it = keys.lower_bound(time); // still not there, reorient by performing lower_bound check on the whole map
}
else
it = keys.lower_bound(time);
typename MapT::MapType::const_iterator it = retrieveKey(time);
// now do the actual interpolation
if (it != keys.end())
{
float aTime = it->first;
const typename MapT::KeyType* aKey = &it->second;
// cache for next time
mLastHighKey = it;
mLastLowKey = --it;
typename MapT::MapType::const_iterator last = --it;
mLastLowKey = last;
float aLastTime = last->first;
const typename MapT::KeyType* aLastKey = &last->second;
float a = (time - mLastLowKey->first) / (mLastHighKey->first - mLastLowKey->first);
float a = (time - aLastTime) / (aTime - aLastTime);
return InterpolationFunc()(aLastKey->mValue, aKey->mValue, a);
return interpolate(mLastLowKey->second, mLastHighKey->second, a, mKeys->mInterpolationType);
}
else
return keys.rbegin()->second.mValue;
return keys.rbegin()->second.mValue;
}
bool empty() const
@ -113,36 +106,44 @@ namespace NifOsg
}
private:
template <typename ValueType>
ValueType interpolate(const Nif::KeyT<ValueType>& a, const Nif::KeyT<ValueType>& b, float fraction, unsigned int type) const
{
switch (type)
{
case Nif::InterpolationType_Constant:
return fraction > 0.5f ? b.mValue : a.mValue;
default:
return a.mValue + ((b.mValue - a.mValue) * fraction);
}
}
osg::Quat interpolate(const Nif::KeyT<osg::Quat>& a, const Nif::KeyT<osg::Quat>& b, float fraction, unsigned int type) const
{
switch (type)
{
case Nif::InterpolationType_Constant:
return fraction > 0.5f ? b.mValue : a.mValue;
default:
{
osg::Quat result;
result.slerp(fraction, a.mValue, b.mValue);
return result;
}
}
}
mutable typename MapT::MapType::const_iterator mLastLowKey;
mutable typename MapT::MapType::const_iterator mLastHighKey;
std::shared_ptr<const MapT> mKeys;
ValueT mDefaultVal;
ValueT mDefaultVal = ValueT();
};
struct LerpFunc
{
template <typename ValueType>
inline ValueType operator()(const ValueType& a, const ValueType& b, float fraction)
{
return a + ((b - a) * fraction);
}
};
struct QuaternionSlerpFunc
{
inline osg::Quat operator()(const osg::Quat& a, const osg::Quat& b, float fraction)
{
osg::Quat result;
result.slerp(fraction, a, b);
return result;
}
};
typedef ValueInterpolator<Nif::QuaternionKeyMap, QuaternionSlerpFunc> QuaternionInterpolator;
typedef ValueInterpolator<Nif::FloatKeyMap, LerpFunc> FloatInterpolator;
typedef ValueInterpolator<Nif::Vector3KeyMap, LerpFunc> Vec3Interpolator;
using QuaternionInterpolator = ValueInterpolator<Nif::QuaternionKeyMap>;
using FloatInterpolator = ValueInterpolator<Nif::FloatKeyMap>;
using Vec3Interpolator = ValueInterpolator<Nif::Vector3KeyMap>;
using Vec4Interpolator = ValueInterpolator<Nif::Vector4KeyMap>;
class ControllerFunction : public SceneUtil::ControllerFunction
{

View file

@ -184,14 +184,16 @@ namespace NifOsg
{
public:
/// @param filename used for warning messages.
LoaderImpl(const std::string& filename)
: mFilename(filename), mFirstRootTextureIndex(-1), mFoundFirstRootTexturingProperty(false)
LoaderImpl(const std::string& filename, unsigned int ver, unsigned int userver, unsigned int bethver)
: mFilename(filename), mVersion(ver), mUserVersion(userver), mBethVersion(bethver)
{
}
std::string mFilename;
size_t mFirstRootTextureIndex;
bool mFoundFirstRootTexturingProperty;
unsigned int mVersion, mUserVersion, mBethVersion;
size_t mFirstRootTextureIndex = -1;
bool mFoundFirstRootTexturingProperty = false;
static void loadKf(Nif::NIFFilePtr nif, KeyframeHolder& target)
{
@ -1846,13 +1848,13 @@ namespace NifOsg
osg::ref_ptr<osg::Node> Loader::load(Nif::NIFFilePtr file, Resource::ImageManager* imageManager)
{
LoaderImpl impl(file->getFilename());
LoaderImpl impl(file->getFilename(), file->getVersion(), file->getUserVersion(), file->getBethVersion());
return impl.load(file, imageManager);
}
void Loader::loadKf(Nif::NIFFilePtr kf, KeyframeHolder& target)
{
LoaderImpl impl(kf->getFilename());
LoaderImpl impl(kf->getFilename(), kf->getVersion(), kf->getUserVersion(), kf->getBethVersion());
impl.loadKf(kf, target);
}

View file

@ -151,7 +151,6 @@ namespace NifOsg
float mCachedDefaultSize;
};
typedef ValueInterpolator<Nif::Vector4KeyMap, LerpFunc> Vec4Interpolator;
class ParticleColorAffector : public osgParticle::Operator
{
public:

View file

@ -3,10 +3,9 @@
#include <sstream>
#include <components/debug/debuglog.hpp>
#include <components/misc/stringops.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>
void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, CategorySettingValueMap& settings)
{
@ -36,7 +35,7 @@ void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, Cat
fail("unterminated category");
currentCategory = line.substr(i+1, end - (i+1));
boost::algorithm::trim(currentCategory);
Misc::StringUtils::trim(currentCategory);
i = end+1;
}
@ -51,11 +50,11 @@ void Settings::SettingsFileParser::loadSettingsFile(const std::string& file, Cat
fail("unterminated setting name");
std::string setting = line.substr(i, (settingEnd-i));
boost::algorithm::trim(setting);
Misc::StringUtils::trim(setting);
size_t valueBegin = settingEnd+1;
std::string value = line.substr(valueBegin);
boost::algorithm::trim(value);
Misc::StringUtils::trim(value);
if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false)
fail(std::string("duplicate setting: [" + currentCategory + "] " + setting));
@ -142,7 +141,7 @@ void Settings::SettingsFileParser::saveSettingsFile(const std::string& file, con
// Update the current category.
currentCategory = line.substr(i+1, end - (i+1));
boost::algorithm::trim(currentCategory);
Misc::StringUtils::trim(currentCategory);
// Write the (new) current category to the file.
ostream << "[" << currentCategory << "]" << std::endl;
@ -176,12 +175,12 @@ void Settings::SettingsFileParser::saveSettingsFile(const std::string& file, con
continue;
}
std::string setting = line.substr(i, (settingEnd-i));
boost::algorithm::trim(setting);
Misc::StringUtils::trim(setting);
// Get the existing value so we can see if we've changed it.
size_t valueBegin = settingEnd+1;
std::string value = line.substr(valueBegin);
boost::algorithm::trim(value);
Misc::StringUtils::trim(value);
// Construct the setting map key to determine whether the setting has already been
// written to the file.

View file

@ -8,9 +8,9 @@
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/fstream.hpp>
#include <boost/algorithm/string.hpp>
#include <components/debug/debuglog.hpp>
#include <components/misc/stringops.hpp>
namespace Shader
{
@ -60,7 +60,7 @@ namespace Shader
bool parseIncludes(boost::filesystem::path shaderPath, std::string& source)
{
boost::replace_all(source, "\r\n", "\n");
Misc::StringUtils::replaceAll(source, "\r\n", "\n");
std::set<boost::filesystem::path> includedFiles;
size_t foundPos = 0;
@ -165,7 +165,7 @@ namespace Shader
std::string list = source.substr(listStart, listEnd - listStart);
std::vector<std::string> listElements;
if (list != "")
boost::split(listElements, list, boost::is_any_of(","));
Misc::StringUtils::split (list, listElements, ",");
size_t contentStart = source.find_first_not_of("\n\r", listEnd);
size_t contentEnd = source.find("$endforeach", contentStart);

View file

@ -6,9 +6,8 @@
#include <osgUtil/TangentSpaceGenerator>
#include <boost/algorithm/string.hpp>
#include <components/debug/debuglog.hpp>
#include <components/misc/stringops.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/vfs/manager.hpp>
#include <components/sceneutil/riggeometry.hpp>
@ -145,7 +144,7 @@ namespace Shader
osg::ref_ptr<osg::Image> image;
bool normalHeight = false;
std::string normalHeightMap = normalMapFileName;
boost::replace_last(normalHeightMap, ".", mNormalHeightMapPattern + ".");
Misc::StringUtils::replaceLast(normalHeightMap, ".", mNormalHeightMapPattern + ".");
if (mImageManager.getVFS()->exists(normalHeightMap))
{
image = mImageManager.getImage(normalHeightMap);
@ -153,7 +152,7 @@ namespace Shader
}
else
{
boost::replace_last(normalMapFileName, ".", mNormalMapPattern + ".");
Misc::StringUtils::replaceLast(normalMapFileName, ".", mNormalMapPattern + ".");
if (mImageManager.getVFS()->exists(normalMapFileName))
{
image = mImageManager.getImage(normalMapFileName);
@ -184,7 +183,7 @@ namespace Shader
if (mAutoUseSpecularMaps && diffuseMap != nullptr && specularMap == nullptr && diffuseMap->getImage(0))
{
std::string specularMapFileName = diffuseMap->getImage(0)->getFileName();
boost::replace_last(specularMapFileName, ".", mSpecularMapPattern + ".");
Misc::StringUtils::replaceLast(specularMapFileName, ".", mSpecularMapPattern + ".");
if (mImageManager.getVFS()->exists(specularMapFileName))
{
osg::ref_ptr<osg::Image> image (mImageManager.getImage(specularMapFileName));

View file

@ -103,13 +103,13 @@ actors processing range
: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.
This setting specifies the actor state update distance from the player in game units.
Actor state update includes AI, animations, and physics processing.
Actors near that border start softly fade out instead of just appearing/disapperaing.
It is not recommended to change this value from default if you use mods with
long-range AiTravel packages (e.g. patrols, caravans and travellers).
Actors close to this distance softly fade in and out instead of appearing or disappearing abruptly.
Keep in mind that actors running Travel AI packages are always active to avoid
issues in mods with long-range AiTravel packages (for example, patrols, caravans and travellers).
This setting can be controlled in game with the "Actors processing range slider" in the Prefs panel of the Options menu.
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
----------------------------------------
@ -282,3 +282,19 @@ equivalent to the one introduced by the equivalent Morrowind Code Patch feature.
This makes the movement speed behavior more fair between different races.
This setting can be controlled in Advanced tab of the launcher.
projectiles enchant multiplier
------------------------------
:Type: floating point
:Range: 0.0 to 1.0
:Default: 0.0
The value of this setting determines how many projectiles (thrown weapons, arrows and bolts) you can enchant at once according to the following formula:
count = (soul gem charge * projectiles enchant multiplier) / enchantment strength
A value of 0 means that you can only enchant one projectile.
If you want to have Morrowind Code Patch-like count of projectiles being enchanted at once, set this value to 0.25 (i.e. 25% of the charge).
This setting can only be configured by editing the settings configuration file.

View file

@ -213,7 +213,7 @@
<Widget type="AutoSizedButton" skin="MW_Button" position="4 194 137 24" align="Left Bottom" name="ResetControlsButton">
<Property key="Caption" value="#{sControlsMenu1}"/>
</Widget>
<Widget type="HBox" skin="" position="4 224 360 24" align="Left Bottom">
<Widget type="HBox" skin="" position="4 224 380 24" align="Left Bottom">
<Widget type="AutoSizedButton" skin="MW_Button" position="0 0 24 24" align="Left Bottom">
<UserString key="SettingCategory" value="Input"/>
<UserString key="SettingName" value="invert x axis"/>
@ -231,10 +231,10 @@
<Property key="Caption" value="#{sMouseFlip}"/>
</Widget>
</Widget>
<Widget type="TextBox" skin="NormalText" position="4 254 336 18" align="Left Bottom">
<Widget type="TextBox" skin="NormalText" position="4 254 352 18" align="Left Bottom">
<Property key="Caption" value="Camera Sensitivity"/>
</Widget>
<Widget type="ScrollBar" skin="MW_HScroll" position="4 278 336 18" align="HStretch Bottom">
<Widget type="ScrollBar" skin="MW_HScroll" position="4 278 352 18" align="HStretch Bottom">
<Property key="Range" value="10000"/>
<Property key="Page" value="300"/>
<UserString key="SettingType" value="Slider"/>
@ -244,15 +244,15 @@
<UserString key="SettingMin" value="0.2"/>
<UserString key="SettingMax" value="5.0"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="4 302 336 18" align="Left Bottom">
<Widget type="TextBox" skin="SandText" position="4 302 352 18" align="Left Bottom">
<Property key="Caption" value="#{sLow}"/>
<Property key="TextAlign" value="Left"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="4 302 336 18" align="Right Bottom">
<Widget type="TextBox" skin="SandText" position="4 302 352 18" align="Right Bottom">
<Property key="Caption" value="#{sHigh}"/>
<Property key="TextAlign" value="Right"/>
</Widget>
<Widget type="HBox" skin="" position="4 324 336 24" align="Left Bottom">
<Widget type="HBox" skin="" position="4 324 352 24" align="Left Bottom">
<Widget type="AutoSizedButton" skin="MW_Button" position="0 0 24 24" align="Left Bottom">
<UserString key="SettingCategory" value="Input"/>
<UserString key="SettingName" value="enable controller"/>
@ -306,9 +306,9 @@
<Property key="Caption" value="Hint: press F3 to show \nthe current frame rate."/>
</Widget>
<Widget type="TextBox" skin="NormalText" position="0 198 329 18" align="Left Top" name="FovText">
<Widget type="TextBox" skin="NormalText" position="0 198 352 18" align="Left Top" name="FovText">
</Widget>
<Widget type="ScrollBar" skin="MW_HScroll" position="0 222 329 18" align="HStretch Top">
<Widget type="ScrollBar" skin="MW_HScroll" position="0 222 352 18" align="HStretch Top">
<Property key="Range" value="81"/>
<Property key="Page" value="1"/>
<UserString key="SettingType" value="Slider"/>
@ -320,18 +320,18 @@
<UserString key="SettingLabelWidget" value="FovText"/>
<UserString key="SettingLabelCaption" value="Field of View (%s)"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 246 329 18" align="Left Top">
<Widget type="TextBox" skin="SandText" position="0 246 352 18" align="Left Top">
<Property key="Caption" value="#{sLow}"/>
<Property key="TextAlign" value="Left"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 246 329 18" align="Right Top">
<Widget type="TextBox" skin="SandText" position="0 246 352 18" align="Right Top">
<Property key="Caption" value="#{sHigh}"/>
<Property key="TextAlign" value="Right"/>
</Widget>
<Widget type="TextBox" skin="NormalText" position="0 268 329 18" align="Left Top" name="GammaText">
<Widget type="TextBox" skin="NormalText" position="0 268 352 18" align="Left Top" name="GammaText">
<Property key="Caption" value="#{sGamma_Correction}"/>
</Widget>
<Widget type="ScrollBar" skin="MW_HScroll" position="0 292 329 18" align="HStretch Top" name="GammaSlider">
<Widget type="ScrollBar" skin="MW_HScroll" position="0 292 352 18" align="HStretch Top" name="GammaSlider">
<Property key="Range" value="10000"/>
<Property key="Page" value="300"/>
<UserString key="SettingType" value="Slider"/>
@ -341,11 +341,11 @@
<UserString key="SettingMin" value="0.1"/>
<UserString key="SettingMax" value="3.0"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 316 329 18" align="Left Top" name="GammaTextDark">
<Widget type="TextBox" skin="SandText" position="0 316 352 18" align="Left Top" name="GammaTextDark">
<Property key="Caption" value="#{sDark_Gamma}"/>
<Property key="TextAlign" value="Left"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="0 316 329 18" align="Right Top" name="GammaTextLight">
<Widget type="TextBox" skin="SandText" position="0 316 352 18" align="Right Top" name="GammaTextLight">
<Property key="Caption" value="#{sLight_Gamma}"/>
<Property key="TextAlign" value="Right"/>
</Widget>
@ -373,10 +373,10 @@
<UserString key="SettingLabelCaption" value="Anisotropy (%s)"/>
</Widget>
</Widget>
<Widget type="TextBox" skin="NormalText" position="4 130 322 18" align="Left Top" name="RenderDistanceLabel">
<Widget type="TextBox" skin="NormalText" position="0 130 352 18" align="Left Top" name="RenderDistanceLabel">
<Property key="Caption" value="#{sRender_Distance}"/>
</Widget>
<Widget type="ScrollBar" skin="MW_HScroll" position="4 154 322 18" align="Left Top" name="RenderingDistanceSlider">
<Widget type="ScrollBar" skin="MW_HScroll" position="0 154 352 18" align="HStretch Top" name="RenderingDistanceSlider">
<Property key="Range" value="4609"/>
<Property key="Page" value="128"/>
<UserString key="SettingType" value="Slider"/>
@ -388,7 +388,7 @@
<UserString key="SettingLabelWidget" value="RenderDistanceLabel"/>
<UserString key="SettingLabelCaption" value="#{sRender_Distance} (%s)"/>
</Widget>
<Widget type="ScrollBar" skin="MW_HScroll" position="4 154 322 18" align="Left Top" name="LargeRenderingDistanceSlider">
<Widget type="ScrollBar" skin="MW_HScroll" position="0 154 352 18" align="HStretch Top" name="LargeRenderingDistanceSlider">
<Property key="Range" value="79873"/>
<Property key="Page" value="2048"/>
<UserString key="SettingType" value="Slider"/>
@ -400,11 +400,11 @@
<UserString key="SettingLabelWidget" value="RenderDistanceLabel"/>
<UserString key="SettingLabelCaption" value="#{sRender_Distance} (x%s)"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="4 178 332 18" align="Left Top">
<Widget type="TextBox" skin="SandText" position="0 178 352 18" align="Left Top">
<Property key="Caption" value="#{sNear}"/>
<Property key="TextAlign" value="Left"/>
</Widget>
<Widget type="TextBox" skin="SandText" position="4 178 332 18" align="Left Top">
<Widget type="TextBox" skin="SandText" position="0 178 352 18" align="Right Top">
<Property key="Caption" value="#{sFar}"/>
<Property key="TextAlign" value="Right"/>
</Widget>

View file

@ -267,6 +267,11 @@ use magic item animations = false
# Don't use race weight in NPC movement speed calculations
normalise race speed = false
# Adjusts the number of projectiles you can enchant at once:
# count = (soul gem charge * projectiles enchant multiplier) / enchantment strength
# A value of 0 means that you can only enchant one projectile.
projectiles enchant multiplier = 0
[General]
# Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16).