diff --git a/.golangci.yml b/.golangci.yml index 891f76c..e7f9fa2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -58,5 +58,4 @@ linters: - wrapcheck - gci - gocritic - - gofumpt - nonamedreturns diff --git a/cli/cache/download.go b/cli/cache/download.go index 5219ee3..e56770e 100644 --- a/cli/cache/download.go +++ b/cli/cache/download.go @@ -6,14 +6,39 @@ import ( "net/http" "os" "path/filepath" + "sync" "github.com/pkg/errors" + "github.com/puzpuzpuz/xsync/v3" "github.com/spf13/viper" "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) { + 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") if err := os.MkdirAll(downloadCache, 0o777); err != nil { if !os.IsExist(err) { @@ -23,6 +48,72 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut 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) if err == nil { existingHash := "" @@ -30,36 +121,31 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut if hash != "" { f, err := os.Open(location) 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() existingHash, err = utils.SHA256Data(f) 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 { - f, err := os.Open(location) - if err != nil { - return nil, 0, errors.Wrap(err, "failed to open file: "+location) - } - - return f, stat.Size(), nil + return stat.Size(), 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) { - return nil, 0, errors.Wrap(err, "failed to stat file: "+location) + return 0, errors.Wrap(err, "failed to stat file: "+location) } if updates != nil { headResp, err := http.Head(url) 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() 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) 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() resp, err := http.Get(url) 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() 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{ @@ -94,12 +180,7 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut _, err = io.Copy(out, progresser) if err != nil { - return nil, 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) + return 0, errors.Wrap(err, "failed writing file to disk") } if updates != nil { @@ -108,8 +189,8 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut _, err = addFileToCache(cacheKey) 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 } diff --git a/cli/context.go b/cli/context.go index cd84acf..66333ec 100644 --- a/cli/context.go +++ b/cli/context.go @@ -26,7 +26,7 @@ func InitCLI(apiOnly bool) (*GlobalContext, error) { apiClient := ficsit.InitAPI() - mixedProvider := provider.InitMixedProvider(apiClient) + mixedProvider := provider.InitMixedProvider(provider.NewFicsitProvider(apiClient), provider.NewLocalProvider()) if viper.GetBool("offline") { mixedProvider.Offline = true diff --git a/cli/dependency_resolver.go b/cli/dependency_resolver.go deleted file mode 100644 index c7765d4..0000000 --- a/cli/dependency_resolver.go +++ /dev/null @@ -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 -} diff --git a/cli/dependency_resolver_error.go b/cli/dependency_resolver_error.go deleted file mode 100644 index 9ae02c2..0000000 --- a/cli/dependency_resolver_error.go +++ /dev/null @@ -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) -} diff --git a/cli/installations.go b/cli/installations.go index 5690f71..fbc853f 100644 --- a/cli/installations.go +++ b/cli/installations.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + resolver "github.com/satisfactorymodding/ficsit-resolver" "github.com/spf13/viper" "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 } -func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) { +func (i *Installation) LockFile(ctx *GlobalContext) (*resolver.LockFile, error) { lockfilePath, err := i.LockFilePath(ctx) if err != nil { return nil, err @@ -268,7 +269,7 @@ func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) { return nil, err } - var lockFile *LockFile + var lockFile *resolver.LockFile lockFileJSON, err := d.Read(lockfilePath) if err != nil { if !d.IsNotExist(err) { @@ -283,7 +284,7 @@ func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) { 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) if err != nil { return err @@ -327,20 +328,20 @@ func (i *Installation) Wipe() error { return nil } -func (i *Installation) ResolveProfile(ctx *GlobalContext) (*LockFile, error) { +func (i *Installation) ResolveProfile(ctx *GlobalContext) (*resolver.LockFile, error) { lockFile, err := i.LockFile(ctx) if err != nil { return nil, err } - resolver := NewDependencyResolver(ctx.Provider) + depResolver := resolver.NewDependencyResolver(ctx.Provider, viper.GetString("api-base")) gameVersion, err := i.GetGameVersion(ctx) if err != nil { 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 { 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") } - lockfile := MakeLockfile() + lockfile := resolver.NewLockfile() if !i.Vanilla { 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") } - resolver := NewDependencyResolver(ctx.Provider) + resolver := resolver.NewDependencyResolver(ctx.Provider, viper.GetString("api-base")) gameVersion, err := i.GetGameVersion(ctx) if err != nil { diff --git a/cli/installations_test.go b/cli/installations_test.go index 0f167e1..bc6720e 100644 --- a/cli/installations_test.go +++ b/cli/installations_test.go @@ -34,8 +34,8 @@ func TestAddInstallation(t *testing.T) { profileName := "InstallationTest" profile, err := ctx.Profiles.AddProfile(profileName) testza.AssertNoError(t, err) - testza.AssertNoError(t, profile.AddMod("AreaActions", ">=1.6.5")) - testza.AssertNoError(t, profile.AddMod("RefinedPower", ">=3.2.10")) + testza.AssertNoError(t, profile.AddMod("AreaActions", "1.6.5")) + testza.AssertNoError(t, profile.AddMod("RefinedPower", "3.2.10")) serverLocation := os.Getenv("SF_DEDICATED_SERVER") if serverLocation != "" { diff --git a/cli/lockfile.go b/cli/lockfile.go deleted file mode 100644 index fb9c312..0000000 --- a/cli/lockfile.go +++ /dev/null @@ -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 -} diff --git a/cli/profiles.go b/cli/profiles.go index f532784..9af83e9 100644 --- a/cli/profiles.go +++ b/cli/profiles.go @@ -1,6 +1,7 @@ package cli import ( + "context" "encoding/json" "fmt" "os" @@ -9,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" + resolver "github.com/satisfactorymodding/ficsit-resolver" "github.com/spf13/viper" "github.com/satisfactorymodding/ficsit-cli/utils" @@ -43,8 +45,9 @@ type Profiles struct { } type Profile struct { - Mods map[string]ProfileMod `json:"mods"` - Name string `json:"name"` + Mods map[string]ProfileMod `json:"mods"` + Name string `json:"name"` + RequiredTargets []resolver.TargetName `json:"required_targets"` } type ProfileMod struct { @@ -288,7 +291,7 @@ func (p *Profile) HasMod(reference string) bool { // An optional lockfile can be passed if one exists. // // 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) for modReference, mod := range p.Mods { 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 { return nil, errors.Wrap(err, "failed resolving profile dependencies") } diff --git a/cli/provider/ficsit.go b/cli/provider/ficsit.go index 9f5ccf1..fa60573 100644 --- a/cli/provider/ficsit.go +++ b/cli/provider/ficsit.go @@ -4,40 +4,117 @@ import ( "context" "github.com/Khan/genqlient/graphql" + "github.com/pkg/errors" + resolver "github.com/satisfactorymodding/ficsit-resolver" "github.com/satisfactorymodding/ficsit-cli/ficsit" ) -type ficsitProvider struct { +type FicsitProvider struct { client graphql.Client } -func initFicsitProvider(client graphql.Client) ficsitProvider { - return ficsitProvider{ +func NewFicsitProvider(client graphql.Client) FicsitProvider { + return FicsitProvider{ 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) } -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) } -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) } -func (p ficsitProvider) SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error) { - return ficsit.SMLVersions(context, p.client) +func (p FicsitProvider) SMLVersions(context context.Context) ([]resolver.SMLVersion, error) { + response, err := ficsit.SMLVersions(context, p.client) + if err != nil { + return nil, err + } + + smlVersions := make([]resolver.SMLVersion, len(response.SmlVersions.Sml_versions)) + 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, + } + } + + smlVersions[i] = resolver.SMLVersion{ + ID: version.Id, + Version: version.Version, + SatisfactoryVersion: version.Satisfactory_version, + Targets: targets, + } + } + + return smlVersions, nil } -func (p ficsitProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) { - return ficsit.GetAllModVersions(modID) +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) (*ficsit.GetModNameResponse, error) { - return ficsit.GetModName(context, p.client, modReference) +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 } diff --git a/cli/provider/local.go b/cli/provider/local.go index 8bdeec1..f59d9a0 100644 --- a/cli/provider/local.go +++ b/cli/provider/local.go @@ -6,18 +6,19 @@ import ( "time" "github.com/pkg/errors" + resolver "github.com/satisfactorymodding/ficsit-resolver" "github.com/satisfactorymodding/ficsit-cli/cli/cache" "github.com/satisfactorymodding/ficsit-cli/ficsit" ) -type localProvider struct{} +type LocalProvider struct{} -func initLocalProvider() localProvider { - return localProvider{} +func NewLocalProvider() 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() if err != nil { return nil, errors.Wrap(err, "failed to get cache") @@ -89,7 +90,7 @@ func (p localProvider) Mods(_ context.Context, filter ficsit.ModFilter) (*ficsit }, 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) if err != nil { return nil, errors.Wrap(err, "failed to get cache") @@ -125,79 +126,44 @@ func (p localProvider) GetMod(_ context.Context, modReference string) (*ficsit.G }, nil } -func (p localProvider) ModVersions(_ context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, 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) { +func (p LocalProvider) SMLVersions(_ context.Context) ([]resolver.SMLVersion, error) { cachedSMLFiles, err := cache.GetCacheMod("SML") if err != nil { return nil, errors.Wrap(err, "failed to get cache") } - smlVersions := make([]ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion, 0) + smlVersions := make([]resolver.SMLVersion, 0) for _, smlFile := range cachedSMLFiles { - smlVersions = append(smlVersions, ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion{ - Id: "SML:" + smlFile.Plugin.SemVersion, - Version: smlFile.Plugin.SemVersion, - Satisfactory_version: 0, // TODO: where can this be obtained from? + smlVersions = append(smlVersions, resolver.SMLVersion{ + ID: "SML:" + smlFile.Plugin.SemVersion, + Version: smlFile.Plugin.SemVersion, + SatisfactoryVersion: 0, // TODO: where can this be obtained from? }) } - return &ficsit.SMLVersionsResponse{ - SmlVersions: ficsit.SMLVersionsSmlVersionsGetSMLVersions{ - Count: len(smlVersions), - Sml_versions: smlVersions, - }, - }, nil + return 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) if err != nil { return nil, errors.Wrap(err, "failed to get cache") } - versions := make([]ficsit.ModVersion, 0) + versions := make([]resolver.ModVersion, 0) for _, modFile := range cachedModFiles { - versions = append(versions, ficsit.ModVersion{ + versions = append(versions, resolver.ModVersion{ ID: modID + ":" + modFile.Plugin.SemVersion, Version: modFile.Plugin.SemVersion, }) } - return &ficsit.AllVersionsResponse{ - Success: true, - Data: versions, - }, nil + return 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) if err != nil { 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 &ficsit.GetModNameResponse{ - Mod: ficsit.GetModNameMod{ - Id: modReference, - Name: cachedModFiles[0].Plugin.FriendlyName, - Mod_reference: modReference, - }, + return &resolver.ModName{ + ID: modReference, + Name: cachedModFiles[0].Plugin.FriendlyName, + ModReference: modReference, }, nil } + +func (p LocalProvider) IsOffline() bool { + return true +} diff --git a/cli/provider/mixed.go b/cli/provider/mixed.go index 00def81..5c6fb86 100644 --- a/cli/provider/mixed.go +++ b/cli/provider/mixed.go @@ -3,65 +3,58 @@ package provider import ( "context" - "github.com/Khan/genqlient/graphql" + resolver "github.com/satisfactorymodding/ficsit-resolver" "github.com/satisfactorymodding/ficsit-cli/ficsit" ) type MixedProvider struct { - ficsitProvider ficsitProvider - localProvider localProvider - Offline bool + onlineProvider Provider + offlineProvider Provider + Offline bool } -func InitMixedProvider(client graphql.Client) *MixedProvider { +func InitMixedProvider(onlineProvider Provider, offlineProvider Provider) *MixedProvider { return &MixedProvider{ - ficsitProvider: initFicsitProvider(client), - localProvider: initLocalProvider(), - Offline: false, + onlineProvider: onlineProvider, + offlineProvider: offlineProvider, + Offline: false, } } func (p MixedProvider) Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) { 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) { 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 { - 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 { - 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 { - return p.localProvider.ModVersionsWithDependencies(context, modID) + return p.offlineProvider.GetModName(context, modReference) // nolint } - return p.ficsitProvider.ModVersionsWithDependencies(context, modID) -} - -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) + return p.onlineProvider.GetModName(context, modReference) // nolint } func (p MixedProvider) IsOffline() bool { diff --git a/cli/provider/provider.go b/cli/provider/provider.go index d0b22a4..1cb4259 100644 --- a/cli/provider/provider.go +++ b/cli/provider/provider.go @@ -3,15 +3,14 @@ package provider import ( "context" + resolver "github.com/satisfactorymodding/ficsit-resolver" + "github.com/satisfactorymodding/ficsit-cli/ficsit" ) type Provider interface { + resolver.Provider Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, 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 } diff --git a/cli/resolving_test.go b/cli/resolving_test.go index 28963e5..811e094 100644 --- a/cli/resolving_test.go +++ b/cli/resolving_test.go @@ -1,12 +1,15 @@ package cli import ( + "context" "math" "os" "testing" "github.com/MarvinJWendt/testza" "github.com/rs/zerolog/log" + resolver "github.com/satisfactorymodding/ficsit-resolver" + "github.com/spf13/viper" "github.com/satisfactorymodding/ficsit-cli/cfg" ) @@ -15,10 +18,6 @@ func init() { cfg.SetDefaults() } -func profilesGetResolver() DependencyResolver { - return NewDependencyResolver(MockProvider{}) -} - func installWatcher() chan<- InstallUpdate { c := make(chan InstallUpdate) go func() { @@ -35,60 +34,6 @@ func installWatcher() chan<- InstallUpdate { 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) { ctx, err := InitCLI(false) testza.AssertNoError(t, err) @@ -98,17 +43,11 @@ func TestUpdateMods(t *testing.T) { ctx.Provider = MockProvider{} - resolver := NewDependencyResolver(ctx.Provider) + depResolver := resolver.NewDependencyResolver(ctx.Provider, viper.GetString("api-base")) - oldLockfile, err := (&Profile{ - Name: DefaultProfileName, - Mods: map[string]ProfileMod{ - "FicsitRemoteMonitoring": { - Version: "0.9.8", - Enabled: true, - }, - }, - }).Resolve(resolver, nil, math.MaxInt) + oldLockfile, err := depResolver.ResolveModDependencies(context.Background(), map[string]string{ + "FicsitRemoteMonitoring": "0.9.8", + }, nil, math.MaxInt, nil) testza.AssertNoError(t, err) testza.AssertNotNil(t, oldLockfile) diff --git a/cli/test_helpers.go b/cli/test_helpers.go index e7d434e..2a719c4 100644 --- a/cli/test_helpers.go +++ b/cli/test_helpers.go @@ -4,13 +4,17 @@ import ( "context" "time" + resolver "github.com/satisfactorymodding/ficsit-resolver" + "github.com/satisfactorymodding/ficsit-cli/cli/provider" "github.com/satisfactorymodding/ficsit-cli/ficsit" ) 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) { if f.Offset > 0 { @@ -66,392 +70,111 @@ func (m MockProvider) Mods(_ context.Context, f ficsit.ModFilter) (*ficsit.ModsR }, nil } +var commonTargets = []resolver.Target{ + { + TargetName: "Windows", + Hash: "698df20278b3de3ec30405569a22050c6721cc682389312258c14948bd8f38ae", + }, + { + TargetName: "WindowsServer", + Hash: "7be01ed372e0cf3287a04f5cb32bb9dcf6f6e7a5b7603b7e43669ec4c6c1457f", + }, + { + TargetName: "LinuxServer", + Hash: "bdbd4cb1b472a5316621939ae2fe270fd0e3c0f0a75666a9cbe74ff1313c3663", + }, +} + +func (m MockProvider) ModVersionsWithDependencies(ctx context.Context, modID string) ([]resolver.ModVersion, error) { + switch modID { + case "AreaActions": + return []resolver.ModVersion{ + { + ID: "7QcfNdo5QAAyoC", + Version: "1.6.7", + Dependencies: []resolver.Dependency{ + { + ModID: "SML", + Condition: "^3.4.1", + Optional: false, + }, + }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "1.6.6", + Dependencies: []resolver.Dependency{ + { + ModID: "SML", + Condition: "^3.2.0", + Optional: false, + }, + }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "1.6.5", + Dependencies: []resolver.Dependency{ + { + ModID: "SML", + Condition: "^3.0.0", + Optional: false, + }, + }, + Targets: commonTargets, + }, + }, nil + case "FicsitRemoteMonitoring": + return []resolver.ModVersion{ + { + ID: "7QcfNdo5QAAyoC", + Version: "0.10.1", + Dependencies: []resolver.Dependency{ + { + ModID: "SML", + Condition: "^3.6.0", + Optional: false, + }, + }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "0.10.0", + Dependencies: []resolver.Dependency{ + { + ModID: "SML", + Condition: "^3.5.0", + Optional: false, + }, + }, + Targets: commonTargets, + }, + { + ID: "7QcfNdo5QAAyoC", + Version: "0.9.8", + Dependencies: []resolver.Dependency{ + { + ModID: "SML", + Condition: "^3.4.1", + Optional: false, + }, + }, + Targets: commonTargets, + }, + }, nil + } + + return m.MockProvider.ModVersionsWithDependencies(ctx, modID) // nolint +} + func (m MockProvider) GetMod(_ context.Context, _ string) (*ficsit.GetModResponse, error) { // 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", - Hash: "62f5c84eca8480b3ffe7d6c90f759e3b463f482530e27d854fd48624fdd3acc9", - }, - { - TargetName: "WindowsServer", - Hash: "8a83fcd4abece4192038769cc672fff6764d72c32fb6c7a8c58d66156bb07917", - }, - { - TargetName: "LinuxServer", - Hash: "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85", - }, -} - -func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) { - 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": - return &ficsit.AllVersionsResponse{ - Success: true, - Data: []ficsit.ModVersion{ - { - ID: "7QcfNdo5QAAyoC", - Version: "1.6.7", - Dependencies: []ficsit.Dependency{ - { - ModID: "SML", - Condition: "^3.4.1", - Optional: false, - }, - }, - Targets: commonTargets, - }, - { - ID: "7QcfNdo5QAAyoC", - Version: "1.6.6", - Dependencies: []ficsit.Dependency{ - { - ModID: "SML", - Condition: "^3.2.0", - Optional: false, - }, - }, - Targets: commonTargets, - }, - { - ID: "7QcfNdo5QAAyoC", - Version: "1.6.5", - Dependencies: []ficsit.Dependency{ - { - ModID: "SML", - Condition: "^3.0.0", - Optional: false, - }, - }, - 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 - case "FicsitRemoteMonitoring": - return &ficsit.AllVersionsResponse{ - Success: true, - Data: []ficsit.ModVersion{ - { - ID: "7QcfNdo5QAAyoC", - Version: "0.10.1", - Dependencies: []ficsit.Dependency{ - { - ModID: "SML", - Condition: "^3.6.0", - Optional: false, - }, - }, - Targets: commonTargets, - }, - { - ID: "7QcfNdo5QAAyoC", - Version: "0.10.0", - Dependencies: []ficsit.Dependency{ - { - ModID: "SML", - Condition: "^3.5.0", - Optional: false, - }, - }, - Targets: commonTargets, - }, - { - ID: "7QcfNdo5QAAyoC", - Version: "0.9.8", - Dependencies: []ficsit.Dependency{ - { - ModID: "SML", - Condition: "^3.4.1", - Optional: false, - }, - }, - Targets: commonTargets, - }, - }, - }, nil - } - - panic("ModVersionsWithDependencies: " + modID) -} - -func (m MockProvider) GetModName(_ context.Context, modReference string) (*ficsit.GetModNameResponse, error) { - switch modReference { - case "RefinedPower": - 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 { return false } diff --git a/cli/types.go b/cli/types.go deleted file mode 100644 index 6440bbf..0000000 --- a/cli/types.go +++ /dev/null @@ -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 -} diff --git a/go.mod b/go.mod index 9fdd0cf..4b28ed7 100644 --- a/go.mod +++ b/go.mod @@ -15,13 +15,13 @@ require ( github.com/charmbracelet/lipgloss v0.9.1 github.com/charmbracelet/x/exp/teatest v0.0.0-20231206171822-6e7b9b308fe7 github.com/jlaffaye/ftp v0.2.0 - github.com/mircearoata/pubgrub-go v0.3.3 github.com/muesli/reflow v0.3.0 github.com/pkg/errors v0.9.1 github.com/pterm/pterm v0.12.71 github.com/puzpuzpuz/xsync/v3 v3.0.2 github.com/rs/zerolog v1.31.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/viper v1.18.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-runewidth v0.0.15 // 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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect diff --git a/go.sum b/go.sum index 9bc362f..a500505 100644 --- a/go.sum +++ b/go.sum @@ -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/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= 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/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= diff --git a/tea/root.go b/tea/root.go index f76d55c..dbbb4c6 100644 --- a/tea/root.go +++ b/tea/root.go @@ -5,6 +5,8 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "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/provider" @@ -14,8 +16,8 @@ import ( type rootModel struct { headerComponent tea.Model - dependencyResolver cli.DependencyResolver global *cli.GlobalContext + dependencyResolver resolver.DependencyResolver currentSize tea.WindowSizeMsg } @@ -26,7 +28,7 @@ func newModel(global *cli.GlobalContext) *rootModel { Width: 20, Height: 14, }, - dependencyResolver: cli.NewDependencyResolver(global.Provider), + dependencyResolver: resolver.NewDependencyResolver(global.Provider, viper.GetString("api-base")), } m.headerComponent = components.NewHeaderComponent(m) diff --git a/tea/scenes/apply.go b/tea/scenes/apply.go index 4f46559..00eb50b 100644 --- a/tea/scenes/apply.go +++ b/tea/scenes/apply.go @@ -248,7 +248,7 @@ func (m apply) View() string { for _, installPath := range installationList { 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, m.overall.ViewAs(s.overallProgress.Percentage()), " - ", @@ -265,22 +265,22 @@ func (m apply) View() string { for _, modReference := range modReferences { p := s.modProgresses[modReference] 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 { if p.downloading { - strs = append(strs, lipgloss.JoinHorizontal( + strs = append(strs, lipgloss.NewStyle().MarginLeft(1).Render(lipgloss.JoinHorizontal( lipgloss.Left, m.sub.ViewAs(p.downloadProgress.Percentage()), " - ", lipgloss.NewStyle().Render(modReference+" (Downloading)"), - )) + ))) } else { - strs = append(strs, lipgloss.JoinHorizontal( + strs = append(strs, lipgloss.NewStyle().MarginLeft(1).Render(lipgloss.JoinHorizontal( lipgloss.Left, m.sub.ViewAs(p.extractProgress.Percentage()), " - ", lipgloss.NewStyle().Render(modReference+" (Extracting)"), - )) + ))) } } } diff --git a/tea/scenes/mods/select_mod_version.go b/tea/scenes/mods/select_mod_version.go index c492c7d..0dbcd36 100644 --- a/tea/scenes/mods/select_mod_version.go +++ b/tea/scenes/mods/select_mod_version.go @@ -11,7 +11,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/satisfactorymodding/ficsit-cli/ficsit" "github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys" "github.com/satisfactorymodding/ficsit-cli/tea/utils" @@ -53,45 +52,26 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo go func() { items := make([]list.Item, 0) - allVersions := make([]ficsit.ModVersionsModVersionsVersion, 0) - 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, + versions, err := root.GetProvider().ModVersionsWithDependencies(context.TODO(), mod.Reference) + if err != nil { + m.err <- err.Error() + return + } + + for _, version := range versions { + tempVersion := version + items = append(items, utils.SimpleItem[selectModVersionList]{ + ItemTitle: tempVersion.Version, + Activate: func(msg tea.Msg, currentModel selectModVersionList) (tea.Model, tea.Cmd) { + err := root.GetCurrentProfile().AddMod(mod.Reference, tempVersion.Version) + if err != nil { + errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5) + currentModel.error = errorComponent + return currentModel, cmd + } + return currentModel.parent, nil + }, }) - if err != nil { - m.err <- err.Error() - return - } - - if len(versions.Mod.Versions) == 0 { - break - } - - 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]{ - ItemTitle: versions.Mod.Versions[i].Version, - Activate: func(msg tea.Msg, currentModel selectModVersionList) (tea.Model, tea.Cmd) { - version := allVersions[currentOffset+currentI] - err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version) - if err != nil { - errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5) - currentModel.error = errorComponent - return currentModel, cmd - } - return currentModel.parent, nil - }, - }) - } - - offset += len(versions.Mod.Versions) } m.items <- items diff --git a/tea/scenes/mods/update_mods.go b/tea/scenes/mods/update_mods.go index b0de81e..bcb42d8 100644 --- a/tea/scenes/mods/update_mods.go +++ b/tea/scenes/mods/update_mods.go @@ -13,8 +13,9 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "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/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys" @@ -111,7 +112,7 @@ func (m updateModsList) LoadModData() { return } - resolver := cli.NewDependencyResolver(m.root.GetProvider()) + resolver := resolver.NewDependencyResolver(m.root.GetProvider(), viper.GetString("api-base")) updatedLockfile, err := currentProfile.Resolve(resolver, nil, gameVersion) if err != nil {