mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-19 13:06:30 +02:00
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).
209 lines
6.1 KiB
Go
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
|
|
}
|