From 539be71d1176358954bab035f37815af53a9fabf Mon Sep 17 00:00:00 2001 From: Matt Artist Date: Wed, 12 Apr 2023 21:23:37 -0400 Subject: [PATCH] Add migrate command (#207) * Add base and loader command * Support updating to latest * Support explicit fabric versions * Support explicit Forge version * Support quilt * Add support for updating Minecraft * Add support for Forge recommended * Fix Forge version sorting * Various fixes * Add documentation to the use * More suggestions --- cmd/init.go | 57 ++------------- cmd/update.go | 10 +-- cmdshared/mcversion.go | 52 ++++++++++++++ cmdshared/utils.go | 16 +++++ core/versionutil.go | 35 +++++++++- main.go | 1 + migrate/loader.go | 154 +++++++++++++++++++++++++++++++++++++++++ migrate/migrate.go | 16 +++++ migrate/minecraft.go | 70 +++++++++++++++++++ 9 files changed, 354 insertions(+), 57 deletions(-) create mode 100644 cmdshared/mcversion.go create mode 100644 cmdshared/utils.go create mode 100644 migrate/loader.go create mode 100644 migrate/migrate.go create mode 100644 migrate/minecraft.go diff --git a/cmd/init.go b/cmd/init.go index 7af6f49..6615d2d 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -2,19 +2,16 @@ package cmd import ( "bufio" - "encoding/json" "fmt" - "os" - "path/filepath" - "sort" - "strings" - "time" - "github.com/fatih/camelcase" "github.com/igorsobreira/titlecase" + "github.com/packwiz/packwiz/cmdshared" "github.com/packwiz/packwiz/core" "github.com/spf13/cobra" "github.com/spf13/viper" + "os" + "path/filepath" + "strings" ) // initCmd represents the init command @@ -59,7 +56,7 @@ var initCmd = &cobra.Command{ version = initReadValue("Version [1.0.0]: ", "1.0.0") } - mcVersions, err := getValidMCVersions() + mcVersions, err := cmdshared.GetValidMCVersions() if err != nil { fmt.Printf("Failed to get latest minecraft versions: %s\n", err) os.Exit(1) @@ -79,7 +76,7 @@ var initCmd = &cobra.Command{ mcVersion = initReadValue("Minecraft version ["+latestVersion+"]: ", latestVersion) } } - mcVersions.checkValid(mcVersion) + mcVersions.CheckValid(mcVersion) modLoaderName := strings.ToLower(viper.GetString("init.modloader")) if len(modLoaderName) == 0 { @@ -239,45 +236,3 @@ func initReadValue(prompt string, def string) string { } return def } - -type mcVersionManifest struct { - Latest struct { - Release string `json:"release"` - Snapshot string `json:"snapshot"` - } `json:"latest"` - Versions []struct { - ID string `json:"id"` - Type string `json:"type"` - URL string `json:"url"` - Time time.Time `json:"time"` - ReleaseTime time.Time `json:"releaseTime"` - } `json:"versions"` -} - -func (m mcVersionManifest) checkValid(version string) { - for _, v := range m.Versions { - if v.ID == version { - return - } - } - fmt.Println("Given version is not a valid Minecraft version!") - os.Exit(1) -} - -func getValidMCVersions() (mcVersionManifest, error) { - res, err := core.GetWithUA("https://launchermeta.mojang.com/mc/game/version_manifest.json", "application/json") - if err != nil { - return mcVersionManifest{}, err - } - dec := json.NewDecoder(res.Body) - out := mcVersionManifest{} - err = dec.Decode(&out) - if err != nil { - return mcVersionManifest{}, err - } - // Sort by newest to oldest - sort.Slice(out.Versions, func(i, j int) bool { - return out.Versions[i].ReleaseTime.Before(out.Versions[j].ReleaseTime) - }) - return out, nil -} diff --git a/cmd/update.go b/cmd/update.go index 965c658..7bc38a1 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -9,8 +9,8 @@ import ( "os" ) -// updateCmd represents the update command -var updateCmd = &cobra.Command{ +// UpdateCmd represents the update command +var UpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Update an external file (or all external files) in the modpack", Aliases: []string{"upgrade"}, @@ -210,8 +210,8 @@ var updateCmd = &cobra.Command{ } func init() { - rootCmd.AddCommand(updateCmd) + rootCmd.AddCommand(UpdateCmd) - updateCmd.Flags().BoolP("all", "a", false, "Update all external files") - _ = viper.BindPFlag("update.all", updateCmd.Flags().Lookup("all")) + UpdateCmd.Flags().BoolP("all", "a", false, "Update all external files") + _ = viper.BindPFlag("update.all", UpdateCmd.Flags().Lookup("all")) } diff --git a/cmdshared/mcversion.go b/cmdshared/mcversion.go new file mode 100644 index 0000000..8c4afaf --- /dev/null +++ b/cmdshared/mcversion.go @@ -0,0 +1,52 @@ +package cmdshared + +import ( + "encoding/json" + "fmt" + "github.com/packwiz/packwiz/core" + "os" + "sort" + "time" +) + +type McVersionManifest struct { + Latest struct { + Release string `json:"release"` + Snapshot string `json:"snapshot"` + } `json:"latest"` + Versions []struct { + ID string `json:"id"` + Type string `json:"type"` + URL string `json:"url"` + Time time.Time `json:"time"` + ReleaseTime time.Time `json:"releaseTime"` + } `json:"versions"` +} + +func (m McVersionManifest) CheckValid(version string) { + for _, v := range m.Versions { + if v.ID == version { + return + } + } + fmt.Println("Given version is not a valid Minecraft version!") + os.Exit(1) +} + +func GetValidMCVersions() (McVersionManifest, error) { + res, err := core.GetWithUA("https://launchermeta.mojang.com/mc/game/version_manifest.json", "application/json") + if err != nil { + return McVersionManifest{}, err + } + dec := json.NewDecoder(res.Body) + out := McVersionManifest{} + err = dec.Decode(&out) + if err != nil { + return McVersionManifest{}, err + } + // Sort by newest to oldest + sort.Slice(out.Versions, func(i, j int) bool { + return out.Versions[i].ReleaseTime.Before(out.Versions[j].ReleaseTime) + }) + return out, nil +} diff --git a/cmdshared/utils.go b/cmdshared/utils.go new file mode 100644 index 0000000..7b7c6bd --- /dev/null +++ b/cmdshared/utils.go @@ -0,0 +1,16 @@ +package cmdshared + +import "strings" + +func GetRawForgeVersion(version string) string { + var wantedVersion string + // Check if we have a "-" in the version + if strings.Contains(version, "-") { + // We have a mcVersion-loaderVersion format + // Strip the mcVersion + wantedVersion = strings.Split(version, "-")[1] + } else { + wantedVersion = version + } + return wantedVersion +} diff --git a/core/versionutil.go b/core/versionutil.go index 0d5beb8..fe4ca5c 100644 --- a/core/versionutil.go +++ b/core/versionutil.go @@ -1,8 +1,10 @@ package core import ( + "encoding/json" "encoding/xml" "errors" + "fmt" "github.com/unascribed/FlexVer/go/flexver" "strings" ) @@ -95,7 +97,7 @@ func FetchMavenVersionPrefixedList(url string, friendlyName string) func(mcVersi return allowedVersions, out.Versioning.Latest, nil } // Sort list to get largest version - flexver.VersionSlice(out.Versioning.Versions.Version).Sort() + flexver.VersionSlice(allowedVersions).Sort() return allowedVersions, allowedVersions[len(allowedVersions)-1], nil } } @@ -158,3 +160,34 @@ func HighestSliceIndex(slice []string, values []string) int { } return highest } + +type ForgeRecommended struct { + Homepage string `json:"homepage"` + Versions map[string]string `json:"promos"` +} + +// GetForgeRecommended gets the recommended version of Forge for the given Minecraft version +func GetForgeRecommended(mcVersion string) string { + res, err := GetWithUA("https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json", "application/json") + if err != nil { + return "" + } + dec := json.NewDecoder(res.Body) + out := ForgeRecommended{} + err = dec.Decode(&out) + if err != nil { + fmt.Println(err) + return "" + } + // Get mcVersion-recommended, if it doesn't exist then get mcVersion-latest + // If neither exist, return empty string + recommendedString := fmt.Sprintf("%s-recommended", mcVersion) + if out.Versions[recommendedString] != "" { + return out.Versions[recommendedString] + } + latestString := fmt.Sprintf("%s-latest", mcVersion) + if out.Versions[latestString] != "" { + return out.Versions[latestString] + } + return "" +} diff --git a/main.go b/main.go index 33cc24c..82e35b8 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( // Modules of packwiz "github.com/packwiz/packwiz/cmd" _ "github.com/packwiz/packwiz/curseforge" + _ "github.com/packwiz/packwiz/migrate" _ "github.com/packwiz/packwiz/modrinth" _ "github.com/packwiz/packwiz/settings" _ "github.com/packwiz/packwiz/url" diff --git a/migrate/loader.go b/migrate/loader.go new file mode 100644 index 0000000..31d0405 --- /dev/null +++ b/migrate/loader.go @@ -0,0 +1,154 @@ +package migrate + +import ( + "fmt" + "github.com/packwiz/packwiz/cmdshared" + "github.com/packwiz/packwiz/core" + "github.com/spf13/cobra" + "golang.org/x/exp/slices" + "os" +) + +var loaderCommand = &cobra.Command{ + Use: "loader [version|latest|recommended]", + Short: "Migrate your modloader version to a newer version.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + modpack, err := core.LoadPack() + if err != nil { + // Check if it's a no such file or directory error + if os.IsNotExist(err) { + fmt.Println("No pack.toml file found, run 'packwiz init' to create one!") + os.Exit(1) + } + fmt.Printf("Error loading pack: %s\n", err) + os.Exit(1) + } + // Get our current loader, would use the modpack.GetLoaders function + // but, it supplements Quilt with Fabric, which isn't needed for this + var currentLoaders []string + // Add all the keys from modpack.Versions + for key := range modpack.Versions { + // If it ain't in core.ModLoaders, we don't want it + if _, ok := core.ModLoaders[key]; !ok { + continue + } + currentLoaders = append(currentLoaders, key) + } + // Do some sanity checks on the current loader slice + if len(currentLoaders) == 0 { + fmt.Println("No loader is currently set in your pack.toml!") + os.Exit(1) + } else if len(currentLoaders) > 1 { + fmt.Println("You have multiple loaders set in your pack.toml, this is not supported!") + os.Exit(1) + } + // Get the Minecraft version for the pack + mcVersion, err := modpack.GetMCVersion() + if err != nil { + fmt.Printf("Error getting Minecraft version: %s\n", err) + os.Exit(1) + } + if args[0] == "latest" { + fmt.Println("Updating to latest loader version") + // We'll be updating to the latest loader version + for _, loader := range currentLoaders { + _, latest, gottenLoader := getVersionsForLoader(loader, mcVersion) + if !updatePackToVersion(latest, modpack, gottenLoader) { + continue + } + // Write the pack to disk + err = modpack.Write() + if err != nil { + fmt.Printf("Error writing pack.toml: %s\n", err) + continue + } + } + } else if args[0] == "recommended" { + // TODO: Figure out a way to get the recommended version, this is Forge only + // Ensure we're on Forge + if !slices.Contains(currentLoaders, "forge") { + fmt.Println("The recommended loader version is only available on Forge!") + os.Exit(1) + } + // We'll be updating to the recommended loader version + recommendedVer := core.GetForgeRecommended(mcVersion) + if recommendedVer == "" { + fmt.Println("Error getting recommended Forge version!") + os.Exit(1) + } + if ok := updatePackToVersion(recommendedVer, modpack, core.ModLoaders["forge"]); !ok { + os.Exit(1) + } + // Write the pack to disk + err = modpack.Write() + if err != nil { + fmt.Printf("Error writing pack.toml: %s", err) + os.Exit(1) + } + } else { + fmt.Println("Updating to explicit loader version") + // This one is easy :D + versions, _, loader := getVersionsForLoader(currentLoaders[0], mcVersion) + // Check if the loader happens to be Forge, since there's two version formats + if loader.Name == "forge" { + wantedVersion := cmdshared.GetRawForgeVersion(args[0]) + validateVersion(versions, wantedVersion, loader) + _ = updatePackToVersion(wantedVersion, modpack, loader) + } else if loader.Name == "liteloader" { + // These are weird and just have a MC version + fmt.Println("LiteLoader only has 1 version per Minecraft version so we're unable to update!") + os.Exit(0) + } else { + // We're on Fabric or quilt + validateVersion(versions, args[0], loader) + if ok := updatePackToVersion(args[0], modpack, loader); !ok { + os.Exit(1) + } + } + // Write the pack to disk + err = modpack.Write() + if err != nil { + fmt.Printf("Error writing pack.toml: %s\n", err) + os.Exit(1) + } + } + }, +} + +func init() { + migrateCmd.AddCommand(loaderCommand) +} + +func getVersionsForLoader(loader, mcVersion string) ([]string, string, core.ModLoaderComponent) { + gottenLoader, ok := core.ModLoaders[loader] + if !ok { + fmt.Printf("Unknown loader %s\n", loader) + os.Exit(1) + } + versions, latestVersion, err := gottenLoader.VersionListGetter(mcVersion) + if err != nil { + fmt.Printf("Error getting version list for %s: %s\n", gottenLoader.FriendlyName, err) + os.Exit(1) + } + return versions, latestVersion, gottenLoader +} + +func validateVersion(versions []string, version string, gottenLoader core.ModLoaderComponent) { + if !slices.Contains(versions, version) { + fmt.Printf("Version %s is not a valid version for %s\n", version, gottenLoader.FriendlyName) + os.Exit(1) + } +} + +func updatePackToVersion(version string, modpack core.Pack, loader core.ModLoaderComponent) bool { + // Check if the version is already set + if version == modpack.Versions[loader.Name] { + fmt.Printf("%s is already on version %s!\n", loader.FriendlyName, version) + return false + } + // Set the latest version + modpack.Versions[loader.Name] = version + fmt.Printf("Updated %s to version %s\n", loader.FriendlyName, version) + return true +} diff --git a/migrate/migrate.go b/migrate/migrate.go new file mode 100644 index 0000000..1db6f67 --- /dev/null +++ b/migrate/migrate.go @@ -0,0 +1,16 @@ +package migrate + +import ( + "github.com/packwiz/packwiz/cmd" + "github.com/spf13/cobra" +) + +// migrateCmd represents the base command when called without any subcommands +var migrateCmd = &cobra.Command{ + Use: "migrate [minecraft|loader]", + Short: "Migrate your Minecraft and loader versions to newer versions.", +} + +func init() { + cmd.Add(migrateCmd) +} diff --git a/migrate/minecraft.go b/migrate/minecraft.go new file mode 100644 index 0000000..b2a419b --- /dev/null +++ b/migrate/minecraft.go @@ -0,0 +1,70 @@ +package migrate + +import ( + "fmt" + packCmd "github.com/packwiz/packwiz/cmd" + "github.com/packwiz/packwiz/cmdshared" + "github.com/packwiz/packwiz/core" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "os" +) + +var minecraftCommand = &cobra.Command{ + Use: "minecraft [version]", + Short: "Migrate your Minecraft version to a newer version.", + Aliases: []string{"mc"}, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + modpack, err := core.LoadPack() + if err != nil { + // Check if it's a no such file or directory error + if os.IsNotExist(err) { + fmt.Println("No pack.toml file found, run 'packwiz init' to create one!") + os.Exit(1) + } + fmt.Printf("Error loading pack: %s\n", err) + os.Exit(1) + } + currentVersion, err := modpack.GetMCVersion() + if err != nil { + fmt.Printf("Error getting Minecraft version from pack: %s\n", err) + os.Exit(1) + } + wantedMCVersion := args[0] + if wantedMCVersion == currentVersion { + fmt.Printf("Minecraft version is already %s!\n", wantedMCVersion) + os.Exit(0) + } + mcVersions, err := cmdshared.GetValidMCVersions() + if err != nil { + fmt.Printf("Error getting Minecraft versions: %s\n", err) + os.Exit(1) + } + mcVersions.CheckValid(wantedMCVersion) + // Set the version in the pack + modpack.Versions["minecraft"] = wantedMCVersion + // Write the pack to disk + err = modpack.Write() + if err != nil { + fmt.Printf("Error writing pack.toml: %s\n", err) + os.Exit(1) + } + fmt.Printf("Successfully updated Minecraft version to %s\n", wantedMCVersion) + // Prompt the user if they want to update the loader too while they're at it. + if cmdshared.PromptYesNo("Would you like to update your loader version to the latest version for this Minecraft version? [Y/n] ") { + // We'll run the loader command to update to latest + loaderCommand.Run(loaderCommand, []string{"latest"}) + } + // Prompt the user to update their mods too. + if cmdshared.PromptYesNo("Would you like to update your mods to the latest versions for this Minecraft version? [Y/n] ") { + // Run the update command + viper.Set("update.all", true) + packCmd.UpdateCmd.Run(packCmd.UpdateCmd, []string{}) + } + }, +} + +func init() { + migrateCmd.AddCommand(minecraftCommand) +}