feat: threaded download pooling (#48)

* feat: threaded download pooling
refactor: splice out resolver

* chore: remove debug
This commit is contained in:
Vilsol 2023-12-16 03:59:58 -08:00 committed by GitHub
parent b6592fe185
commit 4195463c60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 409 additions and 1056 deletions

View file

@ -58,5 +58,4 @@ linters:
- wrapcheck - wrapcheck
- gci - gci
- gocritic - gocritic
- gofumpt
- nonamedreturns - nonamedreturns

125
cli/cache/download.go vendored
View file

@ -6,14 +6,39 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/puzpuzpuz/xsync/v3"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/utils" "github.com/satisfactorymodding/ficsit-cli/utils"
) )
type downloadGroup struct {
err error
wait chan bool
hash string
updates []chan<- utils.GenericProgress
size int64
}
var downloadSync = *xsync.NewMapOf[string, *downloadGroup]()
func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- utils.GenericProgress, downloadSemaphore chan int) (*os.File, int64, error) { func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- utils.GenericProgress, downloadSemaphore chan int) (*os.File, int64, error) {
group, loaded := downloadSync.LoadOrCompute(cacheKey, func() *downloadGroup {
return &downloadGroup{
hash: hash,
updates: make([]chan<- utils.GenericProgress, 0),
wait: make(chan bool),
}
})
_, _ = downloadSync.Compute(cacheKey, func(oldValue *downloadGroup, loaded bool) (*downloadGroup, bool) {
oldValue.updates = append(oldValue.updates, updates)
return oldValue, false
})
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache") downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
if err := os.MkdirAll(downloadCache, 0o777); err != nil { if err := os.MkdirAll(downloadCache, 0o777); err != nil {
if !os.IsExist(err) { if !os.IsExist(err) {
@ -23,6 +48,72 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
location := filepath.Join(downloadCache, cacheKey) location := filepath.Join(downloadCache, cacheKey)
if loaded {
if group.hash != hash {
return nil, 0, errors.New("hash mismatch in download group")
}
<-group.wait
if group.err != nil {
return nil, 0, group.err
}
f, err := os.Open(location)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
}
return f, group.size, nil
}
defer downloadSync.Delete(cacheKey)
upstreamUpdates := make(chan utils.GenericProgress)
defer close(upstreamUpdates)
upstreamWaiter := make(chan bool)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
outer:
for {
select {
case update := <-upstreamUpdates:
for _, u := range group.updates {
u <- update
}
case <-upstreamWaiter:
break outer
}
}
}()
size, err := downloadInternal(cacheKey, location, hash, url, upstreamUpdates, downloadSemaphore)
if err != nil {
group.err = err
close(group.wait)
return nil, 0, err
}
close(upstreamWaiter)
wg.Wait()
group.size = size
close(group.wait)
f, err := os.Open(location)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
}
return f, size, nil
}
func downloadInternal(cacheKey string, location string, hash string, url string, updates chan<- utils.GenericProgress, downloadSemaphore chan int) (int64, error) {
stat, err := os.Stat(location) stat, err := os.Stat(location)
if err == nil { if err == nil {
existingHash := "" existingHash := ""
@ -30,36 +121,31 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
if hash != "" { if hash != "" {
f, err := os.Open(location) f, err := os.Open(location)
if err != nil { if err != nil {
return nil, 0, errors.Wrap(err, "failed to open file: "+location) return 0, errors.Wrap(err, "failed to open file: "+location)
} }
defer f.Close() defer f.Close()
existingHash, err = utils.SHA256Data(f) existingHash, err = utils.SHA256Data(f)
if err != nil { if err != nil {
return nil, 0, errors.Wrap(err, "could not compute hash for file: "+location) return 0, errors.Wrap(err, "could not compute hash for file: "+location)
} }
} }
if hash == existingHash { if hash == existingHash {
f, err := os.Open(location) return stat.Size(), nil
if err != nil {
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
}
return f, stat.Size(), nil
} }
if err := os.Remove(location); err != nil { if err := os.Remove(location); err != nil {
return nil, 0, errors.Wrap(err, "failed to delete file: "+location) return 0, errors.Wrap(err, "failed to delete file: "+location)
} }
} else if !os.IsNotExist(err) { } else if !os.IsNotExist(err) {
return nil, 0, errors.Wrap(err, "failed to stat file: "+location) return 0, errors.Wrap(err, "failed to stat file: "+location)
} }
if updates != nil { if updates != nil {
headResp, err := http.Head(url) headResp, err := http.Head(url)
if err != nil { if err != nil {
return nil, 0, errors.Wrap(err, "failed to head: "+url) return 0, errors.Wrap(err, "failed to head: "+url)
} }
defer headResp.Body.Close() defer headResp.Body.Close()
updates <- utils.GenericProgress{Total: headResp.ContentLength} updates <- utils.GenericProgress{Total: headResp.ContentLength}
@ -72,18 +158,18 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
out, err := os.Create(location) out, err := os.Create(location)
if err != nil { if err != nil {
return nil, 0, errors.Wrap(err, "failed creating file at: "+location) return 0, errors.Wrap(err, "failed creating file at: "+location)
} }
defer out.Close() defer out.Close()
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return nil, 0, errors.Wrap(err, "failed to fetch: "+url) return 0, errors.Wrap(err, "failed to fetch: "+url)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, 0, fmt.Errorf("bad status: %s on url: %s", resp.Status, url) return 0, fmt.Errorf("bad status: %s on url: %s", resp.Status, url)
} }
progresser := &utils.Progresser{ progresser := &utils.Progresser{
@ -94,12 +180,7 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
_, err = io.Copy(out, progresser) _, err = io.Copy(out, progresser)
if err != nil { if err != nil {
return nil, 0, errors.Wrap(err, "failed writing file to disk") return 0, errors.Wrap(err, "failed writing file to disk")
}
f, err := os.Open(location)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
} }
if updates != nil { if updates != nil {
@ -108,8 +189,8 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
_, err = addFileToCache(cacheKey) _, err = addFileToCache(cacheKey)
if err != nil { if err != nil {
return nil, 0, errors.Wrap(err, "failed to add file to cache") return 0, errors.Wrap(err, "failed to add file to cache")
} }
return f, resp.ContentLength, nil return resp.ContentLength, nil
} }

View file

@ -26,7 +26,7 @@ func InitCLI(apiOnly bool) (*GlobalContext, error) {
apiClient := ficsit.InitAPI() apiClient := ficsit.InitAPI()
mixedProvider := provider.InitMixedProvider(apiClient) mixedProvider := provider.InitMixedProvider(provider.NewFicsitProvider(apiClient), provider.NewLocalProvider())
if viper.GetBool("offline") { if viper.GetBool("offline") {
mixedProvider.Offline = true mixedProvider.Offline = true

View file

@ -1,209 +0,0 @@
package cli
import (
"context"
"fmt"
"slices"
"github.com/mircearoata/pubgrub-go/pubgrub"
"github.com/mircearoata/pubgrub-go/pubgrub/helpers"
"github.com/mircearoata/pubgrub-go/pubgrub/semver"
"github.com/pkg/errors"
"github.com/puzpuzpuz/xsync/v3"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)
const (
rootPkg = "$$root$$"
smlPkg = "SML"
factoryGamePkg = "FactoryGame"
)
type DependencyResolver struct {
provider provider.Provider
}
func NewDependencyResolver(provider provider.Provider) DependencyResolver {
return DependencyResolver{provider}
}
type ficsitAPISource struct {
provider provider.Provider
lockfile *LockFile
toInstall map[string]semver.Constraint
modVersionInfo *xsync.MapOf[string, ficsit.AllVersionsResponse]
gameVersion semver.Version
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
}
func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersion, error) {
if pkg == rootPkg {
return []pubgrub.PackageVersion{{Version: semver.Version{}, Dependencies: f.toInstall}}, nil
}
if pkg == factoryGamePkg {
return []pubgrub.PackageVersion{{Version: f.gameVersion}}, nil
}
if pkg == smlPkg {
versions := make([]pubgrub.PackageVersion, len(f.smlVersions))
for i, smlVersion := range f.smlVersions {
v, err := semver.NewVersion(smlVersion.Version)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse version %s", smlVersion.Version)
}
gameConstraint, err := semver.NewConstraint(fmt.Sprintf(">=%d", smlVersion.Satisfactory_version))
if err != nil {
return nil, errors.Wrapf(err, "failed to parse constraint %s", fmt.Sprintf(">=%d", smlVersion.Satisfactory_version))
}
versions[i] = pubgrub.PackageVersion{
Version: v,
Dependencies: map[string]semver.Constraint{
factoryGamePkg: gameConstraint,
},
}
}
return versions, nil
}
response, err := f.provider.ModVersionsWithDependencies(context.TODO(), pkg)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch mod %s", pkg)
}
if !response.Success {
if response.Error != nil {
return nil, errors.Errorf("mod %s not found: %s", pkg, response.Error.Message)
}
return nil, errors.Errorf("mod %s not found", pkg)
}
f.modVersionInfo.Store(pkg, *response)
versions := make([]pubgrub.PackageVersion, len(response.Data))
for i, modVersion := range response.Data {
v, err := semver.NewVersion(modVersion.Version)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse version %s", modVersion.Version)
}
dependencies := make(map[string]semver.Constraint)
optionalDependencies := make(map[string]semver.Constraint)
for _, dependency := range modVersion.Dependencies {
c, err := semver.NewConstraint(dependency.Condition)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse constraint %s", dependency.Condition)
}
if dependency.Optional {
optionalDependencies[dependency.ModID] = c
} else {
dependencies[dependency.ModID] = c
}
}
versions[i] = pubgrub.PackageVersion{
Version: v,
Dependencies: dependencies,
OptionalDependencies: optionalDependencies,
}
}
return versions, nil
}
func (f *ficsitAPISource) PickVersion(pkg string, versions []semver.Version) semver.Version {
if f.lockfile != nil {
if existing, ok := f.lockfile.Mods[pkg]; ok {
v, err := semver.NewVersion(existing.Version)
if err == nil {
if slices.ContainsFunc(versions, func(version semver.Version) bool {
return v.Compare(version) == 0
}) {
return v
}
}
}
}
return helpers.StandardVersionPriority(versions)
}
func (d DependencyResolver) ResolveModDependencies(constraints map[string]string, lockFile *LockFile, gameVersion int) (*LockFile, error) {
smlVersionsDB, err := d.provider.SMLVersions(context.TODO())
if err != nil {
return nil, errors.Wrap(err, "failed fetching SML versions")
}
gameVersionSemver, err := semver.NewVersion(fmt.Sprintf("%d", gameVersion))
if err != nil {
return nil, errors.Wrap(err, "failed parsing game version")
}
toInstall := make(map[string]semver.Constraint, len(constraints))
for k, v := range constraints {
c, err := semver.NewConstraint(v)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse constraint %s", v)
}
toInstall[k] = c
}
ficsitSource := &ficsitAPISource{
provider: d.provider,
smlVersions: smlVersionsDB.SmlVersions.Sml_versions,
gameVersion: gameVersionSemver,
lockfile: lockFile,
toInstall: toInstall,
modVersionInfo: xsync.NewMapOf[string, ficsit.AllVersionsResponse](),
}
result, err := pubgrub.Solve(helpers.NewCachingSource(ficsitSource), rootPkg)
if err != nil {
finalError := err
var solverErr pubgrub.SolvingError
if errors.As(err, &solverErr) {
finalError = DependencyResolverError{SolvingError: solverErr, provider: d.provider, smlVersions: smlVersionsDB.SmlVersions.Sml_versions, gameVersion: gameVersion}
}
return nil, errors.Wrap(finalError, "failed to solve dependencies")
}
delete(result, rootPkg)
delete(result, factoryGamePkg)
outputLock := MakeLockfile()
for k, v := range result {
if k == smlPkg {
for _, version := range ficsitSource.smlVersions {
if version.Version == v.String() {
targets := make(map[string]LockedModTarget)
for _, target := range version.Targets {
targets[string(target.TargetName)] = LockedModTarget{
Link: target.Link,
}
}
outputLock.Mods[k] = LockedMod{
Version: v.String(),
Targets: targets,
}
break
}
}
continue
}
value, _ := ficsitSource.modVersionInfo.Load(k)
versions := value.Data
for _, ver := range versions {
if ver.Version == v.RawString() {
targets := make(map[string]LockedModTarget)
for _, target := range ver.Targets {
targets[target.TargetName] = LockedModTarget{
Link: viper.GetString("api-base") + "/v1/version/" + ver.ID + "/" + target.TargetName + "/download",
Hash: target.Hash,
}
}
outputLock.Mods[k] = LockedMod{
Version: v.String(),
Targets: targets,
}
break
}
}
}
return outputLock, nil
}

