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();
if (!mSettingDefinitions->contains (settingKey))
return QString();
defs = mSettingDefinitions->value (settingKey).toStringList();
}
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,37 +278,34 @@ void MWMechanics::Alchemy::removeIngredients()
void MWMechanics::Alchemy::addPotion (const std::string& name)
{
const ESM::Potion *record = getRecord();
if (!record)
{
ESM::Potion newRecord;
ESM::Potion newRecord;
newRecord.mData.mWeight = 0;
newRecord.mData.mWeight = 0;
for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter)
if (!iter->isEmpty())
newRecord.mData.mWeight += iter->get<ESM::Ingredient>()->mBase->mData.mWeight;
for (TIngredientsIterator iter (beginIngredients()); iter!=endIngredients(); ++iter)
if (!iter->isEmpty())
newRecord.mData.mWeight += iter->get<ESM::Ingredient>()->mBase->mData.mWeight;
newRecord.mData.mWeight /= countIngredients();
newRecord.mData.mWeight /= countIngredients();
newRecord.mData.mValue = mValue;
newRecord.mData.mAutoCalc = 0;
newRecord.mData.mValue = mValue;
newRecord.mData.mAutoCalc = 0;
newRecord.mName = name;
newRecord.mName = name;
int index = static_cast<int> (std::rand()/(static_cast<double> (RAND_MAX)+1)*6);
assert (index>=0 && index<6);
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;
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);
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)
{
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);
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

@ -1,25 +1,25 @@
\section{OpenCS starting dialog}
\subsection{Introduction}
The great day has come. Today, you shall open \OCS{} application. And when you do this, you shall see our starting dialog window that holds three buttons
that can bring both pain and happiness. So just do this, please.
\subsection{Basics}
Back to the manual? Great! As you can see, the starting window holds just three buttons. Since you are already familiar with our files system, they come
to you with no surprise.\\
First, there is a \textbf{Create A New Game} button. Clearly, you should press it when you want to create a game file. Than, what \textbf{Create A New Addon} button do?
Yes! You are right! This button will create any addon content file (and new project file associated with it)! Wonderful! And what the last remaining button do? \textbf{Edit A Content File}? Well, it comes with no surprise that this should be used when you need to alter existing content file, either a game or addon.\\
\paragraph{Selecting Files For New Addon}
As We wrote earlier, both \OMW{} and \OCS{} are operating with dependency idea in mind. As You remember you should only depend on files you are actually using. But how?\\
It is simple. When you click either \textbf{Create new Addon} you will be asked to choose those with a new dialog window. The window is using vertical layout, first you should consider the the top element, the one that allows you to select a game file with drop down menu. Since we are operating on the assumption that there is only one game file loaded at the time, you can depend only on one game file. Next, choose addons that you want to use in your addon with checkboxes.\\
The last thing to do is to name your your addon and click create.
\paragraph{Selecting File for Editing}
Clicking \textbf{Edit A Content File} will show somewhat similar window. Here you should select your Game file with drop down menu. If you want to edit this game file, simply click \textbf{OK} button. If you want to alter addon depending on that file, mark it with checkbox and than click \textbf{Ok} button.
\subsection{Advanced}
If you are paying attention, you noticed any extra icon with wrench. This one will open small settings window. Those are general OpenCS settings. We will cover this is separate section.\\
\section{OpenCS starting dialog}
\subsection{Introduction}
The great day has come. Today, you shall open \OCS{} application. And when you do this, you shall see our starting dialog window that holds three buttons
that can bring both pain and happiness. So just do this, please.
\subsection{Basics}
Back to the manual? Great! As you can see, the starting window holds just three buttons. Since you are already familiar with our files system, they come
to you with no surprise.\\
First, there is a \textbf{Create A New Game} button. Clearly, you should press it when you want to create a game file. Than, what \textbf{Create A New Addon} button do?
Yes! You are right! This button will create any addon content file (and new project file associated with it)! Wonderful! And what the last remaining button do? \textbf{Edit A Content File}? Well, it comes with no surprise that this should be used when you need to alter existing content file, either a game or addon.\\
\paragraph{Selecting Files For New Addon}
As We wrote earlier, both \OMW{} and \OCS{} are operating with dependency idea in mind. As You remember you should only depend on files you are actually using. But how?\\
It is simple. When you click either \textbf{Create new Addon} you will be asked to choose those with a new dialog window. The window is using vertical layout, first you should consider the the top element, the one that allows you to select a game file with drop down menu. Since we are operating on the assumption that there is only one game file loaded at the time, you can depend only on one game file. Next, choose addons that you want to use in your addon with checkboxes.\\
The last thing to do is to name your your addon and click create.
\paragraph{Selecting File for Editing}
Clicking \textbf{Edit A Content File} will show somewhat similar window. Here you should select your Game file with drop down menu. If you want to edit this game file, simply click \textbf{OK} button. If you want to alter addon depending on that file, mark it with checkbox and than click \textbf{Ok} button.
\subsection{Advanced}
If you are paying attention, you noticed any extra icon with wrench. This one will open small settings window. Those are general OpenCS settings. We will cover this is separate section.\\
And that would be it. There is no point spending more time here. We should go forward now.

