From 11e4507a7bcfd84fb0388ceaafc5470f55d74f8e Mon Sep 17 00:00:00 2001 From: Jordan Davidson Date: Fri, 29 May 2026 22:26:16 -0600 Subject: [PATCH] Add Steam Deck EmuDeck ES-DE support --- .github/workflows/release.yml | 12 +- README.md | 4 +- app/setup.go | 28 +++ cfw/cfw.go | 7 +- cfw/directories.go | 23 ++ cfw/esde/data/platforms.json | 265 ++++++++++++++++++++++++ cfw/esde/data/save_directories.json | 200 ++++++++++++++++++ cfw/esde/esde.go | 55 +++++ cfw/metadata.go | 8 + cfw/platforms.go | 4 + cfw/saves.go | 3 + docs/_includes/cfw-links.md | 2 + docs/contributing/development.md | 3 +- docs/getting-started/index.md | 3 +- docs/getting-started/install-emudeck.md | 54 +++++ docs/index.md | 2 +- docs/platforms/esde.md | 87 ++++++++ docs/usage/guide.md | 1 + docs/usage/settings.md | 1 + mkdocs.yml | 2 + scripts/ESDE/Grout.sh | 43 ++++ scripts/ESDE/logo.png | Bin 0 -> 39378 bytes taskfiles/build.yml | 13 +- taskfiles/package.yml | 14 +- update/updater.go | 7 + 25 files changed, 824 insertions(+), 17 deletions(-) create mode 100644 cfw/esde/data/platforms.json create mode 100644 cfw/esde/data/save_directories.json create mode 100644 cfw/esde/esde.go create mode 100644 docs/getting-started/install-emudeck.md create mode 100644 docs/platforms/esde.md create mode 100644 scripts/ESDE/Grout.sh create mode 100644 scripts/ESDE/logo.png diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e72fc180..013825f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -140,12 +140,13 @@ jobs: env: GROUT_VERSION: ${{ needs.prepare.outputs.version }} - - name: Package Batocera AMD64 - run: task package:batocera-amd64 + - name: Package AMD64 platforms + run: task package:batocera-amd64 package:esde - - name: Create distribution + - name: Create distributions run: | - cd dist/Batocera-amd64 && zip -r ../Grout-Batocera-amd64.zip Grout.sh Grout + cd dist/Batocera-amd64 && zip -r ../Grout-Batocera-amd64.zip Grout.sh Grout && cd ../.. + cd dist/ESDE && zip -r ../Grout-ESDE.zip Grout.sh Grout && cd ../.. - name: Upload artifacts uses: actions/upload-artifact@v4 @@ -153,6 +154,7 @@ jobs: name: amd64-artifacts path: | dist/Grout-Batocera-amd64.zip + dist/Grout-ESDE.zip build-x86: needs: prepare @@ -306,6 +308,7 @@ jobs: **/Grout-Batocera-arm64.zip **/Grout-Batocera-x86.zip **/Grout-Batocera-amd64.zip + **/Grout-ESDE.zip **/grout draft: false prerelease: ${{ inputs.beta }} @@ -383,6 +386,7 @@ jobs: "Grout-Batocera-arm64.zip" "Grout-Batocera-amd64.zip" "Grout-Batocera-x86.zip" + "Grout-ESDE.zip" ) # Build the assets JSON object diff --git a/README.md b/README.md index 248c16c2..14cb2d82 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A RomM Client for your retro doo-dad. -[Allium][allium] · [Batocera][batocera] · [Knulli][knulli] · [Koriki][koriki] · [MinUI][minui] · [muOS][muos] · [NextUI][nextui] · [Onion][onion] · [ROCKNIX][rocknix] · [Spruce][spruce] · [SprigUI][sprigui] · [TwigUI][twigui] · [TrimUI][trimui] +[Allium][allium] · [Batocera][batocera] · [EmuDeck / ES-DE][emudeck] · [Knulli][knulli] · [Koriki][koriki] · [MinUI][minui] · [muOS][muos] · [NextUI][nextui] · [Onion][onion] · [ROCKNIX][rocknix] · [Spruce][spruce] · [SprigUI][sprigui] · [TwigUI][twigui] · [TrimUI][trimui]
@@ -88,6 +88,8 @@ If you enjoy using Grout and feel inclined to pay it forward, go do something ni [allium]: https://github.com/goweiwen/Allium [batocera]: https://batocera.org +[emudeck]: https://www.emudeck.com +[es-de]: https://es-de.org [knulli]: https://knulli.org [koriki]: https://github.com/Rparadise-Team/Koriki [minui]: https://github.com/shauninman/MinUI diff --git a/app/setup.go b/app/setup.go index a09d4154..18dbf891 100644 --- a/app/setup.go +++ b/app/setup.go @@ -21,12 +21,14 @@ import ( "log/slog" "os" "path/filepath" + "strings" "time" gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool" buttons "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/constants" "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/i18n" goi18n "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/veandco/go-sdl2/sdl" ) type SetupResult struct { @@ -58,6 +60,24 @@ func setup() SetupResult { } } +func isSteamDeckHardware() bool { + for _, path := range []string{ + "/sys/class/dmi/id/product_name", + "/sys/class/dmi/id/product_version", + "/sys/class/dmi/id/board_name", + } { + data, err := os.ReadFile(path) + if err != nil { + continue + } + value := strings.ToLower(strings.TrimSpace(string(data))) + if strings.Contains(value, "steam deck") || strings.Contains(value, "jupiter") || strings.Contains(value, "galileo") { + return true + } + } + return false +} + func setupInputMapping(currentCFW cfw.CFW) { cwd, err := os.Getwd() if err != nil { @@ -126,6 +146,14 @@ func initFramework(currentCFW cfw.CFW) { } gaba.Init(gabaOptions) + if currentCFW == cfw.ESDE || isSteamDeckHardware() { + // Steam Deck/Steam Input can emit continuous tiny analog-stick motion events, + // which flood SDL's event queue and make button input appear delayed or jittery. + // D-pad/buttons are sufficient for Grout navigation, so drop axis motion events. + sdl.EventState(sdl.CONTROLLERAXISMOTION, sdl.IGNORE) + sdl.EventState(sdl.JOYAXISMOTION, sdl.IGNORE) + } + gaba.RegisterChord("unlock-kid-mode", []buttons.VirtualButton{ buttons.VirtualButtonL1, buttons.VirtualButtonR1, diff --git a/cfw/cfw.go b/cfw/cfw.go index b45cde6e..320cc90a 100644 --- a/cfw/cfw.go +++ b/cfw/cfw.go @@ -19,6 +19,7 @@ const ( Onion CFW = "ONION" Koriki CFW = "KORIKI" Batocera CFW = "BATOCERA" + ESDE CFW = "ESDE" MinUI CFW = "MINUI" ) @@ -27,18 +28,18 @@ func GetCFW() CFW { cfw := CFW(cfwEnv) switch cfw { - case MuOS, NextUI, Knulli, Spruce, ROCKNIX, Trimui, Allium, Onion, Koriki, Batocera, MinUI: + case MuOS, NextUI, Knulli, Spruce, ROCKNIX, Trimui, Allium, Onion, Koriki, Batocera, ESDE, MinUI: return cfw default: log.SetOutput(os.Stderr) - log.Fatalf("Unsupported CFW: '%s'. Valid options: NextUI, muOS, Knulli, Spruce, ROCKNIX, Trimui, Allium, Onion, Koriki, Batocera, MinUI", cfwEnv) + log.Fatalf("Unsupported CFW: '%s'. Valid options: NextUI, muOS, Knulli, Spruce, ROCKNIX, Trimui, Allium, Onion, Koriki, Batocera, ESDE, MinUI", cfwEnv) return "" } } func (c CFW) IsBasedOnEmulationStation() bool { switch c { - case Knulli, ROCKNIX, Batocera: + case Knulli, ROCKNIX, Batocera, ESDE: return true default: return false diff --git a/cfw/directories.go b/cfw/directories.go index 5d648670..b78dad8c 100644 --- a/cfw/directories.go +++ b/cfw/directories.go @@ -3,6 +3,7 @@ package cfw import ( "grout/cfw/allium" "grout/cfw/batocera" + "grout/cfw/esde" "grout/cfw/knulli" "grout/cfw/koriki" "grout/cfw/minui" @@ -38,6 +39,8 @@ func GetRomDirectory() string { return koriki.GetRomDirectory() case Batocera: return batocera.GetRomDirectory() + case ESDE: + return esde.GetRomDirectory() case MinUI: return minui.GetRomDirectory() } @@ -79,6 +82,8 @@ func GetBIOSDirectory() string { return koriki.GetBIOSDirectory() case Batocera: return batocera.GetBIOSDirectory() + case ESDE: + return esde.GetBIOSDirectory() case MinUI: return minui.GetBIOSDirectory() } @@ -130,6 +135,8 @@ func GetArtDirectory(romDir string, platformFSSlug, platformName string) string return koriki.GetArtDirectory(romDir) case Batocera: return batocera.GetArtDirectory(romDir) + case ESDE: + return esde.GetArtDirectory(romDir) case MinUI: return minui.GetArtDirectory(romDir) default: @@ -178,6 +185,8 @@ func BaseSavePath() string { return koriki.GetBaseSavePath() case Batocera: return batocera.GetBaseSavePath() + case ESDE: + return esde.GetBaseSavePath() case MinUI: return minui.GetBaseSavePath() } @@ -192,6 +201,8 @@ func GetArtMarqueeDirectory(romDir string, platformFSSlug, platformName string) return knulli.GetArtDirectory(romDir) case Batocera: return batocera.GetArtDirectory(romDir) + case ESDE: + return esde.GetArtDirectory(romDir) default: return "" } @@ -205,6 +216,8 @@ func GetArtVideoDirectory(romDir string, platformFSSlug, platformName string) st return knulli.GetVideoDirectory(romDir) case Batocera: return batocera.GetVideoDirectory(romDir) + case ESDE: + return esde.GetVideoDirectory(romDir) default: return "" @@ -219,6 +232,8 @@ func GetArtThumbnailDirectory(romDir string, platformFSSlug, platformName string return knulli.GetArtDirectory(romDir) case Batocera: return knulli.GetArtDirectory(romDir) + case ESDE: + return esde.GetArtDirectory(romDir) default: return "" } @@ -232,6 +247,8 @@ func GetArtBezelDirectory(romDir string, platformFSSlug, platformName string) st return knulli.GetBezelDirectory(romDir) case Batocera: return batocera.GetBezelDirectory(romDir) + case ESDE: + return esde.GetBezelDirectory(romDir) default: return "" } @@ -245,6 +262,8 @@ func GetManualDirectory(romDir string, platformFSSlug, platformName string) stri return knulli.GetManualDirectory(romDir) case Batocera: return batocera.GetManualDirectory(romDir) + case ESDE: + return esde.GetManualDirectory(romDir) default: return "" } @@ -258,6 +277,8 @@ func GetBoxbackDirectory(romDir string, platformFSSlug, platformName string) str return knulli.GetArtDirectory(romDir) case Batocera: return batocera.GetArtDirectory(romDir) + case ESDE: + return esde.GetArtDirectory(romDir) default: return "" } @@ -271,6 +292,8 @@ func GetFanartDirectory(romDir string, platformFSSlug, platformName string) stri return knulli.GetArtDirectory(romDir) case Batocera: return batocera.GetArtDirectory(romDir) + case ESDE: + return esde.GetArtDirectory(romDir) default: return "" } diff --git a/cfw/esde/data/platforms.json b/cfw/esde/data/platforms.json new file mode 100644 index 00000000..b7031d62 --- /dev/null +++ b/cfw/esde/data/platforms.json @@ -0,0 +1,265 @@ +{ + "3do": [ + "3do" + ], + "3ds": [ + "n3ds", + "3ds" + ], + "acpc": [ + "amstradcpc" + ], + "amiga": [ + "amiga", + "amigacd32" + ], + "arcade": [ + "arcade", + "mame", + "fbneo", + "neogeo" + ], + "arduboy": [ + "arduboy" + ], + "atari-st": [ + "atarist" + ], + "atari2600": [ + "atari2600" + ], + "atari5200": [ + "atari5200" + ], + "atari7800": [ + "atari7800" + ], + "atari800": [ + "atari800" + ], + "c128": [ + "c128" + ], + "c16": [ + "c16" + ], + "c64": [ + "c64" + ], + "cave-story": [ + "cavestory" + ], + "colecovision": [ + "colecovision" + ], + "cpet": [ + "pet" + ], + "dc": [ + "dreamcast" + ], + "dos": [ + "dos" + ], + "fairchild-channel-f": [ + "channelf" + ], + "famicom": [ + "famicom", + "nes" + ], + "fds": [ + "fds" + ], + "g-and-w": [ + "gameandwatch" + ], + "galaksija": [], + "gamegear": [ + "gamegear" + ], + "gb": [ + "gb" + ], + "gba": [ + "gba" + ], + "gbc": [ + "gbc" + ], + "genesis": [ + "genesis", + "megadrive" + ], + "intellivision": [ + "intellivision" + ], + "j2me": [ + "j2me" + ], + "jaguar": [ + "atarijaguar", + "jaguar" + ], + "lynx": [ + "atarilynx", + "lynx" + ], + "mega-duck-slash-cougar-boy": [ + "megaduck" + ], + "msx": [ + "msx", + "msx1", + "msx2", + "msx2plus", + "msxturbor" + ], + "n64": [ + "n64" + ], + "nds": [ + "nds" + ], + "neo-geo-cd": [ + "neogeocd" + ], + "neo-geo-pocket": [ + "ngp" + ], + "neo-geo-pocket-color": [ + "ngpc" + ], + "neogeoaes": [ + "neogeo" + ], + "neogeomvs": [ + "neogeo" + ], + "nes": [ + "nes" + ], + "ngc": [ + "gc", + "gamecube" + ], + "odyssey": [ + "odyssey2", + "videopac" + ], + "openbor": [ + "openbor" + ], + "pc-8000": [ + "pc88" + ], + "pc-9800-series": [ + "pc98" + ], + "pc-fx": [ + "pcfx" + ], + "philips-cd-i": [ + "cdi" + ], + "pico": [ + "pico8" + ], + "pico-8": [ + "pico8" + ], + "pokemon-mini": [ + "pokemini" + ], + "ps2": [ + "ps2" + ], + "ps3": [ + "ps3" + ], + "psp": [ + "psp" + ], + "psx": [ + "psx", + "ps1" + ], + "saturn": [ + "saturn" + ], + "sega32": [ + "sega32x" + ], + "segacd": [ + "segacd" + ], + "sfam": [ + "snes" + ], + "sg1000": [ + "sg-1000", + "sg1000" + ], + "sharp-x68000": [ + "x68000" + ], + "sms": [ + "mastersystem" + ], + "snes": [ + "snes" + ], + "supergrafx": [ + "supergrafx" + ], + "supervision": [ + "supervision" + ], + "tg16": [ + "pcengine", + "tg16" + ], + "tic-80": [ + "tic80" + ], + "turbografx-cd": [ + "pcenginecd", + "tg-cd" + ], + "vectrex": [ + "vectrex" + ], + "vic-20": [ + "vic20", + "c20" + ], + "videopac": [ + "videopac" + ], + "virtualboy": [ + "virtualboy" + ], + "wii": [ + "wii" + ], + "wiiu": [ + "wiiu" + ], + "wonderswan": [ + "wonderswan", + "wswan" + ], + "wonderswan-color": [ + "wonderswancolor", + "wswanc" + ], + "x1": [ + "x1" + ], + "zx81": [ + "zx81" + ], + "zxs": [ + "zxspectrum" + ] +} diff --git a/cfw/esde/data/save_directories.json b/cfw/esde/data/save_directories.json new file mode 100644 index 00000000..b772cbe5 --- /dev/null +++ b/cfw/esde/data/save_directories.json @@ -0,0 +1,200 @@ +{ + "3do": [ + "retroarch/saves" + ], + "acpc": [ + "retroarch/saves" + ], + "arcade": [ + "retroarch/saves" + ], + "atari-st": [ + "retroarch/saves" + ], + "atari2600": [ + "retroarch/saves" + ], + "atari5200": [ + "retroarch/saves" + ], + "atari7800": [ + "retroarch/saves" + ], + "atari800": [ + "retroarch/saves" + ], + "c128": [ + "retroarch/saves" + ], + "c16": [ + "retroarch/saves" + ], + "c64": [ + "retroarch/saves" + ], + "colecovision": [ + "retroarch/saves" + ], + "cpet": [ + "retroarch/saves" + ], + "fairchild-channel-f": [ + "retroarch/saves" + ], + "famicom": [ + "retroarch/saves" + ], + "fds": [ + "retroarch/saves" + ], + "gamegear": [ + "retroarch/saves" + ], + "gb": [ + "retroarch/saves" + ], + "gba": [ + "retroarch/saves" + ], + "gbc": [ + "retroarch/saves" + ], + "genesis": [ + "retroarch/saves" + ], + "intellivision": [ + "retroarch/saves" + ], + "jaguar": [ + "retroarch/saves" + ], + "lynx": [ + "retroarch/saves" + ], + "msx": [ + "retroarch/saves" + ], + "n64": [ + "retroarch/saves" + ], + "nds": [ + "retroarch/saves" + ], + "neo-geo-cd": [ + "retroarch/saves" + ], + "neo-geo-pocket": [ + "retroarch/saves" + ], + "neo-geo-pocket-color": [ + "retroarch/saves" + ], + "neogeoaes": [ + "retroarch/saves" + ], + "neogeomvs": [ + "retroarch/saves" + ], + "nes": [ + "retroarch/saves" + ], + "odyssey": [ + "retroarch/saves" + ], + "pc-8000": [ + "retroarch/saves" + ], + "pc-9800-series": [ + "retroarch/saves" + ], + "pc-fx": [ + "retroarch/saves" + ], + "pico": [ + "retroarch/saves" + ], + "pico-8": [ + "retroarch/saves" + ], + "pokemon-mini": [ + "retroarch/saves" + ], + "psx": [ + "retroarch/saves" + ], + "saturn": [ + "retroarch/saves" + ], + "sega32": [ + "retroarch/saves" + ], + "segacd": [ + "retroarch/saves" + ], + "sfam": [ + "retroarch/saves" + ], + "sg1000": [ + "retroarch/saves" + ], + "sharp-x68000": [ + "retroarch/saves" + ], + "sms": [ + "retroarch/saves" + ], + "snes": [ + "retroarch/saves" + ], + "supergrafx": [ + "retroarch/saves" + ], + "supervision": [ + "retroarch/saves" + ], + "tg16": [ + "retroarch/saves" + ], + "tic-80": [ + "retroarch/saves" + ], + "turbografx-cd": [ + "retroarch/saves" + ], + "vectrex": [ + "retroarch/saves" + ], + "vic-20": [ + "retroarch/saves" + ], + "videopac": [ + "retroarch/saves" + ], + "virtualboy": [ + "retroarch/saves" + ], + "wonderswan": [ + "retroarch/saves" + ], + "wonderswan-color": [ + "retroarch/saves" + ], + "x1": [ + "retroarch/saves" + ], + "zx81": [ + "retroarch/saves" + ], + "zxs": [ + "retroarch/saves" + ], + "dc": [ + "flycast" + ], + "psp": [ + "ppsspp/PSP/SAVEDATA" + ], + "3ds": [ + "citra/sdmc" + ] +} diff --git a/cfw/esde/esde.go b/cfw/esde/esde.go new file mode 100644 index 00000000..93804f08 --- /dev/null +++ b/cfw/esde/esde.go @@ -0,0 +1,55 @@ +package esde + +import ( + "embed" + "grout/internal/jsonutil" + "os" + "path/filepath" +) + +//go:embed data/*.json +var embeddedFiles embed.FS + +var ( + Platforms = jsonutil.MustLoadJSONMap[string, []string](embeddedFiles, "data/platforms.json") + SaveDirectories = jsonutil.MustLoadJSONMap[string, []string](embeddedFiles, "data/save_directories.json") +) + +func GetBasePath() string { + if basePath := os.Getenv("BASE_PATH"); basePath != "" { + return basePath + } + return filepath.Join(os.Getenv("HOME"), "Emulation") +} + +func GetRomDirectory() string { + return filepath.Join(GetBasePath(), "roms") +} + +func GetBIOSDirectory() string { + return filepath.Join(GetBasePath(), "bios") +} + +func GetBaseSavePath() string { + return filepath.Join(GetBasePath(), "saves") +} + +func GetArtDirectory(romDir string) string { + return filepath.Join(romDir, "images") +} + +func GetGroutGamelist() string { + return filepath.Join(GetRomDirectory(), "ports", "gamelist.xml") +} + +func GetVideoDirectory(romDir string) string { + return filepath.Join(romDir, "videos") +} + +func GetBezelDirectory(romDir string) string { + return filepath.Join(romDir, "bezels") +} + +func GetManualDirectory(romDir string) string { + return filepath.Join(romDir, "manuals") +} diff --git a/cfw/metadata.go b/cfw/metadata.go index de344ca1..bfd4d1ee 100644 --- a/cfw/metadata.go +++ b/cfw/metadata.go @@ -2,6 +2,7 @@ package cfw import ( "grout/cfw/batocera" + "grout/cfw/esde" "grout/cfw/knulli" "grout/cfw/muos" "grout/cfw/rocknix" @@ -26,6 +27,9 @@ func AddGroutToGamelist(c CFW) { gamelist.AddGroutEntry(rocknix.GetGroutGamelist(), "./Grout.sh") case Batocera: gamelist.AddGroutEntry(batocera.GetGroutGamelist(), "./Grout/Grout.sh") + case ESDE: + gamelist.AddGroutEntry(esde.GetGroutGamelist(), "./Grout.sh") + return default: return } @@ -40,6 +44,10 @@ func FillGamesMetadata(entries []gamelist.RomGameEntry) { logger.Warn("Failed to add games to ES gamelist.xml", "error", err) } scheduleESRestart() + case ESDE: + if err := gamelist.AddRomGamesToGamelist(entries, gamelist.GameListFileName); err != nil { + logger.Warn("Failed to add games to ES-DE gamelist.xml", "error", err) + } case Spruce, Allium, Onion, Koriki: if err := gamelist.AddRomGamesToGamelist(entries, gamelist.MiyooGameListFileName); err != nil { logger.Warn("Failed to add games to miyoogamelist.xml", "error", err) diff --git a/cfw/platforms.go b/cfw/platforms.go index 82a5b41c..4d9476c2 100644 --- a/cfw/platforms.go +++ b/cfw/platforms.go @@ -3,6 +3,7 @@ package cfw import ( "grout/cfw/allium" "grout/cfw/batocera" + "grout/cfw/esde" "grout/cfw/knulli" "grout/cfw/koriki" "grout/cfw/minui" @@ -32,6 +33,7 @@ func buildPlatformAliasMap() map[string][]string { onion.Platforms, koriki.Platforms, batocera.Platforms, + esde.Platforms, minui.Platforms, } @@ -136,6 +138,8 @@ func GetPlatformMap(c CFW) map[string][]string { return koriki.Platforms case Batocera: return batocera.Platforms + case ESDE: + return esde.Platforms case MinUI: return minui.Platforms default: diff --git a/cfw/saves.go b/cfw/saves.go index 71ecce50..31bc0634 100644 --- a/cfw/saves.go +++ b/cfw/saves.go @@ -3,6 +3,7 @@ package cfw import ( "grout/cfw/allium" "grout/cfw/batocera" + "grout/cfw/esde" "grout/cfw/knulli" "grout/cfw/koriki" "grout/cfw/minui" @@ -38,6 +39,8 @@ func EmulatorFolderMap(c CFW) map[string][]string { return trimui.SaveDirectories case Batocera: return batocera.Platforms + case ESDE: + return esde.SaveDirectories case MinUI: return minui.SaveDirectories default: diff --git a/docs/_includes/cfw-links.md b/docs/_includes/cfw-links.md index 1fbdf27a..213dc74f 100644 --- a/docs/_includes/cfw-links.md +++ b/docs/_includes/cfw-links.md @@ -1,5 +1,7 @@ [allium]: https://github.com/goweiwen/Allium [batocera]: https://batocera.org +[emudeck]: https://www.emudeck.com +[es-de]: https://es-de.org [knulli]: https://knulli.org [koriki]: https://github.com/Rparadise-Team/Koriki [minui]: https://github.com/shauninman/MinUI diff --git a/docs/contributing/development.md b/docs/contributing/development.md index 91919ee2..b20ef3b4 100644 --- a/docs/contributing/development.md +++ b/docs/contributing/development.md @@ -33,7 +33,7 @@ brew install sdl2 sdl2_image sdl2_ttf sdl2_gfx - `WINDOW_WIDTH` (optional) - `WINDOW_HEIGHT` (optional) - `NITRATES` [true | false] (optional) This is used for Gabagool development debugging - - `CFW` [MUOS | KNULLI | SPRUCE | NEXTUI] (mandatory), this controls how Grout interacts with and places files + - `CFW` [MUOS | KNULLI | SPRUCE | NEXTUI | ESDE] (mandatory), this controls how Grout interacts with and places files - `BASE_PATH` (mandatory), this acts as the root path like you would have on a handheld (e.g. `/mmc/sdcard` on muOS). Have the subdirectory structure of this path match the CFW you are working on. 5. Run / Debug `app/grout.go`, making sure to reference the `.env` file in your run configuration. @@ -93,6 +93,7 @@ After building, you can package for individual platforms: | `task package-next` | NextUI (TrimUI) | `build/Grout.pak/` | | `task package-muos` | muOS | `build/muOS/Grout/`, `build/Grout.muxapp` | | `task package-knulli` | Knulli | `build/Knulli/Grout/` | +| `task package-esde` | EmuDeck / ES-DE | `dist/ESDE/` | | `task package-spruce` | Spruce | `build/Spruce/Grout/` | Each packaging task copies the binary, launch scripts from `scripts//`, shared libraries, and documentation diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 3751d966..02ba82d4 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -7,7 +7,7 @@ Get up and running with Grout in five steps. Make sure you have: - A RomM server running and accessible -- A compatible device running [Allium][allium], [Batocera][batocera], [Knulli][knulli], [muOS][muos], [NextUI][nextui], [Onion][onion], [ROCKNIX][rocknix], [Spruce v4][spruce], or [TrimUI][trimui] +- A compatible device running [Allium][allium], [Batocera][batocera], [EmuDeck / ES-DE][emudeck], [Knulli][knulli], [muOS][muos], [NextUI][nextui], [Onion][onion], [ROCKNIX][rocknix], [Spruce v4][spruce], or [TrimUI][trimui] - Your device connected to Wi-Fi --- @@ -20,6 +20,7 @@ Choose your platform: - [Allium Installation](install-allium.md) - [Batocera Installation](install-batocera.md) +- [EmuDeck / ES-DE Installation](install-emudeck.md) - [Knulli Installation](install-knulli.md) - [muOS Installation](install-muos.md) - [NextUI Installation](install-nextui.md) diff --git a/docs/getting-started/install-emudeck.md b/docs/getting-started/install-emudeck.md new file mode 100644 index 00000000..27b25df6 --- /dev/null +++ b/docs/getting-started/install-emudeck.md @@ -0,0 +1,54 @@ +# Installation Guide for Steam Deck / EmuDeck ES-DE + +This guide will help you install Grout as an ES-DE port on a Steam Deck using [EmuDeck][emudeck]. + +## Tested Devices + +Grout has been tested on the following devices running EmuDeck with ES-DE: + +| Manufacturer | Device | +|--------------|------------| +| _None yet_ | _Please report your results!_ | + +_Please help verify compatibility by reporting your results!_ + +## Installation Steps + +1. Ensure EmuDeck and ES-DE are installed on your Steam Deck. +2. Download the [latest Grout release](https://github.com/rommapp/grout/releases/latest/download/Grout-ESDE.zip) for + EmuDeck / ES-DE. +3. Unzip the downloaded archive. +4. Copy the `Grout` folder and `Grout.sh` file to your EmuDeck Ports directory: + - Internal storage: `~/Emulation/roms/ports/` + - SD card: `/run/media//Emulation/roms/ports/` +5. Restart ES-DE or refresh your game list. +6. Launch Grout from the `Ports` system and enjoy! + +## Important Configuration + +!!! important + Grout expects the standard EmuDeck folder layout where `roms`, `bios`, and `saves` are inside the same + `Emulation` directory. The launcher derives this path from the `roms/ports` folder that contains `Grout.sh`. + +!!! note + Save sync support on EmuDeck is best-effort because save locations vary by emulator. ROM downloads, BIOS downloads, + artwork downloads, and ES-DE `gamelist.xml` updates use the standard EmuDeck directories. + +## Update + +### In-App update (Recommended) + +Grout has a built-in update mechanism. To update Grout, launch the application and navigate to the `Settings` menu. From there, +select `Check for Updates`. If a new version is available, follow the on-screen prompts to download and install the update. + +### Manual update + +To update Grout manually, download the latest release and replace the existing `Grout` folder and `Grout.sh` file in your +EmuDeck Ports directory. If you have made any custom configurations, ensure to back them up before replacing the folder. +Be sure to keep the `config.json` file if you do not want to authenticate again, and configure platform folder mappings again. + +## Next Steps + +After installation is complete, check out the [User Guide](../usage/guide.md) to learn how to use Grout. + +--8<-- "docs/_includes/cfw-links.md" diff --git a/docs/index.md b/docs/index.md index 4be37f8f..a5529c67 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ hide: Download and manage games from your [RomM](https://romm.app) instance directly on your Linux based retro handheld. -[Allium](getting-started/install-allium.md) · [Batocera](getting-started/install-batocera.md) · [Knulli](getting-started/install-knulli.md) · [MinUI](getting-started/install-minui.md) · [muOS](getting-started/install-muos.md) · [NextUI](getting-started/install-nextui.md) · [Onion](getting-started/install-onion.md) · [ROCKNIX](getting-started/install-rocknix.md) · [Spruce](getting-started/install-spruce.md) · [TrimUI](getting-started/install-trimui.md) +[Allium](getting-started/install-allium.md) · [Batocera](getting-started/install-batocera.md) · [EmuDeck / ES-DE](getting-started/install-emudeck.md) · [Knulli](getting-started/install-knulli.md) · [MinUI](getting-started/install-minui.md) · [muOS](getting-started/install-muos.md) · [NextUI](getting-started/install-nextui.md) · [Onion](getting-started/install-onion.md) · [ROCKNIX](getting-started/install-rocknix.md) · [Spruce](getting-started/install-spruce.md) · [TrimUI](getting-started/install-trimui.md) [:fontawesome-solid-gamepad: Get Started](getting-started/index.md){ .md-button .md-button--primary }    diff --git a/docs/platforms/esde.md b/docs/platforms/esde.md new file mode 100644 index 00000000..0b205e71 --- /dev/null +++ b/docs/platforms/esde.md @@ -0,0 +1,87 @@ +# EmuDeck / ES-DE Platform Mappings + +This table shows the mappings of RomM Fs Slug to EmuDeck's ES-DE platform folders. + +| Platform Name | RomM Fs Slug | Folder(s) | +|---|---|---| +| 3DO Interactive Multiplayer | 3do | 3do | +| Amiga | amiga | amiga, amigacd32 | +| Amstrad CPC | acpc | amstradcpc | +| Arcade | arcade | arcade, mame, fbneo, neogeo | +| Arduboy | arduboy | arduboy | +| Atari 2600 | atari2600 | atari2600 | +| Atari 5200 | atari5200 | atari5200 | +| Atari 7800 | atari7800 | atari7800 | +| Atari 800 | atari800 | atari800 | +| Atari Jaguar | jaguar | atarijaguar, jaguar | +| Atari Lynx | lynx | atarilynx, lynx | +| Atari ST/STE | atari-st | atarist | +| Cave Story | cave-story | cavestory | +| ColecoVision | colecovision | colecovision | +| Commodore 128 | c128 | c128 | +| Commodore 16 | c16 | c16 | +| Commodore C64/128/MAX | c64 | c64 | +| Commodore PET | cpet | pet | +| Commodore VIC-20 | vic-20 | vic20, c20 | +| DOS | dos | dos | +| Dreamcast | dc | dreamcast | +| Fairchild Channel F | fairchild-channel-f | channelf | +| Family Computer | famicom | famicom, nes | +| Family Computer Disk System | fds | fds | +| Galaksija | galaksija | *(none)* | +| Game & Watch | g-and-w | gameandwatch | +| Game Boy | gb | gb | +| Game Boy Advance | gba | gba | +| Game Boy Color | gbc | gbc | +| Intellivision | intellivision | intellivision | +| J2ME | j2me | j2me | +| MSX | msx | msx, msx1, msx2, msx2plus, msxturbor | +| Mega Duck/Cougar Boy | mega-duck-slash-cougar-boy | megaduck | +| Neo Geo AES | neogeoaes | neogeo | +| Neo Geo CD | neo-geo-cd | neogeocd | +| Neo Geo MVS | neogeomvs | neogeo | +| Neo Geo Pocket | neo-geo-pocket | ngp | +| Neo Geo Pocket Color | neo-geo-pocket-color | ngpc | +| Nintendo 3DS | 3ds | n3ds, 3ds | +| Nintendo 64 | n64 | n64 | +| Nintendo DS | nds | nds | +| Nintendo Entertainment System | nes | nes | +| Nintendo GameCube | ngc | gc, gamecube | +| Nintendo Wii | wii | wii | +| Nintendo Wii U | wiiu | wiiu | +| Odyssey | odyssey | odyssey2, videopac | +| OpenBOR | openbor | openbor | +| PC Engine SuperGrafx | supergrafx | supergrafx | +| PC-8000 | pc-8000 | pc88 | +| PC-9800 Series | pc-9800-series | pc98 | +| PC-FX | pc-fx | pcfx | +| PICO-8 | pico | pico8 | +| PICO-8 | pico-8 | pico8 | +| Philips CD-i | philips-cd-i | cdi | +| PlayStation | psx | psx, ps1 | +| PlayStation 2 | ps2 | ps2 | +| PlayStation 3 | ps3 | ps3 | +| PlayStation Portable | psp | psp | +| Pokemon Mini | pokemon-mini | pokemini | +| SG-1000 | sg1000 | sg-1000, sg1000 | +| Sega 32X | sega32 | sega32x | +| Sega CD | segacd | segacd | +| Sega Game Gear | gamegear | gamegear | +| Sega Genesis | genesis | genesis, megadrive | +| Sega Master System | sms | mastersystem | +| Sega Saturn | saturn | saturn | +| Sharp X1 | x1 | x1 | +| Sharp X68000 | sharp-x68000 | x68000 | +| Super Famicom | sfam | snes | +| Super Nintendo | snes | snes | +| Supervision | supervision | supervision | +| TIC-80 | tic-80 | tic80 | +| TurboGrafx-16 | tg16 | pcengine, tg16 | +| TurboGrafx-CD | turbografx-cd | pcenginecd, tg-cd | +| Vectrex | vectrex | vectrex | +| Videopac | videopac | videopac | +| Virtual Boy | virtualboy | virtualboy | +| WonderSwan | wonderswan | wonderswan, wswan | +| WonderSwan Color | wonderswan-color | wonderswancolor, wswanc | +| ZX Spectrum | zxs | zxspectrum | +| ZX81 | zx81 | zx81 | diff --git a/docs/usage/guide.md b/docs/usage/guide.md index e099d3d2..c18354f8 100644 --- a/docs/usage/guide.md +++ b/docs/usage/guide.md @@ -177,6 +177,7 @@ Grout uses platform mappings to determine where to save downloaded games on your different folder naming conventions. Use these references to see the exact folder names used by your CFW: - [KNULLI](../platforms/knulli.md) - ES-DE style folder names (e.g., `gb`, `snes`, `psx`) +- [EmuDeck / ES-DE](../platforms/esde.md) - ES-DE style folder names (e.g., `gb`, `snes`, `psx`) - [muOS](../platforms/muos.md) - Mixed short codes and descriptive names (e.g., `gb`, `Nintendo Game Boy`) - [NextUI](../platforms/nextui.md) - Descriptive names with tags (e.g., `Game Boy (GB)`) - [ROCKNIX](../platforms/rocknix.md) - ES-DE style folder names (e.g., `gb`, `snes`, `psx`) diff --git a/docs/usage/settings.md b/docs/usage/settings.md index a3d2d188..f2959224 100644 --- a/docs/usage/settings.md +++ b/docs/usage/settings.md @@ -127,6 +127,7 @@ For detailed documentation on platform mapping, see the [User Guide](guide.md#pl Each CFW uses different folder naming conventions: - [KNULLI](../platforms/knulli.md) - ES-DE style folder names (e.g., `gb`, `snes`, `psx`) +- [EmuDeck / ES-DE](../platforms/esde.md) - ES-DE style folder names (e.g., `gb`, `snes`, `psx`) - [muOS](../platforms/muos.md) - Mixed short codes and descriptive names (e.g., `gb`, `Nintendo Game Boy`) - [NextUI](../platforms/nextui.md) - Descriptive names with tags (e.g., `Game Boy (GB)`) - [ROCKNIX](../platforms/rocknix.md) - ES-DE style folder names (e.g., `gb`, `snes`, `psx`) diff --git a/mkdocs.yml b/mkdocs.yml index 8377774f..e963c067 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -68,6 +68,7 @@ nav: - getting-started/index.md - Allium: getting-started/install-allium.md - Batocera: getting-started/install-batocera.md + - EmuDeck / ES-DE: getting-started/install-emudeck.md - Knulli: getting-started/install-knulli.md - MinUI: getting-started/install-minui.md - muOS: getting-started/install-muos.md @@ -84,6 +85,7 @@ nav: - CFW Specific Info: - Allium: platforms/allium.md - Batocera: platforms/BATOCERA.md + - EmuDeck / ES-DE: platforms/esde.md - Knulli: platforms/knulli.md - Koriki: platforms/koriki.md - MinUI: platforms/minui.md diff --git a/scripts/ESDE/Grout.sh b/scripts/ESDE/Grout.sh new file mode 100644 index 00000000..ba340e6d --- /dev/null +++ b/scripts/ESDE/Grout.sh @@ -0,0 +1,43 @@ +#!/bin/bash +CUR_DIR="$(cd "$(dirname "$0")" && pwd)" +APP_DIR="$CUR_DIR/Grout" +LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp}/grout-esde.lock" + +if ! mkdir "$LOCK_DIR" 2>/dev/null; then + if [ -f "$LOCK_DIR/pid" ] && kill -0 "$(cat "$LOCK_DIR/pid")" 2>/dev/null; then + exit 0 + fi + rm -rf "$LOCK_DIR" + mkdir "$LOCK_DIR" 2>/dev/null || exit 0 +fi +printf '%s\n' "$$" > "$LOCK_DIR/pid" +trap 'rm -rf "$LOCK_DIR"' EXIT + +# Apply pending update +if [ -d "$CUR_DIR/.update" ]; then + cp -rf "$CUR_DIR/.update/"* "$CUR_DIR/" + rm -rf "$CUR_DIR/.update" +fi + +cd "$APP_DIR" || exit 1 + +# EmuDeck's ES-DE ports folder is normally /roms/ports. +export CFW=ESDE +export BASE_PATH="$(cd "$CUR_DIR/../.." && pwd)" +export LD_LIBRARY_PATH="$APP_DIR/lib:$LD_LIBRARY_PATH" + +# Steam Deck uses Xbox-style face buttons, so use direct A=A/B=B mappings. +export FLIP_FACE_BUTTONS=1 + +# Prefer SDL's game controller API and ignore duplicate keyboard/raw joystick events. +export DISABLE_KEYBOARD_INPUT=1 +export DISABLE_JOYSTICK_INPUT=1 + +# Grout uses its own gamepad-driven keyboard. On Steam Deck, SDL/Steam can +# also surface the Steam keyboard, which can interfere with Grout's keyboard. +export SDL_ENABLE_SCREEN_KEYBOARD=0 +export SDL_IME_SHOW_UI=0 + +./grout + +exit 0 diff --git a/scripts/ESDE/logo.png b/scripts/ESDE/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bb49d84bc27cbdb168a73596be3201bec6f1e34b GIT binary patch literal 39378 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/dev/null 2>&1 - docker image prune --filter "label={{.LABEL}}" -f >/dev/null 2>&1 || true - echo "Extract {{.BUILD_DIR}} complete" @@ -35,7 +36,7 @@ tasks: - task: _build vars: { PLATFORM: linux/arm64, BUILD_DIR: build64, IMAGE_TAG: "{{.IMAGE_NAME}}-arm64", DOCKERFILE: docker/Dockerfile, LOCAL: "{{.LOCAL}}" } - task: _extract - vars: { BUILD_DIR: build64, IMAGE_TAG: "{{.IMAGE_NAME}}-arm64", LIB_PATH: /usr/lib/aarch64-linux-gnu/libSDL2_gfx-1.0.so.0.0.2 } + vars: { BUILD_DIR: build64, IMAGE_TAG: "{{.IMAGE_NAME}}-arm64", LIB_PATH: /usr/lib/aarch64-linux-gnu/libSDL2_gfx-1.0.so.0.0.2, EXTRA_LIBS: "" } amd64: desc: Build and extract for AMD64 (x86_64) @@ -43,7 +44,11 @@ tasks: - task: _build vars: { PLATFORM: linux/amd64, BUILD_DIR: build, IMAGE_TAG: "{{.IMAGE_NAME}}-amd64", DOCKERFILE: docker/Dockerfile } - task: _extract - vars: { BUILD_DIR: build, IMAGE_TAG: "{{.IMAGE_NAME}}-amd64", LIB_PATH: /usr/lib/x86_64-linux-gnu/libSDL2_gfx-1.0.so.0.0.2 } + vars: + BUILD_DIR: build + IMAGE_TAG: "{{.IMAGE_NAME}}-amd64" + LIB_PATH: /usr/lib/x86_64-linux-gnu/libSDL2_gfx-1.0.so.0.0.2 + EXTRA_LIBS: "/usr/lib/x86_64-linux-gnu/libSDL2-2.0.so.0.14.0:libSDL2-2.0.so.0 /usr/lib/x86_64-linux-gnu/libSDL2_image-2.0.so.0.2.3:libSDL2_image-2.0.so.0 /usr/lib/x86_64-linux-gnu/libSDL2_ttf-2.0.so.0.14.1:libSDL2_ttf-2.0.so.0 /usr/lib/x86_64-linux-gnu/libjpeg.so.62.3.0:libjpeg.so.62 /usr/lib/x86_64-linux-gnu/libpng16.so.16.37.0:libpng16.so.16 /usr/lib/x86_64-linux-gnu/libtiff.so.5.6.0:libtiff.so.5 /usr/lib/x86_64-linux-gnu/libwebp.so.6.0.2:libwebp.so.6 /usr/lib/x86_64-linux-gnu/libfreetype.so.6.17.4:libfreetype.so.6 /usr/lib/x86_64-linux-gnu/libbrotlidec.so.1.0.9:libbrotlidec.so.1 /usr/lib/x86_64-linux-gnu/libbrotlicommon.so.1.0.9:libbrotlicommon.so.1 /usr/lib/x86_64-linux-gnu/libzstd.so.1.4.8:libzstd.so.1 /lib/x86_64-linux-gnu/liblzma.so.5.2.5:liblzma.so.5 /usr/lib/x86_64-linux-gnu/libjbig.so.0:libjbig.so.0 /usr/lib/x86_64-linux-gnu/libdeflate.so.0:libdeflate.so.0 /lib/x86_64-linux-gnu/libz.so.1.2.11:libz.so.1" x86: desc: Build and extract for x86 (32-bit) @@ -51,7 +56,7 @@ tasks: - task: _build vars: { PLATFORM: linux/386, BUILD_DIR: buildx86, IMAGE_TAG: "{{.IMAGE_NAME}}-x86", DOCKERFILE: docker/Dockerfile } - task: _extract - vars: { BUILD_DIR: buildx86, IMAGE_TAG: "{{.IMAGE_NAME}}-x86", LIB_PATH: /usr/lib/i386-linux-gnu/libSDL2_gfx-1.0.so.0.0.2 } + vars: { BUILD_DIR: buildx86, IMAGE_TAG: "{{.IMAGE_NAME}}-x86", LIB_PATH: /usr/lib/i386-linux-gnu/libSDL2_gfx-1.0.so.0.0.2, EXTRA_LIBS: "" } arm32: desc: Build and extract for ARM32 @@ -62,4 +67,4 @@ tasks: vars: { PLATFORM: linux/arm/v7, BUILD_DIR: build32, IMAGE_TAG: "{{.IMAGE_NAME}}-arm32", DOCKERFILE: docker/32.Dockerfile, LOCAL: "{{.LOCAL}}" } # TODO: add LIB_PATH once custom SDL build for Miyoo is ready - task: _extract - vars: { BUILD_DIR: build32, IMAGE_TAG: "{{.IMAGE_NAME}}-arm32", LIB_PATH: "" } + vars: { BUILD_DIR: build32, IMAGE_TAG: "{{.IMAGE_NAME}}-arm32", LIB_PATH: "", EXTRA_LIBS: "" } diff --git a/taskfiles/package.yml b/taskfiles/package.yml index 9007c443..e5cba408 100644 --- a/taskfiles/package.yml +++ b/taskfiles/package.yml @@ -4,9 +4,9 @@ tasks: all: desc: Package for all platforms - deps: [ next, muos, knulli, spruce, rocknix, trimui, allium, onion, koriki, minui, batocera, batocera-x86, batocera-amd64 ] + deps: [ next, muos, knulli, spruce, rocknix, trimui, allium, onion, koriki, minui, batocera, batocera-x86, batocera-amd64, esde ] cmds: - - echo "Packaging complete (14 platforms)" + - echo "Packaging complete (15 platforms)" silent: true next: @@ -147,3 +147,13 @@ tasks: - cp -R build/lib/* dist/Batocera-amd64/Grout/lib/ - chmod a+x dist/Batocera-amd64/Grout/grout dist/Batocera-amd64/Grout.sh silent: true + + esde: + cmds: + - rm -rf dist/ESDE + - mkdir -p dist/ESDE/Grout/lib + - cp scripts/ESDE/Grout.sh dist/ESDE/ + - cp build/grout scripts/ESDE/logo.png README.md LICENSE dist/ESDE/Grout/ + - cp -R build/lib/* dist/ESDE/Grout/lib/ + - chmod a+x dist/ESDE/Grout/grout dist/ESDE/Grout.sh + silent: true diff --git a/update/updater.go b/update/updater.go index dd9b0f1a..f62755d5 100644 --- a/update/updater.go +++ b/update/updater.go @@ -64,6 +64,11 @@ func GetDistributionAssetName(c cfw.CFW) string { default: return "" } + case cfw.ESDE: + if runtime.GOARCH == "amd64" { + return "Grout-ESDE.zip" + } + return "" default: return "" } @@ -257,6 +262,8 @@ func getLaunchScriptPath(c cfw.CFW) string { return "Grout.pak/launch.sh" case cfw.Batocera: return "Grout.sh" + case cfw.ESDE: + return "Grout.sh" default: return "" }