Merge pull request #34 from OpenMW/master

Add OpenMW commits from 1st week of August
coverity_scan^2
David Cernat 9 years ago committed by GitHub
commit babba95413

@ -37,6 +37,7 @@ Programmers
Cory F. Cohen (cfcohen)
Cris Mihalache (Mirceam)
darkf
devnexen
Dieho
Dmitry Shkurskiy (endorph)
Douglas Diniz (Dgdiniz)
@ -75,6 +76,7 @@ Programmers
Lars Söderberg (Lazaroth)
lazydev
Leon Saunders (emoose)
lohikaarme
Lukasz Gromanowski (lgro)
Manuel Edelmann (vorenon)
Marc Bouvier (CramitDeFrog)

@ -1,3 +1,9 @@
#include <string>
#include <iostream>
#include <limits>
#include <components/misc/stringops.hpp>
#include "convertacdt.hpp"
namespace ESSImport
@ -49,4 +55,49 @@ namespace ESSImport
npcStats.mTimeToStartDrowning = actorData.mACDT.mBreathMeter;
}
void convertANIS (const ANIS& anis, ESM::AnimationState& state)
{
static const char* animGroups[] =
{
"Idle", "Idle2", "Idle3", "Idle4", "Idle5", "Idle6", "Idle7", "Idle8", "Idle9", "Idlehh", "Idle1h", "Idle2c",
"Idle2w", "IdleSwim", "IdleSpell", "IdleCrossbow", "IdleSneak", "IdleStorm", "Torch", "Hit1", "Hit2", "Hit3",
"Hit4", "Hit5", "SwimHit1", "SwimHit2", "SwimHit3", "Death1", "Death2", "Death3", "Death4", "Death5",
"DeathKnockDown", "DeathKnockOut", "KnockDown", "KnockOut", "SwimDeath", "SwimDeath2", "SwimDeath3",
"SwimDeathKnockDown", "SwimDeathKnockOut", "SwimKnockOut", "SwimKnockDown", "SwimWalkForward",
"SwimWalkBack", "SwimWalkLeft", "SwimWalkRight", "SwimRunForward", "SwimRunBack", "SwimRunLeft",
"SwimRunRight", "SwimTurnLeft", "SwimTurnRight", "WalkForward", "WalkBack", "WalkLeft", "WalkRight",
"TurnLeft", "TurnRight", "RunForward", "RunBack", "RunLeft", "RunRight", "SneakForward", "SneakBack",
"SneakLeft", "SneakRight", "Jump", "WalkForwardhh", "WalkBackhh", "WalkLefthh", "WalkRighthh",
"TurnLefthh", "TurnRighthh", "RunForwardhh", "RunBackhh", "RunLefthh", "RunRighthh", "SneakForwardhh",
"SneakBackhh", "SneakLefthh", "SneakRighthh", "Jumphh", "WalkForward1h", "WalkBack1h", "WalkLeft1h",
"WalkRight1h", "TurnLeft1h", "TurnRight1h", "RunForward1h", "RunBack1h", "RunLeft1h", "RunRight1h",
"SneakForward1h", "SneakBack1h", "SneakLeft1h", "SneakRight1h", "Jump1h", "WalkForward2c", "WalkBack2c",
"WalkLeft2c", "WalkRight2c", "TurnLeft2c", "TurnRight2c", "RunForward2c", "RunBack2c", "RunLeft2c",
"RunRight2c", "SneakForward2c", "SneakBack2c", "SneakLeft2c", "SneakRight2c", "Jump2c", "WalkForward2w",
"WalkBack2w", "WalkLeft2w", "WalkRight2w", "TurnLeft2w", "TurnRight2w", "RunForward2w", "RunBack2w",
"RunLeft2w", "RunRight2w", "SneakForward2w", "SneakBack2w", "SneakLeft2w", "SneakRight2w", "Jump2w",
"SpellCast", "SpellTurnLeft", "SpellTurnRight", "Attack1", "Attack2", "Attack3", "SwimAttack1",
"SwimAttack2", "SwimAttack3", "HandToHand", "Crossbow", "BowAndArrow", "ThrowWeapon", "WeaponOneHand",
"WeaponTwoHand", "WeaponTwoWide", "Shield", "PickProbe", "InventoryHandToHand", "InventoryWeaponOneHand",
"InventoryWeaponTwoHand", "InventoryWeaponTwoWide"
};
if (anis.mGroupIndex < (sizeof(animGroups) / sizeof(*animGroups)))
{
std::string group(animGroups[anis.mGroupIndex]);
Misc::StringUtils::lowerCaseInPlace(group);
ESM::AnimationState::ScriptedAnimation scriptedAnim;
scriptedAnim.mGroup = group;
scriptedAnim.mTime = anis.mTime;
scriptedAnim.mAbsolute = true;
// Neither loop count nor queueing seems to be supported by the ess format.
scriptedAnim.mLoopCount = std::numeric_limits<size_t>::max();
state.mScriptedAnims.push_back(scriptedAnim);
}
else
// TODO: Handle 0xFF index, which seems to be used for finished animations.
std::cerr << "unknown animation group index: " << static_cast<unsigned int>(anis.mGroupIndex) << std::endl;
}
}

@ -4,6 +4,7 @@
#include <components/esm/creaturestats.hpp>
#include <components/esm/npcstats.hpp>
#include <components/esm/loadskil.hpp>
#include <components/esm/animationstate.hpp>
#include "importacdt.hpp"
@ -18,6 +19,8 @@ namespace ESSImport
void convertACSC (const ACSC& acsc, ESM::CreatureStats& cStats);
void convertNpcData (const ActorData& actorData, ESM::NpcStats& npcStats);
void convertANIS (const ANIS& anis, ESM::AnimationState& state);
}
#endif