@ -1,120 +1,120 @@
\section{Files and Directories}
\subsection{Introduction}
This section of the manual covers usage of files and directories by the OpenCS. Files and directories are file system concepts,
and you are probably already familiar with it. We won't try to explain this concepts, we will just focus on \OCS.
\subsection{Used terms} %TODO
\subsection{Basics}
\paragraph{Directories}
OpenMW and \OCS{} uses multiple directories on file systems. First of, there is a \textbf{user directory} that holds configuration
files and few different folders. The location of the user directory is hard coded for each supported operating system.
%TODO list paths.
In addition to this single hard coded directory, both \OMW{} and \OCS{} need a~place to seek for actual data files of the game:
textures, models, sounds and files that store records of objects in game; dialogues and so one -- so called content files. We support
multiple such paths (we call it \textbf{data paths}) as specified in the configuration. Usually one data path points to the directory
where original \MW{} is either installed or unpacked. You are free to specify as many data paths as you would like,
however, there is one special data path that, as described later, is used to store newly created content files.
\paragraph{Content files}
\BS{} \MW{} engine is using two types of files: ESM (master) and ESP (plugin). The distinction between those
is not clear, and often confusing. You would expect the ESM (master) file is used to specify one master, that is modified by the ESPs plugins,
and indeed: this is the basic idea. However, original expansions also were made as ESM files, even though they essentially could be
described as a really large plugins, and therefore rather use ESP files. There were technical reasons behind this decision -- somewhat valid
in the case of original engine, but clearly it's better to create a system that can be used is more sensible way. \OMW{} achieves
this with our own content file types.
We support both ESM and ESP files, but in order to make use of new features of OpenMW one should consider using new file types designed
with our engine in mind: game files and addon files together called ``content files``.
\subparagraph{OpenMW content files}
Game and Addon files are concept somewhat similar to the old ESM/ESP, only in the way it should be from the very beginning. Nothing easier
to describe. If you want to make new game using \OMW{} as engine (so called ``total conversion'') you should create a game file.
If you want to create a addon for existing game file -- simply create addon file. Nothing else matters: The only distinction you should
consider is if your project is about changing other game, or creating a new one. Simple as that.
Other simple thing about content files are extensions. We are using .omwaddon for addon files and .omwgame for game files.
%TODO describe what content files contains. and what not.
\subparagraph{\MW{} content files}
Using our content files is recommended solution for projects that are intended to used with \OMW{} engine. However some players
wish to use original \MW{} engine, even with it large flaws and lacking features\footnote{If this is actually wrong, we are very
successful project. Yay!}. Also, since 2002 thousands of ESP/ESM files were created, some with really outstanding content.
Because of this \OCS{} simply has no other choice but support ESP/ESM files. However, if you decided to choose ESP/ESM file instead
using our own content file types you are most likely aim at the original engine compatibility. This subject is covered in the very
last section of this manual. %not finished TODO add the said section. Most likely when more features are present.
The actual creation of new files is described in the next chapter. Here we are gonna focus only on details that you need to know
in order to create your first \OCS{} file while full understanding your needs. For now let's jut remember that content files
are created inside the user directory, in the the \textbf{data} subfolder (that is the one special data directory mentioned earlier).
\subparagraph{Dependencies}
Since addon is supposed to change the game it is logical that it also depends on the said game. It simply can not work otherwise.
Just think about it: your modification is changing prize of the iron sword. But what if there is no iron sword in game? That is right:
we get nonsense. What you want to do is to tie your addon to the files you are changing. Those can be either game files (expansion island
for a game) or other addon files (house on the said island). It is a good idea to be dependent only on files that are really changed
in your addon obviously, but sadly there is no other way to achieve this than knowing what you want to do. Again, please remember that
this section of the manual does not cover creating the content files -- it is only theoretical introduction to the subject. For now just
keep in mind that dependencies exist, and is up to you what to decide if your content file should depend on other content file.
Game files are not intend to have any dependencies for a very simple reasons: player is using only one game file (excluding original
and dirty {ESP/ESM} system) at the time and therefore no game file can depend on other game file, and since game file makes the base
for addon files -- it can not depend on addon files.
%\subparagraph{Loading order} %TODO
\paragraph{Project files}
Project files act as containers for data not used by the \OMW{} game engine itself, but still useful for OpenCS. The shining example
of this data category are without doubt record filters (described in the later section of the manual you are reading currently).
As a mod author you probably do not need and/or want to distribute project files at all, they are meant to be used only by you.
As you would imagine, project file makes sense only in combination with actual content files. In fact, each time you start to work
on new content file and project file was not found, it will be created.
Project files extension is, to not surprise ``.project''. The whole name of the project file is the whole name of the content file
with appended extensions. For instance swords.omwaddon file is associated with swords.omwaddon.project file.
%TODO where are they stored.
Project files are stored inside the user directory, in the \textbf{projects} subfolder. This is the path location for both freshly
created project files, and a place where \OCS{} looks for already existing files.
\paragraph{Resources files}
%textures, sounds, whatever
Unless we are talking about the fully text based game, like Zork or Rogue, you are expecting that a video game is using some media files:
models with textures, pictures acting as icons, sounds and everything else. Since content files, no matter if it is ESP, ESM or new \OMW{}
file type do not contain any of those, it is clear that they have to be deliver with a different file. It is also clear that this,
let's call it ``resources file``, have to be supported by the engine. Without code handling those files, it is nothing more than
a mathematical abstraction -- something, that lacks meaning for human beings\footnote{Unless we call programmers a human beings.}.
Therefore this section must cover ways to add resources files to your content file, and point out what is supported. We are going
to do just that. Later, you will learn how to make use of those files in your content.
\subparagraph{Audio}
OpenMW is using {FFmpeg} for audio playback, and so we support every audio type that is supported by this library. This makes a huge list.
Below is only small portion of supported file types.
\begin{description}
\item mp3 ({MPEG}-1 {Part 3 Layer 3}) popular audio file format and \textit{de facto} standard for storing audio. Used by the \MW{} game.
\item ogg open source, multimedia container file using high quality vorbis audio codec. Recommended.
\end{description}
\subparagraph{Video}
As in the case of audio files, we are using {FFmepg} to decode video files. The list of supported files is long, we will cover
only the most significant.
\begin{description}
\item bik videos used by original \MW{} game.
\item mp4 multimedia container which use more advanced codecs ({MPEG-4 Parts 2,3,10}) with a better audio and video compression rate,
but also requiring more {CPU} intensive decoding -- this makes it probably less suited for storing sounds in computer games, but good for videos.
\item webm is a new, shiny and open source video format with excellent compression. It needs quite a lot of processing power to be decoded,
but since game logic is not running during cut scenes we can recommend it for use with \OMW.
\item ogv alternative, open source container using theora codec for video and vorbis for audio.
\end{description}
\subparagraph{Textures and images}
Original \MW{} game uses {DDS} and {TGA} files for all kind of two dimensional images and textures alike. In addition, engine supported BMP
files for some reason ({BMP} is a terrible format for a video game). We also support extended set of image files -- including {JPEG} and {PNG}.
JPEG and PNG files can be useful in some cases, for instance JPEG file is a valid option for skybox texture and PNG can useful for masks.
However please, keep in mind that JPEG can grow into large sizes quickly and are not the best option with {DirectX} rendering backend. You probabbly still want
to use {DDS} files for textures.
\section{Files and Directories}
\subsection{Introduction}
This section of the manual covers usage of files and directories by the OpenCS. Files and directories are file system concepts,
and you are probably already familiar with it. We won't try to explain this concepts, we will just focus on \OCS.
\subsection{Used terms} %TODO
\subsection{Basics}
\paragraph{Directories}
OpenMW and \OCS{} uses multiple directories on file systems. First of, there is a \textbf{user directory} that holds configuration
files and few different folders. The location of the user directory is hard coded for each supported operating system.
%TODO list paths.
In addition to this single hard coded directory, both \OMW{} and \OCS{} need a~place to seek for actual data files of the game:
textures, models, sounds and files that store records of objects in game; dialogues and so one -- so called content files. We support
multiple such paths (we call it \textbf{data paths}) as specified in the configuration. Usually one data path points to the directory
where original \MW{} is either installed or unpacked. You are free to specify as many data paths as you would like,
however, there is one special data path that, as described later, is used to store newly created content files.
\paragraph{Content files}
\BS{} \MW{} engine is using two types of files: ESM (master) and ESP (plugin). The distinction between those
is not clear, and often confusing. You would expect the ESM (master) file is used to specify one master, that is modified by the ESPs plugins,
and indeed: this is the basic idea. However, original expansions also were made as ESM files, even though they essentially could be
described as a really large plugins, and therefore rather use ESP files. There were technical reasons behind this decision -- somewhat valid
in the case of original engine, but clearly it's better to create a system that can be used is more sensible way. \OMW{} achieves
this with our own content file types.
We support both ESM and ESP files, but in order to make use of new features of OpenMW one should consider using new file types designed
with our engine in mind: game files and addon files together called ``content files``.
\subparagraph{OpenMW content files}
Game and Addon files are concept somewhat similar to the old ESM/ESP, only in the way it should be from the very beginning. Nothing easier
to describe. If you want to make new game using \OMW{} as engine (so called ``total conversion'') you should create a game file.
If you want to create a addon for existing game file -- simply create addon file. Nothing else matters: The only distinction you should
consider is if your project is about changing other game, or creating a new one. Simple as that.
Other simple thing about content files are extensions. We are using .omwaddon for addon files and .omwgame for game files.
%TODO describe what content files contains. and what not.
\subparagraph{\MW{} content files}
Using our content files is recommended solution for projects that are intended to used with \OMW{} engine. However some players
wish to use original \MW{} engine, even with it large flaws and lacking features\footnote{If this is actually wrong, we are very
successful project. Yay!}. Also, since 2002 thousands of ESP/ESM files were created, some with really outstanding content.
Because of this \OCS{} simply has no other choice but support ESP/ESM files. However, if you decided to choose ESP/ESM file instead
using our own content file types you are most likely aim at the original engine compatibility. This subject is covered in the very
last section of this manual. %not finished TODO add the said section. Most likely when more features are present.
The actual creation of new files is described in the next chapter. Here we are gonna focus only on details that you need to know
in order to create your first \OCS{} file while full understanding your needs. For now let's jut remember that content files
are created inside the user directory, in the the \textbf{data} subfolder (that is the one special data directory mentioned earlier).
\subparagraph{Dependencies}
Since addon is supposed to change the game it is logical that it also depends on the said game. It simply can not work otherwise.
Just think about it: your modification is changing prize of the iron sword. But what if there is no iron sword in game? That is right:
we get nonsense. What you want to do is to tie your addon to the files you are changing. Those can be either game files (expansion island
for a game) or other addon files (house on the said island). It is a good idea to be dependent only on files that are really changed
in your addon obviously, but sadly there is no other way to achieve this than knowing what you want to do. Again, please remember that
this section of the manual does not cover creating the content files -- it is only theoretical introduction to the subject. For now just
keep in mind that dependencies exist, and is up to you what to decide if your content file should depend on other content file.
Game files are not intend to have any dependencies for a very simple reasons: player is using only one game file (excluding original
and dirty {ESP/ESM} system) at the time and therefore no game file can depend on other game file, and since game file makes the base
for addon files -- it can not depend on addon files.
%\subparagraph{Loading order} %TODO
\paragraph{Project files}
Project files act as containers for data not used by the \OMW{} game engine itself, but still useful for OpenCS. The shining example
of this data category are without doubt record filters (described in the later section of the manual you are reading currently).
As a mod author you probably do not need and/or want to distribute project files at all, they are meant to be used only by you.
As you would imagine, project file makes sense only in combination with actual content files. In fact, each time you start to work
on new content file and project file was not found, it will be created.
Project files extension is, to not surprise ``.project''. The whole name of the project file is the whole name of the content file
with appended extensions. For instance swords.omwaddon file is associated with swords.omwaddon.project file.
%TODO where are they stored.
Project files are stored inside the user directory, in the \textbf{projects} subfolder. This is the path location for both freshly
created project files, and a place where \OCS{} looks for already existing files.
\paragraph{Resources files}
%textures, sounds, whatever
Unless we are talking about the fully text based game, like Zork or Rogue, you are expecting that a video game is using some media files:
models with textures, pictures acting as icons, sounds and everything else. Since content files, no matter if it is ESP, ESM or new \OMW{}
file type do not contain any of those, it is clear that they have to be deliver with a different file. It is also clear that this,
let's call it ``resources file``, have to be supported by the engine. Without code handling those files, it is nothing more than
a mathematical abstraction -- something, that lacks meaning for human beings\footnote{Unless we call programmers a human beings.}.
Therefore this section must cover ways to add resources files to your content file, and point out what is supported. We are going
to do just that. Later, you will learn how to make use of those files in your content.
\subparagraph{Audio}
OpenMW is using {FFmpeg} for audio playback, and so we support every audio type that is supported by this library. This makes a huge list.
Below is only small portion of supported file types.
\begin{description}
\item mp3 ({MPEG}-1 {Part 3 Layer 3}) popular audio file format and \textit{de facto} standard for storing audio. Used by the \MW{} game.
\item ogg open source, multimedia container file using high quality vorbis audio codec. Recommended.
\end{description}
\subparagraph{Video}
As in the case of audio files, we are using {FFmepg} to decode video files. The list of supported files is long, we will cover
only the most significant.
\begin{description}
\item bik videos used by original \MW{} game.
\item mp4 multimedia container which use more advanced codecs ({MPEG-4 Parts 2,3,10}) with a better audio and video compression rate,
but also requiring more {CPU} intensive decoding -- this makes it probably less suited for storing sounds in computer games, but good for videos.
\item webm is a new, shiny and open source video format with excellent compression. It needs quite a lot of processing power to be decoded,
but since game logic is not running during cut scenes we can recommend it for use with \OMW.
\item ogv alternative, open source container using theora codec for video and vorbis for audio.
\end{description}
\subparagraph{Textures and images}
Original \MW{} game uses {DDS} and {TGA} files for all kind of two dimensional images and textures alike. In addition, engine supported BMP
files for some reason ({BMP} is a terrible format for a video game). We also support extended set of image files -- including {JPEG} and {PNG}.
JPEG and PNG files can be useful in some cases, for instance JPEG file is a valid option for skybox texture and PNG can useful for masks.
However please, keep in mind that JPEG can grow into large sizes quickly and are not the best option with {DirectX} rendering backend. You probabbly still want
to use {DDS} files for textures.
%\subparagraph{Meshes} %TODO once we will support something more than just nifs

@ -1,205 +1,202 @@
\section{Record filters}
\subsection{Introduction}
Filters are the key element of \OCS{} use cases by allowing rapid and easy access to the searched records presented in all tables.
Therefore: in order to use this application fully effective you should make sure that all concepts and instructions written in
the this section of the manual are perfectly clear to you.
Do not be afraid though, filters are fairly intuitive and easy to use.
\subsubsection{Used Terms}
\begin{description}
\item[Filter] is generally speaking a tool able to ``Filter'' (that is: select some elements, while discarding others) according
to the some criteria. In case of \OCS: records are being filtered according to the criteria of user choice. Criteria are written
down in language with simple syntax.
\item[Criteria] describes condition under with any any record is being select by the filter.
\item[Syntax] as you may noticed computers (in general) are rather strict, and expect only strictly formulated orders -- that is:
written with correct syntax.
\item[Expression] is way we are actually performing filtering. Filter can be treated as ``functions'': accepts arguments, and evaluates
either to the true or false for every column record at the time.
\item[N-ary] is any expression that expects one or more expressions as arguments. It is useful for grouping two (or more) other expressions
together in order to create filter that will check for criteria placed in two (again: or more) columns (logical \textit{or}, \textit{and}).
\item[unary] is any expression that expects one other expression. The example is \textit{not} expression. In fact \textit{not} is the only useful
unary expression in \OCS{} record filters.
\item[nullary] is expression that does not accepts other expressions. It accepts arguments specified later.
\end{description}
\subsubsection{Basics}
In fact you do not need to learn everything about filters in order to use them. In fact all you need to know to achieve decent productivity
with \OCS{} is inside basics section.
\subsubsection{Interface}
Above each table there is a field that is used to enter filter: either predefined by the \OMW{} developers or made by you, the user.
You probably noticed it before. However there is also completely new element, although using familiar table layout. Go to the application
menu view, and click filters. You should see set of default filters, made by the \OMW{} team in the table with the following columns: filter,
description and modified.
\begin{description}
\item[ID] contains the name of the filter.
\item[Modified] just like in all other tables you have seen so far modified indicates if a filter was added, modified or removed.
\item[Filter] column containing expression of the filter.
\item[Description] contains the short description of the filter function. Do not expect any surprises there.
\end{description}
So let's learn how to actually use those to speed up your work.
\subsubsection{Using predefined filters}
Using those filters is quite easy and involves typing inside the filter field above the table. For instance, try to open referencables
table and type in the filters field the following: \mono{project::weapons}. As soon as you complete the text, table will magicly alter
and will show only the weapons. As you could noticed \mono{project::weapons} is nothing else than a~ID of one of the predefined filters. That is it:
in order to use the filter inside the table you simply type it is name inside the filter field.
To make life easier filter IDs follow simple convention.
\begin{itemize}
\item Filter ID filtering a specific record type contains usually the name of a specific group. For instance \mono{project::weapons} filter
contains the word weapons (did you noticed?). Plural form is always used.
\item When filtering specific subgroup the ID starts just like in the case of general filter. For instance \mono{project::weaponssilver} will
filter only silver weapons (new mechanic introduced by the \BM{}, silver weapons deal double damage against werewolfs) and
\mono{project::weaponsmagical} will filter only magical weapons (able to hurt ghosts and other supernatural creatures).
\item There are few exceptions from the above rule. For instance there is a \mono{project::added}, \mono{project::removed},
\mono{project::modyfied}, \mono{project::base}.
You would probably except something more like \mono{project::statusadded} but in this case typing this few extra characters would only
help to break your keyboard faster.
\end{itemize}
We strongly recommend to take a look at the filters table right now to see what you can filter with that. And try using it! It is very simple.
\subsection{Advanced}
Back to the manual? Great.
If you want to create your own filter you have to know exactly what do you want to get in order to translate this into the expressions.
Finally, you will have to write this with correct syntax. As a result table will show only desired rows.
Advance subsection covers everything that you need to know in order to create any filter you may want to %TODO the filter part is actually wrong
\subsubsection{Namespaces}
Did you noticed that every default filter has \mono{project::} prefix? It is a \textit{namespace}, a~term borrowed from the \CPP{} language.
In case of \OCS{} namespace always means scope of the said object\footnote{You are not supposed to understand this at the moment.}.
But what does it mean in case of filters? Well, short explanation is actually simple.
\begin{description}
\item[project::] namespace indicates that filter is used with the project, in multiple sessions. You can restart \OCS{} and filter
is still there.
\item[session::] namespace indicates that filter is not stored trough multiple sessions and once you will quit \OCS{} (close session)
the filter will be gone. Forever! Until then it can be found inside the filters table.
\end{description}
In addition to this two scopes, there is a third one; called one-shot. One-shot filters are not stored (even during single session)
anywhere and as the name implies they are supposed to be created when needed only once. Good thing about the one-shot filters is that
you do not need to open filters table in order to create it. Instead you just type it directly inside the filter field, starting with
exclamation mark: ``!''.
Still, you may wonder how you are supposed to write expressions, what expressions you should use, and what syntax looks like. Let's start
with nullary expressions that will allow you to create a basic filter.
\subsubsection{Nullary expressions}
All nullary expressions are used in similar manner. First off: you have to write it is name (for instance: \mono{string}) and secondly:
condition that will be checked inside brackets (for instance \mono{string(something, something)}). If conditions of your expression will be meet
by a record (technical speaking: expression will evaluate to true) the record will show up in the table.
It is clear that you need to know what are you checking, that is: what column of the table contains information that you are interested
in and what should be inside specific cell inside this column to meet your requirements. In most cases first word inside brackets sets column
you want to see, while the second one sets desired value inside of the cell. To separate column argument from the value argument use comma.
\paragraph{String -- string(``column'', ``value'')}
String in programmers language is often\footnote{Often, not always. There are different programming languages using slightly different terms.}
just a word for anything composed of characters. In case of \OCS{} this is in fact true for every value inside the column that is not composed
of the pure numbers. Even columns containing only ``true`` and ``false`` values can be targeted by the string expression\footnote{There is no
Boolean (``true'' or ``false'') value in the \OCS. You should use string for those.}. String evaluates to true,
when record contains in the specified column exactly the same value as specified.
Since majority of the columns contain string values, string is among the most often used expressions. Examples:
\begin{itemize}
\item \mono{string(``Record Type'', ``Weapon'')} -- will evaluate to true for all records containing \mono{Weapon} in the \mono{Record Type} column cell.
This group contains every weapon (including arrows and bolts) found in the game.
\item \mono{string(``Portable'', ``true'')} -- will evaluate to true for all records containing word true inside \mono{Portable} column cell.
This group contains every portable light sources (lanterns, torches etc.).
\end{itemize}
This is probably enough to create around 90 string filters you will eventually need. However, this expression is even more powerful
-- 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?
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:
when you are writing any word, you are using strictly defined letters -- that is: letters create a word. What you want to do with regular
expression is to use set of rules that will match to many words. It is not that difficult to see what it's needed to do so: first,
you will clearly need way to determinate what letters you want to match (word is composed by letters).
Before introducing other ways to choose between characters, I want explain anchors. Anchors allows you to decide where to ``look'' in the string.
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.
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}.
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.
\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
``weight``. To filter those we need a value expression. This one works in similar manner to the string filter: first token name and criteria
inside brackets. Clearly, conditions should hold column to test in. However in this case wanted value is specified as a range.
As you would imagine the range can be specified as including a border value, or excluding. We are using two types of brackets for this:
\begin{itemize}
\item To include value use [] brackets. For value equal 5, expression \mono{value(something, [5, 10])} will evaluate to true.
\item To exclude value use () brackets. For value equal 5, expression \mono{value(something, (5, 10))} will evaluate to false.
\item Mixing brackets is completely legal. For value equal 10, expression \mono{value(something, [5, 10)} will evaluate to true.
The same expression will evaluate to false for value equal 10.
\end{itemize}
\paragraph{``true'' and ``false''}
Nullary \textit{true} and \textit{false} do not accept any arguments, and always evaluates to true (in case of \textit{true})
and false (in case of \textit{false}) no matter what. The main usage of this expressions is the give users ability to quickly
disable some part of the filter that makes heavy use of the logical expressions.
\subsubsection{Logical expressions}
This subsection takes care of two remaining groups of expressions: binary and unary. The only unary expression present in the \OCS{} is logical
\textit{not}, while the remaining binary expressions are: \textit{or}, \textit{and}. This clearly makes them (from the user point of view)
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.
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
\textit{or}, \textit{and} filters.
\paragraph{or -- or(expression1(), expression2())}
\textit{Or} is a expression that will return true if one of the arguments evaluates to true. You can use two or more arguments, separated by the comma.
\textit{Or} expression is useful when showing two different group of records is needed. For instance the standard actor filter is using the following
\mono{or(string(``record type'', npc), string(``record type'', creature))} and will show both npcs and creatures.
\paragraph{and -- and(expression1(), expression2())}
\textit{And} is a expression that will return true if all arguments evaluates to true. As in the case of ``or'' you can use two or more arguments,
separated by a comma.
As we mentioned earlier in the \textit{not} filter, combining \textit{not} with \textit{and} can be very useful. For instance to show all armor types,
excluding gauntlets you can write the following: \mono{and (not string(``armor type'', ``.* gauntlet''), string(``Record Type'', ``Armor''))}.
\subsubsection{Creating and saving filter}
In order to create and save new filter, you should go to the filters table, right click and select option ``add record'' from the context menu.
A horizontal widget group at the bottom of the table should show up. From there you should select a namespace responsible for scope of
the filter (described earlier) and desired ID of the filter. After pressing OK button new entry will show up in the filters table. This filter
does nothing at the moment, since it still lacks expressions. In order to add your formula simply double click the filter cell of the new entry
and write it down there.
Done! You are free to use your filter.
\subsubsection{Replacing the default filters set}
OpenCS allows you to substitute default filters set provided by us, with your own filters. In order to do so you should create a new project,
add desired filters, remove undesired and save. Rename the file to the ``defaultfilters'' (do not forget to remove .omwaddon.project extension)
and place it inside your configuration directory.
The file acts as template for all new project files from now. If you wish to go back to the old default set, simply rename or remove the custom file.
\section{Record filters}
\subsection{Introduction}
Filters are the key element of \OCS{} use cases by allowing rapid and easy access to the searched records presented in all tables.
Therefore: in order to use this application fully effective you should make sure that all concepts and instructions written in
the this section of the manual are perfectly clear to you.
Do not be afraid though, filters are fairly intuitive and easy to use.
\subsubsection{Used Terms}
\begin{description}
\item[Filter] is generally speaking a tool able to ``Filter'' (that is: select some elements, while discarding others) according
to the some criteria. In case of \OCS: records are being filtered according to the criteria of user choice. Criteria are written
down in language with simple syntax.
\item[Criteria] describes condition under with any any record is being select by the filter.
\item[Syntax] as you may noticed computers (in general) are rather strict, and expect only strictly formulated orders -- that is:
written with correct syntax.
\item[Expression] is way we are actually performing filtering. Filter can be treated as ``functions'': accepts arguments, and evaluates
either to the true or false for every column record at the time.
\item[N-ary] is any expression that expects one or more expressions as arguments. It is useful for grouping two (or more) other expressions
together in order to create filter that will check for criteria placed in two (again: or more) columns (logical \textit{or}, \textit{and}).
\item[unary] is any expression that expects one other expression. The example is \textit{not} expression. In fact \textit{not} is the only useful
unary expression in \OCS{} record filters.
\item[nullary] is expression that does not accepts other expressions. It accepts arguments specified later.
\end{description}
\subsubsection{Basics}
In fact you do not need to learn everything about filters in order to use them. In fact all you need to know to achieve decent productivity
with \OCS{} is inside basics section.
\subsubsection{Interface}
Above each table there is a field that is used to enter filter: either predefined by the \OMW{} developers or made by you, the user.
You probably noticed it before. However there is also completely new element, although using familiar table layout. Go to the application
menu view, and click filters. You should see set of default filters, made by the \OMW{} team in the table with the following columns: filter,
description and modified.
\begin{description}
\item[ID] contains the name of the filter.
\item[Modified] just like in all other tables you have seen so far modified indicates if a filter was added, modified or removed.
\item[Filter] column containing expression of the filter.
\item[Description] contains the short description of the filter function. Do not expect any surprises there.
\end{description}
So let's learn how to actually use those to speed up your work.
\subsubsection{Using predefined filters}
Using those filters is quite easy and involves typing inside the filter field above the table. For instance, try to open referencables
table and type in the filters field the following: \mono{project::weapons}. As soon as you complete the text, table will magicly alter
and will show only the weapons. As you could noticed \mono{project::weapons} is nothing else than a~ID of one of the predefined filters. That is it:
in order to use the filter inside the table you simply type it is name inside the filter field.
To make life easier filter IDs follow simple convention.
\begin{itemize}
\item Filter ID filtering a specific record type contains usually the name of a specific group. For instance \mono{project::weapons} filter
contains the word weapons (did you noticed?). Plural form is always used.
\item When filtering specific subgroup the ID starts just like in the case of general filter. For instance \mono{project::weaponssilver} will
filter only silver weapons (new mechanic introduced by the \BM{}, silver weapons deal double damage against werewolfs) and
\mono{project::weaponsmagical} will filter only magical weapons (able to hurt ghosts and other supernatural creatures).
\item There are few exceptions from the above rule. For instance there is a \mono{project::added}, \mono{project::removed},
\mono{project::modyfied}, \mono{project::base}.
You would probably except something more like \mono{project::statusadded} but in this case typing this few extra characters would only
help to break your keyboard faster.
\end{itemize}
We strongly recommend to take a look at the filters table right now to see what you can filter with that. And try using it! It is very simple.
\subsection{Advanced}
Back to the manual? Great.
If you want to create your own filter you have to know exactly what do you want to get in order to translate this into the expressions.
Finally, you will have to write this with correct syntax. As a result table will show only desired rows.
Advance subsection covers everything that you need to know in order to create any filter you may want to %TODO the filter part is actually wrong
\subsubsection{Namespaces}
Did you noticed that every default filter has \mono{project::} prefix? It is a \textit{namespace}, a~term borrowed from the \CPP{} language.
In case of \OCS{} namespace always means scope of the said object\footnote{You are not supposed to understand this at the moment.}.
But what does it mean in case of filters? Well, short explanation is actually simple.
\begin{description}
\item[project::] namespace indicates that filter is used with the project, in multiple sessions. You can restart \OCS{} and filter
is still there.
\item[session::] namespace indicates that filter is not stored trough multiple sessions and once you will quit \OCS{} (close session)
the filter will be gone. Forever! Until then it can be found inside the filters table.
\end{description}
In addition to this two scopes, there is a third one; called one-shot. One-shot filters are not stored (even during single session)
anywhere and as the name implies they are supposed to be created when needed only once. Good thing about the one-shot filters is that
you do not need to open filters table in order to create it. Instead you just type it directly inside the filter field, starting with
exclamation mark: ``!''.
Still, you may wonder how you are supposed to write expressions, what expressions you should use, and what syntax looks like. Let's start
with nullary expressions that will allow you to create a basic filter.
\subsubsection{Nullary expressions}
All nullary expressions are used in similar manner. First off: you have to write it is name (for instance: \mono{string}) and secondly:
condition that will be checked inside brackets (for instance \mono{string(something, something)}). If conditions of your expression will be meet
by a record (technical speaking: expression will evaluate to true) the record will show up in the table.
It is clear that you need to know what are you checking, that is: what column of the table contains information that you are interested
in and what should be inside specific cell inside this column to meet your requirements. In most cases first word inside brackets sets column
you want to see, while the second one sets desired value inside of the cell. To separate column argument from the value argument use comma.
\paragraph{String -- string(``column'', ``value'')}
String in programmers language is often\footnote{Often, not always. There are different programming languages using slightly different terms.}
just a word for anything composed of characters. In case of \OCS{} this is in fact true for every value inside the column that is not composed
of the pure numbers. Even columns containing only ``true`` and ``false`` values can be targeted by the string expression\footnote{There is no
Boolean (``true'' or ``false'') value in the \OCS. You should use string for those.}. String evaluates to true,
when record contains in the specified column exactly the same value as specified.
Since majority of the columns contain string values, string is among the most often used expressions. Examples:
\begin{itemize}
\item \mono{string(``Record Type'', ``Weapon'')} -- will evaluate to true for all records containing \mono{Weapon} in the \mono{Record Type} column cell.
This group contains every weapon (including arrows and bolts) found in the game.
\item \mono{string(``Portable'', ``true'')} -- will evaluate to true for all records containing word true inside \mono{Portable} column cell.
This group contains every portable light sources (lanterns, torches etc.).
\end{itemize}
This is probably enough to create around 90 string filters you will eventually need. However, this expression is even more powerful
-- 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 \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
\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:
when you are writing any word, you are using strictly defined letters -- that is: letters create a word. What you want to do with regular
expression is to use set of rules that will match to many words. It is not that difficult to see what it's needed to do so: first,
you will clearly need way to determinate what letters you want to match (word is composed by letters).
Before introducing other ways to choose between characters, I want explain anchors. Anchors allows you to decide where to ``look'' in the string.
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.
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, 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
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.
\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
``weight``. To filter those we need a value expression. This one works in similar manner to the string filter: first token name and criteria
inside brackets. Clearly, conditions should hold column to test in. However in this case wanted value is specified as a range.
As you would imagine the range can be specified as including a border value, or excluding. We are using two types of brackets for this:
\begin{itemize}
\item To include value use [] brackets. For value equal 5, expression \mono{value(something, [5, 10])} will evaluate to true.
\item To exclude value use () brackets. For value equal 5, expression \mono{value(something, (5, 10))} will evaluate to false.
\item Mixing brackets is completely legal. For value equal 10, expression \mono{value(something, [5, 10)} will evaluate to true.
The same expression will evaluate to false for value equal 10.
\end{itemize}
\paragraph{``true'' and ``false''}
Nullary \textit{true} and \textit{false} do not accept any arguments, and always evaluates to true (in case of \textit{true})
and false (in case of \textit{false}) no matter what. The main usage of this expressions is the give users ability to quickly
disable some part of the filter that makes heavy use of the logical expressions.
\subsubsection{Logical expressions}
This subsection takes care of two remaining groups of expressions: binary and unary. The only unary expression present in the \OCS{} is logical
\textit{not}, while the remaining binary expressions are: \textit{or}, \textit{and}. This clearly makes them (from the user point of view)
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. 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. The real value of \textit{not} expression shines when combined with
\textit{or}, \textit{and} filters.
\paragraph{or -- or(expression1(), expression2())}
\textit{Or} is a expression that will return true if one of the arguments evaluates to true. You can use two or more arguments, separated by the comma.
\textit{Or} expression is useful when showing two different group of records is needed. For instance the standard actor filter is using the following
\mono{or(string(``record type'', npc), string(``record type'', creature))} and will show both npcs and creatures.
\paragraph{and -- and(expression1(), expression2())}
\textit{And} is a expression that will return true if all arguments evaluates to true. As in the case of ``or'' you can use two or more arguments,
separated by a comma.
As we mentioned earlier in the \textit{not} filter, combining \textit{not} with \textit{and} can be very useful. For instance to show all armor types,
excluding gauntlets you can write the following: \mono{and (not string(``armor type'', ``.* gauntlet''), string(``Record Type'', ``Armor''))}.
\subsubsection{Creating and saving filter}
In order to create and save new filter, you should go to the filters table, right click and select option ``add record'' from the context menu.
A horizontal widget group at the bottom of the table should show up. From there you should select a namespace responsible for scope of
the filter (described earlier) and desired ID of the filter. After pressing OK button new entry will show up in the filters table. This filter
does nothing at the moment, since it still lacks expressions. In order to add your formula simply double click the filter cell of the new entry
and write it down there.
Done! You are free to use your filter.
\subsubsection{Replacing the default filters set}
OpenCS allows you to substitute default filters set provided by us, with your own filters. In order to do so you should create a new project,
add desired filters, remove undesired and save. Rename the file to the ``defaultfilters'' (do not forget to remove .omwaddon.project extension)
and place it inside your configuration directory.
The file acts as template for all new project files from now. If you wish to go back to the old default set, simply rename or remove the custom file.

@ -1,34 +1,35 @@
\documentclass[american]{article}
\usepackage[T1]{fontenc}
\usepackage{babel}
\usepackage{txfonts} % Public Times New Roman text & math font
\usepackage[pdftex]{graphicx}
\usepackage[final,colorlinks,pdftex,pdfpagelabels=true]{hyperref}
\author{OpenMW Team}
\def\pdfBorderAttrs{/Border [0 0 0] } % No border arround Links
\def\CPP{{C\kern-.05em\raise.23ex\hbox{+\kern-.05em+}}}
\hypersetup{%
pdfauthor={Copyright \textcopyright{} OpenMW Team },
pdftitle={OpenCS user manual}
}
\def\mono{\texttt}
\def\MW{\textit{Morrowind\texttrademark{}}}
\def\TB{\textit{Tribunal}}
\def\BM{\textit{Bloodmon}}
\def\BS{Bethesda Softworks}
\def\OMW{\hbox{OpenMW}}
\def\OCS{\hbox{OpenCS}}
\begin{document}
\title{OpenCS User Manual}
\maketitle
\tableofcontents{}
\input{files_and_directories}
\input{creating_file}
\input{windows}
\input{tables}
\input{filters}
\end{document}
\documentclass[american]{article}
\usepackage[T1]{fontenc}
\usepackage{babel}
\usepackage{txfonts} % Public Times New Roman text & math font
\usepackage[pdftex]{graphicx}
\usepackage[final,colorlinks,pdftex,pdfpagelabels=true]{hyperref}
\author{OpenMW Team}
\def\pdfBorderAttrs{/Border [0 0 0] } % No border arround Links
\def\CPP{{C\kern-.05em\raise.23ex\hbox{+\kern-.05em+}}}
\hypersetup{%
pdfauthor={Copyright \textcopyright{} OpenMW Team },
pdftitle={OpenCS user manual}
}
\def\mono{\texttt}
\def\MW{\textit{Morrowind\texttrademark{}}}
\def\TB{\textit{Tribunal}}
\def\BM{\textit{Bloodmon}}
\def\BS{Bethesda Softworks}
\def\OMW{\hbox{OpenMW}}
\def\OCS{\hbox{OpenCS}}
\begin{document}
\title{OpenCS User Manual}
\maketitle
\tableofcontents{}
\input{files_and_directories}
\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}

@ -1,103 +1,96 @@
\section{Tables}
\subsection{Introduction}
If you have launched \OCS{} already and played around with it for a bit, you have probably gotten the impression that it contains lots of tables.
You'd be spot on: \OCS{} is built around using tables. This does not mean it works just like Microsoft Excel or Libre Office Calc, though.
Due to the vast amounts of information involved with \MW, tables just made the most sense. You have to be able to spot information quickly
and be able to change them on the fly.
Let's browse through the various screens and see what all these tables show.
\subsection{Used Terms}
\subsubsection{Glossary}
\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.
\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[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.
\item[Added] means that this record was not in the base game and has been added by a~modder.
\item[Modified] means that the record is part of the base game, but has been changed in some way.
\item[Deleted] means that this record used to be part of the base game, but has been removed as an entry. This does not mean, however, that the occurrences
in the game itself have been removed! For example, if you remove the CharGen\_Bed entry from morrowind.esm, it does not mean the bedroll in the basement
of the Census and Excise Office in Seyda Neen is gone. You're going to have to delete that reference yourself or make sure that that object is replaced
by something that still exists otherwise you will get crashes in the worst case scenario.
\end{description}
\subsection{World Screens}
The contents of the game world can be changed by choosing one of the options in the appropriate menu at the top of the screen.
\subsubsection{Regions}
This describes the general areas of Vvardenfell. Each of these areas has different rules about things such as encounters and weather.
\begin{description}
\item[Name:] This is how the game will show your location in-game.
\item[Map Colour:] This is a six-digit hexidecimal representation of the colour used to identify the region on the map available in
World > Region Map. If you do not have an application with a colour picker, you can use your favourite search engine to find a colour picker online.
\item[Sleep Encounter:] These are the rules for what kind of enemies you might encounter when you sleep outside in the wild.
\end{description}
\subsubsection{Cells}
Expansive worlds such as Vvardenfell, with all its items, NPCs, etc. have a lot going on simultaneously. But if you are in Balmora,
why would the computer need to keep track the exact locations of NPCs walking through the corridors in a Vivec canton? All that work would
be quite useless and bring your system to its knees! So the world has been divided up into squares we call "cells". Once your character enters a cell,
the game will load everything that is going on in that cell so you can interact with it.
In the original \MW{} this could be seen when you were travelling and you would see a small loading bar at the bottom of the screen;
you had just entered a new cell and the game would have to load all the items and NPCs. The Cells screen in \OCS{} provides you with a list of cells
in the game, both the interior cells (houses, dungeons, mines, etc.) and the exterior cells (the outside world).
\begin{description}
\item[Sleep Forbidden:] Can the player sleep on the floor? In most cities it is forbidden to sleep outside. Sleeping in the wild carries its
own risks of attack, though, and this entry lets you decide if a player should be allowed to sleep on the floor in this cell or not.
\item[Interior Water:] Should water be rendered in this interior cell? The game world consists of an endless ocean at height 0. Then the landscape
is added. If part of the landscape goes below height 0, the player will see water. (See illustration.)
Setting the cell's Interior Water to true tells the game that this cell is both an interior cell (inside a building, for example, rather than
in the open air) but that there still needs to be water at height 0. This is useful for dungeons or mines that have water in them.
Setting the cell's Interior Water to false tells the game that the water at height 0 should not be used. Remember that cells that are in
the outside world are exterior cells and should thus \textit{always} be set to false!
\item[Interior Sky:] Should this interior cell have a sky? This is a rather unique case. The \TB{} expansion took place in a city on
the mainland. Normally this would require the city to be composed of exterior cells so it has a sky, weather and the like. But if the player is
in an exterior cell and looks at his in-game map, he sees Vvardenfell with an overview of all exterior cells. The player would have to see
the city's very own map, as if he was walking around in an interior cell.
So the developers decided to create a workaround and take a bit of both: The whole city would technically work exactly like an interior cell,
but it would need a sky as if it was an exterior cell. That is what this is. This is why the vast majority of the cells you will find in this screen
will have this option set to false: It is only meant for these "fake exteriors".
\item[Region:] To which Region does this cell belong? This has an impact on the way the game handles weather and encounters in this area.
It is also possible for a cell not to belong to any region.
\end{description}
\subsubsection{Referenceables}
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.
Let's go through all Record Types and discuss what you can tell \OCS{} about them.
\begin{description}
\item[Activator:] This is an item that, when activated, starts a script or even just shows a~tooltip.
\end{description}
\section{Tables}
\subsection{Introduction}
If you have launched \OCS{} already and played around with it for a bit, you have probably gotten the impression that it contains lots of tables.
You'd be spot on: \OCS{} is built around using tables. This does not mean it works just like Microsoft Excel or Libre Office Calc, though.
Due to the vast amounts of information involved with \MW, tables just made the most sense. You have to be able to spot information quickly
and be able to change them on the fly.
Let's browse through the various screens and see what all these tables show.
\subsection{Used Terms}
\subsubsection{Glossary}
\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 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] 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.
\item[Added] means that this record was not in the base game and has been added by a~modder.
\item[Modified] means that the record is part of the base game, but has been changed in some way.
\item[Deleted] means that this record used to be part of the base game, but has been removed as an entry. This does not mean, however, that the occurrences
in the game itself have been removed! For example, if you remove the CharGen\_Bed entry from morrowind.esm, it does not mean the bedroll in the basement
of the Census and Excise Office in Seyda Neen is gone. You're going to have to delete that reference yourself or make sure that that object is replaced
by something that still exists otherwise you will get crashes in the worst case scenario.
\end{description}
\subsection{World Screens}
The contents of the game world can be changed by choosing one of the options in the appropriate menu at the top of the screen.
\subsubsection{Regions}
This describes the general areas of Vvardenfell. Each of these areas has different rules about things such as encounters and weather.
\begin{description}
\item[Name:] This is how the game will show your location in-game.
\item[Map Colour:] This is a six-digit hexidecimal representation of the colour used to identify the region on the map available in
World > Region Map. If you do not have an application with a colour picker, you can use your favourite search engine to find a colour picker online.
\item[Sleep Encounter:] These are the rules for what kind of enemies you might encounter when you sleep outside in the wild.
\end{description}
\subsubsection{Cells}
Expansive worlds such as Vvardenfell, with all its items, NPCs, etc. have a lot going on simultaneously. But if you are in Balmora,
why would the computer need to keep track the exact locations of NPCs walking through the corridors in a Vivec canton? All that work would
be quite useless and bring your system to its knees! So the world has been divided up into squares we call "cells". Once your character enters a cell,
the game will load everything that is going on in that cell so you can interact with it.
In the original \MW{} this could be seen when you were travelling and you would see a small loading bar at the bottom of the screen;
you had just entered a new cell and the game would have to load all the items and NPCs. The Cells screen in \OCS{} provides you with a list of cells
in the game, both the interior cells (houses, dungeons, mines, etc.) and the exterior cells (the outside world).
\begin{description}
\item[Sleep Forbidden:] Can the player sleep on the floor? In most cities it is forbidden to sleep outside. Sleeping in the wild carries its
own risks of attack, though, and this entry lets you decide if a player should be allowed to sleep on the floor in this cell or not.
\item[Interior Water:] Should water be rendered in this interior cell? The game world consists of an endless ocean at height 0. Then the landscape
is added. If part of the landscape goes below height 0, the player will see water. (See illustration.)
Setting the cell's Interior Water to true tells the game that this cell is both an interior cell (inside a building, for example, rather than
in the open air) but that there still needs to be water at height 0. This is useful for dungeons or mines that have water in them.
Setting the cell's Interior Water to false tells the game that the water at height 0 should not be used. Remember that cells that are in
the outside world are exterior cells and should thus \textit{always} be set to false!
\item[Interior Sky:] Should this interior cell have a sky? This is a rather unique case. The \TB{} expansion took place in a city on
the mainland. Normally this would require the city to be composed of exterior cells so it has a sky, weather and the like. But if the player is
in an exterior cell and looks at his in-game map, he sees Vvardenfell with an overview of all exterior cells. The player would have to see
the city's very own map, as if he was walking around in an interior cell.
So the developers decided to create a workaround and take a bit of both: The whole city would technically work exactly like an interior cell,
but it would need a sky as if it was an exterior cell. That is what this is. This is why the vast majority of the cells you will find in this screen
will have this option set to false: It is only meant for these "fake exteriors".
\item[Region:] To which Region does this cell belong? This has an impact on the way the game handles weather and encounters in this area.
It is also possible for a cell not to belong to any region.
\end{description}
\subsubsection{Referenceables}
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.
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.
\end{description}

