diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7d4430fd2..81f9331f7 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1095,6 +1095,10 @@ SET(LAUNCHER_SOURCES ui/dialogs/ResourceUpdateDialog.h ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h + + minecraft/BabricCreationTask.h + minecraft/BabricCreationTask.cpp + ui/dialogs/ChooseOfflineNameDialog.cpp ui/dialogs/ChooseOfflineNameDialog.h diff --git a/launcher/minecraft/BabricCreationTask.cpp b/launcher/minecraft/BabricCreationTask.cpp new file mode 100644 index 000000000..91c119ebf --- /dev/null +++ b/launcher/minecraft/BabricCreationTask.cpp @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + */ + +#include "BabricCreationTask.h" + +#include +#include +#include +#include + +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "settings/INISettingsObject.h" + +// --------------------------------------------------------------------------- +// Babric component stack (mirrors babric/prism-instance on GitHub): +// +// org.lwjgl 2.9.4+legacyfabric.17 [local patch] +// net.minecraft b1.7.3 [local patch] +// babric b1.7.3 [local patch] +// net.fabricmc.intermediary b1.7.3 [local patch] +// net.fabricmc.fabric-loader 0.18.4 [Prism meta server] +// +// The four local patches are embedded directly as C++ raw string literals +// so they are always compiled into the binary without any QRC dependency. +// To update, replace the JSON text below and rebuild. +// --------------------------------------------------------------------------- + +static constexpr const char* FABRIC_LOADER_VERSION = "0.18.4"; + +static const char* PATCH_ORG_LWJGL = R"({ + "formatVersion": 1, + "libraries": [ + { + "downloads": { + "classifiers": { + "natives-linux": { + "sha1": "7ff832a6eb9ab6a767f1ade2b548092d0fa64795", + "size": 10362, + "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar" + }, + "natives-linux-arm32": { + "sha1": "f3c455b71c5146acb5f8a9513247fc06db182fd5", + "size": 4521, + "url": "https://github.com/theofficialgman/lwjgl3-binaries-arm32/raw/lwjgl-2.9.4/jinput-platform-2.0.5-natives-linux.jar" + }, + "natives-linux-arm64": { + "sha1": "42b388ccb7c63cec4e9f24f4dddef33325f8b212", + "size": 10932, + "url": "https://github.com/theofficialgman/lwjgl3-binaries-arm64/raw/lwjgl-2.9.4/jinput-platform-2.0.5-natives-linux.jar" + }, + "natives-osx": { + "sha1": "53f9c919f34d2ca9de8c51fc4e1e8282029a9232", + "size": 12186, + "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar" + }, + "natives-osx-arm64": { + "sha1": "5189eb40db3087fb11ca063b68fa4f4c20b199dd", + "size": 10031, + "url": "https://github.com/r58Playz/jinput-m1/raw/main/plugins/OSX/bin/jinput-platform-2.0.5.jar" + }, + "natives-windows": { + "sha1": "385ee093e01f587f30ee1c8a2ee7d408fd732e16", + "size": 155179, + "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar" + } + } + }, + "extract": { "exclude": ["META-INF/"] }, + "name": "net.java.jinput:jinput-platform:2.0.5", + "natives": { + "linux": "natives-linux", + "linux-arm32": "natives-linux-arm32", + "linux-arm64": "natives-linux-arm64", + "osx": "natives-osx", + "osx-arm64": "natives-osx-arm64", + "windows": "natives-windows" + } + }, + { + "downloads": { + "artifact": { + "sha1": "39c7796b469a600f72380316f6b1f11db6c2c7c4", + "size": 208338, + "url": "https://libraries.minecraft.net/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar" + } + }, + "name": "net.java.jinput:jinput:2.0.5" + }, + { + "downloads": { + "artifact": { + "sha1": "e12fe1fda814bd348c1579329c86943d2cd3c6a6", + "size": 7508, + "url": "https://libraries.minecraft.net/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar" + } + }, + "name": "net.java.jutils:jutils:1.0.0" + }, + { + "extract": { "exclude": ["META-INF/"] }, + "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4+legacyfabric.17", + "url": "https://maven.legacyfabric.net/", + "natives": { + "linux": "natives-linux", + "linux-arm32": "natives-linux", + "linux-arm64": "natives-linux", + "osx": "natives-osx", + "osx-arm64": "natives-osx", + "windows": "natives-windows" + } + }, + { + "name": "org.lwjgl.lwjgl:lwjgl:2.9.4+legacyfabric.17", + "url": "https://maven.legacyfabric.net/" + }, + { + "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4+legacyfabric.17", + "url": "https://maven.legacyfabric.net/" + } + ], + "name": "LWJGL 2", + "type": "release", + "uid": "org.lwjgl", + "version": "2.9.4+legacyfabric.17", + "volatile": true +})"; + +static const char* PATCH_NET_MINECRAFT = R"({ + "+traits": ["legacyLaunch", "legacyServices", "texturepacks"], + "assetIndex": { + "id": "pre-1.6", + "sha1": "3d8e55480977e32acd9844e545177e69a52f594b", + "size": 74091, + "totalSize": 49505710, + "url": "https://piston-meta.mojang.com/v1/packages/3d8e55480977e32acd9844e545177e69a52f594b/pre-1.6.json" + }, + "compatibleJavaMajors": [8, 17, 21, 25], + "compatibleJavaName": "java-runtime-delta", + "formatVersion": 1, + "mainJar": { + "downloads": { + "artifact": { + "sha1": "43db9b498cb67058d2e12d394e6507722e71bb45", + "size": 1465375, + "url": "https://launcher.mojang.com/v1/objects/43db9b498cb67058d2e12d394e6507722e71bb45/client.jar" + } + }, + "name": "com.mojang:minecraft:b1.7.3:client" + }, + "minecraftArguments": "${auth_player_name} ${auth_session} --gameDir ${game_directory} --assetsDir ${game_assets}", + "name": "Minecraft", + "releaseTime": "2011-07-08T00:00:00+02:00", + "requires": [{"suggests": "2.9.4+legacyfabric.17", "uid": "org.lwjgl"}], + "type": "old_beta", + "uid": "net.minecraft", + "version": "b1.7.3" +})"; + +static const char* PATCH_BABRIC = R"({ + "formatVersion": 1, + "name": "Babric", + "uid": "babric", + "version": "b1.7.3", + "minecraftArguments": "--username ${auth_player_name} --session ${auth_session} --gameDir ${game_directory} --assetsDir ${game_assets}", + "+traits": ["noapplet"], + "libraries": [ + {"name": "babric:log4j-config:1.0.0", "url": "https://maven.glass-launcher.net/babric/"}, + {"name": "net.minecrell:terminalconsoleappender:1.2.0", "url": "https://repo1.maven.org/maven2/"}, + {"name": "org.slf4j:slf4j-api:1.8.0-beta4"}, + {"name": "org.apache.logging.log4j:log4j-slf4j18-impl:2.16.0"}, + {"name": "org.apache.logging.log4j:log4j-api:2.16.0"}, + {"name": "org.apache.logging.log4j:log4j-core:2.16.0"}, + {"name": "com.google.code.gson:gson:2.8.9"}, + {"name": "com.google.guava:guava:31.0.1-jre"}, + {"name": "com.google.guava:failureaccess:1.0.1"}, + {"name": "org.apache.commons:commons-lang3:3.12.0"}, + {"name": "commons-io:commons-io:2.11.0"}, + {"name": "commons-codec:commons-codec:1.15"} + ] +})"; + +static const char* PATCH_INTERMEDIARY = R"({ + "formatVersion": 1, + "libraries": [ + {"name": "babric:intermediary-upstream:b1.7.3", "url": "https://maven.glass-launcher.net/babric"} + ], + "name": "Intermediary Mappings", + "releaseTime": "2024-10-18T18:57:03.000Z", + "requires": [{"equals": "b1.7.3", "uid": "net.minecraft"}], + "type": "release", + "uid": "net.fabricmc.intermediary", + "version": "b1.7.3", + "volatile": true +})"; + +// Ordered: org.lwjgl before net.minecraft so LWJGL 2 is on the classpath first. +static const QList> LOCAL_PATCHES = { + { "org.lwjgl", PATCH_ORG_LWJGL }, + { "net.minecraft", PATCH_NET_MINECRAFT }, + { "babric", PATCH_BABRIC }, + { "net.fabricmc.intermediary", PATCH_INTERMEDIARY }, +}; + +// --------------------------------------------------------------------------- + +static bool writeFile(const QString& path, const QByteArray& data) +{ + QDir().mkpath(QFileInfo(path).absolutePath()); + QFile f(path); + if (!f.open(QFile::WriteOnly | QFile::Truncate)) { + qCritical() << "[Babric] Cannot write" << path << ":" << f.errorString(); + return false; + } + const qint64 written = f.write(data); + f.close(); + if (written != data.size()) { + qCritical() << "[Babric] Short write to" << path; + return false; + } + return true; +} + + +bool BabricCreationTask::installPatches(PackProfile* profile) +{ + qDebug() << "[Babric] installPatches() start"; + + // Step 1: Write all four patch files from the embedded string literals. + QMap written; + for (const auto& [uid, json] : LOCAL_PATCHES) { + const QByteArray data(json); + const QString target = profile->patchFilePathForUid(uid); + qDebug() << "[Babric] Writing" << uid << "to" << target; + if (!writeFile(target, data)) { + qCritical() << "[Babric] Step 1 FAILED for" << uid; + return false; + } + written[uid] = data; + } + + // Step 2: Register component versions. + // + // setComponentVersion("net.minecraft", ..., important=true) calls + // Component::revert() internally, which can delete our net.minecraft.json. + // We re-write it immediately afterwards to be safe. + profile->setComponentVersion("org.lwjgl", "2.9.4+legacyfabric.17"); + + profile->setComponentVersion("net.minecraft", "b1.7.3", /*important=*/true); + writeFile(profile->patchFilePathForUid("net.minecraft"), written["net.minecraft"]); + + profile->setComponentVersion("babric", "b1.7.3"); + profile->setComponentVersion("net.fabricmc.intermediary", "b1.7.3"); + + // Fabric Loader is fetched from the Prism meta server (no local patch). + profile->setComponentVersion("net.fabricmc.fabric-loader", + QLatin1String(FABRIC_LOADER_VERSION)); + + // Step 3: Re-write ALL patch files a second time to survive any + // Component::revert() calls that setComponentVersion() may have fired above. + // revert() deletes local patch files for UIDs that exist on the Prism meta + // server (net.minecraft and possibly org.lwjgl). + for (const auto& [uid, data] : written.asKeyValueRange()) { + if (!writeFile(profile->patchFilePathForUid(uid), data)) + qWarning() << "[Babric] Failed to re-write patch for" << uid; + } + + // Step 4: Save immediately so the profile is consistent on disk. + profile->saveNow(); + + qDebug() << "[Babric] installPatches() done"; + return true; +} + + +void BabricCreationTask::removePatches(PackProfile* profile) +{ + qDebug() << "[Babric] removePatches() start"; + + // Remove the four loader components. + // Re-scan by UID each iteration because each removal shifts row indices. + static const QList loaderUIDs = { + "net.fabricmc.fabric-loader", + "babric", + "net.fabricmc.intermediary", + "org.lwjgl", + }; + for (const QString& uid : loaderUIDs) { + for (int i = 0; i < profile->rowCount(); i++) { + auto comp = profile->getComponent(i); + if (comp && comp->getID() == uid) { + profile->remove(i); + break; + } + } + // Also delete the local patch file explicitly. + QFile::remove(profile->patchFilePathForUid(uid)); + } + + // Revert net.minecraft to meta-backed state so the "Custom" label disappears. + // Component::revert() deletes patches/net.minecraft.json and clears m_file. + auto netMinecraft = profile->getComponent("net.minecraft"); + if (netMinecraft) + netMinecraft->revert(); + QFile::remove(profile->patchFilePathForUid("net.minecraft")); + + profile->saveNow(); + qDebug() << "[Babric] removePatches() done"; +} + + +std::unique_ptr BabricCreationTask::createInstance() +{ + setStatus(tr("Creating Babric instance for Minecraft b1.7.3")); + + auto inst = std::make_unique( + m_globalSettings, + std::make_unique(FS::PathCombine(m_stagingPath, "instance.cfg")), + m_stagingPath); + + SettingsObject::Lock lock(inst->settings()); + + auto* profile = inst->getPackProfile(); + profile->buildingFromScratch(); + + if (!installPatches(profile)) { + setError(tr("Failed to write Babric patch files.")); + return nullptr; + } + + inst->setName(name()); + inst->setIconKey(m_instIcon); + return inst; +} diff --git a/launcher/minecraft/BabricCreationTask.h b/launcher/minecraft/BabricCreationTask.h new file mode 100644 index 000000000..5364f9557 --- /dev/null +++ b/launcher/minecraft/BabricCreationTask.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + */ + +#pragma once + +#include "InstanceCreationTask.h" +#include "minecraft/PackProfile.h" + +class BabricCreationTask final : public InstanceCreationTask { + Q_OBJECT + public: + BabricCreationTask() = default; + + /** + * Writes the four Babric patch files from embedded string literals into + * the instance's patches/ directory and registers all five component + * versions in the PackProfile. + * + * Safe to call on an already-configured Babric instance (re-install). + * Does NOT call resolve() — the caller is responsible for that. + * + * Returns false on any I/O error. + */ + static bool installPatches(PackProfile* profile); + + /** + * Removes all Babric-related components from the profile and deletes + * their local patch files. net.minecraft is intentionally kept so the + * user can later install a different loader or change the MC version. + */ + static void removePatches(PackProfile* profile); + + std::unique_ptr createInstance() override; +}; diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index 6a8bb27c0..cbc597ad0 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -54,7 +54,8 @@ const QMap Component::KNOWN_MODLOADERS = { { "net.minecraftforge", { ModPlatform::Forge, { "net.neoforged", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, { "net.fabricmc.fabric-loader", { ModPlatform::Fabric, { "net.minecraftforge", "net.neoforged", "org.quiltmc.quilt-loader" } } }, { "org.quiltmc.quilt-loader", { ModPlatform::Quilt, { "net.minecraftforge", "net.neoforged", "net.fabricmc.fabric-loader" } } }, - { "com.mumfrey.liteloader", { ModPlatform::LiteLoader, {} } } + { "babric", { ModPlatform::Babric, { "net.minecraftforge", "net.neoforged", "org.quiltmc.quilt-loader" } } }, + { "com.mumfrey.liteloader", { ModPlatform::LiteLoader, {} } } }; Component::Component(PackProfile* parent, const QString& uid) diff --git a/launcher/ui/dialogs/InstallLoaderDialog.cpp b/launcher/ui/dialogs/InstallLoaderDialog.cpp index deb5358fb..42ccafe43 100644 --- a/launcher/ui/dialogs/InstallLoaderDialog.cpp +++ b/launcher/ui/dialogs/InstallLoaderDialog.cpp @@ -19,13 +19,15 @@ #include "InstallLoaderDialog.h" #include +#include #include +#include #include #include "Application.h" #include "BuildConfig.h" #include "DesktopServices.h" #include "meta/Index.h" -#include "minecraft/MinecraftInstance.h" +#include "minecraft/BabricCreationTask.h" #include "minecraft/PackProfile.h" #include "ui/widgets/PageContainer.h" #include "ui/widgets/VersionSelectWidget.h" @@ -33,7 +35,11 @@ class InstallLoaderPage : public VersionSelectWidget, public BasePage { Q_OBJECT public: - InstallLoaderPage(const QString& id, const QString& iconName, const QString& name, const Version& oldestVersion, PackProfile* profile) + InstallLoaderPage(const QString& id, + const QString& iconName, + const QString& name, + const Version& oldestVersion, + PackProfile* profile) : VersionSelectWidget(nullptr), uid(id), iconName(iconName), name(name) { const QString minecraftVersion = profile->getComponentVersion("net.minecraft"); @@ -77,17 +83,89 @@ class InstallLoaderPage : public VersionSelectWidget, public BasePage { bool loaded = false; }; -static InstallLoaderPage* pageCast(BasePage* page) +class InstallBabricPage : public QWidget, public BasePage { + Q_OBJECT + public: + explicit InstallBabricPage(PackProfile* profile, QWidget* parent = nullptr) + : QWidget(parent), m_profile(profile) + { + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(16, 16, 16, 16); + layout->setSpacing(12); + + m_label = new QLabel(this); + m_label->setWordWrap(true); + m_label->setOpenExternalLinks(true); + m_label->setAlignment(Qt::AlignTop | Qt::AlignLeft); + layout->addWidget(m_label); + layout->addStretch(); + + updateLabel(); + } + + QString id() const override { return "babric"; } + QString displayName() const override { return tr("Babric"); } + QIcon icon() const override { return QIcon::fromTheme("fabricmc"); } + + bool isCompatible() const + { + const QString mc = m_profile->getComponentVersion("net.minecraft"); + return mc.isEmpty() || mc == "b1.7.3"; + } + + bool install() + { + if (!BabricCreationTask::installPatches(m_profile)) + return false; + m_profile->resolve(Net::Mode::Online); + return true; + } + + private: + void updateLabel() + { + if (isCompatible()) { + m_label->setText( + tr("Babric is a Fabric-based mod loader " + "for Minecraft Beta 1.7.3." + "

" + "Clicking OK will:" + "
    " + "
  • Replace this instance's Minecraft version with b1.7.3
  • " + "
  • Install Fabric Loader 0.18.4 and the Babric mapping layer
  • " + "
  • Configure the mod browser to show Babric-compatible mods
  • " + "
")); + } else { + const QString mc = m_profile->getComponentVersion("net.minecraft"); + m_label->setText( + tr("No versions are currently available for Minecraft %1." + "

" + "Babric only supports Minecraft Beta 1.7.3.").arg(mc)); + } + } + + PackProfile* m_profile = nullptr; + QLabel* m_label = nullptr; +}; + + +static InstallLoaderPage* asLoaderPage(BasePage* page) { - auto result = dynamic_cast(page); - Q_ASSERT(result != nullptr); - return result; + return dynamic_cast(page); +} + +static InstallBabricPage* asBabricPage(BasePage* page) +{ + return dynamic_cast(page); } InstallLoaderDialog::InstallLoaderDialog(PackProfile* profile, const QString& uid, QWidget* parent) - : QDialog(parent), profile(profile), container(new PageContainer(this, QString(), this)), buttons(new QDialogButtonBox(this)) + : QDialog(parent) + , profile(profile) + , container(new PageContainer(this, QString(), this)) + , buttons(new QDialogButtonBox(this)) { - auto layout = new QVBoxLayout(this); + auto* layout = new QVBoxLayout(this); // small margins look ugly on macOS on modal windows #ifndef Q_OS_MACOS layout->setContentsMargins(0, 0, 0, 0); @@ -95,13 +173,19 @@ InstallLoaderDialog::InstallLoaderDialog(PackProfile* profile, const QString& ui container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); layout->addWidget(container); - auto buttonLayout = new QHBoxLayout(this); + auto* buttonLayout = new QHBoxLayout(); // small margins look ugly on macOS on modal windows #ifndef Q_OS_MACOS buttonLayout->setContentsMargins(0, 0, 6, 6); #endif - auto refreshButton = new QPushButton(tr("&Refresh"), this); - connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(true); }); + + auto* refreshButton = new QPushButton(tr("&Refresh"), this); + connect(refreshButton, &QPushButton::clicked, this, [this] { + auto* page = container->selectedPage(); + if (auto* lp = asLoaderPage(page)) + lp->loadList(true); + // Babric has no remote list to refresh. + }); buttonLayout->addWidget(refreshButton); buttons->setOrientation(Qt::Horizontal); @@ -118,17 +202,29 @@ InstallLoaderDialog::InstallLoaderDialog(PackProfile* profile, const QString& ui setWindowModality(Qt::WindowModal); resize(520, 347); + // Pre-select requested uid (if any) and wire version-selection → OK state. for (BasePage* page : container->getPages()) { - if (page->id() == uid) + if (!uid.isEmpty() && page->id() == uid) container->selectPage(page->id()); - connect(pageCast(page), &VersionSelectWidget::selectedVersionChanged, this, [this, page] { - if (page->id() == container->selectedPage()->id()) - validate(container->selectedPage()); - }); + if (auto* lp = dynamic_cast(page)) { + connect(lp, &VersionSelectWidget::selectedVersionChanged, this, [this, page] { + if (page == container->selectedPage()) + validate(container->selectedPage()); + }); + } } - connect(container, &PageContainer::selectedPageChanged, this, [this](BasePage* previous, BasePage* current) { validate(current); }); - pageCast(container->selectedPage())->selectSearch(); + + connect(container, &PageContainer::selectedPageChanged, this, + [this, refreshButton](BasePage* /*prev*/, BasePage* current) { + refreshButton->setVisible(!asBabricPage(current)); + validate(current); + }); + + if (auto* lp = asLoaderPage(container->selectedPage())) + lp->selectSearch(); + + refreshButton->setVisible(!asBabricPage(container->selectedPage())); validate(container->selectedPage()); } @@ -142,6 +238,8 @@ QList InstallLoaderDialog::getPages() new InstallLoaderPage("net.fabricmc.fabric-loader", "fabricmc", tr("Fabric"), Version("1.14"), profile), // Quilt new InstallLoaderPage("org.quiltmc.quilt-loader", "quiltmc", tr("Quilt"), Version("1.14"), profile), + // Babric + new InstallBabricPage(profile), // LiteLoader new InstallLoaderPage("com.mumfrey.liteloader", "liteloader", tr("LiteLoader"), {}, profile) }; @@ -154,16 +252,34 @@ QString InstallLoaderDialog::dialogTitle() void InstallLoaderDialog::validate(BasePage* page) { - buttons->button(QDialogButtonBox::Ok)->setEnabled(pageCast(page)->selectedVersion() != nullptr); + bool ok = false; + if (auto* bp = asBabricPage(page)) { + ok = bp->isCompatible(); + } else if (auto* lp = asLoaderPage(page)) { + ok = lp->selectedVersion() != nullptr; + } + buttons->button(QDialogButtonBox::Ok)->setEnabled(ok); } void InstallLoaderDialog::done(int result) { if (result == Accepted) { - auto* page = pageCast(container->selectedPage()); - if (page->selectedVersion()) { - profile->setComponentVersion(page->id(), page->selectedVersion()->descriptor()); - profile->resolve(Net::Mode::Online); + BasePage* page = container->selectedPage(); + + if (auto* bp = asBabricPage(page)) { + if (!bp->install()) { + QMessageBox::critical( + this, tr("Install Error"), + tr("Failed to install Babric.\n\n" + "Run the launcher from a terminal to see details:\n" + " cd install && .\\prismlauncher.exe 2>&1")); + return; // keep dialog open so user can try again or cancel + } + } else if (auto* lp = asLoaderPage(page)) { + if (lp->selectedVersion()) { + profile->setComponentVersion(lp->id(), lp->selectedVersion()->descriptor()); + profile->resolve(Net::Mode::Online); + } } } diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index fea759bb2..d9a30dc17 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -55,6 +55,7 @@ #include "VersionPage.h" #include "meta/JsonFormat.h" #include "tasks/SequentialTask.h" +#include "minecraft/BabricCreationTask.h" #include "ui/dialogs/InstallLoaderDialog.h" #include "ui_VersionPage.h" @@ -288,6 +289,31 @@ void VersionPage::on_actionRemove_triggered() if (response != QMessageBox::Yes) return; } + // Removing any component from the Babric stack removes all of them to prevent a partially-installed, broken state. + static const QSet babricStack = { + "babric", "net.fabricmc.intermediary", + "org.lwjgl", "net.fabricmc.fabric-loader", + }; + const bool isBabricInstance = m_profile->getComponent("babric") != nullptr; + if (isBabricInstance && babricStack.contains(component->getID())) { + auto response = + CustomMessageBox::selectable( + this, tr("Remove Babric"), + tr("Removing \"%1\" will remove all Babric components from this instance:\n" + "Babric, Intermediary Mappings, LWJGL 2, Fabric Loader.\n\n" + "Minecraft b1.7.3 will be kept. Are you sure?") + .arg(component->getName()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + if (response != QMessageBox::Yes) + return; + BabricCreationTask::removePatches(m_profile); + updateButtons(); + reloadPackProfile(); + m_container->refreshContainer(); + return; + } + // FIXME: use actual model, not reloading. if (!m_profile->remove(index)) { QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); diff --git a/launcher/ui/pages/modplatform/CustomPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp index f24abf9fb..ddaa69bdc 100644 --- a/launcher/ui/pages/modplatform/CustomPage.cpp +++ b/launcher/ui/pages/modplatform/CustomPage.cpp @@ -43,28 +43,45 @@ #include "Version.h" #include "meta/Index.h" #include "meta/VersionList.h" +#include "minecraft/BabricCreationTask.h" #include "minecraft/VanillaInstanceCreationTask.h" #include "ui/dialogs/NewInstanceDialog.h" +#include CustomPage::CustomPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog(dialog), ui(new Ui::CustomPage) { ui->setupUi(this); + + // babricInfoLabel and loaderStack come from CustomPage.ui — + // no runtime widget creation or layout manipulation needed here. + ui->babricInfoLabel->setText( + tr("Babric is a Fabric-based mod loader " + "for Minecraft Beta 1.7.3.

