Merge pull request #113 from OpenMW/master

Add OpenMW commits up to 17 Dec
pull/133/head
David Cernat 8 years ago committed by GitHub
commit 973db7c78a

@ -1,6 +1,6 @@
os:
- linux
- osx
# - osx
osx_image: xcode7.2
language: cpp
sudo: required

@ -23,6 +23,7 @@ Programmers
Alexander Olofsson (Ace)
Allofich
AnyOldName3
Aussiemon
Austin Salgat (Salgat)
Artem Kotsynyak (greye)
artemutin
@ -74,8 +75,10 @@ Programmers
Karl-Felix Glatzer (k1ll)
Kevin Poitra (PuppyKevin)
Koncord
Kurnevsky Evgeny (kurnevsky)
Lars Söderberg (Lazaroth)
lazydev
Leon Krieg (lkrieg)
Leon Saunders (emoose)
lohikaarme
Lukasz Gromanowski (lgro)

@ -13,7 +13,7 @@ namespace ESM
namespace ESSImport
{
/// Local variable assigments for a running script
/// Local variable assignments for a running script
struct SCRI
{
std::string mScript;

@ -29,7 +29,7 @@ namespace CSMPrefs
enum SecondaryMode
{
SM_Replace, ///< The secondary signal replaces the regular signal when the modifier is active
SM_Detach, ///< The secondary signal is emitted independant of the regular signal, even if not active
SM_Detach, ///< The secondary signal is emitted independent of the regular signal, even if not active
SM_Ignore ///< The secondary signal will not ever be emitted
};

@ -425,7 +425,7 @@ void CSMTools::ReferenceableCheckStage::creatureCheck (
//stats checks
if (creature.mData.mLevel < 1)
messages.push_back (std::make_pair (id, creature.mId + " has non-postive level"));
messages.push_back (std::make_pair (id, creature.mId + " has non-positive level"));
if (creature.mData.mStrength < 0)
messages.push_back (std::make_pair (id, creature.mId + " has negative strength"));
@ -659,7 +659,7 @@ void CSMTools::ReferenceableCheckStage::npcCheck (
{
if ((npc.mFlags & ESM::NPC::Autocalc) == 0) //0x0010 = autocalculated flag
{
messages.push_back (std::make_pair (id, npc.mId + " mNpdtType or flags mismatch!")); //should not happend?
messages.push_back (std::make_pair (id, npc.mId + " mNpdtType or flags mismatch!")); //should not happen?
return;
}
@ -915,7 +915,7 @@ void CSMTools::ReferenceableCheckStage::inventoryListCheck(
id + " contains non-existing item (" + itemName + ")"));
else
{
// Needs to accomodate Containers, Creatures, and NPCs
// Needs to accommodate containers, creatures, and NPCs
switch (localIndex.second)
{
case CSMWorld::UniversalId::Type_Potion:

@ -330,6 +330,8 @@ namespace CSMWorld
{ ColumnId_WeatherName, "Type" },
{ ColumnId_WeatherChance, "Percent Chance" },
{ ColumnId_Text, "Text" },
{ ColumnId_UseValue1, "Use value 1" },
{ ColumnId_UseValue2, "Use value 2" },
{ ColumnId_UseValue3, "Use value 3" },

@ -329,6 +329,8 @@ namespace CSMWorld
ColumnId_WeatherName = 295,
ColumnId_WeatherChance = 296,
ColumnId_Text = 297,
// Allocated to a separate value range, so we don't get a collision should we ever need
// to extend the number of use values.
ColumnId_UseValue1 = 0x10000,

@ -65,7 +65,7 @@ namespace CSMWorld
void setRecord (const std::string& id, const RecordBase& record,
UniversalId::Type type = UniversalId::Type_None);
///< Add record or overwrite existing recrod.
///< Add record or overwrite existing record.
const RecordBase& getRecord (const std::string& id) const;

@ -198,12 +198,12 @@ QModelIndex CSMWorld::IdTree::parent (const QModelIndex& index) const
return QModelIndex();
unsigned int id = index.internalId();
const std::pair<int, int>& adress(unfoldIndexAddress(id));
const std::pair<int, int>& address(unfoldIndexAddress(id));
if (adress.first >= this->rowCount() || adress.second >= this->columnCount())
if (address.first >= this->rowCount() || address.second >= this->columnCount())
throw "Parent index is not present in the model";
return createIndex(adress.first, adress.second);
return createIndex(address.first, address.second);
}
unsigned int CSMWorld::IdTree::foldIndexAddress (const QModelIndex& index) const

@ -8,9 +8,9 @@
/*! \brief
* Class for holding the model. Uses typical qt table abstraction/interface for granting access
* to the individiual fields of the records, Some records are holding nested data (for instance
* inventory list of the npc). In casses like this, table model offers interface to access
* inventory list of the npc). In cases like this, table model offers interface to access
* nested data in the qt way - that is specify parent. Since some of those nested data require
* multiple columns to represent informations, single int (default way to index model in the
* multiple columns to represent information, single int (default way to index model in the
* qmodelindex) is not sufficiant. Therefore tablemodelindex class can hold two ints for the
* sake of indexing two dimensions of the table. This model does not support multiple levels of
* the nested data. Vast majority of methods makes sense only for the top level data.

@ -8,7 +8,7 @@
* Adapters acts as indirection layer, abstracting details of the record types (in the wrappers) from the higher levels of model.
* Please notice that nested adaptor uses helper classes for actually performing any actions. Different record types require different helpers (needs to be created in the subclass and then fetched via member function).
*
* Important point: don't forget to make sure that getData on the nestedColumn returns true (otherwise code will not treat the index pointing to the column as having childs!
* Important point: don't forget to make sure that getData on the nestedColumn returns true (otherwise code will not treat the index pointing to the column as having children!
*/
class QVariant;

@ -301,9 +301,9 @@ void CSMWorld::ArmorRefIdAdapter::setData (const RefIdColumn *column, RefIdData&
}
CSMWorld::BookRefIdAdapter::BookRefIdAdapter (const EnchantableColumns& columns,
const RefIdColumn *scroll, const RefIdColumn *skill)
const RefIdColumn *scroll, const RefIdColumn *skill, const RefIdColumn *text)
: EnchantableRefIdAdapter<ESM::Book> (UniversalId::Type_Book, columns),
mScroll (scroll), mSkill (skill)
mScroll (scroll), mSkill (skill), mText (text)
{}
QVariant CSMWorld::BookRefIdAdapter::getData (const RefIdColumn *column,
@ -318,6 +318,9 @@ QVariant CSMWorld::BookRefIdAdapter::getData (const RefIdColumn *column,
if (column==mSkill)
return record.get().mData.mSkillID;
if (column==mText)
return QString::fromUtf8 (record.get().mText.c_str());
return EnchantableRefIdAdapter<ESM::Book>::getData (column, data, index);
}
@ -333,6 +336,8 @@ void CSMWorld::BookRefIdAdapter::setData (const RefIdColumn *column, RefIdData&
book.mData.mIsScroll = value.toInt();
else if (column==mSkill)
book.mData.mSkillID = value.toInt();
else if (column==mText)
book.mText = value.toString().toUtf8().data();
else
{
EnchantableRefIdAdapter<ESM::Book>::setData (column, data, index, value);

@ -696,11 +696,12 @@ namespace CSMWorld
{
const RefIdColumn *mScroll;
const RefIdColumn *mSkill;
const RefIdColumn *mText;
public:
BookRefIdAdapter (const EnchantableColumns& columns, const RefIdColumn *scroll,
const RefIdColumn *skill);
const RefIdColumn *skill, const RefIdColumn *text);
virtual QVariant getData (const RefIdColumn *column, const RefIdData& data, int index)
const;

@ -297,6 +297,9 @@ CSMWorld::RefIdCollection::RefIdCollection()
mColumns.push_back (RefIdColumn (Columns::ColumnId_Attribute, ColumnBase::Display_Attribute));
const RefIdColumn *attribute = &mColumns.back();
mColumns.push_back (RefIdColumn (Columns::ColumnId_Text, ColumnBase::Display_LongString));
const RefIdColumn *text = &mColumns.back();
mColumns.push_back (RefIdColumn (Columns::ColumnId_ClothingType, ColumnBase::Display_ClothingType));
const RefIdColumn *clothingType = &mColumns.back();
@ -656,7 +659,7 @@ CSMWorld::RefIdCollection::RefIdCollection()
mAdapters.insert (std::make_pair (UniversalId::Type_Armor,
new ArmorRefIdAdapter (enchantableColumns, armorType, health, armor, partRef)));
mAdapters.insert (std::make_pair (UniversalId::Type_Book,
new BookRefIdAdapter (enchantableColumns, scroll, attribute)));
new BookRefIdAdapter (enchantableColumns, scroll, attribute, text)));
mAdapters.insert (std::make_pair (UniversalId::Type_Clothing,
new ClothingRefIdAdapter (enchantableColumns, clothingType, partRef)));
mAdapters.insert (std::make_pair (UniversalId::Type_Container,

@ -50,7 +50,7 @@ namespace CSVRender
updateCellData(mData.getCells().getRecord(cellIndex));
}
// Keep water existance/height up to date
// Keep water existence/height up to date
QAbstractItemModel* cells = mData.getTableModel(CSMWorld::UniversalId::Type_Cells);
connect(cells, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
this, SLOT(cellDataChanged(const QModelIndex&, const QModelIndex&)));

@ -516,7 +516,7 @@ void CSVRender::PagedWorldspaceWidget::useViewHint (const std::string& hint)
// Current coordinate
int x, y;
// Loop throught all the coordinates to add them to selection
// Loop through all the coordinates to add them to selection
while (stream >> ignore1 >> ignore2 >> x >> y)
selection.add (CSMWorld::CellCoordinates (x, y));

@ -214,7 +214,7 @@ SceneWidget::SceneWidget(boost::shared_ptr<Resource::ResourceSystem> resourceSys
SceneWidget::~SceneWidget()
{
// Since we're holding on to the scene templates past the existance of this graphics context, we'll need to manually release the created objects
// Since we're holding on to the scene templates past the existence of this graphics context, we'll need to manually release the created objects
mResourceSystem->getSceneManager()->releaseGLObjects(mView->getCamera()->getGraphicsContext()->getState());
}

@ -45,8 +45,8 @@ namespace CSVWorld
///< \note Non-existent cells are not listed.
QModelIndexList getSelectedCells (bool existent = true, bool nonExistent = false) const;
///< \param existant Include existant cells.
/// \param nonExistant Include non-existant cells.
///< \param existent Include existent cells.
/// \param nonExistent Include non-existent cells.
QModelIndexList getMissingRegionCells() const;
///< Unselected cells within all regions that have at least one selected cell.

@ -41,4 +41,4 @@
/// \namespace MWScript
/// \ingroup openmw
/// \brief MW-specific script extentions and integration of the script system into OpenMW
/// \brief MW-specific script extensions and integration of the script system into OpenMW

@ -514,7 +514,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
mEnvironment.setWindowManager (window);
// Create sound system
mEnvironment.setSoundManager (new MWSound::SoundManager(mVFS.get(), mUseSound));
mEnvironment.setSoundManager (new MWSound::SoundManager(mVFS.get(), mFallbackMap, mUseSound));
if (!mSkipMenu)
{

@ -233,7 +233,7 @@ namespace MWBase
virtual void removeDialog(MWGui::Layout* dialog) = 0;
///Gracefully attempts to exit the topmost GUI mode
/** No guarentee of actually closing the window **/
/** No guarantee of actually closing the window **/
virtual void exitCurrentGuiMode() = 0;
virtual void messageBox (const std::string& message, enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) = 0;

@ -231,7 +231,7 @@ namespace MWClass
if(lockLevel!=0)
ptr.getCellRef().setLockLevel(abs(lockLevel)); //Changes lock to locklevel, in positive
else
ptr.getCellRef().setLockLevel(abs(ptr.getCellRef().getLockLevel())); //No locklevel given, just flip the origional one
ptr.getCellRef().setLockLevel(abs(ptr.getCellRef().getLockLevel())); //No locklevel given, just flip the original one
}
void Door::unlock (const MWWorld::Ptr& ptr) const

@ -122,7 +122,7 @@ namespace MWClass
info.effects = MWGui::Widgets::MWEffectList::effectListFromESM(&ref->mBase->mEffects);
// hide effects the player doesnt know about
// hide effects the player doesn't know about
MWWorld::Ptr player = MWBase::Environment::get().getWorld ()->getPlayerPtr();
for (unsigned int i=0; i<info.effects.size(); ++i)
info.effects[i].mKnown = MWMechanics::Alchemy::knownEffect(i, player);

@ -174,7 +174,7 @@ namespace MWDialogue
executeScript (info->mResultScript);
mLastTopic = Misc::StringUtils::lowerCase(it->mId);
// update topics again to accomodate changes resulting from executeScript
// update topics again to accommodate changes resulting from executeScript
updateTopics();
return;

@ -36,7 +36,6 @@ void DragAndDrop::startDrag (int index, SortFilterItemModel* sortModel, ItemMode
mSourceModel = sourceModel;
mSourceView = sourceView;
mSourceSortModel = sortModel;
mIsOnDragAndDrop = true;
// If picking up an item that isn't from the player's inventory, the item gets added to player inventory backend
// immediately, even though it's still floating beneath the mouse cursor. A bit counterintuitive,
@ -88,6 +87,8 @@ void DragAndDrop::startDrag (int index, SortFilterItemModel* sortModel, ItemMode
sourceView->update();
MWBase::Environment::get().getWindowManager()->setDragDrop(true);
mIsOnDragAndDrop = true;
}
void DragAndDrop::drop(ItemModel *targetModel, ItemView *targetView)

@ -101,6 +101,8 @@ namespace MWGui
{
if (mFrame)
mFrame->setImageTexture("");
if (mItemShadow)
mItemShadow->setImageTexture("");
mItem->setImageTexture("");
mText->setCaption("");
return;

@ -517,8 +517,8 @@ namespace MWMechanics
if (magnitude > 0 && remainingTime > 0 && remainingTime < mDuration)
{
CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor);
effectTick(creatureStats, mActor, key, magnitude * remainingTime);
creatureStats.getMagicEffects().add(key, -magnitude);
if (effectTick(creatureStats, mActor, key, magnitude * remainingTime))
creatureStats.getMagicEffects().add(key, -magnitude);
}
}
};
@ -532,8 +532,10 @@ namespace MWMechanics
if (duration > 0)
{
// apply correct magnitude for tickable effects that have just expired,
// in case duration > remaining time of effect
// Apply correct magnitude for tickable effects that have just expired,
// in case duration > remaining time of effect.
// One case where this will happen is when the player uses the rest/wait command
// while there is a tickable effect active that should expire before the end of the rest/wait.
ExpiryVisitor visitor(ptr, duration);
creatureStats.getActiveSpells().visitEffectSources(visitor);
@ -1289,8 +1291,6 @@ namespace MWMechanics
stats.getActiveSpells().clear();
calculateCreatureStatModifiers(iter->first, 0);
MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false);
if (cls.isEssential(iter->first))
MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}");
}
@ -1308,6 +1308,11 @@ namespace MWMechanics
//player's death animation is over
MWBase::Environment::get().getStateManager()->askLoadRecent();
}
else
{
// NPC death animation is over, disable actor collision
MWBase::Environment::get().getWorld()->enableActorCollision(iter->first, false);
}
// Play Death Music if it was the player dying
if(iter->first == getPlayer())

@ -60,30 +60,30 @@ namespace MWMechanics
FleeState_RunToDestination
};
FleeState mFleeState;
bool mFleeLOS;
float mFleeUpdateLOSTimer;
bool mLOS;
float mUpdateLOSTimer;
float mFleeBlindRunTimer;
ESM::Pathgrid::Point mFleeDest;
AiCombatStorage():
mAttackCooldown(0),
mAttackCooldown(0.0f),
mTimerReact(AI_REACTION_TIME),
mTimerCombatMove(0),
mTimerCombatMove(0.0f),
mReadyToAttack(false),
mAttack(false),
mAttackRange(0),
mAttackRange(0.0f),
mCombatMove(false),
mLastTargetPos(0,0,0),
mCell(NULL),
mCurrentAction(),
mActionCooldown(0),
mActionCooldown(0.0f),
mStrength(),
mForceNoShortcut(false),
mShortcutFailPos(),
mMovement(),
mFleeState(FleeState_None),
mFleeLOS(false),
mFleeUpdateLOSTimer(0.0f),
mLOS(false),
mUpdateLOSTimer(0.0f),
mFleeBlindRunTimer(0.0f)
{}
@ -157,7 +157,7 @@ namespace MWMechanics
*
* TODO:
*
* Use the Observer Pattern to co-ordinate attacks, provide intelligence on
* Use the observer pattern to coordinate attacks, provide intelligence on
* whether the target was hit, etc.
*/
@ -181,10 +181,14 @@ namespace MWMechanics
if (!storage.isFleeing())
{
if (storage.mCurrentAction.get()) // need to wait to init action with it's attack range
if (storage.mCurrentAction.get()) // need to wait to init action with its attack range
{
//Update every frame
bool is_target_reached = pathTo(actor, target.getRefData().getPosition().pos, duration, storage.mAttackRange);
//Update every frame. UpdateLOS uses a timer, so the LOS check does not happen every frame.
updateLOS(actor, target, duration, storage);
float targetReachedTolerance = 0.0f;
if (storage.mLOS)
targetReachedTolerance = storage.mAttackRange;
bool is_target_reached = pathTo(actor, target.getRefData().getPosition().pos, duration, targetReachedTolerance);
if (is_target_reached) storage.mReadyToAttack = true;
}
@ -283,7 +287,7 @@ namespace MWMechanics
osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target);
float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target);
storage.mReadyToAttack = (currentAction->isAttackingOrSpell() && distToTarget <= rangeAttack);
storage.mReadyToAttack = (currentAction->isAttackingOrSpell() && distToTarget <= rangeAttack && storage.mLOS);
if (storage.mReadyToAttack)
{
@ -309,18 +313,23 @@ namespace MWMechanics
}
}
void MWMechanics::AiCombat::updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage)
void MWMechanics::AiCombat::updateLOS(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage)
{
static const float LOS_UPDATE_DURATION = 0.5f;
static const float BLIND_RUN_DURATION = 1.0f;
if (storage.mFleeUpdateLOSTimer <= 0.f)
if (storage.mUpdateLOSTimer <= 0.f)
{
storage.mFleeLOS = MWBase::Environment::get().getWorld()->getLOS(actor, target);
storage.mFleeUpdateLOSTimer = LOS_UPDATE_DURATION;
storage.mLOS = MWBase::Environment::get().getWorld()->getLOS(actor, target);
storage.mUpdateLOSTimer = LOS_UPDATE_DURATION;
}
else
storage.mFleeUpdateLOSTimer -= duration;
storage.mUpdateLOSTimer -= duration;
}
void MWMechanics::AiCombat::updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, MWMechanics::AiCombatStorage& storage)
{
static const float BLIND_RUN_DURATION = 1.0f;
updateLOS(actor, target, duration, storage);
AiCombatStorage::FleeState& state = storage.mFleeState;
switch (state)
@ -332,7 +341,7 @@ namespace MWMechanics
{
float triggerDist = getMaxAttackDistance(target);
if (storage.mFleeLOS &&
if (storage.mLOS &&
(triggerDist >= 1000 || getDistanceMinusHalfExtents(actor, target) <= triggerDist))
{
const ESM::Pathgrid* pathgrid =
@ -399,7 +408,7 @@ namespace MWMechanics
static const float fFleeDistance = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fFleeDistance")->getFloat();
float dist = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length();
if ((dist > fFleeDistance && !storage.mFleeLOS)
if ((dist > fFleeDistance && !storage.mLOS)
|| pathTo(actor, storage.mFleeDest, duration))
{
state = AiCombatStorage::FleeState_Idle;
@ -602,9 +611,6 @@ namespace MWMechanics
mMovement.mPosition[2] = 0;
mFleeState = FleeState_None;
mFleeDest = ESM::Pathgrid::Point(0, 0, 0);
mFleeLOS = false;
mFleeUpdateLOSTimer = 0.0f;
mFleeUpdateLOSTimer = 0.0f;
}
bool AiCombatStorage::isFleeing()

@ -61,6 +61,8 @@ namespace MWMechanics
void attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController);
void updateLOS(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, AiCombatStorage& storage);
void updateFleeing(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float duration, AiCombatStorage& storage);
/// Transfer desired movement (from AiCombatStorage) to Actor

@ -152,7 +152,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
if (dist > 450)
actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run
else if (dist < 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshhold
else if (dist < 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshold
actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk
}

@ -32,7 +32,7 @@ namespace MWMechanics
class AiPackage
{
public:
///Enumerates the various AITypes availible.
///Enumerates the various AITypes available
enum TypeId {
TypeIdNone = -1,
TypeIdWander = 0,

@ -336,7 +336,7 @@ namespace MWMechanics
{
ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mReturnPosition));
// actor position is already in world co-ordinates
// actor position is already in world coordinates
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos));
// don't take shortcuts for wandering
@ -473,8 +473,8 @@ namespace MWMechanics
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor,
float duration, AiWanderStorage& storage, ESM::Position& pos)
{
// Are we there yet?
if (pathTo(actor, mPathFinder.getPath().back(), duration, DESTINATION_TOLERANCE))
// Is there no destination or are we there yet?
if ((!mPathFinder.isPathConstructed()) || pathTo(actor, mPathFinder.getPath().back(), duration, DESTINATION_TOLERANCE))
{
stopWalking(actor, storage);
storage.setState(Wander_ChooseAction);
@ -649,7 +649,7 @@ namespace MWMechanics
ESM::Pathgrid::Point dest(storage.mAllowedNodes[randNode]);
ToWorldCoordinates(dest, storage.mCell->getCell());
// actor position is already in world co-ordinates
// actor position is already in world coordinates
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos));
// don't take shortcuts for wandering
@ -693,8 +693,8 @@ namespace MWMechanics
ESM::Pathgrid::Point pt = paths.back();
for(unsigned int j = 0; j < nodes.size(); j++)
{
// FIXME: doesn't hadle a door with the same X/Y
// co-ordinates but with a different Z
// FIXME: doesn't handle a door with the same X/Y
// coordinates but with a different Z
if(nodes[j].mX == pt.mX && nodes[j].mY == pt.mY)
{
nodes.erase(nodes.begin() + j);
@ -828,7 +828,7 @@ namespace MWMechanics
// ... pathgrids don't usually include water, so swimmers ignore them
if (mDistance && storage.mCanWanderAlongPathGrid && !actor.getClass().isPureWaterCreature(actor))
{
// get NPC's position in local (i.e. cell) co-ordinates
// get NPC's position in local (i.e. cell) coordinates
osg::Vec3f npcPos(mInitialActorPosition);
CoordinateConverter(cell).toLocal(npcPos);
@ -837,7 +837,7 @@ namespace MWMechanics
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance
// and if the point is connected to the closest current point
// NOTE: mPoints and mAllowedNodes are in local co-ordinates
// NOTE: mPoints and mAllowedNodes are in local coordinates
int pointIndex = 0;
for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++)
{

@ -119,7 +119,7 @@ namespace MWMechanics
GroupIndex_MaxIdle = 9
};
/// convert point from local (i.e. cell) to world co-ordinates
/// convert point from local (i.e. cell) to world coordinates
void ToWorldCoordinates(ESM::Pathgrid::Point& point, const ESM::Cell * cell);
void SetCurrentNodeToClosestAllowedNode(osg::Vec3f npcPos, AiWanderStorage& storage);

@ -1052,6 +1052,9 @@ bool CharacterController::updateCreatureState()
mUpperBodyState = UpperCharState_StartToMinAttack;
mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability());
if (weapType == WeapType_HandToHand)
playSwishSound(0.0f);
}
}
@ -1380,13 +1383,7 @@ bool CharacterController::updateWeaponState()
}
else
{
std::string sound = "SwishM";
if(attackStrength < 0.5f)
sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack
else if(attackStrength < 1.0f)
sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack
else
sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack
playSwishSound(attackStrength);
}
}
mAttackStrength = attackStrength;
@ -2281,6 +2278,19 @@ void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr &target)
mHeadTrackTarget = target;
}
void CharacterController::playSwishSound(float attackStrength)
{
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
std::string sound = "Weapon Swish";
if(attackStrength < 0.5f)
sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack
else if(attackStrength < 1.0f)
sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack
else
sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack
}
void CharacterController::updateHeadTracking(float duration)
{
const osg::Node* head = mAnimation->getNode("Bip01 Head");

@ -281,6 +281,8 @@ public:
/// Make this character turn its head towards \a target. To turn off head tracking, pass an empty Ptr.
void setHeadTrackTarget(const MWWorld::ConstPtr& target);
void playSwishSound(float attackStrength);
};
MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype);

