diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e72fc18..3f4eba6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,7 @@ jobs: GROUT_VERSION: ${{ needs.prepare.outputs.version }} - name: Package ARM64 platforms - run: task package:next package:muos package:knulli package:rocknix package:trimui package:batocera + run: task package:next package:muos package:knulli package:rocknix package:amberelec package:trimui package:batocera - name: Create distributions run: | @@ -101,6 +101,7 @@ jobs: cd dist/muOS && zip -r Grout.muxapp Grout && mv Grout.muxapp ../Grout.muxapp && cd ../.. cd dist/Knulli && zip -r ../Grout-Knulli.zip Grout && cd ../.. cd dist/ROCKNIX && zip -r ../Grout-ROCKNIX.zip Grout.sh Grout && cd ../.. + cd dist/AmberELEC && zip -r ../Grout-AmberELEC.zip Grout.sh Grout && cd ../.. cd dist/Trimui && zip -r ../Grout-Trimui.zip Grout && cd ../.. cd dist/Batocera-arm64 && zip -r ../Grout-Batocera-arm64.zip Grout.sh Grout && cd ../.. @@ -113,6 +114,7 @@ jobs: dist/Grout.muxapp dist/Grout-Knulli.zip dist/Grout-ROCKNIX.zip + dist/Grout-AmberELEC.zip dist/Grout-Trimui.zip dist/Grout-Batocera-arm64.zip build64/grout @@ -301,6 +303,7 @@ jobs: **/Grout-Koriki.zip **/Grout-Knulli.zip **/Grout-ROCKNIX.zip + **/Grout-AmberELEC.zip **/Grout-Trimui.zip **/Grout-MinUI.zip **/Grout-Batocera-arm64.zip @@ -375,6 +378,7 @@ jobs: "Grout-Knulli.zip" "Grout.spruce.zip" "Grout-ROCKNIX.zip" + "Grout-AmberELEC.zip" "Grout-Trimui.zip" "Grout-Allium.zip" "Grout-Onion.zip" diff --git a/app/setup.go b/app/setup.go index a09d415..03ef98e 100644 --- a/app/setup.go +++ b/app/setup.go @@ -4,6 +4,7 @@ import ( "errors" "grout/cache" "grout/cfw" + "grout/cfw/amberelec" "grout/cfw/allium" "grout/cfw/koriki" "grout/cfw/minui" @@ -77,6 +78,8 @@ func setupInputMapping(currentCFW cfw.CFW) { var mappingBytes []byte var mappingErr error switch currentCFW { + case cfw.AmberELEC: + mappingBytes, mappingErr = amberelec.GetInputMappingBytes() case cfw.MuOS: mappingBytes, mappingErr = muos.GetInputMappingBytes() case cfw.Allium: diff --git a/cfw/amberelec/amberelec.go b/cfw/amberelec/amberelec.go new file mode 100644 index 0000000..4295b57 --- /dev/null +++ b/cfw/amberelec/amberelec.go @@ -0,0 +1,108 @@ +package amberelec + +import ( + "embed" + "fmt" + "os" + "path/filepath" + "strings" + + "grout/internal/jsonutil" +) + +//go:embed data/*.json input_mappings/*.json +var embeddedFiles embed.FS + +type Device string + +const ( + DeviceGeneric Device = "generic" + DeviceRG351P Device = "rg351p" + DeviceRG351V Device = "rg351v" +) + +// Platforms is sourced from dedicated AmberELEC data instead of inheriting ROCKNIX folders at runtime. +// AmberELEC wiki systems that are still absent from RomM fs_slugs remain intentionally out of +// platforms.json for now: atomiswave, laserdisc, naomi, advision, gamepocketcomputer, gamate, +// gamemaster, gamecom, gameking, gameking3, pspminis, pv1000, satellaview, scv, sufami, +// tvboy, uzebox, vsmile, chip-8, lowresnx, piece, vircon32, wasm4, build, doom, easyrpg, +// ecwolf, scummvm, solarus, zmachine, ep64-128, sc-3000, thomson, tvc. +var Platforms = jsonutil.MustLoadJSONMap[string, []string](embeddedFiles, "data/platforms.json") + +func DetectDevice() Device { + arch, err := os.ReadFile("/storage/.config/.OS_ARCH") + if err != nil { + return DeviceGeneric + } + + switch strings.ToUpper(strings.TrimSpace(string(arch))) { + case "RG351P": + return DeviceRG351P + case "RG351V": + return DeviceRG351V + default: + return DeviceGeneric + } +} + +func GetInputMappingBytes() ([]byte, error) { + var filename string + switch DetectDevice() { + case DeviceRG351P: + filename = "input_mappings/rg351p.json" + case DeviceRG351V: + filename = "input_mappings/rg351v.json" + default: + return nil, nil + } + + overridePath := filepath.Join("overrides", "cfw", "amberelec", filename) + data, err := os.ReadFile(overridePath) + if err != nil { + data, err = embeddedFiles.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read embedded input mapping %s: %w", filename, err) + } + } + + return data, nil +} + +func GetBasePath() string { + if basePath := os.Getenv("BASE_PATH"); basePath != "" { + return basePath + } + return "/storage" +} + +func GetRomDirectory() string { + return filepath.Join(GetBasePath(), "roms") +} + +func GetBIOSDirectory() string { + return filepath.Join(GetRomDirectory(), "bios") +} + +func GetBaseSavePath() string { + return GetRomDirectory() +} + +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 GetManualDirectory(romDir string) string { + return filepath.Join(romDir, "manuals") +} + +func GetBezelDirectory(romDir string) string { + return filepath.Join(romDir, "bezels") +} diff --git a/cfw/amberelec/data/platforms.json b/cfw/amberelec/data/platforms.json new file mode 100644 index 0000000..0a0456c --- /dev/null +++ b/cfw/amberelec/data/platforms.json @@ -0,0 +1,294 @@ +{ + "3do": [ + "3do" + ], + "3ds": [ + "3ds" + ], + "acpc": [ + "amstradcpc" + ], + "amiga": [ + "amiga", + "amigacd32" + ], + "arcade": [ + "arcade", + "mame", + "fbneo" + ], + "arduboy": [ + "arduboy" + ], + "atari-st": [ + "atarist" + ], + "atari2600": [ + "atari2600" + ], + "atari5200": [ + "atari5200" + ], + "atari7800": [ + "atari7800" + ], + "atari800": [ + "atari800" + ], + "c128": [ + "c128" + ], + "c16": [ + "c16" + ], + "c64": [ + "c64" + ], + "cave-story": [], + "colecovision": [ + "coleco" + ], + "cpet": [ + "pet" + ], + "dc": [ + "dreamcast" + ], + "dos": [ + "pc" + ], + "fairchild-channel-f": [ + "channelf" + ], + "famicom": [ + "famicom" + ], + "fds": [ + "fds" + ], + "g-and-w": [ + "gameandwatch" + ], + "galaksija": [], + "gamegear": [ + "gamegear", + "gamegearh" + ], + "gamegearh": [ + "gamegearh" + ], + "gb": [ + "gb", + "gbh" + ], + "gba": [ + "gba", + "gbah" + ], + "gbah": [ + "gbah" + ], + "gbc": [ + "gbc", + "gbch" + ], + "gbch": [ + "gbch" + ], + "gbh": [ + "gbh" + ], + "genesis": [ + "genesis", + "genh", + "megadrive", + "megadrive-japan" + ], + "genh": [ + "genh" + ], + "intellivision": [ + "intellivision" + ], + "j2me": [ + "j2me" + ], + "jaguar": [ + "atarijaguar" + ], + "lynx": [ + "atarilynx" + ], + "mega-duck-slash-cougar-boy": [ + "megaduck" + ], + "megadrive-japan": [ + "megadrive-japan" + ], + "msx": [ + "msx", + "msx2" + ], + "n64": [ + "n64" + ], + "nds": [ + "nds" + ], + "neo-geo-cd": [ + "neocd" + ], + "neo-geo-pocket": [ + "ngp" + ], + "neo-geo-pocket-color": [ + "ngpc" + ], + "neogeoaes": [ + "neogeo" + ], + "neogeomvs": [ + "neogeo" + ], + "nes": [ + "nes", + "nesh" + ], + "nesh": [ + "nesh" + ], + "ngc": [ + "gamecube" + ], + "odyssey": [ + "odyssey" + ], + "openbor": [ + "openbor" + ], + "pc-8000": [ + "pc88" + ], + "pc-9800-series": [ + "pc98" + ], + "pc-fx": [ + "pcfx" + ], + "philips-cd-i": [ + "cdi" + ], + "pico": [ + "pico-8" + ], + "pico-8": [ + "pico-8" + ], + "pokemon-mini": [ + "pokemini" + ], + "ps2": [ + "ps2" + ], + "ps3": [ + "ps3" + ], + "psp": [ + "psp" + ], + "psx": [ + "psx" + ], + "saturn": [ + "saturn" + ], + "sega32": [ + "sega32x" + ], + "segacd": [ + "segacd", + "megacd" + ], + "sfam": [ + "sfc" + ], + "sfc": [ + "sfc" + ], + "sg1000": [ + "sg-1000" + ], + "sharp-x68000": [ + "x68000" + ], + "sms": [ + "mastersystem" + ], + "snes": [ + "snes", + "snesh" + ], + "snesh": [ + "snesh" + ], + "snesmsu1": [ + "snesmsu1" + ], + "supergrafx": [ + "sgfx" + ], + "supervision": [ + "supervision" + ], + "tg16": [ + "tg16", + "pcengine" + ], + "tic-80": [ + "tic-80" + ], + "turbografx-cd": [ + "tg16cd", + "pcenginecd" + ], + "vectrex": [ + "vectrex" + ], + "vic-20": [ + "vic20" + ], + "vic20": [ + "vic20" + ], + "videopac": [ + "videopac" + ], + "virtualboy": [ + "virtualboy" + ], + "wii": [ + "wii", + "wiiware" + ], + "wiiu": [ + "wiiu" + ], + "windows": [ + "windows" + ], + "wonderswan": [ + "wonderswan" + ], + "wonderswan-color": [ + "wonderswancolor" + ], + "x1": [ + "x1" + ], + "zx81": [ + "zx81" + ], + "zxs": [ + "zxspectrum" + ] +} diff --git a/cfw/amberelec/input_mappings/rg351p.json b/cfw/amberelec/input_mappings/rg351p.json new file mode 100644 index 0000000..a686c4e --- /dev/null +++ b/cfw/amberelec/input_mappings/rg351p.json @@ -0,0 +1,33 @@ +{ + "keyboard_map": {}, + "controller_button_map": { + "0": 5, + "1": 6, + "2": 7, + "3": 8, + "4": 14, + "6": 13, + "8": 15, + "9": 9, + "10": 11, + "11": 1, + "12": 3, + "13": 2, + "14": 4 + }, + "controller_hat_map": {}, + "joystick_axis_map": { + "4": { + "positive_button": 12, + "negative_button": 0, + "threshold": 16000 + }, + "5": { + "positive_button": 10, + "negative_button": 0, + "threshold": 16000 + } + }, + "joystick_button_map": {}, + "joystick_hat_map": {} +} \ No newline at end of file diff --git a/cfw/amberelec/input_mappings/rg351v.json b/cfw/amberelec/input_mappings/rg351v.json new file mode 100644 index 0000000..d7b525a --- /dev/null +++ b/cfw/amberelec/input_mappings/rg351v.json @@ -0,0 +1,37 @@ +{ + "keyboard_map": { + "59": 15 + }, + "controller_button_map": { + "0": 5, + "1": 6, + "2": 7, + "3": 8, + "4": 14, + "5": 15, + "6": 13, + "9": 9, + "10": 11, + "11": 1, + "12": 2, + "13": 3, + "14": 4 + }, + "controller_hat_map": {}, + "joystick_axis_map": { + "4": { + "positive_button": 10, + "negative_button": 0, + "threshold": 16000 + }, + "5": { + "positive_button": 12, + "negative_button": 0, + "threshold": 16000 + } + }, + "joystick_button_map": { + "4": 9 + }, + "joystick_hat_map": {} +} \ No newline at end of file diff --git a/cfw/cfw.go b/cfw/cfw.go index b45cde6..460859f 100644 --- a/cfw/cfw.go +++ b/cfw/cfw.go @@ -9,6 +9,7 @@ import ( type CFW string const ( + AmberELEC CFW = "AMBERELEC" NextUI CFW = "NEXTUI" MuOS CFW = "MUOS" Knulli CFW = "KNULLI" @@ -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 AmberELEC, MuOS, NextUI, Knulli, Spruce, ROCKNIX, Trimui, Allium, Onion, Koriki, Batocera, 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: AmberELEC, NextUI, muOS, Knulli, Spruce, ROCKNIX, Trimui, Allium, Onion, Koriki, Batocera, MinUI", cfwEnv) return "" } } func (c CFW) IsBasedOnEmulationStation() bool { switch c { - case Knulli, ROCKNIX, Batocera: + case AmberELEC, Knulli, ROCKNIX, Batocera: return true default: return false diff --git a/cfw/directories.go b/cfw/directories.go index 5d64867..917b949 100644 --- a/cfw/directories.go +++ b/cfw/directories.go @@ -1,6 +1,7 @@ package cfw import ( + "grout/cfw/amberelec" "grout/cfw/allium" "grout/cfw/batocera" "grout/cfw/knulli" @@ -18,6 +19,8 @@ import ( // GetRomDirectory returns the ROM directory for the current CFW. func GetRomDirectory() string { switch GetCFW() { + case AmberELEC: + return amberelec.GetRomDirectory() case MuOS: return muos.GetRomDirectory() case NextUI: @@ -59,6 +62,8 @@ func RomFolderBase(path string, tagParser func(string) string) string { // GetBIOSDirectory returns the BIOS directory for the current CFW. func GetBIOSDirectory() string { switch GetCFW() { + case AmberELEC: + return amberelec.GetBIOSDirectory() case MuOS: return muos.GetBIOSDirectory() case NextUI: @@ -110,6 +115,8 @@ func GetPlatformRomDirectory(relativePath, platformFSSlug string) string { // GetArtDirectory returns the artwork directory for a platform. func GetArtDirectory(romDir string, platformFSSlug, platformName string) string { switch GetCFW() { + case AmberELEC: + return amberelec.GetArtDirectory(romDir) case NextUI: return nextui.GetArtDirectory(romDir) case Knulli: @@ -158,6 +165,8 @@ func GetArtSplashDirectory(romDir string, platformFSSlug, platformName string) s // BaseSavePath returns the base save path for the current CFW. func BaseSavePath() string { switch GetCFW() { + case AmberELEC: + return amberelec.GetBaseSavePath() case MuOS: return muos.GetBaseSavePath() case NextUI: @@ -186,6 +195,8 @@ func BaseSavePath() string { func GetArtMarqueeDirectory(romDir string, platformFSSlug, platformName string) string { switch GetCFW() { + case AmberELEC: + return amberelec.GetArtDirectory(romDir) case ROCKNIX: return rocknix.GetArtDirectory(romDir) case Knulli: @@ -199,6 +210,8 @@ func GetArtMarqueeDirectory(romDir string, platformFSSlug, platformName string) func GetArtVideoDirectory(romDir string, platformFSSlug, platformName string) string { switch GetCFW() { + case AmberELEC: + return amberelec.GetVideoDirectory(romDir) case ROCKNIX: return rocknix.GetVideoDirectory(romDir) case Knulli: @@ -213,6 +226,8 @@ func GetArtVideoDirectory(romDir string, platformFSSlug, platformName string) st func GetArtThumbnailDirectory(romDir string, platformFSSlug, platformName string) string { switch GetCFW() { + case AmberELEC: + return amberelec.GetArtDirectory(romDir) case ROCKNIX: return rocknix.GetArtDirectory(romDir) case Knulli: @@ -226,6 +241,8 @@ func GetArtThumbnailDirectory(romDir string, platformFSSlug, platformName string func GetArtBezelDirectory(romDir string, platformFSSlug, platformName string) string { switch GetCFW() { + case AmberELEC: + return amberelec.GetBezelDirectory(romDir) case ROCKNIX: return rocknix.GetBezelDirectory(romDir) case Knulli: @@ -239,6 +256,8 @@ func GetArtBezelDirectory(romDir string, platformFSSlug, platformName string) st func GetManualDirectory(romDir string, platformFSSlug, platformName string) string { switch GetCFW() { + case AmberELEC: + return amberelec.GetManualDirectory(romDir) case ROCKNIX: return rocknix.GetManualDirectory(romDir) case Knulli: @@ -252,6 +271,8 @@ func GetManualDirectory(romDir string, platformFSSlug, platformName string) stri func GetBoxbackDirectory(romDir string, platformFSSlug, platformName string) string { switch GetCFW() { + case AmberELEC: + return amberelec.GetArtDirectory(romDir) case ROCKNIX: return rocknix.GetArtDirectory(romDir) case Knulli: @@ -265,6 +286,8 @@ func GetBoxbackDirectory(romDir string, platformFSSlug, platformName string) str func GetFanartDirectory(romDir string, platformFSSlug, platformName string) string { switch GetCFW() { + case AmberELEC: + return amberelec.GetArtDirectory(romDir) case ROCKNIX: return rocknix.GetArtDirectory(romDir) case Knulli: diff --git a/cfw/metadata.go b/cfw/metadata.go index de344ca..e39e718 100644 --- a/cfw/metadata.go +++ b/cfw/metadata.go @@ -1,6 +1,7 @@ package cfw import ( + "grout/cfw/amberelec" "grout/cfw/batocera" "grout/cfw/knulli" "grout/cfw/muos" @@ -20,6 +21,8 @@ func scheduleESRestart() { func AddGroutToGamelist(c CFW) { switch c { + case AmberELEC: + gamelist.AddGroutEntry(amberelec.GetGroutGamelist(), "./Grout.sh") case Knulli: gamelist.AddGroutEntry(knulli.GetGroutGamelist(), "./Grout/Grout.sh") case ROCKNIX: @@ -35,7 +38,7 @@ func AddGroutToGamelist(c CFW) { func FillGamesMetadata(entries []gamelist.RomGameEntry) { logger := gaba.GetLogger() switch GetCFW() { - case Knulli, ROCKNIX, Batocera: + case AmberELEC, Knulli, ROCKNIX, Batocera: if err := gamelist.AddRomGamesToGamelist(entries, gamelist.GameListFileName); err != nil { logger.Warn("Failed to add games to ES gamelist.xml", "error", err) } diff --git a/cfw/platforms.go b/cfw/platforms.go index 82a5b41..5060ff8 100644 --- a/cfw/platforms.go +++ b/cfw/platforms.go @@ -1,6 +1,7 @@ package cfw import ( + "grout/cfw/amberelec" "grout/cfw/allium" "grout/cfw/batocera" "grout/cfw/knulli" @@ -22,6 +23,7 @@ var platformAliasMap = buildPlatformAliasMap() func buildPlatformAliasMap() map[string][]string { // Combine all platform maps to find aliases across all CFWs allMaps := []map[string][]string{ + amberelec.Platforms, knulli.Platforms, muos.Platforms, nextui.Platforms, @@ -116,6 +118,8 @@ func GetPlatformAliases(fsSlug string) []string { // GetPlatformMap returns the platform mapping for the given CFW. func GetPlatformMap(c CFW) map[string][]string { switch c { + case AmberELEC: + return amberelec.Platforms case MuOS: return muos.Platforms case NextUI: diff --git a/cfw/saves.go b/cfw/saves.go index 71ecce5..124b40f 100644 --- a/cfw/saves.go +++ b/cfw/saves.go @@ -1,6 +1,7 @@ package cfw import ( + "grout/cfw/amberelec" "grout/cfw/allium" "grout/cfw/batocera" "grout/cfw/knulli" @@ -18,6 +19,8 @@ import ( // EmulatorFolderMap returns the emulator/save directory mapping for the given CFW. func EmulatorFolderMap(c CFW) map[string][]string { switch c { + case AmberELEC: + return amberelec.Platforms case MuOS: return muos.SaveDirectories case NextUI: diff --git a/docs/_includes/cfw-links.md b/docs/_includes/cfw-links.md index 1fbdf27..ea9361c 100644 --- a/docs/_includes/cfw-links.md +++ b/docs/_includes/cfw-links.md @@ -1,4 +1,5 @@ [allium]: https://github.com/goweiwen/Allium +[amberelec]: https://amberelec.org [batocera]: https://batocera.org [knulli]: https://knulli.org [koriki]: https://github.com/Rparadise-Team/Koriki diff --git a/docs/contributing/development.md b/docs/contributing/development.md index 91919ee..dd73ae0 100644 --- a/docs/contributing/development.md +++ b/docs/contributing/development.md @@ -26,14 +26,14 @@ brew install sdl2 sdl2_image sdl2_ttf sdl2_gfx ## Getting Started 1. Clone the [Grout](https://github.com/rommapp/grout) repository. -2. Run `task hooks-setup` to install git hooks. +2. Run `task code:hooks-setup` to install git hooks. 3. Make a copy of `.env.dev` and save it as `.env` in the root of the cloned repository. 4. Fill out the `.env` file. Here are descriptions of the various values you can set. - `ENVIRONMENT=DEV` (mandatory), this will disable some Gabagool features behind the scenes - `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` (mandatory), set this to one of the supported values such as `AMBERELEC`, `MUOS`, `KNULLI`, `SPRUCE`, `NEXTUI`, `ROCKNIX`, `TRIMUI`, `ALLIUM`, `ONION`, `KORIKI`, `BATOCERA`, or `MINUI` - `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. @@ -71,29 +71,32 @@ and use Docker for cross-compilation. task all # Or build with a local gabagool workspace (for gabagool development) -task all-local +task all LOCAL=true ``` ### Build Process The build happens in two stages: -1. **Docker Build** (`task build-arm64`) - Cross-compiles the Go binary for ARM64 Linux inside a Docker container. This +1. **Docker Build** (`task build:arm64`) - Cross-compiles the Go binary for ARM64 Linux inside a Docker container. This ensures consistent builds regardless of your host OS and handles SDL2 dependencies. -2. **Extract** (`task extract-arm64`) - Copies the compiled binary and required shared libraries (like `libSDL2_gfx`) from - the Docker container to the local `build64/` directory. +2. **Extract** - `task build:arm64` also extracts the compiled binary and required shared libraries (like + `libSDL2_gfx`) from the Docker container to the local `build64/` directory. ### Platform-Specific Packaging After building, you can package for individual platforms: -| Task | Platform | Output Location | -|-----------------------|-----------------|-------------------------------------------| -| `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-spruce` | Spruce | `build/Spruce/Grout/` | +| Task | Platform | Output Location | +|--------------------------|-----------------|-----------------------------------------| +| `task package:next` | NextUI (TrimUI) | `dist/Grout.pak/` | +| `task package:muos` | muOS | `dist/muOS/Grout/`, `dist/Grout.muxapp` | +| `task package:knulli` | Knulli | `dist/Knulli/Grout/` | +| `task package:spruce` | Spruce | `dist/Spruce/Grout/` | +| `task package:rocknix` | ROCKNIX | `dist/ROCKNIX/Grout/` | +| `task package:amberelec` | AmberELEC | `dist/AmberELEC/Grout/` | +| `task package:trimui` | TrimUI | `dist/Trimui/Grout/` | Each packaging task copies the binary, launch scripts from `scripts//`, shared libraries, and documentation into the appropriate directory structure for that CFW. @@ -104,43 +107,49 @@ For rapid testing, you can deploy directly to a connected device, assuming that ```shell # NextUI (TrimUI devices) -task adb-next +task deploy:next # muOS (SD card 1 or 2) -task adb-muos-sd1 -task adb-muos-sd2 +task deploy:muos-sd1 +task deploy:muos-sd2 # Knulli -task adb-knulli +task deploy:knulli ``` These tasks will remove any existing installation and push the freshly built package to the device. ### Local Gabagool Development -When developing gabagool alongside Grout, use the `-local` variants: +When developing gabagool alongside Grout, pass `LOCAL=true`: ```shell -task build-arm64-local # Build using local gabagool via go.work -task all-local # Build and package all platforms with local gabagool +task build:arm64 LOCAL=true # Build using local gabagool via go.work +task all LOCAL=true # Build and package all platforms with local gabagool ``` This requires a `go.work` file in the parent directory that references both projects. ### Output Structure -After running `task all`, the `build/` directory will contain: +After running `task all`, the build output is split between the architecture-specific build directories and the final +packages in `dist/`: ``` -build/ +build64/ ├── grout # ARM64 Linux binary -├── lib/ # Shared libraries -│ └── libSDL2_gfx-1.0.so.0 +└── lib/ + └── libSDL2_gfx-1.0.so.0 + +dist/ ├── Grout.pak/ # NextUI package ├── Grout.muxapp # muOS archive (ready to install) ├── muOS/Grout/ # muOS package (unpacked) ├── Knulli/Grout/ # Knulli package -└── Spruce/Grout/ # Spruce package +├── Spruce/Grout/ # Spruce package +├── ROCKNIX/Grout/ # ROCKNIX package +├── AmberELEC/Grout/ # AmberELEC package +└── Trimui/Grout/ # TrimUI package ``` ## Helper Tools @@ -171,7 +180,7 @@ The `i18n` task will: ```shell # Run all linters (fmt, vet, staticcheck) -task lint +task code:lint ``` This runs `go fmt`, `go vet`, and `staticcheck` across the codebase. @@ -183,10 +192,10 @@ Requires [staticcheck](https://staticcheck.dev/) to be installed ( ```shell # Convert MP4 video to animated WebP (interactive, prompts for paths) -task mp4-to-webp +task media:mp4-to-webp # Resize all user guide screenshots to 1024px width -task resize-user-guide-images +task media:resize-user-guide-images ``` The `mp4-to-webp` task is useful for creating animated preview images for documentation. diff --git a/docs/contributing/index.md b/docs/contributing/index.md index 2f39e18..1d70cbb 100644 --- a/docs/contributing/index.md +++ b/docs/contributing/index.md @@ -64,7 +64,7 @@ Help keep CFW platform directory mappings current: - Follow existing code conventions and patterns - Test your changes locally before submitting - Update documentation if your changes affect user-facing behavior -- Ensure the build passes (`task build-arm64` or `go build ./...`) +- Ensure the build passes (`task build:arm64` or `go build ./...`) - Provide a clear description of what your PR does and why ## Code Style @@ -79,7 +79,7 @@ We use standard Go formatting and conventions: Please [create an issue](https://github.com/rommapp/grout/issues/new/choose) with: -- Your CFW and version (muOS, Knulli, Spruce, NextUI) +- Your CFW and version (AmberELEC, muOS, Knulli, Spruce, NextUI, ROCKNIX, TrimUI, Allium, Onion, Koriki, Batocera, MinUI) - Grout version - Steps to reproduce the issue - Expected vs actual behavior diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 3751d96..f2713d9 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], [AmberELEC][amberelec], [Batocera][batocera], [Knulli][knulli], [muOS][muos], [NextUI][nextui], [Onion][onion], [ROCKNIX][rocknix], [Spruce v4][spruce], or [TrimUI][trimui] - Your device connected to Wi-Fi --- @@ -19,6 +19,7 @@ Make sure you have: Choose your platform: - [Allium Installation](install-allium.md) +- [AmberELEC Installation](install-amberelec.md) - [Batocera Installation](install-batocera.md) - [Knulli Installation](install-knulli.md) - [muOS Installation](install-muos.md) diff --git a/docs/getting-started/install-amberelec.md b/docs/getting-started/install-amberelec.md new file mode 100644 index 0000000..478e39d --- /dev/null +++ b/docs/getting-started/install-amberelec.md @@ -0,0 +1,44 @@ +# Installation Guide for AmberELEC + +This guide will help you install Grout on devices running [AmberELEC][amberelec]. + +## Tested Devices + +AmberELEC support is still being verified across devices. + +_Please help verify compatibility on your device by reporting your results!_ + +## Installation Steps + +1. Ensure your device is running AmberELEC. +2. Download the [latest Grout release](https://github.com/rommapp/grout/releases/latest/download/Grout-AmberELEC.zip) for AmberELEC. +3. Unzip the downloaded archive. +4. Create the directory `/storage/roms/ports/Grout/`. +5. Copy the extracted data, that includes `Grout.sh` into `/storage/roms/ports/Grout/`, so you end up with `/storage/roms/ports/Grout/Grout.sh` as AmberELEC's EmulationStation will launch the port directly when the folder name matches the executable inside it. +6. Refresh your ports list from the frontend if Grout does not appear immediately. +7. Launch Grout from the `Ports` menu and enjoy. + +## Important Configuration + +!!! important + If artwork does not appear in the frontend, enable `Search For Local Art` in the frontend's developer options. + +## 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, download the latest release and replace the existing `Grout.sh` launcher and `Grout` folder inside +`/storage/roms/ports/Grout.sh/`. +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" \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 4be37f8..75cd5db 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) · [AmberELEC](getting-started/install-amberelec.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) [:fontawesome-solid-gamepad: Get Started](getting-started/index.md){ .md-button .md-button--primary }    diff --git a/docs/usage/guide.md b/docs/usage/guide.md index e099d3d..7a9a57c 100644 --- a/docs/usage/guide.md +++ b/docs/usage/guide.md @@ -176,6 +176,7 @@ You can change these mappings later from [Settings](settings.md#directory-mappin Grout uses platform mappings to determine where to save downloaded games on your device. Each Custom Firmware (CFW) uses different folder naming conventions. Use these references to see the exact folder names used by your CFW: +- AmberELEC - ES-DE style folder names with AmberELEC-specific handheld variants (e.g., `gb`, `gbh`, `snesh`) - [KNULLI](../platforms/knulli.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)`) diff --git a/docs/usage/settings.md b/docs/usage/settings.md index a3d2d18..d2d8547 100644 --- a/docs/usage/settings.md +++ b/docs/usage/settings.md @@ -126,6 +126,7 @@ For detailed documentation on platform mapping, see the [User Guide](guide.md#pl Each CFW uses different folder naming conventions: +- AmberELEC - ES-DE style folder names with AmberELEC-specific handheld variants (e.g., `gb`, `gbh`, `snesh`) - [KNULLI](../platforms/knulli.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)`) diff --git a/internal/gamelist/add.go b/internal/gamelist/add.go index e842ad4..9a9e36b 100644 --- a/internal/gamelist/add.go +++ b/internal/gamelist/add.go @@ -6,6 +6,7 @@ import ( "grout/internal/stringutil" "grout/romm" "os" + "path/filepath" "strconv" "strings" "time" @@ -37,8 +38,43 @@ type RomGameEntry struct { Platform *romm.Platform } +func relativeGameListPath(baseDir, value string) string { + if value == "" { + return "" + } + + if !filepath.IsAbs(value) { + relative := filepath.ToSlash(value) + if strings.HasPrefix(relative, "./") || strings.HasPrefix(relative, "../") { + return relative + } + return "./" + relative + } + + relative, err := filepath.Rel(baseDir, value) + if err != nil { + return filepath.ToSlash(value) + } + + relative = filepath.ToSlash(relative) + if relative == "." { + return "./" + } + if relative == ".." || strings.HasPrefix(relative, "../") { + return filepath.ToSlash(value) + } + if strings.HasPrefix(relative, "./") { + return relative + } + + return "./" + relative +} + func (gl *GameList) AddRomGame(entry RomGameEntry) { gameMetadata := make(map[string]string) + toGameListPath := func(value string) string { + return relativeGameListPath(entry.RomDirectory, value) + } gameMetadata[NameElement] = stringutil.PrepareRomName(entry.Game.Name, entry.Game.Regions) gameMetadata[DescElement] = entry.Game.Summary gameMetadata[MD5Element] = entry.Game.Md5Hash @@ -53,39 +89,39 @@ func (gl *GameList) AddRomGame(entry RomGameEntry) { } if entry.ArtLocation.ImagePath != "" { - gameMetadata[ImageElement] = entry.ArtLocation.ImagePath + gameMetadata[ImageElement] = toGameListPath(entry.ArtLocation.ImagePath) } if entry.ArtLocation.ThumbnailPath != "" { - gameMetadata[ThumbnailElement] = entry.ArtLocation.ThumbnailPath + gameMetadata[ThumbnailElement] = toGameListPath(entry.ArtLocation.ThumbnailPath) } if entry.ArtLocation.MarqueePath != "" { - gameMetadata[MarqueeElement] = entry.ArtLocation.MarqueePath + gameMetadata[MarqueeElement] = toGameListPath(entry.ArtLocation.MarqueePath) } if entry.ArtLocation.VideoPath != "" { - gameMetadata[VideoElement] = entry.ArtLocation.VideoPath + gameMetadata[VideoElement] = toGameListPath(entry.ArtLocation.VideoPath) } if entry.ArtLocation.BezelPath != "" { - gameMetadata[BezelElement] = entry.ArtLocation.BezelPath + gameMetadata[BezelElement] = toGameListPath(entry.ArtLocation.BezelPath) } if entry.ArtLocation.ManualPath != "" { - gameMetadata[ManualElement] = entry.ArtLocation.ManualPath + gameMetadata[ManualElement] = toGameListPath(entry.ArtLocation.ManualPath) } if entry.ArtLocation.BoxBackPath != "" { - gameMetadata[BoxbackElement] = entry.ArtLocation.BoxBackPath + gameMetadata[BoxbackElement] = toGameListPath(entry.ArtLocation.BoxBackPath) } if entry.ArtLocation.FanartPath != "" { - gameMetadata[FanartElement] = entry.ArtLocation.FanartPath + gameMetadata[FanartElement] = toGameListPath(entry.ArtLocation.FanartPath) } if entry.GamePath != "" { - gameMetadata[PathElement] = entry.GamePath + gameMetadata[PathElement] = toGameListPath(entry.GamePath) } maxPlayers := entry.Game.MaxPlayerCount() diff --git a/mkdocs.yml b/mkdocs.yml index 8377774..3f162a3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -67,6 +67,7 @@ nav: - Quick Start: - getting-started/index.md - Allium: getting-started/install-allium.md + - AmberELEC: getting-started/install-amberelec.md - Batocera: getting-started/install-batocera.md - Knulli: getting-started/install-knulli.md - MinUI: getting-started/install-minui.md diff --git a/scripts/AmberELEC/Grout.sh b/scripts/AmberELEC/Grout.sh new file mode 100644 index 0000000..85d7dc0 --- /dev/null +++ b/scripts/AmberELEC/Grout.sh @@ -0,0 +1,22 @@ +#!/bin/bash +CUR_DIR="$(dirname "$0")" +FLAG_FILE="./es_restart_request" +cd "$CUR_DIR/Grout" || exit 1 + +# Apply pending update +if [ -d "../.update" ]; then + cp -rf ../.update/* .. + rm -rf ../.update +fi + +export CFW=AMBERELEC +export LD_LIBRARY_PATH="$CUR_DIR/Grout/lib:$LD_LIBRARY_PATH" + +./grout + +if [ -f "$FLAG_FILE" ]; then + rm -f "$FLAG_FILE" + killall emulationstation +fi + +exit 0 \ No newline at end of file diff --git a/taskfile.yml b/taskfile.yml index bcd16ba..d7e46ad 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -56,6 +56,7 @@ tasks: - task: package:knulli - task: package:spruce - task: package:rocknix + - task: package:amberelec - task: package:trimui silent: true diff --git a/taskfiles/package.yml b/taskfiles/package.yml index 9007c44..b53e639 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, amberelec, trimui, allium, onion, koriki, minui, batocera, batocera-x86, batocera-amd64 ] cmds: - - echo "Packaging complete (14 platforms)" + - echo "Packaging complete (15 platforms)" silent: true next: @@ -95,6 +95,16 @@ tasks: - chmod a+x dist/ROCKNIX/Grout/grout dist/ROCKNIX/Grout.sh silent: true + amberelec: + cmds: + - rm -rf dist/AmberELEC + - mkdir -p dist/AmberELEC/Grout/lib + - cp scripts/AmberELEC/Grout.sh dist/AmberELEC/ + - cp build64/grout scripts/ROCKNIX/logo.png README.md LICENSE dist/AmberELEC/Grout/ + - cp -R build64/lib/* dist/AmberELEC/Grout/lib/ + - chmod a+x dist/AmberELEC/Grout/grout dist/AmberELEC/Grout.sh + silent: true + trimui: cmds: - rm -rf dist/Trimui diff --git a/ui/general_settings.go b/ui/general_settings.go index a684f58..3e0f1a3 100644 --- a/ui/general_settings.go +++ b/ui/general_settings.go @@ -66,7 +66,7 @@ func (s *GeneralSettingsScreen) Draw(input GeneralSettingsInput) (GeneralSetting func (s *GeneralSettingsScreen) buildMenuItems(config *internal.Config) []gaba.ItemWithOptions { c := cfw.GetCFW() isMuOS := c == cfw.MuOS - isESBasedOS := c == cfw.Knulli || c == cfw.ROCKNIX + isESBasedOS := c == cfw.AmberELEC || c == cfw.Knulli || c == cfw.ROCKNIX showArtKind := atomic.Bool{} showArtKind.Store(config.DownloadArt) displayDownloadArtPreview := atomic.Bool{} diff --git a/update/updater.go b/update/updater.go index dd9b0f1..ea01b8c 100644 --- a/update/updater.go +++ b/update/updater.go @@ -41,6 +41,8 @@ func GetDistributionAssetName(c cfw.CFW) string { return "Grout-Knulli.zip" case cfw.Spruce: return "Grout.spruce.zip" + case cfw.AmberELEC: + return "Grout-AmberELEC.zip" case cfw.ROCKNIX: return "Grout-ROCKNIX.zip" case cfw.Trimui: @@ -243,6 +245,8 @@ func getLaunchScriptPath(c cfw.CFW) string { return "Grout/Grout.sh" case cfw.Spruce: return "Grout/launch.sh" + case cfw.AmberELEC: + return "Grout.sh" case cfw.ROCKNIX: return "Grout.sh" case cfw.Trimui: