Merge branch 'develop' of github.com:MultiMC/MultiMC5 into feature_news

Conflicts:
	CMakeLists.txt
	gui/MainWindow.h
This commit is contained in:
Forkk
2014-01-02 13:38:20 -06:00
153 changed files with 4702 additions and 2180 deletions

View File

@ -27,6 +27,7 @@
#include "pathutils.h"
#include "lists/MinecraftVersionList.h"
#include "logic/icons/IconList.h"
BaseInstance::BaseInstance(BaseInstancePrivate *d_in, const QString &rootDir,
SettingsObject *settings_obj, QObject *parent)
@ -36,10 +37,11 @@ BaseInstance::BaseInstance(BaseInstancePrivate *d_in, const QString &rootDir,
d->m_settings = settings_obj;
d->m_rootDir = rootDir;
settings().registerSetting(new Setting("name", "Unnamed Instance"));
settings().registerSetting(new Setting("iconKey", "default"));
settings().registerSetting(new Setting("notes", ""));
settings().registerSetting(new Setting("lastLaunchTime", 0));
settings().registerSetting("name", "Unnamed Instance");
settings().registerSetting("iconKey", "default");
connect(MMC->icons().get(), SIGNAL(iconUpdated(QString)), SLOT(iconUpdated(QString)));
settings().registerSetting("notes", "");
settings().registerSetting("lastLaunchTime", 0);
/*
* custom base jar has no default. it is determined in code... see the accessor methods for
@ -48,54 +50,45 @@ BaseInstance::BaseInstance(BaseInstancePrivate *d_in, const QString &rootDir,
* for instances that DO NOT have the CustomBaseJar setting (legacy instances),
* [.]minecraft/bin/mcbackup.jar is the default base jar
*/
settings().registerSetting(new Setting("UseCustomBaseJar", true));
settings().registerSetting(new Setting("CustomBaseJar", ""));
settings().registerSetting("UseCustomBaseJar", true);
settings().registerSetting("CustomBaseJar", "");
auto globalSettings = MMC->settings();
// Java Settings
settings().registerSetting(new Setting("OverrideJava", false));
settings().registerSetting(
new OverrideSetting("JavaPath", globalSettings->getSetting("JavaPath")));
settings().registerSetting(
new OverrideSetting("JvmArgs", globalSettings->getSetting("JvmArgs")));
settings().registerSetting("OverrideJava", false);
settings().registerOverride(globalSettings->getSetting("JavaPath"));
settings().registerOverride(globalSettings->getSetting("JvmArgs"));
// Custom Commands
settings().registerSetting(new Setting("OverrideCommands", false));
settings().registerSetting(new OverrideSetting(
"PreLaunchCommand", globalSettings->getSetting("PreLaunchCommand")));
settings().registerSetting(
new OverrideSetting("PostExitCommand", globalSettings->getSetting("PostExitCommand")));
settings().registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false);
settings().registerOverride(globalSettings->getSetting("PreLaunchCommand"));
settings().registerOverride(globalSettings->getSetting("PostExitCommand"));
// Window Size
settings().registerSetting(new Setting("OverrideWindow", false));
settings().registerSetting(
new OverrideSetting("LaunchMaximized", globalSettings->getSetting("LaunchMaximized")));
settings().registerSetting(new OverrideSetting(
"MinecraftWinWidth", globalSettings->getSetting("MinecraftWinWidth")));
settings().registerSetting(new OverrideSetting(
"MinecraftWinHeight", globalSettings->getSetting("MinecraftWinHeight")));
settings().registerSetting("OverrideWindow", false);
settings().registerOverride(globalSettings->getSetting("LaunchMaximized"));
settings().registerOverride(globalSettings->getSetting("MinecraftWinWidth"));
settings().registerOverride(globalSettings->getSetting("MinecraftWinHeight"));
// Memory
settings().registerSetting(new Setting("OverrideMemory", false));
settings().registerSetting(
new OverrideSetting("MinMemAlloc", globalSettings->getSetting("MinMemAlloc")));
settings().registerSetting(
new OverrideSetting("MaxMemAlloc", globalSettings->getSetting("MaxMemAlloc")));
settings().registerSetting(
new OverrideSetting("PermGen", globalSettings->getSetting("PermGen")));
// Auto login
settings().registerSetting(new Setting("OverrideLogin", false));
settings().registerSetting(
new OverrideSetting("AutoLogin", globalSettings->getSetting("AutoLogin")));
settings().registerSetting("OverrideMemory", false);
settings().registerOverride(globalSettings->getSetting("MinMemAlloc"));
settings().registerOverride(globalSettings->getSetting("MaxMemAlloc"));
settings().registerOverride(globalSettings->getSetting("PermGen"));
// Console
settings().registerSetting(new Setting("OverrideConsole", false));
settings().registerSetting(
new OverrideSetting("ShowConsole", globalSettings->getSetting("ShowConsole")));
settings().registerSetting(new OverrideSetting(
"AutoCloseConsole", globalSettings->getSetting("AutoCloseConsole")));
settings().registerSetting("OverrideConsole", false);
settings().registerOverride(globalSettings->getSetting("ShowConsole"));
settings().registerOverride(globalSettings->getSetting("AutoCloseConsole"));
}
void BaseInstance::iconUpdated(QString key)
{
if(iconKey() == key)
{
emit propertiesChanged(this);
}
}
void BaseInstance::nuke()

View File

@ -184,6 +184,9 @@ signals:
*/
void nuked(BaseInstance *inst);
protected slots:
void iconUpdated(QString key);
protected:
std::shared_ptr<BaseInstancePrivate> inst_d;
};

View File

@ -20,7 +20,9 @@
#include "BaseInstance.h"
#include "LegacyInstance.h"
#include "LegacyFTBInstance.h"
#include "OneSixInstance.h"
#include "OneSixFTBInstance.h"
#include "NostalgiaInstance.h"
#include "BaseVersion.h"
#include "MinecraftVersion.h"
@ -43,7 +45,7 @@ InstanceFactory::InstLoadError InstanceFactory::loadInstance(BaseInstance *&inst
{
auto m_settings = new INISettingsObject(PathCombine(instDir, "instance.cfg"));
m_settings->registerSetting(new Setting("InstanceType", "Legacy"));
m_settings->registerSetting("InstanceType", "Legacy");
QString inst_type = m_settings->get("InstanceType").toString();
@ -60,6 +62,14 @@ InstanceFactory::InstLoadError InstanceFactory::loadInstance(BaseInstance *&inst
{
inst = new NostalgiaInstance(instDir, m_settings, this);
}
else if (inst_type == "LegacyFTB")
{
inst = new LegacyFTBInstance(instDir, m_settings, this);
}
else if (inst_type == "OneSixFTB")
{
inst = new OneSixFTBInstance(instDir, m_settings, this);
}
else
{
return InstanceFactory::UnknownLoadError;
@ -69,7 +79,8 @@ InstanceFactory::InstLoadError InstanceFactory::loadInstance(BaseInstance *&inst
InstanceFactory::InstCreateError InstanceFactory::createInstance(BaseInstance *&inst,
BaseVersionPtr version,
const QString &instDir)
const QString &instDir,
const InstType type)
{
QDir rootDir(instDir);
@ -83,34 +94,65 @@ InstanceFactory::InstCreateError InstanceFactory::createInstance(BaseInstance *&
return InstanceFactory::NoSuchVersion;
auto m_settings = new INISettingsObject(PathCombine(instDir, "instance.cfg"));
m_settings->registerSetting(new Setting("InstanceType", "Legacy"));
m_settings->registerSetting("InstanceType", "Legacy");
switch (mcVer->type)
if (type == NormalInst)
{
case MinecraftVersion::Legacy:
m_settings->set("InstanceType", "Legacy");
inst = new LegacyInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor());
inst->setShouldUseCustomBaseJar(false);
break;
case MinecraftVersion::OneSix:
m_settings->set("InstanceType", "OneSix");
inst = new OneSixInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor());
inst->setShouldUseCustomBaseJar(false);
break;
case MinecraftVersion::Nostalgia:
m_settings->set("InstanceType", "Nostalgia");
inst = new NostalgiaInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor());
inst->setShouldUseCustomBaseJar(false);
break;
default:
switch (mcVer->type)
{
case MinecraftVersion::Legacy:
m_settings->set("InstanceType", "Legacy");
inst = new LegacyInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor());
inst->setShouldUseCustomBaseJar(false);
break;
case MinecraftVersion::OneSix:
m_settings->set("InstanceType", "OneSix");
inst = new OneSixInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor());
inst->setShouldUseCustomBaseJar(false);
break;
case MinecraftVersion::Nostalgia:
m_settings->set("InstanceType", "Nostalgia");
inst = new NostalgiaInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor());
inst->setShouldUseCustomBaseJar(false);
break;
default:
{
delete m_settings;
return InstanceFactory::NoSuchVersion;
}
}
}
else if (type == FTBInstance)
{
switch (mcVer->type)
{
case MinecraftVersion::Legacy:
m_settings->set("InstanceType", "LegacyFTB");
inst = new LegacyFTBInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor());
inst->setShouldUseCustomBaseJar(false);
break;
case MinecraftVersion::OneSix:
m_settings->set("InstanceType", "OneSixFTB");
inst = new OneSixFTBInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor());
inst->setShouldUseCustomBaseJar(false);
break;
default:
{
delete m_settings;
return InstanceFactory::NoSuchVersion;
}
}
}
else
{
delete m_settings;
return InstanceFactory::NoSuchVersion;
}
}
// FIXME: really, how do you even know?
return InstanceFactory::NoCreateError;
@ -128,7 +170,17 @@ InstanceFactory::InstCreateError InstanceFactory::copyInstance(BaseInstance *&ne
rootDir.removeRecursively();
return InstanceFactory::CantCreateDir;
}
auto m_settings = new INISettingsObject(PathCombine(instDir, "instance.cfg"));
m_settings->registerSetting("InstanceType", "Legacy");
QString inst_type = m_settings->get("InstanceType").toString();
if(inst_type == "OneSixFTB")
m_settings->set("InstanceType", "OneSix");
if(inst_type == "LegacyFTB")
m_settings->set("InstanceType", "Legacy");
auto error = loadInstance(newInstance, instDir);
switch (error)
{
case NoLoadError:

View File

@ -55,18 +55,25 @@ public:
CantCreateDir
};
enum InstType
{
NormalInst,
FTBInstance
};
/*!
* \brief Creates a stub instance
*
* \param inst Pointer to store the created instance in.
* \param inst Game version to use for the instance
* \param version Game version to use for the instance
* \param instDir The new instance's directory.
* \param type The type of instance to create
* \return An InstCreateError error code.
* - InstExists if the given instance directory is already an instance.
* - CantCreateDir if the given instance directory cannot be created.
*/
InstCreateError createInstance(BaseInstance *&inst, BaseVersionPtr version,
const QString &instDir);
const QString &instDir, const InstType type = NormalInst);
/*!
* \brief Creates a copy of an existing instance with a new name

View File

@ -99,6 +99,7 @@ void JavaChecker::error(QProcess::ProcessError err)
if(err == QProcess::FailedToStart)
{
killTimer.stop();
checkerJar.remove();
JavaCheckResult result;
{
@ -116,6 +117,5 @@ void JavaChecker::timeout()
if(process)
{
process->kill();
process.reset();
}
}

View File

@ -177,10 +177,10 @@ QList<QString> JavaUtils::FindJavaPaths()
#elif OSX
QList<QString> JavaUtils::FindJavaPaths()
{
QLOG_INFO() << "OS X Java detection incomplete - defaulting to \"java\"";
QList<QString> javas;
javas.append(this->GetDefaultJava()->path);
javas.append("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java");
javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java");
return javas;
}

View File

@ -0,0 +1,16 @@
#include "LegacyFTBInstance.h"
LegacyFTBInstance::LegacyFTBInstance(const QString &rootDir, SettingsObject *settings, QObject *parent) :
LegacyInstance(rootDir, settings, parent)
{
}
QString LegacyFTBInstance::getStatusbarDescription()
{
return "Legacy FTB: " + intendedVersionId();
}
bool LegacyFTBInstance::menuActionEnabled(QString action_name) const
{
return false;
}

13
logic/LegacyFTBInstance.h Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include "LegacyInstance.h"
class LegacyFTBInstance : public LegacyInstance
{
Q_OBJECT
public:
explicit LegacyFTBInstance(const QString &rootDir, SettingsObject *settings,
QObject *parent = 0);
virtual QString getStatusbarDescription();
virtual bool menuActionEnabled(QString action_name) const;
};

View File

@ -27,7 +27,7 @@
#include "logic/MinecraftProcess.h"
#include "logic/LegacyUpdate.h"
#include "logic/lists/IconList.h"
#include "logic/icons/IconList.h"
#include "gui/dialogs/LegacyModEditDialog.h"
@ -37,11 +37,11 @@ LegacyInstance::LegacyInstance(const QString &rootDir, SettingsObject *settings,
QObject *parent)
: BaseInstance(new LegacyInstancePrivate(), rootDir, settings, parent)
{
settings->registerSetting(new Setting("NeedsRebuild", true));
settings->registerSetting(new Setting("ShouldUpdate", false));
settings->registerSetting(new Setting("JarVersion", "Unknown"));
settings->registerSetting(new Setting("LwjglVersion", "2.9.0"));
settings->registerSetting(new Setting("IntendedJarVersion", ""));
settings->registerSetting("NeedsRebuild", true);
settings->registerSetting("ShouldUpdate", false);
settings->registerSetting("JarVersion", "Unknown");
settings->registerSetting("LwjglVersion", "2.9.0");
settings->registerSetting("IntendedJarVersion", "");
}
std::shared_ptr<Task> LegacyInstance::doUpdate(bool only_prepare)
@ -150,6 +150,7 @@ std::shared_ptr<ModList> LegacyInstance::jarModList()
void LegacyInstance::jarModsChanged()
{
QLOG_INFO() << "Jar mods of instance " << name() << " have changed. Jar will be rebuilt.";
setShouldRebuild(true);
}

View File

@ -76,7 +76,7 @@ void LegacyUpdate::lwjglStart()
return;
}
setStatus("Downloading new LWJGL.");
setStatus(tr("Downloading new LWJGL..."));
auto version = list->getVersion(lwjglVersion);
if (!version)
{
@ -144,7 +144,7 @@ void LegacyUpdate::lwjglFinished(QNetworkReply *reply)
saveMe.open(QIODevice::WriteOnly);
saveMe.write(m_reply->readAll());
saveMe.close();
setStatus("Installing new LWJGL...");
setStatus(tr("Installing new LWJGL..."));
extractLwjgl();
jarStart();
}
@ -220,7 +220,7 @@ void LegacyUpdate::extractLwjgl()
// Now if destFileName is still empty, go to the next file.
if (!destFileName.isEmpty())
{
setStatus("Installing new LWJGL - Extracting " + name);
setStatus(tr("Installing new LWJGL - extracting ") + name + "...");
QFile output(destFileName);
output.open(QIODevice::WriteOnly);
output.write(file.readAll()); // FIXME: wste of memory!?
@ -250,7 +250,7 @@ void LegacyUpdate::jarStart()
return;
}
setStatus("Checking for jar updates...");
setStatus(tr("Checking for jar updates..."));
// Make directories
QDir binDir(inst->binDir());
if (!binDir.exists() && !binDir.mkpath("."))
@ -260,7 +260,7 @@ void LegacyUpdate::jarStart()
}
// Build a list of URLs that will need to be downloaded.
setStatus("Downloading new minecraft.jar");
setStatus(tr("Downloading new minecraft.jar ..."));
QString version_id = inst->intendedVersionId();
QString localPath = version_id + "/" + version_id + ".jar";
@ -294,7 +294,7 @@ void LegacyUpdate::jarFailed()
bool LegacyUpdate::MergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained,
MetainfAction metainf)
{
setStatus("Installing mods - Adding " + from.fileName());
setStatus(tr("Installing mods: Adding ") + from.fileName() + " ...");
QuaZip modZip(from.filePath());
modZip.open(QuaZip::mdUnzip);
@ -380,7 +380,7 @@ void LegacyUpdate::ModTheJar()
return;
}
setStatus("Installing mods - backing up minecraft.jar...");
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 "
@ -405,7 +405,7 @@ void LegacyUpdate::ModTheJar()
}
// TaskStep(); // STEP 1
setStatus("Installing mods - Opening minecraft.jar");
setStatus(tr("Installing mods: Opening minecraft.jar ..."));
QuaZip zipOut(runnableJar.filePath());
if (!zipOut.open(QuaZip::mdCreate))
@ -419,10 +419,15 @@ void LegacyUpdate::ModTheJar()
QSet<QString> addedFiles;
// Modify the jar
setStatus("Installing mods - Adding mod files...");
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))

View File

@ -0,0 +1,102 @@
/* 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 "LiteLoaderInstaller.h"
#include "OneSixVersion.h"
#include "OneSixLibrary.h"
QMap<QString, QString> LiteLoaderInstaller::m_launcherWrapperVersionMapping;
LiteLoaderInstaller::LiteLoaderInstaller(const QString &mcVersion) : m_mcVersion(mcVersion)
{
if (m_launcherWrapperVersionMapping.isEmpty())
{
m_launcherWrapperVersionMapping["1.6.2"] = "1.3";
m_launcherWrapperVersionMapping["1.6.4"] = "1.8";
//m_launcherWrapperVersionMapping["1.7.2"] = "1.8";
//m_launcherWrapperVersionMapping["1.7.4"] = "1.8";
}
}
bool LiteLoaderInstaller::canApply() const
{
return m_launcherWrapperVersionMapping.contains(m_mcVersion);
}
bool LiteLoaderInstaller::apply(std::shared_ptr<OneSixVersion> to)
{
to->externalUpdateStart();
applyLaunchwrapper(to);
applyLiteLoader(to);
to->mainClass = "net.minecraft.launchwrapper.Launch";
if (!to->minecraftArguments.contains(
" --tweakClass com.mumfrey.liteloader.launch.LiteLoaderTweaker"))
{
to->minecraftArguments.append(
" --tweakClass com.mumfrey.liteloader.launch.LiteLoaderTweaker");
}
to->externalUpdateFinish();
return to->toOriginalFile();
}
void LiteLoaderInstaller::applyLaunchwrapper(std::shared_ptr<OneSixVersion> to)
{
const QString intendedVersion = m_launcherWrapperVersionMapping[m_mcVersion];
QMutableListIterator<std::shared_ptr<OneSixLibrary>> it(to->libraries);
while (it.hasNext())
{
it.next();
if (it.value()->rawName().startsWith("net.minecraft:launchwrapper:"))
{
if (it.value()->version() >= intendedVersion)
{
return;
}
else
{
it.remove();
}
}
}
std::shared_ptr<OneSixLibrary> lib(new OneSixLibrary(
"net.minecraft:launchwrapper:" + m_launcherWrapperVersionMapping[m_mcVersion]));
lib->finalize();
to->libraries.prepend(lib);
}
void LiteLoaderInstaller::applyLiteLoader(std::shared_ptr<OneSixVersion> to)
{
QMutableListIterator<std::shared_ptr<OneSixLibrary>> it(to->libraries);
while (it.hasNext())
{
it.next();
if (it.value()->rawName().startsWith("com.mumfrey:liteloader:"))
{
it.remove();
}
}
std::shared_ptr<OneSixLibrary> lib(
new OneSixLibrary("com.mumfrey:liteloader:" + m_mcVersion));
lib->setBaseUrl("http://dl.liteloader.com/versions/");
lib->finalize();
to->libraries.prepend(lib);
}

View File

@ -0,0 +1,39 @@
/* 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.
*/
#pragma once
#include <QString>
#include <QMap>
#include <memory>
class OneSixVersion;
class LiteLoaderInstaller
{
public:
LiteLoaderInstaller(const QString &mcVersion);
bool canApply() const;
bool apply(std::shared_ptr<OneSixVersion> to);
private:
QString m_mcVersion;
void applyLaunchwrapper(std::shared_ptr<OneSixVersion> to);
void applyLiteLoader(std::shared_ptr<OneSixVersion> to);
static QMap<QString, QString> m_launcherWrapperVersionMapping;
};

