Merge remote-tracking branch 'upstream/master' into alpha-meddling

pull/593/head
AnyOldName3 4 years ago
commit 4f510d85ba

@ -116,12 +116,13 @@ variables: &cs-targets
- .\ActivateMSVC.ps1
- cmake --build . --config $config --target ($targets.Split(','))
- cd $config
- echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt
- |
if (Get-ChildItem -Recurse *.pdb) {
7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb'
7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' CI-ID.txt
Get-ChildItem -Recurse *.pdb | Remove-Item
}
- 7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}.zip '*'
- 7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip '*'
after_script:
- Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log
cache:
@ -206,12 +207,13 @@ Windows_Ninja_CS_RelWithDebInfo:
- cd MSVC2019_64
- cmake --build . --config $config --target ($targets.Split(','))
- cd $config
- echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt
- |
if (Get-ChildItem -Recurse *.pdb) {
7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb'
7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip '*.pdb' CI-ID.txt
Get-ChildItem -Recurse *.pdb | Remove-Item
}
- 7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}.zip '*'
- 7z a -tzip ..\..\OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip '*'
after_script:
- Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log
cache:

@ -215,6 +215,7 @@ Programmers
Yohaulticetl
Yuri Krupenin
zelurker
Noah Gooder
Documentation
-------------

@ -3,12 +3,14 @@
Bug #832: OpenMW-CS: Handle deleted references
Bug #1662: Qt4 and Windows binaries crash if there's a non-ASCII character in a file path/config path
Bug #1901: Actors colliding behaviour is different from vanilla
Bug #1952: Incorrect particle lighting
Bug #2069: Fireflies in Fireflies invade Morrowind look wrong
Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs
Bug #2473: Unable to overstock merchants
Bug #2798: Mutable ESM records
Bug #2976 [reopened]: Issues combining settings from the command line and both config files
Bug #3137: Walking into a wall prevents jumping
Bug #3372: Projectiles and magic bolts go through moving targets
Bug #3676: NiParticleColorModifier isn't applied properly
Bug #3714: Savegame fails to load due to conflict between SpellState and MagicEffects
@ -16,15 +18,26 @@
Bug #3862: Random container contents behave differently than vanilla
Bug #3929: Leveled list merchant containers respawn on barter
Bug #4021: Attributes and skills are not stored as floats
Bug #4039: Multiple followers should have the same following distance
Bug #4055: Local scripts don't inherit variables from their base record
Bug #4083: Door animation freezes when colliding with actors
Bug #4201: Projectile-projectile collision
Bug #4247: Cannot walk up stairs in Ebonheart docks
Bug #4357: OpenMW-CS: TopicInfos index sorting and rearranging isn't fully functional
Bug #4363: Editor: Defect in Clone Function for Dialogue Info records
Bug #4447: Actor collision capsule shape allows looking through some walls
Bug #4465: Collision shape overlapping causes twitching
Bug #4476: Abot Gondoliers: player hangs in air during scenic travel
Bug #4568: Too many actors in one spot can push other actors out of bounds
Bug #4623: Corprus implementation is incorrect
Bug #4631: Setting MSAA level too high doesn't fall back to highest supported level
Bug #4764: Data race in osg ParticleSystem
Bug #4765: Data race in ChunkManager -> Array::setBinding
Bug #4774: Guards are ignorant of an invisible player that tries to attack them
Bug #5101: Hostile followers travel with the player
Bug #5108: Savegame bloating due to inefficient fog textures format
Bug #5165: Active spells should use real time intead of timestamps
Bug #5300: NPCs don't switch from torch to shield when starting combat
Bug #5358: ForceGreeting always resets the dialogue window completely
Bug #5363: Enchantment autocalc not always 0/1
Bug #5364: Script fails/stops if trying to startscript an unknown script
@ -33,6 +46,7 @@
Bug #5370: Opening an unlocked but trapped door uses the key
Bug #5384: openmw-cs: deleting an instance requires reload of scene window to show in editor
Bug #5387: Move/MoveWorld don't update the object's cell properly
Bug #5391: Races Redone 1.2 bodies don't show on the inventory
Bug #5397: NPC greeting does not reset if you leave + reenter area
Bug #5400: Editor: Verifier checks race of non-skin bodyparts
Bug #5403: Enchantment effect doesn't show on an enemy during death animation
@ -47,6 +61,7 @@
Bug #5441: Enemies can't push a player character when in critical strike stance
Bug #5451: Magic projectiles don't disappear with the caster
Bug #5452: Autowalk is being included in savegames
Bug #5469: Local map is reset when re-entering certain cells
Bug #5472: Mistify mod causes CTD in 0.46 on Mac
Bug #5479: NPCs who should be walking around town are standing around without walking
Bug #5484: Zero value items shouldn't be able to be bought or sold for 1 gold
@ -64,25 +79,39 @@
Bug #5604: Only one valid NIF root node is loaded from a single file
Bug #5611: Usable items with "0 Uses" should be used only once
Bug #5622: Can't properly interact with the console when in pause menu
Bug #5627: Bookart not shown if it isn't followed by <BR> statement
Bug #5633: Damage Spells in effect before god mode is enabled continue to hurt the player character and can kill them
Bug #5639: Tooltips cover Messageboxes
Bug #5644: Summon effects running on the player during game initialization cause crashes
Bug #5656: Sneaking characters block hits while standing
Bug #5661: Region sounds don't play at the right interval
Bug #5675: OpenMW-cs. FRMR subrecords are saved with the wrong MastIdx
Bug #5681: Player character can clip or pass through bridges instead of colliding against them
Bug #5687: Bound items covering the same inventory slot expiring at the same time freezes the game
Bug #5688: Water shader broken indoors with enable indoor shadows = false
Bug #5695: ExplodeSpell for actors doesn't target the ground
Bug #5703: OpenMW-CS menu system crashing on XFCE
Bug #5706: AI sequences stop looping after the saved game is reloaded
Bug #5713: OpenMW-CS: Collada models are corrupted in Qt-based scene view
Bug #5731: Editor: skirts are invisible on characters
Bug #5739: Saving and loading the save a second or two before hitting the ground doesn't count fall damage
Bug #5758: Paralyzed actors behavior is inconsistent with vanilla
Bug #5762: Movement solver is insufficiently robust
Bug #5821: NPCs from mods getting removed if mod order was changed
Bug #5835: OpenMW doesn't accept negative values for NPC's hello, alarm, fight, and flee
Bug #5836: OpenMW dialogue/greeting/voice filter doesn't accept negative Ai values for NPC's hello, alarm, fight, and flee
Bug #5838: Local map and other menus become blank in some locations while playing Wizards' Islands mod.
Bug #5840: GetSoundPlaying "Health Damage" doesn't play when NPC hits target with shield effect ( vanilla engine behavior )
Bug #5841: Can't Cast Zero Cost Spells When Magicka is < 0
Feature #390: 3rd person look "over the shoulder"
Feature #1536: Show more information about level on menu
Feature #2386: Distant Statics in the form of Object Paging
Feature #2404: Levelled List can not be placed into a container
Feature #2686: Timestamps in openmw.log
Feature #3171: OpenMW-CS: Instance drag selection
Feature #4894: Consider actors as obstacles for pathfinding
Feature #4899: Alpha-To-Coverage Anti-Aliasing for alpha testing
Feature #4977: Use the "default icon.tga" when an item's icon is not found
Feature #5043: Head Bobbing
Feature #5199: Improve Scene Colors
Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher
@ -104,8 +133,11 @@
Feature #5672: Make stretch menu background configuration more accessible
Feature #5692: Improve spell/magic item search to factor in magic effect names
Feature #5730: Add graphic herbalism option to the launcher and documents
Feature #5771: ori command should report where a mesh is loaded from and whether the x version is used.
Feature #5813: Instanced groundcover support
Task #5480: Drop Qt4 support
Task #5520: Improve cell name autocompleter implementation
Task #5844: Update 'toggle sneak' documentation
0.46.0
------

