Compare commits
10 Commits
d179a58c29
...
fd18f10a9c
| Author | SHA1 | Date | |
|---|---|---|---|
| fd18f10a9c | |||
| 55087d6940 | |||
| dad6742927 | |||
| 703d31c91d | |||
| cf65351d4e | |||
| 0553968907 | |||
| fb0c743e7f | |||
| a14fd98113 | |||
| ec40f2b695 | |||
|
|
a18e7874d0 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,5 +2,9 @@
|
||||
bin/
|
||||
examples/
|
||||
|
||||
fyne_*.go
|
||||
fyne.*
|
||||
|
||||
cpms.exe
|
||||
config.yaml
|
||||
config.yaml
|
||||
cpms.7z
|
||||
|
||||
29
README.md
Normal file
29
README.md
Normal 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
13
admin/admin_linux.go
Normal 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
41
admin/admin_windows.go
Normal 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,2 +0,0 @@
|
||||
profiles_root: E:/GoWork/cpms/examples/profiles
|
||||
archive_root: E:/GoWork/cpms/examples/archive
|
||||
7
go.mod
7
go.mod
@@ -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
3
go.sum
@@ -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=
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
114
main.go
114
main.go
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user