@ -236,7 +236,7 @@ namespace MWMechanics
: mWatchedTimeToStartDrowning(0), mWatchedStatsEmpty (true), mUpdatePlayer (true), mClassSelected (false),
mRaceSelected (false), mAI(true)
{
//buildPlayer no longer here, needs to be done explicitely after all subsystems are up and running
//buildPlayer no longer here, needs to be done explicitly after all subsystems are up and running
}
void MechanicsManager::add(const MWWorld::Ptr& ptr)

@ -19,7 +19,7 @@ namespace MWMechanics
bool proximityToDoor(const MWWorld::Ptr& actor,
float minSqr = MIN_DIST_TO_DOOR_SQUARED);
/// Returns door pointer within range. No guarentee is given as too which one
/// Returns door pointer within range. No guarantee is given as to which one
/** \return Pointer to the door, or NULL if none exists **/
MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor,
float minSqr = MIN_DIST_TO_DOOR_SQUARED);

@ -139,7 +139,7 @@ namespace MWMechanics
* NOTE: It may be desirable to simply go directly to the endPoint if for
* example there are no pathgrids in this cell.
*
* NOTE: startPoint & endPoint are in world co-ordinates
* NOTE: startPoint & endPoint are in world coordinates
*
* Updates mPath using aStarSearch() or ray test (if shortcut allowed).
* mPath consists of pathgrid points, except the last element which is
@ -148,7 +148,7 @@ namespace MWMechanics
* pathgrid point (e.g. wander) then it may be worth while to call
* pop_back() to remove the redundant entry.
*
* NOTE: co-ordinates must be converted prior to calling GetClosestPoint()
* NOTE: coordinates must be converted prior to calling GetClosestPoint()
*
* |
* | cell
@ -164,8 +164,8 @@ namespace MWMechanics
* +-----------------------------
*
* i = x value of cell itself (multiply by ESM::Land::REAL_SIZE to convert)
* j = @.x in local co-ordinates (i.e. within the cell)
* k = @.x in world co-ordinates
* j = @.x in local coordinates (i.e. within the cell)
* k = @.x in world coordinates
*/
void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint,
@ -188,7 +188,7 @@ namespace MWMechanics
return;
}
// NOTE: GetClosestPoint expects local co-ordinates
// NOTE: GetClosestPoint expects local coordinates
CoordinateConverter converter(mCell->getCell());
// NOTE: It is possible that GetClosestPoint returns a pathgrind point index
@ -230,7 +230,7 @@ namespace MWMechanics
{
mPath = mCell->aStarSearch(startNode, endNode.first);
// convert supplied path to world co-ordinates
// convert supplied path to world coordinates
for (std::list<ESM::Pathgrid::Point>::iterator iter(mPath.begin()); iter != mPath.end(); ++iter)
{
converter.toWorld(*iter);

@ -84,7 +84,7 @@ namespace MWMechanics
/** Synchronize new path with old one to avoid visiting 1 waypoint 2 times
@note
BuildPath() takes closest PathGrid point to NPC as first point of path.
This is undesireable if NPC has just passed a Pathgrid point, as this
This is undesirable if NPC has just passed a Pathgrid point, as this
makes the 2nd point of the new path == the 1st point of old path.
Which results in NPC "running in a circle" back to the just passed waypoint.
*/
@ -122,11 +122,11 @@ namespace MWMechanics
return (MWMechanics::PathFinder::MakeOsgVec3(point) - pos).length2();
}
// Return the closest pathgrid point index from the specified position co
// -ordinates. NOTE: Does not check if there is a sensible way to get there
// Return the closest pathgrid point index from the specified position
// coordinates. NOTE: Does not check if there is a sensible way to get there
// (e.g. a cliff in front).
//
// NOTE: pos is expected to be in local co-ordinates, as is grid->mPoints
// NOTE: pos is expected to be in local coordinates, as is grid->mPoints
//
static int GetClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos)
{

@ -225,7 +225,7 @@ namespace MWMechanics
* Should be possible to make this MT safe.
*
* Returns path which may be empty. path contains pathgrid points in local
* cell co-ordinates (indoors) or world co-ordinates (external).
* cell coordinates (indoors) or world coordinates (external).
*
* Input params:
* start, goal - pathgrid point indexes (for this cell)
@ -239,7 +239,7 @@ namespace MWMechanics
* TODO: An intersting exercise might be to cache the paths created for a
* start/goal pair. To cache the results the paths need to be in
* pathgrid points form (currently they are converted to world
* co-ordinates). Essentially trading speed w/ memory.
* coordinates). Essentially trading speed w/ memory.
*/
std::list<ESM::Pathgrid::Point> PathgridGraph::aStarSearch(const int start,
const int goal) const
@ -312,7 +312,7 @@ namespace MWMechanics
if(current != goal)
return path; // for some reason couldn't build a path
// reconstruct path to return, using local co-ordinates
// reconstruct path to return, using local coordinates
while(graphParent[current] != -1)
{
path.push_front(mPathgrid->mPoints[current]);

@ -30,7 +30,7 @@ namespace MWMechanics
// the input parameters are pathgrid point indexes
// the output list is in local (internal cells) or world (external
// cells) co-ordinates
// cells) coordinates
//
// NOTE: if start equals end an empty path is returned
std::list<ESM::Pathgrid::Point> aStarSearch(const int start,

@ -495,7 +495,7 @@ namespace MWMechanics
appliedLastingEffects.push_back(effect);
// For absorb effects, also apply the effect to the caster - but with a negative
// magnitude, since we're transfering stats from the target to the caster
// magnitude, since we're transferring stats from the target to the caster
if (!caster.isEmpty() && caster.getClass().isActor())
{
for (int i=0; i<5; ++i)
@ -927,7 +927,7 @@ namespace MWMechanics
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster);
if (mCaster.getClass().isActor()) // TODO: Non-actors should also create a spell cast vfx
if (animation && mCaster.getClass().isActor()) // TODO: Non-actors should also create a spell cast vfx even if they are disabled (animation == NULL)
{
const ESM::Static* castStatic;
@ -941,7 +941,7 @@ namespace MWMechanics
animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", texture);
}
if (!mCaster.getClass().isActor())
if (animation && !mCaster.getClass().isActor())
animation->addSpellCastGlow(effect);
static const std::string schools[] = {
@ -994,8 +994,10 @@ namespace MWMechanics
if (charge == 0)
return false;
// FIXME: charge should be a float, not int so that damage < 1 per frame can be applied.
// This was also a bug in the original engine.
// Store remainder of disintegrate amount (automatically subtracted if > 1)
item->getCellRef().applyChargeRemainderToBeSubtracted(disintegrate - std::floor(disintegrate));
charge = item->getClass().getItemHealth(*item);
charge -=
std::min(static_cast<int>(disintegrate),
charge);
@ -1023,10 +1025,10 @@ namespace MWMechanics
creatureStats.setDynamic(index, stat);
}
void effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude)
bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const EffectKey &effectKey, float magnitude)
{
if (magnitude == 0.f)
return;
return false;
bool receivedMagicDamage = false;
@ -1158,10 +1160,13 @@ namespace MWMechanics
case ESM::MagicEffect::RemoveCurse:
actor.getClass().getCreatureStats(actor).getSpells().purgeCurses();
break;
default:
return false;
}
if (receivedMagicDamage && actor == getPlayer())
MWBase::Environment::get().getWindowManager()->activateHitOverlay(false);
return true;
}
}

