1#!/bin/sh
2
3# Copyright 2013 Arx Libertatis Team (see the AUTHORS file)
4#
5# This file is part of Arx Libertatis.
6#
7# Arx Libertatis is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Arx Libertatis is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Arx Libertatis.  If not, see <http://www.gnu.org/licenses/>.
19
20##########################################################################################
21# Install script for Arx Fatalis data files to be used with Arx Libertatis
22# Usage: just run the damned script, maybe check --help
23
24# This scripts targets Linux and FreeBSD, but may also work on other UNIX-like systems.
25
26# Is this a multi-thousand-line bas^H^H^HPOSIX shell script?
27#  Sure looks like it.
28# Am I mad?
29#  Most likely.
30
31# If you want to edit the required files and checksums, scroll to the end.
32
33
34##########################################################################################
35# Colors
36
37disable_color() {
38	red='' ; green='' ; yellow='' ; blue='' ; pink='' ; cyan='' ; white=''
39	dim_red='' ; dim_green='' ; dim_yellow='' ; dim_blue='' ; dim_pink=''
40	dim_cyan='' ; dim_white='' ; reset=''
41}
42disable_color
43if [ -t 1 ] && [ "$(tput colors 2> /dev/null)" != -1 ] ; then
44
45	       red="$(printf '\033[1;31m')"
46	     green="$(printf '\033[1;32m')"
47	    yellow="$(printf '\033[1;33m')"
48	      blue="$(printf '\033[1;34m')"
49	      pink="$(printf '\033[1;35m')"
50	      cyan="$(printf '\033[1;36m')"
51	     white="$(printf '\033[1;37m')"
52
53	   dim_red="$(printf '\033[0;31m')"
54	 dim_green="$(printf '\033[0;32m')"
55	dim_yellow="$(printf '\033[0;33m')"
56	  dim_blue="$(printf '\033[0;34m')"
57	  dim_pink="$(printf '\033[0;35m')"
58	  dim_cyan="$(printf '\033[0;36m')"
59	 dim_white="$(printf '\033[0;37m')"
60
61	     reset="$(printf '\033[0m')"
62fi
63
64
65##########################################################################################
66# Constants
67
68# Name and download locations for the 1.21 patch
69patch_ver='1.21'
70patch_name="ArxFatalis_${patch_ver}_MULTILANG.exe"
71patch_name_localized="ArxFatalis_${patch_ver}_%s.exe"
72patch_url_path="arxfatalis/patches/${patch_ver}/${patch_name}"
73patch_url_master="http://cdn.bethsoft.com/${patch_url_path}"
74patch_urls="http://arx.vg/${patch_name} ${patch_url_master}"
75patch_urls="$patch_urls http://download.zenimax.com/${patch_url_path}"
76patch_urls="$patch_urls http://web.archive.org/web/${patch_url_master}"
77
78# Name and download locations for the Japanese 1.02j patch
79patch_jp_ver='1.02j'
80patch_jp_name="arx_jpn_patch_${patch_jp_ver}.exe"
81patch_jp_url_master="http://www.capcom.co.jp/pc/arx/patch/${patch_jp_name}"
82patch_jp_urls="http://arx.vg/${patch_jp_name}" # master URL is no longer available
83patch_jp_urls="$patch_jp_urls http://web.archive.org/web/${patch_jp_url_master}"
84
85# Name and store page for the GOG.com download
86gog_names='setup_arx_fatalis.exe'
87gog_url='http://www.gog.com/gamecard/arx_fatalis'
88
89# Store page for the Steam download
90steam_url='http://store.steampowered.com/app/1700/'
91
92# Name and wiki page for the demo download
93demo_names="arx_demo_english.zip arxdemoenglish.zip arx_jpn_demo.exe"
94demo_url='http://arx.vg/Getting_the_game_data#Demo'
95
96bug_tracker_url='http://bugs.arx-libertatis.org/'
97
98cabextract_url='http://www.cabextract.org.uk/'
99innoextract_url='http://constexpr.org/innoextract/'
100
101
102##########################################################################################
103# Standard directories
104
105user_pwd="$PWD"
106user_pwd="${user_pwd%/}"
107platform="$(uname)"
108command="$(basename "$0")"
109scommand="$(printf '%s' "$command" | tr - _)"
110if [ "$platform" = 'Darwin' ] ; then
111	# Mac OS X
112	data_dirs='/Applications'
113	data_home="$HOME/Library/Application Support"
114	config_home="$HOME/Library/Application Support"
115	data_dir_suffixes='ArxLibertatis'
116	user_dir_suffixes='ArxLibertatis'
117	config_dir_suffixes='ArxLibertatis'
118	downloads_dir="$HOME/Downloads"
119else
120	# Linux, FreeBSD, ...
121	data_dirs="${XDG_DATA_DIRS:-"/usr/local/share/:/usr/share/"}:/opt"
122	data_home="${XDG_DATA_HOME:-"$HOME/.local/share"}"
123	config_home="${XDG_CONFIG_HOME:-"$HOME/.config"}"
124	data_dir_suffixes='games/arx:arx'
125	user_dir_suffixes='arx'
126	config_dir_suffixes='arx'
127	[ -f "${config_home}/user-dirs.dirs" ] && . "${config_home}/user-dirs.dirs"
128	downloads_dir="${XDG_DOWNLOAD_DIR:-"$HOME/Downloads"}"
129fi
130downloads_dir="${downloads_dir%/}"
131tempdir="${TMPDIR:-"/tmp"}"
132tempdir="${tempdir%/}"
133[ -d "$tempdir" ] || tempdir="$PWD"
134eval "data_path=\"\$${scommand}_PATH\""
135[ -z "$data_path" ] && data_path="$arx_PATH"
136
137
138##########################################################################################
139# Helper functions
140
141exec 4>&2  # fd to the original stderr (we redirect output to a log file in some cases)
142logfile='' # log file receiving sdout and stderr
143
144true=0  # Return value / exit status that evaluates to true
145false=1 # Return value / exit status that evaluates to false
146
147# 1 if the script is being run as root, false otherwise
148if [ "$(id -u)" = 0 ] ; then is_root=1 ; else is_root=0 ; fi
149
150# Print one line of text, without escape codes or other shell-specific shenanigans.
151# Seriously, shells, you can't even agree on a consistent implementation of echo?
152# Usage: print <text>
153print() {
154	printf '%s\n' "$1"
155}
156
157puts() {
158	printf '%s' "$1"
159}
160
161disabled_commands=' ' # List of commands that should not be used, even if they exist
162
163# Make `have` return false for a comand
164# Usage: disable_command <command>
165disable_command() {
166	disabled_commands="$disabled_commands$1 "
167}
168
169# Check if a command is available.
170# Usage: have <command>
171# Return: $true if the command is available, $false otherwise
172have() {
173	case "$disabled_commands" in *" $1 "*) return $false ; esac
174	command -v "$1" > /dev/null 2>&1
175}
176
177# Make a path absolute no matter if it is relative or not
178# Usage: abspath <path>
179# Too bad we can't just use readlink -m
180abspath() {
181	case "$1" in
182		/*) print "$1" ;;
183		 *) print "$PWD/$1" ;;
184	esac
185}
186
187# Get the canonical representation of an existing path
188# Usage: canonicalize <path>
189# Too bad we can't just use readlink -f
190if have realpath ; then
191	canonicalize() { realpath "$1" ; }
192else if have grealpath ; then
193	canonicalize() { grealpath "$1" ; }
194else if have greadlink ; then
195	canonicalize() { greadlink -f "$1" ; }
196else
197	canonicalize() {
198		_canonicalize_old_pwd="$PWD"
199		_canonicalize_file="$1"
200		while true ; do
201			cd "$(dirname "$_canonicalize_file")"
202			_canonicalize_file="$(basename "$_canonicalize_file")"
203			[ -L "$_canonicalize_file" ] || break;
204			_canonicalize_file="$(readlink "$_canonicalize_file")"
205		done
206		echo "$(pwd -P)/$_canonicalize_file"
207		cd "$_canonicalize_old_pwd"
208	}
209fi ; fi ; fi
210
211cleanup_functions='' # List of functions to be run on exit
212
213# Add a function to ron on exit.
214# Functions are un in the order they are added.
215# Usage: on_exit <code>
216# Cleanup functions will receive one argument: the exit message if any or an empty string.
217on_exit() {
218	[ -z "$cleanup_functions" ] || cleanup_functions=" $cleanup_functions"
219	cleanup_functions="$1$cleanup_functions"
220}
221
222# Run exit runctions.
223cleanup() {
224	_cleanup_functions="$cleanup_functions" ; cleanup_functions=''
225	[ -z "$_cleanup_functions" ] && return
226	eval "for _cleanup_func in $_cleanup_functions ; do \"\$_cleanup_func\" \"\$@\" ; done"
227}
228
229# Register our cleanup handler.
230trap "cleanup" EXIT
231# Some shells don't have their own (non-libc) SIGINT handler, but the EXIT trap
232# won't trigger if there is none!
233trap 'print >&4 ; quit 1' INT
234
235# Run cleanup functions with a possible message and then exit.
236# Usage: quit <status> [<message>]
237quit() {
238	cleanup "$2"
239	exit $1
240}
241
242# Exit with a non-zero status and optionally print a message.
243# Usage: die [<message>...]
244die() {
245	_die_message=''
246	if [ $# -gt 0 ] ; then
247		_die_message="$1" ; shift
248		for _die_arg ; do _die_message="$_die_message $_die_arg" ; done
249		_die_message="$_die_message
250
251If you think this is a bug in the install script
252please report the complete output at
253  $bug_tracker_url"
254		if [ ! -z "$logfile" ] && [ -f "$logfile" ] ; then
255			_die_message="$_die_message
256
257Also attach the contents of
258  $logfile"
259			logfile='' # so that we don't remove it on exit
260			printf "${red}%s${reset}\\n" "$_die_message" >&4 # also print to priginal stdout
261			printf '\n%s\n' 'Preserving log file.' >&4
262		fi
263
264		printf "${red}%s${reset}\\n" "$_die_message"
265	fi
266	quit 1 "$_die_message"
267}
268
269# Escape a string from stdin for use in a whitespace-seperated list.
270# Usage: print <string> | escape_pipe
271escape_pipe() {
272	sed "s:[^a-zA-Z0-9/_.$1]:\\\\&:g"
273}
274
275# Escape a string for use in a whitespace-seperated list.
276# Usage: escape <string>
277escape() {
278	print "$1" | escape_pipe "$2"
279}
280
281# Convert a colon-seperated list into an escaped whitespace-seperated list.
282# Usage: to_list <colon-list>
283to_list() {
284	escape "$1" | sed 's/\\:/ /g'
285}
286
287# Line-based output into a list
288# Usage: ls | lines_to_list
289lines_to_list() {
290	escape_pipe | tr '\n' ' '
291}
292
293# Check if a whitespace separated list contains a string.
294# Usage: list_contains <list-var> <needle>
295list_contains() {
296	eval "_list_contents=\"\$$1\""
297	[ -z "$_list_contents" ] && return $false
298	eval "for _list_contains_entry in $_list_contents ; do" \
299		" [ \"\$_list_contains_entry\" = \"\$2\" ] && return \$true ; done"
300	return $false
301}
302
303# Append a string to a whitespace separated list.
304# Usage: list_append <list-var> <string> [comment]
305# Whitespace seperated lists can be loaded into the argument list using:
306#  eval "set -- $var"
307list_append() {
308	_list_entry="$(escape "$2")"
309	eval "_list_contents=\"\$$1\""
310	if [ -z "$_list_contents" ]
311		then eval "$1=\"\$_list_entry\""
312		else eval "$1=\"\$_list_contents \$_list_entry\""
313	fi
314	eval "[ -z \"\$$1__list_count\" ] && $1__list_count=0"
315	eval "_list_count=\$$1__list_count"
316	eval "$1__list_comment_$_list_count=\"\$3\""
317	eval "$1__list_count=\$((\$$1__list_count + 1))"
318}
319
320# Append one list to another, preserving comments.
321# Usage: list_merge <list-var> <append-list-var>
322list_merge() {
323	eval "_list_append=\"\$$2\""
324	[ -z "$_list_append" ] && return
325	eval "
326		_list_merge_i=0
327		for _list_merge_entry in $_list_append ; do
328			list_append $1 \"\$_list_merge_entry\" \"\$(list_comment $2 \$_list_merge_i)\"
329			_list_merge_i=\$((\$_list_merge_i + 1))
330		done
331	"
332}
333
334# Get a comment associated with alist entry
335# Usage: list_comment <list-var> <index>
336list_comment() {
337	eval "print \"\$$1__list_comment_$2\""
338}
339
340# Set a comment associated with alist entry
341# Usage: list_comment <list-var> <index> <comment>
342set_list_comment() {
343	eval "$1__list_comment_$2=\"\$3\""
344}
345
346# Append a string to a whitespace separated list if it isn't already in the list.
347# Usage: set_append <list-var> <string> [comment]
348set_append() {
349	if ! list_contains "$1" "$2" ; then
350		list_append "$1" "$2" "$3"
351	fi
352}
353
354# Check if a directory contains a file while ignoring case differences.
355# Usage: icontains <dir> <filename>
356icontains() {
357	[ ! -z "$(find "$1" -mindepth 1 -maxdepth 1 -iname "$2")" ]
358}
359
360# Check if a directory or file is writable or can be created.
361# Usage: is_writable <path>
362is_writable() {
363	[ -w "$1" ] && return $true
364	[ ! -e "$1" ] && is_writable "$(dirname "$1")"
365}
366
367# Create a directory and die with a message on error.
368# Usage: create_dir <path> <type>
369create_dir() {
370	mkdir -p "$1" || die "Could not create $2 directory: $1"
371}
372
373probe_file_dirs=''
374set_append probe_file_dirs "$user_pwd"
375set_append probe_file_dirs "$downloads_dir"
376set_append probe_file_dirs "$HOME"
377set_append probe_file_dirs "$tempdir"
378
379# Find a file in standard directories.
380# Usage: probe_file <command> <filename> [comment]
381# Will call `command <file>` for each file found.
382probe_file() {
383	eval "for _probe_file_d in $probe_file_dirs ; do [ -f \"\$_probe_file_d/\$2\" ] && \$1 \"\$_probe_file_d/\$2\" \"\$3\" && return \$true ; done"
384}
385
386# Find files in standard directories.
387# Usage: probe_file <command> <list> [comment]
388# Will call `command <file>` for each file found.
389probe_files() {
390	[ -z "$2" ] && return $false
391	eval "for _probe_files_file in $2 ; do probe_file \"\$1\" \"\$_probe_files_file\" \"\$3\" && return \$true ; done"
392	return $false
393}
394
395
396##########################################################################################
397# Parse command-line arguments
398
399extract_zip_reqs=''
400list_append extract_zip_reqs 'bsdtar' 'libarchive'
401list_append extract_zip_reqs 'unzip'
402list_append extract_zip_reqs '7za'
403list_append extract_zip_reqs '7z' 'p7zip'
404extract_ms_cab_reqs=''
405list_append extract_ms_cab_reqs 'bsdtar' 'with libarchive 3.1+'
406list_append extract_ms_cab_reqs 'cabextract' "$cabextract_url"
407list_append extract_ms_cab_reqs '7za'
408list_append extract_ms_cab_reqs '7z' 'p7zip'
409extract_installshield_reqs=''
410list_append extract_installshield_reqs 'unshield'
411extract_innosetup_reqs=''
412list_append extract_innosetup_reqs 'innoextract' "$innoextract_url"
413mount_cdrom_reqs=''
414list_append mount_cdrom_reqs 'fuseiso'
415extract_iso_reqs=''
416list_append extract_iso_reqs 'isoinfo'
417list_append extract_iso_reqs 'bsdtar' 'libarchive'
418list_append extract_iso_reqs '7z' 'p7zip'
419extract_cdrom_reqs=''
420list_merge extract_cdrom_reqs mount_cdrom_reqs
421list_merge extract_cdrom_reqs extract_iso_reqs
422download_reqs=''
423list_append download_reqs 'wget'
424list_append download_reqs 'curl'
425list_append download_reqs 'fetch' 'FreeBSD'
426
427printf '%s %s\n' "${white}Welome to the ${green}Arx Fatalis${white} ${patch_ver} data" \
428     "install script for UNIX-like systems!${reset}"
429
430patchfile=''      # Main patch file
431patchfile_jp=''   # Japanese patch file
432sourcefile=''     # Source file or directory
433datadir=''        # Output data directory
434batch=0           # Never wait for user input
435gui=0             # Display a graphical user interface (command-line interface otherwise)
436install=1         # Install new non-patch files
437installed_stuff=0 # Have we already installed anything?
438patch=1           # Install patch files if needed
439probe_patch=1     # Look for patch files in standard locations and download if needed
440redirect_log=1    # Redirect standard output/error output to a log file in GUI mode
441
442# Enable compatiblity with old install-* scripts.
443# Usage: enable_compat_mode <help-flag> <sourcefile> <patchfile> <datadir>
444enable_compat_mode() {
445	print \
446		"${yellow}Enabling compatibility mode for ${pink}$command${yellow}.${reset}
447
448${dim_yellow}The individual ${dim_pink}install-*${dim_yellow} scripts have been merged.
449Rename this script to something else (like ${dim_pink}arx-install-data${dim_yellow}) to unlock its full power!${reset}
450" >&2
451	batch=1
452	probe_patch=0
453	if [ -z "$1" ] || [ "$1" = '--help' ] || [ "$1" = '-h' ] ; then
454		printf '%s\n\n%s\n' "$5" \
455			"${yellow}More options are available in the non-compatiblity mode.${reset}"
456		exit $false
457	fi
458	if [ -z "$2" ] ; then install=0           ; else sourcefile="$2" ; fi
459	if [ -z "$3" ] ; then patch=0             ; else patchfile="$3"  ; fi
460	if [ -z "$4" ] ; then datadir="$user_pwd" ; else datadir="$4"    ; fi
461}
462
463case "$command" in
464
465install-cd)
466[ "$1" = "--no-progress" ] && shift # ignore - not supported
467enable_compat_mode "$1" "$1" "$2" "$3" "\
468Usage: $command path/to/mount/point/ path/to/ArxFatalis_1.21_MULTILANG.exe [output_dir]
469or     $command path/to/cd.iso path/to/ArxFatalis_1.21_MULTILANG.exe [output_dir]" ;;
470
471install-copy)
472enable_compat_mode "$1" "$1" '' "$2" "\
473Usage: $command path/to/ArxFatalis/ [output_dir]" ;;
474
475install-demo)
476enable_compat_mode "$1" "$1" '' "$2" "\
477Usage: $command path/to/arx_demo_english.zip [output_dir]" ;;
478
479install-gog)
480[ "$1" = "--no-progress" ] && shift # ignore - not supported
481enable_compat_mode "$1" "$1" '' "$2" "\
482Usage: $command path/to/setup_arx_fatalis.exe [output_dir]" ;;
483
484install-verify)
485enable_compat_mode "$1" '' '' "$1" "\
486Usage: $command [directory]" ;;
487
488*) # non-compatibility mode
489
490# Print elements in a list, joined by ' or '
491# Usage: print_help_or <list-var> [color]
492print_help_or() {
493	_print_help_or_var=$1
494	eval "_print_help_or_list=\"\$$1\""
495	_print_help_or_color="$2"
496	[ -z "$1" ] && return
497	eval "
498		_print_help_or_i=0
499		for _print_help_or_entry in $_print_help_or_list ; do
500			[ \$_print_help_or_i = 0 ] || puts ' or '
501			printf '%s%s' \"\$_print_help_or_color\" \"\$_print_help_or_entry\"
502			[ -z \"\$_print_help_or_color\" ] || puts \"\$reset\"
503			_print_help_or_comment=\"\$(list_comment \$_print_help_or_var \$_print_help_or_i)\"
504			[ -z \"\$_print_help_or_comment\" ] || printf ' (%s)' \"\$_print_help_or_comment\"
505			_print_help_or_i=\$((\$_print_help_or_i + 1))
506		done
507	"
508}
509
510# Print elements in a list, one per line.
511# Usage: print_help_list <prefix-format> <list-var>
512# prefi-format will receive one argument: the list index starting at 1
513print_help_list() {
514	_print_help_list_prefix="$1"
515	_print_help_list_var=$2
516	eval "_print_help_list_list=\"\$$2\""
517	[ -z "$1" ] && return
518	eval "
519		_print_help_list_i=0
520		for _print_help_list_entry in $_print_help_list_list ; do
521			case \"\$_print_help_list_prefix\" in
522				*%*) printf \"\$_print_help_list_prefix\" \$((\$_print_help_list_i + 1)) ;;
523				*)   puts \"\$_print_help_list_prefix\"
524			esac
525			printf \"%s\${reset}\" \"\$_print_help_list_entry\"
526			_print_help_list_comment=\"\$(list_comment \$_print_help_list_var \$_print_help_list_i)\"
527			[ -z \"\$_print_help_list_comment\" ] || printf ' (%s)' \"\$_print_help_list_comment\"
528			printf '\n'
529			_print_help_list_i=\$((\$_print_help_list_i + 1))
530		done
531	"
532}
533
534# Print help output.
535# Usage: print_help [<error-message>]
536print_help() {
537	[ -z "${1-}" ] || ( printf '%s\n\n' "${red}$1${reset}" )
538	print "
539${white}Simply start the script without any arguments to select paths interactively:
540       \$ $command${reset}
541
542Usage: $command [--source] source [--patch patchfile] [[--data-dir] datadir]
543       $command [--patch patchfile] [--data-dir datadir]
544       $command --verify [[--data-dir] datadir]
545
546 ${green}-s, --source PATH${reset}   Path to the source file or directory
547 ${cyan}-d, --data-dir DIR${reset}  Where to install the data
548 ${blue}-p, --patch FILE${reset}    Path to the ${patch_ver} patch file
549 --patch-jp FILE     Path to the ${patch_jp_ver} Japanese patch file
550 -v, --verify        Only verify the files in the data-dir, don't install new ones,
551                     except for patch files.
552 -n, --no-patch      Don't use a patch file unless explicitly specified.
553 -h, --help          Print this message and maybe more
554 -b, --batch         Never ask the user questions
555 -g, --gui           Show a GUI asking the user what to do
556                     Requires ${dim_pink}KDialog${reset}, ${dim_pink}Zenity${reset}, or ${dim_pink}Xmessage${reset}.
557                     If none of them are available the script is re-launched
558                     in a terminal emulator.
559 -c, --cli           Interactively ask the user to select files/directories (no GUI)
560 --no-redirect-log   Don't redirect output to a log file when in GUI mode
561 --disable-COMMAND   Don't use the given tool, even if it exists.
562                     Valid values are unzip, bsdtar, cabextract, isoinfo,
563                     fuseiso, fusermount, mount, umount and innoextract,
564                     wget, curl, fetch, unshield, kdialog, zenity, Xdialog,
565                     qdbus, dcop, x-terminal-emulator, urxvt, gtkterm, aterm,
566                     rxvt, gnome-terminal, konsole, xterm, gxmessage, xmessage,
567                     md5sum, md5.
568
569--gui is enabled by default if there are no arguments *and* stdin, stdout or stderr is not a terminal
570"
571	[ ! -z "${1-}" ] && exit $false
572	help_innosetup="$(print_help_or extract_innosetup_reqs "$dim_pink")"
573	help_cdrom="$(print_help_or extract_cdrom_reqs "$dim_pink") or root access"
574	help_cab="$(print_help_or extract_ms_cab_reqs "$dim_pink")"
575	help_zip="$(print_help_or extract_zip_reqs "$dim_pink")"
576	help_unshield="$(print_help_or extract_installshield_reqs "$dim_pink")"
577	help_download="$(print_help_or download_reqs "$dim_pink")"
578	help_optpatch="may use the 1.21 patch file and require ${help_innosetup} if not already patched"
579	help_probe_file_dirs="
580   a) the current working directory  (\$PWD):              $user_pwd
581   b) the user's downloads directory (\$HOME):             $HOME
582   c) the user's home directory      (\$XDG_DOWNLOAD_DIR): $downloads_dir
583   d) the temp directory             (\$TMPDIR):           $tempdir"
584  help_probe_file_dirs_patch="${help_probe_file_dirs}
585   e) the directory containing the source file"
586  help_probed_files="$gog_names $demo_names"
587	print "
588The ${pink}dependencies${reset} required by the ${command} script depend on the source files.
589However, you always need either ${dim_pink}md5sum${reset} or ${dim_pink}md5${reset}.
590
591
592The ${green}source${reset} can be one of many things:
593
594 * ${white}Mounted Arx Fatalis ${green}cdrom${reset}
595   requires:
596    - ${help_cab}
597    - ${help_innosetup}
598   needs the 1.21 patch file
599
600 * ${white}Arx Fatalis cdrom ${green}ISO${white} image / device file${reset}
601   requires:
602    - ${help_cdrom}
603    - ${help_cab}
604    - ${help_innosetup}
605   needs the 1.21 patch file
606
607 * ${white}Arx Fatalis installer from ${green}GOG.com${white}${reset} ($(print_help_or gog_names))
608   requires:
609    - ${help_innosetup}
610   never uses the 1.21 patch file
611   get it from ${dim_green}${gog_url}${reset}
612
613 * ${green}Installed${white} copy of Arx Fatalis${reset} (for example from ${green}Steam${reset})
614   ${help_optpatch}
615   get it from ${dim_green}${steam_url}${reset}
616
617 * ${white}Arx Fatalis ${green}demo${white} zip${reset} ($(print_help_or demo_names))
618   requires:
619    - ${help_zip}
620    - ${help_cab}
621   never uses the 1.21 patch file
622   get it from ${dim_green}${demo_url}${reset}
623
624 * ${white}Extracted Arx Fatalis demo installer${reset}
625   requires:
626    - ${help_cab}
627   never uses the 1.21 patch file
628
629 * ${white}Installed copy of the Arx Fatalis demo${reset}
630   never uses the 1.21 patch file
631
632If no source is specified, these files will be probed:
6331. The following files in${help_probe_file_dirs}
634$(print_help_list "   1.%d ${green}" help_probed_files)
6352. If \$WINEPREFIX is set, any installation in there
6363. Any installation in the default WINEPREFIX (${green}~/.wine${reset})
6374. Any mounted ${green}cdrom${reset} or ISO file
638
639
640If no ${blue}patch${reset} file is specified, but is needed and
641the --no-patch option wasn't specified specified:
6421. Try to find the following files in${help_probe_file_dirs_patch}
643   1.1. ${blue}${patch_name}${reset}
644   1.2. $(printf "$patch_name_localized" '<LANG>')
645        Where <LANG> is one of EN, ES, FR, GE, IT, RU,
646        depending on the language of the data files.
6472. Downloaded from:
648$(print_help_list " - ${dim_blue}" patch_urls)
649Downloading the patch file requires ${help_download}.
650Extracting the ${patch_ver} patch file requires ${help_innosetup}.
651
652For the Japanese version, if no ${blue}patch-jp${reset} file is specified,
653but is needed and the --no-patch option wasn't specified specified:
6541. Try to find ${blue}${patch_jp_name}${reset} in${help_probe_file_dirs_patch}
6552. Downloaded from:
656$(print_help_list " - ${dim_blue}" patch_jp_urls)
657Downloading the patch file requires ${help_download}.
658Extracting the Japanese patch file requires:
659    - ${help_unshield}
660    - ${help_cab}.
661
662
663If no ${cyan}data-dir${reset} to install into is specified,
664one is automatically selected similarly to how Arx Libertatis would:
665If --verify and --no-patch (and --no-patch) are give, use the first existing
666directory of the following, otherwise, use the first existing writable directory
667or, if none exists, the first directory that can be created:
6681. Any path in \$${scommand}_PATH (for use in wrapper scripts)
6692. \"\${XDG_DATA_DIRS:-\"/usr/local/share/:/usr/share/\"}:/opt\" / \"$data_dir_suffixes\":"
670	i=1
671	eval "set -- $(to_list "$data_dirs")"
672	for prefix in "$@" ; do
673		eval "set -- $(to_list "$data_dir_suffixes")"
674		for suffix ; do
675			printf "   2.%d. ${dim_cyan}%s${reset}\\n" $i "$prefix/$suffix"
676			i=$(($i + 1))
677		done
678	done
679print "3. \"\${XDG_DATA_HOME:-\"\$HOME/.local/share\"}\" / \"$user_dir_suffixes\""
680	i=1
681	eval "set -- $(to_list "$user_dir_suffixes")"
682	for suffix ; do
683		printf "   3.%d. ${dim_cyan}%s${reset}\\n" $i "$data_home/$suffix"
684			i=$(($i + 1))
685	done
686	print
687	exit $true
688}
689
690user_is_sane=1
691if [ ! -t 0 ] || [ ! -t 1 ] || [ ! -t 2 ] ; then
692	[ $# = 0 ] && gui=1
693fi
694while [ $# -gt 0 ] ; do
695	case "$1" in
696		--source=*)               sourcefile="${1#--source=}"   ; install=1 ;;
697		-s|--source)      shift ; sourcefile="$1"               ; install=1 ;;
698		--data-dir=*)             datadir="${1#--data-dir=}"                ;;
699		-d|--data-dir)    shift ; datadir="$1"                              ;;
700		--patch=*)                patchfile="${1#--patch=}"       ; patch=1 ;;
701		-p|--patch)       shift ; patchfile="$1"                  ; patch=1 ;;
702		--patch-jp=*)             patchfile_jp="${1#--patch-jp=}" ; patch=1 ;;
703		--patch-jp)       shift ; patchfile_jp="$1"               ; patch=1 ;;
704		-v|--verify)                                              install=0 ;;
705		-n|--no-patch)    [ -z "$patchfile" ] && [ -z "$patchfile_jp" ] && patch=0
706		                  probe_patch=0   ;;
707		-b|--batch)       batch=1         ;;
708		-g|--gui)                   gui=1 ;;
709		-c|--cui|--cli)   batch=0 ; gui=0 ;;
710		--no-redirect-log) redirect_log=0 ;;
711		--i-am-insane)    user_is_sane=0  ;;
712		--disable-*)              disable_command "${1#--disable-}" ;;
713		--disable)        shift ; disable_command "${1#--disable-}" ;;
714		-h|--help)        print_help                     ;;
715		-*)               print_help "Uknown option: $1" ;;
716		*)
717			if [ -z "${sourcefile-}" ] && [ $install = 1 ] ; then sourcefile="$1"
718			else if [ -z "${datadir-}"    ] ; then datadir="$1"
719			else print_help "Too many options: $1" ; fi ;fi
720	esac
721	[ -z "${1-}" ] && print_help "Expected more options"
722	shift;
723done
724
725print "See \`${dim_pink}$command --help${reset}\` for available options."
726
727esac
728
729# Make user-provided paths absolute
730[ ! -z "$sourcefile" ] && sourcefile="$(abspath "$sourcefile")"
731[ ! -z "$datadir" ]    && datadir="$(abspath "$datadir")"
732[ ! -z "$patchfile" ]  && patchfile="$(abspath "$patchfile")"
733
734# Sanity check
735[ $install = 1 ] && [ $batch = 1 ] && [ -z "$sourcefile" ] && [ $user_is_sane = 1 ] \
736	&& die "You have used --batch without providing a source file!
737This would just pick the first source file found, which is a bad idea™.
738If you really want this, add the --i-am-insane option."
739
740
741##########################################################################################
742# User interface abstraction
743
744_dialog_title="Arx Fatalis ${patch_ver} data installer"
745
746# Handle magic environment variable to tell the script that it has been launched
747# in its own terminal and should not try to create a GUI.
748if [ $batch = 0 ] && [ "$_arx_install_data_force_cli" = 1 ] ; then
749	trap '_arx_install_data_force_cli=0 ; quit 1' INT
750	printf "\n${yellow}%s${reset}\n\n" \
751		'Note: Install KDialog, Zenity or Xdialog for a better GUI'
752	wait_exit() {
753		[ "$_arx_install_data_force_cli" = 1 ] && print 'Press enter to exit...' && read f
754		exit $false
755	}
756	on_exit wait_exit
757	gui=0
758	for var in batch install patch probe_patch sourcefile datadir \
759		patchfile patchfile_jp disabled_commands; do
760		eval "$var=\"\$_arx_install_data_force_$var\""
761	done
762fi
763
764# Select the dialog backend to use
765if [ $gui = 1 ] ; then
766
767	# Detect if we are running in a KDE session
768	is_kde=0
769	case "$DESKTOP_SESSION" in *kde*|*KDE*) is_kde=1 ; esac
770	[ -z "$KDE_FULL_SESSION" ]           || is_kde=1
771	[ -z "$KDE_SESSION_UID" ]            || is_kde=1
772	[ -z "$KDE_SESSION_VERSION" ]        || is_kde=1
773
774	# Select the GUI backend, prefer kdialog for KDE sessions, zenity otherwise
775	if [ $is_kde = 1 ] ; then preferred=kdialog ; else preferred=zenity ; fi
776	for backend in $preferred zenity kdialog Xdialog ; do
777		have $backend && gui=$backend && break
778	done
779
780	if [ $gui = 1 ] ; then
781
782		# No dialog backend available
783		# Try opening a graphical terminal and launching the script in there.
784		print 'No GUI dialog backend is available - trying to launch a terminal emulator'
785		term_cmd="$(abspath "$(command -v "$0" 2> /dev/null)")"
786		# Not all terminals accept command arguments in the same way.
787		# Instead of hacking terminal-specific code, use a magic environment
788		# variable to tell the sub-process how to behave.
789		_arx_install_data_force_cli=1
790		export _arx_install_data_force_cli
791		for var in batch install patch probe_patch sourcefile datadir \
792			patchfile patchfile_jp disabled_commands ; do
793			eval "_arx_install_data_force_$var=\"\$$var\""
794			eval "export _arx_install_data_force_$var"
795		done
796		if [ $is_kde = 1 ] ; then preferred=konsole ; else preferred=x-terminal-emulator ; fi
797		for backend in x-terminal-emulator $preferred \
798			aterm urxvt rxvt konsole xterm gnome-terminal
799		do
800			if have $backend ; then
801				$backend -e "$term_cmd" || continue
802				exit $true
803			fi
804		done
805
806		# Hm, that didn't work either - bail
807		message="No GUI dialog backend is available"
808		message="$message - install KDialog, Zenity or Xdialog, or use the --cli option."
809		# Final attempt to let the user know what happened
810		for backend in gxmessage xmessage ; do
811			if have $backend ; then
812				$backend -center -buttons OK "$_dialog_title
813
814$message"
815				break
816			fi
817		done
818		die "$message"
819
820	fi
821
822	# We don't need colors for the UI, but they may cause problems - get rit of them
823	disable_color
824
825	if [ $redirect_log = 1 ] ; then
826		# Redirect all further output into a log file
827		logfile="$(abspath "$(mktemp "$tempdir/arx-install-data.log.XXXXX")")"
828		clean_logfile() {
829			[ -z "$logfile" ] || rm -f "$logfile"
830		}
831		on_exit clean_logfile
832		print "Enabling GUI mode, standard output/error saved to $logfile"
833		print "Use the --cli option for an interactive command-line interface."
834		exec > "$logfile" 2>&1
835	else
836		print "Enabling GUI mode..."
837		print "Use the --cli option for an interactive command-line interface."
838	fi
839
840else
841
842	print "Enabling CLI mode, use the --gui option for a graphical interface."
843
844fi
845
846#----------------------------------------------------------------------------------------#
847# Functions for controlling an asynchronous process via stdin
848
849pipe_file=''
850pipe_pid=0
851
852# Run a command in the background and open a pipe to pass commmands to it
853# Usage: pipe_create <command> [<args>...]
854pipe_create() {
855
856	pipe_destroy
857
858	# Pipe commands via a FIFO or, if that fails, via a regular file
859	pipe_file="$(mktemp -u "$tempdir/arx-install-data.pipe.XXXXX")"
860	if mkfifo -m 600 "$pipe_file" 2> /dev/null ; then
861		# Fast communication via a FIFO
862		cat "$pipe_file" | "$@" &
863		pipe_pid="$!"
864	else
865		# Fallback via regular file, may use polling
866		pipe_file="$(mktemp "$tempdir/arx-install-data.pipe.XXXXX")"
867		[ -z "$pipe_file" ] && return $false
868		tail -f "$pipe_file" | "$@" &
869		pipe_pid="$!"
870	fi
871
872	# Open fd 3 for writing into the pipe
873	exec 3> "$pipe_file"
874
875	return $true
876}
877
878# Kill the program created via pipe_create and cleanup files
879# Usage: pipe_destroy
880pipe_destroy() {
881
882	# Close fd pointing to the pipe
883	exec 3<&-
884
885	# Remove the FIFO or temp file
886	[ -z "$pipe_file" ] || rm -f "$pipe_file" > /dev/null 2>&1
887	pipe_file=''
888
889	# Terminate the remote process
890	[ "$pipe_pid" = 0 ] || kill "$pipe_pid" > /dev/null 2>&1
891	pipe_pid=0
892
893	return $true
894}
895
896# Check if the remote process is running
897# Usage: pipe_exists || print 'oh noes'
898pipe_exists() {
899	[ -z "$pipe_file" ] && return $false
900	[ "$pipe_pid" = 0 ] && return $false
901	kill -s 0 "$pipe_pid" > /dev/null 2>&1
902}
903
904# Send a message to the remote process
905# Usage: pipe_write <command>
906pipe_write() {
907	[ -z "$pipe_file" ] || print "$1" >&3
908}
909
910#----------------------------------------------------------------------------------------#
911# Code for the different GUI/CLI implementations
912# Each implementation exposes dialog_* primitives that are used by generic functions.
913
914case $gui in
915
916#----------------------------------------------------------------------------------------#
917zenity)
918
919# Helper functions
920
921# Run Zenity
922# Usage: zenity_run <title-prefix> <dialog-type> [<args>...]
923zenity_run() {
924	_zenity_run_t="$1" ; shift
925	[ -z "$_zenity_run_t" ] || _zenity_run_t="$_zenity_run_t - "
926	zenity --title "$_zenity_run_t$_dialog_title" "$@"
927}
928
929# Dialog abstraction
930
931# Create the main progress window.
932# Usage: dialog_create
933dialog_create() {
934	pipe_create zenity  --title "$_dialog_title$1" --width 450 --progress
935}
936
937# Destroy the main progress window.
938# Usage: dialog_destroy
939dialog_destroy() {
940	pipe_destroy
941}
942
943# Show an error dialog.
944# Usage: dialog_error <message>
945dialog_error() {
946	zenity_run 'Error' --error --no-wrap --text="$1"
947}
948
949# Show a message box.
950# Usage: dialog_message <message>
951dialog_message() {
952	zenity_run 'Status' --info --no-wrap --text="$1"
953}
954
955# Ask a yes/no question.
956# Usage: dialog_ask <question>
957dialog_ask() {
958	zenity_run 'Confirm' --question --no-wrap --ok-label=Yes --cancel-label=No --text="$1"
959}
960
961# Has the user quested to cancel the operation?
962# Usage: dialog_cancelled && print "cancelled"
963dialog_cancelled() {
964	! pipe_exists
965}
966
967# Set the status text.
968# Usage: dialog_set_text <text>
969dialog_set_text() {
970	pipe_write "#$1"
971}
972
973# Set if the progress bar should continously animate instead of showing the value.
974# Usage: dialog_set_pulsate <enable>
975dialog_set_pulsate() {
976	if [ $1 = 1 ]
977		then pipe_write "pulsate:true"
978		else pipe_write "pulsate:false"
979	fi
980}
981
982# Set the current progress value.
983# Usage: dialog_set_value <percentage>
984dialog_set_value() {
985	pipe_write "$1"
986}
987
988# Select an entry in a list.
989# Usage dialog_select_entry <var> <label> <tag1> <item1> [ <tag2> < item2> ... ]
990dialog_select_entry() {
991	_zenity_select_entry_v="$1" ; shift
992	_zenity_select_entry_t="$1" ; shift
993	_zenity_select_entry_r="$(
994		zenity_run 'Select path' --width 550 --height 300 \
995			--list --text="$_zenity_select_entry_t" \
996			--column '#'  --column 'Path' --hide-column=1 "$@" --hide-header
997	)"
998	[ -z "$_zenity_select_entry_r" ] && return $false
999	eval "$_zenity_select_entry_v=\"\$_zenity_select_entry_r\""
1000	return $true
1001}
1002
1003# dialog_select_path does not support the --any flag
1004dialog_select_path_any=0
1005
1006# Let the user select a path.
1007# Usage: dialog_select_path (--file|--dir|--any) <result-var> <label>
1008# Any is only supported if $dialog_select_path_any is 1.
1009dialog_select_path() {
1010	case "$1" in
1011		--any) die 'not implemented' ;;
1012		--file) _zenity_select_path_f='--file-selection' ;;
1013		--dir)  _zenity_select_path_f='--file-selection --directory' ;;
1014	esac
1015	_zenity_select_path="$(
1016		eval "zenity_run \"\$3\" $_zenity_select_path_f" 2> /dev/null
1017	)"
1018	[ -z "$_zenity_select_path" ] && return $false
1019	eval "$2=\"\$_zenity_select_path\""
1020	return $true
1021}
1022
1023dialog_retry() {
1024	zenity_run 'Error' --question --no-wrap --text="$1" \
1025		--ok-label='Retry' --cancel-label='Ignore'
1026	case $? in
1027		0) dialog_retry_choice='retry' ;;
1028		1) dialog_retry_choice='ignore' ;;
1029		*) dialog_retry_choice='abort' ;;
1030	esac
1031}
1032
1033;;
1034
1035#----------------------------------------------------------------------------------------#
1036kdialog)
1037
1038# Helper functions
1039
1040kdialog_handle='' # dbus/dcop handle for the main progress window
1041
1042# Send a message to the main KDialog instance non-_q variants hide all output
1043kdialog_qdbus_q() { have qdbus && eval "qdbus $kdialog_handle \"\$@\"" 2> /dev/null ; }
1044kdialog_qdbus()   { kdialog_qdbus_q "$@" > /dev/null                                ; }
1045kdialog_dcop_q()  { have dcop  && eval "dcop  $kdialog_handle \"\$@\"" 2> /dev/null ; }
1046kdialog_dcop()    { kdialog_dcop_q  "$@" > /dev/null                                ; }
1047kdialog_cmd_q()   { kdialog_qdbus_q "$@" || kdialog_dcop_q "$@"                     ; }
1048kdialog_cmd()     { kdialog_cmd_q "$@" > /dev/null                                  ; }
1049
1050# Run KDialog
1051# Usage: kdialog_run <title-prefix> <dialog-type> [<args>...]
1052kdialog_run() {
1053	_kdialog_run_t="$1" ; shift
1054	[ -z "$_kdialog_run_t" ] || _kdialog_run_t="$_kdialog_run_t - "
1055	kdialog --icon arx-libertatis --title "$_kdialog_run_t$_dialog_title" "$@"
1056}
1057
1058# Dialog abstraction
1059
1060# Create the main progress window.
1061# Usage: dialog_create
1062dialog_create() {
1063	dialog_destroy
1064	_kdialog_force_width='WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'
1065	kdialog_handle="$(kdialog_run '' --progressbar "$_kdialog_force_width" 0)"
1066	[ -z "$kdialog_handle" ] && return $false
1067	kdialog_cmd showCancelButton true
1068	return $true
1069}
1070
1071# Destroy the main progress window.
1072# Usage: dialog_destroy
1073dialog_destroy() {
1074	[ -z "$kdialog_handle" ] && return $true
1075	kdialog_cmd close
1076	kdialog_handle=''
1077}
1078
1079# Show an error dialog.
1080# Usage: dialog_error <message>
1081dialog_error() {
1082	kdialog_run 'Error' --error "$1" > /dev/null 2> /dev/null
1083}
1084
1085# Show a message box.
1086# Usage: dialog_message <message>
1087dialog_message() {
1088	kdialog_run 'Status' --msgbox "$1" > /dev/null 2> /dev/null
1089}
1090
1091# Ask a yes/no question.
1092# Usage: dialog_ask <question>
1093dialog_ask() {
1094	kdialog_run 'Confirm' --warningyesno "$1" > /dev/null 2> /dev/null
1095}
1096
1097# Has the user quested to cancel the operation?
1098# Usage: dialog_cancelled && print "cancelled"
1099dialog_cancelled() {
1100	[ "$(kdialog_cmd_q wasCancelled || print true)" = true ]
1101}
1102
1103# Set the status text.
1104# Usage: dialog_set_text <text>
1105dialog_set_text() {
1106	kdialog_qdbus setLabelText "$1" || kdialog_dcop setLabel "$1"
1107}
1108
1109# Set if the progress bar should continously animate instead of showing the value.
1110# Usage: dialog_set_pulsate <enable>
1111dialog_set_pulsate() {
1112	_kdialog_max=100
1113	[ $1 = 1 ] && _kdialog_max=0
1114	kdialog_qdbus Set "" maximum $_kdialog_max || kdialog_dcop setMaximum $_kdialog_max
1115}
1116
1117# Set the current progress value.
1118# Usage: dialog_set_value <percentage>
1119dialog_set_value() {
1120	kdialog_qdbus Set "" value "$1" || kdialog_dcop setProgress "$1"
1121	[ $1 = 100 ] && kdialog_cmd showCancelButton true
1122}
1123
1124# Select an entry in a list.
1125# Usage dialog_select_entry <var> <label> <tag1> <item1> [ <tag2> < item2> ... ]
1126dialog_select_entry() {
1127	_kdialog_select_entry_v="$1" ; shift
1128	_kdialog_select_entry_t="$1" ; shift
1129	_kdialog_select_entry_w="                                   "
1130	_kdialog_select_entry_w="$_kdialog_select_entry_w$_kdialog_select_entry_w"
1131	_kdialog_select_entry_r="$(
1132		kdialog_run 'Select path' \
1133			--menu "$_kdialog_select_entry_t$_kdialog_select_entry_w" "$@" 2> /dev/null
1134	)"
1135	[ -z "$_kdialog_select_entry_r" ] && return $false
1136	eval "$_kdialog_select_entry_v=\"\$_kdialog_select_entry_r\""
1137	return $true
1138}
1139
1140# dialog_select_path does not support the --any flag
1141dialog_select_path_any=0
1142
1143# Let the user select a path.
1144# Usage: dialog_select_path (--file|--dir|--any) <result-var> <label>
1145# Any is only supported if $dialog_select_path_any is 1.
1146dialog_select_path() {
1147	case "$1" in
1148		--any) die 'not implemented' ;;
1149		--file) _kdialog_select_path_f=--getopenfilename ;;
1150		--dir)  _kdialog_select_path_f=--getexistingdirectory ;;
1151	esac
1152	_kdialog_select_path="$(
1153		kdialog_run "$3" $_kdialog_select_path_f "$HOME" 2> /dev/null
1154	)"
1155	[ -z "$_kdialog_select_path" ] && return $false
1156	eval "$2=\"\$_kdialog_select_path\""
1157	return $true
1158}
1159
1160dialog_retry() {
1161	kdialog_run 'Error' \
1162		--yes-label 'Retry' --no-label 'Ignore' --cancel-label 'Abort' \
1163		--warningyesnocancel "$1" 2>&1
1164	case $? in
1165		0) dialog_retry_choice='retry' ;;
1166		1) dialog_retry_choice='ignore' ;;
1167		*) dialog_retry_choice='abort' ;;
1168	esac
1169}
1170
1171;;
1172
1173#----------------------------------------------------------------------------------------#
1174Xdialog)
1175
1176# Helper functions
1177
1178# Run Xdialog
1179# Usage: Xdialog_run <title-prefix> <dialog-type> [<args>...]
1180Xdialog_run() {
1181	_Xdialog_run_t="$1" ; shift
1182	[ -z "$_Xdialog_run_t" ] || _Xdialog_run_t="$_Xdialog_run_t - "
1183	Xdialog --left --title "$_Xdialog_run_t$_dialog_title" "$@"
1184}
1185
1186# Dialog abstraction
1187
1188# Create the main progress window.
1189# Usage: dialog_create
1190dialog_create() {
1191	_Xdialog_width='WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW'
1192	pipe_create Xdialog --left --title "$_dialog_title$1" --gauge "$_Xdialog_width" 0 0
1193}
1194
1195# Destroy the main progress window.
1196# Usage: dialog_destroy
1197dialog_destroy() {
1198	pipe_destroy
1199}
1200
1201# Show an error dialog.
1202# Usage: dialog_error <message>
1203dialog_error() {
1204	dialog_message "$1" # no dedicated error box for Xdialog
1205}
1206
1207# Show a message box.
1208# Usage: dialog_message <message>
1209dialog_message() {
1210	Xdialog_run 'Status' --msgbox "$1" 0 0
1211}
1212
1213# Ask a yes/no question.
1214# Usage: dialog_ask <question>
1215dialog_ask() {
1216	Xdialog_run 'Confirm' --yesno "$1" 0 0
1217}
1218
1219# Has the user quested to cancel the operation?
1220# Usage: dialog_cancelled && print "cancelled"
1221dialog_cancelled() {
1222	! pipe_exists
1223}
1224
1225# Set the status text.
1226# Usage: dialog_set_text <text>
1227dialog_set_text() {
1228	pipe_write 'XXX'
1229	pipe_write "$1"
1230	pipe_write 'XXX'
1231}
1232
1233# Set if the progress bar should continously animate instead of showing the value.
1234# Usage: dialog_set_pulsate <enable>
1235dialog_set_pulsate() {
1236	true # Pulsate is not supported by Xdialog
1237}
1238
1239# Set the current progress value.
1240# Usage: dialog_set_value <percentage>
1241dialog_set_value() {
1242	pipe_write "$1"
1243}
1244
1245# Select an entry in a list.
1246# Usage dialog_select_entry <var> <label> <tag1> <item1> [ <tag2> < item2> ... ]
1247dialog_select_entry() {
1248	_Xdialog_select_entry_v="$1" ; shift
1249	_Xdialog_select_entry_t="$1" ; shift
1250	_Xdialog_select_entry_r="$(
1251		Xdialog_run 'Select path' \
1252			--menubox "$_Xdialog_select_entry_t" 20 80 10 "$@" 2>&1
1253	)"
1254	[ -z "$_Xdialog_select_entry_r" ] && return $false
1255	eval "$_Xdialog_select_entry_v=\"\$_Xdialog_select_entry_r\""
1256	return $true
1257}
1258
1259# dialog_select_path does not support the --any flag
1260dialog_select_path_any=0
1261
1262# Let the user select a path.
1263# Usage: dialog_select_path (--file|--dir|--any) <result-var> <label>
1264# Any is only supported if $dialog_select_path_any is 1.
1265dialog_select_path() {
1266	case "$1" in
1267		--any) die 'not implemented' ;;
1268		--file) _Xdialog_select_path_f=--fselect ;;
1269		--dir)  _Xdialog_select_path_f=--dselect ;;
1270	esac
1271	_Xdialog_select_path="$(
1272		Xdialog_run "$3" $_Xdialog_select_path_f "$HOME" 0 0 2>&1
1273	)"
1274	[ -z "$_Xdialog_select_path" ] && return $false
1275	eval "$2=\"\$_Xdialog_select_path\""
1276	return $true
1277}
1278
1279dialog_retry() {
1280	Xdialog_run 'Error' --ok-label='Retry' --cancel-label='Ignore' --yesno "$1" 0 0
1281	case $? in
1282		0) dialog_retry_choice='retry' ;;
1283		1) dialog_retry_choice='ignore' ;;
1284		*) dialog_retry_choice='abort' ;;
1285	esac
1286}
1287
1288;;
1289
1290#----------------------------------------------------------------------------------------#
12910) # command-line
1292
1293# Dialog abstraction
1294
1295# Create the main progress window.
1296# Usage: dialog_create
1297dialog_create() {
1298	true
1299}
1300
1301# Destroy the main progress window.
1302# Usage: dialog_destroy
1303dialog_destroy() {
1304	true
1305}
1306
1307# Show an error dialog.
1308# Usage: dialog_error <message>
1309dialog_error() {
1310	true # error messages are always printed to stdout
1311}
1312
1313# Show a message box.
1314# Usage: dialog_message <message>
1315dialog_message() {
1316	true
1317}
1318
1319# Ask a yes/no question.
1320# Usage: dialog_ask <question>
1321dialog_ask() {
1322	die 'unimplemented'
1323}
1324
1325# Has the user quested to cancel the operation?
1326# Usage: dialog_cancelled && print "cancelled"
1327dialog_cancelled() {
1328	false # never cancelled SIGINT is not trapped
1329}
1330
1331# Set the status text.
1332# Usage: dialog_set_text <text>
1333dialog_set_text() {
1334	true
1335}
1336
1337# Set if the progress bar should continously animate instead of showing the value.
1338# Usage: dialog_set_pulsate <enable>
1339dialog_set_pulsate() {
1340	true
1341}
1342
1343# Set the current progress value.
1344# Usage: dialog_set_value <percentage>
1345dialog_set_value() {
1346	true
1347}
1348
1349# Select an entry in a list.
1350# Usage dialog_select_entry <var> <label> <tag1> <item1> [ <tag2> < item2> ... ]
1351dialog_select_entry() {
1352	_cli_select_entry_var="$1" ; shift
1353
1354	# Print a list for the user to select from
1355	print "$1:" ; shift
1356	_cli_select_entry_min=$1
1357	_cli_select_entry_max=$1
1358	_cli_select_entry_f=' [default]'
1359	while [ $# -gt 0 ] ; do
1360		_cli_select_entry_i=$1 ; shift
1361		_cli_select_entry_t="$1" ; shift
1362		if [ $_cli_select_entry_i -lt $_cli_select_entry_min ] ; then
1363			_cli_select_entry_min=$_cli_select_entry_i
1364		fi
1365		if [ $_cli_select_entry_i -gt $_cli_select_entry_max ] ; then
1366			_cli_select_entry_max=$_cli_select_entry_i
1367		fi
1368		printf ' %d) %s%s\n' $_cli_select_entry_i \
1369			"$_cli_select_entry_t" "$_cli_select_entry_f"
1370		_cli_select_entry_f=''
1371	done
1372
1373	# Read a number (or empty string for the first entry)
1374	while true ; do
1375
1376		puts '> #'
1377		read -r _cli_select_entry_r
1378
1379		[ -z "$_cli_select_entry_r" ] && _cli_select_entry_r=1
1380
1381		case "$_cli_select_entry_r" in
1382			'quit') ;; 'q') ;; 'exit') ;; 'abort') ;;
1383			*)
1384			if [ ! "$_cli_select_entry_r" -lt $_cli_select_entry_min ] 2> /dev/null \
1385			   && [ ! "$_cli_select_entry_r" -gt $_cli_select_entry_max ] 2> /dev/null
1386			then
1387				eval "$_cli_select_entry_var=\"\$_cli_select_entry_r\""
1388				return $true
1389			else
1390				printf "Please enter a number between %d and %d.\n" \
1391					$_cli_select_entry_min $_cli_select_entry_max
1392				continue
1393			fi
1394		esac
1395		die
1396
1397	done
1398
1399	return $true
1400}
1401
1402# dialog_select_path supports the --any flag
1403dialog_select_path_any=1
1404
1405# Let the user select a path.
1406# Usage: dialog_select_path (--file|--dir|--any) <result-var> <label>
1407# Any is only supported if $dialog_select_path_any is 1.
1408dialog_select_path() {
1409	_cli_select_path_var="$2"
1410	print "$3:" ; shift
1411	puts '> '
1412	read -r _cli_select_path_r
1413	[ -z "$_cli_select_path_r" ] && return $false
1414	eval "$_cli_select_path_var=\"\$_cli_select_path_r\""
1415	return $true
1416}
1417
1418dialog_retry() {
1419	printf '\n%s\n' "${red}Error:${reset} $1"
1420	while true ; do
1421		print "Abort / [Retry] / Ignore"
1422		puts '> '
1423		read -r _cli_dialog_retry_r
1424		case "$_cli_dialog_retry_r" in
1425			a|A|abort|Abort|ABORT)    dialog_retry_choice='abort'  ; return ;;
1426			''|r|R|retry|Retry|RETRY) dialog_retry_choice='retry'  ; return ;;
1427			i|I|ignore|Ignore|IGNORE) dialog_retry_choice='ignore' ; return ;;
1428		esac
1429	done
1430}
1431
1432esac
1433
1434
1435##########################################################################################
1436# Common user interface implementation
1437
1438# Ask the user if the setup should really be cancelled.
1439handle_cancel() {
1440	_handle_cancel_message="Are you sure you want to exit the Arx Fatalis data installer?"
1441	if [ $installed_stuff = 1 ] ; then
1442		_handle_cancel_message="$_handle_cancel_message
1443
1444Already installed files will not be removed!"
1445	fi
1446	dialog_ask "$_handle_cancel_message" && print 'Aborted by user' && die
1447}
1448
1449# Update the status.
1450# Usage: status (<percent>|--temp) [<message>]
1451_status_text="Initializing..."
1452_status_cur=''
1453_status_pulsate=default
1454status() {
1455
1456	if [ "$1" = '--temp' ] ; then
1457		_status_value=0
1458		_status_temp=1
1459	else
1460		_status_value=$1
1461		_status_temp=0
1462	fi
1463	_status_new="${2:-$_status_text}"
1464
1465	# Handle the cancel and close buttons
1466	if dialog_cancelled ; then
1467		handle_cancel
1468		dialog_create || die "Could not re-create progress window."
1469		_status_cur=''
1470		_status_pulsate=default
1471	fi
1472
1473	# Update the progress text if one was provided
1474	if [ ! "$_status_cur" = "$_status_new" ] ; then
1475		_status_cur="$_status_new"
1476		print "$_status_new"
1477		dialog_set_text "$_status_new"
1478	fi
1479	[ $_status_temp = 0 ] && _status_text="$_status_new"
1480
1481	# Set the maximum progress value
1482	if [ $_status_value = 0 ] ; then _new_status_pulsate=1 ; else _new_status_pulsate=0 ; fi
1483	if [ ! "$_status_pulsate" = $_new_status_pulsate ] ; then
1484		_status_pulsate="$_new_status_pulsate"
1485		dialog_set_pulsate $_status_pulsate
1486	fi
1487
1488	# Update the progress value
1489	[ $_status_pulsate = 0 ] && dialog_set_value $_status_value
1490
1491}
1492
1493# Print an error message and show an error dialog if we have a GUI/
1494# Usage: error <message>
1495error() {
1496	print "${dim_red}$1${reset}"
1497	dialog_error "$1"
1498}
1499
1500# Let the user select an item from a list or enter a custom one.
1501# In batch mode, select the first one if --first is given, die otherwise.
1502# Usage: user_select_entry (--existing|--writable) (--any|--file|--dir) \
1503#                          <list> <result-var> <desc> <desc-color> <list-color> <verb>
1504user_select_entry() {
1505
1506	_user_select_entry_access="$1"
1507	_user_select_entry_t="$2"
1508	_user_select_entry_lname="$3"
1509	eval "_user_select_entry_list=\"\$$_user_select_entry_lname\""
1510	_user_select_entry_var="$4"
1511	_user_select_entry_desc="$5"
1512	_user_select_entry_color1="$6"
1513	_user_select_entry_color2="$7"
1514	_user_select_entry_verb="$8"
1515
1516	# Select the first element if in batch mode
1517	eval "_user_select_entry_current=\"\$$_user_select_entry_var\""
1518	if [ $batch = 1 ] || [ ! -z "$_user_select_entry_current" ] ; then
1519		_user_select_entry_=''
1520		eval "set -- $_user_select_entry_list"
1521		for _user_select_entry ; do
1522			_user_select_entry_="$_user_select_entry"
1523			break
1524		done
1525		[ -z "$_user_select_entry_" ] && die "Missing $_user_select_entry_desc!"
1526		eval "$_user_select_entry_var=\"\$_user_select_entry_\""
1527		return $true
1528	fi
1529
1530	if [ $gui = 0 ]
1531		then print
1532		else status --temp "Select a ${_user_select_entry_desc}"
1533	fi
1534
1535	_user_select_entry_i=1
1536	_user_select_entry_nolist=0
1537	if [ -z "$_user_select_entry_list" ] \
1538	&& [ ! $_user_select_entry_t = --any ] \
1539	&& [ ! $dialog_select_path_any = 1 ] ; then
1540
1541		# No entries detected - direclty promt the user
1542		_user_select_entry_num=1
1543		_user_select_entry_nolist=1
1544
1545	else
1546
1547		_user_select_entry_tlist=''
1548
1549		# Format the list entries in a user friendly way
1550		eval "set -- $_user_select_entry_list"
1551		for _user_select_entry ; do
1552
1553			# Use 'command (file)' if comment available or 'file' otherwise
1554			_user_select_entry_comment="$(
1555				list_comment "$_user_select_entry_lname" "$(($_user_select_entry_i - 1))"
1556			)"
1557			if [ -z "$_user_select_entry_comment" ] ; then
1558				_user_select_entry_label="$_user_select_entry_color2$_user_select_entry$reset"
1559			else
1560				_user_select_entry_label="$_user_select_entry_color2$_user_select_entry_comment$reset"
1561				_user_select_entry_label="$_user_select_entry_label: $_user_select_entry"
1562			fi
1563
1564			# Add the tag and label to the arguments
1565			list_append _user_select_entry_tlist $_user_select_entry_i
1566			list_append _user_select_entry_tlist "$_user_select_entry_label"
1567
1568			# Remeber the file for the tag
1569			eval "_user_select_entry_$_user_select_entry_i=\"\$_user_select_entry\""
1570
1571			_user_select_entry_i=$(($_user_select_entry_i + 1))
1572		done
1573
1574		# Add entries for custom files/diectories
1575		if [ $_user_select_entry_t = --any ] && [ $dialog_select_path_any = 1 ] ; then
1576			list_append _user_select_entry_tlist $_user_select_entry_i
1577			list_append _user_select_entry_tlist "Select file or directory to $_user_select_entry_verb.."
1578		else
1579			_user_select_entry_ii=$_user_select_entry_i
1580			if [ $_user_select_entry_t = --any ] || [ $_user_select_entry_t = --file ] ; then
1581				list_append _user_select_entry_tlist $_user_select_entry_ii
1582				list_append _user_select_entry_tlist "Select file to $_user_select_entry_verb..."
1583				_user_select_entry_ii=$(($_user_select_entry_ii + 1))
1584			fi
1585			if [ $_user_select_entry_t = --any ] || [ $_user_select_entry_t = --dir ] ; then
1586				list_append _user_select_entry_tlist $_user_select_entry_ii
1587				list_append _user_select_entry_tlist "Select directory to $_user_select_entry_verb..."
1588				_user_select_entry_ii=$(($_user_select_entry_ii + 1))
1589			fi
1590		fi
1591
1592	fi
1593
1594	# Loop until we have selected a value
1595	while true ; do
1596
1597		if [ $_user_select_entry_nolist = 0 ] ; then
1598
1599			# Ask the user to select an entry
1600			eval "set -- $_user_select_entry_tlist"
1601			while true ; do
1602				if dialog_select_entry _user_select_entry_num \
1603					"${_user_select_entry_color1}Select a ${_user_select_entry_desc}${reset}" "$@"
1604				then
1605					break
1606				else
1607					handle_cancel
1608				fi
1609			done
1610
1611		fi
1612
1613		if [ $_user_select_entry_num -lt $_user_select_entry_i ] ; then
1614
1615			# User selected an entry -- return that
1616			eval "$_user_select_entry_var=\"\$_user_select_entry_$_user_select_entry_num\""
1617			return $true
1618
1619		fi
1620
1621		# Loop until we have selected a path of the correct type
1622		while true ; do
1623
1624			# Adjust type based on user selection
1625			if [ $_user_select_entry_t = --any ] && [ ! $dialog_select_path_any = 1 ] ; then
1626				if [ $_user_select_entry_num = $_user_select_entry_i ]
1627					then _user_select_entry_type=--file
1628					else _user_select_entry_type=--dir
1629				fi
1630			else
1631				_user_select_entry_type=$_user_select_entry_t
1632			fi
1633
1634			_user_select_entry_c="Select a custom source "
1635
1636			if ! dialog_select_path "$_user_select_entry_type" _user_select_entry_path \
1637				"${_user_select_entry_color1}Choose a custom ${_user_select_entry_desc}${reset}" \
1638				"$@" ; then
1639
1640				# User cancelled the dialog - return to the main selection, unless there is none
1641				if [ $_user_select_entry_nolist = 1 ] ; then
1642					handle_cancel
1643					continue # let the user try again
1644				fi
1645				break # return to list selection
1646
1647			fi
1648
1649			_user_select_entry_path="$(abspath "$_user_select_entry_path")"
1650
1651			# The user selected a path, now check if it exists our criteria.
1652			case "$_user_select_entry_access" in
1653				--existing)
1654				if [ ! -e "$_user_select_entry_path" ]  ; then
1655					error "$_user_select_entry_path does not exist!"
1656					continue # let the user try again
1657				fi ;;
1658				--writable)
1659				if ! is_writable "$_user_select_entry_path" ; then
1660					error "$_user_select_entry_path is not writable!"
1661					continue # let the user try again
1662				fi ;;
1663			esac
1664			case "$_user_select_entry_t" in
1665				--any) ;; # anything goes
1666				--file)
1667				if [ -e "$_user_select_entry_path" ] && [ -d "$_user_select_entry_path" ] ; then
1668					error "$_user_select_entry_path is is a directory, but we need a file!"
1669					continue
1670				fi ;;
1671				--dir)
1672				if [ -e "$_user_select_entry_path" ] && [ ! -d "$_user_select_entry_path" ] ; then
1673					error "$_user_select_entry_path is is a file, but we need a directory!"
1674					continue
1675				fi ;;
1676			esac
1677
1678			# Everything went better than expected - save the result
1679			eval "$_user_select_entry_var=\"\$_user_select_entry_path\""
1680			return $true
1681
1682		done
1683
1684	done
1685
1686}
1687
1688# Show error message and close windows on exit
1689ui_cleanup() {
1690	[ -z "$1" ] || dialog_error "$1"
1691	dialog_destroy
1692}
1693on_exit ui_cleanup
1694
1695# Initilize the UI
1696dialog_create || die "
1697Could not create main window.
1698
1699You could try the --cli option.
1700"
1701[ $gui = 0 ] || status --temp "$_status_text"
1702
1703
1704##########################################################################################
1705# Autodetect source file/dir
1706
1707sourcefiles='' # List of source file/directory candidates
1708
1709# Add a source file or directory to the list of candidates.
1710# Usage: found_source_file <file> [comment]
1711# Return: $true if more source files should be probed, $false otherwise.
1712found_source_file() {
1713	set_append sourcefiles "$(canonicalize "$1")" "$2"
1714	if [ $batch = 1 ] ; then return $true ; else return $false ; fi
1715}
1716
1717# Find a match for a case insensitive path containing arx.exe
1718# Usage: probe_wine_path <baseprefix> <ipath>
1719probe_wine_path() {
1720	[ -d "$1" ] || return $false
1721	if [ -z "$2" ] ; then
1722		if icontains "$1" 'arx.exe' ; then
1723			found_source_file "$1" "Wine"
1724		fi
1725		return $false
1726	fi
1727	_wine_path_dir="${2%%\\*}"
1728	if [ "$_wine_path_dir" = "$2" ]
1729		then _wine_path=''
1730		else _wine_path="${2#*\\}"
1731	fi
1732	_wine_paths="$(
1733		find "$1/" -mindepth 1 -maxdepth 1 -iname "$(escape "$_wine_path_dir")" -print \
1734			| lines_to_list
1735	)"
1736	[ -z "$_wine_paths" ] && return $false
1737	_wine_path_call='probe_wine_path "$_wine_path_prefix" "$_wine_path" && return $true'
1738	eval "for _wine_path_prefix in $_wine_paths ; do $_wine_path_call ; done"
1739}
1740
1741# Find possible source directories from a registry key.
1742# Usage: probe_wine_registry <key> <variable>
1743# Return: $true if more source files should be probed, $false otherwise.
1744probe_wine_registry() {
1745
1746	# This is intentionally implemented without calling wine as we don't want to
1747	# modify the source.
1748
1749	# Get candidate paths from the registry file
1750	_wine_pattern='Software\\\\(Wow6432Node\\\\)?'
1751	_wine_pattern="$_wine_pattern$(print "$1" | sed 's/\\/\\\\/g' | escape_pipe '()|')"
1752	_wine_paths="$(
1753		# The --after is just an arbitrary limit
1754		# We verify the paths by checking for arx.exe, but for effiency we still want
1755		# to avoid false positives.
1756		cat "$_wine_prefix"/*.reg \
1757			| grep -iPA 20 "^\\[$_wine_pattern\\]" 2> /dev/null \
1758			| grep -iP "^\"?$2\"?=" 2> /dev/null \
1759			| sed 's/^[^=]*="\([^"]*\)".*$/\1/;s/\\\(.\)/\1/g' \
1760			| lines_to_list
1761	)"
1762
1763	# For each candidate, find a case-insensitive match and check if it contains arx.exe
1764	eval "set -- $_wine_paths"
1765	for _wine_path ; do
1766		probe_wine_path "$_wine_prefix/dosdevices" "$_wine_path" && return $true
1767	done
1768
1769	return $false
1770}
1771
1772# Find possible source directories from uninstall registry entries.
1773# Usage: probe_wine_uninstall_info <id>
1774# Return: $true if more source files should be probed, $false otherwise.
1775probe_wine_uninstall_info() {
1776	probe_wine_registry "Microsoft\\Windows\\CurrentVersion\\Uninstall\\$1" \
1777	                    'InstallLocation'
1778}
1779
1780# Find possible source directories in a WINEPREFIX.
1781# Usage: probe_wineprefix <wineprefix>
1782# Return: $true if more source files should be probed, $false otherwise.
1783probe_wineprefix() {
1784
1785	[ -d "$1" ] || return $false
1786	[ -e "$1/system.reg" ] || [ -e "$1/user.reg" ] || return $false
1787	_wine_prefix="$1"
1788
1789	# Normal install
1790	probe_wine_registry 'Arkane Studios\Installed Apps\Arx Fatalis' 'Folder' && return $true
1791
1792	# GOG version
1793	probe_wine_registry 'GOG.com\GOGARXFATALIS' 'PATH' && return $true
1794
1795	# Probe uninstall entries - combined for effiency
1796	_wine_uninstall='Microsoft\Windows\CurrentVersion\Uninstall\'
1797	# Steam
1798	_wine_steam='Steam App 1700'
1799	# Original game
1800	_wine_orig='{96443F45-13E2-11D6-AC87-00D0B7A9E540}'
1801	# 1.21 patch
1802	_wine_patch='{171251E0-4EED-4EA1-A46D-3213A226F2B3}_is1'
1803	probe_wine_registry "$_wine_uninstall($_wine_steam|$_wine_orig|$_wine_patch)" \
1804		'InstallLocation' && return $true
1805
1806}
1807
1808# Check a mounte point if it's an Arx Fatalis cd
1809# Usage: probe_cd <mountpoint> <fstype>
1810# Return: $true if more source files should be probed, $false otherwise
1811probe_cd() {
1812	node="$1"
1813	mountpoint="$2"
1814	fstype="$3"
1815
1816	case "$fstype" in
1817		cd9660) ;; iso9660) ;; udf) ;; *fuseiso) ;;
1818		*) return $true
1819	esac
1820
1821	if [ -d "$mountpoint/bin" ] && icontains "$mountpoint/bin" 'arx.ttf' 2> /dev/null ; then
1822		found_source_file "$mountpoint" "CDROM at $node" && return $true
1823	fi
1824
1825}
1826
1827# Find mounted Arx Fatalis CDs
1828# Usage: probe_cdrom
1829# Return: $true if more source files should be probed, $false otherwise
1830probe_cdrom() {
1831
1832	_probe_cdrom_mountpoints="$(
1833		cat /proc/mounts 2> /dev/null | lines_to_list # Linux - use /proc/mounts
1834		mount -p 2> /dev/null | lines_to_list         # Hope that mount has a -p option
1835	)"
1836
1837	eval "set -- $_probe_cdrom_mountpoints"
1838	for _probe_cdrom_line ; do
1839		eval "probe_cd $(escape "$_probe_cdrom_line" ' ' | sed 's/\\\\\040/\\ /g')"  # space-separated
1840		eval "probe_cd $(print "$_probe_cdrom_line" | tr '\t' '\n' | lines_to_list)" # tab-separated
1841	done
1842
1843}
1844
1845# Find possible source files/directories
1846# Usage: probe_source_files
1847# If sourcefile is already set, uses that.
1848probe_source_files() {
1849
1850	if [ ! -z "$sourcefile" ] ; then
1851
1852		# Trust the user
1853		found_source_file "$sourcefile" && return $true
1854
1855		# But also allow to refile the dir in non-batch mode if it is a wineprefix
1856		[ -d "$sourcefile" ] && probe_wineprefix "$sourcefile"
1857
1858		[ "$sourcefiles" = "$sourcefile" ] || sourcefile=''
1859
1860		return $true
1861	fi
1862
1863	status 5 "Searching for source files..."
1864
1865	# Find by filename
1866	probe_files found_source_file "$gog_names" 'GOG.com setup'
1867	probe_files found_source_file "$demo_names" 'Demo'
1868
1869	# Find an Arx Fatalis installation in $WINEPREFIX
1870	[ ! -z "${WINEPREFIX-}" ] && probe_wineprefix "$WINEPREFIX" && return $true
1871
1872	# Find an Arx Fatalis installation in ~/.wine
1873	[ ! "${WINEPREFIX-}" = "$HOME/.wine" ] && probe_wineprefix "$HOME/.wine" && return $true
1874
1875	# Find an Arx Fatalis cdrom
1876	probe_cdrom && return $true
1877
1878	# Just in case it will ever be installable under non-Windows systems
1879	steam_source_path="$HOME/.steam/root/SteamApps/common/Arx Fatalis"
1880	[ -d "$steam_source_path" ] && found_source_file "$steam_source_path" 'Steam'
1881
1882}
1883
1884
1885##########################################################################################
1886# Autodetect destination dir
1887
1888datadirs='' # List of destination data directory candidate
1889
1890# Add a destination data directory to the list of candidates.
1891# Usage: found_data_dir <dir> [comment]
1892# Return: $true if more data directories should be probed, $false otherwise.
1893found_data_dir() {
1894	set_append datadirs "$(abspath "$1")" "$2"
1895	if [ $batch = 1 ] ; then return $true ; else return $false ; fi
1896}
1897
1898# Add a destination data directory to the list of candidates if it is writable.
1899# In verify mode, also existing read-only directories are added.
1900# Usage: probe_data_dir <must-exists> <dir> [comment]
1901# Return: $true if more data directories should be probed, $false otherwise.
1902probe_data_dir() {
1903	[ "$1" = 1 ] && [ ! -e "$2" ] && return $false
1904	[ $install = 0 ] && [ ! -e "$2" ] && return $false
1905	if [ $install = 1 ] || [ $patch = 1 ] ; then is_writable "$2" || return $false ; fi
1906	found_data_dir "$2" "$3"
1907}
1908
1909# Find possible destination data directories
1910# Usage: probe_data_dirs
1911# If datadir is already set, uses that.
1912probe_data_dirs() {
1913
1914	if [ ! -z "$datadir" ] ; then
1915		found_data_dir "$datadir"
1916		return $true
1917	fi
1918
1919	status 10 "Searching for destination directories..."
1920
1921	for _probe_data_dirs_existing in 1 0 ; do
1922
1923		# Try paths supplied by wrapper scripts
1924		if [ ! -z "$data_path" ] ; then
1925			eval "set -- $(to_list "$data_path")"
1926			for _probe_data_dirs_path ; do
1927				probe_data_dir $_probe_data_dirs_existing "$_probe_data_dirs_path" "portable" \
1928					&& return $true
1929			done
1930		fi
1931
1932		# Try system paths
1933		eval "set -- $(to_list "$data_dirs")"
1934		for _probe_data_dirs_prefix in "$@" ; do
1935			eval "set -- $(to_list "$data_dir_suffixes")"
1936			for _probe_data_dirs_suffix ; do
1937				probe_data_dir $_probe_data_dirs_existing \
1938					"$_probe_data_dirs_prefix/$_probe_data_dirs_suffix" "system" \
1939					&& return $true
1940			done
1941		done
1942
1943		# Try user paths
1944		if [ $is_root = 0 ] ; then
1945			eval "set -- $(to_list "$user_dir_suffixes")"
1946			for _probe_data_dirs_suffix ; do
1947				probe_data_dir $_probe_data_dirs_existing \
1948					"$data_home/$_probe_data_dirs_suffix" "user" \
1949					&& return $true
1950			done
1951		fi
1952
1953	done
1954
1955}
1956
1957
1958##########################################################################################
1959# Extract helpers and other utility abstractions
1960
1961# Calculate the MD5 checksum of a file.
1962# Usage: checksum <result-var> <file>
1963checksum() {
1964
1965	if have md5sum ; then
1966		_checksum_result="$(md5sum -b "$2" | sed 's/ .*//g')"
1967		eval "$1=\"\$_checksum_result\""
1968		return $true
1969	fi
1970
1971	if have md5 ; then
1972		_checksum_result="$(md5 -q "$2")"
1973		eval "$1=\"\$_checksum_result\""
1974		return $true
1975	fi
1976
1977	die "You need either md5sum or md5."
1978}
1979
1980have_run() {
1981	eval "_have_run_p=\"\$${1}_reqs\""
1982	eval "for _have_run_program in $_have_run_p ; do have \$_have_run_program && return $true ; done"
1983	return $false
1984}
1985
1986# Extract an archive file to the current directory.
1987# Usage: extract <file> <types>...
1988have_extract() {
1989	have_run "extract_$1"
1990}
1991extract() {
1992	_extract_file="$1" ; shift
1993
1994	while true ; do
1995
1996		_extract_missing=''
1997		for _extract_type ; do
1998
1999			if "have_extract_${_extract_type}" ; then
2000				"extract_${_extract_type}" "$_extract_file" && return "$true"
2001			else
2002				list_merge _extract_missing extract_${_extract_type}_reqs
2003			fi
2004
2005		done
2006
2007		_extract_msg="${white}Could not extract $(basename "$_extract_file")${reset}"
2008		[ -z "$_extract_missing" ] \
2009			|| _extract_msg="$_extract_msg
2010
2011Please install one or more of the following:
2012$(print_help_list " - $dim_pink" _extract_missing)"
2013
2014		[ $batch = 1 ] && die "Error: $_extract_msg"
2015
2016		dialog_retry "$_extract_msg"
2017		case $dialog_retry_choice in
2018			abort)  die "Error extracting files" ;;
2019			retry)  continue ;;
2020			ignore) return $true ;;
2021		esac
2022
2023	done
2024
2025}
2026
2027# Extract a .zip file to the current directory.
2028# Usage: extract_zip <zipfile>
2029have_extract_zip() { have_extract zip ; }
2030extract_zip() {
2031
2032	if have bsdtar ; then
2033		printf 'Extracting %s using bsdtar\n' "$1"
2034		bsdtar xvf "$1"
2035		return $?
2036	fi
2037
2038	if have unzip ; then
2039		puts 'unzip: '
2040		unzip "$1"
2041		return $?
2042	fi
2043
2044	for _extract_zip_sz in 7za 7z ; do
2045		if have $_extract_zip_sz ; then
2046			$_extract_zip_sz x "$1"
2047			return $?
2048		fi
2049	done
2050
2051	die "no program to extract $1"
2052}
2053
2054# Mount a .iso file or CDROM using fuseiso if available  or normal mount if root.
2055# Usage: mount_cdrom <cdromfile> <mountpoint>
2056have_mount_cdrom() {
2057	have fuseiso && have fusermount && return $true
2058	have mount && have umount && [ $is_root = 1 ] && return $true
2059	return $false
2060}
2061mount_cdrom() {
2062
2063	if have fuseiso && have fusermount &&  fuseiso "$1" "$2" ; then
2064		printf 'Mounted %s at %s using fuseiso\n' "$1" "$2"
2065		return $true
2066	fi
2067
2068	if have mount && have umount && [ $is_root = 1 ] && mount -o loop,ro "$1" "$2" ; then
2069		printf 'Mounted %s at %s\n' "$1" "$2"
2070		return $true
2071	fi
2072
2073	die "no program to extract $1"
2074}
2075
2076# Unmount a CDROM that was mounted using mount_cdrom.
2077# Usage: unmount_cdrom <mountpoint>
2078unmount_cdrom() {
2079	have fusermount && fusermount -u "$1" > /dev/null 2>&1
2080	have umount && [ $is_root = 1 ] && umount "$1" > /dev/null 2>&1
2081}
2082
2083# isoinfo wrapper to extract all files to the current directory
2084extract_isoinfo() {
2085	_extract_isoinfo_file="$1"
2086
2087	# Get a list of all files in the ISO image
2088	_extract_isoinfo_files="$(
2089		isoinfo -i "$_extract_isoinfo_file" -J -f | grep ';1$' | lines_to_list
2090	)"
2091	[ -z "$_extract_isoinfo_files" ] && return $false
2092
2093	eval "set -- $_extract_isoinfo_files"
2094	for _extract_isoinfo_e ; do
2095
2096		# Remove leading / and trailing ;1 from filenames
2097		_extract_isoinfo_f="$(print "$_extract_isoinfo_e" | sed 's:^/*::;s:;1$::')"
2098		[ -z "$_extract_isoinfo_f" ] && continue
2099		printf ' - %s\n' "$_extract_isoinfo_f"
2100
2101		# Create subdirectories as needed
2102		_extract_isoinfo_d="$(dirname "$_extract_isoinfo_f")"
2103		[ -z "$_extract_isoinfo_d" ] || mkdir -p "$_extract_isoinfo_d" || return $false
2104
2105		# Extract the file
2106		isoinfo -i "$_extract_isoinfo_file" -J -x "$_extract_isoinfo_e" \
2107			> "$_extract_isoinfo_f" || return $false
2108
2109		# Don't rely on isoinfo setting a non-zero return code, check that we got something
2110		[ -s "$_extract_isoinfo_f" ] || return $false
2111
2112	done
2113
2114	return $true
2115}
2116
2117# Extract a .iso file or CDROM to the current directory.
2118# Usage: extract_iso <cdromfile>
2119have_extract_iso() { have_extract iso ; }
2120extract_iso() {
2121
2122	if have isoinfo ; then
2123		printf 'Extracting %s using isoinfo\n' "$1"
2124		extract_isoinfo "$1"
2125		return $?
2126	fi
2127
2128	ret=$false
2129
2130	if have bsdtar ; then
2131		printf 'Extracting %s using bsdtar\n' "$1"
2132		bsdtar xvf "$1"
2133		ret="$?"
2134
2135		# Older versions of bsdtar don't always get the names right - fix them
2136		_extrac_cdrom_f="$(find "$PWD" -depth -iname '*;1' | lines_to_list)"
2137		if [ ! -z "$_extrac_cdrom_f" ] ; then
2138			eval "for _extract_iso_f in $_extrac_cdrom_f ; do mv -f \"\$_extract_iso_f\" \"\$(print \"\$_extract_iso_f\" | sed 's:;1$::')\" ; done"
2139		fi
2140
2141	else if have 7z ; then
2142		7z x -tiso "$1"
2143		ret="$?"
2144	fi ; fi
2145
2146	# For some iso files bsdtar just does nothing - at least let the user know
2147	if [ ! -d "$PWD/bin" ] || ! icontains "$PWD/bin" 'arx.ttf' ; then
2148		_extract_iso_err="${yellow}It looks like bsdtar/p7zip didn't do what it was supposed to - this will likely fail!${reset}
2149
2150You might have better luck with ${dim_pink}isoinfo${reset} or ${dim_pink}fuseiso${reset}, or by manually mounting the CD/ISO."
2151		printf '%s\n' "$_extract_iso_err"
2152		[ $batch = 0 ] && dialog_message "$_extract_iso_err"
2153	fi
2154
2155	return $ret
2156}
2157
2158# Extract a microsoft .cab or .exe file to the current directory.
2159# Usage: extract_ms_cab <cabfile>
2160extract_cab_check_bsdtar() {
2161	if have bsdtar ; then
2162		case "$(bsdtar --version)" in
2163			# These versions have bugs that cause corrupted files
2164			'') ;;
2165			*'libarchive 1.'*) ;;
2166			*'libarchive 2.'*) ;;
2167			*'libarchive 3.0') ;;
2168			*'libarchive 3.0.'*) ;;
2169			# Newer versions should work fine
2170			*)
2171			return $true
2172		esac
2173	fi
2174	return $false
2175}
2176have_extract_ms_cab() {
2177	extract_cab_check_bsdtar && return $true
2178	have cabextract || have 7za || have 7z
2179}
2180extract_ms_cab() {
2181
2182	if extract_cab_check_bsdtar ; then
2183		printf 'Extracting %s using bsdtar\n' "$1"
2184		bsdtar xvf "$1"
2185		return $?
2186	fi
2187
2188	if have cabextract ; then
2189		puts 'cabextract: '
2190		cabextract "$1"
2191		return $?
2192	fi
2193
2194	for _extract_ms_cab_sz in 7za 7z ; do
2195		if have $_extract_ms_cab_sz ; then
2196			$_extract_ms_cab_sz x "$1"
2197			return $?
2198		fi
2199	done
2200
2201	die "no program to extract $1"
2202}
2203
2204# Extract an InstallShield .cab or .exe file to the current directory.
2205# Usage: extract_installshield <cabfile>
2206have_extract_installshield() { have_extract installshield ; }
2207extract_installshield() {
2208
2209	if have unshield ; then
2210		puts 'unshield: '
2211		unshield x "$1"
2212		return $?
2213	fi
2214
2215	die "no program to extract $1"
2216}
2217
2218# Extract an Inno Setup .exe file to the current directory.
2219# Usage: extract_innosetup <exefile>
2220have_extract_innosetup() { have innoextract ; }
2221innosetup_language=''
2222extract_innosetup() {
2223
2224	if have innoextract ; then
2225		puts 'innoextract: '
2226		if [ -z "$innosetup_language" ]
2227			then innoextract --color=off "$1" ; return $?
2228			else innoextract --color=off --language="$innosetup_language" "$1" ; return $?
2229		fi
2230	fi
2231
2232	die "no program to extract $1"
2233}
2234
2235# Extract all .cab files in a directory.
2236# Usage: extract_cab_files <sourcedir> <destdir>
2237extract_cab_files() {
2238
2239	_extract_cab_files_i=0
2240	_extract_cab_files_files="$(
2241		find "$1/" -mindepth 1 -type f -iname '*.cab' -print \
2242		| lines_to_list
2243	)"
2244	eval "set -- $_extract_cab_files_files"
2245	for _extract_cab_files_cabfile ; do
2246		[ -z "$_extract_cab_files_cabfile" ] && continue
2247
2248		while true ; do
2249			_extract_cab_files_cabdir="$sourcedir/cab.$_extract_cab_files_i"
2250			if [ -e "$_extract_cab_files_cabdir" ] ; then
2251				_extract_cab_files_i=$(($_extract_cab_files_i + 1))
2252				continue
2253			fi
2254			create_dir "$_extract_cab_files_cabdir" "cab #$i work"
2255			break
2256		done
2257
2258		cd "$_extract_cab_files_cabdir"
2259		case "$(basename "$_extract_cab_files_cabfile")" in
2260			data*) extract "$_extract_cab_files_cabfile" installshield ms_cab ;;
2261			*)     extract "$_extract_cab_files_cabfile" ms_cab installshield ;;
2262		esac
2263
2264		_extract_cab_files_i=$(($_extract_cab_files_i + 1))
2265	done
2266
2267}
2268
2269# Download a file.
2270# Usage: download_file <url> <destination>
2271have_download() { have_run download ; }
2272download_impl() {
2273
2274	if have wget ; then
2275		wget -O "$2" "$1"
2276		return $?
2277	fi
2278
2279	if have curl ; then
2280		curl --location --fail -o "$2" "$1"
2281		return $?
2282	fi
2283
2284	if have fetch ; then
2285		fetch -o "$2" "$1"
2286		return $?
2287	fi
2288
2289	die "no program to download $1"
2290}
2291download_file() {
2292
2293	download_impl "$1" "$2" || return $false
2294
2295	# Check that we got something useful
2296	case "$(file --dereference --brief --mime-type "$2" 2> /dev/null)" in
2297		*/html*) ;;
2298		*/xml*) ;;
2299		*) return $true
2300	esac
2301
2302	rm -f "$2"
2303	return $false;
2304}
2305
2306# Download a file from a list of mirrors.
2307# Usage: download <callback> <name> <names> <urls> <destdir>
2308download() {
2309	_download_callback="$1"
2310	_download_name="$2"
2311	_download_names="$3"
2312	_download_urls="$4"
2313	_download_dest="$5"
2314
2315	while true ; do
2316
2317		probe_files "$_download_callback" "$_download_names" && return $true
2318
2319		_download_missing=''
2320		if have_download ; then
2321			eval "for _download_url in $_download_urls ; do download_file \"\$_download_url\" \"\$_download_dest\" && \$_download_callback \"\$_download_dest\" && return $true ; done"
2322		else
2323			list_merge _download_missing download_reqs
2324		fi
2325
2326		_download_msg="${white}Could not download ${_download_name}${reset}"
2327		[ -z "$_download_missing" ] \
2328			|| _download_msg="$_download_msg
2329
2330Please install one of the following:
2331$(print_help_list " - $dim_pink" _download_missing)"
2332
2333		_download_msg="$_download_msg
2334
2335You can download the file manually from
2336$(print_help_list " - $dim_blue" _download_urls)
2337
2338and put it in one of these locations:
2339$(print_help_list " - $dim_white" probe_file_dirs)"
2340
2341		[ $batch = 1 ] && die "Error: $_download_msg"
2342
2343		dialog_retry "$_download_msg"
2344		case $dialog_retry_choice in
2345			abort)  die "Error downloading files" ;;
2346			retry)  continue ;;
2347			ignore) return $true ;;
2348		esac
2349
2350	done
2351
2352}
2353
2354
2355##########################################################################################
2356# Unpack source
2357
2358workdir=''
2359cleanup_workdir() {
2360	[ ! -z "$workdir" ] && [ -e "$workdir" ] && rm -rf "$workdir"
2361}
2362# Create the work directory if it doesn't exist.
2363# Also regiter a cleanup function to remove the work directory on exit.
2364# Usage: create_workdir
2365create_workdir() {
2366	[ -z "$workdir" ] || return $true
2367	workdir="$datadir/$command-temp"
2368	cleanup_workdir
2369	on_exit cleanup_workdir
2370	create_dir "$workdir" 'work'
2371}
2372
2373# Extract setup*.cab files from a source directory into $sourcedir/cab.*.
2374# Usage: extract_source_dir <sourcedir>
2375extract_source_dir() {
2376	extract_cab_files "$1" "$sourcedir"
2377}
2378
2379# Extract a source executable into $sourcedir/exe.
2380# Usage: extract_source_exe <sourcefile>
2381extract_source_exe() {
2382
2383	sourcedir_exe="$sourcedir/exe"
2384	create_dir "$sourcedir_exe" 'exe work'
2385
2386	cd "$sourcedir_exe" || die
2387	case "$(basename "$1")" in
2388		arx_jpn_*.exe) extract "$1" ms_cab installshield innosetup ;; # Japanese demo
2389		*)             extract "$1" innosetup ms_cab installshield ;; # GOG.com setup
2390	esac
2391
2392	extract_source_dir "$sourcedir_exe"
2393
2394}
2395
2396# Wrap mount_cdrom et al so we can use them with extract()
2397extract_mount_cdrom_reqs=''
2398list_merge extract_mount_cdrom_reqs mount_cdrom_reqs
2399have_extract_mount_cdrom() { have_mount_cdrom ; }
2400extract_mount_cdrom() {
2401	# Ignore the current working directory, always mount to $cdromdir
2402	if mount_cdrom "$1" "$cdromdir" ; then
2403		# Pretent the mountpoint is the original source
2404		sourcefile="$cdromdir"
2405		sourcedir_cdrom="$cdromdir"
2406		return $true
2407	else
2408		return $false
2409	fi
2410}
2411
2412cdromdir=''
2413cleanup_cdrom() {
2414	[ ! -z "$cdromdir" ] && [ -e "$cdromdir" ] && unmount_cdrom "$cdromdir"
2415}
2416# Mount a source CDROM/ISO and adjust $sourcefile or extract it into $sourcedir/cdrom.
2417# Usage: extract_source_cdrom <sourcefile>
2418extract_source_cdrom() {
2419
2420	# Try to mount the cdrom to avoid unneeded copies
2421	# Keep the mount point out of $sourcedir so we don't try to mv files from it
2422	cdromdir="$workdir/cdrom"
2423	cleanup_cdrom
2424	on_exit cleanup_cdrom
2425	create_dir "$cdromdir" 'mount work'
2426
2427	# Otherwise, extract the files from the CDROM if we have the required tools
2428	sourcedir_cdrom="$sourcedir/cdrom"
2429	create_dir "$sourcedir_cdrom" 'cdrom work'
2430	cd "$sourcedir_cdrom" || die
2431
2432	extract "$1" mount_cdrom iso
2433
2434	# Extract any cab files on the CDROM
2435	extract_source_dir "$sourcedir_cdrom"
2436}
2437
2438# Extract a source executable into $sourcedir/zip.
2439# Also extracts contained setup*.cab files into $sourcedir/cab.*.
2440# Usage: extract_source_zip <sourcefile>
2441extract_source_zip() {
2442
2443	sourcedir_zip="$sourcedir/zip"
2444	create_dir "$sourcedir_zip" 'zip work'
2445
2446	cd "$sourcedir_zip" || die
2447	extract "$1" zip
2448
2449	extract_source_dir "$sourcedir_zip"
2450}
2451
2452sourcedir=''
2453# Extract the source file or directory if it hansn't been extracted already.
2454# Usage: extract_source
2455extract_source() {
2456	[ -z "$sourcedir" ] || return $true
2457
2458	status --temp "${white}Extracting source...${reset}"
2459
2460	create_workdir
2461	sourcedir="$workdir/source"
2462	create_dir "$sourcedir" 'source work'
2463
2464	if [ -d "$sourcefile" ] ; then
2465		extract_source_dir "$sourcefile"
2466	else if [ -f "$sourcefile" ] ; then
2467		case "$sourcefile" in
2468			*.zip) extract_source_zip "$sourcefile" ;;
2469			*.exe) extract_source_exe "$sourcefile" ;;
2470			*.iso) extract_source_cdrom "$sourcefile" ;;
2471			*)
2472			case "$(file --dereference --brief --mime-type "$sourcefile" 2> /dev/null)" in
2473				application/zip)             extract_source_zip "$sourcefile" ;;
2474				application/x-dosexec)       extract_source_exe "$sourcefile" ;;
2475				application/x-iso9660-image) extract_source_cdrom "$sourcefile" ;;
2476				*) die "Unknown source file type: $sourcefile"
2477			esac
2478		esac
2479	else
2480		extract_source_cdrom "$sourcefile"
2481	fi ; fi
2482
2483	print
2484}
2485
2486
2487##########################################################################################
2488# Detect data language
2489
2490find_file_impl() {
2491	_patchable="$1"
2492
2493	# Search for both file.ext and file_default.ext
2494	_file="$(escape "$(basename "$2")")"
2495	_file_d="$(print "$_file" | sed 's/^\(.*\)\(\.[^.]*\)$/\1_default\2/')"
2496
2497	# Prefer files from the patch - if availbale, they are most likely the correct ones
2498	set -- -iname "$_file" -print -o -iname "$_file_d" -print
2499	[ "$_patchable" = 1 ] && [ ! -z "$patchdir" ]  && find "$patchdir" "$@"
2500
2501	# Find the file in the source
2502	[ ! -z "$sourcedir" ] && find "$sourcedir" "$@"
2503	[ -d "$sourcefile" ]  && find "$sourcefile" "$@"
2504
2505	# Also find the file if it is already in the data directory, but don't ignore case
2506	find "$datadir" -path '*-temp' -prune -o \
2507		-name "$_file" -print -o -name "$_file_d" -print
2508
2509}
2510# Find a file in patch, source and data directories.
2511# Usage: find_file <is-patchable> <return-list-var> <filename/path>
2512find_file() {
2513	eval "$2=\"\$(find_file_impl \"\$1\" \"\$3\" | lines_to_list)\""
2514}
2515
2516# Copy/move a file to the data diectory.
2517# Usage: use_file <file> <data-path>
2518# Action taken depends on the source directory.
2519use_file() {
2520	_in="$1"
2521	_out="$datadir/$2"
2522
2523	# Don't change anything on verify-only mode
2524	[ $install = 0 ] && [ $patch = 0 ] && return $true
2525
2526	# Don't try to copy/move a file onto itself
2527	[ "$_in" = "$_out" ] && return $true
2528
2529	# Create directories as needed
2530	_outdir="$(dirname "$_out")"
2531	create_dir "$_outdir" 'output'
2532
2533	# Copy or move the file
2534	if [ "${_in#"$sourcefile"}" = "$_in" ]
2535		then mv -f "$_in" "$_out" || die "Could not move $_in to $_out!"
2536		else cp -f "$_in" "$_out" || die "Could not copy $_in to $_out!"
2537	fi
2538	installed_stuff=1
2539
2540	# Fix permissions
2541	chmod --reference="$_outdir" "$_out" > /dev/null 2>&1
2542	chmod -x "$_out" > /dev/null 2>&1
2543}
2544
2545data_lang=''      # Data language/tipe ID
2546data_lang_desc='' # Friendly data language/type label
2547
2548# Detect the data language and type
2549# Usage: detect_data_langauge <callback>
2550# <callback> receives a speech.pak checksum and sets data_lang and data_lang_desc
2551detect_data_langauge() {
2552	callback="$1"
2553
2554	if [ $install = 1 ]
2555		then _detect_data_langauge_status=40
2556		else _detect_data_langauge_status=10
2557	fi
2558	[ $gui = 0 ] || status $_detect_data_langauge_status 'Detecting data language...'
2559	puts "${white}Detecting data language..."
2560
2561	_speech_checksums=''
2562
2563	find_file 0 _speech_files 'speech.pak'
2564	eval "set -- $_speech_files"
2565	for _speech_file ; do
2566
2567		checksum _speech_checksum "$_speech_file"
2568
2569		data_lang=''
2570		"$callback" "$_speech_checksum"
2571
2572		if [ -z "$data_lang" ] ; then
2573			list_append _speech_checksums "$_speech_checksum"
2574		else
2575			use_file "$_speech_file" 'speech.pak'
2576			break
2577		fi
2578
2579	done
2580
2581	if [ -z "$data_lang" ] ; then
2582		printf '\n'
2583		case "$_speech_checksums" in
2584			'') die "speech*.pak not found" ;;
2585			*)  die "Unsupported data language - speech*.pak checksum: $_speech_checksums" ;;
2586		esac
2587	fi
2588
2589	printf " ${green}%s${reset}\n\n" "${data_lang_desc}"
2590
2591}
2592
2593
2594##########################################################################################
2595# Get the patch file
2596
2597# Warn if the user-supplied patch file has a suspicious name.
2598# Usage: patch_check_file_name <file> <expected-name-list>
2599patch_check_file_name() {
2600	_user_patch_name="$(basename "$1")"
2601	_patch_check_file_names="$2"
2602	if ! list_contains _patch_check_file_names "$_user_patch_name" ; then
2603		print "${yellow}Warning: unexpected patch file name: %s" "$_user_patch_name" >&2
2604		printf "Expected %s${reset}\n" "$(print_help_or _patch_check_file_names)" >&2
2605	fi
2606}
2607
2608# Find or download a patch file.
2609# Usage: probe_patch_file_impl <callback> <name> <user-supplied-file> \
2610#                              <expected-name-list> <url-list> \
2611# <callback> will be called with the patch file
2612probe_patch_file_impl() {
2613
2614	_patch_found="$1"
2615	_patch_name="$2"
2616	_patch_file="$3"
2617	_patch_names="$4"
2618	_patch_urls="$5"
2619
2620	if [ ! -z "$_patch_file" ] ; then
2621
2622		# Check the filename of user-supplied patchse
2623		patch_check_file_name "$_file" "$_patch_names"
2624
2625		"$_patch_found" "$_patch_file"
2626		return $true
2627	fi
2628
2629	[ $probe_patch = 0 ] && return $true
2630
2631	# Probe local files now so we don't lie abut downloading it
2632	probe_files "$_patch_found" "$_patch_names" && return $true
2633
2634	status --temp "${white}Downloading patch ${blue}${_patch_name}${reset}..."
2635
2636	create_workdir
2637	download "$_patch_found" "$_patch_name" \
2638	         "$_patch_names" "$_patch_urls" "${workdir}/${_patch_name}" \
2639		&& return $true
2640
2641}
2642
2643# Callback for the japanese patch.
2644patch_jp_found() {
2645	patchfile_jp="$(abspath "$1")"
2646	printf "Using Japanese %s patch: ${blue}%s${reset}\n" \
2647		"$patch_jp_ver" "$patchfile_jp"
2648	return $true
2649}
2650
2651# Callback for the main patch.
2652patch_found() {
2653	patchfile="$(abspath "$1")"
2654	printf "Using %s patch: ${blue}%s${reset}\n" "$patch_ver" "$patchfile"
2655	return $true
2656}
2657
2658# Find the patch file(s) for a specific language.
2659# Usage: probe_patch_file <language>
2660# If patchfile is already set, uses that.
2661probe_patch_file() {
2662
2663	_patch_file_lang=''
2664	case "$1" in
2665		'german')   _patch_file_lang='GE' ;;
2666		'english')  _patch_file_lang='EN' ;;
2667		'spanish')  _patch_file_lang='ES' ;;
2668		'french')   _patch_file_lang='FR' ;;
2669		'italian')  _patch_file_lang='IT' ;;
2670		'russian')  _patch_file_lang='RU' ;;
2671		'japanese')
2672			_patch_jp_names=''
2673			list_append _patch_jp_names "$patch_jp_name"
2674			probe_patch_file_impl patch_jp_found "$patch_jp_name" "$patchfile_jp" \
2675			                      "$_patch_jp_names" "$patch_jp_urls"
2676		;;
2677	esac
2678	_patch_names=''
2679	list_append _patch_names "$patch_name"
2680	if [ ! -z "$_patch_file_lang" ] ; then
2681		list_append _patch_names "$(printf "$patch_name_localized" "$_patch_file_lang")"
2682	fi
2683
2684	probe_patch_file_impl patch_found "$patch_name" "$patchfile" \
2685	                      "$_patch_names" "$patch_urls"
2686
2687}
2688
2689patchdir='' # Directory where the patch file(s) are extracted
2690
2691# Extract all patch files for a specific language.
2692# Usage: extract_patch <language>
2693# Does nothing if the patch files are already extracted.
2694extract_patch() {
2695	[ -z "$patchdir" ] || return $true
2696	_extract_patch_lang="$1"
2697
2698	print
2699
2700	# Search for and download the patch files if needed
2701	probe_patch_file "$_extract_patch_lang"
2702
2703	status --temp "${white}Extracting patch...${reset}"
2704
2705	create_workdir
2706	patchdir="$workdir/patch"
2707	create_dir "$patchdir" 'patch work'
2708
2709	# Extract the main patch file
2710	if [ ! -z "$patchfile" ] ; then
2711		_patchdir_main="$patchdir/main"
2712		create_dir "$_patchdir_main" 'main patch work'
2713		cd "$_patchdir_main"
2714		innosetup_language="$_extract_patch_lang"
2715		extract "$patchfile" innosetup
2716		innosetup_language=''
2717	fi
2718
2719	if [ ! -z "$patchfile_jp" ] ; then
2720
2721		# Extract the Japanese patch file
2722		_patchdir_jp="$patchdir/main"
2723		create_dir "$_patchdir_jp" 'jp patch work'
2724		cd "$_patchdir_jp"
2725		extract "$patchfile_jp" ms_cab
2726
2727		# Also extract contained files
2728		extract_cab_files "$_patchdir_jp" "$patchdir"
2729
2730	fi
2731
2732	print
2733	status --temp
2734}
2735
2736
2737##########################################################################################
2738# Copy and verify files
2739
2740checksum_failed=0 # Was there any mismatched checksum or missing file so far?
2741
2742# Handle a required file: find, compare checksum and copy/move if needed.
2743# Usage: required_file <is-patchable> <filepath> <checksums>
2744required_file() {
2745
2746	_patchable="$1"
2747	_name="$2"
2748	_valid="$3"
2749
2750	find_file 1 _files "$_name"
2751	eval "set -- $_files"
2752	_checksums=''
2753	for _file ; do
2754		checksum _checksum "$_file"
2755
2756		if list_contains _valid "$_checksum" ; then
2757			# We found a match - use it
2758			printf ' - %s\n' "$_name"
2759			use_file "$_file" "$_name"
2760			return $true
2761		fi
2762
2763		# Remember mismatched checksums so we can output debug info if none matched
2764		list_append _checksums "$_checksum"
2765		continue
2766
2767	done
2768
2769	# No matching file found!
2770
2771	# If we didn't use the patch yet, fetch it and try again!
2772	if [ $patch = 1 ] && [ $_patchable = 1 ] && [ -z "$patchdir" ] ; then
2773		extract_patch "$data_lang"
2774		if [ ! -z "$patchdir" ] ; then
2775			required_file "$_patchable" "$_name" "$_valid"
2776			return $?
2777		fi
2778	fi
2779
2780	# Let the user know that something is wrong!
2781	if [ -z "$_checksums" ] ; then
2782		printf "${red}Missing ${dim_red}%s${red}!${reset}\n" "$_name" >&2
2783	else
2784		printf "${red}Checksum failed for ${dim_red}%s${reset}:\n" "$_name" >&2
2785		printf "  expected: ${dim_red}%s${reset}\n" "$(print_help_or _valid)" >&2
2786		printf "  actual:   ${dim_red}%s${reset}\n" "$(print_help_or _checksums)" >&2
2787	fi
2788
2789	# Be optimistic, copy the first result even if the checksum doesn't match!
2790	# We will display an error at the end (end exit with $false), but it may still work.
2791	eval "set -- $_files"
2792	for _file ; do
2793		use_file "$_file" "$_name"
2794		break
2795	done
2796
2797	checksum_failed=1
2798	return $false
2799}
2800
2801# Handle an optional file: find and copy/move if it exists.
2802# Usage: optional_file <filepath>
2803optional_file() {
2804	_name="$1"
2805	find_file 1 _files "$_name"
2806	eval "set -- $_files"
2807	for _file ; do
2808		# There is no checksum, just copy the first file
2809		printf ' - %s\n' "$_name"
2810		use_file "$_file" "$_name"
2811		break
2812	done
2813}
2814
2815
2816##########################################################################################
2817# Setup
2818
2819# Select source file / directory
2820if [ $install = 1 ] ; then
2821	probe_source_files
2822	if [ $batch = 0 ] ; then
2823		list_append sourcefiles 'Patch existing install' ''
2824		list_append sourcefiles 'Verify existing install only' ''
2825	fi
2826	user_select_entry --existing --any sourcefiles sourcefile \
2827		"source file or directory to install from" "$green" "$dim_green" 'install from'
2828	case "$sourcefile" in
2829		'Patch existing install')       install=0 ; patch=1 ;;
2830		'Verify existing install only') install=0 ; patch=0 ;;
2831		*) [ -e "$sourcefile" ] || die "Missing source file: $sourcefile"
2832	esac
2833	set_append probe_file_dirs "$(dirname "$sourcefile")"
2834fi
2835
2836# Select destination data directory
2837probe_data_dirs
2838if [ $install = 1 ] ; then
2839	verb='install to' ; access=--writable
2840else if [ $patch = 1 ] ; then
2841	verb='patch' ; access=--existing
2842else
2843	verb='verify'     ; access=--existing
2844fi ; fi
2845user_select_entry $access --dir datadirs datadir \
2846	"data directory to $verb" "$cyan" "$dim_cyan" "$verb"
2847[ -z "$datadir" ] && die "Missing data dir."
2848if [ $install = 1 ] ; then
2849	create_dir "$datadir" 'data'
2850else
2851	[ -d "$datadir" ] || die "Missing data dir: $datadir"
2852fi
2853
2854# Extract source files
2855if [ $install = 1 ] ; then
2856	printf "\nInstalling Arx Fatalis data files \nfrom %s\nto   %s\n\n" \
2857		"${green}$sourcefile${reset}" "${cyan}$datadir${reset}"
2858	extract_source
2859else
2860	printf "\nVerifying Arx Fatalis data files \nin %s\n\n" "${cyan}$datadir${reset}"
2861fi
2862
2863
2864##########################################################################################
2865# Required files
2866
2867# Detect language
2868determine_language() {
2869	speech_checksum="$1" # speech.pak
2870
2871	case "$speech_checksum" in
2872		'4e8f962d8204bcfd79ce6f3226d6d6de') data_lang='english'       ;;
2873		'4c3fdb1f702700255924afde49081b6e') data_lang='german'        ;;
2874		'ab8a93161688d793a7c78fbefd7d133e') data_lang='german'        ;;
2875		'2f88c67ae1537919e69386d27583125b') data_lang='spanish'       ;;
2876		'4edf9f8c799190590b4cd52cfa5f91b1') data_lang='french'        ;;
2877		'81f05dea47c52d43f01c9b44dd8fe962') data_lang='italian'       ;;
2878		'677163bc319cd1e9aa1b53b5fb3e9402') data_lang='russian'       ;;
2879		'235b86700fc80b3eb86731d748013a38') data_lang='japanese'      ;;
2880		'62ca7b1751c0615ee131a94f0856b389') data_lang='english-demo'  ;;
2881		'eeacbd9a845ecc00054934e82e9d7dd3') data_lang='japanese-demo' ;;
2882	esac
2883
2884	case "$data_lang" in
2885		'english')       data_lang_desc='English' ;;
2886		'german')        data_lang_desc='German' ;;
2887		'spanish')       data_lang_desc='Spanish' ;;
2888		'french')        data_lang_desc='French' ;;
2889		'italian')       data_lang_desc='Italian' ;;
2890		'russian')       data_lang_desc='Russian' ;;
2891		'japanese')      data_lang_desc='Japanese' ;;
2892		'english-demo')  data_lang_desc='English (demo)' ;;
2893		'japanese-demo') data_lang_desc='Japanese (demo)' ;;
2894	esac
2895
2896}
2897detect_data_langauge determine_language
2898
2899if [ $install = 1 ] ; then
2900	progress=50
2901	status $progress "${white}Copying and verifying files...${reset}"
2902	case "$data_lang" in *-demo) increment=8 ;; *) increment=1 ;; esac
2903else
2904	progress=15
2905	status $progress "${white}Verifying files...${reset}"
2906	case "$data_lang" in *-demo) increment=14 ;; *) increment=2 ;; esac
2907fi
2908print " - speech.pak"
2909
2910# Usage: f <is-patchable> <file> <checksums>...
2911f() {
2912
2913	# Update progress bar
2914	progress=$(($progress + $increment))
2915	status $progress
2916
2917	# Verify & copy file
2918	required_file "$@"
2919}
2920
2921# speech.pak - already copied in detect_data_langauge
2922
2923# loc.pak contains the localized text, so it's different for each language!
2924case "$data_lang" in
2925	german)        loc_checksum='31bc35bca48e430e108db1b8bcc2621d' ;;
2926	english)       loc_checksum='a47b192493afb5794e2161a62d35b69f' ;;
2927	spanish)       loc_checksum='121f99608814a2c9c5857cfadb665553' ;;
2928	french)        loc_checksum='f8fc448fea12469ed94f417c313fe5ea' ;;
2929	italian)       loc_checksum='a9e162f2916f5737a95bd8c5bd8a979e' ;;
2930	russian)       loc_checksum='a131bf2398ee70a9c22a2bbffd9d0d99' ;;
2931	japanese)      loc_checksum='9dcb0f5d7a517be4f1d9190419900892' ;;
2932	english-demo)  loc_checksum='2ae16d3925c597dca70f960f175def3a' ;;
2933	japanese-demo) loc_checksum='9d84cede805b13fdf7fce856ecc15b19' ;;
2934	*)             loc_checksum=''
2935esac
2936if [ ! -z "$loc_checksum" ] ; then
2937	f 1 'loc.pak' "$loc_checksum"
2938fi
2939
2940# misc/arx.ttf is the same for everything except japanese
2941# there are also separate misc/arx_russian.ttf and misc/arx_taiwanese.ttf handled later
2942case "$data_lang" in
2943	japanese*)    font_checksum='58eab00842d8adea8d553ae1f66b0c9b' ;;
2944	*)            font_checksum='9a95ff96795c034524ba1c2e94ea12c7' ;;
2945esac
2946if [ ! -z "$font_checksum" ] ; then
2947	f 1 'misc/arx.ttf' "$font_checksum"
2948fi
2949
2950case "$data_lang" in
2951
2952	english-demo)
2953	f 0 'data2.pak'                                        958b78f8f370b06d769843137138c461
2954	f 0 'data.pak'                                         5d7ba6e6c79ebf7fbb232eaced9e8ad9
2955	f 0 'misc/logo.bmp'                                    aa3dfbd4bc9c863d10a0c5345ae5a4c9
2956	f 0 'sfx.pak'                                          ea1b3e6d6f4906905d4a34f07e9a59ac
2957	;;
2958
2959	japanese-demo)
2960	f 0 'data2.pak'                                        958b78f8f370b06d769843137138c461
2961	f 0 'data.pak'                                         903dfe1878a0cedff3b941fd3aa22ba9
2962	f 0 'misc/logo.bmp'                                    aa3dfbd4bc9c863d10a0c5345ae5a4c9
2963	f 0 'sfx.pak'                                          ea1b3e6d6f4906905d4a34f07e9a59ac
2964	;;
2965
2966	*) # full game
2967
2968	f 1 'graph/interface/misc/arkane.bmp'                  afff1099c01ffeb03b9a351f7b5966b6
2969	f 1 'graph/interface/misc/quit1.bmp'                   41445d3792a1f8818d950aca47254488
2970	f 1 'graph/obj3d/textures/fixinter_barrel.jpg'         8419274acbff7346c3661b18d6aad6dc
2971	f 1 'graph/obj3d/textures/fixinter_bell.bmp'           5743b9047c9ad65540c318dfcc98123a
2972	f 1 'graph/obj3d/textures/fixinter_metal_door.jpg'     f246eff6b19c9c710313b4a4dce96a69
2973	f 1 'graph/obj3d/textures/fixinter_public_notice.bmp'  f81394abbb9006ce0950843b7909db33
2974	f 1 'graph/obj3d/textures/item_bread.bmp'              544448f8eedc912aa231a6a04fffb7c5
2975	f 1 'graph/obj3d/textures/item_club.jpg'               7e26c4199ddaca494c8b369294306b0b
2976	f 1 'graph/obj3d/textures/item_long_sword.jpg'         3a6196fe9b7666c7d80d82be06f6de86
2977	f 1 'graph/obj3d/textures/item_mauld_sabre.jpg'        18492c25ebac02f83e2f0ebda61ecb00
2978	f 1 'graph/obj3d/textures/item_mauldsword.jpg'         503a5c2f23668040c675aefdde6dbbe5
2979	f 1 'graph/obj3d/textures/item_mirror.jpg'             c0a22b4f7a7a6461da68206e94928637
2980	f 1 'graph/obj3d/textures/item_ring_casting.bmp'       348f9add709bacee08556d1f8cf10f3f
2981	f 1 'graph/obj3d/textures/item_rope.bmp'               ff05de281c8b380ee98f6e123d3d51cb
2982	f 1 'graph/obj3d/textures/item_spell_sheet.jpg'        024ccbb520020f92fba5a5a4f0270cea
2983	f 1 'graph/obj3d/textures/item_torch2.jpg'             027951899b4829599ca611010ea3484f
2984	f 1 'graph/obj3d/textures/item_torch.jpg'              9ada166f23ddcb775ac20836e752187e
2985	f 1 'graph/obj3d/textures/item_zohark.bmp'             cd206a4027f86c6e57b7710c94049efa
2986	f 1 'graph/obj3d/textures/l7_dwarf_[wood]_board08.jpg' 79ccc81adb7c37b98f40b478ef1fccd4
2987	f 1 'graph/obj3d/textures/l7_dwarf_[wood]_board80.jpg' 691611087b13d38ef02bb9dfd6a2518e
2988	f 1 'graph/obj3d/textures/npc_dog.bmp'                 116bd374c14ae8c387a4da1899e1dca7
2989	f 1 'graph/obj3d/textures/npc_pig.bmp'                 b7a4d0d3d230b2d1470176909004e38b
2990	f 1 'graph/obj3d/textures/npc_pig_dirty.bmp'           76034d8d74056c8a982479d36321c228
2991	f 1 'graph/obj3d/textures/npc_rat_base.bmp'            00c585ec9ebe8006d7ca72993de7b51b
2992	f 1 'graph/obj3d/textures/npc_rat_base_cm.bmp'         cae38facbf77db742180b9e58d0eb42f
2993	f 1 'graph/obj3d/textures/npc_worm_body_part1.jpg'     0b220bffaedc89fa663f08d12630c342
2994	f 1 'graph/obj3d/textures/npc_worm_body_part2.bmp'     20797cb78f6393a0fb5405969ba9f805
2995	f 1 'graph/obj3d/textures/[wood]_light_door.jpg'       00d0b018e995e7d013d6e52e92126901
2996	f 1 'misc/arx_russian.ttf'                             921561e83786efcd25f92147b60a13db
2997	f 1 'misc/arx_taiwanese.ttf'                           da59198061cef0761c6b2fca113f76f6
2998	f 1 'misc/logo.avi'                                    63ed31a4eb3d226c23e58cfaa974d484
2999	f 1 'misc/logo.bmp'                                    afff1099c01ffeb03b9a351f7b5966b6
3000	f 1 'data2.pak'                                        f7e0ce700bf963429ac535ca86f8a7b4
3001
3002	f 0 'sfx.pak'                                          2efc9a74c517fd1ee9919900cf4091d2
3003
3004	# data.pak is censored in some versions (presumably has less gore)
3005	# At least the original german and italian CDs have the censored version.
3006	# The censored version has different level files and a different
3007	# human_female_villager model.
3008	# There are also minor differences in the scripts, but those are
3009	# overwritten by data2.pak from the 1.21 patch.
3010	data_checksum_original='a91a0b39a046233debbb10b4850e13eb'
3011	data_checksum_censored='a88d239dc7919ab113ff45483cb4ad46'
3012	f 0 'data.pak' "$data_checksum_original $data_checksum_censored"
3013
3014esac
3015
3016# Optional files - we don't need them, but copy them anyway if available
3017optional_file 'manual.pdf'
3018optional_file 'map.pdf'
3019
3020print
3021
3022
3023##########################################################################################
3024# Print a summary
3025
3026if [ $install = 1 ] ; then verb='Installed' ; else verb='Verified' ; fi
3027printf "${white}%s Arx Fatalis %s data: ${green}%s${reset}\n" "$verb" \
3028	"$patch_ver" "$data_lang_desc"
3029
3030if [ $checksum_failed = 1 ] ; then
3031	[ $gui = 0 ] || status 100 "Error!"
3032	die "There are wrong or missing files!${reset}
3033
3034The game may run fine, or it may fail - good luck!" >&2
3035fi
3036
3037status 100 "${dim_green}All good!${reset}"
3038if [ $install = 1 ] ; then verb='Installation' ; else verb='Verification' ; fi
3039dialog_message "$verb complete: $data_lang_desc
3040
3041Have fun playing Arx Fatalis!"
3042
3043quit $true
3044