demomanager.cpp
1 //------------------------------------------------------------------------------
2 // demomanager.h
3 //------------------------------------------------------------------------------
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library 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 GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; 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) 2011 Braden "Blzut3" Obrzut <admin@maniacsvault.net>
22 //------------------------------------------------------------------------------
23 #include "demomanager.h"
24 
25 #include "datapaths.h"
26 #include "gui/commongui.h"
27 #include "ini/ini.h"
28 #include "ini/settingsproviderqt.h"
29 #include "pathfinder/pathfinder.h"
30 #include "pathfinder/wadpathfinder.h"
31 #include "plugins/engineplugin.h"
32 #include "plugins/pluginloader.h"
33 #include "serverapi/gamecreateparams.h"
34 #include "serverapi/gameexeretriever.h"
35 #include "serverapi/gamehost.h"
36 #include "serverapi/message.h"
37 #include "serverapi/server.h"
38 #include "templatedpathresolver.h"
39 #include "ui_demomanager.h"
40 
41 #include <QDir>
42 #include <QFileDialog>
43 #include <QLabel>
44 #include <QMessageBox>
45 #include <QPushButton>
46 #include <QStandardItemModel>
47 
48 class Demo
49 {
50 public:
51  QString filename;
52  QString port;
53  QDateTime time;
54  QStringList wads;
55  QStringList optionalWads;
56 };
57 
58 DClass<DemoManagerDlg> : public Ui::DemoManagerDlg
59 {
60 public:
61  Demo *selectedDemo;
62  QStandardItemModel *demoModel;
63  QList<QList<Demo> > demoTree;
64 };
65 
66 DPointered(DemoManagerDlg)
67 
69 {
70  d->setupUi(this);
72  d->selectedDemo = nullptr;
73 
74  d->demoModel = new QStandardItemModel();
75  adjustDemoList();
76 
77  connect(d->demoList->selectionModel(), SIGNAL(currentChanged(const QModelIndex&,const QModelIndex&)), this, SLOT(updatePreview(const QModelIndex&)));
78 }
79 
80 DemoManagerDlg::~DemoManagerDlg()
81 {
82 }
83 
84 void DemoManagerDlg::adjustDemoList()
85 {
86  // Get valid extensions
87  QStringList demoExtensions;
88  for (unsigned i = 0; i < gPlugins->numPlugins(); ++i)
89  {
90  QString ext = QString("*.%1").arg(gPlugins->info(i)->data()->demoExtension);
91 
92  if (!demoExtensions.contains(ext))
93  demoExtensions << ext;
94  }
95 
96  // In order to index the demos we'll convert the dates to integers by calculating the days until today.
97  // Also we need to convert double underscores to a single underscore
98  QDate today = QDate::currentDate();
99  QTime referenceTime(23, 59, 59);
100  QDir demosDirectory(gDefaultDataPaths->demosDirectoryPath());
101  QStringList demos = demosDirectory.entryList(demoExtensions, QDir::Files);
102  typedef QMap<int, Demo> DemoMap;
103  QMap<int, DemoMap> demoMap;
104  for (const QString &demoName : demos)
105  {
106  QStringList demoData;
107  QString metaData = demoName.left(demoName.lastIndexOf("."));
108  // We need to split manually to handle escaping.
109  for (int i = 0; i < metaData.length(); ++i)
110  {
111  if (metaData[i] == '_')
112  {
113  // If our underscore is followed by another just continue on...
114  if (i + 1 < metaData.length() && metaData[i + 1] == '_')
115  {
116  ++i;
117  continue;
118  }
119 
120  // Split the meta data and then restart from the beginning.
121  demoData << metaData.left(i).replace("__", "_");
122  metaData = metaData.mid(i + 1);
123  i = 0;
124  }
125  }
126  // Whatever is left is a part of our data.
127  demoData << metaData.replace("__", "_");
128  if (demoData.size() < 3) // Should have at least 3 elements port, date, time[, iwad[, pwads]]
129  continue;
130 
131  QDate date = QDate::fromString(demoData[1], "dd.MM.yyyy");
132  QTime time = QTime::fromString(demoData[2], "hh.mm.ss");
133  Demo demo;
134  demo.filename = demoName;
135  demo.port = demoData[0];
136  demo.time = QDateTime(date, time);
137  if (demoData.size() >= 4)
138  demo.wads = demoData.mid(3);
139  else
140  {
141  // New format, read meta data from file!
142  QSettings settings(
143  gDefaultDataPaths->demosDirectoryPath() + QDir::separator() + demoName + ".ini",
144  QSettings::IniFormat);
145  SettingsProviderQt settingsProvider(&settings);
146  Ini metaData(&settingsProvider);
147  demo.wads << metaData.retrieveSetting("meta", "iwad");
148  QString pwads = metaData.retrieveSetting("meta", "pwads");
149  if (pwads.length() > 0)
150  demo.wads << pwads.split(";");
151  demo.optionalWads = metaData.retrieveSetting("meta", "optionalPwads").value().toStringList();
152  }
153 
154  demoMap[date.daysTo(today)][time.secsTo(referenceTime)] = demo;
155  }
156 
157  // Convert to a model
158  d->demoModel->clear();
159  d->demoTree.clear();
160  for (const DemoMap &demoDate : demoMap)
161  {
162  QStandardItem *item = new QStandardItem(demoDate.begin().value().time.toString("ddd. MMM d, yyyy"));
163  QList<Demo> demoDateList;
164  for (const Demo &demo : demoDate)
165  {
166  demoDateList << demo;
167  item->appendRow(new QStandardItem(demo.time.toString("hh:mm:ss")));
168  }
169  d->demoTree << demoDateList;
170  d->demoModel->appendRow(item);
171  }
172  d->demoList->setModel(d->demoModel);
173 }
174 
175 bool DemoManagerDlg::doRemoveDemo(const QString &file)
176 {
177  if (!QFile::remove(file))
178  QMessageBox::critical(this, tr("Unable to remove"), tr("Could not remove the selected demo."));
179  else
180  {
181  // Remove ini file as well, but don't bother warning if it can't be deleted for whatever reason
182  QFile::remove(file + ".ini");
183  d->selectedDemo = nullptr;
184  return true;
185  }
186  return false;
187 }
188 
189 void DemoManagerDlg::deleteSelected()
190 {
191  if (QMessageBox::question(this, tr("Remove demo?"),
192  tr("Are you sure you want to remove the selected demo?"),
193  QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes)
194  {
195  QModelIndex index = d->demoList->selectionModel()->currentIndex();
196  if (d->selectedDemo == nullptr)
197  {
198  int dateRow = index.row();
199  for (int timeRow = 0; index.model()->index(timeRow, 0).isValid(); ++timeRow)
200  {
201  if (doRemoveDemo(gDefaultDataPaths->demosDirectoryPath() + QDir::separator() + d->demoTree[dateRow][timeRow].filename))
202  {
203  d->demoModel->removeRow(timeRow, index);
204  d->demoTree[dateRow].removeAt(timeRow);
205  if (d->demoTree[dateRow].size() == 0)
206  {
207  d->demoModel->removeRow(dateRow);
208  d->demoTree.removeAt(dateRow);
209  break;
210  }
211 
212  // We deleted the top row, so decrement our pointer
213  --timeRow;
214  }
215  }
216  }
217  else
218  {
219  if (doRemoveDemo(gDefaultDataPaths->demosDirectoryPath() + QDir::separator() + d->selectedDemo->filename))
220  {
221  // Adjust the tree
222  int dateRow = index.parent().row();
223  int timeRow = index.row();
224 
225  d->demoModel->removeRow(timeRow, index.parent());
226  d->demoTree[dateRow].removeAt(timeRow);
227  if (d->demoTree[dateRow].size() == 0)
228  {
229  d->demoModel->removeRow(dateRow);
230  d->demoTree.removeAt(dateRow);
231  }
232  }
233  }
234  }
235 }
236 
237 void DemoManagerDlg::exportSelected()
238 {
239  if (d->selectedDemo == nullptr)
240  return;
241 
242  QFileDialog saveDialog(this);
243  saveDialog.setAcceptMode(QFileDialog::AcceptSave);
244  saveDialog.selectFile(d->selectedDemo->filename);
245  if (saveDialog.exec() == QDialog::Accepted)
246  {
247  // Copy the demo to the new location.
248  if (!QFile::copy(gDefaultDataPaths->demosDirectoryPath() + QDir::separator() + d->selectedDemo->filename, saveDialog.selectedFiles().first()))
249  QMessageBox::critical(this, tr("Unable to save"), tr("Could not write to the specified location."));
250  }
251 }
252 
253 void DemoManagerDlg::itemDoubleClicked(const QModelIndex &index)
254 {
255  if (d->selectedDemo != nullptr)
256  playSelected();
257 }
258 
259 void DemoManagerDlg::playSelected()
260 {
261  if (d->selectedDemo == nullptr)
262  return;
263 
264  // Look for the plugin used to record.
265  EnginePlugin *plugin = nullptr;
266  for (unsigned i = 0; i < gPlugins->numPlugins(); i++)
267  {
268  if (d->selectedDemo->port == gPlugins->info(i)->data()->name)
269  plugin = gPlugins->info(i);
270  }
271  if (plugin == nullptr)
272  {
273  QMessageBox::critical(this, tr("No plugin"),
274  tr("The \"%1\" plugin does not appear to be loaded.").arg(d->selectedDemo->port));
275  return;
276  }
277 
278  // Get executable path for pathfinder.
279  Message binMessage;
280  const QString binPath = gDoomseekerTemplatedPathResolver().resolve(
281  GameExeRetriever(*plugin->gameExe()).pathToOfflineExe(binMessage));
282 
283  // Locate all the files needed to play the demo
284  PathFinder pf;
285  pf.addPrioritySearchDir(binPath);
286  WadPathFinder wadFinder = WadPathFinder(pf);
287 
288  QStringList missingWads;
289  QStringList wadPaths;
290 
291  for (const QString &wad : d->selectedDemo->wads)
292  {
293  WadFindResult findResult = wadFinder.find(wad);
294  if (findResult.isValid())
295  wadPaths << findResult.path();
296  else
297  missingWads << wad;
298  }
299 
300  if (!missingWads.isEmpty())
301  {
302  QMessageBox::critical(this, tr("Files not found"),
303  tr("The following files could not be located: ") + missingWads.join(", "));
304  return;
305  }
306  QStringList optionalWadPaths;
307  for (const QString &wad : d->selectedDemo->optionalWads)
308  {
309  WadFindResult findResult = wadFinder.find(wad);
310  if (findResult.isValid())
311  optionalWadPaths << findResult.path();
312  }
313 
314  // Play the demo
315  GameCreateParams params;
316  params.setDemoPath(QFileInfo(gDefaultDataPaths->demosDirectoryPath()
317  + QDir::separator() + d->selectedDemo->filename).absoluteFilePath());
318  params.setIwadPath(wadPaths[0]);
319  params.setPwadsPaths(wadPaths.mid(1) + optionalWadPaths);
320  params.setHostMode(GameCreateParams::Demo);
321  params.setExecutablePath(binPath);
322 
323  GameHost *gameRunner = plugin->gameHost();
324  Message message = gameRunner->host(params);
325 
326  if (message.isError())
327  QMessageBox::critical(this, tr("Doomseeker - error"), message.contents());
328 
329  delete gameRunner;
330 }
331 
332 
333 void DemoManagerDlg::performAction(QAbstractButton *button)
334 {
335  if (button == d->buttonBox->button(QDialogButtonBox::Close))
336  reject();
337 }
338 
339 void DemoManagerDlg::updatePreview(const QModelIndex &index)
340 {
341  if (!index.isValid() || !index.parent().isValid())
342  {
343  d->preview->setText("");
344  d->selectedDemo = nullptr;
345  return;
346  }
347 
348  int dateRow = index.parent().row();
349  int timeRow = index.row();
350  d->selectedDemo = &d->demoTree[dateRow][timeRow];
351 
352  QString text = "<b>" + tr("Port") + ":</b><p style=\"margin: 0px 0px 0px 10px\">" + d->selectedDemo->port + "</p>" +
353  "<b>" + tr("WADs") + ":</b><p style=\"margin: 0px 0px 0px 10px\">";
354  for (const QString &wad : d->selectedDemo->wads)
355  {
356  text += wad + "<br />";
357  }
358  for (const QString &wad : d->selectedDemo->optionalWads)
359  {
360  text += "[" + wad + "]<br />";
361  }
362  text += "</p>";
363  d->preview->setText(text);
364 }