mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-01-21 10:53:51 +00:00
Merge remote-tracking branch 'refs/remotes/upstream/master'
This commit is contained in:
commit
759e6fb804
45 changed files with 2095 additions and 179 deletions
|
@ -6,4 +6,6 @@ DATE=`date +'%d%m%Y'`
|
||||||
SHORT_COMMIT=`git rev-parse --short ${TRAVIS_COMMIT}`
|
SHORT_COMMIT=`git rev-parse --short ${TRAVIS_COMMIT}`
|
||||||
TARGET_FILENAME="OpenMW-${DATE}-${SHORT_COMMIT}.dmg"
|
TARGET_FILENAME="OpenMW-${DATE}-${SHORT_COMMIT}.dmg"
|
||||||
|
|
||||||
curl --ssl --ftp-create-dirs -T *.dmg -u $OSX_FTP_USER:$OSX_FTP_PASSWORD "${OSX_FTP_URL}${TARGET_FILENAME}"
|
if ! curl --ssl -u $OSX_FTP_USER:$OSX_FTP_PASSWORD "${OSX_FTP_URL}" --silent | grep $SHORT_COMMIT > /dev/null; then
|
||||||
|
curl --ssl --ftp-create-dirs -T *.dmg -u $OSX_FTP_USER:$OSX_FTP_PASSWORD "${OSX_FTP_URL}${TARGET_FILENAME}"
|
||||||
|
fi
|
||||||
|
|
|
@ -360,6 +360,8 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-but-set-parameter")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-but-set-parameter")
|
||||||
endif()
|
endif()
|
||||||
elseif (MSVC)
|
elseif (MSVC)
|
||||||
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} /Zi /bigobj")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:REF /OPT:ICF /INCREMENTAL:NO")
|
||||||
# Enable link-time code generation globally for all linking
|
# Enable link-time code generation globally for all linking
|
||||||
if (OPENMW_LTO_BUILD)
|
if (OPENMW_LTO_BUILD)
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /GL")
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /GL")
|
||||||
|
|
|
@ -23,6 +23,7 @@ bool Launcher::AdvancedPage::loadSettings()
|
||||||
loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game");
|
loadSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game");
|
||||||
loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game");
|
loadSettingBool(showMeleeInfoCheckBox, "show melee info", "Game");
|
||||||
loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game");
|
loadSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game");
|
||||||
|
loadSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game");
|
||||||
|
|
||||||
// Expected values are (0, 1, 2, 3)
|
// Expected values are (0, 1, 2, 3)
|
||||||
int showOwnedIndex = mEngineSettings.getInt("show owned", "Game");
|
int showOwnedIndex = mEngineSettings.getInt("show owned", "Game");
|
||||||
|
@ -61,6 +62,7 @@ void Launcher::AdvancedPage::saveSettings()
|
||||||
saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game");
|
saveSettingBool(showEnchantChanceCheckBox, "show enchant chance", "Game");
|
||||||
saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game");
|
saveSettingBool(showMeleeInfoCheckBox, "show melee info", "Game");
|
||||||
saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game");
|
saveSettingBool(showProjectileDamageCheckBox, "show projectile damage", "Game");
|
||||||
|
saveSettingBool(rebalanceSoulGemValuesCheckBox, "rebalance soul gem values", "Game");
|
||||||
|
|
||||||
int showOwnedCurrentIndex = showOwnedComboBox->currentIndex();
|
int showOwnedCurrentIndex = showOwnedComboBox->currentIndex();
|
||||||
if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game"))
|
if (showOwnedCurrentIndex != mEngineSettings.getInt("show owned", "Game"))
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <components/misc/stringops.hpp>
|
#include <components/misc/stringops.hpp>
|
||||||
|
#include <components/esm/esmreader.hpp>
|
||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <boost/filesystem/fstream.hpp>
|
#include <boost/filesystem/fstream.hpp>
|
||||||
|
@ -653,12 +654,6 @@ void MwIniImporter::setVerbose(bool verbose) {
|
||||||
mVerbose = verbose;
|
mVerbose = verbose;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string MwIniImporter::numberToString(int n) {
|
|
||||||
std::stringstream str;
|
|
||||||
str << n;
|
|
||||||
return str.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
MwIniImporter::multistrmap MwIniImporter::loadIniFile(const boost::filesystem::path& filename) const {
|
MwIniImporter::multistrmap MwIniImporter::loadIniFile(const boost::filesystem::path& filename) const {
|
||||||
std::cout << "load ini file: " << filename << std::endl;
|
std::cout << "load ini file: " << filename << std::endl;
|
||||||
|
|
||||||
|
@ -800,7 +795,7 @@ void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) con
|
||||||
multistrmap::const_iterator it = ini.begin();
|
multistrmap::const_iterator it = ini.begin();
|
||||||
for(int i=0; it != ini.end(); i++) {
|
for(int i=0; it != ini.end(); i++) {
|
||||||
archive = baseArchive;
|
archive = baseArchive;
|
||||||
archive.append(this->numberToString(i));
|
archive.append(std::to_string(i));
|
||||||
|
|
||||||
it = ini.find(archive);
|
it = ini.find(archive);
|
||||||
if(it == ini.end()) {
|
if(it == ini.end()) {
|
||||||
|
@ -824,33 +819,105 @@ void MwIniImporter::importArchives(multistrmap &cfg, const multistrmap &ini) con
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, const boost::filesystem::path& iniFilename) const {
|
void MwIniImporter::dependencySortStep(std::string& element, MwIniImporter::dependencyList& source, std::vector<std::string>& result)
|
||||||
std::vector<std::pair<std::time_t, std::string> > contentFiles;
|
{
|
||||||
|
auto iter = std::find_if(
|
||||||
|
source.begin(),
|
||||||
|
source.end(),
|
||||||
|
[&element](std::pair< std::string, std::vector<std::string> >& sourceElement)
|
||||||
|
{
|
||||||
|
return sourceElement.first == element;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (iter != source.end())
|
||||||
|
{
|
||||||
|
auto foundElement = std::move(*iter);
|
||||||
|
source.erase(iter);
|
||||||
|
for (auto name : foundElement.second)
|
||||||
|
{
|
||||||
|
MwIniImporter::dependencySortStep(name, source, result);
|
||||||
|
}
|
||||||
|
result.push_back(std::move(foundElement.first));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> MwIniImporter::dependencySort(MwIniImporter::dependencyList source)
|
||||||
|
{
|
||||||
|
std::vector<std::string> result;
|
||||||
|
while (!source.empty())
|
||||||
|
{
|
||||||
|
MwIniImporter::dependencySortStep(source.begin()->first, source, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string>::iterator MwIniImporter::findString(std::vector<std::string>& source, const std::string& string)
|
||||||
|
{
|
||||||
|
return std::find_if(source.begin(), source.end(), [&string](const std::string& sourceString)
|
||||||
|
{
|
||||||
|
return Misc::StringUtils::ciEqual(sourceString, string);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MwIniImporter::addPaths(std::vector<boost::filesystem::path>& output, std::vector<std::string> input) {
|
||||||
|
for (auto& path : input) {
|
||||||
|
if (path.front() == '"')
|
||||||
|
{
|
||||||
|
path.erase(path.begin());
|
||||||
|
path.erase(path.end() - 1);
|
||||||
|
}
|
||||||
|
output.emplace_back(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, const boost::filesystem::path& iniFilename) const
|
||||||
|
{
|
||||||
|
std::vector<std::pair<std::time_t, boost::filesystem::path>> contentFiles;
|
||||||
std::string baseGameFile("Game Files:GameFile");
|
std::string baseGameFile("Game Files:GameFile");
|
||||||
std::string gameFile("");
|
std::string gameFile("");
|
||||||
std::time_t defaultTime = 0;
|
std::time_t defaultTime = 0;
|
||||||
|
ToUTF8::Utf8Encoder encoder(mEncoding);
|
||||||
|
|
||||||
// assume the Game Files are all in a "Data Files" directory under the directory holding Morrowind.ini
|
std::vector<boost::filesystem::path> dataPaths;
|
||||||
const boost::filesystem::path gameFilesDir(iniFilename.parent_path() /= "Data Files");
|
if (cfg.count("data"))
|
||||||
|
addPaths(dataPaths, cfg["data"]);
|
||||||
|
|
||||||
|
if (cfg.count("data-local"))
|
||||||
|
addPaths(dataPaths, cfg["data-local"]);
|
||||||
|
|
||||||
|
dataPaths.push_back(iniFilename.parent_path() /= "Data Files");
|
||||||
|
|
||||||
multistrmap::const_iterator it = ini.begin();
|
multistrmap::const_iterator it = ini.begin();
|
||||||
for(int i=0; it != ini.end(); i++) {
|
for (int i=0; it != ini.end(); i++)
|
||||||
|
{
|
||||||
gameFile = baseGameFile;
|
gameFile = baseGameFile;
|
||||||
gameFile.append(this->numberToString(i));
|
gameFile.append(std::to_string(i));
|
||||||
|
|
||||||
it = ini.find(gameFile);
|
it = ini.find(gameFile);
|
||||||
if(it == ini.end()) {
|
if(it == ini.end())
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
for(std::vector<std::string>::const_iterator entry = it->second.begin(); entry!=it->second.end(); ++entry) {
|
for(std::vector<std::string>::const_iterator entry = it->second.begin(); entry!=it->second.end(); ++entry)
|
||||||
|
{
|
||||||
std::string filetype(entry->substr(entry->length()-3));
|
std::string filetype(entry->substr(entry->length()-3));
|
||||||
Misc::StringUtils::lowerCaseInPlace(filetype);
|
Misc::StringUtils::lowerCaseInPlace(filetype);
|
||||||
|
|
||||||
if(filetype.compare("esm") == 0 || filetype.compare("esp") == 0) {
|
if(filetype.compare("esm") == 0 || filetype.compare("esp") == 0)
|
||||||
boost::filesystem::path filepath(gameFilesDir);
|
{
|
||||||
filepath /= *entry;
|
bool found = false;
|
||||||
contentFiles.push_back(std::make_pair(lastWriteTime(filepath, defaultTime), *entry));
|
for (auto & dataPath : dataPaths)
|
||||||
|
{
|
||||||
|
boost::filesystem::path path = dataPath / *entry;
|
||||||
|
std::time_t time = lastWriteTime(path, defaultTime);
|
||||||
|
if (time != defaultTime)
|
||||||
|
{
|
||||||
|
contentFiles.push_back({time, path});
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found)
|
||||||
|
std::cout << "Warning: " << *entry << " not found, ignoring" << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -858,11 +925,46 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co
|
||||||
cfg.erase("content");
|
cfg.erase("content");
|
||||||
cfg.insert( std::make_pair("content", std::vector<std::string>() ) );
|
cfg.insert( std::make_pair("content", std::vector<std::string>() ) );
|
||||||
|
|
||||||
// this will sort files by time order first, then alphabetical (maybe), I suspect non ASCII filenames will be stuffed.
|
// sort by timestamp
|
||||||
sort(contentFiles.begin(), contentFiles.end());
|
sort(contentFiles.begin(), contentFiles.end());
|
||||||
for(std::vector<std::pair<std::time_t, std::string> >::const_iterator iter=contentFiles.begin(); iter!=contentFiles.end(); ++iter) {
|
|
||||||
cfg["content"].push_back(iter->second);
|
MwIniImporter::dependencyList unsortedFiles;
|
||||||
|
|
||||||
|
ESM::ESMReader reader;
|
||||||
|
reader.setEncoder(&encoder);
|
||||||
|
for (auto& file : contentFiles)
|
||||||
|
{
|
||||||
|
reader.open(file.second.string());
|
||||||
|
std::vector<std::string> dependencies;
|
||||||
|
for (auto& gameFile : reader.getGameFiles())
|
||||||
|
{
|
||||||
|
dependencies.push_back(gameFile.name);
|
||||||
|
}
|
||||||
|
unsortedFiles.emplace_back(boost::filesystem::path(reader.getName()).filename().string(), dependencies);
|
||||||
|
reader.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto sortedFiles = dependencySort(unsortedFiles);
|
||||||
|
|
||||||
|
// hard-coded dependency Morrowind - Tribunal - Bloodmoon
|
||||||
|
if(findString(sortedFiles, "Morrowind.esm") != sortedFiles.end())
|
||||||
|
{
|
||||||
|
auto tribunalIter = findString(sortedFiles, "Tribunal.esm");
|
||||||
|
auto bloodmoonIter = findString(sortedFiles, "Bloodmoon.esm");
|
||||||
|
|
||||||
|
if (bloodmoonIter != sortedFiles.end() && tribunalIter != sortedFiles.end())
|
||||||
|
{
|
||||||
|
size_t bloodmoonIndex = std::distance(sortedFiles.begin(), bloodmoonIter);
|
||||||
|
size_t tribunalIndex = std::distance(sortedFiles.begin(), tribunalIter);
|
||||||
|
if (bloodmoonIndex < tribunalIndex)
|
||||||
|
tribunalIndex++;
|
||||||
|
sortedFiles.insert(bloodmoonIter, *tribunalIter);
|
||||||
|
sortedFiles.erase(sortedFiles.begin() + tribunalIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& file : sortedFiles)
|
||||||
|
cfg["content"].push_back(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MwIniImporter::writeToFile(std::ostream &out, const multistrmap &cfg) {
|
void MwIniImporter::writeToFile(std::ostream &out, const multistrmap &cfg) {
|
||||||
|
@ -901,9 +1003,5 @@ std::time_t MwIniImporter::lastWriteTime(const boost::filesystem::path& filename
|
||||||
std::cout << "content file: " << resolved << " timestamp = (" << writeTime <<
|
std::cout << "content file: " << resolved << " timestamp = (" << writeTime <<
|
||||||
") " << timeStrBuffer << std::endl;
|
") " << timeStrBuffer << std::endl;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
std::cout << "content file: " << filename << " not found" << std::endl;
|
|
||||||
}
|
|
||||||
return writeTime;
|
return writeTime;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ class MwIniImporter {
|
||||||
public:
|
public:
|
||||||
typedef std::map<std::string, std::string> strmap;
|
typedef std::map<std::string, std::string> strmap;
|
||||||
typedef std::map<std::string, std::vector<std::string> > multistrmap;
|
typedef std::map<std::string, std::vector<std::string> > multistrmap;
|
||||||
|
typedef std::vector< std::pair< std::string, std::vector<std::string> > > dependencyList;
|
||||||
|
|
||||||
MwIniImporter();
|
MwIniImporter();
|
||||||
void setInputEncoding(const ToUTF8::FromType& encoding);
|
void setInputEncoding(const ToUTF8::FromType& encoding);
|
||||||
|
@ -27,9 +28,14 @@ class MwIniImporter {
|
||||||
void importArchives(multistrmap &cfg, const multistrmap &ini) const;
|
void importArchives(multistrmap &cfg, const multistrmap &ini) const;
|
||||||
static void writeToFile(std::ostream &out, const multistrmap &cfg);
|
static void writeToFile(std::ostream &out, const multistrmap &cfg);
|
||||||
|
|
||||||
|
static std::vector<std::string> dependencySort(MwIniImporter::dependencyList source);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
static void dependencySortStep(std::string& element, MwIniImporter::dependencyList& source, std::vector<std::string>& result);
|
||||||
|
static std::vector<std::string>::iterator findString(std::vector<std::string>& source, const std::string& string);
|
||||||
|
|
||||||
static void insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value);
|
static void insertMultistrmap(multistrmap &cfg, const std::string& key, const std::string& value);
|
||||||
static std::string numberToString(int n);
|
static void addPaths(std::vector<boost::filesystem::path>& output, std::vector<std::string> input);
|
||||||
|
|
||||||
/// \return file's "last modified time", used in original MW to determine plug-in load order
|
/// \return file's "last modified time", used in original MW to determine plug-in load order
|
||||||
static std::time_t lastWriteTime(const boost::filesystem::path& filename, std::time_t defaultTime);
|
static std::time_t lastWriteTime(const boost::filesystem::path& filename, std::time_t defaultTime);
|
||||||
|
@ -40,5 +46,4 @@ class MwIniImporter {
|
||||||
ToUTF8::FromType mEncoding;
|
ToUTF8::FromType mEncoding;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -81,14 +81,14 @@ opencs_units_noqt (view/world
|
||||||
|
|
||||||
opencs_units (view/widget
|
opencs_units (view/widget
|
||||||
scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton
|
scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton
|
||||||
scenetooltoggle2 completerpopup coloreditor colorpickerpopup droplineedit
|
scenetooltoggle2 scenetooltexturebrush completerpopup coloreditor colorpickerpopup droplineedit
|
||||||
)
|
)
|
||||||
|
|
||||||
opencs_units (view/render
|
opencs_units (view/render
|
||||||
scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget
|
scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget
|
||||||
previewwidget editmode instancemode instanceselectionmode instancemovemode
|
previewwidget editmode instancemode instanceselectionmode instancemovemode
|
||||||
orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller
|
orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller
|
||||||
cellwater
|
cellwater terraintexturemode
|
||||||
)
|
)
|
||||||
|
|
||||||
opencs_units_noqt (view/render
|
opencs_units_noqt (view/render
|
||||||
|
|
|
@ -222,7 +222,15 @@ void CSMPrefs::State::declare()
|
||||||
EnumValues insertOutsideVisibleCell;
|
EnumValues insertOutsideVisibleCell;
|
||||||
insertOutsideVisibleCell.add (showAndInsert).add (dontInsert).add (insertAnyway);
|
insertOutsideVisibleCell.add (showAndInsert).add (dontInsert).add (insertAnyway);
|
||||||
|
|
||||||
declareCategory ("Scene Drops");
|
EnumValue createAndLandEdit ("Create cell and land, then edit");
|
||||||
|
EnumValue showAndLandEdit ("Show cell and edit");
|
||||||
|
EnumValue dontLandEdit ("Discard");
|
||||||
|
EnumValues landeditOutsideCell;
|
||||||
|
landeditOutsideCell.add (createAndLandEdit).add (dontLandEdit);
|
||||||
|
EnumValues landeditOutsideVisibleCell;
|
||||||
|
landeditOutsideVisibleCell.add (showAndLandEdit).add (dontLandEdit);
|
||||||
|
|
||||||
|
declareCategory ("3D Scene Editing");
|
||||||
declareInt ("distance", "Drop Distance", 50).
|
declareInt ("distance", "Drop Distance", 50).
|
||||||
setTooltip ("If an instance drop can not be placed against another object at the "
|
setTooltip ("If an instance drop can not be placed against another object at the "
|
||||||
"insert point, it will be placed by this distance from the insert point instead");
|
"insert point, it will be placed by this distance from the insert point instead");
|
||||||
|
@ -230,6 +238,12 @@ void CSMPrefs::State::declare()
|
||||||
addValues (insertOutsideCell);
|
addValues (insertOutsideCell);
|
||||||
declareEnum ("outside-visible-drop", "Handling drops outside of visible cells", showAndInsert).
|
declareEnum ("outside-visible-drop", "Handling drops outside of visible cells", showAndInsert).
|
||||||
addValues (insertOutsideVisibleCell);
|
addValues (insertOutsideVisibleCell);
|
||||||
|
declareEnum ("outside-landedit", "Handling land edit outside of cells", createAndLandEdit).
|
||||||
|
addValues (landeditOutsideCell);
|
||||||
|
declareEnum ("outside-visible-landedit", "Handling land edit outside of visible cells", showAndLandEdit).
|
||||||
|
addValues (landeditOutsideVisibleCell);
|
||||||
|
declareInt ("texturebrush-maximumsize", "Maximum texture brush size", 50).
|
||||||
|
setMin (1);
|
||||||
|
|
||||||
declareCategory ("Key Bindings");
|
declareCategory ("Key Bindings");
|
||||||
|
|
||||||
|
|
|
@ -28,9 +28,13 @@ void CSMTools::ClassCheckStage::perform (int stage, CSMDoc::Messages& messages)
|
||||||
|
|
||||||
CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Class, class_.mId);
|
CSMWorld::UniversalId id (CSMWorld::UniversalId::Type_Class, class_.mId);
|
||||||
|
|
||||||
// test for empty name and description
|
// A class should have a name
|
||||||
if (class_.mName.empty())
|
if (class_.mName.empty())
|
||||||
messages.push_back (std::make_pair (id, class_.mId + " has an empty name"));
|
messages.push_back (std::make_pair (id, class_.mId + " doesn't have a name"));
|
||||||
|
|
||||||
|
// A playable class should have a description
|
||||||
|
if (class_.mData.mIsPlayable != 0 && class_.mDescription.empty())
|
||||||
|
messages.push_back (std::make_pair (id, class_.mId + " doesn't have a description and it's playable"));
|
||||||
|
|
||||||
// test for invalid attributes
|
// test for invalid attributes
|
||||||
for (int i=0; i<2; ++i)
|
for (int i=0; i<2; ++i)
|
||||||
|
|
|
@ -11,11 +11,6 @@ namespace CSMWorld
|
||||||
|
|
||||||
bool LandTextureTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const
|
bool LandTextureTableProxyModel::filterAcceptsRow (int sourceRow, const QModelIndex& sourceParent) const
|
||||||
{
|
{
|
||||||
int columnIndex = mSourceModel->findColumnIndex(Columns::ColumnId_Modification);
|
|
||||||
QModelIndex index = mSourceModel->index(sourceRow, columnIndex);
|
|
||||||
if (mSourceModel->data(index).toInt() != RecordBase::State_ModifiedOnly)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return IdTableProxyModel::filterAcceptsRow(sourceRow, sourceParent);
|
return IdTableProxyModel::filterAcceptsRow(sourceRow, sourceParent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -551,7 +551,7 @@ void CSVRender::InstanceMode::dropEvent (QDropEvent* event)
|
||||||
|
|
||||||
if (noCell)
|
if (noCell)
|
||||||
{
|
{
|
||||||
std::string mode = CSMPrefs::get()["Scene Drops"]["outside-drop"].toString();
|
std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-drop"].toString();
|
||||||
|
|
||||||
// target cell does not exist
|
// target cell does not exist
|
||||||
if (mode=="Discard")
|
if (mode=="Discard")
|
||||||
|
@ -585,7 +585,7 @@ void CSVRender::InstanceMode::dropEvent (QDropEvent* event)
|
||||||
{
|
{
|
||||||
// target cell exists, but is not shown
|
// target cell exists, but is not shown
|
||||||
std::string mode =
|
std::string mode =
|
||||||
CSMPrefs::get()["Scene Drops"]["outside-visible-drop"].toString();
|
CSMPrefs::get()["3D Scene Editing"]["outside-visible-drop"].toString();
|
||||||
|
|
||||||
if (mode=="Discard")
|
if (mode=="Discard")
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
#include "mask.hpp"
|
#include "mask.hpp"
|
||||||
#include "cameracontroller.hpp"
|
#include "cameracontroller.hpp"
|
||||||
#include "cellarrow.hpp"
|
#include "cellarrow.hpp"
|
||||||
|
#include "terraintexturemode.hpp"
|
||||||
|
|
||||||
bool CSVRender::PagedWorldspaceWidget::adjustCells()
|
bool CSVRender::PagedWorldspaceWidget::adjustCells()
|
||||||
{
|
{
|
||||||
|
@ -136,7 +137,7 @@ void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons (
|
||||||
new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain shape editing"),
|
new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain shape editing"),
|
||||||
"terrain-shape");
|
"terrain-shape");
|
||||||
tool->addButton (
|
tool->addButton (
|
||||||
new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain texture editing"),
|
new TerrainTextureMode (this, tool),
|
||||||
"terrain-texture");
|
"terrain-texture");
|
||||||
tool->addButton (
|
tool->addButton (
|
||||||
new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain vertex paint editing"),
|
new EditMode (this, QIcon (":placeholder"), Mask_Reference, "Terrain vertex paint editing"),
|
||||||
|
|
542
apps/opencs/view/render/terraintexturemode.cpp
Normal file
542
apps/opencs/view/render/terraintexturemode.cpp
Normal file
|
@ -0,0 +1,542 @@
|
||||||
|
#include "terraintexturemode.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QDropEvent>
|
||||||
|
#include <QDragEnterEvent>
|
||||||
|
#include <QDrag>
|
||||||
|
|
||||||
|
#include <components/esm/loadland.hpp>
|
||||||
|
|
||||||
|
#include "../widget/modebutton.hpp"
|
||||||
|
#include "../widget/scenetoolbar.hpp"
|
||||||
|
#include "../widget/scenetooltexturebrush.hpp"
|
||||||
|
|
||||||
|
#include "../../model/doc/document.hpp"
|
||||||
|
#include "../../model/prefs/state.hpp"
|
||||||
|
#include "../../model/world/columnbase.hpp"
|
||||||
|
#include "../../model/world/commandmacro.hpp"
|
||||||
|
#include "../../model/world/commands.hpp"
|
||||||
|
#include "../../model/world/data.hpp"
|
||||||
|
#include "../../model/world/idtable.hpp"
|
||||||
|
#include "../../model/world/idtree.hpp"
|
||||||
|
#include "../../model/world/land.hpp"
|
||||||
|
#include "../../model/world/landtexture.hpp"
|
||||||
|
#include "../../model/world/resourcetable.hpp"
|
||||||
|
#include "../../model/world/tablemimedata.hpp"
|
||||||
|
#include "../../model/world/universalid.hpp"
|
||||||
|
|
||||||
|
#include "editmode.hpp"
|
||||||
|
#include "pagedworldspacewidget.hpp"
|
||||||
|
#include "mask.hpp"
|
||||||
|
#include "object.hpp" // Something small needed regarding pointers from here ()
|
||||||
|
#include "worldspacewidget.hpp"
|
||||||
|
|
||||||
|
CSVRender::TerrainTextureMode::TerrainTextureMode (WorldspaceWidget *worldspaceWidget, QWidget *parent)
|
||||||
|
: EditMode (worldspaceWidget, QIcon {":scenetoolbar/editing-terrain-texture"}, Mask_Terrain | Mask_Reference, "Terrain texture editing", parent),
|
||||||
|
mBrushTexture("L0#0"),
|
||||||
|
mBrushSize(0),
|
||||||
|
mBrushShape(0),
|
||||||
|
mTextureBrushScenetool(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::activate(CSVWidget::SceneToolbar* toolbar)
|
||||||
|
{
|
||||||
|
if(!mTextureBrushScenetool)
|
||||||
|
{
|
||||||
|
mTextureBrushScenetool = new CSVWidget::SceneToolTextureBrush (toolbar, "scenetooltexturebrush", getWorldspaceWidget().getDocument());
|
||||||
|
connect(mTextureBrushScenetool, SIGNAL (clicked()), mTextureBrushScenetool, SLOT (activate()));
|
||||||
|
connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passBrushSize(int)), this, SLOT(setBrushSize(int)));
|
||||||
|
connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passBrushShape(int)), this, SLOT(setBrushShape(int)));
|
||||||
|
connect(mTextureBrushScenetool->mTextureBrushWindow->mSizeSliders->mBrushSizeSlider, SIGNAL(valueChanged(int)), this, SLOT(setBrushSize(int)));
|
||||||
|
connect(mTextureBrushScenetool, SIGNAL(passTextureId(std::string)), this, SLOT(setBrushTexture(std::string)));
|
||||||
|
connect(mTextureBrushScenetool->mTextureBrushWindow, SIGNAL(passTextureId(std::string)), this, SLOT(setBrushTexture(std::string)));
|
||||||
|
|
||||||
|
connect(mTextureBrushScenetool, SIGNAL(passEvent(QDropEvent*)), this, SLOT(handleDropEvent(QDropEvent*)));
|
||||||
|
connect(this, SIGNAL(passBrushTexture(std::string)), mTextureBrushScenetool->mTextureBrushWindow, SLOT(setBrushTexture(std::string)));
|
||||||
|
connect(this, SIGNAL(passBrushTexture(std::string)), mTextureBrushScenetool, SLOT(updateBrushHistory(std::string)));
|
||||||
|
}
|
||||||
|
|
||||||
|
EditMode::activate(toolbar);
|
||||||
|
toolbar->addTool (mTextureBrushScenetool);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::deactivate(CSVWidget::SceneToolbar* toolbar)
|
||||||
|
{
|
||||||
|
if(mTextureBrushScenetool)
|
||||||
|
{
|
||||||
|
toolbar->removeTool (mTextureBrushScenetool);
|
||||||
|
delete mTextureBrushScenetool;
|
||||||
|
mTextureBrushScenetool = 0;
|
||||||
|
}
|
||||||
|
EditMode::deactivate(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::primaryEditPressed(const WorldspaceHitResult& hit) // Apply changes here
|
||||||
|
{
|
||||||
|
CSMDoc::Document& document = getWorldspaceWidget().getDocument();
|
||||||
|
CSMWorld::IdTable& landTable = dynamic_cast<CSMWorld::IdTable&> (
|
||||||
|
*document.getData().getTableModel (CSMWorld::UniversalId::Type_Land));
|
||||||
|
CSMWorld::IdTable& ltexTable = dynamic_cast<CSMWorld::IdTable&> (
|
||||||
|
*document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures));
|
||||||
|
|
||||||
|
mCellId = getWorldspaceWidget().getCellId (hit.worldPos);
|
||||||
|
|
||||||
|
QUndoStack& undoStack = document.getUndoStack();
|
||||||
|
CSMWorld::IdCollection<CSMWorld::LandTexture>& landtexturesCollection = document.getData().getLandTextures();
|
||||||
|
int index = landtexturesCollection.searchId(mBrushTexture);
|
||||||
|
|
||||||
|
if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit == true)
|
||||||
|
{
|
||||||
|
undoStack.beginMacro ("Edit texture records");
|
||||||
|
if(allowLandTextureEditing(mCellId)==true)
|
||||||
|
{
|
||||||
|
undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId));
|
||||||
|
editTerrainTextureGrid(hit);
|
||||||
|
}
|
||||||
|
undoStack.endMacro();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::primarySelectPressed(const WorldspaceHitResult& hit)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::secondarySelectPressed(const WorldspaceHitResult& hit)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CSVRender::TerrainTextureMode::primaryEditStartDrag (const QPoint& pos)
|
||||||
|
{
|
||||||
|
WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask());
|
||||||
|
|
||||||
|
CSMDoc::Document& document = getWorldspaceWidget().getDocument();
|
||||||
|
CSMWorld::IdTable& landTable = dynamic_cast<CSMWorld::IdTable&> (
|
||||||
|
*document.getData().getTableModel (CSMWorld::UniversalId::Type_Land));
|
||||||
|
CSMWorld::IdTable& ltexTable = dynamic_cast<CSMWorld::IdTable&> (
|
||||||
|
*document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures));
|
||||||
|
|
||||||
|
mCellId = getWorldspaceWidget().getCellId (hit.worldPos);
|
||||||
|
|
||||||
|
QUndoStack& undoStack = document.getUndoStack();
|
||||||
|
|
||||||
|
CSMWorld::IdCollection<CSMWorld::LandTexture>& landtexturesCollection = document.getData().getLandTextures();
|
||||||
|
int index = landtexturesCollection.searchId(mBrushTexture);
|
||||||
|
|
||||||
|
if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted())
|
||||||
|
{
|
||||||
|
undoStack.beginMacro ("Edit texture records");
|
||||||
|
if(allowLandTextureEditing(mCellId)==true && hit.hit == true)
|
||||||
|
{
|
||||||
|
undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId));
|
||||||
|
editTerrainTextureGrid(hit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CSVRender::TerrainTextureMode::secondaryEditStartDrag (const QPoint& pos)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CSVRender::TerrainTextureMode::primarySelectStartDrag (const QPoint& pos)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CSVRender::TerrainTextureMode::secondarySelectStartDrag (const QPoint& pos)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::drag (const QPoint& pos, int diffX, int diffY, double speedFactor)
|
||||||
|
{
|
||||||
|
WorldspaceHitResult hit = getWorldspaceWidget().mousePick (pos, getWorldspaceWidget().getInteractionMask());
|
||||||
|
CSMDoc::Document& document = getWorldspaceWidget().getDocument();
|
||||||
|
|
||||||
|
CSMWorld::IdCollection<CSMWorld::LandTexture>& landtexturesCollection = document.getData().getLandTextures();
|
||||||
|
int index = landtexturesCollection.searchId(mBrushTexture);
|
||||||
|
|
||||||
|
if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit == true)
|
||||||
|
{
|
||||||
|
editTerrainTextureGrid(hit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::dragCompleted(const QPoint& pos) {
|
||||||
|
CSMDoc::Document& document = getWorldspaceWidget().getDocument();
|
||||||
|
QUndoStack& undoStack = document.getUndoStack();
|
||||||
|
|
||||||
|
CSMWorld::IdCollection<CSMWorld::LandTexture>& landtexturesCollection = document.getData().getLandTextures();
|
||||||
|
int index = landtexturesCollection.searchId(mBrushTexture);
|
||||||
|
|
||||||
|
if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted())
|
||||||
|
{
|
||||||
|
undoStack.endMacro();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::dragAborted() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::dragWheel (int diff, double speedFactor) {}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::handleDropEvent (QDropEvent *event) {
|
||||||
|
const CSMWorld::TableMimeData* mime = dynamic_cast<const CSMWorld::TableMimeData*> (event->mimeData());
|
||||||
|
|
||||||
|
if (!mime) // May happen when non-records (e.g. plain text) are dragged and dropped
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mime->holdsType (CSMWorld::UniversalId::Type_LandTexture))
|
||||||
|
{
|
||||||
|
const std::vector<CSMWorld::UniversalId> ids = mime->getData();
|
||||||
|
|
||||||
|
for (const CSMWorld::UniversalId& uid : ids)
|
||||||
|
{
|
||||||
|
mBrushTexture = uid.getId();
|
||||||
|
emit passBrushTexture(mBrushTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mime->holdsType (CSMWorld::UniversalId::Type_Texture))
|
||||||
|
{
|
||||||
|
const std::vector<CSMWorld::UniversalId> ids = mime->getData();
|
||||||
|
|
||||||
|
for (const CSMWorld::UniversalId& uid : ids)
|
||||||
|
{
|
||||||
|
std::string textureFileName = uid.toString();
|
||||||
|
createTexture(textureFileName);
|
||||||
|
emit passBrushTexture(mBrushTexture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::editTerrainTextureGrid(const WorldspaceHitResult& hit)
|
||||||
|
{
|
||||||
|
CSMDoc::Document& document = getWorldspaceWidget().getDocument();
|
||||||
|
CSMWorld::IdTable& landTable = dynamic_cast<CSMWorld::IdTable&> (
|
||||||
|
*document.getData().getTableModel (CSMWorld::UniversalId::Type_Land));
|
||||||
|
|
||||||
|
mCellId = getWorldspaceWidget().getCellId (hit.worldPos);
|
||||||
|
if(allowLandTextureEditing(mCellId)==true) {}
|
||||||
|
|
||||||
|
std::pair<CSMWorld::CellCoordinates, bool> cellCoordinates_pair = CSMWorld::CellCoordinates::fromId (mCellId);
|
||||||
|
|
||||||
|
int cellX = cellCoordinates_pair.first.getX();
|
||||||
|
int cellY = cellCoordinates_pair.first.getY();
|
||||||
|
|
||||||
|
// The coordinates of hit in mCellId
|
||||||
|
int xHitInCell (float(((hit.worldPos.x() - (cellX* cellSize)) * landTextureSize / cellSize) - 0.5));
|
||||||
|
int yHitInCell (float(((hit.worldPos.y() - (cellY* cellSize)) * landTextureSize / cellSize) + 0.5));
|
||||||
|
if (xHitInCell < 0)
|
||||||
|
{
|
||||||
|
xHitInCell = xHitInCell + landTextureSize;
|
||||||
|
cellX = cellX - 1;
|
||||||
|
}
|
||||||
|
if (yHitInCell > 15)
|
||||||
|
{
|
||||||
|
yHitInCell = yHitInCell - landTextureSize;
|
||||||
|
cellY = cellY + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCellId = "#" + std::to_string(cellX) + " " + std::to_string(cellY);
|
||||||
|
if(allowLandTextureEditing(mCellId)==true) {}
|
||||||
|
|
||||||
|
std::string iteratedCellId;
|
||||||
|
|
||||||
|
int textureColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandTexturesIndex);
|
||||||
|
|
||||||
|
std::size_t hashlocation = mBrushTexture.find("#");
|
||||||
|
std::string mBrushTextureInt = mBrushTexture.substr (hashlocation+1);
|
||||||
|
int brushInt = stoi(mBrushTexture.substr (hashlocation+1))+1; // All indices are offset by +1
|
||||||
|
|
||||||
|
float rf = mBrushSize/2;
|
||||||
|
int r = (mBrushSize/2)+1;
|
||||||
|
float distance = 0;
|
||||||
|
|
||||||
|
if (mBrushShape == 0)
|
||||||
|
{
|
||||||
|
CSMWorld::LandTexturesColumn::DataType mPointer = landTable.data(landTable.getModelIndex(mCellId, textureColumn)).value<CSMWorld::LandTexturesColumn::DataType>();
|
||||||
|
CSMWorld::LandTexturesColumn::DataType mNew(mPointer);
|
||||||
|
|
||||||
|
if(allowLandTextureEditing(mCellId)==true)
|
||||||
|
{
|
||||||
|
mNew[yHitInCell*landTextureSize+xHitInCell] = brushInt;
|
||||||
|
pushEditToCommand(mNew, document, landTable, mCellId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mBrushShape == 1)
|
||||||
|
{
|
||||||
|
int upperLeftCellX = cellX - std::floor(r / landTextureSize);
|
||||||
|
int upperLeftCellY = cellY - std::floor(r / landTextureSize);
|
||||||
|
if (xHitInCell - (r % landTextureSize) < 0) upperLeftCellX--;
|
||||||
|
if (yHitInCell - (r % landTextureSize) < 0) upperLeftCellY--;
|
||||||
|
|
||||||
|
int lowerrightCellX = cellX + std::floor(r / landTextureSize);
|
||||||
|
int lowerrightCellY = cellY + std::floor(r / landTextureSize);
|
||||||
|
if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellX++;
|
||||||
|
if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellY++;
|
||||||
|
|
||||||
|
for(int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++)
|
||||||
|
{
|
||||||
|
for(int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++)
|
||||||
|
{
|
||||||
|
iteratedCellId = "#" + std::to_string(i_cell) + " " + std::to_string(j_cell);
|
||||||
|
if(allowLandTextureEditing(iteratedCellId)==true)
|
||||||
|
{
|
||||||
|
CSMWorld::LandTexturesColumn::DataType mPointer = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)).value<CSMWorld::LandTexturesColumn::DataType>();
|
||||||
|
CSMWorld::LandTexturesColumn::DataType mNew(mPointer);
|
||||||
|
for(int i = 0; i < landTextureSize; i++)
|
||||||
|
{
|
||||||
|
for(int j = 0; j < landTextureSize; j++)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (i_cell == cellX && j_cell == cellY && abs(i-xHitInCell) < r && abs(j-yHitInCell) < r)
|
||||||
|
{
|
||||||
|
mNew[j*landTextureSize+i] = brushInt;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
if (distanceX < r && distanceY < r) mNew[j*landTextureSize+i] = brushInt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushEditToCommand(mNew, document, landTable, iteratedCellId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mBrushShape == 2)
|
||||||
|
{
|
||||||
|
int upperLeftCellX = cellX - std::floor(r / landTextureSize);
|
||||||
|
int upperLeftCellY = cellY - std::floor(r / landTextureSize);
|
||||||
|
if (xHitInCell - (r % landTextureSize) < 0) upperLeftCellX--;
|
||||||
|
if (yHitInCell - (r % landTextureSize) < 0) upperLeftCellY--;
|
||||||
|
|
||||||
|
int lowerrightCellX = cellX + std::floor(r / landTextureSize);
|
||||||
|
int lowerrightCellY = cellY + std::floor(r / landTextureSize);
|
||||||
|
if (xHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellX++;
|
||||||
|
if (yHitInCell + (r % landTextureSize) > landTextureSize - 1) lowerrightCellY++;
|
||||||
|
|
||||||
|
for(int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++)
|
||||||
|
{
|
||||||
|
for(int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++)
|
||||||
|
{
|
||||||
|
iteratedCellId = "#" + std::to_string(i_cell) + " " + std::to_string(j_cell);
|
||||||
|
if(allowLandTextureEditing(iteratedCellId)==true)
|
||||||
|
{
|
||||||
|
CSMWorld::LandTexturesColumn::DataType mPointer = landTable.data(landTable.getModelIndex(iteratedCellId, textureColumn)).value<CSMWorld::LandTexturesColumn::DataType>();
|
||||||
|
CSMWorld::LandTexturesColumn::DataType mNew(mPointer);
|
||||||
|
for(int i = 0; i < landTextureSize; i++)
|
||||||
|
{
|
||||||
|
for(int j = 0; j < landTextureSize; j++)
|
||||||
|
{
|
||||||
|
|
||||||
|
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);
|
||||||
|
distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2)));
|
||||||
|
if (distance < rf) mNew[j*landTextureSize+i] = brushInt;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
distance = std::round(sqrt(pow(distanceX, 2)+pow(distanceY, 2)));
|
||||||
|
if (distance < rf) mNew[j*landTextureSize+i] = brushInt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushEditToCommand(mNew, document, landTable, iteratedCellId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mBrushShape == 3)
|
||||||
|
{
|
||||||
|
// Not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document,
|
||||||
|
CSMWorld::IdTable& landTable, std::string cellId)
|
||||||
|
{
|
||||||
|
CSMWorld::IdTable& ltexTable = dynamic_cast<CSMWorld::IdTable&> (
|
||||||
|
*document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures));
|
||||||
|
|
||||||
|
QVariant changedLand;
|
||||||
|
changedLand.setValue(newLandGrid);
|
||||||
|
|
||||||
|
QModelIndex index(landTable.getModelIndex (cellId, landTable.findColumnIndex (CSMWorld::Columns::ColumnId_LandTexturesIndex)));
|
||||||
|
|
||||||
|
QUndoStack& undoStack = document.getUndoStack();
|
||||||
|
undoStack.push (new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId));
|
||||||
|
undoStack.push (new CSMWorld::ModifyCommand(landTable, index, changedLand));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::createTexture(std::string textureFileName)
|
||||||
|
{
|
||||||
|
CSMDoc::Document& document = getWorldspaceWidget().getDocument();
|
||||||
|
|
||||||
|
CSMWorld::IdTable& ltexTable = dynamic_cast<CSMWorld::IdTable&> (
|
||||||
|
*document.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures));
|
||||||
|
|
||||||
|
QUndoStack& undoStack = document.getUndoStack();
|
||||||
|
|
||||||
|
std::string newId;
|
||||||
|
|
||||||
|
int counter=0;
|
||||||
|
bool freeIndexFound = false;
|
||||||
|
do {
|
||||||
|
const size_t maxCounter = std::numeric_limits<uint16_t>::max() - 1;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter);
|
||||||
|
if (ltexTable.getRecord(newId).isDeleted() == 0) counter = (counter + 1) % maxCounter;
|
||||||
|
} catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
newId = CSMWorld::LandTexture::createUniqueRecordId(0, counter);
|
||||||
|
freeIndexFound = true;
|
||||||
|
}
|
||||||
|
} while (freeIndexFound == false);
|
||||||
|
|
||||||
|
std::size_t idlocation = textureFileName.find("Texture: ");
|
||||||
|
textureFileName = textureFileName.substr (idlocation + 9);
|
||||||
|
|
||||||
|
QVariant textureNameVariant;
|
||||||
|
|
||||||
|
QVariant textureFileNameVariant;
|
||||||
|
textureFileNameVariant.setValue(QString::fromStdString(textureFileName));
|
||||||
|
|
||||||
|
undoStack.beginMacro ("Add land texture record");
|
||||||
|
|
||||||
|
undoStack.push (new CSMWorld::CreateCommand (ltexTable, newId));
|
||||||
|
QModelIndex index(ltexTable.getModelIndex (newId, ltexTable.findColumnIndex (CSMWorld::Columns::ColumnId_Texture)));
|
||||||
|
undoStack.push (new CSMWorld::ModifyCommand(ltexTable, index, textureFileNameVariant));
|
||||||
|
undoStack.endMacro();
|
||||||
|
mBrushTexture = newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CSVRender::TerrainTextureMode::allowLandTextureEditing(std::string cellId)
|
||||||
|
{
|
||||||
|
CSMDoc::Document& document = getWorldspaceWidget().getDocument();
|
||||||
|
CSMWorld::IdTable& landTable = dynamic_cast<CSMWorld::IdTable&> (
|
||||||
|
*document.getData().getTableModel (CSMWorld::UniversalId::Type_Land));
|
||||||
|
CSMWorld::IdTree& cellTable = dynamic_cast<CSMWorld::IdTree&> (
|
||||||
|
*document.getData().getTableModel (CSMWorld::UniversalId::Type_Cells));
|
||||||
|
|
||||||
|
bool noCell = document.getData().getCells().searchId (cellId)==-1;
|
||||||
|
bool noLand = document.getData().getLand().searchId (cellId)==-1;
|
||||||
|
|
||||||
|
if (noCell)
|
||||||
|
{
|
||||||
|
std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString();
|
||||||
|
|
||||||
|
// target cell does not exist
|
||||||
|
if (mode=="Discard")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (mode=="Create cell and land, then edit")
|
||||||
|
{
|
||||||
|
std::unique_ptr<CSMWorld::CreateCommand> createCommand (
|
||||||
|
new CSMWorld::CreateCommand (cellTable, cellId));
|
||||||
|
int parentIndex = cellTable.findColumnIndex (CSMWorld::Columns::ColumnId_Cell);
|
||||||
|
int index = cellTable.findNestedColumnIndex (parentIndex, CSMWorld::Columns::ColumnId_Interior);
|
||||||
|
createCommand->addNestedValue (parentIndex, index, false);
|
||||||
|
document.getUndoStack().push (createCommand.release());
|
||||||
|
|
||||||
|
if (CSVRender::PagedWorldspaceWidget *paged =
|
||||||
|
dynamic_cast<CSVRender::PagedWorldspaceWidget *> (&getWorldspaceWidget()))
|
||||||
|
{
|
||||||
|
CSMWorld::CellSelection selection = paged->getCellSelection();
|
||||||
|
selection.add (CSMWorld::CellCoordinates::fromId (cellId).first);
|
||||||
|
paged->setCellSelection (selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (CSVRender::PagedWorldspaceWidget *paged =
|
||||||
|
dynamic_cast<CSVRender::PagedWorldspaceWidget *> (&getWorldspaceWidget()))
|
||||||
|
{
|
||||||
|
CSMWorld::CellSelection selection = paged->getCellSelection();
|
||||||
|
if (!selection.has (CSMWorld::CellCoordinates::fromId (cellId).first))
|
||||||
|
{
|
||||||
|
// target cell exists, but is not shown
|
||||||
|
std::string mode =
|
||||||
|
CSMPrefs::get()["3D Scene Editing"]["outside-visible-landedit"].toString();
|
||||||
|
|
||||||
|
if (mode=="Discard")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (mode=="Show cell and edit")
|
||||||
|
{
|
||||||
|
selection.add (CSMWorld::CellCoordinates::fromId (cellId).first);
|
||||||
|
paged->setCellSelection (selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noLand)
|
||||||
|
{
|
||||||
|
std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString();
|
||||||
|
|
||||||
|
// target cell does not exist
|
||||||
|
if (mode=="Discard")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (mode=="Create cell and land, then edit")
|
||||||
|
{
|
||||||
|
document.getUndoStack().push (new CSMWorld::CreateCommand (landTable, cellId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::dragMoveEvent (QDragMoveEvent *event) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::setBrushSize(int brushSize)
|
||||||
|
{
|
||||||
|
mBrushSize = brushSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::setBrushShape(int brushShape)
|
||||||
|
{
|
||||||
|
mBrushShape = brushShape;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVRender::TerrainTextureMode::setBrushTexture(std::string brushTexture)
|
||||||
|
{
|
||||||
|
mBrushTexture = brushTexture;
|
||||||
|
}
|
100
apps/opencs/view/render/terraintexturemode.hpp
Normal file
100
apps/opencs/view/render/terraintexturemode.hpp
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
#ifndef CSV_RENDER_TERRAINTEXTUREMODE_H
|
||||||
|
#define CSV_RENDER_TERRAINTEXTUREMODE_H
|
||||||
|
|
||||||
|
#include "editmode.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QEvent>
|
||||||
|
|
||||||
|
#include "../../model/world/data.hpp"
|
||||||
|
#include "../../model/world/land.hpp"
|
||||||
|
|
||||||
|
#include "../../model/doc/document.hpp"
|
||||||
|
#include "../../model/world/commands.hpp"
|
||||||
|
#include "../../model/world/idtable.hpp"
|
||||||
|
#include "../../model/world/landtexture.hpp"
|
||||||
|
|
||||||
|
namespace CSVWidget
|
||||||
|
{
|
||||||
|
class SceneToolTextureBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace CSVRender
|
||||||
|
{
|
||||||
|
|
||||||
|
class TerrainTextureMode : public EditMode
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/// \brief Editmode for terrain texture grid
|
||||||
|
TerrainTextureMode(WorldspaceWidget*, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
/// \brief Create single command for one-click texture editing
|
||||||
|
void primaryEditPressed (const WorldspaceHitResult& hit);
|
||||||
|
|
||||||
|
/// \brief Open brush settings window
|
||||||
|
void primarySelectPressed(const WorldspaceHitResult&);
|
||||||
|
|
||||||
|
void secondarySelectPressed(const WorldspaceHitResult&);
|
||||||
|
|
||||||
|
void activate(CSVWidget::SceneToolbar*);
|
||||||
|
void deactivate(CSVWidget::SceneToolbar*);
|
||||||
|
|
||||||
|
/// \brief Start texture editing command macro
|
||||||
|
virtual bool primaryEditStartDrag (const QPoint& pos);
|
||||||
|
|
||||||
|
virtual bool secondaryEditStartDrag (const QPoint& pos);
|
||||||
|
virtual bool primarySelectStartDrag (const QPoint& pos);
|
||||||
|
virtual bool secondarySelectStartDrag (const QPoint& pos);
|
||||||
|
|
||||||
|
/// \brief Handle texture edit behavior during dragging
|
||||||
|
virtual void drag (const QPoint& pos, int diffX, int diffY, double speedFactor);
|
||||||
|
|
||||||
|
/// \brief End texture editing command macro
|
||||||
|
virtual void dragCompleted(const QPoint& pos);
|
||||||
|
|
||||||
|
virtual void dragAborted();
|
||||||
|
virtual void dragWheel (int diff, double speedFactor);
|
||||||
|
virtual void dragMoveEvent (QDragMoveEvent *event);
|
||||||
|
|
||||||
|
/// \brief Handle brush mechanics, maths regarding worldspace hit etc.
|
||||||
|
void editTerrainTextureGrid (const WorldspaceHitResult& hit);
|
||||||
|
|
||||||
|
/// \brief Push texture edits to command macro
|
||||||
|
void pushEditToCommand (CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document,
|
||||||
|
CSMWorld::IdTable& landTable, std::string cellId);
|
||||||
|
|
||||||
|
/// \brief Create new land texture record from texture asset
|
||||||
|
void createTexture(std::string textureFileName);
|
||||||
|
|
||||||
|
/// \brief Create new cell and land if needed
|
||||||
|
bool allowLandTextureEditing(std::string textureFileName);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string mCellId;
|
||||||
|
std::string mBrushTexture;
|
||||||
|
int mBrushSize;
|
||||||
|
int mBrushShape;
|
||||||
|
CSVWidget::SceneToolTextureBrush *mTextureBrushScenetool;
|
||||||
|
|
||||||
|
const int cellSize {ESM::Land::REAL_SIZE};
|
||||||
|
const int landSize {ESM::Land::LAND_SIZE};
|
||||||
|
const int landTextureSize {ESM::Land::LAND_TEXTURE_SIZE};
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void passBrushTexture(std::string brushTexture);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void handleDropEvent(QDropEvent *event);
|
||||||
|
void setBrushSize(int brushSize);
|
||||||
|
void setBrushShape(int brushShape);
|
||||||
|
void setBrushTexture(std::string brushShape);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
|
@ -445,7 +445,7 @@ CSVRender::WorldspaceHitResult CSVRender::WorldspaceWidget::mousePick (const QPo
|
||||||
|
|
||||||
// Default placement
|
// Default placement
|
||||||
direction.normalize();
|
direction.normalize();
|
||||||
direction *= CSMPrefs::get()["Scene Drops"]["distance"].toInt();
|
direction *= CSMPrefs::get()["3D Scene Editing"]["distance"].toInt();
|
||||||
|
|
||||||
WorldspaceHitResult hit = { false, 0, 0, 0, 0, start + direction };
|
WorldspaceHitResult hit = { false, 0, 0, 0, 0, start + direction };
|
||||||
return hit;
|
return hit;
|
||||||
|
@ -648,6 +648,12 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event)
|
||||||
mDragX = event->posF().x();
|
mDragX = event->posF().x();
|
||||||
mDragY = height() - event->posF().y();
|
mDragY = height() - event->posF().y();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (mDragMode == InteractionType_PrimaryEdit)
|
||||||
|
{
|
||||||
|
EditMode& editMode = dynamic_cast<CSVRender::EditMode&> (*mEditMode->getCurrent());
|
||||||
|
editMode.drag (event->pos(), mDragX, mDragY, mDragFactor); // note: terraintexturemode only uses pos
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
379
apps/opencs/view/widget/scenetooltexturebrush.cpp
Normal file
379
apps/opencs/view/widget/scenetooltexturebrush.cpp
Normal file
|
@ -0,0 +1,379 @@
|
||||||
|
#include "scenetooltexturebrush.hpp"
|
||||||
|
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QTableWidget>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QSlider>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QDropEvent>
|
||||||
|
#include <QButtonGroup>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QDragEnterEvent>
|
||||||
|
#include <QDrag>
|
||||||
|
#include <QTableWidget>
|
||||||
|
#include <QHeaderView>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QSizePolicy>
|
||||||
|
|
||||||
|
#include "scenetool.hpp"
|
||||||
|
|
||||||
|
#include "../../model/doc/document.hpp"
|
||||||
|
#include "../../model/prefs/state.hpp"
|
||||||
|
#include "../../model/world/commands.hpp"
|
||||||
|
#include "../../model/world/data.hpp"
|
||||||
|
#include "../../model/world/idcollection.hpp"
|
||||||
|
#include "../../model/world/idtable.hpp"
|
||||||
|
#include "../../model/world/landtexture.hpp"
|
||||||
|
#include "../../model/world/universalid.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
CSVWidget::BrushSizeControls::BrushSizeControls(const QString &title, QWidget *parent)
|
||||||
|
: QGroupBox(title, parent)
|
||||||
|
{
|
||||||
|
mBrushSizeSlider = new QSlider(Qt::Horizontal);
|
||||||
|
mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides);
|
||||||
|
mBrushSizeSlider->setTickInterval(10);
|
||||||
|
mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt());
|
||||||
|
mBrushSizeSlider->setSingleStep(1);
|
||||||
|
|
||||||
|
mBrushSizeSpinBox = new QSpinBox;
|
||||||
|
mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt());
|
||||||
|
mBrushSizeSpinBox->setSingleStep(1);
|
||||||
|
|
||||||
|
mLayoutSliderSize = new QHBoxLayout;
|
||||||
|
mLayoutSliderSize->addWidget(mBrushSizeSlider);
|
||||||
|
mLayoutSliderSize->addWidget(mBrushSizeSpinBox);
|
||||||
|
|
||||||
|
connect(mBrushSizeSlider, SIGNAL(valueChanged(int)), mBrushSizeSpinBox, SLOT(setValue(int)));
|
||||||
|
connect(mBrushSizeSpinBox, SIGNAL(valueChanged(int)), mBrushSizeSlider, SLOT(setValue(int)));
|
||||||
|
|
||||||
|
setLayout(mLayoutSliderSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
CSVWidget::TextureBrushWindow::TextureBrushWindow(CSMDoc::Document& document, QWidget *parent)
|
||||||
|
: QFrame(parent, Qt::Popup),
|
||||||
|
mBrushShape(0),
|
||||||
|
mBrushSize(0),
|
||||||
|
mBrushTexture("L0#0"),
|
||||||
|
mDocument(document)
|
||||||
|
{
|
||||||
|
mBrushTextureLabel = "Selected texture: " + mBrushTexture + " ";
|
||||||
|
|
||||||
|
CSMWorld::IdCollection<CSMWorld::LandTexture>& landtexturesCollection = mDocument.getData().getLandTextures();
|
||||||
|
|
||||||
|
int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture);
|
||||||
|
int index = landtexturesCollection.searchId(mBrushTexture);
|
||||||
|
|
||||||
|
if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted())
|
||||||
|
{
|
||||||
|
mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value<QString>());
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
mBrushTextureLabel = "No selected texture or invalid texture";
|
||||||
|
mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel));
|
||||||
|
}
|
||||||
|
|
||||||
|
mButtonPoint = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-point")), "", this);
|
||||||
|
mButtonSquare = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-square")), "", this);
|
||||||
|
mButtonCircle = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-circle")), "", this);
|
||||||
|
mButtonCustom = new QPushButton(QIcon (QPixmap (":scenetoolbar/brush-custom")), "", this);
|
||||||
|
|
||||||
|
mSizeSliders = new BrushSizeControls("Brush size", this);
|
||||||
|
|
||||||
|
QVBoxLayout *layoutMain = new QVBoxLayout;
|
||||||
|
layoutMain->setSpacing(0);
|
||||||
|
layoutMain->setContentsMargins(4,0,4,4);
|
||||||
|
|
||||||
|
QHBoxLayout *layoutHorizontal = new QHBoxLayout;
|
||||||
|
layoutHorizontal->setSpacing(0);
|
||||||
|
layoutHorizontal->setContentsMargins (QMargins (0, 0, 0, 0));
|
||||||
|
|
||||||
|
configureButtonInitialSettings(mButtonPoint);
|
||||||
|
configureButtonInitialSettings(mButtonSquare);
|
||||||
|
configureButtonInitialSettings(mButtonCircle);
|
||||||
|
configureButtonInitialSettings(mButtonCustom);
|
||||||
|
|
||||||
|
mButtonPoint->setToolTip (toolTipPoint);
|
||||||
|
mButtonSquare->setToolTip (toolTipSquare);
|
||||||
|
mButtonCircle->setToolTip (toolTipCircle);
|
||||||
|
mButtonCustom->setToolTip (toolTipCustom);
|
||||||
|
|
||||||
|
QButtonGroup* brushButtonGroup = new QButtonGroup(this);
|
||||||
|
brushButtonGroup->addButton(mButtonPoint);
|
||||||
|
brushButtonGroup->addButton(mButtonSquare);
|
||||||
|
brushButtonGroup->addButton(mButtonCircle);
|
||||||
|
brushButtonGroup->addButton(mButtonCustom);
|
||||||
|
|
||||||
|
brushButtonGroup->setExclusive(true);
|
||||||
|
|
||||||
|
layoutHorizontal->addWidget(mButtonPoint, 0, Qt::AlignTop);
|
||||||
|
layoutHorizontal->addWidget(mButtonSquare, 0, Qt::AlignTop);
|
||||||
|
layoutHorizontal->addWidget(mButtonCircle, 0, Qt::AlignTop);
|
||||||
|
layoutHorizontal->addWidget(mButtonCustom, 0, Qt::AlignTop);
|
||||||
|
|
||||||
|
mHorizontalGroupBox = new QGroupBox(tr(""));
|
||||||
|
mHorizontalGroupBox->setLayout(layoutHorizontal);
|
||||||
|
|
||||||
|
layoutMain->addWidget(mHorizontalGroupBox);
|
||||||
|
layoutMain->addWidget(mSizeSliders);
|
||||||
|
layoutMain->addWidget(mSelectedBrush);
|
||||||
|
|
||||||
|
setLayout(layoutMain);
|
||||||
|
|
||||||
|
connect(mButtonPoint, SIGNAL(clicked()), this, SLOT(setBrushShape()));
|
||||||
|
connect(mButtonSquare, SIGNAL(clicked()), this, SLOT(setBrushShape()));
|
||||||
|
connect(mButtonCircle, SIGNAL(clicked()), this, SLOT(setBrushShape()));
|
||||||
|
connect(mButtonCustom, SIGNAL(clicked()), this, SLOT(setBrushShape()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::TextureBrushWindow::configureButtonInitialSettings(QPushButton *button)
|
||||||
|
{
|
||||||
|
button->setSizePolicy (QSizePolicy (QSizePolicy::Fixed, QSizePolicy::Fixed));
|
||||||
|
button->setContentsMargins (QMargins (0, 0, 0, 0));
|
||||||
|
button->setIconSize (QSize (48-6, 48-6));
|
||||||
|
button->setFixedSize (48, 48);
|
||||||
|
button->setCheckable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::TextureBrushWindow::setBrushTexture(std::string brushTexture)
|
||||||
|
{
|
||||||
|
mBrushTexture = brushTexture;
|
||||||
|
|
||||||
|
CSMWorld::IdCollection<CSMWorld::LandTexture>& landtexturesCollection = mDocument.getData().getLandTextures();
|
||||||
|
|
||||||
|
int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture);
|
||||||
|
int columnModification = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Modification);
|
||||||
|
int index = landtexturesCollection.searchId(mBrushTexture);
|
||||||
|
|
||||||
|
// Check if texture exists in current plugin
|
||||||
|
if(landtexturesCollection.getData(index, columnModification).value<int>() == 0)
|
||||||
|
{
|
||||||
|
CSMWorld::IdTable& ltexTable = dynamic_cast<CSMWorld::IdTable&> (
|
||||||
|
*mDocument.getData().getTableModel (CSMWorld::UniversalId::Type_LandTextures));
|
||||||
|
|
||||||
|
QUndoStack& undoStack = mDocument.getUndoStack();
|
||||||
|
|
||||||
|
QVariant textureFileNameVariant;
|
||||||
|
textureFileNameVariant.setValue(landtexturesCollection.getData(index, landTextureFilename).value<QString>());
|
||||||
|
|
||||||
|
std::size_t hashlocation = mBrushTexture.find("#");
|
||||||
|
std::string mBrushTexturePlugin = "L0#" + mBrushTexture.substr (hashlocation+1);
|
||||||
|
int indexPlugin = landtexturesCollection.searchId(mBrushTexturePlugin);
|
||||||
|
|
||||||
|
// Reindex texture if needed
|
||||||
|
if (indexPlugin != -1 && !landtexturesCollection.getRecord(indexPlugin).isDeleted())
|
||||||
|
{
|
||||||
|
int counter=0;
|
||||||
|
bool freeIndexFound = false;
|
||||||
|
do {
|
||||||
|
const size_t maxCounter = std::numeric_limits<uint16_t>::max() - 1;
|
||||||
|
mBrushTexturePlugin = CSMWorld::LandTexture::createUniqueRecordId(0, counter);
|
||||||
|
if (landtexturesCollection.searchId(mBrushTexturePlugin) != -1 && landtexturesCollection.getRecord(mBrushTexturePlugin).isDeleted() == 0) counter = (counter + 1) % maxCounter;
|
||||||
|
else freeIndexFound = true;
|
||||||
|
} while (freeIndexFound == false);
|
||||||
|
}
|
||||||
|
|
||||||
|
undoStack.beginMacro ("Add land texture record");
|
||||||
|
undoStack.push (new CSMWorld::CloneCommand (ltexTable, mBrushTexture, mBrushTexturePlugin, CSMWorld::UniversalId::Type_LandTexture));
|
||||||
|
undoStack.endMacro();
|
||||||
|
mBrushTexture = mBrushTexturePlugin;
|
||||||
|
emit passTextureId(mBrushTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted())
|
||||||
|
{
|
||||||
|
mBrushTextureLabel = "Selected texture: " + mBrushTexture + " ";
|
||||||
|
mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(index, landTextureFilename).value<QString>());
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
mBrushTextureLabel = "No selected texture or invalid texture";
|
||||||
|
mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel));
|
||||||
|
}
|
||||||
|
|
||||||
|
emit passBrushShape(mBrushShape); // update icon
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::TextureBrushWindow::setBrushSize(int brushSize)
|
||||||
|
{
|
||||||
|
mBrushSize = brushSize;
|
||||||
|
emit passBrushSize(mBrushSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::TextureBrushWindow::setBrushShape()
|
||||||
|
{
|
||||||
|
if(mButtonPoint->isChecked()) mBrushShape = 0;
|
||||||
|
if(mButtonSquare->isChecked()) mBrushShape = 1;
|
||||||
|
if(mButtonCircle->isChecked()) mBrushShape = 2;
|
||||||
|
if(mButtonCustom->isChecked()) mBrushShape = 3;
|
||||||
|
emit passBrushShape(mBrushShape);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::SceneToolTextureBrush::adjustToolTips()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
CSVWidget::SceneToolTextureBrush::SceneToolTextureBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document)
|
||||||
|
: SceneTool (parent, Type_TopAction),
|
||||||
|
mToolTip (toolTip),
|
||||||
|
mDocument (document),
|
||||||
|
mTextureBrushWindow(new TextureBrushWindow(document, this))
|
||||||
|
{
|
||||||
|
mBrushHistory.resize(1);
|
||||||
|
mBrushHistory[0] = "L0#0";
|
||||||
|
|
||||||
|
setAcceptDrops(true);
|
||||||
|
connect(mTextureBrushWindow, SIGNAL(passBrushShape(int)), this, SLOT(setButtonIcon(int)));
|
||||||
|
setButtonIcon(mTextureBrushWindow->mBrushShape);
|
||||||
|
|
||||||
|
mPanel = new QFrame (this, Qt::Popup);
|
||||||
|
|
||||||
|
QHBoxLayout *layout = new QHBoxLayout (mPanel);
|
||||||
|
|
||||||
|
layout->setContentsMargins (QMargins (0, 0, 0, 0));
|
||||||
|
|
||||||
|
mTable = new QTableWidget (0, 2, this);
|
||||||
|
|
||||||
|
mTable->setShowGrid (true);
|
||||||
|
mTable->verticalHeader()->hide();
|
||||||
|
mTable->horizontalHeader()->hide();
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5,0,0)
|
||||||
|
mTable->horizontalHeader()->setSectionResizeMode (0, QHeaderView::Stretch);
|
||||||
|
mTable->horizontalHeader()->setSectionResizeMode (1, QHeaderView::Stretch);
|
||||||
|
#else
|
||||||
|
mTable->horizontalHeader()->setResizeMode (0, QHeaderView::Stretch);
|
||||||
|
mTable->horizontalHeader()->setResizeMode (1, QHeaderView::Stretch);
|
||||||
|
#endif
|
||||||
|
mTable->setSelectionMode (QAbstractItemView::NoSelection);
|
||||||
|
|
||||||
|
layout->addWidget (mTable);
|
||||||
|
|
||||||
|
connect (mTable, SIGNAL (clicked (const QModelIndex&)),
|
||||||
|
this, SLOT (clicked (const QModelIndex&)));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::SceneToolTextureBrush::setButtonIcon (int brushShape)
|
||||||
|
{
|
||||||
|
QString tooltip = "Change brush settings <p>Currently selected: ";
|
||||||
|
|
||||||
|
switch (brushShape)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
|
||||||
|
setIcon (QIcon (QPixmap (":scenetoolbar/brush-point")));
|
||||||
|
tooltip += mTextureBrushWindow->toolTipPoint;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
|
||||||
|
setIcon (QIcon (QPixmap (":scenetoolbar/brush-square")));
|
||||||
|
tooltip += mTextureBrushWindow->toolTipSquare;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
|
||||||
|
setIcon (QIcon (QPixmap (":scenetoolbar/brush-circle")));
|
||||||
|
tooltip += mTextureBrushWindow->toolTipCircle;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
|
||||||
|
setIcon (QIcon (QPixmap (":scenetoolbar/brush-custom")));
|
||||||
|
tooltip += mTextureBrushWindow->toolTipCustom;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip += "<p>(right click to access of previously used brush settings)";
|
||||||
|
|
||||||
|
|
||||||
|
CSMWorld::IdCollection<CSMWorld::LandTexture>& landtexturesCollection = mDocument.getData().getLandTextures();
|
||||||
|
|
||||||
|
int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture);
|
||||||
|
int index = landtexturesCollection.searchId(mTextureBrushWindow->mBrushTexture);
|
||||||
|
|
||||||
|
if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted())
|
||||||
|
{
|
||||||
|
tooltip += "<p>Selected texture: " + QString::fromStdString(mTextureBrushWindow->mBrushTexture) + " ";
|
||||||
|
|
||||||
|
tooltip += landtexturesCollection.getData(index, landTextureFilename).value<QString>();
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
tooltip += "<p>No selected texture or invalid texture";
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip += "<br>(drop texture here to change)";
|
||||||
|
setToolTip (tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::SceneToolTextureBrush::showPanel (const QPoint& position)
|
||||||
|
{
|
||||||
|
updatePanel();
|
||||||
|
mPanel->move (position);
|
||||||
|
mPanel->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::SceneToolTextureBrush::updatePanel()
|
||||||
|
{
|
||||||
|
mTable->setRowCount (mBrushHistory.size());
|
||||||
|
|
||||||
|
for (int i = mBrushHistory.size()-1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
CSMWorld::IdCollection<CSMWorld::LandTexture>& landtexturesCollection = mDocument.getData().getLandTextures();
|
||||||
|
int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture);
|
||||||
|
int index = landtexturesCollection.searchId(mBrushHistory[i]);
|
||||||
|
|
||||||
|
if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted())
|
||||||
|
{
|
||||||
|
mTable->setItem (i, 1, new QTableWidgetItem (landtexturesCollection.getData(index, landTextureFilename).value<QString>()));
|
||||||
|
mTable->setItem (i, 0, new QTableWidgetItem (QString::fromStdString(mBrushHistory[i])));
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
mTable->setItem (i, 1, new QTableWidgetItem ("Invalid/deleted texture"));
|
||||||
|
mTable->setItem (i, 0, new QTableWidgetItem (QString::fromStdString(mBrushHistory[i])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::SceneToolTextureBrush::updateBrushHistory (const std::string& brushTexture)
|
||||||
|
{
|
||||||
|
mBrushHistory.insert(mBrushHistory.begin(), brushTexture);
|
||||||
|
if(mBrushHistory.size() > 5) mBrushHistory.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::SceneToolTextureBrush::clicked (const QModelIndex& index)
|
||||||
|
{
|
||||||
|
if (index.column()==0 || index.column()==1)
|
||||||
|
{
|
||||||
|
std::string brushTexture = mBrushHistory[index.row()];
|
||||||
|
std::swap(mBrushHistory[index.row()], mBrushHistory[0]);
|
||||||
|
mTextureBrushWindow->setBrushTexture(brushTexture);
|
||||||
|
emit passTextureId(brushTexture);
|
||||||
|
updatePanel();
|
||||||
|
mPanel->hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::SceneToolTextureBrush::activate ()
|
||||||
|
{
|
||||||
|
QPoint position = QCursor::pos();
|
||||||
|
mTextureBrushWindow->mSizeSliders->mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt());
|
||||||
|
mTextureBrushWindow->mSizeSliders->mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt());
|
||||||
|
mTextureBrushWindow->move (position);
|
||||||
|
mTextureBrushWindow->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CSVWidget::SceneToolTextureBrush::dragEnterEvent (QDragEnterEvent *event)
|
||||||
|
{
|
||||||
|
emit passEvent(event);
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
void CSVWidget::SceneToolTextureBrush::dropEvent (QDropEvent *event)
|
||||||
|
{
|
||||||
|
emit passEvent(event);
|
||||||
|
event->accept();
|
||||||
|
}
|
133
apps/opencs/view/widget/scenetooltexturebrush.hpp
Normal file
133
apps/opencs/view/widget/scenetooltexturebrush.hpp
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
#ifndef CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H
|
||||||
|
#define CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H
|
||||||
|
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QModelIndex>
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QSlider>
|
||||||
|
#include <QEvent>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QPushButton>
|
||||||
|
|
||||||
|
#include "scenetool.hpp"
|
||||||
|
|
||||||
|
#include "../../model/doc/document.hpp"
|
||||||
|
|
||||||
|
class QTableWidget;
|
||||||
|
|
||||||
|
namespace CSVRender
|
||||||
|
{
|
||||||
|
class TerrainTextureMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace CSVWidget
|
||||||
|
{
|
||||||
|
class SceneToolTextureBrush;
|
||||||
|
|
||||||
|
/// \brief Layout-box for some brush button settings
|
||||||
|
class BrushSizeControls : public QGroupBox
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
BrushSizeControls(const QString &title, QWidget *parent);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QHBoxLayout *mLayoutSliderSize;
|
||||||
|
QSlider *mBrushSizeSlider;
|
||||||
|
QSpinBox *mBrushSizeSpinBox;
|
||||||
|
|
||||||
|
friend class SceneToolTextureBrush;
|
||||||
|
friend class CSVRender::TerrainTextureMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SceneToolTextureBrush;
|
||||||
|
|
||||||
|
/// \brief Brush settings window
|
||||||
|
class TextureBrushWindow : public QFrame
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
TextureBrushWindow(CSMDoc::Document& document, QWidget *parent = 0);
|
||||||
|
void configureButtonInitialSettings(QPushButton *button);
|
||||||
|
|
||||||
|
const QString toolTipPoint = "Paint single point";
|
||||||
|
const QString toolTipSquare = "Paint with square brush";
|
||||||
|
const QString toolTipCircle = "Paint with circle brush";
|
||||||
|
const QString toolTipCustom = "Paint custom selection (not implemented yet)";
|
||||||
|
|
||||||
|
private:
|
||||||
|
int mBrushShape;
|
||||||
|
int mBrushSize;
|
||||||
|
std::string mBrushTexture;
|
||||||
|
CSMDoc::Document& mDocument;
|
||||||
|
QLabel *mSelectedBrush;
|
||||||
|
QGroupBox *mHorizontalGroupBox;
|
||||||
|
std::string mBrushTextureLabel;
|
||||||
|
QPushButton *mButtonPoint;
|
||||||
|
QPushButton *mButtonSquare;
|
||||||
|
QPushButton *mButtonCircle;
|
||||||
|
QPushButton *mButtonCustom;
|
||||||
|
BrushSizeControls* mSizeSliders;
|
||||||
|
|
||||||
|
friend class SceneToolTextureBrush;
|
||||||
|
friend class CSVRender::TerrainTextureMode;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void setBrushTexture(std::string brushTexture);
|
||||||
|
void setBrushShape();
|
||||||
|
void setBrushSize(int brushSize);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void passBrushSize (int brushSize);
|
||||||
|
void passBrushShape(int brushShape);
|
||||||
|
void passTextureId(std::string brushTexture);
|
||||||
|
};
|
||||||
|
|
||||||
|
class SceneToolTextureBrush : public SceneTool
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
QString mToolTip;
|
||||||
|
CSMDoc::Document& mDocument;
|
||||||
|
QFrame *mPanel;
|
||||||
|
QTableWidget *mTable;
|
||||||
|
std::vector<std::string> mBrushHistory;
|
||||||
|
TextureBrushWindow *mTextureBrushWindow;
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
void adjustToolTips();
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
SceneToolTextureBrush (SceneToolbar *parent, const QString& toolTip, CSMDoc::Document& document);
|
||||||
|
|
||||||
|
virtual void showPanel (const QPoint& position);
|
||||||
|
void updatePanel ();
|
||||||
|
|
||||||
|
void dropEvent (QDropEvent *event);
|
||||||
|
void dragEnterEvent (QDragEnterEvent *event);
|
||||||
|
|
||||||
|
friend class CSVRender::TerrainTextureMode;
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void setButtonIcon(int brushShape);
|
||||||
|
void updateBrushHistory (const std::string& mBrushTexture);
|
||||||
|
void clicked (const QModelIndex& index);
|
||||||
|
virtual void activate();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void passEvent(QDropEvent *event);
|
||||||
|
void passEvent(QDragEnterEvent *event);
|
||||||
|
void passTextureId(std::string brushTexture);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -1,6 +1,7 @@
|
||||||
#include "misc.hpp"
|
#include "misc.hpp"
|
||||||
|
|
||||||
#include <components/esm/loadmisc.hpp>
|
#include <components/esm/loadmisc.hpp>
|
||||||
|
#include <components/settings/settings.hpp>
|
||||||
|
|
||||||
#include "../mwbase/environment.hpp"
|
#include "../mwbase/environment.hpp"
|
||||||
#include "../mwbase/world.hpp"
|
#include "../mwbase/world.hpp"
|
||||||
|
@ -85,7 +86,22 @@ namespace MWClass
|
||||||
{
|
{
|
||||||
const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get<ESM::Creature>().search(ref->mRef.getSoul());
|
const ESM::Creature *creature = MWBase::Environment::get().getWorld()->getStore().get<ESM::Creature>().search(ref->mRef.getSoul());
|
||||||
if (creature)
|
if (creature)
|
||||||
value *= creature->mData.mSoul;
|
{
|
||||||
|
int soul = creature->mData.mSoul;
|
||||||
|
if (Settings::Manager::getBool("rebalance soul gem values", "Game"))
|
||||||
|
{
|
||||||
|
// use the 'soul gem value rebalance' formula from the Morrowind Code Patch
|
||||||
|
float soulValue = 0.0001 * pow(soul, 3) + 2 * soul;
|
||||||
|
|
||||||
|
// for Azura's star add the unfilled value
|
||||||
|
if (Misc::StringUtils::ciEqual(ptr.getCellRef().getRefId(), "Misc_SoulGem_Azura"))
|
||||||
|
value += soulValue;
|
||||||
|
else
|
||||||
|
value = soulValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
value *= soul;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
|
|
|
@ -988,7 +988,7 @@ namespace MWClass
|
||||||
const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects();
|
const MWMechanics::MagicEffects &mageffects = npcdata->mNpcStats.getMagicEffects();
|
||||||
const float encumbranceTerm = gmst.fJumpEncumbranceBase->getFloat() +
|
const float encumbranceTerm = gmst.fJumpEncumbranceBase->getFloat() +
|
||||||
gmst.fJumpEncumbranceMultiplier->getFloat() *
|
gmst.fJumpEncumbranceMultiplier->getFloat() *
|
||||||
(1.0f - Npc::getEncumbrance(ptr)/Npc::getCapacity(ptr));
|
(1.0f - Npc::getNormalizedEncumbrance(ptr));
|
||||||
|
|
||||||
float a = static_cast<float>(npcdata->mNpcStats.getSkill(ESM::Skill::Acrobatics).getModified());
|
float a = static_cast<float>(npcdata->mNpcStats.getSkill(ESM::Skill::Acrobatics).getModified());
|
||||||
float b = 0.0f;
|
float b = 0.0f;
|
||||||
|
|
|
@ -244,7 +244,7 @@ BookTypesetter::Ptr JournalBooks::createLatinJournalIndex ()
|
||||||
const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours();
|
const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours();
|
||||||
BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic,
|
BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic,
|
||||||
textColours.journalTopicOver,
|
textColours.journalTopicOver,
|
||||||
textColours.journalTopicPressed, (uint32_t) ch);
|
textColours.journalTopicPressed, (Utf8Stream::UnicodeChar) ch);
|
||||||
|
|
||||||
if (i == 13)
|
if (i == 13)
|
||||||
typesetter->sectionBreak ();
|
typesetter->sectionBreak ();
|
||||||
|
@ -274,7 +274,7 @@ BookTypesetter::Ptr JournalBooks::createCyrillicJournalIndex ()
|
||||||
sprintf(buffer, "( %c%c )", ch[0], ch[1]);
|
sprintf(buffer, "( %c%c )", ch[0], ch[1]);
|
||||||
|
|
||||||
Utf8Stream stream ((char*) ch);
|
Utf8Stream stream ((char*) ch);
|
||||||
uint32_t first = stream.peek();
|
Utf8Stream::UnicodeChar first = stream.peek();
|
||||||
|
|
||||||
const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours();
|
const MWGui::TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours();
|
||||||
BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic,
|
BookTypesetter::Style* style = typesetter->createHotStyle (body, textColours.journalTopic,
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
#include <components/translation/translation.hpp>
|
#include <components/translation/translation.hpp>
|
||||||
#include <components/misc/stringops.hpp>
|
#include <components/misc/stringops.hpp>
|
||||||
#include <components/misc/utf8stream.hpp>
|
|
||||||
|
|
||||||
#include "../mwbase/world.hpp"
|
#include "../mwbase/world.hpp"
|
||||||
#include "../mwbase/journal.hpp"
|
#include "../mwbase/journal.hpp"
|
||||||
|
@ -307,39 +306,22 @@ struct JournalViewModelImpl : JournalViewModel
|
||||||
visitor (toUtf8Span (topic.getName()));
|
visitor (toUtf8Span (topic.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void visitTopicNamesStartingWith (uint32_t character, std::function < void (const std::string&) > visitor) const
|
void visitTopicNamesStartingWith (Utf8Stream::UnicodeChar character, std::function < void (const std::string&) > visitor) const
|
||||||
{
|
{
|
||||||
MWBase::Journal * journal = MWBase::Environment::get().getJournal();
|
MWBase::Journal * journal = MWBase::Environment::get().getJournal();
|
||||||
|
|
||||||
for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i)
|
for (MWBase::Journal::TTopicIter i = journal->topicBegin (); i != journal->topicEnd (); ++i)
|
||||||
{
|
{
|
||||||
Utf8Stream stream (i->first.c_str());
|
Utf8Stream stream (i->first.c_str());
|
||||||
uint32_t first = toUpper(stream.peek());
|
Utf8Stream::UnicodeChar first = Misc::StringUtils::toLowerUtf8(stream.peek());
|
||||||
|
|
||||||
if (first != character)
|
if (first != Misc::StringUtils::toLowerUtf8(character))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
visitor (i->second.getName());
|
visitor (i->second.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint32_t toUpper(uint32_t ch)
|
|
||||||
{
|
|
||||||
// Russian alphabet
|
|
||||||
if (ch >= 0x0430 && ch < 0x0450)
|
|
||||||
ch -= 0x20;
|
|
||||||
|
|
||||||
// Cyrillic IO character
|
|
||||||
if (ch == 0x0451)
|
|
||||||
ch -= 0x50;
|
|
||||||
|
|
||||||
// Latin alphabet
|
|
||||||
if (ch >= 0x61 && ch < 0x80)
|
|
||||||
ch -= 0x20;
|
|
||||||
|
|
||||||
return ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TopicEntryImpl : BaseEntry <MWDialogue::Topic::TEntryIter, TopicEntry>
|
struct TopicEntryImpl : BaseEntry <MWDialogue::Topic::TEntryIter, TopicEntry>
|
||||||
{
|
{
|
||||||
MWDialogue::Topic const & mTopic;
|
MWDialogue::Topic const & mTopic;
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <components/misc/utf8stream.hpp>
|
||||||
|
|
||||||
namespace MWGui
|
namespace MWGui
|
||||||
{
|
{
|
||||||
/// View-Model for the journal GUI
|
/// View-Model for the journal GUI
|
||||||
|
@ -76,7 +78,7 @@ namespace MWGui
|
||||||
virtual void visitTopicName (TopicId topicId, std::function <void (Utf8Span)> visitor) const = 0;
|
virtual void visitTopicName (TopicId topicId, std::function <void (Utf8Span)> visitor) const = 0;
|
||||||
|
|
||||||
/// walks over the topics whose names start with the character
|
/// walks over the topics whose names start with the character
|
||||||
virtual void visitTopicNamesStartingWith (uint32_t character, std::function < void (const std::string&) > visitor) const = 0;
|
virtual void visitTopicNamesStartingWith (Utf8Stream::UnicodeChar character, std::function < void (const std::string&) > visitor) const = 0;
|
||||||
|
|
||||||
/// walks over the topic entries for the topic specified by its identifier
|
/// walks over the topic entries for the topic specified by its identifier
|
||||||
virtual void visitTopicEntries (TopicId topicId, std::function <void (TopicEntry const &)> visitor) const = 0;
|
virtual void visitTopicEntries (TopicId topicId, std::function <void (TopicEntry const &)> visitor) const = 0;
|
||||||
|
|
|
@ -14,6 +14,9 @@ namespace MWGui
|
||||||
|
|
||||||
bool shouldAcceptKeyFocus(MyGUI::Widget* w)
|
bool shouldAcceptKeyFocus(MyGUI::Widget* w)
|
||||||
{
|
{
|
||||||
|
if (w && w->getUserString("IgnoreTabKey") == "y")
|
||||||
|
return false;
|
||||||
|
|
||||||
return w && !w->castType<MyGUI::Window>(false) && w->getInheritedEnabled() && w->getInheritedVisible() && w->getVisible() && w->getEnabled();
|
return w && !w->castType<MyGUI::Window>(false) && w->getInheritedEnabled() && w->getInheritedVisible() && w->getVisible() && w->getEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,10 +32,14 @@ namespace
|
||||||
namespace MWGui
|
namespace MWGui
|
||||||
{
|
{
|
||||||
|
|
||||||
|
SpellModel::SpellModel(const MWWorld::Ptr &actor, const std::string& filter)
|
||||||
|
: mActor(actor), mFilter(filter)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
SpellModel::SpellModel(const MWWorld::Ptr &actor)
|
SpellModel::SpellModel(const MWWorld::Ptr &actor)
|
||||||
: mActor(actor)
|
: mActor(actor)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpellModel::update()
|
void SpellModel::update()
|
||||||
|
@ -48,12 +52,19 @@ namespace MWGui
|
||||||
const MWWorld::ESMStore &esmStore =
|
const MWWorld::ESMStore &esmStore =
|
||||||
MWBase::Environment::get().getWorld()->getStore();
|
MWBase::Environment::get().getWorld()->getStore();
|
||||||
|
|
||||||
|
std::string filter = Misc::StringUtils::lowerCaseUtf8(mFilter);
|
||||||
|
|
||||||
for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it)
|
for (MWMechanics::Spells::TIterator it = spells.begin(); it != spells.end(); ++it)
|
||||||
{
|
{
|
||||||
const ESM::Spell* spell = it->first;
|
const ESM::Spell* spell = it->first;
|
||||||
if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell)
|
if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
std::string name = Misc::StringUtils::lowerCaseUtf8(spell->mName);
|
||||||
|
|
||||||
|
if (name.find(filter) == std::string::npos)
|
||||||
|
continue;
|
||||||
|
|
||||||
Spell newSpell;
|
Spell newSpell;
|
||||||
newSpell.mName = spell->mName;
|
newSpell.mName = spell->mName;
|
||||||
if (spell->mData.mType == ESM::Spell::ST_Spell)
|
if (spell->mData.mType == ESM::Spell::ST_Spell)
|
||||||
|
@ -89,6 +100,11 @@ namespace MWGui
|
||||||
if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce)
|
if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
std::string name = Misc::StringUtils::lowerCaseUtf8(item.getClass().getName(item));
|
||||||
|
|
||||||
|
if (name.find(filter) == std::string::npos)
|
||||||
|
continue;
|
||||||
|
|
||||||
Spell newSpell;
|
Spell newSpell;
|
||||||
newSpell.mItem = item;
|
newSpell.mItem = item;
|
||||||
newSpell.mId = item.getCellRef().getRefId();
|
newSpell.mId = item.getCellRef().getRefId();
|
||||||
|
|
|
@ -35,6 +35,7 @@ namespace MWGui
|
||||||
class SpellModel
|
class SpellModel
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
SpellModel(const MWWorld::Ptr& actor, const std::string& filter);
|
||||||
SpellModel(const MWWorld::Ptr& actor);
|
SpellModel(const MWWorld::Ptr& actor);
|
||||||
|
|
||||||
typedef int ModelIndex;
|
typedef int ModelIndex;
|
||||||
|
@ -50,6 +51,8 @@ namespace MWGui
|
||||||
MWWorld::Ptr mActor;
|
MWWorld::Ptr mActor;
|
||||||
|
|
||||||
std::vector<Spell> mSpells;
|
std::vector<Spell> mSpells;
|
||||||
|
|
||||||
|
std::string mFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <boost/format.hpp>
|
#include <boost/format.hpp>
|
||||||
|
|
||||||
|
#include <MyGUI_EditBox.h>
|
||||||
#include <MyGUI_InputManager.h>
|
#include <MyGUI_InputManager.h>
|
||||||
|
|
||||||
#include <components/settings/settings.hpp>
|
#include <components/settings/settings.hpp>
|
||||||
|
@ -38,8 +39,12 @@ namespace MWGui
|
||||||
|
|
||||||
getWidget(mSpellView, "SpellView");
|
getWidget(mSpellView, "SpellView");
|
||||||
getWidget(mEffectBox, "EffectsBox");
|
getWidget(mEffectBox, "EffectsBox");
|
||||||
|
getWidget(mFilterEdit, "FilterEdit");
|
||||||
|
|
||||||
|
mFilterEdit->setUserString("IgnoreTabKey", "y");
|
||||||
|
|
||||||
mSpellView->eventSpellClicked += MyGUI::newDelegate(this, &SpellWindow::onModelIndexSelected);
|
mSpellView->eventSpellClicked += MyGUI::newDelegate(this, &SpellWindow::onModelIndexSelected);
|
||||||
|
mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &SpellWindow::onFilterChanged);
|
||||||
|
|
||||||
setCoord(498, 300, 302, 300);
|
setCoord(498, 300, 302, 300);
|
||||||
}
|
}
|
||||||
|
@ -64,6 +69,11 @@ namespace MWGui
|
||||||
|
|
||||||
void SpellWindow::onOpen()
|
void SpellWindow::onOpen()
|
||||||
{
|
{
|
||||||
|
// Reset the filter focus when opening the window
|
||||||
|
MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget();
|
||||||
|
if (focus == mFilterEdit)
|
||||||
|
MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(NULL);
|
||||||
|
|
||||||
updateSpells();
|
updateSpells();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +92,7 @@ namespace MWGui
|
||||||
{
|
{
|
||||||
mSpellIcons->updateWidgets(mEffectBox, false);
|
mSpellIcons->updateWidgets(mEffectBox, false);
|
||||||
|
|
||||||
mSpellView->setModel(new SpellModel(MWMechanics::getPlayer()));
|
mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), mFilterEdit->getCaption()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpellWindow::onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped)
|
void SpellWindow::onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped)
|
||||||
|
@ -167,6 +177,11 @@ namespace MWGui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SpellWindow::onFilterChanged(MyGUI::EditBox *sender)
|
||||||
|
{
|
||||||
|
mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), sender->getCaption()));
|
||||||
|
}
|
||||||
|
|
||||||
void SpellWindow::onSpellSelected(const std::string& spellId)
|
void SpellWindow::onSpellSelected(const std::string& spellId)
|
||||||
{
|
{
|
||||||
MWWorld::Ptr player = MWMechanics::getPlayer();
|
MWWorld::Ptr player = MWMechanics::getPlayer();
|
||||||
|
@ -202,7 +217,7 @@ namespace MWGui
|
||||||
if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery())
|
if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mSpellView->setModel(new SpellModel(MWMechanics::getPlayer()));
|
mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), ""));
|
||||||
|
|
||||||
SpellModel::ModelIndex selected = 0;
|
SpellModel::ModelIndex selected = 0;
|
||||||
for (SpellModel::ModelIndex i = 0; i<int(mSpellView->getModel()->getItemCount()); ++i)
|
for (SpellModel::ModelIndex i = 0; i<int(mSpellView->getModel()->getItemCount()); ++i)
|
||||||
|
|
|
@ -32,6 +32,7 @@ namespace MWGui
|
||||||
void onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped);
|
void onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped);
|
||||||
void onSpellSelected(const std::string& spellId);
|
void onSpellSelected(const std::string& spellId);
|
||||||
void onModelIndexSelected(SpellModel::ModelIndex index);
|
void onModelIndexSelected(SpellModel::ModelIndex index);
|
||||||
|
void onFilterChanged(MyGUI::EditBox *sender);
|
||||||
void onDeleteSpellAccept();
|
void onDeleteSpellAccept();
|
||||||
void askDeleteSpell(const std::string& spellId);
|
void askDeleteSpell(const std::string& spellId);
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ namespace MWGui
|
||||||
|
|
||||||
SpellView* mSpellView;
|
SpellView* mSpellView;
|
||||||
SpellIcons* mSpellIcons;
|
SpellIcons* mSpellIcons;
|
||||||
|
MyGUI::EditBox* mFilterEdit;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float mUpdateTimer;
|
float mUpdateTimer;
|
||||||
|
|
|
@ -1751,8 +1751,7 @@ void CharacterController::update(float duration)
|
||||||
|
|
||||||
if (cls.getEncumbrance(mPtr) <= cls.getCapacity(mPtr))
|
if (cls.getEncumbrance(mPtr) <= cls.getCapacity(mPtr))
|
||||||
{
|
{
|
||||||
const float encumbrance = cls.getEncumbrance(mPtr) / cls.getCapacity(mPtr);
|
const float encumbrance = cls.getNormalizedEncumbrance(mPtr);
|
||||||
|
|
||||||
if (sneak)
|
if (sneak)
|
||||||
fatigueLoss = fFatigueSneakBase + encumbrance * fFatigueSneakMult;
|
fatigueLoss = fFatigueSneakBase + encumbrance * fFatigueSneakMult;
|
||||||
else
|
else
|
||||||
|
|
|
@ -222,45 +222,42 @@ namespace MWMechanics
|
||||||
magicEffects = effects;
|
magicEffects = effects;
|
||||||
|
|
||||||
float resisted = 0;
|
float resisted = 0;
|
||||||
if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful)
|
// Effects with no resistance attribute belonging to them can not be resisted
|
||||||
|
if (ESM::MagicEffect::getResistanceEffect(effectId) == -1)
|
||||||
|
return 0.f;
|
||||||
|
|
||||||
|
float resistance = getEffectResistanceAttribute(effectId, magicEffects);
|
||||||
|
|
||||||
|
int willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified();
|
||||||
|
float luck = static_cast<float>(stats.getAttribute(ESM::Attribute::Luck).getModified());
|
||||||
|
float x = (willpower + 0.1f * luck) * stats.getFatigueTerm();
|
||||||
|
|
||||||
|
// This makes spells that are easy to cast harder to resist and vice versa
|
||||||
|
float castChance = 100.f;
|
||||||
|
if (spell != NULL && !caster.isEmpty() && caster.getClass().isActor())
|
||||||
{
|
{
|
||||||
// Effects with no resistance attribute belonging to them can not be resisted
|
castChance = getSpellSuccessChance(spell, caster, NULL, false); // Uncapped casting chance
|
||||||
if (ESM::MagicEffect::getResistanceEffect(effectId) == -1)
|
|
||||||
return 0.f;
|
|
||||||
|
|
||||||
float resistance = getEffectResistanceAttribute(effectId, magicEffects);
|
|
||||||
|
|
||||||
int willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified();
|
|
||||||
float luck = static_cast<float>(stats.getAttribute(ESM::Attribute::Luck).getModified());
|
|
||||||
float x = (willpower + 0.1f * luck) * stats.getFatigueTerm();
|
|
||||||
|
|
||||||
// This makes spells that are easy to cast harder to resist and vice versa
|
|
||||||
float castChance = 100.f;
|
|
||||||
if (spell != NULL && !caster.isEmpty() && caster.getClass().isActor())
|
|
||||||
{
|
|
||||||
castChance = getSpellSuccessChance(spell, caster, NULL, false); // Uncapped casting chance
|
|
||||||
}
|
|
||||||
if (castChance > 0)
|
|
||||||
x *= 50 / castChance;
|
|
||||||
|
|
||||||
float roll = Misc::Rng::rollClosedProbability() * 100;
|
|
||||||
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
|
|
||||||
roll -= resistance;
|
|
||||||
|
|
||||||
if (x <= roll)
|
|
||||||
x = 0;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
|
|
||||||
x = 100;
|
|
||||||
else
|
|
||||||
x = roll / std::min(x, 100.f);
|
|
||||||
}
|
|
||||||
|
|
||||||
x = std::min(x + resistance, 100.f);
|
|
||||||
|
|
||||||
resisted = x;
|
|
||||||
}
|
}
|
||||||
|
if (castChance > 0)
|
||||||
|
x *= 50 / castChance;
|
||||||
|
|
||||||
|
float roll = Misc::Rng::rollClosedProbability() * 100;
|
||||||
|
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
|
||||||
|
roll -= resistance;
|
||||||
|
|
||||||
|
if (x <= roll)
|
||||||
|
x = 0;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
|
||||||
|
x = 100;
|
||||||
|
else
|
||||||
|
x = roll / std::min(x, 100.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
x = std::min(x + resistance, 100.f);
|
||||||
|
|
||||||
|
resisted = x;
|
||||||
|
|
||||||
return resisted;
|
return resisted;
|
||||||
}
|
}
|
||||||
|
@ -458,13 +455,11 @@ namespace MWMechanics
|
||||||
}
|
}
|
||||||
|
|
||||||
float magnitudeMult = 1;
|
float magnitudeMult = 1;
|
||||||
if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && target.getClass().isActor())
|
|
||||||
{
|
|
||||||
if (absorbed)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Try reflecting
|
if (!absorbed)
|
||||||
if (!reflected && !caster.isEmpty() && caster != target && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable))
|
{
|
||||||
|
// Reflect harmful effects
|
||||||
|
if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && !reflected && !caster.isEmpty() && caster != target && !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable))
|
||||||
{
|
{
|
||||||
float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude();
|
float reflect = target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Reflect).getMagnitude();
|
||||||
bool isReflected = (Misc::Rng::roll0to99() < reflect);
|
bool isReflected = (Misc::Rng::roll0to99() < reflect);
|
||||||
|
@ -488,16 +483,17 @@ namespace MWMechanics
|
||||||
else if (castByPlayer)
|
else if (castByPlayer)
|
||||||
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}");
|
MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResisted}");
|
||||||
}
|
}
|
||||||
|
else if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful && castByPlayer && target != caster)
|
||||||
|
{
|
||||||
|
// If player is attempting to cast a harmful spell and it wasn't fully resisted, show the target's HP bar
|
||||||
|
MWBase::Environment::get().getWindowManager()->setEnemy(target);
|
||||||
|
}
|
||||||
|
|
||||||
if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState())
|
if (target == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState())
|
||||||
magnitudeMult = 0;
|
magnitudeMult = 0;
|
||||||
|
|
||||||
// If player is attempting to cast a harmful spell, show the target's HP bar
|
|
||||||
if (castByPlayer && target != caster)
|
|
||||||
MWBase::Environment::get().getWindowManager()->setEnemy(target);
|
|
||||||
|
|
||||||
// Notify the target actor they've been hit
|
// Notify the target actor they've been hit
|
||||||
if (target != caster && !caster.isEmpty())
|
if (target != caster && !caster.isEmpty() && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful)
|
||||||
target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true);
|
target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -462,10 +462,15 @@ namespace MWWorld
|
||||||
float Class::getNormalizedEncumbrance(const Ptr &ptr) const
|
float Class::getNormalizedEncumbrance(const Ptr &ptr) const
|
||||||
{
|
{
|
||||||
float capacity = getCapacity(ptr);
|
float capacity = getCapacity(ptr);
|
||||||
|
float encumbrance = getEncumbrance(ptr);
|
||||||
|
|
||||||
|
if (encumbrance == 0)
|
||||||
|
return 0.f;
|
||||||
|
|
||||||
if (capacity == 0)
|
if (capacity == 0)
|
||||||
return 1.f;
|
return 1.f;
|
||||||
|
|
||||||
return getEncumbrance(ptr) / capacity;
|
return encumbrance / capacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Class::getSound(const MWWorld::ConstPtr&) const
|
std::string Class::getSound(const MWWorld::ConstPtr&) const
|
||||||
|
|
|
@ -1621,7 +1621,7 @@ namespace MWWorld
|
||||||
if (!paused)
|
if (!paused)
|
||||||
doPhysics (duration);
|
doPhysics (duration);
|
||||||
|
|
||||||
updatePlayer(paused);
|
updatePlayer();
|
||||||
|
|
||||||
mPhysics->debugDraw();
|
mPhysics->debugDraw();
|
||||||
|
|
||||||
|
@ -1637,7 +1637,7 @@ namespace MWWorld
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void World::updatePlayer(bool paused)
|
void World::updatePlayer()
|
||||||
{
|
{
|
||||||
MWWorld::Ptr player = getPlayerPtr();
|
MWWorld::Ptr player = getPlayerPtr();
|
||||||
|
|
||||||
|
@ -1670,7 +1670,7 @@ namespace MWWorld
|
||||||
bool swimming = isSwimming(player);
|
bool swimming = isSwimming(player);
|
||||||
|
|
||||||
static const float i1stPersonSneakDelta = getStore().get<ESM::GameSetting>().find("i1stPersonSneakDelta")->getFloat();
|
static const float i1stPersonSneakDelta = getStore().get<ESM::GameSetting>().find("i1stPersonSneakDelta")->getFloat();
|
||||||
if(!paused && sneaking && !(swimming || inair))
|
if (sneaking && !(swimming || inair))
|
||||||
mRendering->getCamera()->setSneakOffset(i1stPersonSneakDelta);
|
mRendering->getCamera()->setSneakOffset(i1stPersonSneakDelta);
|
||||||
else
|
else
|
||||||
mRendering->getCamera()->setSneakOffset(0.f);
|
mRendering->getCamera()->setSneakOffset(0.f);
|
||||||
|
|
|
@ -129,7 +129,7 @@ namespace MWWorld
|
||||||
Ptr copyObjectToCell(const ConstPtr &ptr, CellStore* cell, ESM::Position pos, int count, bool adjustPos);
|
Ptr copyObjectToCell(const ConstPtr &ptr, CellStore* cell, ESM::Position pos, int count, bool adjustPos);
|
||||||
|
|
||||||
void updateSoundListener();
|
void updateSoundListener();
|
||||||
void updatePlayer(bool paused);
|
void updatePlayer();
|
||||||
|
|
||||||
void preloadSpells();
|
void preloadSpells();
|
||||||
|
|
||||||
|
|
|
@ -231,29 +231,13 @@ void Wizard::MainWizard::setupInstallations()
|
||||||
|
|
||||||
void Wizard::MainWizard::runSettingsImporter()
|
void Wizard::MainWizard::runSettingsImporter()
|
||||||
{
|
{
|
||||||
|
writeSettings();
|
||||||
|
|
||||||
QString path(field(QLatin1String("installation.path")).toString());
|
QString path(field(QLatin1String("installation.path")).toString());
|
||||||
|
|
||||||
// Create the file if it doesn't already exist, else the importer will fail
|
|
||||||
QString userPath(toQString(mCfgMgr.getUserConfigPath()));
|
QString userPath(toQString(mCfgMgr.getUserConfigPath()));
|
||||||
QFile file(userPath + QLatin1String("openmw.cfg"));
|
QFile file(userPath + QLatin1String("openmw.cfg"));
|
||||||
|
|
||||||
if (!file.exists()) {
|
|
||||||
if (!file.open(QIODevice::ReadWrite)) {
|
|
||||||
// File cannot be created
|
|
||||||
QMessageBox msgBox;
|
|
||||||
msgBox.setWindowTitle(tr("Error writing OpenMW configuration file"));
|
|
||||||
msgBox.setIcon(QMessageBox::Critical);
|
|
||||||
msgBox.setStandardButtons(QMessageBox::Ok);
|
|
||||||
msgBox.setText(tr("<html><head/><body><p><b>Could not open or create %1 for writing</b></p> \
|
|
||||||
<p>Please make sure you have the right permissions \
|
|
||||||
and try again.</p></body></html>").arg(file.fileName()));
|
|
||||||
msgBox.exec();
|
|
||||||
return qApp->quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the arguments to run the importer
|
// Construct the arguments to run the importer
|
||||||
QStringList arguments;
|
QStringList arguments;
|
||||||
|
|
||||||
|
|
14
appveyor.yml
14
appveyor.yml
|
@ -18,8 +18,8 @@ platform:
|
||||||
- x64
|
- x64
|
||||||
|
|
||||||
configuration:
|
configuration:
|
||||||
- Debug
|
# - Debug
|
||||||
# - Release
|
- Release
|
||||||
|
|
||||||
# For the Qt, Boost, CMake, etc installs
|
# For the Qt, Boost, CMake, etc installs
|
||||||
#os: Visual Studio 2017
|
#os: Visual Studio 2017
|
||||||
|
@ -55,6 +55,10 @@ build_script:
|
||||||
- cmd: if %PLATFORM%==x64 set build=MSVC%msvc%_64
|
- cmd: if %PLATFORM%==x64 set build=MSVC%msvc%_64
|
||||||
- cmd: msbuild %build%\OpenMW.sln /t:Build /p:Configuration=%configuration% /m:2 /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
- cmd: msbuild %build%\OpenMW.sln /t:Build /p:Configuration=%configuration% /m:2 /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
|
||||||
|
|
||||||
|
after_build:
|
||||||
|
- cmd: if %PLATFORM%==x64 7z a OpenMW_MSVC%msvc%_x64.zip %APPVEYOR_BUILD_FOLDER%\MSVC%msvc%_64\Release\ -xr"!*.pdb"
|
||||||
|
- cmd: if %PLATFORM%==x64 7z a OpenMW_MSVC%msvc%_x64_pdb.zip %APPVEYOR_BUILD_FOLDER%\MSVC%msvc%_64\Release\*.pdb
|
||||||
|
|
||||||
test: off
|
test: off
|
||||||
|
|
||||||
#notifications:
|
#notifications:
|
||||||
|
@ -63,3 +67,9 @@ test: off
|
||||||
# -
|
# -
|
||||||
# on_build_failure: true
|
# on_build_failure: true
|
||||||
# on_build_status_changed: true
|
# on_build_status_changed: true
|
||||||
|
|
||||||
|
artifacts:
|
||||||
|
- path: OpenMW_MSVC%msvc%_x64.zip
|
||||||
|
name: OpenMW_MSVC%msvc%_x64
|
||||||
|
- path: OpenMW_MSVC%msvc%_x64_pdb.zip
|
||||||
|
name: OpenMW_MSVC%msvc%_x64_pdb
|
||||||
|
|
|
@ -57,6 +57,7 @@ void ESMReader::close()
|
||||||
mCtx.subCached = false;
|
mCtx.subCached = false;
|
||||||
mCtx.recName.clear();
|
mCtx.recName.clear();
|
||||||
mCtx.subName.clear();
|
mCtx.subName.clear();
|
||||||
|
mHeader.blank();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESMReader::openRaw(Files::IStreamPtr _esm, const std::string& name)
|
void ESMReader::openRaw(Files::IStreamPtr _esm, const std::string& name)
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "utf8stream.hpp"
|
||||||
|
|
||||||
namespace Misc
|
namespace Misc
|
||||||
{
|
{
|
||||||
class StringUtils
|
class StringUtils
|
||||||
|
@ -56,6 +58,70 @@ public:
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Utf8Stream::UnicodeChar toLowerUtf8(Utf8Stream::UnicodeChar ch)
|
||||||
|
{
|
||||||
|
// Russian alphabet
|
||||||
|
if (ch >= 0x0410 && ch < 0x0430)
|
||||||
|
return ch += 0x20;
|
||||||
|
|
||||||
|
// Cyrillic IO character
|
||||||
|
if (ch == 0x0401)
|
||||||
|
return ch += 0x50;
|
||||||
|
|
||||||
|
// Latin alphabet
|
||||||
|
if (ch >= 0x41 && ch < 0x60)
|
||||||
|
return ch += 0x20;
|
||||||
|
|
||||||
|
// Deutch characters
|
||||||
|
if (ch == 0xc4 || ch == 0xd6 || ch == 0xdc)
|
||||||
|
return ch += 0x20;
|
||||||
|
if (ch == 0x1e9e)
|
||||||
|
return 0xdf;
|
||||||
|
|
||||||
|
// TODO: probably we will need to support characters from other languages
|
||||||
|
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string lowerCaseUtf8(const std::string str)
|
||||||
|
{
|
||||||
|
if (str.empty())
|
||||||
|
return str;
|
||||||
|
|
||||||
|
// Decode string as utf8 characters, convert to lower case and pack them to string
|
||||||
|
std::string out;
|
||||||
|
Utf8Stream stream (str.c_str());
|
||||||
|
while (!stream.eof ())
|
||||||
|
{
|
||||||
|
Utf8Stream::UnicodeChar character = toLowerUtf8(stream.peek());
|
||||||
|
|
||||||
|
if (character <= 0x7f)
|
||||||
|
out.append(1, static_cast<char>(character));
|
||||||
|
else if (character <= 0x7ff)
|
||||||
|
{
|
||||||
|
out.append(1, static_cast<char>(0xc0 | ((character >> 6) & 0x1f)));
|
||||||
|
out.append(1, static_cast<char>(0x80 | (character & 0x3f)));
|
||||||
|
}
|
||||||
|
else if (character <= 0xffff)
|
||||||
|
{
|
||||||
|
out.append(1, static_cast<char>(0xe0 | ((character >> 12) & 0x0f)));
|
||||||
|
out.append(1, static_cast<char>(0x80 | ((character >> 6) & 0x3f)));
|
||||||
|
out.append(1, static_cast<char>(0x80 | (character & 0x3f)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out.append(1, static_cast<char>(0xf0 | ((character >> 18) & 0x07)));
|
||||||
|
out.append(1, static_cast<char>(0x80 | ((character >> 12) & 0x3f)));
|
||||||
|
out.append(1, static_cast<char>(0x80 | ((character >> 6) & 0x3f)));
|
||||||
|
out.append(1, static_cast<char>(0x80 | (character & 0x3f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.consume();
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
static bool ciLess(const std::string &x, const std::string &y) {
|
static bool ciLess(const std::string &x, const std::string &y) {
|
||||||
return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci());
|
return std::lexicographical_compare(x.begin(), x.end(), y.begin(), y.end(), ci());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#ifndef MISC_UTF8ITER_HPP
|
#ifndef MISC_UTF8ITER_HPP
|
||||||
#define MISC_UTF8ITER_HPP
|
#define MISC_UTF8ITER_HPP
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
#include <tuple>
|
#include <tuple>
|
||||||
|
|
||||||
class Utf8Stream
|
class Utf8Stream
|
||||||
|
|
|
@ -191,35 +191,22 @@ public:
|
||||||
node->setViewDataMap(mViewDataMap);
|
node->setViewDataMap(mViewDataMap);
|
||||||
parent->addChild(node);
|
parent->addChild(node);
|
||||||
|
|
||||||
if (center.x() - size > mMaxX
|
if (node->getSize() > mMinSize)
|
||||||
|| center.x() + size < mMinX
|
|
||||||
|| center.y() - size > mMaxY
|
|
||||||
|| center.y() + size < mMinY )
|
|
||||||
// Out of bounds of the actual terrain - this will happen because
|
|
||||||
// we rounded the size up to the next power of two
|
|
||||||
{
|
|
||||||
// Still create and return an empty node so as to not break the assumption that each QuadTreeNode has either 4 or 0 children.
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node->getSize() <= mMinSize)
|
|
||||||
{
|
|
||||||
// We arrived at a leaf
|
|
||||||
float minZ,maxZ;
|
|
||||||
if (mStorage->getMinMaxHeights(size, center, minZ, maxZ))
|
|
||||||
{
|
|
||||||
float cellWorldSize = mStorage->getCellWorldSize();
|
|
||||||
osg::BoundingBox boundingBox(osg::Vec3f((center.x()-size)*cellWorldSize, (center.y()-size)*cellWorldSize, minZ),
|
|
||||||
osg::Vec3f((center.x()+size)*cellWorldSize, (center.y()+size)*cellWorldSize, maxZ));
|
|
||||||
node->setBoundingBox(boundingBox);
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
addChildren(node);
|
addChildren(node);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We arrived at a leaf
|
||||||
|
float minZ, maxZ;
|
||||||
|
mStorage->getMinMaxHeights(size, center, minZ, maxZ);
|
||||||
|
|
||||||
|
float cellWorldSize = mStorage->getCellWorldSize();
|
||||||
|
osg::BoundingBox boundingBox(osg::Vec3f((center.x()-size)*cellWorldSize, (center.y()-size)*cellWorldSize, minZ),
|
||||||
|
osg::Vec3f((center.x()+size)*cellWorldSize, (center.y()+size)*cellWorldSize, maxZ));
|
||||||
|
node->setBoundingBox(boundingBox);
|
||||||
|
|
||||||
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
osg::ref_ptr<RootNode> getRootNode()
|
osg::ref_ptr<RootNode> getRootNode()
|
||||||
|
@ -248,7 +235,6 @@ QuadTreeWorld::QuadTreeWorld(osg::Group *parent, osg::Group *compileRoot, Resour
|
||||||
|
|
||||||
QuadTreeWorld::~QuadTreeWorld()
|
QuadTreeWorld::~QuadTreeWorld()
|
||||||
{
|
{
|
||||||
ensureQuadTreeBuilt();
|
|
||||||
mViewDataMap->clear();
|
mViewDataMap->clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
293
docs/cs-manual/source/record-filters.rst
Normal file
293
docs/cs-manual/source/record-filters.rst
Normal file
|
@ -0,0 +1,293 @@
|
||||||
|
Record Filters
|
||||||
|
##############
|
||||||
|
|
||||||
|
Filters are a key element of the OpenMW CS user interface, they allow rapid and
|
||||||
|
easy access to records presented in all tables. In order to use this
|
||||||
|
application effectively you need to familiarise yourself with all the concepts
|
||||||
|
and instructions explained in this chapter. The filter system is somewhat
|
||||||
|
unusual at first glance, but once you understand the basics it will be fairly
|
||||||
|
intuitive and easy to use
|
||||||
|
|
||||||
|
Filters are a key element to using the OpenMW CS efficiently by allowing you to
|
||||||
|
narrow down the table entries very quickly and find what you are looking for.
|
||||||
|
The filter system might appear unusual at first, you don't just type in a word
|
||||||
|
and get all instances where it occurs, instead filters are first-class objects
|
||||||
|
in the CS with their own table. This allows you to define very specific filters
|
||||||
|
for your project and store them on disc to use in the next session. The CS
|
||||||
|
allows you fine-grained control, you can choose whether to make a filter
|
||||||
|
persistent between session, only for one session or use a one-off filter by
|
||||||
|
typing it directly into the filter field.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Terms used
|
||||||
|
**********
|
||||||
|
|
||||||
|
Filter
|
||||||
|
A Filter is generally speaking a tool able to filter the elements of a
|
||||||
|
table, that is select some elements while discarding others, according to
|
||||||
|
some criteria. These criteria are written using their own syntax.
|
||||||
|
|
||||||
|
Criterion
|
||||||
|
A criterion describes some condition a record needs to satisfy in order to
|
||||||
|
be selected. They are written using a special syntax which is explained
|
||||||
|
below. We can logically combine multiple criteria in a filter for finer
|
||||||
|
control.
|
||||||
|
|
||||||
|
Expression
|
||||||
|
Expressions are how we perform filtering. They look like functions in a
|
||||||
|
programming language: they have a name and accept a number of arguments.
|
||||||
|
The expression evaluates to either ``true`` or ``false`` for every record in
|
||||||
|
the table. The arguments are expressions themselves.
|
||||||
|
|
||||||
|
Arity
|
||||||
|
The arity of an expression tells us how many arguments it takes. Expressions
|
||||||
|
taking no arguments are called *nullary*, those taking one argument are
|
||||||
|
known as *unary* expressions and those taking two arguments are called
|
||||||
|
*binary*.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Interface
|
||||||
|
*********
|
||||||
|
|
||||||
|
Above each table there is a text field which is used to enter a filter: either
|
||||||
|
one predefined by the OpenMW CS developers or one made by you. Another
|
||||||
|
important element is the filter table found under *View* → *Filters*. You
|
||||||
|
should see the default filters made by the OpenMW team in the table. The table
|
||||||
|
has the columns *Filter*, *Description* and *Modified*.
|
||||||
|
|
||||||
|
ID
|
||||||
|
A unique name used to refer to this filter. Note that every ID has a
|
||||||
|
scope prefix, we will explain these soon.
|
||||||
|
|
||||||
|
Modified
|
||||||
|
This is the same as for all the other records, it tells us whether the
|
||||||
|
filter is *added* or *removed*. Filters are specific to a project instead of
|
||||||
|
a content file, they have no effect on the game itself.
|
||||||
|
|
||||||
|
Filter
|
||||||
|
The actual contents of the filter are given here using the filter syntax.
|
||||||
|
Change the expressions to modify what the filter returns.
|
||||||
|
|
||||||
|
Description
|
||||||
|
A textual description of what the filter does.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Using predefined filters
|
||||||
|
************************
|
||||||
|
|
||||||
|
To use a filter you have to type its ID into the filter field above a table.
|
||||||
|
|
||||||
|
For instance, try to opening the objects table (under the world menu) and type
|
||||||
|
into the filters field ``project::weapons``. As soon as you complete the text
|
||||||
|
the table will show only the weapons. The string ``project::weapons`` is the ID
|
||||||
|
of one of the predefined filters. This means that in order to use the filter
|
||||||
|
inside the table you type its name inside the filter field.
|
||||||
|
|
||||||
|
Filter IDs follow these general conventions:
|
||||||
|
|
||||||
|
- IDs of filters for a specific record type contain usually the name of a
|
||||||
|
specific group. For instance the ``project::weapons`` filter contains the
|
||||||
|
term ``weapons``. Plural form is always used.
|
||||||
|
|
||||||
|
- When filtering a specific subgroup the ID is prefixed with the name of the
|
||||||
|
more general filter. For instance ``project::weaponssilver`` will filter only
|
||||||
|
silver weapons and ``project::weaponsmagical`` will filter only magical
|
||||||
|
weapons.
|
||||||
|
|
||||||
|
- There are few exceptions from the above rule. For instance there are
|
||||||
|
``project::added``, ``project::removed``, ``project::modified`` and
|
||||||
|
``project::base``. You might except something more like
|
||||||
|
``project::statusadded`` but in this case requiring these extra characters
|
||||||
|
would not improve readability.
|
||||||
|
|
||||||
|
We strongly recommend you take a look at the filters table right now to see
|
||||||
|
what you can filter with the defaults. Try using the default filters first
|
||||||
|
before writing you own.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Writing your own filters
|
||||||
|
************************
|
||||||
|
|
||||||
|
As mentioned before, filters are just another type of record in the OpenMW CS.
|
||||||
|
To create a new filter you will have to add a new record to the *Filters* table
|
||||||
|
and set its properties to your liking. Filters are created by combining
|
||||||
|
existing filters into more complex ones.
|
||||||
|
|
||||||
|
|
||||||
|
Scopes
|
||||||
|
======
|
||||||
|
|
||||||
|
Every default filter has the prefix ``project``. This is a *scpoe*, a mechanism
|
||||||
|
that determines the lifetime of the filter. These are the supported scopes:
|
||||||
|
|
||||||
|
``project::``
|
||||||
|
Indicates that the filter is to be used throughout the project in multiple
|
||||||
|
sessions. You can restart the CS and the filter will still be there.
|
||||||
|
|
||||||
|
``session::``
|
||||||
|
Indicates that the filter is not stored between multiple sessions and once
|
||||||
|
you quit the OpenMW CS application the filter will be gone. Until then it
|
||||||
|
can be found inside the filters table.
|
||||||
|
|
||||||
|
Project-filters are stored in an internal project file, not final content file
|
||||||
|
meant for the player. Keep in mind when collaborating with other modders that
|
||||||
|
you need to share the same project file.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Writing expressions
|
||||||
|
===================
|
||||||
|
|
||||||
|
The syntax for expressions is as follows:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
<name>
|
||||||
|
<name>(<arg1>)
|
||||||
|
<name>(<arg1>, <arg2>, ..., <argn>)
|
||||||
|
|
||||||
|
Where ``<name>`` is the name of the expression, such as ``string`` and the
|
||||||
|
``<arg>`` are expressions themselves. A nullary expression consists only of its
|
||||||
|
name. A unary expression contains its argument within a pair of parentheses
|
||||||
|
following the name. If there is more than one argument they are separated by
|
||||||
|
commas inside the parentheses.
|
||||||
|
|
||||||
|
An example of a binary expression is ``string("Record Type", weapon)``; the
|
||||||
|
name is ``string``, and it takes two arguments which are strings of string
|
||||||
|
type. The meaning of arguments depends on the expression itself. In this case
|
||||||
|
the first argument is the name of a record column and the second field is the
|
||||||
|
values we want to test it against.
|
||||||
|
|
||||||
|
Strings are sequences of characters and are case-insensitive. If a string
|
||||||
|
contains spaces it must be quoted, otherwise the quotes are optional and
|
||||||
|
ignored.
|
||||||
|
|
||||||
|
|
||||||
|
Constant Expressions
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
These expressions take no arguments and always return the same result.
|
||||||
|
|
||||||
|
``true``
|
||||||
|
Always evaluates to ``true``.
|
||||||
|
|
||||||
|
``false``
|
||||||
|
Always evaluates to ``false``.
|
||||||
|
|
||||||
|
|
||||||
|
Comparison Expressions
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
``string(<column>, <value>)``
|
||||||
|
The ``<value>`` is a regular expression pattern. The expressions evaluates
|
||||||
|
to ``true`` when the value of a record in ``<column>`` matches the pattern.
|
||||||
|
Since the majority of the columns contain string values, ``string`` is among
|
||||||
|
the most often used expressions. Examples:
|
||||||
|
|
||||||
|
``string("Record Type", "Weapon")``
|
||||||
|
Will evaluate to ``true`` for all records containing ``Weapon`` in the
|
||||||
|
*Record Type* column cell.
|
||||||
|
|
||||||
|
``string("Portable", "true")``
|
||||||
|
Will evaluate to ``true`` [#]_ for all records containing word ``true`` inside
|
||||||
|
*Portable* column cell.
|
||||||
|
|
||||||
|
.. [#] There is no Boolean (``true`` or ``false``) value in the OpenMW CS. You
|
||||||
|
should use a string for those.
|
||||||
|
|
||||||
|
|
||||||
|
``value(<value>, (<lower>, <upper>))``
|
||||||
|
Match a value type, such as a number, with a range of possible values. The
|
||||||
|
argument ``<value>`` is the string name of the value we want to compare, the
|
||||||
|
second argument is a pair of lower and upper bounds for the range interval.
|
||||||
|
|
||||||
|
One can use either parentheses ``()`` or brackets ``[]`` to surround the
|
||||||
|
pair. Brackets are inclusive and parentheses are exclusive. We can also mix
|
||||||
|
both styles:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
value("Weight", [20, 50))
|
||||||
|
|
||||||
|
This will match any objects with a weight greater or equal to 20 and
|
||||||
|
strictly less than 50.
|
||||||
|
|
||||||
|
|
||||||
|
Logical Expressions
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
``not <expression>``
|
||||||
|
Logically negates the result of an expression. If ``<expression>`` evaluates
|
||||||
|
to ``true`` the negation is ``false``, and if ``<expression>`` evaluates to
|
||||||
|
``false`` the negation is ``true``. Note that there are no parentheses
|
||||||
|
around the argument.
|
||||||
|
|
||||||
|
``or(<expr1>, <expr2>, ..., <exprN>)``
|
||||||
|
Logical disjunction, evaluates to ``true`` if at least one argument
|
||||||
|
evaluates to ``true`` as well, otherwise the expression evaluates to
|
||||||
|
``false``.
|
||||||
|
|
||||||
|
As an example assume we want to filter for both NPCs and creatures; the
|
||||||
|
expression for that use-case is
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
or(string("record type", "npc"), string("record type", "creature"))
|
||||||
|
|
||||||
|
In this particular case only one argument can evaluate to ``true``, but one
|
||||||
|
can write expressions where multiple arguments can be ``true`` at a time.
|
||||||
|
|
||||||
|
``or(<expr1>, <expr2>, ..., <exprN>)``
|
||||||
|
Logical conjunction, evaluates to ``true`` if and only if all arguments
|
||||||
|
evaluate to ``true`` as well, otherwise the expression evaluates to
|
||||||
|
``false``.
|
||||||
|
|
||||||
|
As an example assume we want to filter for weapons weighting less than a hundred
|
||||||
|
units The expression for that use-case is
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
and(string("record type", "weapon"), value("weight", (0, 100)))
|
||||||
|
|
||||||
|
|
||||||
|
Anonymous filters
|
||||||
|
=================
|
||||||
|
|
||||||
|
Creating a whole new filter when you only intend to use it once can be
|
||||||
|
cumbersome. For that reason the OpenMW CS supports *anonymous* filters which
|
||||||
|
can be typed directly into the filters field of a table. They are not stored
|
||||||
|
anywhere, when you clear the field the filter is gone forever.
|
||||||
|
|
||||||
|
In order to define an anonymous filter you type an exclamation mark as the
|
||||||
|
first character into the field followed by the filter definition (e.g.
|
||||||
|
``!string("Record Type", weapon)`` to filter only for weapons).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Creating and saving filters
|
||||||
|
***************************
|
||||||
|
|
||||||
|
Filters are managed the same way as other records: go to the filters table,
|
||||||
|
right click and select the option *Add Record* from the context menu. You are
|
||||||
|
given a choice between project- or session scope. Choose the scope from the
|
||||||
|
dropdown and type in your desired ID for the filter. A newly created filter
|
||||||
|
does nothing since it still lacks expressions. In order to add your queries you
|
||||||
|
have to edit the filter record.
|
||||||
|
|
||||||
|
|
||||||
|
Replacing the default filters set
|
||||||
|
=================================
|
||||||
|
|
||||||
|
OpenMW CS allows you to substitute the default filter set for the entire
|
||||||
|
application. This will affect the default filters for all content files that
|
||||||
|
have not been edited on this computer and user account.
|
||||||
|
|
||||||
|
Create a new content file, add the desired filters, remove the undesired ones
|
||||||
|
and save. Now rename the *project* file to ``defaultfilters`` and make sure the
|
||||||
|
``.omwaddon.project`` file extension is removed. This file will act as a
|
||||||
|
template for all new files from now on. If you wish to go back to the
|
||||||
|
old default set rename or remove this custom file.
|
62
docs/cs-manual/source/record-types.rst
Normal file
62
docs/cs-manual/source/record-types.rst
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
Record Types
|
||||||
|
############
|
||||||
|
|
||||||
|
A game world contains many items, such as chests, weapons and monsters. All
|
||||||
|
these items are merely instances of templates we call *Objects*. The OpenMW CS
|
||||||
|
*Objects* table contains information about each of these template objects, such
|
||||||
|
as its value and weight in the case of items, or an aggression level in the
|
||||||
|
case of NPCs.
|
||||||
|
|
||||||
|
The following is a list of all Record Types and what you can tell OpenMW CS
|
||||||
|
about each of them.
|
||||||
|
|
||||||
|
Activator
|
||||||
|
Activators can have a script attached to them. As long as the cell this
|
||||||
|
object is in is active the script will be run once per frame.
|
||||||
|
|
||||||
|
Potion
|
||||||
|
This is a potion which is not self-made. It has an Icon for your inventory,
|
||||||
|
weight, coin value, and an attribute called *Auto Calc* set to ``False``.
|
||||||
|
This means that the effects of this potion are pre-configured. This does not
|
||||||
|
happen when the player makes their own potion.
|
||||||
|
|
||||||
|
Apparatus
|
||||||
|
This is a tool to make potions. Again there’s an icon for your inventory as
|
||||||
|
well as a weight and a coin value. It also has a *Quality* value attached to
|
||||||
|
it: the higher the number, the better the effect on your potions will be.
|
||||||
|
The *Apparatus Type* describes if the item is a *Calcinator*, *Retort*,
|
||||||
|
*Alembic* or *Mortar & Pestle*.
|
||||||
|
|
||||||
|
Armor
|
||||||
|
This type of item adds *Enchantment Points* to the mix. Every piece of
|
||||||
|
clothing or armor has a "pool" of potential *Magicka* that gets unlocked
|
||||||
|
when the player enchants it. Strong enchantments consume more magicka from
|
||||||
|
this pool: the stronger the enchantment, the more *Enchantment Points* each
|
||||||
|
cast will take up. *Health* means the amount of hit points this piece of
|
||||||
|
armor has. If it sustains enough damage, the armor will be destroyed.
|
||||||
|
Finally, *Armor Value* tells the game how much points to add to the player
|
||||||
|
character’s *Armor Rating*.
|
||||||
|
|
||||||
|
Book
|
||||||
|
This includes scrolls and notes. For the game to make the distinction
|
||||||
|
between books and scrolls, an extra property, *Scroll*, has been added.
|
||||||
|
Under the *Skill* column a scroll or book can have an in-game skill listed.
|
||||||
|
Reading this item will raise the player’s level in that specific skill.
|
||||||
|
|
||||||
|
Clothing
|
||||||
|
These items work just like armors, but confer no protective properties.
|
||||||
|
Rather than *Armor Type*, these items have a *Clothing Type*.
|
||||||
|
|
||||||
|
Container
|
||||||
|
This is all the stuff that stores items, from chests to sacks to plants. Its
|
||||||
|
*Capacity* shows how much stuff you can put in the container. You can
|
||||||
|
compare it to the maximum allowed load a player character can carry. A
|
||||||
|
container, however, will just refuse to take the item in question when it
|
||||||
|
gets "over-encumbered". Organic Containers are containers such as plants.
|
||||||
|
Containers that respawn are not safe to store stuff in. After a certain
|
||||||
|
amount of time they will reset to their default contents, meaning that
|
||||||
|
everything in them is gone forever.
|
||||||
|
|
||||||
|
Creature
|
||||||
|
These can be monsters, animals and the like.
|
||||||
|
|
168
docs/cs-manual/source/tables.rst
Normal file
168
docs/cs-manual/source/tables.rst
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
Tables
|
||||||
|
######
|
||||||
|
|
||||||
|
If you have launched OpenMW CS already and played around with it for a bit, you
|
||||||
|
will have noticed that the interface is made entirely of tables. This does not
|
||||||
|
mean it works just like a spreadsheet application though, it would be more
|
||||||
|
accurate to think of databases instead. Due to the vast amounts of information
|
||||||
|
involved with Morrowind tables made the most sense. You have to be able to spot
|
||||||
|
information quickly and be able to change them on the fly.
|
||||||
|
|
||||||
|
|
||||||
|
Used Terms
|
||||||
|
**********
|
||||||
|
|
||||||
|
Record
|
||||||
|
An entry in OpenMW CS representing an item, location, sound, NPC or anything
|
||||||
|
else.
|
||||||
|
|
||||||
|
Instance, Object
|
||||||
|
When an item is placed in the world, it does not create a whole new record
|
||||||
|
each time, but an *instance* of the *object*.
|
||||||
|
|
||||||
|
For example, the game world might contain a lot of exquisite belts on
|
||||||
|
different NPCs and in many crates, but they all refer to one specific
|
||||||
|
instance: the Exquisite Belt record. In this case, all those belts in crates
|
||||||
|
and on NPCs are instances. The central Exquisite Belt instance is called an
|
||||||
|
*object*. This allows modders to make changes to all items of the same type
|
||||||
|
in one place.
|
||||||
|
|
||||||
|
If you wanted all exquisite belts to have 4000 enchantment points rather
|
||||||
|
than 400, you would only need to change the object Exquisite Belt rather
|
||||||
|
than all exquisite belt instances individually.
|
||||||
|
|
||||||
|
Some columns are recurring throughout OpenMW CS, they show up in (nearly) every
|
||||||
|
table.
|
||||||
|
|
||||||
|
ID
|
||||||
|
Each item, location, sound, etc. gets the same unique identifier in both
|
||||||
|
OpenMW CS and Morrowind. This is usually a very self-explanatory name. For
|
||||||
|
example, the ID for the (unique) black pants of Caius Cosades is
|
||||||
|
``Caius_pants``. This allows players to manipulate the game in many ways.
|
||||||
|
For example, they could add these pants to their inventory by opening the
|
||||||
|
console and entering: ``player- >addItem Caius_pants``. In both Morrowind
|
||||||
|
and OpenMW CS the ID is the primary way to identify all these different
|
||||||
|
parts of the game.
|
||||||
|
|
||||||
|
Modified
|
||||||
|
This column shows what has happened (if anything) to this record. There are
|
||||||
|
four possible states in which it can exist:
|
||||||
|
|
||||||
|
Base
|
||||||
|
The record is unmodified and from a content file other than the one
|
||||||
|
currently being edited.
|
||||||
|
|
||||||
|
Added
|
||||||
|
This record has been added in the currently content file.
|
||||||
|
|
||||||
|
Modified
|
||||||
|
Similar to *base*, but has been changed in some way.
|
||||||
|
|
||||||
|
Deleted
|
||||||
|
Similar to *base*, but has been removed as an entry. This does not mean,
|
||||||
|
however, that the occurrences in the game itself have been removed! For
|
||||||
|
example, if you were to remove the ``CharGen_Bed`` entry from
|
||||||
|
``morrowind.esm``, it does not mean the bedroll in the basement of the
|
||||||
|
Census and Excise Office in Seyda Neen will be gone. You will have to
|
||||||
|
delete that instance yourself or make sure that that object is replaced
|
||||||
|
by something that still exists otherwise the player will get crashes in
|
||||||
|
the worst case scenario.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
World Screens
|
||||||
|
*************
|
||||||
|
|
||||||
|
The contents of the game world can be changed by choosing one of the options in
|
||||||
|
the appropriate menu at the top of the screen.
|
||||||
|
|
||||||
|
|
||||||
|
Regions
|
||||||
|
=======
|
||||||
|
|
||||||
|
This describes the general areas of Vvardenfell. Each of these areas has
|
||||||
|
different rules about things such as encounters and weather.
|
||||||
|
|
||||||
|
Name
|
||||||
|
This is how the game will show the player's location in-game.
|
||||||
|
|
||||||
|
MapColour
|
||||||
|
This is a six-digit hexadecimal representation of the colour used to
|
||||||
|
identify the region on the map available in *World* → *Region Map*.
|
||||||
|
|
||||||
|
Sleep Encounter
|
||||||
|
These are the rules for what kinds of enemies the player might encounter
|
||||||
|
when sleeping outside in the wilderness.
|
||||||
|
|
||||||
|
|
||||||
|
Cells
|
||||||
|
=====
|
||||||
|
|
||||||
|
Expansive worlds such as Vvardenfell, with all its items, NPCs, etc. have a lot
|
||||||
|
going on simultaneously. But if the player is in Balmora, why would the
|
||||||
|
computer need to keep track the exact locations of NPCs walking through the
|
||||||
|
corridors in a Vivec canton? All that work would be quite useless and bring
|
||||||
|
the player's system down to its knees! So the world has been divided up into
|
||||||
|
squares we call *cells*. Once your character enters a cell, the game will load
|
||||||
|
everything that is going on in that cell so the player can interact with it.
|
||||||
|
|
||||||
|
In the original Morrowind this could be seen when a small loading bar would
|
||||||
|
appear near the bottom of the screen while travelling; the player had just
|
||||||
|
entered a new cell and the game had to load all the items and NPCs. The *Cells*
|
||||||
|
screen in OpenMW CS provides you with a list of cells in the game, both the
|
||||||
|
interior cells (houses, dungeons, mines, etc.) and the exterior cells (the
|
||||||
|
outside world).
|
||||||
|
|
||||||
|
Sleep Forbidden
|
||||||
|
Can the player sleep on the floor? In most cities it is forbidden to sleep
|
||||||
|
outside. Sleeping in the wilderness carries its own risks of attack, though,
|
||||||
|
and this entry lets you decide if a player should be allowed to sleep on the
|
||||||
|
floor in this cell or not.
|
||||||
|
|
||||||
|
Interior Water
|
||||||
|
Should water be rendered in this interior cell? The game world consists of
|
||||||
|
an endless ocean at height 0, then the landscape is added. If part of the
|
||||||
|
landscape goes below height 0, the player will see water.
|
||||||
|
|
||||||
|
Setting the cell’s Interior Water to true tells the game that this cell that
|
||||||
|
there needs to be water at height 0. This is useful for dungeons or mines
|
||||||
|
that have water in them.
|
||||||
|
|
||||||
|
Setting the cell’s Interior Water to ``false`` tells the game that the water
|
||||||
|
at height 0 should not be used. This flag is useless for outside cells.
|
||||||
|
|
||||||
|
Interior Sky
|
||||||
|
Should this interior cell have a sky? This is a rather unique case. The
|
||||||
|
Tribunal expansion took place in a city on the mainland. Normally this would
|
||||||
|
require the city to be composed of exterior cells so it has a sky, weather
|
||||||
|
and the like. But if the player is in an exterior cell and were to look at
|
||||||
|
their in-game map, they would see Vvardenfell with an overview of all
|
||||||
|
exterior cells. The player would have to see the city’s very own map, as if
|
||||||
|
they were walking around in an interior cell.
|
||||||
|
|
||||||
|
So the developers decided to create a workaround and take a bit of both: The
|
||||||
|
whole city would technically work exactly like an interior cell, but it
|
||||||
|
would need a sky as if it was an exterior cell. That is what this is. This
|
||||||
|
is why the vast majority of the cells you will find in this screen will have
|
||||||
|
this option set to false: It is only meant for these "fake exteriors".
|
||||||
|
|
||||||
|
Region
|
||||||
|
To which Region does this cell belong? This has an impact on the way the
|
||||||
|
game handles weather and encounters in this area. It is also possible for a
|
||||||
|
cell not to belong to any region.
|
||||||
|
|
||||||
|
|
||||||
|
Objects
|
||||||
|
=======
|
||||||
|
|
||||||
|
This is a library of all the items, triggers, containers, NPCs, etc. in the
|
||||||
|
game. There are several kinds of Record Types. Depending on which type a record
|
||||||
|
is, it will need specific information to function. For example, an NPC needs a
|
||||||
|
value attached to its aggression level. A chest, of course, does not. All
|
||||||
|
Record Types contain at least a 3D model or else the player would not see them.
|
||||||
|
Usually they also have a *Name*, which is what the players sees when they hover
|
||||||
|
their reticle over the object during the game.
|
||||||
|
|
||||||
|
Please refer to the Record Types chapter for an overview of what each type of
|
||||||
|
object does and what you can tell OpenMW CS about these objects.
|
||||||
|
|
|
@ -21,4 +21,6 @@ few chapters to familiarise yourself with the new interface.
|
||||||
tour
|
tour
|
||||||
files-and-directories
|
files-and-directories
|
||||||
starting-dialog
|
starting-dialog
|
||||||
|
tables
|
||||||
|
record-types
|
||||||
|
record-filters
|
||||||
|
|
|
@ -10,7 +10,11 @@
|
||||||
</Widget>
|
</Widget>
|
||||||
|
|
||||||
<!-- Spell list -->
|
<!-- Spell list -->
|
||||||
<Widget type="SpellView" skin="MW_SpellView" position="8 38 268 518" align="Left Top Stretch" name="SpellView">
|
<Widget type="SpellView" skin="MW_SpellView" position="8 38 268 490" align="Left Top Stretch" name="SpellView">
|
||||||
|
</Widget>
|
||||||
|
|
||||||
|
<!-- Search box-->
|
||||||
|
<Widget type="EditBox" skin="MW_TextBoxEditWithBorder" position="8 535 268 23" align="Left Bottom HStretch" name="FilterEdit">
|
||||||
</Widget>
|
</Widget>
|
||||||
|
|
||||||
</Widget>
|
</Widget>
|
||||||
|
|
|
@ -150,5 +150,9 @@
|
||||||
<file alias="selection-mode-cube">selection-mode-cube.png</file>
|
<file alias="selection-mode-cube">selection-mode-cube.png</file>
|
||||||
<file alias="selection-mode-cube-corner">selection-mode-cube-corner.png</file>
|
<file alias="selection-mode-cube-corner">selection-mode-cube-corner.png</file>
|
||||||
<file alias="selection-mode-cube-sphere">selection-mode-cube-sphere.png</file>
|
<file alias="selection-mode-cube-sphere">selection-mode-cube-sphere.png</file>
|
||||||
|
<file alias="brush-point">brush-point.png</file>
|
||||||
|
<file alias="brush-square">brush-square.png</file>
|
||||||
|
<file alias="brush-circle">brush-circle.png</file>
|
||||||
|
<file alias="brush-custom">brush-custom.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
|
@ -216,6 +216,9 @@ followers attack on sight = false
|
||||||
# Can loot non-fighting actors during death animation
|
# Can loot non-fighting actors during death animation
|
||||||
can loot during death animation = true
|
can loot during death animation = true
|
||||||
|
|
||||||
|
# Makes the value of filled soul gems dependent only on soul magnitude (with formula from the Morrowind Code Patch)
|
||||||
|
rebalance soul gem values = false
|
||||||
|
|
||||||
[General]
|
[General]
|
||||||
|
|
||||||
# Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16).
|
# Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16).
|
||||||
|
|
|
@ -109,6 +109,16 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="rebalanceSoulGemValuesCheckBox">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><html><head/><body><p>If this setting is true, the value of filled soul gems is dependent only on soul magnitude.</p><p>The default value is false.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Rebalance soul gem values</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item alignment="Qt::AlignLeft">
|
<item alignment="Qt::AlignLeft">
|
||||||
<widget class="QWidget" name="showOwnedGroup" native="true">
|
<widget class="QWidget" name="showOwnedGroup" native="true">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
|
|
Loading…
Reference in a new issue