Merge remote-tracking branch 'upstream/master' into mouse-picking

loadfix
cc9cii 10 years ago
commit dd9208afeb

@ -549,6 +549,7 @@ endif(WIN32)
# Extern
add_subdirectory (extern/shiny)
add_subdirectory (extern/ogre-ffmpeg-videoplayer)
add_subdirectory (extern/oics)
add_subdirectory (extern/sdl4ogre)

@ -345,7 +345,7 @@ std::auto_ptr<sh::Factory> CS::Editor::setupGraphics()
factory->loadAllFiles();
bool shaders = mUserSettings.setting("Objects/shaders", QString("true")) == "true" ? true : false;
bool shaders = mUserSettings.setting("3d-render/shaders", QString("true")) == "true" ? true : false;
sh::Factory::getInstance ().setShadersEnabled (shaders);
std::string fog = mUserSettings.setting("Shader/fog", QString("true")).toStdString();
@ -364,7 +364,7 @@ std::auto_ptr<sh::Factory> CS::Editor::setupGraphics()
// internal setting - may be switched on or off by the use of shader configurations
sh::Factory::getInstance ().setGlobalSetting ("viewproj_fix", "false");
std::string num_lights = mUserSettings.setting("Objects/num_lights", QString("8")).toStdString();
std::string num_lights = mUserSettings.setting("3d-render-adv/num_lights", QString("8")).toStdString();
sh::Factory::getInstance ().setGlobalSetting ("num_lights", num_lights);
/// \todo add more configurable shiny settings

@ -2,8 +2,8 @@
#include "support.hpp"
CSMSettings::Setting::Setting(SettingType typ, const QString &settingName,
const QString &pageName)
: mIsEditorSetting (false)
const QString &pageName, const QString& label)
: mIsEditorSetting (true)
{
buildDefaultSetting();
@ -17,6 +17,7 @@ CSMSettings::Setting::Setting(SettingType typ, const QString &settingName,
setProperty (Property_SettingType, QVariant (settingType).toString());
setProperty (Property_Page, pageName);
setProperty (Property_Name, settingName);
setProperty (Property_Label, label.isEmpty() ? settingName : label);
}
void CSMSettings::Setting::buildDefaultSetting()
@ -290,14 +291,16 @@ CSMSettings::SettingType CSMSettings::Setting::type() const
Property_SettingType).at(0).toInt());
}
void CSMSettings::Setting::setMaximum (int value)
void CSMSettings::Setting::setRange (int min, int max)
{
setProperty (Property_Maximum, value);
setProperty (Property_Minimum, min);
setProperty (Property_Maximum, max);
}
void CSMSettings::Setting::setMaximum (double value)
void CSMSettings::Setting::setRange (double min, double max)
{
setProperty (Property_Maximum, value);
setProperty (Property_Minimum, min);
setProperty (Property_Maximum, max);
}
QString CSMSettings::Setting::maximum() const
@ -305,16 +308,6 @@ QString CSMSettings::Setting::maximum() const
return property (Property_Maximum).at(0);
}
void CSMSettings::Setting::setMinimum (int value)
{
setProperty (Property_Minimum, value);
}
void CSMSettings::Setting::setMinimum (double value)
{
setProperty (Property_Minimum, value);
}
QString CSMSettings::Setting::minimum() const
{
return property (Property_Minimum).at(0);
@ -372,6 +365,26 @@ bool CSMSettings::Setting::wrapping() const
return (property (Property_Wrapping).at(0) == "true");
}
void CSMSettings::Setting::setLabel (const QString& label)
{
setProperty (Property_Label, label);
}
QString CSMSettings::Setting::getLabel() const
{
return property (Property_Label).at (0);
}
void CSMSettings::Setting::setToolTip (const QString& toolTip)
{
setProperty (Property_ToolTip, toolTip);
}
QString CSMSettings::Setting::getToolTip() const
{
return property (Property_ToolTip).at (0);
}
void CSMSettings::Setting::setProperty (SettingProperty prop, bool value)
{
setProperty (prop, QStringList() << QVariant (value).toString());

@ -29,8 +29,8 @@ namespace CSMSettings
public:
explicit Setting(SettingType typ, const QString &settingName,
const QString &pageName);
Setting(SettingType typ, const QString &settingName,
const QString &pageName, const QString& label = "");
void addProxy (const Setting *setting, const QStringList &vals);
void addProxy (const Setting *setting, const QList <QStringList> &list);
@ -66,12 +66,11 @@ namespace CSMSettings
void setMask (const QString &value);
QString mask() const;
void setMaximum (int value);
void setMaximum (double value);
void setRange (int min, int max);
void setRange (double min, double max);
QString maximum() const;
void setMinimum (int value);
void setMinimum (double value);
QString minimum() const;
void setName (const QString &value);
@ -132,6 +131,13 @@ namespace CSMSettings
void setWidgetWidth (int value);
int widgetWidth() const;
/// This is the text the user gets to see.
void setLabel (const QString& label);
QString getLabel() const;
void setToolTip (const QString& toolTip);
QString getToolTip() const;
///returns the specified property value
QStringList property (SettingProperty prop) const;

@ -36,12 +36,14 @@ namespace CSMSettings
Property_TicksAbove = 20,
Property_TicksBelow = 21,
Property_StyleSheet = 22,
Property_Label = 23,
Property_ToolTip = 24,
//Stringlists should always be the last items
Property_DefaultValues = 23,
Property_DeclaredValues = 24,
Property_DefinedValues = 25,
Property_Proxies = 26
Property_DefaultValues = 25,
Property_DeclaredValues = 26,
Property_DefinedValues = 27,
Property_Proxies = 28
};
///Basic setting widget types.

@ -30,183 +30,127 @@ namespace boost
} /* namespace boost */
#endif /* (BOOST_VERSION <= 104600) */
CSMSettings::UserSettings *CSMSettings::UserSettings::mUserSettingsInstance = 0;
CSMSettings::UserSettings *CSMSettings::UserSettings::sUserSettingsInstance = 0;
CSMSettings::UserSettings::UserSettings (const Files::ConfigurationManager& configurationManager)
: mCfgMgr (configurationManager)
, mSettingDefinitions(NULL)
, mSettingCfgDefinitions(NULL)
{
assert(!mUserSettingsInstance);
mUserSettingsInstance = this;
assert(!sUserSettingsInstance);
sUserSettingsInstance = this;
buildSettingModelDefaults();
// for overriding opencs.ini settings with those from settings.cfg
mSettingCfgDefinitions = new QSettings(QSettings::IniFormat, QSettings::UserScope, "", QString(), this);
}
void CSMSettings::UserSettings::buildSettingModelDefaults()
{
QString section;
section = "Objects";
declareSection ("3d-render", "3D Rendering");
{
Setting *numLights = createSetting (Type_SpinBox, section, "num_lights");
numLights->setDefaultValue(8);
numLights->setEditorSetting(true);
numLights->setColumnSpan (1);
numLights->setMinimum (0);
numLights->setMaximum (100); // FIXME: not sure what the max value should be
numLights->setWidgetWidth (10);
numLights->setViewLocation(1, 2);
Setting *shaders = createSetting (Type_CheckBox, section, "shaders");
shaders->setDeclaredValues(QStringList() << "true" << "false");
shaders->setDefaultValue("true");
shaders->setEditorSetting(true);
shaders->setSpecialValueText("Enable Shaders");
shaders->setWidgetWidth(25);
shaders->setColumnSpan (3);
shaders->setStyleSheet ("QGroupBox { border: 0px; }");
shaders->setViewLocation(2, 1);
Setting *shaders = createSetting (Type_CheckBox, "shaders", "Enable Shaders");
shaders->setDefaultValue ("true");
Setting *farClipDist = createSetting (Type_DoubleSpinBox, "far-clip-distance", "Far clipping distance");
farClipDist->setDefaultValue (300000);
farClipDist->setRange (0, 1000000);
farClipDist->setToolTip ("The maximum distance objects are still rendered at.");
QString defaultValue = "None";
Setting *antialiasing = createSetting (Type_ComboBox, "antialiasing", "Antialiasing");
antialiasing->setDeclaredValues (QStringList()
<< defaultValue << "MSAA 2" << "MSAA 4" << "MSAA 8" << "MSAA 16");
antialiasing->setDefaultValue (defaultValue);
}
section = "Scene";
declareSection ("3d-render-adv", "3D Rendering (Advanced)");
{
Setting *fastFactor = createSetting (Type_SpinBox, section, "fast factor");
fastFactor->setDefaultValue(4);
fastFactor->setEditorSetting(true);
fastFactor->setColumnSpan (1);
fastFactor->setMinimum (1);
fastFactor->setSpecialValueText ("1"); // FIXME: workaround
fastFactor->setMaximum (100); // FIXME: not sure what the max value should be
fastFactor->setWidgetWidth (10);
fastFactor->setViewLocation(1, 2);
Setting *farClipDist = createSetting (Type_DoubleSpinBox, section, "far clip distance");
farClipDist->setDefaultValue(300000);
farClipDist->setEditorSetting(true);
farClipDist->setColumnSpan (1);
farClipDist->setMinimum (0);
farClipDist->setMaximum (1000000); // FIXME: not sure what the max value should be
farClipDist->setWidgetWidth (10);
farClipDist->setViewLocation(2, 2);
Setting *timerStart = createSetting (Type_SpinBox, section, "timer start");
timerStart->setDefaultValue(20);
timerStart->setEditorSetting(true);
timerStart->setColumnSpan (1);
timerStart->setMinimum (0);
timerStart->setMaximum (100); // FIXME: not sure what the max value should be
timerStart->setWidgetWidth (10);
timerStart->setViewLocation(3, 2);
Setting *numLights = createSetting (Type_SpinBox, "num_lights",
"Number of lights per pass");
numLights->setDefaultValue (8);
numLights->setRange (1, 100);
}
section = "SubView";
declareSection ("scene-input", "Scene Input");
{
Setting *maxSubView = createSetting (Type_SpinBox, section, "max subviews");
maxSubView->setDefaultValue(256);
maxSubView->setEditorSetting(true);
maxSubView->setColumnSpan (1);
maxSubView->setMinimum (1);
maxSubView->setSpecialValueText ("1");
maxSubView->setMaximum (256); // FIXME: not sure what the max value should be
maxSubView->setWidgetWidth (10);
maxSubView->setViewLocation(1, 2);
Setting *minWidth = createSetting (Type_SpinBox, section, "minimum width");
minWidth->setDefaultValue(325);
minWidth->setEditorSetting(true);
minWidth->setColumnSpan (1);
minWidth->setMinimum (50);
minWidth->setSpecialValueText ("50");
minWidth->setMaximum (10000); // FIXME: not sure what the max value should be
minWidth->setWidgetWidth (10);
minWidth->setViewLocation(2, 2);
Setting *reuse = createSetting (Type_CheckBox, section, "reuse");
reuse->setDeclaredValues(QStringList() << "true" << "false");
reuse->setDefaultValue("true");
reuse->setEditorSetting(true);
reuse->setSpecialValueText("Reuse SubView");
reuse->setWidgetWidth(25);
reuse->setColumnSpan (3);
reuse->setStyleSheet ("QGroupBox { border: 0px; }");
reuse->setViewLocation(3, 2);
Setting *timer = createSetting (Type_SpinBox, "timer", "Input responsiveness");
timer->setDefaultValue (20);
timer->setRange (1, 100);
timer->setToolTip ("The time between two checks for user input in milliseconds.<p>"
"Lower value result in higher responsiveness.");
Setting *fastFactor = createSetting (Type_SpinBox, "fast-factor",
"Fast movement factor");
fastFactor->setDefaultValue (4);
fastFactor->setRange (1, 100);
fastFactor->setToolTip (
"Factor by which movement is speed up while the shift key is held down.");
}
section = "Window Size";
declareSection ("window", "Window");
{
Setting *width = createSetting (Type_LineEdit, section, "Width");
Setting *height = createSetting (Type_LineEdit, section, "Height");
width->setWidgetWidth (5);
height->setWidgetWidth (8);
width->setDefaultValues (QStringList() << "1024");
height->setDefaultValues (QStringList() << "768");
width->setEditorSetting (true);
height->setEditorSetting (true);
height->setViewLocation (2,2);
width->setViewLocation (2,1);
/*
*Create the proxy setting for predefined values
*/
Setting *preDefined = createSetting (Type_ComboBox, section,
"Pre-Defined");
preDefined->setDeclaredValues (QStringList() << "640 x 480"
<< "800 x 600" << "1024 x 768" << "1440 x 900");
Setting *preDefined = createSetting (Type_ComboBox, "pre-defined",
"Default window size");
preDefined->setEditorSetting (false);
preDefined->setDeclaredValues (
QStringList() << "640 x 480" << "800 x 600" << "1024 x 768" << "1440 x 900");
preDefined->setViewLocation (1, 1);
preDefined->setWidgetWidth (10);
preDefined->setColumnSpan (2);
preDefined->setToolTip ("Newly opened top-level windows will open with this size "
"(picked from a list of pre-defined values)");
preDefined->addProxy (width,
QStringList() << "640" << "800" << "1024" << "1440"
);
Setting *width = createSetting (Type_LineEdit, "default-width",
"Default window width");
width->setDefaultValues (QStringList() << "1024");
width->setViewLocation (2, 1);
width->setColumnSpan (1);
width->setToolTip ("Newly opened top-level windows will open with this width.");
preDefined->addProxy (width, QStringList() << "640" << "800" << "1024" << "1440");
preDefined->addProxy (height,
QStringList() << "480" << "600" << "768" << "900"
);
Setting *height = createSetting (Type_LineEdit, "default-height",
"Default window height");
height->setDefaultValues (QStringList() << "768");
height->setViewLocation (2, 2);
height->setColumnSpan (1);
height->setToolTip ("Newly opened top-level windows will open with this height.");
preDefined->addProxy (height, QStringList() << "480" << "600" << "768" << "900");
Setting *reuse = createSetting (Type_CheckBox, "reuse", "Reuse Subviews");
reuse->setDefaultValue ("true");
reuse->setToolTip ("When a new subview is requested and a matching subview already "
" exist, do not open a new subview and use the existing one instead.");
Setting *maxSubView = createSetting (Type_SpinBox, "max-subviews",
"Maximum number of subviews per top-level window");
maxSubView->setDefaultValue (256);
maxSubView->setRange (1, 256);
maxSubView->setToolTip ("If the maximum number is reached and a new subview is opened "
"it will be placed into a new top-level window.");
Setting *minWidth = createSetting (Type_SpinBox, "minimum-width",
"Minimum subview width");
minWidth->setDefaultValue (325);
minWidth->setRange (50, 10000);
minWidth->setToolTip ("Minimum width of subviews.");
}
section = "Display Format";
declareSection ("records", "Records");
{
QString defaultValue = "Icon and Text";
QStringList values = QStringList() << defaultValue << "Icon Only" << "Text Only";
QStringList values = QStringList()
<< defaultValue << "Icon Only" << "Text Only";
Setting *rsd = createSetting (Type_RadioButton,
section, "Record Status Display");
Setting *ritd = createSetting (Type_RadioButton,
section, "Referenceable ID Type Display");
Setting *rsd = createSetting (Type_RadioButton, "status-format",
"Modification status display format");
rsd->setDefaultValue (defaultValue);
rsd->setDeclaredValues (values);
ritd->setDeclaredValues (values);
rsd->setEditorSetting (true);
ritd->setEditorSetting (true);
Setting *ritd = createSetting (Type_RadioButton, "type-format",
"ID type display format");
ritd->setDefaultValue (defaultValue);
ritd->setDeclaredValues (values);
}
section = "Video";
{
QString defaultValue = "None";
QStringList values = QStringList()
<< defaultValue << "MSAA 2" << "MSAA 4" << "MSAA 8" << "MSAA 16";
Setting *antialiasing = createSetting (Type_SpinBox, section, "antialiasing");
antialiasing->setDeclaredValues (values);
antialiasing->setEditorSetting (true);
antialiasing->setWidgetWidth(15);
}
section = "Proxy Selection Test";
{
/******************************************************************
* There are three types of values:
@ -382,7 +326,7 @@ void CSMSettings::UserSettings::buildSettingModelDefaults()
CSMSettings::UserSettings::~UserSettings()
{
mUserSettingsInstance = 0;
sUserSettingsInstance = 0;
}
void CSMSettings::UserSettings::loadSettings (const QString &fileName)
@ -411,13 +355,9 @@ void CSMSettings::UserSettings::loadSettings (const QString &fileName)
mSettingDefinitions = new QSettings
(QSettings::IniFormat, QSettings::UserScope, "opencs", QString(), this);
// check if override entry exists (default: disabled)
if(!mSettingDefinitions->childGroups().contains("Video", Qt::CaseInsensitive))
mSettingDefinitions->setValue("Video/use settings.cfg", "false");
}
// if the key is not found create one with a defaut value
// if the key is not found create one with a default value
QString CSMSettings::UserSettings::setting(const QString &viewKey, const QString &value)
{
if(mSettingDefinitions->contains(viewKey))
@ -451,23 +391,10 @@ QString CSMSettings::UserSettings::settingValue (const QString &settingKey)
{
QStringList defs;
// check if video settings are overriden
if(settingKey.contains(QRegExp("^Video\\b", Qt::CaseInsensitive)) &&
mSettingDefinitions->value("Video/use settings.cfg") == "true" &&
settingKey.contains(QRegExp("^Video/\\brender|antialiasing|vsync|fullscreen\\b", Qt::CaseInsensitive)))
{
if (!mSettingCfgDefinitions->contains (settingKey))
return QString();
else
defs = mSettingCfgDefinitions->value (settingKey).toStringList();
}
else
{
if (!mSettingDefinitions->contains (settingKey))
return QString();
defs = mSettingDefinitions->value (settingKey).toStringList();
}
if (defs.isEmpty())
return QString();
@ -477,8 +404,8 @@ QString CSMSettings::UserSettings::settingValue (const QString &settingKey)
CSMSettings::UserSettings& CSMSettings::UserSettings::instance()
{
assert(mUserSettingsInstance);
return *mUserSettingsInstance;
assert(sUserSettingsInstance);
return *sUserSettingsInstance;
}
void CSMSettings::UserSettings::updateUserSetting(const QString &settingKey,
@ -486,11 +413,11 @@ void CSMSettings::UserSettings::updateUserSetting(const QString &settingKey,
{
mSettingDefinitions->setValue (settingKey ,list);
if(settingKey == "Objects/num_lights" && !list.empty())
if(settingKey == "3d-render-adv/num_lights" && !list.empty())
{
sh::Factory::getInstance ().setGlobalSetting ("num_lights", list.at(0).toStdString());
}
else if(settingKey == "Objects/shaders" && !list.empty())
else if(settingKey == "3d-render/shaders" && !list.empty())
{
sh::Factory::getInstance ().setShadersEnabled (list.at(0).toStdString() == "true" ? true : false);
}
@ -539,24 +466,57 @@ CSMSettings::SettingPageMap CSMSettings::UserSettings::settingPageMap() const
SettingPageMap pageMap;
foreach (Setting *setting, mSettings)
pageMap[setting->page()].append (setting);
{
SettingPageMap::iterator iter = pageMap.find (setting->page());
if (iter==pageMap.end())
{
QPair<QString, QList <Setting *> > value;
std::map<QString, QString>::const_iterator iter2 =
mSectionLabels.find (setting->page());
value.first = iter2!=mSectionLabels.end() ? iter2->second : "";
iter = pageMap.insert (setting->page(), value);
}
iter->second.append (setting);
}
return pageMap;
}
CSMSettings::Setting *CSMSettings::UserSettings::createSetting
(CSMSettings::SettingType typ, const QString &page, const QString &name)
(CSMSettings::SettingType type, const QString &name, const QString& label)
{
//get list of all settings for the current setting name
if (findSetting (page, name))
{
qWarning() << "Duplicate declaration encountered: "
<< (name + '/' + page);
return 0;
}
Setting *setting = new Setting (type, name, mSection, label);
// set useful defaults
int row = 1;
if (!mSettings.empty())
row = mSettings.back()->viewRow()+1;
setting->setViewLocation (row, 1);
setting->setColumnSpan (3);
int width = 10;
Setting *setting = new Setting (typ, name, page);
if (type==Type_CheckBox)
width = 40;
setting->setWidgetWidth (width);
if (type==Type_CheckBox)
setting->setStyleSheet ("QGroupBox { border: 0px; }");
if (type==Type_CheckBox)
setting->setDeclaredValues(QStringList() << "true" << "false");
if (type==Type_CheckBox)
setting->setSpecialValueText (setting->getLabel());
//add declaration to the model
mSettings.append (setting);
@ -564,6 +524,12 @@ CSMSettings::Setting *CSMSettings::UserSettings::createSetting
return setting;
}
void CSMSettings::UserSettings::declareSection (const QString& page, const QString& label)
{
mSection = page;
mSectionLabels[page] = label;
}
QStringList CSMSettings::UserSettings::definitions (const QString &viewKey) const
{
if (mSettingDefinitions->contains (viewKey))

@ -1,10 +1,13 @@
#ifndef USERSETTINGS_HPP
#define USERSETTINGS_HPP
#include <map>
#include <QList>
#include <QStringList>
#include <QString>
#include <QMap>
#include <QPair>
#include <boost/filesystem/path.hpp>
#include "support.hpp"
@ -22,20 +25,20 @@ class QSettings;
namespace CSMSettings {
class Setting;
typedef QMap <QString, QList <Setting *> > SettingPageMap;
typedef QMap <QString, QPair<QString, QList <Setting *> > > SettingPageMap;
class UserSettings: public QObject
{
Q_OBJECT
static UserSettings *mUserSettingsInstance;
static UserSettings *sUserSettingsInstance;
const Files::ConfigurationManager& mCfgMgr;
QSettings *mSettingDefinitions;
QSettings *mSettingCfgDefinitions;
QList <Setting *> mSettings;
QString mSection;
std::map<QString, QString> mSectionLabels;
public:
@ -64,7 +67,7 @@ namespace CSMSettings {
void removeSetting
(const QString &pageName, const QString &settingName);
///Retreive a map of the settings, keyed by page name
///Retrieve a map of the settings, keyed by page name
SettingPageMap settingPageMap() const;
///Returns a string list of defined vlaues for the specified setting
@ -84,8 +87,13 @@ namespace CSMSettings {
void buildSettingModelDefaults();
///add a new setting to the model and return it
Setting *createSetting (CSMSettings::SettingType typ,
const QString &page, const QString &name);
Setting *createSetting (CSMSettings::SettingType type, const QString &name,
const QString& label);
/// Set the section for createSetting calls.
///
/// Sections can be declared multiple times.
void declareSection (const QString& page, const QString& label);
signals:

@ -96,7 +96,7 @@ bool CSMWorld::ScriptContext::isId (const std::string& name) const
{
mIds = mData.getIds();
std::for_each (mIds.begin(), mIds.end(), &Misc::StringUtils::lowerCase);
std::for_each (mIds.begin(), mIds.end(), &Misc::StringUtils::toLower);
std::sort (mIds.begin(), mIds.end());
mIdsUpdated = true;

@ -369,10 +369,10 @@ CSVDoc::View::View (ViewManager& viewManager, CSMDoc::Document *document, int to
mViewTotal (totalViews)
{
QString width = CSMSettings::UserSettings::instance().settingValue
("Window Size/Width");
("window/default-width");
QString height = CSMSettings::UserSettings::instance().settingValue
("Window Size/Height");
("window/default-height");
// trick to get the window decorations and their sizes
show();
@ -459,7 +459,7 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin
// User setting to reuse sub views (on a per top level view basis)
bool reuse =
userSettings.setting("SubView/reuse", QString("true")) == "true" ? true : false;
userSettings.setting ("window/reuse", QString("true")) == "true" ? true : false;
if(reuse)
{
foreach(SubView *sb, mSubViews)
@ -478,7 +478,7 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin
//
// If the sub view limit setting is one, the sub view title bar is hidden and the
// text in the main title bar is adjusted accordingly
int maxSubView = userSettings.setting("SubView/max subviews", QString("256")).toInt();
int maxSubView = userSettings.setting("window/max-subviews", QString("256")).toInt();
if(mSubViews.size() >= maxSubView) // create a new top level view
{
mViewManager.addView(mDocument, id, hint);
@ -501,7 +501,7 @@ void CSVDoc::View::addSubView (const CSMWorld::UniversalId& id, const std::strin
if (!hint.empty())
view->useHint (hint);
int minWidth = userSettings.setting("SubView/minimum width", QString("325")).toInt();
int minWidth = userSettings.setting ("window/minimum-width", QString("325")).toInt();
view->setMinimumWidth(minWidth);
view->setStatusBar (mShowStatusBar->isChecked());

@ -54,10 +54,10 @@ namespace CSVRender
CSMSettings::UserSettings &userSettings = CSMSettings::UserSettings::instance();
float farClipDist = userSettings.setting("Scene/far clip distance", QString("300000")).toFloat();
float farClipDist = userSettings.setting("3d-render/far-clip-distance", QString("300000")).toFloat();
mCamera->setFarClipDistance (farClipDist);
mFastFactor = userSettings.setting("Scene/fast factor", QString("4")).toInt();
mFastFactor = userSettings.setting("scene-input/fast-factor", QString("4")).toInt();
mCamera->roll (Ogre::Degree (90));
@ -72,7 +72,7 @@ namespace CSVRender
connect (timer, SIGNAL (timeout()), this, SLOT (update()));
int timerStart = userSettings.setting("Scene/timer start", QString("20")).toInt();
int timerStart = userSettings.setting("scene-input/timer", QString("20")).toInt();
timer->start (timerStart);
/// \todo make shortcut configurable
@ -141,7 +141,7 @@ namespace CSVRender
params.insert(std::make_pair("title", windowTitle.str()));
std::string antialiasing =
CSMSettings::UserSettings::instance().settingValue("Video/antialiasing").toStdString();
CSMSettings::UserSettings::instance().settingValue("3d-render/antialiasing").toStdString();
if(antialiasing == "MSAA 16") antialiasing = "16";
else if(antialiasing == "MSAA 8") antialiasing = "8";
else if(antialiasing == "MSAA 4") antialiasing = "4";
@ -444,14 +444,14 @@ namespace CSVRender
if(key.contains(QRegExp("^\\b(Objects|Shader|Scene)", Qt::CaseInsensitive)))
flagAsModified();
if(key == "Scene/far clip distance" && !list.empty())
if(key == "3d-render/far-clip-distance" && !list.empty())
{
if(mCamera->getFarClipDistance() != list.at(0).toFloat())
mCamera->setFarClipDistance(list.at(0).toFloat());
}
// minimise unnecessary ogre window creation by updating only when there is a change
if(key == "Video/antialiasing")
if(key == "3d-render/antialiasing")
{
unsigned int aa = mWindow->getFSAA();
unsigned int antialiasing = 0;

@ -1,10 +1,13 @@
#include "dialog.hpp"
#include <algorithm>
#include <QListWidgetItem>
#include <QApplication>
#include <QWidget>
#include <QStackedWidget>
#include <QtGui>
#include <QSplitter>
#include "../../model/settings/usersettings.hpp"
@ -12,8 +15,6 @@
#include <QApplication>
#include <QSplitter>
#include <QTreeView>
#include <QListView>
#include <QTableView>
@ -26,6 +27,10 @@ CSVSettings::Dialog::Dialog(QMainWindow *parent)
{
setWindowTitle(QString::fromUtf8 ("User Settings"));
setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
setMinimumSize (600, 400);
setupDialog();
connect (mPageListWidget,
@ -39,20 +44,14 @@ void CSVSettings::Dialog::slotChangePage
{
mStackedWidget->changePage
(mPageListWidget->row (cur), mPageListWidget->row (prev));
layout()->activate();
setFixedSize(minimumSizeHint());
}
void CSVSettings::Dialog::setupDialog()
{
//create central widget with it's layout and immediate children
QWidget *centralWidget = new QGroupBox (this);
QSplitter *centralWidget = new QSplitter (this);
centralWidget->setSizePolicy (QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
centralWidget->setLayout (new QHBoxLayout());
centralWidget->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Preferred);
setCentralWidget (centralWidget);
setDockOptions (QMainWindow::AllowNestedDocks);
buildPageListWidget (centralWidget);
buildStackedWidget (centralWidget);
@ -64,37 +63,39 @@ void CSVSettings::Dialog::buildPages()
QFontMetrics fm (QApplication::font());
int maxWidth = 1;
foreach (Page *page, SettingWindow::pages())
{
QString pageName = page->objectName();
maxWidth = std::max (maxWidth, fm.width(page->getLabel()));
int textWidth = fm.width(pageName);
new QListWidgetItem (page->getLabel(), mPageListWidget);
new QListWidgetItem (pageName, mPageListWidget);
mPageListWidget->setFixedWidth (textWidth + 50);
mStackedWidget->addWidget (&dynamic_cast<QWidget &>(*(page)));
mStackedWidget->addWidget (page);
}
mPageListWidget->setMaximumWidth (maxWidth + 10);
resize (mStackedWidget->sizeHint());
}
void CSVSettings::Dialog::buildPageListWidget (QWidget *centralWidget)
void CSVSettings::Dialog::buildPageListWidget (QSplitter *centralWidget)
{
mPageListWidget = new QListWidget (centralWidget);
mPageListWidget->setMinimumWidth(50);
mPageListWidget->setSizePolicy (QSizePolicy::Fixed, QSizePolicy::Expanding);
mPageListWidget->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Expanding);
mPageListWidget->setSelectionBehavior (QAbstractItemView::SelectItems);
centralWidget->layout()->addWidget(mPageListWidget);
centralWidget->addWidget(mPageListWidget);
}
void CSVSettings::Dialog::buildStackedWidget (QWidget *centralWidget)
void CSVSettings::Dialog::buildStackedWidget (QSplitter *centralWidget)
{
mStackedWidget = new ResizeableStackedWidget (centralWidget);
mStackedWidget->setSizePolicy (QSizePolicy::Preferred, QSizePolicy::Expanding);
centralWidget->layout()->addWidget (mStackedWidget);
centralWidget->addWidget (mStackedWidget);
}
void CSVSettings::Dialog::closeEvent (QCloseEvent *event)

@ -8,6 +8,7 @@
class QStackedWidget;
class QListWidget;
class QListWidgetItem;
class QSplitter;
namespace CSVSettings {
@ -39,8 +40,8 @@ namespace CSVSettings {
private:
void buildPages();
void buildPageListWidget (QWidget *centralWidget);
void buildStackedWidget (QWidget *centralWidget);
void buildPageListWidget (QSplitter *centralWidget);
void buildStackedWidget (QSplitter *centralWidget);
public slots:

@ -17,7 +17,6 @@ CSVSettings::Frame::Frame (bool isVisible, const QString &title,
{
// must be Page, not a View
setStyleSheet (sInvisibleBoxStyle);
mLayout->setContentsMargins(10, 15, 10, 15);
}
setLayout (mLayout);
@ -39,7 +38,7 @@ void CSVSettings::Frame::hideWidgets()
QWidget *widg = static_cast <QWidget *> (obj);
if (widg->property("sizePolicy").isValid())
widg->setSizePolicy (QSizePolicy::Ignored, QSizePolicy::Ignored);
widg->setSizePolicy (QSizePolicy::Expanding, QSizePolicy::Expanding);
}
layout()->activate();

@ -17,10 +17,9 @@
QMap <CSVSettings::ViewType, CSVSettings::IViewFactory *>
CSVSettings::Page::mViewFactories;
CSVSettings::Page::Page(const QString &pageName,
QList <CSMSettings::Setting *> settingList,
SettingWindow *parent) :
mParent(parent), mIsEditorPage (false), Frame(false, "", parent)
CSVSettings::Page::Page (const QString &pageName, QList <CSMSettings::Setting *> settingList,
SettingWindow *parent, const QString& label)
: mParent(parent), mIsEditorPage (false), Frame(false, "", parent), mLabel (label)
{
setObjectName (pageName);
@ -104,3 +103,8 @@ void CSVSettings::Page::buildFactories()
mViewFactories[ViewType_List] = new ListViewFactory (this);
mViewFactories[ViewType_Range] = new RangeViewFactory (this);
}
QString CSVSettings::Page::getLabel() const
{
return mLabel;
}

@ -25,11 +25,11 @@ namespace CSVSettings
SettingWindow *mParent;
static QMap <ViewType, IViewFactory *> mViewFactories;
bool mIsEditorPage;
QString mLabel;
public:
explicit Page(const QString &pageName,
QList <CSMSettings::Setting *> settingList,
SettingWindow *parent);
Page (const QString &pageName, QList <CSMSettings::Setting *> settingList,
SettingWindow *parent, const QString& label);
///Creates a new view based on the passed setting and adds it to
///the page.
@ -42,6 +42,8 @@ namespace CSVSettings
///returns the list of views associated with the page
const QList <View *> &views () const { return mViews; }
QString getLabel() const;
private:
///Creates views based on the passed setting list

@ -126,8 +126,6 @@ void CSVSettings::RangeView::buildSpinBox (CSMSettings::Setting *setting)
mRangeWidget->setProperty ("minimum", setting->minimum());
mRangeWidget->setProperty ("maximum", setting->maximum());
mRangeWidget->setProperty ("singleStep", setting->singleStep());
mRangeWidget->setProperty ("specialValueText",
setting->specialValueText());
}
mRangeWidget->setProperty ("prefix", setting->prefix());

@ -34,7 +34,6 @@ void CSVSettings::ResizeableStackedWidget::changePage
curPage->showWidgets();
layout()->activate();
setFixedSize(minimumSizeHint());
setCurrentIndex (current);
}

@ -20,9 +20,9 @@ void CSVSettings::SettingWindow::createPages()
foreach (const QString &pageName, pageMap.keys())
{
QList <CSMSettings::Setting *> pageSettings = pageMap.value (pageName);
QList <CSMSettings::Setting *> pageSettings = pageMap.value (pageName).second;
mPages.append (new Page (pageName, pageSettings, this));
mPages.append (new Page (pageName, pageSettings, this, pageMap.value (pageName).first));
for (int i = 0; i < pageSettings.size(); i++)
{

@ -17,8 +17,11 @@ CSVSettings::View::View(CSMSettings::Setting *setting,
mIsMultiValue (setting->isMultiValue()),
mViewKey (setting->page() + '/' + setting->name()),
mSerializable (setting->serializable()),
Frame(true, setting->name(), parent)
Frame(true, setting->getLabel(), parent)
{
if (!setting->getToolTip().isEmpty())
setToolTip (setting->getToolTip());
setObjectName (setting->name());
buildView();
buildModel (setting);

@ -5,7 +5,7 @@
CSVWorld::IdTypeDelegate::IdTypeDelegate
(const ValueList &values, const IconList &icons, CSMDoc::Document& document, QObject *parent)
: DataDisplayDelegate (values, icons, document,
"Display Format", "Referenceable ID Type Display",
"records", "type-format",
parent)
{}

@ -11,7 +11,7 @@ CSVWorld::RecordStatusDelegate::RecordStatusDelegate(const ValueList& values,
const IconList & icons,
CSMDoc::Document& document, QObject *parent)
: DataDisplayDelegate (values, icons, document,
"Display Format", "Record Status Display",
"records", "status-format",
parent)
{}

@ -21,7 +21,7 @@ source_group(game FILES ${GAME} ${GAME_HEADER})
add_openmw_dir (mwrender
renderingmanager debugging sky camera animation npcanimation creatureanimation activatoranimation
actors objects renderinginterface localmap occlusionquery water shadows
characterpreview globalmap videoplayer ripplesimulation refraction
characterpreview globalmap ripplesimulation refraction
terrainstorage renderconst effectmanager weaponanimation
)
@ -38,13 +38,13 @@ add_openmw_dir (mwgui
itemselection spellbuyingwindow loadingscreen levelupdialog waitdialog spellcreationdialog
enchantingdialog trainingwindow travelwindow exposedwindow cursor spellicons
merchantrepair repair soulgemdialog companionwindow bookpage journalviewmodel journalbooks
keywordsearch itemmodel containeritemmodel inventoryitemmodel sortfilteritemmodel itemview
itemmodel containeritemmodel inventoryitemmodel sortfilteritemmodel itemview
tradeitemmodel companionitemmodel pickpocketitemmodel controllers savegamedialog
recharge mode videowidget backgroundimage itemwidget screenfader debugwindow
)
add_openmw_dir (mwdialogue
dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper
dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper hypertextparser keywordsearch
)
add_openmw_dir (mwscript
@ -55,7 +55,7 @@ add_openmw_dir (mwscript
)
add_openmw_dir (mwsound
soundmanagerimp openal_output ffmpeg_decoder sound sound_decoder sound_output loudness libavwrapper
soundmanagerimp openal_output ffmpeg_decoder sound sound_decoder sound_output loudness libavwrapper movieaudiofactory
)
add_openmw_dir (mwworld
@ -132,6 +132,7 @@ target_link_libraries(openmw
${MYGUI_LIBRARIES}
${SDL2_LIBRARY}
${MYGUI_PLATFORM_LIBRARIES}
"ogre-ffmpeg-videoplayer"
"oics"
"sdl4ogre"
components

@ -42,6 +42,7 @@
#include "../mwmechanics/npcstats.hpp"
#include "filter.hpp"
#include "hypertextparser.hpp"
namespace MWDialogue
{
@ -82,42 +83,27 @@ namespace MWDialogue
void DialogueManager::parseText (const std::string& text)
{
std::vector<HyperTextToken> hypertext = ParseHyperText(text);
std::vector<HyperTextParser::Token> hypertext = HyperTextParser::parseHyperText(text);
//calculation of standard form fir all hyperlinks
for (size_t i = 0; i < hypertext.size(); ++i)
for (std::vector<HyperTextParser::Token>::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok)
{
if (hypertext[i].mLink)
std::string topicId = Misc::StringUtils::lowerCase(tok->mText);
if (tok->isExplicitLink())
{
size_t asterisk_count = MWDialogue::RemovePseudoAsterisks(hypertext[i].mText);
// calculation of standard form for all hyperlinks
size_t asterisk_count = HyperTextParser::removePseudoAsterisks(topicId);
for(; asterisk_count > 0; --asterisk_count)
hypertext[i].mText.append("*");
topicId.append("*");
hypertext[i].mText = mTranslationDataStorage.topicStandardForm(hypertext[i].mText);
}
topicId = mTranslationDataStorage.topicStandardForm(topicId);
}
for (size_t i = 0; i < hypertext.size(); ++i)
{
std::list<std::string>::iterator it;
for(it = mActorKnownTopics.begin(); it != mActorKnownTopics.end(); ++it)
{
if (hypertext[i].mLink)
{
if( hypertext[i].mText == *it )
{
mKnownTopics[hypertext[i].mText] = true;
}
}
else if( !mTranslationDataStorage.hasTranslation() )
{
size_t pos = Misc::StringUtils::lowerCase(hypertext[i].mText).find(*it, 0);
if(pos !=std::string::npos)
{
mKnownTopics[*it] = true;
}
}
}
if (tok->isImplicitKeyword() && mTranslationDataStorage.hasTranslation())
continue;
if (std::find(mActorKnownTopics.begin(), mActorKnownTopics.end(), topicId) != mActorKnownTopics.end())
mKnownTopics[topicId] = true;
}
updateTopics();
@ -724,55 +710,4 @@ namespace MWDialogue
mLastTopic, actor.getClass().getName(actor));
}
}
std::vector<HyperTextToken> ParseHyperText(const std::string& text)
{
std::vector<HyperTextToken> result;
MyGUI::UString utext(text);
size_t pos_end, iteration_pos = 0;
for(;;)
{
size_t pos_begin = utext.find('@', iteration_pos);
if (pos_begin != std::string::npos)
pos_end = utext.find('#', pos_begin);
if (pos_begin != std::string::npos && pos_end != std::string::npos)
{
result.push_back( HyperTextToken(utext.substr(iteration_pos, pos_begin - iteration_pos), false) );
std::string link = utext.substr(pos_begin + 1, pos_end - pos_begin - 1);
result.push_back( HyperTextToken(link, true) );
iteration_pos = pos_end + 1;
}
else
{
result.push_back( HyperTextToken(utext.substr(iteration_pos), false) );
break;
}
}
return result;
}
size_t RemovePseudoAsterisks(std::string& phrase)
{
size_t pseudoAsterisksCount = 0;
if( !phrase.empty() )
{
std::string::reverse_iterator rit = phrase.rbegin();
const char specialPseudoAsteriskCharacter = 127;
while( rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter )
{
pseudoAsterisksCount++;
++rit;
}
}
phrase = phrase.substr(0, phrase.length() - pseudoAsterisksCount);
return pseudoAsterisksCount;
}
}

@ -103,21 +103,6 @@ namespace MWDialogue
/// Removes the last added topic response for the given actor from the journal
virtual void clearInfoActor (const MWWorld::Ptr& actor) const;
};
struct HyperTextToken
{
HyperTextToken(const std::string& text, bool link) : mText(text), mLink(link) {}
std::string mText;
bool mLink;
};
// In translations (at least Russian) the links are marked with @#, so
// it should be a function to parse it
std::vector<HyperTextToken> ParseHyperText(const std::string& text);
size_t RemovePseudoAsterisks(std::string& phrase);
}
#endif

@ -0,0 +1,89 @@
#include <components/esm/loaddial.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/store.hpp"
#include "../mwworld/esmstore.hpp"
#include "keywordsearch.hpp"
#include "hypertextparser.hpp"
namespace MWDialogue
{
namespace HyperTextParser
{
std::vector<Token> parseHyperText(const std::string & text)
{
std::vector<Token> result;
size_t pos_end, iteration_pos = 0;
for(;;)
{
size_t pos_begin = text.find('@', iteration_pos);
if (pos_begin != std::string::npos)
pos_end = text.find('#', pos_begin);
if (pos_begin != std::string::npos && pos_end != std::string::npos)
{
if (pos_begin != iteration_pos)
tokenizeKeywords(text.substr(iteration_pos, pos_begin - iteration_pos), result);
std::string link = text.substr(pos_begin + 1, pos_end - pos_begin - 1);
result.push_back(Token(link, Token::ExplicitLink));
iteration_pos = pos_end + 1;
}
else
{
if (iteration_pos != text.size())
tokenizeKeywords(text.substr(iteration_pos), result);
break;
}
}
return result;
}
void tokenizeKeywords(const std::string & text, std::vector<Token> & tokens)
{
const MWWorld::Store<ESM::Dialogue> & dialogs =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Dialogue>();
std::list<std::string> keywordList;
for (MWWorld::Store<ESM::Dialogue>::iterator it = dialogs.begin(); it != dialogs.end(); ++it)
keywordList.push_back(Misc::StringUtils::lowerCase(it->mId));
keywordList.sort(Misc::StringUtils::ciLess);
KeywordSearch<std::string, int /*unused*/> keywordSearch;
KeywordSearch<std::string, int /*unused*/>::Match match;
for (std::list<std::string>::const_iterator it = keywordList.begin(); it != keywordList.end(); ++it)
keywordSearch.seed(*it, 0 /*unused*/);
for (std::string::const_iterator it = text.begin(); it != text.end() && keywordSearch.search(it, text.end(), match, text.begin()); it = match.mEnd)
tokens.push_back(Token(std::string(match.mBeg, match.mEnd), Token::ImplicitKeyword));
}
size_t removePseudoAsterisks(std::string & phrase)
{
size_t pseudoAsterisksCount = 0;
if( !phrase.empty() )
{
std::string::reverse_iterator rit = phrase.rbegin();
const char specialPseudoAsteriskCharacter = 127;
while( rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter )
{
pseudoAsterisksCount++;
++rit;
}
}
phrase = phrase.substr(0, phrase.length() - pseudoAsterisksCount);
return pseudoAsterisksCount;
}
}
}

@ -0,0 +1,36 @@
#ifndef GAME_MWDIALOGUE_HYPERTEXTPARSER_H
#define GAME_MWDIALOGUE_HYPERTEXTPARSER_H
#include <string>
#include <vector>
namespace MWDialogue
{
namespace HyperTextParser
{
struct Token
{
enum Type
{
ExplicitLink, // enclosed in @#
ImplicitKeyword
};
Token(const std::string & text, Type type) : mText(text), mType(type) {}
bool isExplicitLink() { return mType == ExplicitLink; }
bool isImplicitKeyword() { return mType == ImplicitKeyword; }
std::string mText;
Type mType;
};
// In translations (at least Russian) the links are marked with @#, so
// it should be a function to parse it
std::vector<Token> parseHyperText(const std::string & text);
void tokenizeKeywords(const std::string & text, std::vector<Token> & tokens);
size_t removePseudoAsterisks(std::string & phrase);
}
}
#endif

@ -1,5 +1,5 @@
#ifndef MWGUI_KEYWORDSEARCH_H
#define MWGUI_KEYWORDSEARCH_H
#ifndef GAME_MWDIALOGUE_KEYWORDSEARCH_H
#define GAME_MWDIALOGUE_KEYWORDSEARCH_H
#include <map>
#include <locale>
@ -9,7 +9,7 @@
#include <components/misc/stringops.hpp>
namespace MWGui
namespace MWDialogue
{
template <typename string_t, typename value_t>
@ -141,7 +141,6 @@ public:
match.mValue = candidate->second.mValue;
match.mBeg = i;
match.mEnd = k;
return true;
}
}

@ -134,11 +134,12 @@ namespace MWGui
for (MWMechanics::Alchemy::TToolsIterator iter (mAlchemy.beginTools());
iter!=mAlchemy.endTools() && index<static_cast<int> (mApparatus.size()); ++iter, ++index)
{
mApparatus.at (index)->setItem(*iter);
mApparatus.at (index)->clearUserStrings();
if (!iter->isEmpty())
{
mApparatus.at (index)->setUserString ("ToolTipType", "ItemPtr");
mApparatus.at (index)->setUserData (*iter);
mApparatus.at (index)->setItem(*iter);
}
}
@ -173,6 +174,11 @@ namespace MWGui
void AlchemyWindow::update()
{
std::string suggestedName = mAlchemy.suggestPotionName();
if (suggestedName != mSuggestedPotionName)
mNameEdit->setCaptionWithReplacing(suggestedName);
mSuggestedPotionName = suggestedName;
mSortModel->clearDragItems();
MWMechanics::Alchemy::TIngredientsIterator it = mAlchemy.beginIngredients ();

@ -23,6 +23,8 @@ namespace MWGui
virtual void exit();
private:
std::string mSuggestedPotionName;
ItemView* mItemView;
SortFilterItemModel* mSortModel;

@ -6,7 +6,7 @@
#include "bookpage.hpp"
#include "keywordsearch.hpp"
#include "../mwdialogue/keywordsearch.hpp"
namespace Gui
{
@ -76,7 +76,7 @@ namespace MWGui
virtual void activated ();
};
typedef KeywordSearch <std::string, intptr_t> KeywordSearchT;
typedef MWDialogue::KeywordSearch <std::string, intptr_t> KeywordSearchT;
struct DialogueText
{

@ -12,9 +12,9 @@
#include "../mwbase/journal.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwdialogue/journalentry.hpp"
#include "keywordsearch.hpp"
#include "../mwdialogue/journalentry.hpp"
#include "../mwdialogue/keywordsearch.hpp"
namespace MWGui {
@ -22,7 +22,7 @@ struct JournalViewModelImpl;
struct JournalViewModelImpl : JournalViewModel
{
typedef KeywordSearch <std::string, intptr_t> KeywordSearchT;
typedef MWDialogue::KeywordSearch <std::string, intptr_t> KeywordSearchT;
mutable bool mKeywordSearchLoaded;
mutable KeywordSearchT mKeywordSearch;

@ -1,5 +1,7 @@
#include "videowidget.hpp"
#include "../mwsound/movieaudiofactory.hpp"
namespace MWGui
{
@ -10,6 +12,7 @@ VideoWidget::VideoWidget()
void VideoWidget::playVideo(const std::string &video)
{
mPlayer.setAudioFactory(new MWSound::MovieAudioFactory());
mPlayer.playVideo(video);
setImageTexture(mPlayer.getTextureName());
@ -36,4 +39,9 @@ void VideoWidget::stop()
mPlayer.close();
}
bool VideoWidget::hasAudioStream()
{
return mPlayer.hasAudioStream();
}
}

@ -3,7 +3,7 @@
#include <MyGUI_ImageBox.h>
#include "../mwrender/videoplayer.hpp"
#include <extern/ogre-ffmpeg-videoplayer/videoplayer.hpp>
namespace MWGui
{
@ -26,11 +26,14 @@ namespace MWGui
/// @return Is the video still playing?
bool update();
/// Return true if a video is currently playing and it has an audio stream.
bool hasAudioStream();
/// Stop video and free resources (done automatically on destruction)
void stop();
private:
MWRender::VideoPlayer mPlayer;
Video::VideoPlayer mPlayer;
};
}

@ -1664,6 +1664,10 @@ namespace MWGui
bool cursorWasVisible = mCursorVisible;
setCursorVisible(false);
if (mVideoWidget->hasAudioStream())
MWBase::Environment::get().getSoundManager()->pauseSounds(
MWBase::SoundManager::Play_TypeMask&(~MWBase::SoundManager::Play_TypeMovie));
while (mVideoWidget->update() && !MWBase::Environment::get().getStateManager()->hasQuitRequest())
{
MWBase::Environment::get().getInputManager()->update(0, true, false);
@ -1672,6 +1676,8 @@ namespace MWGui
}
mVideoWidget->stop();
MWBase::Environment::get().getSoundManager()->resumeSounds();
setCursorVisible(cursorWasVisible);
// Restore normal rendering

@ -205,7 +205,7 @@ void MWMechanics::Alchemy::updateEffects()
}
}
const ESM::Potion *MWMechanics::Alchemy::getRecord() const
const ESM::Potion *MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) const
{
const MWWorld::Store<ESM::Potion> &potions =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Potion>();
@ -216,6 +216,18 @@ const ESM::Potion *MWMechanics::Alchemy::getRecord() const
if (iter->mEffects.mList.size() != mEffects.size())
continue;
if (iter->mName != toFind.mName
|| iter->mScript != toFind.mScript
|| iter->mData.mWeight != toFind.mData.mWeight
|| iter->mData.mValue != toFind.mData.mValue
|| iter->mData.mAutoCalc != toFind.mData.mAutoCalc)
continue;
// Don't choose an ID that came from the content files, would have unintended side effects
// where alchemy can be used to produce quest-relevant items
if (!potions.isDynamic(iter->mId))
continue;
bool mismatch = false;
for (int i=0; i<static_cast<int> (iter->mEffects.mList.size()); ++i)
@ -266,10 +278,6 @@ void MWMechanics::Alchemy::removeIngredients()
void MWMechanics::Alchemy::addPotion (const std::string& name)
{
const ESM::Potion *record = getRecord();
if (!record)
{
ESM::Potion newRecord;
newRecord.mData.mWeight = 0;
@ -288,15 +296,16 @@ void MWMechanics::Alchemy::addPotion (const std::string& name)
int index = static_cast<int> (std::rand()/(static_cast<double> (RAND_MAX)+1)*6);
assert (index>=0 && index<6);
static const char *name[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" };
static const char *meshes[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" };
newRecord.mModel = "m\\misc_potion_" + std::string (name[index]) + "_01.nif";
newRecord.mIcon = "m\\tx_potion_" + std::string (name[index]) + "_01.dds";
newRecord.mModel = "m\\misc_potion_" + std::string (meshes[index]) + "_01.nif";
newRecord.mIcon = "m\\tx_potion_" + std::string (meshes[index]) + "_01.dds";
newRecord.mEffects.mList = mEffects;
const ESM::Potion* record = getRecord(newRecord);
if (!record)
record = MWBase::Environment::get().getWorld()->createRecord (newRecord);
}
mAlchemist.getClass().getContainerStore (mAlchemist).add (record->mId, 1, mAlchemist);
}
@ -436,14 +445,6 @@ MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::endEffects() const
return mEffects.end();
}
std::string MWMechanics::Alchemy::getPotionName() const
{
if (const ESM::Potion *potion = getRecord())
return potion->mName;
return "";
}
MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& name)
{
if (mTools[ESM::Apparatus::MortarPestle].isEmpty())
@ -452,7 +453,7 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& na
if (countIngredients()<2)
return Result_LessThanTwoIngredients;
if (name.empty() && getPotionName().empty())
if (name.empty())
return Result_NoName;
if (listEffects().empty())
@ -479,3 +480,14 @@ MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& na
return Result_Success;
}
std::string MWMechanics::Alchemy::suggestPotionName()
{
std::set<MWMechanics::EffectKey> effects = listEffects();
if (effects.empty())
return "";
int effectId = effects.begin()->mId;
return MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
ESM::MagicEffect::effectIdToString(effectId))->getString();
}

@ -56,8 +56,9 @@ namespace MWMechanics
void updateEffects();
const ESM::Potion *getRecord() const;
///< Return existing record for created potion (may return 0)
const ESM::Potion *getRecord(const ESM::Potion& toFind) const;
///< Try to find a potion record similar to \a toFind in the record store, or return 0 if not found
/// \note Does not account for record ID, model or icon
void removeIngredients();
///< Remove selected ingredients from alchemist's inventory, cleanup selected ingredients and
@ -108,15 +109,13 @@ namespace MWMechanics
void removeIngredient (int index);
///< Remove ingredient from slot (calling this function on an empty slot is a no-op).
std::string getPotionName() const;
///< Return the name of the potion that would be created when calling create (if a record for such
/// a potion already exists) or return an empty string.
std::string suggestPotionName ();
///< Suggest a name for the potion, based on the current effects
Result create (const std::string& name);
///< Try to create a potion from the ingredients, place it in the inventory of the alchemist and
/// adjust the skills of the alchemist accordingly.
/// \param name must not be an empty string, unless there is already a potion record (
/// getPotionName() does not return an empty string).
/// \param name must not be an empty string, or Result_NoName is returned
};
}

