Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/cache/uplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type UPlugin struct {
CreatedBy string `json:"CreatedBy"`
GameVersion string `json:"GameVersion"`
Plugins []Plugins `json:"Plugins"`
GameFeature bool `json:"GameFeature"`
}
type Plugins struct {
Name string `json:"Name"`
Expand Down
175 changes: 131 additions & 44 deletions cli/installations.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package cli

import (
"archive/zip"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/url"
"os"
Expand All @@ -12,6 +14,7 @@ import (
"strings"
"sync"

"github.com/puzpuzpuz/xsync/v3"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -395,6 +398,8 @@ type InstallUpdateItem struct {
Version string
}

var modRoots = []string{"", "GameFeatures"}

func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate) error {
platform, err := i.GetPlatform(ctx)
if err != nil {
Expand All @@ -421,44 +426,9 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate)
return fmt.Errorf("failed creating Mods directory: %w", err)
}

dir, err := d.ReadDir(modsDirectory)
oldModLocations, err := getExistingMods(d, modsDirectory)
if err != nil {
return fmt.Errorf("failed to read mods directory: %w", err)
}

var deleteWait errgroup.Group
for _, entry := range dir {
if entry.IsDir() {
modName := entry.Name()
mod, hasMod := lockfile.Mods[modName]
if hasMod {
_, hasTarget := mod.Targets[platform.TargetName]
hasMod = hasTarget
}
if !hasMod {
modName := entry.Name()
modDir := filepath.Join(modsDirectory, modName)
deleteWait.Go(func() error {
exists, err := d.Exists(filepath.Join(modDir, ".smm"))
if err != nil {
return err
}

if exists {
slog.Info("deleting mod", slog.String("mod_reference", modName))
if err := d.Remove(modDir); err != nil {
return fmt.Errorf("failed to delete mod directory: %w", err)
}
}

return nil
})
}
}
}

if err := deleteWait.Wait(); err != nil {
return fmt.Errorf("failed to remove old mods: %w", err)
return fmt.Errorf("failed to get existing mods: %w", err)
}

slog.Info("starting installation", slog.Int("concurrency", viper.GetInt("concurrent-downloads")), slog.String("path", i.Path))
Expand Down Expand Up @@ -490,6 +460,8 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate)
}()
}

newModLocations := xsync.NewMapOfPresized[string, string](len(lockfile.Mods))

for modReference, version := range lockfile.Mods {
channelUsers.Add(1)
modReference := modReference
Expand All @@ -507,10 +479,11 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate)

// Only install if a link is provided, otherwise assume mod is already installed
if target.Link != "" {
err := downloadAndExtractMod(modReference, version.Version, target.Link, target.Hash, platform.TargetName, modsDirectory, updates, downloadSemaphore, d)
location, err := downloadAndExtractMod(modReference, version.Version, target.Link, target.Hash, platform.TargetName, modsDirectory, updates, downloadSemaphore, d)
if err != nil {
return fmt.Errorf("failed to install %s@%s: %w", modReference, version.Version, err)
}
newModLocations.Store(modReference, location)
}

if modComplete != nil {
Expand All @@ -524,6 +497,38 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate)
return fmt.Errorf("failed to install mods: %w", err)
}

newModLocations.Range(func(mod, location string) bool {
oldLocation, ok := oldModLocations[mod]
if !ok {
return true
}
delete(oldLocation, location)
if len(oldLocation) == 0 {
delete(oldModLocations, mod)
}
return true
})

var deleteWait errgroup.Group
for modName, modLocations := range oldModLocations {
for modLocation := range modLocations {
modDir := filepath.Join(modsDirectory, modLocation)
deleteWait.Go(func() error {
slog.Info("deleting mod", slog.String("mod_reference", modName))

if err := d.Remove(modDir); err != nil {
return fmt.Errorf("failed to delete mod directory: %w", err)
}

return nil
})
}
}

if err := deleteWait.Wait(); err != nil {
return fmt.Errorf("failed to remove old mods: %w", err)
}