@ -63,7 +63,9 @@ namespace MWMechanics
int getEffectiveEnchantmentCastCost (float castCost, const MWWorld::Ptr& actor);
void effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const MWMechanics::EffectKey& effectKey, float magnitude);
/// Apply a magic effect that is applied in tick intervals until its remaining time ends or it is removed
/// @return Was the effect a tickable effect with a magnitude?
bool effectTick(CreatureStats& creatureStats, const MWWorld::Ptr& actor, const MWMechanics::EffectKey& effectKey, float magnitude);
class CastSpell
{

@ -47,7 +47,7 @@ Actor::Actor(const MWWorld::Ptr& ptr, osg::ref_ptr<const Resource::BulletShape>
updateScale();
updatePosition();
updateCollisionMask();
addCollisionMask(getCollisionMask());
}
Actor::~Actor()
@ -70,15 +70,26 @@ void Actor::enableCollisionBody(bool collision)
}
}
void Actor::addCollisionMask(int collisionMask)
{
mCollisionWorld->addCollisionObject(mCollisionObject.get(), CollisionType_Actor, collisionMask);
}
void Actor::updateCollisionMask()
{
mCollisionWorld->removeCollisionObject(mCollisionObject.get());
addCollisionMask(getCollisionMask());
}
int Actor::getCollisionMask()
{
int collisionMask = CollisionType_World | CollisionType_HeightMap;
if (mExternalCollisionMode)
collisionMask |= CollisionType_Actor | CollisionType_Projectile | CollisionType_Door;
if (mCanWaterWalk)
collisionMask |= CollisionType_Water;
mCollisionWorld->addCollisionObject(mCollisionObject.get(), CollisionType_Actor, collisionMask);
return collisionMask;
}
void Actor::updatePosition()

