packwiz/core/pack.go
comp500 42d8370d0c Latest version fixes: correctly order preferences (fixes #198)
Both CurseForge and Modrinth preferences were not being done in a specific order - so
a file with a newer Minecraft version would not necessarily take priority over a file
with a more preferred loader (e.g. Quilt over Fabric) or a file with a newer release
date / CurseForge ID.
Also fixed a loop variable reference in the CurseForge loop, which caused eagerly resolved
(included in API response) file info to be inconsistent with the chosen version, and
added filtering for duplicate values in the acceptable-game-versions/primary version list
to ensure game versions are always compared properly (so the same index == same version).
2023-03-10 16:35:34 +00:00

209 lines
6.1 KiB
Go

package core
import (
"errors"
"fmt"
"golang.org/x/exp/slices"
"io"
"os"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
"github.com/Masterminds/semver/v3"
"github.com/spf13/viper"
)
// Pack stores the modpack metadata, usually in pack.toml
type Pack struct {
Name string `toml:"name"`
Author string `toml:"author,omitempty"`
Version string `toml:"version,omitempty"`
Description string `toml:"description,omitempty"`
PackFormat string `toml:"pack-format"`
Index struct {
// Path is stored in forward slash format relative to pack.toml
File string `toml:"file"`
HashFormat string `toml:"hash-format"`
Hash string `toml:"hash,omitempty"`
} `toml:"index"`
Versions map[string]string `toml:"versions"`
Export map[string]map[string]interface{} `toml:"export"`
Options map[string]interface{} `toml:"options"`
}
const CurrentPackFormat = "packwiz:1.1.0"
var PackFormatConstraintAccepted = mustParseConstraint("~1")
var PackFormatConstraintSuggestUpgrade = mustParseConstraint("~1.1")
func mustParseConstraint(s string) *semver.Constraints {
c, err := semver.NewConstraint(s)
if err != nil {
panic(err)
}
return c
}
// LoadPack loads the modpack metadata to a Pack struct
func LoadPack() (Pack, error) {
var modpack Pack
if _, err := toml.DecodeFile(viper.GetString("pack-file"), &modpack); err != nil {
return Pack{}, err
}
// Check pack-format
if len(modpack.PackFormat) == 0 {
fmt.Println("Modpack manifest has no pack-format field; assuming packwiz:1.1.0")
modpack.PackFormat = "packwiz:1.1.0"
}
// Auto-migrate versions
if modpack.PackFormat == "packwiz:1.0.0" {
fmt.Println("Automatically migrating pack to packwiz:1.1.0 format...")
modpack.PackFormat = "packwiz:1.1.0"
}
if !strings.HasPrefix(modpack.PackFormat, "packwiz:") {
return Pack{}, errors.New("pack-format field does not indicate a valid packwiz pack")
}
ver, err := semver.StrictNewVersion(strings.TrimPrefix(modpack.PackFormat, "packwiz:"))
if err != nil {
return Pack{}, fmt.Errorf("pack-format field is not valid semver: %w", err)
}
if !PackFormatConstraintAccepted.Check(ver) {
return Pack{}, errors.New("the modpack is incompatible with this version of packwiz; please update")
}
if !PackFormatConstraintSuggestUpgrade.Check(ver) {
fmt.Println("Modpack has a newer feature number than is supported by this version of packwiz. Update to the latest version of packwiz for new features and bugfixes!")
}
// TODO: suggest migration if necessary (primarily for 2.0.0)
// Read options into viper
if modpack.Options != nil {
err := viper.MergeConfigMap(modpack.Options)
if err != nil {
return Pack{}, err
}
}
if len(modpack.Index.File) == 0 {
modpack.Index.File = "index.toml"
}
return modpack, nil
}
// LoadIndex attempts to load the index file of this modpack
func (pack Pack) LoadIndex() (Index, error) {
if filepath.IsAbs(pack.Index.File) {
return LoadIndex(pack.Index.File)
}
fileNative := filepath.FromSlash(pack.Index.File)
return LoadIndex(filepath.Join(filepath.Dir(viper.GetString("pack-file")), fileNative))
}
// UpdateIndexHash recalculates the hash of the index file of this modpack
func (pack *Pack) UpdateIndexHash() error {
if viper.GetBool("no-internal-hashes") {
pack.Index.HashFormat = "sha256"
pack.Index.Hash = ""
return nil
}
fileNative := filepath.FromSlash(pack.Index.File)
indexFile := filepath.Join(filepath.Dir(viper.GetString("pack-file")), fileNative)
if filepath.IsAbs(pack.Index.File) {
indexFile = pack.Index.File
}
f, err := os.Open(indexFile)
if err != nil {
return err
}
// Hash usage strategy (may change):
// Just use SHA256, overwrite existing hash regardless of what it is
// May update later to continue using the same hash that was already being used
h, err := GetHashImpl("sha256")
if err != nil {
_ = f.Close()
return err
}
if _, err := io.Copy(h, f); err != nil {
_ = f.Close()
return err
}
hashString := h.HashToString(h.Sum(nil))
pack.Index.HashFormat = "sha256"
pack.Index.Hash = hashString
return f.Close()
}
// Write saves the pack file
func (pack Pack) Write() error {
f, err := os.Create(viper.GetString("pack-file"))
if err != nil {
return err
}
enc := toml.NewEncoder(f)
// Disable indentation
enc.Indent = ""
err = enc.Encode(pack)
if err != nil {
_ = f.Close()
return err
}
return f.Close()
}
// GetMCVersion gets the version of Minecraft this pack uses, if it has been correctly specified
func (pack Pack) GetMCVersion() (string, error) {
mcVersion, ok := pack.Versions["minecraft"]
if !ok {
return "", errors.New("no minecraft version specified in modpack")
}
return mcVersion, nil
}
// GetSupportedMCVersions gets the versions of Minecraft this pack allows in downloaded mods, ordered by preference (highest = most desirable)
func (pack Pack) GetSupportedMCVersions() ([]string, error) {
mcVersion, ok := pack.Versions["minecraft"]
if !ok {
return nil, errors.New("no minecraft version specified in modpack")
}
allVersions := append(append([]string(nil), viper.GetStringSlice("acceptable-game-versions")...), mcVersion)
// Deduplicate values
allVersionsDeduped := []string(nil)
for i, v := range allVersions {
// If another copy of this value exists past this point in the array, don't insert
// (i.e. prefer a later copy over an earlier copy, so the main version is last)
if !slices.Contains(allVersions[i+1:], v) {
allVersionsDeduped = append(allVersionsDeduped, v)
}
}
return allVersionsDeduped, nil
}
func (pack Pack) GetPackName() string {
if pack.Name == "" {
return "export"
} else if pack.Version == "" {
return pack.Name
} else {
return pack.Name + "-" + pack.Version
}
}
func (pack Pack) GetLoaders() (loaders []string) {
if _, hasQuilt := pack.Versions["quilt"]; hasQuilt {
loaders = append(loaders, "quilt")
loaders = append(loaders, "fabric") // Backwards-compatible; for now (could be configurable later)
} else if _, hasFabric := pack.Versions["fabric"]; hasFabric {
loaders = append(loaders, "fabric")
}
if _, hasForge := pack.Versions["forge"]; hasForge {
loaders = append(loaders, "forge")
}
return
}