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