launcher/demo.cpp

Sun, 28 Nov 2021 23:53:23 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Sun, 28 Nov 2021 23:53:23 +0200
changeset 66
c68545f1aecb
parent 64
c55e46b7ddeb
permissions
-rw-r--r--

Simplify ZandronumVersion structure

/*
 *  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 <errno.h>
#include <assert.h>
#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 (int i = 0; i < in.length(); ++i)
	{
		if (skip-- > 0)
			continue;

		QChar ch = in[i];

		if (ch == QChar ('\034'))
		{
			skip = 1;
			continue;
		}

		out += ch;
	}

	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();

	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;
	}

	// WAD names are case-sensitive under non-Windows and they can appear in uppercase
	// so we need to test that too.
	if (name != name.toUpper())
		return findWad (name.toUpper());

	return "";
}

//
// -------------------------------------------------------------------------------------------------
//

QString readString (QDataStream& stream)
{
	QString out;
	quint8 ch;

	for (stream >> ch; ch != 0; stream >> ch)
		out += QChar (ch);

	return out;
}

//
// -------------------------------------------------------------------------------------------------
//

struct UserInfo
{
	QString netname;
	QString skin;
	QString className;
	quint32 color;
	quint32 aimdist;
	quint32 railcolor;
	quint8 gender;
	quint8 handicap;
	quint8 unlagged;
	quint8 respawnOnFire;
	quint8 ticsPerUpdate;
	quint8 connectionType;
	quint8 colorset;
};

//
// -------------------------------------------------------------------------------------------------
//

struct DemoHeaders
{
	quint8 length;
	quint8 version;
	quint8 userInfo;
	quint8 bodyStart;
	quint8 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;
	quint32 length;
	quint16 zanversionID, numWads;
	quint32 longSink;
	QString zanversion;
	QString dateversion = "00000000";
	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
	{
		quint32 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 (;;)
	{
		quint8 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-"))
			{
				quint8 a;
				stream >> a;
				buildID = (BuildType) a;
			}
			if (zanversion.contains("-r"))
			{
				dateversion = zanversion.split("-r").last();
			}

			// 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;
			// Zandronum commit 507a744e69c4 added support for colorsets and versions since that
			// write a byte for the colorset in the userinfo.
			if (dateversion >= "210307-0319")
			{
				stream >> userinfo.colorset;
			}
			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 (quint8 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;

		version = findVersion (zanversion);
		assert (not version.name.isNull());
	}

	QString iwadpath;
	QStringList pwadpaths;
	bool doneAssimilation = false;

	// Find the WADs
	for (int i = 0; i < wads.size(); ++i)
	{
		const QString& wad = wads[i];
		QString path = findWad (wad);

		if (path.isEmpty() and not doneAssimilation and Config::get ("autoassimilate").toBool())
		{
			QStringList wadpaths = Config::get ("wadpaths").toStringList();

			// If there are no wad paths, try assimilate from other sources.
			assimilateWadPaths (wadpaths);
			Config::set ("wadpaths", wadpaths);

			// Try find the wad again
			path = findWad (wad);
		}

		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 (int i = 0; i < wads.size(); ++i)
		{
			if (i == 0)
				continue; // skip the IWAD

			if (not pwadtext.isEmpty())
				pwadtext += "<br />";

			pwadtext += wads[i];
		}

		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;
}

mercurial