@ -387,7 +387,8 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat
{
float vel, speedmult = 1.0f;
bool isrunning = mPtr.getClass().getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run);
bool isrunning = mPtr.getClass().getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run)
&& !MWBase::Environment::get().getWorld()->isFlying(mPtr);
// For non-flying creatures, MW uses the Walk animation to calculate the animation velocity
// even if we are running. This must be replicated, otherwise the observed speed would differ drastically.
@ -1269,9 +1270,10 @@ void CharacterController::update(float duration)
{
bool onground = world->isOnGround(mPtr);
bool inwater = world->isSwimming(mPtr);
bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run);
bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak);
bool flying = world->isFlying(mPtr);
// Can't run while flying (see speed formula in Npc/Creature::getSpeed)
bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying;
CreatureStats &stats = cls.getCreatureStats(mPtr);
//Force Jump Logic

@ -298,7 +298,7 @@ void Animation::addAnimSource(const std::string &model)
}
}
if (grp == 0 && dstval->getNode()->getName() == "Bip01")
if (grp == 0 && (dstval->getNode()->getName() == "Bip01" || dstval->getNode()->getName() == "Root Bone"))
{
mNonAccumRoot = dstval->getNode();
mAccumRoot = mNonAccumRoot->getParent();

@ -200,10 +200,10 @@ NpcAnimation::NpcAnimation(const MWWorld::Ptr& ptr, Ogre::SceneNode* node, int v
mPartPriorities[i] = 0;
}
updateNpcBase();
if (!disableListener)
mPtr.getClass().getInventoryStore(mPtr).setListener(this, mPtr);
updateNpcBase();
}
void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode)

File diff suppressed because it is too large Load Diff

@ -1,37 +0,0 @@
#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H
#include <string>
namespace MWRender
{
struct VideoState;
/**
* @brief Plays a video on an Ogre texture.
*/
class VideoPlayer
{
public:
VideoPlayer();
~VideoPlayer();
void playVideo (const std::string& resourceName);
void update();
void close();
bool isPlaying();
std::string getTextureName();
int getVideoWidth();
int getVideoHeight();
private:
VideoState* mState;
};
}
#endif

@ -7,7 +7,7 @@
extern "C" {
#ifndef HAVE_LIBSWRESAMPLE
/* FIXME: remove this section once libswresample is available on all platforms */
// FIXME: remove this section once libswresample is packaged for Debian
int swr_init(AVAudioResampleContext *avr);
void swr_free(AVAudioResampleContext **avr);
int swr_convert( AVAudioResampleContext *avr, uint8_t** output, int out_samples, const uint8_t** input, int in_samples);

@ -4,7 +4,9 @@
// FIXME: This can't be right? The headers refuse to build without UINT64_C,
// which only gets defined in stdint.h in either C99 mode or with this macro
// defined...
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
#include <stdint.h>
extern "C"
{

@ -1,7 +1,9 @@
#ifndef HAVE_LIBSWRESAMPLE
extern "C"
{
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
#include <stdint.h>
#include <libavcodec/avcodec.h>
@ -16,7 +18,7 @@ extern "C"
#include <libavresample/avresample.h>
#include <libavutil/opt.h>
/* FIXME: delete this file once libswresample is available on all platforms */
/* FIXME: delete this file once libswresample is packaged for Debian */
int swr_init(AVAudioResampleContext *avr) { return 1; }

@ -0,0 +1,173 @@
#include "movieaudiofactory.hpp"
#include <extern/ogre-ffmpeg-videoplayer/audiodecoder.hpp>
#include <extern/ogre-ffmpeg-videoplayer/videostate.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/soundmanager.hpp"
#include "sound_decoder.hpp"
#include "sound.hpp"
namespace MWSound
{
class MovieAudioDecoder;
class MWSoundDecoderBridge : public Sound_Decoder
{
public:
MWSoundDecoderBridge(MWSound::MovieAudioDecoder* decoder)
: mDecoder(decoder)
{
}
private:
MWSound::MovieAudioDecoder* mDecoder;
virtual void open(const std::string &fname);
virtual void close();
virtual void rewind();
virtual std::string getName();
virtual void getInfo(int *samplerate, ChannelConfig *chans, SampleType *type);
virtual size_t read(char *buffer, size_t bytes);
virtual size_t getSampleOffset();
};
class MovieAudioDecoder : public Video::MovieAudioDecoder
{
public:
MovieAudioDecoder(Video::VideoState *videoState)
: Video::MovieAudioDecoder(videoState)
{
mDecoderBridge.reset(new MWSoundDecoderBridge(this));
}
size_t getSampleOffset()
{
ssize_t clock_delay = (mFrameSize-mFramePos) / mAVStream->codec->channels /
av_get_bytes_per_sample(mAVStream->codec->sample_fmt);
return (size_t)(mAudioClock*mAVStream->codec->sample_rate) - clock_delay;
}
std::string getStreamName()
{
return mVideoState->stream->getName();
}
private:
// MovieAudioDecoder overrides
virtual double getAudioClock()
{
return mAudioTrack->getTimeOffset();
}
virtual void adjustAudioSettings(AVSampleFormat& sampleFormat, uint64_t& channelLayout, int& sampleRate)
{
if (sampleFormat == AV_SAMPLE_FMT_U8P)
sampleFormat = AV_SAMPLE_FMT_U8;
else if (sampleFormat == AV_SAMPLE_FMT_S16P)
sampleFormat = AV_SAMPLE_FMT_S16;
else if (sampleFormat == AV_SAMPLE_FMT_FLTP)
sampleFormat = AV_SAMPLE_FMT_FLT;
else
sampleFormat = AV_SAMPLE_FMT_FLT;
if (channelLayout != AV_CH_LAYOUT_MONO
&& channelLayout != AV_CH_LAYOUT_5POINT1
&& channelLayout != AV_CH_LAYOUT_7POINT1
&& channelLayout != AV_CH_LAYOUT_STEREO
&& channelLayout != AV_CH_LAYOUT_QUAD)
channelLayout = AV_CH_LAYOUT_STEREO;
}
public:
~MovieAudioDecoder()
{
mAudioTrack.reset();
mDecoderBridge.reset();
}
MWBase::SoundPtr mAudioTrack;
boost::shared_ptr<MWSoundDecoderBridge> mDecoderBridge;
};
void MWSoundDecoderBridge::open(const std::string &fname)
{
throw std::runtime_error("unimplemented");
}
void MWSoundDecoderBridge::close() {}
void MWSoundDecoderBridge::rewind() {}
std::string MWSoundDecoderBridge::getName()
{
return mDecoder->getStreamName();
}
void MWSoundDecoderBridge::getInfo(int *samplerate, ChannelConfig *chans, SampleType *type)
{
*samplerate = mDecoder->getOutputSampleRate();
uint64_t outputChannelLayout = mDecoder->getOutputChannelLayout();
if (outputChannelLayout == AV_CH_LAYOUT_MONO)
*chans = ChannelConfig_Mono;
else if (outputChannelLayout == AV_CH_LAYOUT_5POINT1)
*chans = ChannelConfig_5point1;
else if (outputChannelLayout == AV_CH_LAYOUT_7POINT1)
*chans = ChannelConfig_7point1;
else if (outputChannelLayout == AV_CH_LAYOUT_STEREO)
*chans = ChannelConfig_Stereo;
else if (outputChannelLayout == AV_CH_LAYOUT_QUAD)
*chans = ChannelConfig_Quad;
else
{
std::stringstream error;
error << "Unsupported channel layout: " << outputChannelLayout;
throw std::runtime_error(error.str());
}
AVSampleFormat outputSampleFormat = mDecoder->getOutputSampleFormat();
if (outputSampleFormat == AV_SAMPLE_FMT_U8)
*type = SampleType_UInt8;
else if (outputSampleFormat == AV_SAMPLE_FMT_FLT)
*type = SampleType_Float32;
else if (outputSampleFormat == AV_SAMPLE_FMT_S16)
*type = SampleType_Int16;
else
{
char str[1024];
av_get_sample_fmt_string(str, sizeof(str), outputSampleFormat);
throw std::runtime_error(std::string("Unsupported sample format: ") + str);
}
}
size_t MWSoundDecoderBridge::read(char *buffer, size_t bytes)
{
return mDecoder->read(buffer, bytes);
}
size_t MWSoundDecoderBridge::getSampleOffset()
{
return mDecoder->getSampleOffset();
}
boost::shared_ptr<Video::MovieAudioDecoder> MovieAudioFactory::createDecoder(Video::VideoState* videoState)
{
boost::shared_ptr<MWSound::MovieAudioDecoder> decoder(new MWSound::MovieAudioDecoder(videoState));
decoder->setupFormat();
MWBase::SoundPtr sound = MWBase::Environment::get().getSoundManager()->playTrack(decoder->mDecoderBridge, MWBase::SoundManager::Play_TypeMovie);
if (!sound.get())
{
decoder.reset();
return decoder;
}
decoder->mAudioTrack = sound;
return decoder;
}
}

@ -0,0 +1,16 @@
#ifndef OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H
#define OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H
#include <extern/ogre-ffmpeg-videoplayer/audiofactory.hpp>
namespace MWSound
{
class MovieAudioFactory : public Video::MovieAudioFactory
{
virtual boost::shared_ptr<Video::MovieAudioDecoder> createDecoder(Video::VideoState* videoState);
};
}
#endif

@ -135,6 +135,7 @@ void ESMStore::setUp()
mSkills.setUp();
mMagicEffects.setUp();
mAttributes.setUp();
mDialogs.setUp();
}
int ESMStore::countSavedGameRecords() const

@ -1174,6 +1174,25 @@ namespace MWWorld
mShared.erase(mShared.begin() + mStatic.size(), mShared.end());
}
template<>
inline void Store<ESM::Dialogue>::setUp()
{
// DialInfos marked as deleted are kept during the loading phase, so that the linked list
// structure is kept intact for inserting further INFOs. Delete them now that loading is done.
for (Static::iterator it = mStatic.begin(); it != mStatic.end(); ++it)
{
ESM::Dialogue& dial = it->second;
dial.clearDeletedInfos();
}
mShared.clear();
mShared.reserve(mStatic.size());
std::map<std::string, ESM::Dialogue>::iterator it = mStatic.begin();
for (; it != mStatic.end(); ++it) {
mShared.push_back(&(it->second));
}
}
} //end namespace
#endif

@ -19,7 +19,7 @@ add_component_dir (bsa
)
add_component_dir (nif
controlled effect niftypes record controller extra node record_ptr data niffile property nifkey data node
controlled effect niftypes record controller extra node record_ptr data niffile property nifkey data node base
)
add_component_dir (nifcache

@ -226,7 +226,7 @@ namespace Compiler
extensions.registerInstruction ("togglefullhelp", "", opcodeToggleFullHelp);
extensions.registerInstruction ("tfh", "", opcodeToggleFullHelp);
extensions.registerInstruction ("showmap", "S", opcodeShowMap);
extensions.registerInstruction ("showmap", "Sxxxx", opcodeShowMap);
extensions.registerInstruction ("fillmap", "", opcodeFillMap);
extensions.registerInstruction ("menutest", "/l", opcodeMenuTest);
extensions.registerInstruction ("togglemenus", "", opcodeToggleMenus);

@ -110,4 +110,15 @@ void Dialogue::readInfo(ESMReader &esm, bool merge)
std::cerr << "Failed to insert info " << id << std::endl;
}
void Dialogue::clearDeletedInfos()
{
for (InfoContainer::iterator it = mInfo.begin(); it != mInfo.end(); )
{
if (it->mQuestStatus == DialInfo::QS_Deleted)
it = mInfo.erase(it);
else
++it;
}
}
}

@ -47,6 +47,9 @@ struct Dialogue
void load(ESMReader &esm);
void save(ESMWriter &esm) const;
/// Remove all INFOs marked as QS_Deleted from mInfos.
void clearDeletedInfos();
/// Read the next info record
/// @param merge Merge with existing list, or just push each record to the end of the list?
void readInfo (ESM::ESMReader& esm, bool merge);

@ -0,0 +1,91 @@
///This file holds the main classes of NIF Records used by everything else.
#ifndef OPENMW_COMPONENTS_NIF_BASE_HPP
#define OPENMW_COMPONENTS_NIF_BASE_HPP
#include "record.hpp"
#include "niffile.hpp"
#include "recordptr.hpp"
#include "nifstream.hpp"
#include "nifkey.hpp"
namespace Nif
{
/** A record that can have extra data. The extra data objects
themselves descend from the Extra class, and all the extra data
connected to an object form a linked list
*/
class Extra : public Record
{
public:
ExtraPtr extra;
void read(NIFStream *nif) { extra.read(nif); }
void post(NIFFile *nif) { extra.post(nif); }
};
class Controller : public Record
{
public:
ControllerPtr next;
int flags;
float frequency, phase;
float timeStart, timeStop;
ControlledPtr target;
void read(NIFStream *nif)
{
next.read(nif);
flags = nif->getUShort();
frequency = nif->getFloat();
phase = nif->getFloat();
timeStart = nif->getFloat();
timeStop = nif->getFloat();
target.read(nif);
}
void post(NIFFile *nif)
{
Record::post(nif);
next.post(nif);
target.post(nif);
}
};
/// Anything that has a controller
class Controlled : public Extra
{
public:
ControllerPtr controller;
void read(NIFStream *nif)
{
Extra::read(nif);
controller.read(nif);
}
void post(NIFFile *nif)
{
Extra::post(nif);
controller.post(nif);
}
};
/// Has name, extra-data and controller
class Named : public Controlled
{
public:
std::string name;
void read(NIFStream *nif)
{
name = nif->getString();
Controlled::read(nif);
}
};
typedef Named NiSequenceStreamHelper;
} // Namespace
#endif

@ -24,44 +24,70 @@
#ifndef OPENMW_COMPONENTS_NIF_CONTROLLED_HPP
#define OPENMW_COMPONENTS_NIF_CONTROLLED_HPP
#include "extra.hpp"
#include "controller.hpp"
#include "base.hpp"
namespace Nif
{
/// Anything that has a controller
class Controlled : public Extra
class NiSourceTexture : public Named
{
public:
ControllerPtr controller;
// Is this an external (references a separate texture file) or
// internal (data is inside the nif itself) texture?
bool external;
std::string filename; // In case of external textures
NiPixelDataPtr data; // In case of internal textures
/* Pixel layout
0 - Palettised
1 - High color 16
2 - True color 32
3 - Compressed
4 - Bumpmap
5 - Default */
int pixel;
/* Mipmap format
0 - no
1 - yes
2 - default */
int mipmap;
/* Alpha
0 - none
1 - binary
2 - smooth
3 - default (use material alpha, or multiply material with texture if present)
*/
int alpha;
void read(NIFStream *nif)
{
Extra::read(nif);
controller.read(nif);
}
Named::read(nif);
void post(NIFFile *nif)
external = !!nif->getChar();
if(external)
filename = nif->getString();
else
{
Extra::post(nif);
controller.post(nif);
nif->getChar(); // always 1
data.read(nif);
}
};
/// Has name, extra-data and controller
class Named : public Controlled
{
public:
std::string name;
pixel = nif->getInt();
mipmap = nif->getInt();
alpha = nif->getInt();
void read(NIFStream *nif)
nif->getChar(); // always 1
}
void post(NIFFile *nif)
{
name = nif->getString();
Controlled::read(nif);
Named::post(nif);
data.post(nif);
}
};
typedef Named NiSequenceStreamHelper;
class NiParticleGrowFade : public Controlled
{
@ -147,5 +173,7 @@ public:
}
};
} // Namespace
#endif