@ -34,6 +34,9 @@ namespace
objstate.mCount = 0;
convertSCRI(cellref.mSCRI, objstate.mLocals);
objstate.mHasLocals = !objstate.mLocals.mVariables.empty();
if (cellref.mHasANIS)
convertANIS(cellref.mANIS, objstate.mAnimationState);
}
bool isIndexedRefId(const std::string& indexedRefId)

@ -123,8 +123,13 @@ namespace ESSImport
if (esm.isNextSub("ND3D"))
esm.skipHSub();
mHasANIS = false;
if (esm.isNextSub("ANIS"))
esm.skipHSub();
{
mHasANIS = true;
esm.getHT(mANIS);
}
}
}

@ -55,6 +55,12 @@ namespace ESSImport
unsigned char mCorpseClearCountdown; // hours?
unsigned char mUnknown3[71];
};
struct ANIS
{
unsigned char mGroupIndex;
unsigned char mUnknown[3];
float mTime;
};
#pragma pack(pop)
struct ActorData : public ESM::CellRef
@ -77,6 +83,9 @@ namespace ESSImport
SCRI mSCRI;
bool mHasANIS;
ANIS mANIS; // scripted animation state
void load(ESM::ESMReader& esm);
};

@ -87,6 +87,7 @@ opencs_units (view/render
scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget
previewwidget editmode instancemode instanceselectionmode instancemovemode
orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller
cellwater
)
opencs_units_noqt (view/render

@ -37,7 +37,8 @@ namespace CSMWorld
///
/// \note The worldspace part of \a id is ignored
static std::pair<CellCoordinates, bool> fromId (const std::string& id);
/// \return cell coordinates such that given world coordinates are in it.
static std::pair<int, int> coordinatesToCellIndex (float x, float y);
};

@ -16,6 +16,7 @@
#include "../../model/world/refcollection.hpp"
#include "../../model/world/cellcoordinates.hpp"
#include "cellwater.hpp"
#include "mask.hpp"
#include "pathgrid.hpp"
#include "terrainstorage.hpp"
@ -111,6 +112,7 @@ CSVRender::Cell::Cell (CSMWorld::Data& data, osg::Group* rootNode, const std::st
}
mPathgrid.reset(new Pathgrid(mData, mCellNode, mId, mCoordinates));
mCellWater.reset(new CellWater(mData, mCellNode, mId, mCoordinates));
}
}