View File

@ -35,20 +35,45 @@ Mod::Mod(const QFileInfo &file)
void Mod::repath(const QFileInfo &file)
{
m_file = file;
m_name = file.completeBaseName();
m_id = file.fileName();
QString name_base = file.fileName();
m_type = Mod::MOD_UNKNOWN;
if (m_file.isDir())
{
m_type = MOD_FOLDER;
m_name = name_base;
m_mmc_id = name_base;
}
else if (m_file.isFile())
{
QString ext = m_file.suffix().toLower();
if (ext == "zip" || ext == "jar")
m_type = MOD_ZIPFILE;
if (name_base.endsWith(".disabled"))
{
m_enabled = false;
name_base.chop(9);
}
else
{
m_enabled = true;
}
m_mmc_id = name_base;
if (name_base.endsWith(".zip") || name_base.endsWith(".jar"))
{
m_type = MOD_ZIPFILE;
name_base.chop(4);
}
else if (name_base.endsWith(".litemod"))
{
m_type = MOD_LITEMOD;
name_base.chop(8);
}
else
{
m_type = MOD_SINGLEFILE;
}
m_name = name_base;
}
if (m_type == MOD_ZIPFILE)
{
QuaZip zip(m_file.filePath());
@ -59,7 +84,7 @@ void Mod::repath(const QFileInfo &file)
if (zip.setCurrentFile("mcmod.info"))
{
if(!file.open(QIODevice::ReadOnly))
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
@ -100,6 +125,27 @@ void Mod::repath(const QFileInfo &file)
ReadMCModInfo(data);
}
}
else if (m_type == MOD_LITEMOD)
{
QuaZip zip(m_file.filePath());
if (!zip.open(QuaZip::mdUnzip))
return;
QuaZipFile file(&zip);
if (zip.setCurrentFile("litemod.json"))
{
if (!file.open(QIODevice::ReadOnly))
{
zip.close();
return;
}
ReadLiteModInfo(file.readAll());
file.close();
}
zip.close();
}
}
// NEW format
@ -114,7 +160,7 @@ void Mod::ReadMCModInfo(QByteArray contents)
if (!arr.at(0).isObject())
return;
auto firstObj = arr.at(0).toObject();
m_id = firstObj.value("modid").toString();
m_mod_id = firstObj.value("modid").toString();
m_name = firstObj.value("name").toString();
m_version = firstObj.value("version").toString();
m_homeurl = firstObj.value("url").toString();
@ -132,8 +178,7 @@ void Mod::ReadMCModInfo(QByteArray contents)
}
m_credits = firstObj.value("credits").toString();
return;
}
;
};
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
// this is the very old format that had just the array
@ -163,7 +208,7 @@ void Mod::ReadForgeInfo(QByteArray contents)
{
// Read the data
m_name = "Minecraft Forge";
m_id = "Forge";
m_mod_id = "Forge";
m_homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini;
if (!ini.loadFile(contents))
@ -177,15 +222,40 @@ void Mod::ReadForgeInfo(QByteArray contents)
m_version = major + "." + minor + "." + revision + "." + build;
}
void Mod::ReadLiteModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
if(object.contains("name"))
{
m_mod_id = m_name = object.value("name").toString();
}
if(object.contains("version"))
{
m_version=object.value("version").toString("");
}
else
{
m_version=object.value("revision").toString("");
}
m_mcversion = object.value("mcversion").toString();
m_authors = object.value("author").toString();
m_description = object.value("description").toString();
m_homeurl = object.value("url").toString();
}
bool Mod::replace(Mod &with)
{
if (!destroy())
return false;
bool success = false;
auto t = with.type();
if (t == MOD_ZIPFILE || t == MOD_SINGLEFILE)
{
success = QFile::copy(with.m_file.filePath(), m_file.path());
QLOG_DEBUG() << "Copy: " << with.m_file.filePath() << " to " << m_file.filePath();
success = QFile::copy(with.m_file.filePath(), m_file.filePath());
}
if (t == MOD_FOLDER)
{
@ -193,11 +263,17 @@ bool Mod::replace(Mod &with)
}
if (success)
{
m_id = with.m_id;
m_mcversion = with.m_mcversion;
m_type = with.m_type;
m_name = with.m_name;
m_mmc_id = with.m_mmc_id;
m_mod_id = with.m_mod_id;
m_version = with.m_version;
m_mcversion = with.m_mcversion;
m_description = with.m_description;
m_authors = with.m_authors;
m_credits = with.m_credits;
m_homeurl = with.m_homeurl;
m_type = with.m_type;
m_file.refresh();
}
return success;
}
@ -232,6 +308,7 @@ QString Mod::version() const
switch (type())
{
case MOD_ZIPFILE:
case MOD_LITEMOD:
return m_version;
case MOD_FOLDER:
return "Folder";
@ -241,3 +318,41 @@ QString Mod::version() const
return "VOID";
}
}
bool Mod::enable(bool value)
{
if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
return false;
if (m_enabled == value)
return false;
QString path = m_file.absoluteFilePath();
if (value)
{
QFile foo(path);
if (!path.endsWith(".disabled"))
return false;
path.chop(9);
if (!foo.rename(path))
return false;
}
else
{
QFile foo(path);
path += ".disabled";
if (!foo.rename(path))
return false;
}
m_file = QFileInfo(path);
m_enabled = value;
return true;
}
bool Mod::operator==(const Mod &other) const
{
return mmc_id() == other.mmc_id();
}
bool Mod::strongCompare(const Mod &other) const
{
return mmc_id() == other.mmc_id() && version() == other.version() && type() == other.type();
}

View File

@ -25,6 +25,7 @@ public:
MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files.
MOD_SINGLEFILE, //!< The mod is a single file (not a zip file).
MOD_FOLDER, //!< The mod is in a folder on the filesystem.
MOD_LITEMOD, //!< The mod is a litemod
};
Mod(const QFileInfo &file);
@ -33,9 +34,13 @@ public:
{
return m_file;
}
QString id() const
QString mmc_id() const
{
return m_id;
return m_mmc_id;
}
QString mod_id() const
{
return m_mod_id;
}
ModType type() const
{
@ -77,6 +82,13 @@ public:
return m_credits;
}
bool enabled() const
{
return m_enabled;
}
bool enable(bool value);
// delete all the files of this mod
bool destroy();
// replace this mod with a copy of the other
@ -85,19 +97,13 @@ public:
void repath(const QFileInfo &file);
// WEAK compare operator - used for replacing mods
bool operator==(const Mod &other) const
{
return filename() == other.filename();
}
bool strongCompare(const Mod &other) const
{
return filename() == other.filename() && id() == other.id() &&
version() == other.version() && type() == other.type();
}
bool operator==(const Mod &other) const;
bool strongCompare(const Mod &other) const;
private:
void ReadMCModInfo(QByteArray contents);
void ReadForgeInfo(QByteArray contents);
void ReadLiteModInfo(QByteArray contents);
protected:
@ -108,7 +114,9 @@ protected:
*/
QFileInfo m_file;
QString m_id;
QString m_mmc_id;
QString m_mod_id;
bool m_enabled = true;
QString m_name;
QString m_version;
QString m_mcversion;

View File

@ -19,6 +19,7 @@
#include <QMimeData>
#include <QUrl>
#include <QUuid>
#include <QString>
#include <QFileSystemWatcher>
#include "logger/QsLog.h"
@ -27,7 +28,7 @@ ModList::ModList(const QString &dir, const QString &list_file)
{
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs |
QDir::NoSymLinks);
m_dir.setSorting(QDir::Name);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_list_id = QUuid::createUuid().toString();
m_watcher = new QFileSystemWatcher(this);
is_watching = false;
@ -66,52 +67,89 @@ bool ModList::update()
if (!isValid())
return false;
QList<Mod> orderedMods;
QList<Mod> newMods;
m_dir.refresh();
auto folderContents = m_dir.entryInfoList();
bool orderWasInvalid = false;
bool orderOrStateChanged = false;
// first, process the ordered items (if any)
QStringList listOrder = readListFile();
OrderList listOrder = readListFile();
for (auto item : listOrder)
{
QFileInfo info(m_dir.filePath(item));
int idx = folderContents.indexOf(info);
QFileInfo infoEnabled(m_dir.filePath(item.id));
QFileInfo infoDisabled(m_dir.filePath(item.id + ".disabled"));
int idxEnabled = folderContents.indexOf(infoEnabled);
int idxDisabled = folderContents.indexOf(infoDisabled);
bool isEnabled;
// if both enabled and disabled versions are present, it's a special case...
if (idxEnabled >= 0 && idxDisabled >= 0)
{
// we only process the one we actually have in the order file.
// and exactly as we have it.
// THIS IS A CORNER CASE
isEnabled = item.enabled;
}
else
{
// only one is present.
// we pick the one that we found.
// we assume the mod was enabled/disabled by external means
isEnabled = idxEnabled >= 0;
}
int idx = isEnabled ? idxEnabled : idxDisabled;
QFileInfo & info = isEnabled ? infoEnabled : infoDisabled;
// if the file from the index file exists
if (idx != -1)
{
// remove from the actual folder contents list
folderContents.takeAt(idx);
// append the new mod
newMods.append(Mod(info));
orderedMods.append(Mod(info));
if (isEnabled != item.enabled)
orderOrStateChanged = true;
}
else
{
orderWasInvalid = true;
orderOrStateChanged = true;
}
}
for (auto entry : folderContents)
// if there are any untracked files...
if (folderContents.size())
{
newMods.append(Mod(entry));
}
if (mods.size() != newMods.size())
{
orderWasInvalid = true;
}
else
for (int i = 0; i < mods.size(); i++)
// the order surely changed!
for (auto entry : folderContents)
{
if (!mods[i].strongCompare(newMods[i]))
{
orderWasInvalid = true;
break;
}
newMods.append(Mod(entry));
}
beginResetModel();
mods.swap(newMods);
endResetModel();
if (orderWasInvalid)
std::sort(newMods.begin(), newMods.end(), [](const Mod & left, const Mod & right)
{ return left.name().localeAwareCompare(right.name()) <= 0; });
orderedMods.append(newMods);
orderOrStateChanged = true;
}
// otherwise, if we were already tracking some mods
else if (mods.size())
{
// if the number doesn't match, order changed.
if (mods.size() != orderedMods.size())
orderOrStateChanged = true;
// if it does match, compare the mods themselves
else
for (int i = 0; i < mods.size(); i++)
{
if (!mods[i].strongCompare(orderedMods[i]))
{
orderOrStateChanged = true;
break;
}
}
}
beginResetModel();
mods.swap(orderedMods);
endResetModel();
if (orderOrStateChanged && !m_list_file.isEmpty())
{
QLOG_INFO() << "Mod list " << m_list_file << " changed!";
saveListFile();
emit changed();
}
@ -123,17 +161,19 @@ void ModList::directoryChanged(QString path)
update();
}
QStringList ModList::readListFile()
ModList::OrderList ModList::readListFile()
{
QStringList stringList;
OrderList itemList;
if (m_list_file.isNull() || m_list_file.isEmpty())
return stringList;
return itemList;
QFile textFile(m_list_file);
if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text))
return QStringList();
return OrderList();
QTextStream textStream(&textFile);
QTextStream textStream;
textStream.setAutoDetectUnicode(true);
textStream.setDevice(&textFile);
while (true)
{
QString line = textStream.readLine();
@ -141,11 +181,18 @@ QStringList ModList::readListFile()
break;
else
{
stringList.append(line);
OrderItem it;
it.enabled = !line.endsWith(".disabled");
if (!it.enabled)
{
line.chop(9);
}
it.id = line;
itemList.append(it);
}
}
textFile.close();
return stringList;
return itemList;
}
bool ModList::saveListFile()
@ -155,12 +202,16 @@ bool ModList::saveListFile()
QFile textFile(m_list_file);
if (!textFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
return false;
QTextStream textStream(&textFile);
QTextStream textStream;
textStream.setGenerateByteOrderMark(true);
textStream.setCodec("UTF-8");
textStream.setDevice(&textFile);
for (auto mod : mods)
{
auto pathname = mod.filename();
QString filename = pathname.fileName();
textStream << filename << endl;
textStream << mod.mmc_id();
if (!mod.enabled())
textStream << ".disabled";
textStream << endl;
}
textFile.close();
return false;
@ -185,6 +236,9 @@ bool ModList::installMod(const QFileInfo &filename, int index)
int idx = mods.indexOf(m);
if (idx != -1)
{
int idx2 = mods.indexOf(m,idx+1);
if(idx2 != -1)
return false;
if (mods[idx].replace(m))
{
@ -201,7 +255,7 @@ bool ModList::installMod(const QFileInfo &filename, int index)
auto type = m.type();
if (type == Mod::MOD_UNKNOWN)
return false;
if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE)
if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
{
QString newpath = PathCombine(m_dir.path(), filename.fileName());
if (!QFile::copy(filename.filePath(), newpath))
@ -327,7 +381,7 @@ bool ModList::moveModsDown(int first, int last)
int ModList::columnCount(const QModelIndex &parent) const
{
return 2;
return 3;
}
QVariant ModList::data(const QModelIndex &index, int role) const
@ -341,43 +395,96 @@ QVariant ModList::data(const QModelIndex &index, int role) const
if (row < 0 || row >= mods.size())
return QVariant();
if (role != Qt::DisplayRole)
return QVariant();
switch (column)
switch (role)
{
case 0:
return mods[row].name();
case 1:
return mods[row].version();
case 2:
return mods[row].mcversion();
case Qt::DisplayRole:
switch (index.column())
{
case NameColumn:
return mods[row].name();
case VersionColumn:
return mods[row].version();
default:
return QVariant();
}
case Qt::ToolTipRole:
return mods[row].mmc_id();
case Qt::CheckStateRole:
switch (index.column())
{
case ActiveColumn:
return mods[row].enabled();
default:
return QVariant();
}
default:
return QVariant();
}
}
bool ModList::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
{
return false;
}
if (role == Qt::CheckStateRole)
{
auto &mod = mods[index.row()];
if (mod.enable(!mod.enabled()))
{
emit dataChanged(index, index);
return true;
}
}
return false;
}
QVariant ModList::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role != Qt::DisplayRole || orientation != Qt::Horizontal)
return QVariant();
switch (section)
switch (role)
{
case 0:
return QString("Name");
case 1:
return QString("Version");
case 2:
return QString("Minecraft");
case Qt::DisplayRole:
switch (section)
{
case ActiveColumn:
return QString();
case NameColumn:
return QString("Name");
case VersionColumn:
return QString("Version");
default:
return QVariant();
}
case Qt::ToolTipRole:
switch (section)
{
case ActiveColumn:
return "Is the mod enabled?";
case NameColumn:
return "The name of the mod.";
case VersionColumn:
return "The version of the mod.";
default:
return QVariant();
}
default:
return QVariant();
}
return QString();
return QVariant();
}
Qt::ItemFlags ModList::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
if (index.isValid())
return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled |
defaultFlags;
else
return Qt::ItemIsDropEnabled | defaultFlags;
}
@ -456,6 +563,14 @@ bool ModList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row
QString filename = url.toLocalFile();
installMod(filename, row);
QLOG_INFO() << "installing: " << filename;
// if there is no ordering, re-sort the list
if (m_list_file.isEmpty())
{
beginResetModel();
std::sort(mods.begin(), mods.end(), [](const Mod & left, const Mod & right)
{ return left.name().localeAwareCompare(right.name()) <= 0; });
endResetModel();
}
}
if (was_watching)
startWatching();