@ -24,44 +24,11 @@
#ifndef OPENMW_COMPONENTS_NIF_CONTROLLER_HPP
#define OPENMW_COMPONENTS_NIF_CONTROLLER_HPP
#include "record.hpp"
#include "niffile.hpp"
#include "recordptr.hpp"
#include "base.hpp"
namespace Nif
{
class Controller : public Record
{
public:
ControllerPtr next;
int flags;
float frequency, phase;
float timeStart, timeStop;
ControlledPtr target;
void read(NIFStream *nif)
{
next.read(nif);
flags = nif->getUShort();
frequency = nif->getFloat();
phase = nif->getFloat();
timeStart = nif->getFloat();
timeStop = nif->getFloat();
target.read(nif);
}
void post(NIFFile *nif)
{
Record::post(nif);
next.post(nif);
target.post(nif);
}
};
class NiParticleSystemController : public Controller
{
public:

@ -24,73 +24,11 @@
#ifndef OPENMW_COMPONENTS_NIF_DATA_HPP
#define OPENMW_COMPONENTS_NIF_DATA_HPP
#include "controlled.hpp"
#include "nifstream.hpp"
#include "nifkey.hpp"
#include "base.hpp"
namespace Nif
{
class NiSourceTexture : public Named
{
public:
// Is this an external (references a separate texture file) or
// internal (data is inside the nif itself) texture?
bool external;
std::string filename; // In case of external textures
NiPixelDataPtr data; // In case of internal textures
/* Pixel layout
0 - Palettised
1 - High color 16
2 - True color 32
3 - Compressed
4 - Bumpmap
5 - Default */
int pixel;
/* Mipmap format
0 - no
1 - yes
2 - default */
int mipmap;
/* Alpha
0 - none
1 - binary
2 - smooth
3 - default (use material alpha, or multiply material with texture if present)
*/
int alpha;
void read(NIFStream *nif)
{
Named::read(nif);
external = !!nif->getChar();
if(external)
filename = nif->getString();
else
{
nif->getChar(); // always 1
data.read(nif);
}
pixel = nif->getInt();
mipmap = nif->getInt();
alpha = nif->getInt();
nif->getChar(); // always 1
}
void post(NIFFile *nif)
{
Named::post(nif);
data.post(nif);
}
};
// Common ancestor for several data classes
class ShapeData : public Record
{

@ -24,26 +24,11 @@
#ifndef OPENMW_COMPONENTS_NIF_EXTRA_HPP
#define OPENMW_COMPONENTS_NIF_EXTRA_HPP
#include "record.hpp"
#include "niffile.hpp"
#include "recordptr.hpp"
#include "base.hpp"
namespace Nif
{
/** A record that can have extra data. The extra data objects
themselves decend from the Extra class, and all the extra data
connected to an object form a linked list
*/
class Extra : public Record
{
public:
ExtraPtr extra;
void read(NIFStream *nif) { extra.read(nif); }
void post(NIFFile *nif) { extra.post(nif); }
};
class NiVertWeightsExtraData : public Extra
{
public:

@ -109,6 +109,23 @@ static std::map<std::string,RecordFactoryEntry> makeFactory()
///Make the factory map used for parsing the file
static const std::map<std::string,RecordFactoryEntry> factories = makeFactory();
/// Get the file's version in a human readable form
std::string NIFFile::printVersion(unsigned int version)
{
union ver_quad
{
uint32_t full;
uint8_t quad[4];
} version_out;
version_out.full = version;
return Ogre::StringConverter::toString(version_out.quad[3])
+"." + Ogre::StringConverter::toString(version_out.quad[2])
+"." + Ogre::StringConverter::toString(version_out.quad[1])
+"." + Ogre::StringConverter::toString(version_out.quad[0]);
}
void NIFFile::parse()
{
NIFStream nif (this, Ogre::ResourceGroupManager::getSingleton().openResource(filename));
@ -119,10 +136,9 @@ void NIFFile::parse()
fail("Invalid NIF header");
// Get BCD version
ver = nif.getInt();
ver = nif.getUInt();
if(ver != VER_MW)
fail("Unsupported NIF version");
fail("Unsupported NIF version: " + printVersion(ver));
// Number of records
size_t recNum = nif.getInt();
records.resize(recNum);

@ -19,7 +19,7 @@ class NIFFile
};
/// Nif file version
int ver;
unsigned int ver;
/// File name, used for error messages and opening the file
std::string filename;
@ -33,6 +33,10 @@ class NIFFile
/// Parse the file
void parse();
/// Get the file's version in a human readable form
///\returns A string containing a human readable NIF version number
std::string printVersion(unsigned int version);
///Private Copy Constructor
NIFFile (NIFFile const &);
///\overload
@ -74,6 +78,9 @@ public:
}
/// Number of roots
size_t numRoots() const { return roots.size(); }
/// Get the name of the file
std::string getFilename(){ return filename; }
};

@ -12,6 +12,7 @@
#include <OgreVector4.h>
#include <OgreMatrix3.h>
#include <OgreQuaternion.h>
#include <OgreStringConverter.h>
#include "niftypes.hpp"
@ -142,7 +143,8 @@ public:
std::vector<char> str (length+1, 0);
if(inp->read(&str[0], length) != length)
throw std::runtime_error ("string length in NIF file does not match");
throw std::runtime_error (": String length in NIF file "+ file->getFilename() +" does not match! Expected length: "
+ Ogre::StringConverter::toString(length));
return &str[0];
}

@ -4,9 +4,12 @@
#include <OgreMatrix4.h>
#include "controlled.hpp"
#include "extra.hpp"
#include "data.hpp"
#include "property.hpp"
#include "niftypes.hpp"
#include "controller.hpp"
#include "base.hpp"
namespace Nif
{

@ -24,7 +24,7 @@
#ifndef OPENMW_COMPONENTS_NIF_PROPERTY_HPP
#define OPENMW_COMPONENTS_NIF_PROPERTY_HPP
#include "controlled.hpp"
#include "base.hpp"
namespace Nif
{

@ -355,11 +355,17 @@ Ogre::String NIFMaterialLoader::getMaterial(const Nif::ShapeData *shapedata,
if((alphaFlags>>9)&1)
{
#ifndef ANDROID
std::string reject;
reject += getTestMode((alphaFlags>>10)&0x7);
reject += " ";
reject += Ogre::StringConverter::toString(alphaTest);
instance->setProperty("alpha_rejection", sh::makeProperty(new sh::StringValue(reject)));
#else
// alpha test not supported in OpenGL ES 2, use manual implementation in shader
instance->setProperty("alphaTestMode", sh::makeProperty(new sh::IntValue((alphaFlags>>10)&0x7)));
instance->setProperty("alphaTestValue", sh::makeProperty(new sh::FloatValue(alphaTest/255.f)));
#endif
}
else
instance->getMaterial()->setShadowCasterMaterial("openmw_shadowcaster_noalpha");

@ -0,0 +1,39 @@
set(OGRE_FFMPEG_VIDEOPLAYER_LIBRARY "ogre-ffmpeg-videoplayer")
# Sources
set(OGRE_FFMPEG_VIDEOPLAYER_SOURCE_FILES
videoplayer.cpp
videostate.cpp
videodefs.hpp
libavwrapper.cpp
audiodecoder.cpp
audiofactory.hpp
)
# Find FFMPEG
set(FFmpeg_FIND_COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE SWRESAMPLE AVRESAMPLE)
unset(FFMPEG_LIBRARIES CACHE)
find_package(FFmpeg)
if ( NOT AVCODEC_FOUND OR NOT AVFORMAT_FOUND OR NOT AVUTIL_FOUND OR NOT SWSCALE_FOUND )
message(FATAL_ERROR "FFmpeg component required, but not found!")
endif()
set(VIDEO_FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} ${SWSCALE_LIBRARIES})
if( SWRESAMPLE_FOUND )
add_definitions(-DHAVE_LIBSWRESAMPLE)
set(VIDEO_FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} ${SWRESAMPLE_LIBRARIES})
else()
if( AVRESAMPLE_FOUND )
set(VIDEO_FFMPEG_LIBRARIES ${FFMPEG_LIBRARIES} ${AVRESAMPLE_LIBRARIES})
else()
message(FATAL_ERROR "Install either libswresample (FFmpeg) or libavresample (Libav).")
endif()
endif()
include_directories(${FFMPEG_INCLUDE_DIRS})
add_library(${OGRE_FFMPEG_VIDEOPLAYER_LIBRARY} STATIC ${OGRE_FFMPEG_VIDEOPLAYER_SOURCE_FILES})
target_link_libraries(${OGRE_FFMPEG_VIDEOPLAYER_LIBRARY} ${VIDEO_FFMPEG_LIBRARIES})
link_directories(${CMAKE_CURRENT_BINARY_DIR})

@ -0,0 +1,9 @@
Copyright (c) 2014 Jannik Heller <scrawl@baseoftrash.de>, Chris Robinson
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,322 @@
#include "audiodecoder.hpp"
extern "C"
{
#include <libavcodec/avcodec.h>
#ifdef HAVE_LIBSWRESAMPLE
#include <libswresample/swresample.h>
#else
// FIXME: remove this section once libswresample is packaged for Debian
#include <libavresample/avresample.h>
#include <libavutil/opt.h>
#define SwrContext AVAudioResampleContext
int swr_init(AVAudioResampleContext *avr);
void swr_free(AVAudioResampleContext **avr);
int swr_convert( AVAudioResampleContext *avr, uint8_t** output, int out_samples, const uint8_t** input, int in_samples);
AVAudioResampleContext * swr_alloc_set_opts( AVAudioResampleContext *avr, int64_t out_ch_layout, AVSampleFormat out_fmt, int out_rate, int64_t in_ch_layout, AVSampleFormat in_fmt, int in_rate, int o, void* l);
#endif
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#endif
}
#include "videostate.hpp"
namespace
{
void fail(const std::string &str)
{
throw std::runtime_error(str);
}
const double AUDIO_DIFF_AVG_NB = 20;
}
namespace Video
{
// Moved to implementation file, so that HAVE_SWRESAMPLE is used at library compile time only
struct AudioResampler
{
AudioResampler()
: mSwr(NULL)
{
}
~AudioResampler()
{
swr_free(&mSwr);
}
SwrContext* mSwr;
};
MovieAudioDecoder::MovieAudioDecoder(VideoState* videoState)
: mVideoState(videoState)
, mAVStream(*videoState->audio_st)
, mFrame(av_frame_alloc())
, mFramePos(0)
, mFrameSize(0)
, mAudioClock(0.0)
, mAudioDiffAccum(0.0)
, mAudioDiffAvgCoef(exp(log(0.01 / AUDIO_DIFF_AVG_NB)))
/* Correct audio only if larger error than this */
, mAudioDiffThreshold(2.0 * 0.050/* 50 ms */)
, mAudioDiffAvgCount(0)
, mOutputSampleFormat(AV_SAMPLE_FMT_NONE)
, mOutputSampleRate(0)
, mOutputChannelLayout(0)
, mDataBuf(NULL)
, mFrameData(NULL)
, mDataBufLen(0)
{
mAudioResampler.reset(new AudioResampler());
}
MovieAudioDecoder::~MovieAudioDecoder()
{
av_freep(&mFrame);
av_freep(&mDataBuf);
}
void MovieAudioDecoder::setupFormat()
{
if (mAudioResampler->mSwr)
return; // already set up
AVSampleFormat inputSampleFormat = mAVStream->codec->sample_fmt;
uint64_t inputChannelLayout = mAVStream->codec->channel_layout;
if (inputChannelLayout == 0)
{
/* Unknown channel layout. Try to guess. */
if(mAVStream->codec->channels == 1)
inputChannelLayout = AV_CH_LAYOUT_MONO;
else if(mAVStream->codec->channels == 2)
inputChannelLayout = AV_CH_LAYOUT_STEREO;
else
{
std::stringstream sstr("Unsupported raw channel count: ");
sstr << mAVStream->codec->channels;
fail(sstr.str());
}
}
int inputSampleRate = mAVStream->codec->sample_rate;
mOutputSampleRate = inputSampleRate;
mOutputSampleFormat = inputSampleFormat;
mOutputChannelLayout = inputChannelLayout;
adjustAudioSettings(mOutputSampleFormat, mOutputChannelLayout, mOutputSampleRate);
if (inputSampleFormat != mOutputSampleFormat
|| inputChannelLayout != mOutputChannelLayout
|| inputSampleRate != mOutputSampleRate)
{
mAudioResampler->mSwr = swr_alloc_set_opts(mAudioResampler->mSwr,
mOutputChannelLayout,
mOutputSampleFormat,
mOutputSampleRate,
inputChannelLayout,
inputSampleFormat,
inputSampleRate,
0, // logging level offset
NULL); // log context
if(!mAudioResampler->mSwr)
fail(std::string("Couldn't allocate SwrContext"));
if(swr_init(mAudioResampler->mSwr) < 0)
fail(std::string("Couldn't initialize SwrContext"));
}
}
int MovieAudioDecoder::synchronize_audio()
{
if(mVideoState->av_sync_type == AV_SYNC_AUDIO_MASTER)
return 0;
int sample_skip = 0;
// accumulate the clock difference
double diff = mVideoState->get_master_clock() - mVideoState->get_audio_clock();
mAudioDiffAccum = diff + mAudioDiffAvgCoef * mAudioDiffAccum;
if(mAudioDiffAvgCount < AUDIO_DIFF_AVG_NB)
mAudioDiffAvgCount++;
else
{
double avg_diff = mAudioDiffAccum * (1.0 - mAudioDiffAvgCoef);
if(fabs(avg_diff) >= mAudioDiffThreshold)
{
int n = av_get_bytes_per_sample(mAVStream->codec->sample_fmt) *
mAVStream->codec->channels;
sample_skip = ((int)(diff * mAVStream->codec->sample_rate) * n);
}
}
return sample_skip;
}
int MovieAudioDecoder::audio_decode_frame(AVFrame *frame)
{
AVPacket *pkt = &mPacket;
for(;;)
{
while(pkt->size > 0)
{
int len1, got_frame;
len1 = avcodec_decode_audio4(mAVStream->codec, frame, &got_frame, pkt);
if(len1 < 0) break;
if(len1 <= pkt->size)
{
/* Move the unread data to the front and clear the end bits */
int remaining = pkt->size - len1;
memmove(pkt->data, &pkt->data[len1], remaining);
av_shrink_packet(pkt, remaining);
}
/* No data yet? Look for more frames */
if(!got_frame || frame->nb_samples <= 0)
continue;
if(mAudioResampler->mSwr)
{
if(!mDataBuf || mDataBufLen < frame->nb_samples)
{
av_freep(&mDataBuf);
if(av_samples_alloc(&mDataBuf, NULL, mAVStream->codec->channels,
frame->nb_samples, mOutputSampleFormat, 0) < 0)
break;
else
mDataBufLen = frame->nb_samples;
}
if(swr_convert(mAudioResampler->mSwr, (uint8_t**)&mDataBuf, frame->nb_samples,
(const uint8_t**)frame->extended_data, frame->nb_samples) < 0)
{
break;
}
mFrameData = &mDataBuf;
}
else
mFrameData = &frame->data[0];
mAudioClock += (double)frame->nb_samples /
(double)mAVStream->codec->sample_rate;
/* We have data, return it and come back for more later */
return frame->nb_samples * mAVStream->codec->channels *
av_get_bytes_per_sample(mAVStream->codec->sample_fmt);
}
av_free_packet(pkt);
/* next packet */
if(mVideoState->audioq.get(pkt, mVideoState) < 0)
return -1;
/* if update, update the audio clock w/pts */
if((uint64_t)pkt->pts != AV_NOPTS_VALUE)
mAudioClock = av_q2d(mAVStream->time_base)*pkt->pts;
}
}
size_t MovieAudioDecoder::read(char *stream, size_t len)
{
int sample_skip = synchronize_audio();
size_t total = 0;
while(total < len)
{
if(mFramePos >= mFrameSize)
{
/* We have already sent all our data; get more */
mFrameSize = audio_decode_frame(mFrame);
if(mFrameSize < 0)
{
/* If error, we're done */
break;
}
mFramePos = std::min<ssize_t>(mFrameSize, sample_skip);
if(sample_skip > 0 || mFrameSize > -sample_skip)
sample_skip -= mFramePos;
continue;
}
size_t len1 = len - total;
if(mFramePos >= 0)
{
len1 = std::min<size_t>(len1, mFrameSize-mFramePos);
memcpy(stream, mFrameData[0]+mFramePos, len1);
}
else
{
len1 = std::min<size_t>(len1, -mFramePos);
int n = av_get_bytes_per_sample(mAVStream->codec->sample_fmt) *
mAVStream->codec->channels;
/* add samples by copying the first sample*/
if(n == 1)
memset(stream, *mFrameData[0], len1);
else if(n == 2)
{
const int16_t val = *((int16_t*)mFrameData[0]);
for(size_t nb = 0;nb < len1;nb += n)
*((int16_t*)(stream+nb)) = val;
}
else if(n == 4)
{
const int32_t val = *((int32_t*)mFrameData[0]);
for(size_t nb = 0;nb < len1;nb += n)
*((int32_t*)(stream+nb)) = val;
}
else if(n == 8)
{
const int64_t val = *((int64_t*)mFrameData[0]);
for(size_t nb = 0;nb < len1;nb += n)
*((int64_t*)(stream+nb)) = val;
}
else
{
for(size_t nb = 0;nb < len1;nb += n)
memcpy(stream+nb, mFrameData[0], n);
}
}
total += len1;
stream += len1;
mFramePos += len1;
}
return total;
}
double MovieAudioDecoder::getAudioClock()
{
return mAudioClock;
}
int MovieAudioDecoder::getOutputSampleRate() const
{
return mOutputSampleRate;
}
uint64_t MovieAudioDecoder::getOutputChannelLayout() const
{
return mOutputChannelLayout;
}
AVSampleFormat MovieAudioDecoder::getOutputSampleFormat() const
{
return mOutputSampleFormat;
}
}

@ -0,0 +1,109 @@
#ifndef VIDEOPLAYER_AUDIODECODER_H
#define VIDEOPLAYER_AUDIODECODER_H
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
#include <stdint.h>
#include <new>
#include <memory>
extern "C"
{
#include <libavutil/avutil.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#if AV_VERSION_INT(52, 2, 0) <= AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO)
#include <libavutil/channel_layout.h>
#endif
}
#ifdef _WIN32
#include <BaseTsd.h>
typedef SSIZE_T ssize_t;
#endif
namespace Video
{
struct AudioResampler;
struct VideoState;
class MovieAudioDecoder
{
protected:
VideoState *mVideoState;
AVStream *mAVStream;
enum AVSampleFormat mOutputSampleFormat;
uint64_t mOutputChannelLayout;
int mOutputSampleRate;
ssize_t mFramePos;
ssize_t mFrameSize;
double mAudioClock;
private:
struct AutoAVPacket : public AVPacket {
AutoAVPacket(int size=0)
{
if(av_new_packet(this, size) < 0)
throw std::bad_alloc();
}
~AutoAVPacket()
{ av_free_packet(this); }
};
std::auto_ptr<AudioResampler> mAudioResampler;
uint8_t *mDataBuf;
uint8_t **mFrameData;
int mDataBufLen;
AutoAVPacket mPacket;
AVFrame *mFrame;
/* averaging filter for audio sync */
double mAudioDiffAccum;
const double mAudioDiffAvgCoef;
const double mAudioDiffThreshold;
int mAudioDiffAvgCount;
/* Add or subtract samples to get a better sync, return number of bytes to
* skip (negative means to duplicate). */
int synchronize_audio();
int audio_decode_frame(AVFrame *frame);
public:
MovieAudioDecoder(VideoState *is);
virtual ~MovieAudioDecoder();
int getOutputSampleRate() const;
AVSampleFormat getOutputSampleFormat() const;
uint64_t getOutputChannelLayout() const;
void setupFormat();
/// Adjust the given audio settings to the application's needs. The data given by the read() function will
/// be in the desired format written to this function's parameters.
/// @par Depending on the application, we may want either fixed settings, or a "closest supported match"
/// for the input that does not incur precision loss (e.g. planar -> non-planar format).
virtual void adjustAudioSettings(AVSampleFormat& sampleFormat, uint64_t& channelLayout, int& sampleRate) = 0;
/// Return the current offset in seconds from the beginning of the audio stream.
/// @par An internal clock exists in the mAudioClock member, and is used in the default implementation. However,
/// for an accurate clock, it's best to also take the current offset in the audio buffer into account.
virtual double getAudioClock();
/// This is the main interface to be used by the user's audio library.
size_t read(char *stream, size_t len);
};
}
#endif

@ -0,0 +1,19 @@
#ifndef VIDEO_MOVIEAUDIOFACTORY_H
#define VIDEO_MOVIEAUDIOFACTORY_H
#include "audiodecoder.hpp"
#include <boost/shared_ptr.hpp>
namespace Video
{
class MovieAudioFactory
{
public:
virtual boost::shared_ptr<MovieAudioDecoder> createDecoder(VideoState* videoState) = 0;
};
}
#endif

@ -0,0 +1,113 @@
// This file is a wrapper around the libavresample library (the API-incompatible swresample replacement in the libav fork of ffmpeg), to make it look like swresample to the user.
#ifndef HAVE_LIBSWRESAMPLE
extern "C"
{
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
#include <stdint.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
// From libavutil version 52.2.0 and onward the declaration of
// AV_CH_LAYOUT_* is removed from libavcodec/avcodec.h and moved to
// libavutil/channel_layout.h
#if AV_VERSION_INT(52, 2, 0) <= AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MICRO)
#include <libavutil/channel_layout.h>
#endif
#include <libavresample/avresample.h>
#include <libavutil/opt.h>
/* FIXME: delete this file once libswresample is packaged for Debian */
int swr_init(AVAudioResampleContext *avr) { return 1; }
void swr_free(AVAudioResampleContext **avr) { avresample_free(avr); }
int swr_convert(
AVAudioResampleContext *avr,
uint8_t** output,
int out_samples,
const uint8_t** input,
int in_samples)
{
// FIXME: potential performance hit
int out_plane_size = 0;
int in_plane_size = 0;
return avresample_convert(avr, output, out_plane_size, out_samples,
(uint8_t **)input, in_plane_size, in_samples);
}
AVAudioResampleContext * swr_alloc_set_opts(
AVAudioResampleContext *avr,
int64_t out_ch_layout,
AVSampleFormat out_fmt,
int out_rate,
int64_t in_ch_layout,
AVSampleFormat in_fmt,
int in_rate,
int o,
void* l)
{
avr = avresample_alloc_context();
if(!avr)
return 0;
int res;
res = av_opt_set_int(avr, "out_channel_layout", out_ch_layout, 0);
if(res < 0)
{
av_log(avr, AV_LOG_ERROR, "av_opt_set_int: out_ch_layout = %d\n", res);
return 0;
}
res = av_opt_set_int(avr, "out_sample_fmt", out_fmt, 0);
if(res < 0)
{
av_log(avr, AV_LOG_ERROR, "av_opt_set_int: out_fmt = %d\n", res);
return 0;
}
res = av_opt_set_int(avr, "out_sample_rate", out_rate, 0);
if(res < 0)
{
av_log(avr, AV_LOG_ERROR, "av_opt_set_int: out_rate = %d\n", res);
return 0;
}
res = av_opt_set_int(avr, "in_channel_layout", in_ch_layout, 0);
if(res < 0)
{
av_log(avr, AV_LOG_ERROR, "av_opt_set_int: in_ch_layout = %d\n", res);
return 0;
}
res = av_opt_set_int(avr, "in_sample_fmt", in_fmt, 0);
if(res < 0)
{
av_log(avr, AV_LOG_ERROR, "av_opt_set_int: in_fmt = %d\n", res);
return 0;
}
res = av_opt_set_int(avr, "in_sample_rate", in_rate, 0);
if(res < 0)
{
av_log(avr, AV_LOG_ERROR, "av_opt_set_int: in_rate = %d\n", res);
return 0;
}
res = av_opt_set_int(avr, "internal_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
if(res < 0)
{
av_log(avr, AV_LOG_ERROR, "av_opt_set_int: internal_sample_fmt\n");
return 0;
}
if(avresample_open(avr) < 0)
{
av_log(avr, AV_LOG_ERROR, "Error opening context\n");
return 0;
}
else
return avr;
}
}
#endif

@ -0,0 +1,17 @@
#ifndef VIDEOPLAYER_DEFS_H
#define VIDEOPLAYER_DEFS_H
namespace Video
{
enum {
AV_SYNC_AUDIO_MASTER, // Play audio with no frame drops, sync video to audio
AV_SYNC_VIDEO_MASTER, // Play video with no frame drops, sync audio to video
AV_SYNC_EXTERNAL_MASTER, // Sync audio and video to an external clock
AV_SYNC_DEFAULT = AV_SYNC_EXTERNAL_MASTER
};
}
#endif

@ -0,0 +1,95 @@
#include "videoplayer.hpp"
#include "videostate.hpp"
namespace Video
{
VideoPlayer::VideoPlayer()
: mState(NULL)
{
}
VideoPlayer::~VideoPlayer()
{
if(mState)
close();
}
void VideoPlayer::setAudioFactory(MovieAudioFactory *factory)
{
mAudioFactory.reset(factory);
}
void VideoPlayer::playVideo(const std::string &resourceName)
{
if(mState)
close();
try {
mState = new VideoState;
mState->setAudioFactory(mAudioFactory.get());
mState->init(resourceName);
}
catch(std::exception& e) {
std::cerr<< "Failed to play video: "<<e.what() <<std::endl;
close();
}
}
void VideoPlayer::update ()
{
if(mState)
{
if(!mState->update())
close();
}
}
std::string VideoPlayer::getTextureName()
{
std::string name;
if (mState)
name = mState->mTexture->getName();
return name;
}
int VideoPlayer::getVideoWidth()
{
int width=0;
if (mState)
width = mState->mTexture->getWidth();
return width;
}
int VideoPlayer::getVideoHeight()
{
int height=0;
if (mState)
height = mState->mTexture->getHeight();
return height;
}
void VideoPlayer::close()
{
if(mState)
{
mState->deinit();
delete mState;
mState = NULL;
}
}
bool VideoPlayer::isPlaying ()
{
return mState != NULL;
}
bool VideoPlayer::hasAudioStream()
{
return mState && mState->audio_st != NULL;
}
}

@ -0,0 +1,58 @@
#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H
#include <string>
#include <memory>
namespace Video
{
struct VideoState;
class MovieAudioFactory;
/**
* @brief Plays a video on an Ogre texture.
*/
class VideoPlayer
{
public:
VideoPlayer();
~VideoPlayer();
/// @brief Set the MovieAudioFactory to use.
/// @par This class must be implemented by the user and is responsible for reading the decoded audio data.
/// @note If you do not set up a MovieAudioFactory, then audio streams will be ignored and the video will be played with no sound.
/// @note Takes ownership of the passed pointer.
void setAudioFactory (MovieAudioFactory* factory);
/// Return true if a video is currently playing and it has an audio stream.
bool hasAudioStream();
/// Play the given video. If a video is already playing, the old video is closed first.
void playVideo (const std::string& resourceName);
/// This should be called every frame by the user to update the video texture.
void update();
/// Stop the currently playing video, if a video is playing.
void close();
bool isPlaying();
/// Return the texture name of the currently playing video, or "" if no video is playing.
std::string getTextureName();
/// Return the width of the currently playing video, or 0 if no video is playing.
int getVideoWidth();
/// Return the height of the currently playing video, or 0 if no video is playing.
int getVideoHeight();
private:
VideoState* mState;
std::auto_ptr<MovieAudioFactory> mAudioFactory;
};
}
#endif

@ -0,0 +1,671 @@
#include "videostate.hpp"
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif
#include <stdint.h>
// Has to be included *before* ffmpeg, due to a macro collision with ffmpeg (#define PixelFormat in avformat.h - grumble)
#include <OgreTextureManager.h>
#include <OgreHardwarePixelBuffer.h>
#include <OgreResourceGroupManager.h>
#include <OgreStringConverter.h>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
// From libavformat version 55.0.100 and onward the declaration of av_gettime() is
// removed from libavformat/avformat.h and moved to libavutil/time.h
// https://github.com/FFmpeg/FFmpeg/commit/06a83505992d5f49846c18507a6c3eb8a47c650e
#if AV_VERSION_INT(55, 0, 100) <= AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
LIBAVFORMAT_VERSION_MINOR, LIBAVFORMAT_VERSION_MICRO)
#include <libavutil/time.h>
#endif
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#endif
}
#include "videoplayer.hpp"
#include "audiodecoder.hpp"
#include "audiofactory.hpp"
namespace
{
const int MAX_AUDIOQ_SIZE = (5 * 16 * 1024);
const int MAX_VIDEOQ_SIZE = (5 * 256 * 1024);
}
namespace Video
{
VideoState::VideoState()
: format_ctx(NULL), av_sync_type(AV_SYNC_DEFAULT)
, external_clock_base(0.0)
, audio_st(NULL)
, video_st(NULL), frame_last_pts(0.0)
, video_clock(0.0), sws_context(NULL), rgbaFrame(NULL), pictq_size(0)
, pictq_rindex(0), pictq_windex(0)
, quit(false)
, mAudioFactory(NULL)
{
// Register all formats and codecs
av_register_all();
}
VideoState::~VideoState()
{
deinit();
}
void VideoState::setAudioFactory(MovieAudioFactory *factory)
{
mAudioFactory = factory;
}
void PacketQueue::put(AVPacket *pkt)
{
AVPacketList *pkt1;
pkt1 = (AVPacketList*)av_malloc(sizeof(AVPacketList));
if(!pkt1) throw std::bad_alloc();
pkt1->pkt = *pkt;
pkt1->next = NULL;
if(pkt1->pkt.destruct == NULL)
{
if(av_dup_packet(&pkt1->pkt) < 0)
{
av_free(pkt1);
throw std::runtime_error("Failed to duplicate packet");
}
av_free_packet(pkt);
}
this->mutex.lock ();
if(!last_pkt)
this->first_pkt = pkt1;
else
this->last_pkt->next = pkt1;
this->last_pkt = pkt1;
this->nb_packets++;
this->size += pkt1->pkt.size;
this->cond.notify_one();
this->mutex.unlock();
}
int PacketQueue::get(AVPacket *pkt, VideoState *is)
{
boost::unique_lock<boost::mutex> lock(this->mutex);
while(!is->quit)
{
AVPacketList *pkt1 = this->first_pkt;
if(pkt1)
{
this->first_pkt = pkt1->next;
if(!this->first_pkt)
this->last_pkt = NULL;
this->nb_packets--;
this->size -= pkt1->pkt.size;
*pkt = pkt1->pkt;
av_free(pkt1);
return 1;
}
if(this->flushing)
break;
this->cond.wait(lock);
}
return -1;
}
void PacketQueue::flush()
{
this->flushing = true;
this->cond.notify_one();
}
void PacketQueue::clear()
{
AVPacketList *pkt, *pkt1;
this->mutex.lock();
for(pkt = this->first_pkt; pkt != NULL; pkt = pkt1)
{
pkt1 = pkt->next;
av_free_packet(&pkt->pkt);
av_freep(&pkt);
}
this->last_pkt = NULL;
this->first_pkt = NULL;
this->nb_packets = 0;
this->size = 0;
this->mutex.unlock ();
}
int VideoState::OgreResource_Read(void *user_data, uint8_t *buf, int buf_size)
{
Ogre::DataStreamPtr stream = static_cast<VideoState*>(user_data)->stream;
return stream->read(buf, buf_size);
}
int VideoState::OgreResource_Write(void *user_data, uint8_t *buf, int buf_size)
{
Ogre::DataStreamPtr stream = static_cast<VideoState*>(user_data)->stream;
return stream->write(buf, buf_size);
}
int64_t VideoState::OgreResource_Seek(void *user_data, int64_t offset, int whence)
{
Ogre::DataStreamPtr stream = static_cast<VideoState*>(user_data)->stream;
whence &= ~AVSEEK_FORCE;
if(whence == AVSEEK_SIZE)
return stream->size();
if(whence == SEEK_SET)
stream->seek(offset);
else if(whence == SEEK_CUR)
stream->seek(stream->tell()+offset);
else if(whence == SEEK_END)
stream->seek(stream->size()+offset);
else
return -1;
return stream->tell();
}
void VideoState::video_display(VideoPicture *vp)
{
if((*this->video_st)->codec->width != 0 && (*this->video_st)->codec->height != 0)
{
if(static_cast<int>(mTexture->getWidth()) != (*this->video_st)->codec->width ||
static_cast<int>(mTexture->getHeight()) != (*this->video_st)->codec->height)
{
mTexture->unload();
mTexture->setWidth((*this->video_st)->codec->width);
mTexture->setHeight((*this->video_st)->codec->height);
mTexture->createInternalResources();
}
Ogre::PixelBox pb((*this->video_st)->codec->width, (*this->video_st)->codec->height, 1, Ogre::PF_BYTE_RGBA, &vp->data[0]);
Ogre::HardwarePixelBufferSharedPtr buffer = mTexture->getBuffer();
buffer->blitFromMemory(pb);
}
}
void VideoState::video_refresh()
{
if(this->pictq_size == 0)
return;
if (this->av_sync_type == AV_SYNC_VIDEO_MASTER)
{
VideoPicture* vp = &this->pictq[this->pictq_rindex];
this->video_display(vp);
this->pictq_rindex = (pictq_rindex+1) % VIDEO_PICTURE_QUEUE_SIZE;
this->frame_last_pts = vp->pts;
this->pictq_mutex.lock();
this->pictq_size--;
this->pictq_cond.notify_one();
this->pictq_mutex.unlock();
}
else
{
const float threshold = 0.03;
if (this->pictq[pictq_rindex].pts > this->get_master_clock() + threshold)
return; // not ready yet to show this picture
// TODO: the conversion to RGBA is done in the decoding thread, so if a picture is skipped here, then it was
// unnecessarily converted. But we may want to replace the conversion by a pixel shader anyway (see comment in queue_picture)
int i=0;
for (; i<this->pictq_size-1; ++i)
{
if (this->pictq[pictq_rindex].pts + threshold <= this->get_master_clock())
this->pictq_rindex = (this->pictq_rindex+1) % VIDEO_PICTURE_QUEUE_SIZE; // not enough time to show this picture
else
break;
}
VideoPicture* vp = &this->pictq[this->pictq_rindex];
this->video_display(vp);
this->frame_last_pts = vp->pts;
this->pictq_mutex.lock();
this->pictq_size -= i;
// update queue for next picture
this->pictq_size--;
this->pictq_rindex++;
this->pictq_cond.notify_one();
this->pictq_mutex.unlock();
}
}
int VideoState::queue_picture(AVFrame *pFrame, double pts)
{
VideoPicture *vp;
/* wait until we have a new pic */
{
boost::unique_lock<boost::mutex> lock(this->pictq_mutex);
while(this->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !this->quit)
this->pictq_cond.timed_wait(lock, boost::posix_time::milliseconds(1));
}
if(this->quit)
return -1;
// windex is set to 0 initially
vp = &this->pictq[this->pictq_windex];
// Convert the image into RGBA format for Ogre
// TODO: we could do this in a pixel shader instead, if the source format
// matches a commonly used format (ie YUV420P)
if(this->sws_context == NULL)
{
int w = (*this->video_st)->codec->width;
int h = (*this->video_st)->codec->height;
this->sws_context = sws_getContext(w, h, (*this->video_st)->codec->pix_fmt,
w, h, PIX_FMT_RGBA, SWS_BICUBIC,
NULL, NULL, NULL);
if(this->sws_context == NULL)
throw std::runtime_error("Cannot initialize the conversion context!\n");
}
vp->pts = pts;
vp->data.resize((*this->video_st)->codec->width * (*this->video_st)->codec->height * 4);
uint8_t *dst = &vp->data[0];
sws_scale(this->sws_context, pFrame->data, pFrame->linesize,
0, (*this->video_st)->codec->height, &dst, this->rgbaFrame->linesize);
// now we inform our display thread that we have a pic ready
this->pictq_windex = (this->pictq_windex+1) % VIDEO_PICTURE_QUEUE_SIZE;
this->pictq_mutex.lock();
this->pictq_size++;
this->pictq_mutex.unlock();
return 0;
}
double VideoState::synchronize_video(AVFrame *src_frame, double pts)
{
double frame_delay;
/* if we have pts, set video clock to it */
if(pts != 0)
this->video_clock = pts;
else
pts = this->video_clock;
/* update the video clock */
frame_delay = av_q2d((*this->video_st)->codec->time_base);
/* if we are repeating a frame, adjust clock accordingly */
frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
this->video_clock += frame_delay;
return pts;
}
/* These are called whenever we allocate a frame
* buffer. We use this to store the global_pts in
* a frame at the time it is allocated.
*/
static uint64_t global_video_pkt_pts = static_cast<uint64_t>(AV_NOPTS_VALUE);
static int our_get_buffer(struct AVCodecContext *c, AVFrame *pic)
{
int ret = avcodec_default_get_buffer(c, pic);
uint64_t *pts = (uint64_t*)av_malloc(sizeof(uint64_t));
*pts = global_video_pkt_pts;
pic->opaque = pts;
return ret;
}
static void our_release_buffer(struct AVCodecContext *c, AVFrame *pic)
{
if(pic) av_freep(&pic->opaque);
avcodec_default_release_buffer(c, pic);
}
void VideoState::video_thread_loop(VideoState *self)
{
AVPacket pkt1, *packet = &pkt1;
int frameFinished;
AVFrame *pFrame;
pFrame = av_frame_alloc();
self->rgbaFrame = av_frame_alloc();
avpicture_alloc((AVPicture*)self->rgbaFrame, PIX_FMT_RGBA, (*self->video_st)->codec->width, (*self->video_st)->codec->height);
while(self->videoq.get(packet, self) >= 0)
{
// Save global pts to be stored in pFrame
global_video_pkt_pts = packet->pts;
// Decode video frame
if(avcodec_decode_video2((*self->video_st)->codec, pFrame, &frameFinished, packet) < 0)
throw std::runtime_error("Error decoding video frame");
double pts = 0;
if((uint64_t)packet->dts != AV_NOPTS_VALUE)
pts = packet->dts;
else if(pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE)
pts = *(uint64_t*)pFrame->opaque;
pts *= av_q2d((*self->video_st)->time_base);
av_free_packet(packet);
// Did we get a video frame?
if(frameFinished)
{
pts = self->synchronize_video(pFrame, pts);
if(self->queue_picture(pFrame, pts) < 0)
break;
}
}
av_free(pFrame);
avpicture_free((AVPicture*)self->rgbaFrame);
av_free(self->rgbaFrame);
}
void VideoState::decode_thread_loop(VideoState *self)
{
AVFormatContext *pFormatCtx = self->format_ctx;
AVPacket pkt1, *packet = &pkt1;
try
{
if(!self->video_st && !self->audio_st)
throw std::runtime_error("No streams to decode");
// main decode loop
while(!self->quit)
{
if((self->audio_st && self->audioq.size > MAX_AUDIOQ_SIZE) ||
(self->video_st && self->videoq.size > MAX_VIDEOQ_SIZE))
{
boost::this_thread::sleep(boost::posix_time::milliseconds(10));
continue;
}
if(av_read_frame(pFormatCtx, packet) < 0)
break;
// Is this a packet from the video stream?
if(self->video_st && packet->stream_index == self->video_st-pFormatCtx->streams)
self->videoq.put(packet);
else if(self->audio_st && packet->stream_index == self->audio_st-pFormatCtx->streams)
self->audioq.put(packet);
else
av_free_packet(packet);
}
/* all done - wait for it */
self->videoq.flush();
self->audioq.flush();
while(!self->quit)
{
// EOF reached, all packets processed, we can exit now
if(self->audioq.nb_packets == 0 && self->videoq.nb_packets == 0 && self->pictq_size == 0)
break;
boost::this_thread::sleep(boost::posix_time::milliseconds(100));
}
}
catch(std::runtime_error& e) {
std::cerr << "An error occured playing the video: " << e.what () << std::endl;
}
catch(Ogre::Exception& e) {
std::cerr << "An error occured playing the video: " << e.getFullDescription () << std::endl;
}
self->quit = true;
}
bool VideoState::update()
{
if(this->quit)
return false;
this->video_refresh();
return true;
}
int VideoState::stream_open(int stream_index, AVFormatContext *pFormatCtx)
{
AVCodecContext *codecCtx;
AVCodec *codec;
if(stream_index < 0 || stream_index >= static_cast<int>(pFormatCtx->nb_streams))
return -1;
// Get a pointer to the codec context for the video stream
codecCtx = pFormatCtx->streams[stream_index]->codec;
codec = avcodec_find_decoder(codecCtx->codec_id);
if(!codec || (avcodec_open2(codecCtx, codec, NULL) < 0))
{
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
switch(codecCtx->codec_type)
{
case AVMEDIA_TYPE_AUDIO:
this->audio_st = pFormatCtx->streams + stream_index;
if (!mAudioFactory)
{
std::cerr << "No audio factory registered, can not play audio stream" << std::endl;
avcodec_close((*this->audio_st)->codec);
this->audio_st = NULL;
return -1;
}
mAudioDecoder = mAudioFactory->createDecoder(this);
if (!mAudioDecoder.get())
{
std::cerr << "Failed to create audio decoder, can not play audio stream" << std::endl;
avcodec_close((*this->audio_st)->codec);
this->audio_st = NULL;
return -1;
}
mAudioDecoder->setupFormat();
break;
case AVMEDIA_TYPE_VIDEO:
this->video_st = pFormatCtx->streams + stream_index;
codecCtx->get_buffer = our_get_buffer;
codecCtx->release_buffer = our_release_buffer;
this->video_thread = boost::thread(video_thread_loop, this);
break;
default:
break;
}
return 0;
}
void VideoState::init(const std::string& resourceName)
{
int video_index = -1;
int audio_index = -1;
unsigned int i;
this->av_sync_type = AV_SYNC_DEFAULT;
this->quit = false;
this->stream = Ogre::ResourceGroupManager::getSingleton().openResource(resourceName);
if(this->stream.isNull())
throw std::runtime_error("Failed to open video resource");
AVIOContext *ioCtx = avio_alloc_context(NULL, 0, 0, this, OgreResource_Read, OgreResource_Write, OgreResource_Seek);
if(!ioCtx) throw std::runtime_error("Failed to allocate AVIOContext");
this->format_ctx = avformat_alloc_context();
if(this->format_ctx)
this->format_ctx->pb = ioCtx;
// Open video file
///
/// format_ctx->pb->buffer must be freed by hand,
/// if not, valgrind will show memleak, see:
///
/// https://trac.ffmpeg.org/ticket/1357
///
if(!this->format_ctx || avformat_open_input(&this->format_ctx, resourceName.c_str(), NULL, NULL))
{
if (this->format_ctx != NULL)
{
if (this->format_ctx->pb != NULL)
{
av_free(this->format_ctx->pb->buffer);
this->format_ctx->pb->buffer = NULL;
av_free(this->format_ctx->pb);
this->format_ctx->pb = NULL;
}
}
// "Note that a user-supplied AVFormatContext will be freed on failure."
this->format_ctx = NULL;
av_free(ioCtx);
throw std::runtime_error("Failed to open video input");
}
// Retrieve stream information
if(avformat_find_stream_info(this->format_ctx, NULL) < 0)
throw std::runtime_error("Failed to retrieve stream information");
// Dump information about file onto standard error
av_dump_format(this->format_ctx, 0, resourceName.c_str(), 0);
for(i = 0;i < this->format_ctx->nb_streams;i++)
{
if(this->format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0)
video_index = i;
if(this->format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0)
audio_index = i;
}
this->external_clock_base = av_gettime();
if(audio_index >= 0)
this->stream_open(audio_index, this->format_ctx);
if(video_index >= 0)
{
this->stream_open(video_index, this->format_ctx);
int width = (*this->video_st)->codec->width;
int height = (*this->video_st)->codec->height;
static int i = 0;
this->mTexture = Ogre::TextureManager::getSingleton().createManual(
"ffmpeg/VideoTexture" + Ogre::StringConverter::toString(++i),
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME,
Ogre::TEX_TYPE_2D,
width, height,
0,
Ogre::PF_BYTE_RGBA,
Ogre::TU_DYNAMIC_WRITE_ONLY_DISCARDABLE);
// initialize to (0,0,0,0)
std::vector<Ogre::uint32> buffer;
buffer.resize(width * height, 0);
Ogre::PixelBox pb(width, height, 1, Ogre::PF_BYTE_RGBA, &buffer[0]);
this->mTexture->getBuffer()->blitFromMemory(pb);
}
this->parse_thread = boost::thread(decode_thread_loop, this);
}
void VideoState::deinit()
{
this->quit = true;
mAudioDecoder.reset();
this->audioq.cond.notify_one();
this->videoq.cond.notify_one();
if (this->parse_thread.joinable())
this->parse_thread.join();
if (this->video_thread.joinable())
this->video_thread.join();
if(this->audio_st)
avcodec_close((*this->audio_st)->codec);
this->audio_st = NULL;
if(this->video_st)
avcodec_close((*this->video_st)->codec);
this->video_st = NULL;
if(this->sws_context)
sws_freeContext(this->sws_context);
this->sws_context = NULL;
if(this->format_ctx)
{
///
/// format_ctx->pb->buffer must be freed by hand,
/// if not, valgrind will show memleak, see:
///
/// https://trac.ffmpeg.org/ticket/1357
///
if (this->format_ctx->pb != NULL)
{
av_free(this->format_ctx->pb->buffer);
this->format_ctx->pb->buffer = NULL;
av_free(this->format_ctx->pb);
this->format_ctx->pb = NULL;
}
avformat_close_input(&this->format_ctx);
}
}
double VideoState::get_external_clock()
{
return ((uint64_t)av_gettime()-this->external_clock_base) / 1000000.0;
}
double VideoState::get_master_clock()
{
if(this->av_sync_type == AV_SYNC_VIDEO_MASTER)
return this->get_video_clock();
if(this->av_sync_type == AV_SYNC_AUDIO_MASTER)
return this->get_audio_clock();
return this->get_external_clock();
}
double VideoState::get_video_clock()
{
return this->frame_last_pts;
}
double VideoState::get_audio_clock()
{
if (!mAudioDecoder.get())
return 0.0;
return mAudioDecoder->getAudioClock();
}
}

@ -0,0 +1,125 @@
#ifndef VIDEOPLAYER_VIDEOSTATE_H
#define VIDEOPLAYER_VIDEOSTATE_H
#include <boost/thread.hpp>
#include <OgreTexture.h>
#include "videodefs.hpp"
#define VIDEO_PICTURE_QUEUE_SIZE 50
extern "C"
{
struct SwsContext;
struct AVPacketList;
struct AVPacket;
struct AVFormatContext;
struct AVStream;
struct AVFrame;
}
namespace Video
{
struct VideoState;
class MovieAudioFactory;
class MovieAudioDecoder;
struct PacketQueue {
PacketQueue()
: first_pkt(NULL), last_pkt(NULL), flushing(false), nb_packets(0), size(0)
{ }
~PacketQueue()
{ clear(); }
AVPacketList *first_pkt, *last_pkt;
volatile bool flushing;
int nb_packets;
int size;
boost::mutex mutex;
boost::condition_variable cond;
void put(AVPacket *pkt);
int get(AVPacket *pkt, VideoState *is);
void flush();
void clear();
};
struct VideoPicture {
VideoPicture() : pts(0.0)
{ }
std::vector<uint8_t> data;
double pts;
};
struct VideoState {
VideoState();
~VideoState();
void setAudioFactory(MovieAudioFactory* factory);
void init(const std::string& resourceName);
void deinit();
int stream_open(int stream_index, AVFormatContext *pFormatCtx);
bool update();
static void video_thread_loop(VideoState *is);
static void decode_thread_loop(VideoState *is);
void video_display(VideoPicture* vp);
void video_refresh();
int queue_picture(AVFrame *pFrame, double pts);
double synchronize_video(AVFrame *src_frame, double pts);
double get_audio_clock();
double get_video_clock();
double get_external_clock();
double get_master_clock();
static int OgreResource_Read(void *user_data, uint8_t *buf, int buf_size);
static int OgreResource_Write(void *user_data, uint8_t *buf, int buf_size);
static int64_t OgreResource_Seek(void *user_data, int64_t offset, int whence);
Ogre::TexturePtr mTexture;
MovieAudioFactory* mAudioFactory;
boost::shared_ptr<MovieAudioDecoder> mAudioDecoder;
Ogre::DataStreamPtr stream;
AVFormatContext* format_ctx;
int av_sync_type;
uint64_t external_clock_base;
AVStream** audio_st;
PacketQueue audioq;
AVStream** video_st;
double frame_last_pts;
double video_clock; ///<pts of last decoded frame / predicted pts of next decoded frame
PacketQueue videoq;
SwsContext* sws_context;
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
AVFrame* rgbaFrame; // used as buffer for the frame converted from its native format to RGBA
int pictq_size, pictq_rindex, pictq_windex;
boost::mutex pictq_mutex;
boost::condition_variable pictq_cond;
boost::thread parse_thread;
boost::thread video_thread;
volatile bool quit;
};
}
#endif

@ -29,6 +29,9 @@ material openmw_objects_base
env_map false
env_map_color 1 1 1
alphaTestMode 0
alphaTestValue 0
pass
{
vertex_program openmw_objects_vertex
@ -50,6 +53,8 @@ material openmw_objects_base
env_map $env_map
env_map_color $env_map_color
use_parallax $use_parallax
alphaTestMode $alphaTestMode
alphaTestValue $alphaTestValue
}
diffuse $diffuse

@ -23,6 +23,8 @@
#define DARK_MAP @shPropertyHasValue(darkMap)
#define SPEC_MAP @shPropertyHasValue(specMap) && SPECULAR
#define ALPHATEST_MODE @shPropertyString(alphaTestMode)
#define PARALLAX @shPropertyBool(use_parallax)
#define PARALLAX_SCALE 0.04
#define PARALLAX_BIAS -0.02
@ -354,6 +356,10 @@
#endif
#endif
#if ALPHATEST_MODE != 0
shUniform(float, alphaTestValue) @shUniformProperty1f(alphaTestValue, alphaTestValue)
#endif
SH_START_PROGRAM
{
float4 newUV = UV;
@ -399,6 +405,29 @@
float4 diffuse = float4(1,1,1,1);
#endif
#if ALPHATEST_MODE == 1
if (diffuse.a >= alphaTestValue)
discard;
#elif ALPHATEST_MODE == 2
if (diffuse.a != alphaTestValue)
discard;
#elif ALPHATEST_MODE == 3
if (diffuse.a > alphaTestValue)
discard;
#elif ALPHATEST_MODE == 4
if (diffuse.a <= alphaTestValue)
discard;
#elif ALPHATEST_MODE == 5
if (diffuse.a == alphaTestValue)
discard;
#elif ALPHATEST_MODE == 6
if (diffuse.a < alphaTestValue)
discard;
#elif ALPHATEST_MODE == 7
discard;
#endif
#if DETAIL_MAP
#if @shPropertyString(detailMapUVSet)
diffuse *= shSample(detailMap, newUV.zw)*2;

@ -1,7 +0,0 @@
[Display%20Format]
Record%20Status%20Display=Icon Only
Referenceable%20ID%20Type%20Display=Text Only
[Window%20Size]
Height=900
Width=1440

@ -116,10 +116,9 @@ This is probably enough to create around 90 string filters you will eventually n
-- it accepts regular expressions (also called regexps). Regular expressions is a way to create string criteria that will be matched
by one than just one specific value in the column. For instance, you can display both left and right gauntlets with the following expression:
\mono{string("armor type", ".* gauntlet"))} because \mono{.*} in regexps means just: ``anything''. This filter says: please, show me ``any'' gauntlet.
There are left and right gauntlets in the morrowind so this will evaluate to true for both. Simple, isn't it?
There are left and right gauntlets in the \MW{} so this will evaluate to true for both. Simple, isn't it?
Creating regexps can be a difficult and annoying -- especially when you need complex criteria. On the other hand, we are under impression
that in reality complex expressions are needed only in sporadic cases. In fact, the truth is: that most of the time only already mentioned
Creating regexps can be a difficult and annoying -- especially when you need complex criteria. On the other hand, we are under impression that in reality complex expressions are needed only in sporadic cases. In fact, the truth is: that most of the time only already mentioned
\mono{.*} is needed and therefore the following description of regexps can be skipped by vast majority of readers.
Before working with Regular Expressions, you should understand what actually are regular expressions. Essentially, the idea is simple:
@ -131,19 +130,18 @@ Before introducing other ways to choose between characters, I want explain ancho
You surely should know about \mono{\textasciicircum} anchor and \mono{\textdollar}. Putting \mono{\textasciicircum} will tell to \OCS{}
to look on the beginning of string, while \mono{\textdollar} is used to mark the end of it. For instance, pattern
\mono{\textasciicircum{}Pink.* elephant.\textdollar} will match any sentence beginning with the word \mono{Pink} and ending with
\mono{ elephant.}. Pink fat elephant. Pink cute elephant. It does not matter what is in between, because \mono{.*} is used.
\mono{ elephant.}. Pink fat elephant. Pink cute elephant. It does not matter what is in between because \mono{.*} is used.
You have already seen the power of the simple \mono{.*}. But what if you want to chose between only two (or more) letters? Well, this is when
\mono{[|]} comes in handy. If you write something like: \mono{\textasciicircum[a|k].*} you are simply telling \OCS{} to filter anything that
starts with either \mono{a} or \mono{k}. Using \mono{\textasciicircum[a|k|l].*} will work in the same manner, but it will also cover
strings starting with \mono{l}.
starts with either \mono{a} or \mono{k}. Using \mono{\textasciicircum[a|k|l].*} will work in the same manner, that is; it will also cover strings starting with \mono{l} as well.
What if you want to match more than just one latter? Just use \mono{(|)}. it is pretty similar to the above one letter as you see, but it is
What if you want to match more than just one latter? Just use \mono{(|)}. It is pretty similar to the above one letter as you see, but it is
used to fit more than just one character. For instance: \mono{\textasciicircum(Pink|Green).* (elephant|crocodile).\textdollar} will be
true for all sentences starting with \mono{Pink} or \mono{Green} and ending with either \mono{elephant.} or \mono{crocodile.}.
Regular expressions are not the main topic of this manual. If you wish to learn more on this subject please, read the documentation on
Qt regular expressions syntax, or TRE regexp syntax (it is almost like in Qt). Above is just enough to use \OCS{} effectively to be sure.
Qt regular expressions syntax, or TRE regexp syntax (it is almost like in Qt). Above is just enough to use \OCS{} effectively.
\paragraph{Value -- value(``value'', (``open'', ``close''))}
While string expression covers vast group of columns containing string values, there are in fact columns with just numerical values like
@ -169,12 +167,11 @@ belonging to the same group of logical expressions.
\paragraph{not -- not expression()}
Sometimes you may be in need of reversing the output of the expression. This is where \textit{not} comes in handy. Adding \textit{not} before
expression will revert it: if expression was returning true, it will return false; if it was returning false, it will return true. Brackets
are not needed: \textit{not} will revert only the first expression following it.
expression will revert it: if expression was returning true, it will return false; if it was returning false, it will return true. Parenthesis are not needed: \textit{not} will revert only the first expression following it.
To show this on know example, let's consider the \mono{string("armor type", ".* gauntlet"))} filter. As we mentioned earlier this will return true
for every gauntlet found in game. In order to show everything, but gauntlets we simply do \mono{not string("armor type", ".* gauntlet"))}.
This is probably not the most useful filter on earth, but this is not a surprise: real value of \textit{not} expression shines when combined with
This is probably not the most useful filter on earth. The real value of \textit{not} expression shines when combined with
\textit{or}, \textit{and} filters.
\paragraph{or -- or(expression1(), expression2())}

@ -30,5 +30,6 @@
\input{creating_file}
\input{windows}
\input{tables}
\input{recordtypes}
\input{filters}
\end{document}

@ -0,0 +1,26 @@
\section{Records Modification}
\subsection{Introduction}
So far you learned how to browse trough records stored inside the content files, but not how to modify them using the \OCS{} editor. Although browsing is certainly a usefull ability on it's own, You probabbly counted on doing actual editing with this editor. There are few ways user can alter records stored in the content files, each suited for certain class of a problem. In this section We will describe how to do change records using tables interface and edit panel.
\subsubsection{Glossary}
\begin{description}
\item[Edit Panel] Interface element used inside the \OCS{} to present records data for editing. Unlike table it showes only one record at the time. However it also presents fields that are not visible inside the table. It is also safe to say that Edit Panel presents data in way that is easier to read thanks to it's horizontal layout.
\end{description}
\subsection{Edit Panel Interface}
Edit Panel is designed to aid you with record modification tasks. As It has been said, it uses vertical layout and presents some additional fields when compared with the table -- and some fields, even if they are actually displayed in the table, clearly ill-suited for modification isnide of them (this applies to fields that holds long text strings -- like descriptions). It also displays visual difference beetween non-editable field and editable.\\
To open edit panel, please open context menu on any record and choose edit action. This will open edit panel in the same window as your table and will present you the record fields. First data fields are actually not user editable and presented in the form of the text labels at the top of the edit panel. Lower data fields are presented in the form of actually user-editable widgets. Those includes spinboxes, text edits and text fields\footnote{Those are actually a valid terms used to describe classes of the user interface elements. If you don't understand those, don't worry -- those are very standard {GUI} elements present in almost every application since the rise of the desktop metaphor.}. Once you will finish editing one of those fields, data will be updated. There is no apply button of any sort -- simply use one of those widgets and be merry.\\
In addition to that you probabbly noticed some icons in the bar located at the very bottom of the edit panel. Those can be used to perform the following actions:
\begin{description}
\item[Preview] This will launch simple preview panel -- which will be described later.
\item[Next] This will switch edit panel to the next record. It is worth noticing that edit panel will skip deleted records.
\item[Prev] Do We really need to say what this button does? I guess we should! Well, this will switch edit panel to former record. Deleted records are skipped.
\end{description}
\subsection{Verification tool}
As you could notice there is nothing that can stop you from breaking the game by violating record fields logic, and yet -- it is something that you are always trying to avoid. To adress this problem \OCS{} utilizes so called verification tool (or verifer as many prefer to call it) that basicly goes trough all records and checks if it contains any illogical fields. This includes for instance torch duration equal 0\footnote{Interestingly negative values are perfectly fine: they indicate that light source has no duration limit at all. There are records like this in the original game.} or characters without name, race or any other record with a mandatory field missing.\\
This tool is even more usefull than it seems. If you somehow delete race that is used by some of the characters, all those characters will be suddenly broken. As a rule of thumb it is a good idea to use verifer before saving your content file.\\
To launch this usefull tool %don't remember, todo...
Resoults are presented as a yet another table with short (and hopefully descriptive enough) description of the identified problem. It is worth noticing that some records located in the \MW{} esm files are listed by the verification tool -- it is not fault of our tool: those records are just broken. For instance, you actually may find the 0 duration torch. However, those records are usually not placed in game world itself -- and that's good since \MW{} game engine will crash if player equip light source like this!\footnote{We would like to thanks \BS{} for such a usefull testing material. It makes us feel special.}

@ -0,0 +1,18 @@
\section{Record Types}
\subsection{Introduction}
A gameworld contains many objects, such as chests, weapons and monsters. All these objects are merely instances of templates that we call Referenceables. The OpenCS Referenceables table contains information about each of these template objects, eg. its value and weight in the case of items and an aggression level in the case of NPCs.
Let's go through all Record Types and discuss what you can tell OpenCS about them.
\begin{description}
\item[Activator:] When the player enters the same cell as this object, a script is started. Often it also has a \textbf{Script} attached to it, though it not mandatory. These scripts are small bits of code written in a special scripting language that OpenCS can read and interpret.
\item[Potion:] This is a potion that is not self-made. It has an \textbf{Icon} for your inventory, Aside from the self-explanatory \textbf{Weight} and \textbf{Coin Value}, it has an attribute called \textbf{Auto Calc} set to ``False''. This means that the effects of this potion are preconfigured. This does not happen when the player makes their own potion.
\item[Apparatus:] This is a tool to make potions. Again there's an icon for your inventory as well as a weight and a coin value. It also has a \textbf{Quality} value attached to it: higher the number, the better the effect on your potions will be. The \textbf{Apparatus Type} describes if the item is a Calcinator, Retort, Alembir or Mortar & Pestal. Each has a different effect on the potion the player makes. For more information on this subject, please refer to the \href{http://www.uesp.net/wiki/Morrowind:Alchemy#Tools}{UESP page on Alchemy Tools}.
\item[Armor:] This type of item adds \textbf{Enchantment Points} to the mix. Every piece of clothing or armor has a ''pool'' of potential magicka that gets unlocked when you enchant it. Strong enchantments consume more magicka from this pool: the stronger the enchantment, the more Enchantment Points each cast will take up. For more information on this subject, please refer to the \href{Enchant page on UESP}{http://www.uesp.net/wiki/Morrowind:Enchant}. \textbf{Health} means the amount of hit points this piece of armor has. If it sustains enough damage, the armor will be destroyed. Finally, \textbf{Armor Value} tells the game how much points to add to the player character's Armor Rating.
\item[Book:] This includes scrolls and notes. For the game to make the distinction between books and scrolls, an extra property, \textbf{Scroll}, has been added. Under the \textbf{Skill} column a scroll or book can have an in-game skill listed. Reading this item will raise the player's level in that specific skill. For more information on this, please refer to the \href{Skill Books page on UESP}{http://www.uesp.net/wiki/Morrowind:Skill_Books}.
\item[Clothing:] These items work just like Armors, but confer no protective properties. Rather than ``Armor Type'', these items have a ``Clothing Type''.
\item[Container:] This is all the stuff that stores items, from chests to sacks to plants. Its \textbf{Capacity} shows how much stuff you can put in the container. You can compare it to the maximum allowed load a player character can carry (who will get over-encumbered and unable to move if he crosses this threshold). A container, however, will just refuse to take the item in question when it gets ''over-encumbered''. \textbf{Organic Container}s are containers such as plants. Containers that \textbf{Respawn} are not safe to store stuff in. After a certain amount of time they will reset to their default contents, meaning that everything in it is gone forever.
\item[Creature:] These can be monsters, animals and the like.
\end{description}

@ -14,21 +14,14 @@ Let's browse through the various screens and see what all these tables show.
\begin{description}
\item[Record:] An entry in \OCS{} representing an item, location, sound, NPC or anything else.
\item[Reference, Referenceable:] When an item is placed in the world, it does not create a new record each time. For example, the game world might
contain a lot of exquisite belts on different NPCs and in many crates, but they all refer to one specific record: the Exquisite Belt record.
In this case, all those belts in crates and on NPCs are references. The central Exquisite Belt record is called a referenceable. This allows modders
to make changes to all items of the same type. For example, if you want all exquisite belts to have 4000 enchantment points rather than 400, you will
only need to change the referenceable Exquisite Belt rather than all exquisite belts references individually.
\item[Reference, Referenceable:] When an item is placed in the world, it isn't an isolated and unique object. For example, the game world might contain a lot of exquisite belts on different NPCs and in many crates, but they all refer to one specific record in the game's library: the Exquisite Belt record. In this case, all those belts in crates and on NPCs are references. The central Exquisite Belt record is called a referenceable. This allows modders to make changes to all items of the same type. For example, if you want all exquisite belts to have 4000 enchantment points rather than 400, you will only need to change the referenceable Exquisite Belt rather than all exquisite belts references individually.
\end{description}
\subsubsection{Recurring Terms}
Some columns are recurring throughout \OCS. They show up in (nearly) every table in \OCS.
\begin{description}
\item[ID] Each item, location, sound, etc. gets the same unique identifier in both \OCS{} and \MW. This is usually a very self-explanatory name.
For example, the ID for the (unique) black pants of Caius Cosades is ``Caius\_pants''. This allows you to manipulate the game in many ways. For example,
you could add these pants to your inventory by simply opening the console and write: ``player->addItem Caius\_pants''. Either way, in both Morrowind
and \OCS, the ID is the primary way to identify all these different parts of the game. %Wrong! Cells do not have ID, only name.
\item[ID] Many items in the OpenCS database have a unique identifier in both OpenCS and Morrowind. This is usually a very self-explanatory name. For example, the ID for the (unique) black pants of Caius Cosades is ``Caius\_pants''. This allows you to manipulate the game in many ways. For example, you could add these pants to your inventory by simply opening the console and write: ``player->addItem Caius\_pants''. Either way, in both Morrowind and OpenCS, the ID is the primary way to identify all these different parts of the game.
\item[Modified] This column shows what has happened (if something has happened) to this record. There are four possible states in which it can exist.
\item[Base] means that this record is part of the base game and is in its original state. Usually, if you create a mod, the base game is Morrowind with
optionally the Bloodmoon and Tribunal expansions.
@ -96,8 +89,8 @@ a record is, it will need specific information to function. For example, an NPC
does not. All Record Types contain at least a~model. How else would the player see them? Usually they also have a Name, which is what you see
when you hover your reticle over the object.
Let's go through all Record Types and discuss what you can tell \OCS{} about them.
This is a library of all the items, triggers, containers, NPCs, etc. in the game. There are several kinds of Record Types. Depending on which type a record is, it will need specific information to function. For example, an NPC needs a value attached to its aggression level. A chest, of course, does not. All Record Types contain at least a model. How else would the player see them? Usually they also have a Name, which is what you see when you hover your reticle over the object.
Please refer to the Record Types section for an overview of what each type of Referenceable does and what you can tell OpenCS about these objects.
\begin{description}
\item[Activator:] This is an item that, when activated, starts a script or even just shows a~tooltip.
\end{description}

@ -98,6 +98,123 @@ Allowed options:
CHANGELOG
0.33.0
Bug #371: If console assigned to ` (probably to any symbolic key), "`" symbol will be added to console every time it closed
Bug #1148: Some books'/scrolls' contents are displayed incorrectly
Bug #1290: Editor: status bar is not updated when record filter is changed
Bug #1292: Editor: Documents are not removed on closing the last view
Bug #1301: Editor: File->Exit only checks the document it was issued from.
Bug #1353: Bluetooth on with no speaker connected results in significantly longer initial load times
Bug #1436: NPCs react from too far distance
Bug #1472: PC is placed on top of following NPC when changing cell
Bug #1487: Tall PC can get stuck in staircases
Bug #1565: Editor: Subviews are deleted on shutdown instead when they are closed
Bug #1623: Door marker on Ghorak Manor's balcony makes PC stuck
Bug #1633: Loaddoor to Sadrith Mora, Telvanni Council House spawns PC in the air
Bug #1655: Use Appropriate Application Icons on Windows
Bug #1679: Tribunal expansion, Meryn Othralas the backstage manager in the theatre group in Mournhold in the great bazaar district is floating a good feet above the ground.
Bug #1705: Rain is broken in third person
Bug #1706: Thunder and lighting still occurs while the game is paused during the rain
Bug #1708: No long jumping
Bug #1710: Editor: ReferenceableID drag to references record filter field creates incorrect filter
Bug #1712: Rest on Water
Bug #1715: "Cancel" button is not always on the same side of menu
Bug #1725: Editor: content file can be opened multiple times from the same dialogue
Bug #1730: [MOD: Less Generic Nerevarine] Compile failure attempting to enter the Corprusarium.
Bug #1733: Unhandled ffmpeg sample formats
Bug #1735: Editor: "Edit Record" context menu button not opening subview for journal infos
Bug #1750: Editor: record edits result in duplicate entries
Bug #1789: Editor: Some characters cannot be used in addon name
Bug #1803: Resizing the map does not keep the pre-resize center at the post-resize center
Bug #1821: Recovering Cloudcleaver quest: attacking Sosia is considered a crime when you side with Hlormar
Bug #1838: Editor: Preferences window appears off screen
Bug #1839: Editor: Record filter title should be moved two pixels to the right
Bug #1849: Subrecord error in MAO_Containers
Bug #1854: Knocked-out actors don't fully act knocked out
Bug #1855: "Soul trapped" sound doesn't play
Bug #1857: Missing sound effect for enchanted items with empty charge
Bug #1859: Missing console command: ResetActors (RA)
Bug #1861: Vendor category "MagicItems" is unhandled
Bug #1862: Launcher doesn't start if a file listed in launcher.cfg has correct name but wrong capitalization
Bug #1864: Editor: Region field for cell record in dialogue subview not working
Bug #1869: Editor: Change label "Musics" to "Music"
Bug #1870: Goblins killed while knocked down remain in knockdown-pose
Bug #1874: CellChanged events should not trigger when crossing exterior cell border
Bug #1877: Spriggans killed instantly if hit while regening
Bug #1878: Magic Menu text not un-highlighting correctly when going from spell to item as active magic
Bug #1881: Stuck in ceiling when entering castle karstaags tower
Bug #1884: Unlit torches still produce a burning sound
Bug #1885: Can type text in price field in barter window
Bug #1887: Equipped items do not emit sounds
Bug #1889: draugr lord aesliip will attack you and remain non-hostile
Bug #1892: Guard asks player to pay bounty of 0 gold
Bug #1895: getdistance should only return max float if ref and target are in different worldspaces
Bug #1896: Crash Report
Bug #1897: Conjured Equipment cant be re-equipped if removed
Bug #1898: Only Gidar Verothan follows you during establish the mine quest
Bug #1900: Black screen when you open the door and breath underwater
Bug #1904: Crash on casting recall spell
Bug #1906: Bound item checks should use the GMSTs
Bug #1907: Bugged door. Mournhold, The Winged Guar
Bug #1908: Crime reported for attacking Drathas Nerus's henchmen while they attack Dilborn
Bug #1909: Weird Quest Flow Infidelities quest
Bug #1910: Follower fighting with gone npc
Bug #1911: Npcs will drown themselves
Bug #1912: World map arrow stays static when inside a building
Bug #1920: Ulyne Henim disappears when game is loaded inside Vas
Bug #1922: alchemy-> potion of paralyze
Bug #1923: "levitation magic cannot be used here" shows outside of tribunal
Bug #1927: AI prefer melee over magic.
Bug #1929: Tamriel Rebuilt: Named cells that lie within the overlap with Morrowind.esm are not shown
Bug #1932: BTB - Spells 14.1 magic effects don´t overwrite the Vanilla ones but are added
Bug #1935: Stacks of items are worth more when sold individually
Bug #1940: Launcher does not list addon files if base game file is renamed to a different case
Bug #1946: Mod "Tel Nechim - moved" breaks savegames
Bug #1947: Buying/Selling price doesn't properly affect the growth of mercantile skill
Bug #1950: followers from east empire company quest will fight each other if combat happens with anything
Bug #1958: Journal can be scrolled indefinitely with a mouse wheel
Bug #1959: Follower not leaving party on quest end
Bug #1960: Key bindings not always saved correctly
Bug #1961: Spell merchants selling racial bonus spells
Bug #1967: segmentation fault on load saves
Bug #1968: Jump sounds are not controlled by footsteps slider, sound weird compared to footsteps
Bug #1970: PC suffers silently when taking damage from lava
Bug #1971: Dwarven Sceptre collision area is not removed after killing one
Bug #1974: Dalin/Daris Norvayne follows player indefinitely
Bug #1975: East Empire Company faction rank breaks during Raven Rock questline
Bug #1979: 0 strength = permanently over encumbered
Bug #1993: Shrine blessing in Maar Gan doesn't work
Bug #2008: Enchanted items do not recharge
Bug #2011: Editor: OpenCS script compiler doesn't handle member variable access properly
Bug #2016: Dagoth Ur already dead in Facility Cavern
Bug #2017: Fighters Guild Quest: The Code Book - dialogue loop when UMP is loaded.
Bug #2019: Animation of 'Correct UV Mudcrabs' broken
Bug #2022: Alchemy window - Removing ingredient doesn't remove the number of ingredients
Bug #2025: Missing mouse-over text for non affordable items
Bug #2028: [MOD: Tamriel Rebuilt] Crashing when trying to enter interior cell "Ruinous Keep, Great Hall"
Bug #2029: Ienith Brothers Thiev's Guild quest journal entry not adding
Feature #471: Editor: Special case implementation for top-level window with single sub-window
Feature #472: Editor: Sub-Window re-use settings
Feature #704: Font colors import from fallback settings
Feature #854: Editor: Add user setting to show status bar
Feature #879: Editor: Open sub-views in a new top-level window
Feature #932: Editor: magic effect table
Feature #937: Editor: Path Grid table
Feature #938: Editor: Sound Gen table
Feature #1117: Death and LevelUp music
Feature #1226: Editor: Request UniversalId editing from table columns
Feature #1310: Editor: Rendering User Settings
Feature #1545: Targeting console on player
Feature #1597: Editor: Render terrain
Feature #1695: Editor: add column for CellRef's global variable
Feature #1696: Editor: use ESM::Cell's RefNum counter
Feature #1697: Redden player's vision when hit
Feature #1856: Spellcasting for non-biped creatures
Feature #1879: Editor: Run OpenMW with the currently edited content list
Task #1851: Move AI temporary state out of AI packages
Task #1865: Replace char type in records
0.32.0
Bug #1132: Unable to jump when facing a wall

Loading…
Cancel
Save