1
0
Fork 1
mirror of https://github.com/TES3MP/openmw-tes3mp.git synced 2025-10-05 02:26:30 +00:00

Add OpenMW commits up to 13 Jan 2020

# Conflicts:
#	apps/openmw/mwmechanics/actors.cpp
This commit is contained in:
David Cernat 2020-01-15 07:49:26 +02:00
commit 60b6f92fa3
47 changed files with 588 additions and 277 deletions

View file

@ -28,6 +28,7 @@
Bug #4262: Rain settings are hardcoded Bug #4262: Rain settings are hardcoded
Bug #4270: Closing doors while they are obstructed desyncs closing sfx Bug #4270: Closing doors while they are obstructed desyncs closing sfx
Bug #4276: Resizing character window differs from vanilla 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 #4329: Removed birthsign abilities are restored after reloading the save
Bug #4341: Error message about missing GDB is too vague Bug #4341: Error message about missing GDB is too vague
Bug #4383: Bow model obscures crosshair when arrow is drawn 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 #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 #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 #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 #4701: PrisonMarker record is not hardcoded like other markers
Bug #4703: Editor: it's possible to preview levelled list records Bug #4703: Editor: it's possible to preview levelled list records
Bug #4705: Editor: unable to open exterior cell views from Instances table 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 #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 #5067: Ranged attacks on unaware opponents ("critical hits") differ from the vanilla engine
Bug #5069: Blocking creatures' attacks doesn't degrade shields 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 #5074: Paralyzed actors greet the player
Bug #5075: Enchanting cast style can be changed if there's no object Bug #5075: Enchanting cast style can be changed if there's no object
Bug #5078: DisablePlayerLooking is broken Bug #5078: DisablePlayerLooking is broken
@ -178,6 +181,7 @@
Bug #5209: Spellcasting ignores race height Bug #5209: Spellcasting ignores race height
Bug #5210: AiActivate allows actors to open dialogue and inventory windows 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 #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 #5213: SameFaction script function is broken
Bug #5218: Crash when disabling ToggleBorders Bug #5218: Crash when disabling ToggleBorders
Bug #5220: GetLOS crashes when actor isn't loaded Bug #5220: GetLOS crashes when actor isn't loaded
@ -185,6 +189,11 @@
Bug #5223: Bow replacement during attack animation removes attached arrow Bug #5223: Bow replacement during attack animation removes attached arrow
Bug #5226: Reputation should be capped Bug #5226: Reputation should be capped
Bug #5229: Crash if mesh controller node has no data node 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
Feature #1774: Handle AvoidNode Feature #1774: Handle AvoidNode
Feature #2229: Improve pathfinding AI Feature #2229: Improve pathfinding AI
Feature #3025: Analogue gamepad movement controls Feature #3025: Analogue gamepad movement controls
@ -196,6 +205,8 @@
Feature #3980: In-game option to disable controller Feature #3980: In-game option to disable controller
Feature #3999: Shift + Double Click should maximize/restore menu size Feature #3999: Shift + Double Click should maximize/restore menu size
Feature #4001: Toggle sneak controller shortcut 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 #4209: Editor: Faction rank sub-table
Feature #4255: Handle broken RepairedOnMe script function Feature #4255: Handle broken RepairedOnMe script function
Feature #4316: Implement RaiseRank/LowerRank functions properly Feature #4316: Implement RaiseRank/LowerRank functions properly

View file

@ -2,6 +2,7 @@
#include <iomanip> #include <iomanip>
#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp> #include <boost/filesystem/fstream.hpp>
#include <osgDB/ReadFile> #include <osgDB/ReadFile>
@ -349,7 +350,7 @@ namespace ESSImport
writer.setFormat (ESM::SavedGame::sCurrentFormat); 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 // all unused
writer.setVersion(0); writer.setVersion(0);
writer.setType(0); writer.setType(0);

View file

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

View file

