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
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
INSTALL_GAME_RUNNER = Path("/home/stormux/.local/.functions/install_game.sh")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GameEntry:
|
class GameEntry:
|
||||||
section: str
|
section: str
|
||||||
@@ -69,6 +72,28 @@ def build_game_command(entry):
|
|||||||
return f"STORMUX_LAUNCH_SCRIPT={launchPath!r} startx"
|
return f"STORMUX_LAUNCH_SCRIPT={launchPath!r} startx"
|
||||||
return shlex.quote(str(entry.installScript))
|
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:
|
class VoicedMenu:
|
||||||
def __init__(self, title="Stormux Game Menu"):
|
def __init__(self, title="Stormux Game Menu"):
|
||||||
self.title = title
|
self.title = title
|
||||||
@@ -113,6 +138,7 @@ class VoicedMenu:
|
|||||||
|
|
||||||
# Load downloadable games registry
|
# Load downloadable games registry
|
||||||
self.downloadable_games = self.load_downloadable_games()
|
self.downloadable_games = self.load_downloadable_games()
|
||||||
|
self.savedMenuState = None
|
||||||
|
|
||||||
def load_downloadable_games(self):
|
def load_downloadable_games(self):
|
||||||
"""Load downloadable games registry from JSON file"""
|
"""Load downloadable games registry from JSON file"""
|
||||||
@@ -641,6 +667,49 @@ class VoicedMenu:
|
|||||||
|
|
||||||
self.menuSections[sectionName].append((name, command))
|
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):
|
def speak(self, text, interrupt=True):
|
||||||
"""Speak the given text with option to interrupt existing speech"""
|
"""Speak the given text with option to interrupt existing speech"""
|
||||||
if self.speechClient is None:
|
if self.speechClient is None:
|
||||||
@@ -1103,8 +1172,11 @@ if __name__ == "__main__":
|
|||||||
menu = VoicedMenu(title="Stormux Gaming Menu")
|
menu = VoicedMenu(title="Stormux Gaming Menu")
|
||||||
|
|
||||||
for gameEntry in discover_game_entries():
|
for gameEntry in discover_game_entries():
|
||||||
label = gameEntry.name if gameEntry.isInstalled else f"{gameEntry.name} (install)"
|
menu.add_item(
|
||||||
menu.add_item(gameEntry.section, label, build_game_command(gameEntry))
|
gameEntry.section,
|
||||||
|
gameEntry.name,
|
||||||
|
lambda entry=gameEntry: menu.show_game_actions(entry),
|
||||||
|
)
|
||||||
|
|
||||||
menu.add_section("Emulators")
|
menu.add_section("Emulators")
|
||||||
menu.add_item("Emulators", "Apple 2e", "/home/stormux/.local/bin/apple_2e.py")
|
menu.add_item("Emulators", "Apple 2e", "/home/stormux/.local/bin/apple_2e.py")
|
||||||
|
|||||||
Reference in New Issue
Block a user