Mudlet  0
Mudclient
Host.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  * Copyright (C) 2008-2013 by Heiko Koehn - KoehnHeiko@googlemail.com *
3  * Copyright (C) 2014 by Ahmed Charles - acharles@outlook.com *
4  * Copyright (C) 2015-2018 by Stephen Lyons - slysven@virginmedia.com *
5  * Copyright (C) 2016 by Ian Adkins - ieadkins@gmail.com *
6  * Copyright (C) 2018 by Huadong Qi - novload@outlook.com *
7  * *
8  * This program is free software; you can redistribute it and/or modify *
9  * it under the terms of the GNU General Public License as published by *
10  * the Free Software Foundation; either version 2 of the License, or *
11  * (at your option) any later version. *
12  * *
13  * This program is distributed in the hope that it will be useful, *
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
16  * GNU General Public License for more details. *
17  * *
18  * You should have received a copy of the GNU General Public License *
19  * along with this program; if not, write to the *
20  * Free Software Foundation, Inc., *
21  * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
22  ***************************************************************************/
23 
24 
25 #include "Host.h"
26 
27 
28 #include "LuaInterface.h"
29 #include "TConsole.h"
30 #include "TEvent.h"
31 #include "TMap.h"
32 #include "TRoomDB.h"
33 #include "TScript.h"
34 #include "XMLimport.h"
35 #include "dlgMapper.h"
36 #include "dlgTriggerEditor.h"
37 #include "mudlet.h"
38 
39 #include "pre_guard.h"
40 #include <QtUiTools>
41 #include <zip.h>
42 #include "post_guard.h"
43 
44 Host::Host(int port, const QString& hostname, const QString& login, const QString& pass, int id)
45 : mTelnet(this)
46 , mpConsole(nullptr)
47 , mLuaInterpreter(this, id)
48 , commandLineMinimumHeight(30)
49 , mAlertOnNewData(true)
50 , mAllowToSendCommand(true)
51 , mAutoClearCommandLineAfterSend(false)
52 , mBlockScriptCompile(true)
53 , mEchoLuaErrors(false)
54 , mBorderBottomHeight(0)
55 , mBorderLeftWidth(0)
56 , mBorderRightWidth(0)
57 , mBorderTopHeight(0)
58 , mCommandLineFont(QFont("Bitstream Vera Sans Mono", 10, QFont::Normal))
59 , mCommandSeparator(QLatin1String(";"))
60 , mDisplayFont(QFont("Bitstream Vera Sans Mono", 10, QFont::Normal))
61 , mEnableGMCP(true)
62 , mEnableMSDP(false)
63 , mFORCE_GA_OFF(false)
64 , mFORCE_NO_COMPRESSION(false)
65 , mFORCE_SAVE_ON_EXIT(false)
66 , mInsertedMissingLF(false)
67 , mIsGoingDown(false)
68 , mIsProfileLoadingSequence(false)
69 , mLF_ON_GA(true)
70 , mNoAntiAlias(false)
71 , mpEditorDialog(nullptr)
72 , mpMap(new TMap(this))
73 , mpNotePad(nullptr)
74 , mPrintCommand(true)
75 , mIsCurrentLogFileInHtmlFormat(false)
76 , mIsNextLogFileInHtmlFormat(false)
77 , mIsLoggingTimestamps(false)
78 , mLogDir(QString())
79 , mLogFileName(QString())
80 , mLogFileNameFormat(QLatin1String("yyyy-MM-dd#HH-mm-ss")) // In the past we have used "yyyy-MM-dd#hh-mm-ss" but we always want a 24-hour clock
81 , mResetProfile(false)
82 , mScreenHeight(25)
83 , mScreenWidth(90)
84 , mTimeout(60)
85 , mUSE_FORCE_LF_AFTER_PROMPT(false)
86 , mUSE_IRE_DRIVER_BUGFIX(true)
87 , mUSE_UNIX_EOL(false)
88 , mWrapAt(100)
89 , mWrapIndentCount(0)
90 , mEditorTheme(QLatin1String("Mudlet"))
91 , mEditorThemeFile(QLatin1String("Mudlet.tmTheme"))
92 , mThemePreviewItemID(-1)
93 , mThemePreviewType(QString())
94 , mBlack(Qt::black)
95 , mLightBlack(Qt::darkGray)
96 , mRed(Qt::darkRed)
97 , mLightRed(Qt::red)
98 , mLightGreen(Qt::green)
99 , mGreen(Qt::darkGreen)
100 , mLightBlue(Qt::blue)
101 , mBlue(Qt::darkBlue)
102 , mLightYellow(Qt::yellow)
103 , mYellow(Qt::darkYellow)
104 , mLightCyan(Qt::cyan)
105 , mCyan(Qt::darkCyan)
106 , mLightMagenta(Qt::magenta)
107 , mMagenta(Qt::darkMagenta)
108 , mLightWhite(Qt::white)
109 , mWhite(Qt::lightGray)
110 , mFgColor(Qt::lightGray)
111 , mBgColor(Qt::black)
112 , mCommandBgColor(Qt::black)
113 , mCommandFgColor(QColor(113, 113, 0))
114 , mBlack_2(Qt::black)
115 , mLightBlack_2(Qt::darkGray)
116 , mRed_2(Qt::darkRed)
117 , mLightRed_2(Qt::red)
118 , mLightGreen_2(Qt::green)
119 , mGreen_2(Qt::darkGreen)
120 , mLightBlue_2(Qt::blue)
121 , mBlue_2(Qt::darkBlue)
122 , mLightYellow_2(Qt::yellow)
123 , mYellow_2(Qt::darkYellow)
124 , mLightCyan_2(Qt::cyan)
125 , mCyan_2(Qt::darkCyan)
126 , mLightMagenta_2(Qt::magenta)
127 , mMagenta_2(Qt::darkMagenta)
128 , mLightWhite_2(Qt::white)
129 , mWhite_2(Qt::lightGray)
130 , mFgColor_2(Qt::lightGray)
131 , mBgColor_2(Qt::black)
132 , mMapStrongHighlight(false)
133 , mSpellDic(QLatin1String("en_US"))
134 , mLogStatus(false)
135 , mEnableSpellCheck(true)
136 , mDiscordDisableServerSide(true)
137 , mDiscordAccessFlags(DiscordLuaAccessEnabled | DiscordSetSubMask)
138 , mLineSize(10.0)
139 , mRoomSize(0.5)
140 , mShowInfo(true)
141 , mBubbleMode(false)
142 , mShowRoomID(false)
143 , mShowPanel(true)
144 , mServerGUI_Package_version(-1)
145 , mServerGUI_Package_name(QLatin1String("nothing"))
146 , mAcceptServerGUI(true)
147 , mCommandLineFgColor(Qt::darkGray)
148 , mCommandLineBgColor(Qt::black)
149 , mMapperUseAntiAlias(true)
150 , mFORCE_MXP_NEGOTIATION_OFF(false)
151 , mpDockableMapWidget()
152 , mTimerDebugOutputSuppressionInterval(QTime())
153 , mTriggerUnit(this)
154 , mTimerUnit(this)
155 , mScriptUnit(this)
156 , mAliasUnit(this)
157 , mActionUnit(this)
158 , mKeyUnit(this)
159 , mCodeCompletion(true)
160 , mDisableAutoCompletion(false)
161 , mHostID(id)
162 , mHostName(hostname)
163 , mIsClosingDown(false)
164 , mLogin(login)
165 , mPass(pass)
166 , mPort(port)
167 , mRetries(5)
168 , mSaveProfileOnExit(false)
169 , mHaveMapperScript(false)
170 , mAutoAmbigousWidthGlyphsSetting(true)
171 , mWideAmbigousWidthGlyphs(false)
172 {
173  // mLogStatus = mudlet::self()->mAutolog;
174  mLuaInterface.reset(new LuaInterface(this));
175  QString directoryLogFile = mudlet::getMudletPath(mudlet::profileDataItemPath, mHostName, QStringLiteral("log"));
176  QString logFileName = QStringLiteral("%1/errors.txt").arg(directoryLogFile);
177  QDir dirLogFile;
178  if (!dirLogFile.exists(directoryLogFile)) {
179  dirLogFile.mkpath(directoryLogFile);
180  }
181  mErrorLogFile.setFileName(logFileName);
182  mErrorLogFile.open(QIODevice::Append);
183  // This is NOW used (for map
184  // file auditing and other issues)
185  mErrorLogStream.setDevice(&mErrorLogFile);
186 
187  QTimer::singleShot(0, this, [this]() {
188  qDebug() << "Host::Host() - restore map case 4 {QTimer::singleShot(0)} lambda.";
189  if (mpMap->restore(QString(), false)) {
190  mpMap->audit();
191  if (mpMap->mpMapper) {
192  mpMap->mpMapper->mp2dMap->init();
193  mpMap->mpMapper->updateAreaComboBox();
194  mpMap->mpMapper->resetAreaComboBoxToPlayerRoomArea();
195  mpMap->mpMapper->show();
196  }
197  }
198  });
199 
200  mGMCP_merge_table_keys.append("Char.Status");
201  mDoubleClickIgnore.insert('"');
202  mDoubleClickIgnore.insert('\'');
203 
204  // search engine load entries
205  mSearchEngineData = QMap<QString, QString>(
206  {
207  {"Bing", "https://www.bing.com/search?q="},
208  {"DuckDuckGo", "https://duckduckgo.com/?q="},
209  {"Google", "https://www.google.com/search?q="}
210  });
211 
212  auto optin = readProfileData(QStringLiteral("discordserveroptin"));
213  if (!optin.isEmpty()) {
214  mDiscordDisableServerSide = optin.toInt() == Qt::Unchecked ? true : false;
215  }
216 }
217 
219 {
220  if (mpDockableMapWidget) {
221  mpDockableMapWidget->deleteLater();
222  }
223  mIsGoingDown = true;
224  mIsClosingDown = true;
226  mErrorLogStream.flush();
227  mErrorLogFile.close();
228 }
229 
230 void Host::saveModules(int sync, bool backup)
231 {
232  QMapIterator<QString, QStringList> it(modulesToWrite);
233  mModulesToSync.clear();
235  auto savePathDir = QDir(savePath);
236  if (!savePathDir.exists()) {
237  savePathDir.mkpath(savePath);
238  }
239  while (it.hasNext()) {
240  it.next();
241  QStringList entry = it.value();
242  QString moduleName = it.key();
243  QString filename_xml = entry[0];
244 
245  if (backup) {
246  // CHECKME: Consider changing datetime spec to more "sortable" "yyyy-MM-dd#HH-mm-ss" (1 of 6)
247  QString time = QDateTime::currentDateTime().toString("dd-MM-yyyy#hh-mm-ss");
248  savePathDir.rename(filename_xml, savePath + moduleName + time); //move the old file, use the key (module name) as the file
249  }
250 
251  auto writer = new XMLexport(this);
252  writers.insert(filename_xml, writer);
253  writer->writeModuleXML(moduleName, filename_xml);
254 
255  if (entry[1].toInt()) {
256  mModulesToSync << moduleName;
257  }
258  }
259  modulesToWrite.clear();
260 
261  if (sync) {
262  connect(this, &Host::profileSaveFinished, this, &Host::slot_reloadModules);
263  }
264 }
265 
267 {
268  // update the module zips
270 
271  //synchronize modules across sessions
272  QMap<Host*, TConsole*> activeSessions = mudlet::self()->mConsoleMap;
273  QMapIterator<Host*, TConsole*> sessionIterator(activeSessions);
274  while (sessionIterator.hasNext()) {
275  sessionIterator.next();
276  Host* otherHost = sessionIterator.key();
277  if (otherHost->getName() == mHostName) {
278  continue;
279  }
280  QMap<QString, int> modulePri = otherHost->mModulePriorities;
281  QMap<int, QStringList> moduleOrder;
282 
283  auto modulePrioritiesIt = modulePri.constBegin();
284  while (modulePrioritiesIt != modulePri.constEnd()) {
285  moduleOrder[modulePrioritiesIt.value()].append(modulePrioritiesIt.key());
286  ++modulePrioritiesIt;
287  }
288 
289  QMapIterator<int, QStringList> it(moduleOrder);
290  while (it.hasNext()) {
291  it.next();
292  QStringList moduleList = it.value();
293  for (int i = 0, total = moduleList.size(); i < total; ++i) {
294  QString moduleName = moduleList[i];
295  if (mModulesToSync.contains(moduleName)) {
296  otherHost->reloadModule(moduleName);
297  }
298  }
299  }
300  }
301 
302  // disconnect the one-time event so we're not always reloading modules whenever a profile save happens
303  mModulesToSync.clear();
304  QObject::disconnect(this, &Host::profileSaveFinished, this, &Host::slot_reloadModules);
305 }
306 
308 {
309  QMapIterator<QString, QStringList> it(modulesToWrite);
310  while (it.hasNext()) {
311  it.next();
312  QStringList entry = it.value();
313  QString moduleName = it.key();
314  QString filename_xml = entry[0];
315 
316  QString zipName;
317  zip* zipFile = nullptr;
318  if (filename_xml.endsWith(QStringLiteral("mpackage"), Qt::CaseInsensitive) || filename_xml.endsWith(QStringLiteral("zip"), Qt::CaseInsensitive)) {
319  QString packagePathName = mudlet::getMudletPath(mudlet::profilePackagePath, mHostName, moduleName);
321  int err;
322  zipFile = zip_open(entry[0].toStdString().c_str(), ZIP_CREATE, &err);
323  zipName = filename_xml;
324  QDir packageDir = QDir(packagePathName);
325  if (!packageDir.exists()) {
326  packageDir.mkpath(packagePathName);
327  }
328 
329  struct zip_source* s = zip_source_file(zipFile, filename_xml.toStdString().c_str(), 0, 0);
330  err = zip_add(zipFile, QString(moduleName + ".xml").toStdString().c_str(), s);
331  //FIXME: error checking
332  if (zipFile) {
333  err = zip_close(zipFile);
334  //FIXME: error checking
335  }
336  }
337  }
338 }
339 
340 
341 void Host::reloadModule(const QString& reloadModuleName)
342 {
343  QMap<QString, QStringList> installedModules = mInstalledModules;
344  QMapIterator<QString, QStringList> moduleIterator(installedModules);
345  while (moduleIterator.hasNext()) {
346  moduleIterator.next();
347  const auto& moduleName = moduleIterator.key();
348  const auto& moduleLocation = moduleIterator.value()[0];
349 
350  if (moduleName == reloadModuleName) {
351  uninstallPackage(moduleName, 2);
352  installPackage(moduleLocation, 2);
353  }
354  }
355  //iterate through mInstalledModules again and reset the entry flag to be correct.
356  //both the installedModules and mInstalled should be in the same order now as well
357  moduleIterator.toFront();
358  while (moduleIterator.hasNext()) {
359  moduleIterator.next();
360  QStringList entry = installedModules[moduleIterator.key()];
361  mInstalledModules[moduleIterator.key()] = entry;
362  }
363 }
364 
366 {
368  mudlet::self()->mTimerMap.clear();
372 
373 
377  mpConsole->resetMainConsole();
378  mEventHandlerMap.clear();
379  mEventMap.clear();
383  mBlockScriptCompile = false;
384 
385 
389  getKeyUnit()->compileAll();
391  // All the Timers are NOT compiled here;
392  mResetProfile = false;
393 
395 
396  TEvent event;
397  event.mArgumentList.append(QLatin1String("sysLoadEvent"));
398  event.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
399  raiseEvent(event);
400  qDebug() << "resetProfile() DONE";
401 }
402 
403 // Saves profile to disk - does not save items dirty in the editor, however.
404 // takes a directory to save in or an empty string for the default location
405 // as well as a boolean whenever to sync the modules or not
406 // returns true+filepath if successful or false+error message otherwise
407 std::tuple<bool, QString, QString> Host::saveProfile(const QString& saveFolder, const QString& saveName, bool syncModules)
408 {
409  emit profileSaveStarted();
410  qApp->processEvents();
411 
412  QString directory_xml;
413  if (saveFolder.isEmpty()) {
415  } else {
416  directory_xml = saveFolder;
417  }
418 
419  // CHECKME: Consider changing datetime spec to more "sortable" "yyyy-MM-dd#HH-mm-ss" (2 of 6)
420  QString filename_xml;
421  if (saveName.isEmpty()) {
422  filename_xml = QStringLiteral("%1/%2.xml").arg(directory_xml, QDateTime::currentDateTime().toString(QStringLiteral("dd-MM-yyyy#hh-mm-ss")));
423  } else {
424  filename_xml = QStringLiteral("%1/%2.xml").arg(directory_xml, saveName);
425  }
426 
427  QDir dir_xml;
428  if (!dir_xml.exists(directory_xml)) {
429  dir_xml.mkpath(directory_xml);
430  }
431 
432  if (currentlySavingProfile()) {
433  return std::make_tuple(false, QString(), QStringLiteral("a save is already in progress"));
434  }
435 
436  auto writer = new XMLexport(this);
437  writers.insert(QStringLiteral("profile"), writer);
438  writer->exportHost(filename_xml);
439  saveModules(syncModules ? 1 : 0, saveName == QStringLiteral("autosave") ? false : true);
440  return std::make_tuple(true, filename_xml, QString());
441 }
442 
443 // exports without the host settings for some reason
444 std::tuple<bool, QString, QString> Host::saveProfileAs(const QString& file)
445 {
446  emit profileSaveStarted();
447  qApp->processEvents();
448 
449  if (currentlySavingProfile()) {
450  return std::make_tuple(false, QString(), QStringLiteral("a save is already in progress"));
451  }
452 
453  auto writer = new XMLexport(this);
454  writers.insert(QStringLiteral("profile"), writer);
455  writer->exportProfile(file);
456  return std::make_tuple(true, file, QString());
457 }
458 
459 void Host::xmlSaved(const QString& xmlName)
460 {
461  qDebug() << "saved" << xmlName;
462  if (writers.contains(xmlName)) {
463  auto writer = writers.take(xmlName);
464  delete writer;
465  }
466 
467  if (writers.empty()) {
468  emit profileSaveFinished();
469  }
470 }
471 
473 {
474  return !writers.empty();
475 }
476 
478 {
479  for (auto& writer : writers) {
480  for (auto& future: writer->saveFutures) {
481  future.waitForFinished();
482  }
483  }
484 }
485 
487 {
488  auto document = QJsonDocument::fromJson(data.toUtf8());
489  if (!document.isObject()) {
490  return;
491  }
492  auto json = document.object();
493  if (json.isEmpty()) {
494  return;
495  }
496 
497  auto urlValue = json.value(QStringLiteral("url"));
498  if (urlValue == QJsonValue::Undefined) {
499  return;
500  }
501  auto url = QUrl(urlValue.toString());
502  if (!url.isValid()) {
503  return;
504  }
505 
506  mpMap->setMmpMapLocation(urlValue.toString());
507 }
508 
510 {
511  return mpMap->getMmpMapLocation();
512 }
513 
514 // Now returns the total weight of the path
515 const unsigned int Host::assemblePath()
516 {
517  unsigned int totalWeight = 0;
518  QStringList pathList;
519  for (int i : mpMap->mPathList) {
520  QString n = QString::number(i);
521  pathList.append(n);
522  }
523  QStringList directionList = mpMap->mDirList;
524  QStringList weightList;
525  for (int stepWeight : mpMap->mWeightList) {
526  totalWeight += stepWeight;
527  QString n = QString::number(stepWeight);
528  weightList.append(n);
529  }
530  QString tableName = QStringLiteral("speedWalkPath");
531  mLuaInterpreter.set_lua_table(tableName, pathList);
532  tableName = QStringLiteral("speedWalkDir");
533  mLuaInterpreter.set_lua_table(tableName, directionList);
534  tableName = QStringLiteral("speedWalkWeight");
535  mLuaInterpreter.set_lua_table(tableName, weightList);
536  return totalWeight;
537 }
538 
540 {
541  // the mapper script reminder is only shown once
542  // because it is too difficult and error prone (->proper script sequence)
543  // to disable this message
545  mHaveMapperScript = true;
546  return ret;
547 }
548 
550 {
551  int totalWeight = assemblePath();
552  Q_UNUSED(totalWeight);
553  QString f = QStringLiteral("doSpeedWalk");
554  QString n = QString();
555  mLuaInterpreter.call(f, n);
556 }
557 
559 {
561 }
562 
564 {
569 }
570 
572 {
577 }
578 
579 QPair<QString, QString> Host::getSearchEngine()
580 {
581  if (mSearchEngineData.contains(mSearchEngineName))
582  return qMakePair(mSearchEngineName, mSearchEngineData.value(mSearchEngineName));
583  else
584  return qMakePair(QStringLiteral("Google"), mSearchEngineData.value(QStringLiteral("Google")));
585 }
586 
587 // cmd is UTF-16BE encoded here, but will be transcoded to Server's one by
588 // cTelnet::sendData(...) call:
589 void Host::send(QString cmd, bool wantPrint, bool dontExpandAliases)
590 {
591  if (wantPrint && mPrintCommand) {
592  mInsertedMissingLF = true;
593  if (!cmd.isEmpty() || !mUSE_IRE_DRIVER_BUGFIX || mUSE_FORCE_LF_AFTER_PROMPT) {
594  // used to print the terminal <LF> that terminates a telnet command
595  // this is important to get the cursor position right
596  mpConsole->printCommand(cmd);
597  }
598  mpConsole->update();
599  }
600  QStringList commandList;
601  if (!mCommandSeparator.isEmpty()) {
602  commandList = cmd.split(QString(mCommandSeparator), QString::SkipEmptyParts);
603  } else {
604  // don't split command if the command separator is blank
605  commandList << cmd;
606  }
607 
608  if (!dontExpandAliases) {
609  // allow sending blank commands
610  if (commandList.empty()) {
611  QString payload(QChar::LineFeed);
612  mTelnet.sendData(payload);
613  return;
614  }
615  }
616 
617  for (int i = 0, total = commandList.size(); i < total; ++i) {
618  if (commandList.at(i).isEmpty()) {
619  continue;
620  }
621  QString command = commandList.at(i);
622  command.remove(QChar::LineFeed);
623  if (dontExpandAliases) {
624  mTelnet.sendData(command);
625  continue;
626  }
627 
628  if (!mAliasUnit.processDataStream(command)) {
629  mTelnet.sendData(command);
630  }
631  }
632 }
633 
635 {
636  int newWatchID = mStopWatchMap.size() + 1;
637  mStopWatchMap[newWatchID] = QTime(0, 0, 0, 0);
638  return newWatchID;
639 }
640 
641 double Host::getStopWatchTime(int watchID)
642 {
643  if (mStopWatchMap.contains(watchID)) {
644  return static_cast<double>(mStopWatchMap[watchID].elapsed()) / 1000;
645  } else {
646  return -1.0;
647  }
648 }
649 
650 bool Host::startStopWatch(int watchID)
651 {
652  if (mStopWatchMap.contains(watchID)) {
653  mStopWatchMap[watchID].start();
654  return true;
655  } else {
656  return false;
657  }
658 }
659 
660 double Host::stopStopWatch(int watchID)
661 {
662  if (mStopWatchMap.contains(watchID)) {
663  return static_cast<double>(mStopWatchMap[watchID].elapsed()) / 1000;
664  } else {
665  return -1.0;
666  }
667 }
668 
669 bool Host::resetStopWatch(int watchID)
670 {
671  if (mStopWatchMap.contains(watchID)) {
672  mStopWatchMap[watchID].setHMS(0, 0, 0, 0);
673  return true;
674  } else {
675  return false;
676  }
677 }
678 
679 void Host::incomingStreamProcessor(const QString& data, int line)
680 {
681  mTriggerUnit.processDataStream(data, line);
682 
684  if (mResetProfile) {
685  resetProfile();
686  }
687 }
688 
689 void Host::registerEventHandler(const QString& name, TScript* pScript)
690 {
691  if (mEventHandlerMap.contains(name)) {
692  if (!mEventHandlerMap[name].contains(pScript)) {
693  mEventHandlerMap[name].append(pScript);
694  }
695  } else {
696  QList<TScript*> scriptList;
697  scriptList.append(pScript);
698  mEventHandlerMap.insert(name, scriptList);
699  }
700 }
702 {
703  if (mAnonymousEventHandlerFunctions.contains(name)) {
704  if (!mAnonymousEventHandlerFunctions[name].contains(fun)) {
705  mAnonymousEventHandlerFunctions[name].push_back(fun);
706  }
707  } else {
708  QStringList newList;
709  newList << fun;
710  mAnonymousEventHandlerFunctions[name] = newList;
711  }
712 }
713 
714 void Host::unregisterEventHandler(const QString& name, TScript* pScript)
715 {
716  if (mEventHandlerMap.contains(name)) {
717  mEventHandlerMap[name].removeAll(pScript);
718  }
719 }
720 
721 void Host::raiseEvent(const TEvent& pE)
722 {
723  if (pE.mArgumentList.isEmpty()) {
724  return;
725  }
726 
727  if (mEventHandlerMap.contains(pE.mArgumentList.at(0))) {
728  QList<TScript*> scriptList = mEventHandlerMap.value(pE.mArgumentList.at(0));
729  for (auto& script : scriptList) {
730  script->callEventHandler(pE);
731  }
732  }
733 
734  if (mAnonymousEventHandlerFunctions.contains(pE.mArgumentList.at(0))) {
735  QStringList functionsList = mAnonymousEventHandlerFunctions.value(pE.mArgumentList.at(0));
736  for (int i = 0, total = functionsList.size(); i < total; ++i) {
737  mLuaInterpreter.callEventHandler(functionsList.at(i), pE);
738  }
739  }
740 }
741 
742 void Host::postIrcMessage(const QString& a, const QString& b, const QString& c)
743 {
744  TEvent event;
745  event.mArgumentList << QLatin1String("sysIrcMessage");
746  event.mArgumentList << a << b << c;
748  raiseEvent(event);
749 }
750 
751 void Host::enableTimer(const QString& name)
752 {
753  mTimerUnit.enableTimer(name);
754 }
755 
756 void Host::disableTimer(const QString& name)
757 {
758  mTimerUnit.disableTimer(name);
759 }
760 
761 bool Host::killTimer(const QString& name)
762 {
763  return mTimerUnit.killTimer(name);
764 }
765 
766 void Host::enableKey(const QString& name)
767 {
768  mKeyUnit.enableKey(name);
769 }
770 
771 void Host::disableKey(const QString& name)
772 {
773  mKeyUnit.disableKey(name);
774 }
775 
776 
777 void Host::enableTrigger(const QString& name)
778 {
780 }
781 
782 void Host::disableTrigger(const QString& name)
783 {
785 }
786 
787 bool Host::killTrigger(const QString& name)
788 {
789  return mTriggerUnit.killTrigger(name);
790 }
791 
792 
794 {
796 }
797 
799 {
800  QMutexLocker locker(&mLock);
801  mIsClosingDown = true;
802 }
803 
805 {
806  QMutexLocker locker(&mLock);
807  return mIsClosingDown;
808 }
809 
810 bool Host::installPackage(const QString& fileName, int module)
811 {
812  // As the pointed to dialog is only used now WITHIN this method and this
813  // method can be re-entered, it is best to use a local rather than a class
814  // pointer just in case we accidentally reenter this method in the future.
815  QDialog* pUnzipDialog = Q_NULLPTR;
816 
817  // Module notes:
818  // For the module install, a module flag of 0 is a package,
819  // a flag of 1 means the module is being installed for the first time via the UI,
820  // a flag of 2 means the module is being synced (so it's "installed" already),
821  // a flag of 3 means the module is being installed from a script.
822  // This separation is necessary to be able to reuse code while avoiding infinite loops from script installations.
823 
824  if (fileName.isEmpty()) {
825  return false;
826  }
827 
828  QFile file(fileName);
829  if (!file.open(QFile::ReadOnly | QFile::Text)) {
830  return false;
831  }
832 
833  QString packageName = fileName.section(QStringLiteral("/"), -1);
834  packageName.remove(QStringLiteral(".trigger"), Qt::CaseInsensitive);
835  packageName.remove(QStringLiteral(".xml"), Qt::CaseInsensitive);
836  packageName.remove(QStringLiteral(".zip"), Qt::CaseInsensitive);
837  packageName.remove(QStringLiteral(".mpackage"), Qt::CaseInsensitive);
838  packageName.remove(QLatin1Char('\\'));
839  packageName.remove(QLatin1Char('.'));
840  if (module) {
841  if ((module == 2) && (mActiveModules.contains(packageName))) {
842  uninstallPackage(packageName, 2);
843  } else if ((module == 3) && (mActiveModules.contains(packageName))) {
844  return false; //we're already installed
845  }
846  } else {
847  if (mInstalledPackages.contains(packageName)) {
848  return false;
849  }
850  }
851  //the extra module check is needed here to prevent infinite loops from script loaded modules
852  if (mpEditorDialog && module != 3) {
854  }
855  QFile file2;
856  if (fileName.endsWith(QStringLiteral(".zip"), Qt::CaseInsensitive) || fileName.endsWith(QStringLiteral(".mpackage"), Qt::CaseInsensitive)) {
859  // home directory for the PROFILE
860  QDir _tmpDir(_home);
861  // directory to store the expanded archive file contents
862  _tmpDir.mkpath(_dest);
863 
864  // TODO: report failure to create destination folder for package/module in profile
865 
866  QUiLoader loader(this);
867  QFile uiFile(QStringLiteral(":/ui/package_manager_unpack.ui"));
868  uiFile.open(QFile::ReadOnly);
869  pUnzipDialog = dynamic_cast<QDialog*>(loader.load(&uiFile, nullptr));
870  uiFile.close();
871  if (!pUnzipDialog) {
872  return false;
873  }
874 
875  auto * pLabel = pUnzipDialog->findChild<QLabel*>(QStringLiteral("label"));
876  if (pLabel) {
877  if (module) {
878  pLabel->setText(tr("Unpacking module:\n\"%1\"\nplease wait...").arg(packageName));
879  } else {
880  pLabel->setText(tr("Unpacking package:\n\"%1\"\nplease wait...").arg(packageName));
881  }
882  }
883  pUnzipDialog->hide(); // Must hide to change WindowModality
884  pUnzipDialog->setWindowTitle(tr("Unpacking"));
885  pUnzipDialog->setWindowModality(Qt::ApplicationModal);
886  pUnzipDialog->show();
887  qApp->processEvents();
888  pUnzipDialog->raise();
889  pUnzipDialog->repaint(); // Force a redraw
890  qApp->processEvents(); // Try to ensure we are on top of any other dialogs and freshly drawn
891 
892  auto successful = mudlet::unzip(fileName, _dest, _tmpDir);
893  pUnzipDialog->deleteLater();
894  pUnzipDialog = Q_NULLPTR;
895  if (!successful) {
896  return false;
897  }
898 
899  // requirements for zip packages:
900  // - packages must be compressed in zip format
901  // - file extension should be .mpackage (though .zip is accepted)
902  // - there can only be a single xml file per package
903  // - 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
904 
905  QDir _dir(_dest);
906  // before we start importing xmls in, see if the config.lua manifest file exists
907  // - if it does, update the packageName from it
908  if (_dir.exists(QStringLiteral("config.lua"))) {
909  // read in the new packageName from Lua. Should be expanded in future to whatever else config.lua will have
910  readPackageConfig(_dir.absoluteFilePath(QStringLiteral("config.lua")), packageName);
911  // now that the packageName changed, redo relevant checks to make sure it's still valid
912  if (module) {
913  if (mActiveModules.contains(packageName)) {
914  uninstallPackage(packageName, 2);
915  }
916  } else {
917  if (mInstalledPackages.contains(packageName)) {
918  // cleanup and quit if already installed
919  removeDir(_dir.absolutePath(), _dir.absolutePath());
920  return false;
921  }
922  }
923  // continuing, so update the folder name on disk
924  QString newpath(QStringLiteral("%1/%2/").arg(_home, packageName));
925  _dir.rename(_dir.absolutePath(), newpath);
926  _dir = QDir(newpath);
927  }
928  QStringList _filterList;
929  _filterList << QStringLiteral("*.xml") << QStringLiteral("*.trigger");
930  QFileInfoList entries = _dir.entryInfoList(_filterList, QDir::Files);
931  for (auto& entry : entries) {
932  file2.setFileName(entry.absoluteFilePath());
933  file2.open(QFile::ReadOnly | QFile::Text);
934  QString profileName = getName();
935  QString login = getLogin();
936  QString pass = getPass();
937  XMLimport reader(this);
938  if (module) {
939  QStringList moduleEntry;
940  moduleEntry << fileName;
941  moduleEntry << QStringLiteral("0");
942  mInstalledModules[packageName] = moduleEntry;
943  mActiveModules.append(packageName);
944  } else {
945  mInstalledPackages.append(packageName);
946  }
947  reader.importPackage(&file2, packageName, module); // TODO: Missing false return value handler
948  setName(profileName);
949  setLogin(login);
950  setPass(pass);
951  file2.close();
952  }
953  } else {
954  file2.setFileName(fileName);
955  file2.open(QFile::ReadOnly | QFile::Text);
956  //mInstalledPackages.append( packageName );
957  QString profileName = getName();
958  QString login = getLogin();
959  QString pass = getPass();
960  XMLimport reader(this);
961  if (module) {
962  QStringList moduleEntry;
963  moduleEntry << fileName;
964  moduleEntry << QStringLiteral("0");
965  mInstalledModules[packageName] = moduleEntry;
966  mActiveModules.append(packageName);
967  } else {
968  mInstalledPackages.append(packageName);
969  }
970  reader.importPackage(&file2, packageName, module); // TODO: Missing false return value handler
971  setName(profileName);
972  setLogin(login);
973  setPass(pass);
974  file2.close();
975  }
976  if (mpEditorDialog) {
978  }
979  if (!module) {
980  saveProfile();
981  }
982  // reorder permanent and temporary triggers: perm first, temp second
984 
985  // make any fonts in the package available to Mudlet for use
986  if (module != 2) {
987  installPackageFonts(packageName);
988  }
989 
990  // raise 2 events - a generic one and a more detailed one to serve both
991  // a simple need ("I just want the install event") and a more specific need
992  // ("I specifically need to know when the module was synced")
993  TEvent genericInstallEvent;
994  genericInstallEvent.mArgumentList.append(QLatin1String("sysInstall"));
995  genericInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
996  genericInstallEvent.mArgumentList.append(packageName);
997  genericInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
998  raiseEvent(genericInstallEvent);
999 
1000  TEvent detailedInstallEvent;
1001  switch (module) {
1002  case 0:
1003  detailedInstallEvent.mArgumentList.append(QLatin1String("sysInstallPackage"));
1004  break;
1005  case 1:
1006  detailedInstallEvent.mArgumentList.append(QLatin1String("sysInstallModule"));
1007  break;
1008  case 2:
1009  detailedInstallEvent.mArgumentList.append(QLatin1String("sysSyncInstallModule"));
1010  break;
1011  case 3:
1012  detailedInstallEvent.mArgumentList.append(QLatin1String("sysLuaInstallModule"));
1013  break;
1014  default:
1015  Q_UNREACHABLE();
1016  }
1017  detailedInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
1018  detailedInstallEvent.mArgumentList.append(packageName);
1019  detailedInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
1020  detailedInstallEvent.mArgumentList.append(fileName);
1021  detailedInstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
1022  raiseEvent(detailedInstallEvent);
1023 
1024  return true;
1025 }
1026 
1027 // credit: http://john.nachtimwald.com/2010/06/08/qt-remove-directory-and-its-contents/
1028 bool Host::removeDir(const QString& dirName, const QString& originalPath)
1029 {
1030  bool result = true;
1031  QDir dir(dirName);
1032  if (dir.exists(dirName)) {
1033  Q_FOREACH (QFileInfo info, dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) {
1034  // prevent recursion outside of the original branch
1035  if (info.isDir() && info.absoluteFilePath().startsWith(originalPath)) {
1036  result = removeDir(info.absoluteFilePath(), originalPath);
1037  } else {
1038  result = QFile::remove(info.absoluteFilePath());
1039  }
1040 
1041  if (!result) {
1042  return result;
1043  }
1044  }
1045  result = dir.rmdir(dirName);
1046  }
1047 
1048  return result;
1049 }
1050 
1051 // This may be called by installPackage(...) in that case however it will have
1052 // module == 2 and in THAT situation it will NOT RE-invoke installPackage(...)
1053 // again - Slysven
1054 bool Host::uninstallPackage(const QString& packageName, int module)
1055 {
1056  // As with the installPackage, the module codes are:
1057  // 0=package, 1=uninstall from dialog, 2=uninstall due to module syncing,
1058  // 3=uninstall from a script
1059 
1060  if (module) {
1061  if (!mInstalledModules.contains(packageName)) {
1062  return false;
1063  }
1064  } else {
1065  if (!mInstalledPackages.contains(packageName)) {
1066  return false;
1067  }
1068  }
1069 
1070  // raise 2 events - a generic one and a more detailed one to serve both
1071  // a simple need ("I just want the uninstall event") and a more specific need
1072  // ("I specifically need to know when the module was uninstalled via Lua")
1073  TEvent genericUninstallEvent;
1074  genericUninstallEvent.mArgumentList.append(QLatin1String("sysUninstall"));
1075  genericUninstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
1076  genericUninstallEvent.mArgumentList.append(packageName);
1077  genericUninstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
1078  raiseEvent(genericUninstallEvent);
1079 
1080  TEvent detailedUninstallEvent;
1081  switch (module) {
1082  case 0:
1083  detailedUninstallEvent.mArgumentList.append(QLatin1String("sysUninstallPackage"));
1084  break;
1085  case 1:
1086  detailedUninstallEvent.mArgumentList.append(QLatin1String("sysUninstallModule"));
1087  break;
1088  case 2:
1089  detailedUninstallEvent.mArgumentList.append(QLatin1String("sysSyncUninstallModule"));
1090  break;
1091  case 3:
1092  detailedUninstallEvent.mArgumentList.append(QLatin1String("sysLuaUninstallModule"));
1093  break;
1094  default:
1095  Q_UNREACHABLE();
1096  }
1097  detailedUninstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
1098  detailedUninstallEvent.mArgumentList.append(packageName);
1099  detailedUninstallEvent.mArgumentTypeList.append(ARGUMENT_TYPE_STRING);
1100  raiseEvent(detailedUninstallEvent);
1101 
1102  int dualInstallations = 0;
1103  if (mInstalledModules.contains(packageName) && mInstalledPackages.contains(packageName)) {
1104  dualInstallations = 1;
1105  }
1106  //we check for the module=3 because if we reset the editor, we will re-execute the
1107  //module uninstall, thus creating an infinite loop.
1108  if (mpEditorDialog && module != 3) {
1110  }
1111  mTriggerUnit.uninstall(packageName);
1112  mTimerUnit.uninstall(packageName);
1113  mAliasUnit.uninstall(packageName);
1114  mActionUnit.uninstall(packageName);
1115  mScriptUnit.uninstall(packageName);
1116  mKeyUnit.uninstall(packageName);
1117  if (module) {
1118  //if module == 2, this is a temporary uninstall for reloading so we exit here
1119  QStringList entry = mInstalledModules[packageName];
1120  mInstalledModules.remove(packageName);
1121  mActiveModules.removeAll(packageName);
1122  if (module == 2) {
1123  return true;
1124  }
1125  //if module == 1/3, we actually uninstall it.
1126  //reinstall the package if it shared a module name. This is a kludge, but it's cleaner than adding extra arguments/etc imo
1127  if (dualInstallations) {
1128  //we're a dual install, reinstalling package
1129  mInstalledPackages.removeAll(packageName); //so we don't get denied from installPackage
1130  //get the pre package list so we don't get duplicates
1131  installPackage(entry[0], 0);
1132  }
1133  } else {
1134  mInstalledPackages.removeAll(packageName);
1135  if (dualInstallations) {
1136  QStringList entry = mInstalledModules[packageName];
1137  installPackage(entry[0], 1);
1138  //restore the module edit flag
1139  mInstalledModules[packageName] = entry;
1140  }
1141  }
1142  if (mpEditorDialog && module != 3) {
1144  }
1145 
1147 
1149  removeDir(dest, dest);
1150  saveProfile();
1151  //NOW we reset if we're uninstalling a module
1152  if (mpEditorDialog && module == 3) {
1154  }
1155  return true;
1156 }
1157 
1158 void Host::readPackageConfig(const QString& luaConfig, QString& packageName)
1159 {
1160  QFile configFile(luaConfig);
1161  QStringList strings;
1162  if (configFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
1163  QTextStream in(&configFile);
1164  while (!in.atEnd()) {
1165  strings += in.readLine();
1166  }
1167  }
1168 
1169  lua_State* L = luaL_newstate();
1170  luaL_openlibs(L);
1171 
1172  int error = luaL_loadstring(L, strings.join("\n").toUtf8().constData());
1173 
1174  if (!error) {
1175  error = lua_pcall(L, 0, 0, 0);
1176  }
1177 
1178  if (!error) {
1179  // for now, only read the mpackage parameter
1180  // would be nice to read author, save & version too later
1181  lua_getglobal(L, "mpackage");
1182  if (lua_isstring(L, -1)) {
1183  packageName = QString(lua_tostring(L, -1));
1184  }
1185  lua_pop(L, -1);
1186  lua_close(L);
1187  return;
1188  } else {
1189  // error
1190  std::string e = "no error message available from Lua";
1191  e = lua_tostring(L, -1);
1192  std::string reason;
1193  switch (error) {
1194  case 4:
1195  reason = "Out of memory";
1196  break;
1197  case 3:
1198  reason = "Syntax error";
1199  break;
1200  case 2:
1201  reason = "Runtime error";
1202  break;
1203  case 1:
1204  reason = "Yield error";
1205  break;
1206  default:
1207  reason = "Unknown error";
1208  break;
1209  }
1210 
1211  if (mudlet::debugMode) {
1212  qDebug() << reason.c_str() << " in config.lua:" << e.c_str();
1213  }
1214  // should print error to main display
1215  QString msg = QString("%1 in config.lua: %2\n").arg(reason.c_str(), e.c_str());
1216  mpConsole->printSystemMessage(msg);
1217 
1218 
1219  lua_pop(L, -1);
1220  lua_close(L);
1221  }
1222 }
1223 
1224 // Derived from the one in dlgConnectionProfile class - but it does not need a
1225 // host name argument...
1226 QPair<bool, QString> Host::writeProfileData(const QString& item, const QString& what)
1227 {
1229  if (file.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) {
1230  QDataStream ofs(&file);
1231  ofs << what;
1232  file.close();
1233  }
1234 
1235  if (file.error() == QFile::NoError) {
1236  return qMakePair(true, QString());
1237  } else {
1238  return qMakePair(false, file.errorString());
1239  }
1240 }
1241 
1242 // Similar to the above, a convenience for reading profile data for this host.
1244 {
1246  bool success = file.open(QIODevice::ReadOnly);
1247  QString ret;
1248  if (success) {
1249  QDataStream ifs(&file);
1250  ifs >> ret;
1251  file.close();
1252  }
1253 
1254  return ret;
1255 }
1256 
1257 // makes fonts in a given package/module be available for Mudlet scripting
1258 // does not install font system-wide
1259 void Host::installPackageFonts(const QString &packageName)
1260 {
1261  auto packagePath = mudlet::getMudletPath(mudlet::profilePackagePath, getName(), packageName);
1262 
1263  QDirIterator it(packagePath, QDirIterator::Subdirectories);
1264  while (it.hasNext()) {
1265  auto filePath = it.next();
1266 
1267  if (filePath.endsWith(QLatin1String(".otf"), Qt::CaseInsensitive) || filePath.endsWith(QLatin1String(".ttf"), Qt::CaseInsensitive) ||
1268  filePath.endsWith(QLatin1String(".ttc"), Qt::CaseInsensitive) || filePath.endsWith(QLatin1String(".otc"), Qt::CaseInsensitive)) {
1269 
1270  mudlet::self()->mFontManager.loadFont(filePath);
1271  }
1272  }
1273 }
1274 
1275 // ensures fonts from all installed packages are loaded in Mudlet
1277 {
1278  for (const auto& package : mInstalledPackages) {
1279  installPackageFonts(package);
1280  }
1281 }
1282 
1283 void Host::setWideAmbiguousEAsianGlyphs(const Qt::CheckState state)
1284 {
1285  bool localState = false;
1286  bool needToEmit = false;
1287  const QString encoding(mTelnet.getEncoding());
1288 
1289  QMutexLocker locker(& mLock);
1290  if (state == Qt::PartiallyChecked) {
1291  // Set things automatically
1293 
1294  if ( encoding == QLatin1String("GBK")
1295  || encoding == QLatin1String("GB18030")
1296  || encoding == QLatin1String("Big5")) {
1297 
1298  // Need to use wide width for ambiguous characters
1299  if (!mWideAmbigousWidthGlyphs) {
1300  // But the last setting was narrow - so we need to change
1301  mWideAmbigousWidthGlyphs = true;
1302  localState = true;
1303  needToEmit = true;
1304  }
1305 
1306  } else {
1307  // Need to use narrow width for ambiguous characters
1309  // But the last setting was wide - so we need to change
1310  mWideAmbigousWidthGlyphs = false;
1311  localState = false;
1312  needToEmit = true;
1313  }
1314 
1315  }
1316 
1317  } else {
1318  // Set things manually:
1320  if (mWideAmbigousWidthGlyphs != (state == Qt::Checked)) {
1321  // The last setting is the opposite to what we want:
1322 
1323  mWideAmbigousWidthGlyphs = (state == Qt::Checked);
1324  localState = (state == Qt::Checked);
1325  needToEmit = true;
1326  };
1327 
1328  }
1329 
1330  locker.unlock();
1331  // We do not need to keep the mutex any longer as we have a local copy to
1332  // work with whilst the connected methods react to the signal:
1333  if (needToEmit) {
1335  }
1336 }
1337 
1338 // handles out of band (OOB) GMCP/MSDP data for Discord - called whenever GMCP
1339 // Telnet sub-option comes in and starts with "External.Discord.(Status|Info)"
1340 void Host::processDiscordGMCP(const QString& packageMessage, const QString& data)
1341 {
1343  return;
1344  }
1345 
1346  auto document = QJsonDocument::fromJson(data.toUtf8());
1347  if (!document.isObject()) {
1348  return;
1349  }
1350 
1351  auto json = document.object();
1352  if (json.isEmpty()) {
1353  return;
1354  }
1355 
1356  if (packageMessage == QLatin1String("External.Discord.Status")) {
1358  } else if (packageMessage == QLatin1String("External.Discord.Info")) {
1359  processGMCPDiscordInfo(json);
1360  }
1361 }
1362 
1363 void Host::processGMCPDiscordInfo(const QJsonObject& discordInfo)
1364 {
1365  mudlet* pMudlet = mudlet::self();
1366  bool hasInvite = false;
1367  auto inviteUrl = discordInfo.value(QStringLiteral("inviteurl"));
1368  // Will be of form: "https://discord.gg/#####"
1369  if (inviteUrl != QJsonValue::Undefined) {
1370  hasInvite = true;
1371  }
1372 
1373  bool hasApplicationId = false;
1374  bool hasCustomAppID = false;
1375  auto appID = discordInfo.value(QStringLiteral("applicationid"));
1376  if (appID != QJsonValue::Undefined) {
1377  hasApplicationId = true;
1378  if (appID.toString() == Discord::mMudletApplicationId) {
1379  pMudlet->mDiscord.setApplicationID(this, QString());
1380  } else {
1381  hasCustomAppID = true;
1382  pMudlet->mDiscord.setApplicationID(this, appID.toString());
1383  auto image = pMudlet->mDiscord.getLargeImage(this);
1384 
1385  if (image.isEmpty() || image == QLatin1String("mudlet")) {
1386  pMudlet->mDiscord.setLargeImage(this, QStringLiteral("server-icon"));
1387  }
1388  }
1389  }
1390 
1391  if (hasInvite) {
1392  if (hasCustomAppID) {
1393  qDebug() << "Game using a custom Discord server. Invite URL: " << inviteUrl.toString();
1394  } else if (hasApplicationId) {
1395  qDebug() << "Game using Mudlets Discord server. Invite URL: " << inviteUrl.toString();
1396  } else {
1397  qDebug() << "Discord invite URL: " << inviteUrl.toString();
1398  }
1399  } else {
1400  if (hasCustomAppID) {
1401  qDebug() << "Game is using custom server Discord application ID";
1402  } else if (hasApplicationId) {
1403  qDebug() << "Game is using Mudlets Discord application ID";
1404  }
1405  }
1406 }
1407 
1408 void Host::processGMCPDiscordStatus(const QJsonObject& discordInfo)
1409 {
1410  auto pMudlet = mudlet::self();
1411  auto gameName = discordInfo.value(QStringLiteral("game"));
1412  if (gameName != QJsonValue::Undefined) {
1413  QPair<bool, QString> richPresenceSupported = pMudlet->mDiscord.gameIntegrationSupported(getUrl());
1414  if (richPresenceSupported.first && pMudlet->mDiscord.usingMudletsDiscordID(this)) {
1415  pMudlet->mDiscord.setDetailText(this, tr("Playing %1").arg(richPresenceSupported.second));
1416  pMudlet->mDiscord.setLargeImage(this, richPresenceSupported.second);
1417  pMudlet->mDiscord.setLargeImageText(this, tr("%1 at %2:%3", "%1 is the game name and %2:%3 is game server address like: mudlet.org:23").arg(gameName.toString(), getUrl(), QString::number(getPort())));
1418  } else {
1419  // We are using a custom application id, so the top line is
1420  // likely to be saying "Playing MudName"
1421  if (richPresenceSupported.first) {
1422  pMudlet->mDiscord.setDetailText(this, QString());
1423  pMudlet->mDiscord.setLargeImageText(this, tr("%1 at %2:%3", "%1 is the game name and %2:%3 is game server address like: mudlet.org:23").arg(gameName.toString(), getUrl(), QString::number(getPort())));
1424  pMudlet->mDiscord.setLargeImage(this, QStringLiteral("server-icon"));
1425  }
1426  }
1427  }
1428 
1429  auto details = discordInfo.value(QStringLiteral("details"));
1430  if (details != QJsonValue::Undefined) {
1431  pMudlet->mDiscord.setDetailText(this, details.toString());
1432  }
1433 
1434  auto state = discordInfo.value(QStringLiteral("state"));
1435  if (state != QJsonValue::Undefined) {
1436  pMudlet->mDiscord.setStateText(this, state.toString());
1437  }
1438 
1439  auto largeImages = discordInfo.value(QStringLiteral("largeimage"));
1440  if (largeImages != QJsonValue::Undefined) {
1441  auto largeImage = largeImages.toArray().first();
1442  if (largeImage != QJsonValue::Undefined) {
1443  pMudlet->mDiscord.setSmallImage(this, largeImage.toString());
1444  }
1445  }
1446 
1447  auto largeImageText = discordInfo.value(QStringLiteral("largeimagetext"));
1448  if (largeImageText != QJsonValue::Undefined) {
1449  pMudlet->mDiscord.setSmallImageText(this, largeImageText.toString());
1450  }
1451 
1452  auto smallImages = discordInfo.value(QStringLiteral("smallimage"));
1453  if (smallImages != QJsonValue::Undefined) {
1454  auto smallImage = smallImages.toArray().first();
1455  if (smallImage != QJsonValue::Undefined) {
1456  pMudlet->mDiscord.setSmallImage(this, smallImage.toString());
1457  }
1458  }
1459 
1460  auto smallImageText = discordInfo.value(QStringLiteral("smallimagetext"));
1461  if ((smallImageText != QJsonValue::Undefined)) {
1462  pMudlet->mDiscord.setSmallImageText(this, smallImageText.toString());
1463  }
1464 
1465  // Use -1 so we can detect (at least during debugging) that a value of 0
1466  // has been seen:
1467  int64_t timeStamp = -1;
1468  auto endTimeStamp = discordInfo.value(QStringLiteral("endtime"));
1469  if (endTimeStamp.isDouble()) {
1470  // It is not entirely clear from the proposed specification
1471  // whether the integral seconds since epoch is a string or a
1472  // double, so handle both:
1473  // This only works properly when the value is less than
1474  // 9007199254740992 but since when I last checked it was
1475  // 1533042027 second since beginning of 1970 it should be
1476  // good enough!
1477  timeStamp = static_cast<int64_t>(endTimeStamp.toDouble());
1478  pMudlet->mDiscord.setEndTimeStamp(this, timeStamp);
1479  } else if (endTimeStamp.isString()) {
1480  timeStamp = endTimeStamp.toString().toLongLong();
1481  pMudlet->mDiscord.setEndTimeStamp(this, timeStamp);
1482  } else {
1483  auto startTimeStamp = discordInfo.value(QStringLiteral("starttime"));
1484  if (startTimeStamp.isDouble()) {
1485  timeStamp = static_cast<int64_t>(startTimeStamp.toDouble());
1486  pMudlet->mDiscord.setStartTimeStamp(this, timeStamp);
1487  } else if (endTimeStamp.isString()) {
1488  timeStamp = endTimeStamp.toString().toLongLong();
1489  pMudlet->mDiscord.setStartTimeStamp(this, timeStamp);
1490  }
1491  }
1492 
1493  // Use -1 so we can detect (at least during debugging) that a value of 0
1494  // has been seen:
1495  int partySizeValue = -1;
1496  int partyMaxValue = -1;
1497  auto partyMax = discordInfo.value(QStringLiteral("partymax"));
1498  auto partySize = discordInfo.value(QStringLiteral("partysize"));
1499  if (partyMax.isDouble()) {
1500  partyMaxValue = static_cast<int>(partyMax.toDouble());
1501  if (partyMaxValue > 0 && partySize.isDouble()) {
1502  partySizeValue = static_cast<int>(partySize.toDouble());
1503  pMudlet->mDiscord.setParty(this, partySizeValue, partyMaxValue);
1504  } else {
1505  // Switches off the party detail from the RP
1506  pMudlet->mDiscord.setParty(this, 0, 0);
1507  }
1508  } else {
1509  if (partySize.isDouble()) {
1510  partySizeValue = static_cast<int>(partySize.toDouble());
1511  pMudlet->mDiscord.setParty(this, partySizeValue);
1512  } else {
1513  pMudlet->mDiscord.setParty(this, 0, 0);
1514  }
1515  }
1516 }
1517 
1519 {
1520  mudlet* pMudlet = mudlet::self();
1521  pMudlet->mDiscord.setDetailText(this, QString());
1522  pMudlet->mDiscord.setStateText(this, QString());
1523  pMudlet->mDiscord.setLargeImage(this, QString());
1524  pMudlet->mDiscord.setLargeImageText(this, QString());
1525  pMudlet->mDiscord.setSmallImage(this, QString());
1526  pMudlet->mDiscord.setSmallImageText(this, QString());
1527  pMudlet->mDiscord.setStartTimeStamp(this, 0);
1528  pMudlet->mDiscord.setParty(this, 0, 0);
1529 }
1530 
1531 
1532 void Host::processDiscordMSDP(const QString& variable, QString value)
1533 {
1535  return;
1536  }
1537 
1538  Q_UNUSED(variable)
1539  Q_UNUSED(value)
1540 // TODO:
1541 // if (!(variable == QLatin1String("SERVER_ID") || variable == QLatin1String("AREA_NAME"))) {
1542 // return;
1543 // }
1544 
1545 // // MSDP value comes padded with quotes - strip them (from the local copy of
1546 // // the supplied argument):
1547 // if (value.startsWith(QLatin1String("\""))) {
1548 // value = value.mid(1);
1549 // }
1550 
1551 // if (value.endsWith(QLatin1String("\""))) {
1552 // value.chop(1);
1553 // }
1554 
1555 // if (variable == QLatin1String("SERVER_ID")) {
1556 // mudlet::self()->mDiscord.setGame(this, value);
1557 // } else if (variable == QLatin1String("AREA_NAME")) {
1558 // mudlet::self()->mDiscord.setArea(this, value);
1559 // }
1560 }
1561 
1563 {
1564  QMutexLocker locker(& mLock);
1566  locker.unlock();
1567 
1568  writeProfileData(QStringLiteral("discordApplicationId"), s);
1569 }
1570 
1572 {
1573  QMutexLocker locker(&mLock);
1574  return mDiscordApplicationID;
1575 }
1576 
1577 // Compares the current discord username and discriminator against the non-empty
1578 // arguments. Returns true if neither match, otherwise false.
1579 bool Host::discordUserIdMatch(const QString& userName, const QString& userDiscriminator) const
1580 {
1581  if (!userName.isEmpty() && !mRequiredDiscordUserName.isEmpty() && userName != mRequiredDiscordUserName) {
1582  return false;
1583  }
1584 
1585  if (!userDiscriminator.isEmpty() && !mRequiredDiscordUserDiscriminator.isEmpty() && userDiscriminator != mRequiredDiscordUserDiscriminator) {
1586  return false;
1587  } else {
1588  return true;
1589  }
1590 }
Host(int port, const QString &mHostName, const QString &login, const QString &pass, int host_id)
Definition: Host.cpp:44
QTextStream mErrorLogStream
Definition: Host.h:231
void setDetailText(Host *, const QString &)
Definition: discord.cpp:129
QHash< QString, XMLexport * > writers
Definition: Host.h:464
void resetProfile()
Definition: Host.cpp:365
void disableKey(const QString &)
Definition: Host.cpp:771
ActionUnit * getActionUnit()
Definition: Host.h:125
const QString & getEncoding() const
Definition: ctelnet.h:145
QString mRequiredDiscordUserDiscriminator
Definition: Host.h:475
ActionUnit mActionUnit
Definition: Host.h:408
void compileAll()
bool killTimer(const QString &)
Definition: Host.cpp:761
QMap< QString, QStringList > modulesToWrite
Definition: Host.h:354
void connectToServer()
Definition: Host.cpp:793
dlgTriggerEditor * mpEditorDialog
Definition: Host.h:243
QScopedPointer< LuaInterface > mLuaInterface
Definition: Host.h:402
void setStateText(Host *, const QString &)
Definition: discord.cpp:142
cTelnet mTelnet
Definition: Host.h:212
bool mUSE_IRE_DRIVER_BUGFIX
Definition: Host.h:284
void incomingStreamProcessor(const QString &paragraph, int line)
Definition: Host.cpp:679
QString getName()
Definition: Host.h:90
QStringList mActiveModules
Definition: Host.h:443
void registerEventHandler(const QString &, TScript *)
Definition: Host.cpp:689
ScriptUnit mScriptUnit
Definition: Host.h:406
void compileAll()
Definition: AliasUnit.cpp:55
void uninstall(QString)
Definition: TimerUnit.cpp:41
bool currentlySavingProfile()
Definition: Host.cpp:472
Definition: mudlet.h:85
void clearDiscordData()
Definition: Host.cpp:1518
void enableTimer(const QString &)
Definition: Host.cpp:751
void uninstall(QString)
Definition: ScriptUnit.cpp:41
bool mInsertedMissingLF
Definition: Host.h:236
int getPort()
Definition: Host.h:96
void processDiscordMSDP(const QString &variable, QString value)
Definition: Host.cpp:1532
void slot_reloadModules()
Definition: Host.cpp:266
Definition: Host.h:62
void setPass(const QString &s)
Definition: Host.h:101
void stopAllTriggers()
Definition: KeyUnit.cpp:99
QString mUrl
Definition: Host.h:281
QString readProfileData(const QString &)
Definition: Host.cpp:1243
void reenableAllTriggers()
Definition: Host.cpp:571
void send(QString cmd, bool wantPrint=true, bool dontExpandAliases=false)
Definition: Host.cpp:589
QStringList mModulesToSync
Definition: Host.h:400
bool sendData(QString &data)
double stopStopWatch(int)
Definition: Host.cpp:660
void enableTrigger(const QString &)
Definition: Host.cpp:777
void reenableAllTriggers()
Definition: AliasUnit.cpp:254
FontManager mFontManager
Definition: mudlet.h:97
bool call(const QString &function, const QString &mName, const bool muteDebugOutput=false)
QMap< QString, QStringList > mAnonymousEventHandlerFunctions
Definition: Host.h:441
QList< int > mArgumentTypeList
Definition: TEvent.h:45
void uninstall(const QString &)
Definition: TriggerUnit.cpp:58
bool mIsClosingDown
Definition: Host.h:422
QStringList mGMCP_merge_table_keys
Definition: Host.h:347
void uninstall(const QString &)
Definition: KeyUnit.cpp:60
void removeAllTempKeys()
Definition: KeyUnit.cpp:174
const QString & getDiscordApplicationID()
Definition: Host.cpp:1571
#define ARGUMENT_TYPE_STRING
Definition: TEvent.h:35
void doCleanup()
Definition: KeyUnit.cpp:445
void stopAllTriggers()
Definition: TimerUnit.cpp:55
void reenableAllTriggers()
Definition: TimerUnit.cpp:71
void setDisplayDimensions()
Definition: ctelnet.cpp:511
bool callEventHandler(const QString &function, const TEvent &pE, const QEvent *qE=nullptr)
void reenableAllTriggers()
QFile mErrorLogFile
Definition: Host.h:415
bool mPrintCommand
Definition: Host.h:247
QString mCommandSeparator
Definition: Host.h:227
bool mWideAmbigousWidthGlyphs
Definition: Host.h:461
void updateToolbar()
Definition: ActionUnit.cpp:542
bool isClosingDown()
Definition: Host.cpp:804
static QString userName(const QString &name, const QStringList &prefixes)
Definition: ircchannel.cpp:81
bool mHaveMapperScript
Definition: Host.h:451
bool uninstallPackage(const QString &, int)
Definition: Host.cpp:1054
QMap< QString, int > mModulePriorities
Definition: Host.h:353
bool resetStopWatch(int)
Definition: Host.cpp:669
int mPort
Definition: Host.h:432
void setSmallImage(Host *, const QString &)
Definition: discord.cpp:166
void processDataStream(const QString &, int)
ScriptUnit * getScriptUnit()
Definition: Host.h:127
void closingDown()
Definition: Host.cpp:798
bool mDiscordDisableServerSide
Definition: Host.h:358
QString getMmpMapLocation() const
Definition: Host.cpp:509
void reloadModule(const QString &reloadModuleName)
Definition: Host.cpp:341
void adjustNAWS()
Definition: Host.cpp:558
void registerAnonymousEventHandler(const QString &name, const QString &fun)
Definition: Host.cpp:701
AliasUnit * getAliasUnit()
Definition: Host.h:124
TimerUnit mTimerUnit
Definition: Host.h:405
TriggerUnit mTriggerUnit
Definition: Host.h:404
static QString getMudletPath(mudletPathType, const QString &extra1=QString(), const QString &extra2=QString())
Definition: mudlet.cpp:3829
int createStopWatch()
Definition: Host.cpp:634
void setLargeImageText(Host *, const QString &)
Definition: discord.cpp:158
KeyUnit * getKeyUnit()
Definition: Host.h:126
static int zip_close(lua_State *L)
Definition: luazip.h:138
std::tuple< bool, QString, QString > saveProfile(const QString &saveLocation=QString(), const QString &saveName=QString(), bool syncModules=false)
Definition: Host.cpp:407
QString getLargeImage(Host *pHost) const
Definition: discord.h:193
bool mResetProfile
Definition: Host.h:275
void xmlSaved(const QString &xmlName)
Definition: Host.cpp:459
TLuaInterpreter mLuaInterpreter
Definition: Host.h:214
void installPackageFonts(const QString &packageName)
Definition: Host.cpp:1259
QString & getPass()
Definition: Host.h:100
bool disableTrigger(const QString &)
QSet< QChar > mDoubleClickIgnore
Definition: Host.h:377
static bool unzip(const QString &archivePath, const QString &destination, const QDir &tmpDir)
Definition: mudlet.cpp:3581
QMap< Host *, TConsole * > mConsoleMap
Definition: mudlet.h:178
bool setApplicationID(Host *, const QString &)
Definition: discord.cpp:529
bool mIsGoingDown
Definition: Host.h:237
bool killTrigger(const QString &name)
QMap< QString, QString > mSearchEngineData
Definition: Host.h:295
void removeAllTempTimers()
Definition: TimerUnit.cpp:134
void connectIt(const QString &address, int port)
KeyUnit mKeyUnit
Definition: Host.h:409
bool installPackage(const QString &, int)
Definition: Host.cpp:810
bool disableKey(const QString &name)
Definition: KeyUnit.cpp:140
friend class XMLexport
Definition: Host.h:66
static const QString mMudletApplicationId
Definition: discord.h:207
bool enableTrigger(const QString &)
QString & getLogin()
Definition: Host.h:98
bool startStopWatch(int)
Definition: Host.cpp:650
QListWidget * moduleList
Definition: Host.h:447
void setSmallImageText(Host *, const QString &)
Definition: discord.cpp:174
void raiseEvent(const TEvent &event)
Definition: Host.cpp:721
bool mBlockScriptCompile
Definition: Host.h:220
bool mAutoAmbigousWidthGlyphsSetting
Definition: Host.h:457
void stopAllTriggers()
Definition: Host.cpp:563
void startSpeedWalk()
Definition: Host.cpp:549
void setName(const QString &s)
Definition: Host.h:91
void stopAllTriggers()
void compileAll()
Definition: ScriptUnit.cpp:204
Definition: TMap.h:79
void unregisterEventHandler(const QString &, TScript *)
Definition: Host.cpp:714
static mudlet * self()
Definition: mudlet.cpp:126
AliasUnit mAliasUnit
Definition: Host.h:407
void refreshPackageFonts()
Definition: Host.cpp:1276
void reorderTriggersAfterPackageImport()
QString mDiscordApplicationID
Definition: Host.h:467
void disconnect()
void enableKey(const QString &)
Definition: Host.cpp:766
QMap< QString, QList< TScript * > > mEventHandlerMap
Definition: Host.h:232
Q_DECLARE_TR_FUNCTIONS(XMLimport) public bool importPackage(QFile *, QString packageName=QString(), int moduleFlag=0, QString *pVersionString=Q_NULLPTR)
Definition: XMLimport.cpp:63
void profileSaveFinished()
void processGMCPDiscordInfo(const QJsonObject &discordInfo)
Definition: Host.cpp:1363
void signal_changeIsAmbigousWidthGlyphsToBeWide(bool)
bool killTrigger(const QString &)
Definition: Host.cpp:787
const bool checkForMappingScript()
Definition: Host.cpp:539
static bool debugMode
Definition: mudlet.h:177
return false
Definition: ctelnet.cpp:465
void saveModules(int sync, bool backup=true)
Definition: Host.cpp:230
QString getUrl()
Definition: Host.h:92
void setWideAmbiguousEAsianGlyphs(Qt::CheckState state)
Definition: Host.cpp:1283
std::tuple< bool, QString, QString > saveProfileAs(const QString &fileName)
Definition: Host.cpp:444
QString mHostName
Definition: Host.h:420
const unsigned int assemblePath()
Definition: Host.cpp:515
void doCleanup()
Definition: TimerUnit.cpp:336
QScopedPointer< TMap > mpMap
Definition: Host.h:244
bool disableTimer(const QString &)
Definition: TimerUnit.cpp:285
void compileAll()
Definition: ActionUnit.cpp:61
void setStartTimeStamp(Host *, int64_t)
Definition: discord.cpp:182
void set_lua_table(const QString &tableName, QStringList &variableList)
QMap< int, QTime > mStopWatchMap
Definition: Host.h:439
bool removeDir(const QString &, const QString &)
Definition: Host.cpp:1028
void disableTimer(const QString &)
Definition: Host.cpp:756
bool processDataStream(const QString &)
Definition: AliasUnit.cpp:227
void postIrcMessage(const QString &, const QString &, const QString &)
Definition: Host.cpp:742
double getStopWatchTime(int)
Definition: Host.cpp:641
QStringList mInstalledPackages
Definition: Host.h:351
void uninstall(const QString &)
Definition: ActionUnit.cpp:47
Definition: TEvent.h:41
void removeAllTempTriggers()
Definition: TriggerUnit.cpp:72
QPointer< TConsole > mpConsole
Definition: Host.h:213
void stopAllTriggers()
Definition: AliasUnit.cpp:247
QMap< QTimer *, TTimer * > mTimerMap
Definition: mudlet.h:221
QMap< QString, TEvent * > mEventMap
Definition: Host.h:417
QStringList mArgumentList
Definition: TEvent.h:44
void doCleanup()
void waitForProfileSave()
Definition: Host.cpp:477
void processGMCPDiscordStatus(const QJsonObject &discordInfo)
Definition: Host.cpp:1408
void setLogin(const QString &s)
Definition: Host.h:99
QPair< QString, QString > getSearchEngine()
Definition: Host.cpp:579
QMutex mLock
Definition: Host.h:425
bool killTimer(const QString &name)
Definition: TimerUnit.cpp:315
void reenableAllTriggers()
Definition: KeyUnit.cpp:106
QMap< QString, QStringList > mInstalledModules
Definition: Host.h:352
void updateModuleZips() const
Definition: Host.cpp:307
QPair< bool, QString > writeProfileData(const QString &, const QString &)
Definition: Host.cpp:1226
QString mSearchEngineName
Definition: Host.h:296
QString mRequiredDiscordUserName
Definition: Host.h:474
bool mUSE_FORCE_LF_AFTER_PROMPT
Definition: Host.h:283
void setDiscordApplicationID(const QString &s)
Definition: Host.cpp:1562
TimerUnit * getTimerUnit()
Definition: Host.h:123
void loadFont(const QString &filePath)
Definition: FontManager.cpp:66
QPointer< QDockWidget > mpDockableMapWidget
Definition: Host.h:378
TriggerUnit * getTriggerUnit()
Definition: Host.h:122
bool enableKey(const QString &name)
Definition: KeyUnit.cpp:123
Discord mDiscord
Definition: mudlet.h:98
void disableTrigger(const QString &)
Definition: Host.cpp:782
bool enableTimer(const QString &)
Definition: TimerUnit.cpp:246
void setLargeImage(Host *, const QString &)
Definition: discord.cpp:150
bool discordUserIdMatch(const QString &userName, const QString &userDiscriminator) const
Definition: Host.cpp:1579
void profileSaveStarted()
void processDiscordGMCP(const QString &packageMessage, const QString &data)
Definition: Host.cpp:1340
void compileAll()
Definition: KeyUnit.cpp:90
static int zip_open(lua_State *L)
Definition: luazip.h:123
void readPackageConfig(const QString &, QString &)
Definition: Host.cpp:1158
void setParty(Host *, int)
Definition: discord.cpp:200
void setMmpMapLocation(const QString &data)
Definition: Host.cpp:486
~Host()
Definition: Host.cpp:218
void uninstall(const QString &)
Definition: AliasUnit.cpp:41