2022-12-03 18:56:36 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-12-05 14:13:58 +01:00
|
|
|
"bufio"
|
2022-12-03 18:56:36 +01:00
|
|
|
"encoding/csv"
|
|
|
|
"fmt"
|
2022-12-05 14:13:58 +01:00
|
|
|
"io"
|
|
|
|
"io/fs"
|
2022-12-03 18:56:36 +01:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
type App struct {
|
|
|
|
ConfigDirPath string
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
var deployedListPath = filepath.Join(cachePath, "deployed.txt")
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if deployedListFile, openError := os.Open(deployedListPath); !(os.IsNotExist(openError)) {
|
|
|
|
{
|
|
|
|
defer deployedListFile.Close()
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
var deployedListScanner = bufio.NewScanner(deployedListFile)
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-12-05 14:13:58 +01:00
|
|
|
}
|
2022-12-06 10:33:48 +01:00
|
|
|
|
|
|
|
os.Truncate(deployedListPath, 0)
|
2022-12-05 14:13:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2022-12-06 10:33:48 +01:00
|
|
|
if cleanError := game.Clean(); cleanError != nil {
|
|
|
|
return cleanError
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
var cachePath, cachePathError = game.CachePath()
|
|
|
|
|
|
|
|
if cachePathError != nil {
|
|
|
|
return cachePathError
|
|
|
|
}
|
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
var deployedListFile, deployedListCreateError = os.Create(
|
|
|
|
filepath.Join(cachePath, "deployed.txt"))
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if deployedListCreateError != nil {
|
|
|
|
return deployedListCreateError
|
2022-12-05 14:13:58 +01:00
|
|
|
}
|
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
defer deployedListFile.Close()
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
{
|
|
|
|
var configPath, configPathError = game.ConfigPath()
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if configPathError != nil {
|
|
|
|
return configPathError
|
2022-12-05 14:13:58 +01:00
|
|
|
}
|
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
var deployedListWriter = bufio.NewWriter(deployedListFile)
|
|
|
|
var modsPath = filepath.Join(configPath, "mods")
|
|
|
|
var restorePath = filepath.Join(cachePath, "restore")
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
for i := range game.ModOrder {
|
|
|
|
var mod = game.ModOrder[i]
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if mod.IsEnabled {
|
|
|
|
var modPath = filepath.Join(modsPath, mod.Name)
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if walkError := filepath.WalkDir(modPath, func(
|
|
|
|
path string, dirEntry fs.DirEntry, dirError error) error {
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if dirError != nil {
|
|
|
|
return dirError
|
|
|
|
}
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
var fileMode = dirEntry.Type()
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if !(fileMode.IsDir()) {
|
|
|
|
var localPath, relativeError = filepath.Rel(modPath, path)
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if relativeError != nil {
|
|
|
|
return relativeError
|
|
|
|
}
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
var linkPath = filepath.Join(game.Path, localPath)
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if pathError := os.MkdirAll(filepath.Dir(linkPath),
|
|
|
|
fileMode.Perm()); pathError != nil {
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
return pathError
|
|
|
|
}
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if _, statError := os.Stat(linkPath); !(os.IsNotExist(statError)) {
|
|
|
|
var sourceFile, sourceOpenError = os.Open(linkPath)
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if sourceOpenError != nil {
|
|
|
|
return sourceOpenError
|
|
|
|
}
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
defer sourceFile.Close()
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
var targetFile, targetOpenError = os.Create(filepath.Join(restorePath, localPath))
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if targetOpenError != nil {
|
|
|
|
return targetOpenError
|
|
|
|
}
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
defer targetFile.Close()
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
var _, copyError = io.Copy(targetFile, sourceFile)
|
2022-12-05 14:13:58 +01:00
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if copyError != nil {
|
|
|
|
return copyError
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if linkError := os.Link(path, linkPath); linkError != nil {
|
|
|
|
return linkError
|
2022-12-05 14:13:58 +01:00
|
|
|
}
|
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
if _, writeError := deployedListWriter.WriteString(linkPath); writeError != nil {
|
|
|
|
return writeError
|
|
|
|
}
|
2022-12-05 14:13:58 +01:00
|
|
|
}
|
|
|
|
|
2022-12-06 10:33:48 +01:00
|
|
|
return nil
|
|
|
|
}); walkError != nil {
|
|
|
|
return walkError
|
|
|
|
}
|
2022-12-05 14:13:58 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-12-03 19:27:53 +01:00
|
|
|
type Extractor func(string, string) error
|
|
|
|
|
2022-12-03 18:56:36 +01:00
|
|
|
type Game struct {
|
|
|
|
ID string
|
2022-12-05 14:13:58 +01:00
|
|
|
ModOrder []Mod
|
|
|
|
ModNames map[string]int
|
|
|
|
Path string
|
2022-12-03 18:56:36 +01:00
|
|
|
HasUpdated bool
|
|
|
|
}
|
|
|
|
|
2022-12-03 19:27:53 +01:00
|
|
|
func (game *Game) InstallMod(extractor Extractor, archivePath string) error {
|
2022-12-05 14:13:58 +01:00
|
|
|
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()
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-03 19:27:53 +01:00
|
|
|
if pathError != nil {
|
|
|
|
return pathError
|
|
|
|
}
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-03 19:27:53 +01:00
|
|
|
if extractError := extractor(archivePath, filepath.Join(
|
2022-12-05 14:13:58 +01:00
|
|
|
configPath, "mods", modName)); extractError != nil {
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-03 19:27:53 +01:00
|
|
|
return extractError
|
|
|
|
}
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
game.ModNames[modName] = len(game.ModOrder)
|
|
|
|
|
|
|
|
game.ModOrder = append(game.ModOrder, Mod{
|
2022-12-03 19:27:53 +01:00
|
|
|
IsEnabled: false,
|
2022-12-05 14:13:58 +01:00
|
|
|
Name: modName,
|
|
|
|
})
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-03 19:27:53 +01:00
|
|
|
game.HasUpdated = true
|
|
|
|
|
|
|
|
return nil
|
2022-12-03 18:56:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type Mod struct {
|
|
|
|
IsEnabled bool
|
2022-12-05 14:13:58 +01:00
|
|
|
Name string
|
|
|
|
Source 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
|
|
|
|
}
|
|
|
|
|
|
|
|
game.HasUpdated = true
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2022-12-03 18:56:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2022-12-05 14:13:58 +01:00
|
|
|
ModOrder: make([]Mod, 0, 512),
|
|
|
|
ModNames: make(map[string]int),
|
2022-12-03 18:56:36 +01:00
|
|
|
HasUpdated: false,
|
2022-12-05 14:13:58 +01:00
|
|
|
Path: "/home/kayomn/.steam/steam/steamapps/common/Fallout 4/Data",
|
2022-12-03 18:56:36 +01:00
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
var configPath, pathError = game.ConfigPath()
|
2022-12-03 18:56:36 +01:00
|
|
|
|
|
|
|
if pathError != nil {
|
|
|
|
return pathError
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
var manifestPath = filepath.Join(configPath, "mods.csv")
|
2022-12-03 18:56:36 +01:00
|
|
|
|
|
|
|
// 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]
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
game.ModNames[name] = len(game.ModOrder)
|
|
|
|
|
|
|
|
game.ModOrder = append(game.ModOrder, Mod{
|
2022-12-03 18:56:36 +01:00
|
|
|
IsEnabled: status == "*",
|
2022-12-05 14:13:58 +01:00
|
|
|
Name: name,
|
|
|
|
Source: "",
|
|
|
|
})
|
2022-12-03 18:56:36 +01:00
|
|
|
|
|
|
|
recordValues, recordError = manifestReader.Read()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if actionError := action(&game); actionError != nil {
|
|
|
|
return actionError
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save manifest back to disk.
|
|
|
|
if game.HasUpdated {
|
2022-12-05 14:13:58 +01:00
|
|
|
var manifestFile, openError = os.Create(manifestPath)
|
2022-12-03 18:56:36 +01:00
|
|
|
|
|
|
|
if openError != nil {
|
|
|
|
return openError
|
|
|
|
}
|
|
|
|
|
|
|
|
defer manifestFile.Close()
|
|
|
|
|
|
|
|
var manifestWriter = csv.NewWriter(manifestFile)
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
for i := range game.ModOrder {
|
|
|
|
var mod = game.ModOrder[i]
|
2022-12-03 18:56:36 +01:00
|
|
|
var status = ""
|
|
|
|
|
|
|
|
if mod.IsEnabled {
|
|
|
|
status = "*"
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
manifestWriter.Write([]string{status, mod.Name, mod.Source})
|
2022-12-03 18:56:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
manifestWriter.Flush()
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("%s: game not supported", gameName)
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
func (game *Game) RemoveMods(names []string) error {
|
|
|
|
var configPath, pathError = game.ConfigPath()
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
if pathError != nil {
|
|
|
|
return pathError
|
2022-12-03 18:56:36 +01:00
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
if len(names) != 0 {
|
|
|
|
game.HasUpdated = true
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
for i := range names {
|
|
|
|
var name = names[i]
|
|
|
|
var index, exists = game.ModNames[name]
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
if !(exists) {
|
|
|
|
return fmt.Errorf("unknown mod: `%s`", name)
|
|
|
|
}
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
if removeError := os.RemoveAll(filepath.Join(configPath, name)); removeError != nil {
|
|
|
|
return removeError
|
|
|
|
}
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
game.ModOrder = append(game.ModOrder[:index], game.ModOrder[index+1:]...)
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
delete(game.ModNames, name)
|
2022-12-03 18:56:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
return nil
|
2022-12-03 18:56:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (game *Game) RenameMod(modName string, newName string) error {
|
2022-12-05 14:13:58 +01:00
|
|
|
var configPath, pathError = game.ConfigPath()
|
2022-12-03 18:56:36 +01:00
|
|
|
|
|
|
|
if pathError != nil {
|
|
|
|
return pathError
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
if _, exists := game.ModNames[modName]; !(exists) {
|
2022-12-03 18:56:36 +01:00
|
|
|
return fmt.Errorf("no mod with that name exists")
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
if _, nameTaken := game.ModNames[newName]; nameTaken {
|
2022-12-03 18:56:36 +01:00
|
|
|
return fmt.Errorf("a mod with the new name already exists")
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
var modsPath = filepath.Join(configPath, "mods")
|
|
|
|
|
|
|
|
if renameError := os.Rename(filepath.Join(modsPath, modName),
|
|
|
|
filepath.Join(modsPath, newName)); renameError != nil {
|
2022-12-03 18:56:36 +01:00
|
|
|
|
|
|
|
return renameError
|
|
|
|
}
|
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
game.ModNames[newName] = game.ModNames[modName]
|
2022-12-03 18:56:36 +01:00
|
|
|
|
2022-12-05 14:13:58 +01:00
|
|
|
delete(game.ModNames, modName)
|
2022-12-03 18:56:36 +01:00
|
|
|
|
|
|
|
game.HasUpdated = true
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|