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