View file

@ -1,134 +0,0 @@
package cli
import (
"context"
"fmt"
"strings"
"github.com/mircearoata/pubgrub-go/pubgrub"
"github.com/mircearoata/pubgrub-go/pubgrub/semver"
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)
type DependencyResolverError struct {
pubgrub.SolvingError
provider provider.Provider
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
gameVersion int
}
func (e DependencyResolverError) Error() string {
rootPkg := e.Cause().Terms()[0].Dependency()
writer := pubgrub.NewStandardErrorWriter(rootPkg).
WithIncompatibilityStringer(
MakeDependencyResolverErrorStringer(e.provider, e.smlVersions, e.gameVersion),
)
e.WriteTo(writer)
return writer.String()
}
type DependencyResolverErrorStringer struct {
pubgrub.StandardIncompatibilityStringer
provider provider.Provider
packageNames map[string]string
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
gameVersion int
}
func MakeDependencyResolverErrorStringer(provider provider.Provider, smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion, gameVersion int) *DependencyResolverErrorStringer {
s := &DependencyResolverErrorStringer{
provider: provider,
smlVersions: smlVersions,
gameVersion: gameVersion,
packageNames: map[string]string{},
}
s.StandardIncompatibilityStringer = pubgrub.NewStandardIncompatibilityStringer().WithTermStringer(s)
return s
}
func (w *DependencyResolverErrorStringer) getPackageName(pkg string) string {
if pkg == smlPkg {
return "SML"
}
if pkg == factoryGamePkg {
return "Satisfactory"
}
if name, ok := w.packageNames[pkg]; ok {
return name
}
result, err := w.provider.GetModName(context.TODO(), pkg)
if err != nil {
return pkg
}
w.packageNames[pkg] = result.Mod.Name
return result.Mod.Name
}
func (w *DependencyResolverErrorStringer) Term(t pubgrub.Term, includeVersion bool) string {
name := w.getPackageName(t.Dependency())
fullName := fmt.Sprintf("%s (%s)", name, t.Dependency())
if name == t.Dependency() {
fullName = t.Dependency()
}
if includeVersion {
if t.Constraint().IsAny() {
return fmt.Sprintf("every version of %s", fullName)
}
switch t.Dependency() {
case factoryGamePkg:
// Remove ".0.0" from the versions mentioned, since only the major is ever used
return fmt.Sprintf("%s \"%s\"", fullName, strings.ReplaceAll(t.Constraint().String(), ".0.0", ""))
case smlPkg:
var matched []semver.Version
for _, v := range w.smlVersions {
ver, err := semver.NewVersion(v.Version)
if err != nil {
// Assume it is contained in the constraint
matched = append(matched, semver.Version{})
continue
}
if t.Constraint().Contains(ver) {
matched = append(matched, ver)
}
}
if len(matched) == 1 {
return fmt.Sprintf("%s \"%s\"", fullName, matched[0])
}
return fmt.Sprintf("%s \"%s\"", fullName, t.Constraint())
default:
res, err := w.provider.ModVersions(context.TODO(), t.Dependency(), ficsit.VersionFilter{
Limit: 100,
})
if err != nil {
return fmt.Sprintf("%s \"%s\"", fullName, t.Constraint())
}
var matched []semver.Version
for _, v := range res.Mod.Versions {
ver, err := semver.NewVersion(v.Version)
if err != nil {
// Assume it is contained in the constraint
matched = append(matched, semver.Version{})
continue
}
if t.Constraint().Contains(ver) {
matched = append(matched, ver)
}
}
if len(matched) == 1 {
return fmt.Sprintf("%s \"%s\"", fullName, matched[0])
}
return fmt.Sprintf("%s \"%s\"", fullName, t.Constraint())
}
}
return fullName
}
func (w *DependencyResolverErrorStringer) IncompatibilityString(incompatibility *pubgrub.Incompatibility, rootPkg string) string {
terms := incompatibility.Terms()
if len(terms) == 1 && terms[0].Dependency() == factoryGamePkg {
return fmt.Sprintf("Satisfactory CL%d is installed", w.gameVersion)
}
return w.StandardIncompatibilityStringer.IncompatibilityString(incompatibility, rootPkg)
}