@ -35,6 +35,7 @@ namespace CSMWorld
namespace CSVRender
{
class CellWater;
class Pathgrid;
class TagBase;
@ -49,6 +50,7 @@ namespace CSVRender
std::auto_ptr<CellArrow> mCellArrows[4];
std::auto_ptr<CellMarker> mCellMarker;
std::auto_ptr<CellBorder> mCellBorder;
std::auto_ptr<CellWater> mCellWater;
std::auto_ptr<Pathgrid> mPathgrid;
bool mDeleted;
int mSubMode;

@ -75,6 +75,7 @@ CSVRender::CellMarker::CellMarker(
mMarkerNode->setAutoRotateMode(osg::AutoTransform::ROTATE_TO_SCREEN);
mMarkerNode->setAutoScaleToScreen(true);
mMarkerNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);
mMarkerNode->getOrCreateStateSet()->setRenderBinDetails(osg::StateSet::TRANSPARENT_BIN + 1, "RenderBin");
mMarkerNode->setUserData(new CellMarkerTag(this));
mMarkerNode->setNodeMask(Mask_CellMarker);

@ -0,0 +1,177 @@
#include "cellwater.hpp"
#include <osg/Geode>
#include <osg/Geometry>
#include <osg/Group>
#include <osg/PositionAttitudeTransform>
#include <components/esm/loadland.hpp>
#include <components/fallback/fallback.hpp>
#include <components/misc/stringops.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/sceneutil/waterutil.hpp>
#include "../../model/world/cell.hpp"
#include "../../model/world/cellcoordinates.hpp"
#include "../../model/world/data.hpp"
#include "mask.hpp"
namespace CSVRender
{
const int CellWater::CellSize = ESM::Land::REAL_SIZE;
CellWater::CellWater(CSMWorld::Data& data, osg::Group* cellNode, const std::string& id,
const CSMWorld::CellCoordinates& cellCoords)
: mData(data)
, mId(id)
, mParentNode(cellNode)
, mWaterTransform(0)
, mWaterNode(0)
, mWaterGeometry(0)
, mDeleted(false)
, mExterior(false)
, mHasWater(false)
{
mWaterTransform = new osg::PositionAttitudeTransform();
mWaterTransform->setPosition(osg::Vec3f(cellCoords.getX() * CellSize + CellSize / 2.f,
cellCoords.getY() * CellSize + CellSize / 2.f, 0));
mWaterTransform->setNodeMask(Mask_Water);
mParentNode->addChild(mWaterTransform);
mWaterNode = new osg::Geode();
mWaterTransform->addChild(mWaterNode);
int cellIndex = mData.getCells().searchId(mId);
if (cellIndex > -1)
{
updateCellData(mData.getCells().getRecord(cellIndex));
}
// Keep water existance/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&)));
}
CellWater::~CellWater()
{
mParentNode->removeChild(mWaterTransform);
}
void CellWater::updateCellData(const CSMWorld::Record<CSMWorld::Cell>& cellRecord)
{
mDeleted = cellRecord.isDeleted();
if (!mDeleted)
{
const CSMWorld::Cell& cell = cellRecord.get();
if (mExterior != cell.isExterior() || mHasWater != cell.hasWater())
{
mExterior = cellRecord.get().isExterior();
mHasWater = cellRecord.get().hasWater();
recreate();
}
float waterHeight = -1;
if (!mExterior)
{
waterHeight = cellRecord.get().mWater;
}
osg::Vec3d pos = mWaterTransform->getPosition();
pos.z() = waterHeight;
mWaterTransform->setPosition(pos);
}
else
{
recreate();
}
}
void CellWater::cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight)
{
const CSMWorld::Collection<CSMWorld::Cell>& cells = mData.getCells();
int rowStart = -1;
int rowEnd = -1;
if (topLeft.parent().isValid())
{
rowStart = topLeft.parent().row();
rowEnd = bottomRight.parent().row();
}
else
{
rowStart = topLeft.row();
rowEnd = bottomRight.row();
}
for (int row = rowStart; row <= rowEnd; ++row)
{
const CSMWorld::Record<CSMWorld::Cell>& cellRecord = cells.getRecord(row);
if (Misc::StringUtils::lowerCase(cellRecord.get().mId) == mId)
updateCellData(cellRecord);
}
}
void CellWater::recreate()
{
const int InteriorScalar = 20;
const int SegmentsPerCell = 1;
const int TextureRepeatsPerCell = 6;
const float Alpha = 0.5f;
const int RenderBin = osg::StateSet::TRANSPARENT_BIN - 1;
if (mWaterGeometry)
{
mWaterNode->removeDrawable(mWaterGeometry);
mWaterGeometry = 0;
}
if (mDeleted || !mHasWater)
return;
float size;
int segments;
float textureRepeats;
if (mExterior)
{
size = CellSize;
segments = SegmentsPerCell;
textureRepeats = TextureRepeatsPerCell;
}
else
{
size = CellSize * InteriorScalar;
segments = SegmentsPerCell * InteriorScalar;
textureRepeats = TextureRepeatsPerCell * InteriorScalar;
}
mWaterGeometry = SceneUtil::createWaterGeometry(size, segments, textureRepeats);
mWaterGeometry->setStateSet(SceneUtil::createSimpleWaterStateSet(Alpha, RenderBin));
// Add water texture
std::string textureName = mData.getFallbackMap()->getFallbackString("Water_SurfaceTexture");
textureName = "textures/water/" + textureName + "00.dds";
Resource::ImageManager* imageManager = mData.getResourceSystem()->getImageManager();
osg::ref_ptr<osg::Texture2D> waterTexture = new osg::Texture2D();
waterTexture->setImage(imageManager->getImage(textureName));
waterTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT);
waterTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT);
mWaterGeometry->getStateSet()->setTextureAttributeAndModes(0, waterTexture, osg::StateAttribute::ON);
mWaterNode->addDrawable(mWaterGeometry);
}
}

@ -0,0 +1,70 @@
#ifndef CSV_RENDER_CELLWATER_H
#define CSV_RENDER_CELLWATER_H
#include <string>
#include <osg/ref_ptr>
#include <QObject>
#include <QModelIndex>
#include "../../model/world/record.hpp"
namespace osg
{
class Geode;
class Geometry;
class Group;
class PositionAttitudeTransform;
}
namespace CSMWorld
{
struct Cell;
class CellCoordinates;
class Data;
}
namespace CSVRender
{
/// For exterior cells, this adds a patch of water to fit the size of the cell. For interior cells with water, this
/// adds a large patch of water much larger than the typical size of a cell.
class CellWater : public QObject
{
Q_OBJECT
public:
CellWater(CSMWorld::Data& data, osg::Group* cellNode, const std::string& id,
const CSMWorld::CellCoordinates& cellCoords);
~CellWater();
void updateCellData(const CSMWorld::Record<CSMWorld::Cell>& cellRecord);
private slots:
void cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight);
private:
void recreate();
static const int CellSize;
CSMWorld::Data& mData;
std::string mId;
osg::Group* mParentNode;
osg::ref_ptr<osg::PositionAttitudeTransform> mWaterTransform;
osg::ref_ptr<osg::Geode> mWaterNode;
osg::ref_ptr<osg::Geometry> mWaterGeometry;
bool mDeleted;
bool mExterior;
bool mHasWater;
};
}
#endif

@ -19,6 +19,7 @@
#include "editmode.hpp"
#include "mask.hpp"
#include "cameracontroller.hpp"
bool CSVRender::PagedWorldspaceWidget::adjustCells()
{
@ -512,12 +513,15 @@ void CSVRender::PagedWorldspaceWidget::useViewHint (const std::string& hint)
{
char ignore1; // : or ;
char ignore2; // #
// Current coordinate
int x, y;
// Loop throught all the coordinates to add them to selection
while (stream >> ignore1 >> ignore2 >> x >> y)
selection.add (CSMWorld::CellCoordinates (x, y));
/// \todo adjust camera position
// Mark that camera needs setup
mCamPositionSet=false;
}
}
else if (hint[0]=='r')

@ -84,7 +84,8 @@ namespace CSVRender
/// hint system.
virtual ~PagedWorldspaceWidget();
/// Decodes the the hint string to set of cell that are rendered.
void useViewHint (const std::string& hint);
void setCellSelection(const CSMWorld::CellSelection& selection);