@ -21,7 +21,7 @@ New Features:
- Basics of Collada animations are now supported via osgAnimation plugin (#5456)
New Editor Features:
- ?
- Instance selection modes are now implemented (centred cube, corner-dragged cube, sphere) with four user-configurable actions (select only, add to selection, remove from selection, invert selection) (#3171)
Bug Fixes:
- NiParticleColorModifier in NIF files is now properly handled which solves issues regarding particle effects, e.g., smoke and fire (#1952, #3676)
@ -35,10 +35,13 @@ Bug Fixes:
Editor Bug Fixes:
- Deleted and moved objects within a cell are now saved properly (#832)
- Disabled record sorting in Topic and Journal Info tables, implemented drag-move for records (#4357)
- Topic and Journal Info records can now be cloned with a different parent Topic/Journal Id (#4363)
- Verifier no longer checks for alleged 'race' entries in clothing body parts (#5400)
- Loading mods now keeps the master index (#5675)
- Flicker and crashing on XFCE4 fixed (#5703)
- Collada models render properly in the Editor (#5713)
Miscellaneous:
- Prevent save-game bloating by using an appropriate fog texture format (#5108)
- Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363)
- Ensure that 'Enchantment autocalc" flag is treated as flag in OpenMW-CS and in our esm tools (#5363)

@ -1,4 +1,4 @@
#!/bin/sh -ex
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201018.zip -o ~/openmw-android-deps.zip
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/android/openmw-android-deps-20201129.zip -o ~/openmw-android-deps.zip
unzip -o ~/openmw-android-deps -d /usr/lib/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr > /dev/null

@ -1,5 +1,9 @@
#!/bin/sh -e
# workaround python issue on travis
HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.8 || true
HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --ignore-dependencies python@3.9 || true
# Some of these tools can come from places other than brew, so check before installing
command -v ccache >/dev/null 2>&1 || brew install ccache
command -v cmake >/dev/null 2>&1 || brew install cmake

@ -73,7 +73,7 @@ CONFIGURATIONS=()
TEST_FRAMEWORK=""
GOOGLE_INSTALL_ROOT=""
INSTALL_PREFIX="."
BULLET_DOUBLE=""
BULLET_DOUBLE=true
BULLET_DBL=""
BULLET_DBL_DISPLAY="Single precision"

@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.1.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Detect OS
include(cmake/OSIdentity.cmake)
# for link time optimization, remove if cmake version is >= 3.9
if(POLICY CMP0069)
cmake_policy(SET CMP0069 NEW)
@ -21,7 +24,7 @@ option(BUILD_NIFTEST "Build nif file tester" ON)
option(BUILD_DOCS "Build documentation." OFF )
option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF)
option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF)
option(BULLET_USE_DOUBLES "Use double precision for Bullet" OFF)
option(BULLET_USE_DOUBLES "Use double precision for Bullet" ON)
set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up.
@ -320,6 +323,7 @@ include_directories("."
${Boost_INCLUDE_DIR}
${MyGUI_INCLUDE_DIRS}
${OPENAL_INCLUDE_DIR}
${OPENGL_INCLUDE_DIR}
${BULLET_INCLUDE_DIRS}
)

@ -10,11 +10,9 @@
#include <cmath>
Launcher::AdvancedPage::AdvancedPage(Files::ConfigurationManager &cfg,
Config::GameSettings &gameSettings,
Launcher::AdvancedPage::AdvancedPage(Config::GameSettings &gameSettings,
Settings::Manager &engineSettings, QWidget *parent)
: QWidget(parent)
, mCfgMgr(cfg)
, mGameSettings(gameSettings)
, mEngineSettings(engineSettings)
{
@ -64,12 +62,12 @@ namespace
double convertToCells(double unitRadius)
{
return std::round((unitRadius / 0.93 + 1024) / CellSizeInUnits);
return std::round((unitRadius + 1024) / CellSizeInUnits);
}
double convertToUnits(double CellGridRadius)
{
return (CellSizeInUnits * CellGridRadius - 1024) * 0.93;
return CellSizeInUnits * CellGridRadius - 1024;
}
}
@ -110,7 +108,7 @@ bool Launcher::AdvancedPage::loadSettings()
loadSettingBool(magicItemAnimationsCheckBox, "use magic item animations", "Game");
connect(animSourcesCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotAnimSourcesToggled(bool)));
loadSettingBool(animSourcesCheckBox, "use additional anim sources", "Game");
if (animSourcesCheckBox->checkState())
if (animSourcesCheckBox->checkState() != Qt::Unchecked)
{
loadSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game");
loadSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game");

@ -9,7 +9,6 @@
#include <components/settings/settings.hpp>
namespace Files { struct ConfigurationManager; }
namespace Config { class GameSettings; }
namespace Launcher
@ -19,7 +18,7 @@ namespace Launcher
Q_OBJECT
public:
AdvancedPage(Files::ConfigurationManager &cfg, Config::GameSettings &gameSettings,
AdvancedPage(Config::GameSettings &gameSettings,
Settings::Manager &engineSettings, QWidget *parent = nullptr);
bool loadSettings();
@ -35,7 +34,6 @@ namespace Launcher
void slotViewOverShoulderToggled(bool checked);
private:
Files::ConfigurationManager &mCfgMgr;
Config::GameSettings &mGameSettings;
Settings::Manager &mEngineSettings;
QCompleter mCellNameCompleter;

@ -88,11 +88,7 @@ namespace Launcher
QStringList previousSelectedFiles;
QString mDataLocal;
void setPluginsCheckstates(Qt::CheckState state);
void buildView();
void setupConfig();
void readConfig();
void setProfile (int index, bool savePrevious);
void setProfile (const QString &previous, const QString &current, bool savePrevious);
void removeProfile (const QString &profile);

@ -29,9 +29,8 @@ QString getAspect(int x, int y)
return QString(QString::number(xaspect) + ":" + QString::number(yaspect));
}
Launcher::GraphicsPage::GraphicsPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent)
Launcher::GraphicsPage::GraphicsPage(Settings::Manager &engineSettings, QWidget *parent)
: QWidget(parent)
, mCfgMgr(cfg)
, mEngineSettings(engineSettings)
{
setObjectName ("GraphicsPage");
@ -207,7 +206,7 @@ void Launcher::GraphicsPage::saveSettings()
if (cScreen != mEngineSettings.getInt("screen", "Video"))
mEngineSettings.setInt("screen", "Video", cScreen);
if (framerateLimitCheckBox->checkState())
if (framerateLimitCheckBox->checkState() != Qt::Unchecked)
{
float cFpsLimit = framerateLimitSpinBox->value();
if (cFpsLimit != mEngineSettings.getFloat("framerate limit", "Video"))
@ -218,7 +217,7 @@ void Launcher::GraphicsPage::saveSettings()
mEngineSettings.setFloat("framerate limit", "Video", 0);
}
int cShadowDist = shadowDistanceCheckBox->checkState() ? shadowDistanceSpinBox->value() : 0;
int cShadowDist = shadowDistanceCheckBox->checkState() != Qt::Unchecked ? shadowDistanceSpinBox->value() : 0;
if (mEngineSettings.getInt("maximum shadow map distance", "Shadows") != cShadowDist)
mEngineSettings.setInt("maximum shadow map distance", "Shadows", cShadowDist);
float cFadeStart = fadeStartSpinBox->value();

@ -20,7 +20,7 @@ namespace Launcher
Q_OBJECT
public:
GraphicsPage(Files::ConfigurationManager &cfg, Settings::Manager &engineSettings, QWidget *parent = nullptr);
GraphicsPage(Settings::Manager &engineSettings, QWidget *parent = nullptr);
void saveSettings();
bool loadSettings();
@ -35,7 +35,6 @@ namespace Launcher
void slotShadowDistLimitToggled(bool checked);
private:
Files::ConfigurationManager &mCfgMgr;
Settings::Manager &mEngineSettings;
QVector<QStringList> mResolutionsPerScreen;

@ -126,9 +126,9 @@ void Launcher::MainDialog::createPages()
mPlayPage = new PlayPage(this);
mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this);
mGraphicsPage = new GraphicsPage(mCfgMgr, mEngineSettings, this);
mGraphicsPage = new GraphicsPage(mEngineSettings, this);
mSettingsPage = new SettingsPage(mCfgMgr, mGameSettings, mLauncherSettings, this);
mAdvancedPage = new AdvancedPage(mCfgMgr, mGameSettings, mEngineSettings, this);
mAdvancedPage = new AdvancedPage(mGameSettings, mEngineSettings, this);
// Set the combobox of the play page to imitate the combobox on the datafilespage
mPlayPage->setProfilesModel(mDataFilesPage->profilesModel());

@ -45,4 +45,5 @@ QString CellNameLoader::getCellName(ESM::ESMReader &esmReader)
cell.loadNameAndData(esmReader, isDeleted);
return QString::fromStdString(cell.mName);
}
}

@ -63,6 +63,9 @@ int runApplication(int argc, char *argv[])
application.setWindowIcon (QIcon (":./openmw-cs.png"));
CS::Editor editor(argc, argv);
#ifdef __linux__
setlocale(LC_NUMERIC,"C");
#endif
if(!editor.makeIPCServer())
{

@ -325,12 +325,6 @@ std::shared_ptr<CSMFilter::Node> CSMFilter::Parser::parseNAry (const Token& keyw
break;
}
if (nodes.empty())
{
error();
return std::shared_ptr<Node>();
}
switch (keyword.mType)
{
case Token::Type_Keyword_And: return std::shared_ptr<CSMFilter::Node> (new AndNode (nodes));

@ -247,6 +247,15 @@ void CSMPrefs::State::declare()
EnumValues landeditOutsideVisibleCell;
landeditOutsideVisibleCell.add (showAndLandEdit).add (dontLandEdit);
EnumValue SelectOnly ("Select only");
EnumValue SelectAdd ("Add to selection");
EnumValue SelectRemove ("Remove from selection");
EnumValue selectInvert ("Invert selection");
EnumValues primarySelectAction;
primarySelectAction.add (SelectOnly).add (SelectAdd).add (SelectRemove).add (selectInvert);
EnumValues secondarySelectAction;
secondarySelectAction.add (SelectOnly).add (SelectAdd).add (SelectRemove).add (selectInvert);
declareCategory ("3D Scene Editing");
declareInt ("distance", "Drop Distance", 50).
setTooltip ("If an instance drop can not be placed against another object at the "
@ -276,6 +285,12 @@ void CSMPrefs::State::declare()
declareBool ("open-list-view", "Open displays list view", false).
setTooltip ("When opening a reference from the scene view, it will open the"
" instance list view instead of the individual instance record view.");
declareEnum ("primary-select-action", "Action for primary select", SelectOnly).
setTooltip("Selection can be chosen between select only, add to selection, remove from selection and invert selection.").
addValues (primarySelectAction);
declareEnum ("secondary-select-action", "Action for secondary select", SelectAdd).
setTooltip("Selection can be chosen between select only, add to selection, remove from selection and invert selection.").
addValues (secondarySelectAction);
declareCategory ("Key Bindings");

@ -397,6 +397,10 @@ void CSMWorld::CloneCommand::redo()
{
mModel.cloneRecord (mIdOrigin, mId, mType);
applyModifications();
for (auto& value : mOverrideValues)
{
mModel.setData(mModel.getModelIndex (mId, value.first), value.second);
}
}
void CSMWorld::CloneCommand::undo()
@ -404,6 +408,11 @@ void CSMWorld::CloneCommand::undo()
mModel.removeRow (mModel.getModelIndex (mId, 0).row());
}
void CSMWorld::CloneCommand::setOverrideValue(int column, QVariant value)
{
mOverrideValues.emplace_back(std::make_pair(column, value));
}
CSMWorld::CreatePathgridCommand::CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand *parent)
: CreateCommand(model, id, parent)
{

@ -183,6 +183,7 @@ namespace CSMWorld
class CloneCommand : public CreateCommand
{
std::string mIdOrigin;
std::vector<std::pair<int, QVariant>> mOverrideValues;
public:
@ -194,6 +195,8 @@ namespace CSMWorld
void redo() override;
void undo() override;
void setOverrideValue(int column, QVariant value);
};
class RevertCommand : public QUndoCommand

@ -10,7 +10,7 @@
#include "nestedtablewrapper.hpp"
CSMWorld::PotionColumns::PotionColumns (const InventoryColumns& columns)
: InventoryColumns (columns) {}
: InventoryColumns (columns), mEffects(nullptr) {}
CSMWorld::PotionRefIdAdapter::PotionRefIdAdapter (const PotionColumns& columns,
const RefIdColumn *autoCalc)

@ -115,7 +115,7 @@ namespace CSMWorld
{
const RefIdColumn *mModel;
ModelColumns (const BaseColumns& base) : BaseColumns (base) {}
ModelColumns (const BaseColumns& base) : BaseColumns (base), mModel(nullptr) {}
};
/// \brief Adapter for IDs with models (all but levelled lists)
@ -1858,18 +1858,18 @@ namespace CSMWorld
break; // always save
case 16:
if (content.mType == ESM::AI_Travel)
content.mTravel.mZ = value.toFloat();
content.mTravel.mX = value.toFloat();
else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort)
content.mTarget.mZ = value.toFloat();
content.mTarget.mX = value.toFloat();
else
return; // return without saving
break; // always save
case 17:
if (content.mType == ESM::AI_Travel)
content.mTravel.mZ = value.toFloat();
content.mTravel.mY = value.toFloat();
else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort)
content.mTarget.mZ = value.toFloat();
content.mTarget.mY = value.toFloat();
else
return; // return without saving

@ -17,7 +17,7 @@ void CSMWorld::ResourcesManager::addResources (const Resources& resources)
const char * const * CSMWorld::ResourcesManager::getMeshExtensions()
{
// maybe we could go over the osgDB::Registry to list all supported node formats
static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", 0 };
static const char * const sMeshTypes[] = { "nif", "osg", "osgt", "osgb", "osgx", "osg2", "dae", 0 };
return sMeshTypes;
}

@ -438,8 +438,8 @@ void CSVDoc::View::updateActions()
for (std::vector<QAction *>::iterator iter (mEditingActions.begin()); iter!=mEditingActions.end(); ++iter)
(*iter)->setEnabled (editing);
mUndo->setEnabled (editing & mDocument->getUndoStack().canUndo());
mRedo->setEnabled (editing & mDocument->getUndoStack().canRedo());
mUndo->setEnabled (editing && mDocument->getUndoStack().canUndo());
mRedo->setEnabled (editing && mDocument->getUndoStack().canRedo());
mSave->setEnabled (!(mDocument->getState() & CSMDoc::State_Saving) && !running);
mVerify->setEnabled (!(mDocument->getState() & CSMDoc::State_Verifying));

@ -1,5 +1,7 @@
#include "cell.hpp"
#include <math.h>
#include <osg/PositionAttitudeTransform>
#include <osg/Geode>
#include <osg/Geometry>
@ -25,6 +27,7 @@
#include "pathgrid.hpp"
#include "terrainstorage.hpp"
#include "object.hpp"
#include "instancedragmodes.hpp"
namespace CSVRender
{
@ -496,6 +499,50 @@ void CSVRender::Cell::selectAllWithSameParentId (int elementMask)
}
}
void CSVRender::Cell::handleSelectDrag(Object* object, DragMode dragMode)
{
if (dragMode == DragMode_Select_Only || dragMode == DragMode_Select_Add)
object->setSelected(true);
else if (dragMode == DragMode_Select_Remove)
object->setSelected(false);
else if (dragMode == DragMode_Select_Invert)
object->setSelected (!object->getSelected());
}
void CSVRender::Cell::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode)
{
for (auto& object : mObjects)
{
if (dragMode == DragMode_Select_Only) object.second->setSelected (false);
if ( ( object.second->getPosition().pos[0] > pointA[0] && object.second->getPosition().pos[0] < pointB[0] ) ||
( object.second->getPosition().pos[0] > pointB[0] && object.second->getPosition().pos[0] < pointA[0] ))
{
if ( ( object.second->getPosition().pos[1] > pointA[1] && object.second->getPosition().pos[1] < pointB[1] ) ||
( object.second->getPosition().pos[1] > pointB[1] && object.second->getPosition().pos[1] < pointA[1] ))
{
if ( ( object.second->getPosition().pos[2] > pointA[2] && object.second->getPosition().pos[2] < pointB[2] ) ||
( object.second->getPosition().pos[2] > pointB[2] && object.second->getPosition().pos[2] < pointA[2] ))
handleSelectDrag(object.second, dragMode);
}
}
}
}
void CSVRender::Cell::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode)
{
for (auto& object : mObjects)
{
if (dragMode == DragMode_Select_Only) object.second->setSelected (false);
float distanceFromObject = (point - object.second->getPosition().asVec3()).length();
if (distanceFromObject < distance) handleSelectDrag(object.second, dragMode);
}
}
void CSVRender::Cell::setCellArrows (int mask)
{
for (int i=0; i<4; ++i)

@ -10,6 +10,7 @@
#include "../../model/world/cellcoordinates.hpp"
#include "terrainstorage.hpp"
#include "instancedragmodes.hpp"
class QModelIndex;
@ -152,6 +153,12 @@ namespace CSVRender
// already selected
void selectAllWithSameParentId (int elementMask);
void handleSelectDrag(Object* object, DragMode dragMode);
void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode);
void selectWithinDistance(const osg::Vec3d& pointA, float distance, DragMode dragMode);
void setCellArrows (int mask);
/// \brief Set marker for this cell.

@ -0,0 +1,18 @@
#ifndef CSV_WIDGET_INSTANCEDRAGMODES_H
#define CSV_WIDGET_INSTANCEDRAGMODES_H
namespace CSVRender
{
enum DragMode
{
DragMode_None,
DragMode_Move,
DragMode_Rotate,
DragMode_Scale,
DragMode_Select_Only,
DragMode_Select_Add,
DragMode_Select_Remove,
DragMode_Select_Invert
};
}
#endif

@ -96,6 +96,33 @@ osg::Vec3f CSVRender::InstanceMode::getScreenCoords(const osg::Vec3f& pos)
return pos * combined;
}
osg::Vec3f CSVRender::InstanceMode::getProjectionSpaceCoords(const osg::Vec3f& pos)
{
osg::Matrix viewMatrix = getWorldspaceWidget().getCamera()->getViewMatrix();
osg::Matrix projMatrix = getWorldspaceWidget().getCamera()->getProjectionMatrix();
osg::Matrix combined = viewMatrix * projMatrix;
return pos * combined;
}
osg::Vec3f CSVRender::InstanceMode::getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart)
{
osg::Matrix viewMatrix;
viewMatrix.invert(getWorldspaceWidget().getCamera()->getViewMatrix());
osg::Matrix projMatrix;
projMatrix.invert(getWorldspaceWidget().getCamera()->getProjectionMatrix());
osg::Matrix combined = projMatrix * viewMatrix;
/* calculate viewport normalized coordinates
note: is there a reason to use getCamera()->getViewport()->computeWindowMatrix() instead? */
float x = (point.x() * 2) / getWorldspaceWidget().getCamera()->getViewport()->width() - 1.0f;
float y = 1.0f - (point.y() * 2) / getWorldspaceWidget().getCamera()->getViewport()->height();
osg::Vec3f mousePlanePoint = osg::Vec3f(x, y, dragStart.z()) * combined;
return mousePlanePoint;
}
CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, osg::ref_ptr<osg::Group> parentNode, QWidget *parent)
: EditMode (worldspaceWidget, QIcon (":scenetoolbar/editing-instance"), Mask_Reference | Mask_Terrain, "Instance editing",
parent), mSubMode (nullptr), mSubModeId ("move"), mSelectionMode (nullptr), mDragMode (DragMode_None),
@ -146,7 +173,7 @@ void CSVRender::InstanceMode::activate (CSVWidget::SceneToolbar *toolbar)
}
if (!mSelectionMode)
mSelectionMode = new InstanceSelectionMode (toolbar, getWorldspaceWidget());
mSelectionMode = new InstanceSelectionMode (toolbar, getWorldspaceWidget(), mParentNode);
mDragMode = DragMode_None;
@ -322,6 +349,42 @@ bool CSVRender::InstanceMode::secondaryEditStartDrag (const QPoint& pos)
return false;
}
bool CSVRender::InstanceMode::primarySelectStartDrag (const QPoint& pos)
{
if (mDragMode!=DragMode_None || mLocked)
return false;
std::string primarySelectAction = CSMPrefs::get()["3D Scene Editing"]["primary-select-action"].toString();
if ( primarySelectAction == "Select only" ) mDragMode = DragMode_Select_Only;
else if ( primarySelectAction == "Add to selection" ) mDragMode = DragMode_Select_Add;
else if ( primarySelectAction == "Remove from selection" ) mDragMode = DragMode_Select_Remove;
else if ( primarySelectAction == "Invert selection" ) mDragMode = DragMode_Select_Invert;
WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask());
mSelectionMode->setDragStart(hit.worldPos);
return true;
}
bool CSVRender::InstanceMode::secondarySelectStartDrag (const QPoint& pos)
{
if (mDragMode!=DragMode_None || mLocked)
return false;
std::string secondarySelectAction = CSMPrefs::get()["3D Scene Editing"]["secondary-select-action"].toString();
if ( secondarySelectAction == "Select only" ) mDragMode = DragMode_Select_Only;
else if ( secondarySelectAction == "Add to selection" ) mDragMode = DragMode_Select_Add;
else if ( secondarySelectAction == "Remove from selection" ) mDragMode = DragMode_Select_Remove;
else if ( secondarySelectAction == "Invert selection" ) mDragMode = DragMode_Select_Invert;
WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask());
mSelectionMode->setDragStart(hit.worldPos);
return true;
}
void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor)
{
osg::Vec3f offset;
@ -432,6 +495,24 @@ void CSVRender::InstanceMode::drag (const QPoint& pos, int diffX, int diffY, dou
// Only uniform scaling is currently supported
offset = osg::Vec3f(scale, scale, scale);
}
else if (mSelectionMode->getCurrentId() == "cube-centre")
{
osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart()));
mSelectionMode->drawSelectionCubeCentre (mousePlanePoint);
return;
}
else if (mSelectionMode->getCurrentId() == "cube-corner")
{
osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart()));
mSelectionMode->drawSelectionCubeCorner (mousePlanePoint);
return;
}
else if (mSelectionMode->getCurrentId() == "sphere")
{
osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart()));
mSelectionMode->drawSelectionSphere (mousePlanePoint);
return;
}
// Apply
for (std::vector<osg::ref_ptr<TagBase> >::iterator iter (selection.begin()); iter!=selection.end(); ++iter)
@ -495,6 +576,22 @@ void CSVRender::InstanceMode::dragCompleted(const QPoint& pos)
case DragMode_Move: description = "Move Instances"; break;
case DragMode_Rotate: description = "Rotate Instances"; break;
case DragMode_Scale: description = "Scale Instances"; break;
case DragMode_Select_Only :
handleSelectDrag(pos);
return;
break;
case DragMode_Select_Add :
handleSelectDrag(pos);
return;
break;
case DragMode_Select_Remove :
handleSelectDrag(pos);
return;
break;
case DragMode_Select_Invert :
handleSelectDrag(pos);
return;
break;
case DragMode_None: break;
}
@ -680,6 +777,13 @@ void CSVRender::InstanceMode::subModeChanged (const std::string& id)
getWorldspaceWidget().setSubMode (getSubModeFromId (id), Mask_Reference);
}
void CSVRender::InstanceMode::handleSelectDrag(const QPoint& pos)
{
osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart()));
mSelectionMode->dragEnded (mousePlanePoint, mDragMode);
mDragMode = DragMode_None;
}
void CSVRender::InstanceMode::deleteSelectedInstances(bool active)
{
std::vector<osg::ref_ptr<TagBase> > selection = getWorldspaceWidget().getSelection (Mask_Reference);
@ -698,39 +802,15 @@ void CSVRender::InstanceMode::deleteSelectedInstances(bool active)
getWorldspaceWidget().clearSelection (Mask_Reference);
}
void CSVRender::InstanceMode::dropInstance(DropMode dropMode, CSVRender::Object* object, float objectHeight)
void CSVRender::InstanceMode::dropInstance(CSVRender::Object* object, float dropHeight)
{
osg::Vec3d point = object->getPosition().asVec3();
osg::Vec3d start = point;
start.z() += objectHeight;
osg::Vec3d end = point;
end.z() = std::numeric_limits<float>::lowest();
osg::ref_ptr<osgUtil::LineSegmentIntersector> intersector (new osgUtil::LineSegmentIntersector(
osgUtil::Intersector::MODEL, start, end) );
intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT);
osgUtil::IntersectionVisitor visitor(intersector);
if (dropMode == TerrainSep)
visitor.setTraversalMask(Mask_Terrain);
if (dropMode == CollisionSep)
visitor.setTraversalMask(Mask_Terrain | Mask_Reference);
mParentNode->accept(visitor);
osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin();
if (it != intersector->getIntersections().end())
{
osgUtil::LineSegmentIntersector::Intersection intersection = *it;
ESM::Position position = object->getPosition();
object->setEdited (Object::Override_Position);
position.pos[2] = intersection.getWorldIntersectPoint().z() + objectHeight;
object->setPosition(position.pos);
}
object->setEdited(Object::Override_Position);
ESM::Position position = object->getPosition();
position.pos[2] -= dropHeight;
object->setPosition(position.pos);
}
float CSVRender::InstanceMode::getDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight)
float CSVRender::InstanceMode::calculateDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight)
{
osg::Vec3d point = object->getPosition().asVec3();
@ -744,9 +824,9 @@ float CSVRender::InstanceMode::getDropHeight(DropMode dropMode, CSVRender::Objec
intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT);
osgUtil::IntersectionVisitor visitor(intersector);
if (dropMode == Terrain)
if (dropMode & Terrain)
visitor.setTraversalMask(Mask_Terrain);
if (dropMode == Collision)
if (dropMode & Collision)
visitor.setTraversalMask(Mask_Terrain | Mask_Reference);
mParentNode->accept(visitor);
@ -774,12 +854,12 @@ void CSVRender::InstanceMode::dropSelectedInstancesToTerrain()
void CSVRender::InstanceMode::dropSelectedInstancesToCollisionSeparately()
{
handleDropMethod(TerrainSep, "Drop instances to next collision level separately");
handleDropMethod(CollisionSep, "Drop instances to next collision level separately");
}
void CSVRender::InstanceMode::dropSelectedInstancesToTerrainSeparately()
{
handleDropMethod(CollisionSep, "Drop instances to terrain level separately");
handleDropMethod(TerrainSep, "Drop instances to terrain level separately");
}
void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString commandMsg)
@ -793,52 +873,44 @@ void CSVRender::InstanceMode::handleDropMethod(DropMode dropMode, QString comman
CSMWorld::CommandMacro macro (undoStack, commandMsg);
DropObjectDataHandler dropObjectDataHandler(&getWorldspaceWidget());
DropObjectHeightHandler dropObjectDataHandler(&getWorldspaceWidget());
switch (dropMode)
if(dropMode & Separate)
{
case Terrain:
case Collision:
{
float smallestDropHeight = std::numeric_limits<float>::max();
int counter = 0;
for(osg::ref_ptr<TagBase> tag: selection)
if (CSVRender::ObjectTag *objectTag = dynamic_cast<CSVRender::ObjectTag *> (tag.get()))
{
float thisDrop = getDropHeight(dropMode, objectTag->mObject, dropObjectDataHandler.mObjectHeights[counter]);
if (thisDrop < smallestDropHeight)
smallestDropHeight = thisDrop;
counter++;
}
for(osg::ref_ptr<TagBase> tag: selection)
if (CSVRender::ObjectTag *objectTag = dynamic_cast<CSVRender::ObjectTag *> (tag.get()))
{
objectTag->mObject->setEdited (Object::Override_Position);
ESM::Position position = objectTag->mObject->getPosition();
position.pos[2] -= smallestDropHeight;
objectTag->mObject->setPosition(position.pos);
objectTag->mObject->apply (macro);
}
}
break;
case TerrainSep:
case CollisionSep:
{
int counter = 0;
for(osg::ref_ptr<TagBase> tag: selection)
if (CSVRender::ObjectTag *objectTag = dynamic_cast<CSVRender::ObjectTag *> (tag.get()))
{
dropInstance(dropMode, objectTag->mObject, dropObjectDataHandler.mObjectHeights[counter]);
objectTag->mObject->apply (macro);
counter++;
}
}
break;
int counter = 0;
for (osg::ref_ptr<TagBase> tag : selection)
if (CSVRender::ObjectTag* objectTag = dynamic_cast<CSVRender::ObjectTag*>(tag.get()))
{
float objectHeight = dropObjectDataHandler.mObjectHeights[counter];
float dropHeight = calculateDropHeight(dropMode, objectTag->mObject, objectHeight);
dropInstance(objectTag->mObject, dropHeight);
objectTag->mObject->apply(macro);
counter++;
}
}
else
{
float smallestDropHeight = std::numeric_limits<float>::max();
int counter = 0;
for (osg::ref_ptr<TagBase> tag : selection)
if (CSVRender::ObjectTag* objectTag = dynamic_cast<CSVRender::ObjectTag*>(tag.get()))
{
float objectHeight = dropObjectDataHandler.mObjectHeights[counter];
float thisDrop = calculateDropHeight(dropMode, objectTag->mObject, objectHeight);
if (thisDrop < smallestDropHeight)
smallestDropHeight = thisDrop;
counter++;
}
for (osg::ref_ptr<TagBase> tag : selection)
if (CSVRender::ObjectTag* objectTag = dynamic_cast<CSVRender::ObjectTag*>(tag.get()))
{
dropInstance(objectTag->mObject, smallestDropHeight);
objectTag->mObject->apply(macro);
}
}
}
CSVRender::DropObjectDataHandler::DropObjectDataHandler(WorldspaceWidget* worldspacewidget)
CSVRender::DropObjectHeightHandler::DropObjectHeightHandler(WorldspaceWidget* worldspacewidget)
: mWorldspaceWidget(worldspacewidget)
{
std::vector<osg::ref_ptr<TagBase> > selection = mWorldspaceWidget->getSelection (Mask_Reference);
@ -865,7 +937,7 @@ CSVRender::DropObjectDataHandler::DropObjectDataHandler(WorldspaceWidget* worlds
}
}
CSVRender::DropObjectDataHandler::~DropObjectDataHandler()
CSVRender::DropObjectHeightHandler::~DropObjectHeightHandler()
{
std::vector<osg::ref_ptr<TagBase> > selection = mWorldspaceWidget->getSelection (Mask_Reference);
int counter = 0;

@ -9,6 +9,7 @@
#include <osg/Vec3f>
#include "editmode.hpp"
#include "instancedragmodes.hpp"
namespace CSVWidget
{
@ -25,20 +26,15 @@ namespace CSVRender
{
Q_OBJECT
enum DragMode
{
DragMode_None,
DragMode_Move,
DragMode_Rotate,
DragMode_Scale
};
enum DropMode
{
Collision,
Terrain,
CollisionSep,
TerrainSep
Separate = 0b1,
Collision = 0b10,
Terrain = 0b100,
CollisionSep = Collision | Separate,
TerrainSep = Terrain | Separate,
};
CSVWidget::SceneToolMode *mSubMode;
@ -57,8 +53,11 @@ namespace CSVRender
osg::Vec3f getSelectionCenter(const std::vector<osg::ref_ptr<TagBase> >& selection) const;
osg::Vec3f getScreenCoords(const osg::Vec3f& pos);
void dropInstance(DropMode dropMode, CSVRender::Object* object, float objectHeight);
float getDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight);
osg::Vec3f getProjectionSpaceCoords(const osg::Vec3f& pos);
osg::Vec3f getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart);
void handleSelectDrag(const QPoint& pos);
void dropInstance(CSVRender::Object* object, float dropHeight);
float calculateDropHeight(DropMode dropMode, CSVRender::Object* object, float objectHeight);
public:
@ -84,6 +83,10 @@ namespace CSVRender
bool secondaryEditStartDrag (const QPoint& pos) override;
bool primarySelectStartDrag(const QPoint& pos) override;
bool secondarySelectStartDrag(const QPoint& pos) override;
void drag (const QPoint& pos, int diffX, int diffY, double speedFactor) override;
void dragCompleted(const QPoint& pos) override;
@ -116,11 +119,11 @@ namespace CSVRender
};
/// \brief Helper class to handle object mask data in safe way
class DropObjectDataHandler
class DropObjectHeightHandler
{
public:
DropObjectDataHandler(WorldspaceWidget* worldspacewidget);
~DropObjectDataHandler();
DropObjectHeightHandler(WorldspaceWidget* worldspacewidget);
~DropObjectHeightHandler();
std::vector<float> mObjectHeights;
private:

@ -2,17 +2,24 @@
#include <QMenu>
#include <QAction>
#include <QPoint>
#include <osg/Group>
#include <osg/PositionAttitudeTransform>
#include <osg/ref_ptr>
#include <osg/Vec3d>
#include "../../model/world/idtable.hpp"
#include "../../model/world/commands.hpp"
#include "instancedragmodes.hpp"
#include "worldspacewidget.hpp"
#include "object.hpp"
namespace CSVRender
{
InstanceSelectionMode::InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget)
: SelectionMode(parent, worldspaceWidget, Mask_Reference)
InstanceSelectionMode::InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group *cellNode)
: SelectionMode(parent, worldspaceWidget, Mask_Reference), mParentNode(cellNode)
{
mSelectSame = new QAction("Extend selection to instances with same object ID", this);
mDeleteSelection = new QAction("Delete selected instances", this);
@ -21,6 +28,342 @@ namespace CSVRender
connect(mDeleteSelection, SIGNAL(triggered()), this, SLOT(deleteSelection()));
}
InstanceSelectionMode::~InstanceSelectionMode()
{
mParentNode->removeChild(mBaseNode);
}
void InstanceSelectionMode::setDragStart(const osg::Vec3d& dragStart)
{
mDragStart = dragStart;
}
const osg::Vec3d& InstanceSelectionMode::getDragStart()
{
return mDragStart;
}
void InstanceSelectionMode::dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode)
{
float dragDistance = (mDragStart - dragEndPoint).length();
if (mBaseNode) mParentNode->removeChild (mBaseNode);
if (getCurrentId() == "cube-centre")
{
osg::Vec3d pointA(mDragStart[0] - dragDistance, mDragStart[1] - dragDistance, mDragStart[2] - dragDistance);
osg::Vec3d pointB(mDragStart[0] + dragDistance, mDragStart[1] + dragDistance, mDragStart[2] + dragDistance);
getWorldspaceWidget().selectInsideCube(pointA, pointB, dragMode);
}
else if (getCurrentId() == "cube-corner")
{
getWorldspaceWidget().selectInsideCube(mDragStart, dragEndPoint, dragMode);
}
else if (getCurrentId() == "sphere")
{
getWorldspaceWidget().selectWithinDistance(mDragStart, dragDistance, dragMode);
}
}
void InstanceSelectionMode::drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint)
{
float dragDistance = (mDragStart - mousePlanePoint).length();
drawSelectionCube(mDragStart, dragDistance);
}
void InstanceSelectionMode::drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint)
{
drawSelectionBox(mDragStart, mousePlanePoint);
}
void InstanceSelectionMode::drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB)
{
if (mBaseNode) mParentNode->removeChild (mBaseNode);
mBaseNode = new osg::PositionAttitudeTransform;
mBaseNode->setPosition(pointA);
osg::ref_ptr<osg::Geometry> geometry (new osg::Geometry);
osg::Vec3Array *vertices = new osg::Vec3Array;
vertices->push_back (osg::Vec3f (0.0f, 0.0f, 0.0f));
vertices->push_back (osg::Vec3f (0.0f, 0.0f, pointB[2] - pointA[2]));
vertices->push_back (osg::Vec3f (0.0f, pointB[1] - pointA[1], 0.0f));
vertices->push_back (osg::Vec3f (0.0f, pointB[1] - pointA[1], pointB[2] - pointA[2]));
vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], 0.0f, 0.0f));
vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], 0.0f, pointB[2] - pointA[2]));
vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], pointB[1] - pointA[1], 0.0f));
vertices->push_back (osg::Vec3f (pointB[0] - pointA[0], pointB[1] - pointA[1], pointB[2] - pointA[2]));
geometry->setVertexArray (vertices);
osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0);
// top
primitives->push_back (2);
primitives->push_back (1);
primitives->push_back (0);
primitives->push_back (3);
primitives->push_back (1);
primitives->push_back (2);
// bottom
primitives->push_back (4);
primitives->push_back (5);
primitives->push_back (6);
primitives->push_back (6);
primitives->push_back (5);
primitives->push_back (7);
// sides
primitives->push_back (1);
primitives->push_back (4);
primitives->push_back (0);
primitives->push_back (4);
primitives->push_back (1);
primitives->push_back (5);
primitives->push_back (4);
primitives->push_back (2);
primitives->push_back (0);
primitives->push_back (6);
primitives->push_back (2);
primitives->push_back (4);
primitives->push_back (6);
primitives->push_back (3);
primitives->push_back (2);
primitives->push_back (7);
primitives->push_back (3);
primitives->push_back (6);
primitives->push_back (1);
primitives->push_back (3);
primitives->push_back (5);
primitives->push_back (5);
primitives->push_back (3);
primitives->push_back (7);
geometry->addPrimitiveSet (primitives);
osg::Vec4Array *colours = new osg::Vec4Array;
colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.5f, 0.2f));
colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f));
colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f));
colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f));
colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f));
colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f));
colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f));
colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f));
geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX);
geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF);
geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON);
geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
mBaseNode->addChild (geometry);
mParentNode->addChild(mBaseNode);
}
void InstanceSelectionMode::drawSelectionCube(const osg::Vec3d& point, float radius)
{
if (mBaseNode) mParentNode->removeChild (mBaseNode);
mBaseNode = new osg::PositionAttitudeTransform;
mBaseNode->setPosition(point);
osg::ref_ptr<osg::Geometry> geometry (new osg::Geometry);
osg::Vec3Array *vertices = new osg::Vec3Array;
for (int i = 0; i < 2; ++i)
{
float height = i ? -radius : radius;
vertices->push_back (osg::Vec3f (height, -radius, -radius));
vertices->push_back (osg::Vec3f (height, -radius, radius));
vertices->push_back (osg::Vec3f (height, radius, -radius));
vertices->push_back (osg::Vec3f (height, radius, radius));
}
geometry->setVertexArray (vertices);
osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLES, 0);
// top
primitives->push_back (2);
primitives->push_back (1);
primitives->push_back (0);
primitives->push_back (3);
primitives->push_back (1);
primitives->push_back (2);
// bottom
primitives->push_back (4);
primitives->push_back (5);
primitives->push_back (6);
primitives->push_back (6);
primitives->push_back (5);
primitives->push_back (7);
// sides
primitives->push_back (1);
primitives->push_back (4);
primitives->push_back (0);
primitives->push_back (4);
primitives->push_back (1);
primitives->push_back (5);
primitives->push_back (4);
primitives->push_back (2);
primitives->push_back (0);
primitives->push_back (6);
primitives->push_back (2);
primitives->push_back (4);
primitives->push_back (6);
primitives->push_back (3);
primitives->push_back (2);
primitives->push_back (7);
primitives->push_back (3);
primitives->push_back (6);
primitives->push_back (1);
primitives->push_back (3);
primitives->push_back (5);
primitives->push_back (5);
primitives->push_back (3);
primitives->push_back (7);
geometry->addPrimitiveSet (primitives);
osg::Vec4Array *colours = new osg::Vec4Array;
colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.5f, 0.2f));
colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f));
colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f));
colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f));
colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f));
colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f));
colours->push_back (osg::Vec4f (0.3f, 0.3f, 0.4f, 0.2f));
colours->push_back (osg::Vec4f (0.9f, 0.9f, 1.0f, 0.2f));
geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX);
geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF);
geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON);
geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
mBaseNode->addChild (geometry);
mParentNode->addChild(mBaseNode);
}
void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3f& mousePlanePoint)
{
float dragDistance = (mDragStart - mousePlanePoint).length();
drawSelectionSphere(mDragStart, dragDistance);
}
void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3d& point, float radius)
{
if (mBaseNode) mParentNode->removeChild (mBaseNode);
mBaseNode = new osg::PositionAttitudeTransform;
mBaseNode->setPosition(point);
osg::ref_ptr<osg::Geometry> geometry (new osg::Geometry);
osg::Vec3Array *vertices = new osg::Vec3Array;
int resolution = 32;
float radiusPerResolution = radius / resolution;
float reciprocalResolution = 1.0f / resolution;
float doubleReciprocalRes = reciprocalResolution * 2;
osg::Vec4Array *colours = new osg::Vec4Array;
for (float i = 0.0; i <= resolution; i += 2)
{
float iShifted = (static_cast<float>(i) - resolution / 2.0f); // i - 16 = -16 ... 16
float xPercentile = iShifted * doubleReciprocalRes;
float x = xPercentile * radius;
float thisRadius = sqrt (radius * radius - x * x);
//the next row
float iShifted2 = (static_cast<float>(i + 1) - resolution / 2.0f);
float xPercentile2 = iShifted2 * doubleReciprocalRes;
float x2 = xPercentile2 * radius;
float thisRadius2 = sqrt (radius * radius - x2 * x2);
for (int j = 0; j < resolution; ++j)
{
float vertexX = thisRadius * sin(j * reciprocalResolution * osg::PI * 2);
float vertexY = i * radiusPerResolution * 2 - radius;
float vertexZ = thisRadius * cos(j * reciprocalResolution * osg::PI * 2);
float heightPercentage = (vertexZ + radius) / (radius * 2);
vertices->push_back (osg::Vec3f (vertexX, vertexY, vertexZ));
colours->push_back (osg::Vec4f (heightPercentage, heightPercentage, heightPercentage, 0.3f));
float vertexNextRowX = thisRadius2 * sin(j * reciprocalResolution * osg::PI * 2);
float vertexNextRowY = (i + 1) * radiusPerResolution * 2 - radius;
float vertexNextRowZ = thisRadius2 * cos(j * reciprocalResolution * osg::PI * 2);
float heightPercentageNextRow = (vertexZ + radius) / (radius * 2);
vertices->push_back (osg::Vec3f (vertexNextRowX, vertexNextRowY, vertexNextRowZ));
colours->push_back (osg::Vec4f (heightPercentageNextRow, heightPercentageNextRow, heightPercentageNextRow, 0.3f));
}
}
geometry->setVertexArray (vertices);
osg::DrawElementsUShort *primitives = new osg::DrawElementsUShort (osg::PrimitiveSet::TRIANGLE_STRIP, 0);
for (int i = 0; i < resolution; ++i)
{
//Even
for (int j = 0; j < resolution * 2; ++j)
{
if (i * resolution * 2 + j > static_cast<int>(vertices->size()) - 1) continue;
primitives->push_back (i * resolution * 2 + j);
}
if (i * resolution * 2 > static_cast<int>(vertices->size()) - 1) continue;
primitives->push_back (i * resolution * 2);
primitives->push_back (i * resolution * 2 + 1);
//Odd
for (int j = 1; j < resolution * 2 - 2; j += 2)
{
if ((i + 1) * resolution * 2 + j - 1 > static_cast<int>(vertices->size()) - 1) continue;
primitives->push_back ((i + 1) * resolution * 2 + j - 1);
primitives->push_back (i * resolution * 2 + j + 2);
}
if ((i + 2) * resolution * 2 - 2 > static_cast<int>(vertices->size()) - 1) continue;
primitives->push_back ((i + 2) * resolution * 2 - 2);
primitives->push_back (i * resolution * 2 + 1);
primitives->push_back ((i + 1) * resolution * 2);
}
geometry->addPrimitiveSet (primitives);
geometry->setColorArray (colours, osg::Array::BIND_PER_VERTEX);
geometry->getOrCreateStateSet()->setMode (GL_LIGHTING, osg::StateAttribute::OFF);
geometry->getOrCreateStateSet()->setMode (GL_BLEND, osg::StateAttribute::ON);
geometry->getOrCreateStateSet()->setMode(GL_CULL_FACE, osg::StateAttribute::OFF);
geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
mBaseNode->addChild (geometry);
mParentNode->addChild(mBaseNode);
}
bool InstanceSelectionMode::createContextMenu(QMenu* menu)
{
if (menu)

@ -1,7 +1,13 @@
#ifndef CSV_RENDER_INSTANCE_SELECTION_MODE_H
#define CSV_RENDER_INSTANCE_SELECTION_MODE_H
#include <QPoint>
#include <osg/PositionAttitudeTransform>
#include <osg/Vec3d>
#include "selectionmode.hpp"
#include "instancedragmodes.hpp"
namespace CSVRender
{
@ -11,8 +17,25 @@ namespace CSVRender
public:
InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget);
InstanceSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget, osg::Group *cellNode);
~InstanceSelectionMode();
/// Store the worldspace-coordinate when drag begins
void setDragStart(const osg::Vec3d& dragStart);
/// Store the worldspace-coordinate when drag begins
const osg::Vec3d& getDragStart();
/// Store the screen-coordinate when drag begins
void setScreenDragStart(const QPoint& dragStartPoint);
/// Apply instance selection changes
void dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode);
void drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint );
void drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint );
void drawSelectionSphere(const osg::Vec3f& mousePlanePoint );
protected:
/// Add context menu items to \a menu.
@ -25,8 +48,15 @@ namespace CSVRender
private:
void drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB);
void drawSelectionCube(const osg::Vec3d& point, float radius);
void drawSelectionSphere(const osg::Vec3d& point, float radius);
QAction* mDeleteSelection;
QAction* mSelectSame;
osg::Vec3d mDragStart;
osg::Group* mParentNode;
osg::ref_ptr<osg::PositionAttitudeTransform> mBaseNode;
private slots:

