From 2df86054ee4192600831549382f25274dba13649 Mon Sep 17 00:00:00 2001 From: FreesmModders Date: Sat, 16 May 2026 08:23:57 -0300 Subject: [PATCH 1/2] feat: Implement Modpack Creator Mode with automatic changelog generation Adds a new 'Modpack Creator' page to the instance settings dialog that allows users to mark instances as their own modpacks and generate changelogs automatically. New files: - ModpackChangelogGenerator.h/cpp: Core logic for creating version snapshots of the mod list and comparing them to detect added, removed, and updated mods based on mod IDs (not just names). - ModpackCreatorPage.h/cpp/ui: Qt UI page with modpack toggle, metadata fields (name/author), changelog generation button, clipboard copy, and version history management. Modified files: - BaseInstance.h/cpp: Added ModpackCreatorEnabled, ModpackCreatorName, and ModpackCreatorAuthor settings with accessor methods. - InstancePageProvider.h: Registered ModpackCreatorPage in the instance page list. - CMakeLists.txt: Added all new source and UI form files. Key features: - One-click 'Mark as My Modpack' toggle per instance - Automatic version snapshots stored as JSON in instance root - Comparison logic categorizes changes into Added/Removed/Updated - Clean formatted changelog output matching the requested format - Copy to clipboard with visual feedback - Version history with clear option - Mod identification by mod_id for accuracy, with filename fallback Signed-off-by: FreesmModders --- launcher/BaseInstance.cpp | 35 ++ launcher/BaseInstance.h | 7 + launcher/CMakeLists.txt | 7 + launcher/InstancePageProvider.h | 3 + .../mod/ModpackChangelogGenerator.cpp | 423 ++++++++++++++++++ .../minecraft/mod/ModpackChangelogGenerator.h | 155 +++++++ .../ui/pages/instance/ModpackCreatorPage.cpp | 179 ++++++++ .../ui/pages/instance/ModpackCreatorPage.h | 67 +++ .../ui/pages/instance/ModpackCreatorPage.ui | 175 ++++++++ 9 files changed, 1051 insertions(+) create mode 100644 launcher/minecraft/mod/ModpackChangelogGenerator.cpp create mode 100644 launcher/minecraft/mod/ModpackChangelogGenerator.h create mode 100644 launcher/ui/pages/instance/ModpackCreatorPage.cpp create mode 100644 launcher/ui/pages/instance/ModpackCreatorPage.h create mode 100644 launcher/ui/pages/instance/ModpackCreatorPage.ui diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 92a38d78a..f8bd7869b 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -125,6 +125,11 @@ BaseInstance::BaseInstance(SettingsObject* globalSettings, std::unique_ptrregisterSetting("ManagedPackVersionName", ""); m_settings->registerSetting("ManagedPackURL", ""); + // Modpack Creator Mode + m_settings->registerSetting("ModpackCreatorEnabled", false); + m_settings->registerSetting("ModpackCreatorName", ""); + m_settings->registerSetting("ModpackCreatorAuthor", ""); + m_settings->registerSetting("Profiler", ""); auto discordSetting = m_settings->registerSetting("OverrideDiscord", false); @@ -208,6 +213,36 @@ void BaseInstance::copyManagedPack(BaseInstance& other) } } +bool BaseInstance::isModpackCreatorEnabled() const +{ + return m_settings->get("ModpackCreatorEnabled").toBool(); +} + +void BaseInstance::setModpackCreatorEnabled(bool enabled) +{ + m_settings->set("ModpackCreatorEnabled", enabled); +} + +QString BaseInstance::getModpackCreatorName() const +{ + return m_settings->get("ModpackCreatorName").toString(); +} + +void BaseInstance::setModpackCreatorName(const QString& name) +{ + m_settings->set("ModpackCreatorName", name); +} + +QString BaseInstance::getModpackCreatorAuthor() const +{ + return m_settings->get("ModpackCreatorAuthor").toString(); +} + +void BaseInstance::setModpackCreatorAuthor(const QString& author) +{ + m_settings->set("ModpackCreatorAuthor", author); +} + QStringList BaseInstance::getLinkedInstances() const { auto setting = m_settings->get("linkedInstances").toString(); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 3513b0240..0691786e1 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -168,6 +168,13 @@ class BaseInstance : public QObject { void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); void copyManagedPack(BaseInstance& other); + bool isModpackCreatorEnabled() const; + void setModpackCreatorEnabled(bool enabled); + QString getModpackCreatorName() const; + void setModpackCreatorName(const QString& name); + QString getModpackCreatorAuthor() const; + void setModpackCreatorAuthor(const QString& author); + virtual QStringList extraArguments(AuthSessionPtr session); /// Traits. Normally inside the version, depends on instance implementation. diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 06e60b307..1fc1cb7e9 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -399,6 +399,10 @@ set(MINECRAFT_SOURCES minecraft/mod/tasks/GetModDependenciesTask.h minecraft/mod/tasks/GetModDependenciesTask.cpp + # Modpack Creator - changelog generator + minecraft/mod/ModpackChangelogGenerator.h + minecraft/mod/ModpackChangelogGenerator.cpp + # Assets minecraft/AssetsUtils.h minecraft/AssetsUtils.cpp @@ -975,6 +979,8 @@ SET(LAUNCHER_SOURCES ui/pages/instance/ModFolderPage.h ui/pages/instance/NotesPage.cpp ui/pages/instance/NotesPage.h + ui/pages/instance/ModpackCreatorPage.cpp + ui/pages/instance/ModpackCreatorPage.h ui/pages/instance/LogPage.cpp ui/pages/instance/LogPage.h ui/pages/instance/InstanceSettingsPage.h @@ -1283,6 +1289,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/instance/OtherLogsPage.ui ui/pages/instance/VersionPage.ui ui/pages/instance/ManagedPackPage.ui + ui/pages/instance/ModpackCreatorPage.ui ui/pages/instance/WorldListPage.ui ui/pages/instance/ScreenshotsPage.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 134fb8f24..efdf4a02f 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -8,6 +8,7 @@ #include "ui/pages/instance/LogPage.h" #include "ui/pages/instance/ManagedPackPage.h" #include "ui/pages/instance/ModFolderPage.h" +#include "ui/pages/instance/ModpackCreatorPage.h" #include "ui/pages/instance/NotesPage.h" #include "ui/pages/instance/OtherLogsPage.h" #include "ui/pages/instance/ResourcePackPage.h" @@ -41,6 +42,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { values.append(new TexturePackPage(onesix, onesix->texturePackList())); values.append(new ShaderPackPage(onesix, onesix->shaderPackList())); values.append(new NotesPage(onesix)); + values.append(new ModpackCreatorPage(onesix)); values.append(new WorldListPage(onesix, onesix->worldList())); values.append(new ServersPage(onesix)); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); @@ -54,3 +56,4 @@ class InstancePageProvider : protected QObject, public BasePageProvider { protected: BaseInstance* inst; }; + diff --git a/launcher/minecraft/mod/ModpackChangelogGenerator.cpp b/launcher/minecraft/mod/ModpackChangelogGenerator.cpp new file mode 100644 index 000000000..1c98a32f2 --- /dev/null +++ b/launcher/minecraft/mod/ModpackChangelogGenerator.cpp @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Freesm Launcher - Minecraft Launcher + * Copyright (C) 2026 FreesmModders + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ModpackChangelogGenerator.h" + +#include +#include +#include +#include +#include + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/mod/Mod.h" +#include "minecraft/mod/ModFolderModel.h" + +// ─── ModSnapshotEntry ─────────────────────────────────────────────────────── + +QJsonObject ModSnapshotEntry::toJson() const +{ + QJsonObject obj; + obj["mod_id"] = modId; + obj["name"] = name; + obj["version"] = version; + obj["file_name"] = fileName; + return obj; +} + +ModSnapshotEntry ModSnapshotEntry::fromJson(const QJsonObject& obj) +{ + ModSnapshotEntry entry; + entry.modId = obj["mod_id"].toString(); + entry.name = obj["name"].toString(); + entry.version = obj["version"].toString(); + entry.fileName = obj["file_name"].toString(); + return entry; +} + +// ─── ModpackVersionSnapshot ──────────────────────────────────────────────── + +QJsonObject ModpackVersionSnapshot::toJson() const +{ + QJsonObject obj; + obj["version_number"] = versionNumber; + obj["version_label"] = versionLabel; + obj["timestamp"] = timestamp.toString(Qt::ISODate); + + QJsonArray modsArray; + for (const auto& mod : mods) { + modsArray.append(mod.toJson()); + } + obj["mods"] = modsArray; + return obj; +} + +ModpackVersionSnapshot ModpackVersionSnapshot::fromJson(const QJsonObject& obj) +{ + ModpackVersionSnapshot snapshot; + snapshot.versionNumber = obj["version_number"].toInt(); + snapshot.versionLabel = obj["version_label"].toString(); + snapshot.timestamp = QDateTime::fromString(obj["timestamp"].toString(), Qt::ISODate); + + QJsonArray modsArray = obj["mods"].toArray(); + for (const auto& modVal : modsArray) { + snapshot.mods.append(ModSnapshotEntry::fromJson(modVal.toObject())); + } + return snapshot; +} + +// ─── ModpackChangelog ────────────────────────────────────────────────────── + +bool ModpackChangelog::isEmpty() const +{ + return addedMods.isEmpty() && removedMods.isEmpty() && updatedMods.isEmpty(); +} + +QString ModpackChangelog::formatAsText() const +{ + QString result; + + if (!updatedMods.isEmpty()) { + result += "**Updated**\n"; + // Sort alphabetically by mod name + auto sorted = updatedMods; + std::sort(sorted.begin(), sorted.end(), [](const ChangelogEntry& a, const ChangelogEntry& b) { + return a.modName.toLower() < b.modName.toLower(); + }); + for (const auto& entry : sorted) { + result += "- " + entry.modName + "\n"; + } + result += "\n"; + } + + if (!addedMods.isEmpty()) { + result += "**Added**\n"; + auto sorted = addedMods; + std::sort(sorted.begin(), sorted.end(), [](const ChangelogEntry& a, const ChangelogEntry& b) { + return a.modName.toLower() < b.modName.toLower(); + }); + for (const auto& entry : sorted) { + result += "- " + entry.modName + "\n"; + } + result += "\n"; + } + + if (!removedMods.isEmpty()) { + result += "**Removed**\n"; + auto sorted = removedMods; + std::sort(sorted.begin(), sorted.end(), [](const ChangelogEntry& a, const ChangelogEntry& b) { + return a.modName.toLower() < b.modName.toLower(); + }); + for (const auto& entry : sorted) { + result += "- " + entry.modName + "\n"; + } + result += "\n"; + } + + return result.trimmed(); +} + +QString ModpackChangelog::formatAsMarkdown() const +{ + QString result; + result += QString("## Changelog (v%1 → v%2)\n\n").arg(fromVersion).arg(toVersion); + + if (!updatedMods.isEmpty()) { + result += "### Updated\n"; + auto sorted = updatedMods; + std::sort(sorted.begin(), sorted.end(), [](const ChangelogEntry& a, const ChangelogEntry& b) { + return a.modName.toLower() < b.modName.toLower(); + }); + for (const auto& entry : sorted) { + if (!entry.oldVersion.isEmpty() && !entry.newVersion.isEmpty()) { + result += QString("- **%1** (%2 → %3)\n").arg(entry.modName, entry.oldVersion, entry.newVersion); + } else { + result += "- **" + entry.modName + "**\n"; + } + } + result += "\n"; + } + + if (!addedMods.isEmpty()) { + result += "### Added\n"; + auto sorted = addedMods; + std::sort(sorted.begin(), sorted.end(), [](const ChangelogEntry& a, const ChangelogEntry& b) { + return a.modName.toLower() < b.modName.toLower(); + }); + for (const auto& entry : sorted) { + result += "- " + entry.modName + "\n"; + } + result += "\n"; + } + + if (!removedMods.isEmpty()) { + result += "### Removed\n"; + auto sorted = removedMods; + std::sort(sorted.begin(), sorted.end(), [](const ChangelogEntry& a, const ChangelogEntry& b) { + return a.modName.toLower() < b.modName.toLower(); + }); + for (const auto& entry : sorted) { + result += "- ~~" + entry.modName + "~~\n"; + } + result += "\n"; + } + + return result.trimmed(); +} + +// ─── ModpackChangelogGenerator ───────────────────────────────────────────── + +ModpackChangelogGenerator::ModpackChangelogGenerator(MinecraftInstance* instance) : m_instance(instance) {} + +QString ModpackChangelogGenerator::historyFilePath() const +{ + return QDir(m_instance->instanceRoot()).filePath("modpack_version_history.json"); +} + +QList ModpackChangelogGenerator::loadHistory() const +{ + QList history; + + QFile file(historyFilePath()); + if (!file.exists() || !file.open(QIODevice::ReadOnly)) { + return history; + } + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError); + file.close(); + + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Failed to parse modpack version history:" << parseError.errorString(); + return history; + } + + if (!doc.isObject()) { + return history; + } + + QJsonObject root = doc.object(); + QJsonArray versionsArray = root["versions"].toArray(); + for (const auto& versionVal : versionsArray) { + history.append(ModpackVersionSnapshot::fromJson(versionVal.toObject())); + } + + // Sort by version number + std::sort(history.begin(), history.end(), [](const ModpackVersionSnapshot& a, const ModpackVersionSnapshot& b) { + return a.versionNumber < b.versionNumber; + }); + + return history; +} + +void ModpackChangelogGenerator::saveHistory(const QList& history) const +{ + QJsonArray versionsArray; + for (const auto& snapshot : history) { + versionsArray.append(snapshot.toJson()); + } + + QJsonObject root; + root["format_version"] = 1; + root["instance_id"] = m_instance->id(); + root["versions"] = versionsArray; + + QJsonDocument doc(root); + + QFile file(historyFilePath()); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qWarning() << "Failed to write modpack version history to" << historyFilePath(); + return; + } + + file.write(doc.toJson(QJsonDocument::Indented)); + file.close(); +} + +ModpackVersionSnapshot ModpackChangelogGenerator::buildSnapshotFromCurrentMods(int versionNumber) const +{ + ModpackVersionSnapshot snapshot; + snapshot.versionNumber = versionNumber; + snapshot.versionLabel = QString("v%1").arg(versionNumber); + snapshot.timestamp = QDateTime::currentDateTime(); + + ModFolderModel* modList = m_instance->loaderModList(); + if (!modList) { + qWarning() << "No mod list available for instance" << m_instance->id(); + return snapshot; + } + + auto mods = modList->allMods(); + for (auto* mod : mods) { + if (!mod) + continue; + + ModSnapshotEntry entry; + // Use the mod_id for identification; fall back to name if unavailable + entry.modId = mod->mod_id(); + if (entry.modId.isEmpty()) { + entry.modId = mod->name(); + } + + entry.name = mod->name(); + if (entry.name.isEmpty()) { + entry.name = mod->fileinfo().completeBaseName(); + } + + entry.version = mod->version(); + entry.fileName = mod->fileinfo().fileName(); + + snapshot.mods.append(entry); + } + + return snapshot; +} + +ModpackVersionSnapshot ModpackChangelogGenerator::createSnapshot() +{ + auto history = loadHistory(); + + int nextVersion = 1; + if (!history.isEmpty()) { + nextVersion = history.last().versionNumber + 1; + } + + auto snapshot = buildSnapshotFromCurrentMods(nextVersion); + history.append(snapshot); + saveHistory(history); + + qDebug() << "Created modpack snapshot v" << nextVersion << "with" << snapshot.mods.size() << "mods"; + return snapshot; +} + +ModpackChangelog ModpackChangelogGenerator::compareSnapshots(const ModpackVersionSnapshot& oldSnapshot, + const ModpackVersionSnapshot& newSnapshot) +{ + ModpackChangelog changelog; + changelog.fromVersion = oldSnapshot.versionNumber; + changelog.toVersion = newSnapshot.versionNumber; + + // Build lookup maps keyed by mod_id + QMap oldMods; + for (const auto& mod : oldSnapshot.mods) { + oldMods[mod.modId] = mod; + } + + QMap newMods; + for (const auto& mod : newSnapshot.mods) { + newMods[mod.modId] = mod; + } + + // Find added and updated mods + for (auto it = newMods.constBegin(); it != newMods.constEnd(); ++it) { + const QString& modId = it.key(); + const ModSnapshotEntry& newMod = it.value(); + + if (!oldMods.contains(modId)) { + // Added mod + ChangelogEntry entry; + entry.modName = newMod.name; + changelog.addedMods.append(entry); + } else { + // Check if updated (different version) + const ModSnapshotEntry& oldMod = oldMods[modId]; + if (!newMod.version.isEmpty() && !oldMod.version.isEmpty() && newMod.version != oldMod.version) { + ChangelogEntry entry; + entry.modName = newMod.name; + entry.oldVersion = oldMod.version; + entry.newVersion = newMod.version; + changelog.updatedMods.append(entry); + } else if (newMod.fileName != oldMod.fileName && (newMod.version.isEmpty() || oldMod.version.isEmpty())) { + // File changed but version info not available — still counts as updated + ChangelogEntry entry; + entry.modName = newMod.name; + entry.oldVersion = oldMod.version; + entry.newVersion = newMod.version; + changelog.updatedMods.append(entry); + } + } + } + + // Find removed mods + for (auto it = oldMods.constBegin(); it != oldMods.constEnd(); ++it) { + const QString& modId = it.key(); + const ModSnapshotEntry& oldMod = it.value(); + + if (!newMods.contains(modId)) { + ChangelogEntry entry; + entry.modName = oldMod.name; + changelog.removedMods.append(entry); + } + } + + return changelog; +} + +ModpackChangelog ModpackChangelogGenerator::generateChangelog() +{ + auto history = loadHistory(); + + // Get the previous snapshot (if any) + ModpackVersionSnapshot previousSnapshot; + if (!history.isEmpty()) { + previousSnapshot = history.last(); + } + + // Create the new snapshot + auto newSnapshot = createSnapshot(); + + // If there was no previous snapshot, return an empty changelog + if (previousSnapshot.versionNumber == 0) { + ModpackChangelog changelog; + changelog.fromVersion = 0; + changelog.toVersion = newSnapshot.versionNumber; + // First snapshot — treat all mods as "added" + for (const auto& mod : newSnapshot.mods) { + ChangelogEntry entry; + entry.modName = mod.name; + changelog.addedMods.append(entry); + } + return changelog; + } + + return compareSnapshots(previousSnapshot, newSnapshot); +} + +QList ModpackChangelogGenerator::getVersionHistory() const +{ + return loadHistory(); +} + +ModpackVersionSnapshot ModpackChangelogGenerator::getLatestSnapshot() const +{ + auto history = loadHistory(); + if (history.isEmpty()) { + return {}; + } + return history.last(); +} + +int ModpackChangelogGenerator::getSnapshotCount() const +{ + return loadHistory().size(); +} + +void ModpackChangelogGenerator::clearHistory() +{ + QFile::remove(historyFilePath()); +} diff --git a/launcher/minecraft/mod/ModpackChangelogGenerator.h b/launcher/minecraft/mod/ModpackChangelogGenerator.h new file mode 100644 index 000000000..6cffa9f01 --- /dev/null +++ b/launcher/minecraft/mod/ModpackChangelogGenerator.h @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Freesm Launcher - Minecraft Launcher + * Copyright (C) 2026 FreesmModders + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class ModFolderModel; +class MinecraftInstance; + +/** + * Represents a single mod entry in a version snapshot. + */ +struct ModSnapshotEntry { + QString modId; // Unique mod ID (from mod metadata) + QString name; // Human-readable name + QString version; // Mod version string + QString fileName; // File name on disk + + QJsonObject toJson() const; + static ModSnapshotEntry fromJson(const QJsonObject& obj); +}; + +/** + * Represents a version snapshot of a modpack's mod list at a specific point in time. + */ +struct ModpackVersionSnapshot { + int versionNumber = 0; + QString versionLabel; // User-facing label, e.g. "v3" + QDateTime timestamp; + QList mods; + + QJsonObject toJson() const; + static ModpackVersionSnapshot fromJson(const QJsonObject& obj); +}; + +/** + * Represents a single change entry in a changelog. + */ +struct ChangelogEntry { + QString modName; + QString oldVersion; // Only for Updated + QString newVersion; // Only for Updated +}; + +/** + * The result of comparing two version snapshots. + */ +struct ModpackChangelog { + int fromVersion = 0; + int toVersion = 0; + QList addedMods; + QList removedMods; + QList updatedMods; + + /** Format the changelog as a clean human-readable string. */ + QString formatAsText() const; + + /** Format the changelog as Markdown. */ + QString formatAsMarkdown() const; + + /** Returns true if there are no changes. */ + bool isEmpty() const; +}; + +/** + * Handles creating version snapshots and generating changelogs for modpacks. + * + * Version history is stored as a JSON file in the instance's root directory. + */ +class ModpackChangelogGenerator { + public: + explicit ModpackChangelogGenerator(MinecraftInstance* instance); + + /** + * Creates a new version snapshot from the current mod list. + * Returns the newly created snapshot. + */ + ModpackVersionSnapshot createSnapshot(); + + /** + * Compares two snapshots and returns a changelog describing the differences. + */ + static ModpackChangelog compareSnapshots(const ModpackVersionSnapshot& oldSnapshot, const ModpackVersionSnapshot& newSnapshot); + + /** + * Generates a changelog by creating a new snapshot and comparing with the previous one. + * This is the main entry point for the "Generate Changelog" button. + * Returns the generated changelog. + */ + ModpackChangelog generateChangelog(); + + /** + * Returns all stored version snapshots, ordered by version number. + */ + QList getVersionHistory() const; + + /** + * Returns the latest snapshot, or a default-constructed one if none exists. + */ + ModpackVersionSnapshot getLatestSnapshot() const; + + /** + * Returns the number of stored snapshots. + */ + int getSnapshotCount() const; + + /** + * Deletes all version history. + */ + void clearHistory(); + + private: + /** + * Builds a snapshot from the current state of the mod folder. + */ + ModpackVersionSnapshot buildSnapshotFromCurrentMods(int versionNumber) const; + + /** + * Loads the version history from disk. + */ + QList loadHistory() const; + + /** + * Saves the version history to disk. + */ + void saveHistory(const QList& history) const; + + /** + * Returns the path to the version history JSON file. + */ + QString historyFilePath() const; + + MinecraftInstance* m_instance; +}; diff --git a/launcher/ui/pages/instance/ModpackCreatorPage.cpp b/launcher/ui/pages/instance/ModpackCreatorPage.cpp new file mode 100644 index 000000000..f1937181c --- /dev/null +++ b/launcher/ui/pages/instance/ModpackCreatorPage.cpp @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Freesm Launcher - Minecraft Launcher + * Copyright (C) 2026 FreesmModders + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ModpackCreatorPage.h" +#include "ui_ModpackCreatorPage.h" + +#include +#include +#include +#include + +#include "minecraft/mod/ModpackChangelogGenerator.h" + +ModpackCreatorPage::ModpackCreatorPage(MinecraftInstance* inst, QWidget* parent) + : QWidget(parent), ui(new Ui::ModpackCreatorPage), m_inst(inst) +{ + ui->setupUi(this); + + // Load existing metadata + auto* settings = m_inst->settings(); + bool isModpack = settings->get("ModpackCreatorEnabled").toBool(); + QString modpackName = settings->get("ModpackCreatorName").toString(); + QString modpackAuthor = settings->get("ModpackCreatorAuthor").toString(); + + ui->modpackNameEdit->setText(modpackName); + ui->modpackAuthorEdit->setText(modpackAuthor); + + updateUI(); +} + +ModpackCreatorPage::~ModpackCreatorPage() +{ + delete ui; +} + +bool ModpackCreatorPage::apply() +{ + auto* settings = m_inst->settings(); + settings->set("ModpackCreatorName", ui->modpackNameEdit->text()); + settings->set("ModpackCreatorAuthor", ui->modpackAuthorEdit->text()); + return true; +} + +bool ModpackCreatorPage::shouldDisplay() const +{ + return true; +} + +void ModpackCreatorPage::retranslate() +{ + ui->retranslateUi(this); +} + +void ModpackCreatorPage::on_markAsModpackBtn_clicked() +{ + auto* settings = m_inst->settings(); + bool isCurrentlyEnabled = settings->get("ModpackCreatorEnabled").toBool(); + + if (isCurrentlyEnabled) { + auto reply = QMessageBox::question(this, tr("Disable Modpack Creator"), + tr("Are you sure you want to unmark this instance as a modpack?\n\n" + "Version history will be preserved but can be cleared separately."), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (reply == QMessageBox::No) { + return; + } + } + + settings->set("ModpackCreatorEnabled", !isCurrentlyEnabled); + updateUI(); +} + +void ModpackCreatorPage::on_generateChangelogBtn_clicked() +{ + ModpackChangelogGenerator generator(m_inst); + + auto changelog = generator.generateChangelog(); + + if (changelog.isEmpty() && changelog.fromVersion > 0) { + ui->changelogOutput->setPlainText(tr("No changes detected since the last snapshot.")); + } else { + ui->changelogOutput->setPlainText(changelog.formatAsText()); + } + + refreshVersionInfo(); +} + +void ModpackCreatorPage::on_copyChangelogBtn_clicked() +{ + QString text = ui->changelogOutput->toPlainText(); + if (text.isEmpty()) { + return; + } + + QGuiApplication::clipboard()->setText(text); + + // Brief visual feedback + ui->copyChangelogBtn->setText(tr("Copied!")); + QTimer::singleShot(2000, this, [this]() { ui->copyChangelogBtn->setText(tr("Copy to Clipboard")); }); +} + +void ModpackCreatorPage::on_clearHistoryBtn_clicked() +{ + auto reply = QMessageBox::warning(this, tr("Clear Version History"), + tr("Are you sure you want to delete all version history for this modpack?\n\n" + "This action cannot be undone."), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + if (reply == QMessageBox::Yes) { + ModpackChangelogGenerator generator(m_inst); + generator.clearHistory(); + ui->changelogOutput->clear(); + refreshVersionInfo(); + } +} + +void ModpackCreatorPage::on_modpackNameEdit_textChanged(const QString& text) +{ + m_inst->settings()->set("ModpackCreatorName", text); +} + +void ModpackCreatorPage::on_modpackAuthorEdit_textChanged(const QString& text) +{ + m_inst->settings()->set("ModpackCreatorAuthor", text); +} + +void ModpackCreatorPage::updateUI() +{ + auto* settings = m_inst->settings(); + bool isModpack = settings->get("ModpackCreatorEnabled").toBool(); + + // Update toggle button + if (isModpack) { + ui->markAsModpackBtn->setText(tr("✓ Marked as My Modpack")); + ui->markAsModpackBtn->setToolTip(tr("Click to unmark this instance as your modpack")); + } else { + ui->markAsModpackBtn->setText(tr("Mark as My Modpack")); + ui->markAsModpackBtn->setToolTip(tr("Click to mark this instance as a modpack you created")); + } + + // Enable/disable modpack tools based on state + ui->modpackInfoGroup->setEnabled(isModpack); + ui->changelogGroup->setEnabled(isModpack); + + if (isModpack) { + refreshVersionInfo(); + } +} + +void ModpackCreatorPage::refreshVersionInfo() +{ + ModpackChangelogGenerator generator(m_inst); + int snapshotCount = generator.getSnapshotCount(); + + if (snapshotCount == 0) { + ui->versionInfoLabel->setText(tr("No snapshots yet. Click \"Generate Changelog\" to create the first one.")); + } else { + auto latest = generator.getLatestSnapshot(); + ui->versionInfoLabel->setText( + tr("Current version: %1 | Total snapshots: %2 | Last snapshot: %3") + .arg(latest.versionLabel) + .arg(snapshotCount) + .arg(latest.timestamp.toString("yyyy-MM-dd hh:mm"))); + } +} diff --git a/launcher/ui/pages/instance/ModpackCreatorPage.h b/launcher/ui/pages/instance/ModpackCreatorPage.h new file mode 100644 index 000000000..f789574ac --- /dev/null +++ b/launcher/ui/pages/instance/ModpackCreatorPage.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Freesm Launcher - Minecraft Launcher + * Copyright (C) 2026 FreesmModders + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include "minecraft/MinecraftInstance.h" +#include "ui/pages/BasePage.h" + +namespace Ui { +class ModpackCreatorPage; +} + +class ModpackCreatorPage : public QWidget, public BasePage { + Q_OBJECT + + public: + explicit ModpackCreatorPage(MinecraftInstance* inst, QWidget* parent = nullptr); + virtual ~ModpackCreatorPage(); + + virtual QString displayName() const override { return tr("Modpack Creator"); } + virtual QIcon icon() const override + { + auto icon = QIcon::fromTheme("project-development"); + if (icon.isNull()) + icon = QIcon::fromTheme("package-x-generic"); + if (icon.isNull()) + icon = QIcon::fromTheme("preferences-plugin"); + return icon; + } + virtual QString id() const override { return "modpack-creator"; } + virtual bool apply() override; + virtual QString helpPage() const override { return "Modpack-Creator"; } + virtual bool shouldDisplay() const override; + void retranslate() override; + + private slots: + void on_markAsModpackBtn_clicked(); + void on_generateChangelogBtn_clicked(); + void on_copyChangelogBtn_clicked(); + void on_clearHistoryBtn_clicked(); + void on_modpackNameEdit_textChanged(const QString& text); + void on_modpackAuthorEdit_textChanged(const QString& text); + + private: + void updateUI(); + void refreshVersionInfo(); + + Ui::ModpackCreatorPage* ui; + MinecraftInstance* m_inst; +}; diff --git a/launcher/ui/pages/instance/ModpackCreatorPage.ui b/launcher/ui/pages/instance/ModpackCreatorPage.ui new file mode 100644 index 000000000..7847d90cf --- /dev/null +++ b/launcher/ui/pages/instance/ModpackCreatorPage.ui @@ -0,0 +1,175 @@ + + + ModpackCreatorPage + + + + 0 + 0 + 700 + 600 + + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + + Mark as My Modpack + + + 36 + + + Click to mark this instance as a modpack you created + + + + + + + + + Modpack Information + + + + + + Modpack Name: + + + + + + + Enter your modpack name... + + + + + + + Author: + + + + + + + Enter your name... + + + + + + + + + + + + Changelog Generator + + + + + + + No snapshots yet. Click "Generate Changelog" to create the first one. + + + true + + + color: gray; font-style: italic; + + + + + + + + + + Generate Changelog + + + Create a new version snapshot and compare with the previous one to generate a changelog + + + 32 + + + + + + + Copy to Clipboard + + + Copy the changelog text to clipboard + + + + + + + Qt::Horizontal + + + + + + + Clear History + + + Delete all version snapshots for this modpack + + + + + + + + + + true + + + Changelog will appear here after generation... + + + + + + + + + + + markAsModpackBtn + modpackNameEdit + modpackAuthorEdit + generateChangelogBtn + copyChangelogBtn + clearHistoryBtn + changelogOutput + + + + From a3fb82f9c532fb4b1f3b56c860fe122cee6f7296 Mon Sep 17 00:00:00 2001 From: FreesmModders Date: Sat, 16 May 2026 09:23:38 -0300 Subject: [PATCH 2/2] fix: Adjust modpack creator workflow to compare with baseline Signed-off-by: FreesmModders --- launcher/CMakeLists.txt | 4 ++-- launcher/InstancePageProvider.h | 4 ++-- .../minecraft/mod/ModpackChangelogGenerator.cpp | 14 +++++++------- launcher/ui/pages/instance/ModpackCreatorPage.cpp | 13 +++++++++++-- launcher/ui/pages/instance/ModpackCreatorPage.h | 5 ++++- launcher/ui/pages/instance/ModpackCreatorPage.ui | 15 ++++++++++++++- 6 files changed, 40 insertions(+), 15 deletions(-) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 1fc1cb7e9..dcda1f257 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1639,8 +1639,8 @@ if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") target_compile_options(Launcher_logic PRIVATE /wd4100) # C4100 - unused parameter target_compile_options(${Launcher_Name} PRIVATE /wd4100) # C4100 - unused parameter else() - target_compile_options(Launcher_logic PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers) - target_compile_options(${Launcher_Name} PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers) + target_compile_options(Launcher_logic PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-error=sfinae-incomplete) + target_compile_options(${Launcher_Name} PRIVATE -Wno-unused-parameter -Wno-missing-field-initializers -Wno-error=sfinae-incomplete) endif() #### The bundle mess! #### diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index efdf4a02f..e18b7c5db 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -1,11 +1,11 @@ #pragma once +#include "ui/pages/instance/InstanceSettingsPage.h" +#include "ui/pages/instance/LogPage.h" #include #include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" #include "ui/pages/BasePageProvider.h" -#include "ui/pages/instance/InstanceSettingsPage.h" -#include "ui/pages/instance/LogPage.h" #include "ui/pages/instance/ManagedPackPage.h" #include "ui/pages/instance/ModFolderPage.h" #include "ui/pages/instance/ModpackCreatorPage.h" diff --git a/launcher/minecraft/mod/ModpackChangelogGenerator.cpp b/launcher/minecraft/mod/ModpackChangelogGenerator.cpp index 1c98a32f2..b194eceae 100644 --- a/launcher/minecraft/mod/ModpackChangelogGenerator.cpp +++ b/launcher/minecraft/mod/ModpackChangelogGenerator.cpp @@ -378,16 +378,16 @@ ModpackChangelog ModpackChangelogGenerator::generateChangelog() previousSnapshot = history.last(); } - // Create the new snapshot - auto newSnapshot = createSnapshot(); + // Create a snapshot of the current state but DO NOT save it + int nextVersion = previousSnapshot.versionNumber + 1; + auto currentSnapshot = buildSnapshotFromCurrentMods(nextVersion); - // If there was no previous snapshot, return an empty changelog + // If there was no previous snapshot, return all mods as added if (previousSnapshot.versionNumber == 0) { ModpackChangelog changelog; changelog.fromVersion = 0; - changelog.toVersion = newSnapshot.versionNumber; - // First snapshot — treat all mods as "added" - for (const auto& mod : newSnapshot.mods) { + changelog.toVersion = currentSnapshot.versionNumber; + for (const auto& mod : currentSnapshot.mods) { ChangelogEntry entry; entry.modName = mod.name; changelog.addedMods.append(entry); @@ -395,7 +395,7 @@ ModpackChangelog ModpackChangelogGenerator::generateChangelog() return changelog; } - return compareSnapshots(previousSnapshot, newSnapshot); + return compareSnapshots(previousSnapshot, currentSnapshot); } QList ModpackChangelogGenerator::getVersionHistory() const diff --git a/launcher/ui/pages/instance/ModpackCreatorPage.cpp b/launcher/ui/pages/instance/ModpackCreatorPage.cpp index f1937181c..7b1222fa4 100644 --- a/launcher/ui/pages/instance/ModpackCreatorPage.cpp +++ b/launcher/ui/pages/instance/ModpackCreatorPage.cpp @@ -25,6 +25,7 @@ #include #include "minecraft/mod/ModpackChangelogGenerator.h" +#include "minecraft/MinecraftInstance.h" ModpackCreatorPage::ModpackCreatorPage(MinecraftInstance* inst, QWidget* parent) : QWidget(parent), ui(new Ui::ModpackCreatorPage), m_inst(inst) @@ -33,7 +34,6 @@ ModpackCreatorPage::ModpackCreatorPage(MinecraftInstance* inst, QWidget* parent) // Load existing metadata auto* settings = m_inst->settings(); - bool isModpack = settings->get("ModpackCreatorEnabled").toBool(); QString modpackName = settings->get("ModpackCreatorName").toString(); QString modpackAuthor = settings->get("ModpackCreatorAuthor").toString(); @@ -85,6 +85,15 @@ void ModpackCreatorPage::on_markAsModpackBtn_clicked() updateUI(); } +void ModpackCreatorPage::on_markAsOldVersionBtn_clicked() +{ + ModpackChangelogGenerator generator(m_inst); + generator.createSnapshot(); + + // Auto-generate the changelog immediately after taking the snapshot to show it + on_generateChangelogBtn_clicked(); +} + void ModpackCreatorPage::on_generateChangelogBtn_clicked() { ModpackChangelogGenerator generator(m_inst); @@ -167,7 +176,7 @@ void ModpackCreatorPage::refreshVersionInfo() int snapshotCount = generator.getSnapshotCount(); if (snapshotCount == 0) { - ui->versionInfoLabel->setText(tr("No snapshots yet. Click \"Generate Changelog\" to create the first one.")); + ui->versionInfoLabel->setText(tr("No snapshots yet. Click \"Mark as Old Version\" to create the baseline.")); } else { auto latest = generator.getLatestSnapshot(); ui->versionInfoLabel->setText( diff --git a/launcher/ui/pages/instance/ModpackCreatorPage.h b/launcher/ui/pages/instance/ModpackCreatorPage.h index f789574ac..6df516c46 100644 --- a/launcher/ui/pages/instance/ModpackCreatorPage.h +++ b/launcher/ui/pages/instance/ModpackCreatorPage.h @@ -20,9 +20,11 @@ #include -#include "minecraft/MinecraftInstance.h" +#include "BaseInstance.h" #include "ui/pages/BasePage.h" +class MinecraftInstance; + namespace Ui { class ModpackCreatorPage; } @@ -52,6 +54,7 @@ class ModpackCreatorPage : public QWidget, public BasePage { private slots: void on_markAsModpackBtn_clicked(); + void on_markAsOldVersionBtn_clicked(); void on_generateChangelogBtn_clicked(); void on_copyChangelogBtn_clicked(); void on_clearHistoryBtn_clicked(); diff --git a/launcher/ui/pages/instance/ModpackCreatorPage.ui b/launcher/ui/pages/instance/ModpackCreatorPage.ui index 7847d90cf..892a4881f 100644 --- a/launcher/ui/pages/instance/ModpackCreatorPage.ui +++ b/launcher/ui/pages/instance/ModpackCreatorPage.ui @@ -102,13 +102,26 @@ + + + + Mark as Old Version + + + Save the current mod list as the baseline to compare against later + + + 32 + + + Generate Changelog - Create a new version snapshot and compare with the previous one to generate a changelog + Compare the current mods with the latest saved old version 32