Merge branch 'master' into fatigue

pull/456/head
Bret Curtis 7 years ago committed by GitHub
commit 3e4dc31e39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -37,6 +37,7 @@ Programmers
Britt Mathis (galdor557)
Capostrophic
cc9cii
Cédric Mocquillon
Chris Boyce (slothlife)
Chris Robinson (KittyCat)
Cory F. Cohen (cfcohen)
@ -62,6 +63,7 @@ Programmers
Evgeniy Mineev (sandstranger)
Federico Guerra (FedeWar)
Fil Krynicki (filkry)
Florian Weber (Florianjw)
Gašper Sedej
gugus/gus
Hallfaer Tuilinn

@ -1,13 +1,32 @@
0.45.0
------
Bug #1990: Sunrise/sunset not set correct
Bug #2222: Fatigue's effect on selling price is backwards
Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped
Bug #2835: Player able to slowly move when overencumbered
Bug #3374: Touch spells not hitting kwama foragers
Bug #3591: Angled hit distance too low
Bug #3629: DB assassin attack never triggers creature spawning
Bug #3876: Landscape texture painting is misaligned
Bug #3897: Have Goodbye give all choices the effects of Goodbye
Bug #3993: Terrain texture blending map is not upscaled
Bug #3997: Almalexia doesn't pace
Bug #4036: Weird behaviour of AI packages if package target has non-unique ID
Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully
Bug #4215: OpenMW shows book text after last <BR> tag
Bug #4221: Characters get stuck in V-shaped terrain
Bug #4251: Stationary NPCs do not return to their position after combat
Bug #4293: Faction members are not aware of faction ownerships in barter
Bug #4327: Missing animations during spell/weapon stance switching
Bug #4368: Settings window ok button doesn't have key focus by default
Bug #4419: MRK NiStringExtraData is handled incorrectly
Bug #4426: RotateWorld behavior is incorrect
Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2
Bug #4432: Guards behaviour is incorrect if they do not have AI packages
Bug #4433: Guard behaviour is incorrect with Alarm = 0
Bug #4443: Goodbye option and dialogue choices are not mutually exclusive
Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts
Feature #4345: Add equivalents for the command line commands to Launcher
Feature #4444: Per-group KF-animation files support
0.44.0

@ -3,7 +3,7 @@ OpenMW
[![Build Status](https://api.travis-ci.org/OpenMW/openmw.svg)](https://travis-ci.org/OpenMW/openmw) [![Build status](https://ci.appveyor.com/api/projects/status/github/openmw/openmw?svg=true)](https://ci.appveyor.com/project/psi29a/openmw) [![Coverity Scan Build Status](https://scan.coverity.com/projects/3740/badge.svg)](https://scan.coverity.com/projects/3740)
OpenMW is a recreation of the engine for the popular role-playing game Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work.
OpenMW is a open-source game engine that supports playing Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work.
OpenMW also comes with OpenMW-CS, a replacement for Morrowind's TES Construction Set.

@ -8,6 +8,7 @@ set(LAUNCHER
settingspage.cpp
advancedpage.cpp
utils/cellnameloader.cpp
utils/profilescombobox.cpp
utils/textinputdialog.cpp
utils/lineedit.cpp
@ -24,6 +25,7 @@ set(LAUNCHER_HEADER
settingspage.hpp
advancedpage.hpp
utils/cellnameloader.hpp
utils/profilescombobox.hpp
utils/textinputdialog.hpp
utils/lineedit.hpp
@ -39,6 +41,7 @@ set(LAUNCHER_HEADER_MOC
settingspage.hpp
advancedpage.hpp
utils/cellnameloader.hpp
utils/textinputdialog.hpp
utils/profilescombobox.hpp
utils/lineedit.hpp

@ -1,10 +1,18 @@
#include "advancedpage.hpp"
#include <components/files/configurationmanager.hpp>
Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent)
#include <components/config/gamesettings.hpp>
#include <components/config/launchersettings.hpp>
#include <QFileDialog>
#include <QCompleter>
#include <components/contentselector/view/contentselector.hpp>
#include <components/contentselector/model/esmfile.hpp>
Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg,
Config::GameSettings &gameSettings,
Settings::Manager &engineSettings, QWidget *parent)
: QWidget(parent)
, mCfgMgr(cfg)
, mGameSettings(gameSettings)
, mEngineSettings(engineSettings)
{
setObjectName ("AdvancedPage");
@ -13,8 +21,54 @@ Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg, Settings:
loadSettings();
}
void Launcher::AdvancedPage::loadCellsForAutocomplete(QStringList cellNames) {
// Set up an auto-completer for the "Start default character at" field
auto *completer = new QCompleter(cellNames);
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
startDefaultCharacterAtField->setCompleter(completer);
}
void Launcher::AdvancedPage::on_skipMenuCheckBox_stateChanged(int state) {
startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked);
startDefaultCharacterAtField->setEnabled(state == Qt::Checked);
}
void Launcher::AdvancedPage::on_runScriptAfterStartupBrowseButton_clicked()
{
QString scriptFile = QFileDialog::getOpenFileName(
this,
QObject::tr("Select script file"),
QDir::currentPath(),
QString(tr("Text file (*.txt)")));
if (scriptFile.isEmpty())
return;
QFileInfo info(scriptFile);
if (!info.exists() || !info.isReadable())
return;
const QString path(QDir::toNativeSeparators(info.absoluteFilePath()));
}
bool Launcher::AdvancedPage::loadSettings()
{
// Testing
bool skipMenu = mGameSettings.value("skip-menu").toInt() == 1;
if (skipMenu) {
skipMenuCheckBox->setCheckState(Qt::Checked);
}
startDefaultCharacterAtLabel->setEnabled(skipMenu);
startDefaultCharacterAtField->setEnabled(skipMenu);
startDefaultCharacterAtField->setText(mGameSettings.value("start"));
runScriptAfterStartupField->setText(mGameSettings.value("script-run"));
// Game Settings
loadSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game");
loadSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game");
@ -54,6 +108,19 @@ void Launcher::AdvancedPage::saveSettings()
// Ensure we only set the new settings if they changed. This is to avoid cluttering the
// user settings file (which by definition should only contain settings the user has touched)
// Testing
int skipMenu = skipMenuCheckBox->checkState() == Qt::Checked;
if (skipMenu != mGameSettings.value("skip-menu").toInt())
mGameSettings.setValue("skip-menu", QString::number(skipMenu));
QString startCell = startDefaultCharacterAtField->text();
if (startCell != mGameSettings.value("start")) {
mGameSettings.setValue("start", startCell);
}
QString scriptRun = runScriptAfterStartupField->text();
if (scriptRun != mGameSettings.value("script-run"))
mGameSettings.setValue("script-run", scriptRun);
// Game Settings
saveSettingBool(canLootDuringDeathAnimationCheckBox, "can loot during death animation", "Game");
saveSettingBool(followersAttackOnSightCheckBox, "followers attack on sight", "Game");
@ -95,4 +162,9 @@ void Launcher::AdvancedPage::saveSettingBool(QCheckBox *checkbox, const std::str
bool cValue = checkbox->checkState();
if (cValue != mEngineSettings.getBool(setting, group))
mEngineSettings.setBool(setting, group, cValue);
}
void Launcher::AdvancedPage::slotLoadedCellsChanged(QStringList cellNames)
{
loadCellsForAutocomplete(cellNames);
}

@ -8,6 +8,7 @@
#include <components/settings/settings.hpp>
namespace Files { struct ConfigurationManager; }
namespace Config { class GameSettings; }
namespace Launcher
{
@ -16,15 +17,29 @@ namespace Launcher
Q_OBJECT
public:
AdvancedPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent = 0);
AdvancedPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings,
Settings::Manager &engineSettings, QWidget *parent = 0);
bool loadSettings();
void saveSettings();
public slots:
void slotLoadedCellsChanged(QStringList cellNames);
private slots:
void on_skipMenuCheckBox_stateChanged(int state);
void on_runScriptAfterStartupBrowseButton_clicked();
private:
Files::ConfigurationManager &mCfgMgr;
Config::GameSettings &mGameSettings;
Settings::Manager &mEngineSettings;
/**
* Load the cells associated with the given content files for use in autocomplete
* @param filePaths the file paths of the content files to be examined
*/
void loadCellsForAutocomplete(QStringList filePaths);
void loadSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group);
void saveSettingBool(QCheckBox *checkbox, const std::string& setting, const std::string& group);
};

@ -7,7 +7,10 @@
#include <QCheckBox>
#include <QMenu>
#include <QSortFilterProxyModel>
#include <thread>
#include <mutex>
#include <apps/launcher/utils/cellnameloader.hpp>
#include <components/files/configurationmanager.hpp>
#include <components/contentselector/model/esmfile.hpp>
@ -16,6 +19,7 @@
#include <components/config/gamesettings.hpp>
#include <components/config/launchersettings.hpp>
#include <iostream>
#include "utils/textinputdialog.hpp"
#include "utils/profilescombobox.hpp"
@ -40,6 +44,13 @@ Launcher::DataFilesPage::DataFilesPage(Files::ConfigurationManager &cfg, Config:
buildView();
loadSettings();
// Connect signal and slot after the settings have been loaded. We only care about the user changing
// the addons and don't want to get signals of the system doing it during startup.
connect(mSelector, SIGNAL(signalAddonDataChanged(QModelIndex,QModelIndex)),
this, SLOT(slotAddonDataChanged()));
// Call manually to indicate all changes to addon data during startup.
slotAddonDataChanged();
}
void Launcher::DataFilesPage::buildView()
@ -142,6 +153,17 @@ void Launcher::DataFilesPage::saveSettings(const QString &profile)
mGameSettings.setContentList(fileNames);
}
QStringList Launcher::DataFilesPage::selectedFilePaths()
{
//retrieve the files selected for the profile
ContentSelectorModel::ContentFileList items = mSelector->selectedFiles();
QStringList filePaths;
foreach(const ContentSelectorModel::EsmFile *item, items) {
filePaths.append(item->filePath());
}
return filePaths;
}
void Launcher::DataFilesPage::removeProfile(const QString &profile)
{
mLauncherSettings.removeContentList(profile);
@ -308,3 +330,31 @@ bool Launcher::DataFilesPage::showDeleteMessageBox (const QString &text)
return (msgBox.clickedButton() == deleteButton);
}
void Launcher::DataFilesPage::slotAddonDataChanged()
{
QStringList selectedFiles = selectedFilePaths();
if (previousSelectedFiles != selectedFiles) {
previousSelectedFiles = selectedFiles;
// Loading cells for core Morrowind + Expansions takes about 0.2 seconds, which is enough to cause a
// barely perceptible UI lag. Splitting into its own thread to alleviate that.
std::thread loadCellsThread(&DataFilesPage::reloadCells, this, selectedFiles);
loadCellsThread.detach();
}
}
// Mutex lock to run reloadCells synchronously.
std::mutex _reloadCellsMutex;
void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles)
{
// Use a mutex lock so that we can prevent two threads from executing the rest of this code at the same time
// Based on https://stackoverflow.com/a/5429695/531762
std::unique_lock<std::mutex> lock(_reloadCellsMutex);
// The following code will run only if there is not another thread currently running it
CellNameLoader cellNameLoader;
QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(selectedFiles));
std::sort(cellNamesList.begin(), cellNamesList.end());
emit signalLoadedCellsChanged(cellNamesList);
}

