Skip to content

Commit 368976e

Browse files
committed
Modpacks Implementation
1 parent 7189ff7 commit 368976e

7 files changed

Lines changed: 305 additions & 1 deletion

File tree

backend/app/interactions.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ func (a *app) ExternalImportProfile(path string) {
111111
wailsRuntime.EventsEmit(common.AppContext, "externalImportProfile", path)
112112
}
113113

114+
func (a *app) ExternalInstallModpack(modpackID, version string) {
115+
wailsRuntime.EventsEmit(common.AppContext, "externalInstallModpack", modpackID, version)
116+
}
117+
114118
func (a *app) Show() {
115119
wailsRuntime.WindowUnminimise(common.AppContext)
116120
wailsRuntime.Show(common.AppContext)

backend/args.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ func handleURI(uri string) error {
3636
switch u.Host {
3737
case "install":
3838
modID := u.Query().Get("modID")
39+
modpackID := u.Query().Get("modpackID")
3940
version := u.Query().Get("version")
40-
app.App.ExternalInstallMod(modID, version)
41+
if modpackID != "" {
42+
app.App.ExternalInstallModpack(modpackID, version)
43+
} else {
44+
app.App.ExternalInstallMod(modID, version)
45+
}
4146
return nil
4247
default:
4348
return fmt.Errorf("unknown URI action %s", u.Host)

backend/ficsitcli/modpack.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package ficsitcli
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"log/slog"
8+
"net/http"
9+
10+
"github.com/spf13/viper"
11+
)
12+
13+
type ModpackReleaseResponse struct {
14+
GetModpackRelease struct {
15+
Lockfile string `json:"lockfile"`
16+
} `json:"getModpackRelease"`
17+
}
18+
19+
type PlatformTarget struct {
20+
Hash string `json:"hash"`
21+
Link string `json:"link"`
22+
}
23+
24+
type ModEntry struct {
25+
Dependencies interface{} `json:"dependencies"`
26+
Targets map[string]PlatformTarget `json:"targets"`
27+
Version string `json:"version"`
28+
}
29+
30+
type Lockfile struct {
31+
Mods map[string]ModEntry `json:"mods"`
32+
Version int `json:"version"`
33+
}
34+
35+
func (f *ficsitCLI) InstallModpackRelease(modpackID string, release string, name string) error {
36+
return f.action(ActionInstall, newItem(modpackID, release), func(l *slog.Logger, taskUpdates chan<- taskUpdate) error {
37+
selectedInstallation := f.GetSelectedInstall()
38+
if selectedInstallation == nil {
39+
return fmt.Errorf("no installation selected")
40+
}
41+
42+
l = l.With(
43+
slog.String("install", selectedInstallation.Path),
44+
slog.String("profile", selectedInstallation.Profile),
45+
)
46+
f.AddProfile(name + "-" + release)
47+
profileErr := f.setProfileModpack(l, name+"-"+release)
48+
if profileErr != nil {
49+
l.Error("failed to set profile", slog.Any("error", profileErr))
50+
return fmt.Errorf("failed to set profile: %w", profileErr)
51+
}
52+
53+
lockfile, err := getLockfile(modpackID, release)
54+
if err != nil {
55+
return fmt.Errorf("failed to get lockfile: %w", err)
56+
}
57+
58+
for modID, mod := range lockfile.Mods {
59+
modErr := f.installModVersionModpack(l, modID, mod.Version)
60+
if modErr != nil {
61+
l.Error("failed to install mod",
62+
slog.String("mod", modID),
63+
slog.String("version", mod.Version),
64+
slog.Any("error", modErr))
65+
66+
return fmt.Errorf("failed to install mod: %s@%s: %w",
67+
modID, mod.Version, modErr)
68+
}
69+
}
70+
71+
installErr := f.apply(l, taskUpdates)
72+
if installErr != nil {
73+
l.Error("failed to install", slog.Any("error", installErr))
74+
return installErr
75+
}
76+
77+
return nil
78+
})
79+
}
80+
81+
type graphQLResponse struct {
82+
Data struct {
83+
GetModpackRelease struct {
84+
Lockfile string `json:"lockfile"`
85+
} `json:"getModpackRelease"`
86+
} `json:"data"`
87+
Errors []struct {
88+
Message string `json:"message"`
89+
} `json:"errors"`
90+
}
91+
92+
func getLockfile(modpackID string, release string) (Lockfile, error) {
93+
endpoint := viper.GetString("api-base") + viper.GetString("graphql-api")
94+
95+
body := map[string]interface{}{
96+
"query": `query GetModpackRelease($modpackID: ModpackID!, $version: String!) {
97+
getModpackRelease(modpackID: $modpackID, version: $version) {
98+
lockfile
99+
}
100+
}`,
101+
"variables": map[string]interface{}{
102+
"modpackID": modpackID,
103+
"version": release,
104+
},
105+
}
106+
107+
jsonBody, _ := json.Marshal(body)
108+
resp, err := (&http.Client{}).Post(endpoint, "application/json", bytes.NewBuffer(jsonBody))
109+
if err != nil {
110+
return Lockfile{}, fmt.Errorf("failed to query GraphQL: %w", err)
111+
}
112+
defer resp.Body.Close()
113+
114+
var gqlResp graphQLResponse
115+
if err := json.NewDecoder(resp.Body).Decode(&gqlResp); err != nil {
116+
return Lockfile{}, fmt.Errorf("failed to decode response: %w", err)
117+
}
118+
119+
if len(gqlResp.Errors) > 0 {
120+
return Lockfile{}, fmt.Errorf("GraphQL error: %s", gqlResp.Errors[0].Message)
121+
}
122+
if gqlResp.Data.GetModpackRelease.Lockfile == "" {
123+
return Lockfile{}, fmt.Errorf("lockfile not found for %s@%s", modpackID, release)
124+
}
125+
126+
var lockfile Lockfile
127+
if err := json.Unmarshal([]byte(gqlResp.Data.GetModpackRelease.Lockfile), &lockfile); err != nil {
128+
return Lockfile{}, fmt.Errorf("failed to parse lockfile: %w", err)
129+
}
130+
return lockfile, nil
131+
}
132+
133+
func (f *ficsitCLI) setProfileModpack(l *slog.Logger, profile string) error {
134+
selectedInstallation := f.GetSelectedInstall()
135+
136+
if selectedInstallation == nil {
137+
l.Error("no installation selected")
138+
return fmt.Errorf("no installation selected")
139+
}
140+
141+
if selectedInstallation.Profile == profile {
142+
return nil
143+
}
144+
145+
err := selectedInstallation.SetProfile(f.ficsitCli, profile)
146+
if err != nil {
147+
l.Error("failed to set profile", slog.Any("error", err))
148+
return fmt.Errorf("failed to set profile: %w", err)
149+
}
150+
151+
err = f.ficsitCli.Installations.Save()
152+
if err != nil {
153+
l.Error("failed to save installations", slog.Any("error", err))
154+
}
155+
156+
f.EmitGlobals()
157+
f.EmitModsChange()
158+
159+
return nil
160+
}
161+
162+
func (f *ficsitCLI) installModVersionModpack(l *slog.Logger, mod string, version string) error {
163+
selectedInstallation := f.GetSelectedInstall()
164+
165+
if selectedInstallation == nil {
166+
return fmt.Errorf("no installation selected")
167+
}
168+
169+
l = l.With(
170+
slog.String("install", selectedInstallation.Path),
171+
slog.String("profile", selectedInstallation.Profile),
172+
)
173+
174+
profile := f.GetProfile(selectedInstallation.Profile)
175+
176+
profileErr := profile.AddMod(mod, version)
177+
if profileErr != nil {
178+
l.Error("failed to add mod", slog.Any("error", profileErr))
179+
return fmt.Errorf("failed to add mod: %s@%s: %w", mod, version, profileErr)
180+
}
181+
182+
err := f.ficsitCli.Profiles.Save()
183+
if err != nil {
184+
l.Error("failed to save profile", slog.Any("error", err))
185+
}
186+
187+
return nil
188+
}

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"Maximised",
2020
"Minimised",
2121
"mircearoata",
22+
"Modpack",
2223
"noclose",
2324
"Nyan",
2425
"smmanager",

frontend/src/App.svelte

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import ErrorDetails from '$lib/components/modals/ErrorDetails.svelte';
1414
import ErrorModal from '$lib/components/modals/ErrorModal.svelte';
1515
import ExternalInstallMod from '$lib/components/modals/ExternalInstallMod.svelte';
16+
import ExternalInstallModpack from '$lib/components/modals/ExternalInstallModpack.svelte';
1617
import MigrationModal from '$lib/components/modals/MigrationModal.svelte';
1718
import { supportedProgressTypes } from '$lib/components/modals/ProgressModal.svelte';
1819
import FirstTimeSetupModal from '$lib/components/modals/first-time-setup/FirstTimeSetupModal.svelte';
@@ -224,6 +225,20 @@
224225
});
225226
});
226227
228+
EventsOn('externalInstallModpack', (modpackReference: string, version: string) => {
229+
if (!modpackReference) return;
230+
modalStore.trigger({
231+
type: 'component',
232+
component: {
233+
ref: ExternalInstallModpack,
234+
props: {
235+
modpackReference,
236+
version,
237+
},
238+
},
239+
});
240+
});
241+
227242
EventsOn('externalImportProfile', async (path: string) => {
228243
if (!path) return;
229244
modalStore.trigger({
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
query GetModpackSummary($modpackID: ModpackID!) {
2+
modpack: getModpack(modpackID: $modpackID) {
3+
name
4+
logo
5+
views
6+
short_description
7+
parent_id
8+
}
9+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<script lang="ts">
2+
import { getContextClient, queryStore } from '@urql/svelte';
3+
4+
import T from '$lib/components/T.svelte';
5+
import { GetModpackSummaryDocument } from '$lib/generated';
6+
import { addQueuedModAction } from '$lib/store/actionQueue';
7+
import { error } from '$lib/store/generalStore';
8+
import { offline } from '$lib/store/settingsStore';
9+
import { InstallModpackRelease } from '$wailsjs/go/ficsitcli/ficsitCLI';
10+
11+
export let parent: { onClose: () => void };
12+
13+
export let modpackReference: string;
14+
export let version: string;
15+
16+
const client = getContextClient();
17+
18+
$: modpackQuery = queryStore(
19+
{
20+
query: GetModpackSummaryDocument,
21+
client,
22+
pause: !!$offline,
23+
variables: {
24+
modpackID: modpackReference,
25+
},
26+
},
27+
);
28+
29+
$: modpack = $modpackQuery.fetching ? null : $modpackQuery.data?.modpack;
30+
31+
function install() {
32+
if (!modpack) return;
33+
const action = async () => (InstallModpackRelease(modpackReference, version, modpack.name)).catch((e) => $error = e);
34+
const actionName = 'install';
35+
addQueuedModAction(
36+
modpackReference,
37+
actionName,
38+
action,
39+
);
40+
parent.onClose();
41+
}
42+
43+
$: renderedLogo = modpack?.logo || 'https://ficsit.app/images/no_image.webp';
44+
</script>
45+
46+
<div style="max-height: calc(100vh - 3rem); max-width: calc(100vw - 3rem);" class="w-[48rem] card flex flex-col gap-2">
47+
<header class="card-header font-bold text-2xl text-center">
48+
<T defaultValue="Install modpack" keyName="external-install-modpack.title" />
49+
</header>
50+
<section class="p-4 overflow-y-auto">
51+
{#if modpack}
52+
<div class="flex">
53+
<div class="grow">
54+
<p>{modpack.name}</p>
55+
{#if version}
56+
<p><T defaultValue={'Version {version}'} keyName="external-install-modpack.version" params={{ version }} /></p>
57+
{:else}
58+
<p><T defaultValue="Latest version" keyName="external-install-modpack.latest-version" /></p>
59+
{/if}
60+
<p>{modpack.short_description}</p>
61+
</div>
62+
<img class="logo h-24 w-24 mx-2" alt="{modpack.name} Logo" src={renderedLogo} />
63+
</div>
64+
{:else if $modpackQuery.fetching}
65+
<p><T defaultValue="Loading..." keyName="common.loading" /></p>
66+
{:else if $modpackQuery.error}
67+
<p><T defaultValue="Error loading modpack details" keyName="external-install-modpack.error-loading" /></p>
68+
{/if}
69+
</section>
70+
<footer class="card-footer">
71+
<button
72+
class="btn text-primary-600 variant-ringed"
73+
on:click={install}>
74+
<T defaultValue="Install" keyName="external-install-modpack.install" />
75+
</button>
76+
<button
77+
class="btn"
78+
on:click={parent.onClose}>
79+
<T defaultValue="Cancel" keyName="common.cancel" />
80+
</button>
81+
</footer>
82+
</div>

0 commit comments

Comments
 (0)