Compare commits

..

10 Commits

10 changed files with 145 additions and 71 deletions

4
.gitignore vendored
View File

@@ -2,5 +2,9 @@
bin/ bin/
examples/ examples/
fyne_*.go
fyne.*
cpms.exe cpms.exe
config.yaml config.yaml
cpms.7z

29
README.md Normal file
View File

@@ -0,0 +1,29 @@
# Swiped Mod Switcher
A simple but effective mod switcher for simple game such as Sims and Cyberpunk 2077.
it works by switching out a games mod folder with one of the ones made by you and placed under the profiles directory.
the switch is really just a symbolic link so it happens in milliseconds even on slow hard drives regardless of the amount or size of mods in the folder.
### Upsides
- it is very very fast even switching a folder with 100+GB on a slow slow hdd in a few milliseconds.
- makes having a normal and lewd version of your mods easy to switch between.
- makes having a different set of mods depending on who your playing with easier aswell.
### Downsides
- it does not manage mods you manage them yourself and place them in the profiles directory.
- it does not deduplicate your mods if you have mods 'A' installed in 2 profiles it will take size of 'A' x2 room on your hdd.
- because it switches your games mods folder it does work with existing mod managers mostly however they may get slow having to figure out what mods are installed already when they open or may even get borked if they keep an external journal of what is installed.
### Never Happening
- deduplicating mods this will never be supported as this would be a drastic change from being a mod switcher to being a mod manager and that's a pile of crazy I don't want to touch.
## Installation
1. Download the latest version from the [releases]("https://git.s1d3sw1ped.com/s1d3sw1ped/SwipedModSwitcher/releases") page.
2. Make a folder and place it in there.
3. Make a second folder in that folder named profiles.
4. Open Swiped Mod Switcher and select the second folder in the Mod Profiles Directory selection.
5. Then select your games current mod folder in the Game Mods Directory selection.
6. Now if you already have some mods installed make a new folder in the Mod Profiles Directory name it whatever you would like to call the profile and then copy the current mods to that folder once done you can delete the games mod folder dont worry Swiped Mod Switcher will remake the folder itself when you select a profile.
7. Click Reload Mod Profiles and it should display your profile then simply click it to switch to that profile.
8. Play your game or create more profiles the choice at this point is yours.

13
admin/admin_linux.go Normal file
View File

@@ -0,0 +1,13 @@
package admin
import "errors"
var ErrNotImplemented = errors.New("not implemented on linux")
func RunSelfElevated() error {
return ErrNotImplemented
}
func Admin() bool {
return true
}

41
admin/admin_windows.go Normal file
View File

@@ -0,0 +1,41 @@
package admin
import (
"fmt"
"os"
"strings"
"syscall"
"golang.org/x/sys/windows"
)
func RunSelfElevated() error {
verb := "runas"
exe, _ := os.Executable()
cwd, _ := os.Getwd()
args := strings.Join(os.Args[1:], " ")
verbPtr, _ := syscall.UTF16PtrFromString(verb)
exePtr, _ := syscall.UTF16PtrFromString(exe)
cwdPtr, _ := syscall.UTF16PtrFromString(cwd)
argPtr, _ := syscall.UTF16PtrFromString(args)
var showCmd int32 = 1 //SW_NORMAL
err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, showCmd)
if err != nil {
fmt.Println(err)
}
return nil
}
func Admin() bool {
_, err := os.Open("\\\\.\\PHYSICALDRIVE0")
if err != nil {
fmt.Println("admin no")
return false
}
fmt.Println("admin yes")
return true
}

1
build.cmd Normal file
View File

@@ -0,0 +1 @@
fyne package -os windows -icon .\cpms.png

View File

@@ -1,2 +0,0 @@
profiles_root: E:/GoWork/cpms/examples/profiles
archive_root: E:/GoWork/cpms/examples/archive

7
go.mod
View File