@ -7,6 +7,7 @@
#include <QDir>
#include <QFile>
#include <QStringList>
class QSortFilterProxyModel;
class QAbstractItemModel;
@ -41,8 +42,15 @@ namespace Launcher
void saveSettings(const QString &profile = "");
bool loadSettings();
/**
* Returns the file paths of all selected content files
* @return the file paths of all selected content files
*/
QStringList selectedFilePaths();
signals:
void signalProfileChanged (int index);
void signalLoadedCellsChanged(QStringList selectedFiles);
public slots:
void slotProfileChanged (int index);
@ -52,6 +60,7 @@ namespace Launcher
void slotProfileChangedByUser(const QString &previous, const QString &current);
void slotProfileRenamed(const QString &previous, const QString &current);
void slotProfileDeleted(const QString &item);
void slotAddonDataChanged ();
void updateOkButton(const QString &text);
@ -72,7 +81,7 @@ namespace Launcher
Config::LauncherSettings &mLauncherSettings;
QString mPreviousProfile;
QStringList previousSelectedFiles;
QString mDataLocal;
void setPluginsCheckstates(Qt::CheckState state);
@ -87,6 +96,7 @@ namespace Launcher
void addProfile (const QString &profile, bool setAsCurrent);
void checkForDefaultProfile();
void populateFileViews(const QString& contentModelName);
void reloadCells(QStringList selectedFiles);
class PathIterator
{

@ -119,7 +119,7 @@ void Launcher::MainDialog::createPages()
mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this);
mGraphicsPage = new GraphicsPage(mCfgMgr, mEngineSettings, this);
mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this);
mAdvancedPage = new AdvancedPage(mCfgMgr, mEngineSettings, this);
mAdvancedPage = new AdvancedPage(mCfgMgr, mGameSettings, mEngineSettings, this);
// Set the combobox of the play page to imitate the combobox on the datafilespage
mPlayPage->setProfilesModel(mDataFilesPage->profilesModel());
@ -139,6 +139,8 @@ void Launcher::MainDialog::createPages()
connect(mPlayPage, SIGNAL(signalProfileChanged(int)), mDataFilesPage, SLOT(slotProfileChanged(int)));
connect(mDataFilesPage, SIGNAL(signalProfileChanged(int)), mPlayPage, SLOT(setProfilesIndex(int)));
// Using Qt::QueuedConnection because signal is emitted in a subthread and slot is in the main thread
connect(mDataFilesPage, SIGNAL(signalLoadedCellsChanged(QStringList)), mAdvancedPage, SLOT(slotLoadedCellsChanged(QStringList)), Qt::QueuedConnection);
}

@ -0,0 +1,48 @@
#include "cellnameloader.hpp"
#include <components/esm/loadcell.hpp>
#include <components/contentselector/view/contentselector.hpp>
QSet<QString> CellNameLoader::getCellNames(QStringList &contentPaths)
{
QSet<QString> cellNames;
ESM::ESMReader esmReader;
// Loop through all content files
for (auto &contentPath : contentPaths) {
esmReader.open(contentPath.toStdString());
// Loop through all records
while(esmReader.hasMoreRecs())
{
ESM::NAME recordName = esmReader.getRecName();
esmReader.getRecHeader();
if (isCellRecord(recordName)) {
QString cellName = getCellName(esmReader);
if (!cellName.isEmpty()) {
cellNames.insert(cellName);
}
}
// Stop loading content for this record and continue to the next
esmReader.skipRecord();
}
}
return cellNames;
}
bool CellNameLoader::isCellRecord(ESM::NAME &recordName)
{
return recordName.intval == ESM::REC_CELL;
}
QString CellNameLoader::getCellName(ESM::ESMReader &esmReader)
{
ESM::Cell cell;
bool isDeleted = false;
cell.loadNameAndData(esmReader, isDeleted);
return QString::fromStdString(cell.mName);
}

@ -0,0 +1,41 @@
#ifndef OPENMW_CELLNAMELOADER_H
#define OPENMW_CELLNAMELOADER_H
#include <QComboBox>
#include <QSet>
#include <QString>
#include <components/esm/esmreader.hpp>
namespace ESM {class ESMReader; struct Cell;}
namespace ContentSelectorView {class ContentSelector;}
class CellNameLoader {
public:
/**
* Returns the names of all cells contained within the given content files
* @param contentPaths the file paths of each content file to be examined
* @return the names of all cells
*/
QSet<QString> getCellNames(QStringList &contentPaths);
private:
/**
* Returns whether or not the given record is of type "Cell"
* @param name The name associated with the record
* @return whether or not the given record is of type "Cell"
*/
bool isCellRecord(ESM::NAME &name);
/**
* Returns the name of the cell
* @param esmReader the reader currently pointed to a loaded cell
* @return the name of the cell
*/
QString getCellName(ESM::ESMReader &esmReader);
};
#endif //OPENMW_CELLNAMELOADER_H

@ -88,6 +88,7 @@ namespace CSMWorld
Display_UnsignedInteger8,
Display_Integer,
Display_Float,
Display_Double,
Display_Var,
Display_GmstVarType,
Display_GlobalVarType,

@ -1371,7 +1371,7 @@ namespace CSMWorld
RotColumn (ESM::Position ESXRecordT::* position, int index, bool door)
: Column<ESXRecordT> (
(door ? Columns::ColumnId_DoorPositionXRot : Columns::ColumnId_PositionXRot)+index,
ColumnBase::Display_Float), mPosition (position), mIndex (index) {}
ColumnBase::Display_Double), mPosition (position), mIndex (index) {}
virtual QVariant get (const Record<ESXRecordT>& record) const
{

@ -184,11 +184,11 @@ CSMWorld::RefIdCollection::RefIdCollection()
mColumns.back().addColumn(
new RefIdColumn (Columns::ColumnId_PosZ, CSMWorld::ColumnBase::Display_Float));
mColumns.back().addColumn(
new RefIdColumn (Columns::ColumnId_RotX, CSMWorld::ColumnBase::Display_Float));
new RefIdColumn (Columns::ColumnId_RotX, CSMWorld::ColumnBase::Display_Double));
mColumns.back().addColumn(
new RefIdColumn (Columns::ColumnId_RotY, CSMWorld::ColumnBase::Display_Float));
new RefIdColumn (Columns::ColumnId_RotY, CSMWorld::ColumnBase::Display_Double));
mColumns.back().addColumn(
new RefIdColumn (Columns::ColumnId_RotZ, CSMWorld::ColumnBase::Display_Float));
new RefIdColumn (Columns::ColumnId_RotZ, CSMWorld::ColumnBase::Display_Double));
// Nested table
mColumns.push_back(RefIdColumn (Columns::ColumnId_AiPackageList,

@ -233,6 +233,15 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO
return dsb;
}
case CSMWorld::ColumnBase::Display_Double:
{
DialogueDoubleSpinBox *dsb = new DialogueDoubleSpinBox(parent);
dsb->setRange(-FLT_MAX, FLT_MAX);
dsb->setSingleStep(0.01f);
dsb->setDecimals(6);
return dsb;
}
case CSMWorld::ColumnBase::Display_LongString:
{
QPlainTextEdit *edit = new QPlainTextEdit(parent);

@ -431,11 +431,8 @@ namespace MWDialogue
void DialogueManager::addChoice (const std::string& text, int choice)
{
if (!mGoodbye)
{
mIsInChoice = true;
mChoices.push_back(std::make_pair(text, choice));
}
mIsInChoice = true;
mChoices.push_back(std::make_pair(text, choice));
}
const std::vector<std::pair<std::string, int> >& DialogueManager::getChoices()
@ -450,8 +447,8 @@ namespace MWDialogue
void DialogueManager::goodbye()
{
if (!mIsInChoice)
mGoodbye = true;
mIsInChoice = false;
mGoodbye = true;
}
void DialogueManager::persuade(int type, ResponseCallback* callback)

@ -635,6 +635,11 @@ namespace MWGui
void DialogueWindow::onChoiceActivated(int id)
{
if (mGoodbye)
{
onGoodbyeActivated();
return;
}
MWBase::Environment::get().getDialogueManager()->questionAnswered(id, mCallback.get());
updateTopics();
}

@ -31,6 +31,14 @@ namespace MWGui
boost::algorithm::replace_all(mText, "\r", "");
// vanilla game does not show any text after last <BR> tag.
const std::string lowerText = Misc::StringUtils::lowerCase(mText);
int index = lowerText.rfind("<br>");
if (index == -1)
mText = "";
else
mText = mText.substr(0, index+4);
registerTag("br", Event_BrTag);
registerTag("p", Event_PTag);
registerTag("img", Event_ImgTag);

@ -562,8 +562,9 @@ namespace MWGui
void SettingsWindow::onOpen()
{
updateControlsBox ();
updateControlsBox();
resetScrollbars();
MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton);
}
void SettingsWindow::onWindowResize(MyGUI::Window *_sender)

@ -310,8 +310,10 @@ namespace MWGui
void WaitDialog::wakeUp ()
{
mSleeping = false;
mTimeAdvancer.stop();
stopWaiting();
if (mInterruptAt != -1)
onWaitingInterrupted();
else
stopWaiting();
}
}