@ -139,6 +139,8 @@ namespace MWPhysics
private:
/// Removes then re-adds the collision object to the dynamics world
void updateCollisionMask();
void addCollisionMask(int collisionMask);
int getCollisionMask();
bool mCanWaterWalk;
bool mWalkingOnWater;

@ -69,7 +69,14 @@ namespace MWPhysics
return osg::RadiansToDegrees(std::acos(normal * osg::Vec3f(0.f, 0.f, 1.f)));
}
static bool stepMove(const btCollisionObject *colobj, osg::Vec3f &position,
enum StepMoveResult
{
Result_Blocked, // unable to move over obstacle
Result_MaxSlope, // unable to end movement on this slope
Result_Success
};
static StepMoveResult stepMove(const btCollisionObject *colobj, osg::Vec3f &position,
const osg::Vec3f &toMove, float &remainingTime, const btCollisionWorld* collisionWorld)
{
/*
@ -120,7 +127,7 @@ namespace MWPhysics
stepper.doTrace(colobj, position, position+osg::Vec3f(0.0f,0.0f,sStepSizeUp), collisionWorld);
if(stepper.mFraction < std::numeric_limits<float>::epsilon())
return false; // didn't even move the smallest representable amount
return Result_Blocked; // didn't even move the smallest representable amount
// (TODO: shouldn't this be larger? Why bother with such a small amount?)
/*
@ -138,7 +145,7 @@ namespace MWPhysics
*/
tracer.doTrace(colobj, stepper.mEndPos, stepper.mEndPos + toMove, collisionWorld);
if(tracer.mFraction < std::numeric_limits<float>::epsilon())
return false; // didn't even move the smallest representable amount
return Result_Blocked; // didn't even move the smallest representable amount
/*
* Try moving back down sStepSizeDown using stepper.
@ -156,22 +163,22 @@ namespace MWPhysics
* ==============================================
*/
stepper.doTrace(colobj, tracer.mEndPos, tracer.mEndPos-osg::Vec3f(0.0f,0.0f,sStepSizeDown), collisionWorld);
if(stepper.mFraction < 1.0f && getSlope(stepper.mPlaneNormal) <= sMaxSlope)
if (getSlope(stepper.mPlaneNormal) > sMaxSlope)
return Result_MaxSlope;
if(stepper.mFraction < 1.0f)
{
// don't allow stepping up other actors
if (stepper.mHitObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor)
return false;
return Result_Blocked;
// only step down onto semi-horizontal surfaces. don't step down onto the side of a house or a wall.
// TODO: stepper.mPlaneNormal does not appear to be reliable - needs more testing
// NOTE: caller's variables 'position' & 'remainingTime' are modified here
position = stepper.mEndPos;
remainingTime *= (1.0f-tracer.mFraction); // remaining time is proportional to remaining distance
return true;
return Result_Success;
}
// moved between 0 and just under sStepSize distance but slope was too great,
// or moved full sStepSize distance (FIXME: is this a bug?)
return false;
return Result_Blocked;
}
@ -361,14 +368,15 @@ namespace MWPhysics
osg::Vec3f oldPosition = newPosition;
// We hit something. Try to step up onto it. (NOTE: stepMove does not allow stepping over)
// NOTE: stepMove modifies newPosition if successful
bool result = stepMove(colobj, newPosition, velocity*remainingTime, remainingTime, collisionWorld);
if (!result) // to make sure the maximum stepping distance isn't framerate-dependent or movement-speed dependent
const float minStep = 10.f;
StepMoveResult result = stepMove(colobj, newPosition, velocity*remainingTime, remainingTime, collisionWorld);
if (result == Result_MaxSlope && (velocity*remainingTime).length() < minStep) // to make sure the maximum stepping distance isn't framerate-dependent or movement-speed dependent
{
osg::Vec3f normalizedVelocity = velocity;
normalizedVelocity.normalize();
result = stepMove(colobj, newPosition, normalizedVelocity*10.f, remainingTime, collisionWorld);
result = stepMove(colobj, newPosition, normalizedVelocity*minStep, remainingTime, collisionWorld);
}
if(result)
if(result == Result_Success)
{
// don't let pure water creatures move out of water after stepMove
if (ptr.getClass().isPureWaterCreature(ptr)
@ -1459,7 +1467,10 @@ namespace MWPhysics
}
if (!mWaterEnabled)
{
mWaterCollisionObject.reset();
return;
}
mWaterCollisionObject.reset(new btCollisionObject());
mWaterCollisionShape.reset(new btStaticPlaneShape(btVector3(0,0,1), mWaterHeight));

@ -200,7 +200,7 @@ namespace MWPhysics
typedef std::map<MWWorld::Ptr, MWWorld::Ptr> CollisionMap;
CollisionMap mStandingCollisions;
// replaces all occurences of 'old' in the map by 'updated', no matter if its a key or value
// replaces all occurrences of 'old' in the map by 'updated', no matter if it's a key or value
void updateCollisionMapPtr(CollisionMap& map, const MWWorld::Ptr &old, const MWWorld::Ptr &updated);
PtrVelocityList mMovementQueue;

@ -302,7 +302,7 @@ protected:
/** Sets the root model of the object.
*
* Note that you must make sure all animation sources are cleared before reseting the object
* Note that you must make sure all animation sources are cleared before resetting the object
* root. All nodes previously retrieved with getNode will also become invalidated.
* @param forceskeleton Wrap the object root in a Skeleton, even if it contains no skinned parts. Use this if you intend to add skinned parts manually.
* @param baseonly If true, then any meshes or particle systems in the model are ignored

@ -110,8 +110,6 @@ void CreatureWeaponAnimation::updatePart(PartHolderPtr& scene, int slot)
osg::ref_ptr<osg::Node> node = mResourceSystem->getSceneManager()->getInstance(item.getClass().getModel(item));
osg::ref_ptr<osg::Node> attached = SceneUtil::attach(node, mObjectRoot, bonename, bonename);
mResourceSystem->getSceneManager()->notifyAttached(attached);
if (mSkeleton)
mSkeleton->markDirty();
scene.reset(new PartHolder(attached));

@ -671,8 +671,6 @@ PartHolderPtr NpcAnimation::insertBoundedPart(const std::string& model, const st
{
osg::ref_ptr<osg::Node> instance = mResourceSystem->getSceneManager()->getInstance(model);
osg::ref_ptr<osg::Node> attached = SceneUtil::attach(instance, mObjectRoot, bonefilter, bonename);
if (mSkeleton)
mSkeleton->markDirty();
mResourceSystem->getSceneManager()->notifyAttached(attached);
if (enchantedGlow)
addGlow(attached, *glowColor);

@ -211,7 +211,7 @@ namespace MWRender
osg::Vec3f mStormDirection;
// remember some settings so we don't have to apply them again if they didnt change
// remember some settings so we don't have to apply them again if they didn't change
std::string mClouds;
std::string mNextClouds;
float mCloudBlendFactor;

@ -11,7 +11,7 @@ namespace MWScript
enum Type
{
Type_Full, // global, local, targetted
Type_Full, // global, local, targeted
Type_Dialogue,
Type_Console
};

@ -34,8 +34,9 @@
namespace MWSound
{
SoundManager::SoundManager(const VFS::Manager* vfs, bool useSound)
SoundManager::SoundManager(const VFS::Manager* vfs, const std::map<std::string,std::string>& fallbackMap, bool useSound)
: mVFS(vfs)
, mFallback(fallbackMap)
, mOutput(new DEFAULT_OUTPUT(*this))
, mMasterVolume(1.0f)
, mSFXVolume(1.0f)
@ -61,6 +62,13 @@ namespace MWSound
mFootstepsVolume = Settings::Manager::getFloat("footsteps volume", "Sound");
mFootstepsVolume = std::min(std::max(mFootstepsVolume, 0.0f), 1.0f);
mNearWaterRadius = mFallback.getFallbackInt("Water_NearWaterRadius");
mNearWaterPoints = mFallback.getFallbackInt("Water_NearWaterPoints");
mNearWaterIndoorTolerance = mFallback.getFallbackFloat("Water_NearWaterIndoorTolerance");
mNearWaterOutdoorTolerance = mFallback.getFallbackFloat("Water_NearWaterOutdoorTolerance");
mNearWaterIndoorID = mFallback.getFallbackString("Water_NearWaterIndoorID");
mNearWaterOutdoorID = mFallback.getFallbackString("Water_NearWaterOutdoorID");
mBufferCacheMin = std::max(Settings::Manager::getInt("buffer cache min", "Sound"), 1);
mBufferCacheMax = std::max(Settings::Manager::getInt("buffer cache max", "Sound"), 1);
mBufferCacheMax *= 1024*1024;
@ -796,6 +804,96 @@ namespace MWSound
}
}
void SoundManager::updateWaterSound(float /*duration*/)
{
MWBase::World* world = MWBase::Environment::get().getWorld();
const MWWorld::ConstPtr player = world->getPlayerPtr();
osg::Vec3f pos = player.getRefData().getPosition().asVec3();
float volume = 0.0f;
const std::string& soundId = player.getCell()->isExterior() ? mNearWaterOutdoorID : mNearWaterIndoorID;
if (!mListenerUnderwater)
{
if (player.getCell()->getCell()->hasWater())
{
float dist = std::abs(player.getCell()->getWaterLevel() - pos.z());
if (player.getCell()->isExterior() && dist < mNearWaterOutdoorTolerance)
{
volume = (mNearWaterOutdoorTolerance - dist) / mNearWaterOutdoorTolerance;
if (mNearWaterPoints > 1)
{
int underwaterPoints = 0;
float step = mNearWaterRadius * 2.0f / (mNearWaterPoints - 1);
for (int x = 0; x < mNearWaterPoints; x++)
{
for (int y = 0; y < mNearWaterPoints; y++)
{
float height = world->getTerrainHeightAt(
osg::Vec3f(pos.x() - mNearWaterRadius + x*step, pos.y() - mNearWaterRadius + y*step, 0.0f));
if (height < 0)
underwaterPoints++;
}
}
volume *= underwaterPoints * 2.0f / (mNearWaterPoints*mNearWaterPoints);
}
}
else if (!player.getCell()->isExterior() && dist < mNearWaterIndoorTolerance)
{
volume = (mNearWaterIndoorTolerance - dist) / mNearWaterIndoorTolerance;
}
}
}
else
volume = 1.0f;
volume = std::min(volume, 1.0f);
if (mNearWaterSound)
{
if (volume == 0.0f)
{
mOutput->finishSound(mNearWaterSound);
mNearWaterSound.reset();
}
else
{
bool soundIdChanged = false;
Sound_Buffer* sfx = lookupSound(Misc::StringUtils::lowerCase(soundId));
for (SoundMap::const_iterator snditer = mActiveSounds.begin(); snditer != mActiveSounds.end(); ++snditer)
{
for (SoundBufferRefPairList::const_iterator pairiter = snditer->second.begin(); pairiter != snditer->second.end(); ++pairiter)
{
if (pairiter->first == mNearWaterSound)
{
if (pairiter->second != sfx)
soundIdChanged = true;
break;
}
}
}
if (soundIdChanged)
{
mOutput->finishSound(mNearWaterSound);
mNearWaterSound = playSound(soundId, volume, 1.0f, Play_TypeSfx, Play_Loop);
}
else if (sfx)
mNearWaterSound->setVolume(volume * sfx->mVolume);
}
}
else if (volume > 0.0f)
mNearWaterSound = playSound(soundId, volume, 1.0f, Play_TypeSfx, Play_Loop);
}
void SoundManager::updateSounds(float duration)
{
static float timePassed = 0.0;
@ -941,6 +1039,7 @@ namespace MWSound
{
updateSounds(duration);
updateRegionSound(duration);
updateWaterSound(duration);
}
}
@ -1105,6 +1204,7 @@ namespace MWSound
mOutput->finishStream(*trkiter);
mActiveTracks.clear();
mUnderwaterSound.reset();
mNearWaterSound.reset();
stopMusic();
}
}