@ -109,14 +109,14 @@ namespace CSVRender
LightingBright mLightingBright;
int mPrevMouseX, mPrevMouseY;
/// Tells update that camera isn't set
bool mCamPositionSet;
FreeCameraController* mFreeCamControl;
OrbitCameraController* mOrbitCamControl;
CameraController* mCurrentCamControl;
private:
bool mCamPositionSet;
public slots:
void update(double dt);

@ -5,7 +5,6 @@
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/param.h>
#include <sys/ucontext.h>
#include <sys/utsname.h>
#include <string.h>
#include <errno.h>
@ -21,10 +20,11 @@
#ifdef __linux__
#include <sys/prctl.h>
#include <sys/ucontext.h>
#ifndef PR_SET_PTRACER
#define PR_SET_PTRACER 0x59616d61
#endif
#elif defined (__APPLE__) || defined (__FreeBSD__)
#elif defined (__APPLE__) || defined (__FreeBSD__) || defined(__OpenBSD__)
#include <signal.h>
#endif

@ -159,12 +159,14 @@ namespace MWBase
virtual void forceStateUpdate(const MWWorld::Ptr &ptr) = 0;
///< Forces an object to refresh its animation state.
virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number=1) = 0;
virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number=1, bool persist=false) = 0;
///< Run animation for a MW-reference. Calls to this function for references that are currently not
/// in the scene should be ignored.
///
/// \param mode 0 normal, 1 immediate start, 2 immediate loop
/// \param count How many times the animation should be run
/// \param persist Whether the animation state should be stored in saved games
/// and persist after cell unload.
/// \return Success or error
virtual void skipAnimation(const MWWorld::Ptr& ptr) = 0;
@ -173,6 +175,9 @@ namespace MWBase
virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) = 0;
/// Save the current animation state of managed references to their RefData.
virtual void persistAnimationStates() = 0;
/// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently
/// paused we may want to do it manually (after equipping permanent enchantment)
virtual void updateMagicEffects (const MWWorld::Ptr& ptr) = 0;

@ -1400,12 +1400,12 @@ namespace MWMechanics
iter->second->getCharacterController()->forceStateUpdate();
}
bool Actors::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number)
bool Actors::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist)
{
PtrActorMap::iterator iter = mActors.find(ptr);
if(iter != mActors.end())
{
return iter->second->getCharacterController()->playGroup(groupName, mode, number);
return iter->second->getCharacterController()->playGroup(groupName, mode, number, persist);
}
else
{
@ -1428,6 +1428,12 @@ namespace MWMechanics
return false;
}
void Actors::persistAnimationStates()
{
for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter)
iter->second->getCharacterController()->persistAnimationState();
}
void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out)
{
for (PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter)

@ -109,9 +109,10 @@ namespace MWMechanics
void forceStateUpdate(const MWWorld::Ptr &ptr);
bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number);
bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false);
void skipAnimation(const MWWorld::Ptr& ptr);
bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName);
void persistAnimationStates();
void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out);

@ -762,12 +762,17 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim
refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true);
mAnimation->runAnimation(0.f);
unpersistAnimationState();
}
CharacterController::~CharacterController()
{
if (mAnimation)
{
persistAnimationState();
mAnimation->setTextKeyListener(NULL);
}
}
void split(const std::string &s, char delim, std::vector<std::string> &elems) {
@ -1557,14 +1562,14 @@ void CharacterController::update(float duration)
{
if(mAnimQueue.size() > 1)
{
if(mAnimation->isPlaying(mAnimQueue.front().first) == false)
if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false)
{
mAnimation->disable(mAnimQueue.front().first);
mAnimation->disable(mAnimQueue.front().mGroup);
mAnimQueue.pop_front();
mAnimation->play(mAnimQueue.front().first, Priority_Default,
mAnimation->play(mAnimQueue.front().mGroup, Priority_Default,
MWRender::Animation::BlendMask_All, false,
1.0f, "start", "stop", 0.0f, mAnimQueue.front().second);
1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount);
}
}
}
@ -1837,14 +1842,14 @@ void CharacterController::update(float duration)
}
else if(mAnimQueue.size() > 1)
{
if(mAnimation->isPlaying(mAnimQueue.front().first) == false)
if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false)
{
mAnimation->disable(mAnimQueue.front().first);
mAnimation->disable(mAnimQueue.front().mGroup);
mAnimQueue.pop_front();
mAnimation->play(mAnimQueue.front().first, Priority_Default,
mAnimation->play(mAnimQueue.front().mGroup, Priority_Default,
MWRender::Animation::BlendMask_All, false,
1.0f, "start", "stop", 0.0f, mAnimQueue.front().second);
1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount);
}
}
@ -1951,8 +1956,74 @@ void CharacterController::update(float duration)
mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead());
}
void CharacterController::persistAnimationState()
{
ESM::AnimationState& state = mPtr.getRefData().getAnimationState();
state.mScriptedAnims.clear();
for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter)
{
if (!iter->mPersist)
continue;
ESM::AnimationState::ScriptedAnimation anim;
anim.mGroup = iter->mGroup;
if (iter == mAnimQueue.begin())
{
anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup);
float complete;
mAnimation->getInfo(anim.mGroup, &complete, NULL);
anim.mTime = complete;
}
else
{
anim.mLoopCount = iter->mLoopCount;
anim.mTime = 0.f;
}
state.mScriptedAnims.push_back(anim);
}
}
void CharacterController::unpersistAnimationState()
{
const ESM::AnimationState& state = mPtr.getRefData().getAnimationState();
if (!state.mScriptedAnims.empty())
{
clearAnimQueue();
for (ESM::AnimationState::ScriptedAnimations::const_iterator iter = state.mScriptedAnims.begin(); iter != state.mScriptedAnims.end(); ++iter)
{
AnimationQueueEntry entry;
entry.mGroup = iter->mGroup;
entry.mLoopCount = iter->mLoopCount;
entry.mPersist = true;
mAnimQueue.push_back(entry);
}
const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front();
float complete = anim.mTime;
if (anim.mAbsolute)
{
float start = mAnimation->getTextKeyTime(anim.mGroup+": start");
float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop");
float time = std::max(start, std::min(stop, anim.mTime));
complete = (time - start) / (stop - start);
}
bool CharacterController::playGroup(const std::string &groupname, int mode, int count)
mAnimation->disable(mCurrentIdle);
mCurrentIdle.clear();
mIdleState = CharState_SpecialIdle;
mAnimation->play(anim.mGroup,
Priority_Default, MWRender::Animation::BlendMask_All, false, 1.0f,
"start", "stop", complete, anim.mLoopCount);
}
}
bool CharacterController::playGroup(const std::string &groupname, int mode, int count, bool persist)
{
if(!mAnimation || !mAnimation->hasAnimation(groupname))
{
@ -1962,10 +2033,16 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int
else
{
count = std::max(count, 1);
if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().first))
AnimationQueueEntry entry;
entry.mGroup = groupname;
entry.mLoopCount = count-1;
entry.mPersist = persist;
if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup))
{
clearAnimQueue();
mAnimQueue.push_back(std::make_pair(groupname, count-1));
mAnimQueue.push_back(entry);
mAnimation->disable(mCurrentIdle);
mCurrentIdle.clear();
@ -1978,9 +2055,9 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int
else if(mode == 0)
{
if (!mAnimQueue.empty())
mAnimation->stopLooping(mAnimQueue.front().first);
mAnimation->stopLooping(mAnimQueue.front().mGroup);
mAnimQueue.resize(1);
mAnimQueue.push_back(std::make_pair(groupname, count-1));
mAnimQueue.push_back(entry);
}
}
return true;
@ -2002,7 +2079,7 @@ bool CharacterController::isAnimPlaying(const std::string &groupName)
void CharacterController::clearAnimQueue()
{
if(!mAnimQueue.empty())
mAnimation->disable(mAnimQueue.front().first);
mAnimation->disable(mAnimQueue.front().mGroup);
mAnimQueue.clear();
}