@ -50,27 +50,36 @@ bool isConscious(const MWWorld::Ptr& ptr)
return !stats.isDead() && !stats.getKnockedDown();
}
void adjustBoundItem (const std::string& item, bool bound, const MWWorld::Ptr& actor)
int getBoundItemSlot (const std::string& itemId)
{
if (bound)
static std::map<std::string, int> boundItemsMap;
if (boundItemsMap.empty())
{
if (actor.getClass().getContainerStore(actor).count(item) == 0)
{
MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor);
MWWorld::Ptr newPtr = *store.MWWorld::ContainerStore::add(item, 1, actor);
MWWorld::ActionEquip action(newPtr);
action.execute(actor);
MWWorld::ConstContainerStoreIterator rightHand = store.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
// change draw state only if the item is in player's right hand
if (actor == MWMechanics::getPlayer()
&& rightHand != store.end() && newPtr == *rightHand)
{
MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState_Weapon);
}
}
std::string boundId = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("sMagicBoundBootsID")->getString();
boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Boots;
boundId = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("sMagicBoundCuirassID")->getString();
boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Cuirass;
boundId = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("sMagicBoundLeftGauntletID")->getString();
boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_LeftGauntlet;
boundId = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("sMagicBoundRightGauntletID")->getString();
boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_RightGauntlet;
boundId = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("sMagicBoundHelmID")->getString();
boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_Helmet;
boundId = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("sMagicBoundShieldID")->getString();
boundItemsMap[boundId] = MWWorld::InventoryStore::Slot_CarriedLeft;
}
else
actor.getClass().getInventoryStore(actor).remove(item, 1, actor, true);
int slot = MWWorld::InventoryStore::Slot_CarriedRight;
std::map<std::string, int>::iterator it = boundItemsMap.find(itemId);
if (it != boundItemsMap.end())
slot = it->second;
return slot;
}
class CheckActorCommanded : public MWMechanics::EffectSourceVisitor
@ -139,7 +148,6 @@ void getRestorationPerHourOfSleep (const MWWorld::Ptr& ptr, float& health, float
namespace MWMechanics
{
const float aiProcessingDistance = 7168;
const float sqrAiProcessingDistance = aiProcessingDistance*aiProcessingDistance;
@ -227,6 +235,83 @@ namespace MWMechanics
}
};
void Actors::addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor)
{
MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor);
int slot = getBoundItemSlot(itemId);
if (actor.getClass().getContainerStore(actor).count(itemId) != 0)
return;
MWWorld::ContainerStoreIterator prevItem = store.getSlot(slot);
MWWorld::Ptr boundPtr = *store.MWWorld::ContainerStore::add(itemId, 1, actor);
MWWorld::ActionEquip action(boundPtr);
action.execute(actor);
if (actor != MWMechanics::getPlayer())
return;
MWWorld::Ptr newItem = *store.getSlot(slot);
if (newItem.isEmpty() || boundPtr != newItem)
return;
MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer();
// change draw state only if the item is in player's right hand
if (slot == MWWorld::InventoryStore::Slot_CarriedRight)
player.setDrawState(MWMechanics::DrawState_Weapon);
if (prevItem != store.end())
player.setPreviousItem(itemId, prevItem->getCellRef().getRefId());
}
void Actors::removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor)
{
MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor);
int slot = getBoundItemSlot(itemId);
MWWorld::ContainerStoreIterator currentItem = store.getSlot(slot);
bool wasEquipped = currentItem != store.end() && Misc::StringUtils::ciEqual(currentItem->getCellRef().getRefId(), itemId);
store.remove(itemId, 1, actor, true);
if (actor != MWMechanics::getPlayer())
return;
MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer();
std::string prevItemId = player.getPreviousItem(itemId);
player.erasePreviousItem(itemId);
if (prevItemId.empty())
return;
// Find the item by id
MWWorld::Ptr item;
for (MWWorld::ContainerStoreIterator iter = store.begin(); iter != store.end(); ++iter)
{
if (Misc::StringUtils::ciEqual(iter->getCellRef().getRefId(), prevItemId))
{
if (item.isEmpty() ||
// Prefer the stack with the lowest remaining uses
!item.getClass().hasItemHealth(*iter) ||
iter->getClass().getItemHealth(*iter) < item.getClass().getItemHealth(item))
{
item = *iter;
}
}
}
// we should equip previous item only if expired bound item was equipped.
if (item.isEmpty() || !wasEquipped)
return;
MWWorld::ActionEquip action(item);
action.execute(actor);
}
void Actors::updateActor (const MWWorld::Ptr& ptr, float duration)
{
// magic effects
@ -756,25 +841,23 @@ namespace MWMechanics
float magnitude = effects.get(it->first).getMagnitude();
if (found != (magnitude > 0))
{
if (magnitude > 0)
creatureStats.mBoundItems.insert(it->first);
else
creatureStats.mBoundItems.erase(it->first);
std::string itemGmst = it->second;
std::string item = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
itemGmst)->getString();
magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr);
if (it->first == ESM::MagicEffect::BoundGloves)
{
item = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
"sMagicBoundLeftGauntletID")->getString();
adjustBoundItem(item, magnitude > 0, ptr);
item = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
"sMagicBoundRightGauntletID")->getString();
adjustBoundItem(item, magnitude > 0, ptr);
magnitude > 0 ? addBoundItem(item, ptr) : removeBoundItem(item, ptr);
}
else
adjustBoundItem(item, magnitude > 0, ptr);
if (magnitude > 0)
creatureStats.mBoundItems.insert(it->first);
else
creatureStats.mBoundItems.erase(it->first);
}
}

@ -4,7 +4,6 @@
#include <set>
#include <vector>
#include <string>
#include <map>
#include <list>
#include "../mwbase/world.hpp"
@ -26,6 +25,9 @@ namespace MWMechanics
{
std::map<std::string, int> mDeathCount;
void addBoundItem (const std::string& itemId, const MWWorld::Ptr& actor);
void removeBoundItem (const std::string& itemId, const MWWorld::Ptr& actor);
void updateNpc(const MWWorld::Ptr &ptr, float duration);
void adjustMagicEffects (const MWWorld::Ptr& creature);

@ -103,9 +103,10 @@ namespace MWMechanics
bool isFleeing();
};
AiCombat::AiCombat(const MWWorld::Ptr& actor) :
mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId())
{}
AiCombat::AiCombat(const MWWorld::Ptr& actor)
{
mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId();
}
AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat)
{

@ -54,9 +54,6 @@ namespace MWMechanics
virtual bool shouldCancelPreviousAi() const { return false; }
private:
int mTargetActorId;
/// Returns true if combat should end
bool attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController);

@ -22,28 +22,32 @@
namespace MWMechanics
{
AiEscort::AiEscort(const std::string &actorId, int duration, float x, float y, float z)
: mActorId(actorId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast<float>(duration))
: mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast<float>(duration))
, mCellX(std::numeric_limits<int>::max())
, mCellY(std::numeric_limits<int>::max())
{
mTargetActorRefId = actorId;
mMaxDist = 450;
}
AiEscort::AiEscort(const std::string &actorId, const std::string &cellId,int duration, float x, float y, float z)
: mActorId(actorId), mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast<float>(duration))
AiEscort::AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z)
: mCellId(cellId), mX(x), mY(y), mZ(z), mDuration(duration), mRemainingDuration(static_cast<float>(duration))
, mCellX(std::numeric_limits<int>::max())
, mCellY(std::numeric_limits<int>::max())
{
mTargetActorRefId = actorId;
mMaxDist = 450;
}
AiEscort::AiEscort(const ESM::AiSequence::AiEscort *escort)
: mActorId(escort->mTargetId), mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ)
: mCellId(escort->mCellId), mX(escort->mData.mX), mY(escort->mData.mY), mZ(escort->mData.mZ)
, mMaxDist(450)
, mRemainingDuration(escort->mRemainingDuration)
, mCellX(std::numeric_limits<int>::max())
, mCellY(std::numeric_limits<int>::max())
{
mTargetActorRefId = escort->mTargetId;
mTargetActorId = escort->mTargetActorId;
// mDuration isn't saved in the save file, so just giving it "1" for now if the package has a duration.
// The exact value of mDuration only matters for repeating packages.
if (mRemainingDuration > 0) // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves.
@ -78,7 +82,7 @@ namespace MWMechanics
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing);
actor.getClass().getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, false);
const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mActorId, false);
const MWWorld::Ptr follower = MWBase::Environment::get().getWorld()->getPtr(mTargetActorRefId, false);
const float* const leaderPos = actor.getRefData().getPosition().pos;
const float* const followerPos = follower.getRefData().getPosition().pos;
double differenceBetween[3];
@ -119,18 +123,14 @@ namespace MWMechanics
return TypeIdEscort;
}
MWWorld::Ptr AiEscort::getTarget() const
{
return MWBase::Environment::get().getWorld()->getPtr(mActorId, false);
}
void AiEscort::writeState(ESM::AiSequence::AiSequence &sequence) const
{
std::unique_ptr<ESM::AiSequence::AiEscort> escort(new ESM::AiSequence::AiEscort());
escort->mData.mX = mX;
escort->mData.mY = mY;
escort->mData.mZ = mZ;
escort->mTargetId = mActorId;
escort->mTargetId = mTargetActorRefId;
escort->mTargetActorId = mTargetActorId;
escort->mRemainingDuration = mRemainingDuration;
escort->mCellId = mCellId;

@ -24,11 +24,11 @@ namespace MWMechanics
/// Implementation of AiEscort
/** The Actor will escort the specified actor to the world position x, y, z until they reach their position, or they run out of time
\implement AiEscort **/
AiEscort(const std::string &actorId,int duration, float x, float y, float z);
AiEscort(const std::string &actorId, int duration, float x, float y, float z);
/// Implementation of AiEscortCell
/** The Actor will escort the specified actor to the cell position x, y, z until they reach their position, or they run out of time
\implement AiEscortCell **/
AiEscort(const std::string &actorId,const std::string &cellId,int duration, float x, float y, float z);
AiEscort(const std::string &actorId, const std::string &cellId, int duration, float x, float y, float z);
AiEscort(const ESM::AiSequence::AiEscort* escort);
@ -38,7 +38,6 @@ namespace MWMechanics
virtual int getTypeId() const;
MWWorld::Ptr getTarget() const;
virtual bool sideWithTarget() const { return true; }
void writeState(ESM::AiSequence::AiSequence &sequence) const;
@ -46,7 +45,6 @@ namespace MWMechanics
void fastForward(const MWWorld::Ptr& actor, AiState& state);
private:
std::string mActorId;
std::string mCellId;
float mX;
float mY;

@ -35,32 +35,53 @@ struct AiFollowStorage : AiTemporaryBase
int AiFollow::mFollowIndexCounter = 0;
AiFollow::AiFollow(const std::string &actorId,float duration, float x, float y, float z)
AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z)
: mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
, mActorRefId(actorId), mActorId(-1), mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++)
, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++)
{
mTargetActorRefId = actorId;
}
AiFollow::AiFollow(const std::string &actorId,const std::string &cellId,float duration, float x, float y, float z)
AiFollow::AiFollow(const std::string &actorId, const std::string &cellId, float duration, float x, float y, float z)
: mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
, mActorRefId(actorId), mActorId(-1), mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++)
, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++)
{
mTargetActorRefId = actorId;
}
AiFollow::AiFollow(const std::string &actorId, bool commanded)
AiFollow::AiFollow(const MWWorld::Ptr& actor, float duration, float x, float y, float z)
: mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++)
{
mTargetActorRefId = actor.getCellRef().getRefId();
mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId();
}
AiFollow::AiFollow(const MWWorld::Ptr& actor, const std::string &cellId, float duration, float x, float y, float z)
: mAlwaysFollow(false), mCommanded(false), mDuration(duration), mRemainingDuration(duration), mX(x), mY(y), mZ(z)
, mCellId(cellId), mActive(false), mFollowIndex(mFollowIndexCounter++)
{
mTargetActorRefId = actor.getCellRef().getRefId();
mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId();
}
AiFollow::AiFollow(const MWWorld::Ptr& actor, bool commanded)
: mAlwaysFollow(true), mCommanded(commanded), mDuration(0), mRemainingDuration(0), mX(0), mY(0), mZ(0)
, mActorRefId(actorId), mActorId(-1), mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++)
, mCellId(""), mActive(false), mFollowIndex(mFollowIndexCounter++)
{
mTargetActorRefId = actor.getCellRef().getRefId();
mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId();
}
AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow)
: mAlwaysFollow(follow->mAlwaysFollow), mCommanded(follow->mCommanded), mRemainingDuration(follow->mRemainingDuration)
, mX(follow->mData.mX), mY(follow->mData.mY), mZ(follow->mData.mZ)
, mActorRefId(follow->mTargetId), mActorId(-1)
, mCellId(follow->mCellId), mActive(follow->mActive), mFollowIndex(mFollowIndexCounter++)
{
// mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration.
// The exact value of mDuration only matters for repeating packages.
mTargetActorRefId = follow->mTargetId;
mTargetActorId = follow->mTargetActorId;
// mDuration isn't saved in the save file, so just giving it "1" for now if the package had a duration.
// The exact value of mDuration only matters for repeating packages.
if (mRemainingDuration > 0) // Previously mRemainingDuration could be negative even when mDuration was 0. Checking for > 0 should fix old saves.
mDuration = 1;
else
@ -204,7 +225,7 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
std::string AiFollow::getFollowedActor()
{
return mActorRefId;
return mTargetActorRefId;
}
AiFollow *MWMechanics::AiFollow::clone() const
@ -228,7 +249,8 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const
follow->mData.mX = mX;
follow->mData.mY = mY;
follow->mData.mZ = mZ;
follow->mTargetId = mActorRefId;
follow->mTargetId = mTargetActorRefId;
follow->mTargetActorId = mTargetActorId;
follow->mRemainingDuration = mRemainingDuration;
follow->mCellId = mCellId;
follow->mAlwaysFollow = mAlwaysFollow;
@ -241,29 +263,6 @@ void AiFollow::writeState(ESM::AiSequence::AiSequence &sequence) const
sequence.mPackages.push_back(package);
}
MWWorld::Ptr AiFollow::getTarget() const
{
if (mActorId == -2)
return MWWorld::Ptr();
if (mActorId == -1)
{
MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mActorRefId, false);
if (target.isEmpty())
{
mActorId = -2;
return target;
}
else
mActorId = target.getClass().getCreatureStats(target).getActorId();
}
if (mActorId != -1)
return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId);
else
return MWWorld::Ptr();
}
int AiFollow::getFollowIndex() const
{
return mFollowIndex;

@ -25,16 +25,17 @@ namespace MWMechanics
class AiFollow : public AiPackage
{
public:
AiFollow(const std::string &actorId, float duration, float x, float y, float z);
AiFollow(const std::string &actorId, const std::string &CellId, float duration, float x, float y, float z);
/// Follow Actor for duration or until you arrive at a world position
AiFollow(const std::string &ActorId,float duration, float X, float Y, float Z);
AiFollow(const MWWorld::Ptr& actor, float duration, float X, float Y, float Z);
/// Follow Actor for duration or until you arrive at a position in a cell
AiFollow(const std::string &ActorId,const std::string &CellId,float duration, float X, float Y, float Z);
AiFollow(const MWWorld::Ptr& actor, const std::string &CellId, float duration, float X, float Y, float Z);
/// Follow Actor indefinitively
AiFollow(const std::string &ActorId, bool commanded=false);
AiFollow(const MWWorld::Ptr& actor, bool commanded=false);
AiFollow(const ESM::AiSequence::AiFollow* follow);
MWWorld::Ptr getTarget() const;
virtual bool sideWithTarget() const { return true; }
virtual bool followTargetThroughDoors() const { return true; }
virtual bool shouldCancelPreviousAi() const { return !mCommanded; }
@ -66,8 +67,6 @@ namespace MWMechanics
float mX;
float mY;
float mZ;
std::string mActorRefId;
mutable int mActorId;
std::string mCellId;
bool mActive; // have we spotted the target?
int mFollowIndex;

@ -27,15 +27,36 @@ MWMechanics::AiPackage::~AiPackage() {}
MWMechanics::AiPackage::AiPackage() :
mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild
mTargetActorRefId(""),
mTargetActorId(-1),
mRotateOnTheRunChecks(0),
mIsShortcutting(false),
mShortcutProhibited(false), mShortcutFailPos()
mShortcutProhibited(false),
mShortcutFailPos()
{
}
MWWorld::Ptr MWMechanics::AiPackage::getTarget() const
{
return MWWorld::Ptr();
if (mTargetActorId == -2)
return MWWorld::Ptr();
if (mTargetActorId == -1)
{
MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false);
if (target.isEmpty())
{
mTargetActorId = -2;
return target;
}
else
mTargetActorId = target.getClass().getCreatureStats(target).getActorId();
}
if (mTargetActorId != -1)
return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId);
else
return MWWorld::Ptr();
}
bool MWMechanics::AiPackage::sideWithTarget() const

@ -48,7 +48,8 @@ namespace MWMechanics
TypeIdPursue = 6,
TypeIdAvoidDoor = 7,
TypeIdFace = 8,
TypeIdBreathe = 9
TypeIdBreathe = 9,
TypeIdInternalTravel = 10
};
///Default constructor
@ -79,6 +80,9 @@ namespace MWMechanics
/// Get the target actor the AI is targeted at (not applicable to all AI packages, default return empty Ptr)
virtual MWWorld::Ptr getTarget() const;
/// Get the destination point of the AI package (not applicable to all AI packages, default return (0, 0, 0))
virtual osg::Vec3f getDestination(const MWWorld::Ptr& actor) const { return osg::Vec3f(0, 0, 0); };
/// Return true if having this AiPackage makes the actor side with the target in fights (default false)
virtual bool sideWithTarget() const;
@ -128,6 +132,9 @@ namespace MWMechanics
float mTimer;
std::string mTargetActorRefId;
mutable int mTargetActorId;
osg::Vec3f mLastActorPos;
short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility

@ -15,13 +15,13 @@ namespace MWMechanics
{
AiPursue::AiPursue(const MWWorld::Ptr& actor)
: mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId())
{
mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId();
}
AiPursue::AiPursue(const ESM::AiSequence::AiPursue *pursue)
: mTargetActorId(pursue->mTargetActorId)
{
mTargetActorId = pursue->mTargetActorId;
}
AiPursue *MWMechanics::AiPursue::clone() const

@ -40,10 +40,6 @@ namespace MWMechanics
virtual bool canCancel() const { return false; }
virtual bool shouldCancelPreviousAi() const { return false; }
private:
int mTargetActorId; // The actor to pursue
};
}
#endif

@ -184,7 +184,8 @@ bool isActualAiPackage(int packageTypeId)
&& packageTypeId != AiPackage::TypeIdPursue
&& packageTypeId != AiPackage::TypeIdAvoidDoor
&& packageTypeId != AiPackage::TypeIdFace
&& packageTypeId != AiPackage::TypeIdBreathe);
&& packageTypeId != AiPackage::TypeIdBreathe
&& packageTypeId != AiPackage::TypeIdInternalTravel);
}
void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration)
@ -298,7 +299,7 @@ void AiSequence::clear()
mPackages.clear();
}
void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor)
void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther)
{
if (actor == getPlayer())
throw std::runtime_error("Can't add AI packages to player");
@ -307,8 +308,33 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor)
if (isActualAiPackage(package.getTypeId()))
stopCombat();
// We should return a wandering actor back after combat or pursuit.
// The same thing for actors without AI packages.
// Also there is no point to stack return packages.
int currentTypeId = getTypeId();
int newTypeId = package.getTypeId();
if (currentTypeId <= MWMechanics::AiPackage::TypeIdWander
&& !hasPackage(MWMechanics::AiPackage::TypeIdInternalTravel)
&& (newTypeId <= MWMechanics::AiPackage::TypeIdCombat
|| newTypeId == MWMechanics::AiPackage::TypeIdPursue))
{
osg::Vec3f dest;
if (currentTypeId == MWMechanics::AiPackage::TypeIdWander)
{
AiPackage* activePackage = getActivePackage();
dest = activePackage->getDestination(actor);
}
else
{
dest = actor.getRefData().getPosition().asVec3();
}
MWMechanics::AiTravel travelPackage(dest.x(), dest.y(), dest.z(), true);
stack(travelPackage, actor, false);
}
// remove previous packages if required
if (package.shouldCancelPreviousAi())
if (cancelOther && package.shouldCancelPreviousAi())
{
for(std::list<AiPackage *>::iterator it = mPackages.begin(); it != mPackages.end();)
{
@ -392,6 +418,8 @@ void AiSequence::writeState(ESM::AiSequence::AiSequence &sequence) const
{
(*iter)->writeState(sequence);
}
sequence.mLastAiPackage = mLastAiPackage;
}
void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence)
@ -403,7 +431,7 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence)
int count = 0;
for (std::vector<ESM::AiSequence::AiPackageContainer>::const_iterator it = sequence.mPackages.begin();
it != sequence.mPackages.end(); ++it)
{
{
if (isActualAiPackage(it->mType))
count++;
}
@ -462,6 +490,8 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence)
mPackages.push_back(package.release());
}
mLastAiPackage = sequence.mLastAiPackage;
}
void AiSequence::fastForward(const MWWorld::Ptr& actor, AiState& state)

@ -115,7 +115,7 @@ namespace MWMechanics
///< Add \a package to the front of the sequence
/** Suspends current package
@param actor The actor that owns this AiSequence **/
void stack (const AiPackage& package, const MWWorld::Ptr& actor);
void stack (const AiPackage& package, const MWWorld::Ptr& actor, bool cancelOther=true);
/// Return the current active package.
/** If there is no active package, it will throw an exception **/

@ -28,15 +28,14 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2)
namespace MWMechanics
{
AiTravel::AiTravel(float x, float y, float z)
: mX(x),mY(y),mZ(z)
AiTravel::AiTravel(float x, float y, float z, bool hidden)
: mX(x),mY(y),mZ(z),mHidden(hidden)
{
}
AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel)
: mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ)
: mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ), mHidden(travel->mHidden)
{
}
AiTravel *MWMechanics::AiTravel::clone() const
@ -64,7 +63,7 @@ namespace MWMechanics
int AiTravel::getTypeId() const
{
return TypeIdTravel;
return mHidden ? TypeIdInternalTravel : TypeIdTravel;
}
void AiTravel::fastForward(const MWWorld::Ptr& actor, AiState& state)
@ -83,6 +82,7 @@ namespace MWMechanics
travel->mData.mX = mX;
travel->mData.mY = mY;
travel->mData.mZ = mZ;
travel->mHidden = mHidden;
ESM::AiSequence::AiPackageContainer package;
package.mType = ESM::AiSequence::Ai_Travel;

@ -20,7 +20,7 @@ namespace MWMechanics
{
public:
/// Default constructor
AiTravel(float x, float y, float z);
AiTravel(float x, float y, float z, bool hidden = false);
AiTravel(const ESM::AiSequence::AiTravel* travel);
/// Simulates the passing of time
@ -38,6 +38,8 @@ namespace MWMechanics
float mX;
float mY;
float mZ;
bool mHidden;
};
}

