joincommandlinebuilder.cpp
1 //------------------------------------------------------------------------------
2 // joincommandlinebuilder.cpp
3 //------------------------------------------------------------------------------
4 //
5 // This program is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License
7 // as published by the Free Software Foundation; either version 2
8 // of the License, or (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software
17 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 // 02110-1301, USA.
19 //
20 //------------------------------------------------------------------------------
21 // Copyright (C) 2014 "Zalewa" <zalewapl@gmail.com>
22 //------------------------------------------------------------------------------
23 #include "joincommandlinebuilder.h"
24 
25 #include "apprunner.h"
26 #include "datapaths.h"
27 #include "log.h"
28 #include "configuration/doomseekerconfig.h"
29 #include "gui/passworddlg.h"
30 #include "gui/wadseekerinterface.h"
31 #include "gui/wadseekershow.h"
32 #include "ini/settingsproviderqt.h"
33 #include "plugins/engineplugin.h"
34 #include "serverapi/exefile.h"
35 #include "serverapi/gameclientrunner.h"
36 #include "serverapi/message.h"
37 #include "serverapi/server.h"
38 #include "application.h"
39 #include "gamedemo.h"
40 
41 #include <wadseeker/wadseeker.h>
42 #include <QDialogButtonBox>
43 #include <QGridLayout>
44 #include <QLabel>
45 #include <QListWidget>
46 #include <QMessageBox>
47 #include <cassert>
48 
49 DClass<JoinCommandLineBuilder>
50 {
51  public:
52  CommandLineInfo cli;
53  bool configurationError;
54  QString connectPassword;
55  QString error;
56  GameDemo demo;
57  QString demoName;
58  QString inGamePassword;
59  ServerPtr server;
60  QWidget *parentWidget;
61  bool passwordsAlreadySet;
62  bool requireOptionals;
63 
64  // For missing wads dialog
65  QDialogButtonBox *buttonBox;
66  QDialogButtonBox::StandardButton lastButtonClicked;
67 };
68 
69 DPointered(JoinCommandLineBuilder)
70 
72  GameDemo demo, QWidget *parentWidget)
73 {
74  d->configurationError = false;
75  d->demo = demo;
76  d->demoName = GameDemo::mkDemoFullPath(demo, *server->plugin());
77  d->parentWidget = parentWidget;
78  d->passwordsAlreadySet = false;
79  d->requireOptionals = false;
80  d->server = server;
81 }
82 
83 JoinCommandLineBuilder::~JoinCommandLineBuilder()
84 {
85 }
86 
87 void JoinCommandLineBuilder::allDownloadableWads(const JoinError &joinError, QStringList &required, QStringList &optional)
88 {
89  if (!joinError.missingIwad().isEmpty())
90  {
91  required << joinError.missingIwad();
92  }
93 
94  const QList<PWad> missingWads = joinError.missingWads();
95  foreach(const PWad &wad, missingWads)
96  {
97  if(wad.isOptional())
98  optional.append(wad.name());
99  else
100  required.append(wad.name());
101  }
102  required = Wadseeker::filterAllowedOnlyWads(required);
103  optional = Wadseeker::filterAllowedOnlyWads(optional);
104 }
105 
106 bool JoinCommandLineBuilder::buildServerConnectParams(ServerConnectParams &params)
107 {
108  if (d->server->isLockedAnywhere())
109  {
110  if (!d->passwordsAlreadySet)
111  {
112  PasswordDlg password(d->server);
113  int ret = password.exec();
114  if (ret != QDialog::Accepted)
115  {
116  return false;
117  }
118  d->connectPassword = password.connectPassword();
119  d->inGamePassword = password.inGamePassword();
120  d->passwordsAlreadySet = true;
121  }
122  params.setConnectPassword(d->connectPassword);
123  params.setInGamePassword(d->inGamePassword);
124  }
125 
126  if (!d->demoName.isEmpty())
127  {
128  params.setDemoName(d->demoName);
129  }
130  return true;
131 }
132 
133 const CommandLineInfo &JoinCommandLineBuilder::builtCommandLine() const
134 {
135  return d->cli;
136 }
137 
138 bool JoinCommandLineBuilder::checkServerStatus()
139 {
140  // Remember to check REFRESHING status first!
141  if (d->server->isRefreshing())
142  {
143  d->error = tr("This server is still refreshing.\nPlease wait until it is finished.");
144  gLog << tr("Attempted to obtain a join command line for a \"%1\" "
145  "server that is under refresh.").arg(d->server->addressWithPort());
146  return false;
147  }
148  // Fail if Doomseeker couldn't get data on this server.
149  else if (!d->server->isKnown())
150  {
151  d->error = tr("Data for this server is not available.\nOperation failed.");
152  gLog << tr("Attempted to obtain a join command line for an unknown server \"%1\"").arg(
153  d->server->addressWithPort());
154  return false;
155  }
156  return true;
157 }
158 
159 bool JoinCommandLineBuilder::checkWadseekerValidity(QWidget *parent)
160 {
161  QString targetDirPath = gConfig.wadseeker.targetDirectory;
162  QDir targetDir(targetDirPath);
163  QFileInfo targetDirFileInfo(targetDirPath);
164 
165  if (targetDirPath.isEmpty() || !targetDir.exists() || !targetDirFileInfo.isWritable())
166  {
167  return false;
168  }
169 
170  return true;
171 }
172 
173 int JoinCommandLineBuilder::displayMissingWadsMessage(const QStringList &downloadableWads,
174  QStringList &optionalWads, const QString &message)
175 {
176  const QString CAPTION = tr("Doomseeker - files are missing");
177 
178  // Can't use QMessageBox here because we need to be able to add our
179  // optional wad selection box.
180  QDialog msgBox;
181  msgBox.setWindowTitle(CAPTION);
182  QGridLayout *grid = new QGridLayout;
183  QLabel *mainMessage = new QLabel;
184  grid->addWidget(mainMessage, 0, 1);
185 
186  QListWidget *optionalList = NULL;
187  if(!optionalWads.isEmpty())
188  {
189  optionalList = new QListWidget;
190  optionalList->setMaximumHeight(64);
191  grid->addWidget(optionalList, 1, 1);
192  foreach(const QString &wad, optionalWads)
193  {
194  QListWidgetItem *item = new QListWidgetItem(wad, optionalList);
195  item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
196  item->setCheckState(Qt::Checked);
197  }
198  }
199 
200  QLabel *questionLabel = new QLabel;
201  grid->addWidget(questionLabel, 2, 1);
202 
203  // We'll need to store this in our d-pointer and connect a signal to this
204  // object since a button box doesn't give us an easy way to get the button
205  // clicked to close the dialog.
206  d->buttonBox = new QDialogButtonBox;
207  d->lastButtonClicked = QDialogButtonBox::NoButton;
208  grid->addWidget(d->buttonBox, 3, 0, 1, 2, Qt::AlignRight);
209  msgBox.connect(d->buttonBox, SIGNAL(accepted()), SLOT(accept()));
210  msgBox.connect(d->buttonBox, SIGNAL(rejected()), SLOT(reject()));
211  connect(d->buttonBox, SIGNAL(clicked(QAbstractButton*)), SLOT(missingWadsClicked(QAbstractButton*)));
212 
213  QLabel *icon = new QLabel;
214  QIcon questionIcon = msgBox.style()->standardIcon(QStyle::SP_MessageBoxQuestion);
215  icon->setPixmap(questionIcon.pixmap(questionIcon.actualSize(QSize(64,64))));
216  grid->addWidget(icon, 0, 0, 3, 1, Qt::AlignTop);
217 
218  msgBox.setLayout(grid);
219 
220  QString ignoreMessage;
221  if (d->server->plugin()->data()->inGameFileDownloads)
222  {
223  ignoreMessage = tr("Alternatively use ignore to connect anyways.");
224  d->buttonBox->addButton(QDialogButtonBox::Ignore);
225  }
226 
227  QString questionMessage;
228  if (!downloadableWads.isEmpty() || !optionalWads.isEmpty())
229  {
230  questionLabel->setText(QString("%1\n%2").arg(tr("Do you want Wadseeker to find the missing WADs?")).arg(ignoreMessage));
231  d->buttonBox->addButton(QDialogButtonBox::Yes);
232  d->buttonBox->addButton(QDialogButtonBox::No);
233  mainMessage->setText(QString("%1\n\n%2").arg(message).arg(tr("Following files can be downloaded: %1").arg(downloadableWads.join(", "))));
234 
235  }
236  else
237  {
238  questionLabel->setText(ignoreMessage);
239  d->buttonBox->addButton(QDialogButtonBox::Ok);
240  mainMessage->setText(message);
241  }
242 
243  msgBox.exec();
244 
245  // Clear out unselected optional wads.
246  for(int i = optionalWads.size();i-- > 0;)
247  {
248  if(optionalList->item(i)->checkState() != Qt::Checked)
249  optionalWads.removeAt(i);
250  }
251 
252  return d->lastButtonClicked;
253 }
254 
255 const QString &JoinCommandLineBuilder::error() const
256 {
257  return d->error;
258 }
259 
260 void JoinCommandLineBuilder::failBuild()
261 {
262  d->cli = CommandLineInfo();
263  emit commandLineBuildFinished();
264 }
265 
266 void JoinCommandLineBuilder::handleError(const JoinError &error)
267 {
268  if (!error.error().isEmpty())
269  {
270  d->error = error.error();
271  }
272  else
273  {
274  d->error = tr("Unknown error.");
275  }
276  d->configurationError = (error.type() == JoinError::ConfigurationError);
277 
278  gLog << tr("Error when obtaining join parameters for server "
279  "\"%1\", game \"%2\": %3").arg(d->server->name()).arg(
280  d->server->engineName()).arg(d->error);
281 }
282 
283 JoinCommandLineBuilder::MissingWadsProceed JoinCommandLineBuilder::handleMissingWads(const JoinError &error)
284 {
285  if (WadseekerInterface::isInstantiated())
286  {
287  QMessageBox::StandardButtons ret =
288  QMessageBox::warning(d->parentWidget, tr("Doomseeker - files are missing"),
289  tr("You don't have all the files required by this server and an instance "
290  "of Wadseeker is already running.\n\n"
291  "Press 'Ignore' to join anyway."),
292  QMessageBox::Abort | QMessageBox::Ignore);
293  return ret == QMessageBox::Ignore ? Ignore : Cancel;;
294  }
295 
296  QString filesMissingMessage = tr("Following files are missing:\n");
297 
298  if (!error.missingIwad().isEmpty())
299  {
300  filesMissingMessage += tr("IWAD: ") + error.missingIwad().toLower() + "\n";
301  if (Wadseeker::isForbiddenWad(error.missingIwad()))
302  {
303  filesMissingMessage += tr("\n"
304  "Make sure that this file is in one of the paths "
305  "specified in Options -> File Paths.\n"
306  "This file belongs to a commercial game or is otherwise "
307  "blocked from download. If you don't have this file, "
308  "and it belongs to a commercial game, "
309  "you need to purchase the game associated with this IWAD.\n"
310  "Wadseeker will not download commercial IWADs.\n\n");
311  }
312  }
313 
314  if (!error.missingWads().isEmpty())
315  {
316  const QList<PWad> missingWads = error.missingWads();
317  QStringList wadlist;
318  QStringList optionals;
319  foreach(const PWad &wad, missingWads)
320  {
321  if(wad.isOptional())
322  optionals << wad.name();
323  else
324  wadlist << wad.name();
325  }
326  filesMissingMessage += tr("PWADS: %1").arg(wadlist.join(", "));
327  if(!optionals.isEmpty())
328  {
329  filesMissingMessage += "\n";
330  filesMissingMessage += QString("Optional PWADS: %1").arg(optionals.join(", "));
331  }
332  }
333 
334  QStringList requiredDownloads, optionalDownloads;
335  allDownloadableWads(error, requiredDownloads, optionalDownloads);
336  QMessageBox::StandardButtons ret = (QMessageBox::StandardButtons)
337  displayMissingWadsMessage(requiredDownloads, optionalDownloads, filesMissingMessage);
338  if (ret == QMessageBox::Yes)
339  {
340  // If all there were no required wads and all optionals are unchecked,
341  // don't display Wadseeker. Also check if Wadseeker setup is valid.
342  if ((requiredDownloads.isEmpty() && optionalDownloads.isEmpty())
343  || !gWadseekerShow->checkWadseekerValidity(d->parentWidget))
344  {
345  return Cancel;
346  }
347 
348  WadseekerInterface *wadseeker = WadseekerInterface::create(d->server);
349  this->connect(wadseeker, SIGNAL(finished(int)), SLOT(onWadseekerDone(int)));
350  requiredDownloads.append(optionalDownloads); // Pass in all requested downloads to Wadseeker
351  wadseeker->setWads(requiredDownloads);
352  wadseeker->setAttribute(Qt::WA_DeleteOnClose);
353  wadseeker->show();
354  return Seeking;
355  }
356  return ret == QMessageBox::Ignore ? Ignore : Cancel;
357 }
358 
359 bool JoinCommandLineBuilder::isConfigurationError() const
360 {
361  return d->configurationError;
362 }
363 
364 void JoinCommandLineBuilder::missingWadsClicked(QAbstractButton *button)
365 {
366  d->lastButtonClicked = d->buttonBox->standardButton(button);
367 }
368 
370 {
371  assert(d->server != NULL);
372  d->cli = CommandLineInfo();
373 
374  if (!checkServerStatus())
375  {
376  failBuild();
377  return;
378  }
379 
380  ServerConnectParams params;
381  if (!buildServerConnectParams(params))
382  {
383  failBuild();
384  return;
385  }
386  GameClientRunner* gameRunner = d->server->gameRunner();
387  JoinError joinError = gameRunner->createJoinCommandLine(d->cli, params);
388  delete gameRunner;
389 
390  if(d->requireOptionals && joinError.type() == JoinError::NoError && !joinError.missingWads().isEmpty())
391  joinError.setType(JoinError::MissingWads);
392 
393  switch (joinError.type())
394  {
396  failBuild();
397  return;
398  case JoinError::ConfigurationError:
399  case JoinError::Critical:
400  {
401  handleError(joinError);
402  failBuild();
403  return;
404  }
405 
407  {
408  if (tryToInstallGame())
409  {
411  }
412  else
413  {
414  failBuild();
415  }
416  return;
417  }
418 
419  case JoinError::MissingWads:
420  {
421  MissingWadsProceed proceed = handleMissingWads(joinError);
422  switch (proceed)
423  {
424  case Cancel:
425  failBuild();
426  return;
427  case Ignore:
428  break;
429  case Seeking:
430  // async process; will call slot
431  return;
432  default:
433  gLog << "Bug: not sure how to proceed after \"MissingWads\".";
434  failBuild();
435  return;
436  }
437  // Intentional fall through
438  }
439 
440  case JoinError::NoError:
441  if (d->demo == GameDemo::Managed)
442  {
443  QStringList pwads;
444  foreach (const PWad &wad, d->server->wads())
445  {
446  pwads << wad.name();
447  }
448  GameDemo::saveDemoMetaData(d->demoName, *d->server->plugin(),
449  d->server->iwad(), pwads);
450  }
451  break;
452 
453  default:
454  gLog << "JoinCommandLineBuilder - unhandled JoinError type!";
455  break;
456  }
457 
458  emit commandLineBuildFinished();
459 }
460 
461 void JoinCommandLineBuilder::onWadseekerDone(int result)
462 {
463  qDebug() << "onWadseekerDone:" << result;
464  if (result == QDialog::Accepted)
465  {
467  }
468 }
469 
470 ServerPtr JoinCommandLineBuilder::server() const
471 {
472  return d->server;
473 }
474 
475 void JoinCommandLineBuilder::setPasswords(const QString &connectPassword, const QString &inGamePassword)
476 {
477  d->passwordsAlreadySet = !(connectPassword.isNull() && inGamePassword.isNull());
478  if(!connectPassword.isNull())
479  d->connectPassword = connectPassword;
480  if(!inGamePassword.isNull())
481  d->inGamePassword = inGamePassword;
482 }
483 
485 {
486  d->requireOptionals = required;
487 }
488 
489 bool JoinCommandLineBuilder::tryToInstallGame()
490 {
491  Message message = d->server->clientExe()->install(gApp->mainWindowAsQWidget());
492  if (message.isError())
493  {
494  QMessageBox::critical(gApp->mainWindowAsQWidget(), tr("Game installation failure"),
495  message.contents(), QMessageBox::Ok);
496  }
497  return message.type() == Message::Type::SUCCESSFUL;
498 }
PWAD hosted on a server.
Structure holding parameters for application launch.
Definition: apprunner.h:37
Message object used to pass messages throughout the Doomseeker's system.
Definition: message.h:62
A DTO for GameClientRunner; exchanges information between main program and plugins, and allows future extensions.
bool isError() const
True if type() is equal to or greater than CUSTOM_ERROR.
Definition: message.cpp:104
void setPasswords(const QString &connectPassword=QString(), const QString &inGamePassword=QString())
Sets the connect/ingame password and bypasses the prompt. Set passwords to a null string to unset...
const QList< PWad > & missingWads() const
Definition: joinerror.cpp:109
Generates command line for joining specified server.
Aborts without printing error.
Definition: joinerror.h:55
Indicator of error for the server join process.
Definition: joinerror.h:41
void setRequireOptionals(bool)
Treats optional wads are required so Wadseeker prompts.
JoinError createJoinCommandLine(CommandLineInfo &cli, const ServerConnectParams &params)
Fills out CommandLineInfo object that allows client executables to be launched.
const QString & missingIwad() const
Definition: joinerror.cpp:104
void obtainJoinCommandLine()
Runs asynchronously and emits commandLineBuildFinished() when done.
void setWads(const QStringList &wads)
Sets WADs to seek.
Wadseeker dialog box, only one instance is allowed.
Game executable was not found but it can be automatically installed by the plugin.
Definition: joinerror.h:60
bool isOptional() const
Is this WAD required to join the server?
static const unsigned SUCCESSFUL
Message indicates that the operation was successful.
Definition: message.h:97
unsigned type() const
Message::Type.
Definition: message.cpp:124
QString contents() const
Customized displayable contents of this Message.
Definition: message.cpp:87
Creates command line that launches the client executable of the game and connects it to a server...
const QString & name() const
File name of the WAD.