" + "Clicking OK will create an instance with:
    " + "
  • Minecraft b1.7.3
  • " + "
  • Fabric Loader 0.18.4 + Babric mapping layer
  • " + "
")); + + ui->loaderPage1->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedVersion); filterChanged(); - connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); - connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); - connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); - connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->betaFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); + connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &CustomPage::filterChanged); - connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh); + connect(ui->refreshBtn, &QPushButton::clicked, this, &CustomPage::refresh); connect(ui->loaderVersionList, &VersionSelectWidget::selectedVersionChanged, this, &CustomPage::setSelectedLoaderVersion); - connect(ui->noneFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); - connect(ui->forgeFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); - connect(ui->fabricFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); - connect(ui->quiltFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); - connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); - connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &CustomPage::loaderRefresh); + connect(ui->noneFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->neoForgeFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->forgeFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->fabricFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->quiltFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->babricFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->liteLoaderFilter, &QRadioButton::toggled, this, &CustomPage::loaderFilterChanged); + connect(ui->loaderRefreshBtn, &QPushButton::clicked, this, &CustomPage::loaderRefresh); } void CustomPage::openedImpl() @@ -85,7 +102,7 @@ void CustomPage::refresh() void CustomPage::loaderRefresh() { - if (ui->noneFilter->isChecked()) + if (ui->noneFilter->isChecked() || ui->babricFilter->isChecked()) return; ui->loaderVersionList->loadList(true); } @@ -107,23 +124,52 @@ void CustomPage::filterChanged() ui->versionList->setFilter(BaseVersionList::TypeRole, Filters::regexp(QRegularExpression(regexp))); } +void CustomPage::setLoaderWidgetMode(bool isBabric) +{ + ui->loaderStack->setCurrentIndex(isBabric ? 1 : 0); + ui->loaderRefreshBtn->setEnabled(!isBabric); + + for (int i = 0; i < ui->verticalLayout_2->count(); i++) { + if (QSpacerItem* spacer = ui->verticalLayout_2->itemAt(i)->spacerItem()) { + spacer->changeSize(20, 0, QSizePolicy::Minimum, + isBabric ? QSizePolicy::Fixed : QSizePolicy::Expanding); + ui->verticalLayout_2->invalidate(); + break; + } + } +} + void CustomPage::loaderFilterChanged() { - QString minecraftVersion; - if (m_selectedVersion) { - minecraftVersion = m_selectedVersion->descriptor(); - } else { - ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list + // Always sync the widget visibility first. + setLoaderWidgetMode(ui->babricFilter->isChecked()); + + const QString minecraftVersion = m_selectedVersion ? m_selectedVersion->descriptor() : QString(); + + if (ui->noneFilter->isChecked()) { + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); + ui->loaderVersionList->setEmptyString(tr("No mod loader is selected.")); + ui->loaderVersionList->setEmptyMode(VersionListView::String); + m_selectedLoader.clear(); + suggestCurrent(); + return; + } + + if (ui->babricFilter->isChecked()) { + // No meta-server lookup needed — everything is embedded. + m_selectedLoader = "babric"; + suggestCurrent(); + return; + } + + if (minecraftVersion.isEmpty()) { + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); ui->loaderVersionList->setEmptyString(tr("No Minecraft version is selected.")); ui->loaderVersionList->setEmptyMode(VersionListView::String); return; } - if (ui->noneFilter->isChecked()) { - ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list - ui->loaderVersionList->setEmptyString(tr("No mod loader is selected.")); - ui->loaderVersionList->setEmptyMode(VersionListView::String); - return; - } else if (ui->neoForgeFilter->isChecked()) { + + if (ui->neoForgeFilter->isChecked()) { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); m_selectedLoader = "net.neoforged"; } else if (ui->forgeFilter->isChecked()) { @@ -186,7 +232,12 @@ QString CustomPage::selectedLoader() const void CustomPage::suggestCurrent() { - if (!isOpened) { + if (!isOpened) + return; + + if (ui->babricFilter->isChecked()) { + dialog->setSuggestedPack("b1.7.3", new BabricCreationTask()); + dialog->setSuggestedIcon("fabric"); return; } diff --git a/launcher/ui/pages/modplatform/CustomPage.h b/launcher/ui/pages/modplatform/CustomPage.h index 2bfb1de29..618afa42e 100644 --- a/launcher/ui/pages/modplatform/CustomPage.h +++ b/launcher/ui/pages/modplatform/CustomPage.h @@ -35,6 +35,7 @@ #pragma once +#include #include #include "BaseVersion.h" @@ -79,6 +80,8 @@ class CustomPage : public QWidget, public BasePage { void loaderRefresh(); void suggestCurrent(); + void setLoaderWidgetMode(bool isBabric); + private: bool initialized = false; NewInstanceDialog* dialog = nullptr; diff --git a/launcher/ui/pages/modplatform/CustomPage.ui b/launcher/ui/pages/modplatform/CustomPage.ui index 39d9aa6dc..2a05156bf 100644 --- a/launcher/ui/pages/modplatform/CustomPage.ui +++ b/launcher/ui/pages/modplatform/CustomPage.ui @@ -155,13 +155,80 @@ - + 0 0 + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + + + @@ -230,6 +297,16 @@ + + + Babric + + + loaderBtnGroup + + + + LiteLoader