Implement most unimplemented functions
This commit is contained in:
parent
e44f196236
commit
21b999e6eb
182
main.go
182
main.go
|
@ -1,118 +1,178 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Name string
|
||||
Description string
|
||||
Action func([]string) Result
|
||||
Action func([]string) (string, error)
|
||||
Arguments []string
|
||||
IsVarargs bool
|
||||
}
|
||||
|
||||
func Failed(message string) Result {
|
||||
return Result{
|
||||
HasFailed: true,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func Ok(message string) Result {
|
||||
return Result{
|
||||
HasFailed: false,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Message string
|
||||
HasFailed bool
|
||||
}
|
||||
|
||||
var commands = []Command{
|
||||
{
|
||||
Name: "install",
|
||||
Description: "Install one or more mod archives into modman",
|
||||
Arguments: []string{"archive path"},
|
||||
Arguments: []string{"game name", "archive path"},
|
||||
IsVarargs: true,
|
||||
|
||||
Action: func(arguments []string) Result {
|
||||
var processed = 0
|
||||
Action: func(arguments []string) (string, error) {
|
||||
var argumentCount = len(arguments)
|
||||
|
||||
for i := range arguments {
|
||||
var archivePath = arguments[i]
|
||||
|
||||
if strings.HasSuffix(archivePath, ".zip") {
|
||||
var reader, err = zip.OpenReader(archivePath)
|
||||
|
||||
if err == nil {
|
||||
defer reader.Close()
|
||||
|
||||
processed += 1
|
||||
if argumentCount == 0 {
|
||||
return "", fmt.Errorf("expected game name")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
var modsToInstall = arguments[1:]
|
||||
var installedMods = []string{}
|
||||
|
||||
if gameError := WithGame(arguments[0], func(game *Game) error {
|
||||
var installed, installError = game.InstallMods(modsToInstall)
|
||||
|
||||
if installError != nil {
|
||||
return installError
|
||||
}
|
||||
|
||||
return Ok(fmt.Sprint(processed, "of", len(arguments), "installed"))
|
||||
installedMods = installed
|
||||
|
||||
return nil
|
||||
}); gameError != nil {
|
||||
return "", gameError
|
||||
}
|
||||
|
||||
return fmt.Sprint(len(installedMods), " of ", len(modsToInstall), " installed"), nil
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
Name: "remove",
|
||||
Description: "Remove one or more mods from modman",
|
||||
Arguments: []string{"mod name"},
|
||||
Arguments: []string{"game name", "mod name"},
|
||||
IsVarargs: true,
|
||||
|
||||
Action: func(arguments []string) Result {
|
||||
// TODO: Implement.
|
||||
return Failed("Not implemented")
|
||||
Action: func(arguments []string) (string, error) {
|
||||
var argumentCount = len(arguments)
|
||||
|
||||
if argumentCount == 0 {
|
||||
return "", fmt.Errorf("expected game name")
|
||||
}
|
||||
|
||||
var modsToRemove = arguments[1:]
|
||||
var removedMods = []string{}
|
||||
|
||||
if gameError := WithGame(arguments[0], func(game *Game) error {
|
||||
var removed, removeError = game.RemoveMods(modsToRemove)
|
||||
|
||||
if removeError != nil {
|
||||
return removeError
|
||||
}
|
||||
|
||||
removedMods = removed
|
||||
|
||||
return nil
|
||||
}); gameError != nil {
|
||||
return "", gameError
|
||||
}
|
||||
|
||||
return fmt.Sprint(len(removedMods), " of ", len(removedMods), " removed"), nil
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
Name: "rename",
|
||||
Description: "Rename a mod within modman",
|
||||
Arguments: []string{"mod name", "new name"},
|
||||
Arguments: []string{"game name", "mod name", "new name"},
|
||||
IsVarargs: false,
|
||||
|
||||
Action: func(arguments []string) Result {
|
||||
// TODO: Implement.
|
||||
return Failed("Not implemented")
|
||||
Action: func(arguments []string) (string, error) {
|
||||
if len(arguments) != 3 {
|
||||
return "", fmt.Errorf("expected game name followed by mod name and new name")
|
||||
}
|
||||
|
||||
if gameError := WithGame(arguments[0], func(game *Game) error {
|
||||
if removeError := game.RenameMod(arguments[1], arguments[2]); removeError != nil {
|
||||
return removeError
|
||||
}
|
||||
|
||||
return nil
|
||||
}); gameError != nil {
|
||||
return "", gameError
|
||||
}
|
||||
|
||||
return "", nil
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List all installed mods",
|
||||
Arguments: []string{},
|
||||
Name: "manifest",
|
||||
Description: "Retrieve a manifest of all installed mods",
|
||||
Arguments: []string{"game name", "format"},
|
||||
IsVarargs: false,
|
||||
|
||||
Action: func(arguments []string) Result {
|
||||
// TODO: Implement.
|
||||
return Failed("Not implemented")
|
||||
Action: func(arguments []string) (string, error) {
|
||||
if len(arguments) != 2 {
|
||||
return "", fmt.Errorf("expected game name followed by format")
|
||||
}
|
||||
|
||||
var modManifest = ""
|
||||
|
||||
if gameError := WithGame(arguments[0], func(game *Game) error {
|
||||
var format = arguments[1]
|
||||
var formatter, formatterExists = formatters[format]
|
||||
|
||||
if !(formatterExists) {
|
||||
return fmt.Errorf("unsupported format: `%s`", format)
|
||||
}
|
||||
|
||||
var formattedManifest, formatError = formatter(game.Mods)
|
||||
|
||||
if formatError != nil {
|
||||
return formatError
|
||||
}
|
||||
|
||||
// TODO: Reconsider if always casting formatted data to string for output is a good
|
||||
// idea.
|
||||
modManifest = string(formattedManifest)
|
||||
|
||||
return nil
|
||||
}); gameError != nil {
|
||||
return "", gameError
|
||||
}
|
||||
|
||||
return modManifest, nil
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
Name: "deploy",
|
||||
Description: "Deploy all installed and enabled mods",
|
||||
Arguments: []string{},
|
||||
Arguments: []string{"game name"},
|
||||
IsVarargs: false,
|
||||
|
||||
Action: func(arguments []string) Result {
|
||||
Action: func(arguments []string) (string, error) {
|
||||
// TODO: Implement.
|
||||
return Failed("Not implemented")
|
||||
return "", fmt.Errorf("not implemented")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var formatters = map[string]func(any) ([]byte, error){
|
||||
"json": func(data any) ([]byte, error) {
|
||||
var marshalledJson, marshalError = json.Marshal(data)
|
||||
|
||||
if marshalError != nil {
|
||||
return nil, marshalError
|
||||
}
|
||||
|
||||
return marshalledJson, nil
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
var argCount = len(os.Args)
|
||||
|
||||
|
@ -145,16 +205,20 @@ func main() {
|
|||
var command = commands[i]
|
||||
|
||||
if command.Name == commandName {
|
||||
var result = command.Action(os.Args[2:])
|
||||
var response, actionError = command.Action(os.Args[2:])
|
||||
|
||||
if result.HasFailed {
|
||||
fmt.Fprint(os.Stderr, result.Message)
|
||||
if actionError != nil {
|
||||
fmt.Fprintln(os.Stderr, actionError.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Print(result.Message)
|
||||
if len(response) != 0 {
|
||||
fmt.Println(response)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "unknown command: `%s`\n", commandName)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
ConfigDirPath string
|
||||
}
|
||||
|
||||
type Game struct {
|
||||
ID string
|
||||
Mods map[string]Mod
|
||||
HasUpdated bool
|
||||
}
|
||||
|
||||
func (game *Game) InstallMods(archivePaths []string) ([]string, error) {
|
||||
var processed = 0
|
||||
|
||||
for i := range archivePaths {
|
||||
var archivePath = archivePaths[i]
|
||||
var suffixIndex = strings.LastIndex(archivePath, ".")
|
||||
|
||||
if suffixIndex < 0 {
|
||||
return nil, fmt.Errorf("cannot determine file type of `%s`", archivePath)
|
||||
}
|
||||
|
||||
var extension = archivePath[suffixIndex+1:]
|
||||
var extractor = extractors[extension]
|
||||
|
||||
if extractor == nil {
|
||||
return nil, fmt.Errorf("unsupported file type `%s`", extension)
|
||||
}
|
||||
|
||||
var gamePath, pathError = game.Path()
|
||||
|
||||
if pathError != nil {
|
||||
return nil, pathError
|
||||
}
|
||||
|
||||
var modName = filepath.Base(archivePath[:suffixIndex])
|
||||
|
||||
if extractError := extractor(archivePath, filepath.Join(
|
||||
gamePath, "mods", modName)); extractError != nil {
|
||||
|
||||
return nil, extractError
|
||||
}
|
||||
|
||||
game.Mods[modName] = Mod{
|
||||
IsEnabled: false,
|
||||
}
|
||||
|
||||
game.HasUpdated = true
|
||||
processed += 1
|
||||
}
|
||||
|
||||
return archivePaths[0:processed], nil
|
||||
}
|
||||
|
||||
type Mod struct {
|
||||
IsEnabled bool
|
||||
}
|
||||
|
||||
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,
|
||||
Mods: make(map[string]Mod),
|
||||
HasUpdated: false,
|
||||
}
|
||||
|
||||
var gamePath, pathError = game.Path()
|
||||
|
||||
if pathError != nil {
|
||||
return pathError
|
||||
}
|
||||
|
||||
var manifestPath = filepath.Join(gamePath, "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.Mods[name] = Mod{
|
||||
IsEnabled: status == "*",
|
||||
}
|
||||
|
||||
recordValues, recordError = manifestReader.Read()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if actionError := action(&game); actionError != nil {
|
||||
return actionError
|
||||
}
|
||||
|
||||
// Save manifest back to disk.
|
||||
if game.HasUpdated {
|
||||
var manifestFile, openError = os.OpenFile(manifestPath,
|
||||
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
|
||||
if openError != nil {
|
||||
return openError
|
||||
}
|
||||
|
||||
defer manifestFile.Close()
|
||||
|
||||
var manifestWriter = csv.NewWriter(manifestFile)
|
||||
|
||||
for name, mod := range game.Mods {
|
||||
var status = ""
|
||||
|
||||
if mod.IsEnabled {
|
||||
status = "*"
|
||||
}
|
||||
|
||||
manifestWriter.Write([]string{status, name})
|
||||
}
|
||||
|
||||
manifestWriter.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("%s: game not supported", gameName)
|
||||
}
|
||||
|
||||
func (game Game) Path() (string, error) {
|
||||
var configDirPath, configError = os.UserConfigDir()
|
||||
|
||||
if configError != nil {
|
||||
return "", configError
|
||||
}
|
||||
|
||||
return filepath.Join(configDirPath, "modman", game.ID), nil
|
||||
}
|
||||
|
||||
func (game *Game) RemoveMods(modNames []string) ([]string, error) {
|
||||
var gamePath, pathError = game.Path()
|
||||
|
||||
if pathError != nil {
|
||||
return nil, pathError
|
||||
}
|
||||
|
||||
var processed = 0
|
||||
|
||||
for i := range modNames {
|
||||
var modName = modNames[i]
|
||||
|
||||
if removeError := os.RemoveAll(filepath.Join(gamePath, modName)); removeError != nil {
|
||||
return nil, removeError
|
||||
}
|
||||
|
||||
delete(game.Mods, modName)
|
||||
|
||||
game.HasUpdated = true
|
||||
processed += 1
|
||||
}
|
||||
|
||||
return modNames[0:processed], nil
|
||||
}
|
||||
|
||||
func (game *Game) RenameMod(modName string, newName string) error {
|
||||
var gamePath, pathError = game.Path()
|
||||
|
||||
if pathError != nil {
|
||||
return pathError
|
||||
}
|
||||
|
||||
if _, exists := game.Mods[modName]; !(exists) {
|
||||
return fmt.Errorf("no mod with that name exists")
|
||||
}
|
||||
|
||||
if _, exists := game.Mods[newName]; exists {
|
||||
return fmt.Errorf("a mod with the new name already exists")
|
||||
}
|
||||
|
||||
if renameError := os.Rename(filepath.Join(gamePath, modName),
|
||||
filepath.Join(gamePath, newName)); renameError != nil {
|
||||
|
||||
return renameError
|
||||
}
|
||||
|
||||
game.Mods[newName] = game.Mods[modName]
|
||||
|
||||
delete(game.Mods, modName)
|
||||
|
||||
game.HasUpdated = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var extractors = map[string]func(string, string) error{
|
||||
"zip": func(archivePath string, destinationPath string) error {
|
||||
var zipReader, openReaderError = zip.OpenReader(archivePath)
|
||||
|
||||
if openReaderError != nil {
|
||||
return openReaderError
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue