ficsit-cli-flake/cli/profiles.go

326 lines
7.5 KiB
Go
Raw Normal View History

2021-11-05 21:42:49 +00:00
package cli
2021-12-02 04:00:33 +00:00
import (
"encoding/json"
"fmt"
"os"
2022-06-05 01:56:46 +00:00
"path/filepath"
"strings"
2021-12-02 04:00:33 +00:00
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/satisfactorymodding/ficsit-cli/utils"
"github.com/spf13/viper"
)
2021-12-04 18:02:05 +00:00
const DefaultProfileName = "Default"
2021-12-02 04:00:33 +00:00
var defaultProfile = Profile{
2021-12-04 18:02:05 +00:00
Name: DefaultProfileName,
2021-12-02 04:00:33 +00:00
}
type ProfilesVersion int
const (
InitialProfilesVersion = ProfilesVersion(iota)
// Always last
nextProfilesVersion
)
2022-04-14 01:27:39 +00:00
type smmProfileFile struct {
Items []struct {
ID string `json:"id"`
Enabled bool `json:"enabled"`
} `json:"items"`
}
2021-12-02 04:00:33 +00:00
type Profiles struct {
2021-12-04 18:02:05 +00:00
Version ProfilesVersion `json:"version"`
Profiles map[string]*Profile `json:"profiles"`
SelectedProfile string `json:"selected_profile"`
2021-12-02 04:00:33 +00:00
}
2021-11-05 21:42:49 +00:00
type Profile struct {
2021-12-02 04:00:33 +00:00
Name string `json:"name"`
Mods map[string]ProfileMod `json:"mods"`
}
type ProfileMod struct {
2022-04-14 01:27:39 +00:00
Version string `json:"version"`
2022-05-02 20:07:15 +00:00
Enabled bool `json:"enabled"`
2021-12-02 04:00:33 +00:00
}
func InitProfiles() (*Profiles, error) {
2022-04-14 01:27:39 +00:00
localDir := viper.GetString("local-dir")
2021-12-02 04:00:33 +00:00
2022-06-05 01:56:46 +00:00
profilesFile := filepath.Join(localDir, viper.GetString("profiles-file"))
2021-12-02 04:00:33 +00:00
_, err := os.Stat(profilesFile)
if err != nil {
if !os.IsNotExist(err) {
return nil, errors.Wrap(err, "failed to stat profiles file")
}
2022-04-14 01:27:39 +00:00
_, err := os.Stat(localDir)
2021-12-02 04:00:33 +00:00
if err != nil {
if !os.IsNotExist(err) {
return nil, errors.Wrap(err, "failed to read cache directory")
}
2022-04-14 01:27:39 +00:00
err = os.MkdirAll(localDir, 0755)
2021-12-02 04:00:33 +00:00
if err != nil {
return nil, errors.Wrap(err, "failed to create cache directory")
}
}
2022-04-14 01:27:39 +00:00
profiles := map[string]*Profile{
DefaultProfileName: &defaultProfile,
}
// Import profiles from SMM if already exists
2022-06-05 01:56:46 +00:00
smmProfilesDir := filepath.Join(viper.GetString("base-local-dir"), "SatisfactoryModManager", "profiles")
2022-04-14 01:27:39 +00:00
_, err = os.Stat(smmProfilesDir)
if err == nil {
dir, err := os.ReadDir(smmProfilesDir)
if err == nil {
for _, entry := range dir {
if entry.IsDir() {
2022-06-05 01:56:46 +00:00
manifestFile := filepath.Join(smmProfilesDir, entry.Name(), "manifest.json")
2022-04-14 01:27:39 +00:00
_, err := os.Stat(manifestFile)
if err == nil {
manifestBytes, err := os.ReadFile(manifestFile)
if err != nil {
log.Err(err).Str("file", manifestFile).Msg("Failed to read file, not importing profile")
continue
}
var smmProfile smmProfileFile
if err := json.Unmarshal(manifestBytes, &smmProfile); err != nil {
log.Err(err).Str("file", manifestFile).Msg("Failed to parse file, not importing profile")
continue
}
profile := &Profile{
Name: entry.Name(),
Mods: make(map[string]ProfileMod),
}
for _, item := range smmProfile.Items {
// Explicitly ignore bootstrapper
if strings.ToLower(item.ID) == "bootstrapper" {
continue
}
2022-04-14 01:27:39 +00:00
profile.Mods[item.ID] = ProfileMod{
Version: ">=0.0.0",
Enabled: item.Enabled,
}
}
profiles[entry.Name()] = profile
}
}
}
}
2021-12-02 04:00:33 +00:00
}
2022-04-14 01:27:39 +00:00
bootstrapProfiles := &Profiles{
Version: nextProfilesVersion - 1,
Profiles: profiles,
}
if err := bootstrapProfiles.Save(); err != nil {
2021-12-02 04:00:33 +00:00
return nil, errors.Wrap(err, "failed to save empty profiles")
}
}
profilesData, err := os.ReadFile(profilesFile)
if err != nil {
return nil, errors.Wrap(err, "failed to read profiles")
}
var profiles Profiles
if err := json.Unmarshal(profilesData, &profiles); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal profiles")
}
if profiles.Version >= nextProfilesVersion {
return nil, fmt.Errorf("unknown profiles version: %d", profiles.Version)
}
if len(profiles.Profiles) == 0 {
2021-12-04 18:02:05 +00:00
profiles.Profiles = map[string]*Profile{
DefaultProfileName: &defaultProfile,
}
profiles.SelectedProfile = DefaultProfileName
2021-12-02 04:00:33 +00:00
}
2021-12-04 18:02:05 +00:00
if profiles.SelectedProfile == "" || profiles.Profiles[profiles.SelectedProfile] == nil {
profiles.SelectedProfile = DefaultProfileName
2021-12-02 04:00:33 +00:00
}
return &profiles, nil
}
// Save the profiles to the profiles file.
func (p *Profiles) Save() error {
if viper.GetBool("dry-run") {
log.Info().Msg("dry-run: skipping profile saving")
return nil
}
2022-06-05 01:56:46 +00:00
profilesFile := filepath.Join(viper.GetString("local-dir"), viper.GetString("profiles-file"))
2021-12-02 04:00:33 +00:00
log.Info().Str("path", profilesFile).Msg("saving profiles")
profilesJSON, err := json.MarshalIndent(p, "", " ")
if err != nil {
return errors.Wrap(err, "failed to marshal profiles")
}
if err := os.WriteFile(profilesFile, profilesJSON, 0755); err != nil {
return errors.Wrap(err, "failed to write profiles")
}
return nil
}
// AddProfile adds a new profile with the given name to the profiles list.
2021-12-04 18:02:05 +00:00
func (p *Profiles) AddProfile(name string) (*Profile, error) {
if _, ok := p.Profiles[name]; ok {
return nil, fmt.Errorf("profile with name %s already exists", name)
2021-12-02 04:00:33 +00:00
}
2021-12-04 18:02:05 +00:00
p.Profiles[name] = &Profile{
Name: name,
}
2021-12-02 04:00:33 +00:00
2021-12-04 18:02:05 +00:00
return p.Profiles[name], nil
2021-12-02 04:00:33 +00:00
}
// DeleteProfile deletes the profile with the given name.
2021-12-04 18:02:05 +00:00
func (p *Profiles) DeleteProfile(name string) error {
if _, ok := p.Profiles[name]; ok {
delete(p.Profiles, name)
if p.SelectedProfile == name {
p.SelectedProfile = DefaultProfileName
2021-12-02 04:00:33 +00:00
}
2021-12-04 18:02:05 +00:00
return nil
2021-12-02 04:00:33 +00:00
}
2021-12-04 18:02:05 +00:00
return fmt.Errorf("profile with name %s does not exist", name)
2021-12-02 04:00:33 +00:00
}
// GetProfile returns the profile with the given name or nil if it doesn't exist.
func (p *Profiles) GetProfile(name string) *Profile {
2021-12-04 18:02:05 +00:00
return p.Profiles[name]
}
func (p *Profiles) RenameProfile(oldName string, newName string) error {
if _, ok := p.Profiles[newName]; ok {
return fmt.Errorf("profile with name %s already exists", newName)
}
if _, ok := p.Profiles[oldName]; !ok {
return fmt.Errorf("profile with name %s does not exist", oldName)
}
p.Profiles[oldName].Name = newName
p.Profiles[newName] = p.Profiles[oldName]
delete(p.Profiles, oldName)
if p.SelectedProfile == oldName {
p.SelectedProfile = newName
2021-12-02 04:00:33 +00:00
}
return nil
}
// AddMod adds a mod to the profile with given version.
func (p *Profile) AddMod(reference string, version string) error {
if p.Mods == nil {
p.Mods = make(map[string]ProfileMod)
}
if !utils.SemVerRegex.MatchString(version) {
return errors.New("invalid semver version")
}
p.Mods[reference] = ProfileMod{
Version: version,
2022-05-02 20:07:15 +00:00
Enabled: true,
2021-12-02 04:00:33 +00:00
}
return nil
}
// RemoveMod removes a mod from the profile.
func (p *Profile) RemoveMod(reference string) {
if p.Mods == nil {
return
}
delete(p.Mods, reference)
}
2022-04-14 01:27:39 +00:00
// HasMod returns true if the profile has a mod with the given reference.
2021-12-02 04:00:33 +00:00
func (p *Profile) HasMod(reference string) bool {
if p.Mods == nil {
return false
}
_, ok := p.Mods[reference]
return ok
2021-11-05 21:42:49 +00:00
}
2022-04-14 01:27:39 +00:00
// Resolve resolves all mods and their dependencies.
//
// An optional lockfile can be passed if one exists.
//
// Returns an error if resolution is impossible.
2022-05-02 20:07:15 +00:00
func (p *Profile) Resolve(resolver DependencyResolver, lockFile *LockFile, gameVersion int) (LockFile, error) {
2022-04-14 01:27:39 +00:00
toResolve := make(map[string]string)
for modReference, mod := range p.Mods {
2022-05-02 20:07:15 +00:00
if mod.Enabled {
toResolve[modReference] = mod.Version
}
2022-04-14 01:27:39 +00:00
}
2022-05-02 20:07:15 +00:00
resultLockfile, err := resolver.ResolveModDependencies(toResolve, lockFile, gameVersion)
2022-04-14 01:27:39 +00:00
if err != nil {
return nil, errors.Wrap(err, "failed resolving profile dependencies")
}
2022-05-02 20:07:15 +00:00
return resultLockfile, nil
2022-04-14 01:27:39 +00:00
}
func (p *Profile) IsModEnabled(reference string) bool {
if p.Mods == nil {
return false
}
if mod, ok := p.Mods[reference]; ok {
return mod.Enabled
}
return false
}
func (p *Profile) SetModEnabled(reference string, enabled bool) {
if p.Mods == nil {
return
}
if _, ok := p.Mods[reference]; !ok {
return
}
p.Mods[reference] = ProfileMod{
Version: p.Mods[reference].Version,
Enabled: enabled,
}
}