@ -59,7 +59,7 @@ namespace MWMechanics
float mTargetAngleRadians;
bool mTurnActorGivingGreetingToFacePlayer;
float mReaction; // update some actions infrequently
AiWander::GreetingState mSaidGreeting;
int mGreetingTimer;
@ -70,7 +70,7 @@ namespace MWMechanics
bool mIsWanderingManually;
bool mCanWanderAlongPathGrid;
unsigned short mIdleAnimation;
std::vector<unsigned short> mBadIdles; // Idle animations that when called cause errors
@ -86,7 +86,7 @@ namespace MWMechanics
float mDoorCheckDuration;
int mStuckCount;
AiWanderStorage():
mTargetAngleRadians(0),
mTurnActorGivingGreetingToFacePlayer(false),
@ -111,7 +111,7 @@ namespace MWMechanics
mIsWanderingManually = isManualWander;
}
};
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat):
mDistance(distance), mDuration(duration), mRemainingDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle),
mRepeat(repeat), mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)), mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0))
@ -223,7 +223,7 @@ namespace MWMechanics
if (mPathFinder.isPathConstructed())
storage.setState(Wander_Walking);
}
doPerFrameActionsForState(actor, duration, storage, pos);
playIdleDialogueRandomly(actor);
@ -298,13 +298,6 @@ namespace MWMechanics
if(mDistance && cellChange)
mDistance = 0;
// For stationary NPCs, move back to the starting location if another AiPackage moved us elsewhere
if (mDistance == 0 && !cellChange
&& (pos.asVec3() - mInitialActorPosition).length2() > (DESTINATION_TOLERANCE * DESTINATION_TOLERANCE))
{
returnToStartLocation(actor, storage, pos);
}
// Allow interrupting a walking actor to trigger a greeting
WanderState& wanderState = storage.mState;
if ((wanderState == Wander_IdleNow) || (wanderState == Wander_Walking))
@ -321,7 +314,7 @@ namespace MWMechanics
{
setPathToAnAllowedNode(actor, storage, pos);
}
}
}
} else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) {
completeManualWalking(actor, storage);
}
@ -330,10 +323,19 @@ namespace MWMechanics
}
bool AiWander::getRepeat() const
{
return mRepeat;
{
return mRepeat;
}
osg::Vec3f AiWander::getDestination(const MWWorld::Ptr& actor) const
{
if (mHasDestination)
return mDestination;
const ESM::Pathgrid::Point currentPosition = actor.getRefData().getPosition().pos;
const osg::Vec3f currentPositionVec3f = osg::Vec3f(currentPosition.mX, currentPosition.mY, currentPosition.mZ);
return currentPositionVec3f;
}
bool AiWander::isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage)
{
@ -342,35 +344,14 @@ namespace MWMechanics
// End package if duration is complete
if (mRemainingDuration <= 0)
{
stopWalking(actor, storage);
return true;
stopWalking(actor, storage);
return true;
}
}
// if get here, not yet completed
return false;
}
void AiWander::returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos)
{
if (!mPathFinder.isPathConstructed())
{
mDestination = mInitialActorPosition;
ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mDestination));
// actor position is already in world coordinates
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos));
// don't take shortcuts for wandering
mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell()));
if (mPathFinder.isPathConstructed())
{
storage.setState(Wander_Walking);
mHasDestination = true;
}
}
}
/*
* Commands actor to walk to a random location near original spawn location.
*/
@ -497,7 +478,7 @@ namespace MWMechanics
}
}
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor,
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor,
float duration, AiWanderStorage& storage, ESM::Position& pos)
{
// Is there no destination or are we there yet?
@ -873,7 +854,7 @@ namespace MWMechanics
state.moveIn(new AiWanderStorage());
MWBase::Environment::get().getWorld()->moveObject(actor, static_cast<float>(dest.mX),
MWBase::Environment::get().getWorld()->moveObject(actor, static_cast<float>(dest.mX),
static_cast<float>(dest.mY), static_cast<float>(dest.mZ));
actor.getClass().adjustPosition(actor, false);
}
@ -914,7 +895,7 @@ namespace MWMechanics
// get NPC's position in local (i.e. cell) coordinates
osg::Vec3f npcPos(mInitialActorPosition);
CoordinateConverter(cell).toLocal(npcPos);
// Find closest pathgrid point
int closestPointIndex = PathFinder::GetClosestPoint(pathgrid, npcPos);
@ -945,7 +926,7 @@ namespace MWMechanics
storage.mPopulateAvailableNodes = false;
}
// When only one path grid point in wander distance,
// When only one path grid point in wander distance,
// additional points for NPC to wander to are:
// 1. NPC's initial location
// 2. Partway along the path between the point and its connected points.
@ -969,7 +950,7 @@ namespace MWMechanics
delta.normalize();
int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE);
// must not travel longer than distance between waypoints or NPC goes past waypoint
distance = std::min(distance, static_cast<int>(length));
delta *= distance;
@ -1041,4 +1022,3 @@ namespace MWMechanics
init();
}
}

@ -47,9 +47,11 @@ namespace MWMechanics
virtual void writeState(ESM::AiSequence::AiSequence &sequence) const;
virtual void fastForward(const MWWorld::Ptr& actor, AiState& state);
bool getRepeat() const;
osg::Vec3f getDestination(const MWWorld::Ptr& actor) const;
enum GreetingState {
Greet_None,
Greet_InProgress,
@ -85,7 +87,6 @@ namespace MWMechanics
bool reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage,
const MWWorld::CellStore*& currentCell, bool cellChange, ESM::Position& pos, float duration);
bool isPackageCompleted(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos);
void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance);
bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination);
bool destinationThroughGround(const osg::Vec3f& startPoint, const osg::Vec3f& destination);

@ -1214,7 +1214,6 @@ bool CharacterController::updateWeaponState()
bool animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
if (!animPlaying || complete >= 1.0f)
{
mUpperBodyState = UpperCharState_Nothing;
forcestateupdate = true;
mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype));

@ -22,6 +22,7 @@
#include "aicombat.hpp"
#include "aipursue.hpp"
#include "aitravel.hpp"
#include "spellcasting.hpp"
#include "autocalcspell.hpp"
#include "npcstats.hpp"
@ -1588,9 +1589,12 @@ namespace MWMechanics
void MechanicsManager::startCombat(const MWWorld::Ptr &ptr, const MWWorld::Ptr &target)
{
if (ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(target))
MWMechanics::AiSequence& aiSequence = ptr.getClass().getCreatureStats(ptr).getAiSequence();
if (aiSequence.isInCombat(target))
return;
ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(MWMechanics::AiCombat(target), ptr);
aiSequence.stack(MWMechanics::AiCombat(target), ptr);
if (target == getPlayer())
{
// if guard starts combat with player, guards pursuing player should do the same

@ -547,7 +547,7 @@ namespace MWMechanics
|| (effectIt->mEffectID == ESM::MagicEffect::CommandCreature && target.getTypeName() == typeid(ESM::Creature).name()))
&& !caster.isEmpty() && caster.getClass().isActor() && target != getPlayer() && magnitude >= target.getClass().getCreatureStats(target).getLevel())
{
MWMechanics::AiFollow package(caster.getCellRef().getRefId(), true);
MWMechanics::AiFollow package(caster, true);
target.getClass().getCreatureStats(target).getAiSequence().stack(package, target);
}

@ -59,7 +59,7 @@ namespace MWMechanics
MWMechanics::CreatureStats& summonedCreatureStats = ref.getPtr().getClass().getCreatureStats(ref.getPtr());
// Make the summoned creature follow its master and help in fights
AiFollow package(mActor.getCellRef().getRefId());
AiFollow package(mActor);
summonedCreatureStats.getAiSequence().stack(package, ref.getPtr());
creatureActorId = summonedCreatureStats.getActorId();

@ -848,6 +848,16 @@ namespace MWPhysics
const osg::Quat &orient,
float queryDistance, std::vector<MWWorld::Ptr> targets)
{
// First of all, try to hit where you aim to
int hitmask = CollisionType_World | CollisionType_Door | CollisionType_HeightMap | CollisionType_Actor;
RayResult result = castRay(origin, origin + (orient * osg::Vec3f(0.0f, queryDistance, 0.0f)), actor, targets, CollisionType_Actor, hitmask);
if (result.mHit)
{
return std::make_pair(result.mHitObject, result.mHitPos);
}
// Use cone shape as fallback
const MWWorld::Store<ESM::GameSetting> &store = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
btConeShape shape (osg::DegreesToRadians(store.find("fCombatAngleXY")->getFloat()/2.0f), queryDistance);

@ -287,6 +287,7 @@ namespace MWWorld
mAttackingOrSpell = false;
mCurrentCrimeId = -1;
mPaidCrimeId = -1;
mPreviousItems.clear();
mLastKnownExteriorPosition = osg::Vec3f(0,0,0);
for (int i=0; i<ESM::Skill::Length; ++i)
@ -341,6 +342,8 @@ namespace MWWorld
for (int i=0; i<ESM::Skill::Length; ++i)
mSaveSkills[i].writeState(player.mSaveSkills[i]);
player.mPreviousItems = mPreviousItems;
writer.startRecord (ESM::REC_PLAY);
player.save (writer);
writer.endRecord (ESM::REC_PLAY);
@ -441,6 +444,8 @@ namespace MWWorld
mForwardBackward = 0;
mTeleported = false;
mPreviousItems = player.mPreviousItems;
return true;
}
@ -461,4 +466,19 @@ namespace MWWorld
{
return mPaidCrimeId;
}
void Player::setPreviousItem(const std::string& boundItemId, const std::string& previousItemId)
{
mPreviousItems[boundItemId] = previousItemId;
}
std::string Player::getPreviousItem(const std::string& boundItemId)
{
return mPreviousItems[boundItemId];
}
void Player::erasePreviousItem(const std::string& boundItemId)
{
mPreviousItems.erase(boundItemId);
}
}

@ -1,6 +1,8 @@
#ifndef GAME_MWWORLD_PLAYER_H
#define GAME_MWWORLD_PLAYER_H
#include <map>
#include "../mwworld/refdata.hpp"
#include "../mwworld/livecellref.hpp"
@ -46,6 +48,9 @@ namespace MWWorld
int mCurrentCrimeId; // the id assigned witnesses
int mPaidCrimeId; // the last id paid off (0 bounty)
typedef std::map<std::string, std::string> PreviousItems; // previous equipped items, needed for bound spells
PreviousItems mPreviousItems;
// Saved stats prior to becoming a werewolf
MWMechanics::SkillValue mSaveSkills[ESM::Skill::Length];
MWMechanics::AttributeValue mSaveAttributes[ESM::Attribute::Length];
@ -120,6 +125,10 @@ namespace MWWorld
int getNewCrimeId(); // get new id for witnesses
void recordCrimeId(); // record the paid crime id when bounty is 0
int getCrimeId() const; // get the last paid crime id
void setPreviousItem(const std::string& boundItemId, const std::string& previousItemId);
std::string getPreviousItem(const std::string& boundItemId);
void erasePreviousItem(const std::string& boundItemId);
};
}
#endif

