From 368976e9b45f6b4fdb2443dd0f0d8443f8d3d819 Mon Sep 17 00:00:00 2001 From: rhit-daughewd <148395658+rhit-daughewd@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:58:41 -0400 Subject: [PATCH 1/4] Modpacks Implementation --- backend/app/interactions.go | 4 + backend/args.go | 7 +- backend/ficsitcli/modpack.go | 188 ++++++++++++++++++ cspell.json | 1 + frontend/src/App.svelte | 15 ++ .../src/gql/modpacks/modpackSummary.graphql | 9 + .../modals/ExternalInstallModpack.svelte | 82 ++++++++ 7 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 backend/ficsitcli/modpack.go create mode 100644 frontend/src/gql/modpacks/modpackSummary.graphql create mode 100644 frontend/src/lib/components/modals/ExternalInstallModpack.svelte diff --git a/backend/app/interactions.go b/backend/app/interactions.go index 268c7e6e..fa9ca8c7 100644 --- a/backend/app/interactions.go +++ b/backend/app/interactions.go @@ -111,6 +111,10 @@ func (a *app) ExternalImportProfile(path string) { wailsRuntime.EventsEmit(common.AppContext, "externalImportProfile", path) } +func (a *app) ExternalInstallModpack(modpackID, version string) { + wailsRuntime.EventsEmit(common.AppContext, "externalInstallModpack", modpackID, version) +} + func (a *app) Show() { wailsRuntime.WindowUnminimise(common.AppContext) wailsRuntime.Show(common.AppContext) diff --git a/backend/args.go b/backend/args.go index 161f9093..3152489d 100644 --- a/backend/args.go +++ b/backend/args.go @@ -36,8 +36,13 @@ func handleURI(uri string) error { switch u.Host { case "install": modID := u.Query().Get("modID") + modpackID := u.Query().Get("modpackID") version := u.Query().Get("version") - app.App.ExternalInstallMod(modID, version) + if modpackID != "" { + app.App.ExternalInstallModpack(modpackID, version) + } else { + app.App.ExternalInstallMod(modID, version) + } return nil default: return fmt.Errorf("unknown URI action %s", u.Host) diff --git a/backend/ficsitcli/modpack.go b/backend/ficsitcli/modpack.go new file mode 100644 index 00000000..1a5fbb06 --- /dev/null +++ b/backend/ficsitcli/modpack.go @@ -0,0 +1,188 @@ +package ficsitcli + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" + "net/http" + + "github.com/spf13/viper" +) + +type ModpackReleaseResponse struct { + GetModpackRelease struct { + Lockfile string `json:"lockfile"` + } `json:"getModpackRelease"` +} + +type PlatformTarget struct { + Hash string `json:"hash"` + Link string `json:"link"` +} + +type ModEntry struct { + Dependencies interface{} `json:"dependencies"` + Targets map[string]PlatformTarget `json:"targets"` + Version string `json:"version"` +} + +type Lockfile struct { + Mods map[string]ModEntry `json:"mods"` + Version int `json:"version"` +} + +func (f *ficsitCLI) InstallModpackRelease(modpackID string, release string, name string) error { + return f.action(ActionInstall, newItem(modpackID, release), func(l *slog.Logger, taskUpdates chan<- taskUpdate) error { + selectedInstallation := f.GetSelectedInstall() + if selectedInstallation == nil { + return fmt.Errorf("no installation selected") + } + + l = l.With( + slog.String("install", selectedInstallation.Path), + slog.String("profile", selectedInstallation.Profile), + ) + f.AddProfile(name + "-" + release) + profileErr := f.setProfileModpack(l, name+"-"+release) + if profileErr != nil { + l.Error("failed to set profile", slog.Any("error", profileErr)) + return fmt.Errorf("failed to set profile: %w", profileErr) + } + + lockfile, err := getLockfile(modpackID, release) + if err != nil { + return fmt.Errorf("failed to get lockfile: %w", err) + } + + for modID, mod := range lockfile.Mods { + modErr := f.installModVersionModpack(l, modID, mod.Version) + if modErr != nil { + l.Error("failed to install mod", + slog.String("mod", modID), + slog.String("version", mod.Version), + slog.Any("error", modErr)) + + return fmt.Errorf("failed to install mod: %s@%s: %w", + modID, mod.Version, modErr) + } + } + + installErr := f.apply(l, taskUpdates) + if installErr != nil { + l.Error("failed to install", slog.Any("error", installErr)) + return installErr + } + + return nil + }) +} + +type graphQLResponse struct { + Data struct { + GetModpackRelease struct { + Lockfile string `json:"lockfile"` + } `json:"getModpackRelease"` + } `json:"data"` + Errors []struct { + Message string `json:"message"` + } `json:"errors"` +} + +func getLockfile(modpackID string, release string) (Lockfile, error) { + endpoint := viper.GetString("api-base") + viper.GetString("graphql-api") + + body := map[string]interface{}{ + "query": `query GetModpackRelease($modpackID: ModpackID!, $version: String!) { + getModpackRelease(modpackID: $modpackID, version: $version) { + lockfile + } + }`, + "variables": map[string]interface{}{ + "modpackID": modpackID, + "version": release, + }, + } + + jsonBody, _ := json.Marshal(body) + resp, err := (&http.Client{}).Post(endpoint, "application/json", bytes.NewBuffer(jsonBody)) + if err != nil { + return Lockfile{}, fmt.Errorf("failed to query GraphQL: %w", err) + } + defer resp.Body.Close() + + var gqlResp graphQLResponse + if err := json.NewDecoder(resp.Body).Decode(&gqlResp); err != nil { + return Lockfile{}, fmt.Errorf("failed to decode response: %w", err) + } + + if len(gqlResp.Errors) > 0 { + return Lockfile{}, fmt.Errorf("GraphQL error: %s", gqlResp.Errors[0].Message) + } + if gqlResp.Data.GetModpackRelease.Lockfile == "" { + return Lockfile{}, fmt.Errorf("lockfile not found for %s@%s", modpackID, release) + } + + var lockfile Lockfile + if err := json.Unmarshal([]byte(gqlResp.Data.GetModpackRelease.Lockfile), &lockfile); err != nil { + return Lockfile{}, fmt.Errorf("failed to parse lockfile: %w", err) + } + return lockfile, nil +} + +func (f *ficsitCLI) setProfileModpack(l *slog.Logger, profile string) error { + selectedInstallation := f.GetSelectedInstall() + + if selectedInstallation == nil { + l.Error("no installation selected") + return fmt.Errorf("no installation selected") + } + + if selectedInstallation.Profile == profile { + return nil + } + + err := selectedInstallation.SetProfile(f.ficsitCli, profile) + if err != nil { + l.Error("failed to set profile", slog.Any("error", err)) + return fmt.Errorf("failed to set profile: %w", err) + } + + err = f.ficsitCli.Installations.Save() + if err != nil { + l.Error("failed to save installations", slog.Any("error", err)) + } + + f.EmitGlobals() + f.EmitModsChange() + + return nil +} + +func (f *ficsitCLI) installModVersionModpack(l *slog.Logger, mod string, version string) error { + selectedInstallation := f.GetSelectedInstall() + + if selectedInstallation == nil { + return fmt.Errorf("no installation selected") + } + + l = l.With( + slog.String("install", selectedInstallation.Path), + slog.String("profile", selectedInstallation.Profile), + ) + + profile := f.GetProfile(selectedInstallation.Profile) + + profileErr := profile.AddMod(mod, version) + if profileErr != nil { + l.Error("failed to add mod", slog.Any("error", profileErr)) + return fmt.Errorf("failed to add mod: %s@%s: %w", mod, version, profileErr) + } + + err := f.ficsitCli.Profiles.Save() + if err != nil { + l.Error("failed to save profile", slog.Any("error", err)) + } + + return nil +} diff --git a/cspell.json b/cspell.json index 1042873d..f7c9a421 100644 --- a/cspell.json +++ b/cspell.json @@ -19,6 +19,7 @@ "Maximised", "Minimised", "mircearoata", + "Modpack", "noclose", "Nyan", "smmanager", diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 83089140..0c20bc81 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -13,6 +13,7 @@ import ErrorDetails from '$lib/components/modals/ErrorDetails.svelte'; import ErrorModal from '$lib/components/modals/ErrorModal.svelte'; import ExternalInstallMod from '$lib/components/modals/ExternalInstallMod.svelte'; + import ExternalInstallModpack from '$lib/components/modals/ExternalInstallModpack.svelte'; import MigrationModal from '$lib/components/modals/MigrationModal.svelte'; import { supportedProgressTypes } from '$lib/components/modals/ProgressModal.svelte'; import FirstTimeSetupModal from '$lib/components/modals/first-time-setup/FirstTimeSetupModal.svelte'; @@ -224,6 +225,20 @@ }); }); + EventsOn('externalInstallModpack', (modpackReference: string, version: string) => { + if (!modpackReference) return; + modalStore.trigger({ + type: 'component', + component: { + ref: ExternalInstallModpack, + props: { + modpackReference, + version, + }, + }, + }); + }); + EventsOn('externalImportProfile', async (path: string) => { if (!path) return; modalStore.trigger({ diff --git a/frontend/src/gql/modpacks/modpackSummary.graphql b/frontend/src/gql/modpacks/modpackSummary.graphql new file mode 100644 index 00000000..52691cb6 --- /dev/null +++ b/frontend/src/gql/modpacks/modpackSummary.graphql @@ -0,0 +1,9 @@ +query GetModpackSummary($modpackID: ModpackID!) { + modpack: getModpack(modpackID: $modpackID) { + name + logo + views + short_description + parent_id + } +} \ No newline at end of file diff --git a/frontend/src/lib/components/modals/ExternalInstallModpack.svelte b/frontend/src/lib/components/modals/ExternalInstallModpack.svelte new file mode 100644 index 00000000..86bba10b --- /dev/null +++ b/frontend/src/lib/components/modals/ExternalInstallModpack.svelte @@ -0,0 +1,82 @@ + + +
{modpack.name}
+ {#if version} +{modpack.short_description}
+