diff --git a/go.mod b/go.mod index d3470a9..65e0a69 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module modman go 1.19 + +require ( + gopkg.in/yaml.v3 v3.0.1 // indirect + sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b93cb87 --- /dev/null +++ b/go.sum @@ -0,0 +1,5 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa h1:Co1oVW0IMEIwk3tlQ2dJEFzNSd4r6tkPb2/6mxjnsOo= +sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa/go.mod h1:lniG+VCTpfcWAKKudVYLrS5NIpRx90H3mQklQNn+eK0= diff --git a/main.go b/main.go index 487c512..2cdb8ed 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,12 @@ package main import ( - "archive/zip" "encoding/json" "fmt" - "io" "os" - "path/filepath" + "strings" + + "gopkg.in/yaml.v3" ) type Command struct { @@ -30,29 +30,13 @@ var commands = []Command{ var argumentCount = len(providedArguments) if argumentCount < len(requiredArguments) { - return "", fmt.Errorf("expected game name folowed by at least one archive path") + return "", fmt.Errorf("expected %s folowed by at least one %ss", + requiredArguments[0], requiredArguments[1]) } - var archivePaths = providedArguments[1:] - if gameError := WithGame(providedArguments[0], func(game *Game) error { - for i := range archivePaths { - var archivePath = archivePaths[i] - var extension = filepath.Ext(archivePath) - - if len(extension) == 0 { - return fmt.Errorf("missing file extension: `%s`", archivePath) - } - - var extractor, extractorExists = extractors[extension[1:]] - - if !(extractorExists) { - return fmt.Errorf("unsupported file format: `%s`", archivePath) - } - - if installError := game.InstallMod( - extractor, archivePath); installError != nil { - + for _, archivePath := range providedArguments[1:] { + if installError := game.InstallMod(archivePath); installError != nil { return installError } } @@ -138,15 +122,13 @@ var commands = []Command{ return fmt.Errorf("unsupported format: `%s`", format) } - var formattedManifest, formatError = formatter(game.ModOrder) + var manifestBuilder = strings.Builder{} - if formatError != nil { + if formatError := formatter(&manifestBuilder, game.Mods); formatError != nil { return formatError } - // TODO: Reconsider if always casting formatted data to string for output is a good - // idea. - modManifest = string(formattedManifest) + modManifest = manifestBuilder.String() return nil }); gameError != nil { @@ -159,20 +141,25 @@ var commands = []Command{ { Name: "deploy", - Description: "Deploy all installed and enabled mods", - Arguments: []string{"game name"}, - IsVarargs: false, + Description: "Deploy all specified mods in order of listing", + Arguments: []string{"game name", "mod name"}, + IsVarargs: true, Action: func(requiredArguments []string, arguments []string) (string, error) { if len(arguments) != len(requiredArguments) { - return "", fmt.Errorf("expected %s", requiredArguments[0]) + return "", fmt.Errorf("expected %s followed by one or more %ss", + requiredArguments[0], requiredArguments[1]) } if gameError := WithGame(arguments[0], func(game *Game) error { - var deployError = game.Deploy() + if cleanError := game.CleanDeployedMods(); cleanError != nil { + return cleanError + } - if deployError != nil { - return deployError + for _, modName := range arguments[1:] { + if deployError := game.DeployMod(modName); deployError != nil { + return deployError + } } return nil @@ -185,135 +172,38 @@ var commands = []Command{ }, { - Name: "disable", - Description: "Disable one or more installed mods", - Arguments: []string{"game name", "mod name"}, - IsVarargs: true, + Name: "clean", + Description: "Clean all deployed mods", + Arguments: []string{"game name"}, + IsVarargs: false, Action: func(requiredArguments []string, arguments []string) (string, error) { - if len(arguments) < len(requiredArguments) { - return "", fmt.Errorf("expected %s followed by one or more %ss", - requiredArguments[0], requiredArguments[1]) - } - - if gameError := WithGame(arguments[0], func(game *Game) error { - if enableError := game.SwitchMods(false, arguments[1:]); enableError != nil { - return enableError - } - - return nil - }); gameError != nil { - return "", gameError - } - - return "enabled", nil - }, - }, - - { - Name: "enable", - Description: "Enable one or more installed mods", - Arguments: []string{"game name", "mod name"}, - IsVarargs: true, - - Action: func(requiredArguments []string, arguments []string) (string, error) { - if len(arguments) < len(requiredArguments) { - return "", fmt.Errorf("expected %s followed by one or more %ss", - requiredArguments[0], requiredArguments[1]) - } - - if gameError := WithGame(arguments[0], func(game *Game) error { - if enableError := game.SwitchMods(true, arguments[1:]); enableError != nil { - return enableError - } - - return nil - }); gameError != nil { - return "", gameError - } - - return "enabled", nil + return "cleaned", nil }, }, } -var extractors = map[string]Extractor{ - "zip": func(archivePath string, destinationPath string) error { - var zipReader, openReaderError = zip.OpenReader(archivePath) +var formatters = map[string]func(*strings.Builder, any) error{ + "json": func(builder *strings.Builder, data any) error { + var encoder = json.NewEncoder(builder) - if openReaderError != nil { - return openReaderError - } - - defer func() { - if closeError := zipReader.Close(); closeError != nil { - panic(closeError.Error()) - } - }() - - if mkdirError := os.MkdirAll(destinationPath, 0755); mkdirError != nil { - return mkdirError - } - - for i := range zipReader.File { - var file = zipReader.File[i] - var fileReader, fileOpenError = file.Open() - - if fileOpenError != nil { - return fileOpenError - } - - defer func() { - if closeError := fileReader.Close(); closeError != nil { - panic(closeError.Error()) - } - }() - - var path = filepath.Join(destinationPath, file.Name) - - if file.FileInfo().IsDir() { - if mkdirError := os.MkdirAll(path, file.Mode()); mkdirError != nil { - return mkdirError - } - } else { - if mkdirError := os.MkdirAll(filepath.Dir(path), file.Mode()); mkdirError != nil { - return mkdirError - } - - var extractedFile, fileOpenError = os.OpenFile(path, - os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) - - if fileOpenError != nil { - return fileOpenError - } - - defer func() { - if fileOpenError := extractedFile.Close(); fileOpenError != nil { - panic(fileOpenError) - } - }() - - var _, copyError = io.Copy(extractedFile, fileReader) - - if copyError != nil { - return copyError - } - } + if encodeError := encoder.Encode(data); encodeError != nil { + return encodeError } return nil }, -} -var formatters = map[string]func(any) ([]byte, error){ - "json": func(data any) ([]byte, error) { - var marshalledJson, marshalError = json.Marshal(data) + "yaml": func(builder *strings.Builder, data any) error { + var encoder = yaml.NewEncoder(builder) - if marshalError != nil { - return nil, marshalError + encoder.SetIndent(2) + + if encodeError := encoder.Encode(data); encodeError != nil { + return encodeError } - return marshalledJson, nil + return nil }, } diff --git a/manager.go b/manager.go index b857200..3ad08eb 100644 --- a/manager.go +++ b/manager.go @@ -1,351 +1,483 @@ package main import ( + "archive/zip" "bufio" - "encoding/csv" "fmt" "io" "io/fs" "os" "path/filepath" "strings" + + "sauce.pizzawednes.day/kayomn/ini-gusher" ) -type App struct { - ConfigDirPath string -} +func (game *Game) CleanDeployedMods() error { + // Clean up currently deployed files first. + for _, filePath := range game.DeployedFilePaths { + var gameFilePath = filepath.Join(game.Path, filePath) -func (game *Game) CachePath() (string, error) { - var path, pathError = os.UserCacheDir() - - if pathError != nil { - return "", pathError - } - - path = filepath.Join(path, "modman", game.ID) - - if mkdirError := os.MkdirAll(path, 0755); mkdirError != nil { - return "", mkdirError - } - - return path, nil -} - -func (game *Game) Clean() error { - var cachePath, cachePathError = game.CachePath() - - if cachePathError != nil { - return cachePathError - } - - var deployedListPath = filepath.Join(cachePath, "deployed.txt") - - if deployedListFile, openError := os.Open(deployedListPath); !(os.IsNotExist(openError)) { - { - defer deployedListFile.Close() - - var deployedListScanner = bufio.NewScanner(deployedListFile) - - for deployedListScanner.Scan() { - var deployedPath = filepath.Join(game.Path, deployedListScanner.Text()) - - if removeError := os.Remove(deployedPath); removeError != nil { - return removeError - } - - var deployedDirPath = filepath.Dir(deployedPath) - - if remainingDirEntries, readDirError := os.ReadDir(deployedDirPath); (readDirError == nil) && (len(remainingDirEntries) == 0) { - if removeError := os.Remove(deployedDirPath); removeError != nil { - return removeError - } - } - } - } - - os.Truncate(deployedListPath, 0) - } - - return nil -} - -func (game *Game) ConfigPath() (string, error) { - var path, pathError = os.UserConfigDir() - - if pathError != nil { - return "", pathError - } - - path = filepath.Join(path, "modman", game.ID) - - if mkdirError := os.MkdirAll(path, 0755); mkdirError != nil { - return "", mkdirError - } - - return path, nil -} - -func (game *Game) Deploy() error { - if cleanError := game.Clean(); cleanError != nil { - return cleanError - } - - var cachePath, cachePathError = game.CachePath() - - if cachePathError != nil { - return cachePathError - } - - var deployedListFile, deployedListCreateError = os.Create( - filepath.Join(cachePath, "deployed.txt")) - - if deployedListCreateError != nil { - return deployedListCreateError - } - - defer deployedListFile.Close() - - { - var configPath, configPathError = game.ConfigPath() - - if configPathError != nil { - return configPathError - } - - var deployedListWriter = bufio.NewWriter(deployedListFile) - var modsPath = filepath.Join(configPath, "mods") - var restorePath = filepath.Join(cachePath, "restore") - - for i := range game.ModOrder { - var mod = game.ModOrder[i] - - if mod.IsEnabled { - var modPath = filepath.Join(modsPath, mod.Name) - - if walkError := filepath.WalkDir(modPath, func( - path string, dirEntry fs.DirEntry, dirError error) error { - - if dirError != nil { - return dirError - } - - var fileMode = dirEntry.Type() - - if !(fileMode.IsDir()) { - var localPath, relativeError = filepath.Rel(modPath, path) - - if relativeError != nil { - return relativeError - } - - var linkPath = filepath.Join(game.Path, localPath) - - if pathError := os.MkdirAll(filepath.Dir(linkPath), - fileMode.Perm()); pathError != nil { - - return pathError - } - - if _, statError := os.Stat(linkPath); !(os.IsNotExist(statError)) { - var sourceFile, sourceOpenError = os.Open(linkPath) - - if sourceOpenError != nil { - return sourceOpenError - } - - defer sourceFile.Close() - - var targetFile, targetOpenError = os.Create(filepath.Join(restorePath, localPath)) - - if targetOpenError != nil { - return targetOpenError - } - - defer targetFile.Close() - - var _, copyError = io.Copy(targetFile, sourceFile) - - if copyError != nil { - return copyError - } - } - - if linkError := os.Link(path, linkPath); linkError != nil { - return linkError - } - - if _, writeError := deployedListWriter.WriteString(linkPath); writeError != nil { - return writeError - } - } - - return nil - }); walkError != nil { - return walkError - } + if removeError := os.Remove(gameFilePath); removeError != nil { + if !(os.IsNotExist(removeError)) { + return removeError } } } + game.DeployedFilePaths = game.DeployedFilePaths[:0] + + // Then restore all files overwritten by previously deployment. + for _, filePath := range game.OverwrittenFilePaths { + var backupDirPath, backupDirPathError = game.cachePath("overwritten") + + if backupDirPathError != nil { + return backupDirPathError + } + + if renameError := os.Rename(filepath.Join(backupDirPath, filePath), + filepath.Join(game.Path, filePath)); renameError != nil { + + return renameError + } + } + + game.OverwrittenFilePaths = game.OverwrittenFilePaths[:0] + return nil } -type Extractor func(string, string) error +func (game *Game) DeployMod(name string) error { + var mod, exists = game.Mods[name] + + if !(exists) { + return fmt.Errorf("mod does not exist: %s", name) + } + + var archivePath, archivePathError = game.configPath(name) + + if archivePathError != nil { + return archivePathError + } + + switch mod.Format { + case "zip": + archivePath += ".zip" + + var zipReadCloser, zipReadCloserOpenError = zip.OpenReader(archivePath) + + if zipReadCloserOpenError != nil { + return zipReadCloserOpenError + } + + defer func() { + if closeError := zipReadCloser.Close(); closeError != nil { + panic(closeError) + } + }() + + for _, zipFile := range zipReadCloser.File { + var deployPath = filepath.Join(game.Path, zipFile.Name) + var fileMode = zipFile.Mode() + + if dirError := os.MkdirAll(filepath.Dir(deployPath), fileMode); dirError != nil { + return dirError + } + + if zipFile.FileInfo().IsDir() { + // All work is done for creating a directory, rest is just for files. + continue + } + + var file, openFileError = os.OpenFile( + deployPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode) + + if openFileError != nil { + if !(os.IsExist(openFileError)) { + return openFileError + } + + var backupPath, backupPathError = game.cachePath("overwritten") // deployPath + + if backupPathError != nil { + return backupPathError + } + + backupPath = filepath.Join(backupPath, zipFile.Name) + + if dirError := os.MkdirAll(filepath.Dir(backupPath), 0755); dirError != nil { + return dirError + } + + if renameError := os.Rename(deployPath, backupPath); renameError != nil { + return renameError + } + + game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, zipFile.Name) + + file, openFileError = os.OpenFile( + deployPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode) + + if openFileError != nil { + return openFileError + } + } + + var zipReadCloser, zipFileOpenError = zipFile.Open() + + if zipFileOpenError != nil { + return zipFileOpenError + } + + defer func() { + if syncError := file.Sync(); syncError != nil { + panic(syncError) + } + + if closeError := file.Close(); closeError != nil { + panic(closeError) + } + }() + + if _, copyError := io.Copy(file, zipReadCloser); copyError != nil { + return copyError + } + + game.DeployedFilePaths = append(game.DeployedFilePaths, zipFile.Name) + } + + default: + return fmt.Errorf("unsupported mod format: %s", mod.Format) + } + + return nil +} type Game struct { - ID string - ModOrder []Mod - ModNames map[string]int - Path string - HasUpdated bool + ID string + DeployedFilePaths []string + OverwrittenFilePaths []string + Mods map[string]Mod + Path string } -func (game *Game) InstallMod(extractor Extractor, archivePath string) error { - var baseName = filepath.Base(archivePath) - var modName = strings.TrimSuffix(baseName, filepath.Ext(baseName)) +func (game *Game) InstallMod(archivePath string) error { + var archiveBaseName = filepath.Base(archivePath) + var archiveExtension = filepath.Ext(archiveBaseName) - if _, exists := game.ModNames[modName]; exists { - return fmt.Errorf("mod with name already exists: `%s`", modName) + if len(archiveExtension) == 0 { + return fmt.Errorf("unknown archive format: %s", archiveExtension) } - var configPath, pathError = game.ConfigPath() + var name = strings.TrimSuffix(archiveBaseName, archiveExtension) - if pathError != nil { - return pathError + if _, exists := game.Mods[name]; exists { + return fmt.Errorf("mod with name already exists: `%s`", name) } - if extractError := extractor(archivePath, filepath.Join( - configPath, "mods", modName)); extractError != nil { + // Copy archive into installation directory. + { + var installPath, installPathError = game.configPath(name) - return extractError + if installPathError != nil { + return installPathError + } + + var sourceFile, sourceOpenError = os.Open(archivePath) + + if sourceOpenError != nil { + return sourceOpenError + } + + defer sourceFile.Close() + + var targetFile, targetCreateError = os.Create(installPath) + + if targetCreateError != nil { + return targetCreateError + } + + defer targetFile.Close() + + if _, copyError := io.Copy(targetFile, sourceFile); copyError != nil { + return copyError + } } - game.ModNames[modName] = len(game.ModOrder) + game.Mods[name] = Mod{ + Format: archiveExtension[1:], + Source: archivePath, + Version: "", + } - game.ModOrder = append(game.ModOrder, Mod{ - IsEnabled: false, - Name: modName, - }) + return nil +} - game.HasUpdated = true +func (game *Game) Load() error { + // Read deployed files from disk. + var deployedListPath, deployedListPathError = game.cachePath("deployed.txt") + + if deployedListPathError != nil { + return deployedListPathError + } + + if file, openError := os.Open(deployedListPath); openError == nil { + defer func() { + if closeError := file.Close(); closeError != nil { + panic(closeError) + } + }() + + var scanner = bufio.NewScanner(file) + + for scanner.Scan() { + game.DeployedFilePaths = append(game.DeployedFilePaths, scanner.Text()) + } + } else if !(os.IsNotExist(openError)) { + return openError + } + + // Read overwritten files from disk. + var overwriteDirPath, overwriteDirPathError = game.cachePath("overwritten") + + if overwriteDirPathError != nil { + return overwriteDirPathError + } + + if _, statError := os.Stat(overwriteDirPath); statError == nil { + if walkError := filepath.WalkDir(overwriteDirPath, func( + path string, dirEntry fs.DirEntry, walkError error) error { + + if walkError != nil { + return walkError + } + + if !(dirEntry.IsDir()) { + game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, path) + } + + return nil + }); walkError != nil { + return walkError + } + } else if !(os.IsNotExist(statError)) { + return statError + } + + // Read mod info from disk. + var modInfoPath, modInfoPathError = game.configPath("mods.ini") + + if modInfoPathError != nil { + return modInfoPathError + } + + if file, openError := os.Open(modInfoPath); openError == nil { + defer func() { + if closeError := file.Close(); closeError != nil { + panic(closeError) + } + }() + + var parser = ini.NewParser(file) + + for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() { + var mod = game.Mods[entry.Section] + + switch entry.Key { + case "format": + mod.Format = entry.Value + + case "source": + mod.Source = entry.Value + + case "version": + mod.Version = entry.Value + } + + game.Mods[entry.Section] = mod + } + + if parserError := parser.Err(); parserError != nil { + return parserError + } + } else if !(os.IsNotExist(openError)) { + return openError + } return nil } type Mod struct { - IsEnabled bool - Name string - Source string + Format string + Source string + Version string } -func (game *Game) SwitchMods(isEnabled bool, names []string) error { - if len(names) != 0 { - for i := range names { - var name = names[i] - var index, exists = game.ModNames[name] - - if !(exists) { - return fmt.Errorf("mod does not exist: `%s`", name) - } - - var mod = game.ModOrder[index] - - mod.IsEnabled = isEnabled - game.ModOrder[index] = mod +func (game *Game) RemoveMods(names []string) error { + for _, name := range names { + if _, exists := game.Mods[name]; !(exists) { + return fmt.Errorf("unknown mod: `%s`", name) } - game.HasUpdated = true + var path, pathError = game.configPath(name) + + if pathError != nil { + return pathError + } + + if removeError := os.RemoveAll(path); removeError != nil { + return removeError + } + + delete(game.Mods, name) } return nil } +func (game *Game) RenameMod(modName string, newName string) error { + if _, exists := game.Mods[modName]; !(exists) { + return fmt.Errorf("no mod with that name exists") + } + + if _, nameTaken := game.Mods[newName]; nameTaken { + return fmt.Errorf("a mod with the new name already exists") + } + + var modPath, modPathError = game.configPath(modName) + + if modPathError != nil { + return modPathError + } + + var newPath, newPathError = game.configPath(modName) + + if newPathError != nil { + return newPathError + } + + if renameError := os.Rename(modPath, newPath); renameError != nil { + return renameError + } + + var mod = game.Mods[modName] + + game.Mods[newName] = mod + + delete(game.Mods, modName) + + return nil +} + +func (game *Game) Save() error { + // Write deployed files to disk. + var deployedListPath, deployedListPathError = game.cachePath("deployed.txt") + + if deployedListPathError != nil { + return deployedListPathError + } + + var deployedListFile, deployedListCreateError = os.Create(deployedListPath) + + if deployedListCreateError != nil { + return deployedListCreateError + } + + defer func() { + if syncError := deployedListFile.Sync(); syncError != nil { + panic(syncError) + } + + if closeError := deployedListFile.Close(); closeError != nil { + panic(closeError) + } + }() + + var deployedListWriter = bufio.NewWriter(deployedListFile) + + defer func() { + if flushError := deployedListWriter.Flush(); flushError != nil { + panic(flushError) + } + }() + + for _, filePath := range game.DeployedFilePaths { + if _, writeError := deployedListWriter.WriteString(filePath); writeError != nil { + return writeError + } + + if writeError := deployedListWriter.WriteByte('\n'); writeError != nil { + return writeError + } + } + + // Read mod info from disk. + var modInfoPath, modInfoPathError = game.configPath("mods.ini") + + if modInfoPathError != nil { + return modInfoPathError + } + + var modInfoFile, modInfoCreateError = os.Create(modInfoPath) + + if modInfoCreateError != nil { + return modInfoCreateError + } + + defer func() { + if closeError := modInfoFile.Close(); closeError != nil { + panic(closeError) + } + }() + + var modInfoEntries = make([]ini.Entry, 0, len(game.Mods)*3) + + for name, mod := range game.Mods { + modInfoEntries = append(modInfoEntries, ini.Entry{ + Section: name, + Key: "source", + Value: mod.Source, + }) + + modInfoEntries = append(modInfoEntries, ini.Entry{ + Section: name, + Key: "format", + Value: mod.Format, + }) + + modInfoEntries = append(modInfoEntries, ini.Entry{ + Section: name, + Key: "version", + Value: mod.Version, + }) + } + + return ini.Write(modInfoFile, modInfoEntries) +} + func WithGame(gameName string, action func(*Game) error) error { var supportedGames = []string{"fallout4", "falloutnv", "skyrim"} - for i := range supportedGames { - var supportedGame = supportedGames[i] - + for _, supportedGame := range supportedGames { if gameName == supportedGame { var game = Game{ - ID: supportedGame, - ModOrder: make([]Mod, 0, 512), - ModNames: make(map[string]int), - HasUpdated: false, - Path: "/home/kayomn/.steam/steam/steamapps/common/Fallout 4/Data", + ID: supportedGame, + OverwrittenFilePaths: make([]string, 0, 512), + DeployedFilePaths: make([]string, 0, 512), + Mods: make(map[string]Mod), + Path: "/home/kayomn/.steam/steam/steamapps/common/Fallout 4/Data", } - var configPath, pathError = game.ConfigPath() - - if pathError != nil { - return pathError - } - - var manifestPath = filepath.Join(configPath, "mods.csv") - - // Load manifest from disk. - { - var manifestFile, openError = os.Open(manifestPath) - - if openError == nil { - defer manifestFile.Close() - - var manifestReader = csv.NewReader(manifestFile) - var recordValues, recordError = manifestReader.Read() - - for recordValues != nil { - if recordError != nil { - return recordError - } - - if len(recordValues) < 2 { - return fmt.Errorf("could not read mod manifest data - may be corrupt") - } - - var status = recordValues[0] - var name = recordValues[1] - - game.ModNames[name] = len(game.ModOrder) - - game.ModOrder = append(game.ModOrder, Mod{ - IsEnabled: status == "*", - Name: name, - Source: "", - }) - - recordValues, recordError = manifestReader.Read() - } - } + if loadError := game.Load(); loadError != nil { + return loadError } if actionError := action(&game); actionError != nil { return actionError } - // Save manifest back to disk. - if game.HasUpdated { - var manifestFile, openError = os.Create(manifestPath) - - if openError != nil { - return openError - } - - defer manifestFile.Close() - - var manifestWriter = csv.NewWriter(manifestFile) - - for i := range game.ModOrder { - var mod = game.ModOrder[i] - var status = "" - - if mod.IsEnabled { - status = "*" - } - - manifestWriter.Write([]string{status, mod.Name, mod.Source}) - } - - manifestWriter.Flush() + if saveError := game.Save(); saveError != nil { + return saveError } return nil @@ -355,65 +487,34 @@ func WithGame(gameName string, action func(*Game) error) error { return fmt.Errorf("%s: game not supported", gameName) } -func (game *Game) RemoveMods(names []string) error { - var configPath, pathError = game.ConfigPath() +func (game *Game) cachePath(path string) (string, error) { + var dirPath, pathError = os.UserCacheDir() if pathError != nil { - return pathError + return "", pathError } - if len(names) != 0 { - game.HasUpdated = true + dirPath = filepath.Join(dirPath, "modman", game.ID) - for i := range names { - var name = names[i] - var index, exists = game.ModNames[name] - - if !(exists) { - return fmt.Errorf("unknown mod: `%s`", name) - } - - if removeError := os.RemoveAll(filepath.Join(configPath, name)); removeError != nil { - return removeError - } - - game.ModOrder = append(game.ModOrder[:index], game.ModOrder[index+1:]...) - - delete(game.ModNames, name) - } + if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil { + return "", mkdirError } - return nil + return filepath.Join(dirPath, path), nil } -func (game *Game) RenameMod(modName string, newName string) error { - var configPath, pathError = game.ConfigPath() +func (game *Game) configPath(path string) (string, error) { + var dirPath, pathError = os.UserConfigDir() if pathError != nil { - return pathError + return "", pathError } - if _, exists := game.ModNames[modName]; !(exists) { - return fmt.Errorf("no mod with that name exists") + dirPath = filepath.Join(dirPath, "modman", game.ID) + + if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil { + return "", mkdirError } - if _, nameTaken := game.ModNames[newName]; nameTaken { - return fmt.Errorf("a mod with the new name already exists") - } - - var modsPath = filepath.Join(configPath, "mods") - - if renameError := os.Rename(filepath.Join(modsPath, modName), - filepath.Join(modsPath, newName)); renameError != nil { - - return renameError - } - - game.ModNames[newName] = game.ModNames[modName] - - delete(game.ModNames, modName) - - game.HasUpdated = true - - return nil + return filepath.Join(dirPath, path), nil }