diff --git a/core/hash.go b/core/hash.go new file mode 100644 index 0000000..ef05119 --- /dev/null +++ b/core/hash.go @@ -0,0 +1,23 @@ +package core + +import ( + "crypto/md5" + "crypto/sha256" + "crypto/sha512" + "errors" + "hash" +) + +// GetHashImpl gets an implementation of hash.Hash for the given hash type string +func GetHashImpl(hashType string) (hash.Hash, error) { + switch hashType { + case "sha256": + return sha256.New(), nil + case "sha512": + return sha512.New(), nil + case "md5": + return md5.New(), nil + } + // TODO: implement murmur2 + return nil, errors.New("hash implementation not found") +} diff --git a/core/index.go b/core/index.go index 1cd9b85..2eb558e 100644 --- a/core/index.go +++ b/core/index.go @@ -1,8 +1,8 @@ package core import ( - "crypto/sha256" "encoding/hex" + "errors" "io" "os" "path/filepath" @@ -130,7 +130,10 @@ func (in *Index) updateFile(path string) error { // 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 := sha256.New() + h, err := GetHashImpl("sha256") + if err != nil { + return err + } if _, err := io.Copy(h, f); err != nil { return err } @@ -310,3 +313,37 @@ func (in Index) GetAllMods() []string { } return list } + +// GetFilePath attempts to get the path of the destination index file as it is stored on disk +func (in Index) GetFilePath(f IndexFile) string { + return filepath.Join(filepath.Dir(in.indexFile), filepath.FromSlash(f.File)) +} + +// SaveFile attempts to read the file from disk +func (in Index) SaveFile(f IndexFile, dest io.Writer) error { + hashFormat := f.HashFormat + if hashFormat == "" { + hashFormat = in.HashFormat + } + src, err := os.Open(in.GetFilePath(f)) + if err != nil { + return err + } + h, err := GetHashImpl(hashFormat) + if err != nil { + return err + } + + w := io.MultiWriter(h, dest) + _, err = io.Copy(w, src) + if err != nil { + return err + } + + calculatedHash := hex.EncodeToString(h.Sum(nil)) + if calculatedHash != f.Hash { + return errors.New("hash of saved file is invalid") + } + + return nil +} diff --git a/core/mod.go b/core/mod.go index 2bfce5e..de08ef2 100644 --- a/core/mod.go +++ b/core/mod.go @@ -1,12 +1,13 @@ package core import ( - "crypto/sha256" "encoding/hex" "errors" "io" + "net/http" "os" "path/filepath" + "strconv" "github.com/BurntSushi/toml" ) @@ -37,6 +38,7 @@ type ModDownload struct { } // The three possible values of Side (the side that the mod is on) are "server", "client", and "both". +//noinspection GoUnusedConst const ( ServerSide = "server" ClientSide = "client" @@ -88,7 +90,10 @@ func (m Mod) Write() (string, string, error) { } defer f.Close() - h := sha256.New() + h, err := GetHashImpl("sha256") + if err != nil { + return "", "", err + } w := io.MultiWriter(h, f) enc := toml.NewEncoder(w) @@ -109,3 +114,36 @@ func (m Mod) GetParsedUpdateData(updaterName string) (interface{}, bool) { func (m Mod) GetFilePath() string { return m.metaFile } + +// GetDestFilePath returns the path of the destination file of the mod +func (m Mod) GetDestFilePath() string { + return filepath.Join(filepath.Dir(m.metaFile), filepath.FromSlash(m.FileName)) +} + +// DownloadFile attempts to resolve and download the file +func (m Mod) DownloadFile(dest io.Writer) error { + resp, err := http.Get(m.Download.URL) + if err != nil { + return err + } + if resp.StatusCode != 200 { + _ = resp.Body.Close() + return errors.New("invalid status code " + strconv.Itoa(resp.StatusCode)) + } + h, err := GetHashImpl(m.Download.HashFormat) + if err != nil { + return err + } + + w := io.MultiWriter(h, dest) + _, err = io.Copy(w, resp.Body) + if err != nil { + return err + } + + calculatedHash := hex.EncodeToString(h.Sum(nil)) + if calculatedHash != m.Download.Hash { + return errors.New("hash of saved file is invalid") + } + return nil +} diff --git a/core/pack.go b/core/pack.go index ec8dbd0..972fe3b 100644 --- a/core/pack.go +++ b/core/pack.go @@ -1,7 +1,6 @@ package core import ( - "crypto/sha256" "encoding/hex" "errors" "io" @@ -65,7 +64,10 @@ func (pack *Pack) UpdateIndexHash() error { // 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 := sha256.New() + h, err := GetHashImpl("sha256") + if err != nil { + return err + } if _, err := io.Copy(h, f); err != nil { return err } diff --git a/curseforge/export.go b/curseforge/export.go index 7e951a9..7d5bd09 100644 --- a/curseforge/export.go +++ b/curseforge/export.go @@ -1,8 +1,15 @@ package curseforge import ( + "archive/zip" + "bufio" "fmt" + "github.com/spf13/viper" "os" + "path/filepath" + "strconv" + + "github.com/comp500/packwiz/curseforge/packinterop" "github.com/comp500/packwiz/core" "github.com/spf13/cobra" @@ -12,7 +19,7 @@ import ( var exportCmd = &cobra.Command{ Use: "export", Short: "Export the current modpack into a .zip for curseforge", - // TODO: argument for file name + // TODO: arguments for file name, author? projectID? Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Loading modpack...") @@ -27,6 +34,155 @@ var exportCmd = &cobra.Command{ os.Exit(1) } - _ = index + // TODO: should index just expose indexPath itself, through a function? + indexPath := filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File)) + + // TODO: filter mods for optional/server/etc + mods := loadMods(index) + + expFile, err := os.Create("export.zip") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer expFile.Close() + exp := zip.NewWriter(expFile) + defer exp.Close() + + cfFileRefs := make([]packinterop.AddonFileReference, 0, len(mods)) + for _, mod := range mods { + projectRaw, ok := mod.GetParsedUpdateData("curseforge") + // If the mod has curseforge metadata, add it to cfFileRefs + // TODO: how to handle files with CF metadata, but with different download path? + if ok { + p := projectRaw.(cfUpdateData) + cfFileRefs = append(cfFileRefs, packinterop.AddonFileReference{ProjectID: p.ProjectID, FileID: p.FileID}) + } else { + // If the mod doesn't have the metadata, save it into the zip + path, err := filepath.Rel(filepath.Dir(indexPath), mod.GetDestFilePath()) + if err != nil { + fmt.Printf("Error resolving mod file: %s\n", err.Error()) + // TODO: exit(1)? + continue + } + modFile, err := exp.Create(filepath.ToSlash(filepath.Join("overrides", path))) + if err != nil { + fmt.Printf("Error creating mod file: %s\n", err.Error()) + // TODO: exit(1)? + continue + } + err = mod.DownloadFile(modFile) + if err != nil { + fmt.Printf("Error downloading mod file: %s\n", err.Error()) + // TODO: exit(1)? + continue + } + } + } + + manifestFile, err := exp.Create("manifest.json") + if err != nil { + fmt.Println("Error creating manifest: " + err.Error()) + os.Exit(1) + } + + err = packinterop.WriteManifestFromPack(pack, cfFileRefs, manifestFile) + if err != nil { + fmt.Println("Error creating manifest: " + err.Error()) + os.Exit(1) + } + + err = createModlist(exp, mods) + if err != nil { + fmt.Println("Error creating mod list: " + err.Error()) + os.Exit(1) + } + + i := 0 + for _, v := range index.Files { + if !v.MetaFile { + // Save all non-metadata files into the zip + path, err := filepath.Rel(filepath.Dir(indexPath), index.GetFilePath(v)) + if err != nil { + fmt.Printf("Error resolving file: %s\n", err.Error()) + // TODO: exit(1)? + continue + } + file, err := exp.Create(filepath.ToSlash(filepath.Join("overrides", path))) + if err != nil { + fmt.Printf("Error creating file: %s\n", err.Error()) + // TODO: exit(1)? + continue + } + err = index.SaveFile(v, file) + if err != nil { + fmt.Printf("Error copying file: %s\n", err.Error()) + // TODO: exit(1)? + continue + } + i++ + } + } + + fmt.Println("Modpack exported to export.zip!") }, } + +func createModlist(zw *zip.Writer, mods []core.Mod) error { + modlistFile, err := zw.Create("modlist.html") + if err != nil { + return err + } + + w := bufio.NewWriter(modlistFile) + + _, err = w.WriteString("\r\n") + if err != nil { + return err + } + return w.Flush() +} + +func loadMods(index core.Index) []core.Mod { + modPaths := index.GetAllMods() + mods := make([]core.Mod, len(modPaths)) + i := 0 + fmt.Println("Reading mod files...") + for _, v := range modPaths { + modData, err := core.LoadMod(v) + if err != nil { + fmt.Printf("Error reading mod file: %s\n", err.Error()) + // TODO: exit(1)? + continue + } + + mods[i] = modData + i++ + } + return mods[:i] +} + +func init() { + curseforgeCmd.AddCommand(exportCmd) +} diff --git a/curseforge/import.go b/curseforge/import.go index d5bea39..5c95401 100644 --- a/curseforge/import.go +++ b/curseforge/import.go @@ -4,9 +4,9 @@ import ( "archive/zip" "bufio" "bytes" - "encoding/json" "errors" "fmt" + "github.com/comp500/packwiz/curseforge/packinterop" "io" "io/ioutil" "os" @@ -19,43 +19,20 @@ import ( "github.com/spf13/viper" ) -// TODO: this file is a mess, I need to refactor it -// TODO: test modpack importing before proceeding with further implementation - -type importPackFile interface { - Name() string - Open() (io.ReadCloser, error) -} - -type importPackMetadata interface { - Name() string - Versions() map[string]string - Mods() []struct { - ModID int - FileID int - } - GetFiles() ([]importPackFile, error) -} - -type importPackSource interface { - GetFile(path string) (importPackFile, error) - //TODO: was GetFileList(base string), is it needed? - GetFileList() ([]importPackFile, error) - GetPackFile() importPackFile -} - // importCmd represents the import command var importCmd = &cobra.Command{ Use: "import [modpack]", - Short: "Import an installed curseforge modpack, from a download URL or a downloaded pack zip, or an installed metadata json file", + Short: "Import a curseforge modpack, from a download URL or a downloaded pack zip, or an installed metadata json file", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { inputFile := args[0] - var packImport importPackMetadata + var packImport packinterop.ImportPackMetadata + // TODO: refactor/extract file checking? if strings.HasPrefix(inputFile, "http") { - fmt.Println("it do be a http doe") - os.Exit(0) + // TODO: implement + fmt.Println("HTTP not supported (yet)") + os.Exit(1) } else { // Attempt to read from file var f *os.File @@ -140,8 +117,7 @@ var importCmd = &cobra.Command{ // Search the zip for minecraftinstance.json or manifest.json var metaFile *zip.File for _, v := range zr.File { - fileName := filepath.Base(v.Name) - if fileName == "minecraftinstance.json" || fileName == "manifest.json" { + if v.Name == "minecraftinstance.json" || v.Name == "manifest.json" { metaFile = v } } @@ -151,17 +127,9 @@ var importCmd = &cobra.Command{ os.Exit(1) } - packImport = readMetadata(zipPackSource{ - MetaFile: metaFile, - Reader: zr, - }) - + packImport = packinterop.ReadMetadata(packinterop.GetZipPackSource(metaFile, zr)) } else { - packImport = readMetadata(diskPackSource{ - MetaSource: buf, - MetaName: inputFile, // TODO: is this always the correct file? - BasePath: filepath.Dir(inputFile), - }) + packImport = packinterop.ReadMetadata(packinterop.GetDiskPackSource(buf, filepath.ToSlash(filepath.Base(inputFile)), filepath.Dir(inputFile))) } } @@ -255,13 +223,10 @@ var importCmd = &cobra.Command{ os.Exit(1) } + // TODO: just use mods-folder directly? does texture pack importing affect this? ref, err := filepath.Abs(filepath.Join(filepath.Dir(core.ResolveMod(modInfoValue.Slug)), fileInfo.FileName)) if err == nil { referencedModPaths = append(referencedModPaths, ref) - if len(ref) == 0 { - fmt.Println(core.ResolveMod(modInfoValue.Slug)) - fmt.Println(filepath.Dir(core.ResolveMod(modInfoValue.Slug))) - } } fmt.Printf("Imported mod \"%s\" successfully!\n", modInfoValue.Name) @@ -280,10 +245,7 @@ var importCmd = &cobra.Command{ successes = 0 indexFolder := filepath.Dir(filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File))) for _, v := range filesList { - filePath := v.Name() - if !filepath.IsAbs(filePath) { - filePath = filepath.Join(indexFolder, v.Name()) - } + filePath := filepath.Join(indexFolder, filepath.FromSlash(v.Name())) filePathAbs, err := filepath.Abs(filePath) if err == nil { found := false @@ -298,12 +260,12 @@ var importCmd = &cobra.Command{ successes++ continue } - if filepath.Base(filePathAbs) == "minecraftinstance.json" { + if v.Name() == "minecraftinstance.json" { fmt.Println("Ignored file \"minecraftinstance.json\"") successes++ continue } - if filepath.Base(filePathAbs) == "manifest.json" { + if v.Name() == "manifest.json" { fmt.Println("Ignored file \"manifest.json\"") successes++ continue @@ -376,240 +338,3 @@ var importCmd = &cobra.Command{ func init() { curseforgeCmd.AddCommand(importCmd) } - -type diskFile struct { - NameInternal string - Path string -} - -func (f diskFile) Name() string { - return f.NameInternal -} - -func (f diskFile) Open() (io.ReadCloser, error) { - return os.Open(f.Path) -} - -type zipReaderFile struct { - NameInternal string - *zip.File -} - -func (f zipReaderFile) Name() string { - return f.NameInternal -} - -type readerFile struct { - NameInternal string - Reader *io.ReadCloser -} - -func (f readerFile) Name() string { - return f.NameInternal -} - -func (f readerFile) Open() (io.ReadCloser, error) { - return *f.Reader, nil -} - -func diskFilesFromPath(base string) ([]importPackFile, error) { - list := make([]importPackFile, 0) - err := filepath.Walk(base, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - name, err := filepath.Rel(base, path) - if err != nil { - return err - } - list = append(list, diskFile{name, path}) - return nil - }) - if err != nil { - return nil, err - } - return list, nil -} - -type diskPackSource struct { - MetaSource *bufio.Reader - MetaName string - BasePath string -} - -func (s diskPackSource) GetFile(path string) (importPackFile, error) { - return diskFile{s.BasePath, path}, nil -} - -func (s diskPackSource) GetFileList() ([]importPackFile, error) { - return diskFilesFromPath(s.BasePath) -} - -func (s diskPackSource) GetPackFile() importPackFile { - rc := ioutil.NopCloser(s.MetaSource) - return readerFile{s.MetaName, &rc} -} - -type zipPackSource struct { - MetaFile *zip.File - Reader *zip.Reader - cachedFileList []importPackFile -} - -func (s zipPackSource) GetFile(path string) (importPackFile, error) { - if s.cachedFileList == nil { - s.cachedFileList = make([]importPackFile, len(s.Reader.File)) - for i, v := range s.Reader.File { - s.cachedFileList[i] = zipReaderFile{v.Name, v} - } - } - for _, v := range s.cachedFileList { - if v.Name() == path { - return v, nil - } - } - return zipReaderFile{}, errors.New("file not found in zip") -} - -func (s zipPackSource) GetFileList() ([]importPackFile, error) { - if s.cachedFileList == nil { - s.cachedFileList = make([]importPackFile, len(s.Reader.File)) - for i, v := range s.Reader.File { - s.cachedFileList[i] = zipReaderFile{v.Name, v} - } - } - return s.cachedFileList, nil -} - -func (s zipPackSource) GetPackFile() importPackFile { - return zipReaderFile{s.MetaFile.Name, s.MetaFile} -} - -type twitchInstalledPackMeta struct { - NameInternal string `json:"name"` - Path string `json:"installPath"` - // TODO: javaArgsOverride? - // TODO: allocatedMemory? - MCVersion string `json:"gameVersion"` - Modloader struct { - Name string `json:"name"` - MavenVersionString string `json:"mavenVersionString"` - } `json:"baseModLoader"` - ModpackOverrides []string `json:"modpackOverrides"` - ModsInternal []struct { - ID int `json:"addonID"` - File struct { - // I've given up on using this cached data, just going to re-request it - ID int `json:"id"` - } `json:"installedFile"` - } `json:"installedAddons"` - // Used to determine if modpackOverrides should be used or not - IsUnlocked bool `json:"isUnlocked"` - srcFile string -} - -func (m twitchInstalledPackMeta) Name() string { - return m.NameInternal -} - -func (m twitchInstalledPackMeta) Versions() map[string]string { - vers := make(map[string]string) - vers["minecraft"] = m.MCVersion - if strings.HasPrefix(m.Modloader.Name, "forge") { - if len(m.Modloader.MavenVersionString) > 0 { - vers["forge"] = strings.TrimPrefix(m.Modloader.MavenVersionString, "net.minecraftforge:forge:") - } else { - vers["forge"] = m.MCVersion + "-" + strings.TrimPrefix(m.Modloader.Name, "forge-") - } - } - return vers -} - -func (m twitchInstalledPackMeta) Mods() []struct { - ModID int - FileID int -} { - list := make([]struct { - ModID int - FileID int - }, len(m.ModsInternal)) - for i, v := range m.ModsInternal { - list[i] = struct { - ModID int - FileID int - }{ - ModID: v.ID, - FileID: v.File.ID, - } - } - return list -} - -func (m twitchInstalledPackMeta) GetFiles() ([]importPackFile, error) { - dir := filepath.Dir(m.srcFile) - if _, err := os.Stat(m.Path); err == nil { - dir = m.Path - } - if m.IsUnlocked { - return diskFilesFromPath(dir) - } - list := make([]importPackFile, len(m.ModpackOverrides)) - for i, v := range m.ModpackOverrides { - list[i] = diskFile{ - Path: filepath.Join(dir, v), - NameInternal: v, - } - } - return list, nil -} - -func readMetadata(s importPackSource) importPackMetadata { - var packImport importPackMetadata - metaFile := s.GetPackFile() - rdr, err := metaFile.Open() - if err != nil { - fmt.Printf("Error reading file: %s\n", err) - os.Exit(1) - } - - // Read the whole file (as we are going to parse it multiple times) - fileData, err := ioutil.ReadAll(rdr) - if err != nil { - fmt.Printf("Error reading file: %s\n", err) - os.Exit(1) - } - - // Determine what format the file is - var jsonFile map[string]interface{} - err = json.Unmarshal(fileData, &jsonFile) - if err != nil { - fmt.Printf("Error parsing JSON: %s\n", err) - os.Exit(1) - } - - isManifest := false - if v, ok := jsonFile["manifestType"]; ok { - isManifest = v.(string) == "minecraftModpack" - } - if isManifest { - fmt.Println("it do be a manifest doe") - os.Exit(0) - // TODO: implement manifest parsing - } else { - // Replace FileNameOnDisk with fileNameOnDisk - fileData = bytes.ReplaceAll(fileData, []byte("FileNameOnDisk"), []byte("fileNameOnDisk")) - packMeta := twitchInstalledPackMeta{} - err = json.Unmarshal(fileData, &packMeta) - if err != nil { - fmt.Printf("Error parsing JSON: %s\n", err) - os.Exit(1) - } - packMeta.srcFile = metaFile.Name() - packImport = packMeta - } - - return packImport -} diff --git a/curseforge/packinterop/disk.go b/curseforge/packinterop/disk.go new file mode 100644 index 0000000..f302bd1 --- /dev/null +++ b/curseforge/packinterop/disk.go @@ -0,0 +1,81 @@ +package packinterop + +import ( + "bufio" + "io" + "io/ioutil" + "os" + "path/filepath" +) + +type diskFile struct { + NameInternal string + Path string +} + +func (f diskFile) Name() string { + return f.NameInternal +} + +func (f diskFile) Open() (io.ReadCloser, error) { + return os.Open(f.Path) +} + +type readerFile struct { + NameInternal string + Reader *io.ReadCloser +} + +func (f readerFile) Name() string { + return f.NameInternal +} + +func (f readerFile) Open() (io.ReadCloser, error) { + return *f.Reader, nil +} + +type diskPackSource struct { + MetaSource *bufio.Reader + MetaName string + BasePath string +} + +func (s diskPackSource) GetFile(path string) (ImportPackFile, error) { + return diskFile{path, filepath.Join(s.BasePath, filepath.FromSlash(path))}, nil +} + +func (s diskPackSource) GetFileList() ([]ImportPackFile, error) { + list := make([]ImportPackFile, 0) + err := filepath.Walk(s.BasePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + // Get the name of the file, relative to the pack folder + name, err := filepath.Rel(s.BasePath, path) + if err != nil { + return err + } + list = append(list, diskFile{filepath.ToSlash(name), path}) + return nil + }) + if err != nil { + return nil, err + } + return list, nil +} + +func (s diskPackSource) GetPackFile() ImportPackFile { + rc := ioutil.NopCloser(s.MetaSource) + return readerFile{s.MetaName, &rc} +} + +func GetDiskPackSource(metaSource *bufio.Reader, metaName string, basePath string) ImportPackSource { + return diskPackSource{ + MetaSource: metaSource, + MetaName: metaName, + BasePath: basePath, + } +} diff --git a/curseforge/packinterop/interfaces.go b/curseforge/packinterop/interfaces.go new file mode 100644 index 0000000..beca2c9 --- /dev/null +++ b/curseforge/packinterop/interfaces.go @@ -0,0 +1,25 @@ +package packinterop + +import "io" + +type ImportPackFile interface { + Name() string + Open() (io.ReadCloser, error) +} + +type ImportPackMetadata interface { + Name() string + Versions() map[string]string + // TODO: use AddonFileReference? + Mods() []struct { + ModID int + FileID int + } + GetFiles() ([]ImportPackFile, error) +} + +type ImportPackSource interface { + GetFile(path string) (ImportPackFile, error) + GetFileList() ([]ImportPackFile, error) + GetPackFile() ImportPackFile +} diff --git a/curseforge/packinterop/manifest.go b/curseforge/packinterop/manifest.go new file mode 100644 index 0000000..06a226b --- /dev/null +++ b/curseforge/packinterop/manifest.go @@ -0,0 +1,104 @@ +package packinterop + +import "strings" + +type cursePackMeta struct { + Minecraft struct { + Version string `json:"version"` + ModLoaders []modLoaderDef `json:"modLoaders"` + } `json:"minecraft"` + ManifestType string `json:"manifestType"` + ManifestVersion int `json:"manifestVersion"` + NameInternal string `json:"name"` + Version string `json:"version"` + Author string `json:"author"` + ProjectID int `json:"projectID"` + Files []struct { + ProjectID int `json:"projectID"` + FileID int `json:"fileID"` + Required bool `json:"required"` + } `json:"files"` + Overrides string `json:"overrides"` + importSrc ImportPackSource +} + +type modLoaderDef struct { + ID string `json:"id"` + Primary bool `json:"primary"` +} + +func (c cursePackMeta) Name() string { + return c.NameInternal +} + +func (c cursePackMeta) Versions() map[string]string { + vers := make(map[string]string) + vers["minecraft"] = c.Minecraft.Version + for _, v := range c.Minecraft.ModLoaders { + // Seperate dash-separated modloader/version pairs + parts := strings.SplitN(v.ID, "-", 2) + if len(parts) == 2 { + vers[parts[0]] = parts[1] + } + } + if val, ok := vers["forge"]; ok { + // Remove the minecraft version prefix, if it exists + vers["forge"] = strings.TrimPrefix(val, c.Minecraft.Version+"-") + } + return vers +} + +func (c cursePackMeta) Mods() []struct { + ModID int + FileID int +} { + list := make([]struct { + ModID int + FileID int + }, len(c.Files)) + for i, v := range c.Files { + list[i] = struct { + ModID int + FileID int + }{ + ModID: v.ProjectID, + FileID: v.FileID, + } + } + return list +} + +type cursePackOverrideWrapper struct { + name string + ImportPackFile +} + +func (w cursePackOverrideWrapper) Name() string { + return w.name +} + +func (c cursePackMeta) GetFiles() ([]ImportPackFile, error) { + // Only import files from overrides directory + if len(c.Overrides) > 0 { + fullList, err := c.importSrc.GetFileList() + if err != nil { + return nil, err + } + overridesList := make([]ImportPackFile, 0, len(fullList)) + overridesPath := c.Overrides + if !strings.HasSuffix(overridesPath, "/") { + overridesPath = c.Overrides + "/" + } + // Wrap files, removing overrides/ from the start + for _, v := range fullList { + if strings.HasPrefix(v.Name(), overridesPath) { + overridesList = append(overridesList, cursePackOverrideWrapper{ + name: strings.TrimPrefix(v.Name(), overridesPath), + ImportPackFile: v, + }) + } + } + return overridesList, nil + } + return []ImportPackFile{}, nil +} diff --git a/curseforge/packinterop/minecraftinstance.go b/curseforge/packinterop/minecraftinstance.go new file mode 100644 index 0000000..0e161df --- /dev/null +++ b/curseforge/packinterop/minecraftinstance.go @@ -0,0 +1,85 @@ +package packinterop + +import ( + "path/filepath" + "strings" +) + +type twitchInstalledPackMeta struct { + NameInternal string `json:"name"` + Path string `json:"installPath"` + // TODO: javaArgsOverride? + // TODO: allocatedMemory? + MCVersion string `json:"gameVersion"` + Modloader struct { + Name string `json:"name"` + MavenVersionString string `json:"mavenVersionString"` + } `json:"baseModLoader"` + ModpackOverrides []string `json:"modpackOverrides"` + ModsInternal []struct { + ID int `json:"addonID"` + File struct { + // I've given up on using this cached data, just going to re-request it + ID int `json:"id"` + } `json:"installedFile"` + } `json:"installedAddons"` + // Used to determine if modpackOverrides should be used or not + IsUnlocked bool `json:"isUnlocked"` + importSrc ImportPackSource +} + +func (m twitchInstalledPackMeta) Name() string { + return m.NameInternal +} + +func (m twitchInstalledPackMeta) Versions() map[string]string { + vers := make(map[string]string) + vers["minecraft"] = m.MCVersion + if strings.HasPrefix(m.Modloader.Name, "forge") { + if len(m.Modloader.MavenVersionString) > 0 { + vers["forge"] = strings.TrimPrefix(m.Modloader.MavenVersionString, "net.minecraftforge:forge:") + } else { + vers["forge"] = strings.TrimPrefix(m.Modloader.Name, "forge-") + } + // Remove the minecraft version prefix, if it exists + vers["forge"] = strings.TrimPrefix(vers["forge"], m.MCVersion+"-") + } + return vers +} + +func (m twitchInstalledPackMeta) Mods() []struct { + ModID int + FileID int +} { + list := make([]struct { + ModID int + FileID int + }, len(m.ModsInternal)) + for i, v := range m.ModsInternal { + list[i] = struct { + ModID int + FileID int + }{ + ModID: v.ID, + FileID: v.File.ID, + } + } + return list +} + +func (m twitchInstalledPackMeta) GetFiles() ([]ImportPackFile, error) { + // If the modpack is unlocked, import all the files + // Otherwise import just the modpack overrides + if m.IsUnlocked { + return m.importSrc.GetFileList() + } + list := make([]ImportPackFile, len(m.ModpackOverrides)) + var err error + for i, v := range m.ModpackOverrides { + list[i], err = m.importSrc.GetFile(filepath.ToSlash(v)) + if err != nil { + return nil, err + } + } + return list, nil +} diff --git a/curseforge/packinterop/translation.go b/curseforge/packinterop/translation.go new file mode 100644 index 0000000..c7f454a --- /dev/null +++ b/curseforge/packinterop/translation.go @@ -0,0 +1,119 @@ +package packinterop + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/comp500/packwiz/core" + "io" + "io/ioutil" + "os" +) + +func ReadMetadata(s ImportPackSource) ImportPackMetadata { + var packImport ImportPackMetadata + metaFile := s.GetPackFile() + rdr, err := metaFile.Open() + if err != nil { + fmt.Printf("Error reading file: %s\n", err) + os.Exit(1) + } + + // Read the whole file (as we are going to parse it multiple times) + fileData, err := ioutil.ReadAll(rdr) + if err != nil { + fmt.Printf("Error reading file: %s\n", err) + os.Exit(1) + } + + // Determine what format the file is + var jsonFile map[string]interface{} + err = json.Unmarshal(fileData, &jsonFile) + if err != nil { + fmt.Printf("Error parsing JSON: %s\n", err) + os.Exit(1) + } + + isManifest := false + if v, ok := jsonFile["manifestType"]; ok { + isManifest = v.(string) == "minecraftModpack" + } + if isManifest { + packMeta := cursePackMeta{importSrc: s} + err = json.Unmarshal(fileData, &packMeta) + if err != nil { + fmt.Printf("Error parsing JSON: %s\n", err) + os.Exit(1) + } + packImport = packMeta + } else { + // Replace FileNameOnDisk with fileNameOnDisk + fileData = bytes.ReplaceAll(fileData, []byte("FileNameOnDisk"), []byte("fileNameOnDisk")) + packMeta := twitchInstalledPackMeta{importSrc: s} + err = json.Unmarshal(fileData, &packMeta) + if err != nil { + fmt.Printf("Error parsing JSON: %s\n", err) + os.Exit(1) + } + packImport = packMeta + } + + return packImport +} + +// AddonFileReference is a pair of Project ID and File ID to reference a single file on CurseForge +type AddonFileReference struct { + ProjectID int + FileID int +} + +func WriteManifestFromPack(pack core.Pack, fileRefs []AddonFileReference, out io.Writer) error { + // TODO: should Required be false sometimes? + files := make([]struct { + ProjectID int `json:"projectID"` + FileID int `json:"fileID"` + Required bool `json:"required"` + }, len(fileRefs)) + for i, fr := range fileRefs { + files[i] = struct { + ProjectID int `json:"projectID"` + FileID int `json:"fileID"` + Required bool `json:"required"` + }{ProjectID: fr.ProjectID, FileID: fr.FileID, Required: true} + } + + modLoaders := make([]modLoaderDef, 0, 1) + forgeVersion, ok := pack.Versions["forge"] + if ok { + modLoaders = append(modLoaders, modLoaderDef{ + ID: "forge-" + forgeVersion, + Primary: true, + }) + } + + manifest := cursePackMeta{ + Minecraft: struct { + Version string `json:"version"` + ModLoaders []modLoaderDef `json:"modLoaders"` + }{ + Version: pack.Versions["minecraft"], + ModLoaders: modLoaders, + }, + ManifestType: "minecraftModpack", + ManifestVersion: 1, + NameInternal: pack.Name, + Version: "", // TODO: store or take this? + Author: "", // TODO: store or take this? + ProjectID: 0, // TODO: store or take this? + Files: files, + Overrides: "overrides", + } + + w := json.NewEncoder(out) + w.SetIndent("", " ") // Match CF export + err := w.Encode(manifest) + if err != nil { + return err + } + return nil +} diff --git a/curseforge/packinterop/zip.go b/curseforge/packinterop/zip.go new file mode 100644 index 0000000..73c311f --- /dev/null +++ b/curseforge/packinterop/zip.go @@ -0,0 +1,57 @@ +package packinterop + +import ( + "archive/zip" + "errors" +) + +type zipReaderFile struct { + NameInternal string + *zip.File +} + +func (f zipReaderFile) Name() string { + return f.NameInternal +} + +type zipPackSource struct { + MetaFile *zip.File + Reader *zip.Reader + cachedFileList []ImportPackFile +} + +func (s zipPackSource) GetFile(path string) (ImportPackFile, error) { + if s.cachedFileList == nil { + s.cachedFileList = make([]ImportPackFile, len(s.Reader.File)) + for i, v := range s.Reader.File { + s.cachedFileList[i] = zipReaderFile{v.Name, v} + } + } + for _, v := range s.cachedFileList { + if v.Name() == path { + return v, nil + } + } + return zipReaderFile{}, errors.New("file not found in zip") +} + +func (s zipPackSource) GetFileList() ([]ImportPackFile, error) { + if s.cachedFileList == nil { + s.cachedFileList = make([]ImportPackFile, len(s.Reader.File)) + for i, v := range s.Reader.File { + s.cachedFileList[i] = zipReaderFile{v.Name, v} + } + } + return s.cachedFileList, nil +} + +func (s zipPackSource) GetPackFile() ImportPackFile { + return zipReaderFile{s.MetaFile.Name, s.MetaFile} +} + +func GetZipPackSource(metaFile *zip.File, reader *zip.Reader) ImportPackSource { + return zipPackSource{ + MetaFile: metaFile, + Reader: reader, + } +}