Files
bd2/go/internal/client/layout/layout.go
T

121 lines
3.5 KiB
Go

// Package layout resolves the supported Windows and macOS Brown Dust II
// installation layouts without relying on the host running bd2client.
package layout
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
type Kind string
const (
Windows Kind = "windows"
MacOS Kind = "macos"
)
type Installation struct {
Kind Kind
Selected string
Root string
Executable string
Data string
Resources string
Managers string
BepInEx string
Config string
Plugins string
Disabled string
}
func Resolve(selected string) (Installation, error) {
if strings.TrimSpace(selected) == "" {
return Installation{}, errors.New("select the Brown Dust II installation directory")
}
abs, err := filepath.Abs(strings.TrimSpace(selected))
if err != nil {
return Installation{}, fmt.Errorf("resolve game directory: %w", err)
}
abs = filepath.Clean(abs)
if installation, ok := windowsLayout(abs); ok {
return installation, nil
}
if installation, ok := macLayout(abs); ok {
return installation, nil
}
return Installation{}, errors.New("the selected directory is not a complete Brown Dust II Windows or macOS client")
}
func windowsLayout(root string) (Installation, bool) {
executable := filepath.Join(root, "BrownDust II.exe")
data := filepath.Join(root, "BrownDust II_Data")
if !regularFile(executable) || !regularFile(filepath.Join(data, "resources.assets")) {
return Installation{}, false
}
return newInstallation(Windows, root, root, executable, data, filepath.Join(root, "BepInEx")), true
}
func macLayout(selected string) (Installation, bool) {
candidates := []string{selected}
if !strings.EqualFold(filepath.Ext(selected), ".app") {
candidates = append(candidates, filepath.Join(selected, "BrownDust II.app"))
}
for _, app := range candidates {
contents := filepath.Join(app, "Contents")
executable := filepath.Join(contents, "MacOS", "BrownDust II")
data := filepath.Join(contents, "Resources", "Data")
if regularFile(executable) && regularFile(filepath.Join(data, "resources.assets")) {
// BepInEx Unix distributions are normally extracted beside the
// .app bundle. Also accept an installation placed inside Contents.
bepInEx := filepath.Join(filepath.Dir(app), "BepInEx")
insideBundle := filepath.Join(contents, "BepInEx")
if directoryExists(insideBundle) && !directoryExists(bepInEx) {
bepInEx = insideBundle
}
return newInstallation(MacOS, selected, app, executable, data, bepInEx), true
}
}
return Installation{}, false
}
func newInstallation(kind Kind, selected, root, executable, data, bepInEx string) Installation {
return Installation{
Kind: kind,
Selected: selected,
Root: root,
Executable: executable,
Data: data,
Resources: filepath.Join(data, "resources.assets"),
Managers: filepath.Join(data, "globalgamemanagers"),
BepInEx: bepInEx,
Config: filepath.Join(bepInEx, "config"),
Plugins: filepath.Join(bepInEx, "plugins"),
Disabled: filepath.Join(bepInEx, "disabled"),
}
}
func (i Installation) LaunchTarget() string {
if i.Kind == MacOS {
return i.Root
}
return i.Executable
}
func (i Installation) SupportedOnHost() bool {
return (i.Kind == Windows && runtime.GOOS == "windows") || (i.Kind == MacOS && runtime.GOOS == "darwin")
}
func regularFile(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsRegular()
}
func directoryExists(path string) bool {
info, err := os.Stat(path)
return err == nil && info.IsDir()
}