View File

@ -34,9 +34,18 @@ class ModList : public QAbstractListModel
{
Q_OBJECT
public:
enum Columns
{
ActiveColumn = 0,
NameColumn,
VersionColumn
};
ModList(const QString &dir, const QString &list_file = QString());
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
virtual bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole);
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const
{
return size();
@ -59,7 +68,6 @@ public:
{
return mods[index];
}
;
/// Reloads the mod list and returns true if the list changed.
virtual bool update();
@ -119,7 +127,13 @@ public:
}
private:
QStringList readListFile();
struct OrderItem
{
QString id;
bool enabled = false;
};
typedef QList<OrderItem> OrderList;
OrderList readListFile();
bool saveListFile();
private
slots:

120
logic/OneSixFTBInstance.cpp Normal file
View File

@ -0,0 +1,120 @@
#include "OneSixFTBInstance.h"
#include "OneSixVersion.h"
#include "OneSixLibrary.h"
#include "tasks/SequentialTask.h"
#include "ForgeInstaller.h"
#include "lists/ForgeVersionList.h"
#include "MultiMC.h"
class OneSixFTBInstanceForge : public Task
{
Q_OBJECT
public:
explicit OneSixFTBInstanceForge(const QString &version, OneSixFTBInstance *inst, QObject *parent = 0) :
Task(parent), instance(inst), version("Forge " + version)
{
}
void executeTask()
{
for (int i = 0; i < MMC->forgelist()->count(); ++i)
{
if (MMC->forgelist()->at(i)->name() == version)
{
forgeVersion = std::dynamic_pointer_cast<ForgeVersion>(MMC->forgelist()->at(i));
break;
}
}
if (!forgeVersion)
{
emitFailed(QString("Couldn't find forge version ") + version );
return;
}
entry = MMC->metacache()->resolveEntry("minecraftforge", forgeVersion->filename);
if (entry->stale)
{
setStatus(tr("Downloading Forge..."));
fjob = new NetJob("Forge download");
fjob->addNetAction(CacheDownload::make(forgeVersion->installer_url, entry));
connect(fjob, &NetJob::failed, [this](){emitFailed(m_failReason);});
connect(fjob, &NetJob::succeeded, this, &OneSixFTBInstanceForge::installForge);
connect(fjob, &NetJob::progress, [this](qint64 c, qint64 total){ setProgress(100 * c / total); });
fjob->start();
}
else
{
installForge();
}
}
private
slots:
void installForge()
{
setStatus(tr("Installing Forge..."));
QString forgePath = entry->getFullPath();
ForgeInstaller forge(forgePath, forgeVersion->universal_url);
if (!instance->reloadFullVersion())
{
emitFailed(tr("Couldn't load the version config"));
return;
}
instance->revertCustomVersion();
instance->customizeVersion();
auto version = instance->getFullVersion();
if (!forge.apply(version))
{
emitFailed(tr("Couldn't install Forge"));
return;
}
emitSucceeded();
}
private:
OneSixFTBInstance *instance;
QString version;
ForgeVersionPtr forgeVersion;
MetaEntryPtr entry;
NetJob *fjob;
};
OneSixFTBInstance::OneSixFTBInstance(const QString &rootDir, SettingsObject *settings, QObject *parent) :
OneSixInstance(rootDir, settings, parent)
{
QFile f(QDir(minecraftRoot()).absoluteFilePath("pack.json"));
if (f.open(QFile::ReadOnly))
{
QString data = QString::fromUtf8(f.readAll());
QRegularExpressionMatch match = QRegularExpression("net.minecraftforge:minecraftforge:[\\.\\d]*").match(data);
m_forge.reset(new OneSixLibrary(match.captured()));
m_forge->finalize();
}
}
QString OneSixFTBInstance::getStatusbarDescription()
{
return "OneSix FTB: " + intendedVersionId();
}
bool OneSixFTBInstance::menuActionEnabled(QString action_name) const
{
return false;
}
std::shared_ptr<Task> OneSixFTBInstance::doUpdate(bool only_prepare)
{
std::shared_ptr<SequentialTask> task;
task.reset(new SequentialTask(this));
if (!MMC->forgelist()->isLoaded())
{
task->addTask(std::shared_ptr<Task>(MMC->forgelist()->getLoadTask()));
}
task->addTask(OneSixInstance::doUpdate(only_prepare));
task->addTask(std::shared_ptr<Task>(new OneSixFTBInstanceForge(m_forge->version(), this, this)));
//FIXME: yes. this may appear dumb. but the previous step can change the list, so we do it all again.
//TODO: Add a graph task. Construct graphs of tasks so we may capture the logic properly.
task->addTask(OneSixInstance::doUpdate(only_prepare));
return task;
}
#include "OneSixFTBInstance.moc"

20
logic/OneSixFTBInstance.h Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include "OneSixInstance.h"
class OneSixLibrary;
class OneSixFTBInstance : public OneSixInstance
{
Q_OBJECT
public:
explicit OneSixFTBInstance(const QString &rootDir, SettingsObject *settings,
QObject *parent = 0);
virtual QString getStatusbarDescription();
virtual bool menuActionEnabled(QString action_name) const;
virtual std::shared_ptr<Task> doUpdate(bool only_prepare) override;
private:
std::shared_ptr<OneSixLibrary> m_forge;
};

View File

@ -33,8 +33,8 @@ OneSixInstance::OneSixInstance(const QString &rootDir, SettingsObject *setting_o
: BaseInstance(new OneSixInstancePrivate(), rootDir, setting_obj, parent)
{
I_D(OneSixInstance);
d->m_settings->registerSetting(new Setting("IntendedVersion", ""));
d->m_settings->registerSetting(new Setting("ShouldUpdate", false));
d->m_settings->registerSetting("IntendedVersion", "");
d->m_settings->registerSetting("ShouldUpdate", false);
reloadFullVersion();
}

View File

@ -68,6 +68,12 @@ public:
m_name = name;
}
/// Returns the raw name field
QString rawName() const
{
return m_name;
}
QJsonObject toJson();
/**

View File

@ -57,7 +57,7 @@ void OneSixUpdate::executeTask()
/*
* FIXME: in offline mode, do not proceed!
*/
setStatus("Testing the Java installation.");
setStatus(tr("Testing the Java installation..."));
QString java_path = m_inst->settings().get("JavaPath").toString();
checker.reset(new JavaChecker());
@ -89,7 +89,7 @@ void OneSixUpdate::executeTask()
void OneSixUpdate::checkJavaOnline()
{
setStatus("Testing the Java installation.");
setStatus(tr("Testing the Java installation..."));
QString java_path = m_inst->settings().get("JavaPath").toString();
checker.reset(new JavaChecker());
@ -128,7 +128,7 @@ void OneSixUpdate::checkFinishedOffline(JavaCheckResult result)
void OneSixUpdate::versionFileStart()
{
QLOG_INFO() << m_inst->name() << ": getting version file.";
setStatus("Getting the version files from Mojang.");
setStatus(tr("Getting the version files from Mojang..."));
QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + targetVersion->descriptor() + "/" + targetVersion->descriptor() + ".json";
auto job = new NetJob("Version index");
@ -196,7 +196,7 @@ void OneSixUpdate::versionFileFailed()
void OneSixUpdate::assetIndexStart()
{
setStatus("Updating asset index.");
setStatus(tr("Updating assets index..."));
OneSixInstance *inst = (OneSixInstance *)m_inst;
std::shared_ptr<OneSixVersion> version = inst->getFullVersion();
QString assetName = version->assets;
@ -247,7 +247,7 @@ void OneSixUpdate::assetIndexFinished()
}
if(dls.size())
{
setStatus("Getting the assets files from Mojang...");
setStatus(tr("Getting the assets files from Mojang..."));
auto job = new NetJob("Assets for " + inst->name());
for(auto dl: dls)
job->addNetAction(dl);
@ -281,7 +281,7 @@ void OneSixUpdate::assetsFailed()
void OneSixUpdate::jarlibStart()
{
setStatus("Getting the library files from Mojang.");
setStatus(tr("Getting the library files from Mojang..."));
QLOG_INFO() << m_inst->name() << ": downloading libraries";
OneSixInstance *inst = (OneSixInstance *)m_inst;
bool successful = inst->reloadFullVersion();
@ -369,7 +369,7 @@ void OneSixUpdate::jarlibFailed()
void OneSixUpdate::prepareForLaunch()
{
setStatus("Preparing for launch.");
setStatus(tr("Preparing for launch..."));
QLOG_INFO() << m_inst->name() << ": preparing for launch";
auto onesix_inst = (OneSixInstance *)m_inst;

View File

@ -0,0 +1,143 @@
#include "AssetsMigrateTask.h"
#include "MultiMC.h"
#include "logger/QsLog.h"
#include <QJsonObject>
#include <QJsonDocument>
#include <QDirIterator>
#include <QCryptographicHash>
#include "gui/dialogs/CustomMessageBox.h"
#include <QDesktopServices>
AssetsMigrateTask::AssetsMigrateTask(int expected, QObject *parent)
: Task(parent)
{
this->m_expected = expected;
}
void AssetsMigrateTask::executeTask()
{
this->setStatus(tr("Migrating legacy assets..."));
this->setProgress(0);
QDir assets_dir("assets");
if (!assets_dir.exists())
{
emitFailed("Assets directory didn't exist");
return;
}
assets_dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
int base_length = assets_dir.path().length();
QList<QString> blacklist = {"indexes", "objects", "virtual"};
if (!assets_dir.exists("objects"))
assets_dir.mkdir("objects");
QDir objects_dir("assets/objects");
QDirIterator iterator(assets_dir, QDirIterator::Subdirectories);
int successes = 0;
int failures = 0;
while (iterator.hasNext())
{
QString currentDir = iterator.next();
currentDir = currentDir.remove(0, base_length + 1);
bool ignore = false;
for (QString blacklisted : blacklist)
{
if (currentDir.startsWith(blacklisted))
ignore = true;
}
if (!iterator.fileInfo().isDir() && !ignore)
{
QString filename = iterator.filePath();
QFile input(filename);
input.open(QIODevice::ReadOnly | QIODevice::WriteOnly);
QString sha1sum =
QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1)
.toHex()
.constData();
QString object_name = filename.remove(0, base_length + 1);
QLOG_DEBUG() << "Processing" << object_name << ":" << sha1sum << input.size();
QString object_tlk = sha1sum.left(2);
QString object_tlk_dir = objects_dir.path() + "/" + object_tlk;
QDir tlk_dir(object_tlk_dir);
if (!tlk_dir.exists())
objects_dir.mkdir(object_tlk);
QString new_filename = tlk_dir.path() + "/" + sha1sum;
QFile new_object(new_filename);
if (!new_object.exists())
{
bool rename_success = input.rename(new_filename);
QLOG_DEBUG() << " Doesn't exist, copying to" << new_filename << ":"
<< QString::number(rename_success);
if (rename_success)
successes++;
else
failures++;
}
else
{
input.remove();
QLOG_DEBUG() << " Already exists, deleting original and not copying.";
}
this->setProgress(100 * ((successes + failures) / (float) this->m_expected));
}
}
if (successes + failures == 0)
{
this->setProgress(100);
QLOG_DEBUG() << "No legacy assets needed importing.";
}
else
{
QLOG_DEBUG() << "Finished copying legacy assets:" << successes << "successes and"
<< failures << "failures.";
this->setStatus("Cleaning up legacy assets...");
this->setProgress(100);
QDirIterator cleanup_iterator(assets_dir);
while (cleanup_iterator.hasNext())
{
QString currentDir = cleanup_iterator.next();
currentDir = currentDir.remove(0, base_length + 1);
bool ignore = false;
for (QString blacklisted : blacklist)
{
if (currentDir.startsWith(blacklisted))
ignore = true;
}
if (cleanup_iterator.fileInfo().isDir() && !ignore)
{
QString path = cleanup_iterator.filePath();
QDir folder(path);
QLOG_DEBUG() << "Cleaning up legacy assets folder:" << path;
folder.removeRecursively();
}
}
}
if(failures > 0)
{
emitFailed(QString("Failed to migrate %1 legacy assets").arg(failures));
}
else
{
emitSucceeded();
}
}

View File

@ -0,0 +1,18 @@
#pragma once
#include "logic/tasks/Task.h"
#include <QMessageBox>
#include <QNetworkReply>
#include <memory>
class AssetsMigrateTask : public Task
{
Q_OBJECT
public:
explicit AssetsMigrateTask(int expected, QObject* parent=0);
protected:
virtual void executeTask();
private:
int m_expected;
};

View File

@ -25,23 +25,18 @@
namespace AssetsUtils
{
void migrateOldAssets()
int findLegacyAssets()
{
QDir assets_dir("assets");
if (!assets_dir.exists())
return;
return 0;
assets_dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
int base_length = assets_dir.path().length();
QList<QString> blacklist = {"indexes", "objects", "virtual"};
if (!assets_dir.exists("objects"))
assets_dir.mkdir("objects");
QDir objects_dir("assets/objects");
QDirIterator iterator(assets_dir, QDirIterator::Subdirectories);
int successes = 0;
int failures = 0;
int found = 0;
while (iterator.hasNext())
{
QString currentDir = iterator.next();
@ -56,79 +51,11 @@ void migrateOldAssets()
if (!iterator.fileInfo().isDir() && !ignore)
{
QString filename = iterator.filePath();
QFile input(filename);
input.open(QIODevice::ReadOnly | QIODevice::WriteOnly);
QString sha1sum =
QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1)
.toHex()
.constData();
QString object_name = filename.remove(0, base_length + 1);
QLOG_DEBUG() << "Processing" << object_name << ":" << sha1sum << input.size();
QString object_tlk = sha1sum.left(2);
QString object_tlk_dir = objects_dir.path() + "/" + object_tlk;
QDir tlk_dir(object_tlk_dir);
if (!tlk_dir.exists())
objects_dir.mkdir(object_tlk);
QString new_filename = tlk_dir.path() + "/" + sha1sum;
QFile new_object(new_filename);
if (!new_object.exists())
{
bool rename_success = input.rename(new_filename);
QLOG_DEBUG() << " Doesn't exist, copying to" << new_filename << ":"
<< QString::number(rename_success);
if (rename_success)
successes++;
else
failures++;
}
else
{
input.remove();
QLOG_DEBUG() << " Already exists, deleting original and not copying.";
}
found++;
}
}
if (successes + failures == 0)
{
QLOG_DEBUG() << "No legacy assets needed importing.";
}
else
{
QLOG_DEBUG() << "Finished copying legacy assets:" << successes << "successes and"
<< failures << "failures.";
QDirIterator cleanup_iterator(assets_dir);
while (cleanup_iterator.hasNext())
{
QString currentDir = cleanup_iterator.next();
currentDir = currentDir.remove(0, base_length + 1);
bool ignore = false;
for (QString blacklisted : blacklist)
{
if (currentDir.startsWith(blacklisted))
ignore = true;
}
if (cleanup_iterator.fileInfo().isDir() && !ignore)
{
QString path = cleanup_iterator.filePath();
QDir folder(path);
QLOG_DEBUG() << "Cleaning up legacy assets folder:" << path;
folder.removeRecursively();
}
}
}
return found;
}
/*

View File

@ -34,6 +34,6 @@ struct AssetsIndex
namespace AssetsUtils
{
void migrateOldAssets();
bool loadAssetsIndexJson(QString file, AssetsIndex* index);
int findLegacyAssets();
}

View File

@ -32,7 +32,8 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
// The JSON object must at least have a username for it to be valid.
if (!object.value("username").isString())
{
QLOG_ERROR() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type.";
QLOG_ERROR() << "Can't load Mojang account info from JSON object. Username field is "
"missing or of the wrong type.";
return nullptr;
}
@ -43,7 +44,8 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
QJsonArray profileArray = object.value("profiles").toArray();
if (profileArray.size() < 1)
{
QLOG_ERROR() << "Can't load Mojang account with username \"" << username << "\". No profiles found.";
QLOG_ERROR() << "Can't load Mojang account with username \"" << username
<< "\". No profiles found.";
return nullptr;
}
@ -63,7 +65,7 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
}
MojangAccountPtr account(new MojangAccount());
if(object.value("user").isObject())
if (object.value("user").isObject())
{
User u;
QJsonObject userStructure = object.value("user").toObject();
@ -92,7 +94,7 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
return account;
}
MojangAccountPtr MojangAccount::createFromUsername(const QString& username)
MojangAccountPtr MojangAccount::createFromUsername(const QString &username)
{
MojangAccountPtr account(new MojangAccount());
account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
@ -152,27 +154,27 @@ bool MojangAccount::setCurrentProfile(const QString &profileId)
return false;
}
const AccountProfile* MojangAccount::currentProfile() const
const AccountProfile *MojangAccount::currentProfile() const
{
if(m_currentProfile == -1)
if (m_currentProfile == -1)
return nullptr;
return &m_profiles[m_currentProfile];
}
AccountStatus MojangAccount::accountStatus() const
{
if(m_accessToken.isEmpty())
if (m_accessToken.isEmpty())
return NotVerified;
if(!m_online)
if (!m_online)
return Verified;
return Online;
}
std::shared_ptr<Task> MojangAccount::login(QString password)
std::shared_ptr<YggdrasilTask> MojangAccount::login(QString password)
{
if(m_currentTask)
if (m_currentTask)
return m_currentTask;
if(password.isEmpty())
if (password.isEmpty())
{
m_currentTask.reset(new RefreshTask(this));
}
@ -196,7 +198,7 @@ void MojangAccount::authFailed(QString reason)
{
// This is emitted when the yggdrasil tasks time out or are cancelled.
// -> we treat the error as no-op
if(reason != "Yggdrasil task cancelled.")
if (reason != "Yggdrasil task cancelled.")
{
m_online = false;
m_accessToken = QString();

View File

@ -95,7 +95,7 @@ public: /* manipulation */
* Attempt to login. Empty password means we use the token.
* If the attempt fails because we already are performing some task, it returns false.
*/
std::shared_ptr<Task> login(QString password = QString());
std::shared_ptr<YggdrasilTask> login(QString password = QString());
void downgrade()
{

View File

@ -48,6 +48,7 @@ void YggdrasilTask::executeTask()
connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply);
connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers);
connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers);
connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors);
timeout_keeper.setSingleShot(true);
timeout_keeper.start(timeout_max);
counter.setSingleShot(false);
@ -75,16 +76,45 @@ void YggdrasilTask::abort()
m_netReply->abort();
}
void YggdrasilTask::sslErrors(QList<QSslError> errors)
{
int i = 1;
for (auto error : errors)
{
QLOG_ERROR() << "LOGIN SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
QLOG_ERROR() << "Certificate in question:\n" << cert.toText();
i++;
}
}
void YggdrasilTask::processReply()
{
setStatus(getStateMessage(STATE_PROCESSING_RESPONSE));
if (m_netReply->error() == QNetworkReply::SslHandshakeFailedError)
{
emitFailed(
tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
"<ul>"
"<li>You use Windows XP and need to <a "
"href=\"http://www.microsoft.com/en-us/download/details.aspx?id=38918\">update "
"your root certificates</a></li>"
"<li>Some device on your network is interfering with SSL traffic. In that case, "
"you have bigger worries than Minecraft not starting.</li>"
"<li>Possibly something else. Check the MultiMC log file for details</li>"
"</ul>"));
return;
}
// any network errors lead to offline mode right now
if (m_netReply->error() >= QNetworkReply::ConnectionRefusedError &&
m_netReply->error() <= QNetworkReply::UnknownNetworkError)
{
// WARNING/FIXME: the value here is used in MojangAccount to detect the cancel/timeout
emitFailed("Yggdrasil task cancelled.");
QLOG_ERROR() << "Yggdrasil task cancelled because of: " << m_netReply->error() << " : "
<< m_netReply->errorString();
return;
}
@ -172,10 +202,10 @@ QString YggdrasilTask::getStateMessage(const YggdrasilTask::State state) const
switch (state)
{
case STATE_SENDING_REQUEST:
return tr("Sending request to auth servers.");
return tr("Sending request to auth servers...");
case STATE_PROCESSING_RESPONSE:
return tr("Processing response from servers.");
return tr("Processing response from servers...");
default:
return tr("Processing. Please wait.");
return tr("Processing. Please wait...");
}
}