if updates != nil {
if i.Vanilla {
updates <- InstallUpdate{
Expand All @@ -546,6 +551,44 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate)
return nil
}

func getExistingMods(d disk.Disk, modsDirectory string) (map[string]map[string]bool, error) {
existingModDirectories := map[string]map[string]bool{}

for _, modRoot := range modRoots {
exists, err := d.Exists(filepath.Join(modsDirectory, modRoot))
if err != nil {
return nil, fmt.Errorf("failed to check if %s exists: %w", modRoot, err)
}
if !exists {
continue
}

dir, err := d.ReadDir(filepath.Join(modsDirectory, modRoot))
if err != nil {
return nil, fmt.Errorf("failed to read %s directory: %w", modRoot, err)
}

for _, entry := range dir {
if entry.IsDir() {
location := filepath.Join(modRoot, entry.Name())
markerPath := filepath.Join(modsDirectory, location, ".smm")
ok, err := d.Exists(markerPath)
if err != nil {
return nil, fmt.Errorf("failed to check if %s exists: %w", markerPath, err)
}
if ok {
if existingModDirectories[entry.Name()] == nil {
existingModDirectories[entry.Name()] = map[string]bool{}
}
existingModDirectories[entry.Name()][location] = true
}
}
}
}

return existingModDirectories, nil
}

func (i *Installation) UpdateMods(ctx *GlobalContext, mods []string) error {
platform, err := i.GetPlatform(ctx)
if err != nil {
Expand Down Expand Up @@ -585,7 +628,7 @@ func (i *Installation) UpdateMods(ctx *GlobalContext, mods []string) error {
return nil
}

func downloadAndExtractMod(modReference string, version string, link string, hash string, target string, modsDirectory string, updates chan<- InstallUpdate, downloadSemaphore chan int, d disk.Disk) error {
func downloadAndExtractMod(modReference string, version string, link string, hash string, target string, modsDirectory string, updates chan<- InstallUpdate, downloadSemaphore chan int, d disk.Disk) (string, error) {
var downloadUpdates chan utils.GenericProgress

var wg sync.WaitGroup
Expand All @@ -612,7 +655,7 @@ func downloadAndExtractMod(modReference string, version string, link string, has
slog.Info("downloading mod", slog.String("mod_reference", modReference), slog.String("version", version), slog.String("link", link))
reader, size, err := cache.DownloadOrCache(modReference+"_"+version+"_"+target+".zip", hash, link, downloadUpdates, downloadSemaphore)
if err != nil {
return fmt.Errorf("failed to download %s from: %s: %w", modReference, link, err)
return "", fmt.Errorf("failed to download %s from: %s: %w", modReference, link, err)
}

defer reader.Close()
Expand All @@ -639,9 +682,19 @@ func downloadAndExtractMod(modReference string, version string, link string, has
}()
}

slog.Info("extracting mod", slog.String("mod_reference", modReference), slog.String("version", version), slog.String("link", link))
if err := utils.ExtractMod(reader, size, filepath.Join(modsDirectory, modReference), hash, extractUpdates, d); err != nil {
return fmt.Errorf("could not extract %s: %w", modReference, err)
zipReader, err := zip.NewReader(reader, size)
if err != nil {
return "", fmt.Errorf("failed to read file as zip: %w", err)
}

location, err := getExtractLocation(zipReader, modReference)
if err != nil {
return "", fmt.Errorf("failed to determine extract location: %w", err)
}

slog.Info("extracting mod", slog.String("mod_reference", modReference), slog.String("version", version), slog.String("link", link), slog.String("location", location))
if err := utils.ExtractMod(zipReader, filepath.Join(modsDirectory, location), hash, extractUpdates, d); err != nil {
return "", fmt.Errorf("could not extract %s: %w", modReference, err)
}

if updates != nil {
Expand All @@ -659,7 +712,41 @@ func downloadAndExtractMod(modReference string, version string, link string, has

wg.Wait()

return nil
return location, nil
}

func getExtractLocation(reader *zip.Reader, modReference string) (string, error) {
var upluginFile *zip.File
Comment thread
mircearoata marked this conversation as resolved.
for _, file := range reader.File {
if strings.HasSuffix(file.Name, ".uplugin") {
upluginFile = file
break
}
}
if upluginFile == nil {
return "", errors.New("no uplugin file found in zip")
}

upluginReader, err := upluginFile.Open()
if err != nil {
return "", fmt.Errorf("failed to open uplugin file: %w", err)
}
defer upluginReader.Close()

var uplugin cache.UPlugin
data, err := io.ReadAll(upluginReader)
if err != nil {
return "", fmt.Errorf("failed to read uplugin file: %w", err)
}
if err := json.Unmarshal(data, &uplugin); err != nil {
return "", fmt.Errorf("failed to unmarshal uplugin file: %w", err)
}

if uplugin.GameFeature {
return filepath.Join("GameFeatures", modReference), nil
}

return modReference, nil
}

func (i *Installation) SetProfile(ctx *GlobalContext, profile string) error {
Expand Down
7 changes: 1 addition & 6 deletions utils/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func SHA256Data(f io.Reader) (string, error) {
return hex.EncodeToString(h.Sum(nil)), nil
}

func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates chan<- GenericProgress, d disk.Disk) error {
func ExtractMod(reader *zip.Reader, location string, hash string, updates chan<- GenericProgress, d disk.Disk) error {
hashFile := filepath.Join(location, ".smm")

exists, err := d.Exists(hashFile)
Expand Down Expand Up @@ -56,11 +56,6 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
}
}

reader, err := zip.NewReader(f, size)
if err != nil {
return fmt.Errorf("failed to read file as zip: %w", err)
}

totalSize := int64(0)

for _, file := range reader.File {
Expand Down
Loading