1 /* Copyright 2013-2019 MultiMC Contributors
2  *
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 #include <QFile>
17 #include <QCryptographicHash>
18 #include <Version.h>
19 #include <QDir>
20 #include <QJsonDocument>
21 #include <QJsonArray>
22 #include <QDebug>
23 
24 #include "Exception.h"
25 #include <minecraft/OneSixVersionFormat.h>
26 #include <FileSystem.h>
27 #include <QSaveFile>
28 #include <Env.h>
29 #include <meta/Index.h>
30 #include <minecraft/MinecraftInstance.h>
31 #include <QUuid>
32 #include <QTimer>
33 #include <Json.h>
34 
35 #include "ComponentList.h"
36 #include "ComponentList_p.h"
37 #include "ComponentUpdateTask.h"
38 
ComponentList(MinecraftInstance * instance)39 ComponentList::ComponentList(MinecraftInstance * instance)
40     : QAbstractListModel()
41 {
42     d.reset(new ComponentListData);
43     d->m_instance = instance;
44     d->m_saveTimer.setSingleShot(true);
45     d->m_saveTimer.setInterval(5000);
46     d->interactionDisabled = instance->isRunning();
47     connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &ComponentList::disableInteraction);
48     connect(&d->m_saveTimer, &QTimer::timeout, this, &ComponentList::save_internal);
49 }
50 
~ComponentList()51 ComponentList::~ComponentList()
52 {
53     saveNow();
54 }
55 
56 // BEGIN: component file format
57 
58 static const int currentComponentsFileVersion = 1;
59 
componentToJsonV1(ComponentPtr component)60 static QJsonObject componentToJsonV1(ComponentPtr component)
61 {
62     QJsonObject obj;
63     // critical
64     obj.insert("uid", component->m_uid);
65     if(!component->m_version.isEmpty())
66     {
67         obj.insert("version", component->m_version);
68     }
69     if(component->m_dependencyOnly)
70     {
71         obj.insert("dependencyOnly", true);
72     }
73     if(component->m_important)
74     {
75         obj.insert("important", true);
76     }
77     if(component->m_disabled)
78     {
79         obj.insert("disabled", true);
80     }
81 
82     // cached
83     if(!component->m_cachedVersion.isEmpty())
84     {
85         obj.insert("cachedVersion", component->m_cachedVersion);
86     }
87     if(!component->m_cachedName.isEmpty())
88     {
89         obj.insert("cachedName", component->m_cachedName);
90     }
91     Meta::serializeRequires(obj, &component->m_cachedRequires, "cachedRequires");
92     Meta::serializeRequires(obj, &component->m_cachedConflicts, "cachedConflicts");
93     if(component->m_cachedVolatile)
94     {
95         obj.insert("cachedVolatile", true);
96     }
97     return obj;
98 }
99 
componentFromJsonV1(ComponentList * parent,const QString & componentJsonPattern,const QJsonObject & obj)100 static ComponentPtr componentFromJsonV1(ComponentList * parent, const QString & componentJsonPattern, const QJsonObject &obj)
101 {
102     // critical
103     auto uid = Json::requireString(obj.value("uid"));
104     auto filePath = componentJsonPattern.arg(uid);
105     auto component = new Component(parent, uid);
106     component->m_version = Json::ensureString(obj.value("version"));
107     component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false);
108     component->m_important = Json::ensureBoolean(obj.value("important"), false);
109 
110     // cached
111     // TODO @RESILIENCE: ignore invalid values/structure here?
112     component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion"));
113     component->m_cachedName = Json::ensureString(obj.value("cachedName"));
114     Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires");
115     Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts");
116     component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false);
117     bool disabled = Json::ensureBoolean(obj.value("disabled"), false);
118     component->setEnabled(!disabled);
119     return component;
120 }
121 
122 // Save the given component container data to a file
saveComponentList(const QString & filename,const ComponentContainer & container)123 static bool saveComponentList(const QString & filename, const ComponentContainer & container)
124 {
125     QJsonObject obj;
126     obj.insert("formatVersion", currentComponentsFileVersion);
127     QJsonArray orderArray;
128     for(auto component: container)
129     {
130         orderArray.append(componentToJsonV1(component));
131     }
132     obj.insert("components", orderArray);
133     QSaveFile outFile(filename);
134     if (!outFile.open(QFile::WriteOnly))
135     {
136         qCritical() << "Couldn't open" << outFile.fileName()
137                      << "for writing:" << outFile.errorString();
138         return false;
139     }
140     auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented);
141     if(outFile.write(data) != data.size())
142     {
143         qCritical() << "Couldn't write all the data into" << outFile.fileName()
144                      << "because:" << outFile.errorString();
145         return false;
146     }
147     if(!outFile.commit())
148     {
149         qCritical() << "Couldn't save" << outFile.fileName()
150                      << "because:" << outFile.errorString();
151     }
152     return true;
153 }
154 
155 // Read the given file into component containers
loadComponentList(ComponentList * parent,const QString & filename,const QString & componentJsonPattern,ComponentContainer & container)156 static bool loadComponentList(ComponentList * parent, const QString & filename, const QString & componentJsonPattern, ComponentContainer & container)
157 {
158     QFile componentsFile(filename);
159     if (!componentsFile.exists())
160     {
161         qWarning() << "Components file doesn't exist. This should never happen.";
162         return false;
163     }
164     if (!componentsFile.open(QFile::ReadOnly))
165     {
166         qCritical() << "Couldn't open" << componentsFile.fileName()
167                      << " for reading:" << componentsFile.errorString();
168         qWarning() << "Ignoring overriden order";
169         return false;
170     }
171 
172     // and it's valid JSON
173     QJsonParseError error;
174     QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
175     if (error.error != QJsonParseError::NoError)
176     {
177         qCritical() << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString();
178         qWarning() << "Ignoring overriden order";
179         return false;
180     }
181 
182     // and then read it and process it if all above is true.
183     try
184     {
185         auto obj = Json::requireObject(doc);
186         // check order file version.
187         auto version = Json::requireInteger(obj.value("formatVersion"));
188         if (version != currentComponentsFileVersion)
189         {
190             throw JSONValidationError(QObject::tr("Invalid component file version, expected %1")
191                                           .arg(currentComponentsFileVersion));
192         }
193         auto orderArray = Json::requireArray(obj.value("components"));
194         for(auto item: orderArray)
195         {
196             auto obj = Json::requireObject(item, "Component must be an object.");
197             container.append(componentFromJsonV1(parent, componentJsonPattern, obj));
198         }
199     }
200     catch (const JSONValidationError &err)
201     {
202         qCritical() << "Couldn't parse" << componentsFile.fileName() << ": bad file format";
203         container.clear();
204         return false;
205     }
206     return true;
207 }
208 
209 // END: component file format
210 
211 // BEGIN: save/load logic
212 
saveNow()213 void ComponentList::saveNow()
214 {
215     if(saveIsScheduled())
216     {
217         d->m_saveTimer.stop();
218         save_internal();
219     }
220 }
221 
saveIsScheduled() const222 bool ComponentList::saveIsScheduled() const
223 {
224     return d->dirty;
225 }
226 
buildingFromScratch()227 void ComponentList::buildingFromScratch()
228 {
229     d->loaded = true;
230     d->dirty = true;
231 }
232 
scheduleSave()233 void ComponentList::scheduleSave()
234 {
235     if(!d->loaded)
236     {
237         qDebug() << "Component list should never save if it didn't successfully load, instance:" << d->m_instance->name();
238         return;
239     }
240     if(!d->dirty)
241     {
242         d->dirty = true;
243         qDebug() << "Component list save is scheduled for" << d->m_instance->name();
244     }
245     d->m_saveTimer.start();
246 }
247 
componentsFilePath() const248 QString ComponentList::componentsFilePath() const
249 {
250     return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json");
251 }
252 
patchesPattern() const253 QString ComponentList::patchesPattern() const
254 {
255     return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json");
256 }
257 
patchFilePathForUid(const QString & uid) const258 QString ComponentList::patchFilePathForUid(const QString& uid) const
259 {
260     return patchesPattern().arg(uid);
261 }
262 
save_internal()263 void ComponentList::save_internal()
264 {
265     qDebug() << "Component list save performed now for" << d->m_instance->name();
266     auto filename = componentsFilePath();
267     saveComponentList(filename, d->components);
268     d->dirty = false;
269 }
270 
load()271 bool ComponentList::load()
272 {
273     auto filename = componentsFilePath();
274     QFile componentsFile(filename);
275 
276     // migrate old config to new one, if needed
277     if(!componentsFile.exists())
278     {
279         if(!migratePreComponentConfig())
280         {
281             // FIXME: the user should be notified...
282             qCritical() << "Failed to convert old pre-component config for instance" << d->m_instance->name();
283             return false;
284         }
285     }
286 
287     // load the new component list and swap it with the current one...
288     ComponentContainer newComponents;
289     if(!loadComponentList(this, filename, patchesPattern(), newComponents))
290     {
291         qCritical() << "Failed to load the component config for instance" << d->m_instance->name();
292         return false;
293     }
294     else
295     {
296         // FIXME: actually use fine-grained updates, not this...
297         beginResetModel();
298         // disconnect all the old components
299         for(auto component: d->components)
300         {
301             disconnect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged);
302         }
303         d->components.clear();
304         d->componentIndex.clear();
305         for(auto component: newComponents)
306         {
307             if(d->componentIndex.contains(component->m_uid))
308             {
309                 qWarning() << "Ignoring duplicate component entry" << component->m_uid;
310                 continue;
311             }
312             connect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged);
313             d->components.append(component);
314             d->componentIndex[component->m_uid] = component;
315         }
316         endResetModel();
317         d->loaded = true;
318         return true;
319     }
320 }
321 
reload(Net::Mode netmode)322 void ComponentList::reload(Net::Mode netmode)
323 {
324     // Do not reload when the update/resolve task is running. It is in control.
325     if(d->m_updateTask)
326     {
327         return;
328     }
329 
330     // flush any scheduled saves to not lose state
331     saveNow();
332 
333     // FIXME: differentiate when a reapply is required by propagating state from components
334     invalidateLaunchProfile();
335 
336     if(load())
337     {
338         resolve(netmode);
339     }
340 }
341 
getCurrentTask()342 shared_qobject_ptr<Task> ComponentList::getCurrentTask()
343 {
344     return d->m_updateTask;
345 }
346 
resolve(Net::Mode netmode)347 void ComponentList::resolve(Net::Mode netmode)
348 {
349     auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this);
350     d->m_updateTask.reset(updateTask);
351     connect(updateTask, &ComponentUpdateTask::succeeded, this, &ComponentList::updateSucceeded);
352     connect(updateTask, &ComponentUpdateTask::failed, this, &ComponentList::updateFailed);
353     d->m_updateTask->start();
354 }
355 
356 
updateSucceeded()357 void ComponentList::updateSucceeded()
358 {
359     qDebug() << "Component list update/resolve task succeeded for" << d->m_instance->name();
360     d->m_updateTask.reset();
361     invalidateLaunchProfile();
362 }
363 
updateFailed(const QString & error)364 void ComponentList::updateFailed(const QString& error)
365 {
366     qDebug() << "Component list update/resolve task failed for" << d->m_instance->name() << "Reason:" << error;
367     d->m_updateTask.reset();
368     invalidateLaunchProfile();
369 }
370 
371 // NOTE this is really old stuff, and only needs to be used when loading the old hardcoded component-unaware format (loadPreComponentConfig).
upgradeDeprecatedFiles(QString root,QString instanceName)372 static void upgradeDeprecatedFiles(QString root, QString instanceName)
373 {
374     auto versionJsonPath = FS::PathCombine(root, "version.json");
375     auto customJsonPath = FS::PathCombine(root, "custom.json");
376     auto mcJson = FS::PathCombine(root, "patches" , "net.minecraft.json");
377 
378     QString sourceFile;
379     QString renameFile;
380 
381     // convert old crap.
382     if(QFile::exists(customJsonPath))
383     {
384         sourceFile = customJsonPath;
385         renameFile = versionJsonPath;
386     }
387     else if(QFile::exists(versionJsonPath))
388     {
389         sourceFile = versionJsonPath;
390     }
391     if(!sourceFile.isEmpty() && !QFile::exists(mcJson))
392     {
393         if(!FS::ensureFilePathExists(mcJson))
394         {
395             qWarning() << "Couldn't create patches folder for" << instanceName;
396             return;
397         }
398         if(!renameFile.isEmpty() && QFile::exists(renameFile))
399         {
400             if(!QFile::rename(renameFile, renameFile + ".old"))
401             {
402                 qWarning() << "Couldn't rename" << renameFile << "to" << renameFile + ".old" << "in" << instanceName;
403                 return;
404             }
405         }
406         auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false);
407         ProfileUtils::removeLwjglFromPatch(file);
408         file->uid = "net.minecraft";
409         file->version = file->minecraftVersion;
410         file->name = "Minecraft";
411 
412         Meta::Require needsLwjgl;
413         needsLwjgl.uid = "org.lwjgl";
414         file->requires.insert(needsLwjgl);
415 
416         if(!ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), mcJson))
417         {
418             return;
419         }
420         if(!QFile::rename(sourceFile, sourceFile + ".old"))
421         {
422             qWarning() << "Couldn't rename" << sourceFile << "to" << sourceFile + ".old" << "in" << instanceName;
423             return;
424         }
425     }
426 }
427 
428 /*
429  * Migrate old layout to the component based one...
430  * - Part of the version information is taken from `instance.cfg` (fed to this class from outside).
431  * - Part is taken from the old order.json file.
432  * - Part is loaded from loose json files in the instance's `patches` directory.
433  */
migratePreComponentConfig()434 bool ComponentList::migratePreComponentConfig()
435 {
436     // upgrade the very old files from the beginnings of MultiMC 5
437     upgradeDeprecatedFiles(d->m_instance->instanceRoot(), d->m_instance->name());
438 
439     QList<ComponentPtr> components;
440     QSet<QString> loaded;
441 
442     auto addBuiltinPatch = [&](const QString &uid, bool asDependency, const QString & emptyVersion, const Meta::Require & req, const Meta::Require & conflict)
443     {
444         auto jsonFilePath = FS::PathCombine(d->m_instance->instanceRoot(), "patches" , uid + ".json");
445         auto intendedVersion = d->getOldConfigVersion(uid);
446         // load up the base minecraft patch
447         ComponentPtr component;
448         if(QFile::exists(jsonFilePath))
449         {
450             if(intendedVersion.isEmpty())
451             {
452                 intendedVersion = emptyVersion;
453             }
454             auto file = ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false);
455             // fix uid
456             file->uid = uid;
457             // if version is missing, add it from the outside.
458             if(file->version.isEmpty())
459             {
460                 file->version = intendedVersion;
461             }
462             // if this is a dependency (LWJGL), mark it also as volatile
463             if(asDependency)
464             {
465                 file->m_volatile = true;
466             }
467             // insert requirements if needed
468             if(!req.uid.isEmpty())
469             {
470                 file->requires.insert(req);
471             }
472             // insert conflicts if needed
473             if(!conflict.uid.isEmpty())
474             {
475                 file->conflicts.insert(conflict);
476             }
477             // FIXME: @QUALITY do not ignore return value
478             ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), jsonFilePath);
479             component = new Component(this, uid, file);
480             component->m_version = intendedVersion;
481         }
482         else if(!intendedVersion.isEmpty())
483         {
484             auto metaVersion = ENV.metadataIndex()->get(uid, intendedVersion);
485             component = new Component(this, metaVersion);
486         }
487         else
488         {
489             return;
490         }
491         component->m_dependencyOnly = asDependency;
492         component->m_important = !asDependency;
493         components.append(component);
494     };
495     // TODO: insert depends and conflicts here if these are customized files...
496     Meta::Require reqLwjgl;
497     reqLwjgl.uid = "org.lwjgl";
498     reqLwjgl.suggests = "2.9.1";
499     Meta::Require conflictLwjgl3;
500     conflictLwjgl3.uid = "org.lwjgl3";
501     Meta::Require nullReq;
502     addBuiltinPatch("org.lwjgl", true, "2.9.1", nullReq, conflictLwjgl3);
503     addBuiltinPatch("net.minecraft", false, QString(), reqLwjgl, nullReq);
504 
505     // first, collect all other file-based patches and load them
506     QMap<QString, ComponentPtr> loadedComponents;
507     QDir patchesDir(FS::PathCombine(d->m_instance->instanceRoot(),"patches"));
508     for (auto info : patchesDir.entryInfoList(QStringList() << "*.json", QDir::Files))
509     {
510         // parse the file
511         qDebug() << "Reading" << info.fileName();
512         auto file = ProfileUtils::parseJsonFile(info, true);
513 
514         // correct missing or wrong uid based on the file name
515         QString uid = info.completeBaseName();
516 
517         // ignore builtins, they've been handled already
518         if (uid == "net.minecraft")
519             continue;
520         if (uid == "org.lwjgl")
521             continue;
522 
523         // handle horrible corner cases
524         if(uid.isEmpty())
525         {
526             // if you have a file named '.json', make it just go away.
527             // FIXME: @QUALITY do not ignore return value
528             QFile::remove(info.absoluteFilePath());
529             continue;
530         }
531         file->uid = uid;
532         // FIXME: @QUALITY do not ignore return value
533         ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), info.absoluteFilePath());
534 
535         auto component = new Component(this, file->uid, file);
536         auto version = d->getOldConfigVersion(file->uid);
537         if(!version.isEmpty())
538         {
539             component->m_version = version;
540         }
541         loadedComponents[file->uid] = component;
542     }
543     // try to load the other 'hardcoded' patches (forge, liteloader), if they weren't loaded from files
544     auto loadSpecial = [&](const QString & uid, int order)
545     {
546         auto patchVersion = d->getOldConfigVersion(uid);
547         if(!patchVersion.isEmpty() && !loadedComponents.contains(uid))
548         {
549             auto patch = new Component(this, ENV.metadataIndex()->get(uid, patchVersion));
550             patch->setOrder(order);
551             loadedComponents[uid] = patch;
552         }
553     };
554     loadSpecial("net.minecraftforge", 5);
555     loadSpecial("com.mumfrey.liteloader", 10);
556 
557     // load the old order.json file, if present
558     ProfileUtils::PatchOrder userOrder;
559     ProfileUtils::readOverrideOrders(FS::PathCombine(d->m_instance->instanceRoot(), "order.json"), userOrder);
560 
561     // now add all the patches by user sort order
562     for (auto uid : userOrder)
563     {
564         // ignore builtins
565         if (uid == "net.minecraft")
566             continue;
567         if (uid == "org.lwjgl")
568             continue;
569         // ordering has a patch that is gone?
570         if(!loadedComponents.contains(uid))
571         {
572             continue;
573         }
574         components.append(loadedComponents.take(uid));
575     }
576 
577     // is there anything left to sort? - this is used when there are leftover components that aren't part of the order.json
578     if(!loadedComponents.isEmpty())
579     {
580         // inserting into multimap by order number as key sorts the patches and detects duplicates
581         QMultiMap<int, ComponentPtr> files;
582         auto iter = loadedComponents.begin();
583         while(iter != loadedComponents.end())
584         {
585             files.insert((*iter)->getOrder(), *iter);
586             iter++;
587         }
588 
589         // then just extract the patches and put them in the list
590         for (auto order : files.keys())
591         {
592             const auto &values = files.values(order);
593             for(auto &value: values)
594             {
595                 // TODO: put back the insertion of problem messages here, so the user knows about the id duplication
596                 components.append(value);
597             }
598         }
599     }
600     // new we have a complete list of components...
601     return saveComponentList(componentsFilePath(), components);
602 }
603 
604 // END: save/load
605 
appendComponent(ComponentPtr component)606 void ComponentList::appendComponent(ComponentPtr component)
607 {
608     insertComponent(d->components.size(), component);
609 }
610 
insertComponent(size_t index,ComponentPtr component)611 void ComponentList::insertComponent(size_t index, ComponentPtr component)
612 {
613     auto id = component->getID();
614     if(id.isEmpty())
615     {
616         qWarning() << "Attempt to add a component with empty ID!";
617         return;
618     }
619     if(d->componentIndex.contains(id))
620     {
621         qWarning() << "Attempt to add a component that is already present!";
622         return;
623     }
624     beginInsertRows(QModelIndex(), index, index);
625     d->components.insert(index, component);
626     d->componentIndex[id] = component;
627     endInsertRows();
628     connect(component.get(), &Component::dataChanged, this, &ComponentList::componentDataChanged);
629     scheduleSave();
630 }
631 
componentDataChanged()632 void ComponentList::componentDataChanged()
633 {
634     auto objPtr = qobject_cast<Component *>(sender());
635     if(!objPtr)
636     {
637         qWarning() << "ComponentList got dataChenged signal from a non-Component!";
638         return;
639     }
640     if(objPtr->getID() == "net.minecraft") {
641         emit minecraftChanged();
642     }
643     // figure out which one is it... in a seriously dumb way.
644     int index = 0;
645     for (auto component: d->components)
646     {
647         if(component.get() == objPtr)
648         {
649             emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1));
650             scheduleSave();
651             return;
652         }
653         index++;
654     }
655     qWarning() << "ComponentList got dataChenged signal from a Component which does not belong to it!";
656 }
657 
remove(const int index)658 bool ComponentList::remove(const int index)
659 {
660     auto patch = getComponent(index);
661     if (!patch->isRemovable())
662     {
663         qWarning() << "Patch" << patch->getID() << "is non-removable";
664         return false;
665     }
666 
667     if(!removeComponent_internal(patch))
668     {
669         qCritical() << "Patch" << patch->getID() << "could not be removed";
670         return false;
671     }
672 
673     beginRemoveRows(QModelIndex(), index, index);
674     d->components.removeAt(index);
675     d->componentIndex.remove(patch->getID());
676     endRemoveRows();
677     invalidateLaunchProfile();
678     scheduleSave();
679     return true;
680 }
681 
remove(const QString id)682 bool ComponentList::remove(const QString id)
683 {
684     int i = 0;
685     for (auto patch : d->components)
686     {
687         if (patch->getID() == id)
688         {
689             return remove(i);
690         }
691         i++;
692     }
693     return false;
694 }
695 
customize(int index)696 bool ComponentList::customize(int index)
697 {
698     auto patch = getComponent(index);
699     if (!patch->isCustomizable())
700     {
701         qDebug() << "Patch" << patch->getID() << "is not customizable";
702         return false;
703     }
704     if(!patch->customize())
705     {
706         qCritical() << "Patch" << patch->getID() << "could not be customized";
707         return false;
708     }
709     invalidateLaunchProfile();
710     scheduleSave();
711     return true;
712 }
713 
revertToBase(int index)714 bool ComponentList::revertToBase(int index)
715 {
716     auto patch = getComponent(index);
717     if (!patch->isRevertible())
718     {
719         qDebug() << "Patch" << patch->getID() << "is not revertible";
720         return false;
721     }
722     if(!patch->revert())
723     {
724         qCritical() << "Patch" << patch->getID() << "could not be reverted";
725         return false;
726     }
727     invalidateLaunchProfile();
728     scheduleSave();
729     return true;
730 }
731 
getComponent(const QString & id)732 Component * ComponentList::getComponent(const QString &id)
733 {
734     auto iter = d->componentIndex.find(id);
735     if (iter == d->componentIndex.end())
736     {
737         return nullptr;
738     }
739     return (*iter).get();
740 }
741 
getComponent(int index)742 Component * ComponentList::getComponent(int index)
743 {
744     if(index < 0 || index >= d->components.size())
745     {
746         return nullptr;
747     }
748     return d->components[index].get();
749 }
750 
data(const QModelIndex & index,int role) const751 QVariant ComponentList::data(const QModelIndex &index, int role) const
752 {
753     if (!index.isValid())
754         return QVariant();
755 
756     int row = index.row();
757     int column = index.column();
758 
759     if (row < 0 || row >= d->components.size())
760         return QVariant();
761 
762     auto patch = d->components.at(row);
763 
764     switch (role)
765     {
766     case Qt::CheckStateRole:
767     {
768         switch (column)
769         {
770             case NameColumn: {
771                 return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
772             }
773             default:
774                 return QVariant();
775         }
776     }
777     case Qt::DisplayRole:
778     {
779         switch (column)
780         {
781         case NameColumn:
782             return patch->getName();
783         case VersionColumn:
784         {
785             if(patch->isCustom())
786             {
787                 return QString("%1 (Custom)").arg(patch->getVersion());
788             }
789             else
790             {
791                 return patch->getVersion();
792             }
793         }
794         default:
795             return QVariant();
796         }
797     }
798     case Qt::DecorationRole:
799     {
800         switch(column)
801         {
802         case NameColumn:
803         {
804             auto severity = patch->getProblemSeverity();
805             switch (severity)
806             {
807                 case ProblemSeverity::Warning:
808                     return "warning";
809                 case ProblemSeverity::Error:
810                     return "error";
811                 default:
812                     return QVariant();
813             }
814         }
815         default:
816         {
817             return QVariant();
818         }
819         }
820     }
821     }
822     return QVariant();
823 }
824 
setData(const QModelIndex & index,const QVariant & value,int role)825 bool ComponentList::setData(const QModelIndex& index, const QVariant& value, int role)
826 {
827     if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index))
828     {
829         return false;
830     }
831 
832     if (role == Qt::CheckStateRole)
833     {
834         auto component = d->components[index.row()];
835         if (component->setEnabled(!component->isEnabled()))
836         {
837             return true;
838         }
839     }
840     return false;
841 }
842 
headerData(int section,Qt::Orientation orientation,int role) const843 QVariant ComponentList::headerData(int section, Qt::Orientation orientation, int role) const
844 {
845     if (orientation == Qt::Horizontal)
846     {
847         if (role == Qt::DisplayRole)
848         {
849             switch (section)
850             {
851             case NameColumn:
852                 return tr("Name");
853             case VersionColumn:
854                 return tr("Version");
855             default:
856                 return QVariant();
857             }
858         }
859     }
860     return QVariant();
861 }
862 
863 // FIXME: zero precision mess
flags(const QModelIndex & index) const864 Qt::ItemFlags ComponentList::flags(const QModelIndex &index) const
865 {
866     if (!index.isValid()) {
867         return Qt::NoItemFlags;
868     }
869 
870     Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
871 
872     int row = index.row();
873 
874     if (row < 0 || row >= d->components.size()) {
875         return Qt::NoItemFlags;
876     }
877 
878     auto patch = d->components.at(row);
879     // TODO: this will need fine-tuning later...
880     if(patch->canBeDisabled() && !d->interactionDisabled)
881     {
882         outFlags |= Qt::ItemIsUserCheckable;
883     }
884     return outFlags;
885 }
886 
rowCount(const QModelIndex & parent) const887 int ComponentList::rowCount(const QModelIndex &parent) const
888 {
889     return d->components.size();
890 }
891 
columnCount(const QModelIndex & parent) const892 int ComponentList::columnCount(const QModelIndex &parent) const
893 {
894     return NUM_COLUMNS;
895 }
896 
move(const int index,const MoveDirection direction)897 void ComponentList::move(const int index, const MoveDirection direction)
898 {
899     int theirIndex;
900     if (direction == MoveUp)
901     {
902         theirIndex = index - 1;
903     }
904     else
905     {
906         theirIndex = index + 1;
907     }
908 
909     if (index < 0 || index >= d->components.size())
910         return;
911     if (theirIndex >= rowCount())
912         theirIndex = rowCount() - 1;
913     if (theirIndex == -1)
914         theirIndex = rowCount() - 1;
915     if (index == theirIndex)
916         return;
917     int togap = theirIndex > index ? theirIndex + 1 : theirIndex;
918 
919     auto from = getComponent(index);
920     auto to = getComponent(theirIndex);
921 
922     if (!from || !to || !to->isMoveable() || !from->isMoveable())
923     {
924         return;
925     }
926     beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap);
927     d->components.swap(index, theirIndex);
928     endMoveRows();
929     invalidateLaunchProfile();
930     scheduleSave();
931 }
932 
invalidateLaunchProfile()933 void ComponentList::invalidateLaunchProfile()
934 {
935     d->m_profile.reset();
936 }
937 
installJarMods(QStringList selectedFiles)938 void ComponentList::installJarMods(QStringList selectedFiles)
939 {
940     installJarMods_internal(selectedFiles);
941 }
942 
installCustomJar(QString selectedFile)943 void ComponentList::installCustomJar(QString selectedFile)
944 {
945     installCustomJar_internal(selectedFile);
946 }
947 
installEmpty(const QString & uid,const QString & name)948 bool ComponentList::installEmpty(const QString& uid, const QString& name)
949 {
950     QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
951     if(!FS::ensureFolderPathExists(patchDir))
952     {
953         return false;
954     }
955     auto f = std::make_shared<VersionFile>();
956     f->name = name;
957     f->uid = uid;
958     f->version = "1";
959     QString patchFileName = FS::PathCombine(patchDir, uid + ".json");
960     QFile file(patchFileName);
961     if (!file.open(QFile::WriteOnly))
962     {
963         qCritical() << "Error opening" << file.fileName()
964                     << "for reading:" << file.errorString();
965         return false;
966     }
967     file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
968     file.close();
969 
970     appendComponent(new Component(this, f->uid, f));
971     scheduleSave();
972     invalidateLaunchProfile();
973     return true;
974 }
975 
removeComponent_internal(ComponentPtr patch)976 bool ComponentList::removeComponent_internal(ComponentPtr patch)
977 {
978     bool ok = true;
979     // first, remove the patch file. this ensures it's not used anymore
980     auto fileName = patch->getFilename();
981     if(fileName.size())
982     {
983         QFile patchFile(fileName);
984         if(patchFile.exists() && !patchFile.remove())
985         {
986             qCritical() << "File" << fileName << "could not be removed because:" << patchFile.errorString();
987             return false;
988         }
989     }
990 
991     // FIXME: we need a generic way of removing local resources, not just jar mods...
992     auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool
993     {
994         if (!jarMod->isLocal())
995         {
996             return true;
997         }
998         QStringList jar, temp1, temp2, temp3;
999         jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath());
1000         QFileInfo finfo (jar[0]);
1001         if(finfo.exists())
1002         {
1003             QFile jarModFile(jar[0]);
1004             if(!jarModFile.remove())
1005             {
1006                 qCritical() << "File" << jar[0] << "could not be removed because:" << jarModFile.errorString();
1007                 return false;
1008             }
1009             return true;
1010         }
1011         return true;
1012     };
1013 
1014     auto vFile = patch->getVersionFile();
1015     if(vFile)
1016     {
1017         auto &jarMods = vFile->jarMods;
1018         for(auto &jarmod: jarMods)
1019         {
1020             ok &= preRemoveJarMod(jarmod);
1021         }
1022     }
1023     return ok;
1024 }
1025 
installJarMods_internal(QStringList filepaths)1026 bool ComponentList::installJarMods_internal(QStringList filepaths)
1027 {
1028     QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
1029     if(!FS::ensureFolderPathExists(patchDir))
1030     {
1031         return false;
1032     }
1033 
1034     if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir()))
1035     {
1036         return false;
1037     }
1038 
1039     for(auto filepath:filepaths)
1040     {
1041         QFileInfo sourceInfo(filepath);
1042         auto uuid = QUuid::createUuid();
1043         QString id = uuid.toString().remove('{').remove('}');
1044         QString target_filename = id + ".jar";
1045         QString target_id = "org.multimc.jarmod." + id;
1046         QString target_name = sourceInfo.completeBaseName() + " (jar mod)";
1047         QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename);
1048 
1049         QFileInfo targetInfo(finalPath);
1050         if(targetInfo.exists())
1051         {
1052             return false;
1053         }
1054 
1055         if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath()))
1056         {
1057             return false;
1058         }
1059 
1060         auto f = std::make_shared<VersionFile>();
1061         auto jarMod = std::make_shared<Library>();
1062         jarMod->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1"));
1063         jarMod->setFilename(target_filename);
1064         jarMod->setDisplayName(sourceInfo.completeBaseName());
1065         jarMod->setHint("local");
1066         f->jarMods.append(jarMod);
1067         f->name = target_name;
1068         f->uid = target_id;
1069         QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
1070 
1071         QFile file(patchFileName);
1072         if (!file.open(QFile::WriteOnly))
1073         {
1074             qCritical() << "Error opening" << file.fileName()
1075                         << "for reading:" << file.errorString();
1076             return false;
1077         }
1078         file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
1079         file.close();
1080 
1081         appendComponent(new Component(this, f->uid, f));
1082     }
1083     scheduleSave();
1084     invalidateLaunchProfile();
1085     return true;
1086 }
1087 
installCustomJar_internal(QString filepath)1088 bool ComponentList::installCustomJar_internal(QString filepath)
1089 {
1090     QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches");
1091     if(!FS::ensureFolderPathExists(patchDir))
1092     {
1093         return false;
1094     }
1095 
1096     QString libDir = d->m_instance->getLocalLibraryPath();
1097     if (!FS::ensureFolderPathExists(libDir))
1098     {
1099         return false;
1100     }
1101 
1102     auto specifier = GradleSpecifier("org.multimc:customjar:1");
1103     QFileInfo sourceInfo(filepath);
1104     QString target_filename = specifier.getFileName();
1105     QString target_id = specifier.artifactId();
1106     QString target_name = sourceInfo.completeBaseName() + " (custom jar)";
1107     QString finalPath = FS::PathCombine(libDir, target_filename);
1108 
1109     QFileInfo jarInfo(finalPath);
1110     if (jarInfo.exists())
1111     {
1112         if(!QFile::remove(finalPath))
1113         {
1114             return false;
1115         }
1116     }
1117     if (!QFile::copy(filepath, finalPath))
1118     {
1119         return false;
1120     }
1121 
1122     auto f = std::make_shared<VersionFile>();
1123     auto jarMod = std::make_shared<Library>();
1124     jarMod->setRawName(specifier);
1125     jarMod->setDisplayName(sourceInfo.completeBaseName());
1126     jarMod->setHint("local");
1127     f->mainJar = jarMod;
1128     f->name = target_name;
1129     f->uid = target_id;
1130     QString patchFileName = FS::PathCombine(patchDir, target_id + ".json");
1131 
1132     QFile file(patchFileName);
1133     if (!file.open(QFile::WriteOnly))
1134     {
1135         qCritical() << "Error opening" << file.fileName()
1136                     << "for reading:" << file.errorString();
1137         return false;
1138     }
1139     file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
1140     file.close();
1141 
1142     appendComponent(new Component(this, f->uid, f));
1143 
1144     scheduleSave();
1145     invalidateLaunchProfile();
1146     return true;
1147 }
1148 
getProfile() const1149 std::shared_ptr<LaunchProfile> ComponentList::getProfile() const
1150 {
1151     if(!d->m_profile)
1152     {
1153         try
1154         {
1155             auto profile = std::make_shared<LaunchProfile>();
1156             for(auto file: d->components)
1157             {
1158                 qDebug() << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD");
1159                 file->applyTo(profile.get());
1160             }
1161             d->m_profile = profile;
1162         }
1163         catch (const Exception &error)
1164         {
1165             qWarning() << "Couldn't apply profile patches because: " << error.cause();
1166         }
1167     }
1168     return d->m_profile;
1169 }
1170 
setOldConfigVersion(const QString & uid,const QString & version)1171 void ComponentList::setOldConfigVersion(const QString& uid, const QString& version)
1172 {
1173     if(version.isEmpty())
1174     {
1175         return;
1176     }
1177     d->m_oldConfigVersions[uid] = version;
1178 }
1179 
setComponentVersion(const QString & uid,const QString & version,bool important)1180 bool ComponentList::setComponentVersion(const QString& uid, const QString& version, bool important)
1181 {
1182     auto iter = d->componentIndex.find(uid);
1183     if(iter != d->componentIndex.end())
1184     {
1185         ComponentPtr component = *iter;
1186         // set existing
1187         if(component->revert())
1188         {
1189             component->setVersion(version);
1190             component->setImportant(important);
1191             return true;
1192         }
1193         return false;
1194     }
1195     else
1196     {
1197         // add new
1198         auto component = new Component(this, uid);
1199         component->m_version = version;
1200         component->m_important = important;
1201         appendComponent(component);
1202         return true;
1203     }
1204 }
1205 
getComponentVersion(const QString & uid) const1206 QString ComponentList::getComponentVersion(const QString& uid) const
1207 {
1208     const auto iter = d->componentIndex.find(uid);
1209     if (iter != d->componentIndex.end())
1210     {
1211         return (*iter)->getVersion();
1212     }
1213     return QString();
1214 }
1215 
disableInteraction(bool disable)1216 void ComponentList::disableInteraction(bool disable)
1217 {
1218     if(d->interactionDisabled != disable) {
1219         d->interactionDisabled = disable;
1220         auto size = d->components.size();
1221         if(size) {
1222             emit dataChanged(index(0), index(size - 1));
1223         }
1224     }
1225 }
1226