@ -11,6 +11,8 @@
#include <components/settings/settings.hpp>
#include <components/fallback/fallback.hpp>
#include "../mwbase/soundmanager.hpp"
namespace VFS
@ -44,6 +46,8 @@ namespace MWSound
{
const VFS::Manager* mVFS;
Fallback::Map mFallback;
std::auto_ptr<Sound_Output> mOutput;
// Caches available music tracks by <playlist name, (sound files) >
@ -56,6 +60,13 @@ namespace MWSound
float mVoiceVolume;
float mFootstepsVolume;
int mNearWaterRadius;
int mNearWaterPoints;
float mNearWaterIndoorTolerance;
float mNearWaterOutdoorTolerance;
std::string mNearWaterIndoorID;
std::string mNearWaterOutdoorID;
typedef std::auto_ptr<std::deque<Sound_Buffer> > SoundBufferList;
// List of sound buffers, grown as needed. New enties are added to the
// back, allowing existing Sound_Buffer references/pointers to remain
@ -94,6 +105,7 @@ namespace MWSound
int mPausedSoundTypes;
MWBase::SoundPtr mUnderwaterSound;
MWBase::SoundPtr mNearWaterSound;
Sound_Buffer *insertSound(const std::string &soundId, const ESM::Sound *sound);
@ -108,6 +120,7 @@ namespace MWSound
void streamMusicFull(const std::string& filename);
void updateSounds(float duration);
void updateRegionSound(float duration);
void updateWaterSound(float duration);
float volumeFromType(PlayType type) const;
@ -119,7 +132,7 @@ namespace MWSound
friend class OpenAL_Output;
public:
SoundManager(const VFS::Manager* vfs, bool useSound);
SoundManager(const VFS::Manager* vfs, const std::map<std::string, std::string>& fallbackMap, bool useSound);
virtual ~SoundManager();
virtual void processChangedSettings(const Settings::CategorySettingVector& settings);

@ -100,6 +100,24 @@ namespace MWWorld
}
}
void CellRef::applyChargeRemainderToBeSubtracted(float chargeRemainder)
{
mCellRef.mChargeIntRemainder += std::abs(chargeRemainder);
if (mCellRef.mChargeIntRemainder > 1.0f)
{
float newChargeRemainder = (mCellRef.mChargeIntRemainder - std::floor(mCellRef.mChargeIntRemainder));
if (mCellRef.mChargeInt <= static_cast<int>(mCellRef.mChargeIntRemainder))
{
mCellRef.mChargeInt = 0;
}
else
{
mCellRef.mChargeInt -= static_cast<int>(mCellRef.mChargeIntRemainder);
}
mCellRef.mChargeIntRemainder = newChargeRemainder;
}
}
float CellRef::getChargeFloat() const
{
return mCellRef.mChargeFloat;

@ -69,6 +69,7 @@ namespace MWWorld
float getChargeFloat() const; // Implemented as union with int charge
void setCharge(int charge);
void setChargeFloat(float charge);
void applyChargeRemainderToBeSubtracted(float chargeRemainder); // Stores remainders and applies if > 1
// The NPC that owns this object (and will get angry if you steal it)
std::string getOwner() const;

@ -16,6 +16,7 @@ namespace
cellRef.mScale = 1;
cellRef.mFactionRank = 0;
cellRef.mChargeInt = -1;
cellRef.mChargeIntRemainder = 0.0f;
cellRef.mGoldValue = 1;
cellRef.mEnchantmentCharge = -1;
cellRef.mTeleport = false;

@ -93,6 +93,31 @@ namespace
}
return projectileEffects;
}
osg::Vec4 getMagicBoltLightDiffuseColor(const ESM::EffectList& effects)
{
// Calculate combined light diffuse color from magical effects
osg::Vec4 lightDiffuseColor;
float lightDiffuseRed = 0.0f;
float lightDiffuseGreen = 0.0f;
float lightDiffuseBlue = 0.0f;
for (std::vector<ESM::ENAMstruct>::const_iterator iter(effects.mList.begin());
iter != effects.mList.end(); ++iter)
{
const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(
iter->mEffectID);
lightDiffuseRed += (static_cast<float>(magicEffect->mData.mRed) / 255.f);
lightDiffuseGreen += (static_cast<float>(magicEffect->mData.mGreen) / 255.f);
lightDiffuseBlue += (static_cast<float>(magicEffect->mData.mBlue) / 255.f);
}
int numberOfEffects = effects.mList.size();
lightDiffuseColor = osg::Vec4(lightDiffuseRed / numberOfEffects
, lightDiffuseGreen / numberOfEffects
, lightDiffuseBlue / numberOfEffects
, 1.0f);
return lightDiffuseColor;
}
}
namespace MWWorld
@ -136,7 +161,8 @@ namespace MWWorld
};
void ProjectileManager::createModel(State &state, const std::string &model, const osg::Vec3f& pos, const osg::Quat& orient, bool rotate, std::string texture)
void ProjectileManager::createModel(State &state, const std::string &model, const osg::Vec3f& pos, const osg::Quat& orient,
bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture)
{
state.mNode = new osg::PositionAttitudeTransform;
state.mNode->setNodeMask(MWRender::Mask_Effect);
@ -167,6 +193,25 @@ namespace MWWorld
mResourceSystem->getSceneManager()->getInstance("meshes\\" + weapon->mModel, findVisitor.mFoundNode);
}
if (createLight)
{
osg::ref_ptr<osg::Light> projectileLight(new osg::Light);
projectileLight->setAmbient(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
projectileLight->setDiffuse(lightDiffuseColor);
projectileLight->setSpecular(osg::Vec4(0.0f, 0.0f, 0.0f, 0.0f));
projectileLight->setConstantAttenuation(0.f);
projectileLight->setLinearAttenuation(0.1f);
projectileLight->setQuadraticAttenuation(0.f);
projectileLight->setPosition(osg::Vec4(pos, 1.0));
SceneUtil::LightSource* projectileLightSource = new SceneUtil::LightSource;
projectileLightSource->setNodeMask(MWRender::Mask_Lighting);
projectileLightSource->setRadius(66.f);
state.mNode->addChild(projectileLightSource);
projectileLightSource->setLight(projectileLight);
}
SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor;
state.mNode->accept(disableFreezeOnCullVisitor);
@ -229,7 +274,8 @@ namespace MWWorld
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0));
MWWorld::Ptr ptr = ref.getPtr();
createModel(state, ptr.getClass().getModel(ptr), pos, orient, true, texture);
osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(effects);
createModel(state, ptr.getClass().getModel(ptr), pos, orient, true, true, lightDiffuseColor, texture);
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
for (size_t it = 0; it != state.mSoundIds.size(); it++)
@ -253,7 +299,7 @@ namespace MWWorld
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), projectile.getCellRef().getRefId());
MWWorld::Ptr ptr = ref.getPtr();
createModel(state, ptr.getClass().getModel(ptr), pos, orient, false);
createModel(state, ptr.getClass().getModel(ptr), pos, orient, false, false, osg::Vec4(0,0,0,0));
mProjectiles.push_back(state);
}
@ -483,7 +529,7 @@ namespace MWWorld
return true;
}
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), false);
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), false, false, osg::Vec4(0,0,0,0));
mProjectiles.push_back(state);
return true;
@ -518,7 +564,8 @@ namespace MWWorld
return true;
}
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, texture);
osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(esm.mEffects);
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, lightDiffuseColor, texture);
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();