@ -6,7 +6,6 @@
#include <components/esm/esmwriter.hpp>
#include <components/esm/savedgame.hpp>
#include <components/esm/weatherstate.hpp>
#include <components/fallback/fallback.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/soundmanager.hpp"
@ -43,49 +42,67 @@ namespace
}
template <typename T>
T TimeOfDayInterpolator<T>::getValue(const float gameHour, const TimeOfDaySettings& timeSettings) const
T TimeOfDayInterpolator<T>::getValue(const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const
{
// TODO: use pre/post sunset/sunrise time values in [Weather] section
WeatherSetting setting = timeSettings.getSetting(prefix);
float preSunriseTime = setting.mPreSunriseTime;
float postSunriseTime = setting.mPostSunriseTime;
float preSunsetTime = setting.mPreSunsetTime;
float postSunsetTime = setting.mPostSunsetTime;
// night
if (gameHour <= timeSettings.mNightEnd || gameHour >= timeSettings.mNightStart + 1)
if (gameHour < timeSettings.mNightEnd - preSunriseTime || gameHour > timeSettings.mNightStart + postSunsetTime)
return mNightValue;
// sunrise
else if (gameHour >= timeSettings.mNightEnd && gameHour <= timeSettings.mDayStart + 1)
else if (gameHour >= timeSettings.mNightEnd - preSunriseTime && gameHour <= timeSettings.mDayStart + postSunriseTime)
{
if (gameHour <= timeSettings.mSunriseTime)
float duration = timeSettings.mDayStart + postSunriseTime - timeSettings.mNightEnd + preSunriseTime;
float middle = timeSettings.mNightEnd - preSunriseTime + duration / 2.f;
if (gameHour <= middle)
{
// fade in
float advance = timeSettings.mSunriseTime - gameHour;
float factor = advance / 0.5f;
float advance = middle - gameHour;
float factor = 0.f;
if (duration > 0)
factor = advance / duration * 2;
return lerp(mSunriseValue, mNightValue, factor);
}
else
{
// fade out
float advance = gameHour - timeSettings.mSunriseTime;
float factor = advance / 3.f;
float advance = gameHour - middle;
float factor = 1.f;
if (duration > 0)
factor = advance / duration * 2;
return lerp(mSunriseValue, mDayValue, factor);
}
}
// day
else if (gameHour >= timeSettings.mDayStart + 1 && gameHour <= timeSettings.mDayEnd - 1)
else if (gameHour > timeSettings.mDayStart + postSunriseTime && gameHour < timeSettings.mDayEnd - preSunsetTime)
return mDayValue;
// sunset
else if (gameHour >= timeSettings.mDayEnd - 1 && gameHour <= timeSettings.mNightStart + 1)
else if (gameHour >= timeSettings.mDayEnd - preSunsetTime && gameHour <= timeSettings.mNightStart + postSunsetTime)
{
if (gameHour <= timeSettings.mDayEnd + 1)
float duration = timeSettings.mNightStart + postSunsetTime - timeSettings.mDayEnd + preSunsetTime;
float middle = timeSettings.mDayEnd - preSunsetTime + duration / 2.f;
if (gameHour <= middle)
{
// fade in
float advance = (timeSettings.mDayEnd + 1) - gameHour;
float factor = (advance / 2);
float advance = middle - gameHour;
float factor = 0.f;
if (duration > 0)
factor = advance / duration * 2;
return lerp(mSunsetValue, mDayValue, factor);
}
else
{
// fade out
float advance = gameHour - (timeSettings.mDayEnd + 1);
float factor = advance / 2.f;
float advance = gameHour - middle;
float factor = 1.f;
if (duration > 0)
factor = advance / duration * 2;
return lerp(mSunsetValue, mNightValue, factor);
}
}
@ -539,10 +556,28 @@ WeatherManager::WeatherManager(MWRender::RenderingManager& rendering, const Fall
, mPlayingSoundID()
{
mTimeSettings.mNightStart = mSunsetTime + mSunsetDuration;
mTimeSettings.mNightEnd = mSunriseTime - 0.5f;
mTimeSettings.mNightEnd = mSunriseTime;
mTimeSettings.mDayStart = mSunriseTime + mSunriseDuration;
mTimeSettings.mDayEnd = mSunsetTime;
mTimeSettings.mSunriseTime = mSunriseTime;
mTimeSettings.addSetting(fallback, "Sky");
mTimeSettings.addSetting(fallback, "Ambient");
mTimeSettings.addSetting(fallback, "Fog");
mTimeSettings.addSetting(fallback, "Sun");
// Morrowind handles stars settings differently for other ones
mTimeSettings.mStarsPostSunsetStart = fallback.getFallbackFloat("Weather_Stars_Post-Sunset_Start");
mTimeSettings.mStarsPreSunriseFinish = fallback.getFallbackFloat("Weather_Stars_Pre-Sunrise_Finish");
mTimeSettings.mStarsFadingDuration = fallback.getFallbackFloat("Weather_Stars_Fading_Duration");
WeatherSetting starSetting = {
mTimeSettings.mStarsPreSunriseFinish,
mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPreSunriseFinish,
mTimeSettings.mStarsPostSunsetStart,
mTimeSettings.mStarsFadingDuration - mTimeSettings.mStarsPostSunsetStart
};
mTimeSettings.mSunriseTransitions["Stars"] = starSetting;
mWeatherSettings.reserve(10);
// These distant land fog factor and offset values are the defaults MGE XE provides. Should be
@ -698,10 +733,13 @@ void WeatherManager::update(float duration, bool paused, const TimeStamp& time,
const float nightDuration = 24.f - dayDuration;
double theta;
if ( !is_night ) {
if ( !is_night )
{
theta = static_cast<float>(osg::PI) * (adjustedHour - mSunriseTime) / dayDuration;
} else {
theta = static_cast<float>(osg::PI) * (1.f - (adjustedHour - adjustedNightStart) / nightDuration);
}
else
{
theta = static_cast<float>(osg::PI) + static_cast<float>(osg::PI) * (adjustedHour - adjustedNightStart) / nightDuration;
}
osg::Vec3f final(
@ -711,7 +749,7 @@ void WeatherManager::update(float duration, bool paused, const TimeStamp& time,
mRendering.setSunDirection( final * -1 );
}
float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings);
float underwaterFog = mUnderwaterFog.getValue(time.getHour(), mTimeSettings, "Fog");
float peakHour = mSunriseTime + (mSunsetTime - mSunriseTime) / 2;
if (time.getHour() < mSunriseTime || time.getHour() > mSunsetTime)
@ -784,7 +822,7 @@ unsigned int WeatherManager::getWeatherID() const
bool WeatherManager::useTorches(float hour) const
{
bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart - 1;
bool isDark = hour < mSunriseTime || hour > mTimeSettings.mNightStart;
return isDark && !mPrecipitation;
}
@ -1060,20 +1098,25 @@ inline void WeatherManager::calculateResult(const int weatherID, const float gam
mResult.mParticleEffect = current.mParticleEffect;
mResult.mRainEffect = current.mRainEffect;
mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart - 1);
mResult.mNight = (gameHour < mSunriseTime || gameHour > mTimeSettings.mNightStart + mTimeSettings.mStarsPostSunsetStart - mTimeSettings.mStarsFadingDuration);
mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings);
mResult.mFogDepth = current.mLandFogDepth.getValue(gameHour, mTimeSettings, "Fog");
mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings, "Fog");
mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings, "Ambient");
mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings, "Sun");
mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings, "Sky");
mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings, "Stars");
mResult.mDLFogFactor = current.mDL.FogFactor;
mResult.mDLFogOffset = current.mDL.FogOffset;
mResult.mFogColor = current.mFogColor.getValue(gameHour, mTimeSettings);
mResult.mAmbientColor = current.mAmbientColor.getValue(gameHour, mTimeSettings);
mResult.mSunColor = current.mSunColor.getValue(gameHour, mTimeSettings);
mResult.mSkyColor = current.mSkyColor.getValue(gameHour, mTimeSettings);
mResult.mNightFade = mNightFade.getValue(gameHour, mTimeSettings);
if (gameHour >= mSunsetTime - mSunPreSunsetTime)
WeatherSetting setting = mTimeSettings.getSetting("Sun");
float preSunsetTime = setting.mPreSunsetTime;
if (gameHour >= mTimeSettings.mDayEnd - preSunsetTime)
{
float factor = (gameHour - (mSunsetTime - mSunPreSunsetTime)) / mSunPreSunsetTime;
float factor = 1.f;
if (preSunsetTime > 0)
factor = (gameHour - (mTimeSettings.mDayEnd - preSunsetTime)) / preSunsetTime;
factor = std::min(1.f, factor);
mResult.mSunDiscColor = lerp(osg::Vec4f(1,1,1,1), current.mSunDiscSunsetColor, factor);
// The SunDiscSunsetColor in the INI isn't exactly the resulting color on screen, most likely because
@ -1087,15 +1130,17 @@ inline void WeatherManager::calculateResult(const int weatherID, const float gam
else
mResult.mSunDiscColor = osg::Vec4f(1,1,1,1);
if (gameHour >= mSunsetTime)
if (gameHour >= mTimeSettings.mDayEnd)
{
float fade = std::min(1.f, (gameHour - mSunsetTime) / 2.f);
// sunset
float fade = std::min(1.f, (gameHour - mTimeSettings.mDayEnd) / (mTimeSettings.mNightStart - mTimeSettings.mDayEnd));
fade = fade*fade;
mResult.mSunDiscColor.a() = 1.f - fade;
}
else if (gameHour >= mSunriseTime && gameHour <= mSunriseTime + 1)
else if (gameHour >= mTimeSettings.mNightEnd && gameHour <= mTimeSettings.mNightEnd + mSunriseDuration / 2.f)
{
mResult.mSunDiscColor.a() = gameHour - mSunriseTime;
// sunrise
mResult.mSunDiscColor.a() = gameHour - mTimeSettings.mNightEnd;
}
else
mResult.mSunDiscColor.a() = 1;

@ -7,6 +7,8 @@
#include <osg/Vec4f>
#include <components/fallback/fallback.hpp>
#include "../mwbase/soundmanager.hpp"
#include "../mwrender/sky.hpp"
@ -38,6 +40,13 @@ namespace MWWorld
{
class TimeStamp;
struct WeatherSetting
{
float mPreSunriseTime;
float mPostSunriseTime;
float mPreSunsetTime;
float mPostSunsetTime;
};
struct TimeOfDaySettings
{
@ -45,7 +54,37 @@ namespace MWWorld
float mNightEnd;
float mDayStart;
float mDayEnd;
float mSunriseTime;
std::map<std::string, WeatherSetting> mSunriseTransitions;
float mStarsPostSunsetStart;
float mStarsPreSunriseFinish;
float mStarsFadingDuration;
WeatherSetting getSetting(const std::string& type) const
{
std::map<std::string, WeatherSetting>::const_iterator it = mSunriseTransitions.find(type);
if (it != mSunriseTransitions.end())
{
return it->second;
}
else
{
return { 1.f, 1.f, 1.f, 1.f };
}
}
void addSetting(const Fallback::Map& fallback, const std::string& type)
{
WeatherSetting setting = {
fallback.getFallbackFloat("Weather_" + type + "_Pre-Sunrise_Time"),
fallback.getFallbackFloat("Weather_" + type + "_Post-Sunrise_Time"),
fallback.getFallbackFloat("Weather_" + type + "_Pre-Sunset_Time"),
fallback.getFallbackFloat("Weather_" + type + "_Post-Sunset_Time")
};
mSunriseTransitions[type] = setting;
}
};
/// Interpolates between 4 data points (sunrise, day, sunset, night) based on the time of day.
@ -59,7 +98,7 @@ namespace MWWorld
{
}
T getValue (const float gameHour, const TimeOfDaySettings& timeSettings) const;
T getValue (const float gameHour, const TimeOfDaySettings& timeSettings, const std::string& prefix) const;
private:
T mSunriseValue, mDayValue, mSunsetValue, mNightValue;

@ -1073,20 +1073,29 @@ namespace MWWorld
osg::Quat rot = osg::Quat(posdata.rot[0], osg::Vec3f(-1,0,0)) * osg::Quat(posdata.rot[2], osg::Vec3f(0,0,-1));
osg::Vec3f pos = ptr.getRefData().getPosition().asVec3();
osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr);
// the origin of hitbox is an actor's front, not center
distance += halfExtents.y();
// special cased for better aiming with the camera
// if we do not hit anything, will use the default approach as fallback
if (ptr == getPlayerPtr())
pos = getActorHeadTransform(ptr).getTrans(); // special cased for better aiming with the camera
else
{
// general case, compatible with all types of different creatures
// note: we intentionally do *not* use the collision box offset here, this is required to make
// some flying creatures work that have their collision box offset in the air
osg::Vec3f halfExtents = mPhysics->getHalfExtents(ptr);
pos.z() += halfExtents.z() * 2 * 0.75;
distance += halfExtents.y();
osg::Vec3f pos = getActorHeadTransform(ptr).getTrans();
std::pair<MWWorld::Ptr,osg::Vec3f> result = mPhysics->getHitContact(ptr, pos, rot, distance, targets);
if(!result.first.isEmpty())
return std::make_pair(result.first, result.second);
}
osg::Vec3f pos = ptr.getRefData().getPosition().asVec3();
// general case, compatible with all types of different creatures
// note: we intentionally do *not* use the collision box offset here, this is required to make
// some flying creatures work that have their collision box offset in the air
pos.z() += halfExtents.z();
std::pair<MWWorld::Ptr,osg::Vec3f> result = mPhysics->getHitContact(ptr, pos, rot, distance, targets);
if(result.first.isEmpty())
return std::make_pair(MWWorld::Ptr(), osg::Vec3f());
@ -2752,64 +2761,66 @@ namespace MWWorld
{
MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor);
// Get the target to use for "on touch" effects, using the facing direction from Head node
MWWorld::Ptr target;
float distance = getActivationDistancePlusTelekinesis();
osg::Vec3f hitPosition = actor.getRefData().getPosition().asVec3();
osg::Vec3f origin = getActorHeadTransform(actor).getTrans();
osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0))
* osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1));
osg::Vec3f direction = orient * osg::Vec3f(0,1,0);
osg::Vec3f dest = origin + direction * distance;
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result.
std::vector<MWWorld::Ptr> targetActors;
if (!actor.isEmpty() && actor != MWMechanics::getPlayer())
actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targetActors);
// For actor targets, we want to use bounding boxes (physics raycast).
// This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise.
// For object targets, we want the detailed shapes (rendering raycast).
// If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf.
MWPhysics::PhysicsSystem::RayResult result1 = mPhysics->castRay(origin, dest, actor, targetActors, MWPhysics::CollisionType_Actor);
const float fCombatDistance = getStore().get<ESM::GameSetting>().find("fCombatDistance")->getFloat();
MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true);
osg::Vec3f hitPosition = actor.getRefData().getPosition().asVec3();
float dist1 = std::numeric_limits<float>::max();
float dist2 = std::numeric_limits<float>::max();
// for player we can take faced object first
MWWorld::Ptr target;
if (actor == MWMechanics::getPlayer())
target = getFacedObject();
if (result1.mHit)
dist1 = (origin - result1.mHitPos).length();
if (result2.mHit)
dist2 = (origin - result2.mHitPointWorld).length();
// if the faced object can not be activated, do not use it
if (!target.isEmpty() && !target.getClass().canBeActivated(target))
target = NULL;
if (result1.mHit)
{
target = result1.mHitObject;
hitPosition = result1.mHitPos;
if (dist1 > getMaxActivationDistance() && !target.isEmpty() && (target.getClass().isActor() || !target.getClass().canBeActivated(target)))
target = NULL;
}
else if (result2.mHit)
if (target.isEmpty())
{
target = result2.mHitObject;
hitPosition = result2.mHitPointWorld;
if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target))
target = NULL;
}
// For actor targets, we want to use hit contact with bounding boxes.
// This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise.
// For object targets, we want the detailed shapes (rendering raycast).
// If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf.
std::pair<MWWorld::Ptr,osg::Vec3f> result1 = getHitContact(actor, fCombatDistance, targetActors);
// When targeting an actor that is in combat with an "on touch" spell,
// compare against the minimum of activation distance and combat distance.
// Get the target to use for "on touch" effects, using the facing direction from Head node
osg::Vec3f origin = getActorHeadTransform(actor).getTrans();
if (!target.isEmpty() && target.getClass().isActor() && target.getClass().getCreatureStats (target).getAiSequence().isInCombat())
{
distance = std::min (distance, getStore().get<ESM::GameSetting>().find("fCombatDistance")->getFloat());
if (distance < dist1)
target = NULL;
osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0))
* osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1));
osg::Vec3f direction = orient * osg::Vec3f(0,1,0);
float distance = getMaxActivationDistance();
osg::Vec3f dest = origin + direction * distance;
MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true);
float dist1 = std::numeric_limits<float>::max();
float dist2 = std::numeric_limits<float>::max();
if (!result1.first.isEmpty() && result1.first.getClass().isActor())
dist1 = (origin - result1.second).length();
if (result2.mHit)
dist2 = (origin - result2.mHitPointWorld).length();
if (!result1.first.isEmpty() && result1.first.getClass().isActor())
{
target = result1.first;
hitPosition = result1.second;
if (dist1 > getMaxActivationDistance())
target = NULL;
}
else if (result2.mHit)
{
target = result2.mHitObject;
hitPosition = result2.mHitPointWorld;
if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target))
target = NULL;
}
}
std::string selectedSpell = stats.getSpells().getSelectedSpell();
@ -3466,7 +3477,7 @@ namespace MWWorld
osg::Vec3f World::aimToTarget(const ConstPtr &actor, const MWWorld::ConstPtr& target)
{
osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3();
weaponPos.z() += mPhysics->getHalfExtents(actor).z() * 2 * 0.75;
weaponPos.z() += mPhysics->getHalfExtents(actor).z();
osg::Vec3f targetPos = mPhysics->getCollisionObjectPosition(target);
return (targetPos - weaponPos);
}
@ -3475,7 +3486,7 @@ namespace MWWorld
{
osg::Vec3f weaponPos = actor.getRefData().getPosition().asVec3();
osg::Vec3f halfExtents = mPhysics->getHalfExtents(actor);
weaponPos.z() += halfExtents.z() * 2 * 0.75;
weaponPos.z() += halfExtents.z();
return mPhysics->getHitDistance(weaponPos, target) - halfExtents.y();
}