@@ -1,10 +1,10 @@
module s1d3sw1ped/cpms module s1d3sw1ped/swipedmodswitcher
go 1.20 go 1.22
require ( require (
fyne.io/fyne/v2 v2.3.5 fyne.io/fyne/v2 v2.3.5
github.com/otiai10/copy v1.12.0 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -32,7 +32,6 @@ require (
golang.org/x/image v0.3.0 // indirect golang.org/x/image v0.3.0 // indirect
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // indirect golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/text v0.6.0 // indirect golang.org/x/text v0.6.0 // indirect
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
) )

3
go.sum
View File

@@ -234,9 +234,6 @@ github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/otiai10/copy v1.12.0 h1:cLMgSQnXBs1eehF0Wy/FAGsgDTDmAqFR7rQylBb1nDY=
github.com/otiai10/copy v1.12.0/go.mod h1:rSaLseMUsZFFbsFGc7wCJnnkTAvdc5L6VWxPE4308Ww=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

112
main.go
View File

@@ -2,9 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"s1d3sw1ped/swipedmodswitcher/admin"
"slices"
"strings" "strings"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
@@ -14,13 +15,11 @@ import (
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
cp "github.com/otiai10/copy"
) )
type Config struct { type Config struct {
ProfilesRoot string `yaml:"profiles_root"` ModProfilesDirectory string `yaml:"mod_profiles_dir"`
ArchiveRoot string `yaml:"archive_root"` GameModsDirectory string `yaml:"game_mods_dir"`
} }
func saveConfig(cfg Config) { func saveConfig(cfg Config) {
@@ -39,7 +38,7 @@ func loadConfig() *Config {
_, err := os.Stat("config.yaml") _, err := os.Stat("config.yaml")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
saveConfig(Config{ProfilesRoot: "", ArchiveRoot: ""}) saveConfig(Config{ModProfilesDirectory: "", GameModsDirectory: ""})
} }
d, err := os.ReadFile("config.yaml") d, err := os.ReadFile("config.yaml")
@@ -57,31 +56,40 @@ func loadConfig() *Config {
return &cfg return &cfg
} }
var cfg = loadConfig() var cfg *Config
func main() { func main() {
if !admin.Admin() {
if err := admin.RunSelfElevated(); err != nil {
panic(err)
}
os.Exit(0)
}
cfg = loadConfig()
ap := app.New() ap := app.New()
wp := ap.NewWindow("Cyberpunk Mod Switcher") wp := ap.NewWindow("Swiped Mod Switcher")
wp.Resize(fyne.NewSize(1024, 768)) wp.Resize(fyne.NewSize(600, 1))
wp.CenterOnScreen() wp.CenterOnScreen()
grid := container.NewVBox() grid := container.NewVBox()
scroll := container.NewVScroll(grid) scroll := container.NewVScroll(grid)
scroll.SetMinSize(fyne.NewSize(-1, 768)) scroll.SetMinSize(fyne.NewSize(-1, 400))
//load grid //load grid
fillGrid(grid, wp) fillGrid(grid, wp)
profilesrootentry := widget.NewEntry() profilesrootentry := widget.NewEntry()
profilesrootentry.SetText(cfg.ProfilesRoot) profilesrootentry.SetText(cfg.ModProfilesDirectory)
profilesrootentry.Disable() profilesrootentry.Disable()
archiverootentry := widget.NewEntry() archiverootentry := widget.NewEntry()
archiverootentry.SetText(cfg.ArchiveRoot) archiverootentry.SetText(cfg.GameModsDirectory)
archiverootentry.Disable() archiverootentry.Disable()
container := container.NewVBox( container := container.NewVBox(
widget.NewForm(widget.NewFormItem("Profiles Directory", profilesrootentry)), widget.NewForm(widget.NewFormItem("Mod Profiles Directory", profilesrootentry)),
widget.NewButtonWithIcon("Select", theme.FolderIcon(), func() { widget.NewButtonWithIcon("Select", theme.FolderIcon(), func() {
dialog.NewFolderOpen(func(lu fyne.ListableURI, err error) { dialog.NewFolderOpen(func(lu fyne.ListableURI, err error) {
if err != nil { if err != nil {
@@ -91,12 +99,12 @@ func main() {
return return
} }
profilesrootentry.SetText(lu.Path()) profilesrootentry.SetText(lu.Path())
cfg.ProfilesRoot = lu.Path() cfg.ModProfilesDirectory = lu.Path()
saveConfig(*cfg) saveConfig(*cfg)
fillGrid(grid, wp) fillGrid(grid, wp)
}, wp).Show() }, wp).Show()
}), }),
widget.NewForm(widget.NewFormItem("Archive Directory", archiverootentry)), widget.NewForm(widget.NewFormItem("Game Mod Directory", archiverootentry)),
widget.NewButtonWithIcon("Select", theme.FolderIcon(), func() { widget.NewButtonWithIcon("Select", theme.FolderIcon(), func() {
dialog.NewFolderOpen(func(lu fyne.ListableURI, err error) { dialog.NewFolderOpen(func(lu fyne.ListableURI, err error) {
if err != nil { if err != nil {
@@ -106,13 +114,13 @@ func main() {
return return
} }
archiverootentry.SetText(lu.Path()) archiverootentry.SetText(lu.Path())
cfg.ArchiveRoot = lu.Path() cfg.GameModsDirectory = lu.Path()
saveConfig(*cfg) saveConfig(*cfg)
fillGrid(grid, wp) fillGrid(grid, wp)
}, wp).Show() }, wp).Show()
}), }),
widget.NewSeparator(), widget.NewSeparator(),
widget.NewButton("Reload Profiles", func() { widget.NewButton("Reload Mod Profiles", func() {
fillGrid(grid, wp) fillGrid(grid, wp)
}), }),
scroll, scroll,
@@ -122,72 +130,56 @@ func main() {
} }
func fillGrid(grid *fyne.Container, parent fyne.Window) { func fillGrid(grid *fyne.Container, parent fyne.Window) {
profiles := scanProfiles(cfg.ProfilesRoot) profiles := scanProfiles(cfg.ModProfilesDirectory)
archive := cfg.ArchiveRoot archive := cfg.GameModsDirectory
grid.Hide() grid.Hide()
defer grid.Show() defer grid.Show()
grid.RemoveAll() grid.RemoveAll()
for _, profile := range profiles { for _, profile := range profiles {
profilename := strings.TrimSuffix(filepath.Base(profile), ".cpms") profilename := filepath.Base(profile)
grid.Add(widget.NewButton(profilename, makeIFunc(profile, archive, parent))) grid.Add(widget.NewButton(profilename, makeIFunc(profile, archive, parent)))
} }
} }
func scanProfiles(profilesroot string) []string { func scanProfiles(profilesroot string) []string {
dir := filepath.Join(profilesroot, "*.cpms/") dir := filepath.Join(profilesroot, "*/")
m, err := filepath.Glob(dir) m, err := filepath.Glob(dir)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
return m mm := []string{}
for _, path := range m {
if s, err := os.Stat(path); err == nil && !strings.HasPrefix(s.Name(), ".") && s.IsDir() {
mm = append(mm, path)
}
}
slices.SortStableFunc(mm, func(a, b string) int {
return strings.Compare(a, b)
})
return mm
} }
func makeIFunc(profile, archive string, parent fyne.Window) func() { func makeIFunc(profile, archive string, parent fyne.Window) func() {
return func() { return func() {
if cfg.ArchiveRoot == "" || cfg.ProfilesRoot == "" { if cfg.GameModsDirectory == "" || cfg.ModProfilesDirectory == "" {
d := dialog.NewInformation("Notice", "you must select a archive root and profiles root before", parent) dialog.NewInformation("Notice", "you must select a game mod directory and mod profiles directory before", parent).Show()
d.Show()
return return
} }
dialog.NewConfirm("Are You Sure?", fmt.Sprintf("the current contents of %s will be lost", archive), func(b bool) { // delete any existing archive folder
if b { _ = os.RemoveAll(archive)
err := walk(archive, func(path string, info fs.FileInfo, err error) error {
// link the profile to the archive
err := os.Symlink(profile, archive)
if err != nil { if err != nil {
return err dialog.NewInformation("Error", "error linking most likely issue is Swiped Mod Switcher is not running as admin", parent).Show()
} return
err = os.Remove(path)
if err != nil {
fmt.Println(err)
}
return nil
})
if err != nil {
fmt.Println(err)
} }
err = cp.Copy(profile, archive) dialog.NewInformation("Success", fmt.Sprintf("filinked files from %s to %s", filepath.Base(profile), archive), parent).Show()
if err != nil {
fmt.Println(err)
}
dialog.NewInformation("Success", fmt.Sprintf("copied all files from %s to %s", filepath.Base(profile), archive), parent).Show()
}
}, parent).Show()
} }
} }
func walk(root string, fn filepath.WalkFunc) error {
initial := false
return filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
if !initial {
initial = true
return nil
}
return fn(path, info, err)
})
}