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