Merge branch 'develop' of github.com:MultiMC/MultiMC5 into feature_news
Conflicts: CMakeLists.txt gui/MainWindow.h
This commit is contained in:
@ -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()
|
||||
|
@ -184,6 +184,9 @@ signals:
|
||||
*/
|
||||
void nuked(BaseInstance *inst);
|
||||
|
||||
protected slots:
|
||||
void iconUpdated(QString key);
|
||||
|
||||
protected:
|
||||
std::shared_ptr<BaseInstancePrivate> inst_d;
|
||||
};
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
16
logic/LegacyFTBInstance.cpp
Normal file
16
logic/LegacyFTBInstance.cpp
Normal 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
13
logic/LegacyFTBInstance.h
Normal 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;
|
||||
};
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
102
logic/LiteLoaderInstaller.cpp
Normal file
102
logic/LiteLoaderInstaller.cpp
Normal 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);
|
||||
}
|
39
logic/LiteLoaderInstaller.h
Normal file
39
logic/LiteLoaderInstaller.h
Normal 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;
|
||||
};
|
143
logic/Mod.cpp
143
logic/Mod.cpp
@ -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();
|
||||
}
|
||||
|
32
logic/Mod.h
32
logic/Mod.h
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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
120
logic/OneSixFTBInstance.cpp
Normal 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
20
logic/OneSixFTBInstance.h
Normal 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;
|
||||
};
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -68,6 +68,12 @@ public:
|
||||
m_name = name;
|
||||
}
|
||||
|
||||
/// Returns the raw name field
|
||||
QString rawName() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
QJsonObject toJson();
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
||||
|
143
logic/assets/AssetsMigrateTask.cpp
Normal file
143
logic/assets/AssetsMigrateTask.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
|
18
logic/assets/AssetsMigrateTask.h
Normal file
18
logic/assets/AssetsMigrateTask.h
Normal 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;
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -34,6 +34,6 @@ struct AssetsIndex
|
||||
|
||||
namespace AssetsUtils
|
||||
{
|
||||
void migrateOldAssets();
|
||||
bool loadAssetsIndexJson(QString file, AssetsIndex* index);
|
||||
int findLegacyAssets();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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...");
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
351
logic/icons/IconList.cpp
Normal 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"
|
@ -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
89
logic/icons/MMCIcon.cpp
Normal 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
52
logic/icons/MMCIcon.h
Normal 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());
|
||||
};
|
@ -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.";
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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"
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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()));
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
}
|
||||
|
77
logic/tasks/SequentialTask.cpp
Normal file
77
logic/tasks/SequentialTask.cpp
Normal 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 ¤t, 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);
|
||||
}
|
32
logic/tasks/SequentialTask.h
Normal file
32
logic/tasks/SequentialTask.h
Normal 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 ¤t, 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;
|
||||
};
|
41
logic/tasks/ThreadTask.cpp
Normal file
41
logic/tasks/ThreadTask.cpp
Normal 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
25
logic/tasks/ThreadTask.h
Normal 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;
|
||||
};
|
@ -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 ¤tVersion,
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -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 ¤tVersion, 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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
Reference in New Issue
Block a user