Compare commits

...

10 Commits

10 changed files with 145 additions and 71 deletions

6
.gitignore vendored
View File

@@ -2,5 +2,9 @@
bin/
examples/
fyne_*.go
fyne.*
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 (
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
)
@@ -32,7 +32,6 @@ require (
golang.org/x/image v0.3.0 // indirect
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // 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
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/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/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/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=

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

114
main.go
View File

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