#!/bin/bash # https://github.com/mrchrisster/MiSTer_SAM/ # Copyright (c) 2023 by mrchrisster and Mellified # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # Description # This cycles through arcade and console cores periodically # Games are randomly pulled from their respective folders # ======== Credits ======== # Original concept and implementation: mrchrisster # Script layout & watchdog functionality: Mellified # tty2oled submodule: Paradox # Indexing tool: wizzomafizzo # # Thanks for the contributions and support: # pocomane, kaloun34, redsteakraw, RetroDriven, woelper, LamerDeluxe, InquisitiveCoder, syntax_x, Sigismond, theypsilon # tty2oled improvements by venice # TODO implement playcurrentgame for amiga # SAM is immune to the signal sent when detaching from tmux trap '' SIGHUP # ======== INI VARIABLES ======== # Change these in the INI file function init_vars() { declare -g mrsampath="/media/fat/Scripts/.MiSTer_SAM" declare -g misterpath="/media/fat" declare -g mrsamtmp="/tmp/.SAM_tmp" # Save our PID and process declare -g sampid="${$}" declare -g samprocess samprocess="$(basename -- "${0}")" declare -g menuonly="Yes" declare -g key_activity_file="/tmp/.SAM_tmp/SAM_Keyboard_Activity" declare -g joy_activity_file="/tmp/.SAM_tmp/SAM_Joy_Activity" declare -g mouse_activity_file="/tmp/.SAM_tmp/SAM_Mouse_Activity" declare -g sam_menu_file="/tmp/.SAMmenu" declare -g brfake="/tmp/.SAM_tmp/brfake" declare -g samini_file="/media/fat/Scripts/MiSTer_SAM.ini" declare -g samini_update_file="${mrsampath}/MiSTer_SAM.default.ini" declare -gi inmenu=0 declare -gi MENU_LOADED=0 declare -gi sam_bgmmenu=0 declare -gi shown=0 declare -gi coreretries=3 declare -gi romloadfails=0 declare -g gamelistpath="${mrsampath}/SAM_Gamelists" declare -g gamelistpathtmp="/tmp/.SAM_List" declare -g tmpfile="/tmp/.SAM_List/tmpfile" declare -g tmpfile2="/tmp/.SAM_List/tmpfile2" declare -g tmpfilefilter="/tmp/.SAM_List/tmpfilefilter" declare -g corelistfile="/tmp/.SAM_List/corelist" declare -g core_count_file="/tmp/.SAM_tmp/sv_corecount" declare -gi disablecoredel="0" declare -gi gametimer=120 declare -gl corelist="amiga,amigacd32,ao486,arcade,atari2600,atari5200,atari7800,atarilynx,c64,cdi,coco2,colecovision,intellivision,fds,gb,gbc,gba,genesis,gg,jaguar,megacd,n64,neogeo,neogeocd,nes,s32x,saturn,sgb,sms,snes,stv,tgfx16,tgfx16cd,vectrex,wonderswan,wonderswancolor,psx,x68k,mgls" declare -gl corelistall="${corelist}" declare -gl skipmessage="Yes" declare -gl disablebootrom="no" declare -gl skiptime="10" declare -gl norepeat="Yes" declare -gl disable_blacklist="No" declare -gl amigaselect="All" declare -gl m82="no" declare -gl sam_goat_list="no" declare -gl mute="No" declare -gi update_done=0 declare -gl ignore_when_skip="no" declare -gl coreweight="No" declare -gi gamelists_created=0 declare -gl playcurrentgame="No" declare -gl kids_safe="No" declare -gl rating="No" declare -gl dupe_mode="normal" declare -gl listenmouse="Yes" declare -gl listenkeyboard="Yes" declare -gl listenjoy="Yes" declare -gl mgls_dirs="" declare -g repository_url="https://github.com/mrchrisster/MiSTer_SAM" declare -g branch="main" declare -g raw_base="https://raw.githubusercontent.com/mrchrisster/MiSTer_SAM/${branch}" declare -gi counter=0 declare -gA corewc declare -gA corep declare -g userstartup="/media/fat/linux/user-startup.sh" declare -g userstartuptpl="/media/fat/linux/_user-startup.sh" declare -gl useneogeotitles="Yes" declare -gl arcadeorient declare -gl checkzipsondisk="No" declare -gl force_zip_scan="No" declare -gl check_for_new_games="Yes" declare -gl update_gamelists_during_play="No" declare -gi bootsleep="60" declare -gi totalgamecount # ======== DEBUG VARIABLES ======== declare -gl samdebug="No" declare -gl samdebuglog="No" # ======== BGM ======= declare -gl bgm="No" declare -gl bgmplay="Yes" declare -gl bgmstop="Yes" declare -gi gvoladjust="0" # ======== TTY2OLED ======= declare -g TTY_cmd_pipe="${mrsamtmp}/TTY_cmd_pipe" declare -gl ttyenable="No" declare -gi ttyupdate_pause=10 declare -g tty_currentinfo_file=${mrsamtmp}/tty_currentinfo declare -g tty_sleepfile="/tmp/tty2oled_sleep" declare -gl ttyname_cleanup="no" declare -gA tty_currentinfo=( [core_pretty]="" [name]="" [core]="" [date]=0 [counter]=0 [name_scroll]="" [name_scroll_position]=0 [name_scroll_direction]=1 [update_pause]=${ttyupdate_pause} ) # ======== SAMVIDEO ======= declare -gA SV_TVC_CL declare -gl samvideo declare -gl samvideo_freq declare -gl samvideo_output="hdmi" declare -gl samvideo_source declare -gl samvideo_tvc_cdi declare -gl samvideo_tvc declare -gl download_manager="yes" declare -gl sv_aspectfix_vmode declare -gl sv_inimod="yes" declare -gl sv_inibackup="yes" declare -gl keep_local_copy="yes" declare -g sv_inibackup_file="/media/fat/MiSTer.ini.sam_backup" declare -g samvideo_crtmode="video_mode=640,16,64,80,240,1,3,14,12380" declare -g samvideo_displaywait="2" declare -g tmpvideo="/tmp/SAMvideo.mp4" declare -g ini_file="/media/fat/MiSTer.ini" declare -g ini_contents=$(cat "$ini_file") declare -g sv_ini_temp_file="/tmp/MiSTer.ini.samvideo" declare -g sv_core="/tmp/.SAM_tmp/sv_core" declare -g sv_gametimer_file="/tmp/.SAM_tmp/sv_gametimer" declare -g sv_loadcounter=0 declare -g samvideo_path="/media/fat/video" declare -g sv_archive_cdi="https://archive.org/download/mister-sam-cdi-vcd-commercials/mister-sam-cdi-vcd-commercials_files.xml" declare -g sv_archive_hdmilist="https://archive.org/download/640x480_videogame_commercials/640x480_videogame_commercials_files.xml" declare -g sv_archive_crtlist="https://archive.org/download/640x240_videogame_commercials/640x240_videogame_commercials_files.xml" # ======== CORE PATHS RBF ======== declare -g amigapathrbf="_Computer" declare -g amigacd32pathrbf="_Computer" declare -g arcadepathrbf="_Arcade" declare -g ao486pathrbf="_Computer" declare -g atari2600pathrbf="_Console" declare -g atari5200pathrbf="_Console" declare -g atari7800pathrbf="_Console" declare -g atarilynxpathrbf="_Console" declare -g c64pathrbf="_Computer" declare -g cdipathrbf="_Console" declare -g coco2pathrbf="_Computer" declare -g colecovisionpathrbf="_Console" declare -g intellivisionpathrbf="_Console" declare -g fdspathrbf="_Console" declare -g gbpathrbf="_Console" declare -g gbcpathrbf="_Console" declare -g gbapathrbf="_Console" declare -g genesispathrbf="_Console" declare -g ggpathrbf="_Console" declare -g jaguarpathrbf="_Console" declare -g megacdpathrbf="_Console" declare -g n64pathrbf="_Console" declare -g neogeopathrbf="_Console" declare -g neogeocdpathrbf="_Console" declare -g nespathrbf="_Console" declare -g s32xpathrbf="_Console" declare -g saturnpathrbf="_Console" declare -g sgbpathrbf="_Console" declare -g smspathrbf="_Console" declare -g snespathrbf="_Console" declare -g stvpathrbf="_Arcade" declare -g tgfx16pathrbf="_Console" declare -g tgfx16cdpathrbf="_Console" declare -g psxpathrbf="_Console" declare -g vectrexpathrbf="_Console" declare -g wonderswanpathrbf="_Console" declare -g wonderswancolorpathrbf="_Console" declare -g x68kpathrbf="_Computer" # SPECIAL CORES if [[ "${corelist[@]}" == *"amiga"* ]] || [[ "${corelist[@]}" == *"amigacd32"* ]] || [[ "${corelist[@]}" == *"ao486"* ]] && [ -f "${mrsampath}"/samindex ]; then declare -g amigapath="$("${mrsampath}"/samindex -q -s amiga -d |awk -F':' '{print $2}')" declare -g amigacore="$(find /media/fat/_Computer/ -iname "*minimig*")" declare -g amigacd32path="$("${mrsampath}"/samindex -q -s amigacd32 -d |awk -F':' '{print $2}')" declare -g ao486path="$("${mrsampath}"/samindex -q -s ao486 -d |awk -F':' '{print $2}')" fi special_cores=(amiga ao486 x68k) #amigacd32 uses normal gamelists since it's chd files # ======= MiSTer.ini AITORGOMEZ FORK ======= declare -g cfgcore_configpath=$( awk -F '=' ' BEGIN { found = 0 } /^cfgcore_subfolder[[:space:]]*=/ { if (!found) { print "/media/fat/config/" $2; found = 1 } } END { if (!found) print "" } ' "$ini_file" | tr -d '"' | sed -e 's|//|/|g' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) declare -g cfgarcade_configpath=$( awk -F '=' ' BEGIN { found = 0 } /^cfgarcade_subfolder[[:space:]]*=/ { if (!found) { print "/media/fat/config/" $2; found = 1 } } END { if (!found) print "" } ' "$ini_file" | tr -d '"' | sed -e 's|//|/|g' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) if [[ -n "$cfgcore_configpath" ]]; then declare -g configpath="$cfgcore_configpath" else declare -g configpath="/media/fat/config/" fi } # ======== CORE CONFIG ======== function init_data() { # Core to long name mappings declare -gA CORE_PRETTY=( ["amiga"]="Commodore Amiga" ["arcade"]="MiSTer Arcade" ["amigacd32"]="Commodore Amiga CD32" ["ao486"]="PC 486 DX-100" ["atari2600"]="Atari 2600" ["atari5200"]="Atari 5200" ["atari7800"]="Atari 7800" ["atarilynx"]="Atari Lynx" ["c64"]="Commodore 64" ["cdi"]="Philips CD-i" ["coco2"]="TRS-80 Color Computer 2" ["colecovision"]="ColecoVision" ["intellivision"]="Mattel Intellivision" ["fds"]="Nintendo Disk System" ["gb"]="Nintendo Game Boy" ["gbc"]="Nintendo Game Boy Color" ["gba"]="Nintendo Game Boy Advance" ["genesis"]="Sega Genesis / Megadrive" ["gg"]="Sega Game Gear" ["jaguar"]="Atari Jaguar" ["megacd"]="Sega CD / Mega CD" ["n64"]="Nintendo N64" ["neogeo"]="SNK NeoGeo" ["neogeocd"]="SNK NeoGeo CD" ["nes"]="Nintendo Entertainment System" ["s32x"]="Sega 32x" ["saturn"]="Sega Saturn" ["sgb"]="Super Gameboy" ["sms"]="Sega Master System" ["snes"]="Super Nintendo" ["stv"]="Sega Titan Video" ["tgfx16"]="NEC TurboGrafx-16 " ["tgfx16cd"]="NEC TurboGrafx-16 CD" ["psx"]="Sony Playstation" ["vectrex"]="GCE Vectrex" ["wonderswan"]="Bandai WonderSwan" ["wonderswancolor"]="Bandai WonderSwan Color" ["x68k"]="Sharp X68000" ["mgls"]="Custom MGL" ) # Core to file extension mappings declare -glA CORE_EXT=( ["amigacd32"]="chd,cue" ["ao486"]="mgl" ["arcade"]="mra" ["atari2600"]="a26" ["atari5200"]="a52,car" ["atari7800"]="a78" ["atarilynx"]="lnx" ["c64"]="crt,prg" # need to be tested "reu,tap,flt,rom,c1581" ["cdi"]="chd,cue" ["coco2"]="ccc" ["colecovision"]="col" ["intellivision"]="int,bin,rom" ["fds"]="fds" ["gb"]="gb" ["gbc"]="gbc" ["gba"]="gba" ["genesis"]="md,gen" ["gg"]="gg" ["jaguar"]="j64,rom,bin,jag" ["megacd"]="chd,cue" ["n64"]="n64,z64" ["neogeo"]="neo" ["neogeocd"]="cue,chd" ["nes"]="nes" ["s32x"]="32x" ["saturn"]="cue,chd" ["sgb"]="gb,gbc" ["sms"]="sms,sg" ["snes"]="sfc,smc" # Should we include? "bin,bs" ["tgfx16"]="pce,sgx" ["tgfx16cd"]="chd,cue" ["psx"]="chd,cue,exe" ["vectrex"]="bin" ["wonderswan"]="ws" ["wonderswancolor"]="wsc" ["x68k"]="mgl" ["mgls"]="mgl" ) # Core to path mappings declare -gA PATHFILTER=( ["amiga"]="${amigapathfilter}" ["amigacd32"]="${amigacd32pathfilter}" ["ao486"]="${ao486pathfilter}" ["arcade"]="${arcadepathfilter}" ["atari2600"]="${atari2600pathfilter}" ["atari5200"]="${atari5200pathfilter}" ["atari7800"]="${atari7800pathfilter}" ["atarilynx"]="${atarilynxpathfilter}" ["c64"]="${c64pathfilter}" ["cdi"]="${cdipathfilter}" ["coco2"]="${coco2pathfilter}" ["colecovision"]="${colecovisionpathfilter}" ["intellivision"]="${intellivisionpathfilter}" ["fds"]="${fdspathfilter}" ["gb"]="${gbpathfilter}" ["gbc"]="${gbcpathfilter}" ["gba"]="${gbapathfilter}" ["genesis"]="${genesispathfilter}" ["gg"]="${ggpathfilter}" ["jaguar"]="${jaguarpathfilter}" ["megacd"]="${megacdpathfilter}" ["n64"]="${n64pathfilter}" ["neogeo"]="${neogeopathfilter}" ["neogeocd"]="${neogeocdpathfilter}" ["nes"]="${nespathfilter}" ["s32x"]="${s32xpathfilter}" ["saturn"]="${saturnpathfilter}" ["sgb"]="${sgbpathfilter}" ["sms"]="${smspathfilter}" ["snes"]="${snespathfilter}" ["stv"]="${stvpathfilter}" ["tgfx16"]="${tgfx16pathfilter}" ["tgfx16"]="${tgfx16pathfilter}" ["tgfx16cd"]="${tgfx16cdpathfilter}" ["psx"]="${psxpathfilter}" ["vectrex"]="${vectrexpathfilter}" ["wonderswan"]="${wonderswanpathfilter}" ["wonderswancolor"]="${wonderswancolorpathfilter}" ["x68k"]="${x68kpathfilter}" ["mgls"]="${mglspathfilter}" ) # Core to path mappings for rbf files declare -gA CORE_PATH_RBF=( ["amiga"]="${amigapathrbf}" ["amigacd32"]="${amigacd32pathrbf}" ["ao486"]="${ao486pathrbf}" ["arcade"]="${arcadepathrbf}" ["atari2600"]="${atari2600pathrbf}" ["atari5200"]="${atari5200pathrbf}" ["atari7800"]="${atari7800pathrbf}" ["atarilynx"]="${atarilynxpathrbf}" ["c64"]="${c64pathrbf}" ["cdi"]="${cdipathrbf}" ["coco2"]="${coco2pathrbf}" ["colecovision"]="${colecovisionpathrbf}" ["intellivision"]="${intellivisionpathrbf}" ["fds"]="${fdspathrbf}" ["gb"]="${gbpathrbf}" ["gbc"]="${gbcpathrbf}" ["gba"]="${gbapathrbf}" ["genesis"]="${genesispathrbf}" ["gg"]="${ggpathrbf}" ["jaguar"]="${jaguarpathrbf}" ["megacd"]="${megacdpathrbf}" ["n64"]="${n64pathrbf}" ["neogeo"]="${neogeopathrbf}" ["neogeocd"]="${neogeocdpathrbf}" ["nes"]="${nespathrbf}" ["s32x"]="${s32xpathrbf}" ["saturn"]="${saturnpathrbf}" ["sgb"]="${sgbpathrbf}" ["sms"]="${smspathrbf}" ["snes"]="${snespathrbf}" ["stv"]="${stvpathrbf}" ["tgfx16"]="${tgfx16pathrbf}" ["tgfx16cd"]="${tgfx16cdpathrbf}" ["psx"]="${psxpathrbf}" ["vectrex"]="${vectrexpathrbf}" ["wonderswan"]="${wonderswanpathrbf}" ["wonderswancolor"]="${wonderswancolorpathrbf}" ["x68k"]="${x68kpathrbf}" ) # Can this core skip Bios/Safety warning messages declare -glA CORE_SKIP=( ["amiga"]="No" ["amigacd32"]="Yes" ["ao486"]="No" ["arcade"]="No" ["atari2600"]="No" ["atari5200"]="No" ["atari7800"]="No" ["atarilynx"]="No" ["c64"]="No" ["cdi"]="No" ["coco2"]="No" ["colecovision"]="No" ["intellivision"]="Yes" ["fds"]="Yes" ["gb"]="No" ["gbc"]="No" ["gba"]="No" ["genesis"]="No" ["gg"]="No" ["jaguar"]="No" ["megacd"]="Yes" ["n64"]="No" ["neogeo"]="No" ["neogeocd"]="Yes" ["nes"]="No" ["s32x"]="No" ["saturn"]="Yes" ["sgb"]="No" ["sms"]="No" ["snes"]="No" ["stv"]="No" ["tgfx16"]="No" ["tgfx16cd"]="Yes" ["psx"]="No" ["vectrex"]="No" ["wonderswan"]="No" ["wonderswancolor"]="No" ["x68k"]="No" ["mgls"]="No" ) # Core to input maps mapping declare -gA CORE_LAUNCH=( ["amiga"]="Minimig" ["amigacd32"]="Minimig" ["ao486"]="ao486" ["arcade"]="Arcade" ["atari2600"]="ATARI7800" ["atari5200"]="ATARI5200" ["atari7800"]="ATARI7800" ["atarilynx"]="AtariLynx" ["c64"]="C64" ["cdi"]="CDi" ["coco2"]="CoCo2" ["colecovision"]="ColecoVision" ["intellivision"]="Intellivision" ["fds"]="NES" ["gb"]="GAMEBOY" ["gbc"]="GAMEBOY" ["gba"]="GBA" ["genesis"]="MEGADRIVE" ["gg"]="SMS" ["jaguar"]="Jaguar" ["megacd"]="MegaCD" ["n64"]="N64" ["neogeo"]="NEOGEO" ["neogeocd"]="NEOGEO" ["nes"]="NES" ["s32x"]="S32X" ["saturn"]="SATURN" ["sgb"]="SGB" ["sms"]="SMS" ["snes"]="SNES" ["stv"]="S-TV" ["tgfx16"]="TGFX16" ["tgfx16cd"]="TGFX16" ["psx"]="PSX" ["vectrex"]="Vectrex" ["wonderswan"]="WonderSwan" ["wonderswancolor"]="WonderSwan" ["x68k"]="X68000" ["mgls"]="MGL" ) # TTY2OLED Core Pic mappings declare -gA TTY2OLED_PIC_NAME=( ["amiga"]="Minimig" ["amigacd32"]="Minimig" ["ao486"]="ao486" ["arcade"]="Arcade" ["atari2600"]="ATARI2600" ["atari5200"]="ATARI5200" ["atari7800"]="ATARI7800" ["atarilynx"]="AtariLynx" ["c64"]="C64" ["cdi"]="CD-i" ["coco2"]="CoCo2" ["colecovision"]="ColecoVision" ["intellivision"]="Intellivision" ["fds"]="fds" ["gb"]="GAMEBOY" ["gbc"]="GAMEBOY" ["gba"]="GBA" ["genesis"]="MegaDrive" ["gg"]="gamegear" ["jaguar"]="Jaguar" ["megacd"]="MegaCD" ["n64"]="N64" ["neogeo"]="NEOGEO" ["neogeocd"]="NEOGEO" ["nes"]="NES" ["s32x"]="S32X" ["saturn"]="SATURN" ["sgb"]="SGB" ["sms"]="SMS" ["snes"]="SNES" ["stv"]="S-TV" ["tgfx16"]="TGFX16" ["tgfx16cd"]="TGFX16" ["psx"]="PSX" ["vectrex"]="Vectrex" ["wonderswan"]="WonderSwan" ["wonderswancolor"]="WonderSwan" ["x68k"]="X68000" ["mgls"]="MGL" ) # MGL core name settings declare -gA MGL_CORE=( ["amiga"]="Minimig" ["amigacd32"]="Minimig" ["ao486"]="ao486" ["arcade"]="Arcade" ["atari2600"]="ATARI7800" ["atari5200"]="ATARI5200" ["atari7800"]="ATARI7800" ["atarilynx"]="AtariLynx" ["c64"]="C64" ["cdi"]="CDi" ["coco2"]="CoCo2" ["colecovision"]="ColecoVision" ["intellivision"]="Intellivision" ["fds"]="NES" ["gb"]="GAMEBOY" ["gbc"]="GAMEBOY" ["gba"]="GBA" ["genesis"]="MegaDrive" ["gg"]="SMS" ["jaguar"]="Jaguar" ["megacd"]="MegaCD" ["n64"]="N64" ["neogeo"]="NEOGEO" ["neogeocd"]="NEOGEO" ["nes"]="NES" ["s32x"]="S32X" ["saturn"]="SATURN" ["sgb"]="SGB" ["sms"]="SMS" ["snes"]="SNES" ["stv"]="S-TV" ["tgfx16"]="TurboGrafx16" ["tgfx16cd"]="TurboGrafx16" ["psx"]="PSX" ["vectrex"]="Vectrex" ["wonderswan"]="WonderSwan" ["wonderswancolor"]="WonderSwan" ["x68k"]="X68000" ) # MGL setname settings declare -gA MGL_SETNAME=( ["amigacd32"]="AmigaCD32" ["gbc"]="GBC" ["gg"]="GameGear" ["wonderswancolor"]="WonderSwanColor" ) # MGL delay settings declare -giA MGL_DELAY=( ["amiga"]="1" ["amigacd32"]="1" ["ao486"]="0" ["arcade"]="2" ["atari2600"]="1" ["atari5200"]="1" ["atari7800"]="1" ["atarilynx"]="1" ["c64"]="1" ["cdi"]="1" ["coco2"]="1" ["colecovision"]="1" ["intellivision"]="1" ["fds"]="2" ["gb"]="2" ["gbc"]="2" ["gba"]="2" ["genesis"]="1" ["gg"]="1" ["jaguar"]="1" ["megacd"]="1" ["n64"]="1" ["neogeo"]="1" ["neogeocd"]="1" ["nes"]="2" ["s32x"]="1" ["saturn"]="1" ["sgb"]="1" ["sms"]="1" ["snes"]="2" ["stv"]="2" ["tgfx16"]="1" ["tgfx16cd"]="1" ["psx"]="1" ["vectrex"]="1" ["wonderswan"]="1" ["wonderswancolor"]="1" ["x68k"]="1" ) # MGL index settings declare -giA MGL_INDEX=( ["amiga"]="0" ["amigacd32"]="0" ["ao486"]="2" ["arcade"]="0" ["atari2600"]="0" ["atari5200"]="1" ["atari7800"]="1" ["atarilynx"]="1" ["c64"]="1" ["cdi"]="1" ["coco2"]="1" ["colecovision"]="1" ["intellivision"]="1" ["fds"]="0" ["gb"]="0" ["gbc"]="0" ["gba"]="0" ["genesis"]="0" ["gg"]="2" ["jaguar"]="1" ["megacd"]="0" ["n64"]="1" ["neogeo"]="1" ["neogeocd"]="1" ["nes"]="0" ["s32x"]="0" ["saturn"]="0" ["sgb"]="1" ["sms"]="1" ["snes"]="0" ["stv"]="0" ["tgfx16"]="1" ["tgfx16cd"]="0" ["psx"]="1" ["vectrex"]="1" ["wonderswan"]="1" ["wonderswancolor"]="1" ["x68k"]="2" ) # MGL type settings declare -glA MGL_TYPE=( ["amiga"]="f" ["amigacd32"]="f" ["ao486"]="s" ["arcade"]="f" ["atari2600"]="f" ["atari5200"]="f" ["atari7800"]="f" ["atarilynx"]="f" ["c64"]="f" ["cdi"]="s" ["coco2"]="f" ["colecovision"]="f" ["intellivision"]="f" ["fds"]="f" ["gb"]="f" ["gbc"]="f" ["gba"]="f" ["genesis"]="f" ["gg"]="f" ["jaguar"]="f" ["megacd"]="s" ["n64"]="f" ["neogeo"]="f" ["neogeocd"]="s" ["nes"]="f" ["s32x"]="f" ["saturn"]="s" ["sgb"]="f" ["sms"]="f" ["snes"]="f" ["stv"]="f" ["tgfx16"]="f" ["tgfx16cd"]="s" ["psx"]="s" ["vectrex"]="f" ["wonderswan"]="f" ["wonderswancolor"]="f" ["x68k"]="s" ) # NEOGEO to long name mappings English declare -gA NEOGEO_PRETTY_ENGLISH=( ["3countb"]="3 Count Bout" ["2020bb"]="2020 Super Baseball" ["2020bba"]="2020 Super Baseball (set 2)" ["2020bbh"]="2020 Super Baseball (set 3)" ["abyssal"]="Abyssal Infants" ["alpham2"]="Alpha Mission II" ["alpham2p"]="Alpha Mission II (prototype)" ["androdun"]="Andro Dunos" ["aodk"]="Aggressors of Dark Kombat" ["aof"]="Art of Fighting" ["aof2"]="Art of Fighting 2" ["aof2a"]="Art of Fighting 2 (NGH-056)" ["aof3"]="Art of Fighting 3: The Path of the Warrior" ["aof3k"]="Art of Fighting 3: The Path of the Warrior (Korean release)" ["b2b"]="Bang Bang Busters" ["badapple"]="Bad Apple Demo" ["bakatono"]="Bakatonosama Mahjong Manyuuki" ["bangbead"]="Bang Bead" ["bjourney"]="Blue's Journey" ["blazstar"]="Blazing Star" ["breakers"]="Breakers" ["breakrev"]="Breakers Revenge" ["brningfh"]="Burning Fight (NGH-018, US)" ["brningfp"]="Burning Fight (prototype, older)" ["brnngfpa"]="Burning Fight (prototype, near final, ver 23.3, 910326)" ["bstars"]="Baseball Stars Professional" ["bstars2"]="Baseball Stars 2" ["bstarsh"]="Baseball Stars Professional (NGH-002)" ["burningf"]="Burning Fight" ["burningfh"]="Burning Fight (NGH-018, US)" ["burningfp"]="Burning Fight (prototype, older)" ["burningfpa"]="Burning Fight (prototype, near final, ver 23.3, 910326)" ["cabalng"]="Cabal" ["columnsn"]="Columns" ["cphd"]="Crouching Pony Hidden Dragon Demo" ["crswd2bl"]="Crossed Swords 2 (CD conversion)" ["crsword"]="Crossed Swords" ["ct2k3sa"]="Crouching Tiger Hidden Dragon 2003 Super Plus (The King of Fighters 2001 bootleg)" ["ctomaday"]="Captain Tomaday" ["cyberlip"]="Cyber-Lip" ["diggerma"]="Digger Man" ["doubledr"]="Double Dragon" ["eightman"]="Eight Man" ["fatfursp"]="Fatal Fury Special" ["fatfurspa"]="Fatal Fury Special (NGM-058 ~ NGH-058, set 2)" ["fatfury1"]="Fatal Fury: King of Fighters" ["fatfury2"]="Fatal Fury 2" ["fatfury3"]="Fatal Fury 3: Road to the Final Victory" ["fbfrenzy"]="Football Frenzy" ["fghtfeva"]="Fight Fever (set 2)" ["fightfev"]="Fight Fever" ["fightfeva"]="Fight Fever (set 2)" ["flipshot"]="Battle Flip Shot" ["frogfest"]="Frog Feast" ["froman2b"]="Idol Mahjong Final Romance 2 (CD conversion)" ["fswords"]="Fighters Swords (Korean release of Samurai Shodown III)" ["ftfurspa"]="Fatal Fury Special (NGM-058 ~ NGH-058, set 2)" ["galaxyfg"]="Galaxy Fight: Universal Warriors" ["ganryu"]="Ganryu" ["garou"]="Garou: Mark of the Wolves" ["garoubl"]="Garou: Mark of the Wolves (bootleg)" ["garouh"]="Garou: Mark of the Wolves (earlier release)" ["garoup"]="Garou: Mark of the Wolves (prototype)" ["ghostlop"]="Ghostlop" ["goalx3"]="Goal! Goal! Goal!" ["gowcaizr"]="Voltage Fighter Gowcaizer" ["gpilots"]="Ghost Pilots" ["gpilotsh"]="Ghost Pilots (NGH-020, US)" ["gururin"]="Gururin" ["hyprnoid"]="Hypernoid" ["irnclado"]="Ironclad (prototype, bootleg)" ["ironclad"]="Ironclad" ["ironclado"]="Ironclad (prototype, bootleg)" ["irrmaze"]="The Irritating Maze" ["janshin"]="Janshin Densetsu: Quest of Jongmaster" ["joyjoy"]="Puzzled" ["kabukikl"]="Far East of Eden: Kabuki Klash" ["karnovr"]="Karnov's Revenge" ["kf2k2mp"]="The King of Fighters 2002 Magic Plus (bootleg)" ["kf2k2mp2"]="The King of Fighters 2002 Magic Plus II (bootleg)" ["kf2k2pla"]="The King of Fighters 2002 Plus (bootleg set 2)" ["kf2k2pls"]="The King of Fighters 2002 Plus (bootleg)" ["kf2k5uni"]="The King of Fighters 10th Anniversary 2005 Unique (The King of Fighters 2002 bootleg)" ["kf10thep"]="The King of Fighters 10th Anniversary Extra Plus (The King of Fighters 2002 bootleg)" ["kizuna"]="Kizuna Encounter: Super Tag Battle" ["kof2k4se"]="The King of Fighters Special Edition 2004 (The King of Fighters 2002 bootleg)" ["kof94"]="The King of Fighters '94" ["kof95"]="The King of Fighters '95" ["kof95a"]="The King of Fighters '95 (NGM-084, alt board)" ["kof95h"]="The King of Fighters '95 (NGH-084)" ["kof96"]="The King of Fighters '96" ["kof96h"]="The King of Fighters '96 (NGH-214)" ["kof97"]="The King of Fighters '97" ["kof97h"]="The King of Fighters '97 (NGH-2320)" ["kof97k"]="The King of Fighters '97 (Korean release)" ["kof97oro"]="The King of Fighters '97 Chongchu Jianghu Plus 2003 (bootleg)" ["kof97pls"]="The King of Fighters '97 Plus (bootleg)" ["kof98"]="The King of Fighters '98: The Slugfest" ["kof98a"]="The King of Fighters '98: The Slugfest (NGM-2420, alt board)" ["kof98h"]="The King of Fighters '98: The Slugfest (NGH-2420)" ["kof98k"]="The King of Fighters '98: The Slugfest (Korean release)" ["kof98ka"]="The King of Fighters '98: The Slugfest (Korean release, set 2)" ["kof99"]="The King of Fighters '99: Millennium Battle" ["kof99e"]="The King of Fighters '99: Millennium Battle (earlier release)" ["kof99h"]="The King of Fighters '99: Millennium Battle (NGH-2510)" ["kof99k"]="The King of Fighters '99: Millennium Battle (Korean release)" ["kof99p"]="The King of Fighters '99: Millennium Battle (prototype)" ["kof2000"]="The King of Fighters 2000" ["kof2000n"]="The King of Fighters 2000" ["kof2001"]="The King of Fighters 2001" ["kof2001h"]="The King of Fighters 2001 (NGH-2621)" ["kof2002"]="The King of Fighters 2002" ["kof2002b"]="The King of Fighters 2002 (bootleg)" ["kof2003"]="The King of Fighters 2003" ["kof2003h"]="The King of Fighters 2003 (NGH-2710)" ["kof2003ps2"]="The King of Fighters 2003 (PS2)" ["kog"]="King of Gladiators (The King of Fighters '97 bootleg)" ["kotm"]="King of the Monsters" ["kotm2"]="King of the Monsters 2: The Next Thing" ["kotm2p"]="King of the Monsters 2: The Next Thing (prototype)" ["kotmh"]="King of the Monsters (set 2)" ["lans2004"]="Lansquenet" ["lastblad"]="The Last Blade" ["lastbladh"]="The Last Blade (NGH-2340)" ["lastbld2"]="The Last Blade 2" ["lasthope"]="Last Hope" ["lastsold"]="The Last Soldier" ["lbowling"]="League Bowling" ["legendos"]="Legend of Success Joe" ["lresort"]="Last Resort" ["lresortp"]="Last Resort (prototype)" ["lstbladh"]="Last Blade (NGH-2340)" ["magdrop2"]="Magical Drop II" ["magdrop3"]="Magical Drop III" ["maglord"]="Magician Lord" ["maglordh"]="Magician Lord (NGH-005)" ["mahretsu"]="Mahjong Kyo Retsuden" ["marukodq"]="Chibi Marukochan Deluxe Quiz" ["matrim"]="Power Instinct Matrimelee" ["miexchng"]="Money Puzzle Exchanger" ["minasan"]="Minasan no Okagesamadesu! Dai Sugoroku Taikai" ["montest"]="Monitor Test ROM" ["moshougi"]="Shougi no Tatsujin: Master of Syougi" ["ms4plus"]="Metal Slug 4 Plus (bootleg)" ["mslug"]="Metal Slug: Super Vehicle-001" ["mslug2"]="Metal Slug 2: Super Vehicle-001/II" ["mslug2t"]="Metal Slug 2 Turbo (hack)" ["mslug3"]="Metal Slug 3" ["mslug3b6"]="Metal Slug 6 (Metal Slug 3 bootleg)" ["mslug3h"]="Metal Slug 3 (NGH-2560)" ["mslug4"]="Metal Slug 4" ["mslug4h"]="Metal Slug 4 (NGH-2630)" ["mslug5"]="Metal Slug 5" ["mslug5h"]="Metal Slug 5 (NGH-2680)" ["mslug6"]="Metal Slug 6 (Metal Slug 3 bootleg)" ["mslugx"]="Metal Slug X: Super Vehicle-001" ["mutnat"]="Mutation Nation" ["nam1975"]="NAM-1975" ["nblktigr"]="Neo Black Tiger" ["ncombat"]="Ninja Combat" ["ncombath"]="Ninja Combat (NGH-009)" ["ncommand"]="Ninja Commando" ["neobombe"]="Neo Bomberman" ["neocup98"]="Neo-Geo Cup 98: The Road to the Victory" ["neodrift"]="Neo Drift Out: New Technology" ["neofight"]="Neo Fight" ["neomrdo"]="Neo Mr. Do!" ["neothund"]="Neo Thunder" ["neotris"]="NeoTRIS (free beta version)" ["ninjamas"]="Ninja Master's" ["nitd"]="Nightmare in the Dark" ["nitdbl"]="Nightmare in the Dark (bootleg)" ["nsmb"]="New Super Mario Bros." ["overtop"]="OverTop" ["panicbom"]="Panic Bomber" ["pbbblenb"]="Puzzle Bobble (bootleg)" ["pbobbl2n"]="Puzzle Bobble 2" ["pbobblen"]="Puzzle Bobble" ["pbobblenb"]="Puzzle Bobble (bootleg)" ["pgoal"]="Pleasure Goal" ["pnyaa"]="Pochi and Nyaa" ["popbounc"]="Pop 'n Bounce" ["preisle2"]="Prehistoric Isle 2" ["pspikes2"]="Power Spikes II" ["pulstar"]="Pulstar" ["puzzldpr"]="Puzzle De Pon! R!" ["puzzledp"]="Puzzle De Pon!" ["quizdai2"]="Quiz Meitantei Neo & Geo: Quiz Daisousa Sen part 2" ["quizdais"]="Quiz Daisousa Sen: The Last Count Down" ["quizdask"]="Quiz Salibtamjeong: The Last Count Down (Korean localized Quiz Daisousa Sen)" ["quizkof"]="Quiz King of Fighters" ["quizkofk"]="Quiz King of Fighters (Korean release)" ["ragnagrd"]="Ragnagard" ["rbff1"]="Real Bout Fatal Fury" ["rbff1a"]="Real Bout Fatal Fury (bug fix revision)" ["rbff2"]="Real Bout Fatal Fury 2: The Newcomers" ["rbff2h"]="Real Bout Fatal Fury 2: The Newcomers (NGH-2400)" ["rbff2k"]="Real Bout Fatal Fury 2: The Newcomers (Korean release)" ["rbffspck"]="Real Bout Fatal Fury Special (Korean release)" ["rbffspec"]="Real Bout Fatal Fury Special" ["rbffspeck"]="Real Bout Fatal Fury Special (Korean release)" ["ridhero"]="Riding Hero" ["ridheroh"]="Riding Hero (set 2)" ["roboarma"]="Robo Army (NGM-032 ~ NGH-032)" ["roboarmy"]="Robo Army" ["roboarmya"]="Robo Army (NGM-032 ~ NGH-032)" ["rotd"]="Rage of the Dragons" ["rotdh"]="Rage of the Dragons (NGH-2640?)" ["s1945p"]="Strikers 1945 Plus" ["samsh5fe"]="Samurai Shodown V Special Final Edition" ["samsh5pf"]="Samurai Shodown V Perfect" ["samsh5sp"]="Samurai Shodown V Special" ["samsh5sph"]="Samurai Shodown V Special (2nd release, less censored)" ["samsh5spho"]="Samurai Shodown V Special (1st release, censored)" ["samsho"]="Samurai Shodown" ["samsho2"]="Samurai Shodown II" ["samsho2k"]="Saulabi Spirits (Korean release of Samurai Shodown II)" ["samsho2ka"]="Saulabi Spirits (Korean release of Samurai Shodown II, set 2)" ["samsho3"]="Samurai Shodown III" ["samsho3h"]="Samurai Shodown III (NGH-087)" ["samsho4"]="Samurai Shodown IV: Amakusa's Revenge" ["samsho4k"]="Pae Wang Jeon Seol: Legend of a Warrior" ["samsho5"]="Samurai Shodown V" ["samsho5b"]="Samurai Shodown V (bootleg)" ["samsho5h"]="Samurai Shodown V (NGH-2700)" ["samsho5x"]="Samurai Shodown V (XBOX version hack)" ["samshoh"]="Samurai Shodown (NGH-045)" ["savagere"]="Savage Reign" ["sbp"]="Super Bubble Pop" ["scbrawlh"]="Soccer Brawl (NGH-031)" ["sdodgeb"]="Super Dodge Ball" ["sengoku"]="Sengoku" ["sengoku2"]="Sengoku 2" ["sengoku3"]="Sengoku 3" ["sengokuh"]="Sengoku (NGH-017, US)" ["shcktroa"]="Shock Troopers (set 2)" ["shocktr2"]="Shock Troopers: 2nd Squad" ["shocktro"]="Shock Troopers" ["shocktroa"]="Shock Troopers (set 2)" ["smbng"]="New Super Mario Bros. Demo" ["smsh5sph"]="Samurai Shodown V Special (2nd release, less censored)" ["smsh5spo"]="Samurai Shodown V Special (1st release, censored)" ["smsho2k2"]="Saulabi Spirits (Korean release of Samurai Shodown II, set 2)" ["socbrawl"]="Soccer Brawl" ["socbrawlh"]="Soccer Brawl (NGH-031)" ["sonicwi2"]="Aero Fighters 2" ["sonicwi3"]="Aero Fighters 3" ["spinmast"]="Spinmaster" ["ssideki"]="Super Sidekicks" ["ssideki2"]="Super Sidekicks 2: The World Championship" ["ssideki3"]="Super Sidekicks 3: The Next Glory" ["ssideki4"]="The Ultimate 11: The SNK Football Championship" ["stakwin"]="Stakes Winner" ["stakwin2"]="Stakes Winner 2" ["strhoop"]="Street Hoop / Street Slam" ["superspy"]="The Super Spy" ["svc"]="SNK vs. Capcom: SVC Chaos" ["svccpru"]="SNK vs. Capcom Remix Ultra" ["svcplus"]="SNK vs. Capcom Plus (bootleg)" ["svcsplus"]="SNK vs. Capcom Super Plus (bootleg)" ["teot"]="The Eye of Typhoon: Tsunami Edition" ["tetrismn"]="Tetris" ["tophuntr"]="Top Hunter: Roddy & Cathy" ["tophuntrh"]="Top Hunter: Roddy & Cathy (NGH-046)" ["totc"]="Treasure of the Caribbean" ["tpgolf"]="Top Player's Golf" ["tphuntrh"]="Top Hunter: Roddy & Cathy (NGH-046)" ["trally"]="Thrash Rally" ["turfmast"]="Neo Turf Masters" ["twinspri"]="Twinkle Star Sprites" ["tws96"]="Tecmo World Soccer '96" ["twsoc96"]="Tecmo World Soccer '96" ["viewpoin"]="Viewpoint" ["wakuwak7"]="Waku Waku 7" ["wh1"]="World Heroes" ["wh1h"]="World Heroes (ALH-005)" ["wh1ha"]="World Heroes (set 3)" ["wh2"]="World Heroes 2" ["wh2j"]="World Heroes 2 Jet" ["whp"]="World Heroes Perfect" ["wjammers"]="Windjammers" ["wjammss"]="Windjammers Supersonic" ["xenocrisis"]="Xeno Crisis" ["zedblade"]="Zed Blade" ["zintrckb"]="ZinTricK" ["zintrkcd"]="ZinTricK (CD conversion)" ["zupapa"]="Zupapa!" ) declare -glA SV_TVC=( ["arcade"]="arcade" ["atari2600"]="atari vcs" ["atari5200"]="atari 5200" ["atari7800"]="atari 7800" ["atarilynx"]="atari lynx" ["gb"]="gb\|game boy" ["gbc"]="gb\|game boy" ["genesis"]="genesis" ["gg"]="sega game" ["megacd"]="megacd" ["n64"]="n64-\|n64" ["neogeo"]="neogeo" ["nes"]="^nes-\| nes" ["psx"]="psx\|playstation" ["s32x"]="sega 32x" ["saturn"]="sega saturn" ["sgb"]="super game boy\|gb-super game boy\|snes-super game boy" ["sms"]="sega master" ["snes"]="snes" ["tgfx16"]="turboduo\|turbografx-16" ["tgfx16cd"]="turboduo" ) RATED_FILES=( amiga_rated.txt ao486_rated.txt arcade_rated.txt fds_rated.txt gb_rated.txt gba_rated.txt gbc_rated.txt genesis_rated.txt gg_rated.txt megacd_rated.txt n64_mature.txt n64_rated.txt neogeo_rated.txt nes_rated.txt psx_rated.txt saturn_mature.txt saturn_rated.txt sms_rated.txt snes_rated.txt tgfx16_rated.txt tgfx16cd_mature.txt tgfx16cd_rated.txt ) BLACKLIST_FILES=( amiga_blacklist.txt arcade_blacklist.txt fds_blacklist.txt gba_blacklist.txt genesis_blacklist.txt megacd_blacklist.txt n64_blacklist.txt neogeo_blacklist.txt nes_blacklist.txt psx_blacklist.txt s32x_blacklist.txt sms_blacklist.txt snes_blacklist.txt tgfx16_blacklist.txt tgfx16cd_blacklist.txt ) } # ========= SOUCRCE INI & UPDATE ========= # Read INI function read_samini() { if [ ! -f "${samini_file}" ]; then echo "Error: MiSTer_SAM.ini not found. Attempting to update now..." get_samstuff MiSTer_SAM.ini /media/fat/Scripts if [ $? -ne 0 ]; then echo "Error: Please try again or update MiSTer_SAM.ini manually." exit 1 fi fi source "${samini_file}" declare -g raw_base="https://raw.githubusercontent.com/mrchrisster/MiSTer_SAM/${branch}" # Remove trailing slash from paths grep "^[^#;]" < "${samini_file}" | grep "pathfilter=" | cut -f1 -d"=" | while IFS= read -r var; do declare -g "${var}"="${!var%/}" done #corelist=("$(echo "${corelist[@]}" | tr ',' ' ' | tr -s ' ')") IFS=',' read -ra corelist <<< "${corelist}" IFS=',' read -ra corelistall <<< "${corelistall}" #BGM mode if [ "${bgm}" == "yes" ]; then # delete n64 and psx # echo "Deleting N64 and PSX from corelist" new_corelist=() for core in "${corelist[@]}"; do if [[ "$core" != "n64" && "$core" != "psx" ]]; then new_corelist+=("$core") fi done corelist=("${new_corelist[@]}") mute="core" fi #Roulette Mode if [ -f /tmp/.SAM_tmp/gameroulette.ini ]; then source /tmp/.SAM_tmp/gameroulette.ini fi #GOAT Mode if [ "$sam_goat_list" == "yes" ]; then build_goat_lists fi #NES M82 Mode if [ "$m82" == "yes" ]; then build_m82_list fi } function update_samini() { [ ! -f /media/fat/Scripts/.config/downloader/downloader.log ] && return [ ! -f ${samini_file} ] && return if [[ "$(cat /media/fat/Scripts/.config/downloader/downloader.log | grep -c "MiSTer_SAM.default.ini")" != "0" ]] && [ "${samini_update_file}" -nt "${samini_file}" ]; then echo "New MiSTer_SAM.ini version downloaded from update_all. Merging with new ini." echo "Backing up MiSTer_SAM.ini to MiSTer_SAM.ini.bak" cp "${samini_file}" "${samini_file}".bak echo -n "Merging ini values.." # In order for the following awk script to replace variable values, we need to change our ASCII art from "=" to "-" sed -i 's/==/--/g' "${samini_file}" sed -i 's/-=/--/g' "${samini_file}" #awk -F= 'NR==FNR{a[$1]=$0;next}($1 in a){$0=a[$1]}1' "${samini_file}" "${samini_update_file}" >/tmp/MiSTer_SAM.tmp && cp -f --force /tmp/MiSTer_SAM.tmp "${samini_file}" echo "Done." fi } # ============== PARSE COMMANDS =============== function parse_cmd() { # 1) No args ⇒ show the pre-menu (( $# == 0 )) && { sam_premenu; return; } # 2) Normalize local first="${1,,}" shift # 3) Single core shorthand if [[ -n ${CORE_PRETTY[$first]} ]]; then tmp_reset echo $first > "${corelistfile}.single" echo "${CORE_PRETTY[$first]} selected!" sam_start "$first" return fi # 4) Built-in commands (now with explicit menu handling) case "$first" in start|restart) sam_start "$@" ;; startmonitor|sm) sam_start "$@"; sleep 1; sam_monitor ;; skip|next) echo "Skipping…"; tmux send-keys -t SAM n ;; stop|kill) tmp_reset; kill_all_sams; exit_sam menu ;; update) sam_update ;; monitor) sam_monitor ;; mcp_monitor) mcp_monitor ;; exit_to_menu) exit_sam menu ;; exit_to_game) exit_sam game ;; unmute) unmute_with_retry ;; enable) env_check enable; sam_enable ;; disable) sam_cleanup; kill_all_sams; sam_disable ;; ignore) ignoregame ;; default) sam_update autoconfig ;; autoconfig|defaultb) tmux kill-session -t MCP &>/dev/null there_can_be_only_one sam_update; mcp_start; sam_enable ;; bootstart) env_check bootstart boot_sleep mcp_start # Pre-run prep so the first idle trigger is instant echo "SAM: Performing boot-time prep..." sam_prep ;; loop_core) loop_core "$@" ;; menu|back) sam_menu ;; help) sam_help ;; sshconfig) sam_sshconfig ;; menu_*) # Check if the function is actually defined before trying to run it if declare -F "$first" > /dev/null; then "$first" "$@" else echo "Error: Unknown menu function '$first'" >&2 sam_help return 1 fi ;; *) # Otherwise unknown (the old catch-all is now just for errors) echo "Unknown command: $first" >&2 sam_help return 1 ;; esac } # ======== SAM MENU ======== function sam_premenu() { echo "+---------------------------+" echo "| MiSTer Super Attract Mode |" echo "+---------------------------+" echo " SAM Configuration:" if grep -iq "mister_sam" "${userstartup}"; then echo " -SAM autoplay ENABLED" else echo " -SAM autoplay DISABLED" fi echo " -Start after ${samtimeout} sec. idle" echo " -Start only on the menu: ${menuonly^}" echo " -Show each game for ${gametimer} sec." echo "" echo " Press UP to open menu" echo " Press DOWN to start SAM" echo "" echo " Or wait for" echo " auto-start" echo "" # default action to Start premenu="Start" for i in {10..1}; do echo -ne " Starting SAM in ${i} secs...\033[0K\r" read -r -s -N 1 -t 1 key case "$key" in A) # UP arrow premenu="Menu" break ;; B) # DOWN arrow premenu="Start" break ;; C) # RIGHT arrow (or Ctrl‑something) premenu="Default" break ;; esac done echo # clear the countdown line parse_cmd "${premenu}" } function sam_menu() { # --- Ensure the menu system is available before showing the menu --- load_menu_if_needed # If you were exporting CORE_PRETTY for the menu script, that logic can stay # in your new load_menu_if_needed() function or here. Let's assume # it's not needed for this example to keep it simple. # --- Then show the main menu dialog --- while true; do dialog --clear --ascii-lines --no-tags \ --ok-label "Select" --cancel-label "Exit" \ --backtitle "Super Attract Mode" --title "[ Main Menu ]" \ --menu "Use arrow keys or d-pad to navigate" 0 0 0 \ Start "Start SAM" \ Startmonitor "Start + Monitor (SSH)" \ Stop "Stop SAM" \ Skip "Skip Game" \ Update "Update to latest" \ Ignore "Ignore current game" \ separator "-----------------------------" \ menu_presets "Presets & Game Modes" \ menu_coreconfig "Configure Core List" \ menu_exitbehavior "Configure Exit Behavior" \ menu_controller "Configure Gamepad" \ menu_filters "Filters" \ menu_addons "Add-ons" \ menu_inieditor "MiSTer_SAM.ini Editor" \ menu_settings "Settings" \ menu_reset "Reset or Uninstall SAM" \ 2> "${sam_menu_file}" local rc=$? choice=$(<"${sam_menu_file}") clear (( rc != 0 )) && break # First, handle UI-only elements like separators. # If the user selected the separator, just restart the loop. if [[ "${choice,,}" == "separator" ]]; then continue fi # Everything dispatches cleanly through parse_cmd parse_cmd "${choice,,}" # If it was a “playback” command, exit the menu loop case "${choice,,}" in start|startmonitor|stop|kill|skip|next|update|ignore) break ;; esac done } function load_menu_if_needed() { # If already loaded, do nothing. if (( MENU_LOADED == 1 )); then return 0 fi local menu_script="${mrsampath}/MiSTer_SAM_menu.sh" # Check if the menu script actually exists before trying to source it if [[ ! -f "$menu_script" ]]; then echo "Error: SAM is not fully installed." echo "Menu script not found at: $menu_script" >&2 # Optionally, exit or show a dialog error env_check return 1 fi # Add a debug message to confirm the source is being attempted # echo "Sourcing menu script..." >&2 # Source the script and set the flag source "$menu_script" MENU_LOADED=1 } # ======== SAM OPERATIONAL FUNCTIONS ======== function toggle_mute() { local volfile="${configpath}/Volume.dat" if [ ! -f "$volfile" ]; then echo -ne "\x00" > "$volfile"; fi local hexval=$(xxd -p -l 1 -s 0 "$volfile") local val=$((16#$hexval)) if (( (val & 16) == 16 )); then val=$((val & ~16)) echo -e "\nUnmuting..." timeout 1s sh -c "echo 'volume unmute' > /dev/MiSTer_cmd" else val=$((val | 16)) echo -e "\nMuting..." timeout 1s sh -c "echo 'volume mute' > /dev/MiSTer_cmd" fi printf "\\x$(printf %02x $val)" | dd of="$volfile" bs=1 count=1 conv=notrunc 2>/dev/null } function loop_core() { # args: [target_core] if [ -n "$1" ]; then SAM_MODE="SINGLE" SAM_TARGET_CORE="$1" else SAM_MODE="ALL" SAM_TARGET_CORE="" fi export SAM_MODE export SAM_TARGET_CORE # Global trap to detach monitor on Ctrl+C instead of killing SAM trap 'tmux detach-client' INT # --- 1. Heavy Initialization (Runs once in background) --- echo "SAM Session: Initializing..." update_samini read_samini # This is the heavy step (downloads/mounts) that was freezing Python sam_prep disable_bootrom bgm_start tty_start # --- 2. Main Loop --- echo "SAM Session: Setup complete. Entering main loop." echo -e "Starting Super Attract Mode...\nLet Mortal Kombat begin!\n" # Reset game log for this session echo "" >/tmp/SAM_Games.log samdebug "Initial corelist: ${corelist[*]}" # The infinite loop while :; do if [ "$SAM_ACTION" == "previous" ]; then if [ -f /tmp/.SAM_tmp/prev_game_info ]; then source /tmp/.SAM_tmp/prev_game_info if [ "$core" == "cdi" ] && [ "${samvideo_tvc_cdi}" == "yes" ]; then samdebug "Replaying previous CDI video: $gamename" # Force the selection of the same video sv_selected="$gamename" sv_ar_cdi_mode elif [ -f /tmp/SAM_game.previous.mgl ]; then samdebug "Replaying previous game" load_core "mgls" "/tmp/SAM_game.previous.mgl" else samdebug "Previous MGL not found." fi SAM_ACTION="" run_countdown_timer continue else samdebug "No previous game info." SAM_ACTION="" fi fi if next_core "${1-}"; then # Cache previous game details for replay { printf "gamename=%q\n" "${gamename}" printf "rompath=%q\n" "${rompath}" printf "core=%q\n" "${core}" } > /tmp/.SAM_tmp/prev_game_info run_countdown_timer else samdebug "next_core failed. Looping to pick another core." continue fi done } function run_countdown_timer() { local countdown=${gametimer} local start_time=$SECONDS local end_time=$((start_time + countdown)) # Set a local trap to handle Ctrl+C. # Inherit global trap (detach) # trap 'echo; return' INT # This loop provides a visible, second-by-second countdown with input handling local video_synced="yes" if [ "${samvideo}" == "yes" ] && [ "$sv_nextcore" == "samvideo" ]; then video_synced="no" # Ensure we don't timeout while loading if (( countdown < 60 )); then countdown=60; fi end_time=$((start_time + countdown)) fi while (( SECONDS < end_time )); do if [ "$video_synced" == "no" ]; then # extend timeout slightly to keep loop alive if loading is slow if (( end_time - SECONDS < 5 )); then end_time=$((SECONDS + 10)) fi if [ -f "$sv_gametimer_file" ]; then local video_time=$(cat "$sv_gametimer_file") if [[ "$video_time" =~ ^[0-9]+$ ]]; then countdown=$video_time end_time=$((SECONDS + countdown)) rm "$sv_gametimer_file" 2>/dev/null samdebug "Timer synced to video: $countdown seconds" video_synced="yes" fi fi fi local current_rem=$((end_time - SECONDS)) if (( current_rem < 0 )); then current_rem=0; fi if [ "$video_synced" == "no" ]; then echo -ne "Loading video...\033[0K\r" else echo -ne "Next in ${current_rem} seconds...\033[0K\r" fi read -s -t 1 -n 1 key if [[ $? -eq 0 ]]; then case "$key" in n|N) echo; return ;; p|P) echo SAM_ACTION="previous" return ;; m|M) toggle_mute ;; esac fi done # Reset the trap to its default behavior after the countdown finishes normally. # trap - INT } # Pick a random core function next_core() { # next_core (core) if [[ -n "$cfgcore_configpath" ]]; then configpath="$cfgcore_configpath" else configpath="/media/fat/config/" fi if [ "${samvideo}" == "yes" ]; then load_samvideo if [ $? -ne 0 ]; then sv_nextcore="samvideo" && return; fi fi if [[ ! ${corelist[*]} ]]; then echo "ERROR: FATAL - List of cores is empty." echo "Using default corelist" declare -ga corelist=("${corelistall[@]}") samdebug "Corelist is now ${corelist[*]}" fi # Pick a core if no corename was supplied as argument (eg "MiSTer_SAM_on.sh psx") if [ -z "${1}" ]; then corelist_update #samdebug "corelist: ${corelist[@]}" if [ "$samvideo" == "yes" ] && [ "$samvideo_tvc" == "yes" ]; then nextcore=$(cat /tmp/.SAM_tmp/sv_core) else pick_core fi else # Single Mode: Use the provided argument nextcore="${1}" fi check_list "${nextcore}" if [ $? -ne 0 ]; then samdebug "check_list function returned an error." return 1 fi # Check if new roms got added if [[ "$check_for_new_games" == "Yes" ]]; then check_list_update ${nextcore} fi pick_rom declare -g romloadfails=0 local rom_is_valid=false while [ ${romloadfails} -lt ${coreretries} ]; do # Call check_rom. It returns 0 on success. if check_rom "${nextcore}"; then # The ROM is valid! Mark as successful and break out of the loop. rom_is_valid=true break fi # If we are here, the ROM was invalid. Increment the failure counter. romloadfails=$((romloadfails + 1)) # If we still have retries left, pick a new ROM to test on the next loop iteration. # The check_rom function may have rebuilt the list, so we need to pick again. if [ ${romloadfails} -lt ${coreretries} ]; then samdebug "ROM check failed. Picking a new ROM to try again (${romloadfails}/${coreretries})..." pick_rom fi done # After the loop, check if we ever found a valid ROM. if [ "$rom_is_valid" = "false" ]; then # All retries have been exhausted. No valid ROM was found. return 1 fi load_core "${nextcore}" "${rompath}" "${romname%.*}" # Capture the exit code from load_core and return it. # This passes the success/failure signal up to the main loop. return $? } function load_samvideo() { sv_loadcounter=$((sv_loadcounter + 1)) #Load the actual rom (or play a video) if [ "${samvideo_freq}" == "only" ]; then samvideo_play & return 1 elif [ "${samvideo_freq}" == "core" ]; then samdebug "samvideo load core counter is now $sv_loadcounter" if ((sv_loadcounter % ${#corelist[@]} == 0)); then samvideo_play & sv_loadcounter=0 return 1 fi sv_nextcore="" return 0 elif [ "${samvideo_freq}" == "alternate" ]; then if ((sv_loadcounter % 2 == 1)); then samvideo_play & return 1 else sv_nextcore="" return 0 fi fi } # Don't repeat same core twice function corelist_update() { #Single Core Mode if [ -s "${corelistfile}.single" ]; then unset corelist mapfile -t corelist < "${corelistfile}.single" rm "${corelistfile}.single" "${corelistfile}" > /dev/null 2>&1 elif [ -s "${corelistfile}" ]; then unset corelist mapfile -t corelist < "${corelistfile}" rm "${corelistfile}" fi # Resynchronize corelisttmp with the potentially updated corelist declare -A valid_cores_map for core in "${corelist[@]}"; do valid_cores_map["$core"]=1 done local updated_corelisttmp=() for tmp_core in "${corelisttmp[@]}"; do if [[ -n "${valid_cores_map["$tmp_core"]}" ]]; then updated_corelisttmp+=("$tmp_core") fi done corelisttmp=("${updated_corelisttmp[@]}") if [[ "${disablecoredel}" == "0" ]]; then delete_from_corelist "$nextcore" tmp fi if [ ${#corelisttmp[@]} -eq 0 ]; then declare -ga corelisttmp=("${corelist[@]}") fi if [[ ! "${corelisttmp[*]}" ]]; then corelisttmp=("${corelist[@]}") fi } # ────────────────────────────────────────────────────────────────────────────── # Main core picker # ────────────────────────────────────────────────────────────────────────────── function pick_core() { # SAFETY: If in SINGLE mode, Force Target Core if [ "$SAM_MODE" == "SINGLE" ] && [ -n "$SAM_TARGET_CORE" ]; then nextcore="$SAM_TARGET_CORE" samdebug "pick_core: Single mode active. Forcing core: $nextcore" return fi # Check if this is a first run by seeing if any gamelists exist. local gamelist_count gamelist_count=$(find "$gamelistpath" -maxdepth 1 -type f -name '*_gamelist.txt' | wc -l) if [ "$gamelist_count" -eq 0 ]; then samdebug "First run detected (no gamelists). Prioritizing Arcade core." # As a safety check, ensure 'arcade' is an available core. if [[ " ${corelistall[*]} " =~ " arcade " ]]; then nextcore="arcade" samdebug "Selected initial core: arcade" create_all_gamelists return # Exit the function immediately else samdebug "Arcade core not available. Falling back to normal selection." fi fi # If it's not a first run, proceed with the standard mode selection. if [[ "$coreweight" == "yes" ]]; then pick_core_weighted elif [[ "$samvideo" == "yes" ]]; then pick_core_samvideo "$1" else pick_core_standard fi # Fallback in case a selection function failed if [[ -z "$nextcore" ]]; then samdebug "nextcore empty. Using arcade core as fallback." nextcore="arcade" fi } # 1) Uniform random selection function pick_core_standard() { nextcore=$(printf "%s\n" "${corelisttmp[@]}" \ | shuf --random-source=/dev/urandom -n1) samdebug "Picked core (standard): $nextcore" } # 2) SAM-video mode (Weighted by _tvc.json) declare -A SAMVC # tvc counts per core SAMVTOTAL=0 # sum of all counts SAMVIDEO_INIT_SENTINEL="/tmp/.SAM_tmp/samvideo_init" function init_core_samvideo() { local arr_name=$1 local core cnt tvc local -n arr_ref=$arr_name # always (re)load counts into SAMVC & SAMVTOTAL SAMVTOTAL=0 if [[ -f "$core_count_file" ]]; then while IFS="=" read -r core cnt; do if [[ "$core" == total_count ]]; then SAMVTOTAL=$cnt else SAMVC["$core"]=$cnt fi done < "$core_count_file" else for core in "${arr_ref[@]}"; do local tvc_suffix="_tvc.json" if [ "${samvideo_tvc_cdi}" == "yes" ]; then tvc_suffix="_tvc_vcd.json" fi tvc="${mrsampath}/tvc/${core}${tvc_suffix}" cnt=0 [[ -f "$tvc" ]] && cnt=$(jq -r 'keys|length' "$tvc" 2>/dev/null || echo 0) SAMVC["$core"]=$cnt (( SAMVTOTAL += cnt )) done mkdir -p "$(dirname "$core_count_file")" : > "$core_count_file" for core in "${!SAMVC[@]}"; do echo "$core=${SAMVC[$core]}" >> "$core_count_file" done echo "total_count=$SAMVTOTAL" >> "$core_count_file" fi # print table only once, guarded by sentinel if [[ ! -f "$SAMVIDEO_INIT_SENTINEL" ]]; then echo -e "\nCore TVC-Entries Percent" printf '%.0s─' {1..34}; echo for core in "${!SAMVC[@]}"; do cnt=${SAMVC[$core]} if (( SAMVTOTAL > 0 )); then pct=$(awk "BEGIN{printf \"%.2f\", ($cnt*100)/$SAMVTOTAL}") else pct="0.00" fi printf "%-8s %10d %6s%%\n" "$core" "$cnt" "$pct" done | sort -k2 -nr echo "─────────────────────────────────────────────────────────────────────────────" # ensure sentinel directory exists and create sentinel mkdir -p "$(dirname "$SAMVIDEO_INIT_SENTINEL")" touch "$SAMVIDEO_INIT_SENTINEL" fi } function pick_core_samvideo() { local arr_name=$1 local -n array=$arr_name init_core_samvideo "$arr_name" # now do the weighted pick nextcore=$(pick_weighted_random SAMVC "$SAMVTOTAL") [[ -z "$nextcore" ]] && nextcore="${array[0]}" # debug likelihood local w=${SAMVC[$nextcore]:-0} local likelihood likelihood=$(awk "BEGIN{printf \"%.2f\", ($w*100)/$SAMVTOTAL}") samdebug "Picked core (samvideo): $nextcore (likelihood: ${likelihood}%)" } # 3) Core-weight mode (weighted by games per core) declare -A COREWC # raw game counts per core declare -A COREP # mirror of COREWC for pick_weighted_random TOTAL_GAME_COUNT=0 COREWEIGHT_INITIALIZED=0 function init_core_weighted() { # only run once (( COREWEIGHT_INITIALIZED )) && return COREWEIGHT_INITIALIZED=1 echo -n "Please wait while calculating core weights..." # a) ensure every core has a gamelist for c in "${corelist[@]}"; do f="${gamelistpathtmp}/${c}_gamelist.txt" [[ -f "$f" ]] || check_list "$c" >/dev/null done # b) build raw counts & total TOTAL_GAME_COUNT=0 for c in "${corelist[@]}"; do f="${gamelistpathtmp}/${c}_gamelist.txt" if [[ -f "$f" ]]; then COREWC["$c"]=$(wc -l < "$f") (( TOTAL_GAME_COUNT += COREWC["$c"] )) fi done # c) fallback to equal if truly empty if (( TOTAL_GAME_COUNT == 0 )); then for c in "${corelist[@]}"; do COREWC["$c"]=1 done TOTAL_GAME_COUNT=${#corelist[@]} fi # d) mirror COREWC → COREP for picking for c in "${!COREWC[@]}"; do COREP["$c"]=${COREWC["$c"]} done # e) print table of counts & percentages echo -e "\nCore Games Percent" printf '%.0s─' {1..28}; echo for core in "${!COREWC[@]}"; do cnt=${COREWC[$core]} pct=$(awk "BEGIN{printf \"%.2f\", ($cnt*100)/${TOTAL_GAME_COUNT}}") printf "%-8s %6d %6s%%\n" "$core" "$cnt" "$pct" done | sort -k2 -nr echo " Done." } function pick_core_weighted() { init_core_weighted # fast pick from prebuilt COREP/TOTAL_GAME_COUNT nextcore=$(pick_weighted_random COREP "$TOTAL_GAME_COUNT") [[ -z "$nextcore" ]] && nextcore="${corelist[0]}" # debug likelihood local w=${COREP[$nextcore]} local likelihood=$(awk "BEGIN{printf \"%.2f\", ($w*100)/$TOTAL_GAME_COUNT}") samdebug "Picked core (coreweight): $nextcore (likelihood: ${likelihood}%)" } function pick_weighted_random() { local -n weights=$1 local total=$2 (( total<=0 )) && echo "" && return local pick sum=0 pick=$(shuf --random-source=/dev/urandom -i 1-"$total" -n1) for key in "${!weights[@]}"; do (( sum += weights[$key] )) if (( pick <= sum )); then echo "$key" return fi done echo "" } # ────────────────────────────────────────────────────────────────────────────── # Game Picker and Checker # ────────────────────────────────────────────────────────────────────────────── function pick_rom() { # 1. Handle special, non-random cases first. if [[ "$m82" == "yes" ]]; then # M82 mode is deterministic; it always takes the first line. rompath="$(head -n 1 "${gamelistpathtmp}/nes_gamelist.txt")" return fi if [[ "$samvideo" == "yes" ]] && [[ "$samvideo_tvc" == "yes" ]] && [[ -f /tmp/.SAM_tmp/sv_gamename ]]; then local sv_gamelist # Declare variable local filtered_list="${gamelistpathtmp}/${nextcore}_gamelist.txt" local master_list="${gamelistpath}/${nextcore}_gamelist.txt" if [ ! -f "${filtered_list}" ]; then samdebug "Filtered list not found for samvideo, generating..." filter_list "${nextcore}" # The filter didn't produce results if [ $? -ne 0 ]; then samdebug "filter_list failed. Falling back to master list for samvideo." sv_gamelist="${master_list}" else samdebug "filter_list succeeded." sv_gamelist="${filtered_list}" fi else samdebug "Filtered list already exists." sv_gamelist="${filtered_list}" fi # samvideo mode tries to find a specific game matching a commercial. local specific_game local search_term=$(cat /tmp/.SAM_tmp/sv_gamename) samdebug "Searching for game matching string: $search_term" specific_game="$(grep -if /tmp/.SAM_tmp/sv_gamename "$sv_gamelist" | grep -iv "VGM\|MSU\|Disc 2\|Sega CD 32X" | head -n 1)" if [[ -z "${specific_game}" ]]; then samdebug "Match not found in session list. Checking master list..." specific_game="$(grep -if /tmp/.SAM_tmp/sv_gamename "${gamelistpath}/${nextcore}_gamelist.txt" | grep -iv "VGM\|MSU\|Disc 2\|Sega CD 32X" | head -n 1)" fi if [[ -n "${specific_game}" ]]; then rompath="${specific_game}" return # Exit successfully if we found the specific game. fi echo "Could not find matching game for commercial. Picking a random game instead." fi # 2. Default Action: If no special game modes applied, use the random picker. rompath=$(pick_random_game "${nextcore}") || true # 3. Final validation. if [[ -z "$rompath" ]]; then echo "Could not pick a game for ${nextcore}. Check for empty gamelists or overly restrictive filters." fi } function check_rom(){ local core="$1" # Use the passed argument for consistency if [ -z "${rompath}" ]; then echo "ERROR: rompath is empty for core '${core}'. Cannot check ROM." >&2 return 1 fi # Special core checks to ensure gamelists weren't built by samindex if [[ "$core" == "amiga" ]]; then gamelist_src="${gamelistpath}/amiga_gamelist.txt" # Make sure samindex didn't build a faulty amiga list grep -q "WheelDriverAkiko.adf" "$gamelist_src" && build_amiga_list return 0 fi if [[ "$core" == "ao486" || "$core" == "x68k" ]]; then # If the gamelist contains anything other than .mgl files, it's corrupt. grep -qv "\.mgl$" "${gamelistpath}/${core}_gamelist.txt" && build_mgl_list "${core}" return 0 fi # Make sure file exists since we're reading from a static list if [[ "${rompath,,}" != *.zip* ]]; then if [ ! -f "${rompath}" ]; then echo "ERROR: File not found - ${rompath}" rm -f "${gamelistpath}/${core}_gamelist.txt" ensure_list "${core}" "${gamelistpath}" return 1 fi else local zipfile="$(echo "$rompath" | awk -F".zip" '{print $1}' | sed -e 's/$/.zip/')" if [ ! -f "${zipfile}" ]; then echo "ERROR: File not found - ${zipfile}" rm -f "${gamelistpath}/${core}_gamelist.txt" ensure_list "${core}" "${gamelistpath}" return 1 fi fi romname=$(basename "${rompath}") # Make sure we have a valid extension as well local extension="${rompath##*.}" local extlist="${CORE_EXT[${core}]//,/ }" # Use the passed argument if [[ -n "${CORE_EXT[$core]}" ]]; then local extension="${rompath##*.}" local extlist="${CORE_EXT[${core}]//,/ }" if [[ "$extlist" != *"$extension"* ]]; then samdebug "Wrong extension found: '${extension^^}' for core: ${core} rom: ${rompath}" ensure_list "${core}" "${gamelistpath}" & # Rebuild in background return 1 fi fi # Check for RBF existence for Arcade/STV MRAs if [[ "${core}" == "arcade" || "${core}" == "stv" ]] && [[ "${rompath,,}" == *.mra ]]; then local rbf_tag # Extract content between and , removing whitespace rbf_tag=$(grep -i "" "${rompath}" | head -n 1 | sed -e 's/.*\(.*\)<\/rbf>.*/\1/' | tr -d '[:space:]') if [[ -n "${rbf_tag}" ]]; then local checks_dir="${misterpath}/${CORE_PATH_RBF[${core}]}" local rbf_found=0 # Check for RBF file (exact match or with timestamp suffix) if [ -n "$(find "${checks_dir}" -iname "${rbf_tag}*.rbf" -print -quit 2>/dev/null)" ]; then rbf_found=1 fi if [[ $rbf_found -eq 0 ]]; then samdebug "ERROR: RBF '${rbf_tag}' not found for MRA '${rompath}'. validation failed." # Remove from the current session list so we don't pick it again immediately local session_list="${gamelistpathtmp}/${core}_gamelist.txt" if [ -f "${session_list}" ]; then grep -F -v "${rompath}" "${session_list}" > "${session_list}.tmp" && mv "${session_list}.tmp" "${session_list}" fi # Also remove from the MASTER list so it doesn't come back next session. # Rebuilding the list wouldn't help here because the MRA file itself still exists. local master_list="${gamelistpath}/${core}_gamelist.txt" if [ -f "${master_list}" ]; then samdebug "Pruning '${rompath}' from master list due to missing RBF." grep -F -v "${rompath}" "${master_list}" > "${master_list}.tmp" && mv "${master_list}.tmp" "${master_list}" fi return 1 fi fi fi # If all checks pass, return 0 for success return 0 } # ────────────────────────────────────────────────────────────────────────────── # Gamelist Builder # ────────────────────────────────────────────────────────────────────────────── function build_mra_list() { # Accept core and destination directory arguments local core_type="$1" local dest_dir="${2:-$gamelistpath}" local output_file="${dest_dir}/${core_type}_gamelist.txt" local mra_path # 1. Determine the correct search path based on the core. case "${core_type}" in "stv") mra_path="/media/fat/_Arcade/_ST-V" ;; "arcade") mra_path="/media/fat/_Arcade" ;; *) samdebug "ERROR: build_mra_list called with unsupported core '${core_type}'" return 1 ;; esac # 2. Check if the search directory exists. if [ ! -d "${mra_path}" ]; then echo "The path ${mra_path} does not exist!" : > "${output_file}" # Create empty list to prevent re-running return 0 fi # Check if the directory contains any MRA files before running a full find. if ! find "${mra_path}" -type f -iname "*.mra" -print -quit | grep -q .; then echo "The path ${mra_path} contains no MRA files!" : > "${output_file}" # Create empty list return 0 fi # 3. Build the list directly into the destination file using find. find "${mra_path}" -not -path '*/.*' -type f -iname "*.mra" > "${output_file}" samdebug "Created ${core_type} MRA gamelist in '${dest_dir}'." sync "${output_file}" } function build_mgl_list() { # Accept core and destination directory arguments local core_type="$1" local dest_dir="${2:-$gamelistpath}" # Define paths, making the output file dynamic local search_paths local output_file="${dest_dir}/${core_type}_gamelist.txt" local game_count local existing_paths=() # Determine which directories to search based on the core case "${core_type}" in "ao486") search_paths=( "/media/fat/_DOS Games" "/media/fat/_Computer/_DOS Games" "/media/fat/games/ao486/_DOS" "/media/usb0/games/ao486/_DOS" ) ;; "x68k") search_paths=( "/media/fat/_X68000 Games" "/media/fat/_Computer/_X68000 Games" ) ;; "mgls") IFS=',' read -ra search_paths <<< "${mgls_dirs}" ;; *) samdebug "No MGL search path defined for ${core_type}." return 1 ;; esac # Collect only the search paths that actually exist for path in "${search_paths[@]}"; do [ -d "$path" ] && existing_paths+=("$path") done # If no valid search directories were found, create an empty list and exit if [ ${#existing_paths[@]} -eq 0 ]; then samdebug "No valid MGL search directories found for ${core_type}." : > "${output_file}" # Create empty list to prevent retry loops return 0 fi # Run find on existing paths and write directly to the destination file find "${existing_paths[@]}" -type f -iname '*.mgl' 2>/dev/null > "${output_file}" # If the resulting list is empty, disable the core if [ ! -s "${output_file}" ]; then samdebug "No .mgl files found for ${core_type}—disabling core." delete_from_corelist "${core_type}" delete_from_corelist "${core_type}" tmp return 1 fi game_count=$(wc -l < "${output_file}") samdebug "Created ${core_type} gamelist in '${dest_dir}' with ${game_count} entries." } function build_amiga_list() { # Accept core and destination directory arguments for consistency local dest_dir="${2:-$gamelistpath}" # Define paths; the output file is now dynamic based on dest_dir local demos_file="${amigapath}/listings/demos.txt" local games_file="${amigapath}/listings/games.txt" local output_file="${dest_dir}/amiga_gamelist.txt" # Check if the source 'games.txt' exists if [ ! -f "${games_file}" ]; then echo "ERROR: Can't find Amiga games.txt file at '${games_file}'" # Create an empty file at the destination to prevent rebuild attempts : > "${output_file}" return 1 fi # Start with a fresh, empty list directly at the final destination > "${output_file}" # Append demos to the output file if selected if [[ "${amigaselect}" == "demos" ]] || [[ "${amigaselect}" == "all" ]]; then if [ -f "${demos_file}" ]; then sed 's/^/Demo: /' "${demos_file}" >> "${output_file}" else samdebug "Demos list not found at ${demos_file}" fi fi # Append games to the output file if selected if [[ "${amigaselect}" == "games" ]] || [[ "${amigaselect}" == "all" ]]; then cat "${games_file}" >> "${output_file}" fi # Verify that the final list is not empty if [ ! -s "${output_file}" ]; then samdebug "No Amiga games or demos matched current selection (${amigaselect})." return 1 fi local total_entries total_entries="$(wc -l < "${output_file}")" samdebug "${total_entries} Amiga Games and/or Demos found for list in '${dest_dir}'." } # General Romfinder function build_gamelist() { local core="$1" local outdir="${2:-$gamelistpath}" local file rc local is_initial_build=0 # Determine if this is an "initial" build by checking the output path. # This makes the function's behavior dependent on its direct inputs. if [[ "$outdir" == "$gamelistpath" ]]; then is_initial_build=1 fi # 1. PRE-FLIGHT CHECK: Only for initial builds, skip if another indexer is running. if (( is_initial_build )) && ps | grep -q '[s]amindex'; then samdebug "samindex already in flight; skipping full build for ${core}" return 0 fi samdebug "Building gamelist for ${core} in ${outdir}" # 2. SETUP: Ensure output directory exists and let the filesystem settle. mkdir -p "$outdir" sync "$outdir" sleep 1 # 3. EXECUTION: Run the indexer to generate the list. # The tool is run twice to work around a potential issue where it misses files on the first pass. "${mrsampath}/samindex" -q -s "$core" -o "$outdir" "${mrsampath}/samindex" -q -s "$core" -o "$outdir" rc=$? # 4. POST-PROCESSING: Handle results and cleanup. file="${outdir}/${core}_gamelist.txt" # Only perform special error handling and seeding for initial builds. if (( is_initial_build )); then # On initial build, an exit code > 1 means "no games found". if (( rc > 1 )); then delete_from_corelist "$core" if [ -n "$core" ]; then echo "Can't find games for ${CORE_PRETTY[$core]}" else echo "Can't find games for (unknown core)" fi samdebug "build_gamelist returned code $rc for $core" return 1 # Return an error fi mkdir -p "${gamelistpathtmp}" cp "${file}" "${gamelistpathtmp}/${core}_gamelist.txt" 2>/dev/null fi # Always sort and de-duplicate the final output file, regardless of build type. if [[ -f "$file" ]]; then sort -u "$file" -o "$file" fi return 0 } # Helper to build a gamelist for a core at a specific destination. # Arg 1: Core type (e.g., "nes") # Arg 2: Destination directory (e.g., "/path/to/gamelists") function ensure_list() { local core_type="$1" local dest_dir="${2:-$gamelistpath}" local list_file="${dest_dir}/${core_type}_gamelist.txt" local build_func # If the list already exists with content, we're done. if [ -s "${list_file}" ]; then return 0 fi samdebug "Gamelist for '${core_type}' not found in '${dest_dir}'. Building..." # Determine which builder to use case "${core_type}" in "arcade"|"stv") build_func="build_mra_list" ;; "ao486"|"x68k"|"mgls") build_func="build_mgl_list" ;; "amiga") build_func="build_amiga_list" ;; *) build_func="build_gamelist" ;; esac # IMPORTANT: Assumes your build functions accept the destination path as an argument. # e.g., build_gamelist "nes" "/path/to/gamelists/comp" ${build_func} "${core_type}" "${dest_dir}" # Final check if [ ! -s "${list_file}" ]; then samdebug "ERROR: Failed to create or find games for '${core_type}' in '${dest_dir}'." >&2 return 1 fi return 0 } # Checks and prepares gamelists for a core. # Arg 1: Core type (e.g., "nes") # Arg 2: [mode] - Optional, e.g., "comp" to build a competitive list. function check_list() { local core_type="$1" local mode="$2" local session_list="${gamelistpathtmp}/${core_type}_gamelist.txt" # 1. Ensure we have Master game list if it doesn't exist. Exit if it fails. ensure_list "${core_type}" "${gamelistpath}" || return 1 # 2. Create "comparison" game lists to /tmp to check if we have new games if [[ "${mode}" == "comp" ]]; then local comp_dir="${gamelistpath}/comp" mkdir -p "${comp_dir}" # Ensure the 'comp' subdirectory exists ensure_list "${core_type}" "${comp_dir}" fi # 3. Handle special session lists (GOAT, M82, etc.) if [ "${sam_goat_list}" == "yes" ] && [ ! -s "${gamelistpathtmp}/${1}_gamelist.txt" ]; then build_goat_lists return fi # m82 populate lists if [ "${m82}" == "yes" ]; then # --- Find M82 BIOS (once per session) --- if [[ -z "$m82_bios_path" ]]; then echo -n "M82 mode active. Finding M82 bios..." # Search the master NES list for the BIOS file and store its path globally declare -g m82_bios_path m82_bios_path="$(fgrep -i "m82 game" "$gamelistpath/nes_gamelist.txt" | head -n 1)" echo "Success." samdebug "m82 bios found at: $m82_bios_path" fi # --- Validate BIOS was found --- if [[ -z "$m82_bios_path" ]]; then echo "Error: No suitable M82 BIOS found in your nes folder. The file should be named 'M82 Game[...].nes'" exit 1 fi # --- Create the special M82 session list if it doesn't exist --- if [ ! -s "${session_list}" ]; then samdebug "Creating M82 game list from m82_list.txt" # Read a predefined list of game titles and build a new gamelist while IFS= read -r line; do echo "$m82_bios_path" fgrep "$line" "${gamelistpath}/nes_gamelist.txt" | head -n 1 done < "${mrsampath}/SAM_Gamelists/m82_list.txt" > "${session_list}" samdebug "Found the following games: \n$(cat "${session_list}" | grep -iv m82)" samdebug "Found $(cat "${session_list}" | grep -iv m82 | wc -l) games" fi # --- Handle game skipping --- # If a button was pushed to skip the current game, remove it from the list if [ "${update_done}" -eq 1 ]; then sed -i '1d' "${session_list}" fi sync # --- Finalize M82 state for this cycle --- gametimer="21" update_done=0 return fi # 4. Default action: Copy the master list to the temp session directory if no # special session list (like M82) was created. if [ ! -s "${session_list}" ]; then cp "${gamelistpath}/${core_type}_gamelist.txt" "${session_list}" 2>/dev/null fi filter_list "${nextcore}" if [ $? -ne 0 ]; then samdebug "filter_list encountered an error" fi return 0 } # Create all gamelists in the background function create_all_gamelists() { # This function now only runs once per script invocation. if (( gamelists_created )); then return 0 fi gamelists_created=1 # Run the entire process in a subshell in the background (&) ( # Wait a moment before starting the background build to keep resources free. sleep 15 samdebug "Starting background build of standard gamelists..." for c in "${corelist[@]}"; do # Only process non-special cores if [[ ! " ${special_cores[*]} " =~ " ${c} " ]]; then # Use the dispatcher to handle the check and call the correct builder. # This is cleaner and respects your modular design. ensure_list "${c}" "${gamelistpath}" fi done samdebug "Background build process complete." ) & } function schedule_gamelist_updates() { local core [[ "$check_for_new_games" != "Yes" ]] && return for core in ${corelist//,/ }; do check_list_update "$core" done } function check_list_update() { [[ "$check_for_new_games" != "Yes" ]] && return local core="$1" local orig="${gamelistpath}/${core}_gamelist.txt" local compdir="${gamelistpathtmp}/comp" local comp="${compdir}/${core}_gamelist.txt" # ── only run this check once per core, per session ── local flag_dir="${gamelistpathtmp}/.checked" mkdir -p "$flag_dir" local flag_file="$flag_dir/$core" if [ -e "$flag_file" ]; then return fi touch "$flag_file" # Skip for special modes like M82 that have their own list logic if [[ "$m82" == "yes" ]]; then return 0 fi ( mkdir -p "$compdir" #wait before building comparison lists sleep 10 ensure_list "$core" "$compdir" # Now, compare the sorted original list with the new sorted comparison list if ! diff -q <(sort "$orig") <(sort "$comp") &>/dev/null; then samdebug "[${core}] Gamelist has changed, updating master list…" # Log up to 10 lines of differences for debugging samdebug "[${core}] DIFF:" comm -3 <(sort "$orig") <(sort "$comp") | head -n10 | \ while read -r ln; do samdebug " $ln"; done # Overwrite the original list with the sorted new one sort "$comp" -o "$orig" samdebug "[${core}] Gamelist updated." else samdebug "[${core}] No changes detected in ${core} gamelist." fi ) & } function build_goat_lists() { local goat_flag="/tmp/.SAM_tmp/goatmode.ready" local goat_list_path="${gamelistpath}/sam_goat_list.txt" echo "SAM GOAT Mode active" # Already built this session? [[ -f "$goat_flag" ]] && return # Ensure working dir mkdir -p "${gamelistpathtmp}" /tmp/.SAM_tmp # Download master list if missing if [[ ! -f "$goat_list_path" ]]; then samdebug "Downloading GOAT master list..." get_samstuff .MiSTer_SAM/SAM_Gamelists/sam_goat_list.txt "$gamelistpath" fi # Parse master list into per-core tmp files local current_core="" while IFS= read -r line; do if [[ "$line" =~ ^\[(.+)\]$ ]]; then current_core="${BASH_REMATCH[1],,}" [[ ! -f "${gamelistpath}/${current_core}_gamelist.txt" ]] && build_gamelist "$current_core" elif [[ -n "$current_core" ]]; then fgrep -i -m1 "$line" "${gamelistpath}/${current_core}_gamelist.txt" \ >> "${gamelistpathtmp}/${current_core}_gamelist.txt" fi done < "$goat_list_path" # Gather cores with entries readarray -t corelist < <( find "${gamelistpathtmp}" -name "*_gamelist.txt" \ -exec basename {} \; | cut -d '_' -f1 ) printf "%s\n" "${corelist[@]}" > "${corelistfile}" # Update INI corelist if changed local newvalue; newvalue="$(IFS=,; echo "${corelist[*]}")" if ! grep -q "^corelist=\"$newvalue\"" "$samini_file"; then samini_mod corelist "$newvalue" fi # Enable GOAT flag if ! grep -q '^sam_goat_list="yes"' "$samini_file"; then samini_mod sam_goat_list yes fi # Mark as built touch "$goat_flag" } function build_m82_list() { [ ! -d "/tmp/.SAM_List" ] && mkdir /tmp/.SAM_List/ [ ! -d "/tmp/.SAM_tmp" ] && mkdir /tmp/.SAM_tmp/ if [ ! -f "${gamelistpath}"/nes_gamelist.txt ]; then samdebug "Creating NES gamelist" ${mrsampath}/samindex -q -s "nes" -o "${gamelistpath}" if [ $? -gt 1 ]; then echo "Error: NES gamelist missing. Make sure you have NES games." fi fi if [ -f "${gamelistpathtmp}"/nes_gamelist.txt ]; then rm "${gamelistpathtmp}"/nes_gamelist.txt fi local m82_list_path="${gamelistpath}"/m82_list.txt # Check if the M82 list file exists if [ ! -f "$m82_list_path" ]; then echo "Error: The M82 list file ($m82_list_path) does not exist. Updating SAM now. Please try again." repository_url="https://github.com/mrchrisster/MiSTer_SAM" get_samstuff .MiSTer_SAM/SAM_Gamelists/m82_list.txt "${gamelistpath}" fi printf "%s\n" nes > "${corelistfile}" if [[ "$m82_muted" == "yes" ]]; then mute="global" else mute="no" only_unmute_if_needed fi gametimer="21" listenjoy=no } # ────────────────────────────────────────────────────────────────────────────── # Core Loader # ────────────────────────────────────────────────────────────────────────────── # This handles list building, filtering, cleaning, random selection, and the 'norepeat' feature. function pick_random_game() { local core_type=${1} local master_list="${gamelistpath}/${core_type}_gamelist.txt" local session_list="${gamelistpathtmp}/${core_type}_gamelist.txt" # 3. Apply filter if [ ! -s "${session_list}" ]; then cp -f "${master_list}" "${session_list}" filter_list "${core_type}" # Remove any blank or whitespace-only lines sed -i '/^[[:space:]]*$/d' "${session_list}" # If filtering resulted in an empty list, we must exit. if [ ! -s "${session_list}" ]; then samdebug "Warning: Filters for '${core_type}' produced an empty list. No games to play." >&2 return 1 fi fi # 4. Extra validation before selection if ! grep -q '[^[:space:]]' "${session_list}"; then samdebug "Session list for '${core_type}' contains no valid entries." return 1 fi # 5. Pick a random line from the now-filtered session list local chosen_path chosen_path="$(shuf --random-source=/dev/urandom --head-count=1 "${session_list}")" # Sanitize the path to remove any control characters chosen_path=$(echo "$chosen_path" | tr -d '[:cntrl:]') # 6. Final check: the chosen path must be a real file (unless it's Amiga) if [[ "${core_type}" == "arcade" || "${core_type}" == "stv" ]]; then if [ ! -f "${chosen_path}" ]; then samdebug "ERROR: MRA file not found after pick and sanitize: '${chosen_path}'" samdebug "The gamelist for '${core_type}' appears out of date. Triggering rebuild." # Delete the stale lists rm -f "${master_list}" "${session_list}" # Rebuild the master list immediately so we can try again on the next pass ensure_list "${core_type}" "${gamelistpath}" # RECURSIVE CALL: Try to pick again immediately from the fresh list pick_random_game "${core_type}" return $? fi fi # 7. If 'norepeat' is enabled, remove the chosen game from the session list if [[ "${norepeat}" == "yes" ]]; then samdebug "(${core_type}) Removing from list for norepeat: ${chosen_path}" awk -vLine="$chosen_path" '!index($0,Line)' "${session_list}" > "${tmpfile}" && mv -f "${tmpfile}" "${session_list}" fi # Output the chosen path so the caller can capture it echo "${chosen_path}" } function load_core() { # load_core core [/path/to/rom] [name_of_rom] local core=${1} local rompath_arg=${2} local romname_arg=${3} # --- Local variables for unified logic --- local gamename tty_corename launch_cmd streamtitle mute_target rompath romname post_launch_hook # This is the primary router for all core-specific logic. case "${core}" in "arcade"|"stv") ### MRA Core Loader (Arcade, ST-V) ### # --- Prerequisite Check --- if [[ -n "$cfgarcade_configpath" ]]; then configpath="$cfgarcade_configpath" fi # --- End Prerequisite Check --- rompath="${rompath_arg}" rompath=$(echo "$rompath" | tr -d '[:cntrl:]') if [ ! -f "${rompath}" ]; then echo "ERROR: MRA file not found after pick and sanitize: '${rompath}'" >&2 return 1 fi gamename="$(basename "${rompath//.mra/}")" tty_corename=$(grep "" "${rompath}" | sed -e 's///' -e 's/<\/setname>//' | tr -cd '[:alnum:]') mute_target="${tty_corename:-$gamename}" launch_cmd="load_core ${rompath}" ;; "ao486") ### ao486 MGL Loader ### # --- Prerequisite Check --- if [[ -z "$mgl_check_status_ao486" ]]; then samdebug "Performing one-time check for ao486 MGL files..." local dir1="/media/fat/_DOS Games" local dir2="/media/fat/_Computer/_DOS Games" local dir3="/media/fat/games/ao486/_DOS" local dir4="/media/usb0/games/ao486/_DOS" if [ -d "$dir1" ] || [ -d "$dir2" ] || [ -d "$dir3" ] || [ -d "$dir4" ]; then local count1=$(find "$dir1" -type f -iname '*.mgl' 2>/dev/null | wc -l) local count2=$(find "$dir2" -type f -iname '*.mgl' 2>/dev/null | wc -l) local count3=$(find "$dir3" -type f -iname '*.mgl' 2>/dev/null | wc -l) local count4=$(find "$dir4" -type f -iname '*.mgl' 2>/dev/null | wc -l) [ "$((count1 + count2 + count3 + count4))" -gt 0 ] && mgl_check_status_ao486="pass" || mgl_check_status_ao486="fail" else mgl_check_status_ao486="fail" fi fi if [[ "$mgl_check_status_ao486" != "pass" ]]; then echo "ERROR - No ao486 MGL files found. Please install the 0Mhz collection." >&2 delete_from_corelist "ao486" return 1 fi # --- End Prerequisite Check --- rompath="${rompath_arg}" romname=$(basename "${rompath}") gamename="$(echo "${romname%.*}" | tr '_' ' ')" tty_corename="${core}" mute_target="${core}" launch_cmd="load_core ${rompath}" skipmessage_ao486 & ;; "x68k") ### x68k MGL Loader ### # --- Prerequisite Check --- if [[ -z "$mgl_check_status_x68k" ]]; then samdebug "Performing one-time check for x68k MGL files..." local dir1="/media/fat/_X68000 Games" local dir2="/media/fat/_Computer/_X68000 Games" if [ -d "$dir1" ] || [ -d "$dir2" ]; then local count1=$(find "$dir1" -type f -iname '*.mgl' 2>/dev/null | wc -l) local count2=$(find "$dir2" -type f -iname '*.mgl' 2>/dev/null | wc -l) [ "$((count1 + count2))" -gt 0 ] && mgl_check_status_x68k="pass" || mgl_check_status_x68k="fail" else mgl_check_status_x68k="fail" fi fi if [[ "$mgl_check_status_x68k" != "pass" ]]; then echo "ERROR - No x68k MGL files found. Please install the neon68k collection." >&2 delete_from_corelist "x68k" return 1 fi # --- End Prerequisite Check --- rompath="${rompath_arg}" romname=$(basename "${rompath}") gamename="$(echo "${romname%.*}" | tr '_' ' ')" tty_corename="${core}" mute_target="${core}" launch_cmd="load_core ${rompath}" ;; "mgls") rompath="${rompath_arg}" romname=$(basename "${rompath}") gamename="${romname%.*}" tty_corename=$(grep -oP '(?<=)[^<]+' "${rompath}" 2>/dev/null | xargs -r basename | cut -d. -f1) mute_target="${tty_corename}" [ -f "${rompath}" ] && cp "${rompath}" /tmp/SAM_Game.mgl launch_cmd="load_core ${rompath}" skipmessage "${core}" & ;; "amiga") ### Amiga (MegaAGS) Loader ### # --- Prerequisite Check --- if ! [ -f "${amigapath}/MegaAGS.hdf" ] && ! [ -f "${amigapath}/AmigaVision.hdf" ]; then echo "ERROR - MegaAGS/AmigaVision pack not found. Skipping core..." >&2 delete_from_corelist amiga return 1 fi # --- End Prerequisite Check --- gamename="${rompath_arg}" if [ -z "${gamename}" ]; then echo "ERROR: Failed to pick an Amiga game from the list." >&2 return 1 fi # Create the directory if it doesn't exist mkdir -p "${amigapath}/shared" local ags_boot_title="${gamename//Demo: /}" echo "${ags_boot_title}" > "${amigapath}/shared/ags_boot" tty_corename="Minimig" mute_target="Minimig" if [ -f "/media/fat/_Computer/Amiga.mgl" ]; then launch_cmd="load_core /media/fat/_Computer/Amiga.mgl" mute_target="Amiga" else launch_cmd="load_core ${amigacore}" fi ;; "amigacd32") ### Amiga CD32 Loader ### # --- Prerequisite Check --- if ! [ -f "/media/fat/_Console/Amiga CD32.mgl" ]; then echo "ERROR - /media/fat/_Console/Amiga CD32.mgl not found. Skipping core..." >&2 delete_from_corelist amigacd32 return 1 fi # --- End Prerequisite Check --- gamename="${romname_arg%.*}" mute_target="amigacd32" local CONFIG_FILE="/media/fat/config/AmigaCD32.cfg" if [ ! -f "$CONFIG_FILE" ]; then echo "ERROR - /media/fat/config/AmigaCD32.cfg not found. Skipping core." >&2 delete_from_corelist amigacd32; return 1 fi local new_path=$(echo "$rompath_arg" | sed -e 's|^/media||' -e 's|^/||') if [[ "$new_path" != ../* ]]; then new_path="../$new_path"; fi dd if=/dev/zero bs=1 count=108 seek=3100 of="$CONFIG_FILE" conv=notrunc &>/dev/null echo -n "$new_path" | dd of="$CONFIG_FILE" bs=1 seek=3100 conv=notrunc &>/dev/null launch_cmd="load_core /media/fat/_Console/Amiga CD32.mgl" post_launch_hook="(sleep 10; /media/fat/Scripts/.MiSTer_SAM/mbc raw_seq :30) &" ;; *) ### Default ROM-based MGL Loader (Consoles, NeoGeo, etc.) ### rompath="${rompath_arg}" romname="${romname_arg}" gamename="${romname_arg}" if [ "${core}" == "neogeo" ] && [ "${useneogeotitles}" == "yes" ]; then for e in "${!NEOGEO_PRETTY_ENGLISH[@]}"; do if [[ "$rompath" == *"$e"* ]]; then gamename="${NEOGEO_PRETTY_ENGLISH[$e]}"; break; fi done fi tty_corename="${TTY2OLED_PIC_NAME[${core}]}" mute_target="${CORE_LAUNCH[${core}]}" if [ -s /tmp/SAM_Game.mgl ]; then mv /tmp/SAM_Game.mgl /tmp/SAM_game.previous.mgl; fi { echo "" echo "${CORE_PATH_RBF[${core}]}/${MGL_CORE[${core}]}" echo "" [ -n "${MGL_SETNAME[${core}]}" ] && echo "${MGL_SETNAME[${core}]}" } >/tmp/SAM_Game.mgl launch_cmd="load_core /tmp/SAM_Game.mgl" skipmessage "${core}" & ;; esac # --- Common Execution Block --- [ -n "${mute_target}" ] && mute "${mute_target}" if [ "${bgm}" == "yes" ]; then streamtitle=$(awk -F"'" '/StreamTitle=/{title=$2} END{print title}' /tmp/bgm.log 2>/dev/null) fi echo -n "Starting now on the "; echo -ne "\e[4m${CORE_PRETTY[${core}]}\e[0m: "; echo -e "\e[1m${gamename}\e[0m" [[ -n "$streamtitle" ]] && echo -e "BGM playing: \e[1m${streamtitle}\e[0m" echo "$(date +%H:%M:%S) - ${core} - ${rompath:-$gamename}" >>/tmp/SAM_Games.log echo "${gamename} (${core})" >/tmp/SAM_Game.txt if [ "${ttyenable}" == "yes" ]; then local tty_gamename="${gamename}" if [[ "${ttyname_cleanup}" == "yes" ]]; then tty_gamename="$(echo "${tty_gamename}" | sed 's/ *([^)]*) *$//')"; fi if [[ -n "$streamtitle" ]]; then tty_gamename="${tty_gamename} - BGM: ${streamtitle}"; fi tty_currentinfo=( [core_pretty]="${CORE_PRETTY[${core}]}" [name]="${tty_gamename}" [core]="${tty_corename}" [date]=$EPOCHSECONDS [counter]=${gametimer} [name_scroll]="${tty_gamename:0:21}" [name_scroll_position]=0 [name_scroll_direction]=1 [update_pause]=${ttyupdate_pause} ) declare -p tty_currentinfo | sed 's/declare -A/declare -gA/' >"${tty_currentinfo_file}" write_to_TTY_cmd_pipe "display_info" & SECONDS=$((EPOCHSECONDS - tty_currentinfo[date])) fi # Time to launch this puppy timeout 1s sh -c "echo \"${launch_cmd}\" >/dev/MiSTer_cmd" if [ -n "${post_launch_hook}" ]; then eval "${post_launch_hook}" fi sleep 1 return 0 } # ========= SAM START AND STOP ========= function sam_start() { local core="$1" # 1. Fast, Non-Blocking Checks (Foreground) env_check there_can_be_only_one read_samini mcp_start echo "Starting SAM session..." if tmux has-session -t SAM 2>/dev/null; then samdebug "SAM session already exists—skipping." return fi # 2. Launch Tmux pointing to the Wrapper Function # We pass 'session_entry' instead of 'loop_core' tmux new-session -d \ -x 180 -y 40 \ -n "SAM Monitor: (n)ext (p)rev (m)ute (ctrl-c)lose" \ -s SAM \ "${misterpath}/Scripts/MiSTer_SAM_on.sh loop_core $core" & } function boot_sleep() { #Wait for rtc sync unset end end=$((SECONDS+60)) while [ $SECONDS -lt $end ]; do if [[ "$(date +%Y)" -gt "2020" ]]; then break else sleep 1 fi done } function there_can_be_only_one() { echo "Stopping other running instances of ${samprocess}…" # 1) kill any tmux “SAM” session tmux kill-session -t SAM 2>/dev/null || true # 2) patterns to match in the ps output local patterns=( "MiSTer_SAM_on.sh initial_start" "MiSTer_SAM_on.sh loop_core" "MiSTer_SAM_on.sh bootstart" "MiSTer_SAM_init start" ) # 3) for each pattern, find and kill all matching PIDs local pat pid for pat in "${patterns[@]}"; do ps -o pid,args \ | grep "$pat" \ | grep -v grep \ | awk '{print $1}' \ | while read -r pid; do [[ -n "$pid" ]] && kill -9 "$pid" 2>/dev/null done done # give it a moment sleep 1 } function kill_all_sams() { # Kill all SAM processes except for currently running ps -ef | grep -i '[M]iSTer_SAM' | awk -v me=${sampid} '$1 != me {print $1}' | xargs kill &>/dev/null } function exit_sam() { # exit_sam [menu|game] local exit_mode=${1:-menu} # Default to menu if no argument sam_cleanup bgm_stop tty_exit # 6. Load menu if requested if [[ "$exit_mode" == "menu" ]]; then echo "SAM stopped. Returning to menu..." timeout 1s sh -c "echo 'load_core /media/fat/menu.rbf' > /dev/MiSTer_cmd" fi } # ======== UTILITY FUNCTIONS ======== function mcp_start() { # MCP monitors when SAM should be launched. # "menuonly" and "samtimeout" determine when MCP launches SAM if tmux has-session -t MCP 2>/dev/null; then return fi if [ -z "$(pidof MiSTer_SAM_MCP)" ]; then tmux new-session -s MCP -d "${mrsampath}/MiSTer_SAM_MCP.py" fi } function mcp_monitor() { # Starts the MCP in an attached tmux session for monitoring/debugging. echo "Starting MCP in a visible tmux session for monitoring." echo "Detach with: Ctrl-b, then d" sleep 2 # Kill any existing MCP session first tmux attach-session -t MCP } function tmp_reset() { [[ -d /tmp/.SAM_List ]] && rm -rf /tmp/.SAM* /tmp/SAM* /tmp/MiSTer_SAM* mkdir -p /tmp/.SAM_List /tmp/.SAM_tmp } function init_paths() { # Create folders if they don't exist mkdir -p "${mrsampath}/SAM_Gamelists" #[ -d "/tmp/.SAM_List" ] && rm -rf /tmp/.SAM_List mkdir -p /tmp/.SAM_List [ -e "${tmpfile}" ] && { rm "${tmpfile}"; } [ -e "${tmpfile2}" ] && { rm "${tmpfile2}"; } touch "${tmpfile}" touch "${tmpfile2}" } function sam_prep() { # samvideo and ratings filter can't both be set # TODO make this smarter if [ "${rating}" == "yes" ]; then samvideo=no fi [ ! -d "/tmp/.SAM_tmp/SAM_config" ] && mkdir -p "/tmp/.SAM_tmp/SAM_config" [ ! -d "${misterpath}/video" ] && mkdir -p "${misterpath}/video" [[ -f /tmp/SAM_game.previous.mgl ]] && rm /tmp/SAM_game.previous.mgl [[ ! -d "${mrsampath}" ]] && mkdir -p "${mrsampath}" [[ ! -d "${mrsamtmp}" ]] && mkdir -p "${mrsamtmp}" mkdir -p /media/fat/Games/SAM &>/dev/null [ ! -d "/tmp/.SAM_tmp/Amiga_shared" ] && mkdir -p "/tmp/.SAM_tmp/Amiga_shared" if [ -d "${amigapath}/shared" ] && [ "$(mount | grep -ic "${amigapath}"/shared)" == "0" ]; then if [ "$(du -m "${amigapath}/shared" | cut -f1)" -lt 30 ]; then cp -r --force "${amigapath}"/shared/* /tmp/.SAM_tmp/Amiga_shared &>/dev/null mount --bind "/tmp/.SAM_tmp/Amiga_shared" "${amigapath}/shared" else echo "WARNING: ${amigapath}/shared folder is bigger than 30 MB. Items in shared folder won't be accessible while SAM is running." mount --bind "/tmp/.SAM_tmp/Amiga_shared" "${amigapath}/shared" fi fi #Downloads rating lists and sets the corelist to match only cores with rated lists if [ "${kids_safe}" == "yes" ]; then rating="kids" fi if [ "${rating}" != "no" ]; then local missing=() # make sure the target dir exists mkdir -p "${mrsampath}/SAM_Rated" # check each expected file for f in "${RATED_FILES[@]}"; do if [[ ! -f "${mrsampath}/SAM_Rated/$f" ]]; then missing+=( "$f" ) fi done if (( ${#missing[@]} )); then echo "Missing rating lists: ${missing[*]}" echo "Downloading..." if ! get_ratedlist; then echo "Ratings Filter failed downloading." return 1 fi else echo "All rating lists present." fi #Set corelist to only include cores with rated lists # build glr from the files on disk if [ "${rating}" == "kids" ]; then readarray -t glr < <( find "${mrsampath}/SAM_Rated" -name "*_rated.txt" \ | awk -F'/' '{print $NF}' \ | awk -F'_' '{print $1}' ) else readarray -t glr < <( find "${mrsampath}/SAM_Rated" -name "*_mature.txt" \ | awk -F'/' '{print $NF}' \ | awk -F'_' '{print $1}' ) fi # intersect glr with corelist clr=() for g in "${glr[@]}"; do for c in "${corelist[@]}"; do [[ "$c" == "$g" ]] && clr+=("$c") done done # if no overlap, warn & use the full rated list if (( ${#clr[@]} == 0 )); then echo "Warning: none of your enabled cores match the '${rating}' list." echo "→ Falling back to ALL rated cores." clr=( "${glr[@]}" ) else # otherwise show which cores have no rating file readarray -t nclr < <( printf '%s\n' "${clr[@]}" "${corelist[@]}" \ | sort \ | uniq -iu ) #echo "Rating lists missing for cores: ${nclr[*]}" fi # finally, write out the new corelist printf "%s\n" "${clr[@]}" > "${corelistfile}" fi [ "${coreweight}" == "yes" ] && echo "Weighted core mode active." [ "${samdebuglog}" == "yes" ] && rm /tmp/samdebug.log 2>/dev/null if [ "${samvideo}" == "yes" ] && [ "${samvideo_tvc_cdi}}" == "no" ]; then # Hide login prompt echo -e '\033[2J' > /dev/tty1 # Hide blinking cursor echo 0 > /sys/class/graphics/fbcon/cursor_blink echo -e '\033[?17;0;0c' > /dev/tty1 misterini_apply_temp get_dlmanager if [ ! -f "${mrsampath}"/mplayer ]; then if [ -f "${mrsampath}"/mplayer.zip ]; then unzip -ojq "${mrsampath}"/mplayer.zip -d "${mrsampath}" else get_samvideo fi fi if { [ "$samvideo_source" == "local" ] || [ "$samvideo_source" == "youtube" ]; } && [ "$samvideo_tvc" == "yes" ]; then samini_mod samvideo_tvc no fi fi # Mute Global Volume # if Volume.dat exists, try to mute only if needed if [ "${mute}" != "no" ]; then if [ -f "${configpath}/Volume.dat" ]; then only_mute_if_needed # if Volume.dat doesn’t exist yet, create it *and* mute else # create a “level=0 + mute” byte = 0x10 write_byte "${configpath}/Volume.dat" "10" timeout 1s sh -c "echo 'volume mute' > /dev/MiSTer_cmd" samdebug "Volume.dat created (0x10) and muted." fi fi } function sam_cleanup() { # Clean up by umounting any mount binds #[ -f "${configpath}/Volume.dat" ] && [ ${mute} == "yes" ] && rm "${configpath}/Volume.dat" only_unmute_if_needed [ "$(mount | grep -ic "${amigapath}"/shared)" == "1" ] && timeout 3s umount -l "${amigapath}/shared" [ -d "${misterpath}/Bootrom" ] && [ "$(mount | grep -ic 'bootrom')" == "1" ] && timeout 3s umount "${misterpath}/Bootrom" [ -f "${misterpath}/Games/NES/boot1.rom" ] && [ "$(mount | grep -ic 'nes/boot1.rom')" == "1" ] && timeout 3s umount "${misterpath}/Games/NES/boot1.rom" [ -f "${misterpath}/Games/NES/boot2.rom" ] && [ "$(mount | grep -ic 'nes/boot2.rom')" == "1" ] && timeout 3s umount "${misterpath}/Games/NES/boot2.rom" [ -f "${misterpath}/Games/NES/boot3.rom" ] && [ "$(mount | grep -ic 'nes/boot3.rom')" == "1" ] && timeout 3s umount "${misterpath}/Games/NES/boot3.rom" if [ "${mute}" != "no" ]; then readarray -t volmount <<< "$(mount | grep -i _volume.cfg | awk '{print $3}')" if [ "${#volmount[@]}" -gt 0 ]; then umount -l "${volmount[@]}" >/dev/null 2>&1 fi fi if [ "${samvideo}" == "yes" ] && [ "${samvideo_tvc_cdi}}" == "no" ]; then echo 1 > /sys/class/graphics/fbcon/cursor_blink echo 'Super Attract Mode Video was used.' > /dev/tty1 echo 'Please reboot for proper MiSTer Terminal' > /dev/tty1 echo '' > /dev/tty1 echo 'Login:' > /dev/tty1 [ -f /tmp/.SAM_tmp/sv_corecount ] && rm /tmp/.SAM_tmp/sv_corecount misterini_restore fi samdebug "Cleanup done." } function sam_monitor() { exec tmux attach-session -t SAM } function sam_enable() { # Enable autoplay echo -n " Enabling MiSTer SAM Autoplay..." # Awaken daemon # Check for and delete old fashioned scripts to prefer /media/fat/linux/user-startup.sh # (https://misterfpga.org/viewtopic.php?p=32159#p32159) if [ -f /etc/init.d/S93mistersam ] || [ -f /etc/init.d/_S93mistersam ]; then mount | grep "on / .*[(,]ro[,$]" -q && RO_ROOT="true" [ "$RO_ROOT" == "true" ] && mount / -o remount,rw sync rm /etc/init.d/S93mistersam &>/dev/null rm /etc/init.d/_S93mistersam &>/dev/null sync [ "$RO_ROOT" == "true" ] && mount / -o remount,ro fi # Add new startup way if [ ! -e "${userstartup}" ] && [ -e /etc/init.d/S99user ]; then if [ -e "${userstartuptpl}" ]; then echo "Copying ${userstartuptpl} to ${userstartup}" cp "${userstartuptpl}" "${userstartup}" sleep 1 else echo "Building ${userstartup}" fi fi if [ "$(grep -ic "mister_sam" ${userstartup})" = "0" ]; then echo -e "Adding SAM to ${userstartup}\n" echo -e "\n# Startup MiSTer_SAM - Super Attract Mode" >>${userstartup} echo -e "[[ -e ${mrsampath}/MiSTer_SAM_init ]] && ${mrsampath}/MiSTer_SAM_init \$1 &" >>"${userstartup}" fi echo "SAM install complete." echo -e "\n\n\n" source "${samini_file}" echo -ne "\e[1m" SAM will start ${samtimeout} sec. after boot"\e[0m" if [ "${menuonly,,}" == "yes" ]; then echo -ne "\e[1m" in the main menu"\e[0m" else echo -ne "\e[1m" whenever controller is not in use"\e[0m" fi echo -e "\e[1m" and show each game for ${gametimer} sec."\e[0m" echo -ne "\e[1m" First run will take some time to compile game list... please wait."\e[0m" echo -e "\n\n\n" sleep 5 "${misterpath}/Scripts/MiSTer_SAM_on.sh" start exit } function sam_disable() { # Disable autoplay echo -n " Disabling SAM autoplay..." # Clean out existing processes to ensure we can update if [ -f /etc/init.d/S93mistersam ] || [ -f /etc/init.d/_S93mistersam ]; then mount | grep "on / .*[(,]ro[,$]" -q && RO_ROOT="true" [ "$RO_ROOT" == "true" ] && mount / -o remount,rw sync rm /etc/init.d/S93mistersam &>/dev/null rm /etc/init.d/_S93mistersam &>/dev/null sync [ "$RO_ROOT" == "true" ] && mount / -o remount,ro fi there_can_be_only_one sed -i '/MiSTer_SAM/d' ${userstartup} sync echo " Done." } function env_check() { # Check if we've been installed if [ ! -f "${mrsampath}/samindex" ]; then echo " SAM required files not found." echo " Installing now." sam_update autoconfig echo " Setup complete." fi #Probably offline or update_all install if [ ! -f "${configpath}/inputs/GBA_input_1234_5678_v3.map" ]; then if [ -f "${mrsampath}/inputs/GBA_input_1234_5678_v3.map" ]; then cp "${mrsampath}/inputs/GBA_input_1234_5678_v3.map" "${configpath}/inputs" >/dev/null cp "${mrsampath}/inputs/NES_input_1234_5678_v3.map" "${configpath}/inputs" >/dev/null cp "${mrsampath}/inputs/TGFX16_input_1234_5678_v3.map" "${configpath}/inputs" >/dev/null cp "${mrsampath}/inputs/SATURN_input_1234_5678_v3.map" "${configpath}/inputs" >/dev/null cp "${mrsampath}/inputs/MegaCD_input_1234_5678_v3.map" "${configpath}/inputs" >/dev/null cp "${mrsampath}/inputs/NEOGEO_input_1234_5678_v3.map" "${configpath}/inputs" >/dev/null else get_inputmap fi fi } function deletegl() { # In case of issues, reset game lists there_can_be_only_one if [ -d "${mrsampath}/SAM_Gamelists" ]; then echo "Deleting MiSTer_SAM Gamelist folder" rm "${mrsampath}"/SAM_Gamelists/*_gamelist.txt fi if [ -d /tmp/.SAM_List ]; then rm -rf /tmp/.SAM_List fi if [ "${inmenu}" -eq 1 ]; then sleep 1 sam_menu else echo -e "\nGamelist reset successful. Please start SAM now.\n" sleep 1 parse_cmd stop fi } function creategl() { create_all_gamelists echo -e "\nGamelist creation successful. Please start SAM now.\n" sleep 1 parse_cmd stop } function skipmessage() { local core=${1} # Exit immediately if the core argument is missing, for safety. if [ -z "${core}" ]; then return fi # Check the global 'skipmessage' setting AND the core-specific setting from the CORE_SKIP array. if [ "${skipmessage}" == "yes" ] && [ "${CORE_SKIP[${core}]}" == "yes" ]; then # If both are 'yes', wait for the configured time and send the button presses. sleep "$skiptime" samdebug "Button push sent for '${core}' to skip BIOS" if [ "${core}" == "intellivision" ]; then "${mrsampath}/mbc" raw_seq :1C sleep 1 "${mrsampath}/mbc" raw_seq :02 sleep 1 "${mrsampath}/mbc" raw_seq :1C sleep 1 "${mrsampath}/mbc" raw_seq :02 sleep 1 "${mrsampath}/mbc" raw_seq :1C sleep 1 "${mrsampath}/mbc" raw_seq :03 sleep 1 "${mrsampath}/mbc" raw_seq :1C else "${mrsampath}/mbc" raw_seq :31 sleep 1 "${mrsampath}/mbc" raw_seq :31 fi fi } function skipmessage_ao486() { sleep "$skiptime" samdebug "Button pushes sent to (hopefully) skip past selection screens" "${mrsampath}/mbc" raw_seq :02 sleep 1 "${mrsampath}/mbc" raw_seq :22 sleep 1 "${mrsampath}/mbc" raw_seq :1C sleep 1 "${mrsampath}/mbc" raw_seq :19 sleep 1 "${mrsampath}/mbc" raw_seq :32 sleep 1 "${mrsampath}/mbc" raw_seq :3B } function mglfavorite() { # Add current game to _Favorites folder if [ ! -d "${misterpath}/_Favorites" ]; then mkdir -p "${misterpath}/_Favorites" fi cp /tmp/SAM_Game.mgl "${misterpath}/_Favorites/$(cat /tmp/SAM_Game.txt).mgl" } function ignoregame() { declare -l currentrbf="$(cat /tmp/SAM_Games.log | tail -n1 | awk -F- '{print $2}')" currentgame="$(cat /tmp/SAM_Games.log | tail -n1 | awk 'BEGIN{FS=OFS="\-"; }{for(i=3;i> "${gamelistpath}/${cr}_excludelist.txt" echo "${currentgame:1} added to ${cr}_excludelist.txt" echo "" echo "Tip: If you want to add the game again, go to ${gamelistpath}/${cr}_excludelist.txt" echo "" } function delete_from_corelist() { # delete_from_corelist core tmp if [ -z "$2" ]; then for i in "${!corelist[@]}"; do if [[ ${corelist[i]} = "$1" ]]; then unset 'corelist[i]' samdebug "Deleted $1 from corelist" fi done samdebug "Corelist now ${corelist[@]}" printf "%s\n" "${corelist[@]}" > "${corelistfile}" else for i in "${!corelisttmp[@]}"; do if [[ ${corelisttmp[i]} = "$1" ]]; then unset 'corelisttmp[i]' fi done fi } function reset_core_gl() { # args ${nextcore} echo " Deleting old game lists for ${1^^}..." rm "${gamelistpath}/${1}_gamelist.txt" &>/dev/null sync "${gamelistpath}" } function core_error_checklist() { # core_error core /path/to/ROM delete_from_corelist "${1}" echo " List of cores is now: ${corelist[*]}" declare -g romloadfails=0 # Load a different core next_core } function disable_bootrom() { if [ "${disablebootrom}" == "yes" ]; then # Make Bootrom folder inaccessible until restart mkdir -p /tmp/.SAM_List/Bootrom [ -d "${misterpath}/Bootrom" ] && [ "$(mount | grep -ic 'bootrom')" == "0" ] && mount --bind /tmp/.SAM_List/Bootrom "${misterpath}/Bootrom" # Disable Nes bootroms except for FDS Bios (boot0.rom) [ -f "${misterpath}/Games/NES/boot1.rom" ] && [ "$(mount | grep -ic 'nes/boot1.rom')" == "0" ] && touch "$brfake" && mount --bind "$brfake" "${misterpath}/Games/NES/boot1.rom" [ -f "${misterpath}/Games/NES/boot2.rom" ] && [ "$(mount | grep -ic 'nes/boot2.rom')" == "0" ] && touch "$brfake" && mount --bind "$brfake" "${misterpath}/Games/NES/boot2.rom" [ -f "${misterpath}/Games/NES/boot3.rom" ] && [ "$(mount | grep -ic 'nes/boot3.rom')" == "0" ] && touch "$brfake" && mount --bind "$brfake" "${misterpath}/Games/NES/boot3.rom" fi } function mute() { if [ "${mute}" == "core" ]; then samdebug "mute=core" only_unmute_if_needed # Create empty volume files. Only SD card write operation necessary for mute to work. [ ! -f "${configpath}/${1}_volume.cfg" ] && touch "${configpath}/${1}_volume.cfg" [ ! -f "/tmp/.SAM_tmp/SAM_config/${1}_volume.cfg" ] && touch "/tmp/.SAM_tmp/SAM_config/${1}_volume.cfg" for i in {1..3}; do if mount | grep -iq "${configpath}/${1}_volume.cfg"; then samdebug "${1}_volume.cfg already mounted" break fi mount --bind "/tmp/.SAM_tmp/SAM_config/${1}_volume.cfg" "${configpath}/${1}_volume.cfg" if [ $? -eq 0 ]; then samdebug "${1}_volume.cfg mounted successfully" break else echo "ERROR: Failed to mute ${1} (attempt ${i})" if [ $i -eq 3 ]; then echo "ERROR: All attempts to mute ${1} failed... Continuing." fi sleep 2 fi done [[ "$(mount | grep -ic "${1}"_volume.cfg)" != "0" ]] && echo -e "\0006\c" > "/tmp/.SAM_tmp/SAM_config/${1}_volume.cfg" # Only keep one volume.cfg file mounted if [ -n "${prevcore}" ] && [ "${prevcore}" != "${1}" ]; then umount "${configpath}/${prevcore}_volume.cfg" sync fi prevcore=${1} fi } # Helper: write_byte # Writes a single byte (given as a two-digit hex string) into a file, then syncs. # # Arguments: # $1 = path to file (e.g. "${configpath}/Volume.dat") # $2 = two-digit hex string representing the byte to write (e.g. "05", "15") function write_byte() { local f="$1"; local hex="$2" printf '%b' "\\x$hex" > "$f" && sync } # Sets the “mute” bit in Volume.dat without altering your current volume level. # Then issues a live “volume mute” command to the running MiSTer core. function global_mute() { local f="${configpath}/Volume.dat" local cur m hex # read the single-byte value, e.g. "05" cur=$(xxd -p -c1 "$f") # OR in the mute-flag (0x10) m=$(( 0x$cur | 0x10 )) # format back to two-digit hex, then write that single byte hex=$(printf '%02x' "$m") write_byte "$f" "$hex" # immediately mute the live core timeout 1s sh -c "echo 'volume mute' > /dev/MiSTer_cmd" samdebug "WRITE TO SD: Global mute → Volume.dat" } function global_unmute() { local f="${configpath}/Volume.dat" local cur hex u cur=$(xxd -p -c1 "$f") u=$((0x$cur & 0x0F)) hex=$(printf '%02x' "$u") write_byte "$f" "$hex" # sent unmute for interactive unmute timeout 1s sh -c "echo 'volume unmute' > /dev/MiSTer_cmd" samdebug "WRITE TO SD: Restored Volume.dat" } function only_mute_if_needed() { local f="${configpath}/Volume.dat" local cur # 1) read the single byte as two hex digits, e.g. "05" or "15" cur=$(xxd -p -c1 "$f") # 2) test bit 4 (0x10). If (cur & 0x10) == 0 then we’re not muted yet. if (( (0x$cur & 0x10) == 0 )); then samdebug "Volume not yet muted (Volume.dat=0x$cur) → muting now" global_mute else samdebug "Already muted (Volume.dat=0x$cur) → skipping write" fi } function only_unmute_if_needed() { local f="${configpath}/Volume.dat" local cur # 1) Read the single-byte value, e.g. "15" if muted at level5, or "05" if unmuted cur=$(xxd -p -c1 "$f") # 2) If bit4 (0x10) *is* set, we’re currently muted → clear it if (( (0x$cur & 0x10) != 0 )); then samdebug "Volume is muted (Volume.dat=0x$cur) → unmuting now" global_unmute return 0 # indicate we did an unmute else samdebug "Volume already unmuted (Volume.dat=0x$cur) → skipping write" return 1 # indicate no action taken fi } function unmute_with_retry() { local max_wait=15 local volume_dat_path="/media/fat/config/Volume.dat" local unmute_command="volume unmute" local counter=0 samdebug "Attempting to unmute volume with a ${max_wait}-second timeout..." while [ $counter -lt $max_wait ]; do if [ -f "$volume_dat_path" ]; then local cur_vol_hex cur_vol_hex=$(xxd -p -l 1 "$volume_dat_path") # Check if the mute bit (0x10) is OFF if (( (0x$cur_vol_hex & 0x10) == 0 )); then samdebug "SUCCESS: Volume is unmuted." return 0 fi fi # If still muted or file not found, send the command samdebug "Attempt $(($counter + 1))/$max_wait: Volume still muted. Sending unmute command..." timeout 1 sh -c "echo '$unmute_command' > /dev/MiSTer_cmd" sleep 1 counter=$(($counter + 1)) done samdebug "FAILED: Timed out trying to unmute volume." } function check_zips() { # check_zips core # Check if zip still exists #samdebug "Checking zips in file..." unset zipsondisk unset zipsinfile unset files unset newfiles mapfile -t zipsinfile < <(fgrep ".zip" "${gamelistpath}/${1}_gamelist.txt" | awk -F".zip" '!seen[$1]++' | awk -F".zip" '{print $1}' | sed -e 's/$/.zip/') if [ ${#zipsinfile[@]} -gt 0 ]; then for zips in "${zipsinfile[@]}"; do if [ ! -f "${zips}" ]; then samdebug "Creating new game list because zip file[s] seems to have changed." build_gamelist "${1}" unset zipsinfile mapfile -t zipsinfile < <(fgrep ".zip" "${gamelistpath}/${1}_gamelist.txt" | awk -F".zip" '!seen[$1]++' | awk -F".zip" '{print $1}' | sed -e 's/$/.zip/') break return fi done #samdebug "Done." #samdebug -n "Checking zips on disk..." if [ "${checkzipsondisk}" == "yes" ] || [ "${force_zip_scan}" == "yes" ]; then # Check for new zips corepath="$("${mrsampath}"/samindex -q -s "${1}" -d |awk -F':' '{print $2}')" readarray -t files <<< "$(find "${corepath}" -maxdepth 2 -type f -name "*.zip")" extgrep=$(echo ".${CORE_EXT[${1}]}" | sed -e "s/,/\\\|/g"| sed 's/,/,./g') # Check which files have valid roms readarray -t newfiles <<< "$(printf '%s\n' "${zipsinfile[@]}" "${files[@]}" | sort | uniq -iu )" if [[ "${newfiles[*]}" ]]; then for f in "${newfiles[@]}"; do if [ -f "${f}" ]; then if "${mrsampath}"/partun -l "${f}" --ext "${extgrep}" | grep -q "${extgrep}"; then zipsondisk+=( "${f}" ) fi else samdebug "Zip file ${f} not found" fi done fi if [[ "${zipsondisk[*]}" ]]; then result="$(printf '%s\n' "${zipsondisk[@]}")" if [[ "${result}" ]]; then samdebug "Found new zip file[s]: ${result##*/}" build_gamelist "${1}" force_zip_scan="No" return fi fi force_zip_scan="No" fi fi #samdebug "Done." } function filter_list() { # args: core local core=${1} local master_list="${gamelistpath}/${core}_gamelist.txt" local session_list="${gamelistpathtmp}/${core}_gamelist.txt" local flag_dir="${gamelistpathtmp}/.checked" mkdir -p "$flag_dir" local flag_file="$flag_dir/$core.filtered" if [ -e "$flag_file" ]; then samdebug "Filters for '${core}' already applied this session. Skipping." return 0 fi # Always start with a fresh copy of the master list in our working file. cp -f "${master_list}" "${tmpfile}" # --- Each filter now reads from $tmpfile and writes its output back to $tmpfile --- # --- ALL informational 'echo' commands are redirected to stderr (>&2) --- if [ -n "${PATHFILTER[${core}]}" ]; then echo "Applying path filter for '${core}': ${PATHFILTER[${core}]}" >&2 grep -F "${PATHFILTER[${core}]}" "${tmpfile}" > "${tmpfile}.filtered" && mv -f "${tmpfile}.filtered" "${tmpfile}" fi if [[ "${core}" == "arcade" ]] && [ -n "${arcadeorient}" ]; then echo "Applying orientation filter for Arcade: ${arcadeorient}" >&2 grep -Fi "${arcadeorient}" "${tmpfile}" > "${tmpfile}.filtered" if [ -s "${tmpfile}.filtered" ]; then mv -f "${tmpfile}.filtered" "${tmpfile}" else echo "Warning: Orientation filter produced no results." >&2 fi fi if [ "$dupe_mode" = "strict" ]; then # samdebug already prints to stderr, so it's safe. samdebug "Using strict mode to filter duplicates..." awk -F'/' ' { full = $0; lowpath = tolower(full) if ( lowpath ~ /\/[^\/]*(hack|beta|proto)[^\/]*\// ) next fname = $NF; if ( tolower(fname) ~ /\([^)]*(hack|beta|proto)[^)]*\)/ ) next name = fname; sub(/\.[^.]+$/, "", name); sub(/\s*\(.*/, "", name) sub(/^([0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?|[0-9]+)[^[:alnum:]]*/, "", name) key = tolower(name); gsub(/^[ \t]+|[ \t]+$/, "", key) if (!seen[key]++) print full }' "${tmpfile}" > "${tmpfile}.filtered" && mv -f "${tmpfile}.filtered" "${tmpfile}" else awk -F'/' '!seen[$NF]++' "${tmpfile}" > "${tmpfile}.filtered" && mv -f "${tmpfile}.filtered" "${tmpfile}" fi if [ -s "${gamelistpath}/${core}_gamelist_exclude.txt" ]; then echo "Applying category excludelist for '${core}'..." >&2 awk 'FNR==NR{a[$0];next} !($0 in a)' "${gamelistpath}/${core}_gamelist_exclude.txt" "${tmpfile}" > "${tmpfile}.filtered" && mv -f "${tmpfile}.filtered" "${tmpfile}" else samdebug "Excludelist for '${core}' is empty, skipping filter." >&2 fi if [ -f "${gamelistpath}/${core}_excludelist.txt" ]; then echo "Applying standard excludelist for '${core}'..." >&2 awk -v EXCL="${gamelistpath}/${core}_excludelist.txt" 'BEGIN{while(getline line "${tmpfile}.filtered" && mv -f "${tmpfile}.filtered" "${tmpfile}" fi if [ "${rating}" != "no" ]; then apply_ratings_filter "${core}" "${tmpfile}" fi if [[ "${exclude[*]}" ]]; then for e in "${exclude[@]}"; do grep -viw "$e" "${tmpfile}" > "${tmpfile}.filtered" && mv -f "${tmpfile}.filtered" "${tmpfile}" || true done fi if [ "${disable_blacklist}" == "no" ] && [ -f "${gamelistpath}/${core}_blacklist.txt" ]; then if [ -f "${tmpfile}" ]; then echo -n "Applying static screen blacklist for '${core}'... " >&2 awk "BEGIN{while(getline<\"${gamelistpath}/${core}_blacklist.txt\"){a[\$0]=1}} {gamelistfile=\$0;sub(/\\.[^.]*\$/,\"\",gamelistfile);sub(/^.*\\//,\"\",gamelistfile);if(!(gamelistfile in a))print}" \ "${tmpfile}" > "${tmpfile}.filtered" if [ -s "${tmpfile}.filtered" ]; then mv -f "${tmpfile}.filtered" "${tmpfile}" fi else samdebug "Warning: '${tmpfile}' missing before blacklist filter." fi else echo -n "No blacklist filter found for '${core}'... " >&2 fi cp -f "${tmpfile}" "${session_list}" echo "$(wc -l <"${session_list}") games are now in the active shuffle list." >&2 if [ ! -s "${session_list}" ]; then echo "Error: All filters combined produced an empty list for '${core}'." >&2 delete_from_corelist "${core}" return 1 fi touch "$flag_file" return 0 } # Helper function for the ratings filter. function apply_ratings_filter() { local core=${1} local target_file=${2} # Pass the file to modify ($tmpfile) echo "Ratings Mode ${rating} active - Filtering Roms..." if [ "${rating}" == "kids" ]; then if [ ${1} == amiga ]; then fgrep -f "${mrsampath}/SAM_Rated/amiga_rated.txt" <(fgrep -v "Demo:" "${gamelistpath}/amiga_gamelist.txt") | awk -F'(' '!seen[$1]++ {print $0}' > "${tmpfilefilter}" else fgrep -f "${mrsampath}/SAM_Rated/${1}_rated.txt" "${gamelistpathtmp}/${1}_gamelist.txt" | awk -F "/" '{split($NF,a," \\("); if (!seen[a[1]]++) print $0}' > "${tmpfilefilter}" fi if [ -s "${tmpfilefilter}" ]; then samdebug "$(wc -l <"${tmpfilefilter}") games after kids safe filter applied." cp -f "${tmpfilefilter}" "${gamelistpathtmp}/${1}_gamelist.txt" else delete_from_corelist "${1}" delete_from_corelist "${1}" tmp echo "${1} kids safe filter produced no results and will be disabled." echo "List of cores is now: ${corelist[*]}" return 1 fi else # $1 is the core name rated_file="${mrsampath}/SAM_Rated/${1}_mature.txt" if [[ ! -f "$rated_file" ]]; then samdebug "No ${1}_mature.txt found—skipping mature filter." else # load your mature names mapfile -t rated_list <"$rated_file" # prepare output file : >"$tmpfilefilter" # choose which gamelist to read (and strip Demos for amiga) if [[ "$1" == "amiga" ]]; then gamelist_src="${gamelistpath}/amiga_gamelist.txt" readarray -t games < <(grep -v '^Demo:' "$gamelist_src") else gamelist_src="${gamelistpathtmp}/${1}_gamelist.txt" readarray -t games < <(cat "$gamelist_src") fi declare -A seen for line in "${games[@]}"; do # strip dir + extension name="${line##*/}" name="${name%.*}" name_lc="${name,,}" # loose substring match for entry in "${rated_list[@]}"; do entry_lc="${entry,,}" if [[ "$name_lc" == *"$entry_lc"* ]]; then if [[ -z "${seen[$name_lc]}" ]]; then seen[$name_lc]=1 printf '%s\n' "$line" >>"$tmpfilefilter" fi break fi done done if [[ -s "$tmpfilefilter" ]]; then samdebug "$(wc -l <"$tmpfilefilter") games after mature filter applied." cp -f "$tmpfilefilter" "${gamelistpathtmp}/${1}_gamelist.txt" else delete_from_corelist "$1" delete_from_corelist "$1" tmp echo "${1} mature filter produced no results and will be disabled." echo "List of cores is now: ${corelist[*]}" return 1 fi fi fi } function samdebug() { local ts msg ts="$(date '+%Y-%m-%d %H:%M:%S')" msg="$*" if [[ "${samdebug}" == "yes" ]]; then # The '>&2' at the end redirects this message to stderr. # This prevents it from being captured by command substitution. echo -e "\e[1m\e[31m[${ts}] ${msg}\e[0m" >&2 fi if [[ "${samdebuglog}" == "yes" ]]; then # Writing to a log file is already separate and is perfectly fine. echo "[${ts}] ${msg}" >> /tmp/samdebug.log fi } samini_mod() { local key="$1" local value="$2" local file="${3:-/media/fat/Scripts/MiSTer_SAM.ini}" local formatted="${key}=\"${value}\"" if grep -q "^${key}=" "$file"; then sed -i "/^${key}=/c\\${formatted}" "$file" else echo "$formatted" >> "$file" fi } function sam_sshconfig() { # Alias to be added alias_m='alias m="/media/fat/Scripts/MiSTer_SAM_on.sh"' alias_ms='alias ms="source /media/fat/Scripts/MiSTer_SAM_on.sh --source-only"' alias_u='alias u="/media/fat/Scripts/update_all.sh"' # Path to the .bash_profile bash_profile="${HOME}/.bash_profile" # Check if .bash_profile exists if [ ! -f "$bash_profile" ]; then touch "$bash_profile" fi # Check if the alias already exists in the file if grep -Fxq "$alias_m" "$bash_profile"; then echo "Alias already exists in $bash_profile" else # Add the alias to .bash_profile echo "$alias_m" >> "$bash_profile" echo "$alias_ms" >> "$bash_profile" echo "$alias_u" >> "$bash_profile" echo "Alias added to $bash_profile. Please relaunch terminal. Type 'm' to start MiSTer_SAM_on.sh" fi source ~/.bash_profile } function sam_help() { # sam_help echo " start - start immediately" echo " skip - skip to the next game" echo " stop - stop immediately" echo "" echo " update - self-update" echo " monitor - monitor SAM output" echo "" echo " enable - enable autoplay" echo " disable - disable autoplay" echo "" echo " deletegl - delete all game lists" echo " creategl - create all game lists" echo "" echo " menu - load to menu" echo "" echo " arcade, genesis, gba..." echo " games from one system only" exit 2 } # ======== BACKGROUND MUSIC PLAYER FUNCTIONS ======== function bgm_start() { if [ "${bgm}" == "yes" ]; then if [ ! "$(ps -o pid,args | grep '[b]gm' | head -1)" ]; then /media/fat/Scripts/bgm.sh &>/dev/null & sleep 2 else echo "BGM already running." fi echo -n "set playincore yes" | socat - UNIX-CONNECT:/tmp/bgm.sock &>/dev/null sleep 1 echo -n "set playback random" | socat - UNIX-CONNECT:/tmp/bgm.sock 2>/dev/null sleep 1 echo -n "play" | socat - UNIX-CONNECT:/tmp/bgm.sock &>/dev/null else # In case BGM is running, let's stop it if [ "$(ps -o pid,args | grep '[b]gm' | head -1)" ]; then bgm_stop force fi fi } function bgm_stop() { if [ "${bgm}" == "yes" ] || [ "$1" == "force" ]; then echo -n "Stopping Background Music Player... " echo -n "set playincore no" | timeout 1s socat - UNIX-CONNECT:/tmp/bgm.sock &>/dev/null echo -n "stop" | timeout 1s socat - UNIX-CONNECT:/tmp/bgm.sock 2>/dev/null sleep 0.2 if [ "${bgmstop}" == "yes" ]; then echo -n "stop" | timeout 1s socat - UNIX-CONNECT:/tmp/bgm.sock 2>/dev/null sleep 0.2 echo -n "set playback disabled" | timeout 1s socat - UNIX-CONNECT:/tmp/bgm.sock 2>/dev/null kill -9 "$(ps -o pid,args | grep '[b]gm.sh' | awk '{print $1}' | head -1)" 2>/dev/null kill -9 "$(ps -o pid,args | grep 'mpg123' | awk '{print $1}' | head -1)" 2>/dev/null rm /tmp/bgm.sock 2>/dev/null if [ "${gvoladjust}" -ne 0 ]; then #local oldvol=$((7 - $currentvol + $gvoladjust)) #samdebug "Changing global volume back to $oldvol" #echo "volume ${oldvol}" > /dev/MiSTer_cmd & echo -e "\00$currentvol\c" >"${configpath}/Volume.dat" fi fi echo "Done." fi } # ======== tty2oled FUNCTIONS ======== function tty_start() { if [ "${ttyenable}" == "yes" ]; then [ -f /tmp/.SAM_tmp/tty_currentinfo ] && rm /tmp/.SAM_tmp/tty_currentinfo #[ -f /media/fat/tty2oled/S60tty2oled ] && /media/fat/tty2oled/S60tty2oled restart && sleep 3 touch "${tty_sleepfile}" echo -n "Starting tty2oled... " tmux new -s OLED -d "${mrsampath}/MiSTer_SAM_tty2oled" &>/dev/null echo "Done." fi } function tty_exit() { if [ "${ttyenable}" == "yes" ]; then echo -n "Stopping tty2oled... " # 1. Timeout for the pipe # Try to write for 3s, then give up. # '2>/dev/null' hides the "timeout: sending signal" message. # The final '&' runs this whole timeout operation in the background. if [[ -p ${TTY_cmd_pipe} ]]; then timeout 3s sh -c "echo 'stop' > ${TTY_cmd_pipe}" 2>/dev/null & fi # 2. Timeout for tmux # Run in background (&) and redirect all output (&>/dev/null) timeout 3s tmux kill-session -t OLED &>/dev/null & # 3. Timeout for rm # Run in background (&) and redirect all output (&>/dev/null) timeout 3s rm "${tty_sleepfile}" &>/dev/null & # This will now print immediately echo "Done." fi } function write_to_TTY_cmd_pipe() { [[ -p ${TTY_cmd_pipe} ]] && echo "${@}" >${TTY_cmd_pipe} } # --- Function to modify MiSTer.ini for SAM Video --- function misterini_apply_temp() { # Check if sv_inimod is set to "no" if [ "$sv_inimod" == "no" ]; then echo "sv_inimod is set to 'no'. Skipping MiSTer.ini modification." return 0 fi # Exit if MiSTer.ini doesn't exist if [ ! -f "$ini_file" ]; then echo "Error: $ini_file not found." return 1 fi # Check if it's *already* mounted by us if mountpoint -q "$ini_file"; then echo "MiSTer.ini is already temporarily mounted. Skipping." return 0 fi echo "Checking and applying temporary settings to $ini_file." # --- Desired settings logic (Copied from your function) --- local fb_terminal="1" local vga_scaler="1" local video_mode if [ "$samvideo_output" == "hdmi" ]; then if [ "${sv_aspectfix_vmode}" == "yes" ]; then video_mode="6" else video_mode="8" fi elif [ "$samvideo_output" == "crt" ]; then if [ "$samvideo_source" == "youtube" ]; then samvideo_crtmode="${samvideo_crtmode320}" elif [ "$samvideo_source" == "archive" ]; then samvideo_crtmode="${samvideo_crtmode640}" fi video_mode="$(echo "$samvideo_crtmode" | awk -F'=' '{print $2}')" else echo "Unknown video output mode: $samvideo_output" return 1 fi # --- INI Modification Logic --- # We now write to our *temporary file*, not the original. # Use awk to read the *original* file and write to the *temp* file. awk ' BEGIN { inside_menu = 0 } /^\[[Mm][Ee][Nn][Uu]\]/ { inside_menu = 1; next } /\[.*\]/ && !/^\[[Mm][Ee][Nn][Uu]\]/ { inside_menu = 0 } !inside_menu { print } ' "$ini_file" > "$sv_ini_temp_file" # Append the new [Menu] section to the temp file { echo "" echo "[Menu]" echo "; Settings temporarily overridden by SAM Video via bind mount." echo "video_mode=$video_mode" echo "vga_scaler=$vga_scaler" echo "fb_terminal=$fb_terminal" } >> "$sv_ini_temp_file" # --- Bind Mount Logic --- # This is the new part. It requires sudo. echo "Applying temporary settings via bind mount..." if ! sudo mount --bind "$sv_ini_temp_file" "$ini_file"; then echo "Error: SAM failed to bind mount." rm -f "$sv_ini_temp_file" # Clean up return 1 fi echo "MiSTer.ini is now temporarily modified." return 0 } # --- Function to restore MiSTer.ini from backup --- # Reverts the entire MiSTer.ini file from the backup, if enabled. function misterini_restore() { # If we never planned to modify, there's nothing to restore. if [ "$sv_inimod" == "no" ]; then return 0 fi echo "Restoring original MiSTer.ini..." # Check if our file is currently a mount point if mountpoint -q "$ini_file"; then echo "Unmounting temporary MiSTer.ini..." if ! sudo umount "$ini_file"; then echo "Error: Failed to unmount $ini_file." echo "You may need to unmount it manually: sudo umount $ini_fsile" return 1 fi echo "Original MiSTer.ini restored." else echo "MiSTer.ini was not mounted. No restore needed." fi # Clean up our temporary file rm -f "$sv_ini_temp_file" return 0 } function sv_ar_cdi_mode() { # 1. Setup Variables samvideo_list="/tmp/.SAM_List/sv_archive_list.txt" tmpvideo="/tmp/SAMvideo.chd" local http_archive="${sv_archive_cdi//https/http}" # Check for CDi core availability local cdi_check_path="/media/fat/_Unstable" local cdi_core_file="" # Find existing CDi core (case-insensitive) if [ -d "$cdi_check_path" ]; then cdi_core_file=$(find "$cdi_check_path" -maxdepth 1 -iname "cdi*.rbf" -print -quit) fi if [ -z "$cdi_core_file" ]; then echo "CDi core not found. Attempting to retrieve..." mkdir -p "$cdi_check_path" # Retrieve URL from JSON local cdi_url=$(curl -k -s "https://raw.githubusercontent.com/MiSTer-unstable-nightlies/Unstable_Folder_MiSTer/main/db_unstable_nightlies_folder.json" | \ jq -r '.files | to_entries[] | select(.key | contains("_Unstable/CDi")) | .value.url' | head -n 1) if [ -n "$cdi_url" ]; then echo "Downloading CDi core from: $cdi_url" curl -k -L -o "${cdi_check_path}/CDi_unstable.rbf" "$cdi_url" if [ $? -eq 0 ]; then echo "CDi core downloaded successfully." cdi_core_file="${cdi_check_path}/CDi_unstable.rbf" else echo "Error downloading CDi core." fi else echo "Failed to fetch CDi core URL." fi fi if [ -z "$cdi_core_file" ]; then echo "Error: CDi core missing and download failed. Skipping video playback." return fi # 2. Populate the samvideo_list if it's empty (only needed for non-TVC mode) if [ ! -s "${samvideo_list}" ]; then curl_download /tmp/SAMvideos.xml "${http_archive}" grep -o '/g; s/"/\"/g; s/#'/\'"'"'/g; s/“/\"/g; s/”/\"/g;' \ > "${samvideo_list}" fi # 3. Select a video and check availability while true; do if [ -n "$sv_selected" ]; then samdebug "Video pre-selected: $sv_selected" elif [ "$samvideo_tvc" == "yes" ]; then samvideo_tvc # Check if selection was made if [ -n "$sv_selected" ]; then samdebug "Video selected via TVC: $sv_selected" else samdebug "TVC selection failed, falling back." sv_selected="$(shuf -n1 "${samvideo_list}")" fi else sv_selected="$(shuf -n1 "${samvideo_list}")" fi # Safety check: if list is empty or shuf failed if [ -z "$sv_selected" ]; then samdebug "Error: No video selected." return fi sv_selected_url="${http_archive%/*}/${sv_selected}" # 4. Check Local Availability First local local_svfile="${samvideo_path}/$(echo "$sv_selected" | sed "s/[\":?]//g")" if [ -f "$local_svfile" ]; then samdebug "Local file found: $local_svfile. Skipping remote check." break fi # Check if the URL is available using wget samdebug "Checking availability of ${sv_selected_url}..." if wget --spider --quiet --timeout=10 --tries=1 "${sv_selected_url}"; then samdebug "URL is available: ${sv_selected_url}" break else samdebug "URL is not available: ${sv_selected_url}. Removing from list and selecting another." awk -v Line="$sv_selected" '!index($0, Line)' "${samvideo_list}" >"${tmpfile}" && cp -f "${tmpfile}" "${samvideo_list}" unset sv_selected fi done # 5. Download / Cache Logic # Flag to track if we just downloaded a new file local fresh_download="no" if [ -f "$local_svfile" ]; then echo "Local file exists: $local_svfile" cp "$local_svfile" "$tmpvideo" else echo "Preloading ${sv_selected} from archive.org for smooth playback" dl_video "${sv_selected_url}" # Check if download succeeded AND file has size > 0 if [ -s "$tmpvideo" ]; then fresh_download="yes" else echo "Error: Download failed or file is empty." echo "1" > "$sv_gametimer_file" return fi fi # 5. Cache the file locally ONLY if it was a fresh download if [ "$fresh_download" == "yes" ] && [ "$keep_local_copy" == "yes" ]; then cp "$tmpvideo" "$local_svfile" samdebug "Saved local copy of video: $local_svfile" fi # 6. Update samvideo_list to remove the processed file if [ "$samvideo_tvc" == "no" ]; then awk -vLine="$sv_selected" '!index($0,Line)' "${samvideo_list}" >${tmpfile} && cp -f ${tmpfile} "${samvideo_list}" fi # 7. Check CD-i autoplay FILE="/media/fat/config/CD-i.cfg" if [ ! -f "$FILE" ]; then # Create file with Autoplay ENABLED echo "1000 0000 0000 0000 0000 0000 0000 0000" | xxd -r -p > "$FILE" echo "CD-i.CFG created with Autoplay ENABLED." else # Fix Autoplay if disabled BYTE=$(xxd -p -s 1 -l 1 "$FILE") if [ "$BYTE" != "00" ]; then printf '\x00' | dd of="$FILE" bs=1 seek=1 count=1 conv=notrunc 2>/dev/null echo "Autoplay was OFF ($BYTE). Patched to ON (00)." fi fi # 8. Calculate Game Timer (+10 second buffer) # Using awk to handle the float calc and integer addition in one step local timer_delay=16 sv_gametimer=$(du -m "$tmpvideo" | awk -v delay="$timer_delay" '{print int($1 * 7.5) + delay}') sv_title="${sv_selected%.*}" sv_title="${sv_title#*-}" sv_title="${sv_title//_/ }" # Check for TVC VCD JSON and override duration/title if available if [ -f "/tmp/.SAM_tmp/sv_core" ]; then local current_core=$(cat "/tmp/.SAM_tmp/sv_core") local vcd_json="${mrsampath}/tvc/${current_core}_tvc_vcd.json" if [ -f "$vcd_json" ]; then samdebug "Found VCD JSON: $vcd_json" local json_duration=$(jq -r --arg f "$sv_selected" '.[$f].duration // empty' "$vcd_json") local json_title=$(jq -r --arg f "$sv_selected" '.[$f].title // empty' "$vcd_json") if [[ -n "$json_duration" ]] && [[ "$json_duration" != "null" ]]; then sv_gametimer=$((json_duration + timer_delay)) samdebug "Duration set to $sv_gametimer (from JSON: $json_duration)" fi fi fi # 9. Show tty2oled splash if [ "${ttyenable}" == "yes" ]; then local tty_gamename="${sv_title}" tty_currentinfo=( [core_pretty]="${nextcore^} Commercial" [name]="${tty_gamename}" [core]=SAM_splash [date]=$EPOCHSECONDS [counter]=${sv_gametimer} [name_scroll]="${tty_gamename:0:21}" [name_scroll_position]=0 [name_scroll_direction]=1 [update_pause]=${ttyupdate_pause} ) declare -p tty_currentinfo | sed 's/declare -A/declare -gA/' >"${tty_currentinfo_file}" write_to_TTY_cmd_pipe "display_info" & SECONDS=$((EPOCHSECONDS - tty_currentinfo[date])) fi # 10. Play file local core_prefix="${sv_selected%%-*}" core_prefix="${core_prefix//_/ }" echo -e "Now playing: \e[1m${core_prefix} Commercial - ${sv_title}\e[0m" if [ -s /tmp/SAM_Game.mgl ]; then mv /tmp/SAM_Game.mgl /tmp/SAM_game.previous.mgl; fi { echo "" echo "_Unstable/CDi_unstable" echo "" } >/tmp/SAM_Game.mgl echo "load_core /tmp/SAM_Game.mgl" > /dev/MiSTer_cmd timeout 1s sh -c "echo 'load_core /tmp/SAM_Game.mgl' > /dev/MiSTer_cmd" # Wait for CD-i core to load before starting timer for i in {1..20}; do if grep -q "CD-i" /tmp/CORENAME 2>/dev/null; then samdebug "CD-i core is loaded" break fi sleep 1 done echo "$sv_gametimer" > "$sv_gametimer_file" unset sv_selected } function dl_video() { rm -f "$tmpvideo" if [ "$download_manager" = "yes" ]; then get_dlmanager # aria2c logic /media/fat/linux/aria2c \ --dir="$(dirname "$tmpvideo")" \ --file-allocation=none \ -o "$(basename "$tmpvideo")" \ -s 4 -x 4 -k 1M \ --summary-interval=0 \ --console-log-level=warn \ --download-result=hide \ --quiet=false \ --allow-overwrite=true \ --ca-certificate=/etc/ssl/certs/cacert.pem \ "${1}" else # wget logic wget -q --show-progress -O "$tmpvideo" "${1}" fi } function sv_ar_download() { local resolution="$1" # Resolution, 480 or 240 local list_file="$2" # Associated list file, sv_archive_hdmilist or sv_archive_crtlist samvideo_list="/tmp/.SAM_List/sv_archive_list.txt" local http_archive="${list_file//https/http}" # Populate the samvideo_list if it's empty if [ ! -s "${samvideo_list}" ]; then curl_download /tmp/SAMvideos.xml "${http_archive}" grep -o '/g; s/"/\"/g; s/#'/\'"'"'/g; s/“/\"/g; s/”/\"/g;' \ > "${samvideo_list}" fi # Select a video and check availability while true; do if [ "$samvideo_tvc" == "yes" ]; then samvideo_tvc else sv_selected="$(shuf -n1 "${samvideo_list}")" fi # Safety check if [ -z "$sv_selected" ]; then samdebug "Error: No video selected." return fi sv_selected_url="${http_archive%/*}/${sv_selected}" # Check Local Availability First local local_svfile="${samvideo_path}/$(echo "$sv_selected" | sed "s/[\":?]//g")" if [ -f "$local_svfile" ]; then samdebug "Local file found: $local_svfile. Skipping remote check." break fi # Check if the URL is available using wget samdebug "Checking availability of ${sv_selected_url}..." if wget --spider --quiet --timeout=10 --tries=1 "${sv_selected_url}"; then samdebug "URL is available: ${sv_selected_url}" break else samdebug "URL is not available: ${sv_selected_url}. Removing from list and selecting another." awk -v Line="$sv_selected" '!index($0, Line)' "${samvideo_list}" >"${tmpfile}" && cp -f "${tmpfile}" "${samvideo_list}" fi done tmpvideo="/tmp/SAMvideo.avi" samdebug "Checking if file is available locally...$local_svfile" if [ -f "$local_svfile" ]; then echo "Local file exists: $local_svfile" cp "$local_svfile" "$tmpvideo" else echo "Preloading ${sv_selected} from archive.org for smooth playback" dl_video "${sv_selected_url}" fi # Update samvideo_list to remove the processed file if [ "$samvideo_tvc" == "no" ]; then awk -vLine="$sv_selected" '!index($0,Line)' "${samvideo_list}" >${tmpfile} && cp -f ${tmpfile} "${samvideo_list}" fi # Set resolution-specific variables if [ "$resolution" -eq 480 ]; then res_space="640 480" else res_space="640 240" fi } function sv_local() { samvideo_list="/tmp/.SAM_List/sv_local_list.txt" if [ ! -s ${samvideo_list} ]; then find "$samvideo_path" -type f > ${samvideo_list} fi tmpvideo=$(cat ${samvideo_list} | shuf -n1) awk -vLine="$tmpvideo" '!index($0,Line)' ${samvideo_list} >${tmpfile} && cp -f ${tmpfile} ${samvideo_list} res="$(LD_LIBRARY_PATH=${mrsampath} ${mrsampath}/mplayer -vo null -ao null -identify -frames 0 "$tmpvideo" 2>/dev/null | grep "VIDEO:" | awk '{print $3}')" res_space=$(echo "$res" | tr 'x' ' ') sv_selected="$(basename "${tmpvideo}")" } function samvideo_tvc() { local tvc_suffix="_tvc.json" if [ "${samvideo_tvc_cdi}" == "yes" ]; then tvc_suffix="_tvc_vcd.json" fi if [ ! -f "${mrsampath}/tvc/nes${tvc_suffix}" ]; then get_tvc_files fi # Setting corelist to available commercials unset TVC_LIST unset SV_TVC_CL for g in "${!SV_TVC[@]}"; do for c in "${corelist[@]}"; do if [[ "$c" == "$g" ]]; then SV_TVC_CL+=("$c") fi done done samdebug "samvideo corelist: ${SV_TVC_CL[@]}" # NEW: Respect Single Core Mode if [ "$SAM_MODE" == "SINGLE" ] && [ -n "$SAM_TARGET_CORE" ]; then nextcore="$SAM_TARGET_CORE" samdebug "Single mode active. Forcing samvideo target: $nextcore" else pick_core SV_TVC_CL fi samdebug "nextcore = $nextcore" # Initialize variables count=0 local gamelist_tmp="${gamelistpathtmp}/${nextcore}${tvc_suffix}" local gamelist_original="${mrsampath}/tvc/${nextcore}${tvc_suffix}" # Ensure a local temporary copy exists or reset it if empty if [ ! -f "$gamelist_tmp" ] || [ ! -s "$gamelist_tmp" ] || [ "$(cat "$gamelist_tmp")" = "{}" ]; then samdebug "Copying original gamelist to temporary file: $gamelist_tmp" cp "$gamelist_original" "$gamelist_tmp" fi while [ $count -lt 15 ]; do if [ -f "$gamelist_tmp" ]; then samdebug "$gamelist_tmp found." # Select a random game and its corresponding entry sv_selected=$(jq -r 'keys[]' "$gamelist_tmp" | shuf -n 1) tvc_selected=$(jq -r --arg key "$sv_selected" '.[$key]' "$gamelist_tmp") # Remove the selected entry from the temporary file samdebug "Removing $sv_selected from $gamelist_tmp" jq --arg key "$sv_selected" 'del(.[$key])' "$gamelist_tmp" > "${gamelist_tmp}.tmp" && mv "${gamelist_tmp}.tmp" "$gamelist_tmp" # Save the selected game information if [ "${samvideo_tvc_cdi}" == "yes" ]; then echo "${tvc_selected}" | jq -r '.title' > /tmp/.SAM_tmp/sv_gamename else echo "${tvc_selected}" > /tmp/.SAM_tmp/sv_gamename fi break else # If the file is not found, select a new core randomly pick_core SV_TVC_CL samdebug "${nextcore}${tvc_suffix} not found, selecting new core." fi ((count++)) done echo $nextcore > /tmp/.SAM_tmp/sv_core samdebug "Searching for ${SV_TVC[$nextcore]}" if [ -z "${tvc_selected}" ]; then echo "Couldn't find TVC list. Selecting random game from system" sv_selected="$(cat ${samvideo_list} | grep -i "${SV_TVC[$nextcore]}" | shuf --random-source=/dev/urandom | head -1)" fi samdebug "Picked $sv_selected" } ## Play video function samvideo_play() { if [ "${samvideo_tvc_cdi}" == "yes" ]; then sv_ar_cdi_mode return fi if [ "${samvideo_source}" == "archive" ] && [ "$samvideo_output" == "hdmi" ]; then sv_ar_download 480 "${sv_archive_hdmilist}" elif [ "${samvideo_source}" == "archive" ] && [ "$samvideo_output" == "crt" ]; then sv_ar_download 240 "${sv_archive_crtlist}" elif [ "${samvideo_source}" == "local" ]; then sv_local fi if [ -z "${sv_selected}" ]; then echo "Error while downloading" echo "1" > "$sv_gametimer_file" return fi sv_gametimer="$(LD_LIBRARY_PATH=${mrsampath} ${mrsampath}/mplayer -vo null -ao null -identify -frames 0 "$tmpvideo" 2>/dev/null | grep "ID_LENGTH" | sed 's/[^0-9.]//g' | awk -F '.' '{print $1}')" sv_title="${sv_selected%.*}" #Show tty2oled splash if [ "${ttyenable}" == "yes" ]; then tty_currentinfo=( [core_pretty]="SAM Video Player" [name]="${sv_title}" [core]=SAM_splash [date]=$EPOCHSECONDS [counter]=${sv_gametimer} [name_scroll]="${sv_title:0:21}" [name_scroll_position]=0 [name_scroll_direction]=1 [update_pause]=${ttyupdate_pause} ) declare -p tty_currentinfo | sed 's/declare -A/declare -gA/' >"${tty_currentinfo_file}" tty_displayswitch=$(($gametimer / $ttycoresleep - 1)) write_to_TTY_cmd_pipe "display_info" & local elapsed=$((EPOCHSECONDS - tty_currentinfo[date])) SECONDS=${elapsed} fi if [ "$mute" != "no" ] || [ "$bgm" == "yes" ]; then options="-nosound" fi if [ -s "$tmpvideo" ]; then timeout 1s sh -c "echo 'load_core /media/fat/menu.rbf' > /dev/MiSTer_cmd" sleep "${samvideo_displaywait}" # TODO delete blinking cursor #echo "\033[?25l" > /dev/tty1 #setterm -cursor off echo $(("$sv_gametimer" + 2)) > "$sv_gametimer_file" ${mrsampath}/mbc raw_seq :43 vmode -r ${res_space} rgb32 echo -e "\nPlaying video now.\n" echo -e "Title: ${sv_selected%.*}" echo -e "Resolution: ${res_space}" echo -e "Length: ${sv_gametimer} seconds\n" nice -n -20 env LD_LIBRARY_PATH=${mrsampath} ${mrsampath}/mplayer -msglevel all=0:statusline=5 "${options}" "$tmpvideo" 2>/dev/null rm "$sv_gametimer_file" 2>/dev/null else echo "No video was downloaded. Skipping video playback.." echo "1" > "$sv_gametimer_file" return fi #echo load_core /media/fat/menu.rbf > /dev/MiSTer_cmd #next_core } # ======== SAM UPDATE ======== function curl_download() { # curl_download ${filepath} ${URL} curl \ --connect-timeout 15 --max-time 600 --retry 3 --retry-delay 5 --silent --show-error \ --insecure \ --fail \ --location \ -o "${1}" \ "${2}" } function check_and_update() { local url="$1" local tmp_file="$2" local local_file="$3" local description="$4" # Fetch the remote file size (follow redirects) remote_size=$(curl -sI --location --insecure "$url" | awk '/^Content-Length:/ {size=$2} END {print size}' | tr -d '\r') if [ -z "$remote_size" ]; then echo "Error: Unable to determine the size of $description at $url" >&2 return 1 fi # Get the local file size, if it exists if [ -f "$local_file" ]; then local_size=$(stat --format="%s" "$local_file") else local_size=0 fi # Debugging output samdebug "Remote size: $remote_size" samdebug "Local size: $local_size" # Compare sizes and update if needed if [ "$remote_size" -eq "$local_size" ]; then echo "$description is up-to-date. No update required." return 0 # File is up-to-date else echo "Updating $description..." curl_download "$tmp_file" "$url" || return 1 # Download failed mv "$tmp_file" "$local_file" || { echo "Error: Unable to move $tmp_file to $local_file" >&2; return 1; } echo "$description updated successfully." return 2 # File was updated fi } function get_samstuff() { #get_samstuff file (path) if [ -z "${1}" ]; then return 1 fi filepath="${2}" if [ -z "${filepath}" ]; then filepath="${mrsampath}" fi echo -n " Downloading from ${raw_base}/${1} to ${filepath}/..." curl_download "/tmp/${1##*/}" "${raw_base}/${1}" if [ ! "${filepath}" == "/tmp" ]; then mv --force "/tmp/${1##*/}" "${filepath}/${1##*/}" fi if [ "${1##*.}" == "sh" ]; then chmod +x "${filepath}/${1##*/}" fi echo " Done." } function get_samindex() { echo "Downloading samindex - needed for creating gamelists..." echo "Created for MiSTer by wizzo" echo "https://github.com/wizzomafizzo/mrext" # Define URLs and file paths latest_url="${raw_base}/.MiSTer_SAM/samindex" tmp_file="/tmp/samindex" local_file="${mrsampath}/samindex" # Check and update samindex check_and_update "$latest_url" "$tmp_file" "$local_file" "samindex" } function get_samvideo() { echo "Checking and updating components for SAM video playback..." echo "Created for MiSTer by wizzo" echo "https://github.com/wizzomafizzo/mrext" # Define URLs and file paths latest_mplayer="${raw_base}/.MiSTer_SAM/mplayer.zip" tmp_mplayer="/tmp/mplayer.zip" local_mplayer="${mrsampath}/mplayer.zip" # Check and update mplayer check_and_update "$latest_mplayer" "$tmp_mplayer" "$local_mplayer" "mplayer" result=$? if [ "$result" -eq 2 ] || [ ! -f "${mrsampath}/mplayer" ]; then echo "Extracting mplayer..." unzip -ojq "$local_mplayer" -d "${mrsampath}" || { echo "Error: Failed to extract mplayer.zip" >&2 } echo "mplayer updated and extracted successfully." fi } function get_mbc() { echo "Downloading mbc - Control MiSTer from cmd..." echo "Created for MiSTer by pocomane" remote_url="${raw_base}/.MiSTer_SAM/mbc" tmp_file="/tmp/mbc" local_file="${mrsampath}/mbc" check_and_update "$remote_url" "$tmp_file" "$local_file" "mbc" } function get_inputmap() { echo "Downloading input maps - needed to skip past BIOS for some systems..." [ ! -d "${configpath}/inputs" ] && mkdir -p "${configpath}/inputs" for input_file in \ "GBA_input_1234_5678_v3.map" \ "MegaCD_input_1234_5678_v3.map" \ "NES_input_1234_5678_v3.map" \ "TGFX16_input_1234_5678_v3.map" \ "NEOGEO_input_1234_5678_v3.map" \ "SATURN_input_1234_5678_v3.map"; do remote_url="${raw_base}/.MiSTer_SAM/inputs/$input_file" tmp_file="/tmp/$input_file" local_file="${configpath}/inputs/$input_file" check_and_update "$remote_url" "$tmp_file" "$local_file" "$input_file" done echo "Input maps updated." } function get_blacklist() { echo "Downloading blacklist files - SAM can auto-detect games with static screens and filter them out..." for blacklist_file in "${BLACKLIST_FILES[@]}"; do remote_url="${raw_base}/.MiSTer_SAM/SAM_Gamelists/$blacklist_file" tmp_file="/tmp/$blacklist_file" local_file="${mrsampath}/SAM_Gamelists/$blacklist_file" check_and_update "$remote_url" "$tmp_file" "$local_file" "$blacklist_file" done echo "Blacklist files updated." } function get_ratedlist() { echo "Downloading lists with kids-friendly games..." for rated_file in "${RATED_FILES[@]}"; do remote_url="${raw_base}/.MiSTer_SAM/SAM_Rated/$rated_file" tmp_file="/tmp/$rated_file" local_file="${mrsampath}/SAM_Rated/$rated_file" check_and_update "$remote_url" "$tmp_file" "$local_file" "$rated_file" done echo "Rated lists updated." } get_dlmanager() { if [ "$download_manager" = yes ]; then aria2_path="/media/fat/linux/aria2c" if [ ! -f "$aria2_path" ]; then aria2_urls=( "https://raw.githubusercontent.com/mrchrisster/0mhz-collection/main/aria2c/aria2c.zip.001" "https://raw.githubusercontent.com/mrchrisster/0mhz-collection/main/aria2c/aria2c.zip.002" "https://raw.githubusercontent.com/mrchrisster/0mhz-collection/main/aria2c/aria2c.zip.003" "https://raw.githubusercontent.com/mrchrisster/0mhz-collection/main/aria2c/aria2c.zip.004" ) echo "" echo -n "Installing aria2c Download Manager... " for url in "${aria2_urls[@]}"; do file_name=$(basename "${url%%\?*}") curl -s --insecure -L $url -o /tmp/"$file_name" if [ $? -ne 0 ]; then echo "Failed to download $file_name" download_manager=no fi done # Check if the download was successful if [ $? -eq 0 ]; then echo "Done." else echo "Failed." fi cat /tmp/aria2c.zip.* > /tmp/aria2c_full.zip unzip -qq -o /tmp/aria2c_full.zip -d /media/fat/linux fi fi } function get_tvc_files() { local target_dir="${mrsampath}/tvc" local api_url="https://api.github.com/repos/mrchrisster/MiSTer_SAM/contents/.MiSTer_SAM/tvc?ref=${branch}" local tmp_json="/tmp/tvc_files.json" mkdir -p "$target_dir" samdebug "Checking for TVC VCD JSON updates..." if curl -s -L --insecure -H "User-Agent: MiSTer_SAM" "$api_url" > "$tmp_json"; then # Check if valid JSON array using jq if jq -e '. | type == "array"' "$tmp_json" >/dev/null 2>&1; then # Parse filename jq -r '.[] | .name' "$tmp_json" | while read -r name; do if [ "$name" != "null" ]; then samdebug "Updating $name..." curl_download "${target_dir}/${name}" "${raw_base}/.MiSTer_SAM/tvc/${name}" fi done samdebug "TVC VCD JSON update complete." else samdebug "Error: Failed to fetch TVC file list from GitHub (Invalid JSON)." local err_msg=$(jq -r '.message // empty' "$tmp_json" 2>/dev/null) if [ -n "$err_msg" ]; then samdebug "GitHub API Message: $err_msg" else samdebug "Response content: $(cat "$tmp_json")" fi fi else samdebug "Error: Could not connect to GitHub API for TVC updates." fi rm -f "$tmp_json" } function sam_update() { # sam_update (next command) if ping -4 -q -w 1 -c 1 github.com > /dev/null; then echo " Connection established" else echo "No connection to Github. Please use offline install." sleep 5 #exit 1 fi # Ensure the MiSTer SAM data directory exists mkdir --parents "${mrsampath}" &>/dev/null mkdir --parents "${mrsampath}/SAM_Rated" &>/dev/null mkdir --parents "${gamelistpath}" &>/dev/null if [ ! "$(dirname -- "${0}")" == "/tmp" ]; then # Warn if using non-default branch for updates if [ ! "${branch}" == "main" ]; then echo "" echo "*******************************" echo " Updating from ${branch}" echo "*******************************" echo "" fi # Download the newest MiSTer_SAM_on.sh to /tmp get_samstuff MiSTer_SAM_on.sh /tmp if [ -f /tmp/MiSTer_SAM_on.sh ]; then if [ "${1}" ]; then echo " Continuing setup with latest MiSTer_SAM_on.sh..." /tmp/MiSTer_SAM_on.sh "${1}" exit 0 else echo " Launching latest" echo " MiSTer_SAM_on.sh..." /tmp/MiSTer_SAM_on.sh update exit 0 fi else # /tmp/MiSTer_SAM_on.sh isn't there! echo " SAM update FAILED" echo " No Internet?" exit 1 fi else # We're running from /tmp - download dependencies and proceed cp --force "/tmp/MiSTer_SAM_on.sh" "/media/fat/Scripts/MiSTer_SAM_on.sh" get_samindex get_mbc #get_samstuff .MiSTer_SAM/MiSTer_SAM.default.ini get_samstuff .MiSTer_SAM/MiSTer_SAM_init get_samstuff .MiSTer_SAM/MiSTer_SAM_MCP.py get_samstuff .MiSTer_SAM/MiSTer_SAM_menu.sh get_samstuff .MiSTer_SAM/MiSTer_SAM_tty2oled if [ ! -f "${mrsampath}/sam_controllers.json" ]; then get_samstuff .MiSTer_SAM/sam_controllers.json fi get_samvideo get_dlmanager get_inputmap get_blacklist get_ratedlist get_tvc_files get_samstuff MiSTer_SAM_off.sh /media/fat/Scripts if [ -f "${samini_file}" ]; then echo " MiSTer SAM INI already exists... Merging with new ini." get_samstuff MiSTer_SAM.ini /tmp echo " Backing up MiSTer_SAM.ini to MiSTer_SAM.ini.bak" cp "${samini_file}" "${samini_file}".bak echo -n " Merging ini values.." # In order for the following awk script to replace variable values, we need to change our ASCII art from "=" to "-" sed -i 's/==/--/g' "${samini_file}" sed -i 's/-=/--/g' "${samini_file}" awk -F= 'NR==FNR{a[$1]=$0;next}($1 in a){$0=a[$1]}1' "${samini_file}" /tmp/MiSTer_SAM.ini >/tmp/MiSTer_SAM.tmp && cp -f --force /tmp/MiSTer_SAM.tmp "${samini_file}" echo " Warning: Overwriting ini in test branch!" get_samstuff MiSTer_SAM.ini /media/fat/Scripts echo "Done." else get_samstuff MiSTer_SAM.ini /media/fat/Scripts fi fi echo " Update complete!" return mcp_start if [ "${inmenu}" -eq 1 ]; then sleep 1 sam_menu fi } # ========= MAIN ========= init_vars read_samini init_paths init_data if [[ "$update_gamelists_during_play" == "Yes" ]]; then schedule_gamelist_updates fi if [ "${1,,}" != "--source-only" ]; then parse_cmd "${@}" # Parse command line parameters for input fi