mudlet/src/Host.cpp
Vadim Peretokin 7bb20fa2ce
add: widget state getters for titles, stylesheets, tooltips and scroll bars (#9645)
#### Brief overview of PR changes/additions

- Seven state getters that are the inverse of setters we already ship:
`getUserWindowTitle`, `getUserWindowStyleSheet`, `getCmdLineStyleSheet`,
`getLabelToolTip`, `getScrollBarVisible`, `getMapWindowTitle` and
`getMapWidgetGeometry`
- Each returns nil plus a message when the window, label or map widget
it names does not exist, reusing the matching setter's wording so the
pair reports the same problems the same way
- 39 specs added to the existing `UI_spec.lua` and `Mapper_spec.lua`

#### Motivation for adding to Mudlet

#9630's audit left 11 Geyser/UI rows untestable purely because the state
those functions set could not be read back; this tranche unblocks them
exactly as #9528's getters unblocked the geometry specs. Scripts get the
same readback symmetry as a side effect.

#### Other info (issues closed, discussion etc)

One deliberate behaviour change: `enableScrollBar`/`disableScrollBar`
now record what they were asked for, so `getScrollBarVisible` answers
for a profile that is not the front tab (whose whole console Mudlet
hides) instead of reporting every background profile's scroll bar as
gone. Wiki pages for the seven functions to follow in Area 51.

**Test case:** busted 1868 passed / 0 failed / 0 errors / 17 pending
(baseline 1829), green twice on the same isolated profile, plus ctest;
31 of the new specs verified by breaking the getters - wrong return
values fail 20, making the not-found branches succeed fails 7, and
dropping the empty-name and nil handling fails 8 more.

Assisted-by: Claude:claude-opus-5
2026-08-05 06:49:23 +02:00

5350 lines
180 KiB
C++

/***************************************************************************
* Copyright (C) 2008-2013 by Heiko Koehn - KoehnHeiko@googlemail.com *
* Copyright (C) 2014 by Ahmed Charles - acharles@outlook.com *
* Copyright (C) 2015-2026 by Stephen Lyons - slysven@virginmedia.com *
* Copyright (C) 2016 by Ian Adkins - ieadkins@gmail.com *
* Copyright (C) 2018 by Huadong Qi - novload@outlook.com *
* Copyright (C) 2023-2025 by Lecker Kebap - Leris@mudlet.org *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "Host.h"
#include "dlgIRC.h"
#include "dlgMapper.h"
#include "dlgModuleManager.h"
#include "dlgNotepad.h"
#include "dlgPackageManager.h"
#include "dlgProfilePreferences.h"
#include "GifTracker.h"
#include "GMCPAuthenticator.h"
#include "LuaInterface.h"
#include "MMCP.h"
#include "MMCPServer.h"
#include "mudlet.h"
#include "TCommandLine.h"
#include "TConsole.h"
#include "TDebug.h"
#include "TDockWidget.h"
#include "TEvent.h"
#include "TLabel.h"
#include "TMainConsole.h"
#include "TMap.h"
#include "TMapViewManager.h"
#include "TMedia.h"
#include "TRoomDB.h"
#include "TScript.h"
#include "TTextBox.h"
#include "TTextEdit.h"
#include "TToolBar.h"
#include "VarUnit.h"
#include "XMLexport.h"
#include "XMLimport.h"
#include "CredentialManager.h"
#include "SecureStringUtils.h"
#include <chrono>
#include <QtConcurrentRun>
#include <QApplication>
#include <QCoreApplication>
#include <QDataStream>
#include <QDirIterator>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QKeyEvent>
#include <QMovie>
#include <QNetworkProxy>
#include <QRandomGenerator>
#include <QRegularExpression>
#include <QSaveFile>
#include <QScopeGuard>
#include <QSettings>
#include <QTemporaryFile>
#include <QTextStream>
#include <zip.h>
#include <memory>
// We are now using code that won't work with really old versions of libzip;
// some of the error handling was improved in 1.0 . Unfortunately libzip 1.7.0
// (and one or two other recent versions) forgot to include the version defines
// and thus broke a test depending on them:
#if defined(LIBZIP_VERSION_MAJOR) && (LIBZIP_VERSION_MAJOR < 1)
#error Mudlet requires a version of libzip of at least 1.0
#endif
using namespace std::chrono;
stopWatch::stopWatch()
: mIsInitialised(false)
, mIsRunning(false)
, mIsPersistent(false)
, mEffectiveStartDateTime()
, mElapsedTime()
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
mEffectiveStartDateTime.setTimeZone(QTimeZone::UTC);
#else
mEffectiveStartDateTime.setTimeSpec(Qt::UTC);
#endif
}
bool stopWatch::start()
{
if (!mIsInitialised) {
mIsInitialised = true;
mEffectiveStartDateTime = QDateTime::currentDateTimeUtc();
mIsRunning = true;
return true;
}
if (mIsRunning) {
// Nothing to do, already running
return false;
}
// Is stopped, so subtract elapsed time from current and set that to be the
// effective start time:
mEffectiveStartDateTime = QDateTime::currentDateTimeUtc().addMSecs(-mElapsedTime);
mIsRunning = true;
return true;
}
bool stopWatch::stop()
{
if (!mIsInitialised) {
// Nothing to do, never started
return false;
}
if (!mIsRunning) {
// Nothing to do, already stopped
return false;
}
// Is running - so stop and note time:
mElapsedTime = mEffectiveStartDateTime.msecsTo(QDateTime::currentDateTimeUtc());
mIsRunning = false;
return true;
}
bool stopWatch::reset()
{
if (!mIsInitialised) {
// Nothing to do, never started
return false;
}
if (!mIsRunning) {
// Not running, so reset elapsed time:
mElapsedTime = 0;
// And reset initialised flag:
mIsInitialised = false;
return true;
}
// Is running so reset effective start time - BUT THIS DOES NOT stop the
// stopwatch:
mEffectiveStartDateTime = QDateTime::currentDateTimeUtc();
return true;
}
void stopWatch::adjustMilliSeconds(const qint64 adjustment)
{
if (!mIsInitialised) {
// We can initialise things in this case by setting the flag and falling
// through into the is not running situation - with the elapsed time
// being zero up to now we just have to add on the adjustment:
mIsInitialised = true;
}
if (!mIsRunning) {
// Not running so adjust stored elapsed time:
mElapsedTime += adjustment;
}
// Is running so adjust effective start time - to increase the effective
// elapsed time we must subtract the adjustment from the effect start time:
mEffectiveStartDateTime = mEffectiveStartDateTime.addMSecs(-adjustment);
}
qint64 stopWatch::getElapsedMilliSeconds() const
{
if (!mIsInitialised) {
// Never started so no elapsed time:
return 0;
}
if (!mIsRunning) {
// Not running - so return elapsed time:
return mElapsedTime;
}
// Is running so calculate elapsed time:
return mEffectiveStartDateTime.msecsTo(QDateTime::currentDateTimeUtc());
}
QString stopWatch::getElapsedDayTimeString() const
{
using namespace std::chrono_literals;
if (!mIsInitialised) {
return qsl("+:0:0:0:0:000");
}
qint64 elapsed = 0;
if (mIsRunning) {
elapsed = mEffectiveStartDateTime.msecsTo(QDateTime::currentDateTimeUtc());
} else {
elapsed = mElapsedTime;
}
bool isNegative = false;
if (elapsed < 0) {
isNegative = true;
elapsed *= -1;
}
qint64 const days = elapsed / std::chrono::milliseconds(24h).count();
qint64 remainder = elapsed - (days * std::chrono::milliseconds(24h).count());
quint8 const hours = static_cast<quint8>(remainder / std::chrono::milliseconds(1h).count());
remainder = remainder - (hours * std::chrono::milliseconds(1h).count());
quint8 const minutes = static_cast<quint8>(remainder / std::chrono::milliseconds(1min).count());
remainder = remainder - (minutes * std::chrono::milliseconds(1min).count());
quint8 const seconds = static_cast<quint8>(remainder / std::chrono::milliseconds(1s).count());
quint16 const milliSeconds = static_cast<quint16>(remainder - (seconds * std::chrono::milliseconds(1s).count()));
return qsl("%1:%2:%3:%4:%5:%6")
.arg((isNegative ? QLatin1String("-") : QLatin1String("+")),
QString::number(days),
QString::number(hours),
QString::number(minutes),
QString::number(seconds),
QString::number(milliSeconds));
}
Host::Host(int port, const QString& hostname, const QString& login, const QString& pass, int id)
: mTelnet(this, hostname)
, mLuaInterpreter(this, hostname, id)
, mMxpClient(this)
, mMxpProcessor(&mMxpClient)
, mMxpFrameManager(this)
, mpMap(new TMap(this, hostname))
, mpMedia(new TMedia(this, hostname))
, mpAuth(new GMCPAuthenticator(this))
, mTimerDebugOutputSuppressionInterval(QTime())
, mSearchOptions(dlgTriggerEditor::SearchOption::SearchOptionNone)
, mBufferSearchOptions(TConsole::SearchOption::SearchOptionNone)
, mpDlgIRC(nullptr)
, mMMCPServer(nullptr)
, mpDlgProfilePreferences(nullptr)
, mMMCPChatPort(csDefaultMMCPHostPort)
, mMMCPChatPrefix(csDefaultChatPrefix)
, mMMCPAutostartServer(false)
, mMMCPAllowPeekRequests(false)
, mMMCPPrefixEmotes(false)
, mMMCPAddChatMessageNewline(true)
, mMMCPAutoAcceptCalls(true)
, mMMCPShowSnoopInMainConsole(true)
, mTutorialForCompactLineAlreadyShown(false)
, mLuaInterface(nullptr)
, mTriggerUnit(this)
, mTimerUnit(this)
, mScriptUnit(this)
, mAliasUnit(this)
, mActionUnit(this)
, mKeyUnit(this)
, mGifTracker()
, mHostID(id)
, mHostName(hostname)
, mLogin(login)
, mPass(pass)
, mPort(port)
{
TDebug::addHost(this, mHostName);
setDisplayFont(QFont(qsl("Bitstream Vera Sans Mono"), 14, QFont::Normal));
// The "autolog" sentinel file controls whether logging the game's text as
// plain text or HTML is immediately resumed on profile loading. Do not
// confuse it with the "autologin" item, which controls whether the profile
// is automatically started when the Mudlet application is run!
mLogStatus = QFile::exists(mudlet::getMudletPath(enums::profileDataItemPath, mHostName, qsl("autolog")));
// "autotimestamp" determines if profile loads with timestamps enabled
mTimeStampStatus = QFile::exists(mudlet::getMudletPath(enums::profileDataItemPath, mHostName, qsl("autotimestamp")));
mLuaInterface.reset(new LuaInterface(this->getLuaInterpreter()->getLuaGlobalState()));
// Copy across the details needed for the "color_table":
mLuaInterpreter.updateAnsi16ColorsInTable();
mLuaInterpreter.updateExtendedAnsiColorsInTable();
const QString directoryLogFile = mudlet::getMudletPath(enums::profileDataItemPath, mHostName, qsl("log"));
const QString logFileName = qsl("%1/errors.txt").arg(directoryLogFile);
const QDir dirLogFile;
if (!dirLogFile.exists(directoryLogFile)) {
if (!dirLogFile.mkpath(directoryLogFile)) {
qWarning() << "Host: failed to create error log directory:" << directoryLogFile;
}
}
mErrorLogFile.setFileName(logFileName);
if (!mErrorLogFile.open(QIODevice::Append)) {
qWarning() << "Host: failed to open error log file for appending:" << mErrorLogFile.errorString();
}
/*
* Mudlet will log messages in ASCII, but force a universal (UTF-8) encoding
* since user-content can contain anything and someone else reviewing
* such logs need not have the same default encoding which would be used
* otherwise - note that this must be done AFTER setDevice(...):
*/
mErrorLogStream.setDevice(&mErrorLogFile);
mGMCP_merge_table_keys.append("Char.Status");
mDoubleClickIgnore.insert('"');
mDoubleClickIgnore.insert('\'');
// search engine load entries
// clang-format off
mSearchEngineData = QMap<QString, QString>(
{
{"Bing", "https://www.bing.com/search?q="},
{"DuckDuckGo", "https://duckduckgo.com/?q="},
{"Google", "https://www.google.com/search?q="}
});
// clang-format on
// These details are filled in by the dlgConnectionProfile class when that
// is used to select a profile however when the profile is auto-loaded or
// selected from a command line argument that does not happen and they need
// to be populated ASAP from the stored details in the profile's settings.
mUrl = readProfileData(qsl("url"));
const QString host_port = readProfileData(qsl("port"));
if (bool isOk = false; !host_port.isEmpty() && host_port.toInt(&isOk) && isOk) {
mPort = host_port.toInt();
}
mLogin = readProfileData(qsl("login"));
const QString sslVal = readProfileData(qsl("ssl_tsl"));
if (!sslVal.isEmpty() && sslVal.toInt() == Qt::Checked) {
mSslTsl = true;
}
const QString val = readProfileData(qsl("autoreconnect"));
setAutoReconnect(!val.isEmpty() && val.toInt() == Qt::Checked);
// This settings also need to be configured, note that the only time not to
// save the setting is on profile loading. Only override the default UTF-8
// encoding if a saved encoding exists:
const QByteArray savedEncoding = readProfileData(qsl("encoding")).toUtf8();
if (!savedEncoding.isEmpty()) {
mTelnet.setEncoding(savedEncoding, false);
}
auto modeStr = readProfileData(qsl("discordmode"));
if (!modeStr.isEmpty()) {
const int modeInt = modeStr.toInt();
if (modeInt >= DiscordDisabled && modeInt <= DiscordShowGameDetails) {
mDiscordMode = static_cast<DiscordMode>(modeInt);
}
} else {
// Migrate old profiles: if the user had opted out of server-side
// Discord, respect that choice by using ShowMudletOnly
auto oldOptin = readProfileData(qsl("discordserveroptin"));
if (!oldOptin.isEmpty() && oldOptin.toInt() != Qt::Checked) {
mDiscordMode = DiscordShowMudletOnly;
}
}
if (mudlet::self()->storingPasswordsSecurely()) {
loadSecuredPassword();
} else {
QString password{readProfileData(qsl("password"))};
if (!password.isEmpty()) {
setPass(password);
password.clear();
}
}
if (mudlet::self()->publicTestVersion) {
thankForUsingPTB();
}
if (mudlet::self()->smFirstLaunch) {
QTimer::singleShot(0ms, this, [this]() {
if (mpConsole) {
mpConsole->mpCommandLine->setPlaceholderText(tr("Text to send to the game"));
}
});
}
connect(&mTelnet, &cTelnet::signal_disconnected, this, [this]() {
purgeTimer.start(1min);
if (getForceMXPProcessorOn()) {
mMxpProcessor.disable();
}
});
connect(&mTelnet, &cTelnet::signal_connected, this, [this]() {
purgeTimer.stop();
// Reset MXP frames on reconnect - per user feedback, frames shouldn't persist
// between sessions as the server should send new FRAME commands
mMxpFrameManager.resetAllFrames();
if (getForceMXPProcessorOn()) {
mMxpProcessor.enable();
// When force-enabling MXP (typically for games like IRE MUDs that don't
// negotiate properly), lock to secure mode for compatibility
mMxpProcessor.setMode(MXP_MODE_CODE_LOCK_SECURE);
qDebug() << "MXP enabled (forced)";
}
});
connect(&purgeTimer, &QTimer::timeout, this, &Host::slot_purgeTemps);
connect(this, &Host::signal_forceMXPProcessorOnChanged, this, [this](bool enabled) {
if (enabled) {
if (!mMxpProcessor.isEnabled()) {
mMxpProcessor.enable();
// When force-enabling MXP (typically for games like IRE MUDs that don't
// negotiate properly), lock to secure mode for compatibility with games
// that use secure tags without sending mode switches
mMxpProcessor.setMode(MXP_MODE_CODE_LOCK_SECURE);
qDebug() << "MXP enabled (forced)";
}
} else if (mMxpProcessor.isEnabled() && !mTelnet.isMXPEnabled()) {
mMxpProcessor.disable();
qDebug() << "MXP disabled (forced)";
}
});
// enable by default in case of offline connection; if the profile connects - timer will be disabled
purgeTimer.start(1min);
auto i = mudlet::self()->mpShortcutsManager->iterator();
while (i.hasNext()) {
auto entry = i.next();
profileShortcuts[entry] = std::make_unique<QKeySequence>(*mudlet::self()->mpShortcutsManager->getSequence(entry));
}
auto settings = mudlet::self()->getQSettings();
const auto interval = settings->value("autosaveIntervalMinutes", 2).toInt();
startMapAutosave(interval);
mMapperCenterSmallAreas = settings->value("mapCenterSmallAreas", false).toBool();
}
Host::~Host()
{
// Mark the host as closing down to prevent keybinding processing during destruction
mIsClosingDown = true;
// This needs to be cleared here while the Host object is still valid,
// otherwise it'll be cleared when the Host object is being destroyed,
// which can lead to a crash when closing multiple profiles at once.
mpLastCommandLineUsed.clear();
mStopWatchMap.clear();
mErrorLogStream.flush();
mErrorLogFile.close();
// Since this is a destructor, it's risky to rely on member variables within the destructor itself.
// To avoid this, we can pass the profile name as an argument instead of accessing it
// directly as a member variable. This ensures the destructor doesn't depend on the
// object's state being valid.
// Don't pass 'this' pointer during destruction - it's unsafe as the Host is being destroyed
TDebug::removeHost(nullptr, mHostName);
}
void Host::forceClose()
{
if (!mpConsole) {
// The main console has gone away so we must already be dying
return;
}
mForcedClose = true;
postMessage(tr("[ ALERT ] - This profile will now save and close."));
// Ensure the above is displayed before proceeding...
qApp->processEvents();
// Because we have set the mForcedClose flag above a requestClose()
// afterwards WILL succeed
}
// Returns true if we are closing down
bool Host::requestClose()
{
if (!mpConsole) {
// The main console has gone away so we must already be dying
return true;
}
// This call ends up at the (void) TMainConsole::closeEvent(...) and causes
// the close() method called here to return a true if the event was
// accepted:
if (!mpConsole->close()) {
// Nope the user doesn't want this to close - and it won't have set its
// mEnableClose flag:
return false;
}
// The above will have initiated a save of the profile (and its map) if it
// got a true returned from the TMainConsole::close() call.
// Get rid of any dialogs we might have open:
closeChildren();
// This time this will succeed as mEnableClose is set:
mpConsole->close();
return true;
}
void Host::closeChildren()
{
mIsClosingDown = true;
const auto hostToolBarMap = getActionUnit()->getToolBarList();
// disconnect before removing objects from memory as sysDisconnectionEvent needs that stuff.
mTelnet.terminateConnection();
stopAllTriggers();
if (mpEditorDialog) {
mpEditorDialog->setAttribute(Qt::WA_DeleteOnClose);
mpEditorDialog->close();
mpEditorDialog = nullptr;
}
for (const QString& consoleName : mpConsole->mSubConsoleMap.keys()) {
// Only user-windows will be in this map:
auto pD = mpConsole->mDockWidgetMap.value(consoleName);
// All User TConsole's will be in this map:
auto pC = mpConsole->mSubConsoleMap.value(consoleName);
if (pD) {
// This undocks the widget
mudlet::self()->removeDockWidget(pD);
}
// This will remove both what pD and pC point at from their respective
// QMaps:
pC->close();
}
if (mpNotePad) {
mpNotePad->save();
mpNotePad->setAttribute(Qt::WA_DeleteOnClose);
mpNotePad->close();
mpNotePad = nullptr;
}
for (TToolBar* pTB : hostToolBarMap) {
if (pTB) {
pTB->setAttribute(Qt::WA_DeleteOnClose);
pTB->deleteLater();
}
}
// close IRC client window if it is open.
if (mpDlgIRC) {
mpDlgIRC->setAttribute(Qt::WA_DeleteOnClose);
mpDlgIRC->deleteLater();
mpDlgIRC = nullptr;
}
}
void Host::loadMap()
{
qDebug() << "Host::loadMap() - restore map case 4.";
if (mpMap->restore(QString())) {
mpMap->audit();
if (mpMap->mpMapper) {
mpMap->mpMapper->mp2dMap->init();
mpMap->mpMapper->updateAreaComboBox();
mpMap->mpMapper->resetAreaComboBoxToPlayerRoomArea();
mpMap->mpMapper->show();
}
}
}
void Host::startMapAutosave(const int interval)
{
if (interval > 0) {
startTimer(interval * 1min);
}
}
void Host::timerEvent(QTimerEvent* event)
{
Q_UNUSED(event)
autoSaveMap();
}
void Host::autoSaveMap()
{
if (mpMap->isUnsaved()) {
#if defined(DEBUG_MAPAUTOSAVE)
QString nowString = QDateTime::currentDateTimeUtc().toString("HH:mm:ss.zzz");
#endif
if (!mIsProfileLoadingSequence) {
#if defined(DEBUG_MAPAUTOSAVE)
qDebug().nospace().noquote() << "Host::autoSaveMap() INFO - map auto save initiated at:" << nowString << ".";
#endif
if (!mpConsole->saveMap(mudlet::getMudletPath(enums::profileMapPathFileName, mHostName, qsl("autosave.dat")))) {
mpMap->setSaveError(true);
} else {
mpMap->setSaveError(false);
}
#if defined(DEBUG_MAPAUTOSAVE)
} else {
qDebug().nospace().noquote() << "Host::autoSaveMap() INFO - map auto save requested at:" << nowString << " but declined whilst \"Host::mIsProfileLoadingSequence\" flag set.";
#endif
}
}
}
void Host::loadPackageInfo()
{
for (const auto& package : std::as_const(mInstalledPackages)) {
const QDir dir(mudlet::self()->getMudletPath(enums::profilePackagePath, getName(), package));
if (dir.exists(qsl("config.lua"))) {
getPackageConfig(dir.absoluteFilePath(qsl("config.lua")));
}
}
}
void Host::createModuleBackup(const QString& filename, const QString& saveName)
{
const QString time = QDateTime::currentDateTime().toString("yyyy-MM-dd#HH-mm-ss");
QFile::copy(filename, saveName + time);
}
QList<QFuture<bool>> Host::pendingXmlSaveFutures() const
{
QList<QFuture<bool>> futures;
for (const auto& writer : writers) {
futures += writer->saveFutures;
}
return futures;
}
void Host::waitForAsyncXmlSave()
{
// Snapshot on the main thread - see pendingXmlSaveFutures()
const auto futures = pendingXmlSaveFutures();
for (auto future : futures) {
future.waitForFinished();
}
}
QList<Host::ModuleWriteJob> Host::prepareModuleSaves(bool backup)
{
// Runs on the main thread so it can safely read the live trigger/timer/... lists
// (via writeModuleXML) and mutate the `writers`/`mModulesToSync`/`modulesToWrite`
// bookkeeping. The returned jobs carry everything writeModuleFiles() needs, so the
// background task never touches any of that shared state.
QList<ModuleWriteJob> jobs;
mModulesToSync.clear();
QMapIterator<QString, QStringList> it(modulesToWrite);
while (it.hasNext()) {
it.next();
const QStringList entry = it.value();
const QString moduleName = it.key();
const QString filename = entry.at(0);
if (!mModulesLoadedOk.contains(moduleName)) {
continue;
}
QString xmlFilename = filename;
if (filename.endsWith(qsl("mpackage"), Qt::CaseInsensitive) || filename.endsWith(qsl("zip"), Qt::CaseInsensitive)) {
xmlFilename = mudlet::getMudletPath(enums::profilePackagePathFileName, mHostName, moduleName);
}
auto writer = std::make_shared<XMLexport>(this);
writer->writeModuleXML(moduleName);
// `writers` is the sole owner; the job carries a non-owning pointer so the
// XMLexport is only ever destroyed on the main thread (via xmlSaved()).
writers.insert(xmlFilename, writer);
jobs.append({writer.get(), moduleName, filename, xmlFilename, backup});
if (entry.at(1).toInt()) {
mModulesToSync << moduleName;
}
}
modulesToWrite.clear();
return jobs;
}
void Host::writeModuleFiles(const QList<ModuleWriteJob>& jobs)
{
// Runs on a background thread: pure file I/O, no access to shared save-bookkeeping.
if (jobs.isEmpty()) {
return;
}
const QString savePath = mudlet::getMudletPath(enums::moduleBackupsPath);
auto savePathDir = QDir(savePath);
if (!savePathDir.exists()) {
savePathDir.mkpath(savePath);
}
for (const auto& job : jobs) {
if (job.backup) {
createModuleBackup(job.filename, savePath + job.moduleName);
}
if (!job.writer->saveModuleXml(job.xmlFilename)) {
qWarning().noquote().nospace() << "Host::writeModuleFiles() WARNING - failed to write module \"" << job.moduleName << "\" to \"" << job.xmlFilename << "\".";
}
updateModuleZips(job.filename, job.moduleName);
}
}
void Host::reloadModules()
{
//synchronize modules across sessions
for (auto otherHost : mudlet::self()->getHostManager()) {
if (otherHost == this || !otherHost->mpConsole) {
continue;
}
const QMap<QString, int>& modulePri = otherHost->mModulePriorities;
QMap<int, QStringList> moduleOrder;
auto modulePrioritiesIt = modulePri.constBegin();
while (modulePrioritiesIt != modulePri.constEnd()) {
moduleOrder[modulePrioritiesIt.value()].append(modulePrioritiesIt.key());
++modulePrioritiesIt;
}
QMapIterator<int, QStringList> it(moduleOrder);
while (it.hasNext()) {
it.next();
const QStringList moduleList = it.value();
for (const auto& moduleName : moduleList) {
if (mModulesToSync.contains(moduleName)) {
otherHost->reloadModule(moduleName, mHostName);
}
}
}
}
mModulesToSync.clear();
}
void Host::updateModuleZips(const QString& zipName, const QString& moduleName)
{
if (!(zipName.endsWith(qsl("mpackage"), Qt::CaseInsensitive) || zipName.endsWith(qsl("zip"), Qt::CaseInsensitive))) {
return;
}
zip* zipFile = nullptr;
const QString packagePathName = mudlet::getMudletPath(enums::profilePackagePath, mHostName, moduleName);
const QString filename_xml = mudlet::getMudletPath(enums::profilePackagePathFileName, mHostName, moduleName);
int err = 0;
zipFile = zip_open(zipName.toStdString().c_str(), ZIP_CREATE, &err);
if (!zipFile) {
zip_error_t zipError;
zip_error_init_with_code(&zipError, err);
/*: This zipError message is shown when the libzip library code is unable
to open the file that was to be the end result of the export process.
As this may be an existing file anywhere in the computer's
file-system(s) it is possible that permissions on the directory or an
existing file that is to be overwritten may be a source of problems
here.
*/
qWarning().noquote().nospace() << "Host::updateModuleZips(\"" << zipName << "\", \"" << moduleName << "\") WARNING - failed to open module to update it, error: \""
<< zip_error_strerror(&zipError) << "\"";
zip_error_fini(&zipError);
return;
}
const QDir packageDir = QDir(packagePathName);
if (!packageDir.exists()) {
packageDir.mkpath(packagePathName);
}
const int xmlIndex = zip_name_locate(zipFile, qsl("%1.xml").arg(moduleName).toUtf8().constData(), ZIP_FL_ENC_GUESS);
zip_delete(zipFile, xmlIndex);
struct zip_source* s = zip_source_file(zipFile, filename_xml.toUtf8().constData(), 0, -1);
if (mudlet::smDebugMode && s == nullptr) {
//: This error message will appear when the xml file inside the module zip cannot be updated for some reason.
TDebug(QColor(Qt::white), QColor(Qt::red)) << tr("Failed to open xml file \"%1\" inside module %2 to update it. Error message was: \"%3\".").arg(filename_xml, zipName, zip_strerror(zipFile));
}
err = zip_file_add(zipFile, qsl("%1.xml").arg(moduleName).toUtf8().constData(), s, ZIP_FL_ENC_UTF_8 | ZIP_FL_OVERWRITE);
if (zipFile) {
// This is the point at which the data is written out to the archive,
// might take a bit of time:
err = zip_close(zipFile);
}
if (err == -1) {
if (mudlet::smDebugMode && err == -1) {
//: This error message will appear when a module is saved as package but cannot be done for some reason.
TDebug(QColor(Qt::white), QColor(Qt::red)) << tr("Failed to save \"%1\" to module \"%2\". Error message was: \"%3\".").arg(moduleName, zipName, zip_strerror(zipFile));
}
// Properly dispose of things after failing to zip_close(...) the
// archive:
zip_discard(zipFile);
}
}
void Host::reloadModule(const QString& syncModuleName, const QString& syncingFromHost)
{
//Wait till profile is finished saving
if (syncingFromHost.isEmpty() && currentlySavingProfile()) {
//create a dummy object to singleshot connect (disconnect/delete after execution)
QObject* obj = new QObject(this);
connect(this, &Host::profileSaveFinished, obj, [=, this]() {
reloadModule(syncModuleName);
obj->deleteLater();
});
return;
}
QMap<QString, QStringList> installedModules = mInstalledModules;
QMapIterator<QString, QStringList> moduleIterator(installedModules);
while (moduleIterator.hasNext()) {
moduleIterator.next();
const auto& moduleName = moduleIterator.key();
const auto& moduleLocation = moduleIterator.value()[0];
QString fileName = moduleLocation;
if (moduleName == syncModuleName) {
if (!syncingFromHost.isEmpty() && (fileName.endsWith(qsl(".zip"), Qt::CaseInsensitive) || fileName.endsWith(qsl(".mpackage"), Qt::CaseInsensitive))) {
uninstallPackage(moduleName, enums::PackageModuleType::ModuleSync);
fileName = mudlet::getMudletPath(enums::profilePackagePathFileName, syncingFromHost, moduleName);
installPackage(fileName, enums::PackageModuleType::ModuleSync);
QStringList moduleEntry;
moduleEntry << moduleLocation;
moduleEntry << qsl("0");
mInstalledModules[moduleName] = moduleEntry;
} else {
uninstallPackage(moduleName, enums::PackageModuleType::ModuleSync);
installPackage(fileName, enums::PackageModuleType::ModuleSync);
}
}
}
//iterate through mInstalledModules again and reset the entry flag to be correct.
//both the installedModules and mInstalled should be in the same order now as well
moduleIterator.toFront();
while (moduleIterator.hasNext()) {
moduleIterator.next();
const QStringList entry = installedModules[moduleIterator.key()];
mInstalledModules[moduleIterator.key()] = entry;
}
}
std::pair<bool, QString> Host::changeModuleSync(const QString& moduleName, const QLatin1String& value)
{
if (moduleName.isEmpty()) {
return {false, qsl("module name cannot be an empty string")};
}
if (mInstalledModules.contains(moduleName)) {
QStringList moduleStringList = mInstalledModules[moduleName];
moduleStringList[1] = value;
mInstalledModules[moduleName] = moduleStringList;
return {true, QString()};
}
return {false, qsl("module name '%1' not found").arg(moduleName)};
}
std::pair<bool, QString> Host::getModuleSync(const QString& moduleName)
{
if (moduleName.isEmpty()) {
return {false, qsl("module name cannot be an empty string")};
}
if (mInstalledModules.contains(moduleName)) {
QStringList moduleStringList = mInstalledModules[moduleName];
return {true, moduleStringList[1]};
}
return {false, qsl("module name '%1' not found").arg(moduleName)};
}
bool Host::resetProfile_phase1()
{
if (mResetProfile) {
qWarning() << "Host::resetProfile_phase1() called while reset is already in progress, ignoring";
return false;
}
// A test-mode waitForEvent() is blocked in a nested event loop on this
// profile's lua_State; phase2 would lua_close() that state underneath it (a
// use-after-free). Refuse until the wait finishes. Always empty (so a no-op)
// outside MUDLET_TEST_MODE, where waitForEvent() is inert.
if (mLuaInterpreter.hasPendingEventWaits()) {
qWarning() << "Host::resetProfile_phase1() called while a waitForEvent() is blocked, ignoring";
return false;
}
mAliasUnit.stopAllTriggers();
mTriggerUnit.stopAllTriggers();
mTimerUnit.stopAllTriggers();
mKeyUnit.stopAllTriggers();
mResetProfile = true;
QTimer::singleShot(0ms, this, [this]() {
resetProfile_phase2();
});
return true;
}
void Host::resetProfile_phase2()
{
getAliasUnit()->removeAllTempAliases();
getTimerUnit()->removeAllTempTimers();
getTriggerUnit()->removeAllTempTriggers();
getKeyUnit()->removeAllTempKeys();
removeAllNonPersistentStopWatches();
mpMedia->stopAllMediaPlayers();
mAliasUnit.doCleanup();
mTimerUnit.doCleanup();
mTriggerUnit.doCleanup();
mKeyUnit.doCleanup();
mActionUnit.doCleanup();
mpConsole->resetMainConsole();
// Drain queued DeferredDelete events so old TLabel destructors run their
// luaL_unref against the still-live Lua state. Without this, those unrefs
// execute after initLuaGlobals() has swapped in a new state and corrupt
// freshly-issued registry indices in the new state, which surfaces as
// "attempt to call a number value" when label callbacks fire.
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
mEventHandlerMap.clear();
mAnonymousEventHandlerFunctions.clear();
mEventMap.clear();
mLuaInterpreter.abortAllDownloads();
mLuaInterpreter.initLuaGlobals();
// initLuaGlobals() closed the old lua_State, so the LuaInterface (and its
// VarUnit tree of registry references) must be rebuilt against the new one.
// The VarUnit's name-keyed saved/hidden bookkeeping does not depend on the
// lua_State though and is only repopulated by XMLimport at profile open, so
// carry it over - otherwise the first save after a reset silently drops all
// of the user's saved variables from the profile:
const QSet<QString> savedVars = mLuaInterface->getVarUnit()->savedVars;
const QSet<QString> hiddenByUserVars = mLuaInterface->getVarUnit()->hiddenByUser;
mLuaInterface.reset(new LuaInterface(mLuaInterpreter.getLuaGlobalState()));
mLuaInterface->getVarUnit()->savedVars = savedVars;
mLuaInterface->getVarUnit()->hiddenByUser = hiddenByUserVars;
mLuaInterpreter.loadGlobal();
// Profile load hides Mudlet's own Lua API right after loadGlobal() (see
// mudlet::slot_connectionDialogueFinished()); do the same here so the
// Variables view keeps showing only the user's variables after a reset:
hideMudletsVariables();
// Have to recopy the values into the Lua "color_table"
mLuaInterpreter.updateAnsi16ColorsInTable();
mLuaInterpreter.updateExtendedAnsiColorsInTable();
mBlockScriptCompile = false;
mAliasUnit.reenableAllTriggers();
mTimerUnit.reenableAllTriggers();
mTriggerUnit.reenableAllTriggers();
mKeyUnit.reenableAllTriggers();
// This is where the scripts for the profile get compiled (which confirms
// that they are valid) and all the Lua code outside of functions gets run:
getTimerUnit()->compileAll();
getTriggerUnit()->compileAll();
getAliasUnit()->compileAll();
getActionUnit()->compileAll();
getKeyUnit()->compileAll();
getScriptUnit()->compileAll(true);
mResetProfile = false;
TEvent event{};
event.mArgumentList.append(QLatin1String("sysLoadEvent"));
event.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
// A zero value is how we send a "false" value - which indicates that
// this is for a reset profile and NOT a freshly loaded one:
event.mArgumentList.append(QString::number(0));
event.mArgumentTypeList.append(ARGUMENT_TYPE_BOOLEAN);
raiseEvent(event);
qDebug() << "resetProfile() DONE";
}
// Saves profile to disk - does not save items dirty in the editor, however.
// takes a directory to save in or an empty string for the default location
// as well as a boolean whenever to sync the modules or not
// returns true+filepath if successful or false+error message otherwise
std::tuple<bool, QString, QString> Host::saveProfile(const QString& saveFolder, const QString& saveName, bool syncModules)
{
QString directory_xml;
if (saveFolder.isEmpty()) {
directory_xml = mudlet::getMudletPath(enums::profileXmlFilesPath, getName());
} else {
directory_xml = saveFolder;
}
QString filename_xml;
if (saveName.isEmpty()) {
filename_xml = qsl("%1/%2.xml").arg(directory_xml, QDateTime::currentDateTime().toString(qsl("yyyy-MM-dd#HH-mm-ss")));
} else {
filename_xml = qsl("%1/%2.xml").arg(directory_xml, saveName);
}
if (!mLoadedOk) {
return {false, filename_xml, qsl("profile was not loaded correctly to begin with")};
}
if (mIsProfileLoadingSequence) {
// If we're inside of profile loading sequence modules might not be loaded yet, thus we can accidentally clear their contents
return {false, filename_xml, qsl("profile loading is in progress")};
}
const QDir dir_xml;
if (!dir_xml.exists(directory_xml)) {
dir_xml.mkpath(directory_xml);
}
if (currentlySavingProfile()) {
return {false, QString(), qsl("a save is already in progress")};
}
if (saveFolder.isEmpty() && saveName.isEmpty()) {
// This is likely to be the save as the profile is closed
qDebug().noquote().nospace() << "Host::saveProfile(...) INFO - called with no saveFolder or saveName arguments for profile '" << mHostName
<< "' so assuming it is an end of session save and the TCommandLines' histories need saving...";
emit signal_saveCommandLinesHistory();
}
auto writer = std::make_shared<XMLexport>(this);
writers.insert(qsl("profile"), writer);
if (!writer->exportHost(filename_xml)) {
writers.remove(qsl("profile"));
return {false, filename_xml, tr("the profile is no longer available")};
}
// Build the module XML documents and register their writers here, on the main
// thread, while the live profile data is quiescent: the background task below then
// only serializes the prepared documents and never touches `writers`,
// `modulesToWrite` or `mModulesToSync` (doing so from a pool thread was a
// heap-corrupting data race).
const bool backupModules = saveName != qsl("autosave");
const QList<ModuleWriteJob> moduleJobs = prepareModuleSaves(backupModules);
mWritingHostAndModules = true;
// emit signal to notify the UI that the save button should get disabled momentarily
// this needs to run after `writers` and `mWritingHostAndModules` have been set
// so that the currentlySavingProfile() check can run properly
emit profileSaveStarted();
// Only process events if we're not in the middle of closing down
// This prevents recursive closure scenarios that can lead to heap-use-after-free
if (!mIsClosingDown) {
qApp->processEvents();
}
// Snapshot the pending profile-save futures on the main thread - see
// pendingXmlSaveFutures(): the background task must not read `writers`/`saveFutures`
// itself, as the main thread mutates them whenever a save starts or finishes.
const QList<QFuture<bool>> xmlSaveFutures = pendingXmlSaveFutures();
auto watcher = new QFutureWatcher<void>;
mModuleFuture = QtConcurrent::run([this, xmlSaveFutures, moduleJobs]() {
// wait for the host xml to be ready before writing the modules out
for (auto future : xmlSaveFutures) {
future.waitForFinished();
}
writeModuleFiles(moduleJobs);
});
connect(watcher, &QFutureWatcher<void>::finished, this, [this, watcher, moduleJobs, syncModules]() {
// Finish on the main thread: the module documents are now on disk. Consume
// mModulesToSync via reloadModules() *before* the xmlSaved() loop below empties
// `writers` and emits profileSaveFinished(): that signal fires synchronously,
// and a deferred handler (e.g. a queued package install) could start another
// save that clears/replaces mModulesToSync, making this save skip the module
// sync it owes to other profiles.
if (syncModules) {
reloadModules();
}
mWritingHostAndModules = false;
// Drop each module writer from `writers`; the last removal emits
// profileSaveFinished() once the profile writer is gone too.
for (const auto& job : moduleJobs) {
xmlSaved(job.xmlFilename);
}
watcher->deleteLater();
});
watcher->setFuture(mModuleFuture);
return {true, filename_xml, QString()};
}
// exports without the host settings for some reason
std::tuple<bool, QString, QString> Host::saveProfileAs(const QString& file)
{
emit profileSaveStarted();
qApp->processEvents();
if (currentlySavingProfile()) {
return {false, QString(), qsl("a save is already in progress")};
}
auto writer = std::make_shared<XMLexport>(this);
writers.insert(qsl("profile"), writer);
writer->exportProfile(file);
return {true, file, QString()};
}
void Host::xmlSaved(const QString& xmlName)
{
if (writers.contains(xmlName)) {
writers.remove(xmlName);
}
if (writers.empty()) {
emit profileSaveFinished();
}
}
bool Host::currentlySavingProfile()
{
return (mWritingHostAndModules || !writers.empty());
}
void Host::waitForProfileSave()
{
waitForAsyncXmlSave();
if (mModuleFuture.isRunning()) {
mModuleFuture.waitForFinished();
}
int iterations = 0;
while (currentlySavingProfile()) {
if (++iterations > 1000) {
qWarning().nospace() << "Host::waitForProfileSave() WARNING - save did not complete after 1000 event loop iterations. " << "State: mWritingHostAndModules=" << mWritingHostAndModules
<< ", writers pending=" << writers.size() << ". Continuing without waiting.";
break;
}
qApp->processEvents();
}
}
void Host::setMmpMapLocation(const QString& data)
{
auto document = QJsonDocument::fromJson(data.toUtf8());
if (!document.isObject()) {
return;
}
auto json = document.object();
if (json.isEmpty()) {
return;
}
auto urlValue = json.value(qsl("url"));
if (urlValue == QJsonValue::Undefined) {
return;
}
auto url = QUrl(urlValue.toString());
if (!url.isValid()) {
return;
}
mpMap->setMmpMapLocation(urlValue.toString());
}
QString Host::getMmpMapLocation() const
{
return mpMap->getMmpMapLocation();
}
// error and debug consoles inherit font of the main console
void Host::updateConsolesFont()
{
if (!mpConsole) {
qWarning().nospace().noquote() << "Host::updateConsolesFont() WARNING - no TMainConsole to deal with font related operations.";
return;
}
mpConsole->refreshView();
TEvent event{};
event.mArgumentList.append(qsl("sysSettingChanged"));
event.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
event.mArgumentList.append(qsl("main window font"));
event.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
event.mArgumentList.append(mpConsole->font().family());
event.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
event.mArgumentList.append(QString::number(mpConsole->font().pointSize()));
event.mArgumentTypeList.append(ARGUMENT_TYPE_NUMBER);
raiseEvent(event);
if (mpEditorDialog) {
mpEditorDialog->setDisplayFont(mpConsole->font());
}
if (mudlet::self()->smpDebugArea && mudlet::self()->smpDebugConsole) {
mudlet::self()->smpDebugConsole->setFont(mpConsole->font());
}
if (mpNotePad) {
mpNotePad->setFont(mpConsole->font());
}
}
// a little message to make the player feel special for helping us find bugs
void Host::thankForUsingPTB()
{
const QStringList happyIcons{"😀", "😃", "😄", "😁", "🙂", "🙃", "🤩", "🎉", "🚀", "🤟", "✌️", "👊"};
const auto randomIcon = happyIcons.at(QRandomGenerator::global()->bounded(happyIcons.size()));
postMessage(tr(R"([ OK ] - %1 Thanks a lot for using the Public Test Build!)", "%1 will be a random happy emoji").arg(randomIcon));
postMessage(tr(R"([ OK ] - %1 Help us make Mudlet better by reporting any problems.)", "%1 will be a random happy emoji").arg(randomIcon));
}
void Host::setMediaLocationGMCP(const QString& mediaUrl)
{
const QUrl url = QUrl(mediaUrl);
if (!url.isValid()) {
return;
}
mMediaLocationGMCP = mediaUrl;
}
QString Host::mediaLocationGMCP() const
{
return mMediaLocationGMCP;
}
void Host::setMediaLocationMSP(const QString& mediaUrl)
{
const QUrl url = QUrl(mediaUrl);
if (!url.isValid()) {
return;
}
mMediaLocationMSP = mediaUrl;
}
QString Host::mediaLocationMSP() const
{
return mMediaLocationMSP;
}
// Completely specifies the font for use in the main console and elsewhere:
std::pair<bool, QString> Host::setDisplayFont(const QFont& font)
{
const QFontMetrics metrics(font);
if (metrics.averageCharWidth() == 0) {
return {false, qsl("specified font is invalid (its letters have 0 width)")};
}
if (mpConsole) {
if (mpConsole->font() != font) {
mpConsole->setFont(font);
updateConsolesFont();
}
return {true, QString()};
}
qDebug().noquote().nospace() << "Host::setDisplayFont(...) INFO - No TMainConsole to set font for - faking it";
mTempDisplayFontAttributes = TFontAttributes(font);
mTempDisplayFont = mTempDisplayFontAttributes.value().makeFont();
return {true, QString()};
}
// Also completely specifies the font for use in the main console and elsewhere:
void Host::setDisplayFontFromString(const QString& fontData)
{
QFont font;
font.fromString(fontData);
setDisplayFont(font);
}
void Host::setDisplayFontSize(int size)
{
if (mpConsole) {
if (mpConsole->font().pointSize() != size) {
mpConsole->setFontSize(size);
updateConsolesFont();
}
return;
}
qDebug().noquote().nospace() << "Host::setDisplayFontSize(...) INFO - No TMainConsole to set font for - faking it";
if (!mTempDisplayFontAttributes.has_value()) {
mTempDisplayFontAttributes = TFontAttributes(!mNoAntiAlias);
}
mTempDisplayFontAttributes.value().mPointSize = size;
mTempDisplayFont = mTempDisplayFontAttributes.value().makeFont();
}
// Helper function to create a QFont with proper antialiasing settings
// This ensures fonts created from a name string inherit the host's antialiasing preference
QFont Host::createFontWithSettings(const QString& fontName, int pointSize) const
{
TFontAttributes attrs(fontsAntiAlias());
attrs.mName = fontName;
attrs.mPointSize = pointSize;
return attrs.makeFont();
}
// Helper function to parse font names that may include style information
// Returns a pair of (base family name, weight) for backward compatibility with static fonts
// Examples: "EB Garamond SemiBold" -> ("EB Garamond", QFont::DemiBold)
// "Arial Bold" -> ("Arial", QFont::Bold)
std::pair<QString, QFont::Weight> Host::parseFontNameAndStyle(const QString& fontName) const
{
// Map of style keywords to QFont::Weight values
static const QMap<QString, QFont::Weight> styleWeightMap = {{qsl("thin"), QFont::Thin},
{qsl("extralight"), QFont::ExtraLight},
{qsl("ultralight"), QFont::ExtraLight},
{qsl("light"), QFont::Light},
{qsl("normal"), QFont::Normal},
{qsl("regular"), QFont::Normal},
{qsl("medium"), QFont::Medium},
{qsl("demibold"), QFont::DemiBold},
{qsl("semibold"), QFont::DemiBold},
{qsl("bold"), QFont::Bold},
{qsl("extrabold"), QFont::ExtraBold},
{qsl("ultrabold"), QFont::ExtraBold},
{qsl("black"), QFont::Black},
{qsl("heavy"), QFont::Black}};
// Try each possible split point from the end of the font name
const QStringList words = fontName.split(' ', Qt::SkipEmptyParts);
if (words.size() < 2) {
return {fontName, QFont::Normal};
}
// Try matching the last word(s) as a style
for (int i = words.size() - 1; i > 0; --i) {
QString possibleStyle = words.mid(i).join(' ').toLower();
if (styleWeightMap.contains(possibleStyle)) {
QString baseName = words.mid(0, i).join(' ');
return {baseName, styleWeightMap.value(possibleStyle)};
}
}
return {fontName, QFont::Normal};
}
// Now returns the total weight of the path
unsigned int Host::assemblePath()
{
unsigned int totalWeight = 0;
QStringList pathList;
for (const int i : std::as_const(mpMap->mPathList)) {
const QString n = QString::number(i);
pathList.append(n);
}
QStringList directionList = mpMap->mDirList;
QStringList weightList;
for (const int stepWeight : std::as_const(mpMap->mWeightList)) {
totalWeight += stepWeight;
const QString n = QString::number(stepWeight);
weightList.append(n);
}
QString tableName = qsl("speedWalkPath");
mLuaInterpreter.set_lua_table(tableName, pathList);
tableName = qsl("speedWalkDir");
mLuaInterpreter.set_lua_table(tableName, directionList);
tableName = qsl("speedWalkWeight");
mLuaInterpreter.set_lua_table(tableName, weightList);
return totalWeight;
}
bool Host::checkForMappingScript()
{
// the mapper script reminder is only shown once
// because it is too difficult and error prone (->proper script sequence)
// to disable this message
const bool ret = (mLuaInterpreter.check_for_mappingscript() || mHaveMapperScript);
mHaveMapperScript = true;
return ret;
}
void Host::check_for_mappingscript()
{
if (!checkForMappingScript()) {
emit signal_showMapperScriptReminder();
}
}
bool Host::checkForCustomSpeedwalk()
{
const bool ret = mLuaInterpreter.check_for_custom_speedwalk();
return ret;
}
void Host::startSpeedWalk()
{
const int totalWeight = assemblePath();
Q_UNUSED(totalWeight)
const QString f = qsl("doSpeedWalk");
const QString n = QString();
mLuaInterpreter.call(f, n);
}
void Host::startSpeedWalk(int sourceRoom, int targetRoom)
{
const QString sourceName = qsl("speedWalkFrom");
mLuaInterpreter.set_lua_integer(sourceName, sourceRoom);
const QString targetName = qsl("speedWalkTo");
mLuaInterpreter.set_lua_integer(targetName, targetRoom);
const QString f = qsl("doSpeedWalk");
const QString n = QString();
mLuaInterpreter.call(f, n);
}
void Host::updateDisplayDimensions()
{
mTelnet.checkNAWS();
}
void Host::stopAllTriggers()
{
mEmergencyStop = true;
mTriggerUnit.stopAllTriggers();
mAliasUnit.stopAllTriggers();
mTimerUnit.stopAllTriggers();
mKeyUnit.stopAllTriggers();
}
void Host::reenableAllTriggers()
{
mEmergencyStop = false;
mTriggerUnit.reenableAllTriggers();
mAliasUnit.reenableAllTriggers();
mTimerUnit.reenableAllTriggers();
mKeyUnit.reenableAllTriggers();
}
QPair<QString, QString> Host::getSearchEngine()
{
if (mSearchEngineData.contains(mSearchEngineName)) {
return qMakePair(mSearchEngineName, mSearchEngineData.value(mSearchEngineName));
}
return qMakePair(qsl("Google"), mSearchEngineData.value(qsl("Google")));
}
// cmd is UTF-16BE encoded here, but will be transcoded to Server's one by
// cTelnet::sendData(...) call:
void Host::send(QString cmd, bool wantPrint, bool dontExpandAliases)
{
// Record that the player (or a script acting for them) has interacted this connection; a later
// unsolicited GMCP Char.Login.URL may then auto-open the browser (see GMCPAuthenticator).
mUserSentInputThisConnection = true;
// Determine if we should print the command based on the echo mode
bool shouldPrint = false;
switch (mCommandEchoMode) {
case CommandEchoMode::Never:
shouldPrint = false;
break;
case CommandEchoMode::Always:
shouldPrint = true;
break;
case CommandEchoMode::ScriptControl:
shouldPrint = wantPrint;
break;
}
if (shouldPrint && !mIsRemoteEchoingActive) {
if (!cmd.isEmpty() || !mUSE_IRE_DRIVER_BUGFIX || mUSE_FORCE_LF_AFTER_PROMPT) {
// used to print the terminal <LF> that terminates a telnet command
// this is important to get the cursor position right
mpConsole->printCommand(cmd);
}
//If 3D Mapper is active mpConsole->update(); seems to be superfluous and even cause problems in MacOS
#if defined(INCLUDE_3DMAPPER)
if (!mpMap->mpMapper || !mpMap->mpMapper->glWidget) {
#else
if (!mpMap->mpMapper) {
#endif
mpConsole->update();
}
}
QStringList commandList;
if (!mCommandSeparator.isEmpty()) {
commandList = cmd.split(QString(mCommandSeparator), Qt::SkipEmptyParts);
} else if (!cmd.isEmpty()) {
// don't split command if the command separator is blank
commandList << cmd;
}
// allow sending blank commands
if (commandList.empty()) {
QString payload(QChar::LineFeed);
mTelnet.sendData(payload, true, true);
return;
}
for (QString& command : commandList) {
if (command.isEmpty()) {
continue;
}
command.remove(QChar::LineFeed);
if (dontExpandAliases) {
mTelnet.sendData(command, true, true);
continue;
}
if (!mAliasUnit.processDataStream(command)) {
mTelnet.sendData(command, true, true);
}
}
}
QPair<int, QString> Host::createStopWatch(const QString& name)
{
if (mResetProfile || mBlockStopWatchCreation) {
// Don't create stopwatches when test loading scripts or during a profile reset:
return qMakePair(0, qsl("unable to create a stopwatch at this time"));
}
if (!mStopWatchMap.empty() && !name.isEmpty()) {
for (auto it = mStopWatchMap.cbegin(); it != mStopWatchMap.cend(); ++it) {
if (it->second->name() == name) {
return qMakePair(0, qsl("stopwatch with id %1 called '%2' already exists").arg(QString::number(it->first), name));
}
}
}
int newWatchId = 1;
while (mStopWatchMap.count(newWatchId) > 0) {
++newWatchId;
}
auto pStopWatch = std::make_unique<stopWatch>();
if (!name.isEmpty()) {
pStopWatch->setName(name);
}
mStopWatchMap[newWatchId] = std::move(pStopWatch);
return qMakePair(newWatchId, QString());
}
bool Host::destroyStopWatch(const int id)
{
auto it = mStopWatchMap.find(id);
if (it == mStopWatchMap.end()) {
return false;
}
mStopWatchMap.erase(it);
return true;
}
bool Host::adjustStopWatch(const int id, const qint64 milliSeconds)
{
auto pStopWatch = getStopWatch(id);
if (pStopWatch) {
pStopWatch->adjustMilliSeconds(milliSeconds);
return true;
}
return false;
}
// Find the first (lowest ID) stopwatch with the given name and return its id,
// returns 0 (not a valid id) if the name is not found. It WILL return the first
// unnamed one if a blank string is given:
int Host::findStopWatchId(const QString& name) const
{
// Scan through existing names, in ascending id order (std::map is sorted)
for (auto it = mStopWatchMap.cbegin(); it != mStopWatchMap.cend(); ++it) {
if (it->second->name() == name) {
return it->first;
}
}
return 0;
}
QPair<bool, double> Host::getStopWatchTime(const int id) const
{
auto pStopWatch = getStopWatch(id);
if (pStopWatch) {
return qMakePair(true, pStopWatch->getElapsedMilliSeconds() / 1000.0);
}
return qMakePair(false, 0.0);
}
QPair<bool, QString> Host::getBrokenDownStopWatchTime(const int id) const
{
auto pStopWatch = getStopWatch(id);
if (pStopWatch) {
return qMakePair(true, pStopWatch->getElapsedDayTimeString());
}
return qMakePair(false, qsl("stopwatch with id %1 not found").arg(id));
}
QPair<bool, QString> Host::startStopWatch(const QString& name)
{
auto watchId = findStopWatchId(name);
if (!watchId) {
if (name.isEmpty()) {
return qMakePair(false, QLatin1String("no unnamed stopwatches found"));
}
return qMakePair(false, qsl("stopwatch with name '%1' not found").arg(name));
}
auto pStopWatch = getStopWatch(watchId);
if (Q_LIKELY(pStopWatch)) {
if (pStopWatch->start()) {
return qMakePair(true, QString());
}
return qMakePair(false, qsl("stopwatch with name '%1' (id:%2) was already running").arg(name, QString::number(watchId)));
}
// This should, indeed, be:
Q_UNREACHABLE();
return qMakePair(false, qsl("stopwatch with name '%1' (id:%2) not found").arg(name, QString::number(watchId)));
}
QPair<bool, QString> Host::startStopWatch(int id)
{
auto pStopWatch = getStopWatch(id);
if (pStopWatch) {
if (pStopWatch->start()) {
return qMakePair(true, QString());
}
return qMakePair(false, qsl("stopwatch with id %1 was already running").arg(id));
}
return qMakePair(false, qsl("stopwatch with id %1 not found").arg(id));
}
QPair<bool, QString> Host::stopStopWatch(const QString& name)
{
auto watchId = findStopWatchId(name);
if (!watchId) {
if (name.isEmpty()) {
return qMakePair(false, QLatin1String("no unnamed stopwatches found"));
}
return qMakePair(false, qsl("stopwatch with name '%1' not found").arg(name));
}
auto pStopWatch = getStopWatch(watchId);
if (Q_LIKELY(pStopWatch)) {
if (pStopWatch->stop()) {
return qMakePair(true, QString());
}
return qMakePair(false, qsl("stopwatch with name '%1' (id:%2) was already stopped").arg(name, QString::number(watchId)));
}
// This should, indeed, be:
Q_UNREACHABLE();
return qMakePair(false, qsl("stopwatch with name '%1' (id:%2) not found").arg(name, QString::number(watchId)));
}
QPair<bool, QString> Host::stopStopWatch(const int id)
{
auto pStopWatch = getStopWatch(id);
if (pStopWatch) {
if (pStopWatch->stop()) {
return qMakePair(true, QString());
}
return qMakePair(false, qsl("stopwatch with id %1 was already stopped").arg(id));
}
return qMakePair(false, qsl("stopwatch with id %1 not found").arg(id));
}
QPair<bool, QString> Host::resetStopWatch(const QString& name)
{
auto watchId = findStopWatchId(name);
if (!watchId) {
if (name.isEmpty()) {
return qMakePair(false, QLatin1String("no unnamed stopwatches found"));
}
return qMakePair(false, qsl("stopwatch with name '%1' not found").arg(name));
}
auto pStopWatch = getStopWatch(watchId);
if (Q_LIKELY(pStopWatch)) {
if (pStopWatch->reset()) {
return qMakePair(true, QString());
}
if (name.isEmpty()) {
return qMakePair(false, qsl("the first unnamed stopwatch (id:%1) was already reset").arg(watchId));
}
return qMakePair(false, qsl("stopwatch with name '%1' (id:%2) was already reset").arg(name, QString::number(watchId)));
}
// This should, indeed, be:
Q_UNREACHABLE();
return qMakePair(false, qsl("stopwatch with name '%1' (id:%2) not found").arg(name, QString::number(watchId)));
}
QPair<bool, QString> Host::resetStopWatch(const int id)
{
auto pStopWatch = getStopWatch(id);
if (pStopWatch) {
if (pStopWatch->reset()) {
return qMakePair(true, QString());
}
return qMakePair(false, qsl("stopwatch with id %1 was already reset").arg(id));
}
return qMakePair(false, qsl("stopwatch with id %1 not found").arg(id));
}
// Used when emulating past behavior for startStopWatch - there is only one
// which takes a numeric argument as that is what old scripts will be using and
// not a text name:
QPair<bool, QString> Host::resetAndRestartStopWatch(const int id)
{
auto pStopWatch = getStopWatch(id);
if (pStopWatch) {
pStopWatch->stop();
pStopWatch->reset();
pStopWatch->start();
return qMakePair(true, QString());
}
return qMakePair(false, qsl("stopwatch with id %1 not found").arg(id));
}
bool Host::makeStopWatchPersistent(const int id, const bool state)
{
auto pStopWatch = getStopWatch(id);
if (pStopWatch) {
pStopWatch->setPersistent(state);
return true;
}
return false;
}
QPair<bool, QString> Host::setStopWatchName(const int id, const QString& newName)
{
stopWatch* pStopWatch = nullptr;
if (!newName.isEmpty()) {
// Scan through existing names
for (auto it = mStopWatchMap.cbegin(); it != mStopWatchMap.cend(); ++it) {
if (it->second->name() == newName) {
if (it->first != id) {
return qMakePair(false, qsl("the name '%1' is already in use for another stopwatch (id:%2)").arg(newName, QString::number(it->first)));
}
// Trivial case - the stopwatch is already called by the NEW name:
return qMakePair(true, QString());
}
}
}
pStopWatch = getStopWatch(id);
if (pStopWatch) {
// Okay found the one we want:
pStopWatch->setName(newName);
return qMakePair(true, QString());
}
return qMakePair(false, qsl("stopwatch with id %1 not found").arg(id));
}
QPair<bool, QString> Host::setStopWatchName(const QString& currentName, const QString& newName)
{
stopWatch* pStopWatch = nullptr;
int id = 0;
// Although it would be quicker code to return immediately if we detect the
// newName is in use the run-time error about it being used will mask a
// failure to find the current one (perhaps because it has already been set
// to the new value) - so don't moan about that until we have (or have not)
// found the current name:
bool isAlreadyUsed = false;
int alreadyUsedId = 0;
// we are looking BOTH for the current name and checking that any other
// ones WITH names do not match the new name (std::map is sorted by key):
for (auto it = mStopWatchMap.cbegin(); it != mStopWatchMap.cend(); ++it) {
const int currentId = it->first;
stopWatch* pCurrentStopWatch = it->second.get();
// This will also pick up the FIRST (lowest id) currently unnamed
// stopwatch:
if (!id && pCurrentStopWatch->name() == currentName) {
pStopWatch = pCurrentStopWatch;
id = currentId;
}
// As zero is never used as an Id it okay to use it here before it
// is set to the value of the wanted entry:
if (!newName.isEmpty() && currentId != id && pCurrentStopWatch->name() == newName) {
isAlreadyUsed = true;
alreadyUsedId = currentId;
}
}
// if pStopWatch is nullptr then the currentName was not found:
if (!pStopWatch) {
if (currentName.isEmpty()) {
return qMakePair(false, QLatin1String("no unnamed stopwatches found"));
}
return qMakePair(false, qsl("stopwatch with name '%1' not found").arg(currentName));
}
if (isAlreadyUsed) {
return qMakePair(false, qsl("the name '%1' is already in use for another stopwatch (id:%2)").arg(newName, QString::number(alreadyUsedId)));
}
if (currentName == newName) {
// Trivial case that always succeeds
return qMakePair(true, QString());
}
pStopWatch->setName(newName);
return qMakePair(true, QString());
}
QList<int> Host::getStopWatchIds() const
{
QList<int> ids;
ids.reserve(static_cast<int>(mStopWatchMap.size()));
for (auto it = mStopWatchMap.cbegin(); it != mStopWatchMap.cend(); ++it) {
ids.append(it->first);
}
return ids;
}
void Host::incomingStreamProcessor(const QString& data, int line)
{
mTriggerUnit.processDataStream(data, line);
mAliasUnit.doCleanup();
mTimerUnit.doCleanup();
mTriggerUnit.doCleanup();
mKeyUnit.doCleanup();
mActionUnit.doCleanup();
// ScriptUnit defers deletes too (a package script uninstalling its own package
// mid-compile or mid-event-dispatch), so flush it here alongside the others:
mScriptUnit.doCleanup();
}
// When Mudlet is running in online mode, deleted temp* objects are cleaned up in bulk
// on every new line. When in offline mode, new lines don't come - so they are
// cleaned up in bulk periodically.
void Host::slot_purgeTemps()
{
mAliasUnit.doCleanup();
mTimerUnit.doCleanup();
mTriggerUnit.doCleanup();
mKeyUnit.doCleanup();
mActionUnit.doCleanup();
mScriptUnit.doCleanup();
}
void Host::registerEventHandler(const QString& name, TScript* pScript)
{
if (mEventHandlerMap.contains(name)) {
if (!mEventHandlerMap[name].contains(pScript)) {
mEventHandlerMap[name].append(pScript);
}
} else {
QList<TScript*> scriptList;
scriptList.append(pScript);
mEventHandlerMap.insert(name, scriptList);
}
}
void Host::registerAnonymousEventHandler(const QString& name, const QString& fun)
{
if (mAnonymousEventHandlerFunctions.contains(name)) {
if (!mAnonymousEventHandlerFunctions[name].contains(fun)) {
mAnonymousEventHandlerFunctions[name].push_back(fun);
}
} else {
QStringList newList;
newList << fun;
mAnonymousEventHandlerFunctions[name] = newList;
}
}
void Host::unregisterEventHandler(const QString& name, TScript* pScript)
{
if (mEventHandlerMap.contains(name)) {
mEventHandlerMap[name].removeAll(pScript);
}
}
// Handlers run on this profile's shared lua_State, but each unwinds it back to
// the level it found, so a C function raising an event mid-flight keeps its own
// arguments and any return values it has already pushed
void Host::raiseEvent(const TEvent& pE)
{
if (Q_UNLIKELY(mEmergencyStop)) {
return;
}
if (pE.mArgumentList.isEmpty()) {
return;
}
static const QString star = qsl("*");
// A handler can uninstall its own package mid-dispatch (a common package
// auto-updater pattern): whilst this frame is on the stack
// ScriptUnit::uninstall() defers its deletes so neither the executing
// handler nor the other TScript pointers in the lists copied below get
// freed under us; the deferred deletes are flushed once the outermost
// dispatch finishes:
mScriptUnit.beginProcessing();
const auto processingGuard = qScopeGuard([this] {
mScriptUnit.endProcessing();
mScriptUnit.doCleanup();
});
if (mEventHandlerMap.contains(pE.mArgumentList.at(0))) {
QList<TScript*> scriptList = mEventHandlerMap.value(pE.mArgumentList.at(0));
for (auto& script : scriptList) {
script->callEventHandler(pE);
}
}
if (mEventHandlerMap.contains(star)) {
QList<TScript*> scriptList = mEventHandlerMap.value(star);
for (auto& script : scriptList) {
script->callEventHandler(pE);
}
}
if (mAnonymousEventHandlerFunctions.contains(pE.mArgumentList.at(0))) {
const QStringList functionsList = mAnonymousEventHandlerFunctions.value(pE.mArgumentList.at(0));
for (const QString& function : functionsList) {
mLuaInterpreter.callEventHandler(function, pE);
}
}
if (mAnonymousEventHandlerFunctions.contains(star)) {
const QStringList functionsList = mAnonymousEventHandlerFunctions.value(star);
for (const QString& function : functionsList) {
mLuaInterpreter.callEventHandler(function, pE);
}
}
// Let any test-mode waitForEvent() call blocked on this event capture its
// arguments and unblock. Cheap (an empty-list check) when nothing is waiting.
mLuaInterpreter.captureEventForWaits(pE);
// After the event has been raised but before 'event' goes out of scope,
// we need to safely dereference the members of 'event' that point to
// values in the Lua registry
mLuaInterpreter.freeAllInLuaRegistry(pE);
}
void Host::postIrcMessage(const QString& a, const QString& b, const QString& c)
{
TEvent event{};
event.mArgumentList << QLatin1String("sysIrcMessage");
event.mArgumentList << a << b << c;
event.mArgumentTypeList << ARGUMENT_TYPE_STRING << ARGUMENT_TYPE_STRING << ARGUMENT_TYPE_STRING << ARGUMENT_TYPE_STRING;
raiseEvent(event);
}
void Host::enableTimer(const QString& name)
{
mTimerUnit.enableTimer(name);
}
void Host::disableTimer(const QString& name)
{
mTimerUnit.disableTimer(name);
}
bool Host::killTimer(const QString& name)
{
return mTimerUnit.killTimer(name);
}
void Host::enableKey(const QString& name)
{
mKeyUnit.enableKey(name);
}
void Host::disableKey(const QString& name)
{
mKeyUnit.disableKey(name);
}
void Host::enableTrigger(const QString& name)
{
mTriggerUnit.enableTrigger(name);
}
void Host::disableTrigger(const QString& name)
{
mTriggerUnit.disableTrigger(name);
}
bool Host::killTrigger(const QString& name)
{
return mTriggerUnit.killTrigger(name);
}
std::pair<bool, QString> Host::installPackage(const QString& fileName, enums::PackageModuleType thing, bool quiet)
{
// Wait for profile save to complete before installing package
// to prevent Lua state corruption during concurrent operations
if (currentlySavingProfile()) {
// Auto-retry installation after save completes
QObject* obj = new QObject(this);
connect(this, &Host::profileSaveFinished, obj, [=, this]() {
// The synchronous caller has already been told {true, ""} below;
// surface any deferred failure via the warning log and the
// profile's message area so it isn't lost silently.
auto [ok, msg] = installPackage(fileName, thing, quiet);
if (!ok) {
qWarning() << "Host::installPackage() deferred install of" << fileName << "failed:" << msg;
postMessage(tr("[ ERROR ] - Package install failed for \"%1\": %2").arg(fileName, msg));
}
obj->deleteLater();
});
return {true, QString()};
}
bool showedUnpackingDialog = false;
QString actualFileName = fileName;
std::unique_ptr<QTemporaryFile> tempFile;
if ((fileName.startsWith(QStringLiteral(":/")) || fileName.startsWith(QStringLiteral("qrc:/")))
&& (fileName.endsWith(qsl(".zip"), Qt::CaseInsensitive) || fileName.endsWith(qsl(".mpackage"), Qt::CaseInsensitive))) {
tempFile = std::make_unique<QTemporaryFile>();
if (!tempFile->open()) {
qWarning() << "Host::installPackage() failed to create temporary file for resource:" << fileName;
return {false, qsl("Failed to create temporary file for resource package")};
}
QFile resourceFile(fileName);
if (!resourceFile.open(QIODevice::ReadOnly)) {
qWarning() << "Host::installPackage() failed to open resource file:" << fileName << "Error:" << resourceFile.errorString();
return {false, qsl("Failed to open resource package file")};
}
if (tempFile->write(resourceFile.readAll()) == -1) {
qWarning() << "Host::installPackage() failed to write resource data to temp file. Error:" << tempFile->errorString();
return {false, qsl("Failed to write resource package data to temporary file")};
}
tempFile->close();
actualFileName = tempFile->fileName();
}
// Module notes:
// For the module install, a module flag of 0 is a package,
// a flag of 1 means the module is being installed for the first time via the UI,
// a flag of 2 means the module is being synced (so it's "installed" already),
// a flag of 3 means the module is being installed from a script.
// This separation is necessary to be able to reuse code while avoiding infinite loops from script installations.
if (actualFileName.isEmpty()) {
return {false, qsl("no package file was actually given")};
}
QFile file(actualFileName);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
return {false, qsl("could not open file '%1").arg(actualFileName)};
}
QString packageName = sanitizePackageName(fileName);
if (thing != enums::PackageModuleType::Package) {
if ((thing == enums::PackageModuleType::ModuleSync) && (mActiveModules.contains(packageName))) {
uninstallPackage(packageName, enums::PackageModuleType::ModuleSync);
} else if ((thing == enums::PackageModuleType::ModuleFromUI) && !mIsProfileLoadingSequence && (mInstalledModules.contains(packageName) || mActiveModules.contains(packageName))) {
// Check if this is just a stale reference by verifying if module files actually exist
// Skip this check during profile loading - modules are expected to be in mInstalledModules already
QString modulePath = mudlet::getMudletPath(enums::profilePackagePath, getName(), packageName);
QString moduleFile = mInstalledModules.value(packageName).value(0); // Get the actual file path from stored reference
bool moduleExists = QDir(modulePath).exists() || QFile::exists(moduleFile);
if (!moduleExists) {
// Module files don't exist, clean up stale references
mInstalledModules.remove(packageName);
mActiveModules.removeAll(packageName);
mModulesLoadedOk.remove(packageName);
} else {
// Module actually exists, show duplicate error
return {false, tr("Module \"%1\" is already installed. Please uninstall it first or choose a different name.").arg(packageName)};
}
} else if ((thing == enums::PackageModuleType::ModuleFromScript) && (mActiveModules.contains(packageName))) {
return {false, qsl("module %1 is already installed").arg(packageName)}; //we're already installed
}
} else {
if (mInstalledPackages.contains(packageName)) {
return {false, qsl("package %1 is already installed").arg(packageName)};
}
}
//the extra module check is needed here to prevent infinite loops from script loaded modules
if (mpEditorDialog && thing != enums::PackageModuleType::ModuleFromScript) {
mpEditorDialog->doCleanReset();
}
QFile file2;
if (fileName.endsWith(qsl(".zip"), Qt::CaseInsensitive) || fileName.endsWith(qsl(".mpackage"), Qt::CaseInsensitive)) {
const QString _home = mudlet::getMudletPath(enums::profileHomePath, getName());
const QString _dest = mudlet::getMudletPath(enums::profilePackagePath, getName(), packageName);
// home directory for the PROFILE
const QDir _tmpDir(_home);
// directory to store the expanded archive file contents
const bool mkpathSuccessful = _tmpDir.mkpath(_dest);
if (!mkpathSuccessful) {
return {false, qsl("could not create destination folder")};
}
// Skip the unpacking dialog for modules created from UI, and for
// script-initiated installs (passed via quiet) to avoid stealing
// window-manager focus from the user's other applications - see
// issue #9170.
if (thing != enums::PackageModuleType::ModuleFromUI && !quiet) {
const QString message =
(thing != enums::PackageModuleType::Package) ? tr("Unpacking module:\n\"%1\"\nplease wait...").arg(packageName) : tr("Unpacking package:\n\"%1\"\nplease wait...").arg(packageName);
emit signal_showUnpackingProgress(message, tr("Unpacking"));
showedUnpackingDialog = true;
}
auto unzipSuccessful = mudlet::unzip(actualFileName, _dest, _tmpDir);
if (showedUnpackingDialog) {
emit signal_hideUnpackingProgress();
}
if (!unzipSuccessful) {
return {false, qsl("could not unzip package")};
}
// requirements for zip packages:
// - packages must be compressed in zip format
// - file extension should be .mpackage (though .zip is accepted)
// - there can only be a single xml file per package
// - the xml file must be located in the root directory of the zip package. example: myPack.zip contains: the folder images and the file myPack.xml
QDir _dir(_dest);
// before we start importing xmls in, see if the config.lua manifest file exists
// - if it does, update the packageName from it
if (_dir.exists(qsl("config.lua"))) {
// read in the new packageName from Lua. Should be expanded in future to whatever else config.lua will have
readPackageConfig(_dir.absoluteFilePath(qsl("config.lua")), packageName, thing != enums::PackageModuleType::Package);
// now that the packageName changed, redo relevant checks to make sure it's still valid
if (thing != enums::PackageModuleType::Package) {
if (mActiveModules.contains(packageName)) {
uninstallPackage(packageName, enums::PackageModuleType::ModuleSync);
}
} else {
if (mInstalledPackages.contains(packageName)) {
// cleanup and quit if already installed
removeDir(_dir.absolutePath(), _dir.absolutePath());
return {false, qsl("package %1 is already installed").arg(packageName)};
}
}
// continuing, so update the folder name on disk
const QString newpath(qsl("%1/%2").arg(_home, packageName));
_dir.rename(_dir.absolutePath(), newpath);
_dir = QDir(newpath);
}
QStringList _filterList;
_filterList << qsl("*.xml") << qsl("*.trigger");
const QFileInfoList entries = _dir.entryInfoList(_filterList, QDir::Files);
for (auto& entry : entries) {
file2.setFileName(entry.absoluteFilePath());
if (!file2.open(QFile::ReadOnly | QFile::Text)) {
qWarning() << "Host: failed to open file for reading:" << entry.absoluteFilePath() << file2.errorString();
continue;
}
XMLimport reader(this);
if (thing != enums::PackageModuleType::Package) {
QStringList moduleEntry;
moduleEntry << fileName;
moduleEntry << qsl("0");
mInstalledModules[packageName] = moduleEntry;
mActiveModules.append(packageName);
} else {
mInstalledPackages.append(packageName);
}
auto [success, errorMsg] = reader.importPackage(&file2, packageName, static_cast<int>(thing));
if (thing != enums::PackageModuleType::Package) {
if (success) {
mModulesLoadedOk.insert(packageName);
} else {
qWarning() << "Host::installPackage() WARNING - failed to load module" << packageName << ":" << errorMsg;
postMessage(tr("[ WARN ] - Failed to load module \"%1\": %2").arg(packageName, errorMsg));
}
}
file2.close();
}
} else {
file2.setFileName(fileName);
if (!file2.open(QFile::ReadOnly | QFile::Text)) {
qWarning() << "Host: failed to open file for reading:" << fileName << file2.errorString();
return {false, qsl("could not open package file")};
}
XMLimport reader(this);
if (thing != enums::PackageModuleType::Package) {
QStringList moduleEntry;
moduleEntry << fileName;
moduleEntry << qsl("0");
mInstalledModules[packageName] = moduleEntry;
mActiveModules.append(packageName);
} else {
mInstalledPackages.append(packageName);
}
auto [success, errorMsg] = reader.importPackage(&file2, packageName, static_cast<int>(thing));
if (thing != enums::PackageModuleType::Package) {
if (success) {
mModulesLoadedOk.insert(packageName);
} else {
qWarning() << "Host::installPackage() WARNING - failed to load module" << packageName << ":" << errorMsg;
postMessage(tr("[ WARN ] - Failed to load module \"%1\": %2").arg(packageName, errorMsg));
}
}
file2.close();
}
if (mpEditorDialog) {
mpEditorDialog->doCleanReset();
}
if (thing == enums::PackageModuleType::Package) {
saveProfile();
}
// reorder permanent and temporary triggers: perm first, temp second
mTriggerUnit.reorderTriggersAfterPackageImport();
// make any fonts in the package available to Mudlet for use
if (thing != enums::PackageModuleType::ModuleSync) {
installPackageFonts(packageName);
}
// Defer raising install events until the next event loop iteration
// This ensures all package installation is complete (including variable loading)
// before event handlers execute, preventing Lua state corruption
QTimer::singleShot(0ms, this, [this, thing, packageName, fileName]() {
// Don't raise events if Host is shutting down to avoid handlers executing during teardown
if (isClosingDown()) {
return;
}
// Don't raise install events for modules that failed to load
if (thing != enums::PackageModuleType::Package && !mModulesLoadedOk.contains(packageName)) {
return;
}
// raise 2 events - a generic one and a more detailed one to serve both
// a simple need ("I just want the install event") and a more specific need
// ("I specifically need to know when the module was synced")
TEvent genericInstallEvent{};
genericInstallEvent.mArgumentList.append(QLatin1String("sysInstall"));
genericInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
genericInstallEvent.mArgumentList.append(packageName);
genericInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
raiseEvent(genericInstallEvent);
TEvent detailedInstallEvent{};
switch (thing) {
case enums::PackageModuleType::Package:
detailedInstallEvent.mArgumentList.append(QLatin1String("sysInstallPackage"));
break;
case enums::PackageModuleType::ModuleFromUI:
detailedInstallEvent.mArgumentList.append(QLatin1String("sysInstallModule"));
break;
case enums::PackageModuleType::ModuleSync:
detailedInstallEvent.mArgumentList.append(QLatin1String("sysSyncInstallModule"));
break;
case enums::PackageModuleType::ModuleFromScript:
detailedInstallEvent.mArgumentList.append(QLatin1String("sysLuaInstallModule"));
break;
}
detailedInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
detailedInstallEvent.mArgumentList.append(packageName);
detailedInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
detailedInstallEvent.mArgumentList.append(fileName);
detailedInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
raiseEvent(detailedInstallEvent);
});
if (mpPackageManager) {
mpPackageManager->resetPackageList();
}
if (mpModuleManager) {
mpModuleManager->layoutModules();
}
// Save profile to ensure modules persist and appear in module manager
if (thing != enums::PackageModuleType::Package) {
// Use a timer to save profile after module installation completes
QTimer::singleShot(100ms, this, [this]() {
saveProfile();
});
}
return {true, QString()};
}
QString Host::sanitizePackageName(const QString packageName) const
{
auto tempName = packageName.section(qsl("/"), -1);
tempName.remove(qsl(".trigger"), Qt::CaseInsensitive);
tempName.remove(qsl(".xml"), Qt::CaseInsensitive);
tempName.remove(qsl(".zip"), Qt::CaseInsensitive);
tempName.remove(qsl(".mpackage"), Qt::CaseInsensitive);
tempName.remove(QLatin1Char('\\'));
return tempName;
}
// credit: http://john.nachtimwald.com/2010/06/08/qt-remove-directory-and-its-contents/
bool Host::removeDir(const QString& dirName, const QString& originalPath)
{
bool result = true;
const QDir dir(dirName);
if (dir.exists(dirName)) {
for (QFileInfo const& info : dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) {
// prevent recursion outside of the original branch
if (info.isDir() && info.absoluteFilePath().startsWith(originalPath)) {
result = removeDir(info.absoluteFilePath(), originalPath);
} else {
result = QFile::remove(info.absoluteFilePath());
}
if (!result) {
return result;
}
}
result = dir.rmdir(dirName);
}
return result;
}
void Host::removePackageInfo(const QString& packageName, const bool isModule)
{
if (isModule) {
mModuleInfo.remove(packageName);
} else {
mPackageInfo.remove(packageName);
}
}
// This may be called by installPackage(...) in that case however it will have
// module == 2 and in THAT situation it will NOT RE-invoke installPackage(...)
// again - Slysven
bool Host::uninstallPackage(const QString& packageName, enums::PackageModuleType thing)
{
// As with the installPackage, the module codes are:
// 0=package, 1=uninstall from dialog, 2=uninstall due to module syncing,
// 3=uninstall from a script
// block packages/modules from being uninstalled while a profile save is in progress
// just so the save mechanism doesn't get surprised with something getting removed from memory under its feet
if (currentlySavingProfile()) {
return false;
}
bool isModule = thing != enums::PackageModuleType::Package;
if (isModule) {
if (!mInstalledModules.contains(packageName)) {
return false;
}
} else {
if (!mInstalledPackages.contains(packageName)) {
return false;
}
}
//PackageModuleType::ModuleSync seems to be only used for reloading/syncing
//No need to remove package info as it can cause the info to be lost
if (thing != enums::PackageModuleType::ModuleSync) {
removePackageInfo(packageName, isModule);
}
// raise 2 events - a generic one and a more detailed one to serve both
// a simple need ("I just want the uninstall event") and a more specific need
// ("I specifically need to know when the module was uninstalled via Lua")
TEvent genericUninstallEvent{};
genericUninstallEvent.mArgumentList.append(QLatin1String("sysUninstall"));
genericUninstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
genericUninstallEvent.mArgumentList.append(packageName);
genericUninstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
raiseEvent(genericUninstallEvent);
TEvent detailedUninstallEvent{};
switch (thing) {
case enums::PackageModuleType::Package:
detailedUninstallEvent.mArgumentList.append(QLatin1String("sysUninstallPackage"));
break;
case enums::PackageModuleType::ModuleFromUI:
detailedUninstallEvent.mArgumentList.append(QLatin1String("sysUninstallModule"));
break;
case enums::PackageModuleType::ModuleSync:
detailedUninstallEvent.mArgumentList.append(QLatin1String("sysSyncUninstallModule"));
break;
case enums::PackageModuleType::ModuleFromScript:
detailedUninstallEvent.mArgumentList.append(QLatin1String("sysLuaUninstallModule"));
break;
}
detailedUninstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
detailedUninstallEvent.mArgumentList.append(packageName);
detailedUninstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
raiseEvent(detailedUninstallEvent);
int dualInstallations = 0;
if (mInstalledModules.contains(packageName) && mInstalledPackages.contains(packageName)) {
dualInstallations = 1;
}
//we check for ModuleFromScript because if we reset the editor, we will re-execute the
//module uninstall, thus creating an infinite loop.
if (mpEditorDialog && thing != enums::PackageModuleType::ModuleFromScript) {
mpEditorDialog->doCleanReset();
}
mTriggerUnit.uninstall(packageName);
mTimerUnit.uninstall(packageName);
mAliasUnit.uninstall(packageName);
mActionUnit.uninstall(packageName);
mScriptUnit.uninstall(packageName);
mKeyUnit.uninstall(packageName);
mudlet::self()->mFontManager.unloadFonts(getName(), packageName);
if (isModule) {
//if ModuleSync, this is a temporary uninstall for reloading so we exit here
QStringList entry = mInstalledModules[packageName];
mInstalledModules.remove(packageName);
mModulesLoadedOk.remove(packageName);
mActiveModules.removeAll(packageName);
if (thing == enums::PackageModuleType::ModuleSync) {
return true;
}
//if ModuleFromUI/ModuleFromScript, we actually uninstall it.
//reinstall the package if it shared a module name. This is a kludge, but it's cleaner than adding extra arguments/etc imo
if (dualInstallations) {
//we're a dual install, reinstalling package
mInstalledPackages.removeAll(packageName); //so we don't get denied from installPackage
//get the pre package list so we don't get duplicates
installPackage(entry[0], enums::PackageModuleType::Package);
}
} else {
mInstalledPackages.removeAll(packageName);
if (dualInstallations) {
QStringList entry = mInstalledModules[packageName];
installPackage(entry[0], enums::PackageModuleType::ModuleFromUI);
//restore the module edit flag
mInstalledModules[packageName] = entry;
}
}
if (mpEditorDialog && thing != enums::PackageModuleType::ModuleFromScript) {
mpEditorDialog->doCleanReset();
}
getActionUnit()->updateAllToolbars();
const QString dest = mudlet::getMudletPath(enums::profilePackagePath, getName(), packageName);
removeDir(dest, dest);
// ensure only one timer is running in case multiple modules are uninstalled at once
if (!mSaveTimer.has_value() || !mSaveTimer.value()) {
mSaveTimer = true;
// save the profile on the next Qt main loop cycle in order for the asyncronous save mechanism
// not to try to write to disk a package/module that just got uninstalled and removed from memory
QTimer::singleShot(0ms, this, [this]() {
mSaveTimer = false;
// If a package's own script uninstalled it mid-compile (from a script
// reached outside the compileAll()/editor/raiseEvent flush points, e.g. a
// permScript() run from an alias or key), the script deletes were deferred
// and are still registered. Flush them now, at depth 0, before saving so
// the save below does not serialize the just-uninstalled scripts back in:
mScriptUnit.doCleanup();
if (auto [ok, filename, error] = saveProfile(); !ok) {
qDebug() << qsl("Host::uninstallPackage: Couldn't save '%1' to '%2' because: %3").arg(getName(), filename, error);
}
});
}
//NOW we reset if we're uninstalling a module
if (mpEditorDialog && thing == enums::PackageModuleType::ModuleFromScript) {
mpEditorDialog->doCleanReset();
}
if (mpPackageManager) {
mpPackageManager->resetPackageList();
}
return true;
}
void Host::readPackageConfig(const QString& luaConfig, QString& packageName, bool isModule)
{
const QString newName = getPackageConfig(luaConfig, isModule);
if (!newName.isEmpty()) {
packageName = sanitizePackageName(newName);
}
}
void Host::setupSandboxedLuaState(lua_State* L)
{
// Load only safe libraries
luaopen_base(L);
luaopen_table(L);
luaopen_string(L);
luaopen_math(L);
// Remove dangerous functions
const char* dangerousFunctions[] = {// File operations
"dofile",
"loadfile",
"load",
"loadstring",
// Module system
"require",
"module",
// Library access
"io",
"os",
"debug",
// Environment manipulation
"getfenv",
"setfenv",
// Raw memory access
"collectgarbage"};
for (const auto& function : dangerousFunctions) {
lua_pushnil(L);
lua_setglobal(L, function);
}
// Remove package system entirely to prevent require() restoration
lua_pushnil(L);
lua_setglobal(L, "package");
// Remove potentially dangerous base functions
lua_getglobal(L, "_G");
if (lua_istable(L, -1)) {
const char* removedBaseFunctions[] = {"rawget", "rawset", "rawequal", "rawlen"};
for (const auto& function : removedBaseFunctions) {
lua_pushnil(L);
lua_setfield(L, -2, function);
}
}
lua_pop(L, 1); // pop _G
}
QString Host::getPackageConfig(const QString& luaConfig, bool isModule)
{
QString packageName;
// We don't use luaL_loadfile here because that breaks on Windows as it won't work if there are accented characters in the path or file name -
// QFile which can work with whatever local8Bit encoding is used for file names - the luaL_loadfile(...) uses std::iostream which doesn't...
QFile configFile(luaConfig);
QStringList strings;
if (configFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&configFile);
while (!in.atEnd()) {
strings += in.readLine();
}
configFile.close();
}
lua_State* L = luaL_newstate();
setupSandboxedLuaState(L);
int error = luaL_loadstring(L, strings.join("\n").toUtf8().constData());
if (!error) {
error = lua_pcall(L, 0, 0, 0);
}
if (!error) {
lua_getglobal(L, "mpackage");
if (lua_isstring(L, -1)) {
packageName = QString(lua_tostring(L, -1));
}
lua_pop(L, -1);
if (!packageName.isEmpty()) {
//get rid of lua version
lua_getglobal(L, "_G");
lua_pushnil(L);
lua_setfield(L, -2, "_VERSION");
QMap<QString, QString> packageInfo;
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -1) && lua_isstring(L, -2)) {
packageInfo[lua_tostring(L, -2)] = lua_tostring(L, -1);
}
lua_pop(L, 1);
}
if (isModule) {
mModuleInfo[packageName] = packageInfo;
} else {
mPackageInfo[packageName] = packageInfo;
}
}
lua_close(L);
return packageName;
}
// error
std::string e = lua_tostring(L, -1);
if (e.empty()) {
e = "no error message available from Lua";
}
std::string reason;
switch (error) {
case 4:
reason = "Out of memory";
break;
case 3:
reason = "Syntax error";
break;
case 2:
reason = "Runtime error";
break;
case 1:
reason = "Yield error";
break;
default:
reason = "Unknown error";
break;
}
if (mudlet::smDebugMode) {
TDebug(QColor(Qt::white), QColor(Qt::red)) << "LUA: " << reason.c_str() << " in " << luaConfig << " ERROR:" << e.c_str() << "\n" >> 0;
}
lua_pop(L, -1);
lua_close(L);
return QString();
}
// writeProfileIniData(...) and readProfileIniData(...) might eventually
// replace writeProfileData(...) and readProfileData(...) but for now are just
// used to store some information about one or more TCommandLine's mHistoryData:
bool Host::writeProfileIniData(const QString& item, const QString& what)
{
QSettings settings(mudlet::getMudletPath(enums::profileDataItemPath, getName(), qsl("profile.ini")), QSettings::IniFormat);
settings.setValue(item, what);
settings.sync();
switch (settings.status()) {
case QSettings::NoError:
return true;
case QSettings::FormatError:
qWarning().nospace().noquote() << "Host::writeProfileIniData(\"" << item << "\", \"" << what << "\") ERROR - failed to save this detail, reason: \"Format error\".";
return false;
case QSettings::AccessError:
qWarning().nospace().noquote() << "Host::writeProfileIniData(\"" << item << "\", \"" << what << "\") ERROR - failed to save this detail, reason: \"Access error\".";
return false;
}
Q_UNREACHABLE();
}
QString Host::readProfileIniData(const QString& item)
{
QSettings settings(mudlet::getMudletPath(enums::profileDataItemPath, getName(), qsl("profile.ini")), QSettings::IniFormat);
return settings.value(item).toString();
}
// This function retrieves command line history settings based on the given
// command line type and name. It reads the saveCommands setting from the
// profile.ini file, which is intended to replace all other single data item
// files in the profile's home directory. For the main command line, a
// predefined file name is used, while for any other command line, the function
// first looks for a mapping from a number-suffixed file to a command line
// name. If the mapping is not found, a new file name and default setting for
// saveCommands is created, stored, and returned.
// Because '/' and '\\' are used by the QSettings class in key names for
// special purposes we MUST filter them out in the name, we'll replace them
// with '_'s:
std::tuple<QString, bool> Host::getCmdLineSettings(const TCommandLine::CommandLineType type, const QString& name)
{
if (type == TCommandLine::MainCommandLine) {
// This one does not need the name to be kept in a QSettings but we
// still need to retrieve the other setting:
auto saveCommands = static_cast<bool>(readProfileIniData(qsl("CommandLines/SaveHistory/main")).compare(qsl("false"), Qt::CaseInsensitive));
return {qsl("command_history_main"), saveCommands};
}
QString localName{name};
localName.replace(QRegularExpression(qsl("[\\/]")), qsl("_"));
// We use a '/' in the name as that denotes a grouping (section) within the
// QSetting's (INI) format with the left-most side of the first '/' as a
// section header. They actually get converted to `\\` (a single backslash)
// inside the actual file but are accessed correctly only if given as a
// forward slash in the code:
auto fileName = readProfileIniData(qsl("CommandLines/NameMapping/%1").arg(localName));
// We want this to default to true if the setting doesn't exist, so we will
// compare it to the opposite (which will be zero for a match) and convert
// it to a boolean - so that a missing value will give a non-zero value
// which becomes a true:
auto saveCommands = static_cast<bool>(readProfileIniData(qsl("CommandLines/SaveHistory/%1").arg(localName)).compare(qsl("false"), Qt::CaseInsensitive));
if (!fileName.isEmpty()) {
// Ah, we've used this name before, so return the details:
return {fileName, saveCommands};
}
// Else the name is not in the settings so we will have to create one:
// Get the highest number used so far:
bool isOk = false;
auto usedIndex = readProfileIniData(qsl("CommandLines/UsedIndexes")).toInt(&isOk);
if (!isOk || !usedIndex) {
// The value was not found / is null - so force it to be the right one
// to start with, remembering that it will be incremented before use:
usedIndex = 0;
}
// Increment it and save the new value
writeProfileIniData(qsl("CommandLines/UsedIndexes"), QString::number(++usedIndex));
// Generate the name
fileName = qsl("command_history_%1").arg(usedIndex, 2, 10, QLatin1Char('0'));
// Save it:
writeProfileIniData(qsl("CommandLines/NameMapping/%1").arg(localName), fileName);
// And a default setting:
writeProfileIniData(qsl("CommandLines/SaveHistory/%1").arg(localName), saveCommands ? qsl("true") : qsl("false"));
// And return it - with the defaulted other setting:
return {fileName, saveCommands};
}
void Host::setCmdLineSettings(const TCommandLine::CommandLineType type, const bool saveCommands, const QString& name)
{
if (type == TCommandLine::MainCommandLine) {
writeProfileIniData(qsl("CommandLines/SaveHistory/main"), saveCommands ? qsl("true") : qsl("false"));
return;
}
QString localName{name};
localName.replace(QRegularExpression(qsl("[\\/]")), qsl("_"));
// We use a '/' in the name as that donotes a grouping (section) within the
// QSetting's (INI) format with the left-most side of the first '/' as a
// section header. They actually get converted to `\\` (a single backslash)
// inside the actual file but are accessed correctly only if given as a
// forward slash in the code:
writeProfileIniData(qsl("CommandLines/SaveHistory/%1").arg(localName), saveCommands ? qsl("true") : qsl("false"));
}
// Derived from the one in dlgConnectionProfile class - but it does not need a
// host name argument...
QPair<bool, QString> Host::writeProfileData(const QString& item, const QString& what)
{
QSaveFile file(mudlet::getMudletPath(enums::profileDataItemPath, getName(), item));
if (file.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) {
QDataStream ofs(&file);
if (mudlet::scmRunTimeQtVersion >= QVersionNumber(5, 13, 0)) {
ofs.setVersion(mudlet::scmQDataStreamFormat_5_12);
}
ofs << what;
if (!file.commit()) {
qDebug() << "Host::writeProfileData: writing host data: " << file.errorString();
}
}
if (file.error() == QFile::NoError) {
return qMakePair(true, QString());
}
return qMakePair(false, file.errorString());
}
// Similar to the above, a convenience for reading profile data for this host.
QString Host::readProfileData(const QString& item)
{
QFile file(mudlet::getMudletPath(enums::profileDataItemPath, getName(), item));
const bool success = file.open(QIODevice::ReadOnly);
QString ret;
if (success) {
QDataStream ifs(&file);
if (mudlet::scmRunTimeQtVersion >= QVersionNumber(5, 13, 0)) {
ifs.setVersion(mudlet::scmQDataStreamFormat_5_12);
}
ifs >> ret;
file.close();
}
return ret;
}
// makes fonts in a given package/module be available for Mudlet scripting
// does not install font system-wide
void Host::installPackageFonts(const QString& packageName)
{
auto packagePath = mudlet::getMudletPath(enums::profilePackagePath, getName(), packageName);
QDirIterator it(packagePath, QDirIterator::Subdirectories);
while (it.hasNext()) {
auto filePath = it.next();
if (filePath.endsWith(QLatin1String(".otf"), Qt::CaseInsensitive) || filePath.endsWith(QLatin1String(".ttf"), Qt::CaseInsensitive)
|| filePath.endsWith(QLatin1String(".ttc"), Qt::CaseInsensitive) || filePath.endsWith(QLatin1String(".otc"), Qt::CaseInsensitive)) {
mudlet::self()->mFontManager.loadFont(filePath, getName(), packageName);
}
}
}
// ensures fonts from all installed packages are loaded in Mudlet
void Host::refreshPackageFonts()
{
for (const auto& package : std::as_const(mInstalledPackages)) {
installPackageFonts(package);
}
}
void Host::setEnableBlinkText(const bool enable)
{
if (mEnableBlinkText != enable) {
mEnableBlinkText = enable;
emit signal_changeEnableBlinkText(enable);
}
}
void Host::setWideAmbiguousEAsianGlyphs(const Qt::CheckState state)
{
bool localState = false;
bool needToEmit = false;
const QByteArray encoding(mTelnet.getEncoding());
if (state == Qt::PartiallyChecked) {
// Set things automatically
mAutoAmbigousWidthGlyphsSetting = true;
if (encoding == "GBK" || encoding == "GB18030" || encoding == "BIG5" || encoding == "BIG5-HKSCS" || encoding == "EUC-KR") {
// Need to use wide width for ambiguous characters
if (!mWideAmbigousWidthGlyphs) {
// But the last setting was narrow - so we need to change
mWideAmbigousWidthGlyphs = true;
localState = true;
needToEmit = true;
}
} else {
// Need to use narrow width for ambiguous characters
if (mWideAmbigousWidthGlyphs) {
// But the last setting was wide - so we need to change
mWideAmbigousWidthGlyphs = false;
localState = false;
needToEmit = true;
}
}
} else {
// Set things manually:
mAutoAmbigousWidthGlyphsSetting = false;
if (mWideAmbigousWidthGlyphs != (state == Qt::Checked)) {
// The last setting is the opposite to what we want:
mWideAmbigousWidthGlyphs = (state == Qt::Checked);
localState = (state == Qt::Checked);
needToEmit = true;
}
}
if (needToEmit) {
emit signal_changeIsAmbigousWidthGlyphsToBeWide(localState);
}
}
QColor Host::getAnsiColor(const int ansiCode, const bool isBackground) const
{
// clang-format off
switch (ansiCode) {
case 0: return mBlack;
case 1: return mRed;
case 2: return mGreen;
case 3: return mYellow;
case 4: return mBlue;
case 5: return mMagenta;
case 6: return mCyan;
case 7: return mWhite;
case 8: return mLightBlack;
case 9: return mLightRed;
case 10: return mLightGreen;
case 11: return mLightYellow;
case 12: return mLightBlue;
case 13: return mLightMagenta;
case 14: return mLightCyan;
case 15: return mLightWhite;
default:
if (ansiCode == TTrigger::scmIgnored) {
// No-op - corresponds to no setting or ignoring this aspect
return QColor();
} else if (ansiCode == TTrigger::scmDefault) {
return isBackground ? mBgColor : mFgColor;
} else if (ansiCode >= 16 && ansiCode <= 231) {
// because color 1-15 behave like normal ANSI colors we need to subtract 16
// 6x6 RGB color space
const int r = (ansiCode - 16) / 36;
const int g = (ansiCode - 16 - (r * 36)) / 6;
const int b = (ansiCode - 16 - (r * 36)) - (g * 6);
// Values are scaled according to the standard Xterm color palette
// http://jonasjacek.github.io/colors/
return QColor(r == 0 ? 0 : (r - 1) * 40 + 95,
g == 0 ? 0 : (g - 1) * 40 + 95,
b == 0 ? 0 : (b - 1) * 40 + 95);
} else if (ansiCode < 256) {
const int k = (ansiCode - 232) * 10 + 8;
return QColor(k, k, k);
} else {
return QColor(); // No-op
}
}
// clang-format on
}
// handles out of band (OOB) GMCP/MSDP data for Discord - called whenever GMCP
// Telnet sub-option comes in and starts with "External.Discord.(Status|Info)"
void Host::processDiscordGMCP(const QString& packageMessage, const QString& data)
{
auto document = QJsonDocument::fromJson(data.toUtf8());
if (!document.isObject()) {
return;
}
auto json = document.object();
if (json.isEmpty()) {
return;
}
if (packageMessage == QLatin1String("External.Discord.Status")) {
processGMCPDiscordStatus(json);
} else if (packageMessage == QLatin1String("External.Discord.Info")) {
processGMCPDiscordInfo(json);
}
}
void Host::processGMCPDiscordInfo(const QJsonObject& discordInfo)
{
if (mDiscordMode != DiscordShowGameDetails) {
return;
}
mudlet* pMudlet = mudlet::self();
bool hasInvite = false;
auto inviteUrl = discordInfo.value(qsl("inviteurl"));
// Will be of form: "https://discord.gg/#####"
if (inviteUrl != QJsonValue::Undefined && !inviteUrl.toString().isEmpty() && inviteUrl.toString() != qsl("0")) {
setDiscordInviteURL(inviteUrl.toString());
pMudlet->updateDiscordNamedIcon();
hasInvite = true;
}
bool hasApplicationId = false;
bool hasCustomAppID = false;
auto appID = discordInfo.value(qsl("applicationid"));
if (appID != QJsonValue::Undefined) {
hasApplicationId = true;
if (appID.toString() == Discord::mMudletApplicationId) {
pMudlet->mDiscord.setApplicationID(this, QString());
} else {
hasCustomAppID = true;
pMudlet->mDiscord.setApplicationID(this, appID.toString());
auto image = pMudlet->mDiscord.getLargeImage(this);
if (image.isEmpty() || image == QLatin1String("mudlet")) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetLargeIcon);
pMudlet->mDiscord.setLargeImage(this, qsl("server-icon"));
}
}
}
if (hasInvite) {
if (hasCustomAppID) {
qDebug() << "Game using a custom Discord server. Invite URL:" << inviteUrl.toString();
} else if (hasApplicationId) {
qDebug() << "Game using Mudlet's Discord server. Invite URL:" << inviteUrl.toString();
} else {
qDebug() << "Discord invite URL: " << inviteUrl.toString();
}
} else {
if (hasCustomAppID) {
qDebug() << "Game is using custom server Discord application ID";
} else if (hasApplicationId) {
qDebug() << "Game is using Mudlet's Discord application ID";
}
}
}
void Host::processGMCPDiscordStatus(const QJsonObject& discordInfo)
{
if (mDiscordMode != DiscordShowGameDetails) {
return;
}
auto pMudlet = mudlet::self();
auto gameName = discordInfo.value(qsl("game"));
if (gameName != QJsonValue::Undefined) {
setDiscordGameName(gameName.toString());
pMudlet->updateDiscordNamedIcon();
QPair<bool, QString> const richPresenceSupported = pMudlet->mDiscord.gameIntegrationSupported(getUrl());
if (richPresenceSupported.first && pMudlet->mDiscord.usingMudletsDiscordID(this)) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetDetail);
pMudlet->mDiscord.setDetailText(this, tr("Playing %1").arg(richPresenceSupported.second));
pMudlet->mDiscord.setServerOrigin(this, DiscordSetLargeIcon);
pMudlet->mDiscord.setLargeImage(this, richPresenceSupported.second);
pMudlet->mDiscord.setServerOrigin(this, DiscordSetLargeIconText);
//: %1 is the game name and %2:%3 is game server address like: mudlet.org:23
pMudlet->mDiscord.setLargeImageText(this, tr("%1 at %2:%3").arg(gameName.toString(), getUrl(), QString::number(getPort())));
} else {
// We are using a custom application id, so the top line is
// likely to be saying "Playing MudName"
if (richPresenceSupported.first) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetDetail);
pMudlet->mDiscord.setDetailText(this, QString());
pMudlet->mDiscord.setServerOrigin(this, DiscordSetLargeIconText);
//: %1 is the game name and %2:%3 is game server address like: mudlet.org:23
pMudlet->mDiscord.setLargeImageText(this, tr("%1 at %2:%3").arg(gameName.toString(), getUrl(), QString::number(getPort())));
pMudlet->mDiscord.setServerOrigin(this, DiscordSetLargeIcon);
pMudlet->mDiscord.setLargeImage(this, qsl("server-icon"));
}
}
}
auto details = discordInfo.value(qsl("details"));
if (details != QJsonValue::Undefined) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetDetail);
pMudlet->mDiscord.setDetailText(this, details.toString());
}
auto state = discordInfo.value(qsl("state"));
if (state != QJsonValue::Undefined) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetState);
pMudlet->mDiscord.setStateText(this, state.toString());
}
auto largeImages = discordInfo.value(qsl("largeimage"));
if (largeImages != QJsonValue::Undefined) {
auto largeImage = largeImages.toArray().first();
if (largeImage != QJsonValue::Undefined) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetLargeIcon);
pMudlet->mDiscord.setLargeImage(this, largeImage.toString());
}
}
auto largeImageText = discordInfo.value(qsl("largeimagetext"));
if (largeImageText != QJsonValue::Undefined) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetLargeIconText);
pMudlet->mDiscord.setLargeImageText(this, largeImageText.toString());
}
auto smallImages = discordInfo.value(qsl("smallimage"));
if (smallImages != QJsonValue::Undefined) {
auto smallImage = smallImages.toArray().first();
if (smallImage != QJsonValue::Undefined) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetSmallIcon);
pMudlet->mDiscord.setSmallImage(this, smallImage.toString());
}
}
auto smallImageText = discordInfo.value(qsl("smallimagetext"));
if ((smallImageText != QJsonValue::Undefined)) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetSmallIconText);
pMudlet->mDiscord.setSmallImageText(this, smallImageText.toString());
}
int64_t timeStamp = -1;
auto endTimeStamp = discordInfo.value(qsl("endtime"));
if (endTimeStamp.isDouble()) {
timeStamp = static_cast<int64_t>(endTimeStamp.toDouble());
pMudlet->mDiscord.setServerOrigin(this, DiscordSetTimeInfo);
pMudlet->mDiscord.setEndTimeStamp(this, timeStamp);
} else if (endTimeStamp.isString()) {
timeStamp = endTimeStamp.toString().toLongLong();
pMudlet->mDiscord.setServerOrigin(this, DiscordSetTimeInfo);
pMudlet->mDiscord.setEndTimeStamp(this, timeStamp);
} else {
auto startTimeStamp = discordInfo.value(qsl("starttime"));
if (startTimeStamp.isDouble()) {
timeStamp = static_cast<int64_t>(startTimeStamp.toDouble());
pMudlet->mDiscord.setServerOrigin(this, DiscordSetTimeInfo);
pMudlet->mDiscord.setStartTimeStamp(this, timeStamp);
} else if (startTimeStamp.isString()) {
timeStamp = startTimeStamp.toString().toLongLong();
pMudlet->mDiscord.setServerOrigin(this, DiscordSetTimeInfo);
pMudlet->mDiscord.setStartTimeStamp(this, timeStamp);
}
}
int partySizeValue = -1;
int partyMaxValue = -1;
auto partyMax = discordInfo.value(qsl("partymax"));
auto partySize = discordInfo.value(qsl("partysize"));
if (partyMax != QJsonValue::Undefined || partySize != QJsonValue::Undefined) {
pMudlet->mDiscord.setServerOrigin(this, DiscordSetPartyInfo);
}
if (partyMax.isDouble()) {
partyMaxValue = static_cast<int>(partyMax.toDouble());
if (partyMaxValue > 0 && partySize.isDouble()) {
partySizeValue = static_cast<int>(partySize.toDouble());
pMudlet->mDiscord.setParty(this, partySizeValue, partyMaxValue);
} else {
pMudlet->mDiscord.setParty(this, 0, 0);
}
} else {
if (partySize.isDouble()) {
partySizeValue = static_cast<int>(partySize.toDouble());
pMudlet->mDiscord.setParty(this, partySizeValue);
} else {
pMudlet->mDiscord.setParty(this, 0, 0);
}
}
}
void Host::clearDiscordData()
{
mudlet* pMudlet = mudlet::self();
pMudlet->mDiscord.setDetailText(this, QString());
pMudlet->mDiscord.setStateText(this, QString());
pMudlet->mDiscord.setLargeImage(this, QString());
pMudlet->mDiscord.setLargeImageText(this, QString());
pMudlet->mDiscord.setSmallImage(this, QString());
pMudlet->mDiscord.setSmallImageText(this, QString());
pMudlet->mDiscord.setStartTimeStamp(this, 0);
pMudlet->mDiscord.setParty(this, 0, 0);
}
void Host::setDiscordMode(DiscordMode mode)
{
const DiscordMode oldMode = mDiscordMode;
mDiscordMode = mode;
writeProfileData(qsl("discordmode"), QString::number(static_cast<int>(mode)));
auto pMudlet = mudlet::self();
if (mode == DiscordShowGameDetails && oldMode != DiscordShowGameDetails) {
// Switching to full integration - advertise Discord support to server.
// Per spec: Hello triggers Info (app ID, invite URL),
// Get triggers Status (rich presence fields).
if (mTelnet.isGMCPEnabled()) {
mTelnet.sendGMCPSupportsAdd(qsl("External.Discord 1"));
mTelnet.sendDiscordHello();
mTelnet.sendDiscordGet();
}
} else if (mode != DiscordShowGameDetails && oldMode == DiscordShowGameDetails) {
// Switching away from full integration - retract Discord support
// and reset all Discord data including server-origin flags and custom app ID
if (mTelnet.isGMCPEnabled()) {
mTelnet.sendGMCPSupportsRemove(qsl("External.Discord 1"));
}
pMudlet->mDiscord.resetData(this);
return;
}
pMudlet->mDiscord.UpdatePresence();
}
void Host::processDiscordMSDP(const QString& variable, QString value)
{
if (mDiscordMode != DiscordShowGameDetails) {
return;
}
Q_UNUSED(variable)
Q_UNUSED(value)
// TODO:
// if (!(variable == QLatin1String("SERVER_ID") || variable == QLatin1String("AREA_NAME"))) {
// return;
// }
// // MSDP value comes padded with quotes - strip them (from the local copy of
// // the supplied argument):
// if (value.startsWith(QLatin1String("\""))) {
// value = value.mid(1);
// }
// if (value.endsWith(QLatin1String("\""))) {
// value.chop(1);
// }
// if (variable == QLatin1String("SERVER_ID")) {
// mudlet::self()->mDiscord.setGame(this, value);
// } else if (variable == QLatin1String("AREA_NAME")) {
// mudlet::self()->mDiscord.setArea(this, value);
// }
}
void Host::setDiscordApplicationID(const QString& s)
{
mDiscordApplicationID = s;
writeProfileData(qsl("discordApplicationId"), s);
}
const QString& Host::getDiscordApplicationID()
{
return mDiscordApplicationID;
}
void Host::setDiscordInviteURL(const QString& s)
{
mDiscordInviteURL = s;
writeProfileData(qsl("discordInviteURL"), s);
}
// Returns false if the profile is restricted to a specific Discord username
// and the currently logged-in Discord user doesn't match.
bool Host::discordUserIdMatch(const QString& userName) const
{
if (!userName.isEmpty() && !mRequiredDiscordUserName.isEmpty() && userName.toLower() != mRequiredDiscordUserName.toLower()) {
return false;
}
return true;
}
void Host::initMMCPServer()
{
if (mMMCPServer) {
return;
}
mMMCPServer = new MMCPServer(this);
}
// Return the MMCP chat name for this host
const QString& Host::getMMCPChatName() const
{
return mMMCPChatName;
}
// Validate and set the MMCP chat name, notify connected peers, and update GUI.
// Returns false if the name is invalid (contains ~ or ,).
bool Host::setMMCPChatName(const QString& name)
{
static const QRegularExpression rx(qsl("~|,"));
if (rx.match(name).hasMatch()) {
return false;
}
// If the name is unchanged, no need to update or emit signals
if (mMMCPChatName == name) {
return true;
}
mMMCPChatName = name;
if (mMMCPServer) {
mMMCPServer->chatName(name);
}
emit mmcpChatNameChanged(name);
return true;
}
quint16 Host::getMMCPPort()
{
return mMMCPChatPort;
}
const QString& Host::getMMCPChatPrefix() const
{
return mMMCPChatPrefix;
}
bool Host::getMMCPAutoStartServer()
{
return mMMCPAutostartServer;
}
bool Host::getMMCPAllowPeekRequests()
{
return mMMCPAllowPeekRequests;
}
bool Host::getMMCPPrefixEmotes()
{
return mMMCPPrefixEmotes;
}
bool Host::getMMCPAddChatMessageNewline()
{
return mMMCPAddChatMessageNewline;
}
bool Host::getMMCPAutoAcceptCalls()
{
return mMMCPAutoAcceptCalls;
}
bool Host::getMMCPShowSnoopInMainConsole()
{
return mMMCPShowSnoopInMainConsole;
}
QString Host::getSpellDic()
{
if (!mSpellDic.isEmpty()) {
return mSpellDic;
}
#if defined(Q_OS_OPENBSD)
// OpenBSD does not ship a USA dictionary so we will have to use
// a different starting one to try and locate system ones
return (qsl("en-GB"));
#else
return (qsl("en_US"));
#endif
}
void Host::setSpellDic(const QString& newDict)
{
bool isChanged = false;
if (!newDict.isEmpty() && mSpellDic != newDict) {
mSpellDic = newDict;
isChanged = true;
}
if (isChanged && mpConsole) {
mpConsole->setSystemSpellDictionary(newDict);
}
}
// When called from dlgProfilePreferences the second flag will only be changed
// if necessary:
// DISABLED: - Prevent "None" option for user dictionary - modified to prevent original useDictionary argument from being false:
void Host::setUserDictionaryOptions(const bool _useDictionary, const bool useShared)
{
Q_UNUSED(_useDictionary)
const bool useDictionary = true;
bool dictionaryChanged{};
// Copy the value while we have the lock:
const bool isSpellCheckingEnabled = mEnableSpellCheck;
if (mEnableUserDictionary != useDictionary) {
mEnableUserDictionary = useDictionary;
dictionaryChanged = true;
}
if (mUseSharedDictionary != useShared) {
mUseSharedDictionary = useShared;
dictionaryChanged = true;
}
if (!mpConsole) {
return;
}
if (dictionaryChanged) {
// This will propagate the changes in the two flags to the main
// TConsole's copies of them - although setProfileSpellDictionary() is
// also called in the main TConsole constructor:
mpConsole->setProfileSpellDictionary();
}
// This also needs to handle the spell checking against the system/mudlet
// bundled dictionary being switched on or off. Given that if it has
// been disabled the spell checking code won't run we need to clear any
// highlights in the TCommandLine instance that may have been present when
// spell checking is turned on or off:
if (isSpellCheckingEnabled) {
// Now enabled - so recheck the whole command line with whichever
// dictionaries are active:
mpConsole->mpCommandLine->recheckWholeLine();
} else {
// Or it is now disabled so clear any spelling marks:
mpConsole->mpCommandLine->clearMarksOnWholeLine();
}
}
// This does not take care of any QMaps or other containers that the mudlet
// and HostManager classes have that use the name of this profile as a key,
// however it should ensure that other classes get updated:
void Host::setName(const QString& name)
{
if (mHostName == name) {
return;
}
TDebug::changeHostName(this, name);
int currentPlayerRoom = 0;
if (mpMap) {
currentPlayerRoom = mpMap->mRoomIdHash.take(mHostName);
}
mHostName = name;
mTelnet.mProfileName = name;
if (mpMap) {
mpMap->mProfileName = name;
if (currentPlayerRoom) {
mpMap->mRoomIdHash.insert(name, currentPlayerRoom);
}
}
if (mpConsole) {
// If skipped they will be taken care of in the TMainConsole constructor:
mpConsole->setProperty("HostName", name);
mpConsole->setProfileName(name);
}
mTimerUnit.changeHostName(name);
}
void Host::removeAllNonPersistentStopWatches()
{
auto it = mStopWatchMap.begin();
while (it != mStopWatchMap.end()) {
if (!it->second || it->second->persistent()) {
++it;
continue;
}
it = mStopWatchMap.erase(it);
}
}
void Host::updateProxySettings(QNetworkAccessManager* manager)
{
if (mUseProxy && !mProxyAddress.isEmpty() && mProxyPort != 0) {
auto& proxy = getConnectionProxy();
manager->setProxy(*proxy);
} else {
manager->setProxy(QNetworkProxy::DefaultProxy);
}
}
std::unique_ptr<QNetworkProxy>& Host::getConnectionProxy()
{
if (!mpConnectionProxy) {
mpConnectionProxy = std::make_unique<QNetworkProxy>(QNetworkProxy::Socks5Proxy);
}
auto& proxy = mpConnectionProxy;
proxy->setHostName(mProxyAddress);
proxy->setPort(mProxyPort);
if (!mProxyUsername.isEmpty()) {
proxy->setUser(mProxyUsername);
}
if (!mProxyPassword.isEmpty()) {
proxy->setPassword(mProxyPassword);
}
return mpConnectionProxy;
}
void Host::loadSecuredPassword()
{
// Use async API for QtKeychain integration with file fallback
auto* credManager = new CredentialManager(this);
credManager->retrievePassword(getName(), "character", [this, credManager](bool success, const QString& password, const QString& errorMessage) {
if (success && !password.isEmpty()) {
setPass(password);
QString passwordCopy = password; // Make a copy for secure clearing
SecureStringUtils::secureStringClear(passwordCopy);
} else if (!success && !errorMessage.isEmpty()) {
qDebug() << "Host::loadSecuredPassword() - Failed to retrieve password:" << errorMessage;
}
// Clean up the credential manager
credManager->deleteLater();
});
}
// Only needed for places outside of this class:
void Host::updateAnsi16ColorsInTable()
{
mLuaInterpreter.updateAnsi16ColorsInTable();
}
void Host::updateExtendedAnsiColorsInTable()
{
mLuaInterpreter.updateExtendedAnsiColorsInTable();
}
void Host::setPlayerRoomStyleDetails(const quint8 styleCode, const quint8 outerDiameter, const quint8 innerDiameter, const QColor& outerColor, const QColor& innerColor)
{
mPlayerRoomStyle = styleCode;
mPlayerRoomOuterDiameterPercentage = outerDiameter;
mPlayerRoomInnerDiameterPercentage = innerDiameter;
mPlayerRoomOuterColor = outerColor;
mPlayerRoomInnerColor = innerColor;
}
void Host::getPlayerRoomStyleDetails(quint8& styleCode, quint8& outerDiameter, quint8& innerDiameter, QColor& primaryColor, QColor& secondaryColor)
{
styleCode = mPlayerRoomStyle;
outerDiameter = mPlayerRoomOuterDiameterPercentage;
innerDiameter = mPlayerRoomInnerDiameterPercentage;
primaryColor = mPlayerRoomOuterColor;
secondaryColor = mPlayerRoomInnerColor;
}
// Used to set the searchOptions here and the one in the editor if present, for
// use by the XMLimporter class:
void Host::setSearchOptions(const dlgTriggerEditor::SearchOptions optionsState)
{
mSearchOptions = optionsState;
if (mpEditorDialog) {
mpEditorDialog->setSearchOptions(optionsState);
}
}
void Host::setBufferSearchOptions(const TConsole::SearchOptions optionsState)
{
mBufferSearchOptions = optionsState;
}
std::pair<bool, QString> Host::setMapperTitle(const QString& title)
{
if (!mpConsole || !mpConsole->mpDockableMapWidget) {
return {false, "no floating/dockable type map window found"};
}
if (title.isEmpty()) {
mpConsole->mpDockableMapWidget->setWindowTitle(tr("Map - %1").arg(mHostName));
} else {
mpConsole->mpDockableMapWidget->setWindowTitle(title);
}
return {true, QString()};
}
std::optional<QString> Host::getMapperTitle() const
{
if (!mpConsole || !mpConsole->mpDockableMapWidget) {
return {};
}
return {mpConsole->mpDockableMapWidget->windowTitle()};
}
std::pair<int, QString> Host::createMapView(int areaId)
{
if (!mpMap) {
return {0, qsl("no map present or loaded")};
}
auto* viewManager = mpMap->getViewManager();
if (!viewManager) {
return {0, qsl("no view manager available")};
}
return viewManager->createView(areaId);
}
std::pair<bool, QString> Host::closeMapView(int viewId)
{
if (!mpMap) {
return {false, qsl("no map present or loaded")};
}
auto* viewManager = mpMap->getViewManager();
if (!viewManager) {
return {false, qsl("no view manager available")};
}
return viewManager->closeView(viewId);
}
std::pair<int, QString> Host::closeAllMapViews()
{
if (!mpMap) {
return {0, qsl("no map present or loaded")};
}
auto* viewManager = mpMap->getViewManager();
if (!viewManager) {
return {0, qsl("no view manager available")};
}
return {viewManager->closeAllViews(), QString()};
}
QList<int> Host::getMapViewIds() const
{
if (!mpMap) {
qWarning() << "Host::getMapViewIds() - no map present or loaded";
return {};
}
auto* viewManager = mpMap->getViewManager();
if (!viewManager) {
qWarning() << "Host::getMapViewIds() - no view manager available";
return {};
}
return viewManager->getViewIds();
}
void Host::setDebugShowAllProblemCodepoints(const bool state)
{
if (mDebugShowAllProblemCodepoints != state) {
mDebugShowAllProblemCodepoints = state;
emit signal_changeDebugShowAllProblemCodepoints(state);
}
}
void Host::setCompactInputLine(const bool state)
{
if (mCompactInputLine != state) {
mCompactInputLine = state;
// When the profile is being loaded and the previously saved data is
// read from the XML file the main TConsole has not been instatiated
// yet - so must check for it existing first - and ensure the read
// setting is applied in the constructor for it:
if (mpConsole && mpConsole->mpButtonMainLayer) {
mpConsole->mpButtonMainLayer->setVisible(!state);
}
}
}
QPointer<TConsole> Host::findConsole(QString name)
{
if (!mpConsole) {
qWarning() << "Host::findConsole() ERROR: main console not initialized";
return nullptr;
}
if (name.isEmpty() or name == qsl("main")) {
// Reason for the deref-plus-ref in the next line: `QPointer`s do not
// follow inheritance. See https://bugreports.qt.io/browse/QTBUG-2258
return &*mpConsole;
}
return mpConsole->mSubConsoleMap.value(name);
}
QPair<bool, QStringList> Host::getLines(const QString& windowName, const int lineFrom, const int lineTo)
{
if (!mpConsole) {
QStringList failMessage;
failMessage << qsl("internal error: no main TConsole - please report").arg(windowName);
return qMakePair(false, failMessage);
}
if (windowName.isEmpty() || windowName == QLatin1String("main")) {
return qMakePair(true, mpConsole->getLines(lineFrom, lineTo));
}
auto pC = mpConsole->mSubConsoleMap.value(windowName);
if (!pC) {
QStringList failMessage;
failMessage << qsl("mini console, user window or buffer '%1' not found").arg(windowName);
return qMakePair(false, failMessage);
}
return qMakePair(true, pC->getLines(lineFrom, lineTo));
}
std::pair<bool, QString> Host::openWindow(const QString& name, bool loadLayout, bool autoDock, const QString& area)
{
if (!mpConsole) {
return {false, QString()};
}
if (name.isEmpty()) {
return {false, QLatin1String("an userwindow cannot have an empty string as its name")};
}
//Dont create Userwindow if there is a Label with the same name already. It breaks the UserWindow
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
return {false, qsl("label with the name '%1' already exists").arg(name)};
}
auto hostName(getName());
auto console = mpConsole->mSubConsoleMap.value(name);
auto dockwidget = mpConsole->mDockWidgetMap.value(name);
if (!console && !dockwidget) {
// The name is not used in either the QMaps of all user created TConsole
// or TDockWidget instances - so we can make a NEW one:
dockwidget = new TDockWidget(this, name);
dockwidget->setObjectName(qsl("dockWindow_%1_%2").arg(hostName, name));
dockwidget->setContentsMargins(0, 0, 0, 0);
dockwidget->setWindowTitle(name);
mpConsole->mDockWidgetMap.insert(name, dockwidget);
// It wasn't obvious but the parent passed to the TConsole constructor
// is sliced down to a QWidget and is NOT a TDockWidget pointer:
console = new TConsole(this, name, TConsole::UserWindow, dockwidget->widget());
console->setObjectName(qsl("dockWindowConsole_%1_%2").arg(hostName, name));
console->setContentsMargins(0, 0, 0, 0);
dockwidget->setTConsole(console);
console->layerCommandLine->hide();
console->setScrollBarVisible(false);
mpConsole->mSubConsoleMap.insert(name, console);
dockwidget->setStyleSheet(mProfileStyleSheet);
mudlet::self()->addDockWidget(Qt::RightDockWidgetArea, dockwidget);
console->setFontSize(10);
}
if (!console || !dockwidget) {
return {false, qsl("userwindow '%1' already exists").arg(name)};
}
// The name is used in BOTH the QMaps of all user created TConsole
// and TDockWidget instances - so we HAVE an existing user window,
// Lets confirm this:
Q_ASSERT_X(console->getType() == TConsole::UserWindow, "host::openWindow(...)", "An existing TConsole was expected to be marked as a User Window type but it isn't");
dockwidget->update();
if (loadLayout && !dockwidget->hasLayoutAlready) {
mudlet::self()->loadWindowLayout();
dockwidget->hasLayoutAlready = true;
}
dockwidget->show();
dockwidget->setAllowedAreas(autoDock ? Qt::AllDockWidgetAreas : Qt::NoDockWidgetArea);
if (area.isEmpty()) {
return {true, QString()};
}
if (area == QLatin1String("f") || area == QLatin1String("floating")) {
if (!dockwidget->isFloating()) {
dockwidget->setFloating(true);
}
return {true, QString()};
}
if (area == QLatin1String("r") || area == QLatin1String("right")) {
dockwidget->setFloating(false);
mudlet::self()->addDockWidget(Qt::RightDockWidgetArea, dockwidget);
return {true, QString()};
}
if (area == QLatin1String("l") || area == QLatin1String("left")) {
dockwidget->setFloating(false);
mudlet::self()->addDockWidget(Qt::LeftDockWidgetArea, dockwidget);
return {true, QString()};
}
if (area == QLatin1String("t") || area == QLatin1String("top")) {
dockwidget->setFloating(false);
mudlet::self()->addDockWidget(Qt::TopDockWidgetArea, dockwidget);
return {true, QString()};
}
if (area == QLatin1String("b") || area == QLatin1String("bottom")) {
dockwidget->setFloating(false);
mudlet::self()->addDockWidget(Qt::BottomDockWidgetArea, dockwidget);
return {true, QString()};
}
return {false, qsl(R"("docking option "%1" not available. available docking options are "t" top, "b" bottom, "r" right, "l" left and "f" floating")").arg(area)};
}
std::pair<bool, QString> Host::createMiniConsole(const QString& windowname, const QString& name, int x, int y, int width, int height)
{
if (!mpConsole) {
return {false, QString()};
}
auto pC = mpConsole->mSubConsoleMap.value(name);
auto pW = mpConsole->mDockWidgetMap.value(name);
if (!pC) {
pC = mpConsole->createMiniConsole(windowname, name, x, y, width, height);
if (pC) {
pC->setFontSize(12);
return {true, QString()};
}
} else if (pC) {
// CHECK: The absence of an explicit return statement in this block means that
// reusing an existing mini console causes the lua function to seem to
// fail - is this as per Wiki?
// This part was causing problems with UserWindows
if (!pW) {
pC->resize(width, height);
pC->move(x, y);
return {false, qsl("miniconsole '%1' already exists, moving/resizing '%1'").arg(name)};
}
}
return {false, qsl("miniconsole/userwindow '%1' already exists").arg(name)};
}
std::pair<bool, QString> Host::createScrollBox(const QString& windowname, const QString& name, int x, int y, int width, int height) const
{
if (!mpConsole) {
return {false, QString()};
}
auto pS = mpConsole->mScrollBoxMap.value(name);
if (!pS) {
pS = mpConsole->createScrollBox(windowname, name, x, y, width, height);
if (pS) {
return {true, QString()};
}
} else if (pS) {
pS->resize(width, height);
pS->move(x, y);
return {false, qsl("scrollBox '%1' already exists, moving/resizing '%1'").arg(name)};
}
return {false, qsl("scrollBox '%1' already exists").arg(name)};
}
std::pair<bool, QString> Host::createLabel(const QString& windowname, const QString& name, int x, int y, int width, int height, bool fillBg, bool clickthrough)
{
if (!mpConsole) {
return {false, QString()};
}
auto pL = mpConsole->mLabelMap.value(name);
auto pC = mpConsole->mSubConsoleMap.value(name);
if (!pL && !pC) {
pL = mpConsole->createLabel(windowname, name, x, y, width, height, fillBg, clickthrough);
if (pL) {
return {true, QString()};
}
} else if (pL) {
return {false, qsl("label '%1' already exists").arg(name)};
} else if (pC) {
return {false, qsl("a miniconsole/userwindow with the name '%1' already exists").arg(name)};
}
return {false, QString()};
}
bool Host::setClickthrough(const QString& name, bool clickthrough)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setClickThrough(clickthrough);
return true;
}
return false;
}
bool Host::setLabelStyleSheet(const QString& name, const QString& styleSheet)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setStyleSheet(styleSheet);
return true;
}
return false;
}
bool Host::setLinkStyle(const QString& name, const QString& linkColor, const QString& linkVisitedColor, bool underline)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setLinkStyle(linkColor, linkVisitedColor, underline);
return true;
}
return false;
}
bool Host::resetLinkStyle(const QString& name)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->resetLinkStyle();
return true;
}
return false;
}
bool Host::clearVisitedLinks(const QString& name)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->clearVisitedLinks();
return true;
}
return false;
}
void Host::hideMudletsVariables()
{
auto varUnit = getLuaInterface()->getVarUnit();
// remember which users' saved variables shouldn't be hidden
QVector<QString> unhideSavedVars;
for (const auto& variable : std::as_const(varUnit->savedVars)) {
if (!varUnit->isHidden(variable)) {
unhideSavedVars.append(variable);
}
}
// mark all currently loaded Lua variables as hidden
// this covers entirety of Mudlets API (good!) and but unfortunately
// user's saved variables as well
LuaInterface* lI = getLuaInterface();
lI->getVars(true);
// unhide user's saved variables
for (const auto& variable : std::as_const(unhideSavedVars)) {
varUnit->removeHidden(variable);
}
}
bool Host::createBuffer(const QString& name)
{
if (!mpConsole) {
return false;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
if (!pC) {
pC = mpConsole->createBuffer(name);
if (pC) {
return true;
}
}
return false;
}
// Doesn't work on the errors or central debug consoles:
bool Host::clearWindow(const QString& name)
{
if (!mpConsole) {
return false;
}
return mpConsole->clear(name);
}
bool Host::showWindow(const QString& name)
{
if (!mpConsole) {
return false;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
auto pL = mpConsole->mLabelMap.value(name);
auto pN = mpConsole->mSubCommandLineMap.value(name);
auto pS = mpConsole->mScrollBoxMap.value(name);
auto pT = mpConsole->mTextBoxMap.value(name);
// check labels first as they are shown/hidden more often
if (pL) {
pL->show();
return true;
}
if (pC) {
auto pD = mpConsole->mDockWidgetMap.value(name);
if (pD) {
pD->update();
pD->show();
// TODO: conside refactoring TConsole::showWindow(name) so that there is a TConsole::showWindow() that can be called directly on the TConsole concerned?
return mpConsole->showWindow(name);
}
return mpConsole->showWindow(name);
}
if (pS) {
pS->show();
return true;
}
if (pN) {
pN->show();
return true;
}
if (pT) {
pT->show();
return true;
}
return false;
}
bool Host::hideWindow(const QString& name)
{
if (!mpConsole) {
return false;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
auto pL = mpConsole->mLabelMap.value(name);
auto pN = mpConsole->mSubCommandLineMap.value(name);
auto pS = mpConsole->mScrollBoxMap.value(name);
auto pT = mpConsole->mTextBoxMap.value(name);
// check labels first as they are shown/hidden more often
if (pL) {
pL->hide();
return true;
}
if (pC) {
auto pD = mpConsole->mDockWidgetMap.value(name);
if (pD) {
pD->hide();
pD->update();
}
return mpConsole->hideWindow(name);
}
if (pS) {
pS->hide();
return true;
}
if (pN) {
pN->hide();
return true;
}
if (pT) {
pT->hide();
return true;
}
return false;
}
bool Host::resizeWindow(const QString& name, int x1, int y1)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
auto pC = mpConsole->mSubConsoleMap.value(name);
auto pD = mpConsole->mDockWidgetMap.value(name);
auto pN = mpConsole->mSubCommandLineMap.value(name);
auto pS = mpConsole->mScrollBoxMap.value(name);
auto pT = mpConsole->mTextBoxMap.value(name);
if (pL) {
pL->resize(x1, y1);
return true;
}
if (pC && !pD) {
// NOT a floatable/dockable "user window"
pC->resize(x1, y1);
return true;
}
if (pC && pD) {
if (!pD->isFloating()) {
// Undock a docked window
pD->setFloating(true);
}
pD->resize(x1, y1);
return true;
}
if (pS) {
pS->resize(x1, y1);
return true;
}
if (pN) {
pN->resize(x1, y1);
return true;
}
if (pT) {
pT->resize(x1, y1);
return true;
}
return false;
}
bool Host::moveWindow(const QString& name, int x1, int y1)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
auto pC = mpConsole->mSubConsoleMap.value(name);
auto pD = mpConsole->mDockWidgetMap.value(name);
auto pN = mpConsole->mSubCommandLineMap.value(name);
auto pS = mpConsole->mScrollBoxMap.value(name);
auto pT = mpConsole->mTextBoxMap.value(name);
if (pL) {
pL->move(x1, y1);
return true;
}
if (pC && !pD) {
// NOT a floatable/dockable "user window"
pC->move(x1, y1);
pC->mOldX = x1;
pC->mOldY = y1;
return true;
}
if (pC && pD) {
if (!pD->isFloating()) {
// Undock a docked window
pD->setFloating(true);
}
pD->move(x1, y1);
return true;
}
if (pS) {
pS->move(x1, y1);
return true;
}
if (pN) {
pN->move(x1, y1);
return true;
}
if (pT) {
pT->move(x1, y1);
return true;
}
return false;
}
std::pair<bool, QString> Host::setWindow(const QString& windowname, const QString& name, int x1, int y1, bool show)
{
if (!mpConsole) {
return {false, QString()};
}
//checks - for reasons why the indicated thing might not be moved to the indicated destination
auto pDCheck = mpConsole->mDockWidgetMap.value(name);
if (pDCheck) {
return {false, qsl("element '%1' is the base of a floating/dockable user window and may not be moved").arg(name)};
}
if (mpConsole->mpDockableMapWidget) {
if (!name.compare(QLatin1String("mapper"), Qt::CaseInsensitive)) {
return {false, qsl("element '%1' is the map in a floating/dockable window and may not be moved").arg(name)};
}
if (!windowname.compare(QLatin1String("mapper"), Qt::CaseInsensitive)) {
return {false, qsl("window '%1' is the map in a floating/dockable window and may not receive other elements").arg(windowname)};
}
}
//children
auto pL = mpConsole->mLabelMap.value(name);
auto pC = mpConsole->mSubConsoleMap.value(name);
auto pM = mpConsole->mpMapper;
auto pN = mpConsole->mSubCommandLineMap.value(name);
auto pS = mpConsole->mScrollBoxMap.value(name);
auto pT = mpConsole->mTextBoxMap.value(name);
//parents
auto pW = mpConsole->mpMainFrame;
auto pD = mpConsole->mDockWidgetMap.value(windowname);
auto pSW = mpConsole->mScrollBoxMap.value(windowname);
if (!pSW && !pD && windowname.compare(QLatin1String("main"), Qt::CaseInsensitive)) {
// Third argument is non-zero (i.e. true) if the window name is NOT
// the given string:
return {false, qsl("window '%1' not found").arg(windowname)};
}
if (pD) {
pW = pD->widget();
}
if (pSW) {
pW = pSW->widget();
}
if (pL) {
pL->setParent(pW);
pL->move(x1, y1);
if (show) {
pL->show();
}
return {true, QString()};
}
if (pC) {
pC->setParent(pW);
pC->move(x1, y1);
pC->mOldX = x1;
pC->mOldY = y1;
if (show) {
pC->show();
}
return {true, QString()};
}
if (pS) {
pS->setParent(pW);
pS->move(x1, y1);
if (show) {
pS->show();
}
return {true, QString()};
}
if (pN) {
pN->setParent(pW);
pN->move(x1, y1);
if (show) {
pN->show();
}
return {true, QString()};
}
if (pT) {
pT->setParent(pW);
pT->move(x1, y1);
if (show) {
pT->show();
}
return {true, QString()};
}
if (pM && !name.compare(QLatin1String("mapper"), Qt::CaseInsensitive)) {
pM->setParent(pW);
pM->move(x1, y1);
if (show) {
pM->show();
}
return {true, QString()};
}
return {false, qsl("element '%1' not found").arg(name)};
}
std::pair<bool, QString> Host::openMapWidget(const QString& area, int x, int y, int width, int height)
{
if (!mpConsole) {
return {false, QString()};
}
auto pM = mpConsole->mpDockableMapWidget;
auto pMapper = mpMap.data()->mpMapper;
if (!pM && !pMapper) {
showHideOrCreateMapper(true);
pM = mpConsole->mpDockableMapWidget;
}
if (!pM) {
return {false, qsl("cannot create map widget. Do you already use an embedded mapper?")};
}
pM->show();
if (area.isEmpty()) {
return {true, QString()};
}
if (area == QLatin1String("f") || area == QLatin1String("floating")) {
if (!pM->isFloating()) {
// Undock a docked window
// Change of position or size is only possible when floating
pM->setFloating(true);
}
if ((x != -1) && (y != -1)) {
pM->move(x, y);
}
if ((width != -1) && (height != -1)) {
pM->resize(width, height);
}
return {true, QString()};
}
if (area == QLatin1String("r") || area == QLatin1String("right")) {
pM->setFloating(false);
mudlet::self()->addDockWidget(Qt::RightDockWidgetArea, pM);
return {true, QString()};
}
if (area == QLatin1String("l") || area == QLatin1String("left")) {
pM->setFloating(false);
mudlet::self()->addDockWidget(Qt::LeftDockWidgetArea, pM);
return {true, QString()};
}
if (area == QLatin1String("t") || area == QLatin1String("top")) {
pM->setFloating(false);
mudlet::self()->addDockWidget(Qt::TopDockWidgetArea, pM);
return {true, QString()};
}
if (area == QLatin1String("b") || area == QLatin1String("bottom")) {
pM->setFloating(false);
mudlet::self()->addDockWidget(Qt::BottomDockWidgetArea, pM);
return {true, QString()};
}
return {false, qsl(R"("docking option "%1" not available. available docking options are "t" top, "b" bottom, "r" right, "l" left and "f" floating")").arg(area)};
}
// The inverse of moveMapWidget()/resizeMapWidget(), which reach the dock widget
// through openMapWidget(). pos()/size() rather than geometry() for the same
// reason as Host::windowGeometry(): they are what move()/resize() were given,
// while a floating dock's geometry() reports the client area instead.
std::optional<QRect> Host::mapWidgetGeometry() const
{
if (!mpConsole || !mpConsole->mpDockableMapWidget) {
return {};
}
auto pM = mpConsole->mpDockableMapWidget;
return {QRect(pM->pos(), pM->size())};
}
std::pair<bool, QString> Host::closeMapWidget()
{
if (!mpConsole) {
return {false, QString()};
}
auto pM = mpConsole->mpDockableMapWidget;
if (!pM) {
return {false, qsl("no map widget found to close")};
}
if (!pM->isVisible()) {
return {false, qsl("map widget already closed")};
}
pM->hide();
return {true, QString()};
}
bool Host::closeWindow(const QString& name)
{
if (!mpConsole) {
return false;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
if (pC) {
auto pD = mpConsole->mDockWidgetMap.value(name);
if (pD) {
pD->hide();
pD->update();
}
return mpConsole->hideWindow(name);
}
return false;
}
bool Host::echoWindow(const QString& name, const QString& text)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
auto pC = mpConsole->mSubConsoleMap.value(name);
if (pC) {
pC->print(text);
return true;
}
if (pL) {
pL->setText(text);
return true;
}
return false;
}
bool Host::pasteWindow(const QString& name)
{
if (!mpConsole) {
return false;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
if (pC) {
pC->pasteWindow(mpConsole->mClipboard);
return true;
}
return false;
}
bool Host::setCmdLineAction(const QString& name, const int func)
{
if (!mpConsole) {
return false;
}
auto pN = mpConsole->mSubCommandLineMap.value(name);
if (pN) {
pN->setAction(func);
return true;
}
return false;
}
bool Host::resetCmdLineAction(const QString& name)
{
if (!mpConsole) {
return false;
}
auto pN = mpConsole->mSubCommandLineMap.value(name);
if (pN) {
pN->resetAction();
return true;
}
return false;
}
bool Host::setLabelClickCallback(const QString& name, const int func)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setClick(func);
return true;
}
return false;
}
bool Host::setLabelDoubleClickCallback(const QString& name, const int func)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setDoubleClick(func);
return true;
}
return false;
}
bool Host::setLabelReleaseCallback(const QString& name, const int func)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setRelease(func);
return true;
}
return false;
}
bool Host::setLabelMoveCallback(const QString& name, const int func)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setMove(func);
return true;
}
return false;
}
bool Host::setLabelWheelCallback(const QString& name, const int func)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setWheel(func);
return true;
}
return false;
}
bool Host::setLabelOnEnter(const QString& name, const int func)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setEnter(func);
return true;
}
return false;
}
bool Host::setLabelOnLeave(const QString& name, const int func)
{
if (!mpConsole) {
return false;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->setLeave(func);
return true;
}
return false;
}
std::pair<bool, QString> Host::setMovie(const QString& name, const QString& moviePath)
{
if (!mpConsole) {
return {false, QString()};
}
auto pL = mpConsole->mLabelMap.value(name);
if (!pL) {
return {false, qsl("label '%1' does not exist").arg(name)};
}
auto myMovie = pL->mpMovie;
if (!myMovie) {
myMovie = new QMovie();
mGifTracker.registerGif(myMovie);
myMovie->setCacheMode(QMovie::CacheAll);
pL->mpMovie = myMovie;
myMovie->setParent(pL);
}
myMovie->setFileName(moviePath);
if (!myMovie->isValid()) {
return {false, qsl("no valid movie found at '%1'").arg(moviePath)};
}
myMovie->stop();
pL->setMovie(myMovie);
myMovie->start();
return {true, QString()};
}
QSize Host::calcFontSize(const QString& windowName)
{
if (!mpConsole) {
return QSize(-1, -1);
}
if (windowName.isEmpty() || windowName.compare(qsl("main"), Qt::CaseSensitive) == 0) {
QFontMetrics fontMetrics(mpConsole->mUpperPane->fontMetrics());
return QSize(fontMetrics.horizontalAdvance(QChar('W')), fontMetrics.height());
}
auto pC = mpConsole->mSubConsoleMap.value(windowName);
if (!pC) {
return QSize(-1, -1);
}
Q_ASSERT_X(pC->mUpperPane, "calcFontSize", "located console does not have the upper pane available");
QFontMetrics fontMetrics(pC->mUpperPane->fontMetrics());
return QSize(fontMetrics.horizontalAdvance(QChar('W')), fontMetrics.height());
}
bool Host::setProfileStyleSheet(const QString& styleSheet)
{
if (!mpConsole) {
return false;
}
mProfileStyleSheet = styleSheet;
mpConsole->setStyleSheet(styleSheet);
if (mpEditorDialog) {
mpEditorDialog->setStyleSheet(styleSheet);
}
if (mpDlgProfilePreferences) {
mpDlgProfilePreferences->setStyleSheet(styleSheet);
}
if (mpNotePad) {
mpNotePad->setStyleSheet(styleSheet);
mpNotePad->setTabsStyleSheet(styleSheet);
}
if (mpConsole->mpDockableMapWidget) {
mpConsole->mpDockableMapWidget->setStyleSheet(styleSheet);
}
for (auto& dockWidget : mpConsole->mDockWidgetMap) {
dockWidget->setStyleSheet(styleSheet);
}
if (this == mudlet::self()->mpCurrentActiveHost) {
mudlet::self()->setGlobalStyleSheet(styleSheet);
}
return true;
}
bool Host::setBackgroundColor(const QString& name, int r, int g, int b, int alpha)
{
if (!mpConsole) {
return false;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
auto pL = mpConsole->mLabelMap.value(name);
if (pC) {
pC->setConsoleBgColor(r, g, b, alpha);
return true;
}
if (pL) {
QString styleSheet = pL->styleSheet();
QString newColor = QString("background-color: rgba(%1, %2, %3, %4);").arg(r).arg(g).arg(b).arg(alpha);
if (styleSheet.contains(qsl("background-color"))) {
QRegularExpression re("background-color:[^;]*;");
styleSheet.replace(re, newColor);
} else {
if (!styleSheet.isEmpty() && !styleSheet.endsWith('\n')) {
styleSheet.append('\n');
}
styleSheet.append(newColor);
}
pL->setStyleSheet(styleSheet);
return true;
}
return false;
}
std::optional<QColor> Host::getBackgroundColor(const QString& name) const
{
if (!mpConsole) {
return {};
}
auto pC = mpConsole->mSubConsoleMap.value(name);
auto pL = mpConsole->mLabelMap.value(name);
if (pC) {
return {pC->mBgColor};
}
if (pL) {
return {pL->palette().color(QPalette::Window)};
}
return {};
}
bool Host::setBackgroundImage(const QString& name, QString& imgPath, int mode, bool fullWindow)
{
if (!mpConsole) {
return false;
}
// Mode 4 ('style') is a raw QSS fragment (e.g. a gradient), not a literal file
// path - normalising separators would corrupt any url()/gradient syntax in it:
if (mode != 4) {
if (QDir::homePath().contains('\\')) {
imgPath.replace('/', R"(\)");
} else {
imgPath.replace('\\', "/");
}
}
if (fullWindow) {
return mpConsole->setWindowBackgroundImage(imgPath, mode);
}
if (name.isEmpty() || name.compare(qsl("main"), Qt::CaseSensitive) == 0) {
mpConsole->setConsoleBackgroundImage(imgPath, mode);
return true;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
const QPixmap bgPixmap(imgPath);
pL->setPixmap(bgPixmap);
return true;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
if (pC) {
pC->setConsoleBackgroundImage(imgPath, mode);
return true;
}
return false;
}
bool Host::resetBackgroundImage(const QString& name, bool fullWindow)
{
if (!mpConsole) {
return false;
}
if (fullWindow) {
return mpConsole->resetWindowBackgroundImage();
}
if (name.isEmpty() || name.compare(qsl("main"), Qt::CaseSensitive) == 0) {
mpConsole->resetConsoleBackgroundImage();
return true;
}
auto pL = mpConsole->mLabelMap.value(name);
if (pL) {
pL->clear();
return true;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
if (pC) {
pC->resetConsoleBackgroundImage();
return true;
}
return false;
}
bool Host::setCommandBackgroundColor(const QString& name, int r, int g, int b, int alpha)
{
if (!mpConsole) {
return false;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
if (pC) {
pC->setCommandBgColor(r, g, b, alpha);
return true;
}
return false;
}
bool Host::setCommandForegroundColor(const QString& name, int r, int g, int b, int alpha)
{
if (!mpConsole) {
return false;
}
auto pC = mpConsole->mSubConsoleMap.value(name);
if (pC) {
pC->setCommandFgColor(r, g, b, alpha);
return true;
}
return false;
}
// Needed to extract into a separate method from mudlet::slot_mapper() so that
// we can use it WITHOUT loading a file - at least for the
// TConsole::importMap(...) case that may need to create a map widget before it
// loads/imports a non-default (last saved map in profile's map directory).
void Host::showHideOrCreateMapper(const bool loadDefaultMap)
{
auto pMap = mpMap.data();
if (pMap->mpMapper) {
toggleMapperVisibility();
return;
}
createMapper(loadDefaultMap);
}
void Host::toggleMapperVisibility()
{
auto pMap = mpMap.data();
if (pMap->mpMapper->isFloatAndDockable()) {
// If we are using a floating/dockable widget we must show/hide that
// only and not the mapper widget (otherwise it messes up {shrinks
// to a minimal size} the mapper inside the container dock widget). This
// is the same as the case for a TConsole inside a TDockWidget in
// (void) TDockWidget::setVisible(bool).
// When in a dock widget, check the parent's visibility, not the child's,
// to correctly handle the case where the dock widget was closed via X button.
const bool isCurrentlyVisible = pMap->mpMapper->parentWidget()->isVisible();
if (isCurrentlyVisible) {
pMap->mpMapper->parentWidget()->setVisible(false);
} else {
// When showing, show child first then parent - same pattern as TDockWidget
pMap->mpMapper->show();
pMap->mpMapper->parentWidget()->setVisible(true);
}
} else {
const bool visStatus = pMap->mpMapper->isVisible();
pMap->mpMapper->setVisible(!visStatus);
}
}
void Host::createMapper(const bool loadDefaultMap)
{
// The console owns the map dock; bail if the profile has no console yet or is
// already being torn down.
if (!mpConsole) {
return;
}
auto pMap = mpMap.data();
auto hostName(getName());
mpConsole->createMapperDock(tr("Map - %1").arg(hostName), qsl("dockMap_%1").arg(hostName));
// Arrange for TMap member values to be copied from the Host masters so they
// are in place when the 2D mapper is created:
getPlayerRoomStyleDetails(pMap->mPlayerRoomStyle, pMap->mPlayerRoomOuterDiameterPercentage, pMap->mPlayerRoomInnerDiameterPercentage, pMap->mPlayerRoomOuterColor, pMap->mPlayerRoomInnerColor);
pMap->mpMapper = new dlgMapper(mpConsole->mpDockableMapWidget, this, pMap); //FIXME: mpHost definieren
pMap->mpMapper->setStyleSheet(mProfileStyleSheet);
mpConsole->mpDockableMapWidget->setWidget(pMap->mpMapper);
if (loadDefaultMap && pMap->mpRoomDB->isEmpty()) {
qDebug() << "Host::create_mapper() - restore map case 3.";
pMap->pushErrorMessagesToFile(tr("Pre-Map loading(3) report"), true);
const QDateTime now(QDateTime::currentDateTime());
if (pMap->restore(QString())) {
pMap->audit();
pMap->mpMapper->mp2dMap->init();
pMap->mpMapper->updateAreaComboBox();
pMap->mpMapper->resetAreaComboBoxToPlayerRoomArea();
pMap->mpMapper->show();
}
pMap->pushErrorMessagesToFile(tr("Loading map(3) at %1 report").arg(now.toString(Qt::ISODate)), true);
} else {
if (pMap->mpMapper) {
// Needed to set the area selector widget to right area when map is
// loaded by clicking on Map main toolbar button:
pMap->mpMapper->updateAreaComboBox();
pMap->mpMapper->resetAreaComboBoxToPlayerRoomArea();
pMap->mpMapper->show();
}
}
mudlet::self()->addDockWidget(Qt::RightDockWidgetArea, mpConsole->mpDockableMapWidget);
// XXX: should this be called multiple times?
mudlet::self()->loadWindowLayout();
// Ensure the mapper is visible after creation - loadWindowLayout() may have
// restored a previous hidden state, but when first creating the mapper, we
// always want it to be visible.
pMap->mpMapper->show();
mpConsole->mpDockableMapWidget->show();
pMap->mpMapper->updateEmptyStateOverlay();
check_for_mappingscript();
TEvent mapOpenEvent{};
mapOpenEvent.mArgumentList.append(QLatin1String("mapOpenEvent"));
mapOpenEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
raiseEvent(mapOpenEvent);
}
void Host::setDockLayoutUpdated(const QString& name)
{
if (!mpConsole) {
return;
}
auto pD = mpConsole->mDockWidgetMap.value(name);
if (Q_LIKELY(pD) && !mDockLayoutChanges.contains(name)) {
pD->setProperty("layoutChanged", QVariant(true));
mDockLayoutChanges.append(name);
}
}
void Host::setToolbarLayoutUpdated(TToolBar* pTB)
{
if (!mToolbarLayoutChanges.contains(pTB)) {
pTB->setProperty("layoutChanged", QVariant(true));
mToolbarLayoutChanges.append(pTB);
}
}
bool Host::commitLayoutUpdates(bool flush)
{
bool updated = false;
if (mpConsole && !flush) {
// commit changes (or rather clear the layout changed flags) for dockwidget
// consoles (user windows)
for (const auto& dockedConsoleName : std::as_const(mDockLayoutChanges)) {
auto pD = mpConsole->mDockWidgetMap.value(dockedConsoleName);
if (Q_LIKELY(pD) && pD->property("layoutChanged").toBool()) {
pD->setProperty("layoutChanged", QVariant(false));
updated = true;
}
}
}
mDockLayoutChanges.clear();
// commit changes (or rather clear the layout changed flags) for
// dockable/floating toolbars across all profiles:
if (!flush) {
for (const auto& pToolBar : std::as_const(mToolbarLayoutChanges)) {
if (!pToolBar || pToolBar.isNull()) {
// This can happen when a TToolBar is deleted
continue;
}
if (Q_UNLIKELY(!pToolBar->property("layoutChanged").isValid())) {
qWarning().nospace().noquote() << "host::commitLayoutUpdates() WARNING - was about to check for \"layoutChanged\" meta-property on a toolbar without that property!";
} else if (pToolBar->property("layoutChanged").toBool()) {
pToolBar->setProperty("layoutChanged", QVariant(false));
updated = true;
}
}
}
mToolbarLayoutChanges.clear();
return updated;
}
void Host::setupIreDriverBugfix()
{
// IRE games suffer from unnecessary linebreaks across split packets
// but other games implementing GA don't. Thus, only enable the workaround
// for the former only
const QStringList ireGameUrls{"achaea.com", "lusternia.com", "imperian.com", "aetolia.com", "starmourn.com"};
if (ireGameUrls.contains(getUrl(), Qt::CaseInsensitive)) {
set_USE_IRE_DRIVER_BUGFIX(true);
}
}
void Host::setControlCharacterMode(const ControlCharacterMode mode)
{
if (Q_UNLIKELY(!(mode == ControlCharacterMode::AsIs || mode == ControlCharacterMode::Picture || mode == ControlCharacterMode::OEM))) {
return;
}
if (mControlCharacter != mode) {
mControlCharacter = mode;
emit signal_controlCharacterHandlingChanged(mode);
}
}
std::optional<QString> Host::windowType(const QString& name) const
{
if (Q_UNLIKELY(name == QLatin1String("main"))) {
return {QLatin1String("main")};
}
if (mpConsole->mLabelMap.contains(name)) {
return {qsl("label")};
}
auto pWindow = mpConsole->mSubConsoleMap.value(name);
if (pWindow) {
switch (pWindow->getType()) {
case TConsole::UserWindow:
return {qsl("userwindow")};
case TConsole::Buffer:
return {qsl("buffer")};
case TConsole::SubConsole:
return {qsl("miniconsole")};
case TConsole::UnknownType:
[[fallthrough]];
case TConsole::CentralDebugConsole:
[[fallthrough]];
case TConsole::ErrorConsole:
[[fallthrough]];
case TConsole::MainConsole:
[[fallthrough]];
default:
Q_UNREACHABLE();
return {};
}
}
if (mpConsole->mScrollBoxMap.contains(name)) {
return {qsl("scrollbox")};
}
if (mpConsole->mSubCommandLineMap.contains(name)) {
return {qsl("commandline")};
}
if (mpConsole->mTextBoxMap.contains(name)) {
return {qsl("textedit")};
}
return {};
}
// Returns the position and size of a named window element, matching what
// moveWindow()/resizeWindow() set. pos()/size() (rather than geometry()) are
// used deliberately: they are the exact inverse of the move()/resize() calls
// those setters make, including for a floating user-window dock where move()
// targets the frame origin while geometry() would report the client area.
// Mirrors the widget dispatch of moveWindow()/resizeWindow(); user windows are
// moved/resized through their dock widget, so read the dock, not the console.
std::optional<QRect> Host::windowGeometry(const QString& name) const
{
if (!mpConsole) {
return {};
}
if (auto pL = mpConsole->mLabelMap.value(name)) {
return {QRect(pL->pos(), pL->size())};
}
if (auto pC = mpConsole->mSubConsoleMap.value(name)) {
if (auto pD = mpConsole->mDockWidgetMap.value(name)) {
return {QRect(pD->pos(), pD->size())};
}
return {QRect(pC->pos(), pC->size())};
}
if (auto pS = mpConsole->mScrollBoxMap.value(name)) {
return {QRect(pS->pos(), pS->size())};
}
if (auto pN = mpConsole->mSubCommandLineMap.value(name)) {
return {QRect(pN->pos(), pN->size())};
}
if (auto pT = mpConsole->mTextBoxMap.value(name)) {
return {QRect(pT->pos(), pT->size())};
}
return {};
}
// Returns whether a named window element is currently visible. Mirrors the
// widget dispatch of hideWindow()/showWindow(); user windows report the
// visibility of their dock widget, which is what those functions toggle.
std::optional<bool> Host::windowVisible(const QString& name) const
{
if (!mpConsole) {
return {};
}
if (auto pL = mpConsole->mLabelMap.value(name)) {
return {pL->isVisible()};
}
if (auto pC = mpConsole->mSubConsoleMap.value(name)) {
if (auto pD = mpConsole->mDockWidgetMap.value(name)) {
return {pD->isVisible()};
}
return {pC->isVisible()};
}
if (auto pS = mpConsole->mScrollBoxMap.value(name)) {
return {pS->isVisible()};
}
if (auto pN = mpConsole->mSubCommandLineMap.value(name)) {
return {pN->isVisible()};
}
if (auto pT = mpConsole->mTextBoxMap.value(name)) {
return {pT->isVisible()};
}
return {};
}
void Host::setLargeAreaExitArrows(const bool state)
{
if (mLargeAreaExitArrows != state) {
mLargeAreaExitArrows = state;
if (mpMap && mpMap->mpMapper && mpMap->mpMapper->mp2dMap) {
mpMap->mpMapper->mp2dMap->mLargeAreaExitArrows = state;
mpMap->mpMapper->mp2dMap->update();
}
}
}
void Host::setEditorShowBidi(const bool state)
{
if (mEditorShowBidi != state) {
mEditorShowBidi = state;
if (mpEditorDialog) {
mpEditorDialog->setEditorShowBidi(state);
}
}
}
bool Host::caretEnabled() const
{
return mCaretEnabled;
}
void Host::setCaretEnabled(bool enabled)
{
mCaretEnabled = enabled;
mpConsole->setCaretMode(enabled);
}
// Whether this key press is the one selected in the accessibility preferences
// to toggle caret mode - such a press must reach the caret-toggling key
// handlers instead of being swallowed by an application-wide QShortcut,
// which the user could have remapped onto any of these keys:
bool Host::caretShortcutMatches(const QKeyEvent* ke) const
{
constexpr Qt::KeyboardModifiers allModifiers = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier | Qt::KeypadModifier | Qt::GroupSwitchModifier;
switch (mCaretShortcut) {
case CaretShortcut::None:
return false;
case CaretShortcut::Tab:
return ke->key() == Qt::Key_Tab && !(ke->modifiers() & Qt::ControlModifier);
case CaretShortcut::CtrlTab:
return ke->key() == Qt::Key_Tab && (ke->modifiers() & Qt::ControlModifier);
case CaretShortcut::F6:
return ke->key() == Qt::Key_F6 && (ke->modifiers() & allModifiers) == Qt::NoModifier;
}
return false;
}
void Host::setFocusOnHostActiveCommandLine()
{
if (mFocusTimerRunning) {
return;
}
mFocusTimerRunning = true;
// Lambda to set focus on command line
auto setCommandLineFocus = [this]() {
auto pCommandLine = activeCommandLine();
TCommandLine* targetCommandLine = nullptr;
if (pCommandLine) {
pCommandLine->activateWindow();
pCommandLine->console()->show();
pCommandLine->console()->raise();
pCommandLine->console()->repaint();
targetCommandLine = pCommandLine;
} else {
mpConsole->mpCommandLine->activateWindow();
mpConsole->show();
mpConsole->raise();
mpConsole->repaint();
targetCommandLine = mpConsole->mpCommandLine;
}
if (targetCommandLine) {
targetCommandLine->setFocus(Qt::OtherFocusReason);
// For Steam Deck and other environments where focus might be unreliable,
// add additional focus attempts with slight delays
QTimer::singleShot(10ms, this, [targetCommandLine]() {
if (targetCommandLine && !targetCommandLine->hasFocus()) {
targetCommandLine->setFocus(Qt::OtherFocusReason);
}
});
QTimer::singleShot(50ms, this, [targetCommandLine]() {
if (targetCommandLine && !targetCommandLine->hasFocus()) {
targetCommandLine->setFocus(Qt::OtherFocusReason);
}
});
}
mFocusTimerRunning = false;
};
QTimer::singleShot(0ms, this, setCommandLineFocus);
}
void Host::recordActiveCommandLine(TCommandLine* pCommandLine)
{
if (!pCommandLine || mIsClosingDown) {
return;
}
mpLastCommandLineUsed.removeAll(QPointer<TCommandLine>(pCommandLine));
mpLastCommandLineUsed.push(QPointer<TCommandLine>(pCommandLine));
}
void Host::forgetCommandLine(TCommandLine* pCommandLine)
{
if (pCommandLine && !mIsClosingDown) {
mpLastCommandLineUsed.removeAll(QPointer<TCommandLine>(pCommandLine));
}
}
// Returns a pointer to the last used TCommandLine for this profile:
TCommandLine* Host::activeCommandLine()
{
TCommandLine* pCommandLine = nullptr;
if (mIsClosingDown || mpLastCommandLineUsed.isEmpty()) {
return nullptr;
}
do {
pCommandLine = mpLastCommandLineUsed.top();
if (!pCommandLine) {
mpLastCommandLineUsed.pop();
}
} while (!mpLastCommandLineUsed.isEmpty() && !pCommandLine);
return pCommandLine;
}
QPointer<TConsole> Host::parentTConsole(QObject* start) const
{
QPointer<TConsole> result;
auto ptr = start;
if (!ptr) {
// Handle pathalogical case:
return result;
}
do {
ptr = ptr->parent();
} while (ptr && !ptr->inherits("TConsole"));
// QObject::inherits(...) uses a const char* - so no need to wrap raw string literal!
if (!ptr) {
// Handle not found case:
return result;
}
return qobject_cast<TConsole*>(ptr);
}
void Host::setBorders(QMargins borders)
{
auto original = mBorders;
if (borders == original) {
return;
}
mBorders = borders;
if (mpConsole.isNull()) {
return;
}
auto x = mpConsole->width();
auto y = mpConsole->height();
const QSize s = QSize(x, y);
QResizeEvent event(s, s);
QCoreApplication::sendEvent(mpConsole, &event);
mpConsole->raiseMudletSysWindowResizeEvent(x, y);
}
void Host::setUserBorders(const QMargins borders)
{
mUserBorders = borders;
setBorders(mUserBorders + mMxpBorders);
}
void Host::setMxpBorders(const QMargins borders)
{
mMxpBorders = borders;
setBorders(mUserBorders + mMxpBorders);
}
void Host::setCommandLineHistorySaveSize(const int lines)
{
if (mCommandLineHistorySaveSize != lines) {
mCommandLineHistorySaveSize = lines;
}
}
QString Host::getEditorTheme() const
{
if (mudlet::self()->inDarkMode() && !mEditorThemeDark.isEmpty() && !mEditorThemeFileDark.isEmpty()) {
return mEditorThemeDark;
}
return mEditorTheme;
}
QString Host::getEditorThemeFile() const
{
if (mudlet::self()->inDarkMode() && !mEditorThemeDark.isEmpty() && !mEditorThemeFileDark.isEmpty()) {
return mEditorThemeFileDark;
}
return mEditorThemeFile;
}
void Host::editorThemeChanged()
{
emit signal_editorThemeChanged();
}
void Host::sendCmdLine(const QString& cmd)
{
if (!mpConsole || !mpConsole->mpCommandLine) {
qWarning() << "Host::sendCmdLine(...) ERROR - No active command line available.";
return;
}
// Set the command in the active command line
mpConsole->mpCommandLine->setPlainText(cmd);
mpConsole->mpCommandLine->selectAll();
}
void Host::setRemoteEchoingActive(bool active)
{
if (mIsRemoteEchoingActive != active) {
mIsRemoteEchoingActive = active;
emit signal_remoteEchoChanged(active);
}
}
QFont Host::getDisplayFont()
{
if (mpConsole) {
return mpConsole->font();
}
qDebug().noquote().nospace() << "Host::getDisplayFont() INFO - No TMainConsole to get font from - faking it";
if (!mTempDisplayFontAttributes.has_value() || !mTempDisplayFont.has_value()) {
mTempDisplayFontAttributes = TFontAttributes(!mNoAntiAlias);
mTempDisplayFont = mTempDisplayFontAttributes.value().makeFont();
}
return mTempDisplayFont.value();
}
QFont Host::getAndClearTempDisplayFont()
{
if (!mTempDisplayFontAttributes.has_value() || !mTempDisplayFont.has_value()) {
qWarning() << "Host::getAndClearTempDisplayFont() WARNING - no temp font was set, using default";
mTempDisplayFontAttributes = TFontAttributes(!mNoAntiAlias);
mTempDisplayFont = mTempDisplayFontAttributes.value().makeFont();
}
QFont tempFont = mTempDisplayFont.value();
mTempDisplayFont.reset();
mTempDisplayFontAttributes.reset();
return tempFont;
}
// Static whitelist of valid experiments
const QSet<QString> Host::mValidExperiments = {qsl("experiment.rendering.originalish"),
qsl("experiment.rendering.more-transparent"),
qsl("experiment.3dmap.modernmapper"),
qsl("experiment.render-in-out-exits"),
qsl("experiment.3d-player-icon")};
bool Host::experimentEnabled(const QString& experimentKey) const
{
return mExperiments.value(experimentKey, false);
}
std::pair<bool, QString> Host::setExperimentEnabled(const QString& experimentKey, bool enabled)
{
// Validate experiment key against whitelist
if (!mValidExperiments.contains(experimentKey)) {
return {false, qsl("Invalid experiment name: %1").arg(experimentKey)};
}
if (enabled) {
// Check if this is a grouped experiment (contains dots beyond "experiment.")
if (experimentKey.count('.') >= 2) {
// Extract group (e.g., "experiment.rendering" from "experiment.rendering.originalish")
QString group = experimentKey.section('.', 0, 1);
// Disable all other experiments in the same group
for (auto it = mExperiments.begin(); it != mExperiments.end(); ++it) {
if (it.key() != experimentKey && it.key().startsWith(group + ".")) {
it.value() = false;
}
}
}
mExperiments[experimentKey] = true;
} else {
mExperiments[experimentKey] = false;
}
#if defined(INCLUDE_3DMAPPER)
// Refresh maps if any experiments changed the 3D map
if (mpMap && mpMap->mpMapper && mpMap->mpMapper->mp2dMap) {
mpMap->mpMapper->mp2dMap->update();
}
if (mpMap && mpMap->mpM) {
mpMap->mpM->update();
}
#endif
return {true, QString()};
}
QString Host::getActiveExperimentInGroup(const QString& group) const
{
QString groupPrefix = group + ".";
for (auto it = mExperiments.constBegin(); it != mExperiments.constEnd(); ++it) {
if (it.key().startsWith(groupPrefix) && it.value()) {
// Return just the experiment name without the group prefix
return it.key().mid(groupPrefix.length());
}
}
return QString(); // No active experiment in this group
}
QStringList Host::getAllExperiments() const
{
QStringList result;
for (auto it = mExperiments.constBegin(); it != mExperiments.constEnd(); ++it) {
if (it.value()) {
result << it.key();
}
}
return result;
}
QStringList Host::getValidExperiments() const
{
return QStringList(mValidExperiments.constBegin(), mValidExperiments.constEnd());
}
bool Host::shouldStripOscHyperlinkConfigParam()
{
return mTelnet.oscHyperlinkConfigFeatureEnabled();
}
bool Host::shouldStripOscHyperlinkPresetParam()
{
return mTelnet.oscHyperlinkPresetsEnabled();
}