25 #include "configuration/doomseekerconfig.h"
26 #include "datapaths.h"
28 #include "fileutils.h"
30 #include "ini/settingsproviderqt.h"
32 #include "plugins/engineplugin.h"
33 #include "plugins/pluginloader.h"
38 #include <QDirIterator>
40 #include <QJsonDocument>
42 #include <QMessageBox>
44 #include <QVariantList>
46 class GameDemoTr :
public QObject
51 static QString title()
53 return tr(
"Doomseeker - Record Demo");
56 static QString pathDoesntExist(
const QString &path,
const QString &details)
59 if (!details.isEmpty())
61 detailsMsg = QString(
"\n") + tr(
"Error: %1").arg(details);
63 return tr(
"The demo storage directory doesn't exist "
64 "and cannot be created!%1\n\n%2").arg(detailsMsg).arg(path);
67 static QString pathMissingPermissions(
const QString &path)
69 return tr(
"The demo storage directory exists but "
70 "lacks the necessary permissions!\n\n%1").arg(path);
74 GameDemoTr() =
delete;
83 static void loadDemoMetaDataFromFilenameV1(
const QString &path,
GameDemo &demo)
85 QString metaData = QFileInfo(path).completeBaseName();
88 for (
int i = 0; i < metaData.length(); ++i)
90 if (metaData[i] ==
'_')
93 if (i + 1 < metaData.length() && metaData[i + 1] ==
'_')
100 demoData << metaData.left(i).replace(
"__",
"_");
101 metaData = metaData.mid(i + 1);
106 demoData << metaData.replace(
"__",
"_");
108 if (demoData.size() < 3)
111 QDate date = QDate::fromString(demoData[1],
"dd.MM.yyyy");
112 QTime time = QTime::fromString(demoData[2],
"hh.mm.ss");
114 demo.
game = demoData[0];
115 demo.
time = QDateTime(date, time, QTimeZone::systemTimeZone());
116 if (demoData.size() >= 4)
118 bool hasIwad =
false;
119 QStringList wadnames = demoData.mid(3);
120 for (QString wad : wadnames)
141 static void loadDemoMetaDataFromFilenameV2(
const QString &path,
GameDemo &demo)
143 QFileInfo fileinfo(path);
146 QDir yearDir = fileinfo.dir();
147 if (yearDir.path() !=
".")
149 QDir playerDir = QFileInfo(yearDir.path()).dir();
150 if (playerDir.path() !=
".")
152 demo.
author = playerDir.dirName();
156 QStringList tokens = fileinfo.baseName().split(
"_");
158 demo.
game = tokens.join(
"_");
164 static void loadDemoMetaDataFromExportedFilename(
const QString &path,
GameDemo &demo)
166 QStringList tokens = QFileInfo(path).baseName().split(
"_");
168 if (tokens.size() >= 1)
169 demo.
game = tokens.takeLast();
170 if (tokens.size() >= 1)
171 demo.
author = tokens.join(
"_");
176 const QString suffix = QFileInfo(this->
demopath).suffix();
182 static const QRegularExpression mangler(
"[^A-Za-z0-9-]");
184 const QString mangledAuthor = !this->
author.isEmpty()
186 .replace(mangler,
"")
187 .left(MAX_AUTHOR_FILENAME_LEN)
189 const QString mangledGame = !this->
game.isEmpty()
190 ? QString(this->
game).replace(mangler,
"")
192 const QString formattedDate = this->time.isValid()
195 return QString(
"%1_%2_%3")
203 const QString suffix = QFileInfo(this->
demopath).suffix();
209 static const QRegularExpression mangler(
"[^A-Za-z0-9-]");
211 QString mangledAuthor = QString(this->
author)
212 .replace(mangler,
"")
213 .left(MAX_AUTHOR_FILENAME_LEN);
214 if (mangledAuthor.isEmpty())
216 const QString year = this->time.isValid() ? QString(
"%1").arg(this->time.date().year(), 4, 10, QChar(
'0')) :
"_";
217 const QString mangledGame = QString(this->
game)
218 .replace(mangler,
"");
219 const QString formattedDate = this->time.isValid()
222 return QString(
"%1/%2/%3_%4")
232 static const QRegularExpression detectV1(R
"(\d{2}\.\d{2}.\d{4}_\d{2}\.\d{2}\.\d{2})");
233 static const QRegularExpression detectV2(R
"(\d{4}-\d{2}-\d{2}T\d{6}Z)");
235 QString metaData = QFileInfo(path).completeBaseName();
236 if (detectV2.match(metaData).hasMatch())
238 if (metaData.count(
"_") > 1)
240 loadDemoMetaDataFromExportedFilename(path, *
this);
244 loadDemoMetaDataFromFilenameV2(path, *
this);
247 else if (detectV1.match(metaData).hasMatch())
249 loadDemoMetaDataFromFilenameV1(path, *
this);
256 DemoRecord::DemoRecord()
261 DemoRecord::DemoRecord(Control control)
274 static const int DOOMSEEKER_METADATA_VERSION = 2;
277 static const QString METAFILE_SUFFIX =
"ini";
278 static const QString SECTION_DOOMSEEKER =
"doomseeker";
279 static const QString SECTION_META =
"meta";
291 d->root = QDir(gDefaultDataPaths->demosDirectoryPath());
301 QStringList extensionFilters;
302 for (
const QString &ext : listDemoExtensions())
303 extensionFilters << QString(
"*.%1").arg(ext);
304 return extensionFilters;
312 auto popup = [parent](
const QString &message) ->
bool
314 return QMessageBox::Ignore ==
315 QMessageBox::warning(parent, GameDemoTr::title(), message,
316 QMessageBox::Abort | QMessageBox::Ignore);
320 if (mkResult.isError())
322 return popup(GameDemoTr::pathDoesntExist(d->root.path(), mkResult.errnoString));
325 QFileInfo demoDirInfo(d->root.path());
326 ++qt_ntfs_permission_lookup;
327 bool permissions = demoDirInfo.isReadable()
328 && demoDirInfo.isWritable()
329 && demoDirInfo.isExecutable();
330 --qt_ntfs_permission_lookup;
333 return popup(GameDemoTr::pathMissingPermissions(d->root.path()));
339 QStringList DemoStore::listDemoExtensions()
341 QStringList demoExtensions;
342 for (
unsigned i = 0; i < gPlugins->numPlugins(); ++i)
344 QString spExt = gPlugins->info(i)->data()->demoExtension;
345 if (!demoExtensions.contains(spExt))
346 demoExtensions << spExt;
348 QString mpExt = gPlugins->info(i)->data()->multiplayerDemoExtension;
349 if (!demoExtensions.contains(mpExt))
350 demoExtensions << mpExt;
352 return demoExtensions;
357 const QString rootpath = d->root.path();
359 QDir::Files, QDirIterator::Subdirectories);
361 while (dirit.hasNext())
363 QString path = dirit.next();
364 result << path.mid(rootpath.length() + 1);
372 if (!importedDemo.exists())
375 QFile managedDemo(d->root.filePath(demo.
managedName()));
378 QDir managedDemoDir = QFileInfo(managedDemo).dir();
382 if (managedDemo.exists())
383 managedDemo.remove();
384 if (metafile.exists())
387 if (!importedDemo.copy(managedDemo.fileName()))
390 return metafile.exists();
395 return path +
"." + METAFILE_SUFFIX;
401 demo.
demopath = QString(
"demo.%1").arg(isMultiplayer
402 ? plugin.data()->multiplayerDemoExtension
403 : plugin.data()->demoExtension);
404 demo.
author = gConfig.doomseeker.realPlayerName();
405 demo.
time = QDateTime::currentDateTime();
408 const bool extensionAddedByGame = isMultiplayer
409 ? plugin.data()->multiplayerDemoExtensionAutomatic
410 : plugin.data()->demoExtensionAutomatic;
416 QFileInfo demoPath(d->root.filePath(extensionAddedByGame
421 return demoPath.absoluteFilePath();
424 return extensionAddedByGame
430 assert(0 &&
"Unknown demo control type");
437 QFile demoFile(d->root.filePath(name));
438 if (demoFile.exists())
440 if (!demoFile.remove())
446 QFile metaFile(
metafile(demoFile.fileName()));
457 const QString &iwad,
const QList<PWad> &pwads,
const bool isMultiplayer,
458 const QString &gameVersion)
461 bool isDemoExtensionAutomatic = isMultiplayer ?
462 plugin.data()->multiplayerDemoExtensionAutomatic : plugin.data()->demoExtensionAutomatic;
463 QString demoExtension = isMultiplayer ?
464 plugin.data()->multiplayerDemoExtension : plugin.data()->demoExtension;
466 QString demoFileName = demoName;
467 if (isDemoExtensionAutomatic)
468 demoFileName +=
"." + plugin.data()->demoExtension;
469 QString metaFileName =
metafile(demoFileName);
475 demo.
author = gConfig.doomseeker.realPlayerName();
485 QSettings settings(path, QSettings::IniFormat);
487 Ini metaFile(&settingsProvider);
489 IniSection doomseekerSection = metaFile.section(SECTION_DOOMSEEKER);
490 doomseekerSection.
setValue(
"version", DOOMSEEKER_METADATA_VERSION);
492 IniSection metaSection = metaFile.section(SECTION_META);
503 QVariantList serializedWad;
505 wads << QVariant(serializedWad);
510 metaSection.
setValue(
"wads", QString::fromUtf8(
511 QJsonDocument::fromVariant(wads).toJson(QJsonDocument::Compact)));
514 QStringList requiredPwads;
515 QStringList optionalPwads;
519 optionalPwads << wad.
name();
521 requiredPwads << wad.
name();
523 metaSection.
setValue(
"pwads", requiredPwads.join(
";"));
524 metaSection.
setValue(
"optionalPwads", optionalPwads);
527 static void loadDemoMetaDataFromIniV1(
Ini &metaData,
GameDemo &demo)
531 if (pwads.length() > 0)
533 for (QString wad : pwads.split(
";"))
540 for (QString wad : metaData.
retrieveSetting(SECTION_META,
"optionalPwads").
value().toStringList())
546 static void loadDemoMetaDataFromIniV2(
Ini &metaData,
GameDemo &demo)
553 QVariantList wads = QJsonDocument::fromJson(metaData.
retrieveSetting(SECTION_META,
"wads").
valueString().toUtf8()).toVariant().toList();
554 for (
const QVariant &wad : wads)
556 QVariantList wadTokens = wad.toList();
557 if (wadTokens.size() >= 2)
559 QString name = wadTokens[0].toString();
560 bool optional = wadTokens[1].toBool();
566 static void loadDemoMetaDataFromIni(
const QString &path,
GameDemo &demo)
568 QFileInfo iniFile(path +
".ini");
569 if (!iniFile.exists())
572 QSettings settings(iniFile.filePath(), QSettings::IniFormat);
574 Ini metaData(&settingsProvider);
575 int version = metaData.
section(SECTION_DOOMSEEKER)[
"version"].
value().toInt();
581 loadDemoMetaDataFromIniV2(metaData, demo);
583 loadDemoMetaDataFromIniV1(metaData, demo);
590 demo.
demopath = path.endsWith(
"." + METAFILE_SUFFIX)
591 ? path.left(path.length() - (1 + METAFILE_SUFFIX.length()))
594 loadDemoMetaDataFromIni(demo.
demopath, demo);
601 QFile demoFile(d->root.filePath(name));
602 if (demoFile.exists())
607 #include "gamedemo.moc"