Start work on game installer and launcher actually working.
This commit is contained in:
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2154
|
||||
|
||||
download_with_fallback() {
|
||||
local outputPath="$1"
|
||||
local sourceUrl="$2"
|
||||
local errorFile=""
|
||||
local curlExitCode=0
|
||||
local retryExitCode=0
|
||||
local fallbackExitCode=0
|
||||
local wgetExitCode=0
|
||||
|
||||
downloadAttemptExitCodes=""
|
||||
downloadErrorLog=""
|
||||
|
||||
errorFile="$(mktemp)" || return 1
|
||||
|
||||
if curl -L4 -C - --retry 10 --output "${outputPath}" "${sourceUrl}" 2> "${errorFile}"; then
|
||||
rm -f "${errorFile}"
|
||||
return 0
|
||||
fi
|
||||
curlExitCode=$?
|
||||
|
||||
rm -f "${outputPath}"
|
||||
if curl -L4 --retry 10 --output "${outputPath}" "${sourceUrl}" 2>> "${errorFile}"; then
|
||||
rm -f "${errorFile}"
|
||||
return 0
|
||||
fi
|
||||
retryExitCode=$?
|
||||
|
||||
rm -f "${outputPath}"
|
||||
if curl -L --retry 10 --output "${outputPath}" "${sourceUrl}" 2>> "${errorFile}"; then
|
||||
rm -f "${errorFile}"
|
||||
return 0
|
||||
fi
|
||||
fallbackExitCode=$?
|
||||
|
||||
if command -v wget > /dev/null 2>&1; then
|
||||
rm -f "${outputPath}"
|
||||
if wget --tries=10 --output-document="${outputPath}" "${sourceUrl}" 2>> "${errorFile}"; then
|
||||
rm -f "${errorFile}"
|
||||
return 0
|
||||
fi
|
||||
wgetExitCode=$?
|
||||
fi
|
||||
|
||||
downloadAttemptExitCodes="curl: ${curlExitCode}, ${retryExitCode}, ${fallbackExitCode}"
|
||||
if [[ "${wgetExitCode}" -ne 0 ]]; then
|
||||
downloadAttemptExitCodes+="; wget: ${wgetExitCode}"
|
||||
fi
|
||||
|
||||
if cp -f "${errorFile}" "${cache}/curl-error.log" 2> /dev/null; then
|
||||
downloadErrorLog="${cache}/curl-error.log"
|
||||
rm -f "${errorFile}"
|
||||
else
|
||||
downloadErrorLog="${errorFile}"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
validate_downloaded_cache_file() {
|
||||
local dest="$1"
|
||||
local downloadError=0
|
||||
|
||||
if [[ "${STORMUX_SKIP_DOWNLOAD_VALIDATION:-0}" == "1" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "${dest##*.}" in
|
||||
pk3|zip)
|
||||
unzip -tq "${cache}/${dest}" | ui_progressbox "Game Installer" "Validating ${dest}"
|
||||
downloadError=${PIPESTATUS[0]}
|
||||
;;
|
||||
7z)
|
||||
7z t "${cache}/${dest}" | ui_progressbox "Game Installer" "Validating ${dest}"
|
||||
downloadError=${PIPESTATUS[0]}
|
||||
;;
|
||||
exe)
|
||||
if ! hexdump -n 2 -v -e '/1 "%02X"' "${cache}/${dest}" | grep -q "4D5A"; then
|
||||
downloadError=1
|
||||
fi
|
||||
;;
|
||||
wad)
|
||||
if [[ "$(file -b --mime-type "${cache}/${dest}")" != "application/octet-stream" ]]; then
|
||||
downloadError=1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if file -b "${cache}/${dest}" | grep -q "HTML document"; then
|
||||
downloadError=1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "${downloadError}" -ne 0 ]]; then
|
||||
rm -fv "${cache:?}/${dest}"
|
||||
alert "Game Installer" "Game Installer" "Error downloading ${dest}. Installation cannot continue."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
download() {
|
||||
local sourceUrls=("$@")
|
||||
local sourceUrl
|
||||
local dest
|
||||
|
||||
for sourceUrl in "${sourceUrls[@]}"; do
|
||||
dest="${sourceUrl##*/}"
|
||||
dest="${dest//%20/ }"
|
||||
dest="${dest#*\?filename=}"
|
||||
dest="${dest%\?*}"
|
||||
|
||||
[[ -s "${cache}/${dest}" ]] || rm -f "${cache:?}/${dest}" 2> /dev/null
|
||||
if [[ "${redownload}" == "true" && -e "${cache}/${dest}" ]]; then
|
||||
rm -v "${cache:?}/${dest}"
|
||||
fi
|
||||
if [[ -e "${cache}/${dest}" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
ui_progressbox "Game Installer" "Downloading ${dest}" < /dev/null
|
||||
if ! download_with_fallback "${cache}/${dest}" "${sourceUrl}"; then
|
||||
ui_msgbox "Game Installer" "Game Installer" "Could not download ${sourceUrl}.\n\nDownload exit codes: ${downloadAttemptExitCodes}\nError details: ${downloadErrorLog}"
|
||||
exit 1
|
||||
fi
|
||||
validate_downloaded_cache_file "${dest}"
|
||||
done
|
||||
}
|
||||
|
||||
download_named() {
|
||||
local dest="$1"
|
||||
local sourceUrl="$2"
|
||||
|
||||
[[ -n "${dest}" && -n "${sourceUrl}" ]] || return 1
|
||||
[[ -s "${cache}/${dest}" ]] || rm -f "${cache:?}/${dest}" 2> /dev/null
|
||||
if [[ "${redownload}" == "true" && -e "${cache}/${dest}" ]]; then
|
||||
rm -v "${cache:?}/${dest}"
|
||||
fi
|
||||
if [[ -e "${cache}/${dest}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! download_with_fallback "${cache}/${dest}" "${sourceUrl}"; then
|
||||
ui_msgbox "Game Installer" "Game Installer" "Could not download ${dest}.\n\nDownload exit codes: ${downloadAttemptExitCodes}\nError details: ${downloadErrorLog}"
|
||||
exit 1
|
||||
fi
|
||||
validate_downloaded_cache_file "${dest}"
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2154
|
||||
|
||||
scriptDir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck disable=SC1091
|
||||
source "${scriptDir}/download.sh"
|
||||
|
||||
speak() {
|
||||
local message="$1"
|
||||
|
||||
printf '%s\n' "${message}"
|
||||
spd-say "${message}" 2> /dev/null || true
|
||||
}
|
||||
|
||||
ui_msgbox() {
|
||||
local _title="$1"
|
||||
local _backTitle="$2"
|
||||
local message="$3"
|
||||
|
||||
speak "${message}"
|
||||
}
|
||||
|
||||
ui_infobox() {
|
||||
ui_msgbox "$@"
|
||||
}
|
||||
|
||||
ui_yesno() {
|
||||
local _title="$1"
|
||||
local _backTitle="$2"
|
||||
local message="$3"
|
||||
|
||||
speak "${message}"
|
||||
return 1
|
||||
}
|
||||
|
||||
ui_progressbox() {
|
||||
local _title="$1"
|
||||
local message="$2"
|
||||
|
||||
speak "${message}"
|
||||
cat
|
||||
}
|
||||
|
||||
alert() {
|
||||
local title="Game Installer"
|
||||
local backTitle="Game Installer"
|
||||
local message="Press enter to continue."
|
||||
|
||||
if [[ $# -eq 1 ]]; then
|
||||
message="$1"
|
||||
elif [[ $# -eq 2 ]]; then
|
||||
title="$1"
|
||||
backTitle="$1"
|
||||
message="$2"
|
||||
elif [[ $# -ge 3 ]]; then
|
||||
title="$1"
|
||||
backTitle="$2"
|
||||
shift 2
|
||||
message="$*"
|
||||
fi
|
||||
|
||||
ui_msgbox "${title}" "${backTitle}" "${message}"
|
||||
}
|
||||
|
||||
check_architecture() {
|
||||
local architecture
|
||||
local supportedArch
|
||||
|
||||
architecture="$(uname -m)"
|
||||
for supportedArch in "$@"; do
|
||||
if [[ "${architecture}" == "${supportedArch}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${architecture}" == "aarch64" && -d "${HOME}/.fex-emu/RootFS/ArchLinux" && "$*" == *"x86_64"* ]]; then
|
||||
export fex="FEXLoader -- "
|
||||
return 0
|
||||
fi
|
||||
|
||||
ui_infobox "Game Installer" "Game Installer" "This game is not compatible with ${architecture}."
|
||||
exit 1
|
||||
}
|
||||
|
||||
check_dependencies() {
|
||||
local missingDependencies=()
|
||||
local dependency
|
||||
|
||||
for dependency in "$@"; do
|
||||
if [[ "${dependency}" == python-* ]]; then
|
||||
if ! python3 -c "import ${dependency#*:}" > /dev/null 2>&1; then
|
||||
missingDependencies+=("${dependency%:*}")
|
||||
fi
|
||||
elif ! command -v "${dependency}" > /dev/null 2>&1; then
|
||||
missingDependencies+=("${dependency}")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "${#missingDependencies[@]}" -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
ui_msgbox "Game Installer" "Game Installer" "Missing dependencies: ${missingDependencies[*]}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
open_url() {
|
||||
GAME="$*" startx
|
||||
}
|
||||
|
||||
get_installer() {
|
||||
local installerName="$1"
|
||||
local sourceDir
|
||||
|
||||
if [[ -f "${cache}/${installerName}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
for sourceDir in "${HOME}/Downloads" "${HOME}/Desktop"; do
|
||||
find "${sourceDir}" -type f -name "${installerName}" -exec mv -v {} "${cache}/" \; 2> /dev/null || true
|
||||
done
|
||||
|
||||
if [[ ! -f "${cache}/${installerName}" ]]; then
|
||||
ui_msgbox "Game Installer" "Game Installer" "Could not find ${installerName}. Manual download support is not implemented yet."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
scriptDir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
installerPath="${1:-}"
|
||||
|
||||
if [[ -z "${installerPath}" || ! -f "${installerPath}" ]]; then
|
||||
printf 'Usage: %s installer.sh\n' "${0##*/}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
gameName="${installerPath##*/}"
|
||||
gameName="${gameName%.sh}"
|
||||
installPath="${installPath:-${HOME}/.local/games}"
|
||||
cache="${cache:-${XDG_CACHE_HOME:-${HOME}/.cache}/stormux-game-install}"
|
||||
launchRoot="${launchRoot:-${HOME}/.local/.launch}"
|
||||
redownload="${redownload:-false}"
|
||||
ipfsGateway="${ipfsGateway:-https://gateway.pinata.cloud}"
|
||||
fex="${fex:-}"
|
||||
|
||||
export installPath cache launchRoot redownload ipfsGateway fex gameName
|
||||
|
||||
mkdir -p "${installPath}" "${cache}" "${launchRoot}"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "${scriptDir}/game_install_functions.sh"
|
||||
|
||||
speak "Installing ${gameName}"
|
||||
# shellcheck source=/dev/null
|
||||
source "${installerPath}"
|
||||
|
||||
if [[ -f "${launchRoot}/${gameName}.game" ]]; then
|
||||
ln -sf "${gameName}.game" "${launchRoot}/${gameName}.sh"
|
||||
fi
|
||||
|
||||
speak "${gameName} installed"
|
||||
@@ -22,6 +22,9 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
INSTALL_GAME_RUNNER = Path("/home/stormux/.local/.functions/install_game.sh")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GameEntry:
|
||||
section: str
|
||||
@@ -69,6 +72,28 @@ def build_game_command(entry):
|
||||
return f"STORMUX_LAUNCH_SCRIPT={launchPath!r} startx"
|
||||
return shlex.quote(str(entry.installScript))
|
||||
|
||||
|
||||
def build_game_install_command(entry):
|
||||
return f"{shlex.quote(str(INSTALL_GAME_RUNNER))} {shlex.quote(str(entry.installScript))}"
|
||||
|
||||
|
||||
def build_game_uninstall_command(entry):
|
||||
if entry.launchScript is None:
|
||||
return ""
|
||||
launchPath = shlex.quote(str(entry.launchScript))
|
||||
gameName = shlex.quote(entry.name)
|
||||
return f"rm -f {launchPath}; echo {gameName} uninstalled"
|
||||
|
||||
|
||||
def build_game_actions(entry):
|
||||
actions = []
|
||||
if entry.isInstalled:
|
||||
actions.append(("Play", build_game_command(entry)))
|
||||
actions.append(("Uninstall", build_game_uninstall_command(entry)))
|
||||
else:
|
||||
actions.append(("Install", build_game_install_command(entry)))
|
||||
return actions
|
||||
|
||||
class VoicedMenu:
|
||||
def __init__(self, title="Stormux Game Menu"):
|
||||
self.title = title
|
||||
@@ -113,6 +138,7 @@ class VoicedMenu:
|
||||
|
||||
# Load downloadable games registry
|
||||
self.downloadable_games = self.load_downloadable_games()
|
||||
self.savedMenuState = None
|
||||
|
||||
def load_downloadable_games(self):
|
||||
"""Load downloadable games registry from JSON file"""
|
||||
@@ -641,6 +667,49 @@ class VoicedMenu:
|
||||
|
||||
self.menuSections[sectionName].append((name, command))
|
||||
|
||||
def save_menu_state(self):
|
||||
"""Save the current menu state so a temporary submenu can restore it."""
|
||||
self.savedMenuState = {
|
||||
"sectionNames": self.sectionNames.copy(),
|
||||
"currentSection": self.currentSection,
|
||||
"currentItemIndices": self.currentItemIndices.copy(),
|
||||
"menuSections": {
|
||||
sectionName: items.copy()
|
||||
for sectionName, items in self.menuSections.items()
|
||||
},
|
||||
}
|
||||
|
||||
def restore_saved_menu(self):
|
||||
"""Restore the menu state saved before entering a temporary submenu."""
|
||||
if self.savedMenuState is None:
|
||||
return
|
||||
|
||||
self.sectionNames = self.savedMenuState["sectionNames"]
|
||||
self.currentSection = self.savedMenuState["currentSection"]
|
||||
self.currentItemIndices = self.savedMenuState["currentItemIndices"]
|
||||
self.menuSections = self.savedMenuState["menuSections"]
|
||||
self.savedMenuState = None
|
||||
self.draw_menu()
|
||||
self.announce_current_section()
|
||||
time.sleep(0.5)
|
||||
self.announce_current_item(interrupt=False)
|
||||
|
||||
def show_action_menu(self, sectionName, actions):
|
||||
"""Replace the current menu with a temporary action submenu."""
|
||||
self.save_menu_state()
|
||||
self.sectionNames = [sectionName]
|
||||
self.currentSection = 0
|
||||
self.currentItemIndices = {sectionName: 0}
|
||||
self.menuSections = {sectionName: actions + [("Back", self.restore_saved_menu)]}
|
||||
self.draw_menu()
|
||||
self.announce_current_section()
|
||||
time.sleep(0.5)
|
||||
self.announce_current_item(interrupt=False)
|
||||
|
||||
def show_game_actions(self, gameEntry):
|
||||
"""Show available actions for a game."""
|
||||
self.show_action_menu(gameEntry.name, build_game_actions(gameEntry))
|
||||
|
||||
def speak(self, text, interrupt=True):
|
||||
"""Speak the given text with option to interrupt existing speech"""
|
||||
if self.speechClient is None:
|
||||
@@ -1103,8 +1172,11 @@ if __name__ == "__main__":
|
||||
menu = VoicedMenu(title="Stormux Gaming Menu")
|
||||
|
||||
for gameEntry in discover_game_entries():
|
||||
label = gameEntry.name if gameEntry.isInstalled else f"{gameEntry.name} (install)"
|
||||
menu.add_item(gameEntry.section, label, build_game_command(gameEntry))
|
||||
menu.add_item(
|
||||
gameEntry.section,
|
||||
gameEntry.name,
|
||||
lambda entry=gameEntry: menu.show_game_actions(entry),
|
||||
)
|
||||
|
||||
menu.add_section("Emulators")
|
||||
menu.add_item("Emulators", "Apple 2e", "/home/stormux/.local/bin/apple_2e.py")
|
||||
|
||||
Reference in New Issue
Block a user