Skip to content

Commit

Permalink
feat: use mod version targets
Browse files Browse the repository at this point in the history
  • Loading branch information
mircearoata committed Jun 4, 2023
1 parent 815c8a3 commit 8f69809
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 88 deletions.
64 changes: 39 additions & 25 deletions cli/dependency_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package cli

import (
"context"
"fmt"
"sort"
"strings"

"github.com/Khan/genqlient/graphql"
"github.com/Masterminds/semver/v3"
Expand All @@ -14,8 +12,6 @@ import (
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)

const smlDownloadTemplate = `https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/%s/SML.zip`

type DependencyResolver struct {
apiClient graphql.Client
}
Expand All @@ -24,7 +20,7 @@ func NewDependencyResolver(apiClient graphql.Client) DependencyResolver {
return DependencyResolver{apiClient: apiClient}
}

func (d DependencyResolver) ResolveModDependencies(constraints map[string]string, lockFile *LockFile, gameVersion int) (LockFile, error) {
func (d DependencyResolver) ResolveModDependencies(constraints map[string]string, lockFile *LockFile, gameVersion int) (*LockFile, error) {
smlVersionsDB, err := ficsit.SMLVersions(context.TODO(), d.apiClient)
if err != nil {
return nil, errors.Wrap(err, "failed fetching SMl versions")
Expand All @@ -39,7 +35,7 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
Resolver: d,
InputLock: lockFile,
ToResolve: copied,
OutputLock: make(LockFile),
OutputLock: MakeLockfile(),
SMLVersions: smlVersionsDB,
GameVersion: gameVersion,
}
Expand All @@ -48,7 +44,7 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
return nil, err
}

return instance.OutputLock, nil
return &instance.OutputLock, nil
}

type resolvingInstance struct {
Expand Down Expand Up @@ -78,7 +74,7 @@ func (r *resolvingInstance) Step() error {
Version: constraint[0],
})
} else {
if existingSML, ok := r.OutputLock[id]; ok {
if existingSML, ok := r.OutputLock.Mods[id]; ok {
for _, cs := range constraint {
smlVersionConstraint, _ := semver.NewConstraint(cs)
if !smlVersionConstraint.Check(semver.MustParse(existingSML.Version)) {
Expand All @@ -92,6 +88,7 @@ func (r *resolvingInstance) Step() error {
}

var chosenSMLVersion *semver.Version
var chosenSMLTargets []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersionTargetsSMLVersionTarget
for _, version := range r.SMLVersions.SmlVersions.Sml_versions {
if version.Satisfactory_version > r.GameVersion {
continue
Expand All @@ -112,6 +109,7 @@ func (r *resolvingInstance) Step() error {
if matches {
if chosenSMLVersion == nil || currentVersion.GreaterThan(chosenSMLVersion) {
chosenSMLVersion = currentVersion
chosenSMLTargets = version.Targets
}
}
}
Expand All @@ -120,14 +118,16 @@ func (r *resolvingInstance) Step() error {
return errors.Errorf("could not find an SML version that matches constraint %s and game version %d", constraint, r.GameVersion)
}

smlVersionStr := chosenSMLVersion.String()
if !strings.HasPrefix(smlVersionStr, "v") {
smlVersionStr = "v" + smlVersionStr
targets := make(map[string]LockedModTarget)
for _, target := range chosenSMLTargets {
targets[target.TargetName] = LockedModTarget{
Link: target.Link,
}
}

r.OutputLock[id] = LockedMod{
r.OutputLock.Mods[id] = LockedMod{
Version: chosenSMLVersion.String(),
Link: fmt.Sprintf(smlDownloadTemplate, smlVersionStr),
Targets: targets,
Dependencies: map[string]string{},
}
}
Expand All @@ -154,11 +154,18 @@ func (r *resolvingInstance) Step() error {
}
}

versionTargets := make(map[string]VersionTarget)
for _, target := range version.Targets {
versionTargets[target.TargetName] = VersionTarget{
Link: viper.GetString("api-base") + target.Link,
Hash: target.Hash,
}
}

modVersions[i] = ModVersion{
ID: version.Id,
Version: version.Version,
Link: viper.GetString("api-base") + version.Link,
Hash: version.Hash,
Targets: versionTargets,
Dependencies: versionDependencies,
}
}
Expand Down Expand Up @@ -193,8 +200,8 @@ func (r *resolvingInstance) Step() error {
return errors.Errorf("no version of %s matches constraints", mod.Mod_reference)
}

if _, ok := r.OutputLock[mod.Mod_reference]; ok {
if r.OutputLock[mod.Mod_reference].Version != selectedVersion.Version {
if _, ok := r.OutputLock.Mods[mod.Mod_reference]; ok {
if r.OutputLock.Mods[mod.Mod_reference].Version != selectedVersion.Version {
return errors.New("failed resolving dependencies. requires different versions of " + mod.Mod_reference)
}
}
Expand All @@ -206,15 +213,22 @@ func (r *resolvingInstance) Step() error {
}
}

r.OutputLock[mod.Mod_reference] = LockedMod{
targets := make(map[string]LockedModTarget)
for targetName, target := range selectedVersion.Targets {
targets[targetName] = LockedModTarget{
Link: target.Link,
Hash: target.Hash,
}
}

r.OutputLock.Mods[mod.Mod_reference] = LockedMod{
Version: selectedVersion.Version,
Hash: selectedVersion.Hash,
Link: selectedVersion.Link,
Targets: targets,
Dependencies: modDependencies,
}

for _, dependency := range selectedVersion.Dependencies {
if previousSelectedVersion, ok := r.OutputLock[dependency.ModReference]; ok {
if previousSelectedVersion, ok := r.OutputLock.Mods[dependency.ModReference]; ok {
constraint, _ := semver.NewConstraint(dependency.Constraint)
if !constraint.Check(semver.MustParse(previousSelectedVersion.Version)) {
return errors.Errorf("mod %s version %s does not match constraint %s",
Expand Down Expand Up @@ -252,7 +266,7 @@ func (r *resolvingInstance) Step() error {
r.ToResolve = nextResolve

for _, constraint := range converted {
if _, ok := r.OutputLock[constraint.ModIdOrReference]; !ok {
if _, ok := r.OutputLock.Mods[constraint.ModIdOrReference]; !ok {
return errors.New("failed resolving dependency: " + constraint.ModIdOrReference)
}
}
Expand All @@ -277,7 +291,7 @@ func (r *resolvingInstance) LockStep(viewed map[string]bool) error {

viewed[modReference] = true

if locked, ok := (*r.InputLock)[modReference]; ok {
if locked, ok := r.InputLock.Mods[modReference]; ok {
passes := true

for _, cs := range constraints {
Expand All @@ -290,7 +304,7 @@ func (r *resolvingInstance) LockStep(viewed map[string]bool) error {

if passes {
delete(r.ToResolve, modReference)
r.OutputLock[modReference] = locked
r.OutputLock.Mods[modReference] = locked
for k, v := range locked.Dependencies {
if alreadyResolving, ok := r.ToResolve[k]; ok {
newConstraint, _ := semver.NewConstraint(v)
Expand All @@ -311,7 +325,7 @@ func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
continue
}

if outVersion, ok := r.OutputLock[k]; ok {
if outVersion, ok := r.OutputLock.Mods[k]; ok {
constraint, _ := semver.NewConstraint(v)
if !constraint.Check(semver.MustParse(outVersion.Version)) {
return errors.Errorf("mod %s version %s does not match constraint %s",
Expand Down
31 changes: 20 additions & 11 deletions cli/installations.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,11 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
return errors.Wrap(err, "failed to validate installation")
}

platform, err := i.GetPlatform(ctx)
if err != nil {
return errors.Wrap(err, "failed to detect platform")
}

lockFile, err := i.LockFile(ctx)
if err != nil {
return err
Expand Down Expand Up @@ -348,7 +353,7 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e

for _, entry := range dir {
if entry.IsDir() {
if _, ok := lockfile[entry.Name()]; !ok {
if _, ok := lockfile.Mods[entry.Name()]; !ok {
modDir := filepath.Join(modsDirectory, entry.Name())
err := d.Exists(filepath.Join(modDir, ".smm"))
if err == nil {
Expand Down Expand Up @@ -377,7 +382,7 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
defer wg.Done()

update := InstallUpdate{
OverallProgress: float64(completed) / float64(len(lockfile)),
OverallProgress: float64(completed) / float64(len(lockfile.Mods)),
DownloadProgress: 0,
ExtractProgress: 0,
}
Expand All @@ -399,7 +404,7 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
update.ModName = *up.ModReference
}

update.OverallProgress = float64(completed) / float64(len(lockfile))
update.OverallProgress = float64(completed) / float64(len(lockfile.Mods))

select {
case updates <- update:
Expand All @@ -409,33 +414,37 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
}()
}

for modReference, version := range lockfile {
for modReference, version := range lockfile.Mods {
// Only install if a link is provided, otherwise assume mod is already installed
if version.Link != "" {
target, ok := version.Targets[platform.TargetName]
if !ok {
return errors.Errorf("%s@%s not available for %s", modReference, version.Version, platform.TargetName)
}
if target.Link != "" {
downloading = true

if genericUpdates != nil {
genericUpdates <- utils.GenericUpdate{ModReference: &modReference}
}

log.Info().Str("mod_reference", modReference).Str("version", version.Version).Str("link", version.Link).Msg("downloading mod")
reader, size, err := utils.DownloadOrCache(modReference+"_"+version.Version+".zip", version.Hash, version.Link, genericUpdates)
log.Info().Str("mod_reference", modReference).Str("version", version.Version).Str("link", target.Link).Msg("downloading mod")
reader, size, err := utils.DownloadOrCache(modReference+"_"+version.Version+".zip", target.Hash, target.Link, genericUpdates)
if err != nil {
return errors.Wrap(err, "failed to download "+modReference+" from: "+version.Link)
return errors.Wrap(err, "failed to download "+modReference+" from: "+target.Link)
}

downloading = false

log.Info().Str("mod_reference", modReference).Str("version", version.Version).Str("link", version.Link).Msg("extracting mod")
if err := utils.ExtractMod(reader, size, filepath.Join(modsDirectory, modReference), version.Hash, genericUpdates, d); err != nil {
log.Info().Str("mod_reference", modReference).Str("version", version.Version).Str("link", target.Link).Msg("extracting mod")
if err := utils.ExtractMod(reader, size, filepath.Join(modsDirectory, modReference), target.Hash, genericUpdates, d); err != nil {
return errors.Wrap(err, "could not extract "+modReference)
}
}

completed++
}

if err := i.WriteLockFile(ctx, lockfile); err != nil {
if err := i.WriteLockFile(ctx, *lockfile); err != nil {
return err
}

Expand Down
45 changes: 37 additions & 8 deletions cli/lockfile.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
package cli

type LockFile map[string]LockedMod
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"`
Version string `json:"version"`
Hash string `json:"hash"`
Link string `json:"link"`
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 := make(LockFile)
for k, v := range l {
lockFile[k] = v
lockFile := LockFile{
Mods: make(map[string]LockedMod),
Version: l.Version,
}
for k, v := range l.Mods {
lockFile.Mods[k] = v
}
return lockFile
}
4 changes: 4 additions & 0 deletions cli/platforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@ import "path/filepath"
type Platform struct {
VersionPath string
LockfilePath string
TargetName string
}

var platforms = []Platform{
{
VersionPath: filepath.Join("Engine", "Binaries", "Linux", "UE4Server-Linux-Shipping.version"),
LockfilePath: filepath.Join("FactoryGame", "Mods"),
TargetName: "LinuxServer",
},
{
VersionPath: filepath.Join("Engine", "Binaries", "Win64", "UE4Server-Win64-Shipping.version"),
LockfilePath: filepath.Join("FactoryGame", "Mods"),
TargetName: "WindowsServer",
},
{
VersionPath: filepath.Join("Engine", "Binaries", "Win64", "FactoryGame-Win64-Shipping.version"),
LockfilePath: filepath.Join("FactoryGame", "Mods"),
TargetName: "WindowsNoEditor", // TODO: Support both WindowsNoEditor (UE4) and Windows (UE5)
},
}
2 changes: 1 addition & 1 deletion cli/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,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 DependencyResolver, lockFile *LockFile, gameVersion int) (*LockFile, error) {
toResolve := make(map[string]string)
for modReference, mod := range p.Mods {
if mod.Enabled {
Expand Down
8 changes: 6 additions & 2 deletions cli/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package cli
type ModVersion struct {
ID string
Version string
Link string
Hash string
Targets map[string]VersionTarget
Dependencies []VersionDependency
}

type VersionTarget struct {
Link string
Hash string
}

type VersionDependency struct {
ModReference string
Constraint string
Expand Down
7 changes: 5 additions & 2 deletions ficsit/queries/resolve_mod_dependencies.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ query ResolveModDependencies ($filter: [ModVersionConstraint!]!) {
versions {
id
version
link
hash
targets {
targetName
link
hash
}
dependencies {
condition
mod_id
Expand Down
Loading

0 comments on commit 8f69809

Please sign in to comment.