@ -4,11 +4,13 @@
#include <QComboBox> #include <QComboBox>
#include <QGridLayout> #include <QGridLayout>
#include <QPushButton>
#include <QStackedLayout> #include <QStackedLayout>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "../../model/prefs/setting.hpp" #include "../../model/prefs/setting.hpp"
#include "../../model/prefs/category.hpp" #include "../../model/prefs/category.hpp"
#include "../../model/prefs/state.hpp"
namespace CSVPrefs namespace CSVPrefs
{ {
@ -29,8 +31,18 @@ namespace CSVPrefs
mPageSelector = new QComboBox(); mPageSelector = new QComboBox();
connect(mPageSelector, SIGNAL(currentIndexChanged(int)), mStackedLayout, SLOT(setCurrentIndex(int))); 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(mPageSelector);
topLayout->addWidget(stackedWidget); topLayout->addWidget(stackedWidget);
topLayout->addWidget(lineSeparator);
topLayout->addWidget(resetButton);
topLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); topLayout->setSizeConstraint(QLayout::SetMinAndMaxSize);
// Add each option // Add each option
@ -85,4 +97,9 @@ namespace CSVPrefs
} }
} }
} }
void KeyBindingPage::resetKeyBindings()
{
CSMPrefs::State::get().resetCategory("Key Bindings");
}
} }

View file

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

View file

@ -400,17 +400,7 @@ namespace MWGui
struct tm* timeinfo; struct tm* timeinfo;
timeinfo = localtime(&time); timeinfo = localtime(&time);
// Use system/environment locale settings for datetime formatting text << std::put_time(timeinfo, "%Y.%m.%d %T") << "\n";
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 << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n"; text << "#{sLevel} " << mCurrentSlot->mProfile.mPlayerLevel << "\n";
text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCell << "}\n"; text << "#{sCell=" << mCurrentSlot->mProfile.mPlayerCell << "}\n";

View file

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

View file

@ -1697,6 +1697,8 @@ namespace MWInput
return "Zoom In"; return "Zoom In";
else if (action == A_ZoomOut) else if (action == A_ZoomOut)
return "Zoom Out"; return "Zoom Out";
else if (action == A_ToggleHUD)
return "Toggle HUD";
descriptions[A_Use] = "sUse"; descriptions[A_Use] = "sUse";
descriptions[A_Activate] = "sActivate"; descriptions[A_Activate] = "sActivate";
@ -1875,6 +1877,7 @@ namespace MWInput
ret.push_back(A_Console); ret.push_back(A_Console);
ret.push_back(A_QuickSave); ret.push_back(A_QuickSave);
ret.push_back(A_QuickLoad); ret.push_back(A_QuickLoad);
ret.push_back(A_ToggleHUD);
ret.push_back(A_Screenshot); ret.push_back(A_Screenshot);
ret.push_back(A_QuickKeysMenu); ret.push_back(A_QuickKeysMenu);
ret.push_back(A_QuickKey1); ret.push_back(A_QuickKey1);
@ -1908,6 +1911,7 @@ namespace MWInput
ret.push_back(A_Rest); ret.push_back(A_Rest);
ret.push_back(A_QuickSave); ret.push_back(A_QuickSave);
ret.push_back(A_QuickLoad); ret.push_back(A_QuickLoad);
ret.push_back(A_ToggleHUD);
ret.push_back(A_Screenshot); ret.push_back(A_Screenshot);
ret.push_back(A_QuickKeysMenu); ret.push_back(A_QuickKeysMenu);
ret.push_back(A_QuickKey1); ret.push_back(A_QuickKey1);
@ -1948,7 +1952,7 @@ namespace MWInput
} }
// Disallow binding reserved keys // 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; return;
#ifndef __APPLE__ #ifndef __APPLE__

View file