@ -122,7 +122,8 @@ namespace MWWorld
void moveProjectiles(float dt);
void moveMagicBolts(float dt);
void createModel (State& state, const std::string& model, const osg::Vec3f& pos, const osg::Quat& orient, bool rotate, std::string texture = "");
void createModel (State& state, const std::string& model, const osg::Vec3f& pos, const osg::Quat& orient,
bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture = "");
void update (State& state, float duration);
void operator=(const ProjectileManager&);

@ -1069,7 +1069,7 @@ inline void WeatherManager::calculateResult(const int weatherID, const float gam
mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor);
// The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because
// MW applied the color to the ambient term as well. After the ambient and emissive terms are added together, the fixed pipeline
// would then clamp the total lighting to (1,1,1). A noticable change in color tone can be observed when only one of the color components gets clamped.
// would then clamp the total lighting to (1,1,1). A noticeable change in color tone can be observed when only one of the color components gets clamped.
// Unfortunately that means we can't use the INI color as is, have to replicate the above nonsense.
mResult.mSunDiscColor = mResult.mSunDiscColor + osg::componentMultiply(mResult.mSunDiscColor, mResult.mAmbientColor);
for (int i=0; i<3; ++i)

@ -198,8 +198,8 @@ bool Wizard::IniSettings::parseInx(const QString &path)
const QString section(array.left(index));
// Figure how many characters to read for the key
int lenght = array.indexOf("\x06", section.length() + 3) - (section.length() + 3);
const QString key(array.mid(section.length() + 3, lenght));
int length = array.indexOf("\x06", section.length() + 3) - (section.length() + 3);
const QString key(array.mid(section.length() + 3, length));
QString value(array.mid(section.length() + key.length() + 6));

