mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-01-16 04:49:54 +00:00
Merge branch 'master' into openxr_vr
This commit is contained in:
commit
3984e2030a
74 changed files with 1200 additions and 405 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,10 @@ namespace CSVPrefs
|
|||
QStackedLayout* mStackedLayout;
|
||||
QGridLayout* mPageLayout;
|
||||
QComboBox* mPageSelector;
|
||||
|
||||
private slots:
|
||||
|
||||
void resetKeyBindings();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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__
|
||||
|
|
|
@ -143,7 +143,8 @@ 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_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++;
|
||||
|
||||
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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -77,24 +77,20 @@ 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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -103,63 +99,72 @@ namespace MWMechanics
|
|||
*
|
||||
* Walking state transitions (player greeting check not shown):
|
||||
*
|
||||
* MoveNow <------------------------------------+
|
||||
* | d|
|
||||
* | |
|
||||
* +-> State_Norm <---> State_CheckStuck --> State_Evade
|
||||
* Initial ----> Norm <--------> CheckStuck -------> Evade ---+
|
||||
* ^ ^ | f ^ | t ^ | |
|
||||
* | | | | | | | |
|
||||
* | +---+ +---+ +---+ | u
|
||||
* | +-+ +---+ +---+ | 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 = State_Norm;
|
||||
mStuckDuration = 0;
|
||||
mEvadeDuration = 0;
|
||||
mWalkState = WalkState::Norm;
|
||||
mStateDuration = 0;
|
||||
mPrev = position;
|
||||
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::Evade)
|
||||
{
|
||||
mStuckDuration = 0;
|
||||
mWalkState = State_Evade;
|
||||
chooseEvasionDirection();
|
||||
}
|
||||
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 = WalkState::Norm;
|
||||
mStateDuration = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
mEvadeDuration += duration;
|
||||
if(mEvadeDuration >= DURATION_TO_EVADE)
|
||||
if (mWalkState == WalkState::Norm)
|
||||
{
|
||||
mWalkState = WalkState::CheckStuck;
|
||||
mStateDuration = duration;
|
||||
return;
|
||||
}
|
||||
|
||||
mStateDuration += duration;
|
||||
if (mStateDuration < DURATION_SAME_SPOT)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mWalkState = WalkState::Evade;
|
||||
mStateDuration = 0;
|
||||
chooseEvasionDirection();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,9 +609,8 @@ 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+i)
|
||||
if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute &&
|
||||
effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill)
|
||||
{
|
||||
std::vector<ActiveSpells::ActiveEffect> absorbEffects;
|
||||
ActiveSpells::ActiveEffect effect_ = effect;
|
||||
|
@ -618,7 +626,6 @@ namespace MWMechanics
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-casting a summon effect will remove the creature from previous castings of that effect.
|
||||
if (isSummoningEffect(effectIt->mEffectID) && !target.isEmpty() && target.getClass().isActor())
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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,15 +538,20 @@ 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");
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(¤tTime), "%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());
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,6 +250,7 @@ namespace
|
|||
}
|
||||
}
|
||||
|
||||
if (!mTest)
|
||||
mLoadingListener.increaseProgress (1);
|
||||
}
|
||||
}
|
||||
|
@ -317,8 +322,9 @@ namespace MWWorld
|
|||
mPreloader->updateCache(mRendering.getReferenceTime());
|
||||
}
|
||||
|
||||
void Scene::unloadCell (CellStoreCollection::iterator iter)
|
||||
void Scene::unloadCell (CellStoreCollection::iterator iter, bool test)
|
||||
{
|
||||
if (!test)
|
||||
Log(Debug::Info) << "Unloading cell " << (*iter)->getCell()->getDescription();
|
||||
|
||||
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
|
||||
|
@ -373,12 +379,15 @@ 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)
|
||||
{
|
||||
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;
|
||||
|
@ -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,9 +427,11 @@ namespace MWWorld
|
|||
cell->respawn();
|
||||
|
||||
// ... then references. This is important for adjustPosition to work correctly.
|
||||
insertCell (*cell, loadingListener);
|
||||
insertCell (*cell, loadingListener, test);
|
||||
|
||||
mRendering.addCell(cell);
|
||||
if (!test)
|
||||
{
|
||||
MWBase::Environment::get().getWindowManager()->addCell(cell);
|
||||
bool waterEnabled = cell->getCell()->hasWater() || cell->isExterior();
|
||||
float waterLevel = cell->getWaterLevel();
|
||||
|
@ -449,8 +460,12 @@ namespace MWWorld
|
|||
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); });
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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() )
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
45
components/detournavigator/findrandompointaroundcircle.cpp
Normal file
45
components/detournavigator/findrandompointaroundcircle.cpp
Normal 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);
|
||||
}
|
||||
}
|
20
components/detournavigator/findrandompointaroundcircle.hpp
Normal file
20
components/detournavigator/findrandompointaroundcircle.hpp
Normal 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
|
20
components/detournavigator/navigator.cpp
Normal file
20
components/detournavigator/navigator.cpp
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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 ());
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace Nif
|
|||
{
|
||||
Named::read(nif);
|
||||
|
||||
external = !!nif->getChar();
|
||||
external = nif->getChar() != 0;
|
||||
if(external)
|
||||
filename = nif->getString();
|
||||
else
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
@ -145,21 +143,12 @@ void NIFFile::parse(Files::IStreamPtr stream)
|
|||
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;
|
||||
|
|
|
@ -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>;
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(); }
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -35,16 +35,32 @@ 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)
|
||||
|
@ -67,43 +83,20 @@ 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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -151,7 +151,6 @@ namespace NifOsg
|
|||
float mCachedDefaultSize;
|
||||
};
|
||||
|
||||
typedef ValueInterpolator<Nif::Vector4KeyMap, LerpFunc> Vec4Interpolator;
|
||||
class ParticleColorAffector : public osgParticle::Operator
|
||||
{
|
||||
public:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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).
|
||||
|
|
Loading…
Reference in a new issue