Major refactor
This commit is contained in:
parent
c55ce0562c
commit
2aed152bdf
5
go.mod
5
go.mod
|
@ -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
|
||||||
|
)
|
||||||
|
|
|
@ -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=
|
188
main.go
188
main.go
|
@ -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,20 +141,25 @@ 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:] {
|
||||||
return deployError
|
if deployError := game.DeployMod(modName); deployError != nil {
|
||||||
|
return deployError
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return 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
|
||||||
},
|
},
|
||||||
}
|
|
||||||
|
|
||||||
var formatters = map[string]func(any) ([]byte, error){
|
"yaml": func(builder *strings.Builder, data any) error {
|
||||||
"json": func(data any) ([]byte, error) {
|
var encoder = yaml.NewEncoder(builder)
|
||||||
var marshalledJson, marshalError = json.Marshal(data)
|
|
||||||
|
|
||||||
if marshalError != nil {
|
encoder.SetIndent(2)
|
||||||
return nil, marshalError
|
|
||||||
|
if encodeError := encoder.Encode(data); encodeError != nil {
|
||||||
|
return encodeError
|
||||||
}
|
}
|
||||||
|
|
||||||
return marshalledJson, nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
765
manager.go
765
manager.go
|
@ -1,351 +1,483 @@
|
||||||
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)) {
|
||||||
|
return 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
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 {
|
type Game struct {
|
||||||
ID string
|
ID string
|
||||||
ModOrder []Mod
|
DeployedFilePaths []string
|
||||||
ModNames map[string]int
|
OverwrittenFilePaths []string
|
||||||
Path string
|
Mods map[string]Mod
|
||||||
HasUpdated bool
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) InstallMod(extractor Extractor, archivePath string) error {
|
func (game *Game) InstallMod(archivePath string) error {
|
||||||
var baseName = filepath.Base(archivePath)
|
var archiveBaseName = filepath.Base(archivePath)
|
||||||
var modName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
|
var archiveExtension = filepath.Ext(archiveBaseName)
|
||||||
|
|
||||||
if _, exists := game.ModNames[modName]; exists {
|
if len(archiveExtension) == 0 {
|
||||||
return fmt.Errorf("mod with name already exists: `%s`", modName)
|
return fmt.Errorf("unknown archive format: %s", archiveExtension)
|
||||||
}
|
}
|
||||||
|
|
||||||
var configPath, pathError = game.ConfigPath()
|
var name = strings.TrimSuffix(archiveBaseName, archiveExtension)
|
||||||
|
|
||||||
if pathError != nil {
|
if _, exists := game.Mods[name]; exists {
|
||||||
return pathError
|
return fmt.Errorf("mod with name already exists: `%s`", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if extractError := extractor(archivePath, filepath.Join(
|
// Copy archive into installation directory.
|
||||||
configPath, "mods", modName)); extractError != nil {
|
{
|
||||||
|
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{
|
return nil
|
||||||
IsEnabled: false,
|
}
|
||||||
Name: modName,
|
|
||||||
})
|
|
||||||
|
|
||||||
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
|
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]
|
|
||||||
|
|
||||||
mod.IsEnabled = isEnabled
|
|
||||||
game.ModOrder[index] = mod
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
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 {
|
func WithGame(gameName string, action func(*Game) error) error {
|
||||||
var supportedGames = []string{"fallout4", "falloutnv", "skyrim"}
|
var supportedGames = []string{"fallout4", "falloutnv", "skyrim"}
|
||||||
|
|
||||||
for i := range supportedGames {
|
for _, supportedGame := range supportedGames {
|
||||||
var supportedGame = supportedGames[i]
|
|
||||||
|
|
||||||
if gameName == supportedGame {
|
if gameName == supportedGame {
|
||||||
var game = Game{
|
var game = Game{
|
||||||
ID: supportedGame,
|
ID: supportedGame,
|
||||||
ModOrder: make([]Mod, 0, 512),
|
OverwrittenFilePaths: make([]string, 0, 512),
|
||||||
ModNames: make(map[string]int),
|
DeployedFilePaths: make([]string, 0, 512),
|
||||||
HasUpdated: false,
|
Mods: make(map[string]Mod),
|
||||||
Path: "/home/kayomn/.steam/steam/steamapps/common/Fallout 4/Data",
|
Path: "/home/kayomn/.steam/steam/steamapps/common/Fallout 4/Data",
|
||||||
}
|
}
|
||||||
|
|
||||||
var configPath, pathError = game.ConfigPath()
|
if loadError := game.Load(); loadError != nil {
|
||||||
|
return loadError
|
||||||
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 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 removeError
|
|
||||||
}
|
|
||||||
|
|
||||||
game.ModOrder = append(game.ModOrder[:index], game.ModOrder[index+1:]...)
|
|
||||||
|
|
||||||
delete(game.ModNames, name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return filepath.Join(dirPath, path), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) RenameMod(modName string, newName string) error {
|
func (game *Game) configPath(path string) (string, error) {
|
||||||
var configPath, pathError = game.ConfigPath()
|
var dirPath, pathError = os.UserConfigDir()
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue