Major refactor

This commit is contained in:
kayomn 2022-12-20 23:28:30 +00:00
parent c55ce0562c
commit 2aed152bdf
4 changed files with 482 additions and 481 deletions

5
go.mod
View File

@ -1,3 +1,8 @@
module modman module modman
go 1.19 go 1.19
require (
gopkg.in/yaml.v3 v3.0.1 // indirect
sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa // indirect
)

5
go.sum Normal file
View File

@ -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=

190
main.go
View File

@ -1,12 +1,12 @@
package main package main
import ( import (
"archive/zip"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "strings"
"gopkg.in/yaml.v3"
) )
type Command struct { type Command struct {
@ -30,29 +30,13 @@ var commands = []Command{
var argumentCount = len(providedArguments) var argumentCount = len(providedArguments)
if argumentCount < len(requiredArguments) { 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 { if gameError := WithGame(providedArguments[0], func(game *Game) error {
for i := range archivePaths { for _, archivePath := range providedArguments[1:] {
var archivePath = archivePaths[i] if installError := game.InstallMod(archivePath); installError != nil {
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 {
return installError return installError
} }
} }
@ -138,15 +122,13 @@ var commands = []Command{
return fmt.Errorf("unsupported format: `%s`", format) 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 return formatError
} }
// TODO: Reconsider if always casting formatted data to string for output is a good modManifest = manifestBuilder.String()
// idea.
modManifest = string(formattedManifest)
return nil return nil
}); gameError != nil { }); gameError != nil {
@ -159,21 +141,26 @@ var commands = []Command{
{ {
Name: "deploy", Name: "deploy",
Description: "Deploy all installed and enabled mods", Description: "Deploy all specified mods in order of listing",
Arguments: []string{"game name"}, Arguments: []string{"game name", "mod name"},
IsVarargs: false, IsVarargs: true,
Action: func(requiredArguments []string, arguments []string) (string, error) { Action: func(requiredArguments []string, arguments []string) (string, error) {
if len(arguments) != len(requiredArguments) { 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 { if gameError := WithGame(arguments[0], func(game *Game) error {
var deployError = game.Deploy() if cleanError := game.CleanDeployedMods(); cleanError != nil {
return cleanError
}
if deployError != nil { for _, modName := range arguments[1:] {
if deployError := game.DeployMod(modName); deployError != nil {
return deployError return deployError
} }
}
return nil return nil
}); gameError != nil { }); gameError != nil {
@ -185,135 +172,38 @@ var commands = []Command{
}, },
{ {
Name: "disable", Name: "clean",
Description: "Disable one or more installed mods", Description: "Clean all deployed mods",
Arguments: []string{"game name", "mod name"}, Arguments: []string{"game name"},
IsVarargs: true, IsVarargs: false,
Action: func(requiredArguments []string, arguments []string) (string, error) { Action: func(requiredArguments []string, arguments []string) (string, error) {
if len(arguments) < len(requiredArguments) { return "cleaned", nil
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
}, },
}, },
} }
var extractors = map[string]Extractor{ var formatters = map[string]func(*strings.Builder, any) error{
"zip": func(archivePath string, destinationPath string) error { "json": func(builder *strings.Builder, data any) error {
var zipReader, openReaderError = zip.OpenReader(archivePath) var encoder = json.NewEncoder(builder)
if openReaderError != nil { if encodeError := encoder.Encode(data); encodeError != nil {
return openReaderError return encodeError
}
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
}
}
} }
return nil return nil
}, },
"yaml": func(builder *strings.Builder, data any) error {
var encoder = yaml.NewEncoder(builder)
encoder.SetIndent(2)
if encodeError := encoder.Encode(data); encodeError != nil {
return encodeError
} }
var formatters = map[string]func(any) ([]byte, error){ return nil
"json": func(data any) ([]byte, error) {
var marshalledJson, marshalError = json.Marshal(data)
if marshalError != nil {
return nil, marshalError
}
return marshalledJson, nil
}, },
} }

View File

@ -1,153 +1,189 @@
package main package main
import ( import (
"archive/zip"
"bufio" "bufio"
"encoding/csv"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sauce.pizzawednes.day/kayomn/ini-gusher"
) )
type App struct { func (game *Game) CleanDeployedMods() error {
ConfigDirPath string // Clean up currently deployed files first.
} for _, filePath := range game.DeployedFilePaths {
var gameFilePath = filepath.Join(game.Path, filePath)
func (game *Game) CachePath() (string, error) { if removeError := os.Remove(gameFilePath); removeError != nil {
var path, pathError = os.UserCacheDir() if !(os.IsNotExist(removeError)) {
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 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
} }
os.Truncate(deployedListPath, 0) if renameError := os.Rename(filepath.Join(backupDirPath, filePath),
filepath.Join(game.Path, filePath)); renameError != nil {
return renameError
}
}
game.OverwrittenFilePaths = game.OverwrittenFilePaths[:0]
return nil
}
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 return nil
} }
func (game *Game) ConfigPath() (string, error) { type Game struct {
var path, pathError = os.UserConfigDir() ID string
DeployedFilePaths []string
if pathError != nil { OverwrittenFilePaths []string
return "", pathError Mods map[string]Mod
Path string
} }
path = filepath.Join(path, "modman", game.ID) func (game *Game) InstallMod(archivePath string) error {
var archiveBaseName = filepath.Base(archivePath)
var archiveExtension = filepath.Ext(archiveBaseName)
if mkdirError := os.MkdirAll(path, 0755); mkdirError != nil { if len(archiveExtension) == 0 {
return "", mkdirError return fmt.Errorf("unknown archive format: %s", archiveExtension)
} }
return path, nil var name = strings.TrimSuffix(archiveBaseName, archiveExtension)
if _, exists := game.Mods[name]; exists {
return fmt.Errorf("mod with name already exists: `%s`", name)
} }
func (game *Game) Deploy() error { // Copy archive into installation directory.
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() var installPath, installPathError = game.configPath(name)
if configPathError != nil { if installPathError != nil {
return configPathError return installPathError
} }
var deployedListWriter = bufio.NewWriter(deployedListFile) var sourceFile, sourceOpenError = os.Open(archivePath)
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 { if sourceOpenError != nil {
return sourceOpenError return sourceOpenError
@ -155,197 +191,293 @@ func (game *Game) Deploy() error {
defer sourceFile.Close() defer sourceFile.Close()
var targetFile, targetOpenError = os.Create(filepath.Join(restorePath, localPath)) var targetFile, targetCreateError = os.Create(installPath)
if targetOpenError != nil { if targetCreateError != nil {
return targetOpenError return targetCreateError
} }
defer targetFile.Close() defer targetFile.Close()
var _, copyError = io.Copy(targetFile, sourceFile) if _, copyError := io.Copy(targetFile, sourceFile); copyError != nil {
if copyError != nil {
return copyError return copyError
} }
} }
if linkError := os.Link(path, linkPath); linkError != nil { game.Mods[name] = Mod{
return linkError Format: archiveExtension[1:],
Source: archivePath,
Version: "",
} }
if _, writeError := deployedListWriter.WriteString(linkPath); writeError != nil { return nil
return writeError
} }
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 return nil
}); walkError != nil { }); walkError != nil {
return walkError return walkError
} }
} } else if !(os.IsNotExist(statError)) {
} return statError
} }
return nil // Read mod info from disk.
var modInfoPath, modInfoPathError = game.configPath("mods.ini")
if modInfoPathError != nil {
return modInfoPathError
} }
type Extractor func(string, string) error if file, openError := os.Open(modInfoPath); openError == nil {
defer func() {
if closeError := file.Close(); closeError != nil {
panic(closeError)
}
}()
type Game struct { var parser = ini.NewParser(file)
ID string
ModOrder []Mod for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() {
ModNames map[string]int var mod = game.Mods[entry.Section]
Path string
HasUpdated bool switch entry.Key {
case "format":
mod.Format = entry.Value
case "source":
mod.Source = entry.Value
case "version":
mod.Version = entry.Value
} }
func (game *Game) InstallMod(extractor Extractor, archivePath string) error { game.Mods[entry.Section] = mod
var baseName = filepath.Base(archivePath)
var modName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
if _, exists := game.ModNames[modName]; exists {
return fmt.Errorf("mod with name already exists: `%s`", modName)
} }
var configPath, pathError = game.ConfigPath() if parserError := parser.Err(); parserError != nil {
return parserError
if pathError != nil {
return pathError
} }
} else if !(os.IsNotExist(openError)) {
if extractError := extractor(archivePath, filepath.Join( return openError
configPath, "mods", modName)); extractError != nil {
return extractError
} }
game.ModNames[modName] = len(game.ModOrder)
game.ModOrder = append(game.ModOrder, Mod{
IsEnabled: false,
Name: modName,
})
game.HasUpdated = true
return nil return nil
} }
type Mod struct { type Mod struct {
IsEnabled bool Format string
Name string
Source string Source string
Version string
} }
func (game *Game) SwitchMods(isEnabled bool, names []string) error { func (game *Game) RemoveMods(names []string) error {
if len(names) != 0 { for _, name := range names {
for i := range names { if _, exists := game.Mods[name]; !(exists) {
var name = names[i] return fmt.Errorf("unknown mod: `%s`", name)
var index, exists = game.ModNames[name]
if !(exists) {
return fmt.Errorf("mod does not exist: `%s`", name)
} }
var mod = game.ModOrder[index] var path, pathError = game.configPath(name)
mod.IsEnabled = isEnabled
game.ModOrder[index] = mod
}
game.HasUpdated = true
}
return nil
}
func WithGame(gameName string, action func(*Game) error) error {
var supportedGames = []string{"fallout4", "falloutnv", "skyrim"}
for i := range supportedGames {
var supportedGame = supportedGames[i]
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",
}
var configPath, pathError = game.ConfigPath()
if pathError != nil { if pathError != nil {
return pathError return pathError
} }
var manifestPath = filepath.Join(configPath, "mods.csv") if removeError := os.RemoveAll(path); removeError != nil {
return removeError
// 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 { delete(game.Mods, name)
return fmt.Errorf("could not read mod manifest data - may be corrupt")
} }
var status = recordValues[0] return nil
var name = recordValues[1] }
game.ModNames[name] = len(game.ModOrder) func (game *Game) RenameMod(modName string, newName string) error {
if _, exists := game.Mods[modName]; !(exists) {
return fmt.Errorf("no mod with that name exists")
}
game.ModOrder = append(game.ModOrder, Mod{ if _, nameTaken := game.Mods[newName]; nameTaken {
IsEnabled: status == "*", return fmt.Errorf("a mod with the new name already exists")
Name: name, }
Source: "",
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,
}) })
recordValues, recordError = manifestReader.Read() 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 _, supportedGame := range supportedGames {
if gameName == supportedGame {
var game = Game{
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",
}
if loadError := game.Load(); loadError != nil {
return loadError
} }
if actionError := action(&game); actionError != nil { if actionError := action(&game); actionError != nil {
return actionError return actionError
} }
// Save manifest back to disk. if saveError := game.Save(); saveError != nil {
if game.HasUpdated { return saveError
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()
} }
return nil return nil
@ -355,65 +487,34 @@ func WithGame(gameName string, action func(*Game) error) error {
return fmt.Errorf("%s: game not supported", gameName) return fmt.Errorf("%s: game not supported", gameName)
} }
func (game *Game) RemoveMods(names []string) error { func (game *Game) cachePath(path string) (string, error) {
var configPath, pathError = game.ConfigPath() var dirPath, pathError = os.UserCacheDir()
if pathError != nil { if pathError != nil {
return pathError return "", pathError
} }
if len(names) != 0 { dirPath = filepath.Join(dirPath, "modman", game.ID)
game.HasUpdated = true
for i := range names { if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil {
var name = names[i] return "", mkdirError
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 filepath.Join(dirPath, path), nil
return removeError
} }
game.ModOrder = append(game.ModOrder[:index], game.ModOrder[index+1:]...) func (game *Game) configPath(path string) (string, error) {
var dirPath, pathError = os.UserConfigDir()
delete(game.ModNames, name)
}
}
return nil
}
func (game *Game) RenameMod(modName string, newName string) error {
var configPath, pathError = game.ConfigPath()
if pathError != nil { if pathError != nil {
return pathError return "", pathError
} }
if _, exists := game.ModNames[modName]; !(exists) { dirPath = filepath.Join(dirPath, "modman", game.ID)
return fmt.Errorf("no mod with that name exists")
if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil {
return "", mkdirError
} }
if _, nameTaken := game.ModNames[newName]; nameTaken { return filepath.Join(dirPath, path), nil
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
} }