#!/usr/bin/env bash # shellcheck disable=SC1091 # Get script directory for relative paths scriptDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Default to running normally unless explicitly set by a caller. agmNoLaunch="${agmNoLaunch:-false}" # Dialog accessibility export DIALOGOPTS='--no-lines --visit-items' # Wine32 version for SAPI compatibility wineThirtyTwoVersion="9.0" # Track nvda2speechd lifecycle for this launcher run. nvda2speechdPid="" nvda2speechdStarted="false" customLaunchHandled="false" cthulhuTitleReaderEnabled="false" cthulhuTitleReaderTried="false" log_msg() { local logFile="${scriptDir}/game.log" local timestamp="" timestamp="$(date)" echo "${1} [${timestamp}]" >> "$logFile" } start_nvda2speechd() { if [[ "$nvda2speechdStarted" == "true" ]]; then return fi if ! ss -ltnp | rg 3457 | grep -q 'cthulhu'; then if [[ -x "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" ]]; then local translateSetting="${TRANSLATE:-unset}" local translateFromSetting="${TRANSLATE_FROM:-unset}" local translateToSetting="${TRANSLATE_TO:-unset}" log_msg "Preparing nvda2speechd with TRANSLATE=${translateSetting} TRANSLATE_FROM=${translateFromSetting} TRANSLATE_TO=${translateToSetting}." if [[ "${translateSetting,,}" == "true" ]] || [[ "$translateSetting" == "1" ]]; then if ! command -v trans &> /dev/null; then log_msg "TRANSLATE is enabled but 'trans' is missing; nvda2speechd may fail to start." fi fi "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" &> /dev/null & nvda2speechdPid=$! nvda2speechdStarted="true" log_msg "Started nvda2speechd (pid ${nvda2speechdPid})." else log_msg "nvda2speechd binary not found or not executable; skipping start." fi else log_msg "cthulhu is already listening on port 3457; skipping nvda2speechd start." fi } stop_nvda2speechd() { if [[ "$nvda2speechdStarted" != "true" ]]; then return fi if [[ -n "$nvda2speechdPid" ]] && kill -0 "$nvda2speechdPid" 2>/dev/null; then kill "$nvda2speechdPid" 2>/dev/null wait "$nvda2speechdPid" 2>/dev/null fi nvda2speechdPid="" nvda2speechdStarted="false" } enable_cthulhu_window_title_reader() { if [[ "$cthulhuTitleReaderEnabled" == "true" ]]; then return 0 fi if [[ "$cthulhuTitleReaderTried" == "true" ]]; then return 1 fi cthulhuTitleReaderTried="true" if ! command -v gdbus &> /dev/null; then log_msg "gdbus not found; cannot enable Cthulhu WindowTitleReader." return 1 fi local dbusService="org.stormux.Cthulhu.Service" local modulePath="/org/stormux/Cthulhu/Service/PluginSystemManager" local titleModulePath="/org/stormux/Cthulhu/Service/Plugin_WindowTitleReader" local moduleInterface="org.stormux.Cthulhu.Module" local enableResult="" local titleEnableResult="" gdbus call --session --dest "$dbusService" \ --object-path "$modulePath" \ --method "${moduleInterface}.ExecuteCommand" \ "RescanPlugins" false &> /dev/null || true enableResult="$(gdbus call --session --dest "$dbusService" \ --object-path "$modulePath" \ --method "${moduleInterface}.ExecuteParameterizedCommand" \ "SetPluginActive" \ '{"plugin_name": <"WindowTitleReader">, "active": }' \ false 2>/dev/null || true)" if [[ "$enableResult" != *"true"* ]]; then log_msg "Failed to activate Cthulhu WindowTitleReader plugin via D-Bus." return 1 fi titleEnableResult="$(gdbus call --session --dest "$dbusService" \ --object-path "$titleModulePath" \ --method "${moduleInterface}.ExecuteParameterizedCommand" \ "SetEnabled" \ '{"enabled": }' \ false 2>/dev/null || true)" if [[ "$titleEnableResult" == *"true"* ]]; then cthulhuTitleReaderEnabled="true" log_msg "Enabled Cthulhu WindowTitleReader tracking via D-Bus." return 0 fi log_msg "Failed to enable Cthulhu WindowTitleReader tracking via D-Bus." return 1 } kill_nvda2speechd_listener() { local nvdaPids=() mapfile -t nvdaPids < <(pgrep -u "${USER}" -x nvda2speechd 2>/dev/null) if [[ ${#nvdaPids[@]} -eq 0 ]]; then log_msg "No nvda2speechd process found to stop." return fi log_msg "Stopping nvda2speechd process(es): ${nvdaPids[*]}." for pid in "${nvdaPids[@]}"; do kill "$pid" 2>/dev/null done local remainingPids=() mapfile -t remainingPids < <(pgrep -u "${USER}" -x nvda2speechd 2>/dev/null) if [[ ${#remainingPids[@]} -gt 0 ]]; then log_msg "nvda2speechd still running after stop attempt: ${remainingPids[*]}." fi } cleanup_and_exit() { stop_nvda2speechd } # Check and manage wine32 installation check_wine32() { local wine32Dir="${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/wine32" local versionFile="$wine32Dir/VERSION" local currentVersion="" # Check current installed version [[ -f "$versionFile" ]] && currentVersion=$(cat "$versionFile") # If version mismatch or missing, install/update if [[ "$currentVersion" != "$wineThirtyTwoVersion" ]]; then # Remove old installation rm -rf "$wine32Dir" 2>/dev/null mkdir -p "$wine32Dir" # Download wine32 using existing download function local wineFile="PlayOnLinux-wine-${wineThirtyTwoVersion}-upstream-linux-x86.tar.gz" local wineUrl="https://www.playonlinux.com/wine/binaries/phoenicis/upstream-linux-x86/${wineFile}" # Use existing download function which handles progress download "$wineUrl" # Extract with progress feedback { echo "# Extracting Wine32 version $wineThirtyTwoVersion..." if tar xf "${cache}/${wineFile}" -C "$wine32Dir" --strip-components=1; then echo "$wineThirtyTwoVersion" > "$versionFile" echo "# Wine32 setup complete." else echo "# Failed to extract wine32. SAPI games may not work properly." rm -rf "$wine32Dir" fi } | agm_progressbox "Wine32 Setup" "Extracting Wine32 for SAPI compatibility..." fi # Export wine32 path for bottle creation if [[ -f "$wine32Dir/bin/wine" ]]; then export wine32="$wine32Dir/bin/wine" export wine32server="$wine32Dir/bin/wineserver" fi } # Ensure wine64 bottle exists with all dependencies (wine32 no longer needed!) ensure_wine_bottles() { local wine64Bottle="$HOME/.local/wine64" # Create wine64 bottle if missing - now includes SAPI support via WINETRICKS_FORCE=1 if [[ ! -d "$wine64Bottle" ]] || [[ ! -f "$wine64Bottle/system.reg" ]]; then { echo "# Creating wine64 bottle for modern games..." # Set up environment for wine64 export WINEPREFIX="$wine64Bottle" export WINE="wine" export WINESERVER="wineserver" unset WINEARCH # Initialize wine64 bottle echo "# Initializing wine64 environment..." DISPLAY="" wine wineboot -u # Install mono and gecko echo "# Installing .NET Framework..." monoPath="$(find /usr/share/wine/ -maxdepth 1 -type d -name mono 2> /dev/null)" if [[ -z "$monoPath" ]]; then download 'http://dl.winehq.org/wine/wine-mono/6.0.0/wine-mono-6.0.0-x86.msi' monoPath="${cache}/wine-mono-6.0.0-x86.msi" fi wine msiexec /i z:"$monoPath" /quiet echo "# Installing web browser support..." geckoPath="$(find /usr/share/wine/ -maxdepth 1 -type d -name "gecko" 2> /dev/null)" if [[ -z "$geckoPath" ]]; then download 'http://dl.winehq.org/wine/wine-gecko/2.40/wine_gecko-2.40-x86.msi' geckoPath="${cache}/wine_gecko-2.40-x86.msi" fi wine msiexec /i z:"$geckoPath" /quiet # Install common dependencies for modern games echo "# Installing common dependencies..." winetricks -q isolate_home corefonts vcrun2019 win10 # Install Speech SDK for SAPI compatibility (experimental - requires WINETRICKS_FORCE=1) echo "# Installing Speech SDK for wine64 SAPI support..." env WINE="$WINE" WINESERVER="$WINESERVER" DISPLAY="${DISPLAY:-:0}" WINETRICKS_FORCE=1 winetricks -q speechsdk # Restore win10 after speechsdk (speechsdk sets winxp) winetricks -q win10 # Initialize SAPI and set Microsoft Mike as default voice echo "# Setting Microsoft Mike as default voice..." mkdir -p "${WINEPREFIX}/drive_c/windows/temp" cat << "EOF" > "${WINEPREFIX}/drive_c/windows/temp/init_sapi.vbs" dim speechobject set speechobject=createobject("sapi.spvoice") speechobject.speak "" EOF wine cscript "c:\\windows\\temp\\init_sapi.vbs" wine reg add "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTokenId" /t REG_SZ /d "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\MSMike" /f wine reg add "HKCU\\SOFTWARE\\Microsoft\\Speech\\Voices" /v "DefaultTTSRate" /t REG_DWORD /d "7" /f echo "Set Microsoft Mike as default voice for wine64" # Setup nvda2speechd for accessibility if needed if ! ss -ltnp | rg 3457 | grep -q 'cthulhu'; then echo "# Setting up accessibility support..." download "${nvda2speechdBinary}" if [[ ! -f "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" ]]; then cp "${cache}/nvda2speechd" "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" chmod +x "${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager/nvda2speechd" fi fi echo "# Wine64 bottle creation complete." } > >(agm_progressbox "Wine Bottle Setup" "Creating unified wine64 bottle with SAPI support (this may take several minutes)...") fi } # Install games game_installer() { export LANG="en_US.UTF-8" # Get list of installed games from config file mapfile -t installedGames < <(sed -e '/^$/d' -e '/^[[:space:]]*#/d' "${configFile}" 2> /dev/null | cut -d '|' -f3) # Create the menu of available games by reading from .install directory declare -a menuList # Get all .sh files from .install directory, excluding those starting with # as first line. mapfile -t sortedGames < <(for f in "${scriptDir}/.install/"*.sh; do # Skip if first line starts with # [[ $(head -n1 "$f") == "#"* ]] && continue echo "${f##*/%.sh}" done | sort) for i in "${sortedGames[@]}"; do local menuItem="${i%.sh}" menuItem="${menuItem##*/}" # Check if game is already installed for j in "${installedGames[@]}"; do if [[ "$j" == "$menuItem" ]]; then unset menuItem break fi done # Add to menu if not installed if [[ -n "$menuItem" ]]; then menuList+=("$menuItem" "$menuItem") fi done # If all games are installed, exit if [[ ${#menuList[@]} -eq 0 ]]; then agm_msgbox "Audio Game Installer" "" "All games are already installed." exit 0 fi menuList+=("Donate" "Donate") menuList+=("Become a Patron" "Become a Patron") # Show game selection dialog local game if ! game="$(agm_menu "Audio Game Installer" "Audio Game Installer" "Please select a game to install" "${menuList[@]}")"; then exit 0 fi # Handle selection if [[ -n "$game" ]]; then case "$game" in "Donate") open_url "https://ko-fi.com/stormux" exit 0 ;; "Become a Patron") open_url "https://patreon.com/stormux" exit 0 ;; *) # Convert game name to filename format local installScript="${scriptDir}/.install/${game}.sh" # Check if install script exists if [[ -f "$installScript" ]]; then # Source and execute the install script # shellcheck disable=SC1090 source "$installScript" # Show success message agm_msgbox "Installation Complete" "Audio Game Installer" "Game \"${game}\" has been successfully installed." else agm_msgbox "Audio Game Installer" "Audio Game Installer" "Installation script not found for ${game}" exit 1 fi ;; esac fi } # remove games game_removal() { # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" # Modern wine is unified - no architecture-specific wine executables needed mapfile -t lines < <(sed -e '/^$/d' -e '/^[[:space:]]*#/d' "${configFile}" 2> /dev/null | sort -t '|' -k3,3f) if [[ ${#lines} -eq 0 ]]; then agm_msgbox "Audio Game Removal" "" "No games found." exit 0 fi # Create the menu of installed games declare -a menuList for i in "${lines[@]}" ; do IFS='|' read -ra gameInfo <<< "$i" menuList+=("${gameInfo[0]}|${gameInfo[1]}" "${gameInfo[2]}") done menuList+=("Donate" "Donate") menuList+=("Become a Patron" "Become a Patron") local game game="$(agm_menu "Audio Game Removal" "Audio Game Removal" "Please select a game to delete" "${menuList[@]}")" if [[ ${#game} -gt 0 ]]; then if [[ "$game" == "Donate" ]]; then open_url "https://ko-fi.com/stormux" exit 0 fi if [[ "$game" == "Become a Patron" ]]; then open_url "https://2mb.games/product/2mb-patron/" exit 0 fi # Parse game info before create_game_array overwrites $game local selectedGame="$game" local winePath="${game#*|}" export winePath="${winePath%\\*.exe}" local wineExec="${game#*|}" wineExec="${wineExec%|*}" wineExec="${wineExec##*\\}" # With shared bottles, always remove only the game files, never the entire bottle create_game_array "$selectedGame" if [[ ${#game[@]} -gt 0 ]]; then # Set up wine environment for this game # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" get_bottle "${game[0]}" if ! agm_yesno "Confirm Removal" "Audio Game Removal" "Are you sure you want to remove \"${game[2]}\"?"; then agm_msgbox "Audio Game Removal" "" "Removal cancelled." exit 0 fi # kill any previous existing wineservers for this prefix in case they didn't shut down properly. wineserver -k # Remove only the game's installation directory if [[ -n "$winePath" ]]; then local gameDir gameDir="$(winepath "$winePath")" if [[ -d "$gameDir" ]]; then rm -rfv "$gameDir" | agm_progressbox "Removing Game" "Removing \"${game[2]}\" files..." else agm_msgbox "Game Removal" "" "Game directory not found, skipping file removal." fi fi fi # remove the launcher using the original selected game info gawk -i inplace -vLine="${selectedGame//\\/\\\\}" '!index($0,Line)' "$configFile" agm_msgbox "Game Removal" "" "\"${game[2]}\" has been successfully removed." fi exit 0 } # kill games that are stuck kill_game() { # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" # Modern wine is unified - no architecture-specific wine executables needed mapfile -t lines < <(sed '/^$/d' "${configFile}" 2> /dev/null | sort -t '|' -k3,3f) if [[ ${#lines} -eq 0 ]]; then agm_msgbox "Audio Game Killer" "" "No games found." exit 0 fi # Create the menu of installed games declare -a menuList for i in "${lines[@]}" ; do IFS='|' read -ra gameInfo <<< "$i" menuList+=("${gameInfo[0]}|${gameInfo[1]}" "${gameInfo[2]}") done menuList+=("Donate" "Donate") menuList+=("Become a Patron" "Become a Patron") local game game="$(agm_menu "Audio Game Killer" "Audio Game Killer" "Please select a game to force stop" "${menuList[@]}")" if [[ ${#game} -gt 0 ]]; then if [[ "$game" == "Donate" ]]; then open_url "https://ko-fi.com/stormux" exit 0 fi if [[ "$game" == "Become a Patron" ]]; then open_url "https://2mb.games/product/2mb-patron/" exit 0 fi local winePath="${game#*|}" winePath="${winePath%\\*.exe}" local wineExec="${game#*|}" wineExec="${wineExec%|*}" wineExec="${wineExec##*\\}" # kill the wine server. get_bottle "${game%|*}" wineserver -k agm_msgbox "Audio Game Killer" "" "The selected game has been stopped." fi exit 0 } # for games that require custom scripts before launch or custom launch parameters custom_launch_parameters() { if [[ "${game[2]}" == "Dragon Pong" ]]; then if ! enable_cthulhu_window_title_reader; then "${scriptDir}/speech/speak_window_title.sh" DragonPong.exe & fi start_nvda2speechd pushd "$(winepath "$winePath")" || exit 1 wine "$wineExec" popd || exit 1 customLaunchHandled="true" return 0 fi if [[ "${game[2]}" == "Dreamland" ]]; then start_nvda2speechd "$WINE" "${game[1]}" &> /dev/null customLaunchHandled="true" return 0 fi # executioner's-rage: DLL replacement now handled by update_nvda_dlls() if [[ "${game[2]}" == "Laser Breakout" ]]; then if ! enable_cthulhu_window_title_reader; then "${scriptDir}/speech/speak_window_title.sh" play.exe & fi fi if [[ "${game[2]}" == "Bokurano Daibouken 2" ]]; then export TRANSLATE=true fi if [[ "${game[2]}" == "Bokurano Daibouken" ]]; then export TRANSLATE=true fi if [[ "${game[2]}" == "Bokurano Daibouken 3" ]]; then dictPath="$(winepath "${winePath}")" dataDir="${WINEPREFIX}/drive_c/nyanchangame/bk3/data" if [[ -r "${cache}/bk3-dict.dat" ]]; then if [[ ! -d "${dictPath}/dict" ]]; then cp "${cache}/bk3-dict.dat" "${dictPath}/dict.dat" fi if [[ -r "${cache}/nvdaControllerClient32.dll" ]]; then cp "${cache}/nvdaControllerClient32.dll" "${dataDir}/nvdaControllerClient32.dll" fi else rm -f "${dataDir}/nvdaControllerClient32.dll" find "${WINEPREFIX}/drive_c/nyanchangame/bk3" -type f -name 'nvdaControllerClient.dll' -exec rm -v "{}" \; fi # DLL replacement now handled by update_nvda_dlls() if [[ ! -d "${dictPath}/dict" ]]; then "${scriptDir}/speech/clipboard_translator.sh" "play.exe" bokurano-daibouken3 & fi fi if [[ "${game[2]}" == "Bop It Emulator" ]]; then if ! enable_cthulhu_window_title_reader; then "${scriptDir}/speech/speak_window_title.sh" bop.exe & fi fi if [[ "${game[2]}" == "Road to Rage" ]]; then if ! enable_cthulhu_window_title_reader; then "${scriptDir}/speech/speak_window_title.sh" trtr.exe & fi fi if [[ "${game[2]}" == "Axel Pong" ]]; then if ! enable_cthulhu_window_title_reader; then "${scriptDir}/speech/speak_window_title.sh" axel_pong.exe & fi fi if [[ "${game[2]}" == "Sequence Storm" ]]; then "${scriptDir}/speech/clipboard_reader.sh" SequenceStorm & fi #if [[ "${game[2]}" == "Shadow Line" ]]; then #find "${WINEPREFIX}/drive_c/" -type f -name 'nvdaControllerClient.dll' -exec rm -v "{}" \; #"${scriptDir}/speech/clipboard_translator.sh" play_sr.exe shadow-line & #fi # sketchbook: DLL replacement now handled by update_nvda_dlls() if [[ "${game[2]}" == "Audiodisc" ]]; then start_nvda2speechd wine "$winePath\\$wineExec" customLaunchHandled="true" return 0 fi if [[ "${game[2]}" == "Audioquake" ]]; then start_nvda2speechd wine "$winePath\\$wineExec" customLaunchHandled="true" return 0 fi if [[ "${game[2]}" == "Screaming Strike 2" ]]; then start_nvda2speechd pushd "$(winepath "$winePath")" || exit 1 wine "$wineExec" popd || exit 1 customLaunchHandled="true" return 0 fi if [[ "${game[2]}" == "Warsim" ]]; then start_nvda2speechd pushd "$(winepath "${game[1]%\\*}")" || exit 1 wine "${game[1]##*\\}" popd || exit 1 customLaunchHandled="true" return 0 fi if [[ "${game[2]}" == "Interceptor" ]]; then start_nvda2speechd pushd "$(winepath "$winePath")" || exit 1 wine "$wineExec" popd || exit 1 customLaunchHandled="true" return 0 fi } # Process game launcher flags process_launcher_flags() { flags=("${game[@]:3}") for i in "${flags[@]}" ; do if [[ "${i}" =~ ^export\ [a-zA-Z_][a-zA-Z0-9_]*=\'?.*\'?$ ]]; then eval "${i}" fi done } create_game_array() { # Game array 0 bottle, 1 path, 2 title, 3+ flags local selectedGame="$1" for i in "${lines[@]}" ; do # Only compare the launcher section local j="${selectedGame#*|}" local k="${i#*|}" k="${k%%|*}" if [[ "$j" == "$k" ]]; then IFS='|' read -ra game <<< "$i" break fi done } # Update NVDA controller client DLLs in wine bottles update_nvda_dlls() { # Ensure we have the replacement DLLs # shellcheck source=.includes/functions.sh source "${scriptDir}/.includes/functions.sh" download "${nvdaControllerClient32Dll}" "${nvdaControllerClient64Dll}" # Update wine64 bottle (most common) if [[ -d "$HOME/.local/wine64" ]]; then find "$HOME/.local/wine64" -type f \( -iname "nvdaControllerClient*.dll" \) -print0 | while IFS= read -r -d '' dllFile; do local basename="${dllFile##*/}" local replacement="" case "${basename,,}" in "nvdacontrollerclient32.dll") replacement="${cache}/nvdaControllerClient32.dll" ;; "nvdacontrollerclient64.dll") replacement="${cache}/nvdaControllerClient64.dll" ;; "nvdacontrollerclient.dll") # Use file command to detect architecture if file "$dllFile" | grep -q "PE32+"; then replacement="${cache}/nvdaControllerClient64.dll" elif file "$dllFile" | grep -q "PE32"; then replacement="${cache}/nvdaControllerClient32.dll" fi ;; esac if [[ -n "$replacement" ]] && [[ -f "$replacement" ]]; then echo "Updating $dllFile with nvdaControllerClient" cp "$replacement" "$dllFile" fi done fi # Note: 32-bit games running via WOW64 in wine64 bottle are handled above # The 'file' command detects PE32 vs PE32+ and applies the correct DLL version } # launch games that are installed game_launcher() { # For use by update scripts that want to source functions in this file. [[ "$agmNoLaunch" == "true" ]] && return # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" # Ensure a clean nvda2speechd state before launching a new game. kill_nvda2speechd_listener # Replace NVDA controller client DLLs in wine64 bottle update_nvda_dlls mapfile -t lines < <(sed -e '/^$/d' -e '/^ *#/d' "${configFile}" 2> /dev/null | sort -t '|' -k3,3f) if [[ ${#lines} -eq 0 ]]; then agm_msgbox "Audio Game Launcher" "" "Install some games first." exit 0 fi if [[ $# -eq 0 ]]; then # Create the menu of installed games declare -a menuList for i in "${lines[@]}" ; do IFS='|' read -ra gameInfo <<< "$i" menuList+=("${gameInfo[0]}|${gameInfo[1]}" "${gameInfo[2]}") done menuList+=("Donate" "Donate") menuList+=("Become a Patron" "Become a Patron") # Loop to handle documentation views local selectedGame="" local menuCode=0 while true; do selectedGame="$(agm_game_menu "Audio Game Launcher" "Audio Game Launcher" "Please select a game to play" "${menuList[@]}")" menuCode=$? if [[ $menuCode -eq 1 ]] || [[ $menuCode -eq 255 ]]; then exit 0 elif [[ $menuCode -eq 2 ]] || [[ $menuCode -eq 3 ]]; then # Documentation button pressed if [[ -n "$selectedGame" ]]; then # shellcheck source=.includes/help.sh source "${scriptDir}/.includes/help.sh" documentation "$selectedGame" "$(echo "$selectedGame" | cut -d '|' -f2)" fi # Return to menu after viewing docs continue else # dialog mode with OK (0) - proceed with launch break fi done # Safety check: don't proceed if game is empty if [[ -z "$selectedGame" ]]; then exit 0 fi create_game_array "$selectedGame" else create_game_array "$1" if [[ ${#game[@]} -eq 0 ]]; then agm_msgbox "Audio Game Launcher" "" "Game $1 not found." exit 1 fi fi if [[ ${#game[@]} -gt 0 ]]; then if [[ "${game[0]}" == "Donate" ]]; then open_url "https://ko-fi.com/stormux" exit 0 fi if [[ "${game[0]}" == "Become a Patron" ]]; then open_url "https://2mb.games/product/2mb-patron/" exit 0 fi get_bottle "${game[0]}" echo -n "launching " wine --version # kill any previous existing wineservers for this prefix in case they didn't shut down properly. wineserver -k # Set default path/exec for custom launch handlers. winePath="${game[1]%\\*.exe}" wineExec="${game[1]##*\\}" # launch the game if command -v qjoypad &> /dev/null ; then mkdir -p ~/.qjoypad3 touch "${HOME}/.qjoypad3/${game[2]}.lyt" # A | separated list of games that should not start with qjoypad. noQjoypadGames="A Hero's Call" if [[ "${noqjoypad}" != "true" ]] && ! [[ "${game[2]}" =~ ${noQjoypadGames} ]]; then if pgrep qjoypad &> /dev/null ; then qjoypad -T "${game[2]}" 2> /dev/null else qjoypad -T "${game[2]}" 2> /dev/null & fi fi fi process_launcher_flags customLaunchHandled="false" custom_launch_parameters if [[ "$customLaunchHandled" == "true" ]]; then exit 0 fi start_nvda2speechd # Change to game directory before launching (required for proper game operation) pushd "$(winepath "${game[1]%\\*}")" > /dev/null || exit 1 if [[ "$debugGdb" == "1" ]]; then winedbg --gdb "${game[1]##*\\}" else # Use direct wine execution instead of 'wine start' to ensure clipboard works # See: https://bugs.winehq.org/show_bug.cgi?id=50598 wine "${game[1]##*\\}" fi popd > /dev/null || exit 1 fi exit 0 } # main script trap cleanup_and_exit EXIT trap "cleanup_and_exit; exit 0" SIGINT SIGTERM # Detect dialog interface type BEFORE potentially setting DISPLAY # This must happen before we modify DISPLAY to preserve console detection # shellcheck disable=SC2034 if [[ -z "$DISPLAY" ]]; then dialogType="dialog" elif command -v yad &> /dev/null; then dialogType="yad" else dialogType="dialog" fi # Source dialog interface early for progress display # shellcheck source=.includes/dialog-interface.sh source "${scriptDir}/.includes/dialog-interface.sh" # If display isn't set assume we are launching from console and an X environment is running using display :0 if [[ -z "$DISPLAY" ]]; then export DISPLAY=":0" fi # Settings file export cache="${XDG_CACHE_HOME:-$HOME/.cache}/audiogame-manager" export configFile="${XDG_CONFIG_HOME:-$HOME/.config}/storm-games/audiogame-manager/games.conf" mkdir -p "${cache}" mkdir -p "${configFile%/*}" # Create the path for AGM's helper programs. # Originally this was only winetricks, thus the name, and I'm too lazy to change it now. winetricksPath="${XDG_DATA_HOME:-$HOME/.local/share}/audiogame-manager" mkdir -p "${winetricksPath}" # Load any arguments from settings.conf file if [[ -r "${configFile%/*}/settings.conf" ]]; then # shellcheck disable=SC1091 source "${configFile%/*}/settings.conf" fi # Update the cache for older versions of audiogame-manager if [[ -d "${configFile%/*}/cache" ]]; then { mv -v "${configFile%/*}/cache/"* "${cache}" rmdir -v "${configFile%/*}/cache/"; } | agm_progressbox "Audiogame Manager" "Updating cache, please wait..." fi # shellcheck disable=SC2034 checkWinetricksUpdate="false" # Turn off debug messages export WINEDEBUG="${winedebug:--all}" # During installation, you can set winVer to the versions available. # To set winetricks arguments, such as virtual desktop, set the winetricksSettings variable. # Example: winetricksSettings="vd=1024x768" # Files are cached unless -N no cache is set. unset noCache # Manual installation is not default, make sure it's unset unset manualInstall unset version # ipfs gateway export ipfsGateway="${ipfsGateway:-https://ipfs.stormux.org}" # nvda2speechd server startup is now handled in game_launcher() # Source helper functions # shellcheck source=.includes/bottle.sh source "${scriptDir}/.includes/bottle.sh" # Also sourced in functions that need it # shellcheck source=.includes/desktop.sh source "${scriptDir}/.includes/desktop.sh" # dialog-interface.sh already sourced earlier # shellcheck source=.includes/functions.sh source "${scriptDir}/.includes/functions.sh" # shellcheck source=.includes/help.sh source "${scriptDir}/.includes/help.sh" # shellcheck source=.includes/update.sh source "${scriptDir}/.includes/update.sh" # Set NVDA controller client DLLs from centralized ipfs array # shellcheck disable=SC2154 export nvdaControllerClient32Dll="${ipfs[nvdaControllerClient32]}" export nvdaControllerClient64Dll="${ipfs[nvdaControllerClient64]}" export nvda2speechdBinary="${ipfs[nvda2speechd]}" # Check minimum requirements check_requirements || exit 1 # Wine32 no longer needed - all games use wine64 with SAPI support via WINETRICKS_FORCE=1 # check_wine32 # Disabled - wine32 eliminated 2025-12-06 # Ensure wine bottles exist with dependencies ensure_wine_bottles # Check for updates update # Get latest news if available check_news # With no arguments, open the game launcher. if [[ $# -eq 0 ]]; then game_launcher fi # Array of command line arguments declare -A command=( [c]="Check your system for necessary components." [C]="Clear the cache. All game installers will be deleted." [D]="Create desktop shortcut. You can launch audiogame-manager from the desktop or applications menu." [d]="Debug mode, wine will be much more verbose when games are installed with this flag." [g]="Debug with GDB. Launch game with winedbg --gdb for step-through debugging." [h]="This help screen." [i]="Install games." [I:]="Noninteractive game installation." [k]="Kill a running game that is stuck." [L]="Display license information." [l:]="Launch given game without interactive audiogame-manager menu specified by its wine bottle." [N]="No cache, delete the installer after it has been extracted." [n]="No RHVoice, do not install RHVoice when the game is installed." [P]="Print a list of packages required by audiogame-manager." [q]="No qjoypad. Does not launch qjoypad if it is detected." [R]="Redownload. Removes old versions of packages from cache before installing." [r]="Remove a game. This will delete all game data." [S:]="Speech rate. Requires -V voice name, the default is 7. Values 0-9 or A." [t]="Total games. Show how many games are currently available." [v:]="Select the voice to be installed, default is Bdl. Options are alan, bdl, clb, or slt." [V:]="Select the default voice for a bottle." ) # Convert the keys of the associative array to a format usable by getopts args="${!command[*]}" args="${args//[[:space:]]/}" while getopts "${args}" i ; do case "$i" in c) "${scriptDir}/.includes/checkup.sh";; C) clear_cache;; D) desktop_launcher;; d) unset WINEDEBUG game_installer ;; g) debugGdb=1 game_launcher;; h) help;; i) game_installer;; I) export selectedGameName="${OPTARG}" export noninteractiveInstall="true" break;; k) kill_game;; L) license;; l) game_launcher "${OPTARG}";; N) # shellcheck disable=SC2034 noCache="true" ;; n) # shellcheck disable=SC2034 norh="true" ;; P) checklist quiet;; q) noqjoypad="true" game_launcher;; R) # shellcheck disable=SC2034 redownload="true" ;; r) game_removal;; S) # shellcheck disable=SC2034 defaultRate="${OPTARG}" ;; t) gameCount=$(find "${scriptDir}/.install" -type f -iname "*.sh" | wc -l) agm_infobox "Linux Game Manager" "Linux Game Manager" "There are currently ${gameCount} games available." exit 0 ;; v) # shellcheck disable=SC2034 voiceName="${OPTARG}" ;; V) # shellcheck disable=SC2034 defaultVoice="${OPTARG}" ;; esac done # If agmNoLaunch is set, exit (script is being sourced) [[ "$agmNoLaunch" == "true" ]] && exit 0 # Only proceed with noninteractive installation if explicitly requested if [[ "$noninteractiveInstall" == "true" ]]; then # If no game specified for noninteractive install, exit [[ -z "$selectedGameName" ]] && exit 0 # Install the specified game noninteractively if [[ -f "${scriptDir}/.install/${selectedGameName}.sh" ]]; then export LANG="en_US.UTF-8" # shellcheck disable=SC1090 . "${scriptDir}/.install/${selectedGameName}.sh" # Show success message agm_msgbox "Installation Complete" "Audio Game Installer" "Game \"${selectedGameName}\" has been successfully installed." else agm_msgbox "Audio Game Installer" "" "Error: Game '${selectedGameName}' not found in .install directory" exit 1 fi fi