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