@ -147,7 +147,13 @@ class CharacterController : public MWRender::Animation::TextKeyListener
MWWorld::Ptr mPtr;
MWRender::Animation *mAnimation;
typedef std::deque<std::pair<std::string,size_t> > AnimationQueue;
struct AnimationQueueEntry
{
std::string mGroup;
size_t mLoopCount;
bool mPersist;
};
typedef std::deque<AnimationQueueEntry> AnimationQueue;
AnimationQueue mAnimQueue;
CharacterState mIdleState;
@ -236,7 +242,10 @@ public:
void update(float duration);
bool playGroup(const std::string &groupname, int mode, int count);
void persistAnimationState();
void unpersistAnimationState();
bool playGroup(const std::string &groupname, int mode, int count, bool persist=false);
void skipAnim();
bool isAnimPlaying(const std::string &groupName);

@ -842,12 +842,12 @@ namespace MWMechanics
mActors.forceStateUpdate(ptr);
}
bool MechanicsManager::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number)
bool MechanicsManager::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist)
{
if(ptr.getClass().isActor())
return mActors.playAnimationGroup(ptr, groupName, mode, number);
return mActors.playAnimationGroup(ptr, groupName, mode, number, persist);
else
return mObjects.playAnimationGroup(ptr, groupName, mode, number);
return mObjects.playAnimationGroup(ptr, groupName, mode, number, persist);
}
void MechanicsManager::skipAnimation(const MWWorld::Ptr& ptr)
{
@ -864,6 +864,12 @@ namespace MWMechanics
return false;
}
void MechanicsManager::persistAnimationStates()
{
mActors.persistAnimationStates();
mObjects.persistAnimationStates();
}
void MechanicsManager::updateMagicEffects(const MWWorld::Ptr &ptr)
{
mActors.updateMagicEffects(ptr);

@ -146,9 +146,10 @@ namespace MWMechanics
/// Attempt to play an animation group
/// @return Success or error
virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number);
virtual bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false);
virtual void skipAnimation(const MWWorld::Ptr& ptr);
virtual bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string &groupName);
virtual void persistAnimationStates();
/// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently
/// paused we may want to do it manually (after equipping permanent enchantment)

@ -79,12 +79,12 @@ void Objects::update(float duration, bool paused)
}
}
bool Objects::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number)
bool Objects::playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist)
{
PtrControllerMap::iterator iter = mObjects.find(ptr);
if(iter != mObjects.end())
{
return iter->second->playGroup(groupName, mode, number);
return iter->second->playGroup(groupName, mode, number, persist);
}
else
{
@ -99,6 +99,12 @@ void Objects::skipAnimation(const MWWorld::Ptr& ptr)
iter->second->skipAnim();
}
void Objects::persistAnimationStates()
{
for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter)
iter->second->persistAnimationState();
}
void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out)
{
for (PtrControllerMap::iterator iter = mObjects.begin(); iter != mObjects.end(); ++iter)

@ -38,8 +38,9 @@ namespace MWMechanics
void update(float duration, bool paused);
///< Update object animations
bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number);
bool playAnimationGroup(const MWWorld::Ptr& ptr, const std::string& groupName, int mode, int number, bool persist=false);
void skipAnimation(const MWWorld::Ptr& ptr);
void persistAnimationStates();
void getObjectsInRange (const osg::Vec3f& position, float radius, std::vector<MWWorld::Ptr>& out);
};