@ -196,6 +196,7 @@ void ESM::CellRef::blank()
mFaction.clear();
mFactionRank = -2;
mChargeInt = -1;
mChargeIntRemainder = 0.0f;
mEnchantmentCharge = -1;
mGoldValue = 0;
mDestCell.clear();

@ -69,6 +69,7 @@ namespace ESM
int mChargeInt; // Used by everything except lights
float mChargeFloat; // Used only by lights
};
float mChargeIntRemainder; // Stores amount of charge not subtracted from mChargeInt
// Remaining enchantment charge. This could be -1 if the charge was not touched yet (i.e. full).
float mEnchantmentCharge;

@ -87,6 +87,15 @@ namespace Nif
nif->skip(17);
}
void NiSphericalCollider::read(NIFStream* nif)
{
Controlled::read(nif);
mBounceFactor = nif->getFloat();
mRadius = nif->getFloat();
mCenter = nif->getVector3();
}

@ -111,6 +111,16 @@ public:
float mPlaneDistance;
};
class NiSphericalCollider : public Controlled
{
public:
float mBounceFactor;
float mRadius;
osg::Vec3f mCenter;
void read(NIFStream *nif);
};
class NiParticleRotation : public Controlled
{
public:

@ -89,6 +89,7 @@ static std::map<std::string,RecordFactoryEntry> makeFactory()
newFactory.insert(makeEntry("NiStringExtraData", &construct <NiStringExtraData> , RC_NiStringExtraData ));
newFactory.insert(makeEntry("NiGravity", &construct <NiGravity> , RC_NiGravity ));
newFactory.insert(makeEntry("NiPlanarCollider", &construct <NiPlanarCollider> , RC_NiPlanarCollider ));
newFactory.insert(makeEntry("NiSphericalCollider", &construct <NiSphericalCollider> , RC_NiSphericalCollider ));
newFactory.insert(makeEntry("NiParticleGrowFade", &construct <NiParticleGrowFade> , RC_NiParticleGrowFade ));
newFactory.insert(makeEntry("NiParticleColorModifier", &construct <NiParticleColorModifier> , RC_NiParticleColorModifier ));
newFactory.insert(makeEntry("NiParticleRotation", &construct <NiParticleRotation> , RC_NiParticleRotation ));

@ -92,7 +92,8 @@ enum RecordType
RC_NiSequenceStreamHelper,
RC_NiSourceTexture,
RC_NiSkinInstance,
RC_RootCollisionNode
RC_RootCollisionNode,
RC_NiSphericalCollider
};
/// Base class for all records

@ -871,6 +871,13 @@ namespace NifOsg
const Nif::NiPlanarCollider* planarcollider = static_cast<const Nif::NiPlanarCollider*>(colliders.getPtr());
program->addOperator(new PlanarCollider(planarcollider));
}
else if (colliders->recType == Nif::RC_NiSphericalCollider)
{
const Nif::NiSphericalCollider* sphericalcollider = static_cast<const Nif::NiSphericalCollider*>(colliders.getPtr());
program->addOperator(new SphericalCollider(sphericalcollider));
}
else
std::cerr << "Unhandled particle collider " << colliders->recName << " in " << mFilename << std::endl;
}
}
@ -1152,10 +1159,11 @@ namespace NifOsg
morphGeom->setUpdateCallback(NULL);
morphGeom->setCullCallback(new UpdateMorphGeometry);
morphGeom->setUseVertexBufferObjects(true);
morphGeom->getOrCreateVertexBufferObject()->setUsage(GL_DYNAMIC_DRAW_ARB);
triShapeToGeometry(triShape, morphGeom, parentNode, composite, boundTextures, animflags);
morphGeom->getOrCreateVertexBufferObject()->setUsage(GL_DYNAMIC_DRAW_ARB);
const std::vector<Nif::NiMorphData::MorphData>& morphs = morpher->data.getPtr()->mMorphs;
if (morphs.empty())
return morphGeom;

@ -376,4 +376,73 @@ void PlanarCollider::operate(osgParticle::Particle *particle, double dt)
}
}
SphericalCollider::SphericalCollider(const Nif::NiSphericalCollider* collider)
: mBounceFactor(collider->mBounceFactor),
mSphere(collider->mCenter, collider->mRadius)
{
}
SphericalCollider::SphericalCollider()
: mBounceFactor(1.0f)
{
}
SphericalCollider::SphericalCollider(const SphericalCollider& copy, const osg::CopyOp& copyop)
: osgParticle::Operator(copy, copyop)
, mBounceFactor(copy.mBounceFactor)
, mSphere(copy.mSphere)
, mSphereInParticleSpace(copy.mSphereInParticleSpace)
{
}
void SphericalCollider::beginOperate(osgParticle::Program* program)
{
mSphereInParticleSpace = mSphere;
if (program->getReferenceFrame() == osgParticle::ParticleProcessor::ABSOLUTE_RF)
mSphereInParticleSpace.center() = program->transformLocalToWorld(mSphereInParticleSpace.center());
}
void SphericalCollider::operate(osgParticle::Particle* particle, double dt)
{
osg::Vec3f cent = (particle->getPosition() - mSphereInParticleSpace.center()); // vector from sphere center to particle
bool insideSphere = cent.length2() <= mSphereInParticleSpace.radius2();
if (insideSphere
|| (cent * particle->getVelocity() < 0.0f)) // if outside, make sure the particle is flying towards the sphere
{
// Collision test (finding point of contact) is performed by solving a quadratic equation:
// ||vec(cent) + vec(vel)*k|| = R /^2
// k^2 + 2*k*(vec(cent)*vec(vel))/||vec(vel)||^2 + (||vec(cent)||^2 - R^2)/||vec(vel)||^2 = 0
float b = -(cent * particle->getVelocity()) / particle->getVelocity().length2();
osg::Vec3f u = cent + particle->getVelocity() * b;
if (insideSphere
|| (u.length2() < mSphereInParticleSpace.radius2()))
{
float d = (mSphereInParticleSpace.radius2() - u.length2()) / particle->getVelocity().length2();
float k = insideSphere ? (std::sqrt(d) + b) : (b - std::sqrt(d));
if (k < dt)
{
// collision detected; reflect off the tangent plane
osg::Vec3f contact = particle->getPosition() + particle->getVelocity() * k;
osg::Vec3 normal = (contact - mSphereInParticleSpace.center());
normal.normalize();
float dotproduct = particle->getVelocity() * normal;
osg::Vec3 reflectedVelocity = particle->getVelocity() - normal * (2 * dotproduct);
reflectedVelocity *= mBounceFactor;
particle->setVelocity(reflectedVelocity);
}
}
}
}
}

