esp-modman/manager.go

583 lines
13 KiB
Go
Raw Normal View History

2022-12-03 18:56:36 +01:00
package main
import (
2022-12-21 00:28:30 +01:00
"archive/zip"
2022-12-05 14:13:58 +01:00
"bufio"
2022-12-03 18:56:36 +01:00
"fmt"
2022-12-05 14:13:58 +01:00
"io"
"io/fs"
2022-12-03 18:56:36 +01:00
"os"
"path/filepath"
"strings"
2022-12-21 00:28:30 +01:00
"sauce.pizzawednes.day/kayomn/ini-gusher"
)
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
func (game *Game) CleanDeployedMods() error {
// Clean up currently deployed files first.
for _, filePath := range game.DeployedFilePaths {
var gameFilePath = filepath.Join(game.Path, filePath)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if removeError := os.Remove(gameFilePath); removeError != nil {
if !(os.IsNotExist(removeError)) {
return removeError
}
}
2022-12-05 14:13:58 +01:00
}
2022-12-21 00:28:30 +01:00
game.DeployedFilePaths = game.DeployedFilePaths[:0]
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
// 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
}
2022-12-05 14:13:58 +01:00
}
2022-12-21 00:28:30 +01:00
game.OverwrittenFilePaths = game.OverwrittenFilePaths[:0]
return nil
2022-12-05 14:13:58 +01:00
}
2022-12-22 00:32:30 +01:00
func (game *Game) Close() 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)
}
2022-12-21 00:28:30 +01:00
func (game *Game) DeployMod(name string) error {
var mod, exists = game.Mods[name]
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if !(exists) {
return fmt.Errorf("mod does not exist: %s", name)
2022-12-05 14:13:58 +01:00
}
2022-12-21 00:28:30 +01:00
var archivePath, archivePathError = game.configPath(name)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if archivePathError != nil {
return archivePathError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
switch mod.Format {
case "zip":
archivePath += ".zip"
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
var zipReadCloser, zipReadCloserOpenError = zip.OpenReader(archivePath)
2022-12-06 10:33:48 +01:00
2022-12-21 00:28:30 +01:00
if zipReadCloserOpenError != nil {
return zipReadCloserOpenError
}
2022-12-06 10:33:48 +01:00
2022-12-21 00:28:30 +01:00
defer func() {
if closeError := zipReadCloser.Close(); closeError != nil {
panic(closeError)
}
}()
2022-12-06 10:33:48 +01:00
2022-12-21 00:28:30 +01:00
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
2022-12-06 10:33:48 +01:00
}
2022-12-21 00:28:30 +01:00
if zipFile.FileInfo().IsDir() {
// All work is done for creating a directory, rest is just for files.
continue
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
var file, openFileError = os.OpenFile(
deployPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if openFileError != nil {
if !(os.IsExist(openFileError)) {
return openFileError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
var backupPath, backupPathError = game.cachePath("overwritten") // deployPath
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if backupPathError != nil {
return backupPathError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
backupPath = filepath.Join(backupPath, zipFile.Name)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if dirError := os.MkdirAll(filepath.Dir(backupPath), 0755); dirError != nil {
return dirError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if renameError := os.Rename(deployPath, backupPath); renameError != nil {
return renameError
}
2022-12-06 10:33:48 +01:00
2022-12-21 00:28:30 +01:00
game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, zipFile.Name)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
file, openFileError = os.OpenFile(
deployPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if openFileError != nil {
return openFileError
}
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
var zipReadCloser, zipFileOpenError = zipFile.Open()
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if zipFileOpenError != nil {
return zipFileOpenError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
defer func() {
if syncError := file.Sync(); syncError != nil {
panic(syncError)
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if closeError := file.Close(); closeError != nil {
panic(closeError)
}
}()
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if _, copyError := io.Copy(file, zipReadCloser); copyError != nil {
return copyError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
game.DeployedFilePaths = append(game.DeployedFilePaths, zipFile.Name)
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
default:
return fmt.Errorf("unsupported mod format: %s", mod.Format)
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
return nil
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
type Game struct {
2022-12-22 00:32:30 +01:00
Name string
2022-12-21 00:28:30 +01:00
DeployedFilePaths []string
OverwrittenFilePaths []string
Mods map[string]Mod
Path string
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
func (game *Game) InstallMod(archivePath string) error {
var archiveBaseName = filepath.Base(archivePath)
var archiveExtension = filepath.Ext(archiveBaseName)
if len(archiveExtension) == 0 {
return fmt.Errorf("unknown archive format: %s", archiveExtension)
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
var name = strings.TrimSuffix(archiveBaseName, archiveExtension)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if _, exists := game.Mods[name]; exists {
return fmt.Errorf("mod with name already exists: `%s`", name)
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
// Copy archive into installation directory.
{
var installPath, installPathError = game.configPath(archiveBaseName)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if installPathError != nil {
return installPathError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
var sourceFile, sourceOpenError = os.Open(archivePath)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if sourceOpenError != nil {
return sourceOpenError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
defer sourceFile.Close()
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
var targetFile, targetCreateError = os.Create(installPath)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if targetCreateError != nil {
return targetCreateError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
defer targetFile.Close()
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if _, copyError := io.Copy(targetFile, sourceFile); copyError != nil {
return copyError
}
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
game.Mods[name] = Mod{
Format: archiveExtension[1:],
Source: archivePath,
Version: "",
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
return nil
}
2022-12-06 10:33:48 +01:00
2022-12-21 00:28:30 +01:00
func (game *Game) Load() error {
// Read deployed files from disk.
var deployedListPath, deployedListPathError = game.cachePath("deployed.txt")
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if deployedListPathError != nil {
return deployedListPathError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if file, openError := os.Open(deployedListPath); openError == nil {
defer func() {
if closeError := file.Close(); closeError != nil {
panic(closeError)
2022-12-05 14:13:58 +01:00
}
2022-12-21 00:28:30 +01:00
}()
var scanner = bufio.NewScanner(file)
for scanner.Scan() {
game.DeployedFilePaths = append(game.DeployedFilePaths, scanner.Text())
2022-12-05 14:13:58 +01:00
}
2022-12-21 00:28:30 +01:00
} else if !(os.IsNotExist(openError)) {
return openError
2022-12-05 14:13:58 +01:00
}
2022-12-21 00:28:30 +01:00
// Read overwritten files from disk.
var overwriteDirPath, overwriteDirPathError = game.cachePath("overwritten")
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if overwriteDirPathError != nil {
return overwriteDirPathError
}
2022-12-03 19:27:53 +01:00
2022-12-21 00:28:30 +01:00
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
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if !(dirEntry.IsDir()) {
game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, path)
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
return nil
}); walkError != nil {
return walkError
}
} else if !(os.IsNotExist(statError)) {
return statError
2022-12-05 14:13:58 +01:00
}
2022-12-21 00:28:30 +01:00
// Read mod info from disk.
var modInfoPath, modInfoPathError = game.configPath("mods.ini")
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if modInfoPathError != nil {
return modInfoPathError
2022-12-03 19:27:53 +01:00
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if file, openError := os.Open(modInfoPath); openError == nil {
defer func() {
if closeError := file.Close(); closeError != nil {
panic(closeError)
}
}()
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
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
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
case "source":
mod.Source = entry.Value
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
case "version":
mod.Version = entry.Value
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
game.Mods[entry.Section] = mod
}
if parserError := parser.Err(); parserError != nil {
return parserError
}
} else if !(os.IsNotExist(openError)) {
return openError
}
2022-12-03 19:27:53 +01:00
return nil
2022-12-03 18:56:36 +01:00
}
type Mod struct {
2022-12-21 00:28:30 +01:00
Format string
Source string
Version string
2022-12-05 14:13:58 +01:00
}
2022-12-22 00:32:30 +01:00
func OpenGame(name string) (Game, error) {
var configPath, configPathError = os.UserConfigDir()
if configPathError != nil {
return Game{}, configPathError
}
var gamesFile, gamesOpenError = os.Open(filepath.Join(configPath, "modman", "games.ini"))
if gamesOpenError != nil {
if os.IsNotExist(gamesOpenError) {
return Game{}, fmt.Errorf("no games registered")
}
return Game{}, gamesOpenError
}
var gamesParser = ini.NewParser(gamesFile)
for entry := gamesParser.Parse(); !(gamesParser.IsEnd()); entry = gamesParser.Parse() {
if (entry.Key == "path") && (entry.Section == name) {
var game = Game{
Name: name,
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 Game{}, loadError
}
return game, nil
}
}
return Game{}, fmt.Errorf("game not registered: %s", name)
}
2022-12-21 00:28:30 +01:00
func (game *Game) RemoveMods(names []string) error {
for _, name := range names {
if _, exists := game.Mods[name]; !(exists) {
return fmt.Errorf("unknown mod: `%s`", name)
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
var path, pathError = game.configPath(name)
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if pathError != nil {
return pathError
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
if removeError := os.RemoveAll(path); removeError != nil {
return removeError
2022-12-05 14:13:58 +01:00
}
2022-12-21 00:28:30 +01:00
delete(game.Mods, name)
2022-12-05 14:13:58 +01:00
}
return nil
2022-12-03 18:56:36 +01:00
}
func RegisterGame(name string, dataPath string) error {
var configPath, configPathError = os.UserConfigDir()
if configPathError != nil {
return configPathError
}
var gamesPath = filepath.Join(configPath, "modman", "games.ini")
var gamesEntries = []ini.Entry{{
Section: name,
Key: "path",
Value: dataPath,
}}
if gamesFile, gamesFileError := os.Open(gamesPath); gamesFileError == nil {
var gamesParser = ini.NewParser(gamesFile)
for entry := gamesParser.Parse(); !(gamesParser.IsEnd()); entry = gamesParser.Parse() {
gamesEntries = append(gamesEntries, entry)
}
if err := gamesFile.Close(); err != nil {
return err
}
if err := gamesParser.Err(); err != nil {
return err
}
} else if !(os.IsNotExist(gamesFileError)) {
return gamesFileError
}
if gamesFile, gamesFileError := os.Create(gamesPath); gamesFileError == nil {
var writeError = ini.Write(gamesFile, gamesEntries)
if err := gamesFile.Sync(); err != nil {
return err
}
if err := gamesFile.Close(); err != nil {
return err
}
if writeError != nil {
return writeError
}
} else {
return gamesFileError
}
return nil
}
2022-12-21 00:28:30 +01:00
func (game *Game) RenameMod(modName string, newName string) error {
if _, exists := game.Mods[modName]; !(exists) {
return fmt.Errorf("no mod with that name exists")
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if _, nameTaken := game.Mods[newName]; nameTaken {
return fmt.Errorf("a mod with the new name already exists")
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
var modPath, modPathError = game.configPath(modName)
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if modPathError != nil {
return modPathError
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
var newPath, newPathError = game.configPath(modName)
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if newPathError != nil {
return newPathError
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if renameError := os.Rename(modPath, newPath); renameError != nil {
return renameError
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
var mod = game.Mods[modName]
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
game.Mods[newName] = mod
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
delete(game.Mods, modName)
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
return nil
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
func (game *Game) cachePath(path string) (string, error) {
var dirPath, pathError = os.UserCacheDir()
2022-12-03 18:56:36 +01:00
if pathError != nil {
2022-12-21 00:28:30 +01:00
return "", pathError
2022-12-03 18:56:36 +01:00
}
2022-12-22 00:32:30 +01:00
dirPath = filepath.Join(dirPath, "modman", game.Name)
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil {
return "", mkdirError
2022-12-03 18:56:36 +01:00
}
2022-12-21 00:28:30 +01:00
return filepath.Join(dirPath, path), nil
}
2022-12-05 14:13:58 +01:00
2022-12-21 00:28:30 +01:00
func (game *Game) configPath(path string) (string, error) {
var dirPath, pathError = os.UserConfigDir()
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if pathError != nil {
return "", pathError
2022-12-03 18:56:36 +01:00
}
2022-12-22 00:32:30 +01:00
dirPath = filepath.Join(dirPath, "modman", game.Name)
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil {
return "", mkdirError
}
2022-12-03 18:56:36 +01:00
2022-12-21 00:28:30 +01:00
return filepath.Join(dirPath, path), nil
2022-12-03 18:56:36 +01:00
}