@ -843,6 +843,15 @@ namespace MWRender
return iter->second.getTime();
}
size_t Animation::getCurrentLoopCount(const std::string& groupname) const
{
AnimStateMap::const_iterator iter = mStates.find(groupname);
if(iter == mStates.end())
return 0;
return iter->second.mLoopCount;
}
void Animation::disable(const std::string &groupname)
{
AnimStateMap::iterator iter = mStates.find(groupname);

@ -415,6 +415,8 @@ public:
/// Get the current absolute position in the animation track for the animation that is currently playing from the given group.
float getCurrentTime(const std::string& groupname) const;
size_t getCurrentLoopCount(const std::string& groupname) const;
/** Disables the specified animation group;
* \param groupname Animation group to disable.
*/

@ -24,6 +24,8 @@
#include <components/resource/resourcesystem.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/sceneutil/waterutil.hpp>
#include <components/nifosg/controller.hpp>
#include <components/sceneutil/controller.hpp>
@ -40,58 +42,6 @@
#include "renderbin.hpp"
#include "util.hpp"
namespace
{
osg::ref_ptr<osg::Geometry> createWaterGeometry(float size, int segments, float textureRepeats)
{
osg::ref_ptr<osg::Vec3Array> verts (new osg::Vec3Array);
osg::ref_ptr<osg::Vec2Array> texcoords (new osg::Vec2Array);
// some drivers don't like huge triangles, so we do some subdivisons
// a paged solution would be even better
const float step = size/segments;
const float texCoordStep = textureRepeats / segments;
for (int x=0; x<segments; ++x)
{
for (int y=0; y<segments; ++y)
{
float x1 = -size/2.f + x*step;
float y1 = -size/2.f + y*step;
float x2 = x1 + step;
float y2 = y1 + step;
verts->push_back(osg::Vec3f(x1, y2, 0.f));
verts->push_back(osg::Vec3f(x1, y1, 0.f));
verts->push_back(osg::Vec3f(x2, y1, 0.f));
verts->push_back(osg::Vec3f(x2, y2, 0.f));
float u1 = x*texCoordStep;
float v1 = y*texCoordStep;
float u2 = u1 + texCoordStep;
float v2 = v1 + texCoordStep;
texcoords->push_back(osg::Vec2f(u1, v2));
texcoords->push_back(osg::Vec2f(u1, v1));
texcoords->push_back(osg::Vec2f(u2, v1));
texcoords->push_back(osg::Vec2f(u2, v2));
}
}
osg::ref_ptr<osg::Geometry> waterGeom (new osg::Geometry);
waterGeom->setVertexArray(verts);
waterGeom->setTexCoordArray(0, texcoords);
osg::ref_ptr<osg::Vec3Array> normal (new osg::Vec3Array);
normal->push_back(osg::Vec3f(0,0,1));
waterGeom->setNormalArray(normal, osg::Array::BIND_OVERALL);
waterGeom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,verts->size()));
return waterGeom;
}
}
namespace MWRender
{
@ -465,7 +415,7 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem
{
mSimulation.reset(new RippleSimulation(parent, resourceSystem, fallback));
mWaterGeom = createWaterGeometry(CELL_SIZE*150, 40, 900);
mWaterGeom = SceneUtil::createWaterGeometry(CELL_SIZE*150, 40, 900);
mWaterGeom->setDrawCallback(new DepthClampCallback);
mWaterGeom->setNodeMask(Mask_Water);
@ -527,26 +477,11 @@ void Water::updateWaterMaterial()
void Water::createSimpleWaterStateSet(osg::Node* node, float alpha)
{
osg::ref_ptr<osg::StateSet> stateset (new osg::StateSet);
osg::ref_ptr<osg::Material> material (new osg::Material);
material->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f));
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, alpha));
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f));
material->setColorMode(osg::Material::OFF);
stateset->setAttributeAndModes(material, osg::StateAttribute::ON);
stateset->setMode(GL_BLEND, osg::StateAttribute::ON);
stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
osg::ref_ptr<osg::Depth> depth (new osg::Depth);
depth->setWriteMask(false);
stateset->setAttributeAndModes(depth, osg::StateAttribute::ON);
stateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin");
osg::ref_ptr<osg::StateSet> stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water);
node->setStateSet(stateset);
// Add animated textures
std::vector<osg::ref_ptr<osg::Texture2D> > textures;
int frameCount = mFallback->getFallbackInt("Water_SurfaceFrameCount");
std::string texture = mFallback->getFallbackString("Water_SurfaceTexture");

@ -56,7 +56,7 @@ namespace MWScript
throw std::runtime_error ("animation mode out of range");
}
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, std::numeric_limits<int>::max());
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, std::numeric_limits<int>::max(), true);
}
};
@ -89,7 +89,7 @@ namespace MWScript
throw std::runtime_error ("animation mode out of range");
}
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, loops);
MWBase::Environment::get().getMechanicsManager()->playAnimationGroup (ptr, group, mode, loops, true);
}
};

@ -219,6 +219,9 @@ void MWState::StateManager::saveGame (const std::string& description, const Slot
else
slot = character->updateSlot (slot, profile);
// Make sure the animation state held by references is up to date before saving the game.
MWBase::Environment::get().getMechanicsManager()->persistAnimationStates();
// Write to a memory stream first. If there is an exception during the save process, we don't want to trash the
// existing save file we are overwriting.
std::stringstream stream;

@ -31,6 +31,8 @@ namespace MWWorld
mDeletedByContentFile = refData.mDeletedByContentFile;
mFlags = refData.mFlags;
mAnimationState = refData.mAnimationState;
mCustomData = refData.mCustomData ? refData.mCustomData->clone() : 0;
}
@ -65,6 +67,7 @@ namespace MWWorld
mEnabled (objectState.mEnabled != 0),
mCount (objectState.mCount),
mPosition (objectState.mPosition),
mAnimationState(objectState.mAnimationState),
mCustomData (0),
mChanged(true), mFlags(objectState.mFlags) // Loading from a savegame -> assume changed
{
@ -96,6 +99,8 @@ namespace MWWorld
objectState.mCount = mCount;
objectState.mPosition = mPosition;
objectState.mFlags = mFlags;
objectState.mAnimationState = mAnimationState;
}
RefData& RefData::operator= (const RefData& refData)
@ -269,4 +274,15 @@ namespace MWWorld
else
return false;
}
const ESM::AnimationState& RefData::getAnimationState() const
{
return mAnimationState;
}
ESM::AnimationState& RefData::getAnimationState()
{
return mAnimationState;
}
}

@ -2,6 +2,7 @@
#define GAME_MWWORLD_REFDATA_H
#include <components/esm/defs.hpp>
#include <components/esm/animationstate.hpp>
#include "../mwscript/locals.hpp"
@ -42,6 +43,8 @@ namespace MWWorld
ESM::Position mPosition;
ESM::AnimationState mAnimationState;
CustomData *mCustomData;
void copy (const RefData& refData);
@ -132,6 +135,9 @@ namespace MWWorld
bool hasChanged() const;
///< Has this RefData changed since it was originally loaded?
const ESM::AnimationState& getAnimationState() const;
ESM::AnimationState& getAnimationState();
};
}

@ -136,7 +136,7 @@ if (BUILD_WITH_CODE_COVERAGE)
endif()
# Workaround for binutil => 2.23 problem when linking, should be fixed eventually upstream
if (UNIX AND NOT APPLE)
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
target_link_libraries(openmw-wizard dl Xt)
endif()

@ -51,7 +51,7 @@ add_component_dir (shader
add_component_dir (sceneutil
clone attach visitor util statesetupdater controller skeleton riggeometry lightcontroller
lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil
lightmanager lightutil positionattitudetransform workqueue unrefqueue pathgridutil waterutil
)
add_component_dir (nif
@ -79,7 +79,7 @@ add_component_dir (esm
loadweap records aipackage effectlist spelllist variant variantimp loadtes3 cellref filter
savedgame journalentry queststate locals globalscript player objectstate cellid cellstate globalmap inventorystate containerstate npcstate creaturestate dialoguestate statstate
npcstats creaturestats weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile
aisequence magiceffects util custommarkerstate stolenitems transport
aisequence magiceffects util custommarkerstate stolenitems transport animationstate
)
add_component_dir (esmterrain

@ -0,0 +1,37 @@
#include "animationstate.hpp"
#include "esmreader.hpp"
#include "esmwriter.hpp"
namespace ESM
{
void AnimationState::load(ESMReader& esm)
{
mScriptedAnims.clear();
while (esm.isNextSub("ANIS"))
{
ScriptedAnimation anim;
anim.mGroup = esm.getHString();
esm.getHNOT(anim.mTime, "TIME");
esm.getHNOT(anim.mAbsolute, "ABST");
esm.getHNT(anim.mLoopCount, "COUN");
mScriptedAnims.push_back(anim);
}
}
void AnimationState::save(ESMWriter& esm) const
{
for (ScriptedAnimations::const_iterator iter = mScriptedAnims.begin(); iter != mScriptedAnims.end(); ++iter)
{
esm.writeHNString("ANIS", iter->mGroup);
if (iter->mTime > 0)
esm.writeHNT("TIME", iter->mTime);
if (iter->mAbsolute)
esm.writeHNT("ABST", iter->mAbsolute);
esm.writeHNT("COUN", iter->mLoopCount);
}
}
}

@ -0,0 +1,34 @@
#ifndef OPENMW_ESM_ANIMATIONSTATE_H
#define OPENMW_ESM_ANIMATIONSTATE_H
#include <string>
#include <vector>
namespace ESM
{
class ESMReader;
class ESMWriter;
// format 0, saved games only
struct AnimationState
{
struct ScriptedAnimation
{
ScriptedAnimation()
: mTime(0.f), mAbsolute(false), mLoopCount(0) {}
std::string mGroup;
float mTime;
bool mAbsolute;
size_t mLoopCount;
};
typedef std::vector<ScriptedAnimation> ScriptedAnimations;
ScriptedAnimations mScriptedAnims;
void load(ESMReader& esm);
void save(ESMWriter& esm) const;
};
}
#endif

@ -34,6 +34,8 @@ void ESM::ObjectState::load (ESMReader &esm)
int unused;
esm.getHNOT(unused, "LTIM");
mAnimationState.load(esm);
// FIXME: assuming "false" as default would make more sense, but also break compatibility with older save files
mHasCustomState = true;
esm.getHNOT (mHasCustomState, "HCUS");
@ -61,6 +63,8 @@ void ESM::ObjectState::save (ESMWriter &esm, bool inInventory) const
if (mFlags != 0)
esm.writeHNT ("FLAG", mFlags);
mAnimationState.save(esm);
if (!mHasCustomState)
esm.writeHNT ("HCUS", false);
}

@ -6,6 +6,7 @@
#include "cellref.hpp"
#include "locals.hpp"
#include "animationstate.hpp"
namespace ESM
{
@ -31,6 +32,8 @@ namespace ESM
unsigned int mVersion;
ESM::AnimationState mAnimationState;
ObjectState() : mHasCustomState(true), mVersion(0)
{}

@ -4,7 +4,7 @@
#include <string>
#include <boost/filesystem.hpp>
#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__)
#ifndef ANDROID
#include <components/files/linuxpath.hpp>
namespace Files { typedef LinuxPath TargetPathType; }

@ -1,6 +1,6 @@
#include "linuxpath.hpp"
#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__)
#include <cstdlib>
#include <cstring>
@ -159,4 +159,4 @@ boost::filesystem::path LinuxPath::getInstallPath() const
} /* namespace Files */
#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) */
#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__) */

@ -1,7 +1,7 @@
#ifndef COMPONENTS_FILES_LINUXPATH_H
#define COMPONENTS_FILES_LINUXPATH_H
#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
#if defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__)
#include <boost/filesystem.hpp>
@ -56,6 +56,6 @@ struct LinuxPath
} /* namespace Files */
#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) */
#endif /* defined(__linux__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__) */
#endif /* COMPONENTS_FILES_LINUXPATH_H */

@ -0,0 +1,79 @@
#include "waterutil.hpp"
#include <osg/Depth>
#include <osg/Geometry>
#include <osg/Material>
#include <osg/StateSet>
namespace SceneUtil
{
osg::ref_ptr<osg::Geometry> createWaterGeometry(float size, int segments, float textureRepeats)
{
osg::ref_ptr<osg::Vec3Array> verts (new osg::Vec3Array);
osg::ref_ptr<osg::Vec2Array> texcoords (new osg::Vec2Array);
// some drivers don't like huge triangles, so we do some subdivisons
// a paged solution would be even better
const float step = size/segments;
const float texCoordStep = textureRepeats / segments;
for (int x=0; x<segments; ++x)
{
for (int y=0; y<segments; ++y)
{
float x1 = -size/2.f + x*step;
float y1 = -size/2.f + y*step;
float x2 = x1 + step;
float y2 = y1 + step;
verts->push_back(osg::Vec3f(x1, y2, 0.f));
verts->push_back(osg::Vec3f(x1, y1, 0.f));
verts->push_back(osg::Vec3f(x2, y1, 0.f));
verts->push_back(osg::Vec3f(x2, y2, 0.f));
float u1 = x*texCoordStep;
float v1 = y*texCoordStep;
float u2 = u1 + texCoordStep;
float v2 = v1 + texCoordStep;
texcoords->push_back(osg::Vec2f(u1, v2));
texcoords->push_back(osg::Vec2f(u1, v1));
texcoords->push_back(osg::Vec2f(u2, v1));
texcoords->push_back(osg::Vec2f(u2, v2));
}
}
osg::ref_ptr<osg::Geometry> waterGeom (new osg::Geometry);
waterGeom->setVertexArray(verts);
waterGeom->setTexCoordArray(0, texcoords);
osg::ref_ptr<osg::Vec3Array> normal (new osg::Vec3Array);
normal->push_back(osg::Vec3f(0,0,1));
waterGeom->setNormalArray(normal, osg::Array::BIND_OVERALL);
waterGeom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS,0,verts->size()));
return waterGeom;
}
osg::ref_ptr<osg::StateSet> createSimpleWaterStateSet(float alpha, int renderBin)
{
osg::ref_ptr<osg::StateSet> stateset (new osg::StateSet);
osg::ref_ptr<osg::Material> material (new osg::Material);
material->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 1.f));
material->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, alpha));
material->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1.f, 1.f, 1.f, 1.f));
material->setColorMode(osg::Material::OFF);
stateset->setAttributeAndModes(material, osg::StateAttribute::ON);
stateset->setMode(GL_BLEND, osg::StateAttribute::ON);
stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
osg::ref_ptr<osg::Depth> depth (new osg::Depth);
depth->setWriteMask(false);
stateset->setAttributeAndModes(depth, osg::StateAttribute::ON);
stateset->setRenderBinDetails(renderBin, "RenderBin");
return stateset;
}
}

@ -0,0 +1,19 @@
#ifndef OPENMW_COMPONENTS_WATERUTIL_H
#define OPENMW_COMPONENTS_WATERUTIL_H
#include <osg/ref_ptr>
namespace osg
{
class Geometry;
class StateSet;
}
namespace SceneUtil
{
osg::ref_ptr<osg::Geometry> createWaterGeometry(float size, int segments, float textureRepeats);
osg::ref_ptr<osg::StateSet> createSimpleWaterStateSet(float alpha, int renderBin);
}
#endif

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

@ -77,7 +77,7 @@
<file alias="day">Sun-48.png</file>
<file alias="bright">Lightbulb-48.png</file>
<file alias="1st-person">eyeballdude.png</file>
<file alias="free-camera">flying eye.png</file>
<file alias="free-camera">flying-eye.png</file>
<file alias="orbiting-camera">orbit2.png</file>
<file alias="play">scene-play.png</file>
<file alias="scene-view-1">scene-view-references.png</file>

Loading…
Cancel
Save