@ -16,6 +16,7 @@ namespace Nif
{
class NiGravity;
class NiPlanarCollider;
class NiSphericalCollider;
class NiColorData;
}
@ -110,6 +111,23 @@ namespace NifOsg
osg::Plane mPlaneInParticleSpace;
};
class SphericalCollider : public osgParticle::Operator
{
public:
SphericalCollider(const Nif::NiSphericalCollider* collider);
SphericalCollider();
SphericalCollider(const SphericalCollider& copy, const osg::CopyOp& copyop);
META_Object(NifOsg, SphericalCollider)
virtual void beginOperate(osgParticle::Program* program);
virtual void operate(osgParticle::Particle* particle, double dt);
private:
float mBounceFactor;
osg::BoundingSphere mSphere;
osg::BoundingSphere mSphereInParticleSpace;
};
class GrowFadeAffector : public osgParticle::Operator
{
public:

@ -120,13 +120,17 @@ void RigGeometry::setSourceGeometry(osg::ref_ptr<osg::Geometry> sourceGeometry)
setVertexArray(vertexArray);
}
osg::ref_ptr<osg::Array> normalArray = osg::clone(from.getNormalArray(), osg::CopyOp::DEEP_COPY_ALL);
if (normalArray)
if (osg::Array* normals = from.getNormalArray())
{
normalArray->setVertexBufferObject(vbo);
setNormalArray(normalArray, osg::Array::BIND_PER_VERTEX);
osg::ref_ptr<osg::Array> normalArray = osg::clone(normals, osg::CopyOp::DEEP_COPY_ALL);
if (normalArray)
{
normalArray->setVertexBufferObject(vbo);
setNormalArray(normalArray, osg::Array::BIND_PER_VERTEX);
}
}
if (osg::Vec4Array* tangents = dynamic_cast<osg::Vec4Array*>(from.getTexCoordArray(7)))
{
mSourceTangents = tangents;
@ -273,7 +277,8 @@ void RigGeometry::update(osg::NodeVisitor* nv)
{
unsigned short vertex = *vertexIt;
(*positionDst)[vertex] = resultMat.preMult((*positionSrc)[vertex]);
(*normalDst)[vertex] = osg::Matrix::transform3x3((*normalSrc)[vertex], resultMat);
if (normalDst)
(*normalDst)[vertex] = osg::Matrix::transform3x3((*normalSrc)[vertex], resultMat);
if (tangentDst)
{
osg::Vec4f srcTangent = (*tangentSrc)[vertex];
@ -284,7 +289,8 @@ void RigGeometry::update(osg::NodeVisitor* nv)
}
positionDst->dirty();
normalDst->dirty();
if (normalDst)
normalDst->dirty();
if (tangentDst)
tangentDst->dirty();
}
@ -314,11 +320,14 @@ void RigGeometry::updateBounds(osg::NodeVisitor *nv)
box.expandBy(bs);
}
_boundingBox = box;
_boundingSphere = osg::BoundingSphere(_boundingBox);
_boundingSphereComputed = true;
for (unsigned int i=0; i<getNumParents(); ++i)
getParent(i)->dirtyBound();
if (box != _boundingBox)
{
_boundingBox = box;
_boundingSphere = osg::BoundingSphere(_boundingBox);
_boundingSphereComputed = true;
for (unsigned int i=0; i<getNumParents(); ++i)
getParent(i)->dirtyBound();
}
}
void RigGeometry::updateGeomToSkelMatrix(const osg::NodePath& nodePath)

@ -148,6 +148,8 @@ void Skeleton::markDirty()
{
mTraversedEvenFrame = false;
mTraversedOddFrame = false;
mBoneCache.clear();
mBoneCacheInit = false;
}
void Skeleton::traverse(osg::NodeVisitor& nv)
@ -160,6 +162,16 @@ void Skeleton::traverse(osg::NodeVisitor& nv)
osg::Group::traverse(nv);
}
void Skeleton::childInserted(unsigned int)
{
markDirty();
}
void Skeleton::childRemoved(unsigned int, unsigned int)
{
markDirty();
}
Bone::Bone()
: mNode(NULL)
{

@ -53,10 +53,12 @@ namespace SceneUtil
bool getActive() const;
/// If a new RigGeometry is added after the Skeleton has already been rendered, you must call markDirty().
void traverse(osg::NodeVisitor& nv);
void markDirty();
void traverse(osg::NodeVisitor& nv);
virtual void childInserted(unsigned int);
virtual void childRemoved(unsigned int, unsigned int);
private:
// The root bone is not a "real" bone, it has no corresponding node in the scene graph.

@ -102,10 +102,17 @@ InputWrapper::InputWrapper(SDL_Window* window, osg::ref_ptr<osgViewer::Viewer> v
if (evt.key.keysym.sym == SDLK_F3)
mViewer->getEventQueue()->keyRelease(osgGA::GUIEventAdapter::KEY_F3);
break;
case SDL_TEXTEDITING:
break;
case SDL_TEXTINPUT:
mKeyboardListener->textInput(evt.text);
break;
#if SDL_VERSION_ATLEAST(2, 0, 4)
case SDL_KEYMAPCHANGED:
break;
#endif
case SDL_JOYHATMOTION: //As we manage everything with GameController, don't even bother with these.
case SDL_JOYAXISMOTION:
case SDL_JOYBUTTONDOWN:

@ -6,6 +6,7 @@
#include <osg/ref_ptr>
#include <SDL_events.h>
#include <SDL_version.h>
#include "OISCompat.hpp"
#include "events.hpp"

@ -3,9 +3,9 @@ A Tour through OpenMW CS: making a magic ring
In this first chapter we will create a mod that adds a new ring with a simple
enchantment to the game. The ring will give its wearer a permanent Night Vision
effect while being worn. You don't need prior knowledge about modding
Morrowind, but you should be familiar with the game itself. There will be no
scripting necessary, we chan achieve everything using just what the base game
effect while being worn. You do not need previous Morrowind modding experience,
but you should be familiar with the game itself. There will be no
scripting necessary, we can achieve everything using just what the base game
offers out of the box. Before continuing make sure that OpenMW is properly
installed and playable.
@ -13,7 +13,7 @@ installed and playable.
Adding the ring to the game's records
*************************************
In this first section we will define what our new ring is, what it looks like
In this first section we will define what our new ring is, what it looks like,
and what it does. Getting it to work is the first step before we go further.
@ -28,11 +28,11 @@ options: create a new game, create a new addon, edit a content file.
:alt: Opening dialogue with three option and setting button (the wrench)
The first option is for creating an entirely new game, that's not what we want.
We want to edit an existing game, so choose the second one. When you save your
We want to edit an existing game, so choose the second option. When you save your
addon you can use the third option to open it again.
You will be presented with another window where you get to chose the content to
edit and the name of your project. We have to chose at least a base game, and
You will be presented with another window where you get to choose the content to
edit and the name of your project. Then we have to select at least the base game and
optionally a number of other addons we want to depend on. The name of the
project is arbitrary, it will be used to identify the addon later in the OpenMW
launcher.
@ -41,7 +41,7 @@ launcher.
:alt: Creation dialogue for a new project, pick content modules and name
Choose Morrowind as your content file and enter `Ring of Night Vision` as the
name. We could also chose further content files as dependencies if we wanted
name. We could also choose further content files as dependencies if we wanted
to, but for this mod the base game is enough.
Once the addon has been created you will be presented with a table. If you see
@ -67,7 +67,7 @@ of the table are the attributes of each object.
Morrowind uses something called a *relational database* for game data. If you
are not familiar with the term, it means that every type of thing can be
expressed as a *table*: there is a table for objects, a table for enchantments,
a table for icons, one for meshes, and so on. Properties of an entry must be
a table for icons, one for meshes and so on. Properties of an entry must be
simple values, like numbers or text strings. If we want a more complicated
property we need to reference an entry from another table. There are a few
exceptions to this though, some tables do have subtables. The effects of
@ -95,7 +95,7 @@ holding Shift to edit it (this is a configurable shortcut), but there is a
better way: right-click the row of our new record and chose *Edit Record*, a
new panel will open.
We can right-click the row of our new record and chose *Edit Record*, a
We can right-click the row of our new record and select *Edit Record*, a
new panel will open. Alternatively we can also define a configurable shortcut
instead of using the context menu; the default is double-clicking while
holding down the shift key.

@ -188,13 +188,13 @@ clamp lighting = true
# If this option is enabled, normal maps are automatically recognized and used if they are named appropriately
# (see 'normal map pattern', e.g. for a base texture foo.dds, the normal map texture would have to be named foo_n.dds).
# If this option is disabled, normal maps are only used if they are explicitely listed within the mesh file (.nif or .osg file).
# If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file (.nif or .osg file).
# Affects objects.
auto use object normal maps = false
# If this option is enabled, specular maps are automatically recognized and used if they are named appropriately
# (see 'specular map pattern', e.g. for a base texture foo.dds, the specular map texture would have to be named foo_spec.dds).
# If this option is disabled, normal maps are only used if they are explicitely listed within the mesh file (.osg file, not supported in .nif files).
# If this option is disabled, normal maps are only used if they are explicitly listed within the mesh file (.osg file, not supported in .nif files).
# Affects objects.
auto use object specular maps = false

Loading…
Cancel
Save