View File

@ -20,6 +20,7 @@
#include <QString>
#include <QJsonObject>
#include <QTimer>
#include <qsslerror.h>
#include "logic/auth/MojangAccount.h"
@ -99,6 +100,7 @@ slots:
void processReply();
void refreshTimers(qint64, qint64);
void heartbeat();
void sslErrors(QList<QSslError>);
public
slots:

View File

@ -194,9 +194,9 @@ QString AuthenticateTask::getStateMessage(const YggdrasilTask::State state) cons
switch (state)
{
case STATE_SENDING_REQUEST:
return tr("Authenticating: Sending request.");
return tr("Authenticating: Sending request...");
case STATE_PROCESSING_RESPONSE:
return tr("Authenticating: Processing response.");
return tr("Authenticating: Processing response...");
default:
return YggdrasilTask::getStateMessage(state);
}

View File

@ -145,9 +145,9 @@ QString RefreshTask::getStateMessage(const YggdrasilTask::State state) const
switch (state)
{
case STATE_SENDING_REQUEST:
return tr("Refreshing login token.");
return tr("Refreshing login token...");
case STATE_PROCESSING_RESPONSE:
return tr("Refreshing login token: Processing response.");
return tr("Refreshing login token: Processing response...");
default:
return YggdrasilTask::getStateMessage(state);
}

View File

@ -55,9 +55,9 @@ QString ValidateTask::getStateMessage(const YggdrasilTask::State state) const
switch (state)
{
case STATE_SENDING_REQUEST:
return tr("Validating Access Token: Sending request.");
return tr("Validating access token: Sending request...");
case STATE_PROCESSING_RESPONSE:
return tr("Validating Access Token: Processing response.");
return tr("Validating access token: Processing response...");
default:
return YggdrasilTask::getStateMessage(state);
}

351
logic/icons/IconList.cpp Normal file
View File

@ -0,0 +1,351 @@
/* 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 "IconList.h"
#include <pathutils.h>
#include <settingsobject.h>
#include <QMap>
#include <QEventLoop>
#include <QMimeData>
#include <QUrl>
#include <QFileSystemWatcher>
#include <MultiMC.h>
#include <setting.h>
#define MAX_SIZE 1024
IconList::IconList(QObject *parent) : QAbstractListModel(parent)
{
// add builtin icons
QDir instance_icons(":/icons/instances/");
auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name);
for (auto file_info : file_info_list)
{
QString key = file_info.baseName();
addIcon(key, key, file_info.absoluteFilePath(), MMCIcon::Builtin);
}
m_watcher.reset(new QFileSystemWatcher());
is_watching = false;
connect(m_watcher.get(), SIGNAL(directoryChanged(QString)),
SLOT(directoryChanged(QString)));
connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString)));
auto setting = MMC->settings()->getSetting("IconsDir");
QString path = setting->get().toString();
connect(setting.get(), SIGNAL(settingChanged(const Setting &, QVariant)),
SLOT(settingChanged(const Setting &, QVariant)));
directoryChanged(path);
}
void IconList::directoryChanged(const QString &path)
{
QDir new_dir (path);
if(m_dir.absolutePath() != new_dir.absolutePath())
{
m_dir.setPath(path);
m_dir.refresh();
if(is_watching)
stopWatching();
startWatching();
}
if(!m_dir.exists())
if(!ensureFolderPathExists(m_dir.absolutePath()))
return;
m_dir.refresh();
auto new_list = m_dir.entryList(QDir::Files, QDir::Name);
for (auto it = new_list.begin(); it != new_list.end(); it++)
{
QString &foo = (*it);
foo = m_dir.filePath(foo);
}
auto new_set = new_list.toSet();
QList<QString> current_list;
for (auto &it : icons)
{
if (!it.has(MMCIcon::FileBased))
continue;
current_list.push_back(it.m_images[MMCIcon::FileBased].filename);
}
QSet<QString> current_set = current_list.toSet();
QSet<QString> to_remove = current_set;
to_remove -= new_set;
QSet<QString> to_add = new_set;
to_add -= current_set;
for (auto remove : to_remove)
{
QLOG_INFO() << "Removing " << remove;
QFileInfo rmfile(remove);
QString key = rmfile.baseName();
int idx = getIconIndex(key);
if (idx == -1)
continue;
icons[idx].remove(MMCIcon::FileBased);
if (icons[idx].type() == MMCIcon::ToBeDeleted)
{
beginRemoveRows(QModelIndex(), idx, idx);
icons.remove(idx);
reindex();
endRemoveRows();
}
else
{
dataChanged(index(idx), index(idx));
}
m_watcher->removePath(remove);
emit iconUpdated(key);
}
for (auto add : to_add)
{
QLOG_INFO() << "Adding " << add;
QFileInfo addfile(add);
QString key = addfile.baseName();
if (addIcon(key, QString(), addfile.filePath(), MMCIcon::FileBased))
{
m_watcher->addPath(add);
emit iconUpdated(key);
}
}
}
void IconList::fileChanged(const QString &path)
{
QLOG_INFO() << "Checking " << path;
QFileInfo checkfile(path);
if (!checkfile.exists())
return;
QString key = checkfile.baseName();
int idx = getIconIndex(key);
if (idx == -1)
return;
QIcon icon(path);
if (!icon.availableSizes().size())
return;
icons[idx].m_images[MMCIcon::FileBased].icon = icon;
dataChanged(index(idx), index(idx));
emit iconUpdated(key);
}
void IconList::settingChanged(const Setting &setting, QVariant value)
{
if(setting.id() != "IconsDir")
return;
directoryChanged(value.toString());
}
void IconList::startWatching()
{
auto abs_path = m_dir.absolutePath();
ensureFolderPathExists(abs_path);
is_watching = m_watcher->addPath(abs_path);
if (is_watching)
{
QLOG_INFO() << "Started watching " << abs_path;
}
else
{
QLOG_INFO() << "Failed to start watching " << abs_path;
}
}
void IconList::stopWatching()
{
m_watcher->removePaths(m_watcher->files());
m_watcher->removePaths(m_watcher->directories());
is_watching = false;
}
QStringList IconList::mimeTypes() const
{
QStringList types;
types << "text/uri-list";
return types;
}
Qt::DropActions IconList::supportedDropActions() const
{
return Qt::CopyAction;
}
bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
const QModelIndex &parent)
{
if (action == Qt::IgnoreAction)
return true;
// check if the action is supported
if (!data || !(action & supportedDropActions()))
return false;
// files dropped from outside?
if (data->hasUrls())
{
auto urls = data->urls();
QStringList iconFiles;
for (auto url : urls)
{
// only local files may be dropped...
if (!url.isLocalFile())
continue;
iconFiles += url.toLocalFile();
}
installIcons(iconFiles);
return true;
}
return false;
}
Qt::ItemFlags IconList::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
if (index.isValid())
return Qt::ItemIsDropEnabled | defaultFlags;
else
return Qt::ItemIsDropEnabled | defaultFlags;
}
QVariant IconList::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
int row = index.row();
if (row < 0 || row >= icons.size())
return QVariant();
switch (role)
{
case Qt::DecorationRole:
return icons[row].icon();
case Qt::DisplayRole:
return icons[row].name();
case Qt::UserRole:
return icons[row].m_key;
default:
return QVariant();
}
}
int IconList::rowCount(const QModelIndex &parent) const
{
return icons.size();
}
void IconList::installIcons(QStringList iconFiles)
{
for (QString file : iconFiles)
{
QFileInfo fileinfo(file);
if (!fileinfo.isReadable() || !fileinfo.isFile())
continue;
QString target = PathCombine("icons", fileinfo.fileName());
QString suffix = fileinfo.suffix();
if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico")
continue;
if (!QFile::copy(file, target))
continue;
}
}
bool IconList::deleteIcon(QString key)
{
int iconIdx = getIconIndex(key);
if (iconIdx == -1)
return false;
auto &iconEntry = icons[iconIdx];
if (iconEntry.has(MMCIcon::FileBased))
{
return QFile::remove(iconEntry.m_images[MMCIcon::FileBased].filename);
}
return false;
}
bool IconList::addIcon(QString key, QString name, QString path, MMCIcon::Type type)
{
// replace the icon even? is the input valid?
QIcon icon(path);
if (!icon.availableSizes().size())
return false;
auto iter = name_index.find(key);
if (iter != name_index.end())
{
auto &oldOne = icons[*iter];
oldOne.replace(type, icon, path);
dataChanged(index(*iter), index(*iter));
return true;
}
else
{
// add a new icon
beginInsertRows(QModelIndex(), icons.size(), icons.size());
{
MMCIcon mmc_icon;
mmc_icon.m_name = name;
mmc_icon.m_key = key;
mmc_icon.replace(type, icon, path);
icons.push_back(mmc_icon);
name_index[key] = icons.size() - 1;
}
endInsertRows();
return true;
}
}
void IconList::reindex()
{
name_index.clear();
int i = 0;
for (auto &iter : icons)
{
name_index[iter.m_key] = i;
i++;
}
}
QIcon IconList::getIcon(QString key)
{
int icon_index = getIconIndex(key);
if (icon_index != -1)
return icons[icon_index].icon();
// Fallback for icons that don't exist.
icon_index = getIconIndex("infinity");
if (icon_index != -1)
return icons[icon_index].icon();
return QIcon();
}
int IconList::getIconIndex(QString key)
{
if (key == "default")
key = "infinity";
auto iter = name_index.find(key);
if (iter != name_index.end())
return *iter;
return -1;
}
//#include "IconList.moc"

View File

@ -17,15 +17,21 @@
#include <QMutex>
#include <QAbstractListModel>
#include <QFile>
#include <QDir>
#include <QtGui/QIcon>
#include <memory>
#include "MMCIcon.h"
#include "setting.h"
class Private;
class QFileSystemWatcher;
class IconList : public QAbstractListModel
{
Q_OBJECT
public:
IconList();
virtual ~IconList();
explicit IconList(QObject *parent = 0);
virtual ~IconList() {};
QIcon getIcon(QString key);
int getIconIndex(QString key);
@ -33,7 +39,7 @@ public:
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const;
bool addIcon(QString key, QString name, QString path, bool is_builtin = false);
bool addIcon(QString key, QString name, QString path, MMCIcon::Type type);
bool deleteIcon(QString key);
virtual QStringList mimeTypes() const;
@ -44,11 +50,28 @@ public:
void installIcons(QStringList iconFiles);
void startWatching();
void stopWatching();
signals:
void iconUpdated(QString key);
private:
// hide copy constructor
IconList(const IconList &) = delete;
// hide assign op
IconList &operator=(const IconList &) = delete;
void reindex();
Private *d;
protected
slots:
void directoryChanged(const QString &path);
void fileChanged(const QString &path);
void settingChanged(const Setting & setting, QVariant value);
private:
std::shared_ptr<QFileSystemWatcher> m_watcher;
bool is_watching;
QMap<QString, int> name_index;
QVector<MMCIcon> icons;
QDir m_dir;
};

89
logic/icons/MMCIcon.cpp Normal file
View File

@ -0,0 +1,89 @@
/* 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 "MMCIcon.h"
#include <QFileInfo>
MMCIcon::Type operator--(MMCIcon::Type &t, int)
{
MMCIcon::Type temp = t;
switch (t)
{
case MMCIcon::Type::Builtin:
t = MMCIcon::Type::ToBeDeleted;
break;
case MMCIcon::Type::Transient:
t = MMCIcon::Type::Builtin;
break;
case MMCIcon::Type::FileBased:
t = MMCIcon::Type::Transient;
break;
default:
{
}
}
return temp;
}
MMCIcon::Type MMCIcon::type() const
{
return m_current_type;
}
QString MMCIcon::name() const
{
if (m_name.size())
return m_name;
return m_key;
}
bool MMCIcon::has(MMCIcon::Type _type) const
{
return m_images[_type].present();
}
QIcon MMCIcon::icon() const
{
if (m_current_type == Type::ToBeDeleted)
return QIcon();
return m_images[m_current_type].icon;
}
void MMCIcon::remove(Type rm_type)
{
m_images[rm_type].filename = QString();
m_images[rm_type].icon = QIcon();
for (auto iter = rm_type; iter != Type::ToBeDeleted; iter--)
{
if (m_images[iter].present())
{
m_current_type = iter;
return;
}
}
m_current_type = Type::ToBeDeleted;
}
void MMCIcon::replace(MMCIcon::Type new_type, QIcon icon, QString path)
{
QFileInfo foo(path);
if (new_type > m_current_type || m_current_type == MMCIcon::ToBeDeleted)
{
m_current_type = new_type;
}
m_images[new_type].icon = icon;
m_images[new_type].changed = foo.lastModified();
m_images[new_type].filename = path;
}

52
logic/icons/MMCIcon.h Normal file
View File

@ -0,0 +1,52 @@
/* 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.
*/
#pragma once
#include <QString>
#include <QDateTime>
#include <QIcon>
struct MMCImage
{
QIcon icon;
QString filename;
QDateTime changed;
bool present() const
{
return !icon.isNull();
}
};
struct MMCIcon
{
enum Type : unsigned
{
Builtin,
Transient,
FileBased,
ICONS_TOTAL,
ToBeDeleted
};
QString m_key;
QString m_name;
MMCImage m_images[ICONS_TOTAL];
Type m_current_type = ToBeDeleted;
Type type() const;
QString name() const;
bool has(Type _type) const;
QIcon icon() const;
void remove(Type rm_type);
void replace(Type new_type, QIcon icon, QString path = QString());
};