@ -768,6 +768,22 @@ void CSVRender::PagedWorldspaceWidget::selectAllWithSameParentId (int elementMas
flagAsModified();
}
void CSVRender::PagedWorldspaceWidget::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode)
{
for (auto& cell : mCells)
{
cell.second->selectInsideCube (pointA, pointB, dragMode);
}
}
void CSVRender::PagedWorldspaceWidget::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode)
{
for (auto& cell : mCells)
{
cell.second->selectWithinDistance (point, distance, dragMode);
}
}
std::string CSVRender::PagedWorldspaceWidget::getCellId (const osg::Vec3f& point) const
{
CSMWorld::CellCoordinates cellCoordinates (

@ -7,6 +7,7 @@
#include "worldspacewidget.hpp"
#include "cell.hpp"
#include "instancedragmodes.hpp"
namespace CSVWidget
{
@ -120,6 +121,10 @@ namespace CSVRender
/// \param elementMask Elements to be affected by the select operation
void selectAllWithSameParentId (int elementMask) override;
void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override;
void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override;
std::string getCellId (const osg::Vec3f& point) const override;
Cell* getCell(const osg::Vec3d& point) const override;

@ -15,30 +15,27 @@ namespace CSVRender
{
addButton(":scenetoolbar/selection-mode-cube", "cube-centre",
"Centred cube"
"<ul><li>Drag with {scene-select-primary} (make instances the selection) or {scene-select-secondary} "
"(invert selection state) from the centre of the selection cube outwards</li>"
"<ul><li>Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select "
"from the centre of the selection cube outwards.</li>"
"<li>The selection cube is aligned to the word space axis</li>"
"<li>If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not "
"starting on an instance will have the same effect</li>"
"</ul>"
"<font color=Red>Not implemented yet</font color>");
"</ul>");
addButton(":scenetoolbar/selection-mode-cube-corner", "cube-corner",
"Cube corner to corner"
"<ul><li>Drag with {scene-select-primary} (make instances the selection) or {scene-select-secondary} "
"(invert selection state) from one corner of the selection cube to the opposite corner</li>"
"<ul><li>Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select "
"from one corner of the selection cube to the opposite corner</li>"
"<li>The selection cube is aligned to the word space axis</li>"
"<li>If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not "
"starting on an instance will have the same effect</li>"
"</ul>"
"<font color=Red>Not implemented yet</font color>");
"</ul>");
addButton(":scenetoolbar/selection-mode-cube-sphere", "sphere",
"Centred sphere"
"<ul><li>Drag with {scene-select-primary} (make instances the selection) or {scene-select-secondary} "
"(invert selection state) from the centre of the selection sphere outwards</li>"
"<ul><li>Drag with {scene-select-primary} for primary select or {scene-select-secondary} for secondary select "
"from the centre of the selection sphere outwards</li>"
"<li>If context selection mode is enabled, a drag with {scene-edit-primary} or {scene-edit-secondary} not "
"starting on an instance will have the same effect</li>"
"</ul>"
"<font color=Red>Not implemented yet</font color>");
"</ul>");
mSelectAll = new QAction("Select all", this);
mDeselectAll = new QAction("Clear selection", this);

@ -427,14 +427,8 @@ void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitRe
{
if (i_cell == cellX && j_cell == cellY && abs(i-xHitInCell) < r && abs(j-yHitInCell) < r)
{
int distanceX(0);
int distanceY(0);
if (i_cell < cellX) distanceX = xHitInCell + landTextureSize * abs(i_cell-cellX) - i;
if (j_cell < cellY) distanceY = yHitInCell + landTextureSize * abs(j_cell-cellY) - j;
if (i_cell > cellX) distanceX = -xHitInCell + landTextureSize* abs(i_cell-cellX) + i;
if (j_cell > cellY) distanceY = -yHitInCell + landTextureSize * abs(j_cell-cellY) + j;
if (i_cell == cellX) distanceX = abs(i-xHitInCell);
if (j_cell == cellY) distanceY = abs(j-yHitInCell);
int distanceX = abs(i-xHitInCell);
int distanceY = abs(j-yHitInCell);
float distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2)));
float rf = static_cast<float>(mBrushSize) / 2;
if (distance < rf) newTerrain[j*landTextureSize+i] = brushInt;

