1
0
Fork 1
mirror of https://github.com/TES3MP/openmw-tes3mp.git synced 2025-07-12 17:51:42 +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 #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,6 +189,11 @@
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
Feature #1774: Handle AvoidNode
Feature #2229: Improve pathfinding AI
Feature #3025: Analogue gamepad movement controls
@ -196,6 +205,8 @@
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1697,6 +1697,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";
@ -1875,6 +1877,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);
@ -1908,6 +1911,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);
@ -1948,7 +1952,7 @@ namespace MWInput
}
// Disallow binding reserved keys
if (key == SDL_SCANCODE_F3 || key == SDL_SCANCODE_F4 || key == SDL_SCANCODE_F10 || key == SDL_SCANCODE_F11)
if (key == SDL_SCANCODE_F3 || key == SDL_SCANCODE_F4 || key == SDL_SCANCODE_F10)
return;
#ifndef __APPLE__

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -464,9 +464,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
@ -569,6 +569,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)
{
@ -641,21 +650,19 @@ namespace MWMechanics
// magnitude, since we're transferring stats from the target to the caster
if (!caster.isEmpty() && caster != target && caster.getClass().isActor())
{
for (int i=0; i<5; ++i)
if (effectIt->mEffectID >= ESM::MagicEffect::AbsorbAttribute &&
effectIt->mEffectID <= ESM::MagicEffect::AbsorbSkill)
{
if (effectIt->mEffectID == ESM::MagicEffect::AbsorbAttribute+i)
{
std::vector<ActiveSpells::ActiveEffect> absorbEffects;
ActiveSpells::ActiveEffect effect_ = effect;
effect_.mMagnitude *= -1;
absorbEffects.push_back(effect_);
if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game"))
target.getClass().getCreatureStats(target).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, caster.getClass().getCreatureStats(caster).getActorId());
else
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId());
}
std::vector<ActiveSpells::ActiveEffect> absorbEffects;
ActiveSpells::ActiveEffect effect_ = effect;
effect_.mMagnitude *= -1;
absorbEffects.push_back(effect_);
if (reflected && Settings::Manager::getBool("classic reflected absorb spells behavior", "Game"))
target.getClass().getCreatureStats(target).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, caster.getClass().getCreatureStats(caster).getActorId());
else
caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell("", true,
absorbEffects, mSourceName, target.getClass().getCreatureStats(target).getActorId());
}
}
}

View file

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

View file

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

View file

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

View file

@ -22,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>
@ -47,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"
@ -767,7 +767,7 @@ namespace MWRender
int screenshotMapping = 0;
std::vector<std::string> settingArgs;
boost::algorithm::split(settingArgs,settingStr,boost::is_any_of(" "));
Misc::StringUtils::split(settingStr, settingArgs);
if (settingArgs.size() > 0)
{

View file

@ -1,6 +1,7 @@
#include "miscextensions.hpp"
#include <cstdlib>
#include <iomanip>
/*
Start of tes3mp addition
@ -19,6 +20,8 @@
#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>
@ -1225,18 +1228,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;
}
@ -1244,7 +1238,6 @@ namespace MWScript
{
MWMechanics::AiCast castPackage(targetId, spellId, true);
ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr);
return;
}
@ -1268,9 +1261,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;
@ -1341,6 +1354,11 @@ namespace MWScript
std::stringstream msg;
msg << "Report time: ";
std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
msg << std::put_time(std::gmtime(&currentTime), "%Y.%m.%d %T UTC") << std::endl;
msg << "Content file: ";
if (!ptr.getCellRef().hasContentFile())
@ -1382,6 +1400,8 @@ namespace MWScript
--arg0;
}
Log(Debug::Warning) << "\n" << msg.str();
runtime.getContext().report(msg.str());
}
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -103,13 +103,13 @@ actors processing range
:Range: 3584 to 7168
:Default: 7168
This setting allows to specify a distance from player in game units, in which OpenMW updates actor's state.
This setting specifies the actor state update distance from the player in game units.
Actor state update includes AI, animations, and physics processing.
Actors near that border start softly fade out instead of just appearing/disapperaing.
It is not recommended to change this value from default if you use mods with
long-range AiTravel packages (e.g. patrols, caravans and travellers).
Actors close to this distance softly fade in and out instead of appearing or disappearing abruptly.
Keep in mind that actors running Travel AI packages are always active to avoid
issues in mods with long-range AiTravel packages (for example, patrols, caravans and travellers).
This setting can be controlled in game with the "Actors processing range slider" in the Prefs panel of the Options menu.
This setting can be controlled in game with the "Actors Processing Range" slider in the Prefs panel of the Options menu.
classic reflected absorb spells behavior
----------------------------------------

View file

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