View File

@ -15,6 +15,7 @@
#include "ForgeVersionList.h"
#include <logic/net/NetJob.h>
#include <logic/net/URLConstants.h>
#include "MultiMC.h"
#include <QtNetwork>
@ -23,8 +24,6 @@
#include "logger/QsLog.h"
#define JSON_URL "http://files.minecraftforge.net/minecraftforge/json"
ForgeVersionList::ForgeVersionList(QObject *parent) : BaseVersionList(parent)
{
}
@ -159,44 +158,43 @@ ForgeListLoadTask::ForgeListLoadTask(ForgeVersionList *vlist) : Task()
void ForgeListLoadTask::executeTask()
{
setStatus(tr("Fetching Forge version lists..."));
auto job = new NetJob("Version index");
// we do not care if the version is stale or not.
auto forgeListEntry = MMC->metacache()->resolveEntry("minecraftforge", "list.json");
auto gradleForgeListEntry = MMC->metacache()->resolveEntry("minecraftforge", "json");
// verify by poking the server.
forgeListEntry->stale = true;
gradleForgeListEntry->stale = true;
job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::FORGE_LEGACY_URL),
forgeListEntry));
job->addNetAction(gradleListDownload = CacheDownload::make(
QUrl(URLConstants::FORGE_GRADLE_URL), gradleForgeListEntry));
connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed()));
connect(gradleListDownload.get(), SIGNAL(failed(int)), SLOT(gradleListFailed()));
job->addNetAction(CacheDownload::make(QUrl(JSON_URL), forgeListEntry));
listJob.reset(job);
connect(listJob.get(), SIGNAL(succeeded()), SLOT(list_downloaded()));
connect(listJob.get(), SIGNAL(failed()), SLOT(list_failed()));
connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded()));
connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
listJob->start();
}
void ForgeListLoadTask::list_failed()
{
auto DlJob = listJob->first();
auto reply = DlJob->m_reply;
if (reply)
{
QLOG_ERROR() << "Getting forge version list failed: " << reply->errorString();
}
else
QLOG_ERROR() << "Getting forge version list failed for reasons unknown.";
}
void ForgeListLoadTask::list_downloaded()
bool ForgeListLoadTask::parseForgeList(QList<BaseVersionPtr> &out)
{
QByteArray data;
{
auto DlJob = listJob->first();
auto filename = std::dynamic_pointer_cast<CacheDownload>(DlJob)->m_target_path;
auto dlJob = listDownload;
auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->m_target_path;
QFile listFile(filename);
if (!listFile.open(QIODevice::ReadOnly))
return;
{
return false;
}
data = listFile.readAll();
DlJob.reset();
dlJob.reset();
}
QJsonParseError jsonError;
@ -205,13 +203,13 @@ void ForgeListLoadTask::list_downloaded()
if (jsonError.error != QJsonParseError::NoError)
{
emitFailed("Error parsing version list JSON:" + jsonError.errorString());
return;
return false;
}
if (!jsonDoc.isObject())
{
emitFailed("Error parsing version list JSON: jsonDoc is not an object");
return;
emitFailed("Error parsing version list JSON: JSON root is not an object");
return false;
}
QJsonObject root = jsonDoc.object();
@ -221,11 +219,10 @@ void ForgeListLoadTask::list_downloaded()
{
emitFailed(
"Error parsing version list JSON: version list object is missing 'builds' array");
return;
return false;
}
QJsonArray builds = root.value("builds").toArray();
QList<BaseVersionPtr> tempList;
for (int i = 0; i < builds.count(); i++)
{
// Load the version info.
@ -246,7 +243,9 @@ void ForgeListLoadTask::list_downloaded()
for (int j = 0; j < files.count(); j++)
{
if (!files[j].isObject())
{
continue;
}
QJsonObject file = files[j].toObject();
buildtype = file.value("buildtype").toString();
if ((buildtype == "client" || buildtype == "universal") && !valid)
@ -262,7 +261,9 @@ void ForgeListLoadTask::list_downloaded()
{
QString ext = file.value("ext").toString();
if (ext.isEmpty())
{
continue;
}
changelog_url = file.value("url").toString();
}
else if (buildtype == "installer")
@ -282,15 +283,161 @@ void ForgeListLoadTask::list_downloaded()
fVersion->jobbuildver = jobbuildver;
fVersion->mcver = mcver;
if (installer_filename.isEmpty())
{
fVersion->filename = filename;
}
else
{
fVersion->filename = installer_filename;
}
fVersion->m_buildnr = build_nr;
tempList.append(fVersion);
out.append(fVersion);
}
}
m_list->updateListData(tempList);
return true;
}
bool ForgeListLoadTask::parseForgeGradleList(QList<BaseVersionPtr> &out)
{
QByteArray data;
{
auto dlJob = gradleListDownload;
auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->m_target_path;
QFile listFile(filename);
if (!listFile.open(QIODevice::ReadOnly))
{
return false;
}
data = listFile.readAll();
dlJob.reset();
}
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
if (jsonError.error != QJsonParseError::NoError)
{
emitFailed("Error parsing gradle version list JSON:" + jsonError.errorString());
return false;
}
if (!jsonDoc.isObject())
{
emitFailed("Error parsing gradle version list JSON: JSON root is not an object");
return false;
}
QJsonObject root = jsonDoc.object();
// we probably could hard code these, but it might still be worth doing it this way
const QString webpath = root.value("webpath").toString();
const QString artifact = root.value("artifact").toString();
QJsonObject numbers = root.value("number").toObject();
for (auto it = numbers.begin(); it != numbers.end(); ++it)
{
QJsonObject number = it.value().toObject();
std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion());
fVersion->m_buildnr = number.value("build").toDouble();
fVersion->jobbuildver = number.value("version").toString();
fVersion->mcver = number.value("mcversion").toString();
fVersion->filename = "";
QString filename, installer_filename;
QJsonArray files = number.value("files").toArray();
for (auto fIt = files.begin(); fIt != files.end(); ++fIt)
{
// TODO with gradle we also get checksums, use them
QJsonArray file = (*fIt).toArray();
if (file.size() < 3)
{
continue;
}
if (file.at(1).toString() == "installer")
{
fVersion->installer_url = QString("%1/%2-%3/%4-%2-%3-installer.%5").arg(
webpath, fVersion->mcver, fVersion->jobbuildver, artifact,
file.at(0).toString());
installer_filename = QString("%1-%2-%3-installer.%4").arg(
artifact, fVersion->mcver, fVersion->jobbuildver, file.at(0).toString());
}
else if (file.at(1).toString() == "universal")
{
fVersion->universal_url = QString("%1/%2-%3/%4-%2-%3-universal.%5").arg(
webpath, fVersion->mcver, fVersion->jobbuildver, artifact,
file.at(0).toString());
filename = QString("%1-%2-%3-universal.%4").arg(
artifact, fVersion->mcver, fVersion->jobbuildver, file.at(0).toString());
}
else if (file.at(1).toString() == "changelog")
{
fVersion->changelog_url = QString("%1/%2-%3/%4-%2-%3-changelog.%5").arg(
webpath, fVersion->mcver, fVersion->jobbuildver, artifact,
file.at(0).toString());
}
}
if (fVersion->installer_url.isEmpty() && fVersion->universal_url.isEmpty())
{
continue;
}
fVersion->filename = fVersion->installer_url.isEmpty() ? filename : installer_filename;
out.append(fVersion);
}
return true;
}
void ForgeListLoadTask::listDownloaded()
{
QList<BaseVersionPtr> list;
bool ret = true;
if (!parseForgeList(list))
{
ret = false;
}
if (!parseForgeGradleList(list))
{
ret = false;
}
if (!ret)
{
return;
}
qSort(list.begin(), list.end(), [](const BaseVersionPtr & p1, const BaseVersionPtr & p2)
{
// TODO better comparison (takes major/minor/build number into account)
return p1->name() > p2->name();
});
m_list->updateListData(list);
emitSucceeded();
return;
}
void ForgeListLoadTask::listFailed()
{
auto reply = listDownload->m_reply;
if (reply)
{
QLOG_ERROR() << "Getting forge version list failed: " << reply->errorString();
}
else
{
QLOG_ERROR() << "Getting forge version list failed for reasons unknown.";
}
}
void ForgeListLoadTask::gradleListFailed()
{
auto reply = gradleListDownload->m_reply;
if (reply)
{
QLOG_ERROR() << "Getting forge version list failed: " << reply->errorString();
}
else
{
QLOG_ERROR() << "Getting forge version list failed for reasons unknown.";
}
}

View File

@ -80,7 +80,7 @@ public:
protected:
QList<BaseVersionPtr> m_vlist;
bool m_loaded;
bool m_loaded = false;
protected
slots:
@ -98,10 +98,18 @@ public:
protected
slots:
void list_downloaded();
void list_failed();
void listDownloaded();
void listFailed();
void gradleListFailed();
protected:
NetJobPtr listJob;
ForgeVersionList *m_list;
CacheDownloadPtr listDownload;
CacheDownloadPtr gradleListDownload;
private:
bool parseForgeList(QList<BaseVersionPtr> &out);
bool parseForgeGradleList(QList<BaseVersionPtr> &out);
};

View File

@ -1,271 +0,0 @@
/* 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 "IconList.h"
#include <pathutils.h>
#include <QMap>
#include <QEventLoop>
#include <QDir>
#include <QMimeData>
#include <QUrl>
#define MAX_SIZE 1024
struct entry
{
QString key;
QString name;
QIcon icon;
bool is_builtin;
QString filename;
};
class Private : public QObject
{
Q_OBJECT
public:
QMap<QString, int> index;
QVector<entry> icons;
Private()
{
}
};
IconList::IconList() : QAbstractListModel(), d(new Private())
{
QDir instance_icons(":/icons/instances/");
auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name);
for (auto file_info : file_info_list)
{
QString key = file_info.baseName();
addIcon(key, key, file_info.absoluteFilePath(), true);
}
// FIXME: get from settings
ensureFolderPathExists("icons");
QDir user_icons("icons");
file_info_list = user_icons.entryInfoList(QDir::Files, QDir::Name);
for (auto file_info : file_info_list)
{
QString filename = file_info.absoluteFilePath();
QString key = file_info.baseName();
addIcon(key, key, filename);
}
}
IconList::~IconList()
{
delete d;
d = nullptr;
}
QStringList IconList::mimeTypes() const
{
QStringList types;
types << "text/uri-list";
return types;
}
Qt::DropActions IconList::supportedDropActions() const
{
return Qt::CopyAction;
}
bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column,
const QModelIndex &parent)
{
if (action == Qt::IgnoreAction)
return true;
// check if the action is supported
if (!data || !(action & supportedDropActions()))
return false;
// files dropped from outside?
if (data->hasUrls())
{
/*
bool was_watching = is_watching;
if(was_watching)
stopWatching();
*/
auto urls = data->urls();
QStringList iconFiles;
for (auto url : urls)
{
// only local files may be dropped...
if (!url.isLocalFile())
continue;
iconFiles += url.toLocalFile();
}
installIcons(iconFiles);
/*
if(was_watching)
startWatching();
*/
return true;
}
return false;
}
Qt::ItemFlags IconList::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
if (index.isValid())
return Qt::ItemIsDropEnabled | defaultFlags;
else
return Qt::ItemIsDropEnabled | defaultFlags;
}
QVariant IconList::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
int row = index.row();
if (row < 0 || row >= d->icons.size())
return QVariant();
switch (role)
{
case Qt::DecorationRole:
return d->icons[row].icon;
case Qt::DisplayRole:
return d->icons[row].name;
case Qt::UserRole:
return d->icons[row].key;
default:
return QVariant();
}
}
int IconList::rowCount(const QModelIndex &parent) const
{
return d->icons.size();
}
void IconList::installIcons(QStringList iconFiles)
{
for (QString file : iconFiles)
{
QFileInfo fileinfo(file);
if (!fileinfo.isReadable() || !fileinfo.isFile())
continue;
QString target = PathCombine("icons", fileinfo.fileName());
QString suffix = fileinfo.suffix();
if (suffix != "jpeg" && suffix != "png" && suffix != "jpg")
continue;
if (!QFile::copy(file, target))
continue;
QString key = fileinfo.baseName();
addIcon(key, key, target);
}
}
bool IconList::deleteIcon(QString key)
{
int iconIdx = getIconIndex(key);
if (iconIdx == -1)
return false;
auto &iconEntry = d->icons[iconIdx];
if (iconEntry.is_builtin)
return false;
if (QFile::remove(iconEntry.filename))
{
beginRemoveRows(QModelIndex(), iconIdx, iconIdx);
d->icons.remove(iconIdx);
reindex();
endRemoveRows();
}
return true;
}
bool IconList::addIcon(QString key, QString name, QString path, bool is_builtin)
{
auto iter = d->index.find(key);
if (iter != d->index.end())
{
if (d->icons[*iter].is_builtin)
return false;
QIcon icon(path);
if (icon.isNull())
return false;
auto &oldOne = d->icons[*iter];
if (!QFile::remove(oldOne.filename))
return false;
// replace the icon
oldOne = {key, name, icon, is_builtin, path};
dataChanged(index(*iter), index(*iter));
return true;
}
else
{
QIcon icon(path);
if (icon.isNull())
return false;
// add a new icon
beginInsertRows(QModelIndex(), d->icons.size(), d->icons.size());
d->icons.push_back({key, name, icon, is_builtin, path});
d->index[key] = d->icons.size() - 1;
endInsertRows();
return true;
}
}
void IconList::reindex()
{
d->index.clear();
int i = 0;
for (auto &iter : d->icons)
{
d->index[iter.key] = i;
i++;
}
}
QIcon IconList::getIcon(QString key)
{
int icon_index = getIconIndex(key);
if (icon_index != -1)
return d->icons[icon_index].icon;
// Fallback for icons that don't exist.
icon_index = getIconIndex("infinity");
if (icon_index != -1)
return d->icons[icon_index].icon;
return QIcon();
}
int IconList::getIconIndex(QString key)
{
if (key == "default")
key = "infinity";
auto iter = d->index.find(key);
if (iter != d->index.end())
return *iter;
return -1;
}
#include "IconList.moc"

View File