@ -140,6 +140,16 @@ void CSVRender::UnpagedWorldspaceWidget::selectAllWithSameParentId (int elementM
flagAsModified();
}
void CSVRender::UnpagedWorldspaceWidget::selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode)
{
mCell->selectInsideCube (pointA, pointB, dragMode);
}
void CSVRender::UnpagedWorldspaceWidget::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode)
{
mCell->selectWithinDistance (point, distance, dragMode);
}
std::string CSVRender::UnpagedWorldspaceWidget::getCellId (const osg::Vec3f& point) const
{
return mCellId;

@ -60,6 +60,10 @@ namespace CSVRender
/// \param elementMask Elements to be affected by the select operation
void selectAllWithSameParentId (int elementMask) override;
void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override;
void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override;
std::string getCellId (const osg::Vec3f& point) const override;
Cell* getCell(const osg::Vec3d& point) const override;

@ -7,6 +7,7 @@
#include "../../model/doc/document.hpp"
#include "../../model/world/tablemimedata.hpp"
#include "instancedragmodes.hpp"
#include "scenewidget.hpp"
#include "mask.hpp"
@ -160,6 +161,10 @@ namespace CSVRender
/// \param elementMask Elements to be affected by the select operation
virtual void selectAllWithSameParentId (int elementMask) = 0;
virtual void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) = 0;
virtual void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) = 0;
/// Return the next intersection with scene elements matched by
/// \a interactionMask based on \a localPos and the camera vector.
/// If there is no such intersection, instead a point "in front" of \a localPos will be

@ -15,12 +15,21 @@ bool CSVWorld::DragDropUtils::canAcceptData(const QDropEvent &event, CSMWorld::C
return data != nullptr && data->holdsType(type);
}
CSMWorld::UniversalId CSVWorld::DragDropUtils::getAcceptedData(const QDropEvent &event,
bool CSVWorld::DragDropUtils::isInfo(const QDropEvent &event, CSMWorld::ColumnBase::Display type)
{
const CSMWorld::TableMimeData *data = getTableMimeData(event);
return data != nullptr && (
data->holdsType(CSMWorld::UniversalId::Type_TopicInfo) ||
data->holdsType(CSMWorld::UniversalId::Type_JournalInfo) );
}
CSMWorld::UniversalId CSVWorld::DragDropUtils::getAcceptedData(const QDropEvent &event,
CSMWorld::ColumnBase::Display type)
{
if (canAcceptData(event, type))
{
return getTableMimeData(event)->returnMatching(type);
if (const CSMWorld::TableMimeData *data = getTableMimeData(event))
return data->returnMatching(type);
}
return CSMWorld::UniversalId::Type_None;
}

@ -20,6 +20,9 @@ namespace CSVWorld
bool canAcceptData(const QDropEvent &event, CSMWorld::ColumnBase::Display type);
///< Checks whether the \a event contains a valid CSMWorld::TableMimeData that holds the \a type
bool isInfo(const QDropEvent &event, CSMWorld::ColumnBase::Display type);
///< Info types can be dragged to sort the info table
CSMWorld::UniversalId getAcceptedData(const QDropEvent &event, CSMWorld::ColumnBase::Display type);
///< Gets the accepted data from the \a event using the \a type
///< \return Type_None if the \a event data doesn't holds the \a type

@ -19,14 +19,10 @@ void CSVWorld::DragRecordTable::startDragFromTable (const CSVWorld::DragRecordTa
}
CSMWorld::TableMimeData* mime = new CSMWorld::TableMimeData (records, mDocument);
if (mime)
{
QDrag* drag = new QDrag (this);
drag->setMimeData (mime);
drag->setPixmap (QString::fromUtf8 (mime->getIcon().c_str()));
drag->exec (Qt::CopyAction);
}
QDrag* drag = new QDrag (this);
drag->setMimeData (mime);
drag->setPixmap (QString::fromUtf8 (mime->getIcon().c_str()));
drag->exec (Qt::CopyAction);
}
CSVWorld::DragRecordTable::DragRecordTable (CSMDoc::Document& document, QWidget* parent) :
@ -50,7 +46,8 @@ void CSVWorld::DragRecordTable::dragEnterEvent(QDragEnterEvent *event)
void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent *event)
{
QModelIndex index = indexAt(event->pos());
if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)))
if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)) ||
CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index)) )
{
if (index.flags() & Qt::ItemIsEditable)
{
@ -79,6 +76,10 @@ void CSVWorld::DragRecordTable::dropEvent(QDropEvent *event)
}
}
}
else if (CSVWorld::DragDropUtils::isInfo(*event, display) && event->source() == this)
{
emit moveRecordsFromSameTable(event);
}
}
CSMWorld::ColumnBase::Display CSVWorld::DragRecordTable::getIndexDisplayType(const QModelIndex &index) const

@ -23,6 +23,8 @@ namespace CSVWorld
{
class DragRecordTable : public QTableView
{
Q_OBJECT
protected:
CSMDoc::Document& mDocument;
bool mEditLock;
@ -45,6 +47,9 @@ namespace CSVWorld
private:
CSMWorld::ColumnBase::Display getIndexDisplayType(const QModelIndex &index) const;
signals:
void moveRecordsFromSameTable(QDropEvent *event);
};
}

@ -34,16 +34,29 @@ void CSVWorld::InfoCreator::configureCreateCommand (CSMWorld::CreateCommand& com
{
CSMWorld::IdTable& table = dynamic_cast<CSMWorld::IdTable&> (*getData().getTableModel (getCollectionId()));
CSMWorld::CloneCommand* cloneCommand = dynamic_cast<CSMWorld::CloneCommand*> (&command);
if (getCollectionId() == CSMWorld::UniversalId::Type_TopicInfos)
{
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text());
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Rank), -1);
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Gender), -1);
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_PcRank), -1);
if (!cloneCommand)
{
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text());
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Rank), -1);
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Gender), -1);
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_PcRank), -1);
}
else
{
cloneCommand->setOverrideValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text());
}
}
else
{
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text());
if (!cloneCommand)
{
command.addValue (table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text());
}
else
cloneCommand->setOverrideValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Journal), mTopic->text());
}
}

@ -75,10 +75,10 @@ void CSVWorld::addSubViewFactories (CSVDoc::SubViewFactoryManager& manager)
new CSVDoc::SubViewFactoryWithCreator<TableSubView, JournalCreatorFactory>);
manager.add (CSMWorld::UniversalId::Type_TopicInfos,
new CSVDoc::SubViewFactoryWithCreator<TableSubView, InfoCreatorFactory>);
new CSVDoc::SubViewFactoryWithCreator<TableSubView, InfoCreatorFactory>(false));
manager.add (CSMWorld::UniversalId::Type_JournalInfos,
new CSVDoc::SubViewFactoryWithCreator<TableSubView, InfoCreatorFactory>);
new CSVDoc::SubViewFactoryWithCreator<TableSubView, InfoCreatorFactory>(false));
manager.add (CSMWorld::UniversalId::Type_Pathgrids,
new CSVDoc::SubViewFactoryWithCreator<TableSubView, PathgridCreatorFactory>);

@ -7,6 +7,7 @@
#include <QString>
#include <QtCore/qnamespace.h>
#include <components/debug/debuglog.hpp>
#include <components/misc/helpviewer.hpp>
#include <components/misc/stringops.hpp>
@ -248,6 +249,7 @@ CSVWorld::Table::Table (const CSMWorld::UniversalId& id,
if (isInfoTable)
{
mProxyModel = new CSMWorld::InfoTableProxyModel(id.getType(), this);
connect (this, &CSVWorld::DragRecordTable::moveRecordsFromSameTable, this, &CSVWorld::Table::moveRecords);
}
else if (isLtexTable)
{
@ -563,6 +565,77 @@ void CSVWorld::Table::moveDownRecord()
}
}
void CSVWorld::Table::moveRecords(QDropEvent *event)
{
if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant))
return;
QModelIndex targedIndex = indexAt(event->pos());
QModelIndexList selectedRows = selectionModel()->selectedRows();
int targetRowRaw = targedIndex.row();
int targetRow = mProxyModel->mapToSource (mProxyModel->index (targetRowRaw, 0)).row();
int baseRowRaw = targedIndex.row() - 1;
int baseRow = mProxyModel->mapToSource (mProxyModel->index (baseRowRaw, 0)).row();
int highestDifference = 0;
for (const auto& thisRowData : selectedRows)
{
int thisRow = mProxyModel->mapToSource (mProxyModel->index (thisRowData.row(), 0)).row();
if (std::abs(targetRow - thisRow) > highestDifference) highestDifference = std::abs(targetRow - thisRow);
if (thisRow - 1 < baseRow) baseRow = thisRow - 1;
}
std::vector<int> newOrder (highestDifference + 1);
for (long unsigned int i = 0; i < newOrder.size(); ++i)
{
newOrder[i] = i;
}
if (selectedRows.size() > 1)
{
Log(Debug::Warning) << "Move operation failed: Moving multiple selections isn't implemented.";
return;
}
for (const auto& thisRowData : selectedRows)
{
/*
Moving algorithm description
a) Remove the (ORIGIN + 1)th list member.
b) Add (ORIGIN+1)th list member with value TARGET
c) If ORIGIN > TARGET,d_INC; ELSE d_DEC
d_INC) increase all members after (and including) the TARGET by one, stop before hitting ORIGINth address
d_DEC) decrease all members after the ORIGIN by one, stop after hitting address TARGET
*/
int originRowRaw = thisRowData.row();
int originRow = mProxyModel->mapToSource (mProxyModel->index (originRowRaw, 0)).row();
newOrder.erase(newOrder.begin() + originRow - baseRow - 1);
newOrder.emplace(newOrder.begin() + originRow - baseRow - 1, targetRow - baseRow - 1);
if (originRow > targetRow)
{
for (int i = targetRow - baseRow - 1; i < originRow - baseRow - 1; ++i)
{
++newOrder[i];
}
}
else
{
for (int i = originRow - baseRow; i <= targetRow - baseRow - 1; ++i)
{
--newOrder[i];
}
}
}
mDocument.getUndoStack().push (new CSMWorld::ReorderRowsCommand (
dynamic_cast<CSMWorld::IdTable&> (*mModel), baseRow + 1, newOrder));
}
void CSVWorld::Table::editCell()
{
emit editRequest(mEditIdAction->getCurrentId(), "");

@ -141,6 +141,8 @@ namespace CSVWorld
void moveDownRecord();
void moveRecords(QDropEvent *event);
void viewRecord();
void previewRecord();

@ -261,16 +261,10 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO
return dsb;
}
/// \todo implement size limit. QPlainTextEdit does not support a size limit.
case CSMWorld::ColumnBase::Display_LongString:
{
QPlainTextEdit *edit = new QPlainTextEdit(parent);
edit->setUndoRedoEnabled (false);
return edit;
}
case CSMWorld::ColumnBase::Display_LongString256:
{
/// \todo implement size limit. QPlainTextEdit does not support a size limit.
QPlainTextEdit *edit = new QPlainTextEdit(parent);
edit->setUndoRedoEnabled (false);
return edit;

@ -19,9 +19,9 @@ source_group(game FILES ${GAME} ${GAME_HEADER})
add_openmw_dir (mwrender
actors objects renderingmanager animation rotatecontroller sky npcanimation vismask
creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation
creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager
bulletdebugdraw globalmap characterpreview camera viewovershoulder localmap water terrainstorage ripplesimulation
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging
renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover
)
add_openmw_dir (mwinput
@ -73,7 +73,7 @@ add_openmw_dir (mwworld
add_openmw_dir (mwphysics
physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback
contacttestresultcallback deepestnotmecontacttestresultcallback stepper movementsolver projectile
closestnotmeconvexresultcallback raycasting mtphysics
actorconvexcallback raycasting mtphysics contacttestwrapper projectileconvexcallback
)
add_openmw_dir (mwclass

@ -177,6 +177,8 @@ namespace
~ScopedProfile()
{
if (!mStats.collectStats("engine"))
return;
const osg::Timer_t end = mTimer.tick();
const UserStats& stats = UserStatsValue<sType>::sValue;
@ -460,6 +462,11 @@ void OMW::Engine::addContentFile(const std::string& file)
mContentFiles.push_back(file);
}
void OMW::Engine::addGroundcoverFile(const std::string& file)
{
mGroundcoverFiles.emplace_back(file);
}
void OMW::Engine::setSkipMenu (bool skipMenu, bool newGame)
{
mSkipMenu = skipMenu;
@ -589,8 +596,8 @@ void OMW::Engine::createWindow(Settings::Manager& settings)
Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->green << " bit green channel.";
if (traits->blue < 8)
Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->blue << " bit blue channel.";
if (traits->depth < 8)
Log(Debug::Warning) << "Warning: Framebuffer only has " << traits->red << " bits of depth precision.";
if (traits->depth < 24)
Log(Debug::Warning) << "Warning: Framebuffer only has " << traits->depth << " bits of depth precision.";
traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel
}
@ -721,7 +728,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
// Create the world
mEnvironment.setWorld( new MWWorld::World (mViewer, rootNode, mResourceSystem.get(), mWorkQueue.get(),
mFileCollections, mContentFiles, mEncoder, mActivationDistanceOverride, mCellName,
mFileCollections, mContentFiles, mGroundcoverFiles, mEncoder, mActivationDistanceOverride, mCellName,
mStartupScript, mResDir.string(), mCfgMgr.getUserDataPath().string()));
mEnvironment.getWorld()->setupPlayer();
@ -749,6 +756,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
// Create dialog system
mEnvironment.setJournal (new MWDialogue::Journal);
mEnvironment.setDialogueManager (new MWDialogue::DialogueManager (mExtensions, mTranslationDataStorage));
mEnvironment.setResourceSystem(mResourceSystem.get());
// scripts
if (mCompileAll)
@ -862,16 +870,29 @@ void OMW::Engine::go()
prepareEngine (settings);
std::ofstream stats;
if (const auto path = std::getenv("OPENMW_OSG_STATS_FILE"))
{
stats.open(path, std::ios_base::out);
if (stats.is_open())
Log(Debug::Info) << "Stats will be written to: " << path;
else
Log(Debug::Warning) << "Failed to open file for stats: " << path;
}
// Setup profiler
osg::ref_ptr<Resource::Profiler> statshandler = new Resource::Profiler;
osg::ref_ptr<Resource::Profiler> statshandler = new Resource::Profiler(stats.is_open());
initStatsHandler(*statshandler);
mViewer->addEventHandler(statshandler);
osg::ref_ptr<Resource::StatsHandler> resourceshandler = new Resource::StatsHandler;
osg::ref_ptr<Resource::StatsHandler> resourceshandler = new Resource::StatsHandler(stats.is_open());
mViewer->addEventHandler(resourceshandler);
if (stats.is_open())
Resource::CollectStatistics(mViewer);
// Start the game
if (!mSaveGameFile.empty())
{
@ -896,14 +917,6 @@ void OMW::Engine::go()
mEnvironment.getWindowManager()->executeInConsole(mStartupScript);
}
std::ofstream stats;
if (const auto path = std::getenv("OPENMW_OSG_STATS_FILE"))
{
stats.open(path, std::ios_base::out);
if (!stats)
Log(Debug::Warning) << "Failed to open file for stats: " << path;
}
// Start the main rendering loop
osg::Timer frameTimer;
double simulationTime = 0.0;

@ -85,6 +85,7 @@ namespace OMW
osgViewer::ScreenCaptureHandler::CaptureOperation *mScreenCaptureOperation;
std::string mCellName;
std::vector<std::string> mContentFiles;
std::vector<std::string> mGroundcoverFiles;
bool mSkipMenu;
bool mUseSound;
bool mCompileAll;
@ -155,6 +156,7 @@ namespace OMW
* @param file - filename (extension is required)
*/
void addContentFile(const std::string& file);
void addGroundcoverFile(const std::string& file);
/// Disable or enable all sounds
void setSoundUsage(bool soundUsage);

@ -62,6 +62,9 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
("content", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
->multitoken()->composing(), "content file(s): esm/esp, or omwgame/omwaddon")
("groundcover", bpo::value<Files::EscapeStringVector>()->default_value(Files::EscapeStringVector(), "")
->multitoken()->composing(), "groundcover content file(s): esm/esp, or omwgame/omwaddon")
("no-sound", bpo::value<bool>()->implicit_value(true)
->default_value(false), "disable all sounds")
@ -190,11 +193,15 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
return false;
}
StringsVector::const_iterator it(content.begin());
StringsVector::const_iterator end(content.end());
for (; it != end; ++it)
for (auto& file : content)
{
engine.addContentFile(file);
}
StringsVector groundcover = variables["groundcover"].as<Files::EscapeStringVector>().toStdStringVector();
for (auto& file : groundcover)
{
engine.addContentFile(*it);
engine.addGroundcoverFile(file);
}
// startup-settings
@ -254,7 +261,19 @@ namespace
level = Debug::Debug;
}
std::string_view s(msgCopy);
Log(level) << (s.back() == '\n' ? s.substr(0, s.size() - 1) : s);
if (s.size() < 1024)
Log(level) << (s.back() == '\n' ? s.substr(0, s.size() - 1) : s);
else
{
while (!s.empty())
{
size_t lineSize = 1;
while (lineSize < s.size() && s[lineSize - 1] != '\n')
lineSize++;
Log(level) << s.substr(0, s[lineSize - 1] == '\n' ? lineSize - 1 : lineSize);
s = s.substr(lineSize);
}
}
}
};
}