@ -172,7 +172,7 @@ macro (get_generator_is_multi_config VALUE)
if (CMAKE_VERSION VERSION_GREATER 3.9 OR CMAKE_VERSION VERSION_EQUAL 3.9)
get_cmake_property(${VALUE} GENERATOR_IS_MULTI_CONFIG)
else (CMAKE_VERSION VERSION_GREATER 3.9 OR CMAKE_VERSION VERSION_EQUAL 3.9)
list(LENGTH "${CMAKE_CONFIGURATION_TYPES}" ${VALUE})
list(LENGTH CMAKE_CONFIGURATION_TYPES ${VALUE})
endif (CMAKE_VERSION VERSION_GREATER 3.9 OR CMAKE_VERSION VERSION_EQUAL 3.9)
endif (DEFINED generator_is_multi_config_var)
endmacro (get_generator_is_multi_config)

@ -119,6 +119,11 @@ namespace Compiler
return false;
}
}
else if (code==Scanner::S_comma && (mState==NameState || mState==EndNameState))
{
// ignoring comma (for now)
return true;
}
return Parser::parseSpecial (code, loc, scanner);
}

@ -512,7 +512,7 @@ namespace Compiler
return true;
}
if (code==Scanner::S_member && mState==PotentialExplicitState)
if (code==Scanner::S_member && mState==PotentialExplicitState && mAllowExpression)
{
mState = MemberState;
parseExpression (scanner, loc);

@ -239,4 +239,4 @@ void ContentSelectorView::ContentSelector::slotUncheckMultiSelectedItems()
void ContentSelectorView::ContentSelector::slotCheckMultiSelectedItems()
{
setCheckStateForMultiSelectedItems(true);
}
}

@ -15,7 +15,6 @@ namespace ContentSelectorView
Q_OBJECT
QMenu *mContextMenu;
QStringList mFilePaths;
protected:
@ -61,6 +60,7 @@ namespace ContentSelectorView
void signalCurrentGamefileIndexChanged (int);
void signalAddonDataChanged (const QModelIndex& topleft, const QModelIndex& bottomright);
void signalSelectedFilesChanged(QStringList selectedFiles);
private slots:

@ -35,17 +35,20 @@ namespace AiSequence
void AiTravel::load(ESMReader &esm)
{
esm.getHNT (mData, "DATA");
esm.getHNOT (mHidden, "HIDD");
}
void AiTravel::save(ESMWriter &esm) const
{
esm.writeHNT ("DATA", mData);
esm.writeHNT ("HIDD", mHidden);
}
void AiEscort::load(ESMReader &esm)
{
esm.getHNT (mData, "DATA");
mTargetId = esm.getHNString("TARG");
esm.getHNOT (mTargetActorId, "TAID");
esm.getHNT (mRemainingDuration, "DURA");
mCellId = esm.getHNOString ("CELL");
}
@ -54,6 +57,7 @@ namespace AiSequence
{
esm.writeHNT ("DATA", mData);
esm.writeHNString ("TARG", mTargetId);
esm.writeHNT ("TAID", mTargetActorId);
esm.writeHNT ("DURA", mRemainingDuration);
if (!mCellId.empty())
esm.writeHNString ("CELL", mCellId);
@ -63,6 +67,7 @@ namespace AiSequence
{
esm.getHNT (mData, "DATA");
mTargetId = esm.getHNString("TARG");
esm.getHNOT (mTargetActorId, "TAID");
esm.getHNT (mRemainingDuration, "DURA");
mCellId = esm.getHNOString ("CELL");
esm.getHNT (mAlwaysFollow, "ALWY");
@ -76,6 +81,7 @@ namespace AiSequence
{
esm.writeHNT ("DATA", mData);
esm.writeHNString("TARG", mTargetId);
esm.writeHNT ("TAID", mTargetActorId);
esm.writeHNT ("DURA", mRemainingDuration);
if (!mCellId.empty())
esm.writeHNString ("CELL", mCellId);
@ -154,6 +160,8 @@ namespace AiSequence
break;
}
}
esm.writeHNT ("LAST", mLastAiPackage);
}
void AiSequence::load(ESMReader &esm)
@ -221,6 +229,8 @@ namespace AiSequence
return;
}
}
esm.getHNOT (mLastAiPackage, "LAST");
}
}
}

