diff --git a/pkg/config/config.yaml b/pkg/config/config.yaml index e02f8c20..fdcba87b 100644 --- a/pkg/config/config.yaml +++ b/pkg/config/config.yaml @@ -28,6 +28,8 @@ library: ignored: - neogeo - pgm + # DOSBox filesystem state + - .pure # an explicit list of supported file extensions # which overrides Libretro emulator ROMs configs supported: @@ -216,6 +218,8 @@ emulator: # - skip_same_thread_save -- skip thread lock save (used with PPSSPP). # - uniqueSaveDir (bool) -- needed only for cores (like DosBox) that persist their state into one shared file. # This will allow for concurrent reading and saving of current states. + # - saveStateFs (string) -- the name of the file that will be initially copied into the save folder. + # All * symbols will be replaced to the name of the ROM. list: gba: lib: mgba_libretro @@ -281,6 +285,7 @@ emulator: folder: dos kbMouseSupport: true nonBlockingSave: true + saveStateFs: "*.pure.zip" hid: 0: [ 257, 513 ] 1: [ 257, 513 ] diff --git a/pkg/config/emulator.go b/pkg/config/emulator.go index 2fc71fe2..21c5b2ad 100644 --- a/pkg/config/emulator.go +++ b/pkg/config/emulator.go @@ -54,6 +54,7 @@ type LibretroCoreConfig struct { Options map[string]string Options4rom map[string]map[string]string // <(^_^)> Roms []string + SaveStateFs string Scale float64 UniqueSaveDir bool UsesLibCo bool diff --git a/pkg/games/library.go b/pkg/games/library.go index 80572a98..5586f464 100644 --- a/pkg/games/library.go +++ b/pkg/games/library.go @@ -21,7 +21,7 @@ type libConf struct { aliasFile string path string supported map[string]struct{} - ignored map[string]struct{} + ignored []string verbose bool watchMode bool sessionPath string @@ -98,7 +98,7 @@ func NewLib(conf config.Library, emu WithEmulatorInfo, log *logger.Logger) GameL aliasFile: conf.AliasFile, path: dir, supported: toMap(conf.Supported), - ignored: toMap(conf.Ignored), + ignored: conf.Ignored, verbose: conf.Verbose, watchMode: conf.WatchMode, sessionPath: emu.SessionStoragePath(), @@ -207,22 +207,36 @@ func (lib *library) Scan() { return err } - if info != nil && !info.IsDir() && lib.isExtAllowed(path) { - meta := getMetadata(path, dir) + if info == nil || info.IsDir() || !lib.isExtAllowed(path) { + return nil + } - meta.System = lib.emuConf.GetEmulator(meta.Type, meta.Path) + meta := metadata(path, dir) + meta.System = lib.emuConf.GetEmulator(meta.Type, meta.Path) - if aliases != nil { - k, ok := aliases[meta.Name] - if ok { - meta.Alias = k - } - } - - if _, ok := lib.config.ignored[meta.Name]; !ok { - games = append(games, meta) + if aliases != nil { + if k, ok := aliases[meta.Name]; ok { + meta.Alias = k } } + + ignored := false + for _, k := range lib.config.ignored { + if meta.Name == k { + ignored = true + break + } + + if len(k) > 0 && k[0] == '.' && strings.Contains(meta.Name, k) { + ignored = true + break + } + } + + if !ignored { + games = append(games, meta) + } + return nil }) @@ -322,8 +336,8 @@ func (lib *library) isExtAllowed(path string) bool { return ok } -// getMetadata returns game info from a path -func getMetadata(path string, basePath string) GameMetadata { +// metadata returns game info from a path +func metadata(path string, basePath string) GameMetadata { name := filepath.Base(path) ext := filepath.Ext(name) relPath, _ := filepath.Rel(basePath, path) diff --git a/pkg/os/os.go b/pkg/os/os.go index eb7c5f47..2887cecf 100644 --- a/pkg/os/os.go +++ b/pkg/os/os.go @@ -51,6 +51,18 @@ func GetUserHome() (string, error) { return me.HomeDir, nil } +func CopyFile(from string, to string) error { + bytesRead, err := os.ReadFile(from) + if err != nil { + return err + } + err = os.WriteFile(to, bytesRead, 0755) + if err != nil { + return err + } + return nil +} + func WriteFile(name string, data []byte, perm os.FileMode) error { return os.WriteFile(name, data, perm) } diff --git a/pkg/worker/caged/libretro/frontend.go b/pkg/worker/caged/libretro/frontend.go index 60792d70..057d7e21 100644 --- a/pkg/worker/caged/libretro/frontend.go +++ b/pkg/worker/caged/libretro/frontend.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "path/filepath" + "strings" "sync" "time" "unsafe" @@ -67,6 +68,7 @@ type Frontend struct { DisableCanvasPool bool SaveOnClose bool UniqueSaveDir bool + SaveStateFs string } type Device byte @@ -154,6 +156,7 @@ func (f *Frontend) LoadCore(emu string) { KbMouseSupport: conf.KbMouseSupport, } f.mu.Lock() + f.SaveStateFs = conf.SaveStateFs if conf.UniqueSaveDir { f.UniqueSaveDir = true f.nano.SetSaveDirSuffix(f.storage.MainPath()) @@ -287,6 +290,13 @@ func (f *Frontend) Start() { } } +func (f *Frontend) LoadGame(path string) error { + if f.UniqueSaveDir { + f.copyFsMaybe(path) + } + return f.nano.LoadGame(path) +} + func (f *Frontend) AspectRatio() float32 { return f.nano.AspectRatio() } func (f *Frontend) AudioSampleRate() int { return f.nano.AudioSampleRate() } func (f *Frontend) FPS() int { return f.nano.VideoFramerate() } @@ -296,7 +306,6 @@ func (f *Frontend) HasSave() bool { return os.Exists(f.HashPath( func (f *Frontend) HashPath() string { return f.storage.GetSavePath() } func (f *Frontend) IsPortrait() bool { return f.nano.IsPortrait() } func (f *Frontend) KbMouseSupport() bool { return f.nano.KbMouseSupport() } -func (f *Frontend) LoadGame(path string) error { return f.nano.LoadGame(path) } func (f *Frontend) PixFormat() uint32 { return f.nano.Video.PixFmt.C } func (f *Frontend) RestoreGameState() error { return f.Load() } func (f *Frontend) Rotation() uint { return f.nano.Rot } @@ -349,6 +358,9 @@ func (f *Frontend) Close() { } } + f.UniqueSaveDir = false + f.SaveStateFs = "" + f.mui.Unlock() f.log.Debug().Msgf("frontend closed") } @@ -420,3 +432,32 @@ func (f *Frontend) autosave(periodSec int) { } } } + +func (f *Frontend) copyFsMaybe(path string) { + if f.SaveStateFs == "" { + return + } + + fileName := f.SaveStateFs + hasPlaceholder := strings.HasPrefix(f.SaveStateFs, "*") + if hasPlaceholder { + game := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) + fileName = strings.Replace(f.SaveStateFs, "*", game, 1) + } + + fullPath := filepath.Join(f.nano.SaveDir(), fileName) + + if os.Exists(fullPath) { + return + } + + storePath := filepath.Dir(path) + fsPath := filepath.Join(storePath, fileName) + if os.Exists(fsPath) { + if err := os.CopyFile(fsPath, fullPath); err != nil { + f.log.Error().Err(err).Msgf("fs copy fail") + } else { + f.log.Debug().Msgf("copied fs %v to %v", fsPath, fullPath) + } + } +} diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch.go b/pkg/worker/caged/libretro/nanoarch/nanoarch.go index ce0d4b15..4db0dcea 100644 --- a/pkg/worker/caged/libretro/nanoarch/nanoarch.go +++ b/pkg/worker/caged/libretro/nanoarch/nanoarch.go @@ -157,6 +157,7 @@ func (n *Nanoarch) WaitReady() { <-n.reserved } func (n *Nanoarch) Close() { n.Stopped.Store(true); n.reserved <- struct{}{} } func (n *Nanoarch) SetLogger(log *logger.Logger) { n.log = log } func (n *Nanoarch) SetVideoDebounce(t time.Duration) { n.limiter = NewLimit(t) } +func (n *Nanoarch) SaveDir() string { return C.GoString(n.cSaveDirectory) } func (n *Nanoarch) SetSaveDirSuffix(sx string) { dir := C.GoString(n.cSaveDirectory) + "/" + sx err := os.CheckCreateDir(dir)