@ -4,6 +4,8 @@
#include <chrono>
#include <thread>
#include <components/resource/resourcesystem.hpp>
#include "world.hpp"
#include "scriptmanager.hpp"
#include "dialoguemanager.hpp"
@ -18,8 +20,8 @@ MWBase::Environment *MWBase::Environment::sThis = nullptr;
MWBase::Environment::Environment()
: mWorld (nullptr), mSoundManager (nullptr), mScriptManager (nullptr), mWindowManager (nullptr),
mMechanicsManager (nullptr), mDialogueManager (nullptr), mJournal (nullptr), mInputManager (nullptr), mStateManager (nullptr),
mFrameDuration (0), mFrameRateLimit(0.f)
mMechanicsManager (nullptr), mDialogueManager (nullptr), mJournal (nullptr), mInputManager (nullptr),
mStateManager (nullptr), mResourceSystem (nullptr), mFrameDuration (0), mFrameRateLimit(0.f)
{
assert (!sThis);
sThis = this;
@ -76,6 +78,11 @@ void MWBase::Environment::setStateManager (StateManager *stateManager)
mStateManager = stateManager;
}
void MWBase::Environment::setResourceSystem (Resource::ResourceSystem *resourceSystem)
{
mResourceSystem = resourceSystem;
}
void MWBase::Environment::setFrameDuration (float duration)
{
mFrameDuration = duration;
@ -158,6 +165,11 @@ MWBase::StateManager *MWBase::Environment::getStateManager() const
return mStateManager;
}
Resource::ResourceSystem *MWBase::Environment::getResourceSystem() const
{
return mResourceSystem;
}
float MWBase::Environment::getFrameDuration() const
{
return mFrameDuration;

@ -6,6 +6,11 @@ namespace osg
class Stats;
}
namespace Resource
{
class ResourceSystem;
}
namespace MWBase
{
class World;
@ -37,6 +42,7 @@ namespace MWBase
Journal *mJournal;
InputManager *mInputManager;
StateManager *mStateManager;
Resource::ResourceSystem *mResourceSystem;
float mFrameDuration;
float mFrameRateLimit;
@ -70,6 +76,8 @@ namespace MWBase
void setStateManager (StateManager *stateManager);
void setResourceSystem (Resource::ResourceSystem *resourceSystem);
void setFrameDuration (float duration);
///< Set length of current frame in seconds.
@ -95,6 +103,8 @@ namespace MWBase
StateManager *getStateManager() const;
Resource::ResourceSystem *getResourceSystem() const;
float getFrameDuration() const;
void cleanup();

@ -185,6 +185,7 @@ namespace MWBase
///
/// \note If cell==0, the cell the player is currently in will be used instead to
/// generate a name.
virtual std::string getCellName(const ESM::Cell* cell) const = 0;
virtual void removeRefScript (MWWorld::RefData *ref) = 0;
//< Remove the script attached to ref from mLocalScripts
@ -497,7 +498,7 @@ namespace MWBase
/// \todo this does not belong here
virtual void screenshot (osg::Image* image, int w, int h) = 0;
virtual bool screenshot360 (osg::Image* image, std::string settingStr) = 0;
virtual bool screenshot360 (osg::Image* image) = 0;
/// Find default position inside exterior cell specified by name
/// \return false if exterior with given name not exists, true otherwise

@ -302,30 +302,15 @@ namespace MWClass
std::string Door::getDestination (const MWWorld::LiveCellRef<ESM::Door>& door)
{
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
std::string dest;
if (door.mRef.getDestCell() != "")
{
// door leads to an interior, use interior name as tooltip
dest = door.mRef.getDestCell();
}
else
std::string dest = door.mRef.getDestCell();
if (dest.empty())
{
// door leads to exterior, use cell name (if any), otherwise translated region name
int x,y;
MWBase::Environment::get().getWorld()->positionToIndex (door.mRef.getDoorDest().pos[0], door.mRef.getDoorDest().pos[1], x, y);
const ESM::Cell* cell = store.get<ESM::Cell>().find(x,y);
if (cell->mName != "")
dest = cell->mName;
else
{
const ESM::Region* region =
store.get<ESM::Region>().find(cell->mRegion);
//name as is, not a token
return MyGUI::TextIterator::toTagsString(region->mName);
}
auto world = MWBase::Environment::get().getWorld();
world->positionToIndex (door.mRef.getDoorDest().pos[0], door.mRef.getDoorDest().pos[1], x, y);
const ESM::Cell* cell = world->getStore().get<ESM::Cell>().search(x,y);
dest = world->getCellName(cell);
}
return "#{sCell=" + dest + "}";

@ -418,10 +418,10 @@ namespace MWClass
{
const MWWorld::LiveCellRef<ESM::NPC> *ref = ptr.get<ESM::NPC>();
std::string model = "meshes\\base_anim.nif";
std::string model = Settings::Manager::getString("baseanim", "Models");
const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get<ESM::Race>().find(ref->mBase->mRace);
if(race->mData.mFlags & ESM::Race::Beast)
model = "meshes\\base_animkna.nif";
model = Settings::Manager::getString("baseanimkna", "Models");
return model;
}
@ -431,12 +431,12 @@ namespace MWClass
const MWWorld::LiveCellRef<ESM::NPC> *npc = ptr.get<ESM::NPC>();
const ESM::Race* race = MWBase::Environment::get().getWorld()->getStore().get<ESM::Race>().search(npc->mBase->mRace);
if(race && race->mData.mFlags & ESM::Race::Beast)
models.emplace_back("meshes\\base_animkna.nif");
models.emplace_back(Settings::Manager::getString("baseanimkna", "Models"));
// keep these always loaded just in case
models.emplace_back("meshes/xargonian_swimkna.nif");
models.emplace_back("meshes/xbase_anim_female.nif");
models.emplace_back("meshes/xbase_anim.nif");
models.emplace_back(Settings::Manager::getString("xargonianswimkna", "Models"));
models.emplace_back(Settings::Manager::getString("xbaseanimfemale", "Models"));
models.emplace_back(Settings::Manager::getString("xbaseanim", "Models"));
if (!npc->mBase->mModel.empty())
models.push_back("meshes/"+npc->mBase->mModel);

@ -316,7 +316,7 @@ int MWDialogue::Filter::getSelectStructInteger (const SelectWrapper& select) con
case SelectWrapper::Function_AiSetting:
return mActor.getClass().getCreatureStats (mActor).getAiSetting (
(MWMechanics::CreatureStats::AiSetting)select.getArgument()).getModified();
(MWMechanics::CreatureStats::AiSetting)select.getArgument()).getModified(false);
case SelectWrapper::Function_PcAttribute:

@ -30,14 +30,18 @@ namespace MWGui
// vanilla game does not show any text after the last EOL tag.
const std::string lowerText = Misc::StringUtils::lowerCase(mText);
int brIndex = lowerText.rfind("<br>");
int pIndex = lowerText.rfind("<p>");
if (brIndex == pIndex)
mText = "";
else if (brIndex > pIndex)
mText = mText.substr(0, brIndex+4);
else
mText = mText.substr(0, pIndex+3);
size_t brIndex = lowerText.rfind("<br>");
size_t pIndex = lowerText.rfind("<p>");
mPlainTextEnd = 0;
if (brIndex != pIndex)
{
if (brIndex != std::string::npos && pIndex != std::string::npos)
mPlainTextEnd = std::max(brIndex, pIndex);
else if (brIndex != std::string::npos)
mPlainTextEnd = brIndex;
else
mPlainTextEnd = pIndex;
}
registerTag("br", Event_BrTag);
registerTag("p", Event_PTag);
@ -103,7 +107,8 @@ namespace MWGui
{
if (!mIgnoreLineEndings || ch != '\n')
{
mBuffer.push_back(ch);
if (mIndex < mPlainTextEnd)
mBuffer.push_back(ch);
mIgnoreLineEndings = false;
mIgnoreNewlineTags = false;
}

@ -73,6 +73,8 @@ namespace MWGui
bool mClosingTag;
std::map<std::string, Events> mTagTypes;
std::string mBuffer;
size_t mPlainTextEnd;
};
class Paginator

@ -587,7 +587,7 @@ namespace MWGui
// effect box can have variable width -> variable left coordinate
int effectsDx = 0;
if (!mMinimapBox->getVisible ())
effectsDx = (viewSize.width - mMinimapBoxBaseRight) - (viewSize.width - mEffectBoxBaseRight);
effectsDx = mEffectBoxBaseRight - mMinimapBoxBaseRight;
mMapVisible = mMinimapBox->getVisible ();
if (!mMapVisible)

@ -6,6 +6,9 @@
#include <MyGUI_TextBox.h>
// correctIconPath
#include <components/resource/resourcesystem.hpp>
#include <components/vfs/manager.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
@ -106,7 +109,10 @@ namespace MWGui
std::string invIcon = ptr.getClass().getInventoryIcon(ptr);
if (invIcon.empty())
invIcon = "default icon.tga";
setIcon(MWBase::Environment::get().getWindowManager()->correctIconPath(invIcon));
invIcon = MWBase::Environment::get().getWindowManager()->correctIconPath(invIcon);
if (!MWBase::Environment::get().getResourceSystem()->getVFS()->exists(invIcon))
invIcon = MWBase::Environment::get().getWindowManager()->correctIconPath("default icon.tga");
setIcon(invIcon);
}

@ -561,7 +561,7 @@ namespace
if (mAllQuests)
{
SetNamesInactive setInactive(list);
mModel->visitQuestNames(!mAllQuests, setInactive);
mModel->visitQuestNames(false, setInactive);
}
MWBase::Environment::get().getWindowManager()->playSound("book page");

@ -28,10 +28,7 @@ namespace MWGui
MessageBoxManager::~MessageBoxManager ()
{
for (MessageBox* messageBox : mMessageBoxes)
{
delete messageBox;
}
MessageBoxManager::clear();
}
int MessageBoxManager::getMessagesCount()

@ -269,7 +269,7 @@ namespace MWGui
mWaterTextureSize->setIndexSelected(2);
int waterReflectionDetail = Settings::Manager::getInt("reflection detail", "Water");
waterReflectionDetail = std::min(4, std::max(0, waterReflectionDetail));
waterReflectionDetail = std::min(5, std::max(0, waterReflectionDetail));
mWaterReflectionDetail->setIndexSelected(waterReflectionDetail);
mWindowBorderButton->setEnabled(!Settings::Manager::getBool("fullscreen", "Video"));
@ -353,7 +353,7 @@ namespace MWGui
void SettingsWindow::onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos)
{
unsigned int level = std::min((unsigned int)4, (unsigned int)pos);
unsigned int level = std::min((unsigned int)5, (unsigned int)pos);
Settings::Manager::setInt("reflection detail", "Water", level);
apply();
}

@ -5,6 +5,7 @@
#include <MyGUI_ProgressBar.h>
#include <MyGUI_ImageBox.h>
#include <MyGUI_InputManager.h>
#include <MyGUI_LanguageManager.h>
#include <MyGUI_Gui.h>
#include <components/settings/settings.hpp>
@ -336,19 +337,22 @@ namespace MWGui
int max = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("iLevelUpTotal")->mValue.getInteger();
getWidget(levelWidget, i==0 ? "Level_str" : "LevelText");
std::string detail;
for (int i = 0; i < ESM::Attribute::Length; ++i)
{
if (auto increase = PCstats.getLevelUpAttributeIncrease(i))
detail += (detail.empty() ? "" : "\n") + ESM::Attribute::sAttributeNames[i] + " x" + MyGUI::utility::toString(increase);
}
if (!detail.empty())
levelWidget->setUserString("Caption_LevelDetailText", detail);
levelWidget->setUserString("RangePosition_LevelProgress", MyGUI::utility::toString(PCstats.getLevelProgress()));
levelWidget->setUserString("Range_LevelProgress", MyGUI::utility::toString(max));
levelWidget->setUserString("Caption_LevelProgressText", MyGUI::utility::toString(PCstats.getLevelProgress()) + "/"
+ MyGUI::utility::toString(max));
}
std::stringstream detail;
for (int attribute = 0; attribute < ESM::Attribute::Length; ++attribute)
{
float mult = PCstats.getLevelupAttributeMultiplier(attribute);
mult = std::min(mult, 100 - PCstats.getAttribute(attribute).getBase());
if (mult > 1)
detail << (detail.str().empty() ? "" : "\n") << "#{"
<< MyGUI::TextIterator::toTagsString(ESM::Attribute::sGmstAttributeIds[attribute])
<< "} x" << MyGUI::utility::toString(mult);
}
levelWidget->setUserString("Caption_LevelDetailText", MyGUI::LanguageManager::getInstance().replaceTags(detail.str()));
setFactions(PCstats.getFactionRanks());
setExpelled(PCstats.getExpelled ());

@ -333,12 +333,8 @@ namespace MWInput
void ActionManager::screenshot()
{
bool regularScreenshot = true;
std::string settingStr;
settingStr = Settings::Manager::getString("screenshot type","Video");
regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0;
const std::string& settingStr = Settings::Manager::getString("screenshot type", "Video");
bool regularScreenshot = settingStr.size() == 0 || settingStr.compare("regular") == 0;
if (regularScreenshot)
{
@ -349,7 +345,7 @@ namespace MWInput
{
osg::ref_ptr<osg::Image> screenshot (new osg::Image);
if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get(), settingStr))
if (MWBase::Environment::get().getWorld()->screenshot360(screenshot.get()))
{
(*mScreenCaptureOperation) (*(screenshot.get()), 0);
// FIXME: mScreenCaptureHandler->getCaptureOperation() causes crash for some reason

@ -171,16 +171,16 @@ namespace MWInput
, mDragDrop(false)
{
std::string file = userFileExists ? userFile : "";
mInputBinder = new InputControlSystem(file);
mListener = new BindingsListener(mInputBinder, this);
mInputBinder->setDetectingBindingListener(mListener);
mInputBinder = std::make_unique<InputControlSystem>(file);
mListener = std::make_unique<BindingsListener>(mInputBinder.get(), this);
mInputBinder->setDetectingBindingListener(mListener.get());
loadKeyDefaults();
loadControllerDefaults();
for (int i = 0; i < A_Last; ++i)
{
mInputBinder->getChannel(i)->addListener(mListener);
mInputBinder->getChannel(i)->addListener(mListener.get());
}
}
@ -192,7 +192,6 @@ namespace MWInput
BindingsManager::~BindingsManager()
{
mInputBinder->save(mUserFile);
delete mInputBinder;
}
void BindingsManager::update(float dt)
@ -315,7 +314,7 @@ namespace MWInput
&& mInputBinder->getMouseButtonBinding(control, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS
&& mInputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE) == ICS::InputControlSystem::MouseWheelClick::UNASSIGNED))
{
clearAllKeyBindings(mInputBinder, control);
clearAllKeyBindings(mInputBinder.get(), control);
if (defaultKeyBindings.find(i) != defaultKeyBindings.end()
&& (force || !mInputBinder->isKeyBound(defaultKeyBindings[i])))
@ -402,7 +401,7 @@ namespace MWInput
if (!controlExists || force || (mInputBinder->getJoystickAxisBinding(control, sFakeDeviceId, ICS::Control::INCREASE) == ICS::InputControlSystem::UNASSIGNED &&
mInputBinder->getJoystickButtonBinding(control, sFakeDeviceId, ICS::Control::INCREASE) == ICS_MAX_DEVICE_BUTTONS))
{
clearAllControllerBindings(mInputBinder, control);
clearAllControllerBindings(mInputBinder.get(), control);
if (defaultButtonBindings.find(i) != defaultButtonBindings.end()
&& (force || !mInputBinder->isJoystickButtonBound(sFakeDeviceId, defaultButtonBindings[i])))

@ -1,6 +1,7 @@
#ifndef MWINPUT_MWBINDINGSMANAGER_H
#define MWINPUT_MWBINDINGSMANAGER_H
#include <memory>
#include <string>
#include <vector>
@ -64,8 +65,8 @@ namespace MWInput
private:
void setupSDLKeyMappings();
InputControlSystem* mInputBinder;
BindingsListener* mListener;
std::unique_ptr<InputControlSystem> mInputBinder;
std::unique_ptr<BindingsListener> mListener;
std::string mUserFile;

@ -951,29 +951,29 @@ namespace MWMechanics
if (actor.getClass().hasInventoryStore(actor))
actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Poison);
}
else if (effects.get(ESM::MagicEffect::CureParalyzation).getModifier() > 0)
if (effects.get(ESM::MagicEffect::CureParalyzation).getModifier() > 0)
{
creatureStats.getActiveSpells().purgeEffect(ESM::MagicEffect::Paralyze);
creatureStats.getSpells().purgeEffect(ESM::MagicEffect::Paralyze);
if (actor.getClass().hasInventoryStore(actor))
actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Paralyze);
}
else if (effects.get(ESM::MagicEffect::CureCommonDisease).getModifier() > 0)
if (effects.get(ESM::MagicEffect::CureCommonDisease).getModifier() > 0)
{
creatureStats.getSpells().purgeCommonDisease();
}
else if (effects.get(ESM::MagicEffect::CureBlightDisease).getModifier() > 0)
if (effects.get(ESM::MagicEffect::CureBlightDisease).getModifier() > 0)
{
creatureStats.getSpells().purgeBlightDisease();
}
else if (effects.get(ESM::MagicEffect::CureCorprusDisease).getModifier() > 0)
if (effects.get(ESM::MagicEffect::CureCorprusDisease).getModifier() > 0)
{
creatureStats.getActiveSpells().purgeCorprusDisease();
creatureStats.getSpells().purgeCorprusDisease();
if (actor.getClass().hasInventoryStore(actor))
actor.getClass().getInventoryStore(actor).purgeEffect(ESM::MagicEffect::Corprus, true);
}
else if (effects.get(ESM::MagicEffect::RemoveCurse).getModifier() > 0)
if (effects.get(ESM::MagicEffect::RemoveCurse).getModifier() > 0)
{
creatureStats.getSpells().purgeCurses();
}
@ -1430,6 +1430,13 @@ namespace MWMechanics
if (heldIter != inventoryStore.end() && heldIter->getTypeName() != typeid(ESM::Light).name())
inventoryStore.unequipItem(*heldIter, ptr);
}
else if (heldIter == inventoryStore.end() || heldIter->getTypeName() == typeid(ESM::Light).name())
{
// For hostile NPCs, see if they have anything better to equip first
auto shield = inventoryStore.getPreferredShield(ptr);
if(shield != inventoryStore.end())
inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, shield, ptr);
}
heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);

