2021-11-05 21:42:49 +00:00
package cli
2021-12-02 04:00:33 +00:00
import (
"encoding/json"
2023-12-16 14:19:53 +00:00
"errors"
2021-12-02 04:00:33 +00:00
"fmt"
2023-12-16 14:19:53 +00:00
"log/slog"
2022-06-22 22:24:35 +00:00
"net/url"
2021-12-02 04:00:33 +00:00
"os"
2022-05-02 20:07:15 +00:00
"path/filepath"
2022-06-03 22:17:02 +00:00
"regexp"
"strings"
2022-06-18 16:09:09 +00:00
"sync"
2022-05-02 20:07:15 +00:00
2023-12-16 11:59:58 +00:00
resolver "github.com/satisfactorymodding/ficsit-resolver"
2021-12-02 04:00:33 +00:00
"github.com/spf13/viper"
2023-12-06 23:39:34 +00:00
"golang.org/x/sync/errgroup"
2022-10-14 16:11:16 +00:00
2023-12-06 04:47:41 +00:00
"github.com/satisfactorymodding/ficsit-cli/cli/cache"
2022-10-14 16:11:16 +00:00
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
"github.com/satisfactorymodding/ficsit-cli/utils"
2021-12-02 04:00:33 +00:00
)
type InstallationsVersion int
const (
InitialInstallationsVersion = InstallationsVersion ( iota )
// Always last
nextInstallationsVersion
)
type Installations struct {
SelectedInstallation string ` json:"selected_installation" `
2022-10-14 16:11:16 +00:00
Installations [ ] * Installation ` json:"installations" `
Version InstallationsVersion ` json:"version" `
2021-12-02 04:00:33 +00:00
}
2021-11-05 21:42:49 +00:00
type Installation struct {
2022-10-14 16:11:16 +00:00
DiskInstance disk . Disk ` json:"-" `
2022-06-22 22:24:35 +00:00
Path string ` json:"path" `
Profile string ` json:"profile" `
2023-12-06 04:02:06 +00:00
Vanilla bool ` json:"vanilla" `
2021-12-02 04:00:33 +00:00
}
func InitInstallations ( ) ( * Installations , error ) {
2022-04-14 01:27:39 +00:00
localDir := viper . GetString ( "local-dir" )
2021-12-02 04:00:33 +00:00
2022-06-05 01:56:46 +00:00
installationsFile := filepath . Join ( localDir , viper . GetString ( "installations-file" ) )
2021-12-02 04:00:33 +00:00
_ , err := os . Stat ( installationsFile )
if err != nil {
if ! os . IsNotExist ( err ) {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to stat installations file: %w" , err )
2021-12-02 04:00:33 +00:00
}
2022-04-14 01:27:39 +00:00
_ , err := os . Stat ( localDir )
2021-12-02 04:00:33 +00:00
if err != nil {
if ! os . IsNotExist ( err ) {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to read cache directory: %w" , err )
2021-12-02 04:00:33 +00:00
}
2022-10-14 16:11:16 +00:00
err = os . MkdirAll ( localDir , 0 o755 )
2021-12-02 04:00:33 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to create cache directory: %w" , err )
2021-12-02 04:00:33 +00:00
}
}
emptyInstallations := Installations {
Version : nextInstallationsVersion - 1 ,
}
if err := emptyInstallations . Save ( ) ; err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to save empty installations: %w" , err )
2021-12-02 04:00:33 +00:00
}
}
installationsData , err := os . ReadFile ( installationsFile )
if err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to read installations: %w" , err )
2021-12-02 04:00:33 +00:00
}
var installations Installations
if err := json . Unmarshal ( installationsData , & installations ) ; err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to unmarshal installations: %w" , err )
2021-12-02 04:00:33 +00:00
}
if installations . Version >= nextInstallationsVersion {
return nil , fmt . Errorf ( "unknown installations version: %d" , installations . Version )
}
return & installations , nil
}
func ( i * Installations ) Save ( ) error {
if viper . GetBool ( "dry-run" ) {
2023-12-16 14:19:53 +00:00
slog . Info ( "dry-run: skipping installation saving" )
2021-12-02 04:00:33 +00:00
return nil
}
2022-06-05 01:56:46 +00:00
installationsFile := filepath . Join ( viper . GetString ( "local-dir" ) , viper . GetString ( "installations-file" ) )
2021-12-02 04:00:33 +00:00
2023-12-16 14:19:53 +00:00
slog . Info ( "saving installations" , slog . String ( "path" , installationsFile ) )
2021-12-02 04:00:33 +00:00
installationsJSON , err := json . MarshalIndent ( i , "" , " " )
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to marshal installations: %w" , err )
2021-12-02 04:00:33 +00:00
}
2022-10-14 16:11:16 +00:00
if err := os . WriteFile ( installationsFile , installationsJSON , 0 o755 ) ; err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to write installations: %w" , err )
2021-12-02 04:00:33 +00:00
}
return nil
}
func ( i * Installations ) AddInstallation ( ctx * GlobalContext , installPath string , profile string ) ( * Installation , error ) {
2022-06-22 22:24:35 +00:00
parsed , err := url . Parse ( installPath )
2022-05-02 20:07:15 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to parse path: %w" , err )
2022-06-22 22:24:35 +00:00
}
2022-10-14 16:11:16 +00:00
absolutePath := installPath
2022-06-22 22:37:36 +00:00
if parsed . Scheme != "ftp" && parsed . Scheme != "sftp" {
2022-06-22 22:24:35 +00:00
absolutePath , err = filepath . Abs ( installPath )
if err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "could not resolve absolute path of: %s: %w" , installPath , err )
2022-06-22 22:24:35 +00:00
}
2022-05-02 20:07:15 +00:00
}
2021-12-02 04:00:33 +00:00
installation := & Installation {
2022-05-02 20:07:15 +00:00
Path : absolutePath ,
2021-12-02 04:00:33 +00:00
Profile : profile ,
2023-12-06 04:02:06 +00:00
Vanilla : false ,
2021-12-02 04:00:33 +00:00
}
if err := installation . Validate ( ctx ) ; err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to validate installation: %w" , err )
2021-12-02 04:00:33 +00:00
}
found := false
2022-04-14 01:27:39 +00:00
for _ , install := range i . Installations {
2022-06-22 22:24:35 +00:00
if filepath . Clean ( installation . Path ) == filepath . Clean ( install . Path ) {
found = true
2021-12-02 04:00:33 +00:00
break
}
}
if found {
return nil , errors . New ( "installation already present" )
}
i . Installations = append ( i . Installations , installation )
return installation , nil
}
func ( i * Installations ) GetInstallation ( installPath string ) * Installation {
for _ , install := range i . Installations {
if install . Path == installPath {
return install
}
}
return nil
}
2022-04-14 01:27:39 +00:00
func ( i * Installations ) DeleteInstallation ( installPath string ) error {
found := - 1
for j , install := range i . Installations {
if install . Path == installPath {
found = j
break
}
}
if found == - 1 {
return errors . New ( "installation not found" )
}
i . Installations = append ( i . Installations [ : found ] , i . Installations [ found + 1 : ] ... )
return nil
}
2024-09-10 23:26:28 +00:00
var rootExecutables = [ ] string { "FactoryGame.exe" , "FactoryServer.sh" , "FactoryServer.exe" , "FactoryGameSteam.exe" , "FactoryGameEGS.exe" }
2024-05-05 21:01:17 +00:00
2021-12-02 04:00:33 +00:00
func ( i * Installation ) Validate ( ctx * GlobalContext ) error {
found := false
for _ , p := range ctx . Profiles . Profiles {
if p . Name == i . Profile {
found = true
break
}
}
if ! found {
return errors . New ( "profile not found" )
}
2022-06-22 22:24:35 +00:00
d , err := i . GetDisk ( )
if err != nil {
return err
}
2022-04-14 01:27:39 +00:00
foundExecutable := false
2024-05-05 21:01:17 +00:00
var checkWait errgroup . Group
2022-04-14 01:27:39 +00:00
2024-05-05 21:01:17 +00:00
for _ , executable := range rootExecutables {
e := executable
checkWait . Go ( func ( ) error {
exists , err := d . Exists ( filepath . Join ( i . BasePath ( ) , e ) )
if ! exists {
if err != nil {
return fmt . Errorf ( "failed reading %s: %w" , e , err )
}
} else {
foundExecutable = true
}
return nil
} )
2022-04-14 01:27:39 +00:00
}
2024-05-05 21:01:17 +00:00
if err = checkWait . Wait ( ) ; err != nil {
return err //nolint:wrapcheck
2022-04-14 01:27:39 +00:00
}
if ! foundExecutable {
2022-06-22 22:24:35 +00:00
return errors . New ( "did not find game executable in " + i . BasePath ( ) )
2022-04-14 01:27:39 +00:00
}
2021-12-02 04:00:33 +00:00
return nil
}
2022-06-03 22:17:02 +00:00
var (
lockFileCleaner = regexp . MustCompile ( ` [^a-zA-Z\d]] ` )
matchFirstCap = regexp . MustCompile ( ` (.)([A-Z][a-z]+) ` )
matchAllCap = regexp . MustCompile ( ` ([a-z\d])([A-Z]) ` )
)
2021-12-02 04:00:33 +00:00
2024-05-05 21:01:17 +00:00
func ( i * Installation ) lockFilePath ( ctx * GlobalContext , platform * Platform ) string {
2022-06-03 22:17:02 +00:00
lockFileName := ctx . Profiles . Profiles [ i . Profile ] . Name
lockFileName = matchFirstCap . ReplaceAllString ( lockFileName , "${1}_${2}" )
lockFileName = matchAllCap . ReplaceAllString ( lockFileName , "${1}_${2}" )
lockFileName = lockFileCleaner . ReplaceAllLiteralString ( lockFileName , "-" )
lockFileName = strings . ToLower ( lockFileName ) + "-lock.json"
2024-05-05 21:01:17 +00:00
return filepath . Join ( i . BasePath ( ) , platform . LockfilePath , lockFileName )
2022-06-03 22:17:02 +00:00
}
2024-05-05 21:01:17 +00:00
func ( i * Installation ) lockfile ( ctx * GlobalContext , platform * Platform ) ( * resolver . LockFile , error ) {
lockfilePath := i . lockFilePath ( ctx , platform )
2022-05-02 20:07:15 +00:00
2022-06-22 22:24:35 +00:00
d , err := i . GetDisk ( )
if err != nil {
return nil , err
}
2023-12-28 00:13:09 +00:00
exists , err := d . Exists ( lockfilePath )
if err != nil {
return nil , err
}
if ! exists {
return nil , nil
}
2023-12-16 11:59:58 +00:00
var lockFile * resolver . LockFile
2022-06-22 22:24:35 +00:00
lockFileJSON , err := d . Read ( lockfilePath )
2022-05-02 20:07:15 +00:00
if err != nil {
2023-12-28 00:13:09 +00:00
return nil , fmt . Errorf ( "failed reading lockfile: %w" , err )
}
if err := json . Unmarshal ( lockFileJSON , & lockFile ) ; err != nil {
return nil , fmt . Errorf ( "failed parsing lockfile: %w" , err )
2022-05-02 20:07:15 +00:00
}
2022-06-03 22:17:02 +00:00
return lockFile , nil
}
2024-05-05 21:01:17 +00:00
func ( i * Installation ) writeLockFile ( ctx * GlobalContext , platform * Platform , lockfile * resolver . LockFile ) error {
lockfilePath := i . lockFilePath ( ctx , platform )
2022-06-03 22:17:02 +00:00
2023-12-06 04:02:06 +00:00
d , err := i . GetDisk ( )
2022-06-03 22:17:02 +00:00
if err != nil {
2023-12-06 04:02:06 +00:00
return err
2022-06-03 22:17:02 +00:00
}
2023-12-06 04:02:06 +00:00
lockfileDir := filepath . Dir ( lockfilePath )
2023-12-28 00:13:09 +00:00
if exists , err := d . Exists ( lockfileDir ) ; ! exists {
if err != nil {
return err
}
2023-12-06 04:02:06 +00:00
if err := d . MkDir ( lockfileDir ) ; err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed creating lockfile directory: %w" , err )
2023-12-06 04:02:06 +00:00
}
}
marshaledLockfile , err := json . MarshalIndent ( lockfile , "" , " " )
2022-06-22 22:24:35 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to serialize lockfile json: %w" , err )
2022-06-22 22:24:35 +00:00
}
if err := d . Write ( lockfilePath , marshaledLockfile ) ; err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed writing lockfile: %w" , err )
2022-06-03 22:17:02 +00:00
}
return nil
}
2023-12-06 19:37:33 +00:00
func ( i * Installation ) Wipe ( ) error {
2023-12-28 00:13:09 +00:00
slog . Info ( "wiping installation" , slog . String ( "path" , i . Path ) )
2023-12-06 19:37:33 +00:00
d , err := i . GetDisk ( )
if err != nil {
return err
}
modsDirectory := filepath . Join ( i . BasePath ( ) , "FactoryGame" , "Mods" )
if err := d . Remove ( modsDirectory ) ; err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed removing Mods directory: %w" , err )
2023-12-06 19:37:33 +00:00
}
return nil
}
2024-05-05 21:01:17 +00:00
func ( i * Installation ) resolveProfile ( ctx * GlobalContext , platform * Platform ) ( * resolver . LockFile , error ) {
lockFile , err := i . lockfile ( ctx , platform )
2023-12-06 04:02:06 +00:00
if err != nil {
return nil , err
}
2024-08-14 00:27:07 +00:00
depResolver := resolver . NewDependencyResolver ( ctx . Provider )
2023-12-06 04:02:06 +00:00
2024-05-05 21:01:17 +00:00
gameVersion , err := i . getGameVersion ( platform )
2023-12-06 04:02:06 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to detect game version: %w" , err )
2023-12-06 04:02:06 +00:00
}
2023-12-16 11:59:58 +00:00
lockfile , err := ctx . Profiles . Profiles [ i . Profile ] . Resolve ( depResolver , lockFile , gameVersion )
2023-12-06 04:02:06 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "could not resolve mods: %w" , err )
2023-12-06 04:02:06 +00:00
}
2024-05-05 21:01:17 +00:00
if err := i . writeLockFile ( ctx , platform , lockfile ) ; err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to write lockfile: %w" , err )
2023-12-06 04:02:06 +00:00
}
return lockfile , nil
}
2024-05-05 21:01:17 +00:00
func ( i * Installation ) GetGameVersion ( ctx * GlobalContext ) ( int , error ) {
platform , err := i . GetPlatform ( ctx )
if err != nil {
return 0 , err
}
return i . getGameVersion ( platform )
}
func ( i * Installation ) LockFile ( ctx * GlobalContext ) ( * resolver . LockFile , error ) {
platform , err := i . GetPlatform ( ctx )
if err != nil {
return nil , err
}
return i . lockfile ( ctx , platform )
}
func ( i * Installation ) WriteLockFile ( ctx * GlobalContext , lockfile * resolver . LockFile ) error {
platform , err := i . GetPlatform ( ctx )
if err != nil {
return err
}
return i . writeLockFile ( ctx , platform , lockfile )
}
2023-12-06 23:39:34 +00:00
type InstallUpdateType string
var (
InstallUpdateTypeOverall InstallUpdateType = "overall"
InstallUpdateTypeModDownload InstallUpdateType = "download"
InstallUpdateTypeModExtract InstallUpdateType = "extract"
InstallUpdateTypeModComplete InstallUpdateType = "complete"
)
2022-06-03 22:17:02 +00:00
type InstallUpdate struct {
2023-12-06 23:39:34 +00:00
Type InstallUpdateType
Item InstallUpdateItem
Progress utils . GenericProgress
2022-06-03 22:17:02 +00:00
}
2023-12-06 23:39:34 +00:00
type InstallUpdateItem struct {
Mod string
Version string
}
func ( i * Installation ) Install ( ctx * GlobalContext , updates chan <- InstallUpdate ) error {
2023-12-07 16:57:31 +00:00
platform , err := i . GetPlatform ( ctx )
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to detect platform: %w" , err )
2023-12-07 16:57:31 +00:00
}
2023-12-16 11:59:58 +00:00
lockfile := resolver . NewLockfile ( )
2022-05-02 20:07:15 +00:00
2023-12-06 04:02:06 +00:00
if ! i . Vanilla {
var err error
2024-05-05 21:01:17 +00:00
lockfile , err = i . resolveProfile ( ctx , platform )
2023-12-06 04:02:06 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to resolve lockfile: %w" , err )
2023-12-06 04:02:06 +00:00
}
2022-05-02 20:07:15 +00:00
}
2022-06-22 22:24:35 +00:00
d , err := i . GetDisk ( )
if err != nil {
return err
}
modsDirectory := filepath . Join ( i . BasePath ( ) , "FactoryGame" , "Mods" )
if err := d . MkDir ( modsDirectory ) ; err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed creating Mods directory: %w" , err )
2022-05-02 20:07:15 +00:00
}
2022-06-22 22:24:35 +00:00
dir , err := d . ReadDir ( modsDirectory )
2022-06-03 22:17:02 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to read mods directory: %w" , err )
2022-06-03 22:17:02 +00:00
}
2024-05-05 21:01:17 +00:00
var deleteWait errgroup . Group
2022-06-03 22:17:02 +00:00
for _ , entry := range dir {
if entry . IsDir ( ) {
2024-10-04 17:23:51 +00:00
modName := entry . Name ( )
mod , hasMod := lockfile . Mods [ modName ]
if hasMod {
_ , hasTarget := mod . Targets [ platform . TargetName ]
hasMod = hasTarget
}
if ! hasMod {
2024-05-05 21:01:17 +00:00
modName := entry . Name ( )
modDir := filepath . Join ( modsDirectory , modName )
deleteWait . Go ( func ( ) error {
exists , err := d . Exists ( filepath . Join ( modDir , ".smm" ) )
if err != nil {
return err
}
2023-12-28 00:13:09 +00:00
2024-05-05 21:01:17 +00:00
if exists {
slog . Info ( "deleting mod" , slog . String ( "mod_reference" , modName ) )
if err := d . Remove ( modDir ) ; err != nil {
return fmt . Errorf ( "failed to delete mod directory: %w" , err )
}
2022-06-07 23:36:28 +00:00
}
2024-05-05 21:01:17 +00:00
return nil
} )
2022-06-03 22:17:02 +00:00
}
}
}
2024-05-05 21:01:17 +00:00
if err := deleteWait . Wait ( ) ; err != nil {
return fmt . Errorf ( "failed to remove old mods: %w" , err )
}
2023-12-16 14:19:53 +00:00
slog . Info ( "starting installation" , slog . Int ( "concurrency" , viper . GetInt ( "concurrent-downloads" ) ) , slog . String ( "path" , i . Path ) )
2022-06-18 16:09:09 +00:00
2023-12-06 23:39:34 +00:00
errg := errgroup . Group { }
channelUsers := sync . WaitGroup { }
downloadSemaphore := make ( chan int , viper . GetInt ( "concurrent-downloads" ) )
defer close ( downloadSemaphore )
2022-06-18 16:09:09 +00:00
2023-12-06 23:39:34 +00:00
var modComplete chan int
if updates != nil {
channelUsers . Add ( 1 )
modComplete = make ( chan int )
defer close ( modComplete )
2022-06-18 16:09:09 +00:00
go func ( ) {
2023-12-06 23:39:34 +00:00
defer channelUsers . Done ( )
completed := 0
for range modComplete {
completed ++
overallUpdate := InstallUpdate {
Type : InstallUpdateTypeOverall ,
Progress : utils . GenericProgress {
Completed : int64 ( completed ) ,
2023-12-07 16:57:31 +00:00
Total : int64 ( len ( lockfile . Mods ) ) ,
2023-12-06 23:39:34 +00:00
} ,
2022-06-18 16:09:09 +00:00
}
2023-12-06 23:39:34 +00:00
updates <- overallUpdate
2022-06-18 16:09:09 +00:00
}
} ( )
}
2023-12-07 16:57:31 +00:00
for modReference , version := range lockfile . Mods {
2023-12-06 23:39:34 +00:00
channelUsers . Add ( 1 )
modReference := modReference
version := version
errg . Go ( func ( ) error {
defer channelUsers . Done ( )
2023-12-07 16:57:31 +00:00
target , ok := version . Targets [ platform . TargetName ]
if ! ok {
2024-10-04 17:23:51 +00:00
// The resolver validates that the resulting lockfile mods can be installed on the sides where they are required
// so if the mod is missing this target, it means it is not required on this target
slog . Info ( "skipping mod not available for target" , slog . String ( "mod_reference" , modReference ) , slog . String ( "version" , version . Version ) , slog . String ( "target" , platform . TargetName ) )
return nil
2023-12-07 16:57:31 +00:00
}
2023-12-06 23:39:34 +00:00
// Only install if a link is provided, otherwise assume mod is already installed
2023-12-07 16:57:31 +00:00
if target . Link != "" {
2023-12-29 19:27:49 +00:00
err := downloadAndExtractMod ( modReference , version . Version , target . Link , target . Hash , platform . TargetName , modsDirectory , updates , downloadSemaphore , d )
2023-12-06 23:39:34 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to install %s@%s: %w" , modReference , version . Version , err )
2023-12-06 23:39:34 +00:00
}
2022-06-03 22:17:02 +00:00
}
2023-12-06 23:39:34 +00:00
if modComplete != nil {
modComplete <- 1
2022-05-02 20:07:15 +00:00
}
2023-12-06 23:39:34 +00:00
return nil
} )
}
2022-05-02 20:07:15 +00:00
2023-12-28 00:13:09 +00:00
if err := errg . Wait ( ) ; err != nil {
return fmt . Errorf ( "failed to install mods: %w" , err )
}
2023-12-06 23:39:34 +00:00
if updates != nil {
2023-12-28 00:13:09 +00:00
if i . Vanilla {
updates <- InstallUpdate {
Type : InstallUpdateTypeOverall ,
Progress : utils . GenericProgress {
Completed : 1 ,
Total : 1 ,
} ,
}
}
2023-12-06 23:39:34 +00:00
go func ( ) {
channelUsers . Wait ( )
close ( updates )
} ( )
}
2022-05-02 20:07:15 +00:00
2023-12-28 00:13:09 +00:00
slog . Info ( "installation completed" , slog . String ( "path" , i . Path ) )
2022-05-02 20:07:15 +00:00
2023-12-06 04:02:06 +00:00
return nil
2022-04-14 01:27:39 +00:00
}
2023-12-06 19:37:33 +00:00
func ( i * Installation ) UpdateMods ( ctx * GlobalContext , mods [ ] string ) error {
2024-05-05 21:01:17 +00:00
platform , err := i . GetPlatform ( ctx )
if err != nil {
return err
2023-12-06 19:37:33 +00:00
}
2024-05-05 21:01:17 +00:00
lockFile , err := i . lockfile ( ctx , platform )
2023-12-06 19:37:33 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to read lock file: %w" , err )
2023-12-06 19:37:33 +00:00
}
2024-08-14 00:27:07 +00:00
resolver := resolver . NewDependencyResolver ( ctx . Provider )
2023-12-06 19:37:33 +00:00
2024-05-05 21:01:17 +00:00
gameVersion , err := i . getGameVersion ( platform )
2023-12-06 19:37:33 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to detect game version: %w" , err )
2023-12-06 19:37:33 +00:00
}
profile := ctx . Profiles . GetProfile ( i . Profile )
if profile == nil {
return errors . New ( "could not find profile " + i . Profile )
}
for _ , modReference := range mods {
lockFile = lockFile . Remove ( modReference )
}
newLockFile , err := profile . Resolve ( resolver , lockFile , gameVersion )
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to resolve dependencies: %w" , err )
2023-12-06 19:37:33 +00:00
}
2024-05-05 21:01:17 +00:00
if err := i . writeLockFile ( ctx , platform , newLockFile ) ; err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to write lock file: %w" , err )
2023-12-06 19:37:33 +00:00
}
return nil
}
2023-12-29 19:27:49 +00:00
func downloadAndExtractMod ( modReference string , version string , link string , hash string , target string , modsDirectory string , updates chan <- InstallUpdate , downloadSemaphore chan int , d disk . Disk ) error {
2023-12-06 23:39:34 +00:00
var downloadUpdates chan utils . GenericProgress
2023-12-28 00:13:09 +00:00
var wg sync . WaitGroup
2023-12-06 23:39:34 +00:00
if updates != nil {
// Forward the inner updates as InstallUpdates
downloadUpdates = make ( chan utils . GenericProgress )
2023-12-28 00:13:09 +00:00
wg . Add ( 1 )
2023-12-06 23:39:34 +00:00
go func ( ) {
2023-12-28 00:13:09 +00:00
defer wg . Done ( )
2023-12-06 23:39:34 +00:00
for up := range downloadUpdates {
updates <- InstallUpdate {
Item : InstallUpdateItem {
Mod : modReference ,
Version : version ,
} ,
Type : InstallUpdateTypeModDownload ,
Progress : up ,
}
}
} ( )
}
2023-12-16 14:19:53 +00:00
slog . Info ( "downloading mod" , slog . String ( "mod_reference" , modReference ) , slog . String ( "version" , version ) , slog . String ( "link" , link ) )
2023-12-29 19:27:49 +00:00
reader , size , err := cache . DownloadOrCache ( modReference + "_" + version + "_" + target + ".zip" , hash , link , downloadUpdates , downloadSemaphore )
2023-12-06 23:39:34 +00:00
if err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "failed to download %s from: %s: %w" , modReference , link , err )
2023-12-06 23:39:34 +00:00
}
2023-12-07 16:57:31 +00:00
defer reader . Close ( )
2023-12-06 23:39:34 +00:00
var extractUpdates chan utils . GenericProgress
if updates != nil {
// Forward the inner updates as InstallUpdates
extractUpdates = make ( chan utils . GenericProgress )
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
for up := range extractUpdates {
2023-12-07 16:57:31 +00:00
updates <- InstallUpdate {
2023-12-06 23:39:34 +00:00
Item : InstallUpdateItem {
Mod : modReference ,
Version : version ,
} ,
Type : InstallUpdateTypeModExtract ,
Progress : up ,
}
}
} ( )
}
2023-12-16 14:19:53 +00:00
slog . Info ( "extracting mod" , slog . String ( "mod_reference" , modReference ) , slog . String ( "version" , version ) , slog . String ( "link" , link ) )
2023-12-06 23:39:34 +00:00
if err := utils . ExtractMod ( reader , size , filepath . Join ( modsDirectory , modReference ) , hash , extractUpdates , d ) ; err != nil {
2023-12-16 14:19:53 +00:00
return fmt . Errorf ( "could not extract %s: %w" , modReference , err )
2023-12-06 23:39:34 +00:00
}
if updates != nil {
2023-12-13 23:34:01 +00:00
close ( downloadUpdates )
close ( extractUpdates )
2023-12-07 16:57:31 +00:00
updates <- InstallUpdate {
2023-12-06 23:39:34 +00:00
Type : InstallUpdateTypeModComplete ,
Item : InstallUpdateItem {
Mod : modReference ,
Version : version ,
} ,
}
}
wg . Wait ( )
return nil
}
2022-04-14 01:27:39 +00:00
func ( i * Installation ) SetProfile ( ctx * GlobalContext , profile string ) error {
found := false
for _ , p := range ctx . Profiles . Profiles {
if p . Name == profile {
found = true
break
}
}
if ! found {
return errors . New ( "could not find profile: " + profile )
}
i . Profile = profile
2021-12-02 04:00:33 +00:00
return nil
2021-11-05 21:42:49 +00:00
}
2022-05-02 20:07:15 +00:00
type gameVersionFile struct {
2022-10-14 16:11:16 +00:00
BranchName string ` json:"BranchName" `
BuildID string ` json:"BuildId" `
2022-05-02 20:07:15 +00:00
MajorVersion int ` json:"MajorVersion" `
MinorVersion int ` json:"MinorVersion" `
PatchVersion int ` json:"PatchVersion" `
Changelist int ` json:"Changelist" `
CompatibleChangelist int ` json:"CompatibleChangelist" `
IsLicenseeVersion int ` json:"IsLicenseeVersion" `
IsPromotedBuild int ` json:"IsPromotedBuild" `
}
2024-05-05 21:01:17 +00:00
func ( i * Installation ) getGameVersion ( platform * Platform ) ( int , error ) {
2022-06-22 22:24:35 +00:00
d , err := i . GetDisk ( )
2022-05-02 20:07:15 +00:00
if err != nil {
2022-06-22 22:24:35 +00:00
return 0 , err
}
fullPath := filepath . Join ( i . BasePath ( ) , platform . VersionPath )
2023-12-28 00:13:09 +00:00
2022-06-22 22:24:35 +00:00
file , err := d . Read ( fullPath )
if err != nil {
2023-12-16 14:19:53 +00:00
return 0 , fmt . Errorf ( "failed reading version file: %w" , err )
2022-05-02 20:07:15 +00:00
}
var versionData gameVersionFile
if err := json . Unmarshal ( file , & versionData ) ; err != nil {
2023-12-16 14:19:53 +00:00
return 0 , fmt . Errorf ( "failed to parse version file json: %w" , err )
2022-05-02 20:07:15 +00:00
}
return versionData . Changelist , nil
}
func ( i * Installation ) GetPlatform ( ctx * GlobalContext ) ( * Platform , error ) {
if err := i . Validate ( ctx ) ; err != nil {
2023-12-16 14:19:53 +00:00
return nil , fmt . Errorf ( "failed to validate installation: %w" , err )
2022-05-02 20:07:15 +00:00
}
2022-06-22 22:24:35 +00:00
d , err := i . GetDisk ( )
if err != nil {
return nil , err
}
2022-05-02 20:07:15 +00:00
for _ , platform := range platforms {
2022-06-22 22:24:35 +00:00
fullPath := filepath . Join ( i . BasePath ( ) , platform . VersionPath )
2023-12-28 00:13:09 +00:00
exists , err := d . Exists ( fullPath )
if ! exists {
if err != nil {
return nil , fmt . Errorf ( "failed detecting version file: %w" , err )
2022-05-02 20:07:15 +00:00
}
2023-12-28 00:13:09 +00:00
continue
2022-05-02 20:07:15 +00:00
}
return & platform , nil
}
return nil , errors . New ( "no platform detected" )
}
2022-06-22 22:24:35 +00:00
func ( i * Installation ) GetDisk ( ) ( disk . Disk , error ) {
if i . DiskInstance != nil {
return i . DiskInstance , nil
}
var err error
i . DiskInstance , err = disk . FromPath ( i . Path )
return i . DiskInstance , err
}
func ( i * Installation ) BasePath ( ) string {
parsed , err := url . Parse ( i . Path )
if err != nil {
return i . Path
}
2022-06-22 23:05:35 +00:00
if parsed . Scheme != "ftp" && parsed . Scheme != "sftp" {
return i . Path
}
2022-06-22 22:24:35 +00:00
return parsed . Path
}