@ -22,11 +22,14 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QXmlStreamReader>
#include <QRegularExpression>
#include <pathutils.h>
#include "MultiMC.h"
#include "logic/lists/InstanceList.h"
#include "logic/lists/IconList.h"
#include "logic/icons/IconList.h"
#include "logic/lists/MinecraftVersionList.h"
#include "logic/BaseInstance.h"
#include "logic/InstanceFactory.h"
#include "logger/QsLog.h"
@ -42,6 +45,9 @@ InstanceList::InstanceList(const QString &instDir, QObject *parent)
{
QDir::current().mkpath(m_instDir);
}
connect(MMC->minecraftlist().get(), &MinecraftVersionList::modelReset, this,
&InstanceList::loadList);
}
InstanceList::~InstanceList()
@ -276,6 +282,125 @@ void InstanceList::loadGroupList(QMap<QString, QString> &groupMap)
}
}
struct FTBRecord
{
QString dir;
QString name;
QString logo;
QString mcVersion;
QString description;
};
void InstanceList::loadForgeInstances(QMap<QString, QString> groupMap)
{
QList<FTBRecord> records;
QDir dir = QDir(MMC->settings()->get("FTBLauncherRoot").toString());
QDir dataDir = QDir(MMC->settings()->get("FTBRoot").toString());
if (!dir.exists())
{
QLOG_INFO() << "The FTB launcher directory specified does not exist. Please check your "
"settings.";
return;
}
else if (!dataDir.exists())
{
QLOG_INFO() << "The FTB directory specified does not exist. Please check your settings";
return;
}
dir.cd("ModPacks");
QFile f(dir.absoluteFilePath("modpacks.xml"));
if (!f.open(QFile::ReadOnly))
return;
// read the FTB packs XML.
QXmlStreamReader reader(&f);
while (!reader.atEnd())
{
switch (reader.readNext())
{
case QXmlStreamReader::StartElement:
{
if (reader.name() == "modpack")
{
QXmlStreamAttributes attrs = reader.attributes();
FTBRecord record;
record.dir = attrs.value("dir").toString();
record.name = attrs.value("name").toString();
record.logo = attrs.value("logo").toString();
record.mcVersion = attrs.value("mcVersion").toString();
record.description = attrs.value("description").toString();
records.append(record);
}
break;
}
case QXmlStreamReader::EndElement:
break;
case QXmlStreamReader::Characters:
break;
default:
break;
}
}
f.close();
// process the records we acquired.
for (auto record : records)
{
auto instanceDir = dataDir.absoluteFilePath(record.dir);
auto templateDir = dir.absoluteFilePath(record.dir);
if (!QFileInfo(instanceDir).exists())
{
continue;
}
QString iconKey = record.logo;
iconKey.remove(QRegularExpression("\\..*"));
MMC->icons()->addIcon(iconKey, iconKey, PathCombine(templateDir, record.logo),
MMCIcon::Transient);
if (!QFileInfo(PathCombine(instanceDir, "instance.cfg")).exists())
{
BaseInstance *instPtr = NULL;
auto &factory = InstanceFactory::get();
auto version = MMC->minecraftlist()->findVersion(record.mcVersion);
if (!version)
{
QLOG_ERROR() << "Can't load instance " << instanceDir
<< " because minecraft version " << record.mcVersion
<< " can't be resolved.";
continue;
}
auto error = factory.createInstance(instPtr, version, instanceDir,
InstanceFactory::FTBInstance);
if (!instPtr || error != InstanceFactory::NoCreateError)
continue;
instPtr->setGroupInitial("FTB");
instPtr->setName(record.name);
instPtr->setIconKey(iconKey);
instPtr->setIntendedVersionId(record.mcVersion);
instPtr->setNotes(record.description);
continueProcessInstance(instPtr, error, instanceDir, groupMap);
}
else
{
BaseInstance *instPtr = NULL;
auto error = InstanceFactory::get().loadInstance(instPtr, instanceDir);
if (!instPtr || error != InstanceFactory::NoCreateError)
continue;
instPtr->setGroupInitial("FTB");
instPtr->setName(record.name);
instPtr->setIconKey(iconKey);
if (instPtr->intendedVersionId() != record.mcVersion)
instPtr->setIntendedVersionId(record.mcVersion);
instPtr->setNotes(record.description);
continueProcessInstance(instPtr, error, instanceDir, groupMap);
}
}
}
InstanceList::InstListError InstanceList::loadList()
{
// load the instance groups
@ -285,57 +410,27 @@ InstanceList::InstListError InstanceList::loadList()
beginResetModel();
m_instances.clear();
QDir dir(m_instDir);
QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable,
QDirIterator::FollowSymlinks);
while (iter.hasNext())
{
QString subDir = iter.next();
if (!QFileInfo(PathCombine(subDir, "instance.cfg")).exists())
continue;
BaseInstance *instPtr = NULL;
auto &loader = InstanceFactory::get();
auto error = loader.loadInstance(instPtr, subDir);
if (error != InstanceFactory::NoLoadError && error != InstanceFactory::NotAnInstance)
QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable,
QDirIterator::FollowSymlinks);
while (iter.hasNext())
{
QString errorMsg = QString("Failed to load instance %1: ")
.arg(QFileInfo(subDir).baseName())
.toUtf8();
QString subDir = iter.next();
if (!QFileInfo(PathCombine(subDir, "instance.cfg")).exists())
continue;
switch (error)
{
default:
errorMsg += QString("Unknown instance loader error %1").arg(error);
break;
}
QLOG_ERROR() << errorMsg.toUtf8();
}
else if (!instPtr)
{
QLOG_ERROR() << QString("Error loading instance %1. Instance loader returned null.")
.arg(QFileInfo(subDir).baseName())
.toUtf8();
}
else
{
std::shared_ptr<BaseInstance> inst(instPtr);
auto iter = groupMap.find(inst->id());
if (iter != groupMap.end())
{
inst->setGroupInitial((*iter));
}
QLOG_INFO() << "Loaded instance " << inst->name();
inst->setParent(this);
m_instances.append(inst);
connect(instPtr, SIGNAL(propertiesChanged(BaseInstance *)), this,
SLOT(propertiesChanged(BaseInstance *)));
connect(instPtr, SIGNAL(groupChanged()), this, SLOT(groupChanged()));
connect(instPtr, SIGNAL(nuked(BaseInstance *)), this,
SLOT(instanceNuked(BaseInstance *)));
BaseInstance *instPtr = NULL;
auto error = InstanceFactory::get().loadInstance(instPtr, subDir);
continueProcessInstance(instPtr, error, subDir, groupMap);
}
}
if (MMC->settings()->get("TrackFTBInstances").toBool())
{
loadForgeInstances(groupMap);
}
endResetModel();
emit dataIsInvalid();
return NoError;
@ -409,6 +504,47 @@ int InstanceList::getInstIndex(BaseInstance *inst) const
return -1;
}
void InstanceList::continueProcessInstance(BaseInstance *instPtr, const int error,
const QDir &dir, QMap<QString, QString> &groupMap)
{
if (error != InstanceFactory::NoLoadError && error != InstanceFactory::NotAnInstance)
{
QString errorMsg = QString("Failed to load instance %1: ")
.arg(QFileInfo(dir.absolutePath()).baseName())
.toUtf8();
switch (error)
{
default:
errorMsg += QString("Unknown instance loader error %1").arg(error);
break;
}
QLOG_ERROR() << errorMsg.toUtf8();
}
else if (!instPtr)
{
QLOG_ERROR() << QString("Error loading instance %1. Instance loader returned null.")
.arg(QFileInfo(dir.absolutePath()).baseName())
.toUtf8();
}
else
{
auto iter = groupMap.find(instPtr->id());
if (iter != groupMap.end())
{
instPtr->setGroupInitial((*iter));
}
QLOG_INFO() << "Loaded instance " << instPtr->name();
instPtr->setParent(this);
m_instances.append(std::shared_ptr<BaseInstance>(instPtr));
connect(instPtr, SIGNAL(propertiesChanged(BaseInstance *)), this,
SLOT(propertiesChanged(BaseInstance *)));
connect(instPtr, SIGNAL(groupChanged()), this, SLOT(groupChanged()));
connect(instPtr, SIGNAL(nuked(BaseInstance *)), this,
SLOT(instanceNuked(BaseInstance *)));
}
}
void InstanceList::instanceNuked(BaseInstance *inst)
{
int i = getInstIndex(inst);

View File

@ -25,6 +25,8 @@
class BaseInstance;
class QDir;
class InstanceList : public QAbstractListModel
{
Q_OBJECT
@ -65,11 +67,6 @@ public:
return m_instDir;
}
/*!
* \brief Loads the instance list. Triggers notifications.
*/
InstListError loadList();
/*!
* \brief Get the instance at index
*/
@ -108,6 +105,12 @@ public
slots:
void on_InstFolderChanged(const Setting &setting, QVariant value);
/*!
* \brief Loads the instance list. Triggers notifications.
*/
InstListError loadList();
void loadForgeInstances(QMap<QString, QString> groupMap);
private
slots:
void propertiesChanged(BaseInstance *inst);
@ -117,6 +120,9 @@ slots:
private:
int getInstIndex(BaseInstance *inst) const;
void continueProcessInstance(BaseInstance *instPtr, const int error, const QDir &dir,
QMap<QString, QString> &groupMap);
protected:
QString m_instDir;
QList<InstancePtr> m_instances;

View File

@ -172,14 +172,14 @@ JavaListLoadTask::~JavaListLoadTask()
void JavaListLoadTask::executeTask()
{
setStatus("Detecting Java installations...");
setStatus(tr("Detecting Java installations..."));
JavaUtils ju;
QList<QString> candidate_paths = ju.FindJavaPaths();
auto job = new JavaCheckerJob("Java detection");
connect(job, SIGNAL(finished(QList<JavaCheckResult>)), this, SLOT(javaCheckerFinished(QList<JavaCheckResult>)));
connect(job, SIGNAL(progress(int, int)), this, SLOT(checkerProgress(int, int)));
m_job = std::shared_ptr<JavaCheckerJob>(new JavaCheckerJob("Java detection"));
connect(m_job.get(), SIGNAL(finished(QList<JavaCheckResult>)), this, SLOT(javaCheckerFinished(QList<JavaCheckResult>)));
connect(m_job.get(), SIGNAL(progress(int, int)), this, SLOT(checkerProgress(int, int)));
QLOG_DEBUG() << "Probing the following Java paths: ";
for(QString candidate : candidate_paths)
@ -188,10 +188,10 @@ void JavaListLoadTask::executeTask()
auto candidate_checker = new JavaChecker();
candidate_checker->path = candidate;
job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker));
m_job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker));
}
job->start();
m_job->start();
}
void JavaListLoadTask::checkerProgress(int current, int total)
@ -203,6 +203,7 @@ void JavaListLoadTask::checkerProgress(int current, int total)
void JavaListLoadTask::javaCheckerFinished(QList<JavaCheckResult> results)
{
QList<JavaVersionPtr> candidates;
m_job.reset();
QLOG_DEBUG() << "Found the following valid Java installations:";
for(JavaCheckResult result : results)

View File

@ -90,6 +90,7 @@ public slots:
void checkerProgress(int current, int total);
protected:
std::shared_ptr<JavaCheckerJob> m_job;
JavaVersionList *m_list;
JavaVersion *m_currentRecommended;
};

View File

@ -139,7 +139,7 @@ MCVListLoadTask::~MCVListLoadTask()
void MCVListLoadTask::executeTask()
{
setStatus("Loading instance version list...");
setStatus(tr("Loading instance version list..."));
auto worker = MMC->qnam();
vlistReply = worker->get(QNetworkRequest(QUrl("http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + "versions.json")));
connect(vlistReply, SIGNAL(finished()), this, SLOT(list_downloaded()));

View File

@ -20,6 +20,7 @@
#include <QCryptographicHash>
#include <QFileInfo>
#include <QDateTime>
#include <QDir>
#include "logger/QsLog.h"
ForgeXzDownload::ForgeXzDownload(QString relative_path, MetaEntryPtr entry) : NetAction()
@ -310,16 +311,51 @@ void ForgeXzDownload::decompressAndInstall()
m_pack200_xz_file.remove();
// revert pack200
pack200_file.close();
QString pack_name = pack200_file.fileName();
pack200_file.seek(0);
int handle_in = pack200_file.handle();
// FIXME: dispose of file handles, pointers and the like. Ideally wrap in objects.
if(handle_in == -1)
{
QLOG_ERROR() << "Error reopening " << pack200_file.fileName();
failAndTryNextMirror();
return;
}
FILE * file_in = fdopen(handle_in,"r");
if(!file_in)
{
QLOG_ERROR() << "Error reopening " << pack200_file.fileName();
failAndTryNextMirror();
return;
}
QFile qfile_out(m_target_path);
if(!qfile_out.open(QIODevice::WriteOnly))
{
QLOG_ERROR() << "Error opening " << qfile_out.fileName();
failAndTryNextMirror();
return;
}
int handle_out = qfile_out.handle();
if(handle_out == -1)
{
QLOG_ERROR() << "Error opening " << qfile_out.fileName();
failAndTryNextMirror();
return;
}
FILE * file_out = fdopen(handle_out,"w");
if(!file_out)
{
QLOG_ERROR() << "Error opening " << qfile_out.fileName();
failAndTryNextMirror();
return;
}
try
{
unpack_200(pack_name.toStdString(), m_target_path.toStdString());
unpack_200(file_in, file_out);
}
catch (std::runtime_error &err)
{
m_status = Job_Failed;
QLOG_ERROR() << "Error unpacking " << pack_name.toUtf8() << " : " << err.what();
QLOG_ERROR() << "Error unpacking " << pack200_file.fileName() << " : " << err.what();
QFile f(m_target_path);
if (f.exists())
f.remove();

View File

@ -23,7 +23,6 @@ MD5EtagDownload::MD5EtagDownload(QUrl url, QString target_path) : NetAction()
{
m_url = url;
m_target_path = target_path;
m_check_md5 = false;
m_status = Job_NotStarted;
}
@ -34,22 +33,26 @@ void MD5EtagDownload::start()
// if there already is a file and md5 checking is in effect and it can be opened
if (m_output_file.exists() && m_output_file.open(QIODevice::ReadOnly))
{
// check the md5 against the expected one
QString hash =
// get the md5 of the local file.
m_local_md5 =
QCryptographicHash::hash(m_output_file.readAll(), QCryptographicHash::Md5)
.toHex()
.constData();
m_output_file.close();
// skip this file if they match
if (m_check_md5 && hash == m_expected_md5)
// if we are expecting some md5sum, compare it with the local one
if (!m_expected_md5.isEmpty())
{
QLOG_INFO() << "Skipping " << m_url.toString() << ": md5 match.";
emit succeeded(m_index_within_job);
return;
// skip if they match
if(m_local_md5 == m_expected_md5)
{
QLOG_INFO() << "Skipping " << m_url.toString() << ": md5 match.";
emit succeeded(m_index_within_job);
return;
}
}
else
{
m_expected_md5 = hash;
// no expected md5. we use the local md5sum as an ETag
}
}
if (!ensureFilePathExists(filename))
@ -58,9 +61,18 @@ void MD5EtagDownload::start()
return;
}
QLOG_INFO() << "Downloading " << m_url.toString() << " expecting " << m_expected_md5;
QNetworkRequest request(m_url);
request.setRawHeader(QString("If-None-Match").toLatin1(), m_expected_md5.toLatin1());
QLOG_INFO() << "Downloading " << m_url.toString() << " got " << m_local_md5;
if(!m_local_md5.isEmpty())
{
QLOG_INFO() << "Got " << m_local_md5;
request.setRawHeader(QString("If-None-Match").toLatin1(), m_local_md5.toLatin1());
}
if(!m_expected_md5.isEmpty())
QLOG_INFO() << "Expecting " << m_expected_md5;
request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
// Go ahead and try to open the file.
@ -107,7 +119,10 @@ void MD5EtagDownload::downloadFinished()
m_status = Job_Finished;
m_output_file.close();
// FIXME: compare with the real written data md5sum
// this is just an ETag
QLOG_INFO() << "Finished " << m_url.toString() << " got " << m_reply->rawHeader("ETag").constData();
m_reply.reset();
emit succeeded(m_index_within_job);
return;
@ -116,6 +131,7 @@ void MD5EtagDownload::downloadFinished()
else
{
m_output_file.close();
m_output_file.remove();
m_reply.reset();
emit failed(m_index_within_job);
return;

View File

@ -23,12 +23,10 @@ class MD5EtagDownload : public NetAction
{
Q_OBJECT
public:
/// if true, check the md5sum against a provided md5sum
/// also, if a file exists, perform an md5sum first and don't download only if they don't
/// match
bool m_check_md5;
/// the expected md5 checksum
/// the expected md5 checksum. Only set from outside
QString m_expected_md5;
/// the md5 checksum of a file that already exists.
QString m_local_md5;
/// if saving to file, use the one specified in this string
QString m_target_path;
/// this is the output file, if any

View File

@ -29,4 +29,6 @@ const QString RESOURCE_BASE("resources.download.minecraft.net/");
const QString LIBRARY_BASE("libraries.minecraft.net/");
const QString SKINS_BASE("skins.minecraft.net/MinecraftSkins/");
const QString AUTH_BASE("authserver.mojang.com/");
const QString FORGE_LEGACY_URL("http://files.minecraftforge.net/minecraftforge/json");
const QString FORGE_GRADLE_URL("http://files.minecraftforge.net/maven/net/minecraftforge/forge/json");
}

View File

@ -0,0 +1,77 @@
#include "SequentialTask.h"
SequentialTask::SequentialTask(QObject *parent) :
Task(parent), m_currentIndex(-1)
{
}
QString SequentialTask::getStatus() const
{
if (m_queue.isEmpty() || m_currentIndex >= m_queue.size())
{
return QString();
}
return m_queue.at(m_currentIndex)->getStatus();
}
void SequentialTask::getProgress(qint64 &current, qint64 &total)
{
current = 0;
total = 0;
for (int i = 0; i < m_queue.size(); ++i)
{
qint64 subCurrent, subTotal;
m_queue.at(i)->getProgress(subCurrent, subTotal);
current += subCurrent;
total += subTotal;
}
}
void SequentialTask::addTask(std::shared_ptr<Task> task)
{
m_queue.append(task);
}
void SequentialTask::executeTask()
{
m_currentIndex = -1;
startNext();
}
void SequentialTask::startNext()
{
if (m_currentIndex != -1)
{
std::shared_ptr<Task> previous = m_queue[m_currentIndex];
disconnect(previous.get(), 0, this, 0);
}
m_currentIndex++;
if (m_queue.isEmpty() || m_currentIndex >= m_queue.size())
{
emitSucceeded();
return;
}
std::shared_ptr<Task> next = m_queue[m_currentIndex];
connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString)));
connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString)));
connect(next.get(), SIGNAL(progress(qint64,qint64)), this, SLOT(subTaskProgress()));
connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext()));
next->start();
emit status(getStatus());
}
void SequentialTask::subTaskFailed(const QString &msg)
{
emitFailed(msg);
}
void SequentialTask::subTaskStatus(const QString &msg)
{
setStatus(msg);
}
void SequentialTask::subTaskProgress()
{
qint64 current, total;
getProgress(current, total);
setProgress(100 * current / total);
}