@ -14,6 +14,16 @@
#include "movement.hpp"
#include "steering.hpp"
namespace
{
osg::Vec3f::value_type getHalfExtents(const MWWorld::ConstPtr& actor)
{
if(actor.getClass().isNpc())
return 64;
return MWBase::Environment::get().getWorld()->getHalfExtents(actor).y();
}
}
namespace MWMechanics
{
int AiFollow::mFollowIndexCounter = 0;
@ -119,16 +129,16 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
auto followers = MWBase::Environment::get().getMechanicsManager()->getActorsFollowingByIndex(target);
if (followers.size() >= 2 && followers.cbegin()->first != mFollowIndex)
{
osg::Vec3f::value_type maxSize = 0;
for(auto& follower : followers)
{
auto halfExtent = MWBase::Environment::get().getWorld()->getHalfExtents(follower.second).y();
auto halfExtent = getHalfExtents(follower.second);
if(halfExtent > floatingDistance)
floatingDistance = halfExtent;
}
floatingDistance += 128;
}
floatingDistance += MWBase::Environment::get().getWorld()->getHalfExtents(target).y();
floatingDistance += MWBase::Environment::get().getWorld()->getHalfExtents(actor).y() * 2;
floatingDistance += getHalfExtents(target) + 64;
floatingDistance += getHalfExtents(actor) * 2;
short followDistance = static_cast<short>(floatingDistance);
if (!mAlwaysFollow) //Update if you only follow for a bit

@ -96,6 +96,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
const float distToTarget = distance(position, dest);
const bool isDestReached = (distToTarget <= destTolerance);
const bool actorCanMoveByZ = canActorMoveByZAxis(actor);
if (!isDestReached && mTimer > AI_REACTION_TIME)
{
@ -104,7 +105,6 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
const bool wasShortcutting = mIsShortcutting;
bool destInLOS = false;
const bool actorCanMoveByZ = canActorMoveByZAxis(actor);
// Prohibit shortcuts for AiWander, if the actor can not move in 3 dimensions.
mIsShortcutting = actorCanMoveByZ
@ -150,7 +150,9 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
+ 1.2 * std::max(halfExtents.x(), halfExtents.y());
const float pointTolerance = std::max(MIN_TOLERANCE, actorTolerance);
mPathFinder.update(position, pointTolerance, DEFAULT_TOLERANCE);
static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game");
mPathFinder.update(position, pointTolerance, DEFAULT_TOLERANCE,
/*shortenIfAlmostStraight=*/smoothMovement, actorCanMoveByZ);
if (isDestReached || mPathFinder.checkPathCompleted()) // if path is finished
{
@ -158,8 +160,10 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
zTurn(actor, getZAngleToPoint(position, dest));
smoothTurn(actor, getXAngleToPoint(position, dest), 0);
world->removeActorPath(actor);
return isDestReached;
return true;
}
else if (mPathFinder.getPath().empty())
return false;
world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest);
@ -178,7 +182,6 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
const auto destination = mPathFinder.getPath().empty() ? dest : mPathFinder.getPath().front();
mObstacleCheck.update(actor, destination, duration);
static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game");
if (smoothMovement)
{
const float smoothTurnReservedDist = 150;

@ -99,7 +99,6 @@ namespace MWMechanics
{
AiPackage::Options options;
options.mUseVariableSpeed = true;
options.mRepeat = false;
return options;
}

@ -446,9 +446,9 @@ std::string CharacterController::fallbackShortWeaponGroup(const std::string& bas
const ESM::WeaponType* weapInfo = getWeaponType(mWeaponType);
// For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones
if (isRealWeapon && weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee)
if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee)
groupName += twoHandFallback;
else if (isRealWeapon)
else
groupName += oneHandFallback;
// Special case for crossbows - we shouls apply 1h animations a fallback only for lower body
@ -883,7 +883,11 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim
}
if(!cls.getCreatureStats(mPtr).isDead())
{
mIdleState = CharState_Idle;
if (cls.getCreatureStats(mPtr).getFallHeight() > 0)
mJumpState = JumpState_InAir;
}
else
{
const MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr);
@ -1898,7 +1902,7 @@ void CharacterController::updateAnimQueue()
mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1);
}
void CharacterController::update(float duration, bool animationOnly)
void CharacterController::update(float duration)
{
MWBase::World *world = MWBase::Environment::get().getWorld();
const MWWorld::Class &cls = mPtr.getClass();
@ -2386,10 +2390,10 @@ void CharacterController::update(float duration, bool animationOnly)
world->rotateObject(mPtr, rot.x(), rot.y(), 0.0f, true);
}
if (!animationOnly && !mMovementAnimationControlled)
if (!mMovementAnimationControlled)
world->queueMovement(mPtr, vec);
}
else if (!animationOnly)
else
// We must always queue movement, even if there is none, to apply gravity.
world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
@ -2414,8 +2418,7 @@ void CharacterController::update(float duration, bool animationOnly)
playDeath(1.f, mDeathState);
}
// We must always queue movement, even if there is none, to apply gravity.
if (!animationOnly)
world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
world->queueMovement(mPtr, osg::Vec3f(0.f, 0.f, 0.f));
}
bool isPersist = isPersistentAnimPlaying();
@ -2429,7 +2432,7 @@ void CharacterController::update(float duration, bool animationOnly)
moved.y() *= scale;
// Ensure we're moving in generally the right direction...
if(speed > 0.f)
if (speed > 0.f && moved != osg::Vec3f())
{
float l = moved.length();
if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2 ||
@ -2445,17 +2448,17 @@ void CharacterController::update(float duration, bool animationOnly)
}
}
if (mFloatToSurface && cls.isActor() && cls.canSwim(mPtr))
if (mFloatToSurface && cls.isActor())
{
if (cls.getCreatureStats(mPtr).isDead()
|| (!godmode && cls.getCreatureStats(mPtr).isParalyzed()))
{
moved.z() = 1.0;
}
}
}
// Update movement
if(!animationOnly && mMovementAnimationControlled && mPtr.getClass().isActor())
if(mMovementAnimationControlled && mPtr.getClass().isActor())
world->queueMovement(mPtr, moved);
mSkipAnim = false;

@ -248,7 +248,7 @@ public:
void updatePtr(const MWWorld::Ptr &ptr);
void update(float duration, bool animationOnly=false);
void update(float duration);
bool onOpen();
void onClose();

@ -345,6 +345,8 @@ namespace MWMechanics
MWMechanics::DynamicStat<float> health = attackerStats.getHealth();
health.setCurrent(health.getCurrent() - x);
attackerStats.setHealth(health);
MWBase::Environment::get().getSoundManager()->playSound3D(attacker, "Health Damage", 1.0f, 1.0f);
}
}

@ -322,11 +322,6 @@ void MWMechanics::NpcStats::updateHealth()
setHealth(floor(0.5f * (strength + endurance)));
}
int MWMechanics::NpcStats::getLevelUpAttributeIncrease(int attribute) const
{
return mSkillIncreases[attribute];
}
int MWMechanics::NpcStats::getLevelupAttributeMultiplier(int attribute) const
{
int num = mSkillIncreases[attribute];

@ -87,8 +87,6 @@ namespace MWMechanics
int getLevelProgress() const;
int getLevelUpAttributeIncrease(int attribute) const;
int getLevelupAttributeMultiplier(int attribute) const;
int getSkillIncreasesForSpecialization(int spec) const;

@ -97,12 +97,12 @@ namespace
float dotProduct = v1.x() * v3.x() + v1.y() * v3.y();
float crossProduct = v1.x() * v3.y() - v1.y() * v3.x();
// Check that the angle between v1 and v3 is less or equal than 10 degrees.
static const float cos170 = std::cos(osg::PI / 180 * 170);
bool checkAngle = dotProduct <= cos170 * v1.length() * v3.length();
// Check that the angle between v1 and v3 is less or equal than 5 degrees.
static const float cos175 = std::cos(osg::PI * (175.0 / 180));
bool checkAngle = dotProduct <= cos175 * v1.length() * v3.length();
// Check that distance from p2 to the line (p1, p3) is less or equal than `pointTolerance`.
bool checkDist = std::abs(crossProduct) <= pointTolerance * (p3 - p1).length() * 2;
bool checkDist = std::abs(crossProduct) <= pointTolerance * (p3 - p1).length();
return checkAngle && checkDist;
}
@ -296,7 +296,8 @@ namespace MWMechanics
return getXAngleToDir(dir);
}
void PathFinder::update(const osg::Vec3f& position, const float pointTolerance, const float destinationTolerance)
void PathFinder::update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance,
bool shortenIfAlmostStraight, bool canMoveByZ)
{
if (mPath.empty())
return;
@ -304,13 +305,24 @@ namespace MWMechanics
while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance)
mPath.pop_front();
while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance))
mPath.erase(mPath.begin() + 1);
if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance))
mPath.pop_front();
if (shortenIfAlmostStraight)
{
while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance))
mPath.erase(mPath.begin() + 1);
if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance))
mPath.pop_front();
}
if (mPath.size() == 1 && sqrDistanceIgnoreZ(mPath.front(), position) < destinationTolerance * destinationTolerance)
mPath.pop_front();
if (mPath.size() == 1)
{
float distSqr;
if (canMoveByZ)
distSqr = (mPath.front() - position).length2();
else
distSqr = sqrDistanceIgnoreZ(mPath.front(), position);
if (distSqr < destinationTolerance * destinationTolerance)
mPath.pop_front();
}
}
void PathFinder::buildStraightPath(const osg::Vec3f& endPoint)
@ -328,7 +340,7 @@ namespace MWMechanics
buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath));
mConstructed = true;
mConstructed = !mPath.empty();
}
void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
@ -341,7 +353,7 @@ namespace MWMechanics
if (!buildPathByNavigatorImpl(actor, startPoint, endPoint, halfExtents, flags, areaCosts, std::back_inserter(mPath)))
mPath.push_back(endPoint);
mConstructed = true;
mConstructed = !mPath.empty();
}
void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
@ -366,7 +378,7 @@ namespace MWMechanics
if (!hasNavMesh && mPath.empty())
mPath.push_back(endPoint);
mConstructed = true;
mConstructed = !mPath.empty();
}
bool PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,

@ -102,7 +102,8 @@ namespace MWMechanics
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts);
/// Remove front point if exist and within tolerance
void update(const osg::Vec3f& position, const float pointTolerance, const float destinationTolerance);
void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance,
bool shortenIfAlmostStraight, bool canMoveByZ);
bool checkPathCompleted() const
{

@ -60,7 +60,10 @@ namespace MWMechanics
void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster,
const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded)
{
if (!target.isEmpty() && target.getClass().isActor())
if (target.isEmpty())
return;
if (target.getClass().isActor())
{
// Early-out for characters that have departed.
const auto& stats = target.getClass().getCreatureStats(target);
@ -82,7 +85,7 @@ namespace MWMechanics
return;
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().search (mId);
if (spell && !target.isEmpty() && (spell->mData.mType == ESM::Spell::ST_Disease || spell->mData.mType == ESM::Spell::ST_Blight))
if (spell && target.getClass().isActor() && (spell->mData.mType == ESM::Spell::ST_Disease || spell->mData.mType == ESM::Spell::ST_Blight))
{
int requiredResistance = (spell->mData.mType == ESM::Spell::ST_Disease) ?
ESM::MagicEffect::ResistCommonDisease
@ -105,13 +108,13 @@ namespace MWMechanics
// This is required for Weakness effects in a spell to apply to any subsequent effects in the spell.
// Otherwise, they'd only apply after the whole spell was added.
MagicEffects targetEffects;
if (!target.isEmpty() && target.getClass().isActor())
if (target.getClass().isActor())
targetEffects += target.getClass().getCreatureStats(target).getMagicEffects();
bool castByPlayer = (!caster.isEmpty() && caster == getPlayer());
ActiveSpells targetSpells;
if (!target.isEmpty() && target.getClass().isActor())
if (target.getClass().isActor())
targetSpells = target.getClass().getCreatureStats(target).getActiveSpells();
bool canCastAnEffect = false; // For bound equipment.If this remains false
@ -123,7 +126,7 @@ namespace MWMechanics
int currentEffectIndex = 0;
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt (effects.mList.begin());
!target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex)
effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex)
{
if (effectIt->mRange != range)
continue;
@ -267,7 +270,7 @@ namespace MWMechanics
}
// Re-casting a summon effect will remove the creature from previous castings of that effect.
if (isSummoningEffect(effectIt->mEffectID) && !target.isEmpty() && target.getClass().isActor())
if (isSummoningEffect(effectIt->mEffectID) && target.getClass().isActor())
{
CreatureStats& targetStats = target.getClass().getCreatureStats(target);
ESM::SummonKey key(effectIt->mEffectID, mId, currentEffectIndex);
@ -310,18 +313,16 @@ namespace MWMechanics
if (!exploded)
MWBase::Environment::get().getWorld()->explodeSpell(mHitPosition, effects, caster, target, range, mId, mSourceName, mFromProjectile);
if (!target.isEmpty()) {
if (!reflectedEffects.mList.empty())
inflict(caster, target, reflectedEffects, range, true, exploded);
if (!reflectedEffects.mList.empty())
inflict(caster, target, reflectedEffects, range, true, exploded);
if (!appliedLastingEffects.empty())
{
int casterActorId = -1;
if (!caster.isEmpty() && caster.getClass().isActor())
casterActorId = caster.getClass().getCreatureStats(caster).getActorId();
target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects,
mSourceName, casterActorId);
}
if (!appliedLastingEffects.empty())
{
int casterActorId = -1;
if (!caster.isEmpty() && caster.getClass().isActor())
casterActorId = caster.getClass().getCreatureStats(caster).getActorId();
target.getClass().getCreatureStats(target).getActiveSpells().addSpell(mId, mStack, appliedLastingEffects,
mSourceName, casterActorId);
}
}

@ -123,7 +123,7 @@ namespace MWMechanics
if (spell->mData.mType != ESM::Spell::ST_Spell)
return 100;
if (checkMagicka && stats.getMagicka().getCurrent() < spell->mData.mCost)
if (checkMagicka && spell->mData.mCost > 0 && stats.getMagicka().getCurrent() < spell->mData.mCost)
return 0;
if (spell->mData.mFlags & ESM::Spell::F_Always)

@ -18,8 +18,10 @@ namespace MWMechanics
}
template<typename T>
T Stat<T>::getModified() const
T Stat<T>::getModified(bool capped) const
{
if(!capped)
return mModified;
return std::max(static_cast<T>(0), mModified);
}

@ -28,7 +28,7 @@ namespace MWMechanics
const T& getBase() const;
T getModified() const;
T getModified(bool capped = true) const;
T getCurrentModified() const;
T getModifier() const;
T getCurrentModifier() const;

