package main import ( "bufio" "encoding/csv" "fmt" "io" "io/fs" "os" "path/filepath" "strings" ) type App struct { ConfigDirPath string } 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 } 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 } } } } return nil } type Extractor func(string, string) error type Game struct { ID string ModOrder []Mod ModNames map[string]int Path string HasUpdated bool } func (game *Game) InstallMod(extractor Extractor, archivePath string) error { 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 pathError != nil { return pathError } if extractError := extractor(archivePath, filepath.Join( 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 } type Mod struct { IsEnabled bool 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 } 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 { 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 { return actionError } // Save manifest back to disk. if game.HasUpdated { 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 fmt.Errorf("%s: game not supported", gameName) } func (game *Game) RemoveMods(names []string) error { var configPath, pathError = game.ConfigPath() if pathError != nil { return pathError } if len(names) != 0 { game.HasUpdated = true for i := range names { var name = names[i] 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 } func (game *Game) RenameMod(modName string, newName string) error { var configPath, pathError = game.ConfigPath() if pathError != nil { return pathError } if _, exists := game.ModNames[modName]; !(exists) { return fmt.Errorf("no mod with that name exists") } if _, nameTaken := game.ModNames[newName]; nameTaken { 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 }