pollymc/logic/LegacyUpdate.cpp
Petr Mrázek ffbc5bb62c Offline mode can be used even when online.
Allow the user to pick a player name for offline mode.
Big auth refactor. Now using session objects instead of the accounts themselves.
Sessions only last for one instance start and hold all the auth and player data.
2014-01-27 03:00:49 +01:00

496 lines
13 KiB
C++

/* Copyright 2013 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "LegacyUpdate.h"
#include "lists/LwjglVersionList.h"
#include "lists/MinecraftVersionList.h"
#include "BaseInstance.h"
#include "LegacyInstance.h"
#include "MultiMC.h"
#include "ModList.h"
#include <pathutils.h>
#include <quazip.h>
#include <quazipfile.h>
#include <JlCompress.h>
#include "logger/QsLog.h"
#include "logic/net/URLConstants.h"
LegacyUpdate::LegacyUpdate(BaseInstance *inst, QObject *parent) : Task(parent), m_inst(inst)
{
}
void LegacyUpdate::executeTask()
{
/*
if(m_only_prepare)
{
// FIXME: think this through some more.
LegacyInstance *inst = (LegacyInstance *)m_inst;
if (!inst->shouldUpdate() || inst->shouldUseCustomBaseJar())
{
ModTheJar();
}
else
{
emitSucceeded();
}
}
else
{
*/
lwjglStart();
//}
}
void LegacyUpdate::lwjglStart()
{
LegacyInstance *inst = (LegacyInstance *)m_inst;
lwjglVersion = inst->lwjglVersion();
lwjglTargetPath = PathCombine(MMC->settings()->get("LWJGLDir").toString(), lwjglVersion);
lwjglNativesPath = PathCombine(lwjglTargetPath, "natives");
// if the 'done' file exists, we don't have to download this again
QFileInfo doneFile(PathCombine(lwjglTargetPath, "done"));
if (doneFile.exists())
{
jarStart();
return;
}
auto list = MMC->lwjgllist();
if (!list->isLoaded())
{
emitFailed("Too soon! Let the LWJGL list load :)");
return;
}
setStatus(tr("Downloading new LWJGL..."));
auto version = list->getVersion(lwjglVersion);
if (!version)
{
emitFailed("Game update failed: the selected LWJGL version is invalid.");
return;
}
QString url = version->url();
QUrl realUrl(url);
QString hostname = realUrl.host();
auto worker = MMC->qnam();
QNetworkRequest req(realUrl);
req.setRawHeader("Host", hostname.toLatin1());
req.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)");
QNetworkReply *rep = worker->get(req);
m_reply = std::shared_ptr<QNetworkReply>(rep);
connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
connect(worker.get(), SIGNAL(finished(QNetworkReply *)),
SLOT(lwjglFinished(QNetworkReply *)));
// connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
// SLOT(downloadError(QNetworkReply::NetworkError)));
}
void LegacyUpdate::lwjglFinished(QNetworkReply *reply)
{
if (m_reply.get() != reply)
{
return;
}
if (reply->error() != QNetworkReply::NoError)
{
emitFailed("Failed to download: " + reply->errorString() +
"\nSometimes you have to wait a bit if you download many LWJGL versions in "
"a row. YMMV");
return;
}
auto worker = MMC->qnam();
// Here i check if there is a cookie for me in the reply and extract it
QList<QNetworkCookie> cookies =
qvariant_cast<QList<QNetworkCookie>>(reply->header(QNetworkRequest::SetCookieHeader));
if (cookies.count() != 0)
{
// you must tell which cookie goes with which url
worker->cookieJar()->setCookiesFromUrl(cookies, QUrl("sourceforge.net"));
}
// here you can check for the 302 or whatever other header i need
QVariant newLoc = reply->header(QNetworkRequest::LocationHeader);
if (newLoc.isValid())
{
QString redirectedTo = reply->header(QNetworkRequest::LocationHeader).toString();
QUrl realUrl(redirectedTo);
QString hostname = realUrl.host();
QNetworkRequest req(redirectedTo);
req.setRawHeader("Host", hostname.toLatin1());
req.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)");
QNetworkReply *rep = worker->get(req);
connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
SIGNAL(progress(qint64, qint64)));
m_reply = std::shared_ptr<QNetworkReply>(rep);
return;
}
QFile saveMe("lwjgl.zip");
saveMe.open(QIODevice::WriteOnly);
saveMe.write(m_reply->readAll());
saveMe.close();
setStatus(tr("Installing new LWJGL..."));
extractLwjgl();
jarStart();
}
void LegacyUpdate::extractLwjgl()
{
// make sure the directories are there
bool success = ensureFolderPathExists(lwjglNativesPath);
if (!success)
{
emitFailed("Failed to extract the lwjgl libs - error when creating required folders.");
return;
}
QuaZip zip("lwjgl.zip");
if (!zip.open(QuaZip::mdUnzip))
{
emitFailed("Failed to extract the lwjgl libs - not a valid archive.");
return;
}
// and now we are going to access files inside it
QuaZipFile file(&zip);
const QString jarNames[] = {"jinput.jar", "lwjgl_util.jar", "lwjgl.jar"};
for (bool more = zip.goToFirstFile(); more; more = zip.goToNextFile())
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
emitFailed("Failed to extract the lwjgl libs - error while reading archive.");
return;
}
QuaZipFileInfo info;
QString name = file.getActualFileName();
if (name.endsWith('/'))
{
file.close();
continue;
}
QString destFileName;
// Look for the jars
for (int i = 0; i < 3; i++)
{
if (name.endsWith(jarNames[i]))
{
destFileName = PathCombine(lwjglTargetPath, jarNames[i]);
}
}
// Not found? look for the natives
if (destFileName.isEmpty())
{
#ifdef Q_OS_WIN32
QString nativesDir = "windows";
#else
#ifdef Q_OS_MAC
QString nativesDir = "macosx";
#else
QString nativesDir = "linux";
#endif
#endif
if (name.contains(nativesDir))
{
int lastSlash = name.lastIndexOf('/');
int lastBackSlash = name.lastIndexOf('\\');
if (lastSlash != -1)
name = name.mid(lastSlash + 1);
else if (lastBackSlash != -1)
name = name.mid(lastBackSlash + 1);
destFileName = PathCombine(lwjglNativesPath, name);
}
}
// Now if destFileName is still empty, go to the next file.
if (!destFileName.isEmpty())
{
setStatus(tr("Installing new LWJGL - extracting ") + name + "...");
QFile output(destFileName);
output.open(QIODevice::WriteOnly);
output.write(file.readAll()); // FIXME: wste of memory!?
output.close();
}
file.close(); // do not forget to close!
}
zip.close();
m_reply.reset();
QFile doneFile(PathCombine(lwjglTargetPath, "done"));
doneFile.open(QIODevice::WriteOnly);
doneFile.write("done.");
doneFile.close();
}
void LegacyUpdate::lwjglFailed()
{
emitFailed("Bad stuff happened while trying to get the lwjgl libs...");
}
void LegacyUpdate::jarStart()
{
LegacyInstance *inst = (LegacyInstance *)m_inst;
if (!inst->shouldUpdate() || inst->shouldUseCustomBaseJar())
{
ModTheJar();
return;
}
setStatus(tr("Checking for jar updates..."));
// Make directories
QDir binDir(inst->binDir());
if (!binDir.exists() && !binDir.mkpath("."))
{
emitFailed("Failed to create bin folder.");
return;
}
// Build a list of URLs that will need to be downloaded.
setStatus(tr("Downloading new minecraft.jar ..."));
QString version_id = inst->intendedVersionId();
QString localPath = version_id + "/" + version_id + ".jar";
QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + localPath;
auto dljob = new NetJob("Minecraft.jar for version " + version_id);
auto metacache = MMC->metacache();
auto entry = metacache->resolveEntry("versions", localPath);
dljob->addNetAction(CacheDownload::make(QUrl(urlstr), entry));
connect(dljob, SIGNAL(succeeded()), SLOT(jarFinished()));
connect(dljob, SIGNAL(failed()), SLOT(jarFailed()));
connect(dljob, SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
legacyDownloadJob.reset(dljob);
legacyDownloadJob->start();
}
void LegacyUpdate::jarFinished()
{
// process the jar
ModTheJar();
}
void LegacyUpdate::jarFailed()
{
// bad, bad
emitFailed("Failed to download the minecraft jar. Try again later.");
}
bool LegacyUpdate::MergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained,
MetainfAction metainf)
{
setStatus(tr("Installing mods: Adding ") + from.fileName() + " ...");
QuaZip modZip(from.filePath());
modZip.open(QuaZip::mdUnzip);
QuaZipFile fileInsideMod(&modZip);
QuaZipFile zipOutFile(into);
for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile())
{
QString filename = modZip.getCurrentFileName();
if (filename.contains("META-INF") && metainf == LegacyUpdate::IgnoreMetainf)
{
QLOG_INFO() << "Skipping META-INF " << filename << " from " << from.fileName();
continue;
}
if (contained.contains(filename))
{
QLOG_INFO() << "Skipping already contained file " << filename << " from "
<< from.fileName();
continue;
}
contained.insert(filename);
QLOG_INFO() << "Adding file " << filename << " from " << from.fileName();
if (!fileInsideMod.open(QIODevice::ReadOnly))
{
QLOG_ERROR() << "Failed to open " << filename << " from " << from.fileName();
return false;
}
/*
QuaZipFileInfo old_info;
fileInsideMod.getFileInfo(&old_info);
*/
QuaZipNewInfo info_out(fileInsideMod.getActualFileName());
/*
info_out.externalAttr = old_info.externalAttr;
*/
if (!zipOutFile.open(QIODevice::WriteOnly, info_out))
{
QLOG_ERROR() << "Failed to open " << filename << " in the jar";
fileInsideMod.close();
return false;
}
if (!JlCompress::copyData(fileInsideMod, zipOutFile))
{
zipOutFile.close();
fileInsideMod.close();
QLOG_ERROR() << "Failed to copy data of " << filename << " into the jar";
return false;
}
zipOutFile.close();
fileInsideMod.close();
}
return true;
}
void LegacyUpdate::ModTheJar()
{
LegacyInstance *inst = (LegacyInstance *)m_inst;
if (!inst->shouldRebuild())
{
emitSucceeded();
return;
}
// Get the mod list
auto modList = inst->jarModList();
QFileInfo runnableJar(inst->runnableJar());
QFileInfo baseJar(inst->baseJar());
bool base_is_custom = inst->shouldUseCustomBaseJar();
// Nothing to do if there are no jar mods to install, no backup and just the mc jar
if (base_is_custom)
{
// yes, this can happen if the instance only has the runnable jar and not the base jar
// it *could* be assumed that such an instance is vanilla, but that wouldn't be safe
// because that's not something mmc4 guarantees
if (runnableJar.isFile() && !baseJar.exists() && modList->empty())
{
inst->setShouldRebuild(false);
emitSucceeded();
return;
}
setStatus(tr("Installing mods: Backing up minecraft.jar ..."));
if (!baseJar.exists() && !QFile::copy(runnableJar.filePath(), baseJar.filePath()))
{
emitFailed("It seems both the active and base jar are gone. A fresh base jar will "
"be used on next run.");
inst->setShouldRebuild(true);
inst->setShouldUpdate(true);
inst->setShouldUseCustomBaseJar(false);
return;
}
}
if (!baseJar.exists())
{
emitFailed("The base jar " + baseJar.filePath() + " does not exist");
return;
}
if (runnableJar.exists() && !QFile::remove(runnableJar.filePath()))
{
emitFailed("Failed to delete old minecraft.jar");
return;
}
// TaskStep(); // STEP 1
setStatus(tr("Installing mods: Opening minecraft.jar ..."));
QuaZip zipOut(runnableJar.filePath());
if (!zipOut.open(QuaZip::mdCreate))
{
QFile::remove(runnableJar.filePath());
emitFailed("Failed to open the minecraft.jar for modding");
return;
}
// Files already added to the jar.
// These files will be skipped.
QSet<QString> addedFiles;
// Modify the jar
setStatus(tr("Installing mods: Adding mod files..."));
for (int i = modList->size() - 1; i >= 0; i--)
{
auto &mod = modList->operator[](i);
// do not merge disabled mods.
if (!mod.enabled())
continue;
if (mod.type() == Mod::MOD_ZIPFILE)
{
if (!MergeZipFiles(&zipOut, mod.filename(), addedFiles, LegacyUpdate::KeepMetainf))
{
zipOut.close();
QFile::remove(runnableJar.filePath());
emitFailed("Failed to add " + mod.filename().fileName() + " to the jar.");
return;
}
}
else if (mod.type() == Mod::MOD_SINGLEFILE)
{
auto filename = mod.filename();
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(),
filename.fileName()))
{
zipOut.close();
QFile::remove(runnableJar.filePath());
emitFailed("Failed to add " + filename.fileName() + " to the jar");
return;
}
addedFiles.insert(filename.fileName());
QLOG_INFO() << "Adding file " << filename.fileName() << " from "
<< filename.absoluteFilePath();
}
else if (mod.type() == Mod::MOD_FOLDER)
{
auto filename = mod.filename();
QString what_to_zip = filename.absoluteFilePath();
QDir dir(what_to_zip);
dir.cdUp();
QString parent_dir = dir.absolutePath();
if (!JlCompress::compressSubDir(&zipOut, what_to_zip, parent_dir, true, addedFiles))
{
zipOut.close();
QFile::remove(runnableJar.filePath());
emitFailed("Failed to add " + filename.fileName() + " to the jar");
return;
}
QLOG_INFO() << "Adding folder " << filename.fileName() << " from "
<< filename.absoluteFilePath();
}
}
if (!MergeZipFiles(&zipOut, baseJar, addedFiles, LegacyUpdate::IgnoreMetainf))
{
zipOut.close();
QFile::remove(runnableJar.filePath());
emitFailed("Failed to insert minecraft.jar contents.");
return;
}
// Recompress the jar
zipOut.close();
if (zipOut.getZipError() != 0)
{
QFile::remove(runnableJar.filePath());
emitFailed("Failed to finalize minecraft.jar!");
return;
}
inst->setShouldRebuild(false);
// inst->UpdateVersion(true);
emitSucceeded();
return;
}