View file

@ -12,6 +12,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper" "github.com/spf13/viper"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@ -257,7 +258,7 @@ func (i *Installation) LockFilePath(ctx *GlobalContext) (string, error) {
return filepath.Join(i.BasePath(), platform.LockfilePath, lockFileName), nil return filepath.Join(i.BasePath(), platform.LockfilePath, lockFileName), nil
} }
func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) { func (i *Installation) LockFile(ctx *GlobalContext) (*resolver.LockFile, error) {
lockfilePath, err := i.LockFilePath(ctx) lockfilePath, err := i.LockFilePath(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -268,7 +269,7 @@ func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) {
return nil, err return nil, err
} }
var lockFile *LockFile var lockFile *resolver.LockFile
lockFileJSON, err := d.Read(lockfilePath) lockFileJSON, err := d.Read(lockfilePath)
if err != nil { if err != nil {
if !d.IsNotExist(err) { if !d.IsNotExist(err) {
@ -283,7 +284,7 @@ func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) {
return lockFile, nil return lockFile, nil
} }
func (i *Installation) WriteLockFile(ctx *GlobalContext, lockfile *LockFile) error { func (i *Installation) WriteLockFile(ctx *GlobalContext, lockfile *resolver.LockFile) error {
lockfilePath, err := i.LockFilePath(ctx) lockfilePath, err := i.LockFilePath(ctx)
if err != nil { if err != nil {
return err return err
@ -327,20 +328,20 @@ func (i *Installation) Wipe() error {
return nil return nil
} }
func (i *Installation) ResolveProfile(ctx *GlobalContext) (*LockFile, error) { func (i *Installation) ResolveProfile(ctx *GlobalContext) (*resolver.LockFile, error) {
lockFile, err := i.LockFile(ctx) lockFile, err := i.LockFile(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
resolver := NewDependencyResolver(ctx.Provider) depResolver := resolver.NewDependencyResolver(ctx.Provider, viper.GetString("api-base"))
gameVersion, err := i.GetGameVersion(ctx) gameVersion, err := i.GetGameVersion(ctx)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to detect game version") return nil, errors.Wrap(err, "failed to detect game version")
} }
lockfile, err := ctx.Profiles.Profiles[i.Profile].Resolve(resolver, lockFile, gameVersion) lockfile, err := ctx.Profiles.Profiles[i.Profile].Resolve(depResolver, lockFile, gameVersion)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not resolve mods") return nil, errors.Wrap(err, "could not resolve mods")
} }
@ -382,7 +383,7 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate)
return errors.Wrap(err, "failed to detect platform") return errors.Wrap(err, "failed to detect platform")
} }
lockfile := MakeLockfile() lockfile := resolver.NewLockfile()
if !i.Vanilla { if !i.Vanilla {
var err error var err error
@ -502,7 +503,7 @@ func (i *Installation) UpdateMods(ctx *GlobalContext, mods []string) error {
return errors.Wrap(err, "failed to read lock file") return errors.Wrap(err, "failed to read lock file")
} }
resolver := NewDependencyResolver(ctx.Provider) resolver := resolver.NewDependencyResolver(ctx.Provider, viper.GetString("api-base"))
gameVersion, err := i.GetGameVersion(ctx) gameVersion, err := i.GetGameVersion(ctx)
if err != nil { if err != nil {

View file

@ -34,8 +34,8 @@ func TestAddInstallation(t *testing.T) {
profileName := "InstallationTest" profileName := "InstallationTest"
profile, err := ctx.Profiles.AddProfile(profileName) profile, err := ctx.Profiles.AddProfile(profileName)
testza.AssertNoError(t, err) testza.AssertNoError(t, err)
testza.AssertNoError(t, profile.AddMod("AreaActions", ">=1.6.5")) testza.AssertNoError(t, profile.AddMod("AreaActions", "1.6.5"))
testza.AssertNoError(t, profile.AddMod("RefinedPower", ">=3.2.10")) testza.AssertNoError(t, profile.AddMod("RefinedPower", "3.2.10"))
serverLocation := os.Getenv("SF_DEDICATED_SERVER") serverLocation := os.Getenv("SF_DEDICATED_SERVER")
if serverLocation != "" { if serverLocation != "" {

View file

@ -1,54 +0,0 @@
package cli
type LockfileVersion int
const (
InitialLockfileVersion = LockfileVersion(iota)
ModTargetsLockfileVersion
// Always last
nextLockfileVersion
CurrentLockfileVersion = nextLockfileVersion - 1
)
type LockFile struct {
Mods map[string]LockedMod `json:"mods"`
Version LockfileVersion `json:"version"`
}
type LockedMod struct {
Dependencies map[string]string `json:"dependencies"`
Targets map[string]LockedModTarget `json:"targets"`
Version string `json:"version"`
}
type LockedModTarget struct {
Hash string `json:"hash"`
Link string `json:"link"`
}
func MakeLockfile() *LockFile {
return &LockFile{
Mods: make(map[string]LockedMod),
Version: CurrentLockfileVersion,
}
}
func (l *LockFile) Clone() *LockFile {
lockFile := &LockFile{
Mods: make(map[string]LockedMod),
Version: l.Version,
}
for k, v := range l.Mods {
lockFile.Mods[k] = v
}
return lockFile
}
func (l *LockFile) Remove(modID ...string) *LockFile {
for _, s := range modID {
delete(l.Mods, s)
}
return l
}

View file

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
@ -9,6 +10,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/utils" "github.com/satisfactorymodding/ficsit-cli/utils"
@ -45,6 +47,7 @@ type Profiles struct {
type Profile struct { type Profile struct {
Mods map[string]ProfileMod `json:"mods"` Mods map[string]ProfileMod `json:"mods"`
Name string `json:"name"` Name string `json:"name"`
RequiredTargets []resolver.TargetName `json:"required_targets"`
} }
type ProfileMod struct { type ProfileMod struct {
@ -288,7 +291,7 @@ func (p *Profile) HasMod(reference string) bool {
// An optional lockfile can be passed if one exists. // An optional lockfile can be passed if one exists.
// //
// Returns an error if resolution is impossible. // Returns an error if resolution is impossible.
func (p *Profile) Resolve(resolver DependencyResolver, lockFile *LockFile, gameVersion int) (*LockFile, error) { func (p *Profile) Resolve(resolver resolver.DependencyResolver, lockFile *resolver.LockFile, gameVersion int) (*resolver.LockFile, error) {
toResolve := make(map[string]string) toResolve := make(map[string]string)
for modReference, mod := range p.Mods { for modReference, mod := range p.Mods {
if mod.Enabled { if mod.Enabled {
@ -296,7 +299,7 @@ func (p *Profile) Resolve(resolver DependencyResolver, lockFile *LockFile, gameV
} }
} }
resultLockfile, err := resolver.ResolveModDependencies(toResolve, lockFile, gameVersion) resultLockfile, err := resolver.ResolveModDependencies(context.TODO(), toResolve, lockFile, gameVersion, p.RequiredTargets)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed resolving profile dependencies") return nil, errors.Wrap(err, "failed resolving profile dependencies")
} }

View file

@ -4,40 +4,117 @@ import (
"context" "context"
"github.com/Khan/genqlient/graphql" "github.com/Khan/genqlient/graphql"
"github.com/pkg/errors"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/ficsit"
) )
type ficsitProvider struct { type FicsitProvider struct {
client graphql.Client client graphql.Client
} }
func initFicsitProvider(client graphql.Client) ficsitProvider { func NewFicsitProvider(client graphql.Client) FicsitProvider {
return ficsitProvider{ return FicsitProvider{
client, client,
} }
} }
func (p ficsitProvider) Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) { func (p FicsitProvider) Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) {
return ficsit.Mods(context, p.client, filter) return ficsit.Mods(context, p.client, filter)
} }
func (p ficsitProvider) GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error) { func (p FicsitProvider) GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error) {
return ficsit.GetMod(context, p.client, modReference) return ficsit.GetMod(context, p.client, modReference)
} }
func (p ficsitProvider) ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) { func (p FicsitProvider) ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) {
return ficsit.ModVersions(context, p.client, modReference, filter) return ficsit.ModVersions(context, p.client, modReference, filter)
} }
func (p ficsitProvider) SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error) { func (p FicsitProvider) SMLVersions(context context.Context) ([]resolver.SMLVersion, error) {
return ficsit.SMLVersions(context, p.client) response, err := ficsit.SMLVersions(context, p.client)
if err != nil {
return nil, err
} }
func (p ficsitProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) { smlVersions := make([]resolver.SMLVersion, len(response.SmlVersions.Sml_versions))
return ficsit.GetAllModVersions(modID) for i, version := range response.GetSmlVersions().Sml_versions {
targets := make([]resolver.SMLVersionTarget, len(version.Targets))
for j, target := range version.Targets {
targets[j] = resolver.SMLVersionTarget{
TargetName: resolver.TargetName(target.TargetName),
Link: target.Link,
}
} }
func (p ficsitProvider) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error) { smlVersions[i] = resolver.SMLVersion{
return ficsit.GetModName(context, p.client, modReference) ID: version.Id,
Version: version.Version,
SatisfactoryVersion: version.Satisfactory_version,
Targets: targets,
}
}
return smlVersions, nil
}
func (p FicsitProvider) ModVersionsWithDependencies(_ context.Context, modID string) ([]resolver.ModVersion, error) {
response, err := ficsit.GetAllModVersions(modID)
if err != nil {
return nil, err
}
if response.Error != nil {
return nil, errors.New(response.Error.Message)
}
modVersions := make([]resolver.ModVersion, len(response.Data))
for i, modVersion := range response.Data {
dependencies := make([]resolver.Dependency, len(modVersion.Dependencies))
for j, dependency := range modVersion.Dependencies {
dependencies[j] = resolver.Dependency{
ModID: dependency.ModID,
Condition: dependency.Condition,
Optional: dependency.Optional,
}
}
targets := make([]resolver.Target, len(modVersion.Targets))
for j, target := range modVersion.Targets {
targets[j] = resolver.Target{
VersionID: target.VersionID,
TargetName: resolver.TargetName(target.TargetName),
Hash: target.Hash,
Size: target.Size,
}
}
modVersions[i] = resolver.ModVersion{
ID: modVersion.ID,
Version: modVersion.Version,
Dependencies: dependencies,
Targets: targets,
}
}
return modVersions, err
}
func (p FicsitProvider) GetModName(context context.Context, modReference string) (*resolver.ModName, error) {
response, err := ficsit.GetModName(context, p.client, modReference)
if err != nil {
return nil, err
}
return &resolver.ModName{
ID: response.Mod.Id,
ModReference: response.Mod.Mod_reference,
Name: response.Mod.Name,
}, nil
}
func (p FicsitProvider) IsOffline() bool {
return false
} }

View file

@ -6,18 +6,19 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/cli/cache" "github.com/satisfactorymodding/ficsit-cli/cli/cache"
"github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/ficsit"
) )
type localProvider struct{} type LocalProvider struct{}
func initLocalProvider() localProvider { func NewLocalProvider() LocalProvider {
return localProvider{} return LocalProvider{}
} }
func (p localProvider) Mods(_ context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) { func (p LocalProvider) Mods(_ context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) {
cachedMods, err := cache.GetCache() cachedMods, err := cache.GetCache()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get cache") return nil, errors.Wrap(err, "failed to get cache")
@ -89,7 +90,7 @@ func (p localProvider) Mods(_ context.Context, filter ficsit.ModFilter) (*ficsit
}, nil }, nil
} }
func (p localProvider) GetMod(_ context.Context, modReference string) (*ficsit.GetModResponse, error) { func (p LocalProvider) GetMod(_ context.Context, modReference string) (*ficsit.GetModResponse, error) {
cachedModFiles, err := cache.GetCacheMod(modReference) cachedModFiles, err := cache.GetCacheMod(modReference)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get cache") return nil, errors.Wrap(err, "failed to get cache")
@ -125,79 +126,44 @@ func (p localProvider) GetMod(_ context.Context, modReference string) (*ficsit.G
}, nil }, nil
} }
func (p localProvider) ModVersions(_ context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) { func (p LocalProvider) SMLVersions(_ context.Context) ([]resolver.SMLVersion, error) {
cachedModFiles, err := cache.GetCacheMod(modReference)
if err != nil {
return nil, errors.Wrap(err, "failed to get cache")
}
if filter.Limit == 0 {
filter.Limit = 25
}
versions := make([]ficsit.ModVersionsModVersionsVersion, 0)
for _, modFile := range cachedModFiles[filter.Offset : filter.Offset+filter.Limit] {
versions = append(versions, ficsit.ModVersionsModVersionsVersion{
Id: modReference + ":" + modFile.Plugin.SemVersion,
Version: modFile.Plugin.SemVersion,
})
}
return &ficsit.ModVersionsResponse{
Mod: ficsit.ModVersionsMod{
Id: modReference,
Versions: versions,
},
}, nil
}
func (p localProvider) SMLVersions(_ context.Context) (*ficsit.SMLVersionsResponse, error) {
cachedSMLFiles, err := cache.GetCacheMod("SML") cachedSMLFiles, err := cache.GetCacheMod("SML")
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get cache") return nil, errors.Wrap(err, "failed to get cache")
} }
smlVersions := make([]ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion, 0) smlVersions := make([]resolver.SMLVersion, 0)
for _, smlFile := range cachedSMLFiles { for _, smlFile := range cachedSMLFiles {
smlVersions = append(smlVersions, ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion{ smlVersions = append(smlVersions, resolver.SMLVersion{
Id: "SML:" + smlFile.Plugin.SemVersion, ID: "SML:" + smlFile.Plugin.SemVersion,
Version: smlFile.Plugin.SemVersion, Version: smlFile.Plugin.SemVersion,
Satisfactory_version: 0, // TODO: where can this be obtained from? SatisfactoryVersion: 0, // TODO: where can this be obtained from?
}) })
} }
return &ficsit.SMLVersionsResponse{ return smlVersions, nil
SmlVersions: ficsit.SMLVersionsSmlVersionsGetSMLVersions{
Count: len(smlVersions),
Sml_versions: smlVersions,
},
}, nil
} }
func (p localProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) { func (p LocalProvider) ModVersionsWithDependencies(_ context.Context, modID string) ([]resolver.ModVersion, error) {
cachedModFiles, err := cache.GetCacheMod(modID) cachedModFiles, err := cache.GetCacheMod(modID)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get cache") return nil, errors.Wrap(err, "failed to get cache")
} }
versions := make([]ficsit.ModVersion, 0) versions := make([]resolver.ModVersion, 0)
for _, modFile := range cachedModFiles { for _, modFile := range cachedModFiles {
versions = append(versions, ficsit.ModVersion{ versions = append(versions, resolver.ModVersion{
ID: modID + ":" + modFile.Plugin.SemVersion, ID: modID + ":" + modFile.Plugin.SemVersion,
Version: modFile.Plugin.SemVersion, Version: modFile.Plugin.SemVersion,
}) })
} }
return &ficsit.AllVersionsResponse{ return versions, nil
Success: true,
Data: versions,
}, nil
} }
func (p localProvider) GetModName(_ context.Context, modReference string) (*ficsit.GetModNameResponse, error) { func (p LocalProvider) GetModName(_ context.Context, modReference string) (*resolver.ModName, error) {
cachedModFiles, err := cache.GetCacheMod(modReference) cachedModFiles, err := cache.GetCacheMod(modReference)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get cache") return nil, errors.Wrap(err, "failed to get cache")
@ -207,11 +173,13 @@ func (p localProvider) GetModName(_ context.Context, modReference string) (*fics
return nil, errors.New("mod not found") return nil, errors.New("mod not found")
} }
return &ficsit.GetModNameResponse{ return &resolver.ModName{
Mod: ficsit.GetModNameMod{ ID: modReference,
Id: modReference,
Name: cachedModFiles[0].Plugin.FriendlyName, Name: cachedModFiles[0].Plugin.FriendlyName,
Mod_reference: modReference, ModReference: modReference,
},
}, nil }, nil
} }
func (p LocalProvider) IsOffline() bool {
return true
}