@ -4,8 +4,6 @@
namespace MWMechanics
{
static const ESM::WeaponType *sWeaponTypeListEnd = &sWeaponTypeList[sizeof(sWeaponTypeList)/sizeof(sWeaponTypeList[0])];
MWWorld::ContainerStoreIterator getActiveWeapon(MWWorld::Ptr actor, int *weaptype)
{
MWWorld::InventoryStore &inv = actor.getClass().getInventoryStore(actor);

@ -1,6 +1,5 @@
#include "actor.hpp"
#include <BulletCollision/CollisionShapes/btCapsuleShape.h>
#include <BulletCollision/CollisionShapes/btBoxShape.h>
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
@ -14,6 +13,8 @@
#include "collisiontype.hpp"
#include "mtphysics.hpp"
#include <cmath>
namespace MWPhysics
{
@ -52,28 +53,22 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic
Log(Debug::Error) << "Error: Failed to calculate bounding box for actor \"" << ptr.getCellRef().getRefId() << "\".";
}
// Use capsule shape only if base is square (nonuniform scaling apparently doesn't work on it)
if (std::abs(mHalfExtents.x()-mHalfExtents.y())<mHalfExtents.x()*0.05 && mHalfExtents.z() >= mHalfExtents.x())
{
mShape.reset(new btCapsuleShapeZ(mHalfExtents.x(), 2*mHalfExtents.z() - 2*mHalfExtents.x()));
mRotationallyInvariant = true;
}
else
{
mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents)));
mRotationallyInvariant = false;
}
mShape.reset(new btBoxShape(Misc::Convert::toBullet(mHalfExtents)));
mRotationallyInvariant = (mMeshTranslation.x() == 0.0 && mMeshTranslation.y() == 0.0) && std::fabs(mHalfExtents.x() - mHalfExtents.y()) < 2.2;
mConvexShape = static_cast<btConvexShape*>(mShape.get());
mCollisionObject.reset(new btCollisionObject);
mCollisionObject = std::make_unique<btCollisionObject>();
mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT);
mCollisionObject->setActivationState(DISABLE_DEACTIVATION);
mCollisionObject->setCollisionShape(mShape.get());
mCollisionObject->setUserPointer(this);
updateRotation();
updateScale();
if(!mRotationallyInvariant)
updateRotation();
updatePosition();
addCollisionMask(getCollisionMask());
updateCollisionObjectPosition();
@ -81,8 +76,7 @@ Actor::Actor(const MWWorld::Ptr& ptr, const Resource::BulletShape* shape, Physic
Actor::~Actor()
{
if (mCollisionObject)
mTaskScheduler->removeCollisionObject(mCollisionObject.get());
mTaskScheduler->removeCollisionObject(mCollisionObject.get());
}
void Actor::enableCollisionMode(bool collision)
@ -154,6 +148,11 @@ osg::Vec3f Actor::getSimulationPosition() const
return mSimulationPosition;
}
osg::Vec3f Actor::getScaledMeshTranslation() const
{
return mRotation * osg::componentMultiply(mMeshTranslation, mScale);
}
void Actor::updateCollisionObjectPosition()
{
std::scoped_lock lock(mPositionMutex);

@ -82,6 +82,9 @@ namespace MWPhysics
*/
osg::Vec3f getOriginalHalfExtents() const;
/// Returns the mesh translation, scaled and rotated as necessary
osg::Vec3f getScaledMeshTranslation() const;
/**
* Returns the position of the collision body
* @note The collision shape's origin is in its center, so the position returned can be described as center of the actor collision box in world space.

@ -0,0 +1,106 @@
#include <mutex>
#include "actorconvexcallback.hpp"
#include "collisiontype.hpp"
#include "contacttestwrapper.h"
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include <components/misc/convert.hpp>
#include "collisiontype.hpp"
#include "projectile.hpp"
namespace MWPhysics
{
class ActorOverlapTester : public btCollisionWorld::ContactResultCallback
{
public:
bool overlapping = false;
btScalar addSingleResult(btManifoldPoint& cp,
const btCollisionObjectWrapper* colObj0Wrap,
int partId0,
int index0,
const btCollisionObjectWrapper* colObj1Wrap,
int partId1,
int index1) override
{
if(cp.getDistance() <= 0.0f)
overlapping = true;
return btScalar(1);
}
};
ActorConvexCallback::ActorConvexCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world)
: btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)),
mMe(me), mMotion(motion), mMinCollisionDot(minCollisionDot), mWorld(world)
{
}
btScalar ActorConvexCallback::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace)
{
if (convexResult.m_hitCollisionObject == mMe)
return btScalar(1);
// override data for actor-actor collisions
// vanilla Morrowind seems to make overlapping actors collide as though they are both cylinders with a diameter of the distance between them
// For some reason this doesn't work as well as it should when using capsules, but it still helps a lot.
if(convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor)
{
ActorOverlapTester isOverlapping;
// FIXME: This is absolutely terrible and bullet should feel terrible for not making contactPairTest const-correct.
ContactTestWrapper::contactPairTest(const_cast<btCollisionWorld*>(mWorld), const_cast<btCollisionObject*>(mMe), const_cast<btCollisionObject*>(convexResult.m_hitCollisionObject), isOverlapping);
if(isOverlapping.overlapping)
{
auto originA = Misc::Convert::toOsg(mMe->getWorldTransform().getOrigin());
auto originB = Misc::Convert::toOsg(convexResult.m_hitCollisionObject->getWorldTransform().getOrigin());
osg::Vec3f motion = Misc::Convert::toOsg(mMotion);
osg::Vec3f normal = (originA-originB);
normal.z() = 0;
normal.normalize();
// only collide if horizontally moving towards the hit actor (note: the motion vector appears to be inverted)
// FIXME: This kinda screws with standing on actors that walk up slopes for some reason. Makes you fall through them.
// It happens in vanilla Morrowind too, but much less often.
// I tried hunting down why but couldn't figure it out. Possibly a stair stepping or ground ejection bug.
if(normal * motion > 0.0f)
{
convexResult.m_hitFraction = 0.0f;
convexResult.m_hitNormalLocal = Misc::Convert::toBullet(normal);
return ClosestConvexResultCallback::addSingleResult(convexResult, true);
}
else
{
return btScalar(1);
}
}
}
if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Projectile)
{
auto* projectileHolder = static_cast<Projectile*>(convexResult.m_hitCollisionObject->getUserPointer());
if (!projectileHolder->isActive())
return btScalar(1);
auto* targetHolder = static_cast<PtrHolder*>(mMe->getUserPointer());
const MWWorld::Ptr target = targetHolder->getPtr();
if (projectileHolder->isValidTarget(target))
projectileHolder->hit(target, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal);
return btScalar(1);
}
btVector3 hitNormalWorld;
if (normalInWorldSpace)
hitNormalWorld = convexResult.m_hitNormalLocal;
else
{
///need to transform normal into worldspace
hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal;
}
// dot product of the motion vector against the collision contact normal
btScalar dotCollision = mMotion.dot(hitNormalWorld);
if (dotCollision <= mMinCollisionDot)
return btScalar(1);
return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace);
}
}

@ -1,5 +1,5 @@
#ifndef OPENMW_MWPHYSICS_CLOSESTNOTMECONVEXRESULTCALLBACK_H
#define OPENMW_MWPHYSICS_CLOSESTNOTMECONVEXRESULTCALLBACK_H
#ifndef OPENMW_MWPHYSICS_ACTORCONVEXCALLBACK_H
#define OPENMW_MWPHYSICS_ACTORCONVEXCALLBACK_H
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
@ -7,10 +7,10 @@ class btCollisionObject;
namespace MWPhysics
{
class ClosestNotMeConvexResultCallback : public btCollisionWorld::ClosestConvexResultCallback
class ActorConvexCallback : public btCollisionWorld::ClosestConvexResultCallback
{
public:
ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot);
ActorConvexCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot, const btCollisionWorld * world);
btScalar addSingleResult(btCollisionWorld::LocalConvexResult& convexResult,bool normalInWorldSpace) override;
@ -18,6 +18,7 @@ namespace MWPhysics
const btCollisionObject *mMe;
const btVector3 mMotion;
const btScalar mMinCollisionDot;
const btCollisionWorld * mWorld;
};
}

@ -1,49 +0,0 @@
#include "closestnotmeconvexresultcallback.hpp"
#include <BulletCollision/CollisionDispatch/btCollisionObject.h>
#include "collisiontype.hpp"
#include "projectile.hpp"
namespace MWPhysics
{
ClosestNotMeConvexResultCallback::ClosestNotMeConvexResultCallback(const btCollisionObject *me, const btVector3 &motion, btScalar minCollisionDot)
: btCollisionWorld::ClosestConvexResultCallback(btVector3(0.0, 0.0, 0.0), btVector3(0.0, 0.0, 0.0)),
mMe(me), mMotion(motion), mMinCollisionDot(minCollisionDot)
{
}
btScalar ClosestNotMeConvexResultCallback::addSingleResult(btCollisionWorld::LocalConvexResult& convexResult, bool normalInWorldSpace)
{
if (convexResult.m_hitCollisionObject == mMe)
return btScalar(1);
if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Projectile)
{
auto* projectileHolder = static_cast<Projectile*>(convexResult.m_hitCollisionObject->getUserPointer());
if (!projectileHolder->isActive())
return btScalar(1);
auto* targetHolder = static_cast<PtrHolder*>(mMe->getUserPointer());
const MWWorld::Ptr target = targetHolder->getPtr();
if (projectileHolder->isValidTarget(target))
projectileHolder->hit(target, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal);
return btScalar(1);
}
btVector3 hitNormalWorld;
if (normalInWorldSpace)
hitNormalWorld = convexResult.m_hitNormalLocal;
else
{
///need to transform normal into worldspace
hitNormalWorld = convexResult.m_hitCollisionObject->getWorldTransform().getBasis()*convexResult.m_hitNormalLocal;
}
// dot product of the motion vector against the collision contact normal
btScalar dotCollision = mMotion.dot(hitNormalWorld);
if (dotCollision <= mMinCollisionDot)
return btScalar(1);
return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace);
}
}

@ -7,16 +7,13 @@
#include "../mwworld/class.hpp"
#include "actor.hpp"
#include "collisiontype.hpp"
#include "projectile.hpp"
#include "ptrholder.hpp"
namespace MWPhysics
{
ClosestNotMeRayResultCallback::ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector<const btCollisionObject*> targets, const btVector3& from, const btVector3& to, Projectile* proj)
ClosestNotMeRayResultCallback::ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector<const btCollisionObject*> targets, const btVector3& from, const btVector3& to)
: btCollisionWorld::ClosestRayResultCallback(from, to)
, mMe(me), mTargets(std::move(targets)), mProjectile(proj)
, mMe(me), mTargets(std::move(targets))
{
}
@ -25,9 +22,6 @@ namespace MWPhysics
if (rayResult.m_collisionObject == mMe)
return 1.f;
if (mProjectile && rayResult.m_collisionObject == mProjectile->getCollisionObject())
return 1.f;
if (!mTargets.empty())
{
if ((std::find(mTargets.begin(), mTargets.end(), rayResult.m_collisionObject) == mTargets.end()))
@ -38,27 +32,6 @@ namespace MWPhysics
}
}
btCollisionWorld::ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace);
if (mProjectile)
{
switch (rayResult.m_collisionObject->getBroadphaseHandle()->m_collisionFilterGroup)
{
case CollisionType_Actor:
{
auto* target = static_cast<Actor*>(rayResult.m_collisionObject->getUserPointer());
mProjectile->hit(target->getPtr(), m_hitPointWorld, m_hitNormalWorld);
break;
}
case CollisionType_Projectile:
{
auto* target = static_cast<Projectile*>(rayResult.m_collisionObject->getUserPointer());
target->hit(mProjectile->getPtr(), m_hitPointWorld, m_hitNormalWorld);
mProjectile->hit(target->getPtr(), m_hitPointWorld, m_hitNormalWorld);
break;
}
}
}
return rayResult.m_hitFraction;
return btCollisionWorld::ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace);
}
}

@ -14,13 +14,13 @@ namespace MWPhysics
class ClosestNotMeRayResultCallback : public btCollisionWorld::ClosestRayResultCallback
{
public:
ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector<const btCollisionObject*> targets, const btVector3& from, const btVector3& to, Projectile* proj=nullptr);
ClosestNotMeRayResultCallback(const btCollisionObject* me, std::vector<const btCollisionObject*> targets, const btVector3& from, const btVector3& to);
btScalar addSingleResult(btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) override;
private:
const btCollisionObject* mMe;
const std::vector<const btCollisionObject*> mTargets;
Projectile* mProjectile;
};
}

@ -5,12 +5,22 @@ namespace MWPhysics
{
static const float sStepSizeUp = 34.0f;
static const float sStepSizeDown = 62.0f;
static const float sMinStep = 10.f;
static const float sMinStep = 10.0f; // hack to skip over tiny unwalkable slopes
static const float sMinStep2 = 20.0f; // hack to skip over shorter but longer/wider/further unwalkable slopes
// whether to do the above stairstepping logic hacks to work around bad morrowind assets - disabling causes problems but improves performance
static const bool sDoExtraStairHacks = true;
static const float sGroundOffset = 1.0f;
static const float sMaxSlope = 49.0f;
// Arbitrary number. To prevent infinite loops. They shouldn't happen but it's good to be prepared.
static const int sMaxIterations = 8;
// Allows for more precise movement solving without getting stuck or snagging too easily.
static const float sCollisionMargin = 0.1;
// Allow for a small amount of penetration to prevent numerical precision issues from causing the "unstuck"ing code to run unnecessarily
// Currently set to 0 because having the "unstuck"ing code run whenever possible prevents some glitchy snagging issues
static const float sAllowedPenetration = 0.0;
}
#endif

@ -0,0 +1,21 @@
#include <mutex>
#include "contacttestwrapper.h"
namespace MWPhysics
{
// Concurrent calls to contactPairTest (and by extension contactTest) are forbidden.
static std::mutex contactMutex;
void ContactTestWrapper::contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback)
{
std::unique_lock lock(contactMutex);
collisionWorld->contactTest(colObj, resultCallback);
}
void ContactTestWrapper::contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback)
{
std::unique_lock lock(contactMutex);
collisionWorld->contactPairTest(colObjA, colObjB, resultCallback);
}
}

@ -0,0 +1,14 @@
#ifndef OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H
#define OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H
#include <BulletCollision/CollisionDispatch/btCollisionWorld.h>
namespace MWPhysics
{
struct ContactTestWrapper
{
static void contactTest(btCollisionWorld* collisionWorld, btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback);
static void contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback);
};
}
#endif

@ -1,4 +1,5 @@
#include "heightfield.hpp"
#include "mtphysics.hpp"
#include <osg/Object>
@ -42,10 +43,12 @@ namespace
namespace MWPhysics
{
HeightField::HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject)
: mHeights(makeHeights(heights, sqrtVerts))
HeightField::HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject, PhysicsTaskScheduler* scheduler)
: mHoldObject(holdObject)
, mHeights(makeHeights(heights, sqrtVerts))
, mTaskScheduler(scheduler)
{
mShape = new btHeightfieldTerrainShape(
mShape = std::make_unique<btHeightfieldTerrainShape>(
sqrtVerts, sqrtVerts,
getHeights(heights, mHeights),
1,
@ -60,31 +63,29 @@ namespace MWPhysics
(y+0.5f) * triSize * (sqrtVerts-1),
(maxH+minH)*0.5f));
mCollisionObject = new btCollisionObject;
mCollisionObject->setCollisionShape(mShape);
mCollisionObject = std::make_unique<btCollisionObject>();
mCollisionObject->setCollisionShape(mShape.get());
mCollisionObject->setWorldTransform(transform);
mHoldObject = holdObject;
mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_HeightMap, CollisionType_Actor|CollisionType_Projectile);
}
HeightField::~HeightField()
{
delete mCollisionObject;
delete mShape;
mTaskScheduler->removeCollisionObject(mCollisionObject.get());
}
btCollisionObject* HeightField::getCollisionObject()
{
return mCollisionObject;
return mCollisionObject.get();
}
const btCollisionObject* HeightField::getCollisionObject() const
{
return mCollisionObject;
return mCollisionObject.get();
}
const btHeightfieldTerrainShape* HeightField::getShape() const
{
return mShape;
return mShape.get();
}
}

@ -5,6 +5,7 @@
#include <LinearMath/btScalar.h>
#include <memory>
#include <vector>
class btCollisionObject;
@ -17,10 +18,12 @@ namespace osg
namespace MWPhysics
{
class PhysicsTaskScheduler;
class HeightField
{
public:
HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject);
HeightField(const float* heights, int x, int y, float triSize, float sqrtVerts, float minH, float maxH, const osg::Object* holdObject, PhysicsTaskScheduler* scheduler);
~HeightField();
btCollisionObject* getCollisionObject();
@ -28,11 +31,13 @@ namespace MWPhysics
const btHeightfieldTerrainShape* getShape() const;
private:
btHeightfieldTerrainShape* mShape;
btCollisionObject* mCollisionObject;
std::unique_ptr<btHeightfieldTerrainShape> mShape;
std::unique_ptr<btCollisionObject> mCollisionObject;
osg::ref_ptr<const osg::Object> mHoldObject;
std::vector<btScalar> mHeights;
PhysicsTaskScheduler* mTaskScheduler;
void operator=(const HeightField&);
HeightField(const HeightField&);
};

@ -17,10 +17,13 @@
#include "actor.hpp"
#include "collisiontype.hpp"
#include "constants.hpp"
#include "contacttestwrapper.h"
#include "physicssystem.hpp"
#include "stepper.hpp"
#include "trace.h"
#include <cmath>
namespace MWPhysics
{
static bool isActor(const btCollisionObject *obj)
@ -29,12 +32,50 @@ namespace MWPhysics
return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor;
}
template <class Vec3>
static bool isWalkableSlope(const Vec3 &normal)
class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback
{
static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope));
return (normal.z() > sMaxSlopeCos);
}
public:
ContactCollectionCallback(const btCollisionObject * me, osg::Vec3f velocity) : mMe(me)
{
m_collisionFilterGroup = me->getBroadphaseHandle()->m_collisionFilterGroup;
m_collisionFilterMask = me->getBroadphaseHandle()->m_collisionFilterMask & ~CollisionType_Projectile;
mVelocity = Misc::Convert::toBullet(velocity);
}
btScalar addSingleResult(btManifoldPoint & contact, const btCollisionObjectWrapper * colObj0Wrap, int partId0, int index0, const btCollisionObjectWrapper * colObj1Wrap, int partId1, int index1) override
{
if (isActor(colObj0Wrap->getCollisionObject()) && isActor(colObj1Wrap->getCollisionObject()))
return 0.0;
// ignore overlap if we're moving in the same direction as it would push us out (don't change this to >=, that would break detection when not moving)
if (contact.m_normalWorldOnB.dot(mVelocity) > 0.0)
return 0.0;
auto delta = contact.m_normalWorldOnB * -contact.m_distance1;
mContactSum += delta;
mMaxX = std::max(std::abs(delta.x()), mMaxX);
mMaxY = std::max(std::abs(delta.y()), mMaxY);
mMaxZ = std::max(std::abs(delta.z()), mMaxZ);
if (contact.m_distance1 < mDistance)
{
mDistance = contact.m_distance1;
mNormal = contact.m_normalWorldOnB;
mDelta = delta;
return mDistance;
}
else
{
return 0.0;
}
}
btScalar mMaxX = 0.0;
btScalar mMaxY = 0.0;
btScalar mMaxZ = 0.0;
btVector3 mContactSum{0.0, 0.0, 0.0};
btVector3 mNormal{0.0, 0.0, 0.0}; // points towards "me"
btVector3 mDelta{0.0, 0.0, 0.0}; // points towards "me"
btScalar mDistance = 0.0; // negative or zero
protected:
btVector3 mVelocity;
const btCollisionObject * mMe;
};
osg::Vec3f MovementSolver::traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight)
{
@ -99,13 +140,13 @@ namespace MWPhysics
}
const btCollisionObject *colobj = physicActor->getCollisionObject();
osg::Vec3f halfExtents = physicActor->getHalfExtents();
// NOTE: here we don't account for the collision box translation (i.e. physicActor->getPosition() - refpos.pos).
// That means the collision shape used for moving this actor is in a different spot than the collision shape
// other actors are using to collide against this actor.
// While this is strictly speaking wrong, it's needed for MW compatibility.
actor.mPosition.z() += halfExtents.z();
// Adjust for collision mesh offset relative to actor's "location"
// (doTrace doesn't take local/interior collision shape translation into account, so we have to do it on our own)
// for compatibility with vanilla assets, we have to derive this from the vertical half extent instead of from internal hull translation
// if not for this hack, the "correct" collision hull position would be physicActor->getScaledMeshTranslation()
osg::Vec3f halfExtents = physicActor->getHalfExtents();
actor.mPosition.z() += halfExtents.z(); // vanilla-accurate
static const float fSwimHeightScale = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fSwimHeightScale")->mValue.getFloat();
float swimlevel = actor.mWaterlevel + halfExtents.z() - (physicActor->getRenderingHalfExtents().z() * 2 * fSwimHeightScale);
@ -156,6 +197,13 @@ namespace MWPhysics
* The initial velocity was set earlier (see above).
*/
float remainingTime = time;
bool seenGround = physicActor->getOnGround() && !physicActor->getOnSlope() && !actor.mFlying;
int numTimesSlid = 0;
osg::Vec3f lastSlideNormal(0,0,1);
osg::Vec3f lastSlideNormalFallback(0,0,1);
bool forceGroundTest = false;
for (int iterations = 0; iterations < sMaxIterations && remainingTime > 0.01f; ++iterations)
{
osg::Vec3f nextpos = newPosition + velocity * remainingTime;
@ -164,7 +212,7 @@ namespace MWPhysics
if(!actor.mFlying && nextpos.z() > swimlevel && newPosition.z() < swimlevel)
{
const osg::Vec3f down(0,0,-1);
velocity = slide(velocity, down);
velocity = reject(velocity, down);
// NOTE: remainingTime is unchanged before the loop continues
continue; // velocity updated, calculate nextpos again
}
@ -193,92 +241,158 @@ namespace MWPhysics
break;
}
// We are touching something.
if (tracer.mFraction < 1E-9f)
{
// Try to separate by backing off slighly to unstuck the solver
osg::Vec3f backOff = (newPosition - tracer.mHitPoint) * 1E-2f;
newPosition += backOff;
}
if (isWalkableSlope(tracer.mPlaneNormal) && !actor.mFlying && newPosition.z() >= swimlevel)
seenGround = true;
// We hit something. Check if we can step up.
float hitHeight = tracer.mHitPoint.z() - tracer.mEndPos.z() + halfExtents.z();
osg::Vec3f oldPosition = newPosition;
bool result = false;
bool usedStepLogic = false;
if (hitHeight < sStepSizeUp && !isActor(tracer.mHitObject))
{
// Try to step up onto it.
// NOTE: stepMove does not allow stepping over, modifies newPosition if successful
result = stepper.step(newPosition, velocity*remainingTime, remainingTime);
// NOTE: this modifies newPosition and velocity on its own if successful
usedStepLogic = stepper.step(newPosition, velocity, remainingTime, seenGround, iterations == 0);
}
if (result)
if (usedStepLogic)
{
// don't let pure water creatures move out of water after stepMove
const auto ptr = physicActor->getPtr();
if (ptr.getClass().isPureWaterCreature(ptr) && newPosition.z() + halfExtents.z() > actor.mWaterlevel)
newPosition = oldPosition;
else if(!actor.mFlying && actor.mPosition.z() >= swimlevel)
forceGroundTest = true;
}
else
{
// Can't move this way, try to find another spot along the plane
osg::Vec3f newVelocity = slide(velocity, tracer.mPlaneNormal);
// Can't step up, so slide against what we ran into
remainingTime *= (1.0f-tracer.mFraction);
auto planeNormal = tracer.mPlaneNormal;
// If we touched the ground this frame, and whatever we ran into is a wall of some sort,
// pretend that its collision normal is pointing horizontally
// (fixes snagging on slightly downward-facing walls, and crawling up the bases of very steep walls because of the collision margin)
if (seenGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0)
{
planeNormal.z() = 0;
planeNormal.normalize();
}
// Move up to what we ran into (with a bit of a collision margin)
if ((newPosition-tracer.mEndPos).length2() > sCollisionMargin*sCollisionMargin)
{
auto direction = velocity;
direction.normalize();
newPosition = tracer.mEndPos;
newPosition -= direction*sCollisionMargin;
}
osg::Vec3f newVelocity = (velocity * planeNormal <= 0.0) ? reject(velocity, planeNormal) : velocity;
bool usedSeamLogic = false;
// check for the current and previous collision planes forming an acute angle; slide along the seam if they do
if(numTimesSlid > 0)
{
auto dotA = lastSlideNormal * planeNormal;
auto dotB = lastSlideNormalFallback * planeNormal;
if(numTimesSlid <= 1) // ignore fallback normal if this is only the first or second slide
dotB = 1.0;
if(dotA <= 0.0 || dotB <= 0.0)
{
osg::Vec3f bestNormal = lastSlideNormal;
// use previous-to-previous collision plane if it's acute with current plane but actual previous plane isn't
if(dotB < dotA)
{
bestNormal = lastSlideNormalFallback;
lastSlideNormal = lastSlideNormalFallback;
}
auto constraintVector = bestNormal ^ planeNormal; // cross product
if(constraintVector.length2() > 0) // only if it's not zero length
{
constraintVector.normalize();
newVelocity = project(velocity, constraintVector);
// version of surface rejection for acute crevices/seams
auto averageNormal = bestNormal + planeNormal;
averageNormal.normalize();
tracer.doTrace(colobj, newPosition, newPosition + averageNormal*(sCollisionMargin*2.0), collisionWorld);
newPosition = (newPosition + tracer.mEndPos)/2.0;
usedSeamLogic = true;
}
}
}
// otherwise just keep the normal vector rejection
// if this isn't the first iteration, or if the first iteration is also the last iteration,
// move away from the collision plane slightly, if possible
// this reduces getting stuck in some concave geometry, like the gaps above the railings in some ald'ruhn buildings
// this is different from the normal collision margin, because the normal collision margin is along the movement path,
// but this is along the collision normal
if(!usedSeamLogic && (iterations > 0 || remainingTime < 0.01f))
{
tracer.doTrace(colobj, newPosition, newPosition + planeNormal*(sCollisionMargin*2.0), collisionWorld);
newPosition = (newPosition + tracer.mEndPos)/2.0;
}
// Do not allow sliding upward if there is gravity.
// Stepping will have taken care of that.
if(!(newPosition.z() < swimlevel || actor.mFlying))
newVelocity.z() = std::min(newVelocity.z(), 0.0f);
// Do not allow sliding up steep slopes if there is gravity.
if (newPosition.z() >= swimlevel && !actor.mFlying && !isWalkableSlope(planeNormal))
newVelocity.z() = std::min(newVelocity.z(), velocity.z());
if ((newVelocity-velocity).length2() < 0.01)
if (newVelocity * origVelocity <= 0.0f)
break;
if ((newVelocity * origVelocity) <= 0.f)
break; // ^ dot product
numTimesSlid += 1;
lastSlideNormalFallback = lastSlideNormal;
lastSlideNormal = planeNormal;
velocity = newVelocity;
}
}
bool isOnGround = false;
bool isOnSlope = false;
if (!(inertia.z() > 0.f) && !(newPosition.z() < swimlevel))
if (forceGroundTest || (inertia.z() <= 0.f && newPosition.z() >= swimlevel))
{
osg::Vec3f from = newPosition;
osg::Vec3f to = newPosition - (physicActor->getOnGround() ? osg::Vec3f(0,0,sStepSizeDown + 2*sGroundOffset) : osg::Vec3f(0,0,2*sGroundOffset));
auto dropDistance = 2*sGroundOffset + (physicActor->getOnGround() ? sStepSizeDown : 0);
osg::Vec3f to = newPosition - osg::Vec3f(0,0,dropDistance);
tracer.doTrace(colobj, from, to, collisionWorld);
if(tracer.mFraction < 1.0f && !isActor(tracer.mHitObject))
if(tracer.mFraction < 1.0f)
{
const btCollisionObject* standingOn = tracer.mHitObject;
PtrHolder* ptrHolder = static_cast<PtrHolder*>(standingOn->getUserPointer());
if (ptrHolder)
actor.mStandingOn = ptrHolder->getPtr();
if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water)
physicActor->setWalkingOnWater(true);
if (!actor.mFlying)
newPosition.z() = tracer.mEndPos.z() + sGroundOffset;
if (!isActor(tracer.mHitObject))
{
isOnGround = true;
isOnSlope = !isWalkableSlope(tracer.mPlaneNormal);
isOnGround = true;
const btCollisionObject* standingOn = tracer.mHitObject;
PtrHolder* ptrHolder = static_cast<PtrHolder*>(standingOn->getUserPointer());
if (ptrHolder)
actor.mStandingOn = ptrHolder->getPtr();
isOnSlope = !isWalkableSlope(tracer.mPlaneNormal);
}
else
{
// standing on actors is not allowed (see above).
// in addition to that, apply a sliding effect away from the center of the actor,
// so that we do not stay suspended in air indefinitely.
if (tracer.mFraction < 1.0f && isActor(tracer.mHitObject))
{
if (osg::Vec3f(velocity.x(), velocity.y(), 0).length2() < 100.f*100.f)
if (standingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water)
physicActor->setWalkingOnWater(true);
if (!actor.mFlying && !isOnSlope)
{
btVector3 aabbMin, aabbMax;
tracer.mHitObject->getCollisionShape()->getAabb(tracer.mHitObject->getWorldTransform(), aabbMin, aabbMax);
btVector3 center = (aabbMin + aabbMax) / 2.f;
inertia = osg::Vec3f(actor.mPosition.x() - center.x(), actor.mPosition.y() - center.y(), 0);
inertia.normalize();
inertia *= 100;
if (tracer.mFraction*dropDistance > sGroundOffset)
newPosition.z() = tracer.mEndPos.z() + sGroundOffset;
else
{
newPosition.z() = tracer.mEndPos.z();
tracer.doTrace(colobj, newPosition, newPosition + osg::Vec3f(0, 0, 2*sGroundOffset), collisionWorld);
newPosition = (newPosition+tracer.mEndPos)/2.0;
}
}
}
else
{
// Vanilla allows actors to float on top of other actors. Do not push them off.
if (!actor.mFlying && isWalkableSlope(tracer.mPlaneNormal) && tracer.mEndPos.z()+sGroundOffset <= newPosition.z())
newPosition.z() = tracer.mEndPos.z() + sGroundOffset;
isOnGround = false;
isOnGround = false;
}
}
}
@ -298,7 +412,98 @@ namespace MWPhysics
physicActor->setOnGround(isOnGround);
physicActor->setOnSlope(isOnSlope);
newPosition.z() -= halfExtents.z(); // remove what was added at the beginning
actor.mPosition = newPosition;
// remove what was added earlier in compensating for doTrace not taking interior transformation into account
actor.mPosition.z() -= halfExtents.z(); // vanilla-accurate
}
btVector3 addMarginToDelta(btVector3 delta)
{
if(delta.length2() == 0.0)
return delta;
return delta + delta.normalized() * sCollisionMargin;
}
void MovementSolver::unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld)
{
const auto& ptr = actor.mActorRaw->getPtr();
if (!ptr.getClass().isMobile(ptr))
return;
auto* physicActor = actor.mActorRaw;
if(!physicActor->getCollisionMode()) // noclipping/tcl
return;
auto* collisionObject = physicActor->getCollisionObject();
auto tempPosition = actor.mPosition;
// use vanilla-accurate collision hull position hack (do same hitbox offset hack as movement solver)
// if vanilla compatibility didn't matter, the "correct" collision hull position would be physicActor->getScaledMeshTranslation()
const auto verticalHalfExtent = osg::Vec3f(0.0, 0.0, physicActor->getHalfExtents().z());
// use a 3d approximation of the movement vector to better judge player intent
const ESM::Position& refpos = ptr.getRefData().getPosition();
auto velocity = (osg::Quat(refpos.rot[0], osg::Vec3f(-1, 0, 0)) * osg::Quat(refpos.rot[2], osg::Vec3f(0, 0, -1))) * actor.mMovement;
// try to pop outside of the world before doing anything else if we're inside of it
if (!physicActor->getOnGround() || physicActor->getOnSlope())
velocity += physicActor->getInertialForce();
// because of the internal collision box offset hack, and the fact that we're moving the collision box manually,
// we need to replicate part of the collision box's transform process from scratch
osg::Vec3f refPosition = tempPosition + verticalHalfExtent;
osg::Vec3f goodPosition = refPosition;
const btTransform oldTransform = collisionObject->getWorldTransform();
btTransform newTransform = oldTransform;
auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback
{
goodPosition = refPosition + Misc::Convert::toOsg(addMarginToDelta(newOffset));
newTransform.setOrigin(Misc::Convert::toBullet(goodPosition));
collisionObject->setWorldTransform(newTransform);
ContactCollectionCallback callback{collisionObject, velocity};
ContactTestWrapper::contactTest(const_cast<btCollisionWorld*>(collisionWorld), collisionObject, callback);
return callback;
};
// check whether we're inside the world with our collision box with manually-derived offset
auto contactCallback = gatherContacts({0.0, 0.0, 0.0});
if(contactCallback.mDistance < -sAllowedPenetration)
{
// we are; try moving it out of the world
auto positionDelta = contactCallback.mContactSum;
// limit rejection delta to the largest known individual rejections
if(std::abs(positionDelta.x()) > contactCallback.mMaxX)
positionDelta *= contactCallback.mMaxX / std::abs(positionDelta.x());
if(std::abs(positionDelta.y()) > contactCallback.mMaxY)
positionDelta *= contactCallback.mMaxY / std::abs(positionDelta.y());
if(std::abs(positionDelta.z()) > contactCallback.mMaxZ)
positionDelta *= contactCallback.mMaxZ / std::abs(positionDelta.z());
auto contactCallback2 = gatherContacts(positionDelta);
// successfully moved further out from contact (does not have to be in open space, just less inside of things)
if(contactCallback2.mDistance > contactCallback.mDistance)
tempPosition = goodPosition - verticalHalfExtent;
// try again but only upwards (fixes some bad coc floors)
else
{
// upwards-only offset
auto contactCallback3 = gatherContacts({0.0, 0.0, std::abs(positionDelta.z())});
// success
if(contactCallback3.mDistance > contactCallback.mDistance)
tempPosition = goodPosition - verticalHalfExtent;
else
// try again but fixed distance up
{
auto contactCallback4 = gatherContacts({0.0, 0.0, 10.0});
// success
if(contactCallback4.mDistance > contactCallback.mDistance)
tempPosition = goodPosition - verticalHalfExtent;
}
}
}
collisionObject->setWorldTransform(oldTransform);
actor.mPosition = tempPosition;
}
}

