openmw/apps/opencs/editor.cpp
elsid 524efc4e18
Fix logging and config for portable setup
Setup logging after setting up paths but before settings loading to get proper
log records earlier.

Read configuration by navmeshtool and bulletobjecttool the same way editor and
engine do to properly handle --replace config and --config arguments.

Remove mode and autoSetupLogging arguments from setupLogging since they are no
longer used.

Use temp path to write crash logs because default paths might not be available
for portable setup.
2023-05-30 09:44:23 +02:00

443 lines
14 KiB
C++

#include "editor.hpp"
#include <QApplication>
#include <QLocalServer>
#include <QLocalSocket>
#include <QMessageBox>
#include <boost/program_options/options_description.hpp>
#include <components/debug/debugging.hpp>
#include <components/debug/debuglog.hpp>
#include <components/fallback/validate.hpp>
#include <components/misc/rng.hpp>
#include <components/nifosg/nifloader.hpp>
#include <components/settings/settings.hpp>
#include "model/doc/document.hpp"
#include "model/world/data.hpp"
#ifdef _WIN32
#include <components/windows.hpp>
#endif
using namespace Fallback;
CS::Editor::Editor (int argc, char **argv)
: mConfigVariables(readConfiguration()), mSettingsState (mCfgMgr), mDocumentManager (mCfgMgr),
mPid(""), mLock(), mMerge (mDocumentManager),
mIpcServerName ("org.openmw.OpenCS"), mServer(nullptr), mClientSocket(nullptr)
{
std::pair<Files::PathContainer, std::vector<std::string> > config = readConfig();
mViewManager = new CSVDoc::ViewManager(mDocumentManager);
if (argc > 1)
{
mFileToLoad = argv[1];
mDataDirs = config.first;
}
NifOsg::Loader::setShowMarkers(true);
mDocumentManager.setFileData(mFsStrict, config.first, config.second);
mNewGame.setLocalData (mLocal);
mFileDialog.setLocalData (mLocal);
mMerge.setLocalData (mLocal);
connect (&mDocumentManager, SIGNAL (documentAdded (CSMDoc::Document *)),
this, SLOT (documentAdded (CSMDoc::Document *)));
connect (&mDocumentManager, SIGNAL (documentAboutToBeRemoved (CSMDoc::Document *)),
this, SLOT (documentAboutToBeRemoved (CSMDoc::Document *)));
connect (&mDocumentManager, SIGNAL (lastDocumentDeleted()),
this, SLOT (lastDocumentDeleted()));
connect (mViewManager, SIGNAL (newGameRequest ()), this, SLOT (createGame ()));
connect (mViewManager, SIGNAL (newAddonRequest ()), this, SLOT (createAddon ()));
connect (mViewManager, SIGNAL (loadDocumentRequest ()), this, SLOT (loadDocument ()));
connect (mViewManager, SIGNAL (editSettingsRequest()), this, SLOT (showSettings ()));
connect (mViewManager, SIGNAL (mergeDocument (CSMDoc::Document *)), this, SLOT (mergeDocument (CSMDoc::Document *)));
connect (&mStartup, SIGNAL (createGame()), this, SLOT (createGame ()));
connect (&mStartup, SIGNAL (createAddon()), this, SLOT (createAddon ()));
connect (&mStartup, SIGNAL (loadDocument()), this, SLOT (loadDocument ()));
connect (&mStartup, SIGNAL (editConfig()), this, SLOT (showSettings ()));
connect (&mFileDialog, SIGNAL(signalOpenFiles (const boost::filesystem::path&)),
this, SLOT(openFiles (const boost::filesystem::path&)));
connect (&mFileDialog, SIGNAL(signalCreateNewFile (const boost::filesystem::path&)),
this, SLOT(createNewFile (const boost::filesystem::path&)));
connect (&mFileDialog, SIGNAL (rejected()), this, SLOT (cancelFileDialog ()));
connect (&mNewGame, SIGNAL (createRequest (const boost::filesystem::path&)),
this, SLOT (createNewGame (const boost::filesystem::path&)));
connect (&mNewGame, SIGNAL (cancelCreateGame()), this, SLOT (cancelCreateGame ()));
}
CS::Editor::~Editor ()
{
delete mViewManager;
mPidFile.close();
if(mServer && boost::filesystem::exists(mPid))
static_cast<void> ( // silence coverity warning
remove(mPid.string().c_str())); // ignore any error
}
boost::program_options::variables_map CS::Editor::readConfiguration()
{
boost::program_options::variables_map variables;
boost::program_options::options_description desc("Syntax: openmw-cs <options>\nAllowed options");
desc.add_options()
("data", boost::program_options::value<Files::MaybeQuotedPathContainer>()->default_value(Files::MaybeQuotedPathContainer(), "data")->multitoken()->composing())
("data-local", boost::program_options::value<Files::MaybeQuotedPathContainer::value_type>()->default_value(Files::MaybeQuotedPathContainer::value_type(), ""))
("fs-strict", boost::program_options::value<bool>()->implicit_value(true)->default_value(false))
("encoding", boost::program_options::value<std::string>()->default_value("win1252"))
("resources", boost::program_options::value<Files::MaybeQuotedPath>()->default_value(Files::MaybeQuotedPath(), "resources"))
("fallback-archive", boost::program_options::value<std::vector<std::string>>()->
default_value(std::vector<std::string>(), "fallback-archive")->multitoken())
("fallback", boost::program_options::value<FallbackMap>()->default_value(FallbackMap(), "")
->multitoken()->composing(), "fallback values")
("script-blacklist", boost::program_options::value<std::vector<std::string>>()->default_value(std::vector<std::string>(), "")
->multitoken(), "exclude specified script from the verifier (if the use of the blacklist is enabled)")
("script-blacklist-use", boost::program_options::value<bool>()->implicit_value(true)
->default_value(true), "enable script blacklisting");
Files::ConfigurationManager::addCommonOptions(desc);
boost::program_options::notify(variables);
mCfgMgr.readConfiguration(variables, desc, false);
Settings::Manager::load(mCfgMgr, true);
setupLogging(mCfgMgr.getLogPath().string(), applicationName);
return variables;
}
std::pair<Files::PathContainer, std::vector<std::string> > CS::Editor::readConfig(bool quiet)
{
boost::program_options::variables_map& variables = mConfigVariables;
Fallback::Map::init(variables["fallback"].as<FallbackMap>().mMap);
mEncodingName = variables["encoding"].as<std::string>();
mDocumentManager.setEncoding(ToUTF8::calculateEncoding(mEncodingName));
mFileDialog.setEncoding (QString::fromUtf8(mEncodingName.c_str()));
mDocumentManager.setResourceDir (mResources = variables["resources"].as<Files::MaybeQuotedPath>());
if (variables["script-blacklist-use"].as<bool>())
mDocumentManager.setBlacklistedScripts (
variables["script-blacklist"].as<std::vector<std::string>>());
mFsStrict = variables["fs-strict"].as<bool>();
Files::PathContainer dataDirs, dataLocal;
if (!variables["data"].empty()) {
dataDirs = asPathContainer(variables["data"].as<Files::MaybeQuotedPathContainer>());
}
Files::PathContainer::value_type local(variables["data-local"].as<Files::MaybeQuotedPathContainer::value_type>());
if (!local.empty())
{
boost::filesystem::create_directories(local);
dataLocal.push_back(local);
}
mCfgMgr.filterOutNonExistingPaths(dataDirs);
mCfgMgr.filterOutNonExistingPaths(dataLocal);
if (!dataLocal.empty())
mLocal = dataLocal[0];
else
{
QMessageBox messageBox;
messageBox.setWindowTitle (tr ("No local data path available"));
messageBox.setIcon (QMessageBox::Critical);
messageBox.setStandardButtons (QMessageBox::Ok);
messageBox.setText(tr("<br><b>OpenCS is unable to access the local data directory. This may indicate a faulty configuration or a broken install.</b>"));
messageBox.exec();
QApplication::exit (1);
}
dataDirs.insert (dataDirs.end(), dataLocal.begin(), dataLocal.end());
//iterate the data directories and add them to the file dialog for loading
mFileDialog.addFiles(dataDirs);
return std::make_pair (dataDirs, variables["fallback-archive"].as<std::vector<std::string>>());
}
void CS::Editor::createGame()
{
mStartup.hide();
if (mNewGame.isHidden())
mNewGame.show();
mNewGame.raise();
mNewGame.activateWindow();
}
void CS::Editor::cancelCreateGame()
{
if (!mDocumentManager.isEmpty())
return;
mNewGame.hide();
if (mStartup.isHidden())
mStartup.show();
mStartup.raise();
mStartup.activateWindow();
}
void CS::Editor::createAddon()
{
mStartup.hide();
mFileDialog.clearFiles();
readConfig(/*quiet*/true);
mFileDialog.showDialog (CSVDoc::ContentAction_New);
}
void CS::Editor::cancelFileDialog()
{
if (!mDocumentManager.isEmpty())
return;
mFileDialog.hide();
if (mStartup.isHidden())
mStartup.show();
mStartup.raise();
mStartup.activateWindow();
}
void CS::Editor::loadDocument()
{
mStartup.hide();
mFileDialog.clearFiles();
readConfig(/*quiet*/true);
mFileDialog.showDialog (CSVDoc::ContentAction_Edit);
}
void CS::Editor::openFiles (const boost::filesystem::path &savePath, const std::vector<boost::filesystem::path> &discoveredFiles)
{
std::vector<boost::filesystem::path> files;
if(discoveredFiles.empty())
{
for (const QString &path : mFileDialog.selectedFilePaths())
files.emplace_back(path.toUtf8().constData());
}
else
{
files = discoveredFiles;
}
mDocumentManager.addDocument (files, savePath, false);
mFileDialog.hide();
}
void CS::Editor::createNewFile (const boost::filesystem::path &savePath)
{
std::vector<boost::filesystem::path> files;
for (const QString &path : mFileDialog.selectedFilePaths()) {
files.emplace_back(path.toUtf8().constData());
}
files.push_back (savePath);
mDocumentManager.addDocument (files, savePath, true);
mFileDialog.hide();
}
void CS::Editor::createNewGame (const boost::filesystem::path& file)
{
std::vector<boost::filesystem::path> files;
files.push_back (file);
mDocumentManager.addDocument (files, file, true);
mNewGame.hide();
}
void CS::Editor::showStartup()
{
if(mStartup.isHidden())
mStartup.show();
mStartup.raise();
mStartup.activateWindow();
}
void CS::Editor::showSettings()
{
if (mSettings.isHidden())
mSettings.show();
mSettings.move (QCursor::pos());
mSettings.raise();
mSettings.activateWindow();
}
bool CS::Editor::makeIPCServer()
{
try
{
mPid = boost::filesystem::temp_directory_path();
mPid /= "openmw-cs.pid";
bool pidExists = boost::filesystem::exists(mPid);
mPidFile.open(mPid);
mLock = boost::interprocess::file_lock(mPid.string().c_str());
if(!mLock.try_lock())
{
Log(Debug::Error) << "Error: OpenMW-CS is already running.";
return false;
}
#ifdef _WIN32
mPidFile << GetCurrentProcessId() << std::endl;
#else
mPidFile << getpid() << std::endl;
#endif
mServer = new QLocalServer(this);
if(pidExists)
{
// hack to get the temp directory path
mServer->listen("dummy");
QString fullPath = mServer->fullServerName();
mServer->close();
fullPath.remove(QRegExp("dummy$"));
fullPath += mIpcServerName;
if(boost::filesystem::exists(fullPath.toUtf8().constData()))
{
// TODO: compare pid of the current process with that in the file
Log(Debug::Info) << "Detected unclean shutdown.";
// delete the stale file
if(remove(fullPath.toUtf8().constData()))
Log(Debug::Error) << "Error: can not remove stale connection file.";
}
}
}
catch(const std::exception& e)
{
Log(Debug::Error) << "Error: " << e.what();
return false;
}
if(mServer->listen(mIpcServerName))
{
connect(mServer, SIGNAL(newConnection()), this, SLOT(showStartup()));
return true;
}
mServer->close();
mServer = nullptr;
return false;
}
void CS::Editor::connectToIPCServer()
{
mClientSocket = new QLocalSocket(this);
mClientSocket->connectToServer(mIpcServerName);
mClientSocket->close();
}
int CS::Editor::run()
{
if (mLocal.empty())
return 1;
Misc::Rng::init();
QApplication::setQuitOnLastWindowClosed(true);
if (mFileToLoad.empty())
{
mStartup.show();
}
else
{
ESM::ESMReader fileReader;
ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncodingName));
fileReader.setEncoder(&encoder);
fileReader.open(mFileToLoad.string());
std::vector<boost::filesystem::path> discoveredFiles;
for (std::vector<ESM::Header::MasterData>::const_iterator itemIter = fileReader.getGameFiles().begin();
itemIter != fileReader.getGameFiles().end(); ++itemIter)
{
for (Files::PathContainer::const_iterator pathIter = mDataDirs.begin();
pathIter != mDataDirs.end(); ++pathIter)
{
const boost::filesystem::path masterPath = *pathIter / itemIter->name;
if (boost::filesystem::exists(masterPath))
{
discoveredFiles.push_back(masterPath);
break;
}
}
}
discoveredFiles.push_back(mFileToLoad);
QString extension = QString::fromStdString(mFileToLoad.extension().string()).toLower();
if (extension == ".esm")
{
mFileToLoad.replace_extension(".omwgame");
mDocumentManager.addDocument(discoveredFiles, mFileToLoad, false);
}
else if (extension == ".esp")
{
mFileToLoad.replace_extension(".omwaddon");
mDocumentManager.addDocument(discoveredFiles, mFileToLoad, false);
}
else
{
openFiles(mFileToLoad, discoveredFiles);
}
}
return QApplication::exec();
}
void CS::Editor::documentAdded (CSMDoc::Document *document)
{
mViewManager->addView (document);
}
void CS::Editor::documentAboutToBeRemoved (CSMDoc::Document *document)
{
if (mMerge.getDocument()==document)
mMerge.cancel();
}
void CS::Editor::lastDocumentDeleted()
{
QApplication::quit();
}
void CS::Editor::mergeDocument (CSMDoc::Document *document)
{
mMerge.configure (document);
mMerge.show();
mMerge.raise();
mMerge.activateWindow();
}