View file

@ -3,65 +3,58 @@ package provider
import ( import (
"context" "context"
"github.com/Khan/genqlient/graphql" resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/ficsit"
) )
type MixedProvider struct { type MixedProvider struct {
ficsitProvider ficsitProvider onlineProvider Provider
localProvider localProvider offlineProvider Provider
Offline bool Offline bool
} }
func InitMixedProvider(client graphql.Client) *MixedProvider { func InitMixedProvider(onlineProvider Provider, offlineProvider Provider) *MixedProvider {
return &MixedProvider{ return &MixedProvider{
ficsitProvider: initFicsitProvider(client), onlineProvider: onlineProvider,
localProvider: initLocalProvider(), offlineProvider: offlineProvider,
Offline: false, Offline: false,
} }
} }
func (p MixedProvider) Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) { func (p MixedProvider) Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) {
if p.Offline { if p.Offline {
return p.localProvider.Mods(context, filter) return p.offlineProvider.Mods(context, filter)
} }
return p.ficsitProvider.Mods(context, filter) return p.onlineProvider.Mods(context, filter)
} }
func (p MixedProvider) GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error) { func (p MixedProvider) GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error) {
if p.Offline { if p.Offline {
return p.localProvider.GetMod(context, modReference) return p.offlineProvider.GetMod(context, modReference)
} }
return p.ficsitProvider.GetMod(context, modReference) return p.onlineProvider.GetMod(context, modReference)
} }
func (p MixedProvider) ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) { func (p MixedProvider) SMLVersions(context context.Context) ([]resolver.SMLVersion, error) {
if p.Offline { if p.Offline {
return p.localProvider.ModVersions(context, modReference, filter) return p.offlineProvider.SMLVersions(context) // nolint
} }
return p.ficsitProvider.ModVersions(context, modReference, filter) return p.onlineProvider.SMLVersions(context) // nolint
} }
func (p MixedProvider) SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error) { func (p MixedProvider) ModVersionsWithDependencies(context context.Context, modID string) ([]resolver.ModVersion, error) {
if p.Offline { if p.Offline {
return p.localProvider.SMLVersions(context) return p.offlineProvider.ModVersionsWithDependencies(context, modID) // nolint
} }
return p.ficsitProvider.SMLVersions(context) return p.onlineProvider.ModVersionsWithDependencies(context, modID) // nolint
} }
func (p MixedProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.AllVersionsResponse, error) { func (p MixedProvider) GetModName(context context.Context, modReference string) (*resolver.ModName, error) {
if p.Offline { if p.Offline {
return p.localProvider.ModVersionsWithDependencies(context, modID) return p.offlineProvider.GetModName(context, modReference) // nolint
} }
return p.ficsitProvider.ModVersionsWithDependencies(context, modID) return p.onlineProvider.GetModName(context, modReference) // nolint
}
func (p MixedProvider) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error) {
if p.Offline {
return p.localProvider.GetModName(context, modReference)
}
return p.ficsitProvider.GetModName(context, modReference)
} }
func (p MixedProvider) IsOffline() bool { func (p MixedProvider) IsOffline() bool {

View file

@ -3,15 +3,14 @@ package provider
import ( import (
"context" "context"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/ficsit"
) )
type Provider interface { type Provider interface {
resolver.Provider
Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error)
GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error) GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error)
ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error)
SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error)
ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.AllVersionsResponse, error)
GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error)
IsOffline() bool IsOffline() bool
} }