@ -161,8 +161,9 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float
namespace MWMechanics namespace MWMechanics
{ {
static const int GREETING_SHOULD_START = 4; //how many updates should pass before NPC can greet player 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; static const float DECELERATE_DISTANCE = 512.f;
class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor class GetStuntedMagickaDuration : public MWMechanics::EffectSourceVisitor
@ -552,9 +553,10 @@ namespace MWMechanics
{ {
greetingTimer++; greetingTimer++;
turnActorToFacePlayer(actor, dir); if (greetingTimer <= GREETING_SHOULD_END || MWBase::Environment::get().getSoundManager()->sayActive(actor))
turnActorToFacePlayer(actor, dir);
if (greetingTimer >= GREETING_SHOULD_END) if (greetingTimer >= GREETING_COOLDOWN)
{ {
greetingState = Greet_Done; greetingState = Greet_Done;
greetingTimer = 0; greetingTimer = 0;
@ -1867,6 +1869,11 @@ namespace MWMechanics
} }
} }
} }
else if ((isLocalActor || aiActive) && iter->first != player && isConscious(iter->first))
{
CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first);
stats.getAiSequence().execute(iter->first, *ctrl, duration, /*outOfRange*/true);
}
/* /*
End of tes3mp change (major) End of tes3mp change (major)
*/ */
@ -1897,7 +1904,15 @@ namespace MWMechanics
{ {
const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length(); const float dist = (playerPos - iter->first.getRefData().getPosition().asVec3()).length();
bool isPlayer = iter->first == player; 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) int activeFlag = 1; // Can be changed back to '2' to keep updating bounding boxes off screen (more accurate, but slower)
if (isPlayer) if (isPlayer)
activeFlag = 2; activeFlag = 2;

View file

@ -104,6 +104,9 @@ namespace MWMechanics
virtual osg::Vec3f getDestination() { return osg::Vec3f(0, 0, 0); } 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 /// Reset pathfinding state
void reset(); void reset();

View file

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

View file

@ -110,7 +110,7 @@ namespace MWMechanics
void stopPursuit(); void stopPursuit();
/// Execute current package, switching if needed. /// 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 /// Simulate the passing of time using the currently active AI package
void fastForward(const MWWorld::Ptr &actor); void fastForward(const MWWorld::Ptr &actor);

View file

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

View file

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

View file

@ -464,9 +464,9 @@ namespace MWMechanics
if (!checkEffectTarget(effectIt->mEffectID, target, caster, castByPlayer)) if (!checkEffectTarget(effectIt->mEffectID, target, caster, castByPlayer))
continue; 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 if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked
&& (caster.isEmpty() || !caster.getClass().isActor() || caster == target)) && (caster.isEmpty() || !caster.getClass().isActor()))
continue; continue;
// If player is healing someone, show the target's HP bar // If player is healing someone, show the target's HP bar
@ -569,6 +569,15 @@ namespace MWMechanics
effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; effect.mArg = MWMechanics::EffectKey(*effectIt).mArg;
effect.mMagnitude = magnitude; 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); bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
if (hasDuration && effectIt->mDuration == 0) if (hasDuration && effectIt->mDuration == 0)
{ {
@ -641,21 +650,19 @@ namespace MWMechanics
// magnitude, since we're transferring stats from the target to the caster // magnitude, since we're transferring stats from the target to the caster
if (!caster.isEmpty() && caster != target && caster.getClass().isActor()) if (!caster.isEmpty() && caster != target && caster.getClass().isActor())
{ {
for (int i=0; i<5; ++i) if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute &&
effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill)
{ {
if (effectIt->mEffectID == ESM::MagicEffect::AbsorbAttribute+i) std::vector<ActiveSpells::ActiveEffect> absorbEffects;
{ ActiveSpells::ActiveEffect effect_ = effect;
std::vector<ActiveSpells::ActiveEffect> absorbEffects; effect_.mMagnitude *= -1;
ActiveSpells::ActiveEffect effect_ = effect; absorbEffects.push_back(effect_);
effect_.mMagnitude *= -1; if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game"))
absorbEffects.push_back(effect_); target.getClass().getCreatureStats(target).getActiveSpells().addSpell("", true,
if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game")) absorbEffects, mSourceName, caster.getClass().getCreatureStats(caster).getActorId());
target.getClass().getCreatureStats(target).getActiveSpells().addSpell("", true, else
absorbEffects, mSourceName, caster.getClass().getCreatureStats(caster).getActorId()); caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true,
else absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId());
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId());
}
} }
} }
} }

View file