@ -1,54 +1,54 @@
\section{Windows}
\subsection{Introduction}
This section describes the multiple windows interface of the \OCS{} editor. This design principle was chosen in order
to extend the flexibility of the editor, especially on the multiple screens setups and on environments providing advanced
windows management features, like for instance: multiple desktops found commonly on many open source desktop environments.
However, it is enough to have a single large screen to see the advantages of this concept.
OpenCS windows interface is easy to describe and understand. In fact we decided to minimize use of many windows concepts
applied commonly in various applications. For instance dialog windows are really hard to find in the \OCS. You are free to try,
though.
Because of this, and the fact that we expect that user is familiar with other applications using windows this section is mostly
focused on practical ways of organizing work with the \OCS.
\subsection{Basics}
After starting \OCS{} and choosing content files to use a editor window should show up. It probably does not look surprising:
there is a menubar at the top, and there is a~large empty area. That is it: a brand new \OCS{} window contains only menubar
and statusbar. In order to make it a little bit more useful you probably want to enable some panels\footnote{Also known as widgets.}.
You are free to do so, just try to explore the menubar.
You probably founded out the way to enable and disable some interesting tables, but those will be described later. For now, let's
just focus on the windows itself.
\paragraph{Creating new windows}
is easy! Just visit view menu, and use the ``New View'' item. Suddenly, out of the blue a new window will show up. As you would expect,
it is also blank, and you are free to add any of the \OCS{} panels.
\paragraph{Closing opened window}
is also easy! Simply close that window decoration button. We suspect that you knew that already, but better to be sure.
Closing last \OCS{} window will also terminate application session.
\paragraph{Multi-everything}
is the main foundation of \OCS{} interface. You are free to create as many windows as you want to, free to populate it with
any panels you may want to, and move everything as you wish to -- even if it makes no sense at all. If you just got crazy idea and
you are wonder if you are able to have one hundred \OCS{} windows showing panels of the same type, well most likely you are
able to do so.
The principle behind this design decision is easy to see for \BS{} made editor, but maybe not so clear for users who are
just about to begin their wonderful journey of modding.
\subsection{Advanced}
So why? Why this is created in such manner. The answer is frankly simple: because it is effective. When creating a mod, you often
have to work only with just one table. For instance you are just balancing weapons damage and other statistics. It makes sense
to have all the space for just that one table. More often, you are required to work with two and switch them from time to time.
All major graphical environments commonly present in operating systems comes with switcher feature, that is a key shortcut to change
active window. It is very effective and fast when you have only two windows, each holding only one table. Sometimes you have to work
with two at the time, and with one from time to time. Here, you can have one window holding two tables, and second holding just one.
OpenCS is designed to simply make sense and do not slowdown users. It is as simple as possible (but not simpler), and uses one
flexible approach in all cases.
There is no point in digging deeper in the windows of \OCS. Let's explore panels, starting with tables.
\section{Windows}
\subsection{Introduction}
This section describes the multiple windows interface of the \OCS{} editor. This design principle was chosen in order
to extend the flexibility of the editor, especially on the multiple screens setups and on environments providing advanced
windows management features, like for instance: multiple desktops found commonly on many open source desktop environments.
However, it is enough to have a single large screen to see the advantages of this concept.
OpenCS windows interface is easy to describe and understand. In fact we decided to minimize use of many windows concepts
applied commonly in various applications. For instance dialog windows are really hard to find in the \OCS. You are free to try,
though.
Because of this, and the fact that we expect that user is familiar with other applications using windows this section is mostly
focused on practical ways of organizing work with the \OCS.
\subsection{Basics}
After starting \OCS{} and choosing content files to use a editor window should show up. It probably does not look surprising:
there is a menubar at the top, and there is a~large empty area. That is it: a brand new \OCS{} window contains only menubar
and statusbar. In order to make it a little bit more useful you probably want to enable some panels\footnote{Also known as widgets.}.
You are free to do so, just try to explore the menubar.
You probably founded out the way to enable and disable some interesting tables, but those will be described later. For now, let's
just focus on the windows itself.
\paragraph{Creating new windows}
is easy! Just visit view menu, and use the ``New View'' item. Suddenly, out of the blue a new window will show up. As you would expect,
it is also blank, and you are free to add any of the \OCS{} panels.
\paragraph{Closing opened window}
is also easy! Simply close that window decoration button. We suspect that you knew that already, but better to be sure.
Closing last \OCS{} window will also terminate application session.
\paragraph{Multi-everything}
is the main foundation of \OCS{} interface. You are free to create as many windows as you want to, free to populate it with
any panels you may want to, and move everything as you wish to -- even if it makes no sense at all. If you just got crazy idea and
you are wonder if you are able to have one hundred \OCS{} windows showing panels of the same type, well most likely you are
able to do so.
The principle behind this design decision is easy to see for \BS{} made editor, but maybe not so clear for users who are
just about to begin their wonderful journey of modding.
\subsection{Advanced}
So why? Why this is created in such manner. The answer is frankly simple: because it is effective. When creating a mod, you often
have to work only with just one table. For instance you are just balancing weapons damage and other statistics. It makes sense
to have all the space for just that one table. More often, you are required to work with two and switch them from time to time.
All major graphical environments commonly present in operating systems comes with switcher feature, that is a key shortcut to change
active window. It is very effective and fast when you have only two windows, each holding only one table. Sometimes you have to work
with two at the time, and with one from time to time. Here, you can have one window holding two tables, and second holding just one.
OpenCS is designed to simply make sense and do not slowdown users. It is as simple as possible (but not simpler), and uses one
flexible approach in all cases.
There is no point in digging deeper in the windows of \OCS. Let's explore panels, starting with tables.
%We should write some tips and tricks here.

@ -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