@ -80,6 +80,7 @@ namespace ESM
struct AiTravel : AiPackage
{
AiTravelData mData;
bool mHidden;
void load(ESMReader &esm);
void save(ESMWriter &esm) const;
@ -89,6 +90,7 @@ namespace ESM
{
AiEscortData mData;
int mTargetActorId;
std::string mTargetId;
std::string mCellId;
float mRemainingDuration;
@ -101,6 +103,7 @@ namespace ESM
{
AiEscortData mData;
int mTargetActorId;
std::string mTargetId;
std::string mCellId;
float mRemainingDuration;
@ -147,10 +150,14 @@ namespace ESM
struct AiSequence
{
AiSequence() {}
AiSequence()
{
mLastAiPackage = -1;
}
~AiSequence();
std::vector<AiPackageContainer> mPackages;
int mLastAiPackage;
void load (ESMReader &esm);
void save (ESMWriter &esm) const;

@ -31,6 +31,18 @@ void ESM::Player::load (ESMReader &esm)
mPaidCrimeId = -1;
esm.getHNOT (mPaidCrimeId, "PAYD");
bool checkPrevItems = true;
while (checkPrevItems)
{
std::string boundItemId = esm.getHNOString("BOUN");
std::string prevItemId = esm.getHNOString("PREV");
if (!boundItemId.empty())
mPreviousItems[boundItemId] = prevItemId;
else
checkPrevItems = false;
}
if (esm.hasMoreSubs())
{
for (int i=0; i<ESM::Attribute::Length; ++i)
@ -62,6 +74,12 @@ void ESM::Player::save (ESMWriter &esm) const
esm.writeHNT ("CURD", mCurrentCrimeId);
esm.writeHNT ("PAYD", mPaidCrimeId);
for (PreviousItems::const_iterator it=mPreviousItems.begin(); it != mPreviousItems.end(); ++it)
{
esm.writeHNString ("BOUN", it->first);
esm.writeHNString ("PREV", it->second);
}
for (int i=0; i<ESM::Attribute::Length; ++i)
mSaveAttributes[i].save(esm);
for (int i=0; i<ESM::Skill::Length; ++i)

@ -34,6 +34,9 @@ namespace ESM
StatState<int> mSaveAttributes[ESM::Attribute::Length];
StatState<int> mSaveSkills[ESM::Skill::Length];
typedef std::map<std::string, std::string> PreviousItems; // previous equipped items, needed for bound spells
PreviousItems mPreviousItems;
void load (ESMReader &esm);
void save (ESMWriter &esm) const;
};

@ -5,7 +5,7 @@
#include "defs.hpp"
unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE;
int ESM::SavedGame::sCurrentFormat = 3;
int ESM::SavedGame::sCurrentFormat = 5;
void ESM::SavedGame::load (ESMReader &esm)
{

@ -430,13 +430,16 @@ namespace ESMTerrain
// Second iteration - create and fill in the blend maps
const int blendmapSize = (realTextureSize-1) * chunkSize + 1;
// We need to upscale the blendmap 2x with nearest neighbor sampling to look like Vanilla
const int imageScaleFactor = 2;
const int blendmapImageSize = blendmapSize * imageScaleFactor;
for (int i=0; i<numBlendmaps; ++i)
{
GLenum format = pack ? GL_RGBA : GL_ALPHA;
osg::ref_ptr<osg::Image> image (new osg::Image);
image->allocateImage(blendmapSize, blendmapSize, 1, format, GL_UNSIGNED_BYTE);
image->allocateImage(blendmapImageSize, blendmapImageSize, 1, format, GL_UNSIGNED_BYTE);
unsigned char* pData = image->data();
for (int y=0; y<blendmapSize; ++y)
@ -449,13 +452,17 @@ namespace ESMTerrain
int blendIndex = (pack ? static_cast<int>(std::floor((layerIndex - 1) / 4.f)) : layerIndex - 1);
int channel = pack ? std::max(0, (layerIndex-1) % 4) : 0;
if (blendIndex == i)
pData[(blendmapSize - y - 1)*blendmapSize*channels + x*channels + channel] = 255;
else
pData[(blendmapSize - y - 1)*blendmapSize*channels + x*channels + channel] = 0;
int alpha = (blendIndex == i) ? 255 : 0;
int realY = (blendmapSize - y - 1)*imageScaleFactor;
int realX = x*imageScaleFactor;
pData[((realY+0)*blendmapImageSize + realX + 0)*channels + channel] = alpha;
pData[((realY+1)*blendmapImageSize + realX + 0)*channels + channel] = alpha;
pData[((realY+0)*blendmapImageSize + realX + 1)*channels + channel] = alpha;
pData[((realY+1)*blendmapImageSize + realX + 1)*channels + channel] = alpha;
}
}
blendmaps.push_back(image);
}
}

@ -104,13 +104,13 @@ namespace Nif
void NiLookAtController::read(NIFStream *nif)
{
Controller::read(nif);
data.read(nif);
target.read(nif);
}
void NiLookAtController::post(NIFFile *nif)
{
Controller::post(nif);
data.post(nif);
target.post(nif);
}
void NiPathController::read(NIFStream *nif)

@ -102,7 +102,7 @@ public:
class NiLookAtController : public Controller
{
public:
NiKeyframeDataPtr data;
NodePtr target;
void read(NIFStream *nif);
void post(NIFFile *nif);

@ -261,7 +261,7 @@ namespace NifOsg
osg::ref_ptr<TextKeyMapHolder> textkeys (new TextKeyMapHolder);
osg::ref_ptr<osg::Node> created = handleNode(nifNode, NULL, imageManager, std::vector<int>(), 0, false, false, &textkeys->mTextKeys);
osg::ref_ptr<osg::Node> created = handleNode(nifNode, NULL, imageManager, std::vector<int>(), 0, false, false, false, &textkeys->mTextKeys);
if (nif->getUseSkinning())
{
@ -463,7 +463,7 @@ namespace NifOsg
}
osg::ref_ptr<osg::Node> handleNode(const Nif::Node* nifNode, osg::Group* parentNode, Resource::ImageManager* imageManager,
std::vector<int> boundTextures, int animflags, bool skipMeshes, bool isAnimated, TextKeyMap* textKeys, osg::Node* rootNode=NULL)
std::vector<int> boundTextures, int animflags, bool skipMeshes, bool hasMarkers, bool isAnimated, TextKeyMap* textKeys, osg::Node* rootNode=NULL)
{
if (rootNode != NULL && Misc::StringUtils::ciEqual(nifNode->name, "Bounding Box"))
return NULL;
@ -510,7 +510,7 @@ namespace NifOsg
if(sd->string == "MRK" && !Loader::getShowMarkers())
{
// Marker objects. These meshes are only visible in the editor.
skipMeshes = true;
hasMarkers = true;
}
}
}
@ -542,7 +542,7 @@ namespace NifOsg
node->setNodeMask(0x1);
}
if (skipMeshes && isAnimated) // make sure the empty node is not optimized away so the physicssystem can find it.
if ((skipMeshes || hasMarkers) && isAnimated) // make sure the empty node is not optimized away so the physicssystem can find it.
{
node->setDataVariance(osg::Object::DYNAMIC);
}
@ -554,13 +554,18 @@ namespace NifOsg
if (nifNode->recType == Nif::RC_NiTriShape && !skipMeshes)
{
const Nif::NiTriShape* triShape = static_cast<const Nif::NiTriShape*>(nifNode);
if (triShape->skin.empty())
handleTriShape(triShape, node, composite, boundTextures, animflags);
else
handleSkinnedTriShape(triShape, node, composite, boundTextures, animflags);
const std::string nodeName = Misc::StringUtils::lowerCase(triShape->name);
static const std::string pattern = "tri editormarker";
if (!hasMarkers || nodeName.compare(0, pattern.size(), pattern) != 0)
{
if (triShape->skin.empty())
handleTriShape(triShape, node, composite, boundTextures, animflags);
else
handleSkinnedTriShape(triShape, node, composite, boundTextures, animflags);
if (!nifNode->controller.empty())
handleMeshControllers(nifNode, node, composite, boundTextures, animflags);
if (!nifNode->controller.empty())
handleMeshControllers(nifNode, node, composite, boundTextures, animflags);
}
}
if(nifNode->recType == Nif::RC_NiAutoNormalParticles || nifNode->recType == Nif::RC_NiRotatingParticles)
@ -598,7 +603,7 @@ namespace NifOsg
for(size_t i = 0;i < children.length();++i)
{
if(!children[i].empty())
handleNode(children[i].getPtr(), node, imageManager, boundTextures, animflags, skipMeshes, isAnimated, textKeys, rootNode);
handleNode(children[i].getPtr(), node, imageManager, boundTextures, animflags, skipMeshes, hasMarkers, isAnimated, textKeys, rootNode);
}
}

@ -25,6 +25,9 @@ namespace Terrain
matrix.preMultTranslate(osg::Vec3f(0.5f, 0.5f, 0.f));
matrix.preMultScale(osg::Vec3f(scale, scale, 1.f));
matrix.preMultTranslate(osg::Vec3f(-0.5f, -0.5f, 0.f));
// We need to nudge the blendmap to look like vanilla.
// This causes visible seams unless the blendmap's resolution is doubled, but Vanilla also doubles the blendmap, apparently.
matrix.preMultTranslate(osg::Vec3f(1.0f/blendmapScale/4.0f, 1.0f/blendmapScale/4.0f, 0.f));
texMat = new osg::TexMat(matrix);

@ -8,6 +8,8 @@
<string>English</string>
<key>CFBundleExecutable</key>
<string>openmw-launcher</string>
<key>CFBundleIdentifier</key>
<string>org.openmw.openmw</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLongVersionString</key>
@ -18,6 +20,8 @@
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>${OPENMW_VERSION}</string>
<key>CFBundleVersion</key>
<string>${OPENMW_VERSION}</string>
<key>CSResourcesFileMapped</key>

@ -11,13 +11,6 @@
</rect>
</property>
<layout class="QVBoxLayout" name="pageVerticalLayout">
<item>
<widget class="QLabel" name="pageDescriptionLabel">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This temporary page contains new settings that will be available in-game in a post-1.0 release of OpenMW.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
@ -27,9 +20,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>-187</y>
<y>0</y>
<width>630</width>
<height>510</height>
<height>746</height>
</rect>
</property>
<layout class="QVBoxLayout" name="scrollAreaVerticalLayout">
@ -276,6 +269,94 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="testingGroup">
<property name="title">
<string>Testing</string>
</property>
<layout class="QVBoxLayout" name="testingGroupVerticalLayout">
<item>
<widget class="QLabel" name="testingLabel">
<property name="text">
<string>These settings are intended for testing mods and will cause issues if used for normal gameplay.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Line" name="testingLine">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="skipMenuCheckBox">
<property name="text">
<string>Skip menu and generate default character</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="startDefaultCharacterAtHorizontalLayout">
<item>
<spacer name="startDefaultCharacterAtHorizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="startDefaultCharacterAtLabel">
<property name="text">
<string>Start default character at</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="startDefaultCharacterAtField">
<property name="placeholderText">
<string>default cell</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Run script after startup:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="runScriptAfterStartupHorizontalLayout">
<item>
<widget class="QLineEdit" name="runScriptAfterStartupField"/>
</item>
<item>
<widget class="QPushButton" name="runScriptAfterStartupBrowseButton">
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="otherGroup">
<property name="title">

Loading…
Cancel
Save