View File

@ -0,0 +1,32 @@
#pragma once
#include "Task.h"
#include <QQueue>
#include <memory>
class SequentialTask : public Task
{
Q_OBJECT
public:
explicit SequentialTask(QObject *parent = 0);
virtual QString getStatus() const;
virtual void getProgress(qint64 &current, qint64 &total);
void addTask(std::shared_ptr<Task> task);
protected:
void executeTask();
private
slots:
void startNext();
void subTaskFailed(const QString &msg);
void subTaskStatus(const QString &msg);
void subTaskProgress();
private:
QQueue<std::shared_ptr<Task> > m_queue;
int m_currentIndex;
};

View File

@ -0,0 +1,41 @@
#include "ThreadTask.h"
#include <QtConcurrentRun>
ThreadTask::ThreadTask(Task * internal, QObject *parent) : Task(parent), m_internal(internal)
{
}
void ThreadTask::start()
{
connect(m_internal, SIGNAL(failed(QString)), SLOT(iternal_failed(QString)));
connect(m_internal, SIGNAL(progress(qint64,qint64)), SLOT(iternal_progress(qint64,qint64)));
connect(m_internal, SIGNAL(started()), SLOT(iternal_started()));
connect(m_internal, SIGNAL(status(QString)), SLOT(iternal_status(QString)));
connect(m_internal, SIGNAL(succeeded()), SLOT(iternal_succeeded()));
m_running = true;
QtConcurrent::run(m_internal, &Task::start);
}
void ThreadTask::iternal_failed(QString reason)
{
emitFailed(reason);
}
void ThreadTask::iternal_progress(qint64 current, qint64 total)
{
progress(current, total);
}
void ThreadTask::iternal_started()
{
emit started();
}
void ThreadTask::iternal_status(QString status)
{
setStatus(status);
}
void ThreadTask::iternal_succeeded()
{
emitSucceeded();
}

25
logic/tasks/ThreadTask.h Normal file
View File

@ -0,0 +1,25 @@
#pragma once
#include "Task.h"
class ThreadTask : public Task
{
Q_OBJECT
public:
explicit ThreadTask(Task * internal, QObject * parent = nullptr);
protected:
void executeTask() {};
public slots:
virtual void start();
private slots:
void iternal_started();
void iternal_progress(qint64 current, qint64 total);
void iternal_succeeded();
void iternal_failed(QString reason);
void iternal_status(QString status);
private:
Task * m_internal;
};

View File

@ -26,9 +26,8 @@
#include <QDomDocument>
DownloadUpdateTask::DownloadUpdateTask(QString repoUrl, int versionId, QObject* parent) :
Task(parent)
DownloadUpdateTask::DownloadUpdateTask(QString repoUrl, int versionId, QObject *parent)
: Task(parent)
{
m_cVersionId = MMC->version().build;
@ -45,68 +44,69 @@ void DownloadUpdateTask::executeTask()
findCurrentVersionInfo();
}
void DownloadUpdateTask::processChannels()
{
auto checker = MMC->updateChecker();
// Now, check the channel list again.
if (!checker->hasChannels())
{
// We still couldn't load the channel list. Give up. Call loadVersionInfo and return.
QLOG_INFO() << "Reloading the channel list didn't work. Giving up.";
loadVersionInfo();
return;
}
QList<UpdateChecker::ChannelListEntry> channels = checker->getChannelList();
QString channelId = MMC->version().channel;
// Search through the channel list for a channel with the correct ID.
for (auto channel : channels)
{
if (channel.id == channelId)
{
QLOG_INFO() << "Found matching channel.";
m_cRepoUrl = fixPathForTests(channel.url);
break;
}
}
// Now that we've done that, load version info.
loadVersionInfo();
}
void DownloadUpdateTask::findCurrentVersionInfo()
{
setStatus(tr("Finding information about the current version."));
setStatus(tr("Finding information about the current version..."));
auto checker = MMC->updateChecker();
// This runs after we've tried loading the channel list.
// If the channel list doesn't need to be loaded, this will be called immediately.
// If the channel list does need to be loaded, this will be called when it's done.
auto processFunc = [this, &checker] () -> void
{
// Now, check the channel list again.
if (checker->hasChannels())
{
// We still couldn't load the channel list. Give up. Call loadVersionInfo and return.
QLOG_INFO() << "Reloading the channel list didn't work. Giving up.";
loadVersionInfo();
return;
}
QList<UpdateChecker::ChannelListEntry> channels = checker->getChannelList();
QString channelId = MMC->version().channel;
// Search through the channel list for a channel with the correct ID.
for (auto channel : channels)
{
if (channel.id == channelId)
{
QLOG_INFO() << "Found matching channel.";
m_cRepoUrl = channel.url;
break;
}
}
// Now that we've done that, load version info.
loadVersionInfo();
};
if (checker->hasChannels())
if (!checker->hasChannels())
{
// Load the channel list and wait for it to finish loading.
QLOG_INFO() << "No channel list entries found. Will try reloading it.";
QObject::connect(checker.get(), &UpdateChecker::channelListLoaded, processFunc);
QObject::connect(checker.get(), &UpdateChecker::channelListLoaded, this,
&DownloadUpdateTask::processChannels);
checker->updateChanList();
}
else
{
processFunc();
processChannels();
}
}
void DownloadUpdateTask::loadVersionInfo()
{
setStatus(tr("Loading version information."));
setStatus(tr("Loading version information..."));
// Create the net job for loading version info.
NetJob* netJob = new NetJob("Version Info");
NetJob *netJob = new NetJob("Version Info");
// Find the index URL.
QUrl newIndexUrl = QUrl(m_nRepoUrl).resolved(QString::number(m_nVersionId) + ".json");
QLOG_DEBUG() << m_nRepoUrl << " turns into " << newIndexUrl;
// Add a net action to download the version info for the version we're updating to.
netJob->addNetAction(ByteArrayDownload::make(newIndexUrl));
@ -115,10 +115,12 @@ void DownloadUpdateTask::loadVersionInfo()
{
QUrl cIndexUrl = QUrl(m_cRepoUrl).resolved(QString::number(m_cVersionId) + ".json");
netJob->addNetAction(ByteArrayDownload::make(cIndexUrl));
QLOG_DEBUG() << m_cRepoUrl << " turns into " << cIndexUrl;
}
// Connect slots so we know when it's done.
QObject::connect(netJob, &NetJob::succeeded, this, &DownloadUpdateTask::vinfoDownloadFinished);
QObject::connect(netJob, &NetJob::succeeded, this,
&DownloadUpdateTask::vinfoDownloadFinished);
QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::vinfoDownloadFailed);
// Store the NetJob in a class member. We don't want to lose it!
@ -136,7 +138,8 @@ void DownloadUpdateTask::vinfoDownloadFinished()
void DownloadUpdateTask::vinfoDownloadFailed()
{
// Something failed. We really need the second download (current version info), so parse downloads anyways as long as the first one succeeded.
// Something failed. We really need the second download (current version info), so parse
// downloads anyways as long as the first one succeeded.
if (m_vinfoNetJob->first()->m_status != Job_Failed)
{
parseDownloadedVersionInfo();
@ -150,61 +153,73 @@ void DownloadUpdateTask::vinfoDownloadFailed()
void DownloadUpdateTask::parseDownloadedVersionInfo()
{
setStatus(tr("Reading file lists."));
setStatus(tr("Reading file list for new version..."));
QLOG_DEBUG() << "Reading file list for new version...";
QString error;
if (!parseVersionInfo(
std::dynamic_pointer_cast<ByteArrayDownload>(m_vinfoNetJob->first())->m_data,
&m_nVersionFileList, &error))
{
emitFailed(error);
return;
}
parseVersionInfo(NEW_VERSION, &m_nVersionFileList);
// If there is a second entry in the network job's list, load it as the current version's info.
// If there is a second entry in the network job's list, load it as the current version's
// info.
if (m_vinfoNetJob->size() >= 2 && m_vinfoNetJob->operator[](1)->m_status != Job_Failed)
{
parseVersionInfo(CURRENT_VERSION, &m_cVersionFileList);
setStatus(tr("Reading file list for current version..."));
QLOG_DEBUG() << "Reading file list for current version...";
QString error;
parseVersionInfo(
std::dynamic_pointer_cast<ByteArrayDownload>(m_vinfoNetJob->operator[](1))->m_data,
&m_cVersionFileList, &error);
}
// We don't need this any more.
m_vinfoNetJob.reset();
// Now that we're done loading version info, we can move on to the next step. Process file lists and download files.
// Now that we're done loading version info, we can move on to the next step. Process file
// lists and download files.
processFileLists();
}
void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list)
bool DownloadUpdateTask::parseVersionInfo(const QByteArray &data, VersionFileList *list,
QString *error)
{
if (vfile == CURRENT_VERSION) setStatus(tr("Reading file list for current version."));
else if (vfile == NEW_VERSION) setStatus(tr("Reading file list for new version."));
QLOG_DEBUG() << "Reading file list for" << (vfile == NEW_VERSION ? "new" : "current") << "version.";
QByteArray data;
{
ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(
vfile == NEW_VERSION ? m_vinfoNetJob->first() : m_vinfoNetJob->operator[](1));
data = dl->m_data;
}
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
if (jsonError.error != QJsonParseError::NoError)
{
QLOG_ERROR() << "Failed to parse version info JSON:" << jsonError.errorString() << "at" << jsonError.offset;
return;
*error = QString("Failed to parse version info JSON: %1 at %2")
.arg(jsonError.errorString())
.arg(jsonError.offset);
QLOG_ERROR() << error;
return false;
}
QJsonObject json = jsonDoc.object();
QLOG_DEBUG() << data;
QLOG_DEBUG() << "Loading version info from JSON.";
QJsonArray filesArray = json.value("Files").toArray();
for (QJsonValue fileValue : filesArray)
{
QJsonObject fileObj = fileValue.toObject();
VersionFileEntry file{
fileObj.value("Path").toString(),
fileObj.value("Perms").toVariant().toInt(),
FileSourceList(),
fileObj.value("MD5").toString(),
};
QString file_path = fileObj.value("Path").toString();
#ifdef Q_OS_MAC
// On OSX, the paths for the updater need to be fixed.
// basically, anything that isn't in the .app folder is ignored.
// everything else is changed so the code that processes the files actually finds
// them and puts the replacements in the right spots.
if (!fixPathForOSX(file_path))
continue;
#endif
VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(),
FileSourceList(), fileObj.value("MD5").toString(), };
QLOG_DEBUG() << "File" << file.path << "with perms" << file.mode;
QJsonArray sourceArray = fileObj.value("Sources").toArray();
for (QJsonValue val : sourceArray)
{
@ -213,11 +228,14 @@ void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFile
QString type = sourceObj.value("SourceType").toString();
if (type == "http")
{
file.sources.append(FileSource("http", sourceObj.value("Url").toString()));
file.sources.append(
FileSource("http", fixPathForTests(sourceObj.value("Url").toString())));
}
else if (type == "httpc")
{
file.sources.append(FileSource("httpc", sourceObj.value("Url").toString(), sourceObj.value("CompressionType").toString()));
file.sources.append(
FileSource("httpc", fixPathForTests(sourceObj.value("Url").toString()),
sourceObj.value("CompressionType").toString()));
}
else
{
@ -229,78 +247,26 @@ void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFile
list->append(file);
}
return true;
}
void DownloadUpdateTask::processFileLists()
{
setStatus(tr("Processing file lists. Figuring out how to install the update."));
// First, if we've loaded the current version's file list, we need to iterate through it and
// delete anything in the current one version's list that isn't in the new version's list.
for (VersionFileEntry entry : m_cVersionFileList)
{
bool keep = false;
for (VersionFileEntry newEntry : m_nVersionFileList)
{
if (newEntry.path == entry.path)
{
QLOG_DEBUG() << "Not deleting" << entry.path << "because it is still present in the new version.";
keep = true;
break;
}
}
// If the loop reaches the end and we didn't find a match, delete the file.
if(!keep)
m_operationList.append(UpdateOperation::DeleteOp(entry.path));
}
// Create a network job for downloading files.
NetJob* netJob = new NetJob("Update Files");
NetJob *netJob = new NetJob("Update Files");
// Next, check each file in MultiMC's folder and see if we need to update them.
for (VersionFileEntry entry : m_nVersionFileList)
if (!processFileLists(netJob, m_cVersionFileList, m_nVersionFileList, m_operationList))
{
// TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a way to do this in the background.
QString fileMD5;
QFile entryFile(entry.path);
if (entryFile.open(QFile::ReadOnly))
{
QCryptographicHash hash(QCryptographicHash::Md5);
hash.addData(entryFile.readAll());
fileMD5 = hash.result().toHex();
}
if (!entryFile.exists() || fileMD5.isEmpty() || fileMD5 != entry.md5)
{
QLOG_DEBUG() << "Found file" << entry.path << "that needs updating.";
// Go through the sources list and find one to use.
// TODO: Make a NetAction that takes a source list and tries each of them until one works. For now, we'll just use the first http one.
for (FileSource source : entry.sources)
{
if (source.type == "http")
{
QLOG_DEBUG() << "Will download" << entry.path << "from" << source.url;
// Download it to updatedir/<filepath>-<md5> where filepath is the file's path with slashes replaced by underscores.
QString dlPath = PathCombine(m_updateFilesDir.path(), QString(entry.path).replace("/", "_"));
// We need to download the file to the updatefiles folder and add a task to copy it to its install path.
auto download = MD5EtagDownload::make(source.url, dlPath);
download->m_check_md5 = true;
download->m_expected_md5 = entry.md5;
netJob->addNetAction(download);
// Now add a copy operation to our operations list to install the file.
m_operationList.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode));
}
}
}
emitFailed(tr("Failed to process update lists..."));
return;
}
// Add listeners to wait for the downloads to finish.
QObject::connect(netJob, &NetJob::succeeded, this, &DownloadUpdateTask::fileDownloadFinished);
QObject::connect(netJob, &NetJob::progress, this, &DownloadUpdateTask::fileDownloadProgressChanged);
QObject::connect(netJob, &NetJob::succeeded, this,
&DownloadUpdateTask::fileDownloadFinished);
QObject::connect(netJob, &NetJob::progress, this,
&DownloadUpdateTask::fileDownloadProgressChanged);
QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::fileDownloadFailed);
// Now start the download.
@ -312,7 +278,154 @@ void DownloadUpdateTask::processFileLists()
writeInstallScript(m_operationList, PathCombine(m_updateFilesDir.path(), "file_list.xml"));
}
void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QString scriptFile)
bool
DownloadUpdateTask::processFileLists(NetJob *job,
const DownloadUpdateTask::VersionFileList &currentVersion,
const DownloadUpdateTask::VersionFileList &newVersion,
DownloadUpdateTask::UpdateOperationList &ops)
{
setStatus(tr("Processing file lists - figuring out how to install the update..."));
// First, if we've loaded the current version's file list, we need to iterate through it and
// delete anything in the current one version's list that isn't in the new version's list.
for (VersionFileEntry entry : currentVersion)
{
QFileInfo toDelete(entry.path);
if (!toDelete.exists())
{
QLOG_ERROR() << "Expected file " << toDelete.absoluteFilePath()
<< " doesn't exist!";
QLOG_ERROR() << "CWD: " << QDir::currentPath();
}
bool keep = false;
//
for (VersionFileEntry newEntry : newVersion)
{
if (newEntry.path == entry.path)
{
QLOG_DEBUG() << "Not deleting" << entry.path
<< "because it is still present in the new version.";
keep = true;
break;
}
}
// If the loop reaches the end and we didn't find a match, delete the file.
if (!keep)
{
QFileInfo toDelete(entry.path);
if (toDelete.exists())
ops.append(UpdateOperation::DeleteOp(entry.path));
}
}
// Next, check each file in MultiMC's folder and see if we need to update them.
for (VersionFileEntry entry : newVersion)
{
// TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a
// way to do this in the background.
QString fileMD5;
QFile entryFile(entry.path);
QFileInfo entryInfo(entry.path);
bool needs_upgrade = false;
if (!entryFile.exists())
{
needs_upgrade = true;
}
else
{
bool pass = true;
if (!entryInfo.isReadable())
{
QLOG_ERROR() << "File " << entry.path << " is not readable.";
pass = false;
}
if (!entryInfo.isWritable())
{
QLOG_ERROR() << "File " << entry.path << " is not writable.";
pass = false;
}
if (!entryFile.open(QFile::ReadOnly))
{
QLOG_ERROR() << "File " << entry.path << " cannot be opened for reading.";
pass = false;
}
if (!pass)
{
QLOG_ERROR() << "CWD: " << QDir::currentPath();
ops.clear();
return false;
}
}
QCryptographicHash hash(QCryptographicHash::Md5);
auto foo = entryFile.readAll();
hash.addData(foo);
fileMD5 = hash.result().toHex();
if ((fileMD5 != entry.md5))
{
QLOG_DEBUG() << "MD5Sum does not match!";
QLOG_DEBUG() << "Expected:'" << entry.md5 << "'";
QLOG_DEBUG() << "Got: '" << fileMD5 << "'";
needs_upgrade = true;
}
// skip file. it doesn't need an upgrade.
if (!needs_upgrade)
{
QLOG_DEBUG() << "File" << entry.path << " does not need updating.";
continue;
}
// yep. this file actually needs an upgrade. PROCEED.
QLOG_DEBUG() << "Found file" << entry.path << " that needs updating.";
// if it's the updater we want to treat it separately
bool isUpdater = entry.path.endsWith("updater") || entry.path.endsWith("updater.exe");
// Go through the sources list and find one to use.
// TODO: Make a NetAction that takes a source list and tries each of them until one
// works. For now, we'll just use the first http one.
for (FileSource source : entry.sources)
{
if (source.type == "http")
{
QLOG_DEBUG() << "Will download" << entry.path << "from" << source.url;
// Download it to updatedir/<filepath>-<md5> where filepath is the file's
// path with slashes replaced by underscores.
QString dlPath =
PathCombine(m_updateFilesDir.path(), QString(entry.path).replace("/", "_"));
if (isUpdater)
{
auto cache_entry = MMC->metacache()->resolveEntry("root", entry.path);
QLOG_DEBUG() << "Updater will be in " << cache_entry->getFullPath();
if(cache_entry->stale)
{
auto download = CacheDownload::make(QUrl(source.url), cache_entry);
job->addNetAction(download);
}
}
else
{
// We need to download the file to the updatefiles folder and add a task
// to copy it to its install path.
auto download = MD5EtagDownload::make(source.url, dlPath);
download->m_expected_md5 = entry.md5;
job->addNetAction(download);
ops.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode));
}
}
}
}
return true;
}
bool DownloadUpdateTask::writeInstallScript(UpdateOperationList &opsList, QString scriptFile)
{
// Build the base structure of the XML document.
QDomDocument doc;
@ -334,36 +447,38 @@ void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QStrin
switch (op.type)
{
case UpdateOperation::OP_COPY:
{
// Install the file.
QDomElement name = doc.createElement("source");
QDomElement path = doc.createElement("dest");
QDomElement mode = doc.createElement("mode");
name.appendChild(doc.createTextNode(op.file));
path.appendChild(doc.createTextNode(op.dest));
// We need to add a 0 at the beginning here, because Qt doesn't convert to octal correctly.
mode.appendChild(doc.createTextNode("0" + QString::number(op.mode, 8)));
file.appendChild(name);
file.appendChild(path);
file.appendChild(mode);
installFiles.appendChild(file);
QLOG_DEBUG() << "Will install file" << op.file;
}
break;
case UpdateOperation::OP_COPY:
{
// Install the file.
QDomElement name = doc.createElement("source");
QDomElement path = doc.createElement("dest");
QDomElement mode = doc.createElement("mode");
name.appendChild(doc.createTextNode(op.file));
path.appendChild(doc.createTextNode(op.dest));
// We need to add a 0 at the beginning here, because Qt doesn't convert to octal
// correctly.
mode.appendChild(doc.createTextNode("0" + QString::number(op.mode, 8)));
file.appendChild(name);
file.appendChild(path);
file.appendChild(mode);
installFiles.appendChild(file);
QLOG_DEBUG() << "Will install file " << op.file << " to " << op.dest;
}
break;
case UpdateOperation::OP_DELETE:
{
// Delete the file.
file.appendChild(doc.createTextNode(op.file));
removeFiles.appendChild(file);
QLOG_DEBUG() << "Will remove file" << op.file;
}
break;
case UpdateOperation::OP_DELETE:
{
// Delete the file.
file.appendChild(doc.createTextNode(op.file));
removeFiles.appendChild(file);
QLOG_DEBUG() << "Will remove file" << op.file;
}
break;
default:
QLOG_WARN() << "Can't write update operation of type" << op.type << "to file. Not implemented.";
continue;
default:
QLOG_WARN() << "Can't write update operation of type" << op.type
<< "to file. Not implemented.";
continue;
}
}
@ -377,6 +492,36 @@ void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QStrin
else
{
emitFailed(tr("Failed to write update script file."));
return false;
}
return true;
}
QString DownloadUpdateTask::fixPathForTests(const QString &path)
{
if (path.startsWith("$PWD"))
{
QString foo = path;
foo.replace("$PWD", qApp->applicationDirPath());
return QUrl::fromLocalFile(foo).toString(QUrl::FullyEncoded);
}
return path;
}
bool DownloadUpdateTask::fixPathForOSX(QString &path)
{
if (path.startsWith("MultiMC.app/"))
{
// remove the prefix and add a new, more appropriate one.
path.remove(0, 12);
path = QString("../../") + path;
return true;
}
else
{
QLOG_ERROR() << "Update path not within .app: " << path;
return false;
}
}
@ -394,11 +539,10 @@ void DownloadUpdateTask::fileDownloadFailed()
void DownloadUpdateTask::fileDownloadProgressChanged(qint64 current, qint64 total)
{
setProgress((int)(((float)current / (float)total)*100));
setProgress((int)(((float)current / (float)total) * 100));
}
QString DownloadUpdateTask::updateFilesDir()
{
return m_updateFilesDir.path();
}