@ -27,6 +27,7 @@
#include "../mwworld/ptr.hpp" #include "../mwworld/ptr.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/weapontype.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 ActorAnimation::getShieldMesh(MWWorld::ConstPtr shield) const
{ {
std::string mesh = shield.getClass().getModel(shield); 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; std::string holsteredName = mesh;
holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif"); holsteredName = holsteredName.replace(holsteredName.size()-4, 4, "_sh.nif");
if(mResourceSystem->getVFS()->exists(holsteredName)) if(mResourceSystem->getVFS()->exists(holsteredName))

View file

@ -13,11 +13,13 @@
#include <components/misc/stringops.hpp> #include <components/misc/stringops.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "../mwmechanics/weapontype.hpp" #include "../mwmechanics/weapontype.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
namespace MWRender namespace MWRender
{ {
@ -114,6 +116,7 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot)
MWWorld::ConstPtr item = *it; MWWorld::ConstPtr item = *it;
std::string bonename; std::string bonename;
std::string itemModel = item.getClass().getModel(item);
if (slot == MWWorld::InventoryStore::Slot_CarriedRight) if (slot == MWWorld::InventoryStore::Slot_CarriedRight)
{ {
if(item.getTypeName() == typeid(ESM::Weapon).name()) if(item.getTypeName() == typeid(ESM::Weapon).name())
@ -132,11 +135,30 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot)
bonename = "Weapon Bone"; bonename = "Weapon Bone";
} }
else else
{
bonename = "Shield Bone"; 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 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(); const NodeMap& nodeMap = getNodeMap();
NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename)); NodeMap::const_iterator found = nodeMap.find(Misc::StringUtils::lowerCase(bonename));

View file

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

View file

@ -22,6 +22,8 @@
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/misc/stringops.hpp>
#include <components/resource/resourcesystem.hpp> #include <components/resource/resourcesystem.hpp>
#include <components/resource/imagemanager.hpp> #include <components/resource/imagemanager.hpp>
#include <components/resource/scenemanager.hpp> #include <components/resource/scenemanager.hpp>
@ -47,8 +49,6 @@
#include <components/detournavigator/navigator.hpp> #include <components/detournavigator/navigator.hpp>
#include <boost/algorithm/string.hpp>
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwgui/loadingscreen.hpp" #include "../mwgui/loadingscreen.hpp"
@ -767,7 +767,7 @@ namespace MWRender
int screenshotMapping = 0; int screenshotMapping = 0;
std::vector<std::string> settingArgs; std::vector<std::string> settingArgs;
boost::algorithm::split(settingArgs,settingStr,boost::is_any_of(" ")); Misc::StringUtils::split(settingStr, settingArgs);
if (settingArgs.size() > 0) if (settingArgs.size() > 0)
{ {

View file

@ -1,6 +1,7 @@
#include "miscextensions.hpp" #include "miscextensions.hpp"
#include <cstdlib> #include <cstdlib>
#include <iomanip>
/* /*
Start of tes3mp addition Start of tes3mp addition
@ -19,6 +20,8 @@
#include <components/compiler/opcodes.hpp> #include <components/compiler/opcodes.hpp>
#include <components/compiler/locals.hpp> #include <components/compiler/locals.hpp>
#include <components/debug/debuglog.hpp>
#include <components/interpreter/interpreter.hpp> #include <components/interpreter/interpreter.hpp>
#include <components/interpreter/runtime.hpp> #include <components/interpreter/runtime.hpp>
#include <components/interpreter/opcodes.hpp> #include <components/interpreter/opcodes.hpp>
@ -1225,18 +1228,9 @@ namespace MWScript
return; 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()) if (ptr == MWMechanics::getPlayer())
{ {
MWWorld::InventoryStore& store = ptr.getClass().getInventoryStore(ptr); MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spellId);
store.setSelectedEnchantItem(store.end());
MWBase::Environment::get().getWindowManager()->setSelectedSpell(spellId, int(MWMechanics::getSpellSuccessChance(spellId, ptr)));
MWBase::Environment::get().getWindowManager()->updateSpellWindow();
return; return;
} }
@ -1244,7 +1238,6 @@ namespace MWScript
{ {
MWMechanics::AiCast castPackage(targetId, spellId, true); MWMechanics::AiCast castPackage(targetId, spellId, true);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr);
return; return;
} }
@ -1268,9 +1261,29 @@ namespace MWScript
{ {
MWWorld::Ptr ptr = R()(runtime); MWWorld::Ptr ptr = R()(runtime);
std::string spell = runtime.getStringLiteral (runtime[0].mInteger); std::string spellId = runtime.getStringLiteral (runtime[0].mInteger);
runtime.pop(); 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); MWMechanics::CastSpell cast(ptr, ptr, false, true);
cast.mHitPosition = ptr.getRefData().getPosition().asVec3(); cast.mHitPosition = ptr.getRefData().getPosition().asVec3();
cast.mAlwaysSucceed = true; cast.mAlwaysSucceed = true;
@ -1341,6 +1354,11 @@ namespace MWScript
std::stringstream msg; std::stringstream msg;
msg << "Report time: ";
std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
msg << std::put_time(std::gmtime(&currentTime), "%Y.%m.%d %T UTC") << std::endl;
msg << "Content file: "; msg << "Content file: ";
if (!ptr.getCellRef().hasContentFile()) if (!ptr.getCellRef().hasContentFile())
@ -1382,6 +1400,8 @@ namespace MWScript
--arg0; --arg0;
} }
Log(Debug::Warning) << "\n" << msg.str();
runtime.getContext().report(msg.str()); runtime.getContext().report(msg.str());
} }
}; };

View file

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

View file

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

View file

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

View file

@ -266,9 +266,13 @@ namespace
MOCK_CONST_METHOD0(numRecords, std::size_t ()); MOCK_CONST_METHOD0(numRecords, std::size_t ());
MOCK_CONST_METHOD1(getRoot, Nif::Record* (std::size_t)); MOCK_CONST_METHOD1(getRoot, Nif::Record* (std::size_t));
MOCK_CONST_METHOD0(numRoots, 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_METHOD1(setUseSkinning, void (bool));
MOCK_CONST_METHOD0(getUseSkinning, bool ()); MOCK_CONST_METHOD0(getUseSkinning, bool ());
MOCK_CONST_METHOD0(getFilename, std::string ()); 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 struct RecordMock : Nif::Record

View file

@ -291,7 +291,6 @@ add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR})
target_link_libraries(components target_link_libraries(components
${Boost_SYSTEM_LIBRARY} ${Boost_SYSTEM_LIBRARY}
${Boost_FILESYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY}
${Boost_THREAD_LIBRARY}
${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY}
${Boost_IOSTREAMS_LIBRARY} ${Boost_IOSTREAMS_LIBRARY}
${OSG_LIBRARIES} ${OSG_LIBRARIES}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,9 +9,7 @@ namespace Nif
/// Open a NIF stream. The name is used for error messages. /// Open a NIF stream. The name is used for error messages.
NIFFile::NIFFile(Files::IStreamPtr stream, const std::string &name) NIFFile::NIFFile(Files::IStreamPtr stream, const std::string &name)
: ver(0) : filename(name)
, filename(name)
, mUseSkinning(false)
{ {
parse(stream); parse(stream);
} }
@ -139,27 +137,18 @@ void NIFFile::parse(Files::IStreamPtr stream)
// Check the header string // Check the header string
std::string head = nif.getVersionString(); std::string head = nif.getVersionString();
if(head.compare(0, 22, "NetImmerse File Format") != 0) if(head.compare(0, 22, "NetImmerse File Format") != 0)
fail("Invalid NIF header: " + head); fail("Invalid NIF header: " + head);
// Get BCD version // Get BCD version
ver = nif.getUInt(); ver = nif.getUInt();
// 4.0.0.0 is an older, practically identical version of the format. // 4.0.0.0 is an older, practically identical version of the format.
// It's not used by Morrowind assets but Morrowind supports it. // 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)); fail("Unsupported NIF version: " + printVersion(ver));
// Number of records // Number of records
size_t recNum = nif.getInt(); size_t recNum = nif.getInt();
records.resize(recNum); 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++) for(size_t i = 0;i < recNum;i++)
{ {
Record *r = nullptr; Record *r = nullptr;

View file

@ -26,21 +26,27 @@ struct File
virtual size_t numRoots() const = 0; virtual size_t numRoots() const = 0;
virtual std::string getString(size_t index) const = 0;
virtual void setUseSkinning(bool skinning) = 0; virtual void setUseSkinning(bool skinning) = 0;
virtual bool getUseSkinning() const = 0; virtual bool getUseSkinning() const = 0;
virtual std::string getFilename() 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 class NIFFile final : public File
{ {
enum NIFVersion { /// File version, user version, Bethesda version
VER_MW = 0x04000002 // Morrowind NIFs unsigned int ver = 0;
}; unsigned int userVer = 0;
unsigned int bethVer = 0;
/// Nif file version
unsigned int ver;
/// File name, used for error messages and opening the file /// File name, used for error messages and opening the file
std::string filename; std::string filename;
@ -51,7 +57,10 @@ class NIFFile final : public File
/// Root list. This is a select portion of the pointers from records /// Root list. This is a select portion of the pointers from records
std::vector<Record*> roots; std::vector<Record*> roots;
bool mUseSkinning; /// String table
std::vector<std::string> strings;
bool mUseSkinning = false;
/// Parse the file /// Parse the file
void parse(Files::IStreamPtr stream); void parse(Files::IStreamPtr stream);
@ -66,6 +75,34 @@ class NIFFile final : public File
void operator = (NIFFile const &); void operator = (NIFFile const &);
public: 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 /// Used if file parsing fails
void fail(const std::string &msg) const void fail(const std::string &msg) const
{ {
@ -101,6 +138,12 @@ public:
/// Number of roots /// Number of roots
size_t numRoots() const override { return roots.size(); } 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. /// 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. /// @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; void setUseSkinning(bool skinning) override;
@ -109,8 +152,17 @@ public:
/// Get the name of the file /// Get the name of the file
std::string getFilename() const override { return filename; } std::string getFilename() const override { return filename; }
/// Get the version of the NIF format used
unsigned int getVersion() const override { return ver; }
/// Get the user version of the NIF format used
unsigned int getUserVersion() const override { return userVer; }
/// Get the Bethesda version of the NIF format used
unsigned int getBethVersion() const override { return bethVer; }
}; };
typedef std::shared_ptr<const Nif::NIFFile> NIFFilePtr; using NIFFilePtr = std::shared_ptr<const Nif::NIFFile>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -35,17 +35,33 @@ namespace NifOsg
{ {
// interpolation of keyframes // interpolation of keyframes
template <typename MapT, typename InterpolationFunc> template <typename MapT>
class ValueInterpolator class ValueInterpolator
{ {
public: typename MapT::MapType::const_iterator retrieveKey(float time) const
typedef typename MapT::ValueType ValueT;
ValueInterpolator()
: mDefaultVal(ValueT())
{ {
// 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()) ValueInterpolator(std::shared_ptr<const MapT> keys, ValueT defaultVal = ValueT())
: mKeys(keys) : mKeys(keys)
, mDefaultVal(defaultVal) , mDefaultVal(defaultVal)
@ -67,44 +83,21 @@ namespace NifOsg
if(time <= keys.begin()->first) if(time <= keys.begin()->first)
return keys.begin()->second.mValue; return keys.begin()->second.mValue;
// retrieve the current position in the map, optimized for the most common case typename MapT::MapType::const_iterator it = retrieveKey(time);
// 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);
// now do the actual interpolation // now do the actual interpolation
if (it != keys.end()) if (it != keys.end())
{ {
float aTime = it->first;
const typename MapT::KeyType* aKey = &it->second;
// cache for next time // cache for next time
mLastHighKey = it; mLastHighKey = it;
mLastLowKey = --it;
typename MapT::MapType::const_iterator last = --it; float a = (time - mLastLowKey->first) / (mLastHighKey->first - mLastLowKey->first);
mLastLowKey = last;
float aLastTime = last->first;
const typename MapT::KeyType* aLastKey = &last->second;
float a = (time - aLastTime) / (aTime - aLastTime); return interpolate(mLastLowKey->second, mLastHighKey->second, a, mKeys->mInterpolationType);
return InterpolationFunc()(aLastKey->mValue, aKey->mValue, a);
} }
else
return keys.rbegin()->second.mValue; return keys.rbegin()->second.mValue;
} }
bool empty() const bool empty() const
@ -113,36 +106,44 @@ namespace NifOsg
} }
private: 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 mLastLowKey;
mutable typename MapT::MapType::const_iterator mLastHighKey; mutable typename MapT::MapType::const_iterator mLastHighKey;
std::shared_ptr<const MapT> mKeys; std::shared_ptr<const MapT> mKeys;
ValueT mDefaultVal; ValueT mDefaultVal = ValueT();
}; };
struct LerpFunc using QuaternionInterpolator = ValueInterpolator<Nif::QuaternionKeyMap>;
{ using FloatInterpolator = ValueInterpolator<Nif::FloatKeyMap>;
template <typename ValueType> using Vec3Interpolator = ValueInterpolator<Nif::Vector3KeyMap>;
inline ValueType operator()(const ValueType& a, const ValueType& b, float fraction) using Vec4Interpolator = ValueInterpolator<Nif::Vector4KeyMap>;
{
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;
class ControllerFunction : public SceneUtil::ControllerFunction class ControllerFunction : public SceneUtil::ControllerFunction
{ {

View file

@ -184,14 +184,16 @@ namespace NifOsg
{ {
public: public:
/// @param filename used for warning messages. /// @param filename used for warning messages.
LoaderImpl(const std::string& filename) LoaderImpl(const std::string& filename, unsigned int ver, unsigned int userver, unsigned int bethver)
: mFilename(filename), mFirstRootTextureIndex(-1), mFoundFirstRootTexturingProperty(false) : mFilename(filename), mVersion(ver), mUserVersion(userver), mBethVersion(bethver)
{ {
} }
std::string mFilename; std::string mFilename;
size_t mFirstRootTextureIndex; unsigned int mVersion, mUserVersion, mBethVersion;
bool mFoundFirstRootTexturingProperty;
size_t mFirstRootTextureIndex = -1;
bool mFoundFirstRootTexturingProperty = false;
static void loadKf(Nif::NIFFilePtr nif, KeyframeHolder& target) 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) 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); return impl.load(file, imageManager);
} }
void Loader::loadKf(Nif::NIFFilePtr kf, KeyframeHolder& target) 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); impl.loadKf(kf, target);
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -103,13 +103,13 @@ actors processing range
:Range: 3584 to 7168 :Range: 3584 to 7168
:Default: 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. Actor state update includes AI, animations, and physics processing.
Actors near that border start softly fade out instead of just appearing/disapperaing. Actors close to this distance softly fade in and out instead of appearing or disappearing abruptly.
It is not recommended to change this value from default if you use mods with Keep in mind that actors running Travel AI packages are always active to avoid
long-range AiTravel packages (e.g. patrols, caravans and travellers). 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 classic reflected absorb spells behavior
---------------------------------------- ----------------------------------------

View file

@ -213,7 +213,7 @@
<Widget type="AutoSizedButton" skin="MW_Button" position="4 194 137 24" align="Left Bottom" name="ResetControlsButton"> <Widget type="AutoSizedButton" skin="MW_Button" position="4 194 137 24" align="Left Bottom" name="ResetControlsButton">
<Property key="Caption" value="#{sControlsMenu1}"/> <Property key="Caption" value="#{sControlsMenu1}"/>
</Widget> </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"> <Widget type="AutoSizedButton" skin="MW_Button" position="0 0 24 24" align="Left Bottom">
<UserString key="SettingCategory" value="Input"/> <UserString key="SettingCategory" value="Input"/>
<UserString key="SettingName" value="invert x axis"/> <UserString key="SettingName" value="invert x axis"/>
@ -231,10 +231,10 @@
<Property key="Caption" value="#{sMouseFlip}"/> <Property key="Caption" value="#{sMouseFlip}"/>
</Widget> </Widget>
</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"/> <Property key="Caption" value="Camera Sensitivity"/>
</Widget> </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="Range" value="10000"/>
<Property key="Page" value="300"/> <Property key="Page" value="300"/>
<UserString key="SettingType" value="Slider"/> <UserString key="SettingType" value="Slider"/>
@ -244,15 +244,15 @@
<UserString key="SettingMin" value="0.2"/> <UserString key="SettingMin" value="0.2"/>
<UserString key="SettingMax" value="5.0"/> <UserString key="SettingMax" value="5.0"/>
</Widget> </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="Caption" value="#{sLow}"/>
<Property key="TextAlign" value="Left"/> <Property key="TextAlign" value="Left"/>
</Widget> </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="Caption" value="#{sHigh}"/>
<Property key="TextAlign" value="Right"/> <Property key="TextAlign" value="Right"/>
</Widget> </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"> <Widget type="AutoSizedButton" skin="MW_Button" position="0 0 24 24" align="Left Bottom">
<UserString key="SettingCategory" value="Input"/> <UserString key="SettingCategory" value="Input"/>
<UserString key="SettingName" value="enable controller"/> <UserString key="SettingName" value="enable controller"/>
@ -306,9 +306,9 @@
<Property key="Caption" value="Hint: press F3 to show \nthe current frame rate."/> <Property key="Caption" value="Hint: press F3 to show \nthe current frame rate."/>
</Widget> </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>
<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="Range" value="81"/>
<Property key="Page" value="1"/> <Property key="Page" value="1"/>
<UserString key="SettingType" value="Slider"/> <UserString key="SettingType" value="Slider"/>
@ -320,18 +320,18 @@
<UserString key="SettingLabelWidget" value="FovText"/> <UserString key="SettingLabelWidget" value="FovText"/>
<UserString key="SettingLabelCaption" value="Field of View (%s)"/> <UserString key="SettingLabelCaption" value="Field of View (%s)"/>
</Widget> </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="Caption" value="#{sLow}"/>
<Property key="TextAlign" value="Left"/> <Property key="TextAlign" value="Left"/>
</Widget> </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="Caption" value="#{sHigh}"/>
<Property key="TextAlign" value="Right"/> <Property key="TextAlign" value="Right"/>
</Widget> </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}"/> <Property key="Caption" value="#{sGamma_Correction}"/>
</Widget> </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="Range" value="10000"/>
<Property key="Page" value="300"/> <Property key="Page" value="300"/>
<UserString key="SettingType" value="Slider"/> <UserString key="SettingType" value="Slider"/>
@ -341,11 +341,11 @@
<UserString key="SettingMin" value="0.1"/> <UserString key="SettingMin" value="0.1"/>
<UserString key="SettingMax" value="3.0"/> <UserString key="SettingMax" value="3.0"/>
</Widget> </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="Caption" value="#{sDark_Gamma}"/>
<Property key="TextAlign" value="Left"/> <Property key="TextAlign" value="Left"/>
</Widget> </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="Caption" value="#{sLight_Gamma}"/>
<Property key="TextAlign" value="Right"/> <Property key="TextAlign" value="Right"/>
</Widget> </Widget>
@ -373,10 +373,10 @@
<UserString key="SettingLabelCaption" value="Anisotropy (%s)"/> <UserString key="SettingLabelCaption" value="Anisotropy (%s)"/>
</Widget> </Widget>
</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}"/> <Property key="Caption" value="#{sRender_Distance}"/>
</Widget> </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="Range" value="4609"/>
<Property key="Page" value="128"/> <Property key="Page" value="128"/>
<UserString key="SettingType" value="Slider"/> <UserString key="SettingType" value="Slider"/>
@ -388,7 +388,7 @@
<UserString key="SettingLabelWidget" value="RenderDistanceLabel"/> <UserString key="SettingLabelWidget" value="RenderDistanceLabel"/>
<UserString key="SettingLabelCaption" value="#{sRender_Distance} (%s)"/> <UserString key="SettingLabelCaption" value="#{sRender_Distance} (%s)"/>
</Widget> </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="Range" value="79873"/>
<Property key="Page" value="2048"/> <Property key="Page" value="2048"/>
<UserString key="SettingType" value="Slider"/> <UserString key="SettingType" value="Slider"/>
@ -400,11 +400,11 @@
<UserString key="SettingLabelWidget" value="RenderDistanceLabel"/> <UserString key="SettingLabelWidget" value="RenderDistanceLabel"/>
<UserString key="SettingLabelCaption" value="#{sRender_Distance} (x%s)"/> <UserString key="SettingLabelCaption" value="#{sRender_Distance} (x%s)"/>
</Widget> </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="Caption" value="#{sNear}"/>
<Property key="TextAlign" value="Left"/> <Property key="TextAlign" value="Left"/>
</Widget> </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="Caption" value="#{sFar}"/>
<Property key="TextAlign" value="Right"/> <Property key="TextAlign" value="Right"/>
</Widget> </Widget>