View file

@ -1,12 +1,15 @@
package cli package cli
import ( import (
"context"
"math" "math"
"os" "os"
"testing" "testing"
"github.com/MarvinJWendt/testza" "github.com/MarvinJWendt/testza"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cfg" "github.com/satisfactorymodding/ficsit-cli/cfg"
) )
@ -15,10 +18,6 @@ func init() {
cfg.SetDefaults() cfg.SetDefaults()
} }
func profilesGetResolver() DependencyResolver {
return NewDependencyResolver(MockProvider{})
}
func installWatcher() chan<- InstallUpdate { func installWatcher() chan<- InstallUpdate {
c := make(chan InstallUpdate) c := make(chan InstallUpdate)
go func() { go func() {
@ -35,60 +34,6 @@ func installWatcher() chan<- InstallUpdate {
return c return c
} }
func TestProfileResolution(t *testing.T) {
resolver := profilesGetResolver()
resolved, err := (&Profile{
Name: DefaultProfileName,
Mods: map[string]ProfileMod{
"RefinedPower": {
Version: "3.2.10",
Enabled: true,
},
},
}).Resolve(resolver, nil, math.MaxInt)
testza.AssertNoError(t, err)
testza.AssertNotNil(t, resolved)
testza.AssertLen(t, resolved.Mods, 4)
}
func TestProfileRequiredOlderVersion(t *testing.T) {
resolver := profilesGetResolver()
_, err := (&Profile{
Name: DefaultProfileName,
Mods: map[string]ProfileMod{
"RefinedPower": {
Version: "3.2.11",
Enabled: true,
},
"RefinedRDLib": {
Version: "1.1.5",
Enabled: true,
},
},
}).Resolve(resolver, nil, math.MaxInt)
testza.AssertEqual(t, "failed resolving profile dependencies: failed to solve dependencies: Because installing Refined Power (RefinedPower) \"3.2.11\" and Refined Power (RefinedPower) \"3.2.11\" depends on RefinedRDLib \"^1.1.6\", installing RefinedRDLib \"^1.1.6\".\nSo, because installing RefinedRDLib \"1.1.5\", version solving failed.", err.Error())
}
func TestResolutionNonExistentMod(t *testing.T) {
resolver := profilesGetResolver()
_, err := (&Profile{
Name: DefaultProfileName,
Mods: map[string]ProfileMod{
"ThisModDoesNotExist$$$": {
Version: ">0.0.0",
Enabled: true,
},
},
}).Resolve(resolver, nil, math.MaxInt)
testza.AssertEqual(t, "failed resolving profile dependencies: failed to solve dependencies: failed to make decision: failed to get package versions: mod ThisModDoesNotExist$$$ not found: mod not found", err.Error())
}
func TestUpdateMods(t *testing.T) { func TestUpdateMods(t *testing.T) {
ctx, err := InitCLI(false) ctx, err := InitCLI(false)
testza.AssertNoError(t, err) testza.AssertNoError(t, err)
@ -98,17 +43,11 @@ func TestUpdateMods(t *testing.T) {
ctx.Provider = MockProvider{} ctx.Provider = MockProvider{}
resolver := NewDependencyResolver(ctx.Provider) depResolver := resolver.NewDependencyResolver(ctx.Provider, viper.GetString("api-base"))
oldLockfile, err := (&Profile{ oldLockfile, err := depResolver.ResolveModDependencies(context.Background(), map[string]string{
Name: DefaultProfileName, "FicsitRemoteMonitoring": "0.9.8",
Mods: map[string]ProfileMod{ }, nil, math.MaxInt, nil)
"FicsitRemoteMonitoring": {
Version: "0.9.8",
Enabled: true,
},
},
}).Resolve(resolver, nil, math.MaxInt)
testza.AssertNoError(t, err) testza.AssertNoError(t, err)
testza.AssertNotNil(t, oldLockfile) testza.AssertNotNil(t, oldLockfile)

View file

@ -4,13 +4,17 @@ import (
"context" "context"
"time" "time"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/cli/provider" "github.com/satisfactorymodding/ficsit-cli/cli/provider"
"github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/ficsit"
) )
var _ provider.Provider = (*MockProvider)(nil) var _ provider.Provider = (*MockProvider)(nil)
type MockProvider struct{} type MockProvider struct {
resolver.MockProvider
}
func (m MockProvider) Mods(_ context.Context, f ficsit.ModFilter) (*ficsit.ModsResponse, error) { func (m MockProvider) Mods(_ context.Context, f ficsit.ModFilter) (*ficsit.ModsResponse, error) {
if f.Offset > 0 { if f.Offset > 0 {
@ -66,200 +70,29 @@ func (m MockProvider) Mods(_ context.Context, f ficsit.ModFilter) (*ficsit.ModsR
}, nil }, nil
} }
func (m MockProvider) GetMod(_ context.Context, _ string) (*ficsit.GetModResponse, error) { var commonTargets = []resolver.Target{
// Currently used only by TUI
return nil, nil
}
func (m MockProvider) ModVersions(_ context.Context, modReference string, _ ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) {
switch modReference {
//nolint
case "RefinedPower":
return &ficsit.ModVersionsResponse{Mod: ficsit.ModVersionsMod{
Id: "DGiLzB3ZErWu2V",
Versions: []ficsit.ModVersionsModVersionsVersion{
{Id: "Eqgr4VcB8y1z9a", Version: "3.2.13"},
{Id: "BwVKMJNP8doDLg", Version: "3.2.11"},
{Id: "4XTjMpqFngbu9r", Version: "3.2.10"},
},
}}, nil
//nolint
case "RefinedRDLib":
return &ficsit.ModVersionsResponse{Mod: ficsit.ModVersionsMod{
Id: "B24emzbs6xVZQr",
Versions: []ficsit.ModVersionsModVersionsVersion{
{Id: "2XcE6RUzGhZW7p", Version: "1.1.7"},
{Id: "52RMLEigqT5Ksn", Version: "1.1.6"},
{Id: "F4HY9eP4D5XjWQ", Version: "1.1.5"},
},
}}, nil
}
panic("ModVersions: " + modReference)
}
func (m MockProvider) SMLVersions(_ context.Context) (*ficsit.SMLVersionsResponse, error) {
return &ficsit.SMLVersionsResponse{
SmlVersions: ficsit.SMLVersionsSmlVersionsGetSMLVersions{
Count: 4,
Sml_versions: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion{
{
Id: "v2.2.1",
Version: "2.2.1",
Satisfactory_version: 125236,
Targets: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersionTargetsSMLVersionTarget{},
},
{
Id: "v3.3.2",
Version: "3.3.2",
Satisfactory_version: 194714,
Targets: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersionTargetsSMLVersionTarget{
{
TargetName: ficsit.TargetNameWindows,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.3.2/SML.zip",
},
},
},
{
Id: "v3.6.0",
Version: "3.6.0",
Satisfactory_version: 264901,
Targets: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersionTargetsSMLVersionTarget{
{
TargetName: ficsit.TargetNameWindows,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.0/SML.zip",
},
{
TargetName: ficsit.TargetNameWindowsserver,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.0/SML.zip",
},
{
TargetName: ficsit.TargetNameLinuxserver,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.0/SML.zip",
},
},
},
{
Id: "v3.6.1",
Version: "3.6.1",
Satisfactory_version: 264901,
Targets: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersionTargetsSMLVersionTarget{
{
TargetName: ficsit.TargetNameWindows,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.1/SML.zip",
},
{
TargetName: ficsit.TargetNameWindowsserver,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.1/SML.zip",
},
{
TargetName: ficsit.TargetNameLinuxserver,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.1/SML.zip",
},
},
},
},
},
}, nil
}
var commonTargets = []ficsit.Target{
{ {
TargetName: "Windows", TargetName: "Windows",
Hash: "62f5c84eca8480b3ffe7d6c90f759e3b463f482530e27d854fd48624fdd3acc9", Hash: "698df20278b3de3ec30405569a22050c6721cc682389312258c14948bd8f38ae",
}, },
{ {
TargetName: "WindowsServer", TargetName: "WindowsServer",
Hash: "8a83fcd4abece4192038769cc672fff6764d72c32fb6c7a8c58d66156bb07917", Hash: "7be01ed372e0cf3287a04f5cb32bb9dcf6f6e7a5b7603b7e43669ec4c6c1457f",
}, },
{ {
TargetName: "LinuxServer", TargetName: "LinuxServer",
Hash: "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85", Hash: "bdbd4cb1b472a5316621939ae2fe270fd0e3c0f0a75666a9cbe74ff1313c3663",
}, },
} }
func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) { func (m MockProvider) ModVersionsWithDependencies(ctx context.Context, modID string) ([]resolver.ModVersion, error) {
switch modID { switch modID {
case "RefinedPower":
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "3.2.13",
Dependencies: []ficsit.Dependency{
{
ModID: "ModularUI",
Condition: "^2.1.11",
Optional: false,
},
{
ModID: "RefinedRDLib",
Condition: "^1.1.7",
Optional: false,
},
{
ModID: "SML",
Condition: "^3.6.1",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "3.2.11",
Dependencies: []ficsit.Dependency{
{
ModID: "ModularUI",
Condition: "^2.1.10",
Optional: false,
},
{
ModID: "RefinedRDLib",
Condition: "^1.1.6",
Optional: false,
},
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "3.2.10",
Dependencies: []ficsit.Dependency{
{
ModID: "ModularUI",
Condition: "^2.1.9",
Optional: false,
},
{
ModID: "RefinedRDLib",
Condition: "^1.1.5",
Optional: false,
},
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
},
}, nil
case "AreaActions": case "AreaActions":
return &ficsit.AllVersionsResponse{ return []resolver.ModVersion{
Success: true,
Data: []ficsit.ModVersion{
{ {
ID: "7QcfNdo5QAAyoC", ID: "7QcfNdo5QAAyoC",
Version: "1.6.7", Version: "1.6.7",
Dependencies: []ficsit.Dependency{ Dependencies: []resolver.Dependency{
{ {
ModID: "SML", ModID: "SML",
Condition: "^3.4.1", Condition: "^3.4.1",
@ -271,7 +104,7 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
{ {
ID: "7QcfNdo5QAAyoC", ID: "7QcfNdo5QAAyoC",
Version: "1.6.6", Version: "1.6.6",
Dependencies: []ficsit.Dependency{ Dependencies: []resolver.Dependency{
{ {
ModID: "SML", ModID: "SML",
Condition: "^3.2.0", Condition: "^3.2.0",
@ -283,7 +116,7 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
{ {
ID: "7QcfNdo5QAAyoC", ID: "7QcfNdo5QAAyoC",
Version: "1.6.5", Version: "1.6.5",
Dependencies: []ficsit.Dependency{ Dependencies: []resolver.Dependency{
{ {
ModID: "SML", ModID: "SML",
Condition: "^3.0.0", Condition: "^3.0.0",
@ -292,108 +125,13 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
}, },
Targets: commonTargets, Targets: commonTargets,
}, },
},
}, nil
case "RefinedRDLib":
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "1.1.7",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.1",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "1.1.6",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "1.1.5",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
},
}, nil
case "ModularUI":
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "2.1.12",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.1",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "2.1.11",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "2.1.10",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
},
}, nil
case "ThisModDoesNotExist$$$":
return &ficsit.AllVersionsResponse{
Success: false,
Error: &ficsit.Error{
Message: "mod not found",
Code: 200,
},
}, nil }, nil
case "FicsitRemoteMonitoring": case "FicsitRemoteMonitoring":
return &ficsit.AllVersionsResponse{ return []resolver.ModVersion{
Success: true,
Data: []ficsit.ModVersion{
{ {
ID: "7QcfNdo5QAAyoC", ID: "7QcfNdo5QAAyoC",
Version: "0.10.1", Version: "0.10.1",
Dependencies: []ficsit.Dependency{ Dependencies: []resolver.Dependency{
{ {
ModID: "SML", ModID: "SML",
Condition: "^3.6.0", Condition: "^3.6.0",
@ -405,7 +143,7 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
{ {
ID: "7QcfNdo5QAAyoC", ID: "7QcfNdo5QAAyoC",
Version: "0.10.0", Version: "0.10.0",
Dependencies: []ficsit.Dependency{ Dependencies: []resolver.Dependency{
{ {
ModID: "SML", ModID: "SML",
Condition: "^3.5.0", Condition: "^3.5.0",
@ -417,7 +155,7 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
{ {
ID: "7QcfNdo5QAAyoC", ID: "7QcfNdo5QAAyoC",
Version: "0.9.8", Version: "0.9.8",
Dependencies: []ficsit.Dependency{ Dependencies: []resolver.Dependency{
{ {
ModID: "SML", ModID: "SML",
Condition: "^3.4.1", Condition: "^3.4.1",
@ -426,30 +164,15 @@ func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID strin
}, },
Targets: commonTargets, Targets: commonTargets,
}, },
},
}, nil }, nil
} }
panic("ModVersionsWithDependencies: " + modID) return m.MockProvider.ModVersionsWithDependencies(ctx, modID) // nolint
} }
func (m MockProvider) GetModName(_ context.Context, modReference string) (*ficsit.GetModNameResponse, error) { func (m MockProvider) GetMod(_ context.Context, _ string) (*ficsit.GetModResponse, error) {
switch modReference { // Currently used only by TUI
case "RefinedPower": return nil, nil
return &ficsit.GetModNameResponse{Mod: ficsit.GetModNameMod{
Id: "DGiLzB3ZErWu2V",
Mod_reference: "RefinedPower",
Name: "Refined Power",
}}, nil
case "RefinedRDLib":
return &ficsit.GetModNameResponse{Mod: ficsit.GetModNameMod{
Id: "B24emzbs6xVZQr",
Mod_reference: "RefinedRDLib",
Name: "RefinedRDLib",
}}, nil
}
panic("GetModName: " + modReference)
} }
func (m MockProvider) IsOffline() bool { func (m MockProvider) IsOffline() bool {

View file

@ -1,19 +0,0 @@
package cli
type ModVersion struct {
ID string
Version string
Targets map[string]VersionTarget
Dependencies []VersionDependency
}
type VersionTarget struct {
Link string
Hash string
}
type VersionDependency struct {
ModReference string
Constraint string
Optional bool
}

3
go.mod
View file

@ -15,13 +15,13 @@ require (
github.com/charmbracelet/lipgloss v0.9.1 github.com/charmbracelet/lipgloss v0.9.1
github.com/charmbracelet/x/exp/teatest v0.0.0-20231206171822-6e7b9b308fe7 github.com/charmbracelet/x/exp/teatest v0.0.0-20231206171822-6e7b9b308fe7
github.com/jlaffaye/ftp v0.2.0 github.com/jlaffaye/ftp v0.2.0
github.com/mircearoata/pubgrub-go v0.3.3
github.com/muesli/reflow v0.3.0 github.com/muesli/reflow v0.3.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pterm/pterm v0.12.71 github.com/pterm/pterm v0.12.71
github.com/puzpuzpuz/xsync/v3 v3.0.2 github.com/puzpuzpuz/xsync/v3 v3.0.2
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
github.com/sahilm/fuzzy v0.1.0 github.com/sahilm/fuzzy v0.1.0
github.com/satisfactorymodding/ficsit-resolver v0.0.2
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.0 github.com/spf13/viper v1.18.0
golang.org/x/sync v0.5.0 golang.org/x/sync v0.5.0
@ -62,6 +62,7 @@ require (
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/mircearoata/pubgrub-go v0.3.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect

2
go.sum
View file

@ -189,6 +189,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/satisfactorymodding/ficsit-resolver v0.0.2 h1:dj/OsDLpaMUqCHpfBVHvDMUv2nf5gT4HS2ydBMkmtcQ=
github.com/satisfactorymodding/ficsit-resolver v0.0.2/go.mod h1:ckKMmMvDoYbbkEbWXEsMes608uvv6EKphXPhHX8LKSc=
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=

View file

@ -5,6 +5,8 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/pkg/errors" "github.com/pkg/errors"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cli" "github.com/satisfactorymodding/ficsit-cli/cli"
"github.com/satisfactorymodding/ficsit-cli/cli/provider" "github.com/satisfactorymodding/ficsit-cli/cli/provider"
@ -14,8 +16,8 @@ import (
type rootModel struct { type rootModel struct {
headerComponent tea.Model headerComponent tea.Model
dependencyResolver cli.DependencyResolver
global *cli.GlobalContext global *cli.GlobalContext
dependencyResolver resolver.DependencyResolver
currentSize tea.WindowSizeMsg currentSize tea.WindowSizeMsg
} }
@ -26,7 +28,7 @@ func newModel(global *cli.GlobalContext) *rootModel {
Width: 20, Width: 20,
Height: 14, Height: 14,
}, },
dependencyResolver: cli.NewDependencyResolver(global.Provider), dependencyResolver: resolver.NewDependencyResolver(global.Provider, viper.GetString("api-base")),
} }
m.headerComponent = components.NewHeaderComponent(m) m.headerComponent = components.NewHeaderComponent(m)

View file

@ -248,7 +248,7 @@ func (m apply) View() string {
for _, installPath := range installationList { for _, installPath := range installationList {
s := m.status[installPath] s := m.status[installPath]
strs = append(strs, lipgloss.NewStyle().Margin(topMargins, 0, bottomMargins, 1).Render(lipgloss.JoinHorizontal( strs = append(strs, lipgloss.NewStyle().Margin(topMargins, 0, bottomMargins).Render(lipgloss.JoinHorizontal(
lipgloss.Left, lipgloss.Left,
m.overall.ViewAs(s.overallProgress.Percentage()), m.overall.ViewAs(s.overallProgress.Percentage()),
" - ", " - ",
@ -265,22 +265,22 @@ func (m apply) View() string {
for _, modReference := range modReferences { for _, modReference := range modReferences {
p := s.modProgresses[modReference] p := s.modProgresses[modReference]
if p.complete || s.done { if p.complete || s.done {
strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+modReference) strs = append(strs, lipgloss.NewStyle().MarginLeft(2).Foreground(lipgloss.Color("22")).Render("✓ ")+modReference)
} else { } else {
if p.downloading { if p.downloading {
strs = append(strs, lipgloss.JoinHorizontal( strs = append(strs, lipgloss.NewStyle().MarginLeft(1).Render(lipgloss.JoinHorizontal(
lipgloss.Left, lipgloss.Left,
m.sub.ViewAs(p.downloadProgress.Percentage()), m.sub.ViewAs(p.downloadProgress.Percentage()),
" - ", " - ",
lipgloss.NewStyle().Render(modReference+" (Downloading)"), lipgloss.NewStyle().Render(modReference+" (Downloading)"),
)) )))
} else { } else {
strs = append(strs, lipgloss.JoinHorizontal( strs = append(strs, lipgloss.NewStyle().MarginLeft(1).Render(lipgloss.JoinHorizontal(
lipgloss.Left, lipgloss.Left,
m.sub.ViewAs(p.extractProgress.Percentage()), m.sub.ViewAs(p.extractProgress.Percentage()),
" - ", " - ",
lipgloss.NewStyle().Render(modReference+" (Extracting)"), lipgloss.NewStyle().Render(modReference+" (Extracting)"),
)) )))
} }
} }
} }

View file

@ -11,7 +11,6 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys" "github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys"
"github.com/satisfactorymodding/ficsit-cli/tea/utils" "github.com/satisfactorymodding/ficsit-cli/tea/utils"
@ -53,34 +52,18 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
go func() { go func() {
items := make([]list.Item, 0) items := make([]list.Item, 0)
allVersions := make([]ficsit.ModVersionsModVersionsVersion, 0) versions, err := root.GetProvider().ModVersionsWithDependencies(context.TODO(), mod.Reference)
offset := 0
for {
versions, err := root.GetProvider().ModVersions(context.TODO(), mod.Reference, ficsit.VersionFilter{
Limit: 100,
Offset: offset,
Order: ficsit.OrderDesc,
Order_by: ficsit.VersionFieldsCreatedAt,
})
if err != nil { if err != nil {
m.err <- err.Error() m.err <- err.Error()
return return
} }
if len(versions.Mod.Versions) == 0 { for _, version := range versions {
break tempVersion := version
}
allVersions = append(allVersions, versions.Mod.Versions...)
for i := 0; i < len(versions.Mod.Versions); i++ {
currentOffset := offset
currentI := i
items = append(items, utils.SimpleItem[selectModVersionList]{ items = append(items, utils.SimpleItem[selectModVersionList]{
ItemTitle: versions.Mod.Versions[i].Version, ItemTitle: tempVersion.Version,
Activate: func(msg tea.Msg, currentModel selectModVersionList) (tea.Model, tea.Cmd) { Activate: func(msg tea.Msg, currentModel selectModVersionList) (tea.Model, tea.Cmd) {
version := allVersions[currentOffset+currentI] err := root.GetCurrentProfile().AddMod(mod.Reference, tempVersion.Version)
err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version)
if err != nil { if err != nil {
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5) errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
currentModel.error = errorComponent currentModel.error = errorComponent
@ -91,9 +74,6 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
}) })
} }
offset += len(versions.Mod.Versions)
}
m.items <- items m.items <- items
}() }()

View file

@ -13,8 +13,9 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/truncate" "github.com/muesli/reflow/truncate"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cli"
"github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys" "github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys"
@ -111,7 +112,7 @@ func (m updateModsList) LoadModData() {
return return
} }
resolver := cli.NewDependencyResolver(m.root.GetProvider()) resolver := resolver.NewDependencyResolver(m.root.GetProvider(), viper.GetString("api-base"))
updatedLockfile, err := currentProfile.Resolve(resolver, nil, gameVersion) updatedLockfile, err := currentProfile.Resolve(resolver, nil, gameVersion)
if err != nil { if err != nil {