View File

@ -34,7 +34,8 @@ public:
*/
QString updateFilesDir();
protected:
public:
// TODO: We should probably put these data structures into a separate header...
/*!
@ -53,7 +54,6 @@ protected:
QString url;
QString compressionType;
};
typedef QList<FileSource> FileSourceList;
/*!
@ -66,10 +66,8 @@ protected:
FileSourceList sources;
QString md5;
};
typedef QList<VersionFileEntry> VersionFileList;
/*!
* Structure that describes an operation to perform when installing updates.
*/
@ -100,9 +98,12 @@ protected:
// Yeah yeah, polymorphism blah blah inheritance, blah blah object oriented. I'm lazy, OK?
};
typedef QList<UpdateOperation> UpdateOperationList;
protected:
friend class DownloadUpdateTaskTest;
/*!
* Used for arguments to parseVersionInfo and friends to specify which version info file to parse.
*/
@ -119,6 +120,13 @@ protected:
*/
virtual void findCurrentVersionInfo();
/*!
* This runs after we've tried loading the channel list.
* If the channel list doesn't need to be loaded, this will be called immediately.
* If the channel list does need to be loaded, this will be called when it's done.
*/
void processChannels();
/*!
* Downloads the version info files from the repository.
* The files for both the current build, and the build that we're updating to need to be downloaded.
@ -142,20 +150,25 @@ protected:
/*!
* Loads the file list from the given version info JSON object into the given list.
*/
virtual void parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list);
virtual bool parseVersionInfo(const QByteArray &data, VersionFileList* list, QString *error);
/*!
* Takes a list of file entries for the current version's files and the new version's files
* and populates the downloadList and operationList with information about how to download and install the update.
*/
virtual bool processFileLists(NetJob *job, const VersionFileList &currentVersion, const VersionFileList &newVersion, UpdateOperationList &ops);
/*!
* Calls \see processFileLists to populate the \see m_operationList and a NetJob, and then executes
* the NetJob to fetch all needed files
*/
virtual void processFileLists();
/*!
* Takes the operations list and writes an install script for the updater to the update files directory.
*/
virtual void writeInstallScript(UpdateOperationList& opsList, QString scriptFile);
virtual bool writeInstallScript(UpdateOperationList& opsList, QString scriptFile);
VersionFileList m_downloadList;
UpdateOperationList m_operationList;
VersionFileList m_nVersionFileList;
@ -181,6 +194,26 @@ protected:
*/
QTemporaryDir m_updateFilesDir;
/*!
* Filters paths
* Path of the format $PWD/path, it is converted to a file:///$PWD/ URL
*/
static QString fixPathForTests(const QString &path);
/*!
* Filters paths
* This fixes destination paths for OSX.
* The updater runs in MultiMC.app/Contents/MacOs by default
* The destination paths are such as this: MultiMC.app/blah/blah
*
* Therefore we chop off the 'MultiMC.app' prefix and prepend ../..
*
* Returns false if the path couldn't be fixed (is invalid)
*
* Has no effect on systems that aren't OSX
*/
static bool fixPathForOSX(QString &path);
protected slots:
void vinfoDownloadFinished();
void vinfoDownloadFailed();

View File

@ -44,17 +44,19 @@ QList<UpdateChecker::ChannelListEntry> UpdateChecker::getChannelList() const
bool UpdateChecker::hasChannels() const
{
return m_channels.isEmpty();
return !m_channels.isEmpty();
}
void UpdateChecker::checkForUpdate()
void UpdateChecker::checkForUpdate(bool notifyNoUpdate)
{
QLOG_DEBUG() << "Checking for updates.";
// If the channel list hasn't loaded yet, load it and defer checking for updates until later.
// If the channel list hasn't loaded yet, load it and defer checking for updates until
// later.
if (!m_chanListLoaded)
{
QLOG_DEBUG() << "Channel list isn't loaded yet. Loading channel list and deferring update check.";
QLOG_DEBUG() << "Channel list isn't loaded yet. Loading channel list and deferring "
"update check.";
m_checkUpdateWaiting = true;
updateChanList();
return;
@ -72,7 +74,8 @@ void UpdateChecker::checkForUpdate()
// TODO: Allow user to select channels. For now, we'll just use the current channel.
QString updateChannel = m_currentChannel;
// Find the desired channel within the channel list and get its repo URL. If if cannot be found, error.
// Find the desired channel within the channel list and get its repo URL. If if cannot be
// found, error.
m_repoUrl = "";
for (ChannelListEntry entry : m_channels)
{
@ -91,20 +94,22 @@ void UpdateChecker::checkForUpdate()
auto job = new NetJob("GoUpdate Repository Index");
job->addNetAction(ByteArrayDownload::make(indexUrl));
connect(job, SIGNAL(succeeded()), SLOT(updateCheckFinished()));
connect(job, &NetJob::succeeded, [this, notifyNoUpdate]()
{ updateCheckFinished(notifyNoUpdate); });
connect(job, SIGNAL(failed()), SLOT(updateCheckFailed()));
indexJob.reset(job);
job->start();
}
void UpdateChecker::updateCheckFinished()
void UpdateChecker::updateCheckFinished(bool notifyNoUpdate)
{
QLOG_DEBUG() << "Finished downloading repo index. Checking for new versions.";
QJsonParseError jsonError;
QByteArray data;
{
ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(indexJob->first());
ByteArrayDownloadPtr dl =
std::dynamic_pointer_cast<ByteArrayDownload>(indexJob->first());
data = dl->m_data;
indexJob.reset();
}
@ -112,7 +117,8 @@ void UpdateChecker::updateCheckFinished()
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject())
{
QLOG_ERROR() << "Failed to parse GoUpdate repository index. JSON error" << jsonError.errorString() << "at offset" << jsonError.offset;
QLOG_ERROR() << "Failed to parse GoUpdate repository index. JSON error"
<< jsonError.errorString() << "at offset" << jsonError.offset;
return;
}
@ -122,7 +128,8 @@ void UpdateChecker::updateCheckFinished()
int apiVersion = object.value("ApiVersion").toVariant().toInt(&success);
if (apiVersion != API_VERSION || !success)
{
QLOG_ERROR() << "Failed to check for updates. API version mismatch. We're using" << API_VERSION << "server has" << apiVersion;
QLOG_ERROR() << "Failed to check for updates. API version mismatch. We're using"
<< API_VERSION << "server has" << apiVersion;
return;
}
@ -132,19 +139,27 @@ void UpdateChecker::updateCheckFinished()
for (QJsonValue versionVal : versions)
{
QJsonObject version = versionVal.toObject();
if (newestVersion.value("Id").toVariant().toInt() < version.value("Id").toVariant().toInt())
if (newestVersion.value("Id").toVariant().toInt() <
version.value("Id").toVariant().toInt())
{
QLOG_DEBUG() << "Found newer version with ID" << version.value("Id").toVariant().toInt();
QLOG_DEBUG() << "Found newer version with ID"
<< version.value("Id").toVariant().toInt();
newestVersion = version;
}
}
// We've got the version with the greatest ID number. Now compare it to our current build number and update if they're different.
// We've got the version with the greatest ID number. Now compare it to our current build
// number and update if they're different.
int newBuildNumber = newestVersion.value("Id").toVariant().toInt();
if (newBuildNumber != MMC->version().build)
{
// Update!
emit updateAvailable(m_repoUrl, newestVersion.value("Name").toVariant().toString(), newBuildNumber);
emit updateAvailable(m_repoUrl, newestVersion.value("Name").toVariant().toString(),
newBuildNumber);
}
else if (notifyNoUpdate)
{
emit noUpdateFound();
}
m_updateChecking = false;
@ -163,12 +178,13 @@ void UpdateChecker::updateChanList()
if (m_channelListUrl.isEmpty())
{
QLOG_ERROR() << "Failed to update channel list. No channel list URL set."
<< "If you'd like to use MultiMC's update system, please pass the channel list URL to CMake at compile time.";
<< "If you'd like to use MultiMC's update system, please pass the channel "
"list URL to CMake at compile time.";
return;
}
m_chanListLoading = true;
NetJob* job = new NetJob("Update System Channel List");
NetJob *job = new NetJob("Update System Channel List");
job->addNetAction(ByteArrayDownload::make(QUrl(m_channelListUrl)));
QObject::connect(job, &NetJob::succeeded, this, &UpdateChecker::chanListDownloadFinished);
QObject::connect(job, &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed);
@ -180,7 +196,8 @@ void UpdateChecker::chanListDownloadFinished()
{
QByteArray data;
{
ByteArrayDownloadPtr dl = std::dynamic_pointer_cast<ByteArrayDownload>(chanListJob->first());
ByteArrayDownloadPtr dl =
std::dynamic_pointer_cast<ByteArrayDownload>(chanListJob->first());
data = dl->m_data;
chanListJob.reset();
}
@ -190,17 +207,20 @@ void UpdateChecker::chanListDownloadFinished()
if (jsonError.error != QJsonParseError::NoError)
{
// TODO: Report errors to the user.
QLOG_ERROR() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" << jsonError.offset;
QLOG_ERROR() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at"
<< jsonError.offset;
return;
}
QJsonObject object = jsonDoc.object();
bool success = false;
int formatVersion = object.value("format_version").toVariant().toInt(&success);
if (formatVersion != CHANLIST_FORMAT || !success)
{
QLOG_ERROR() << "Failed to check for updates. Channel list format version mismatch. We're using" << CHANLIST_FORMAT << "server has" << formatVersion;
QLOG_ERROR()
<< "Failed to check for updates. Channel list format version mismatch. We're using"
<< CHANLIST_FORMAT << "server has" << formatVersion;
return;
}
@ -210,12 +230,10 @@ void UpdateChecker::chanListDownloadFinished()
for (QJsonValue chanVal : channelArray)
{
QJsonObject channelObj = chanVal.toObject();
ChannelListEntry entry{
channelObj.value("id").toVariant().toString(),
channelObj.value("name").toVariant().toString(),
channelObj.value("description").toVariant().toString(),
channelObj.value("url").toVariant().toString()
};
ChannelListEntry entry{channelObj.value("id").toVariant().toString(),
channelObj.value("name").toVariant().toString(),
channelObj.value("description").toVariant().toString(),
channelObj.value("url").toVariant().toString()};
if (entry.id.isEmpty() || entry.name.isEmpty() || entry.url.isEmpty())
{
QLOG_ERROR() << "Channel list entry with empty ID, name, or URL. Skipping.";
@ -233,7 +251,7 @@ void UpdateChecker::chanListDownloadFinished()
// If we're waiting to check for updates, do that now.
if (m_checkUpdateWaiting)
checkForUpdate();
checkForUpdate(false);
emit channelListLoaded();
}
@ -244,4 +262,3 @@ void UpdateChecker::chanListDownloadFailed()
QLOG_ERROR() << "Failed to download channel list.";
emit channelListLoaded();
}

View File

@ -25,7 +25,10 @@ class UpdateChecker : public QObject
public:
UpdateChecker();
void checkForUpdate();
void checkForUpdate(bool notifyNoUpdate);
void setCurrentChannel(const QString &channel) { m_currentChannel = channel; }
void setChannelListUrl(const QString &url) { m_channelListUrl = url; }
/*!
* Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake).
@ -62,14 +65,18 @@ signals:
//! Signal emitted when the channel list finishes loading or fails to load.
void channelListLoaded();
void noUpdateFound();
private slots:
void updateCheckFinished();
void updateCheckFinished(bool notifyNoUpdate);
void updateCheckFailed();
void chanListDownloadFinished();
void chanListDownloadFailed();
private:
friend class UpdateCheckerTest;
NetJobPtr indexJob;
NetJobPtr chanListJob;