@ -5,6 +5,9 @@
#include <osg/Vec3f>
#include "constants.hpp"
#include "../mwworld/ptr.hpp"
class btCollisionWorld;
namespace MWWorld
@ -14,29 +17,35 @@ namespace MWWorld
namespace MWPhysics
{
/// Vector projection
static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v)
{
return v * (u * v);
}
/// Vector rejection
static inline osg::Vec3f reject(const osg::Vec3f& direction, const osg::Vec3f &planeNormal)
{
return direction - project(direction, planeNormal);
}
template <class Vec3>
static bool isWalkableSlope(const Vec3 &normal)
{
static const float sMaxSlopeCos = std::cos(osg::DegreesToRadians(sMaxSlope));
return (normal.z() > sMaxSlopeCos);
}
class Actor;
struct ActorFrameData;
struct WorldFrameData;
class MovementSolver
{
private:
///Project a vector u on another vector v
static inline osg::Vec3f project(const osg::Vec3f& u, const osg::Vec3f &v)
{
return v * (u * v);
// ^ dot product
}
///Helper for computing the character sliding
static inline osg::Vec3f slide(const osg::Vec3f& direction, const osg::Vec3f &planeNormal)
{
return direction - project(direction, planeNormal);
}
public:
static osg::Vec3f traceDown(const MWWorld::Ptr &ptr, const osg::Vec3f& position, Actor* actor, btCollisionWorld* collisionWorld, float maxHeight);
static void move(ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, WorldFrameData& worldData);
static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld);
};
}

@ -13,6 +13,7 @@
#include "../mwworld/player.hpp"
#include "actor.hpp"
#include "contacttestwrapper.h"
#include "movementsolver.hpp"
#include "mtphysics.hpp"
#include "object.hpp"
@ -151,6 +152,9 @@ namespace MWPhysics
, mNextLOS(0)
, mFrameNumber(0)
, mTimer(osg::Timer::instance())
, mTimeBegin(0)
, mTimeEnd(0)
, mFrameStart(0)
{
mNumThreads = Config::computeNumThreads(mThreadSafeBullet);
@ -167,7 +171,16 @@ namespace MWPhysics
mPreStepBarrier = std::make_unique<Misc::Barrier>(mNumThreads, [&]()
{
if (mDeferAabbUpdate)
updateAabbs();
if (!mRemainingSteps)
return;
for (auto& data : mActorsFrameData)
if (data.mActor.lock())
{
std::unique_lock lock(mCollisionWorldMutex);
MovementSolver::unstuck(data, mCollisionWorld.get());
}
});
mPostStepBarrier = std::make_unique<Misc::Barrier>(mNumThreads, [&]()
@ -295,7 +308,7 @@ namespace MWPhysics
void PhysicsTaskScheduler::contactTest(btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback)
{
std::shared_lock lock(mCollisionWorldMutex);
mCollisionWorld->contactTest(colObj, resultCallback);
ContactTestWrapper::contactTest(mCollisionWorld.get(), colObj, resultCallback);
}
std::optional<btVector3> PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target)
@ -349,7 +362,6 @@ namespace MWPhysics
{
if (!mDeferAabbUpdate || immediate)
{
std::unique_lock lock(mCollisionWorldMutex);
updatePtrAabb(ptr);
}
else
@ -402,7 +414,7 @@ namespace MWPhysics
void PhysicsTaskScheduler::updateAabbs()
{
std::scoped_lock lock(mCollisionWorldMutex, mUpdateAabbMutex);
std::scoped_lock lock(mUpdateAabbMutex);
std::for_each(mUpdateAabb.begin(), mUpdateAabb.end(),
[this](const std::weak_ptr<PtrHolder>& ptr) { updatePtrAabb(ptr); });
mUpdateAabb.clear();
@ -412,6 +424,7 @@ namespace MWPhysics
{
if (const auto p = ptr.lock())
{
std::scoped_lock lock(mCollisionWorldMutex);
if (const auto actor = std::dynamic_pointer_cast<Actor>(p))
{
actor->updateCollisionObjectPosition();
@ -438,15 +451,16 @@ namespace MWPhysics
if (!mNewFrame)
mHasJob.wait(lock, [&]() { return mQuit || mNewFrame; });
if (mDeferAabbUpdate)
mPreStepBarrier->wait();
mPreStepBarrier->wait();
int job = 0;
while (mRemainingSteps && (job = mNextJob.fetch_add(1, std::memory_order_relaxed)) < mNumJobs)
{
MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet);
if(const auto actor = mActorsFrameData[job].mActor.lock())
{
MaybeSharedLock lockColWorld(mCollisionWorldMutex, mThreadSafeBullet);
MovementSolver::move(mActorsFrameData[job], mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData);
}
}
mPostStepBarrier->wait();
@ -471,13 +485,13 @@ namespace MWPhysics
void PhysicsTaskScheduler::updateActorsPositions()
{
std::unique_lock lock(mCollisionWorldMutex);
for (auto& actorData : mActorsFrameData)
{
if(const auto actor = actorData.mActor.lock())
{
if (actor->setPosition(actorData.mPosition))
{
std::scoped_lock lock(mCollisionWorldMutex);
actor->updateCollisionObjectPosition();
mCollisionWorld->updateSingleAabb(actor->getCollisionObject());
}
@ -505,7 +519,10 @@ namespace MWPhysics
while (mRemainingSteps--)
{
for (auto& actorData : mActorsFrameData)
{
MovementSolver::unstuck(actorData, mCollisionWorld.get());
MovementSolver::move(actorData, mPhysicsDt, mCollisionWorld.get(), *mWorldFrameData);
}
updateActorsPositions();
}
@ -523,6 +540,8 @@ namespace MWPhysics
void PhysicsTaskScheduler::updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats)
{
if (!stats.collectStats("engine"))
return;
if (mFrameNumber == frameNumber - 1)
{
stats.setAttribute(mFrameNumber, "physicsworker_time_begin", mTimer->delta_s(mFrameStart, mTimeBegin));

@ -14,29 +14,29 @@
namespace MWPhysics
{
Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance, PhysicsTaskScheduler* scheduler)
Object::Object(const MWWorld::Ptr& ptr, osg::ref_ptr<Resource::BulletShapeInstance> shapeInstance, int collisionType, PhysicsTaskScheduler* scheduler)
: mShapeInstance(shapeInstance)
, mSolid(true)
, mTaskScheduler(scheduler)
{
mPtr = ptr;
mCollisionObject.reset(new btCollisionObject);
mCollisionObject = std::make_unique<btCollisionObject>();
mCollisionObject->setCollisionShape(shapeInstance->getCollisionShape());
mCollisionObject->setUserPointer(this);
setScale(ptr.getCellRef().getScale());
setRotation(Misc::Convert::toBullet(ptr.getRefData().getBaseNode()->getAttitude()));
const float* pos = ptr.getRefData().getPosition().pos;
setOrigin(btVector3(pos[0], pos[1], pos[2]));
setOrigin(Misc::Convert::toBullet(ptr.getRefData().getPosition().asVec3()));
commitPositionChange();
mTaskScheduler->addCollisionObject(mCollisionObject.get(), collisionType, CollisionType_Actor|CollisionType_HeightMap|CollisionType_Projectile);
}
Object::~Object()
{
if (mCollisionObject)
mTaskScheduler->removeCollisionObject(mCollisionObject.get());
mTaskScheduler->removeCollisionObject(mCollisionObject.get());
}
const Resource::BulletShapeInstance* Object::getShapeInstance() const

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save