--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/launcher/demo.cpp Sat Jun 06 23:12:59 2015 +0300 @@ -0,0 +1,369 @@ +/* + * ZCinema: Zandronum demo launcher + * Copyright (C) 2013-2015 Teemu Piippo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <QFile> +#include <QDataStream> +#include <QMessageBox> +#include <QProcess> +#include <commonlib/misc.h> +#include <commonlib/config.h> +#include <commonlib/version.h> +#include "demo.h" +#include "prompts.h" +#include "ui_demoprompt.h" + +// +// ------------------------------------------------------------------------------------------------- +// + +QString uncolorize (const QString& in) +{ + // TODO: Handle long-form colors like \c[Red] + QString out; + int skip = 0; + + for (QChar c : in) + { + if (skip-- > 0) + continue; + + if (c == QChar ('\034')) + { + skip = 1; + continue; + } + + out += c; + } + + return out; +} + +// +// ------------------------------------------------------------------------------------------------- +// + +static QString tr (const char* msg) +{ + return QObject::tr (msg); +} + +// +// ------------------------------------------------------------------------------------------------- +// + +static void error (QString msg) +{ + QMessageBox::critical (NULL, "Error", msg); +} + +// +// ------------------------------------------------------------------------------------------------- +// + +static ZandronumVersion findVersion (QString versionName) +{ + QList<QVariant> versions = Config::get ("versions").toList(); + + for (int i = 0; i < versions.size(); ++i) + { + if (not versions[i].canConvert<ZandronumVersion>()) + continue; + + ZandronumVersion version = versions[i].value<ZandronumVersion>(); + + if (version.name == versionName + or version.name + "M" == versionName + or version.name == versionName + "M") + { + return version; + } + } + + return ZandronumVersion(); +} + +// +// ------------------------------------------------------------------------------------------------- +// + +static QString findWAD (QString name) +{ + QStringList wadpaths = Config::get ("wadpaths").toStringList(); + + if (wadpaths.empty()) + { + error (tr ("No WAD paths configured!")); + + // Cannot just return an empty string here since that'd trigger another error prompt - skip + // ahead and exit. + exit (1); + } + + for (int i = 0; i < wadpaths.size(); ++i) + { + QString fullpath = QString ("%1/%2").arg (wadpaths[i]).arg (name); + QFile f (fullpath); + + if (f.exists()) + return fullpath; + } + + return ""; +} + +// +// ------------------------------------------------------------------------------------------------- +// + +QString readString (QDataStream& stream) +{ + QString out; + uint8 ch; + + for (stream >> ch; ch != 0; stream >> ch) + out += QChar (ch); + + return out; +} + +// +// ------------------------------------------------------------------------------------------------- +// + +struct UserInfo +{ + QString netname; + QString skin; + QString className; + uint32 color; + uint32 aimdist; + uint32 railcolor; + uint8 gender; + uint8 handicap; + uint8 unlagged; + uint8 respawnOnFire; + uint8 ticsPerUpdate; + uint8 connectionType; +}; + +struct DemoHeaders +{ + uint8 length; + uint8 version; + uint8 userInfo; + uint8 bodyStart; + uint8 wads; +}; + +int launchDemo (QString path) +{ + QFile f (path); + + if (not f.open (QIODevice::ReadOnly)) + { + error (tr ("Couldn't open '%1' for reading: %2").arg (path).arg (strerror (errno))); + return 1; + } + + QDataStream stream (&f); + stream.setByteOrder (QDataStream::LittleEndian); + + DemoHeaders headers; + uint32 length; + uint16 zanversionID, numWads; + uint32 longSink; + QString zanversion; + QStringList wads; + UserInfo userinfo; + + // Assume a release build if the build ID not supplied. The demo only got the "ZCLD" signature + // in the 1.1 release build, 1.1.1 had no testing binaries and the build ID is included in 1.2 + // onward. + BuildType buildID = ReleaseBuild; + + bool ready = false; + + // Check signature + { + uint32 demosignature; + stream >> demosignature; + + if (demosignature != makeByteID ('Z', 'C', 'L', 'D')) + { + error (tr ("'%1' is not a valid Zandronum demo file!").arg (path)); + return 1; + } + } + + stream >> headers.length; + stream >> length; + + // The remaining headers are variable and relative to the length header. + headers.version = headers.length + 1; + headers.userInfo = headers.length + 3, + headers.bodyStart = headers.length + 4; + headers.wads = headers.length + 10; + + // Read the demo header and get data + for (;;) + { + uint8 header; + stream >> header; + + if (header == headers.bodyStart) + { + ready = true; + break; + } + else if (header == headers.version) + { + stream >> zanversionID; + zanversion = readString (stream); + + if (not zanversion.startsWith ("1.1-") and not zanversion.startsWith ("1.1.1-")) + { + uint8 a; + stream >> a; + buildID = (BuildType) a; + } + + // The demo wads header accidentally changed in 1.3. :( + if (zanversion.left(1).toInt() >= 2 or zanversion.startsWith ("1.3")) + headers.wads = headers.length + 8; + + stream >> longSink; // rng seed - we don't need it + } + else if (header == headers.userInfo) + { + userinfo.netname = readString (stream); + stream >> userinfo.gender; + stream >> userinfo.color; + stream >> userinfo.aimdist; + userinfo.skin = readString (stream); + stream >> userinfo.railcolor; + stream >> userinfo.handicap; + stream >> userinfo.unlagged; + stream >> userinfo.respawnOnFire; + stream >> userinfo.ticsPerUpdate; + stream >> userinfo.connectionType; + userinfo.className = readString (stream); + } + else if (header == headers.wads) + { + QString sink; + stream >> numWads; + + for (uint8 i = 0; i < numWads; ++i) + { + QString wad = readString (stream); + wads << wad; + } + + // The demo has two checksum strings. We're not interested in them, though. + (sink = readString (stream)) = readString (stream); + } + else + { + error (tr ("Unknown header %1!\n").arg (int (header))); + return 1; + } + } + + if (not ready) + { + error (tr ("Incomplete demo header in '%s'!").arg (path)); + return 1; + } + + ZandronumVersion version = findVersion (zanversion); + + if (version.name.isNull()) + { + QDialog* prompt = new UnknownVersionPrompt (path, zanversion, (buildID == ReleaseBuild)); + + if (not prompt->exec()) + return 1; + } + + QString iwadpath; + QStringList pwadpaths; + + // Find the WADs + for (const QString& wad : wads) + { + QString path = findWAD (wad); + + // WAD names are case-sensitive under non-Windows and they can appear in uppercase + // so we need to test that too. + if (path.isEmpty()) + path = findWAD (wad.toUpper()); + + if (path.isEmpty()) + { + error (tr ("Couldn't find %1!").arg (wad)); + return 1; + } + + if (&wad == &wads.first()) + iwadpath = path; + else + pwadpaths << path; + } + + if (not Config::get ("noprompt").toBool()) + { + QString pwadtext; + + for (const QString& wad : wads) + { + if (&wad == &wads.first()) + continue; // skip the IWAD + + if (not pwadtext.isEmpty()) + pwadtext += "<br />"; + + pwadtext += wad; + } + + QDialog* dlg = new QDialog; + Ui_DemoPrompt ui; + ui.setupUi (dlg); + ui.demoNameLabel->setText (basename (path)); + ui.demoRecorder->setText (uncolorize (userinfo.netname)); + ui.versionLabel->setText (zanversion); + ui.iwadLabel->setText (wads[0]); + ui.pwadsLabel->setText (pwadtext); + dlg->setWindowTitle (versionSignature()); + + if (not dlg->exec()) + return 0; + } + + QStringList cmdlineList; + cmdlineList << "-playdemo" << path << "-iwad" << iwadpath; + + if (pwadpaths.size() > 0) + cmdlineList << "-file" << pwadpaths; + + // print ("Executing: %1 %2\n", binarypath, cmdlineList.join (" ")); + QProcess* proc = new QProcess; + proc->start (version.binaryPath, cmdlineList); + proc->waitForFinished (-1); + return 0; +}