#!/usr/bin/env bash # This file is part of The RetroPie Project # # The RetroPie Project is the legal property of its developers, whose names are # too numerous to list here. Please refer to the COPYRIGHT.md file distributed with this source. # # See the LICENSE.md file at the top-level directory of this distribution and # at https://raw.githubusercontent.com/RetroPie/RetroPie-Setup/master/LICENSE.md # rp_module_id="moonlight" rp_module_desc="Moonlight Embedded - an open source gamestream client for embedded systems" rp_module_help="ROM Extensions: .ml\n\nCopy your moonlight launch configurations to $romdir/steam\n\nDon't forget to first pair with your remote host before using moonlight. You can use the configuration menu for pairing/unpairing to/from a remote machine." rp_module_licence="GPL3 https://raw.githubusercontent.com/irtimmer/moonlight-embedded/master/LICENSE" rp_module_repo="git https://github.com/irtimmer/moonlight-embedded.git master" rp_module_section="exp" rp_module_flags="!all arm" function _scriptmodule_cfg_file_moonlight() { echo "$configdir/all/moonlight/scriptmodule.cfg" } function _global_cfg_file_moonlight() { echo "$configdir/all/moonlight/global.conf" } function _mangle_moonlight() { local -r type="$1" shift case "$type" in 1) # slugify, ref: https://gist.github.com/oneohthree/f528c7ae1e701ad990e6 iconv -c -t ascii//TRANSLIT <<< "$@" | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | tr "[:upper:]" "[:lower:]" ;; 2) # windows-compatible, ref: https://stackoverflow.com/a/35352640 iconv -c -t ascii//TRANSLIT <<< "$@" | sed -r s/[\<\>]+/\ /g | sed -r s/[\\/\|]+/-/g | sed -r s/[:\*\"]+//g ;; 0|*) # no mangling, but replace invalid "/" with "-" sed -r s/\\//-/g <<< "$@" ;; esac } function _mfmt_moonlight() { case "$1" in 1) echo "SLUGIFY" ;; 2) echo "WINDOWS" ;; 0|*) echo "NONE " ;; esac } function _bfmt_moonlight() { if [[ "$1" -eq 1 ]]; then echo "YES"; else echo "NO "; fi } function depends_moonlight() { # ref: https://github.com/irtimmer/moonlight-embedded/wiki/Compilation#debian-raspbian--osmc local depends=( libssl-dev libopus-dev libasound2-dev libudev-dev libavahi-client-dev libcurl4-openssl-dev libevdev-dev libexpat1-dev libpulse-dev libenet-dev uuid-dev cmake ) # for remote host autodiscovery features depends+=(avahi-daemon libnss-mdns) # platform-specific dependencies isPlatform "rpi" && depends+=(libraspberrypi-dev) isPlatform "osmc" && depends+=(rbp-userland-dev-osmc) isPlatform "vero4k" && depends+=(vero3-userland-dev-osmc) # install selected dependencies getDepends "${depends[@]}" } function sources_moonlight() { gitPullOrClone } function build_moonlight() { # ref: https://github.com/irtimmer/moonlight-embedded/wiki/Compilation rm -rf build mkdir build cd build cmake ../ -DCMAKE_INSTALL_PREFIX="$md_inst" \ -DCMAKE_INSTALL_RPATH="$md_inst/lib" \ -DCMAKE_INSTALL_RPATH_USE_LINK_PATH=TRUE make } function install_moonlight() { cd build make install strip "$md_inst/bin/moonlight" } function configure_moonlight() { addEmulator 1 "$md_id" "steam" "$md_inst/moonlight.sh stream -config %ROM%" addSystem "steam" "Steam Game Streaming" ".ml" [[ "$md_mode" == "remove" ]] && return # ensure rom dir mkRomDir "steam" # create and symlink user configuration directory mkUserDir "$configdir/all/moonlight" moveConfigDir "$home/.config/moonlight" "$configdir/all/moonlight" # create a new global config file if not there already if [[ ! -f "$(_global_cfg_file_moonlight)" ]]; then cat > "$(_global_cfg_file_moonlight)" << "_EOF_" # global config file for moonlight quitappafter = true _EOF_ chown "$__user":"$__group" "$(_global_cfg_file_moonlight)" fi # create wrapper for moonlight with appropriate directories set # note: moonlight adds /moonlight to XDG_* variables cat > "$md_inst/moonlight.sh" << _EOF_ #!/usr/bin/env bash export XDG_DATA_DIRS=$md_inst/share export XDG_CONFIG_DIR=$configdir/all export XDG_CACHE_DIR=$configdir/all $md_inst/bin/moonlight "\$@" _EOF_ chmod +x "$md_inst/moonlight.sh" } function get_scriptmodule_cfg_moonlight() { local address local overwrite=0 local wipe=0 local mangle=0 iniConfig " = " "" "$(_scriptmodule_cfg_file_moonlight)" iniGet "address" && address="$ini_value" iniGet "overwrite" && overwrite="$ini_value" iniGet "wipe" && wipe="$ini_value" iniGet "mangle" && mangle="$ini_value" echo "$address;$overwrite;$wipe;$mangle" } function set_scriptmodule_cfg_moonlight() { local -r address="$1" local -r overwrite="$2" local -r wipe="$3" local -r mangle="$4" [[ -z "$overwrite" || -z "$wipe" || -z "$mangle" ]] && return iniConfig " = " "" "$(_scriptmodule_cfg_file_moonlight)" if [[ -n "$address" ]]; then iniSet "address" "$address" else iniDel "address" fi iniSet "overwrite" "$overwrite" iniSet "wipe" "$wipe" iniSet "mangle" "$mangle" chown "$__user":"$__group" "$(_scriptmodule_cfg_file_moonlight)" } function get_resolution_moonlight() { local width=0 local height=0 local fps=0 iniConfig " = " "" "$(_global_cfg_file_moonlight)" iniGet "width" && width="$ini_value" iniGet "height" && height="$ini_value" iniGet "fps" && fps="$ini_value" if [[ -n "$width" && -n "$height" && -n "$fps" ]]; then echo "$width;$height;$fps" else echo "0;0;0" fi } function get_host_moonlight() { local sops="true" local unsupported="false" iniConfig " = " "" "$(_global_cfg_file_moonlight)" iniGet "sops" && sops="$ini_value" iniGet "unsupported" && unsupported="$ini_value" if [[ -n "$sops" && -n "$unsupported" ]]; then echo "$sops;$unsupported" else echo "true;false" fi } function set_host_moonlight() { local -r sops="$1" local -r unsupported="$2" [[ -z "$sops" || -z "$unsupported" ]] && return iniConfig " = " "" "$(_global_cfg_file_moonlight)" iniSet "sops" "$sops" iniSet "unsupported" "$unsupported" chown "$__user":"$__group" "$(_global_cfg_file_moonlight)" } function set_resolution_moonlight() { local -r width="$1" local -r height="$2" local -r fps="$3" [[ -z "$width" || -z "$height" || -z "$fps" ]] && return iniConfig " = " "" "$(_global_cfg_file_moonlight)" if [[ "$width" -gt 0 && "$height" -gt 0 && "$fps" -gt 0 ]]; then iniSet "width" "$width" iniSet "height" "$height" iniSet "fps" "$fps" else iniDel "width" iniDel "height" iniDel "fps" fi chown "$__user":"$__group" "$(_global_cfg_file_moonlight)" } function get_bitrate_moonlight() { local bitrate=0 iniConfig " = " "" "$(_global_cfg_file_moonlight)" iniGet "bitrate" && bitrate="$ini_value" if [[ -n "$bitrate" ]]; then echo "$bitrate" else echo "0" fi } function set_bitrate_moonlight() { local -r bitrate="$1" [[ -z "$bitrate" ]] && return iniConfig " = " "" "$(_global_cfg_file_moonlight)" if [[ "$bitrate" -gt 0 ]]; then iniSet "bitrate" "$bitrate" else iniDel "bitrate" fi chown "$__user":"$__group" "$(_global_cfg_file_moonlight)" } function exec_moonlight() { trap "trap INT; echo; return" INT sudo -u "$__user" "$md_inst/moonlight.sh" "$@" trap INT } function pair_moonlight() { exec_moonlight pair "$@" } function unpair_moonlight() { exec_moonlight unpair "$@" } function list_moonlight() { exec_moonlight list "$@" } function clear_pairing_moonlight() { rm -rf "$configdir/all/moonlight"/{client*,key*,uniqueid.dat} } function gen_configs_moonlight() { local apps=() local app local fname local config # read scriptmodule config IFS=";" read -r -a config < <(get_scriptmodule_cfg_moonlight) # wipe existing configuration files? if [[ "${config[2]}" -eq 1 ]]; then printMsgs "console" "Wiping existing config files ..." rm -f "$romdir/steam/"*.ml fi # iterate over all apps in remote host mapfile -t apps < <(list_moonlight ${config[0]:+"${config[0]}"} | sed -nE 's/^[0-9]+\. //gp') for app in "${apps[@]}"; do if [[ "$app" == "." || "$app" == ".." ]]; then printMsgs "console" "warning: app name '$app' is not valid" continue fi fname="$(_mangle_moonlight "${config[3]}" "$app")" # app filename mangle [[ "${config[1]}" -eq 0 && -f "$romdir/steam/$fname.ml" ]] && continue # overwrite? # generate config file with defaults printMsgs "console" "Generating config file for '$app' ..." iniConfig " = " "" "$romdir/steam/$fname.ml" iniSet "config" "$(_global_cfg_file_moonlight)" [[ -n "${config[0]}" ]] && iniSet "address" "${config[0]}" iniSet "app" "$app" chown "$__user":"$__group" "$romdir/steam/$fname.ml" 2>/dev/null done } function apps_gui_moonlight() { local options=() local default local cmd local choice local config # read scriptmodule config IFS=";" read -r -a config < <(get_scriptmodule_cfg_moonlight) # start the menu gui default="O" while true; do # create menu options options=( O "Overwrite existing config files: $(_bfmt_moonlight ${config[1]})" "Overwrite existing files in '$romdir/steam'?" W "Wipe existing config files: $(_bfmt_moonlight ${config[2]})" "Delete all files in '$romdir/steam'?" S "Config filename mangling: $(_mfmt_moonlight ${config[3]})" "Use original app names, slugified names or Windows-compatible names?" G "Generate config files" "Start remote apps config files generation" ) # show main menu cmd=(dialog --backtitle "$__backtitle" --default-item "$default" --item-help --menu "Remote Apps" 13 60 16) choice=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) default="$choice" case "$choice" in O) config[1]=$((1 - config[1])) set_scriptmodule_cfg_moonlight "${config[@]}" ;; W) config[2]=$((1 - config[2])) set_scriptmodule_cfg_moonlight "${config[@]}" ;; S) config[3]=$((config[3] + 1)) [[ "${config[3]}" -gt 2 ]] && config[3]=0 set_scriptmodule_cfg_moonlight "${config[@]}" ;; G) gen_configs_moonlight read -p "Press ENTER to continue... " ;; *) break ;; esac done } function host_gui_moonlight() { local options=() local default local cmd local choice local tuple # get current host options IFS=";" read -r -a tuple < <(get_host_moonlight) default="U" [[ "${tuple[0]}" == "false" && "${tuple[1]}" == "false" ]] && default="1" [[ "${tuple[0]}" == "true" && "${tuple[1]}" == "true" ]] && default="2" [[ "${tuple[0]}" == "false" && "${tuple[1]}" == "true" ]] && default="3" # create menu options options=( U "Unset (use default)" "Do not force host compatibility settings" 1 "No SOPS" "Don't allow GFE to modify game settings" 2 "Allow unsupported" "Try streaming if GFE version or options are unsupported" 3 "Open-source host compatibility" "Turn off SOPS and allow unsupported options (for Sunshine/Open-Stream GFE server)" ) # show main menu cmd=(dialog --backtitle "$__backtitle" --default-item "$default" --item-help --menu "Host Compatibility Options" 16 45 16) choice=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) case "$choice" in U) set_host_moonlight "true" "false" ;; 1) set_host_moonlight "false" "false" ;; 2) set_host_moonlight "true" "true" ;; 3) set_host_moonlight "false" "true" ;; esac } function resolution_gui_moonlight() { local options=() local default local cmd local choice local resolution # get current resolution IFS=";" read -r -a resolution < <(get_resolution_moonlight) if [[ "${resolution[0]}" -gt 0 && "${resolution[1]}" -gt 0 && "${resolution[2]}" -gt 0 ]]; then default="C" [[ "${resolution[0]}" == 1920 && "${resolution[1]}" == 1080 && "${resolution[2]}" == 60 ]] && default="1" [[ "${resolution[0]}" == 1920 && "${resolution[1]}" == 1080 && "${resolution[2]}" == 30 ]] && default="2" [[ "${resolution[0]}" == 1280 && "${resolution[1]}" == 720 && "${resolution[2]}" == 60 ]] && default="3" [[ "${resolution[0]}" == 1280 && "${resolution[1]}" == 720 && "${resolution[2]}" == 30 ]] && default="4" resolution="${resolution[0]} x ${resolution[1]} @ ${resolution[2]} fps" else default="U" resolution="(using default)" fi # create menu options options=( U "Unset (use default)" "Do not force a resolution setting" 1 "1080p60" "Set resolution to 1920 x 1080 @ 60 fps" 2 "1080p30" "Set resolution to 1920 x 1080 @ 30 fps" 3 "720p60" "Set resolution to 1280 x 720 @ 60 fps" 4 "720p30" "Set resolution to 1280 x 720 @ 30 fps" C "Custom" "Set a custom resolution" ) # show main menu cmd=(dialog --backtitle "$__backtitle" --default-item "$default" --item-help --menu "Global Resolution\nCurrent: $resolution" 16 45 16) choice=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) case "$choice" in U) set_resolution_moonlight "0" "0" "0" ;; 1) set_resolution_moonlight "1920" "1080" "60" ;; 2) set_resolution_moonlight "1920" "1080" "30" ;; 3) set_resolution_moonlight "1280" "720" "60" ;; 4) set_resolution_moonlight "1280" "720" "30" ;; C) cmd=(dialog --backtitle "$__backtitle" --inputbox "Please enter a custom resolution as WIDTH HEIGHT FPS (separated by spaces)" 10 50) choice=$("${cmd[@]}" 2>&1 >/dev/tty) if [[ $? -eq 0 ]]; then IFS=" " read -r -a choice <<< "$choice" set_resolution_moonlight "${choice[0]}" "${choice[1]}" "${choice[2]}" fi ;; esac } function bitrate_gui_moonlight() { local options=() local default local cmd local choice local bitrate # get current bitrate bitrate=$(get_bitrate_moonlight) if [[ "$bitrate" -gt 0 ]]; then default="C" [[ "$bitrate" == 20000 ]] && default="1" [[ "$bitrate" == 10000 ]] && default="2" [[ "$bitrate" == 5000 ]] && default="3" bitrate="$bitrate Kbps" else default="U" bitrate="(using default)" fi # create menu options options=( U "Unset (use default)" "Do not force a stream bitrate setting" 1 "20000" "Set stream bitrate to 20000 Kbps" 2 "10000" "Set stream bitrate to 10000 Kbps" 3 "5000" "Set stream bitrate to 5000 Kbps" C "Custom" "Set a custom stream bitrate" ) # show main menu cmd=(dialog --backtitle "$__backtitle" --default-item "$default" --item-help --menu "Stream Bitrate\nCurrent: $bitrate" 16 45 16) choice=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) case "$choice" in U) set_bitrate_moonlight "0" ;; 1) set_bitrate_moonlight "20000" ;; 2) set_bitrate_moonlight "10000" ;; 3) set_bitrate_moonlight "5000" ;; C) cmd=(dialog --backtitle "$__backtitle" --inputbox "Please enter a custom stream bitrate in Kbps" 10 50) choice=$("${cmd[@]}" 2>&1 >/dev/tty) [[ $? -eq 0 ]] && set_bitrate_moonlight "$choice" ;; esac } function gui_moonlight() { local options=() local default local cmd local choice local config # read scriptmodule config IFS=";" read -r -a config < <(get_scriptmodule_cfg_moonlight) # start the menu gui default="A" while true; do # create menu options, if no address show "autodiscover" options=( A "Set remote host address (${config[0]:-autodiscover})" P "Pair to remote host" U "Unpair from remote host" G "Configure remote apps" R "Configure global resolution" B "Configure global stream bitrate" H "Configure host compatibility" C "Clear all pairing data" ) # show main menu cmd=(dialog --backtitle "$__backtitle" --default-item "$default" --menu "Choose an option" 16 60 16) choice=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) default="$choice" case "$choice" in A) cmd=(dialog --backtitle "$__backtitle" --inputbox "Please enter the address of the remote host (leave BLANK for autodiscovery of the remote host)" 10 65) choice=$("${cmd[@]}" 2>&1 >/dev/tty) if [[ $? -eq 0 ]]; then config[0]="$choice" set_scriptmodule_cfg_moonlight "${config[@]}" fi ;; P) pair_moonlight ${config[0]:+"${config[0]}"} /dev/tty read -p "Press ENTER to continue... " ;; U) unpair_moonlight ${config[0]:+"${config[0]}"} /dev/tty read -p "Press ENTER to continue... " ;; G) apps_gui_moonlight ;; R) resolution_gui_moonlight ;; B) bitrate_gui_moonlight ;; H) host_gui_moonlight ;; C) if dialog --defaultno --yesno "Are you sure you want to CLEAR ALL pairing data?" 8 40 2>&1 >/dev/tty; then if clear_pairing_moonlight; then printMsgs "dialog" "All pairing data cleared." else printMsgs "dialog" "Could not clear pairing data." fi fi ;; *) break ;; esac done }