esp-modman/manager.go

521 lines
11 KiB
Go

package main
import (
"archive/zip"
"bufio"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"sauce.pizzawednes.day/kayomn/ini-gusher"
)
func (game *Game) CleanDeployedMods() error {
// Clean up currently deployed files first.
for _, filePath := range game.DeployedFilePaths {
var gameFilePath = filepath.Join(game.Path, filePath)
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
}
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
DeployedFilePaths []string
OverwrittenFilePaths []string
Mods map[string]Mod
Path string
}
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)
}
var name = strings.TrimSuffix(archiveBaseName, archiveExtension)
if _, exists := game.Mods[name]; exists {
return fmt.Errorf("mod with name already exists: `%s`", name)
}
// Copy archive into installation directory.
{
var installPath, installPathError = game.configPath(name)
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.Mods[name] = Mod{
Format: archiveExtension[1:],
Source: archivePath,
Version: "",
}
return nil
}
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 {
Format string
Source string
Version string
}
func (game *Game) RemoveMods(names []string) error {
for _, name := range names {
if _, exists := game.Mods[name]; !(exists) {
return fmt.Errorf("unknown mod: `%s`", name)
}
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 _, 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 {
return actionError
}
if saveError := game.Save(); saveError != nil {
return saveError
}
return nil
}
}
return fmt.Errorf("%s: game not supported", gameName)
}
func (game *Game) cachePath(path string) (string, error) {
var dirPath, pathError = os.UserCacheDir()
if pathError != nil {
return "", pathError
}
dirPath = filepath.Join(dirPath, "modman", game.ID)
if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil {
return "", mkdirError
}
return filepath.Join(dirPath, path), nil
}
func (game *Game) configPath(path string) (string, error) {
var dirPath, pathError = os.UserConfigDir()
if pathError != nil {
return "", pathError
}
dirPath = filepath.Join(dirPath, "modman", game.ID)
if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil {
return "", mkdirError
}
return filepath.Join(dirPath, path), nil
}