###
# Copyright © 2015, Antoine Le Gonidec <vv221@dotslashplay.it>
# Copyright © 2016, Mopi
# Copyright © 2017, Phil Morrell
# Copyright © 2017, Jacek Szafarkiewicz
# Copyright © 2018, VA
# Copyright © 2018, Janeene "dawnmist" Beeforth
# Copyright © 2018, BetaRays
# Copyright © 2018, Andrey
# Copyright © 2019, Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
# Copyright © 2020, macaron
# Copyright © 2020, Hoël Bézier
# Copyright © 2020, quinao
# Copyright © 2022, JashinYoda
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# This software is provided by the copyright holders and contributors "as is"
# and any express or implied warranties, including, but not limited to, the
# implied warranties of merchantability and fitness for a particular purpose
# are disclaimed. In no event shall the copyright holder or contributors be
# liable for any direct, indirect, incidental, special, exemplary, or
# consequential damages (including, but not limited to, procurement of
# substitute goods or services; loss of use, data, or profits; or business
# interruption) however caused and on any theory of liability, whether in
# contract, strict liability, or tort (including negligence or otherwise)
# arising in any way out of the use of this software, even if advised of the
# possibility of such damage.
###

###
# common functions for ./play.it scripts
# send your bug reports to contact@dotslashplay.it
###

LIBRARY_VERSION=2.30.0
# shellcheck disable=SC2034
library_revision=20240710.1

# Apply minimal permissions on all files in the given paths:
# - 755 for directories
# - 644 for files
# USAGE: set_standard_permissions $path[…]
set_standard_permissions() {
	local path
	for path in "$@"; do
		# Error out if something does not look like a path to a directory
		if [ ! -d "$path" ]; then
			return 1
		fi
		find "$path" -type d -exec chmod 755 '{}' +
		find "$path" -type f -exec chmod 644 '{}' +
	done
}

# convert files name to lower case
# USAGE: tolower $dir[…]
# CALLS: tolower_convmv tolower_shell
tolower() {
	local directory
	for directory in "$@"; do
		if [ ! -d "$directory" ]; then
			error_not_a_directory "$directory"
			return 1
		fi
		if command -v convmv > /dev/null; then
			tolower_convmv "$directory"
		else
			tolower_shell "$directory"
		fi
	done
}

# convert files name to lower case using convmv
# USAGE: tolower_convmv $directory
# RETURN: nothing
# SIDE EFFECT: convert all file names in a given path to lowercase
tolower_convmv() {

	local directory convmv_options find_options
	directory="$1"
	convmv_options='-f utf8 --notest --lower -r'
	find_options='-mindepth 1 -maxdepth 1'

	# Hide convmv output unless $PLAYIT_OPTION_DEBUG is set to ≥ 1
	local option_debug
	option_debug=$(option_value 'debug')
	if [ "$option_debug" -ge 1 ]; then
		# shellcheck disable=SC2086
		find "$directory" $find_options -exec \
			convmv $convmv_options {} +
	else
		# shellcheck disable=SC2086
		find "$directory" $find_options -exec \
			convmv $convmv_options {} + >/dev/null 2>&1
	fi

}

# convert files name to lower case using pure shell
# USAGE: tolower_shell $dir
# CALLED BY: tolower
tolower_shell() {
	local dir="$1"

	find "$dir" -depth -mindepth 1 | while read -r file; do
		newfile=$(dirname "$file")/$(basename "$file" | tr '[:upper:]' '[:lower:]')
		[ -e "$newfile" ] || mv "$file" "$newfile"
	done
}

# convert files name to upper case
# USAGE: toupper $dir[…]
# CALLS: toupper_convmv toupper_shell
toupper() {
	local directory
	for directory in "$@"; do
		if [ ! -d "$directory" ]; then
			error_not_a_directory "$directory"
			return 1
		fi
		if command -v convmv > /dev/null; then
			toupper_convmv "$directory"
		else
			toupper_shell "$directory"
		fi
	done
}

# convert files name to upper case using convmv
# USAGE: toupper_convmv $directory
# RETURN: nothing
# SIDE EFFECT: convert all file names in a given path to uppercase
toupper_convmv() {

	local convmv_options find_options directory
	directory="$1"
	convmv_options='-f utf8 --notest --upper -r'
	find_options='-mindepth 1 -maxdepth 1'

	# Hide convmv output unless $PLAYIT_OPTION_DEBUG is set to ≥ 1
	local option_debug
	option_debug=$(option_value 'debug')
	if [ "$option_debug" -ge 1 ]; then
		# shellcheck disable=SC2086
		find "$directory" $find_options -exec \
			convmv $convmv_options {} +
	else
		# shellcheck disable=SC2086
		find "$directory" $find_options -exec \
			convmv $convmv_options {} + >/dev/null 2>&1
	fi

}

# convert files name to upper case using pure shell
# USAGE: toupper_shell $dir
# CALLED BY: toupper
toupper_shell() {
	local dir="$1"

	find "$dir" -depth -mindepth 1 | while read -r file; do
		newfile="$(dirname "$file")/$(basename "$file" | tr '[:lower:]' '[:upper:]')"
		[ -e "$newfile" ] || mv "$file" "$newfile"
	done
}

# try to guess the tar implementation used for `tar` on the current system
# USAGE: guess_tar_implementation
guess_tar_implementation() {
	case "$(tar --version | head --lines 1)" in
		(*'GNU tar'*)
			PLAYIT_TAR_IMPLEMENTATION='gnutar'
		;;
		(*'libarchive'*)
			PLAYIT_TAR_IMPLEMENTATION='bsdtar'
		;;
		(*)
			error_unknown_tar_implementation
			return 1
		;;
	esac
}

# Return the MIME type of a given file
# USAGE: file_type $file
# RETURNS: the MIME type, as a string
file_type() {
	local file
	file="$1"

	local file_type
	file_type=$(file --brief --dereference --mime-type "$file")

	# Everything behind the first ";" is removed,
	# so "application/x-executable; charset=binary"
	# would be returned as "application/x-executable".
	file_type=$(printf '%s' "$file_type" | cut --delimiter=';' --fields=1)

	printf '%s' "$file_type"
}
# check that the target directory is on a case-sensitive filesystem
# USAGE: check_directory_is_case_sensitive $tested_directory
# RETURNS: 0 if case-sensitive, 1 if case-insensitive
check_directory_is_case_sensitive() {
	# the first argument should be a writable directory
	local tested_directory
	tested_directory="$1"
	if [ ! -d "$tested_directory" ]; then
		error_not_a_directory "$tested_directory"
		return 1
	fi
	if [ ! -w "$tested_directory" ]; then
		error_not_writable "$tested_directory"
		return 1
	fi

	# check if "a" and "A" are created as distinct files, or as a single one
	# tests are done in an inner temporary directory to avoid messing up with existing files
	local inner_temp_directory
	inner_temp_directory=$(mktemp --directory --tmpdir="$tested_directory")
	touch "${inner_temp_directory}/a"
	touch "${inner_temp_directory}/A"
	local files_count
	files_count=$(find "$inner_temp_directory" -mindepth 1 -maxdepth 1 -iname a | wc --lines)
	rm --recursive "$inner_temp_directory"

	# if "a" and "A" were created as distinct files, the file system is case-sensitive
	# if they were created as a single file, it is case-insensitive
	case "$files_count" in
		(1)
			return 1
		;;
		(2)
			return 0
		;;
		(*)
			# we did not get an expected numeric value (1 or 2), let us assume we do not want to work with this directory
			# it might be better to actually throw some kind of explicit error with a message here
			return 1
		;;
	esac
}

# check that the target directory is on a filesystem supporting UNIX permissions
# USAGE: check_directory_supports_unix_permissions $tested_directory
# RETURNS: 0 if has support for UNIX permissions, 1 if has no support for UNIX permissions
check_directory_supports_unix_permissions() {
	local tested_directory
	tested_directory="$1"
	if [ ! -d "$tested_directory" ]; then
		error_not_a_directory "$tested_directory"
		return 1
	fi
	if [ ! -w "$tested_directory" ]; then
		error_not_writable "$tested_directory"
		return 1
	fi

	# Change permissions on a file, and check it has an actual effect
	# Tests are done in an inner temporary directory to avoid messing up with existing files
	local inner_temp_directory tested_temp_file file_permissions_expected file_permissions_real
	inner_temp_directory=$(mktemp --directory --tmpdir="$tested_directory")
	tested_temp_file="${inner_temp_directory}/a"
	touch "$tested_temp_file"
	for file_permissions_expected in '600' '700'; do
		chmod "$file_permissions_expected" "$tested_temp_file" 2>/dev/null || true
		file_permissions_real=$(stat --printf='%a' "$tested_temp_file")
		if [ "$file_permissions_real" != "$file_permissions_expected" ]; then
			return 1
		fi
	done
	rm --recursive "$inner_temp_directory"
}

# check that the target directory is on a filesystem supporting executable files
# USAGE: check_directory_supports_executable_files $tested_directory
# RETURNS: 0 if has support for executable files, 1 otherwise
check_directory_supports_executable_files() {
	local tested_directory
	tested_directory="$1"
	if [ ! -d "$tested_directory" ]; then
		error_not_a_directory "$tested_directory"
		return 1
	fi

	findmnt --first-only --list --options +noexec --target "$tested_directory" >/dev/null
	case $? in
		(0)
			return 1
			;;
		(1)
			return 0
			;;
		(*)
			# Something unexpected happened, we do not want to deal with it
			return 1
			;;
	esac
}

# Check if the current process is running under the root account
# USAGE: check_is_running_as_root
# RETURN: 0 if running as root, 1 otherwise
check_is_running_as_root() {
	local current_user_id
	current_user_id=$(id --user)

	test "$current_user_id" -eq 0
}

# assert a variable is not empty and display an error message otherwise
# USAGE: assert_not_empty $variable_name $calling_function
assert_not_empty() {
	local variable_name calling_function
	variable_name="$1"
	calling_function="$2"

	local variable_value
	variable_value=$(get_value "$variable_name")
	if [ -z "$variable_value" ]; then
		error_empty_string "$calling_function" "$variable_name"
		return 1
	fi
}

# Get the compatibility level that has been requested
# USAGE: compatibility_level
# RETURN: the requested compatibility level,
#         defaulting to the current library version
compatibility_level() {
	local compatibility_level

	if [ -n "${PLAYIT_COMPATIBILITY_LEVEL:-}" ]; then
		compatibility_level="$PLAYIT_COMPATIBILITY_LEVEL"
	elif [ -n "${target_version:-}" ]; then
		## FIXME: Find a way to show a deprecation warning *once*, not repeating it each time this function is called.
		##        The warning should be shown when the compatibility level is set to a value ≥ 2.26.
		compatibility_level="$target_version"
	else
		## The bugfix version number is trimmed, to get a value respecting the expected "major.minor" format.
		compatibility_level="${LIBRARY_VERSION%.*}"
	fi

	# Check the validity of the compatibility level format
	local regexp
	regexp='^[1-9][0-9]*\.[0-9]\+$'
	if ! printf '%s' "$compatibility_level" | grep --quiet --regexp="$regexp"; then
		error_invalid_compatibility_level "$compatibility_level"
		return 1
	fi

	printf '%s' "$compatibility_level"
}

# Check the compatibility level against a given version
# USAGE: compatibility_level_is_at_least $minimum_version
# RETURN: 0 if the compatibility level is equal or superior to the given level,
#         1 if the compatibility level is inferior to the given level
compatibility_level_is_at_least() {
	local minimum_version compatibility_level
	minimum_version="$1"
	if ! compatibility_level=$(compatibility_level); then
		## Stop the script execution if the compatibility level is not set to a valid value.
		## This needs to be done explicitly because this functions is called from tests.
		exit 1
	fi

	# Compare major version fields
	local minimum_version_major compatibility_level_major
	minimum_version_major=$(printf '%s' "$minimum_version" | cut --delimiter='.' --fields=1)
	compatibility_level_major=$(printf '%s' "$compatibility_level" | cut --delimiter='.' --fields=1)
	if [ "$compatibility_level_major" -lt "$minimum_version_major" ]; then
		return 1
	elif [ "$compatibility_level_major" -gt "$minimum_version_major" ]; then
		return 0
	else
		# Compare minor version fields
		local minimum_version_minor compatibility_level_minor
		minimum_version_minor=$(printf '%s' "$minimum_version" | cut --delimiter='.' --fields=2)
		compatibility_level_minor=$(printf '%s' "$compatibility_level" | cut --delimiter='.' --fields=2)
		if [ "$compatibility_level_minor" -lt "$minimum_version_minor" ]; then
			return 1
		elif [ "$compatibility_level_minor" -ge "$minimum_version_minor" ]; then
			return 0
		fi
	fi
}

# Clean-up the output of a command returning a list
# USAGE: … | list_clean
# RETURN: a list including only unique lines
list_clean() {
	## - remove leading and trailing spaces
	## - sort the list and merge duplicate entries
	## - remove empty lines, ignore errors on empty output
	sed 's/^\s*//;s/\s*$//' | \
		sort --unique | \
		grep --invert-match --regexp='^$' || true
}

# Clean-up the output of a command printing a snippet
# USAGE: … | snippet_clean
# RETURN: a snippet indented with tabulations
snippet_clean() {
	## - convert the series of 4 spaces to tabulations
	sed --regexp-extended 's/( ){4}/\t/g'
}

# Expand the given variable name and print its value
# USAGE: get_value $variable_name
# RETURN: the variable value
get_value() {
	local variable_name
	variable_name="$1"

	eval printf -- '%s' '"${'"${variable_name}"':-}"'
}

# Check if the given varible is unset or set to an empty value.
# USAGE: variable_is_empty $variable_name
# RETURN: 0 if the variable is unset or empty, 1 if it is set to a non empty value
variable_is_empty() {
	local variable_name
	variable_name="$1"

	local variable_value
	variable_value=$(get_value "$variable_name")
	test -z "$variable_value"
}
# Set the path to the temporary directory
# This path is exported as $PLAYIT_WORKDIR.
# USAGE: set_temp_directories
set_temp_directories() {

	# Get path to temporary directory
	local temporary_directory_path
	temporary_directory_path=$(temporary_directory_path)

	# Check that the given path is valid for temporary files storage
	temporary_directory_checks "$temporary_directory_path"

	# Generate a directory with a unique name for the current instance
	local game_id
	game_id=$(game_id)
	PLAYIT_WORKDIR=$(mktemp --directory --tmpdir="$temporary_directory_path" "${game_id}.XXXXX")
	## Ensure that we are always using an absolute path for PLAYIT_WORKDIR,
	## to avoid issues when a relative path has been used with the ./play.it --tmpdir option.
	PLAYIT_WORKDIR=$(realpath "$PLAYIT_WORKDIR")
	export PLAYIT_WORKDIR

}

# Print the path to the temporary directory to use
# USAGE: temporary_directory_path
temporary_directory_path() {
	option_value 'tmpdir'
}

# Run checks on the path used for temporary files
# USAGE: temporary_directory_checks $temporary_directory_path
temporary_directory_checks() {
	local temporary_directory_path
	temporary_directory_path="$1"

	# Skip all checks if no file operation is going to take place
	local noop_option noop_option_value
	for noop_option in \
		'help' \
		'list-available-scripts' \
		'list-packages' \
		'list-requirements' \
		'list-supported-games' \
		'show-game-script' \
		'version'
	do
		noop_option_value=$(option_value "$noop_option")
		## Setting a default value to 0 allows calling this function even when some of these options are not set,
		## making it easier to use in unit tests.
		if [ "${noop_option_value:-0}" -eq 1 ]; then
			return 0
		fi
	done

	# Check that the given path is an existing directory
	if [ ! -d "$temporary_directory_path" ]; then
		error_temporary_path_not_a_directory "$temporary_directory_path"
		return 1
	fi

	# Check that the given path is writable
	if [ ! -w "$temporary_directory_path" ]; then
		error_temporary_path_not_writable "$temporary_directory_path"
		return 1
	fi

	# Check that the given path is on a case-sensitive filesystem
	if ! check_directory_is_case_sensitive "$temporary_directory_path"; then
		error_temporary_path_not_case_sensitive "$temporary_directory_path"
		return 1
	fi

	# Check that the given path is on a filesystem with support for UNIX permissions
	if ! check_directory_supports_unix_permissions "$temporary_directory_path"; then
		error_temporary_path_no_unix_permissions "$temporary_directory_path"
		return 1
	fi

	# Check that the given path has allow use of the execution bit
	if ! check_directory_supports_executable_files "$temporary_directory_path"; then
		error_temporary_path_noexec "$temporary_directory_path"
		return 1
	fi

	# Check that there is enough free space under the given path
	local option_free_space_check
	option_free_space_check=$(option_value 'free-space-check')
	if [ "$option_free_space_check" -eq 1 ]; then
		temporary_directory_check_free_space "$temporary_directory_path"
	fi
}

# Check that there is enough free space under the given path
# USAGE: temporary_directory_check_free_space $path
# RETURN: 0 if there is enough space,
#         1 otherwise
temporary_directory_check_free_space() {
	local path
	path="$1"

	# Compute the total size of the archives contents
	local free_space_required archives_list archive archive_size
	free_space_required=0
	archives_list=$(archives_used_list)
	for archive in $archives_list; do
		archive_size=$(archive_size "$archive")
		free_space_required=$((free_space_required + archive_size))
	done

	# Compare the free space available with twice the archives contents size
	local df_options free_space_available free_space_required_double
	df_options='--block-size=1K --output=avail'
	free_space_available=$(env --ignore-environment df $df_options "$path" | tail --lines=1)
	free_space_required_double=$((free_space_required * 2))
	if [ "$free_space_available" -lt "$free_space_required_double" ]; then
		error_temporary_path_not_enough_space "$path"
		return 1
	fi
}

# Information - All the packages are already built, there is no need to run a new build
# USAGE: info_all_packages_already_built
info_all_packages_already_built() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Tous les paquets sont déjà présents, il nʼy a pas besoin de les reconstruire.\n'
		;;
		('en'|*)
			message='All the packages are already built, there is no need to run a new build.\n'
		;;
	esac

	print_message 'info' "$message"
}

# Error - The given path is not a file
# USAGE: error_not_a_file $path
error_not_a_file() {
	local path
	path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='"%s" nʼest pas un fichier valide.\n'
		;;
		('en'|*)
			message='"%s" is not a valid file.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$path"
}

# Error - The available tar implementation is not supported
# USAGE: error_unknown_tar_implementation
error_unknown_tar_implementation() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La version de tar présente sur ce système nʼest pas reconnue.\n'
			message="$message"'./play.it ne peut utiliser que GNU tar ou bsdtar.\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de gestion de bugs : %s\n'
		;;
		('en'|*)
			message='The tar implementation on this system wasnʼt recognized.\n'
			message="$message"'./play.it can only use GNU tar or bsdtar.\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$PLAYIT_BUG_TRACKER_URL"
}

# Error - The given path is not a directory
# USAGE: error_not_a_directory $path
error_not_a_directory() {
	local path
	path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='"%s" nʼest pas un répertoire.\n'
		;;
		('en'|*)
			message='"%s" is not a directory.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$path"
}

# Error - The given path is not writable
# USAGE: error_not_writable $path
error_not_writable() {
	local path
	path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='"%s" nʼest pas accessible en écriture.\n'
		;;
		('en'|*)
			message='"%s" is not writable.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$path"
}

# Error - The function has been given an unexpected empty string
# USAGE: error_empty_string $calling_function $variable_name
error_empty_string() {
	local calling_function variable_name
	calling_function="$1"
	variable_name="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La variable "%s" de la fonction "%s" ne doit pas être vide.\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de gestion de bugs : %s\n'
		;;
		('en'|*)
			message='Variable "%s" in function "%s" can not be empty.\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$variable_name" \
		"$calling_function" \
		"$PLAYIT_BUG_TRACKER_URL"
}

# Error - A required command is missing
# USAGE: error_unavailable_command $function $required_command
error_unavailable_command() {
	local function required_command
	function="$1"
	required_command="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La commande "%s" nʼest pas disponible, mais elle est requise par la fonction "%s".\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de gestion de bugs : %s\n'
		;;
		('en'|*)
			message='"%s" command is not available, but it is required by function "%s".\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$required_command" \
		"$function" \
		"$PLAYIT_BUG_TRACKER_URL"
}

# Error - The calling script is not compatible with the provided library version
# USAGE: error_incompatible_versions
error_incompatible_versions() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Ce script nʼest pas compatible avec la version fournie de la bibliothèque ./play.it.\n'
			message="$message"'La bibliothèque utilisée fournit actuellement la version %s, mais le script attend une version ≥ %s et < %s.\n'
		;;
		('en'|*)
			message='This script is not compatible with the provided ./play.it library version.\n'
			message="$message"'The library in use currently provides version %s, but the script expects a version ≥ %s and < %s.\n'
		;;
	esac

	local compatibility_level version_major_minimum
	compatibility_level=$(compatibility_level)
	version_major_minimum=$(printf '%s' "$compatibility_level" | cut --delimiter='.' --fields=1)
	print_message 'error' "$message" \
			"$LIBRARY_VERSION" \
			"$compatibility_level" \
			"$((version_major_minimum + 1)).0"
}

# Error - The wrapper has been called with no archive argument
# USAGE: error_archive_missing_from_arguments
error_archive_missing_from_arguments() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Aucune archive nʼa été fournie sur la ligne de commande.\n'
		;;
		('en'|*)
			message='No archive has been provided on the command line.\n'
		;;
	esac
	print_message 'error' "$message"
}

# Error - No game script has been found for the given archive
# USAGE: error_no_script_found_for_archive $archive
error_no_script_found_for_archive() {
	local archive
	archive="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Impossible de trouver un script pour le fichier %s\n'
		;;
		('en'|*)
			message='Could not find script for file %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$archive"
}

# Error - A variable is spanning multiple lines
# USAGE: error_variable_multiline $variable_name
error_variable_multiline() {
	local variable_name
	variable_name="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La valeur %s sʼétend sur plusieurs lignes, ce qui nʼest pas autorisé.\n'
		;;
		('en')
			message='%s value is spanning multiple lines, but this is not allowed.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$variable_name"
}

# Error - A type-restricted function has been called on the wrong application type
# USAGE: error_application_wrong_type $function_name $application_type
error_application_wrong_type() {
	local function_name application_type
	function_name="$1"
	application_type="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='%s ne peut pas être appelée sur les applications utilisant le type "%s".\n'
		;;
		('en')
			message='%s can not be called on applications using "%s" type.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$function_name" \
		"$application_type"
}

# Error - The directory for temporary files storage does not exist
# USAGE: error_temporary_path_not_a_directory $temporary_directory_path
error_temporary_path_not_a_directory() {
	local temporary_directory_path
	temporary_directory_path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le chemin demandé pour stocker les fichiers temporaires nʼexiste pas'
			message="$message"', ou nʼest pas un répertoire : %s\n'
			message="$message"'Un chemin alternatif peut être fourni avec --tmpdir.\n'
		;;
		('en'|*)
			message='The path set for temporary files storage does not exist'
			message="$message"', or is not a directory: %s\n'
			message="$message"'An alternative path can be provided with --tmpdir.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$temporary_directory_path"
}

# Error - The directory for temporary files storage has no write access
# USAGE: error_temporary_path_not_writable $temporary_directory_path
error_temporary_path_not_writable() {
	local temporary_directory_path
	temporary_directory_path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le chemin demandé pour stocker les fichiers temporaires nʼest pas accessible en écriture : %s\n'
			message="$message"'Un chemin alternatif peut être fourni avec --tmpdir.\n'
		;;
		('en'|*)
			message='The path set for temporary files storage has no write access: %s\n'
			message="$message"'An alternative path can be provided with --tmpdir.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$temporary_directory_path"
}

# Error - The directory for temporary files storage is not case-sensitive
# USAGE: error_temporary_path_not_case_sensitive $temporary_directory_path
error_temporary_path_not_case_sensitive() {
	local temporary_directory_path
	temporary_directory_path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le chemin demandé pour stocker les fichiers temporaires est sur un système de fichiers qui nʼest pas sensible à la casse des noms de fichiers : %s\n'
			message="$message"'Un chemin alternatif peut être fourni avec --tmpdir.\n'
		;;
		('en'|*)
			message='The path set for temporary files storage is on a case-insensitive file system: %s\n'
			message="$message"'An alternative path can be provided with --tmpdir.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$temporary_directory_path"
}

# Error - The directory for temporary files storage has no support for UNIX permissions
# USAGE: error_temporary_path_no_unix_permissions $temporary_directory_path
error_temporary_path_no_unix_permissions() {
	local temporary_directory_path
	temporary_directory_path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le chemin demandé pour stocker les fichiers temporaires ne prend pas en charge les permissions UNIX : %s\n'
			message="$message"'Un chemin alternatif peut être fourni avec --tmpdir.\n'
		;;
		('en'|*)
			message='The path set for temporary files storage has no support for UNIX permissions: %s\n'
			message="$message"'An alternative path can be provided with --tmpdir.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$temporary_directory_path"
}

# Error - The directory for temporary files storage is mounted with noexec
# USAGE: error_temporary_path_noexec $temporary_directory_path
error_temporary_path_noexec() {
	local temporary_directory_path
	temporary_directory_path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le chemin demandé pour stocker les fichiers temporaires ne permet pas la création de fichiers exécutables : %s\n'
			message="$message"'Un chemin alternatif peut être fourni avec --tmpdir.\n'
		;;
		('en'|*)
			message='The path set for temporary files storage forbid the creation of executable files: %s\n'
			message="$message"'An alternative path can be provided with --tmpdir.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$temporary_directory_path"
}

# Error - There is not enough free space in the directory for temporary files storage
# USAGE: error_temporary_path_not_enough_space $temporary_directory_path
error_temporary_path_not_enough_space() {
	local temporary_directory_path
	temporary_directory_path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le chemin demandé pour stocker les fichiers temporaires ne dispose pas dʼassez dʼespace libre : %s\n'
			message="$message"'Un chemin alternatif peut être fourni avec --tmpdir.\n'
			message="$message"'Cette vérification de lʼespace libre peut aussi être contournée avec --no-free-space-check.\n'
		;;
		('en'|*)
			message='The path set for temporary files storage has not enough free space: %s\n'
			message="$message"'An alternative path can be provided with --tmpdir.\n'
			message="$message"'This free space check can also be disabled using --no-free-space-check.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$temporary_directory_path"
}

# Error - A mandatory variable is not set
# USAGE: error_missing_variable $variable_name
error_missing_variable() {
	local variable_name
	variable_name="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La variable suivante est requise, mais elle nʼa pas été définie ou sa valeur est nulle : %s\n'
		;;
		('en'|*)
			message='The following variable is mandatory, but it is unset or its value is null: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$variable_name"
}

# Error - ./play.it should not be run with the root account
# USAGE: error_run_as_root
error_run_as_root() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='./play.it ne doit pas être exécuté par le compte root.\n'
		;;
		('en'|*)
			message='./play.it should not be executed by the root account.\n'
		;;
	esac
	print_message 'error' "$message"
}

# Error - The current archive identifier uses an invalid format.
# USAGE: error_current_archive_format_invalid $archive
error_current_archive_format_invalid() {
	local archive
	archive="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼidentifiant dʼarchive "%s" ne respecte pas le format attendu: ARCHIVE_BASE_xxx\n'
		;;
		('en'|*)
			message='The archive identifier "%s" does not follow the expected format: ARCHIVE_BASE_xxx\n'
		;;
	esac
	print_message 'error' "$message" \
		"$archive"
}

# Error - The current package identifier uses an invalid format.
# USAGE: error_current_package_format_invalid $package
error_current_package_format_invalid() {
	local package
	package="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼidentifiant de paquet "%s" ne respecte pas le format attendu: PKG_xxx\n'
		;;
		('en'|*)
			message='The package identifier "%s" does not follow the expected format: PKG_xxx\n'
		;;
	esac
	print_message 'error' "$message" \
		"$package"
}

# Error - The current package identifier is not included in the list of packages to build.
# USAGE: error_current_package_not_in_list $package
error_current_package_not_in_list() {
	local package
	package="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼidentifiant de paquet "%s" ne fait pas partie de la liste de paquets à construire.\n'
		;;
		('en'|*)
			message='The package identifier "%s" is not included in the list of packages that should be built.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$package"
}

# Error - The compatibility level of the current game script is set to an invalid value
# USAGE: error_invalid_compatibility_level $compatibility_level
error_invalid_compatibility_level() {
	local compatibility_level
	compatibility_level="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le niveau de compatibilité du script courant est défini à une valeur invalide : "%s"\n'
			message="$message"'Le format attendu est "version_majeure.version_mineure", par exemple "2.29".\n'
		;;
		('en'|*)
			message='The compatibility level of the current script is set to an invalid value: "%s"\n'
			message="$message"'The expected format is "major_version.minor_version", for example "2.29".\n'
		;;
	esac
	print_message 'error' "$message" \
		"$compatibility_level"
}

# Set the identifier of the current archive
# USAGE: set_current_archive $archive
set_current_archive() {
	local archive
	archive="$1"

	# Check that the identifier uses the expected ARCHIVE_BASE_xxx format.
	local regexp
	regexp='^ARCHIVE_BASE\(_[0-9A-Z]\+\)*_[0-9]\+$'
	if ! printf '%s' "$archive" | grep --quiet --regexp="$regexp"; then
		# Allow the old ARCHIVE_xxx format when targeting a compatibility level < 2.21.
		if ! compatibility_level_is_at_least '2.21'; then
			regexp='^ARCHIVE\(_[0-9A-Z]\+\)\+$'
			if ! printf '%s' "$archive" | grep --quiet --regexp="$regexp"; then
				error_current_archive_format_invalid "$archive"
				return 1
			fi
		else
			error_current_archive_format_invalid "$archive"
			return 1
		fi
	fi

	export PLAYIT_CONTEXT_ARCHIVE="$archive"
}

# Print the identifier of the current archive.
# USAGE: current_archive
# RETURNS: the current archive identifier,
#          or an empty string if no archive is set
current_archive() {
	# To ensure backwards-compatibility, the legacy variable should have a higher priority than the modern one.
	# Otherwise the ability to set the context using $ARCHIVE from game scripts would be lost as soon as the library calls set_current_archive.
	local archive
	archive="${ARCHIVE:-}"
	if \
		[ -n "$archive" ] && \
		compatibility_level_is_at_least '2.27'
	then
		warning_context_legacy_archive
	fi

	if [ -z "$archive" ]; then
		archive="${PLAYIT_CONTEXT_ARCHIVE:-}"
	fi

	printf '%s' "$archive"
}

# Set the identifier of the current package
# USAGE: set_current_package $package
set_current_package() {
	local package
	package="$1"

	# Check that the identifier uses the expected PKG_xxx format.
	local regexp
	regexp='^PKG\(_[0-9A-Z]\+\)\+$'
	if ! printf '%s' "$package" | grep --quiet --regexp="$regexp"; then
		error_current_package_format_invalid "$package"
		return 1
	fi

	# Check that the identifier is included in the list of packages to build.
	if ! package_is_included_in_packages_list "$package"; then
		error_current_package_not_in_list "$package"
		return 1
	fi

	export PLAYIT_CONTEXT_PACKAGE="$package"
}

# Set the identifier of the default package
# USAGE: set_default_package $package
set_default_package() {
	local package
	package="$1"

	# Check that the identifier uses the expected PKG_xxx format.
	local regexp
	regexp='^PKG\(_[0-9A-Z]\+\)\+$'
	if ! printf '%s' "$package" | grep --quiet --regexp="$regexp"; then
		error_current_package_format_invalid "$package"
		return 1
	fi

	# Check that the identifier is included in the list of packages to build.
	if ! package_is_included_in_packages_list "$package"; then
		error_current_package_not_in_list "$package"
		return 1
	fi

	export PLAYIT_CONTEXT_PACKAGE_DEFAULT="$package"
}

# Print the identifier of the current package.
# USAGE: current_package
# RETURN: the current package identifier
current_package() {
	# To ensure backwards-compatibility, the legacy variable should have a higher priority than the modern one.
	# Otherwise the ability to set the context using $PKG from game scripts would be lost as soon as the library calls set_current_package.
	local package
	package="${PKG:-}"
	if \
		[ -n "$package" ] && \
		compatibility_level_is_at_least '2.27'
	then
		warning_context_legacy_package
	fi

	if [ -z "$package" ]; then
		package="${PLAYIT_CONTEXT_PACKAGE:-}"
	fi

	# If no package context is explicitly set, set it to the first package in the list of packages to build.
	if [ -z "$package" ]; then
		package=$(default_package)
	fi

	printf '%s' "$package"
}

# Print the identifier of the default package.
# USAGE: default_package
# RETURN: the default package identifier
default_package() {
	printf '%s' "$PLAYIT_CONTEXT_PACKAGE_DEFAULT"
}

# Print the suffix of the identifier of the current archive.
# USAGE: current_archive_suffix
# RETURN: the current archive identifier suffix including the leading underscore,
#         or an empty string if no archive is set
current_archive_suffix() {
	local archive
	archive=$(current_archive)

	printf '%s' "${archive#ARCHIVE_BASE}"
}

# Print the suffix of the identifier of the current package.
# USAGE: current_package_suffix
# RETURN: the current package identifier suffix including the leading underscore
current_package_suffix() {
	local package
	package=$(current_package)

	printf '%s' "${package#PKG}"
}

# Print the name of the variable containing the context-specific value of the given variable
# Context priority order is the following one:
# - archive-specific
# - package-specific
# - default
# - empty
# USAGE: context_name $variable_name
# RETURN: the name of the variable containing the context-specific value,
#         or an empty string
context_name() {
	local variable_name
	variable_name="$1"

	local current_archive_suffix current_package_suffix
	current_archive_suffix=$(current_archive_suffix)
	current_package_suffix=$(current_package_suffix)

	# Try to find an archive-specific value for the given variable.
	local context_name_archive
	if [ -n "$current_archive_suffix" ]; then
		context_name_archive=$(context_name_archive "$variable_name")
		if [ -n "$context_name_archive" ]; then
			printf '%s' "$context_name_archive"
			return 0
		fi
	fi

	# Try to find a package-specific value for the given variable.
	local context_name_package
	if [ -n "$current_package_suffix" ] ; then
		context_name_package=$(context_name_package "$variable_name")
		if [ -n "$context_name_package" ]; then
			printf '%s' "$context_name_package"
			return 0
		fi
	fi

	# Check if the base variable value is set.
	if ! variable_is_empty "$variable_name"; then
		printf '%s' "$variable_name"
		return 0
	fi

	# If no value has been found for the given variable, an empty string is returned.
}

# Print the name of the variable containing the archive-specific value of the given variable
# USAGE: context_name_archive $variable_name
# RETURN: the name of the variable containing the archive-specific value,
#         or an empty string
context_name_archive() {
	local variable_name
	variable_name="$1"

	local current_archive_suffix
	current_archive_suffix=$(current_archive_suffix)
	# Return early if no archive context is set
	if [ -z "$current_archive_suffix" ]; then
		return 0
	fi

	local current_archive_name
	while [ -n "$current_archive_suffix" ]; do
		current_archive_name="${variable_name}${current_archive_suffix}"
		if ! variable_is_empty "$current_archive_name"; then
			printf '%s' "$current_archive_name"
			return 0
		fi
		current_archive_suffix="${current_archive_suffix%_*}"
	done

	# If no value has been found for the given variable, an empty string is returned.
}

# Print the name of the variable containing the package-specific value of the given variable
# USAGE: context_name_package $variable_name
# RETURN: the name of the variable containing the package-specific value,
#         or an empty string
context_name_package() {
	local variable_name
	variable_name="$1"

	local current_package_suffix
	current_package_suffix=$(current_package_suffix)
	# Return early if no package context is set
	if [ -z "$current_package_suffix" ]; then
		return 0
	fi

	local current_package_name
	while [ -n "$current_package_suffix" ]; do
		current_package_name="${variable_name}${current_package_suffix}"
		if ! variable_is_empty "$current_package_name"; then
			printf '%s' "$current_package_name"
			return 0
		fi
		current_package_suffix="${current_package_suffix%_*}"
	done

	# If no value has been found for the given variable, an empty string is returned.
}

# Print the context-sensitive value for the given variable
# Context priority order is the following one:
# - archive-specific
# - package-specific
# - default
# - empty
# USAGE: context_value $variable_name
# RETURN: the context-sensitive value of the given variable,
#         or an empty string
context_value() {
	local variable_name
	variable_name="$1"

	local context_name
	context_name=$(context_name "$variable_name")
	# Return early if this variable has no set value.
	if [ -z "$context_name" ]; then
		return 0
	fi

	get_value "$context_name"
}

# Returns a list of directories to scan for game scripts
# USAGE: games_list_sources
# RETURNS: A list of directories, separated by line breaks
games_list_sources() {
	# Include the current user game scripts collections
	local user_collections_basedir
	user_collections_basedir="${XDG_DATA_HOME:=$HOME/.local/share}/play.it/games"
	if [ -d "$user_collections_basedir" ]; then
		find "$user_collections_basedir" -mindepth 1 -maxdepth 1 -type d | sort
	fi

	# Include the system-provided game scripts collections
	local system_prefix system_collections_basedir
	for system_prefix in \
		'/usr/local/share/games' \
		'/usr/local/share' \
		'/usr/share/games' \
		'/usr/share'
	do
		system_collections_basedir="${system_prefix}/play.it/games"
		if [ -d "$system_collections_basedir" ]; then
			find "$system_collections_basedir" -mindepth 1 -maxdepth 1 -type d | sort
		fi
	done
}

# List all available game scripts
# USAGE: games_list_scripts_all
# RETURN: a list of available game scripts,
#         separated by line breaks
games_list_scripts_all() {
	local games_sources
	games_sources=$(games_list_sources)

	# Return early if no game script could be found
	if [ -z "$games_sources" ]; then
		return 0
	fi

	while read -r games_collection; do
		find "$games_collection" -name play-\*.sh | sort
	done <<- EOF
	$(printf '%s' "$games_sources")
	EOF
}

# List the game scripts providing support for the given archive name
# USAGE: games_find_scripts_for_archive $archive_name
# RETURN: a list of game scripts,
#         separated by line breaks
games_find_scripts_for_archive() {
	local archive_name
	archive_name="$1"

	## xargs return code is ignored,
	## to prevent a failure state if no available script has support for the given archive.
	set +o errexit
	games_list_scripts_all | xargs grep \
		--files-with-matches \
		--regexp="^ARCHIVE_[0-9A-Z_]\\+=['\"]${archive_name}['\"]"
	set -o errexit
}

# Print the path to the first game script with support with the given archive name
# USAGE: games_find_script_for_archive $archive_name
# RETURN: the path to a single game script
games_find_script_for_archive() {
	local archive_name
	archive_name="$1"

	local scripts_list
	scripts_list=$(games_find_scripts_for_archive "$archive_name")
	printf '%s' "$scripts_list" | head --lines=1
}

# Print the version of the current game script
# USAGE: script_version
# RETURN: the script version string,
#         throw an error if it is not set,
#         throw an error if it does not follow the expected format
script_version() {
	local version_string
	version_string="${script_version:-}"

	# Throw an error if the script version is not set
	if [ -z "$version_string" ]; then
		error_missing_variable 'script_version'
		return 1
	fi

	# Throw an error if the version string does not use the expected format
	local regexp
	regexp='^[0-9]\{8\}\.[0-9]\+$'
	if ! printf '%s' "$version_string" | grep --quiet --regexp="$regexp"; then
		error_invalid_version_string "$version_string"
		return 1
	fi

	printf '%s' "$version_string"
}

# List the games supported by the current script
# USAGE: games_list_supported
# RETURN: a list of games,
#         separated by line breaks,
#         using the following format for each line:
#         game-id | Game name
games_list_supported() {
	local archives_list
	archives_list=$(archives_return_list)

	local archive game_id game_name
	for archive in $archives_list; do
		set_current_archive "$archive"
		game_id=$(game_id)
		game_name=$(game_name)
		printf '%s | %s\n' "$game_id" "$game_name"
	done | sort --unique
}

# List all games supported by the available scripts
# USAGE: games_list_supported_all
# RETURN: a list of games,
#         separated by line breaks,
#         using the following format for each line:
#         game-id | Game name
games_list_supported_all() {
	local scripts_list
	scripts_list=$(games_list_scripts_all)

	local available_threads
	available_threads=$(nproc)
	## Passing the --list-supported-games switch is not required,
	## because $PLAYIT_OPTION_LIST_SUPPORTED_GAMES is already set.
	printf '%s' "$scripts_list" | \
		xargs --delimiter='\n' --max-args=1 --max-procs="$available_threads" sh | \
		sort --unique
}

# Error - $script_version does not follow the expected format
# USAGE: error_invalid_version_string $version_string
error_invalid_version_string() {
	local version_string
	version_string="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La valeur suivante de $script_version ne respecte pas le format attendu : %s\n'
			message="$message"'Le format correct est "YYYYMMDD.N", avec "YYYYMMDD" la date de dernière édition du script, et "N" un nombre incrémenté si le script est édité plusieurs fois dans la même journée.\n'
		;;
		('en'|*)
			message='The following $script_version value does not follow the expected format: %s\n'
			message="$message"'The correct format is "YYYYMMDD.N", with "YYYYMMDD" the date of the last edition of the script, and "N" a number incremented if the script is edited multiple times at the same date.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$version_string"
}

# display full usage instructions
# USAGE: help
help() {
	local script_name
	script_name=$(basename "$0")

	# print general usage instructions
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			if [ "$script_name" = 'play.it' ]; then
				message='\nUtilisation : %s ARCHIVE [OPTION]…\n\n'
			else
				message='\nUtilisation : %s [OPTION]… [ARCHIVE]\n\n'
			fi
		;;
		('en'|*)
			if [ "$script_name" = 'play.it' ]; then
				message='\nUsage: %s ARCHIVE [OPTION]…\n\n'
			else
				message='\nUsage: %s [OPTION]… [ARCHIVE]\n\n'
			fi
		;;
	esac
	print_message 'info' "$message" \
		"$script_name"

	# print details about options usage
	print_message 'info' '%s\n\n' \
		'OPTIONS'
	help_checksum
	help_compression
	help_prefix
	help_package
	help_icons
	help_overwrite
	help_output_dir
	help_debug
	help_no_mtree
	help_tmpdir
	help_skipfreespacecheck
	help_configfile
	help_listpackages
	help_listrequirements
	help_listavailablescripts
	help_listsupportedgames

	# do not print a list of supported archives if called throught the "play.it" wrapper script
	if [ "$script_name" = 'play.it' ]; then
		help_show_game_script
		return 0
	fi

	# print list of supported archives
	print_message 'info' '%s\n\n' \
		'ARCHIVE'
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Ce script reconnaît les archives suivantes :\n'
		;;
		('en'|*)
			message='This script can work on the following archives:\n'
		;;
	esac
	print_message 'info' "$message"
	# shellcheck disable=SC2046
	information_archives_list $(archives_return_list)

	return 0
}

# display --checksum option usage
# USAGE: help_checksum
help_checksum() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tChoix de la méthode de vérification dʼintégrité de lʼarchive\n\n'
			message="$message"'\t%s\tvérification via md5sum\n' # md5
			message="$message"'\t%s\tpas de vérification\n\n'   # none
		;;
		('en'|*)
			message='\tArchive integrity verification method selection\n\n'
			message="$message"'\t%s\tmd5sum verification\n' # md5
			message="$message"'\t%s\tno verification\n\n'   # none
		;;
	esac
	print_message 'info' '--%s %s\n' \
		'checksum' \
		'md5|none'
	print_message 'info' "$message" \
		'md5' \
		'none'
}

# Display --compression option usage
# USAGE: help_compression
help_compression() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tChoix de la méthode de compression des paquets générés\n'
			message="$message"'\t%s\tpas de compression\n'
			message="$message"'\t%s\tméthode de compression mettant lʼaccent sur la rapidité\n'
			message="$message"'\t%s\tméthode de compression mettant lʼaccent sur la réduction de taille\n'
			message="$message"'\t%s\tméthode de compression par défaut du système actuel\n\n'
		;;
		('en'|*)
			message='\tGenerated packages compression method selection\n'
			message="$message"'\t%s\tno compression\n'
			message="$message"'\t%s\tcompression method focusing on compression speed\n'
			message="$message"'\t%s\tcompression method focusing on size reduction\n'
			message="$message"'\t%s\tdefault compression method on the current system\n\n'
		;;
	esac
	print_message 'info' '--%s %s\n' \
		'compression' \
		'none|speed|size|auto'
	print_message 'info' "$message" \
		'none' \
		'speed' \
		'size' \
		'auto'
}

# display --prefix option usage
# USAGE: help_prefix
help_prefix() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tChoix du chemin dʼinstallation du jeu\n\n'
			message="$message"'\tCette option accepte uniquement un chemin absolu.\n\n'
		;;
		('en'|*)
			message='\tGame installation path setting\n\n'
			message="$message"'\tThis option accepts an absolute path only.\n\n'
		;;
	esac
	print_message 'info' '--%s %s\n' \
		'prefix' \
		'path'
	print_message 'info' "$message"
}

# display --package option usage
# USAGE: help_package
help_package() {
	## TODO: "egentoo" format should be included.
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tChoix du type de paquet à construire\n\n'
			message="$message"'\t%s\tpaquet .pkg.tar (Arch Linux)\n'
			message="$message"'\t%s\tpaquet .deb (Debian, Ubuntu)\n'
			message="$message"'\t%s\tpaquet .tbz2 (Gentoo)\n\n'
		;;
		('en'|*)
			message='\tGenerated package type selection\n\n'
			message="$message"'\t%s\t.pkg.tar package (Arch Linux)\n'
			message="$message"'\t%s\t.deb package (Debian, Ubuntu)\n'
			message="$message"'\t%s\t.tbz2 package (Gentoo)\n\n'
		;;
	esac
	print_message 'info' '--%s %s\n' \
		'prefix' \
		'arch|deb|gentoo'
	print_message 'info' "$message" \
		'arch' \
		'deb' \
		'gentoo'
}

# display --no-icons option usage
# USAGE: help_icons
help_icons() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tNe pas inclure les icônes du jeu.\n\n'
		;;
		('en'|*)
			message='\tDo not include game icons.\n\n'
		;;
	esac
	print_message 'info' '--%s\n' \
		'no-icons'
	print_message 'info' "$message"
}

# display --overwrite option usage
# USAGE: help_overwrite
help_overwrite() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tRemplace les paquets si ils existent déjà.\n\n'
		;;
		('en'|*)
			message='\tReplace packages if they already exist.\n\n'
		;;
	esac
	print_message 'info' '--%s\n' \
		'overwrite'
	print_message 'info' "$message"
}

# display --output-dir option usage
# USAGE: help_output_dir
help_output_dir() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tDéfinit le répertoire de destination des paquets générés.\n\n'
		;;
		('en'|*)
			message='\tSet the output directory for generated packages.\n\n'
		;;
	esac
	print_message 'info' '--%s\n' \
		'output-dir'
	print_message 'info' "$message"
}

# display --debug option usage
# USAGE: help_debug
help_debug() {
	local messages_language message
	messages_language=$(messages_language)
	# shellcheck disable=SC2050
	if [ 1 -eq 1 ]; then
		case "$messages_language" in
			('fr')
				message='\tLe debug a été désactivé lors de la compilation.\n'
				message="$message"'\tCette option est sans effet.\n\n'
				;;
			('en'|*)
				message='\tDebug was disabled at compile-time.\n'
				message="$message"'\tThis option has no effect.\n\n'
				;;
		esac
	else
		case "$messages_language" in
			('fr')
				message='\tDéfinit le niveau de debug. Il vaut 1 par défaut.\n\n'
				;;
			('en'|*)
				message='\tSet the debug level. Default is 1.\n\n'
				;;
		esac
	fi
	print_message 'info' '--%s %s\n' \
		'debug' \
		'N'
	print_message 'info' "$message"
}

# display --show-game-script option usage
# USAGE: help_show_game_script
help_show_game_script() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tAffiche uniquement le chemin vers le script à utiliser, sans le lancer.\n\n'
			;;
		('en'|*)
			message='\tOnly displays the name of the script to use, without running it.\n\n'
			;;
	esac
	print_message 'info' '--%s\n' \
		'show-game-script'
	print_message 'info' "$message"
}

# display --no-mtree option usage
# USAGE: help_no_mtree
help_no_mtree() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tNe crée pas de fichier .MTREE pour les paquets Arch Linux.\n\n'
			;;
		('en'|*)
			message='\tDo not make .MTREE file in Arch Linux packages\n\n'
			;;
	esac
	print_message 'info' '--%s\n' \
		'no-mtree'
	print_message 'info' "$message"
}

# Display --tmpdir option usage
# USAGE: help_tmpdir
help_tmpdir() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tDéfinit le répertoire utilisé pour le stockage des fichiers temporaire.\n'
			message="$message"'\tLa valeur par défaut est : %s\n\n'
		;;
		('en'|*)
			message='\tSet the directory used for temporary files storage.\n'
			message="$message"'\tDefault value is: %s\n\n'
		;;
	esac
	print_message 'info' '--%s\n' \
		'tmpdir'
	print_message 'info' "$message" \
		"${TMPDIR:-/tmp}"
}

# Display --no-free-space-check option usage
# USAGE: help_skipfreespacecheck
help_skipfreespacecheck() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tNe pas tester lʼespace libre disponible.\n\n'
		;;
		('en'|*)
			message='\tDo not check for free space.\n\n'
		;;
	esac
	print_message 'info' '--%s\n' \
		'no-free-space-check'
	print_message 'info' "$message"
}

# Display --config-file option usage
# USAGE: help_configfile
help_configfile() {
	local config_file_path
	config_file_path=$(configuration_file_default_path)

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tDéfinit le fichier de configuration à utiliser.\n'
			message="$message"'\tLe fichier par défaut est : %s\n\n'
			;;
		('en'|*)
			message='\tSet the configuration file to use.\n'
			message="$message"'\tDefault file is: %s\n\n'
			;;
	esac
	print_message 'info' '--%s\n' \
		'config-file'
	print_message 'info' "$message" \
		"$config_file_path"
}

# Display --list-packages option usage
# USAGE: help_listpackages
help_listpackages() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tAffiche la liste des paquets à construire.\n\n'
			;;
		('en'|*)
			message='\tPrint the list of packages to build.\n\n'
			;;
	esac
	print_message 'info' '--%s\n' \
		'list-packages'
	print_message 'info' "$message"
}

# Display --list-requirements option usage
# USAGE: help_listrequirements
help_listrequirements() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tAffiche la liste des commandes nécessaire à la construction de paquets à partir de lʼarchive donnée.\n\n'
		;;
		('en'|*)
			message='\tPrint the list of commands required to build packages from the given archive.\n\n'
		;;
	esac
	print_message 'info' '--%s\n' \
		'list-requirements'
	print_message 'info' "$message"
}

# Display --list-available-scripts option usage
# USAGE: help_listavailablescripts
help_listavailablescripts() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tAffiche la liste des scripts de prise en charge de jeux disponibles sur ce système.\n\n'
		;;
		('en'|*)
			message='\tPrint the list of game scripts available on this system.\n\n'
		;;
	esac
	print_message 'info' '--%s\n' \
		'list-available-scripts'
	print_message 'info' "$message"
}

# Display --list-supported-games option usage
# USAGE: help_listsupportedgames
help_listsupportedgames() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\tAffiche la liste des jeux pris en charge.\n'
			message="$message"'\tAttention : cette opération peut prendre plusieurs minutes.\n\n'
		;;
		('en'|*)
			message='\tPrint the list of supported games.\n'
			message="$message"'\tWarning: this operation can take several minutes.\n\n'
		;;
	esac
	print_message 'info' '--%s\n' \
		'list-supported-games'
	print_message 'info' "$message"
}

# Get the language set for messages in the current environment
# USAGE: messages_language
# RETURN: the language code, as a two-letters code, or the fallback value "C"
messages_language() {
	# Relying on the "locale" command prevents the need to query the values of multiple variables,
	# and handle the priority between them.
	locale | sed --silent 's/LC_MESSAGES="\?\([^_"]*\).*/\1/p'
}

# Print a localized message
# USAGE: print_message $level $message $extra_values[…]
print_message() {
	local level
	# Valid levels are:
	# - error
	# - warning
	# - warning_once
	# - info
	# - info_once
	# Unknown levels will be handled similar to "info".
	level="$1"
	shift 1

	case "$level" in
		('error')
			print_message_error "$@"
		;;
		('warning')
			print_message_warning "$@"
		;;
		('warning_once')
			print_message_warning_once "$@"
		;;
		('info_once')
			print_message_info_once "$@"
		;;
		('info'|*)
			print_message_info "$@"
		;;
	esac
}

# Print a localized error message
# USAGE: print_message_error $message $extra_values[…]
print_message_error() (
	local message
	# The message is a format string, any included "%s" will be replaced by the values passed as extra arguments.
	# See printf(1) for details on the sequences that can be used in the format string.
	message="$1"
	shift 1

	## Since this is called from a subshell, this should not trigger unwanted output redirections.
	exec 1>&2
	local messages_language error_string
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			error_string='Erreur :'
		;;
		('en'|*)
			error_string='Error:'
		;;
	esac
	printf '\n\033[1;31m%s\033[0m\n' "$error_string"
	## Silence ShellCheck false-positive
	## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
	# shellcheck disable=SC2059
	printf -- "$message" "$@"
)

# Print a localized warning message
# USAGE: print_message_warning $message $extra_values[…]
print_message_warning() {
	local message
	# The message is a format string, any included "%s" will be replaced by the values passed as extra arguments.
	# See printf(1) for details on the sequences that can be used in the format string.
	message="$1"
	shift 1

	local messages_language warning_string
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			warning_string='Avertissement :'
		;;
		('en'|*)
			warning_string='Warning:'
		;;
	esac
	printf '\n\033[1;33m%s\033[0m\n' "$warning_string"
	## Silence ShellCheck false-positive
	## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
	# shellcheck disable=SC2059
	printf -- "$message" "$@"
}

# Print a localized warning message, only if it has not already been shown
# USAGE: print_message_warning_once $message $extra_values[…]
print_message_warning_once() {
	# Return early if this message has already been shown.
	if message_has_been_shown_already "$@"; then
		return 0
	fi

	print_message_warning "$@"

	# Prevent this message from being shown again.
	messages_already_shown_add "$@"
}

# Print a localized information message
# USAGE: print_message_info $message $extra_values[…]
print_message_info() {
	local message
	# The message is a format string, any included "%s" will be replaced by the values passed as extra arguments.
	# See printf(1) for details on the sequences that can be used in the format string.
	message="$1"
	shift 1

	## Silence ShellCheck false-positive
	## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
	# shellcheck disable=SC2059
	printf -- "$message" "$@"
}

# Print a localized information message, only if it has not already been shown
# USAGE: print_message_info_once $message $extra_values[…]
print_message_info_once() {
	# Return early if this message has already been shown.
	if message_has_been_shown_already "$@"; then
		return 0
	fi

	print_message_info "$@"

	# Prevent this message from being shown again.
	messages_already_shown_add "$@"
}

# Compute a unique message indentifier from a message function name and its arguments
# USAGE: message_identifier $message_function $extra_values[…]
message_identifier() {
	local message_function
	message_function="$1"
	shift 1

	printf '%s' "$message_function"
	if [ $# -ge 1 ]; then
		printf ':%s' "$@"
	fi
}

# Print the path to a file listing messages already shown
# USAGE: messages_already_shown_file
# RETURN: the path to the file listing the messages,
#         or an empty string if such a path can not be computed yet
messages_already_shown_file() {
	# The list of shown messages can only be used when PLAYIT_WORKDIR is already set.
	if [ -z "${PLAYIT_WORKDIR:-}" ]; then
		return 0
	fi

	printf '%s/messages-shown' "$PLAYIT_WORKDIR"
}

# Add a message to the list of already shown ones.
# USAGE: messages_already_shown_add $message_function $extra_values[…]
messages_already_shown_add() {
	local messages_list
	messages_list=$(messages_already_shown_file)
	# The list of shown messages can only be used when PLAYIT_WORKDIR is already set.
	if [ -z "$messages_list" ]; then
		return 0
	fi

	local message_identifier
	message_identifier=$(message_identifier "$@")

	printf '%s\n' "$message_identifier" >> "$messages_list"
}

# Check if a message has already been shown.
# USAGE: messages_has_been_shown_already $message_function $extra_values[…]
# RETURN: 0 if the given message has already been shown,
#         1 if there is no messages list yet,
#         1 otherwise
message_has_been_shown_already() {
	local messages_list
	messages_list=$(messages_already_shown_file)
	# The list of shown messages can only be used when PLAYIT_WORKDIR is already set.
	if [ -z "$messages_list" ]; then
		return 1
	fi

	# Return early if there is no list of shown messages yet.
	if [ ! -e "$messages_list" ]; then
		return 1
	fi

	local message_identifier
	message_identifier=$(message_identifier "$@")

	grep --quiet --fixed-strings --line-regexp --regexp="$message_identifier" "$messages_list"
}

# Load options values from the configuration file.
# USAGE: load_configuration_file $config_file_path
load_configuration_file() {
	local config_file_path
	config_file_path="$1"

	# Default configuration file may not exist, ignoring this then.
	if [ ! -f "$config_file_path" ]; then
		return 0
	fi

	# Parse the configuration file.
	local arguments
	arguments=''
	while read -r line; do
		case $line in
			('#'*)
				# Ignore commented lines.
			;;
			(*)
				arguments="$arguments $line"
			;;
		esac
	done <<- EOF
		$(cat "$config_file_path")
	EOF

	parse_arguments_default $arguments
}

# Print the configuration file path.
# USAGE: find_configuration_file $arguments[…]
find_configuration_file() {
	local arguments_string
	arguments_string=$(getopt_arguments_cleanup "$@")
	eval set -- "$arguments_string"

	local config_file_path
	config_file_path=''

	# Override the default path if another one has been specified.
	while [ $# -gt 0 ]; do
		case "$1" in
			('--config-file')
				config_file_path="$2"
				break
			;;
		esac
		shift 1
	done

	if \
		[ -n "$config_file_path" ] \
		&& [ ! -f "$config_file_path" ]
	then
		error_config_file_not_found "$config_file_path"
		return 1
	fi

	# Fall back on the default path if no custom one is set.
	if [ -z "$config_file_path" ]; then
		config_file_path=$(configuration_file_default_path)
	fi

	printf '%s' "$config_file_path"
}

# Print the default path to the configuration file
# USAGE: configuration_file_default_path
# RETURN: a string representing a path to a file,
#         no check of the file actual existence is done
configuration_file_default_path() {
	local configuration_path
	if variable_is_empty 'XDG_CONFIG_HOME'; then
		configuration_path="${HOME}/.config"
	else
		configuration_path="$XDG_CONFIG_HOME"
	fi
	printf '%s/play.it/config' "$configuration_path"
}

# Clean up the command-line parameters using getopt
# USAGE: getopt_arguments_cleanup $arguments[…]
# RETURN: a standardized parameters string
getopt_arguments_cleanup() {
	getopt \
		--name 'play.it' \
		--shell 'sh' \
		--options '' \
		--longoptions 'help' \
		--longoptions 'list-available-scripts' \
		--longoptions 'list-packages' \
		--longoptions 'list-supported-games' \
		--longoptions 'overwrite' \
		--longoptions 'show-game-script' \
		--longoptions 'version' \
		--longoptions 'no-free-space-check' \
		--longoptions 'no-icons' \
		--longoptions 'no-mtree' \
		--longoptions 'config-file:' \
		--longoptions 'checksum:' \
		--longoptions 'compression:' \
		--longoptions 'output-dir:' \
		--longoptions 'package:' \
		--longoptions 'prefix:' \
		--longoptions 'tmpdir:' \
		--longoptions 'debug::' \
		-- "$@"
}

# Parse the arguments given to the game script or wrapper
# WARNING: Options that are already set from the user environment are not overriden.
# USAGE: parse_arguments $arguments[…]
parse_arguments() {
	local arguments_string
	arguments_string=$(getopt_arguments_cleanup "$@")
	eval set -- "$arguments_string"

	local option_name option_variable option_value
	while [ $# -gt 0 ]; do
		unset option_name option_variable option_value
		case "$1" in
			( \
				'--help' | \
				'--list-available-scripts' | \
				'--list-packages' | \
				'--list-requirements' | \
				'--list-supported-games' | \
				'--overwrite' | \
				'--show-game-script' | \
				'--version' \
			)
				option_name=$(printf '%s' "$1" | sed 's/^--//')
				option_update "$option_name" 1
			;;
			( \
				'--no-free-space-check' | \
				'--no-icons' | \
				'--no-mtree' \
			)
				option_name=$(printf '%s' "$1" | sed 's/^--no-//')
				option_update "$option_name" 0
			;;
			('--config-file')
				# Skip this argument, has it should have already been handled by find_configuration_file.
				shift 1
			;;
			('--debug')
				option_name=$(printf '%s' "$1" | sed 's/^--//')
				option_variable=$(option_variable "$option_name")
				option_value="$2"
				shift 1
				case "$option_value" in
					([0-9])
						option_update "$option_name" "$option_value"
					;;
					(*)
						option_update "$option_name" 1
					;;
				esac
			;;
			( \
				'--checksum' | \
				'--compression' | \
				'--output-dir' | \
				'--package' | \
				'--prefix' | \
				'--tmpdir' \
			)
				option_name=$(printf '%s' "$1" | sed 's/^--//')
				option_variable=$(option_variable "$option_name")
				option_value="$2"
				shift 1
				option_update "$option_name" "$option_value"
			;;
			('--')
				# Skip the "--" separator.
			;;
			(*)
				if [ -f "$1" ]; then
					SOURCE_ARCHIVE_PATH="$1"
					SOURCE_ARCHIVE_NAME=$(basename "$SOURCE_ARCHIVE_PATH")
					PLAYIT_ARCHIVES_PATH_BASE=$(dirname "$SOURCE_ARCHIVE_PATH")
					export SOURCE_ARCHIVE_PATH SOURCE_ARCHIVE_NAME PLAYIT_ARCHIVES_PATH_BASE
				else
					error_not_a_file "$1"
					return 1
				fi
			;;
		esac
		shift 1
	done
}

# Parse the arguments set through the configuration file
# WARNING: Only a subset of the supported options are allowed here.
# USAGE: parse_arguments $arguments[…]
parse_arguments_default() {
	local arguments_string
	arguments_string=$(getopt_arguments_cleanup "$@")
	eval set -- "$arguments_string"

	local option_name option_value
	while [ $# -gt 0 ]; do
		unset option_name option_value
		case "$1" in
			('--overwrite')
				option_name=$(printf '%s' "$1" | sed 's/^--//')
				option_update_default "$option_name" 1
			;;
			( \
				'--no-free-space-check' | \
				'--no-icons' | \
				'--no-mtree' \
			)
				option_name=$(printf '%s' "$1" | sed 's/^--//')
				option_update_default "$option_name" 0
			;;
			('--debug')
				option_name=$(printf '%s' "$1" | sed 's/^--//')
				option_value="$2"
				shift 1
				case "$option_value" in
					([0-9])
						option_update_default "$option_name" "$option_value"
					;;
					(*)
						option_update_default "$option_name" 1
					;;
				esac
			;;
			( \
				'--checksum' | \
				'--compression' | \
				'--output-dir' | \
				'--package' | \
				'--prefix' | \
				'--tmpdir' \
			)
				option_name=$(printf '%s' "$1" | sed 's/^--//')
				option_value="$2"
				shift 1
				option_update_default "$option_name" "$option_value"
			;;
		esac
		shift 1
	done
}

# Set default values for all options
# USAGE: options_init_default
options_init_default() {
	# Try to guess the desired package format based on the host system
	local default_package_format
	## Get system codename.
	local host_system
	if [ -e '/etc/os-release' ]; then
		host_system=$(grep '^ID=' '/etc/os-release' | cut --delimiter='=' --fields=2)
	elif command -v lsb_release >/dev/null 2>&1; then
		host_system=$(lsb_release --id --short | tr '[:upper:]' '[:lower:]')
	fi
	## Set the most appropriate package type for the current system.
	case "${host_system:-}" in
		( \
			'debian' | \
			'ubuntu' | \
			'linuxmint' | \
			'handylinux' \
		)
			default_package_format='deb'
		;;
		( \
			'arch' | \
			'artix' | \
			'manjaro' | \
			'manjarolinux' | \
			'endeavouros' | \
			'steamos' \
		)
			default_package_format='arch'
		;;
		( \
			'gentoo' \
		)
			default_package_format='gentoo'
		;;
	esac

	# Using a direct export call here instead of relying on option_update_default
	# massively improves performances when running a lot of ./play.it calls,
	# like what is done by the --list-supported-games option.
	export \
		PLAYIT_DEFAULT_OPTION_CHECKSUM='md5' \
		PLAYIT_DEFAULT_OPTION_COMPRESSION='none' \
		PLAYIT_DEFAULT_OPTION_OUTPUT_DIR="$PWD" \
		PLAYIT_DEFAULT_OPTION_PACKAGE="${default_package_format:-deb}" \
		PLAYIT_DEFAULT_OPTION_PREFIX='/usr' \
		PLAYIT_DEFAULT_OPTION_TMPDIR="${TPMDIR:-/tmp}" \
		PLAYIT_DEFAULT_OPTION_FREE_SPACE_CHECK=1 \
		PLAYIT_DEFAULT_OPTION_ICONS=1 \
		PLAYIT_DEFAULT_OPTION_MTREE=1 \
		PLAYIT_DEFAULT_OPTION_DEBUG=0 \
		PLAYIT_DEFAULT_OPTION_HELP=0 \
		PLAYIT_DEFAULT_OPTION_LIST_AVAILABLE_SCRIPTS=0 \
		PLAYIT_DEFAULT_OPTION_LIST_PACKAGES=0 \
		PLAYIT_DEFAULT_OPTION_LIST_REQUIREMENTS=0 \
		PLAYIT_DEFAULT_OPTION_LIST_SUPPORTED_GAMES=0 \
		PLAYIT_DEFAULT_OPTION_OVERWRITE=0 \
		PLAYIT_DEFAULT_OPTION_SHOW_GAME_SCRIPT=0 \
		PLAYIT_DEFAULT_OPTION_VERSION=0
}

# Get the name of the variable used to store the value of the given option
# USAGE: option_variable $option_name
# RETURN: the variable name
option_variable() {
	local option_name
	option_name="$1"

	# The environment variable used to store an option is derived from its name:
	# - replace "-" with "_"
	# - convert to uppercase
	# - prepend "PLAYIT_OPTION_"
	# As an exemple, the value of the option "output-dir" would be stored in the following variable:
	# PLAYIT_OPTION_OUTPUT_DIR
	printf 'PLAYIT_OPTION_%s' "$(
		printf '%s' "$option_name" | \
			sed 's/-/_/g' | \
			tr '[:lower:]' '[:upper:]'
	)"
}

# Get the name of the variable used to store the default value of the given option
# USAGE: option_variable_default $option_name
# RETURN: the variable name
option_variable_default() {
	local option_name
	option_name="$1"

	# The environment variable used to store an option is derived from its name:
	# - replace "-" with "_"
	# - convert to uppercase
	# - prepend "PLAYIT_DEFAULT_OPTION_"
	# As an exemple, the default value of the option "output-dir" would be stored in the following variable:
	# PLAYIT_DEFAULT_OPTION_OUTPUT_DIR
	printf 'PLAYIT_DEFAULT_OPTION_%s' "$(
		printf '%s' "$option_name" | \
			sed 's/-/_/g' | \
			tr '[:lower:]' '[:upper:]'
	)"
}

# Update the value of the given option
# USAGE: option_update $option_name $option_value
option_update() {
	local option_name option_value
	option_name="$1"
	option_value="$2"

	local option_variable
	option_variable=$(option_variable "$option_name")
	export $option_variable="$option_value"
}

# Update the default value of the given option
# USAGE: option_update_default $option_name $option_value
option_update_default() {
	local option_name option_value
	option_name="$1"
	option_value="$2"

	local option_variable
	option_variable=$(option_variable_default "$option_name")
	export $option_variable="$option_value"
}

# Get the value of the given option
# USAGE: option_value $option_name
# RETURN: the option value
option_value() {
	local option_name
	option_name="$1"

	local option_variable option_value
	option_variable=$(option_variable "$option_name")
	option_value=$(get_value "$option_variable")
	if [ -n "$option_value" ]; then
		printf '%s' "$option_value"
		return 0
	fi

	# If no value is explicitly set, return the default one
	option_variable=$(option_variable_default "$option_name")
	get_value "$option_variable"
}

# Check the validity of all options
# USAGE: options_validity_check
# RETURN: nothing if all option values are valid,
#         throw an error otherwise
options_validity_check() {
	local option_checksum
	option_checksum=$(option_value 'checksum')
	case "$option_checksum" in
		('md5'|'none') ;;
		(*)
			error_option_invalid 'checksum' "$option_checksum"
			return 1
		;;
	esac

	local option_compression
	option_compression=$(option_value 'compression')
	case "$option_compression" in
		('none'|'speed'|'size'|'auto') ;;
		(*)
			error_option_invalid 'compression' "$option_compression"
			return 1
		;;
	esac

	local option_debug
	option_debug=$(option_value 'debug')
	case "$option_debug" in
		([0-9]) ;;
		(*)
			error_option_invalid 'debug' "$option_debug"
			return 1
		;;
	esac

	local option_free_space_check
	option_free_space_check=$(option_value 'free-space-check')
	case "$option_free_space_check" in
		(0|1) ;;
		(*)
			error_option_invalid 'free-space-check' "$option_free_space_check"
			return 1
		;;
	esac

	local option_help
	option_help=$(option_value 'help')
	case "$option_help" in
		(0|1) ;;
		(*)
			error_option_invalid 'help' "$option_help"
			return 1
		;;
	esac

	local option_icons
	option_icons=$(option_value 'icons')
	case "$option_icons" in
		(0|1) ;;
		(*)
			error_option_invalid 'icons' "$option_icons"
			return 1
		;;
	esac

	local option_list_available_scripts
	option_list_available_scripts=$(option_value 'list-available-scripts')
	case "$option_list_available_scripts" in
		(0|1) ;;
		(*)
			error_option_invalid 'list-available-scripts' "$option_list_available_scripts"
			return 1
		;;
	esac

	local option_list_packages
	option_list_packages=$(option_value 'list-packages')
	case "$option_list_packages" in
		(0|1) ;;
		(*)
			error_option_invalid 'list-packages' "$option_list_packages"
			return 1
		;;
	esac

	local option_list_requirements
	option_list_requirements=$(option_value 'list-requirements')
	case "$option_list_requirements" in
		(0|1) ;;
		(*)
			error_option_invalid 'list-requirements' "$option_list_requirements"
			return 1
		;;
	esac

	local option_list_supported_games
	option_list_supported_games=$(option_value 'list-supported-games')
	case "$option_list_supported_games" in
		(0|1) ;;
		(*)
			error_option_invalid 'list-supported-games' "$option_list_supported_games"
			return 1
		;;
	esac

	local option_mtree
	option_mtree=$(option_value 'mtree')
	case "$option_mtree" in
		(0|1) ;;
		(*)
			error_option_invalid 'mtree' "$option_mtree"
			return 1
		;;
	esac

	local option_output_dir
	option_output_dir=$(option_value 'output-dir')
	# Check that the value of "output-dir" is a path to a writable directory.
	## This check is not useful if a no-op option has been set.
	local noop_option noop_option_value noop_option_set
	noop_option_set=0
	for noop_option in \
		'help' \
		'list-available-scripts' \
		'list-packages' \
		'list-requirements' \
		'list-supported-games' \
		'show-game-script' \
		'version'
	do
		noop_option_value=$(option_value "$noop_option")
		if [ "$noop_option_value" -eq 1 ]; then
			noop_option_set=1
			break
		fi
	done
	if [ "$noop_option_set" = 0 ]; then
		local output_dir_path
		output_dir_path=$(printf '%s' "$option_output_dir" | sed "s#^~/#${HOME}/#")
		if [ ! -d "$output_dir_path" ]; then
			error_not_a_directory "$output_dir_path"
			return 1
		fi
		if [ ! -w "$output_dir_path" ]; then
			error_not_writable "$output_dir_path"
			return 1
		fi
	fi

	local option_overwrite
	option_overwrite=$(option_value 'overwrite')
	case "$option_overwrite" in
		(0|1) ;;
		(*)
			error_option_invalid 'overwrite' "$option_overwrite"
			return 1
		;;
	esac

	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'deb'|'gentoo'|'egentoo') ;;
		(*)
			error_option_invalid 'package' "$option_package"
			return 1
		;;
	esac

	local option_prefix
	option_prefix=$(option_value 'prefix')
	# Check that the value of "prefix" is representing an absolute path.
	if printf '%s' "$option_prefix" | grep --quiet --invert-match --regexp '^/'; then
		return 0
	fi

	local option_show_game_script
	option_show_game_script=$(option_value 'show-game-script')
	case "$option_show_game_script" in
		(0|1) ;;
		(*)
			error_option_invalid 'show-game-script' "$option_show_game_script"
			return 1
		;;
	esac

	local option_tmpdir
	option_tmpdir=$(option_value 'tmpdir')
	# Check that the value of "tmpdir" is a path to a writable directory.
	## This check is not useful if a no-op option has been set.
	local noop_option noop_option_value noop_option_set
	noop_option_set=0
	for noop_option in \
		'help' \
		'list-available-scripts' \
		'list-packages' \
		'list-requirements' \
		'list-supported-games' \
		'show-game-script' \
		'version'
	do
		noop_option_value=$(option_value "$noop_option")
		if [ "$noop_option_value" -eq 1 ]; then
			noop_option_set=1
			break
		fi
	done
	if [ "$noop_option_set" = 0 ]; then
		local tmpdir_path
		tmpdir_path=$(printf '%s' "$option_tmpdir" | sed "s#^~/#${HOME}/#")
		if [ ! -d "$tmpdir_path" ]; then
			error_not_a_directory "$tmpdir_path"
			return 1
		fi
		if [ ! -w "$tmpdir_path" ]; then
			error_not_writable "$tmpdir_path"
			return 1
		fi
	fi

	local option_version
	option_version=$(option_value 'version')
	case "$option_version" in
		(0|1) ;;
		(*)
			error_option_invalid 'version' "$option_version"
			return 1
		;;
	esac
}

# Check the compatibility of all set options
# USAGE: options_compatibility_check
# RETURN: nothing if all the current options are valid used together,
#         throw an error otherwise
options_compatibility_check() {
	# Check the compatibility of --compression auto with the target package format.
	local option_compression
	option_compression=$(option_value 'compression')
	case "$option_compression" in
		('none')
			local option_package
			option_package=$(option_value 'package')
			case "$option_package" in
				('gentoo')
					# --compression none has not been implemented for Gentoo packages yet.
					error_incompatible_options 'package' 'compression'
					return 1
				;;
			esac
		;;
		('auto')
			local option_package
			option_package=$(option_value 'package')
			case "$option_package" in
				('arch')
					# --compression auto has not been implemented for Arch Linux packages yet.
					error_incompatible_options 'package' 'compression'
					return 1
				;;
			esac
		;;
	esac
}

# Error - An invalid value has been provided for the given option
# USAGE: error_option_invalid $option_name $option_value
error_option_invalid() {
	local option_name option_value
	option_name="$1"
	option_value="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='"%s" nʼest pas une valeur valide pour --%s.\n'
		;;
		('en'|*)
			message='"%s" is not a valid value for --%s.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$option_value" \
		"$option_name"
}

# Error - The configuration file could not be found
# USAGE: error_config_file_not_found $config_file_path
error_config_file_not_found() {
	local config_file_path
	config_file_path="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le fichier de configuration %s nʼa pas pu être trouvé.\n'
			;;
		('en'|*)
			message='The configuration file %s has not been found.\n'
			;;
	esac
	print_message 'error' "$message" \
		"$config_file_path"
}

# Error - Some options are currently set to incompatible values
# USAGE: error_incompatible_options $option_name_1 $option_name_2
error_incompatible_options() {
	local option_name_1 option_name_2
	option_name_1="$1"
	option_name_2="$2"

	local option_value_1 option_value_2
	option_value_1=$(option_value "$option_name_1")
	option_value_2=$(option_value "$option_name_2")

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Les options suivantes ne sont pas compatibles :\n'
			message="$message"'\t--%s %s\n'
			message="$message"'\t--%s %s\n\n'
		;;
		('en'|*)
			message='The following options are not compatible:\n'
			message="$message"'\t--%s %s\n'
			message="$message"'\t--%s %s\n\n'
		;;
	esac
	print_message 'error' "$message" \
		"$option_name_1" \
		"$option_value_1" \
		"$option_name_2" \
		"$option_value_2"
}

# print the id of the current game
# USAGE: game_id
# RETURN: the game id, limited to the characters set [-0-9a-z],
#         the id can not start nor end with an hyphen (-) character
game_id() {
	# The game id might might be archive-specific
	local game_id
	game_id=$(context_value 'GAME_ID')

	# Check that the id fits the format restrictions
	if ! game_id_validity_check "$game_id"; then
		error_game_id_invalid "$game_id"
		return 1
	fi

	printf '%s' "$game_id"
}

# Print the id of the current expansion
# USAGE: expansion_id
# RETURN: the expansion id, limited to the characters set [-0-9a-z],
#         the id can not start nor end with an hyphen (-) character,
#         an empty value is returned if no expansion id is set
expansion_id() {
	# The expansion id might might be archive-specific
	local expansion_id
	expansion_id=$(context_value 'EXPANSION_ID')

	# Return early if no expansion id is set.
	if [ -z "$expansion_id" ]; then
		return 0
	fi

	# Check that the id fits the format restrictions
	if ! game_id_validity_check "$expansion_id"; then
		error_expansion_id_invalid "$expansion_id"
		return 1
	fi

	printf '%s' "$expansion_id"
}

# Check the validity of the given game (or expansion) id
# USAGE: game_id_validity_check $id_string
# RETURN: 0 if the id is valid, 1 if it is not
game_id_validity_check() {
	local game_id
	game_id="$1"

	# Check that the given id:
	# - is limited to the characters set [-0-9a-z]
	# - does not start with an hyphen (-) character
	# - does not end with an hyphen (-) character
	printf '%s' "$game_id" | \
		grep --quiet --regexp='^[0-9a-z][-0-9a-z]\+[0-9a-z]$'
}

# Print the display name of the current game
# If an expansion name is set, it is included
# USAGE: game_name
# RETURN: the game name, for use in package description and menu entries
game_name() {
	local game_name expansion_name
	game_name=$(context_value 'GAME_NAME')
	expansion_name=$(context_value 'EXPANSION_NAME')

	if [ -n "$expansion_name" ]; then
		printf '%s - %s' "$game_name" "$expansion_name"
	else
		printf '%s' "$game_name"
	fi
}
# Print the name of the engine used by the current game
# USAGE: game_engine
# RETURN: the game engine,
#         or an empty string if none is set
game_engine() {
	local game_engine
	game_engine="${GAME_ENGINE:-}"

	# Try to identify games using Unity3D
	if [ -z "$game_engine" ]; then
		local unity3d_name
		unity3d_name=$(unity3d_name)
		if [ -n "$unity3d_name" ]; then
			game_engine='unity3d'
		fi
	fi

	# Try to identify games using Unreal Engine 4
	if [ -z "$game_engine" ]; then
		local unrealengine4_name
		unrealengine4_name=$(unrealengine4_name)
		if [ -n "$unrealengine4_name" ]; then
			game_engine='unrealengine4'
		fi
	fi

	# Try to identify games using Visionaire
	if [ -z "$game_engine" ]; then
		local visionaire_name
		visionaire_name=$(visionaire_name)
		if [ -n "$visionaire_name" ]; then
			game_engine='visionaire'
		fi
	fi

	printf '%s' "$game_engine"
}

# Error - An invalid format is used for game id
# USAGE: error_game_id_invalid $game_id
error_game_id_invalid() {
	local game_id
	game_id="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼid de jeu fourni ne correspond pas au format attendu : "%s"\n'
			message="$message"'Cette valeur ne peut utiliser que des caractères du set [-a-z0-9],'
			message="$message"' et ne peut ni débuter ni sʼachever par un tiret.\n'
		;;
		('en'|*)
			message='The provided game id is not using the expected format: "%s"\n'
			message="$message"'The value should only include characters from the set [-a-z0-9],'
			message="$message"' and can not begin nor end with an hyphen.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$game_id"
}

# Error - An invalid format is used for expansion id
# USAGE: error_expansion_id_invalid $expansion_id
error_expansion_id_invalid() {
	local expansion_id
	expansion_id="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼid dʼextension fourni ne correspond pas au format attendu : "%s"\n'
			message="$message"'Cette valeur ne peut utiliser que des caractères du set [-a-z0-9],'
			message="$message"' et ne peut ni débuter ni sʼachever par un tiret.\n'
		;;
		('en'|*)
			message='The provided expansion id is not using the expected format: "%s"\n'
			message="$message"'The value should only include characters from the set [-a-z0-9],'
			message="$message"' and can not begin nor end with an hyphen.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$expansion_id"
}

# List the requirements for the current game script
# USAGE: requirements_list
# RETURN: a list for required commands, one per line
requirements_list() {
	local requirements_list
	requirements_list=$(
		# List explicit requirements
		if ! variable_is_empty 'SCRIPT_DEPS'; then
			printf '%s\n' $SCRIPT_DEPS
		fi

		# List requirements for the current archive integrity setting
		requirements_list_checksum

		# List requirements for the current output package format setting
		requirements_list_package

		# List requirements for the current icons setting
		requirements_list_icons

		# List requirements for the current archive
		requirements_list_archive
	)

	printf '%s' "$requirements_list" | list_clean
}

# List requirements for the current archive integrity setting
# USAGE: requirements_list_checksum
# RETURN: a list for required commands, one per line
requirements_list_checksum() {
	local option_checksum requirements
	option_checksum=$(option_value 'checksum')
	case "$option_checksum" in
		('md5')
			requirements='md5sum'
		;;
	esac

	if ! variable_is_empty 'requirements'; then
		printf '%s\n' $requirements
	fi
}

# List requirements for the current output package format setting
# USAGE: requirements_list_package
# RETURN: a list for required commands, one per line
requirements_list_package() {
	local option_package requirements
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			# bsdtar and gzip are required for .MTREE
			requirements='bsdtar gzip'
		;;
		('deb')
			requirements='dpkg-deb'
		;;
		('gentoo')
			# fakeroot-ng doesn't work anymore, fakeroot >=1.25.1 does
			requirements='fakeroot ebuild'
		;;
	esac

	if ! variable_is_empty 'requirements'; then
		printf '%s\n' $requirements
	fi
}

# List requirements for the current icons setting
# USAGE: requirements_list_icons
# RETURN: a list for required commands, one per line
requirements_list_icons() {
	# Return early if icons inclusion is disabled
	local option_icons
	option_icons=$(option_value 'icons')
	if [ "$option_icons" -eq 0 ]; then
		return 0
	fi

	# Get list of icons
	local icons_list
	icons_list=$(icons_list_all)
	# Return early if there is no icon for the current game script
	if [ -z "$icons_list" ]; then
		return 0
	fi

	# Print requirements for each icon.
	local icon icon_path
	for icon in $icons_list; do
		icon_path=$(icon_path "$icon" 2>/dev/null || true)
		case "$icon_path" in
			(*'.png')
				printf '%s\n' 'identify'
			;;
			(*'.bmp'|*'.ico')
				printf '%s\n' 'identify' 'convert'
			;;
			(*'.exe')
				printf '%s\n' 'identify' 'convert' 'wrestool'
			;;
		esac
	done
}

# List requirements for the current archive
# USAGE: requirements_list_archive
# RETURN: a list for required commands, one per line
requirements_list_archive() {
	local archive
	archive=$(current_archive)

	{
		requirements_list_archive_single "$archive"
		local archive_part part_index
		for part_index in $(seq 1 9); do
			archive_part="${archive}_PART${part_index}"
			# Stop looking at the first unset archive extra part.
			if variable_is_empty "$archive_part"; then
				break
			fi
			requirements_list_archive_single "$archive_part"
		done
	} | list_clean
}

# List requirements for the given archive
# USAGE: requirements_list_archive_single $archive
# RETURN: a list for required commands, one per line
requirements_list_archive_single() {
	local archive
	archive="$1"

	local archive_extractor
	archive_extractor=$(archive_extractor "$archive")
	if [ -n "$archive_extractor" ]; then
		printf '%s\n' "$archive_extractor"
		return 0
	fi

	local archive_type requirements
	archive_type=$(archive_type "$archive")
	case "$archive_type" in
		('7z')
			requirements='7zr'
		;;
		('cabinet')
			requirements='cabextract'
		;;
		('debian')
			requirements='dpkg-deb'
		;;
		('innosetup')
			requirements='innoextract'
		;;
		('installshield')
			requirements='unshield'
		;;
		('iso')
			requirements='bsdtar'
		;;
		('lha')
			requirements='lha'
		;;
		('makeself')
			requirements=$(archive_requirements_makeself_list)
		;;
		('mojosetup')
			requirements=$(archive_requirements_mojosetup_list)
		;;
		('msi')
			requirements='msiextract'
		;;
		('nullsoft-installer')
			requirements='unar'
		;;
		('rar')
			requirements='unar'
		;;
		('tar')
			requirements='tar'
		;;
		('tar.bz2')
			requirements='tar bunzip2'
		;;
		('tar.gz')
			requirements='tar gunzip'
		;;
		('tar.xz')
			requirements='tar unxz'
		;;
		('zip')
			requirements='unzip'
		;;
	esac
	if ! variable_is_empty 'requirements'; then
		printf '%s\n' $requirements
	fi
}

# Check the presence of the current game script requirements
# The requirements specific to the current archive are omitted,
# they are handled by another function: archive_dependencies_check.
# USAGE: check_deps
# RETURN: 0 if all required dependencies are available,
#         1 if a dependency is missing
check_deps() {
	local requirements_list_checksum requirements_list_package
	requirements_list_checksum=$(requirements_list_checksum)
	requirements_list_package=$(requirements_list_package)
	SCRIPT_DEPS="${SCRIPT_DEPS:-}
	$requirements_list_checksum
	$requirements_list_package"

	local requirement
	for requirement in $SCRIPT_DEPS; do
		if ! command -v "$requirement" >/dev/null 2>&1; then
			error_dependency_not_found "$requirement"
			return 1
		fi
	done

	# Debian - Check the available version of dpkg-deb
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('deb')
			## Explicitly return an error status if the version check failed.
			if ! requirements_check_dpkg_deb_version; then
				return 1
			fi
		;;
	esac

	# Check for the dependencies required to extract the icons
	local option_icons
	option_icons=$(option_value 'icons')
	if [ "$option_icons" -eq 1 ]; then
		requirements_check_icons
	fi
}

# Check the available version of dpkg-deb
# USAGE: requirements_check_dpkg_deb_version
# RETURN: 0 if dpkg-deb is recent enough,
#         1 if dpkg-deb is too old.
requirements_check_dpkg_deb_version() {
	local version_available version_required
	version_available=$(
		LANG=C dpkg-deb --version | \
		sed --silent "s/Debian 'dpkg-deb' package archive backend version \\([\\.0-9]\\+\\) (amd64)\\./\\1/p"
	)
	## The required option --root-owner-group is only available with dpkg-deb ≥ 1.19.0.
	version_required='1.19.0'

	## If dpkg-deb is available, we can assume dpkg is available too.
	if ! dpkg --compare-versions "$version_available" '>=' "$version_required"; then
		error_requirement_too_old 'dpkg-deb' "$version_available" "$version_required"
		return 1
	fi
}

# Check the presence of the required commands for icons extraction
# USAGE: requirements_check_icons
requirements_check_icons() {
	# Return early if icons inclusion has been disabled.
	local option_icons
	option_icons=$(option_value 'icons')
	if [ "$option_icons" -eq 0 ]; then
		return 0
	fi

	local icons_requirements requirement
	icons_requirements=$(requirements_list_icons)
	for requirement in $icons_requirements; do
		if ! command -v "$requirement" >/dev/null 2>&1; then
			error_dependency_not_found "$requirement"
			return 1
		fi
	done
}

# output what a command is provided by
# USAGE: dependency_provided_by $command
# CALLED BY: error_dependency_not_found
dependency_provided_by() {
	local command provider
	command="$1"
	case "$command" in
		('7zr')
			provider='p7zip'
		;;
		('bsdtar')
			provider='libarchive'
		;;
		('convert'|'identify')
			provider='imagemagick'
		;;
		('lha')
			provider='lhasa'
		;;
		('icotool'|'wrestool')
			provider='icoutils'
		;;
		('dpkg-deb')
			provider='dpkg'
		;;
		(*)
			provider="$command"
		;;
	esac
	printf '%s' "$provider"
	return 0
}

# Error - A required dependency is missing
# USAGE: error_dependency_not_found $command_name
error_dependency_not_found() {
	local command_name provider_package
	command_name="$1"
	provider_package=$(dependency_provided_by "$command_name")

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='%s est introuvable. Installez %s avant de lancer ce script.\n'
		;;
		('en'|*)
			message='%s not found. Install %s before running this script.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$command_name" \
		"$provider_package"
}

# Error - A required dependency is available, but in a build that is too old
# USAGE: error_requirement_too_old $command_name $version_available $version_required
error_requirement_too_old() {
	local command_name version_available version_required
	command_name="$1"
	version_available="$2"
	version_required="$3"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='%s est disponible dans la version %s, mais la version %s ou plus récente est requise.\n'
		;;
		('en'|*)
			message='%s is available in version %s, but version %s or newer is required.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$command_name" \
		"$version_available" \
		"$version_required"
}

# List the application identifiers for the current game or expansion
# USAGE: applications_list
# RETURN: a list of application identifiers, one per line,
#         or an empty string if there is no application (common case for expansions)
applications_list() {
	# Fetch the explicit list if it is set
	local applications_list
	applications_list=$(context_value 'APPLICATIONS_LIST')

	# Parse the environment to compute an applications list from it
	if [ -z "$applications_list" ]; then
		local sed_expression
		# The following expression matches:
		# - APP_xxx_EXE
		# - APP_xxx_SCUMMID
		# - APP_xxx_TYPE
		# and the suffixed variants of these variables.
		sed_expression='s/^\(APP_[0-9A-Z]\+\)_\(EXE\|SCUMMID\|TYPE\)\(_[0-9A-Z]\+\)*=.*/\1/p'
		applications_list=$(set | sed --silent --expression="$sed_expression")
	fi

	# Fall back on the default applications list for the current game engine
	if [ -z "$applications_list" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unity3d')
				# Unity3D games are expected to provide a single application
				applications_list='APP_MAIN'
			;;
			('visionaire')
				applications_list=$(visionaire_applications_list)
			;;
		esac
	fi

	printf '%s\n' $applications_list | list_clean
}

# Print the type of prefix to use for the given application.
# If no type is explicitely set from the game script, it defaults to "symlinks".
# The supported prefix types are:
# - "symlinks", the default, generate our usual symbolic links farm
# - "none", no prefix is generated, the game is run from the read-only system directory
# USAGE: application_prefix_type $application
# RETURN: the prefix type keyword, from the supported values
application_prefix_type() {
	# Prefix types:
	# - "symlinks", the default, generate our usual symbolic links farm
	# - "none", no prefix is generated, the game is run from the read-only system directory

	local application
	application="$1"

	# Set the prefix type for the current application.
	local prefix_type
	prefix_type=$(context_value "${application}_PREFIX_TYPE")

	# Fall back on the default prefix type for the current game.
	if [ -z "$prefix_type" ]; then
		prefix_type=$(context_value 'APPLICATIONS_PREFIX_TYPE')
	fi

	# Fall back on the default prefix type for the current application type.
	if [ -z "$prefix_type" ]; then
		local application_type
		application_type=$(application_type "$application")
		case "$application_type" in
			('renpy')
				prefix_type='none'
			;;
			('scummvm')
				prefix_type='none'
			;;
			(*)
				prefix_type='symlinks'
			;;
		esac
	fi

	# Check that a supported prefix type has been set.
	case "$prefix_type" in
		('symlinks'|'none')
			## This is a supported type, no error to throw.
		;;
		(*)
			error_unknown_prefix_type "$prefix_type"
			return 1
		;;
	esac

	printf '%s' "$prefix_type"
}

# print the id of the given application
# USAGE: application_id $application
# RETURN: the application id, limited to the characters set [-_0-9a-z]
#         the id can not start nor end with a character from the set [-_]
application_id() {
	local application
	application="$1"

	# Get the application type from its identifier
	# Fall back on the game id if no value is set
	local application_id
	application_id=$(context_value "${application}_ID")
	if [ -z "$application_id" ]; then
		application_id=$(game_id)
	fi

	# Check that the id fits the format restrictions
	if ! printf '%s' "$application_id" | \
		grep --quiet --regexp='^[0-9a-z][-_0-9a-z]\+[0-9a-z]$'
	then
		error_application_id_invalid "$application" "$application_id"
		return 1
	fi

	printf '%s' "$application_id"
}

# Print the name of the binary targeted by the given application
# USAGE: application_exe $application
# RETURN: the binary file name,
#         or an empty string is none is set
application_exe() {
	local application
	application="$1"

	local application_exe
	application_exe=$(context_value "${application}_EXE")

	# If no binary is explicitly set, fall back on the default value for the current game engine.
	if [ -z "$application_exe" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unity3d')
				application_exe=$(unity3d_application_exe_default "$application")
			;;
			('visionaire')
				application_exe=$(visionaire_application_exe)
			;;
		esac
	fi

	printf '%s' "$application_exe"
}

# Print the path to the application binary, with single quotes escaped,
# for inclusion in a single quote delimited variable declaration.
# USAGE: application_exe_escaped $application
application_exe_escaped() {
	local application
	application="$1"

	local application_exe
	application_exe=$(application_exe "$application")

	# If the file name includes single quotes, replace each one with: '\''
	printf '%s' "$application_exe" | sed "s/'/'\\\''/g"
}

# Print the full path to the application binary.
# USAGE: application_exe_path $application_exe
# RETURN: the full path to the application binary,
#         or an empty string if it could not be found.
application_exe_path() {
	local application_exe
	application_exe="$1"

	# Look for the application binary in the temporary path for archive content.
	## Do not throw an error if CONTENT_PATH_DEFAULT is not set,
	## but skip searching for the application binary in the archive content.
	local content_path
	content_path=$(content_path_default) 2>/dev/null || true
	if [ -n "$content_path" ]; then
		local application_exe_path
		application_exe_path="${PLAYIT_WORKDIR}/gamedata/${content_path}/${application_exe}"
		if [ -f "$application_exe_path" ]; then
			printf '%s' "$application_exe_path"
			return 0
		fi
	fi

	# Look for the application binary in the current package.
	local package package_path path_game_data application_exe_path
	package=$(current_package)
	package_path=$(package_path "$package")
	path_game_data=$(path_game_data)
	application_exe_path="${package_path}${path_game_data}/${application_exe}"
	if [ -f "$application_exe_path" ]; then
		printf '%s' "$application_exe_path"
		return 0
	fi

	# Look for the application binary in all packages.
	local packages_list
	packages_list=$(packages_list)
	for package in $packages_list; do
		package_path=$(package_path "$package")
		application_exe_path="${package_path}${path_game_data}/${application_exe}"
		if [ -f "$application_exe_path" ]; then
			printf '%s' "$application_exe_path"
			return 0
		fi
	done
}

# print the name of the given application, for display in menus
# USAGE: application_name $application
# RETURN: the pretty version of the application name
application_name() {
	local application
	application="$1"

	# Get the application name from its identifier
	# Fall back on the game name if no value is set
	local application_name
	application_name=$(context_value "${application}_NAME")
	if [ -z "$application_name" ]; then
		application_name=$(game_name)
	fi

	printf '%s' "$application_name"
}

# print the category of the given application, for sorting in menus with categories support
# USAGE: application_category $application
# RETURN: the application XDG menu category
application_category() {
	local application
	application="$1"

	# Get the application category from its identifier
	local application_category
	application_category=$(get_value "${application}_CAT")
	## If no category is explicitely set, fall back on "Game"
	if [ -z "$application_category" ]; then
		application_category='Game'
	fi

	# TODO - We could check that the category is part of the 1.0 XDG spec:
	# https://specifications.freedesktop.org/menu-spec/menu-spec-1.0.html#category-registry

	printf '%s' "$application_category"
}

# Print the pre-run actions for the given application.
# USAGE: application_prerun $application
# RETURN: the pre-run actions, can span over multiple lines,
#         or an empty string if there are none
application_prerun() {
	local application
	application="$1"

	local application_prerun
	application_prerun=$(context_value "${application}_PRERUN")

	# Run engine specific actions
	local game_engine
	game_engine=$(game_engine)
	case "$game_engine" in
		('unity3d')
			## Use a dedicated log file for the current game session.
			application_prerun="$application_prerun
			mkdir --parents logs"
		;;
	esac

	# If LD_PRELOAD hacks are provided in the current package, include them in the pre-run actions.
	local package hacks_list
	package=$(current_package)
	hacks_list=$(hacks_included_in_package "$package")
	if [ -n "$hacks_list" ]; then
		local hack hack_prerun
		for hack in $hacks_list; do
			hack_prerun=$(hack_application_prerun "$hack")
			application_prerun="$application_prerun
$hack_prerun"
		done
	fi

	# Ensure the pre-run actions string always end with a line break.
	printf '%s\n' "$application_prerun"
}

# Print the post-run actions for the given application.
# USAGE: application_postrun $application
# RETURN: the post-run actions, can span over multiple lines,
#         or an empty string if there are none
application_postrun() {
	local application
	application="$1"

	local application_postrun
	application_postrun=$(context_value "${application}_POSTRUN")

	# Ensure the post-run actions string always end with a line break.
	printf '%s\n' "$application_postrun"
}

# print the options string for the given application
# USAGE: application_options $application
# RETURN: the options string on a single line,
#         or an empty string if no options are set
application_options() {
	# Get the application options string from its identifier
	local application application_options
	application="$1"
	application_options=$(context_value "${application}_OPTIONS")

	# Add engine specific options
	local game_engine
	game_engine=$(game_engine)
	case "$game_engine" in
		('unity3d')
			## Use a dedicated log file for the current game session.
			## The quotes are not truly required, but they make ShellCheck happy.
			application_options="$application_options -logFile \"./logs/\$(date +%F-%R).log\""
		;;
	esac

	# Check that the options string does not span multiple lines
	local line_breaks_number
	line_breaks_number=$(printf '%s' "$application_options" | wc --lines)
	if [ "$line_breaks_number" -gt 0 ]; then
		error_variable_multiline "${application}_OPTIONS"
		return 1
	fi

	printf '%s' "$application_options"
}

# Print the type of the given application
# USAGE: application_type $application
# RETURN: the application type keyword, from the supported values:
#         - dosbox
#         - java
#         - mono
#         - native
#         - renpy
#         - scummvm
#         - wine
#         or an empty string if the type is not set and could not be guessed
application_type() {
	local application
	application="$1"

	local application_type
	application_type=$(context_value "${application}_TYPE")

	# If no type has been explicitely set, try to guess one
	## Try to detect ScummVM applications
	if [ -z "$application_type" ]; then
		local application_scummid
		application_scummid=$(application_scummvm_scummid "$application")
		if [ -n "$application_scummid" ]; then
			application_type='scummvm'
		fi
	fi
	## Try to detect the application type based of the binary MIME type
	if [ -z "$application_type" ]; then
		if [ -n "${PLAYIT_WORKDIR:-}" ]; then
			application_type=$(application_type_guess_from_file "$application")
		fi
	fi

	# Return early if no type has been found
	if [ -z "$application_type" ]; then
		return 0
	fi

	# Check that a supported type has been fetched
	case "$application_type" in
		( \
			'custom' | \
			'dosbox' | \
			'java' | \
			'mono' | \
			'native' | \
			'renpy' | \
			'scummvm' | \
			'wine' \
		)
			## This is a supported type, no error to throw.
		;;
		(*)
			error_unknown_application_type "$application_type"
			return 1
		;;
	esac

	printf '%s' "$application_type"
}

# Try to find the application type from the MIME type of its binary file
# USAGE: application_type_guess_from_file $application
# RETURN: the guessed application type,
#         or an empty string if none could be guessed
application_type_guess_from_file() {
	local application
	application="$1"

	# Get the path to the application binary.
	local application_exe application_exe_path
	application_exe=$(application_exe "$application")

	# If no binary is found for the current package, try to find one for any of the packages.
	if [ -z "$application_exe" ]; then
		local package packages_list
		packages_list=$(packages_list)
		for package in $packages_list; do
			application_exe=$(
				set_current_package "$package"
				application_exe "$application"
			)
			if [ -n "$application_exe" ]; then
				break
			fi
		done
	fi

	# Return early if no binary seems to be set for the current application.
	if [ -z "$application_exe" ]; then
		return 0
	fi

	# Compute the full path to the application binary.
	application_exe_path=$(application_exe_path "$application_exe")

	# Return early if no binary file can be found for the given application.
	if [ -z "$application_exe_path" ]; then
		return 0
	fi

	local file_type application_type
	file_type=$(file_type "$application_exe_path")
	case "$file_type" in
		( \
			'application/x-executable' | \
			'application/x-pie-executable' | \
			'application/x-sharedlib' \
		)
			application_type='native'
		;;
		( \
			'application/x-dosexec' | \
			'application/vnd.microsoft.portable-executable' \
		)
			local file_type_extended
			file_type_extended=$( \
				env --ignore-environment file --brief --dereference "$application_exe_path" | \
				cut --delimiter=',' --fields=1 \
			)
			case "$file_type_extended" in
				( \
					'DOS executable (COM)' | \
					'MS-DOS executable' \
				)
					application_type='dosbox'
				;;
				(*'Mono/.Net assembly')
					application_type='mono'
				;;
				( \
					'PE32 executable'* | \
					'PE32+ executable'* \
				)
					application_type='wine'
				;;
			esac
		;;
		('application/octet-stream')
			local file_type_extended
			file_type_extended=$(env --ignore-environment file --brief --dereference "$application_exe_path")
			case "$file_type_extended" in
				('MS-DOS executable')
					application_type='dosbox'
				;;
			esac
		;;
	esac

	printf '%s' "$application_type"
}

# Error - An unknown application type is used
# USAGE: error_unknown_application_type $app_type
error_unknown_application_type() {
	local application_type
	application_type="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le type dʼapplication "%s" est inconnu.\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de gestion de bugs : %s\n'
		;;
		('en'|*)
			message='"%s" application type is unknown.\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$application_type" \
		"$PLAYIT_GAMES_BUG_TRACKER_URL"
}

# Error - No application type could be found
# USAGE: error_no_application_type $application
error_no_application_type() {
	local application
	application="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le type de lʼapplication "%s" nʼest pas défini, et nʼa pas pu être détecté automatiquement.\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de suivi des problèmes : %s\n'
		;;
		('en'|*)
			message='The type of application "%s" is not set, and could not be guessed.\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$application" \
		"$PLAYIT_GAMES_BUG_TRACKER_URL"
}

# Error - An unknown prefix type is requested
# USAGE: error_unknown_prefix_type $prefix_type
error_unknown_prefix_type() {
	local prefix_type
	prefix_type="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le type de préfixe "%s" est inconnu.\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de suivi des problèmes : %s\n'
		;;
		('en'|*)
			message='"%s" prefix type is unknown.\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$prefix_type" \
		"$PLAYIT_GAMES_BUG_TRACKER_URL"
}

# Error - An invalid format is used for the given application id
# USAGE: error_application_id_invalid $application $application_id
error_application_id_invalid() {
	local application application_id
	application="$1"
	application_id="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼid fourni pour lʼapplication %s ne correspond pas au format attendu : "%s"\n'
			message="$message"'Cette valeur ne peut utiliser que des caractères du set [-a-z0-9],'
			message="$message"' et ne peut ni débuter ni sʼachever par un tiret.\n'
		;;
		('en')
			message='The id provided for application %s is not using the expected format: "%s"\n'
			message="$message"'The value should only include characters from the set [-a-z0-9],'
			message="$message"' and can not begin nor end with an hyphen.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$application" \
		"$application_id"
}

# Error - APP_xxx_EXE is unset but has been required
# USAGE: error_application_exe_empty $application $function_name
error_application_exe_empty() {
	local application function_name
	application="$1"
	function_name="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='%s nʼest pas défini, mais cette valeur est requise par la fonction "%s".\n'
		;;
		('en')
			message='%s is not set, but is required by the "%s" function.\n'
		;;
	esac
	print_message 'error' "$message" \
		"${application}_EXE" \
		"$function_name"
}

# Error - The applications list for the current game script is empty
# USAGE: error_applications_list_empty
error_applications_list_empty() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La liste dʼapplications à prendre en charge pour ce jeu semble vide'
			message="$message"', mais un traitement de cette liste a été demandé.\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de gestion de bugs : %s\n'
		;;
		('en'|*)
			message='The applications list for the current game seems to be empty'
			message="$message"', but some action on this list has been requested.\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$PLAYIT_BUG_TRACKER_URL"
}

# Get the path to search for archives
# USAGE: archives_path_base
# RETURN: a string representing a path
archives_path_base() {
	# Try to get a path from the $PLAYIT_ARCHIVES_PATH_BASE environment variable,
	# if it is not set $PWD is used as a fallback.
	printf '%s' "${PLAYIT_ARCHIVES_PATH_BASE:-$PWD}"
}

# Set up the base archive
# USAGE: archive_initialize_base
# RETURN: 0 if the archive is found,
#         1 if it is missing
archive_initialize_base() {
	local archive_identifier
	archive_identifier='SOURCE_ARCHIVE'

	# A file path might have already been set, if one has been given on the command line.
	# If this is the case, we only want to use an archive that uses the same name.
	## The output redirection must be done inside the subshell, or bash --posix will ignore it.
	archive_name_expected=$(archive_name "$archive_identifier" 2>/dev/null) || true

	local archives_list archive_candidate
	archives_list=$(archives_return_list)
	for archive_candidate in $archives_list; do
		archive_path=$(archive_path "$archive_candidate")
		if [ -f "$archive_path" ]; then
			local archive_name
			archive_name=$(archive_name "$archive_candidate")

			# Skip candidate archives that do not have the expected name.
			if \
				[ -n "$archive_name_expected" ] && \
				[ "$archive_name" != "$archive_name_expected" ]
			then
				continue
			fi

			export "${archive_identifier}_NAME=${archive_name}"
			export "${archive_identifier}_PATH=${archive_path}"
			## Cache the path to the candidate archive, to prevent the need to re-compute it later.
			export "${archive_candidate}_PATH=${archive_path}"
			## Old game scripts relying on `archive_extraction 'SOURCE_ARCHIVE'` instead of `archive_extraction_default` expect either SOURCE_ARCHIVE_TYPE or SOURCE_ARCHIVE_EXTRACTOR to be set.
			local archive_extractor archive_extractor_options archive_type
			archive_extractor=$(archive_extractor "$archive_candidate")
			archive_extractor_options=$(archive_extractor_options "$archive_candidate")
			archive_type=$(archive_type "$archive_candidate")
			export "${archive_identifier}_EXTRACTOR=${archive_extractor}"
			export "${archive_identifier}_EXTRACTOR_OPTIONS=${archive_extractor_options}"
			export "${archive_identifier}_TYPE=${archive_type}"
			## Export the context archive
			set_current_archive "$archive_candidate"
			## Some old game scripts might expect the variable $SOURCE_ARCHIVE to be set
			export SOURCE_ARCHIVE="$archive_path"
			## Look for extra parts
			archive_initialize_extra_parts "$archive_candidate"
			## Update the list of archives that are going to be used
			archives_used_add "$archive_candidate"
			## Archives integrity can not yet be checked, because $PLAYIT_WORKDIR is not set yet,
			## and it is required to cache computed hashes. archives_integrity_check must be called later.
			return 0
		fi
	done

	# Throw an error if no archive candidate has been found
	error_archive_not_found $archives_list
	return 1
}

# Set up a required archive
# USAGE: archive_initialize_required $archive_identifier $archive_candidate[…]
# RETURN: 0 if the archive is found,
#         1 if it is missing
archive_initialize_required() {
	local archive_identifier
	archive_identifier="$1"
	shift 1

	local archive_candidate archive_path
	for archive_candidate in "$@"; do
		archive_path=$(archive_path "$archive_candidate")
		if [ -f "$archive_path" ]; then
			local archive_name
			archive_name=$(archive_name "$archive_candidate")
			export "${archive_identifier}_NAME=${archive_name}"
			export "${archive_identifier}_PATH=${archive_path}"
			## Cache the path to the candidate archive, to prevent the need to re-compute it later.
			export "${archive_candidate}_PATH=${archive_path}"
			## Export the legacy variable, its value can be expected by game scripts.
			export "${archive_identifier}=${archive_path}"
			## Set the extractor / type of the archive.
			local archive_extractor archive_extractor_options archive_type
			archive_extractor=$(archive_extractor "$archive_candidate")
			archive_extractor_options=$(archive_extractor_options "$archive_candidate")
			archive_type=$(archive_type "$archive_candidate")
			export "${archive_identifier}_EXTRACTOR=${archive_extractor}"
			export "${archive_identifier}_EXTRACTOR_OPTIONS=${archive_extractor_options}"
			export "${archive_identifier}_TYPE=${archive_type}"
			## Look for extra parts
			archive_initialize_extra_parts "$archive_candidate"
			## Update the list of archives that are going to be used
			archives_used_add "$archive_candidate"
			## Check the archives integrity
			archives_integrity_check
			return 0
		fi
	done

	# Throw an error if no archive candidate has been found
	error_archive_not_found "$@"
	return 1
}

# Set up an optional archive
# USAGE: archive_initialize_optional $archive_identifier $archive_candidate[…]
archive_initialize_optional() {
	local archive_identifier
	archive_identifier="$1"
	shift 1

	local archive_candidate archive_path
	for archive_candidate in "$@"; do
		archive_path=$(archive_path "$archive_candidate")
		if [ -f "$archive_path" ]; then
			local archive_name
			archive_name=$(archive_name "$archive_candidate")
			export "${archive_identifier}_NAME=${archive_name}"
			export "${archive_identifier}_PATH=${archive_path}"
			## Cache the path to the candidate archive, to prevent the need to re-compute it later.
			export "${archive_candidate}_PATH=${archive_path}"
			## Export the legacy variable, its value can be expected by game scripts.
			export "${archive_identifier}=${archive_path}"
			## Set the extractor / type of the archive.
			local archive_extractor archive_extractor_options archive_type
			archive_extractor=$(archive_extractor "$archive_candidate")
			archive_extractor_options=$(archive_extractor_options "$archive_candidate")
			archive_type=$(archive_type "$archive_candidate")
			export "${archive_identifier}_EXTRACTOR=${archive_extractor}"
			export "${archive_identifier}_EXTRACTOR_OPTIONS=${archive_extractor_options}"
			export "${archive_identifier}_TYPE=${archive_type}"
			## Look for extra parts
			archive_initialize_extra_parts "$archive_candidate"
			## Update the list of archives that are going to be used
			archives_used_add "$archive_candidate"
			## Check the archives integrity
			archives_integrity_check
			return 0
		fi
	done

	# No archive has been found, but this does not warrant an error
	return 0
}

# Set up a list of extra parts for a given archive
# USAGE: archive_initialize_extra_parts $archive
archive_initialize_extra_parts() {
	local archive
	archive="$1"

	local archive_part archive_part_name archive_part_path index
	for index in $(seq 1 99); do
		archive_part="${archive}_PART${index}"
		## This would fail if no archive part is expected at this index.
		## The output redirection must be done inside the subshell, or bash --posix will ignore it.
		archive_part_name=$(archive_name "$archive_part" 2>/dev/null) || true
		## Exit at the first unset archive part.
		if [ -z "$archive_part_name" ]; then
			return 0
		fi
		archive_part_path=$(archive_path "$archive_part")
		if [ -f "$archive_part_path" ]; then
			export "${archive_part}_PATH=${archive_part_path}"
			## Update the list of archives that are going to be used
			archives_used_add "$archive_part"
		else
			error_archive_not_found "$archive_part_name"
			return 1
		fi
	done
}

# Check the integrity of all archives
# USAGE: archives_integrity_check
archives_integrity_check() {
	local option_checksum
	option_checksum=$(option_value 'checksum')

	case "$option_checksum" in
		('md5')
			archives_integrity_check_md5
		;;
	esac
}

# Check the integrity of all archives, using MD5
# USAGE: archives_integrity_check_md5
archives_integrity_check_md5() {
	local archives_list archive archive_hash_expected archive_hash_computed
	archives_list=$(archives_used_list)
	for archive in $archives_list; do
		archive_hash_expected=$(archive_hash_md5 "$archive")
		## Skip archives that have no expected MD5 hash set.
		if [ -z "$archive_hash_expected" ]; then
			continue
		fi
		archive_hash_computed=$(archive_hash_md5_computed "$archive")
		if [ "$archive_hash_computed" != "$archive_hash_expected" ]; then
			local archive_path
			archive_path=$(archive_path "$archive")
			error_hashsum_mismatch "$archive_path"
			return 1
		fi
	done
}

# List all the archives that are going to be used
# USAGE: archives_used_list
# RETURN: a list of archive identifiers, one per line,
archives_used_list() {
	local archives_list
	archives_list="${PLAYIT_ARCHIVES_USED_LIST:-}"

	local archive
	for archive in $archives_list; do
		printf '%s\n' "$archive"
	done
}

# Add an archive to the list of archives that are going to be used
# USAGE: archives_used_add $archive
archives_used_add() {
	local archive
	archive="$1"

	local archives_list
	archives_list="$(archives_used_list)"

	export PLAYIT_ARCHIVES_USED_LIST="$archives_list
	$archive"
}

# Print the list of archives supported vby the current game script
# USAGE: archives_return_list
# RETURNS: the list of identifiers of the supported archives,
#          as a list of strings separated by spaces or line breaks
archives_return_list() {
	# If a list is already explicitely set, return early
	if ! variable_is_empty 'ARCHIVES_LIST'; then
		# ShellCheck false-positive
		# Possible misspelling: ARCHIVES_LIST may not be assigned. Did you mean archives_list?
		# shellcheck disable=SC2153
		printf '%s' "$ARCHIVES_LIST"
		return 0
	fi

	# Try to find archives using the ARCHIVE_BASE_xxx_[0-9]+ naming scheme
	local archives_list
	archives_list=$(set | sed --silent 's/^\(ARCHIVE_BASE\(_[0-9A-Z]\+\)*_[0-9]\+\)\(_NAME\)\?=.*/\1/p')
	if [ -n "$archives_list" ]; then
		printf '%s' "$archives_list"
		return 0
	fi

	error_no_archive_supported
	return 1
}

# Check for the presence of optional extra archives
# USAGE: archives_optional_extra_presence_check
archives_optional_extra_presence_check() {
	# Check for the presence of an optional icons pack
	local icons_pack_name
	icons_pack_name=$(context_value 'ARCHIVE_OPTIONAL_ICONS_NAME')
	if [ -n "$icons_pack_name" ]; then
		## Set contextual values, for game scripts with support for multiple games.
		ARCHIVE_OPTIONAL_ICONS_NAME=$(context_value 'ARCHIVE_OPTIONAL_ICONS_NAME')
		ARCHIVE_OPTIONAL_ICONS_MD5=$(context_value 'ARCHIVE_OPTIONAL_ICONS_MD5')
		ARCHIVE_OPTIONAL_ICONS_URL=$(context_value 'ARCHIVE_OPTIONAL_ICONS_URL')
		export ARCHIVE_OPTIONAL_ICONS_NAME ARCHIVE_OPTIONAL_ICONS_MD5 ARCHIVE_OPTIONAL_ICONS_URL
		archive_initialize_optional \
			'ARCHIVE_ICONS' \
			'ARCHIVE_OPTIONAL_ICONS'
		if ! archive_is_available 'ARCHIVE_ICONS'; then
			warning_optional_archive_missing_icons 'ARCHIVE_OPTIONAL_ICONS'
		fi
	fi
}

# Check for the presence of required extra archives
# USAGE: archives_required_extra_presence_check
# RETURN: 0 if no extra archive is required, or all required archives are found,
#         1 if a required archive is missing
archives_required_extra_presence_check() {
	# Check the presence of archives providing required native libraries
	local libraries_required library_required
	libraries_required=$(dependencies_list_native_libraries_all)
	while read -r library_required; do
		## When this function is called inside `{ … } || true`, errexit does not stop execution on errors.
		## This is the expected errexit behaviour for commands called in the left hand operand of an || operator.
		case "$library_required" in
			('libcurl.so.4+CURL_OPENSSL_3')
				archive_required_extra_presence_check_libcurl3 || return 1
			;;
			('libFLAC.so.8')
				archive_required_extra_presence_check_libflac8 || return 1
			;;
			('libgconf-2.so.4')
				archive_required_extra_presence_check_libgconf2 || return 1
			;;
			('libidn.so.11')
				archive_required_extra_presence_check_libidn11 || return 1
			;;
			('libpng12.so.0')
				archive_required_extra_presence_check_libpng12 || return 1
			;;
			('libssl.so.1.0.0')
				archive_required_extra_presence_check_libssl100 || return 1
			;;
			('libssl.so.1.1')
				archive_required_extra_presence_check_libssl11 || return 1
			;;
		esac
	done <<- EOL
	$(printf '%s' "$libraries_required")
	EOL

	# Check the presence of archives required to apply tweaks on the WINE prefix
	local wineprefix_tweaks wineprefix_tweak
	wineprefix_tweaks=$(wine_wineprefix_tweaks)
	while read -r wineprefix_tweak; do
		## When this function is called inside `{ … } || true`, errexit does not stop execution on errors.
		## This is the expected errexit behaviour for commands called in the left hand operand of an || operator.
		case "$wineprefix_tweak" in
			('mono')
				archive_required_extra_presence_check_mono || return 1
			;;
		esac
	done <<- EOL
	$(printf '%s' "$wineprefix_tweaks")
	EOL
}

# Check for the presence of the extra archive providing libcurl.so.3 and libcurl.so.4 including the CURL_OPENSSL_3 symbol
# USAGE: archive_required_extra_presence_check_libcurl3
# RETURN: 0 if the required archive is found,
#         1 if it is missing
archive_required_extra_presence_check_libcurl3() {
	ARCHIVE_REQUIRED_LIBCURL3_NAME='curl_7.52.1.tar.xz'
	ARCHIVE_REQUIRED_LIBCURL3_MD5='ae0368de97368164801618a08c70cb34'
	ARCHIVE_REQUIRED_LIBCURL3_URL='https://downloads.dotslashplay.it/resources/curl/'
	## The archive properties will be reused later, for checking its integrity.
	export ARCHIVE_REQUIRED_LIBCURL3_NAME ARCHIVE_REQUIRED_LIBCURL3_MD5 ARCHIVE_REQUIRED_LIBCURL3_URL

	archive_initialize_required \
		'ARCHIVE_LIBCURL3' \
		'ARCHIVE_REQUIRED_LIBCURL3'
}

# Check for the presence of the extra archive providing libFLAC.so.8
# USAGE: archive_required_extra_presence_check_libflac8
# RETURN: 0 if the required archive is found,
#         1 if it is missing
archive_required_extra_presence_check_libflac8() {
	# On Arch Linux, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			return 0
		;;
	esac

	ARCHIVE_REQUIRED_LIBFLAC8_NAME='libflac8.tar.xz'
	ARCHIVE_REQUIRED_LIBFLAC8_MD5='1f0d785f52474cb2232a2f8f8b561eda'
	ARCHIVE_REQUIRED_LIBFLAC8_URL='https://downloads.dotslashplay.it/resources/flac/'
	## The archive properties will be reused later, for checking its integrity.
	export ARCHIVE_REQUIRED_LIBFLAC8_NAME ARCHIVE_REQUIRED_LIBFLAC8_MD5 ARCHIVE_REQUIRED_LIBFLAC8_URL

	archive_initialize_required \
		'ARCHIVE_LIBFLAC8' \
		'ARCHIVE_REQUIRED_LIBFLAC8'
}

# Check for the presence of the extra archive providing GConf 2 library
# USAGE: archive_required_extra_presence_check_libgconf2
# RETURN: 0 if the required archive is found,
#         1 if it is missing
archive_required_extra_presence_check_libgconf2() {
	# On Arch Linux, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			return 0
		;;
	esac

	ARCHIVE_REQUIRED_LIBGCONF2_NAME='libgconf-2-4.tar.xz'
	ARCHIVE_REQUIRED_LIBGCONF2_MD5='4ae540fd4114ee2ddd7bd841017aad3b'
	ARCHIVE_REQUIRED_LIBGCONF2_URL='https://downloads.dotslashplay.it/resources/gconf/'
	## The archive properties will be reused later, for checking its integrity.
	export ARCHIVE_REQUIRED_LIBGCONF2_NAME ARCHIVE_REQUIRED_LIBGCONF2_MD5 ARCHIVE_REQUIRED_LIBGCONF2_URL

	archive_initialize_required \
		'ARCHIVE_LIBGCONF2' \
		'ARCHIVE_REQUIRED_LIBGCONF2'
}

# Check for the presence of the extra archive providing GNU Libidn 11 library
# USAGE: archive_required_extra_presence_check_libidn11
# RETURN: 0 if the required archive is found,
#         1 if it is missing
archive_required_extra_presence_check_libidn11() {
	# On Arch Linux, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			return 0
		;;
	esac

	ARCHIVE_REQUIRED_LIBIDN11_NAME='libidn11_1.33.tar.xz'
	ARCHIVE_REQUIRED_LIBIDN11_MD5='75d95702ec6a2b327c8902cc14998926'
	ARCHIVE_REQUIRED_LIBIDN11_URL='https://downloads.dotslashplay.it/resources/libidn/'
	## The archive properties will be reused later, for checking its integrity.
	export ARCHIVE_REQUIRED_LIBIDN11_NAME ARCHIVE_REQUIRED_LIBIDN11_MD5 ARCHIVE_REQUIRED_LIBIDN11_URL

	archive_initialize_required \
		'ARCHIVE_LIBIDN11' \
		'ARCHIVE_REQUIRED_LIBIDN11'
}

# Check for the presence of the extra archive providing PNG 1.2 libraries
# USAGE: archive_required_extra_presence_check_libpng12
# RETURN: 0 if the required archive is found,
#         1 if it is missing
archive_required_extra_presence_check_libpng12() {
	# On Arch Linux and Gentoo, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'egentoo'|'gentoo')
			return 0
		;;
	esac

	ARCHIVE_REQUIRED_LIBPNG12_NAME='libpng_1.2.tar.xz'
	ARCHIVE_REQUIRED_LIBPNG12_MD5='121c92152cce69f589a6d66a1e613bdb'
	ARCHIVE_REQUIRED_LIBPNG12_URL='https://downloads.dotslashplay.it/resources/libpng/'
	## The archive properties will be reused later, for checking its integrity.
	export ARCHIVE_REQUIRED_LIBPNG12_NAME ARCHIVE_REQUIRED_LIBPNG12_MD5 ARCHIVE_REQUIRED_LIBPNG12_URL

	archive_initialize_required \
		'ARCHIVE_LIBPNG12' \
		'ARCHIVE_REQUIRED_LIBPNG12'
}

# Check for the presence of the extra archive providing OpenSSL 1.0.0 libraries
# USAGE: archive_required_extra_presence_check_libssl100
# RETURN: 0 if the required archive is found,
#         1 if it is missing
archive_required_extra_presence_check_libssl100() {
	# On Arch Linux and Gentoo, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'egentoo'|'gentoo')
			return 0
		;;
	esac

	ARCHIVE_REQUIRED_OPENSSL100_NAME='openssl_1.0.0.tar.xz'
	ARCHIVE_REQUIRED_OPENSSL100_MD5='9822e4dd8cb467dad843044c3135b5c5'
	ARCHIVE_REQUIRED_OPENSSL100_URL='https://downloads.dotslashplay.it/resources/openssl/'
	## The archive properties will be reused later, for checking its integrity.
	export ARCHIVE_REQUIRED_OPENSSL100_NAME ARCHIVE_REQUIRED_OPENSSL100_MD5 ARCHIVE_REQUIRED_OPENSSL100_URL

	archive_initialize_required \
		'ARCHIVE_OPENSSL100' \
		'ARCHIVE_REQUIRED_OPENSSL100'
}

# Check for the presence of the extra archive providing OpenSSL 1.1 libraries
# USAGE: archive_required_extra_presence_check_libssl11
# RETURN: 0 if the required archive is found,
#         1 if it is missing
archive_required_extra_presence_check_libssl11() {
	# On Arch Linux and Gentoo, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'egentoo'|'gentoo')
			return 0
		;;
	esac

	ARCHIVE_REQUIRED_OPENSSL11_NAME='openssl_1.1.1n.tar.xz'
	ARCHIVE_REQUIRED_OPENSSL11_MD5='dade7a54b213be8ac6e0bc2b571570cc'
	ARCHIVE_REQUIRED_OPENSSL11_URL='https://downloads.dotslashplay.it/resources/openssl/'
	## The archive properties will be reused later, for checking its integrity.
	export ARCHIVE_REQUIRED_OPENSSL11_NAME ARCHIVE_REQUIRED_OPENSSL11_MD5 ARCHIVE_REQUIRED_OPENSSL11_URL

	archive_initialize_required \
		'ARCHIVE_OPENSSL11' \
		'ARCHIVE_REQUIRED_OPENSSL11'
}

# Check for the presence of the extra archive providing Mono, for inclusion in WINE prefixes
# USAGE: archive_required_extra_presence_check_mono
# RETURN: 0 if the required archive is found,
#         1 if it is missing
archive_required_extra_presence_check_mono() {
	ARCHIVE_REQUIRED_MONO_NAME='wine-mono-8.0.0-x86.msi'
	ARCHIVE_REQUIRED_MONO_MD5='4fe5c683fcd9634c7f6571f252b3603c'
	ARCHIVE_REQUIRED_MONO_URL='https://dl.winehq.org/wine/wine-mono/8.0.0/'
	## The archive properties will be reused later, for checking its integrity.
	export ARCHIVE_REQUIRED_MONO_NAME ARCHIVE_REQUIRED_MONO_MD5 ARCHIVE_REQUIRED_MONO_URL

	archive_initialize_required \
		'ARCHIVE_MONO' \
		'ARCHIVE_REQUIRED_MONO'
}

# Check if the given archive is available
# USAGE: archive_is_available $archive
# RETURN: 0 if the archive is available,
#         1 if it is not
archive_is_available() {
	local archive
	archive="$1"

	local archive_path
	archive_path=$(archive_path "$archive" 2>/dev/null || true)

	test -n "$archive_path"
}

# Get the file name of a given archive
# USAGE: archive_name $archive
# RETURN: the archive name,
#         throws an error if no name can be found
archive_name() {
	local archive
	archive="$1"

	# The file name should be set using the ARCHIVE_xxx_NAME variable.
	local archive_name
	archive_name=$(get_value "${archive}_NAME")

	# Try to get a name from the archive path.
	# We can not use the archive_path function here, to prevent a loop between archive_path and archive_name.
	local archive_path
	archive_path=$(get_value "${archive}_PATH")
	if [ -n "$archive_path" ]; then
		archive_name=$(basename "$archive_path")
	fi

	# If no name is set using the dedicated varible, try to find one using the legacy ARCHIVE_xxx variable.
	if [ -z "$archive_name" ]; then
		local archive_value
		archive_value=$(get_value "$archive")
		## The value of the legacy variable could be either a path or a file name.
		archive_name=$(basename "$archive_value")
		if \
			compatibility_level_is_at_least '2.26' && \
			[ -n "$archive_name" ]
		then
			warning_deprecated_variable "$archive" "${archive}_NAME"
		fi
	fi

	# Throw an error if no name is found for the given archive.
	if [ -z "$archive_name" ]; then
		error_missing_variable "${archive}_NAME"
		return 1
	fi

	printf '%s' "$archive_name"
}

# Get the path to a given archive
# WARNING: No check is done that this path actually exists.
# USAGE: archive_path $archive
# RETURN: the absolute path to the archive,
#         or an empty path if the given archive is not available
archive_path() {
	local archive
	archive="$1"

	# If the path to the archive has already been computed, it has been cached in the ARCHIVE_xxx_PATH variable.
	local archive_path
	archive_path=$(get_value "${archive}_PATH")

	# If no path could be found, try to get one using the legacy ARCHIVE_xxx variable.
	if [ -z "$archive_path" ]; then
		local archive_value
		archive_value=$(get_value "$archive")
		## If the value includes a "/", we assume it is a path. Otherwise, it is probably a file name.
		if printf '%s' "$archive_value" | grep --fixed-strings --quiet --regexp='/'; then
			archive_path="$archive_value"
			if \
				compatibility_level_is_at_least '2.26' && \
				[ -n "$archive_path" ]
			then
				warning_deprecated_variable "$archive" "${archive}_PATH"
			fi
		fi
	fi

	# Compute the path from the archive name
	if [ -z "$archive_path" ]; then
		local archives_path_base archive_name
		archives_path_base=$(archives_path_base)
		archive_name=$(archive_name "$archive" 2>/dev/null || true)
		## Do not try to compute a path if the archive does not seem to be available.
		## We can only reach this situation if errexit has been disabled due to calling `archive_path (…) || true`.
		if [ -z "$archive_name" ]; then
			return 0
		fi
		archive_path="${archives_path_base}/${archive_name}"
	fi

	# An absolute path should always be used, to allow using the archive path even after a directory change.
	archive_path=$(realpath --canonicalize-missing --no-symlinks "$archive_path")

	printf '%s' "$archive_path"
}

# Get the size of the archive contents
# This is not the size of the archive file itself, but the total size of the files it includes, without compression.
# USAGE: archive_size $archive
# RETURN: the archive contents size, as a number of KiB
archive_size() {
	local archive
	archive="$1"

	local archive_size
	archive_size=$(get_value "${archive}_SIZE")

	# If no size is set, assuming a size of 0
	if [ -z "$archive_size" ]; then
		archive_size=0
	fi

	printf '%s' "$archive_size"
}

# Get the expected MD5 hash of a given archive
# USAGE: archive_hash_md5 $archive
# RETURN: the expected MD5 hash,
#         or an empty string if none is set
archive_hash_md5() {
	local archive
	archive="$1"

	get_value "${archive}_MD5"
}

# Get the computed MD5 hash of a given archive
# USAGE: archive_hash_md5_computed $archive
# RETURN: the computed MD5 hash
archive_hash_md5_computed() {
	local archive
	archive="$1"

	# If the hash has already been computed, use the cached value.
	## We can not use a global variable here, because the current function can be called from a subshell.
	local cache_directory cache_file_hashes archive_hash_computed
	cache_directory="${PLAYIT_WORKDIR}/cache"
	cache_file_hashes="${cache_directory}/hashes"
	if [ -f "$cache_file_hashes" ]; then
		archive_hash_computed=$(sed --silent "s/^${archive} | \([0-9a-f]\{32\}\)$/\1/p" "$cache_file_hashes")
	fi

	# If no cached value has been found, compute the hash.
	if [ -z "${archive_hash_computed:-}" ]; then
		local archive_name archive_path
		archive_name=$(archive_name "$archive")
		archive_path=$(archive_path "$archive")
		## Print the message to the error output, to prevent messing up with the current function output.
		info_archive_hash_computation "$archive_name" >/dev/stderr
		archive_hash_computed=$(md5sum "$archive_path" | awk '{print $1}')

		# Cache the hash to prevent computing it again.
		## We can not use a global variable here, because the current function can be called from a subshell.
		mkdir --parents "$cache_directory"
		cat >> "$cache_file_hashes" <<- EOF
		$archive | $archive_hash_computed
		EOF
	fi

	printf '%s' "$archive_hash_computed"
}

# Get the type of a given archive
# USAGE: archive_type $archive
# RETURNS: an archive type,
#          or an empty string if no type is set and none can be guessed
archive_type() {
	local archive
	archive="$1"

	local archive_type
	archive_type=$(get_value "${archive}_TYPE")

	# Guess the archive type from its file name
	if [ -z "$archive_type" ]; then
		local archive_name
		archive_name=$(archive_name "$archive")
		archive_type=$(archive_guess_type_from_name "$archive_name")
	fi

	# Guess the archive type from its headers
	if [ -z "$archive_type" ]; then
		local archive_path
		archive_path=$(archive_path "$archive")
		archive_type=$(archive_guess_type_from_headers "$archive_path")
	fi

	# Fall back on using the type of the parent archive, if there is one
	if \
		[ -z "$archive_type" ] && \
		printf '%s' "$archive" | \
		grep --quiet --word-regexp '^ARCHIVE_.*_PART[0-9]\+$'
	then
		local parent_archive
		parent_archive=$(printf '%s' "$archive" | sed 's/^\(ARCHIVE_.*\)_PART[0-9]\+$/\1/')
		archive_type=$(archive_type "$parent_archive")
	fi

	printf '%s' "$archive_type"
}

# Guess the archive type from its file name
# USAGE: archive_guess_type_from_name $archive_file
# RETURNS: the archive type,
#          or an empty string is none could be guessed
archive_guess_type_from_name() {
	local archive_file
	archive_file="$1"

	local archive_type
	case "$archive_file" in
		(*'.cab')
			archive_type='cabinet'
		;;
		(*'.deb')
			archive_type='debian'
		;;
		('setup_'*'.exe'|'patch_'*'.exe')
			archive_type='innosetup'
		;;
		(*'.iso')
			archive_type='iso'
		;;
		(*'.msi')
			archive_type='msi'
		;;
		(*'.rar')
			archive_type='rar'
		;;
		(*'.tar')
			archive_type='tar'
		;;
		(*'.tar.bz2'|*'.tbz2')
			archive_type='tar.bz2'
		;;
		(*'.tar.gz'|*'.tgz')
			archive_type='tar.gz'
		;;
		(*'.tar.xz'|*'.txz')
			archive_type='tar.xz'
		;;
		(*'.zip')
			archive_type='zip'
		;;
		(*'.7z')
			archive_type='7z'
		;;
		(*)
			# No type could be guessed from the archive file name
			archive_type=''
		;;
	esac

	printf '%s' "$archive_type"
}

# Guess the archive type from its headers
# USAGE: archive_guess_type_from_headers $archive_path
# RETURNS: the archive type,
#          or an empty string is none could be guessed
archive_guess_type_from_headers() {
	local archive_path
	archive_path="$1"

	local archive_type
	if head --lines=20 "$archive_path" | grep --quiet 'Makeself'; then
		if head --lines=50 "$archive_path" | grep --quiet 'script="./startmojo.sh"'; then
			archive_type='mojosetup'
		else
			archive_type='makeself'
		fi
	fi

	printf '%s' "${archive_type:-}"
}

# get the extractor for the given archive
# USAGE: archive_extractor $archive_identifier
# RETURNS: the specific extractor to use for the given archive (as a single word string),
#          or an empty string if none has been explicitely set.
archive_extractor() {
	local archive_identifier
	archive_identifier="$1"
	assert_not_empty 'archive_identifier' 'archive_extractor'

	# Return archive extractor early if it is already set
	local archive_extractor
	archive_extractor=$(get_value "${archive_identifier}_EXTRACTOR")
	if [ -n "$archive_extractor" ]; then
		printf '%s' "$archive_extractor"
		return 0
	fi

	# Fall back on using the extractor of the parent archive, if there is one
	if \
		printf '%s' "$archive_identifier" | \
		grep --quiet --word-regexp '^ARCHIVE_.*_PART[0-9]\+$'
	then
		local parent_archive
		parent_archive=$(printf '%s' "$archive_identifier" | sed 's/^\(ARCHIVE_.*\)_PART[0-9]\+$/\1/')
		archive_extractor=$(archive_extractor "$parent_archive")
		if [ -n "$archive_extractor" ]; then
			printf '%s' "$archive_extractor"
			return 0
		fi
	fi

	# No failure if no extractor could be found
	return 0
}

# get the extractor options string for the given archive
# USAGE: archive_extractor_options $archive
# RETURNS: the options string to pass to the specific extractor to use for the given archive,
#          or an empty string if no options string has been explicitely set.
archive_extractor_options() {
	local archive
	archive="$1"

	get_value "${archive}_EXTRACTOR_OPTIONS"
}

# Check that the tools required to extract the content of a given archive are available,
# including the archive extra parts.
# USAGE: archive_dependencies_check $archive
archive_dependencies_check() {
	local archive
	archive="$1"
	assert_not_empty 'archive' 'archive_dependencies_check'

	# Check extraction dependencies for main archive.
	## When this function is called inside `{ … } || true`, errexit does not stop execution on errors. This is the expected errexit behaviour for commands called in the left hand operand of an || operator.
	archive_dependencies_check_single "$archive" || return 1

	# Check extraction dependencies for archive extra parts.
	local archive_part
	for i in $(seq 1 9); do
		archive_part="${archive}_PART${i}"
		# Stop looking at the first unset archive extra part.
		if variable_is_empty "$archive_part"; then
			break
		fi
		archive_dependencies_check_single "$archive_part"
	done
}

# Check that the tools required to extract the content of a given single archive are available.
# USAGE: archive_dependencies_check_single $archive
archive_dependencies_check_single() {
	local archive
	archive="$1"
	assert_not_empty 'archive' 'archive_dependencies_check_single'

	local archive_extractor
	archive_extractor=$(archive_extractor "$archive")
	if [ -n "$archive_extractor" ]; then
		## When this function is called inside `{ … } || true`, errexit does not stop execution on errors. This is the expected errexit behaviour for commands called in the left hand operand of an || operator.
		archive_dependencies_check_using_extractor "$archive" || return 1
	else
		## When this function is called inside `{ … } || true`, errexit does not stop execution on errors. This is the expected errexit behaviour for commands called in the left hand operand of an || operator.
		archive_dependencies_check_from_type "$archive" || return 1
	fi
}

# Check that the tools required to extract the content of a given archive are available.
#
# Check the presence of the specific tools required to provide the given archive extractor.
#
# USAGE: archive_dependencies_check_using_extractor $archive
archive_dependencies_check_using_extractor() {
	local archive
	archive="$1"
	assert_not_empty 'archive' 'archive_dependencies_check'

	local archive_extractor
	archive_extractor=$(archive_extractor "$archive")
	case "$archive_extractor" in
		( \
			'7za' | \
			'7zr' | \
			'bsdtar' | \
			'cabextract' | \
			'dpkg-deb' | \
			'innoextract' | \
			'lha' | \
			'msiextract' | \
			'tar' | \
			'unar' | \
			'unshield' | \
			'unzip' \
		)
			# Supported extractor, no error to throw.
		;;
		(*)
			error_archive_extractor_invalid "$archive_extractor"
			return 1
		;;
	esac

	if ! command -v "$archive_extractor" >/dev/null 2>&1; then
		error_dependency_not_found "$archive_extractor"
		return 1
	fi
}

# Check that the tools required to extract the content of a given archive are available.
#
# Check the presence of any tool supporting the given archive type.
#
# USAGE: archive_dependencies_check_from_type $archive
archive_dependencies_check_from_type() {
	local archive
	archive="$1"
	assert_not_empty 'archive' 'archive_dependencies_check'

	local archive_type
	archive_type=$(archive_type "$archive")
	case "$archive_type" in
		('7z')
			archive_dependencies_check_type_7z
		;;
		('cabinet')
			archive_dependencies_check_type_cabinet
		;;
		('debian')
			archive_dependencies_check_type_debian
		;;
		('innosetup')
			archive_dependencies_check_type_innosetup
		;;
		('innosetup_nolowercase')
			archive_dependencies_check_type_innosetup
		;;
		('installshield')
			archive_dependencies_check_type_installshield
		;;
		('iso')
			archive_dependencies_check_type_iso
		;;
		('lha')
			archive_dependencies_check_type_lha
		;;
		('makeself')
			archive_requirements_makeself_check
		;;
		('mojosetup')
			archive_requirements_mojosetup_check
		;;
		('msi')
			archive_dependencies_check_type_msi
		;;
		('nullsoft-installer')
			archive_dependencies_check_type_nullsoft
		;;
		('rar')
			archive_dependencies_check_type_rar
		;;
		('tar')
			archive_dependencies_check_type_tar
		;;
		('tar.bz2')
			archive_dependencies_check_type_tarbz2
		;;
		('tar.gz')
			archive_dependencies_check_type_targz
		;;
		('tar.xz')
			archive_dependencies_check_type_tarxz
		;;
		('zip')
			archive_dependencies_check_type_zip
		;;
	esac
}

# Extract data from the current archive
# USAGE: archive_extraction_default
archive_extraction_default() {
	local archive
	archive=$(current_archive)
	archive_extraction "$archive"

	# Extract the contents from the extra archives providing required libraries
	archive_extraction_extra_libraries

	# Extract the contents from the extra archives providing icons
	if archive_is_available 'ARCHIVE_ICONS'; then
		archive_extraction_extra_icons
	fi
}

# Extract data from a given archive file
# USAGE: archive_extraction $archive
archive_extraction() {
	local archive
	archive="$1"

	local archive_name
	archive_name=$(archive_name "$archive")

	information_archive_data_extraction "$archive_name"

	local destination_directory
	destination_directory="${PLAYIT_WORKDIR}/gamedata"
	mkdir --parents "$destination_directory"

	# Get the path to the extraction log file
	local log_file log_directory
	log_file=$(archive_extraction_log_path)
	log_directory=$(dirname "$log_file")
	mkdir --parents "$log_directory"

	local archive_extractor
	archive_extractor=$(archive_extractor "$archive")
	if [ -n "$archive_extractor" ]; then
		archive_extraction_using_extractor "$archive" "$destination_directory" "$log_file"
	else
		archive_extraction_from_type "$archive" "$destination_directory" "$log_file"
	fi

	# Apply minimal permissions on extracted files
	set_standard_permissions "$destination_directory"
}

# extract data from the target archive, using the specified extractor
# USAGE: archive_extraction_using_extractor $archive $destination_directory $log_file
archive_extraction_using_extractor() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_extractor
	archive_extractor=$(archive_extractor "$archive")
	case "$archive_extractor" in
		('7za')
			archive_extraction_using_7za "$archive" "$destination_directory" "$log_file"
		;;
		('7zr')
			archive_extraction_using_7zr "$archive" "$destination_directory" "$log_file"
		;;
		('bsdtar')
			archive_extraction_using_bsdtar "$archive" "$destination_directory" "$log_file"
		;;
		('cabextract')
			archive_extraction_using_cabextract "$archive" "$destination_directory" "$log_file"
		;;
		('dpkg-deb')
			archive_extraction_using_dpkgdeb "$archive" "$destination_directory" "$log_file"
		;;
		('innoextract')
			archive_extraction_using_innoextract "$archive" "$destination_directory" "$log_file"
		;;
		('lha')
			archive_extraction_using_lha "$archive" "$destination_directory" "$log_file"
		;;
		('msiextract')
			archive_extraction_using_msiextract "$archive" "$destination_directory" "$log_file"
		;;
		('tar')
			archive_extraction_using_tar "$archive" "$destination_directory" "$log_file"
		;;
		('unar')
			archive_extraction_using_unar "$archive" "$destination_directory" "$log_file"
		;;
		('unshield')
			archive_extraction_using_unshield "$archive" "$destination_directory" "$log_file"
		;;
		('unzip')
			archive_extraction_using_unzip "$archive" "$destination_directory" "$log_file"
		;;
		(*)
			error_archive_extractor_invalid "$archive_extractor"
			return 1
		;;
	esac
}

# extract data from the target archive, guessing the extractor from the given type
# USAGE: archive_extraction_from_type $archive $destination_directory $log_file
archive_extraction_from_type() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_type
	archive_type=$(archive_type "$archive")
	if [ -z "$archive_type" ]; then
		error_archive_type_not_set "$archive"
		return 1
	fi
	case "$archive_type" in
		('7z')
			archive_extraction_7z "$archive" "$destination_directory" "$log_file"
		;;
		('cabinet')
			archive_extraction_cabinet "$archive" "$destination_directory" "$log_file"
		;;
		('debian')
			archive_extraction_debian "$archive" "$destination_directory" "$log_file"
		;;
		('innosetup')
			archive_extraction_innosetup "$archive" "$destination_directory" "$log_file"
		;;
		('innosetup_nolowercase')
			warning_archive_type_deprecated "$archive"
			export ${archive}_EXTRACTOR_OPTIONS='--progress=1 --silent'
			archive_extraction_innosetup "$archive" "$destination_directory" "$log_file"
		;;
		('installshield')
			archive_extraction_installshield "$archive" "$destination_directory" "$log_file"
		;;
		('iso')
			archive_extraction_iso "$archive" "$destination_directory" "$log_file"
		;;
		('lha')
			archive_extraction_lha "$archive" "$destination_directory" "$log_file"
		;;
		('makeself')
			archive_extraction_makeself "$archive" "$destination_directory" "$log_file"
		;;
		('mojosetup')
			archive_extraction_mojosetup "$archive" "$destination_directory" "$log_file"
		;;
		('msi')
			archive_extraction_msi "$archive" "$destination_directory" "$log_file"
		;;
		('nullsoft-installer')
			archive_extraction_nullsoft "$archive" "$destination_directory" "$log_file"
		;;
		('rar')
			archive_extraction_rar "$archive" "$destination_directory" "$log_file"
		;;
		('tar'|'tar.bz2'|'tar.gz'|'tar.xz')
			archive_extraction_tar "$archive" "$destination_directory" "$log_file"
		;;
		('zip')
			archive_extraction_zip "$archive" "$destination_directory" "$log_file"
		;;
		(*)
			error_archive_type_invalid "$archive_type"
			return 1
		;;
	esac
}

# Extract the contents from the extra archives providing required libraries
# USAGE: archive_extraction_extra_libraries
archive_extraction_extra_libraries() {
	local libraries_required library_required
	libraries_required=$(dependencies_list_native_libraries_all)
	while read -r library_required; do
		case "$library_required" in
			('libcurl.so.4+CURL_OPENSSL_3')
				archive_extraction_extra_libcurl3
			;;
			('libFLAC.so.8')
				archive_extraction_extra_libflac8
			;;
			('libgconf-2.so.4')
				archive_extraction_extra_libgconf2
			;;
			('libidn.so.11')
				archive_extraction_extra_libidn11
			;;
			('libpng12.so.0')
				archive_extraction_extra_libpng12
			;;
			('libssl.so.1.0.0')
				archive_extraction_extra_libssl100
			;;
			('libssl.so.1.1')
				archive_extraction_extra_libssl11
			;;
		esac
	done <<- EOL
	$(printf '%s' "$libraries_required")
	EOL
}

# Extract libcurl.so.3 and libcurl.so.4 including the CURL_OPENSSL_3 symbol
# USAGE: archive_extraction_extra_libcurl3
archive_extraction_extra_libcurl3() {
	archive_extraction 'ARCHIVE_LIBCURL3'
}

# Extract libFLAC.so.8
# USAGE: archive_extraction_extra_libflac8
archive_extraction_extra_libflac8() {
	# On Arch Linux, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			return 0
		;;
	esac

	archive_extraction 'ARCHIVE_LIBFLAC8'
}

# Extract GConf 2 library
# USAGE: archive_extraction_extra_libgconf2
archive_extraction_extra_libgconf2() {
	archive_extraction 'ARCHIVE_LIBGCONF2'
}

# Extract GNU Libidn 11 library
# USAGE: archive_extraction_extra_libidn11
archive_extraction_extra_libidn11() {
	# On Arch Linux, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			return 0
		;;
	esac

	archive_extraction 'ARCHIVE_LIBIDN11'
}

# Extract PNG 1.2 libraries
# USAGE: archive_extraction_extra_libpng12
archive_extraction_extra_libpng12() {
	# On Arch Linux and Gentoo, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'egentoo'|'gentoo')
			return 0
		;;
	esac

	archive_extraction 'ARCHIVE_LIBPNG12'
}

# Extract OpenSSL 1.0.0 libraries
# USAGE: archive_extraction_extra_libssl100
archive_extraction_extra_libssl100() {
	# On Arch Linux and Gentoo, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'egentoo'|'gentoo')
			return 0
		;;
	esac

	archive_extraction 'ARCHIVE_OPENSSL100'
}

# Extract OpenSSL 1.1 libraries
# USAGE: archive_extraction_extra_libssl11
archive_extraction_extra_libssl11() {
	# On Arch Linux and Gentoo, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'egentoo'|'gentoo')
			return 0
		;;
	esac

	archive_extraction 'ARCHIVE_OPENSSL11'
}

# Extract the content of an icons pack archive
# USAGE: archive_extraction_extra_icons
archive_extraction_extra_icons() {
	archive_extraction 'ARCHIVE_ICONS'
}

# check the presence of required tools to handle a 7z archive
# USAGE: archive_dependencies_check_type_7z
archive_dependencies_check_type_7z() {
	local required_command
	for required_command in '7zr' '7za' 'unar'; do
		if command -v "$required_command" >/dev/null 2>&1; then
			return 0
		fi
	done
	error_dependency_not_found '7zr'
	return 1
}

# extract the content of a 7z archive
# USAGE: archive_extraction_7z $archive $destination_directory $log_file
archive_extraction_7z() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v '7zr' >/dev/null 2>&1; then
		archive_extraction_using_7zr "$archive" "$destination_directory" "$log_file"
	elif command -v '7za' >/dev/null 2>&1; then
		archive_extraction_using_7za "$archive" "$destination_directory" "$log_file"
	elif command -v 'unar' >/dev/null 2>&1; then
		archive_extraction_using_unar "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found '7z'
		return 1
	fi
}
# check the presence of required tools to handle a Microsoft Cabinet (.cab) archive
# USAGE: archive_dependencies_check_type_cabinet
archive_dependencies_check_type_cabinet() {
	if command -v 'cabextract' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'cabextract'
	return 1
}

# extract the content of a Microsoft Cabinet (.cab) archive
# USAGE: archive_extraction_cabinet $archive $destination_directory $log_file
archive_extraction_cabinet() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'cabextract' >/dev/null 2>&1; then
		archive_extraction_using_cabextract "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'cabinet'
		return 1
	fi
}
# check the presence of required tools to handle a Debian package (.deb)
# USAGE: archive_dependencies_check_type_debian
archive_dependencies_check_type_debian() {
	if command -v 'dpkg-deb' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'dpkg-deb'
	return 1
}

# extract the content of a Debian package (.deb)
# USAGE: archive_extraction_debian $archive $destination_directory $log_file
archive_extraction_debian() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'dpkg-deb' >/dev/null 2>&1; then
		archive_extraction_using_dpkgdeb "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'debian'
		return 1
	fi
}
# check the presence of required tools to handle a InnoSetup installer
# USAGE: archive_dependencies_check_type_innosetup
archive_dependencies_check_type_innosetup() {
	if command -v 'innoextract' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'innoextract'
	return 1
}

# extract the content of a InnoSetup installer
# USAGE: archive_extraction_innosetup $archive $destination_directory $log_file
archive_extraction_innosetup() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'innoextract' >/dev/null 2>&1; then
		archive_extraction_using_innoextract "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'innosetup'
		return 1
	fi
}
# check the presence of required tools to handle an InstallShield installer
# USAGE: archive_dependencies_check_type_installshield
archive_dependencies_check_type_installshield() {
	if command -v 'unshield' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'unshield'
	return 1
}

# extract the content of an InstallShield installer
# USAGE: archive_extraction_installshield $archive $destination_directory $log_file
archive_extraction_installshield() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'unshield' >/dev/null 2>&1; then
		archive_extraction_using_unshield "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'installshield'
		return 1
	fi
}
# check the presence of required tools to handle an ISO9660 CD-ROM image
# USAGE: archive_dependencies_check_type_iso
archive_dependencies_check_type_iso() {
	if command -v 'bsdtar' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'bsdtar'
	return 1
}

# extract the content of an ISO9660 CD-ROM image
# USAGE: archive_extraction_iso $archive $destination_directory $log_file
archive_extraction_iso() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'bsdtar' >/dev/null 2>&1; then
		archive_extraction_using_bsdtar "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'iso'
		return 1
	fi
}
# check the presence of required tools to handle a LHA archive (.lzh)
# USAGE: archive_dependencies_check_type_lha
archive_dependencies_check_type_lha() {
	local required_command
	for required_command in 'lha' 'bsdtar'; do
		if command -v "$required_command" >/dev/null 2>&1; then
			return 0
		fi
	done
	error_dependency_not_found 'lha'
	return 1
}

# extract the content of a LHA archive (.lzh)
# USAGE: archive_extraction_lha $archive $destination_directory $log_file
archive_extraction_lha() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'lha' >/dev/null 2>&1; then
		archive_extraction_using_lha "$archive" "$destination_directory" "$log_file"
	elif command -v 'bsdtar' >/dev/null 2>&1; then
		archive_extraction_using_bsdtar "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'lha'
		return 1
	fi
}
# List the requirements to extract the contents of a Makeself installer
# USAGE: archive_requirements_makeself_list
archive_requirements_makeself_list() {
	printf '%s\n' \
		'head' \
		'sed' \
		'wc' \
		'tr' \
		'gzip' \
		'tar'
}

# Check the presence of required tools to handle a Makeself installer
# USAGE: archive_requirements_makeself_check
archive_requirements_makeself_check() {
	local commands_list required_command
	commands_list=$(archive_requirements_makeself_list)
	for required_command in $commands_list; do
		if ! command -v "$required_command" >/dev/null 2>&1; then
			error_dependency_not_found "$required_command"
			return 1
		fi
	done
}

# Extract the content of a Makeself installer
# USAGE: archive_extraction_makeself $archive $destination_directory $log_file
archive_extraction_makeself() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	# Fetch the archive properties
	local archive_offset archive_filesize archive_block_size archive_blocks archive_bytes
	archive_offset=$(makeself_offset "$archive_path")
	archive_filesize=$(makeself_filesize "$archive_path")
	## Arbitrary value, small values would increase the time spent on the dd calls.
	archive_block_size=4096
	archive_blocks=$((archive_filesize / archive_block_size))
	archive_bytes=$((archive_filesize % archive_block_size))

	# Proceed with the contents extraction
	local archive_extraction_return_code
	{
		printf 'dd if="%s" ibs="%s" skip=1 obs=%s conv=sync 2>/dev/null | ' "$archive_path" "$archive_offset" "$archive_block_size"
		printf '{ test "%s" -gt 0 && dd ibs=%s obs=%s count="%s" ; ' "$archive_blocks" "$archive_block_size" "$archive_block_size" "$archive_blocks"
		printf 'test "%s" -gt 0 && dd ibs=1 obs=%s count="%s" ; } 2>/dev/null | ' "$archive_bytes" "$archive_block_size" "$archive_bytes"
		printf 'gzip --stdout --decompress | tar xvf - --directory="%s"\n' "$destination_directory"
	} >> "$log_file"
	{
		dd if="$archive_path" ibs="$archive_offset" skip=1 obs="$archive_block_size" conv=sync 2>/dev/null | \
			{
				test "$archive_blocks" -gt 0 && dd ibs="$archive_block_size" obs="$archive_block_size" count="$archive_blocks"
				test "$archive_bytes" -gt 0 && dd ibs=1 obs="$archive_block_size" count="$archive_bytes"
			} 2>/dev/null | \
			gzip --stdout --decompress | \
			tar xvf - --directory="$destination_directory" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# Makeself - Get the offset of the given file
# USAGE: makeself_offset $archive_path
# RETURN: the archive offset
makeself_offset() {
	local archive_path
	archive_path="$1"

	local archive_header_length archive_offset
	archive_header_length=$( \
		head --lines=200 "$archive_path" | \
		sed --silent 's/^\s*offset=`head -n \([0-9]\+\) "$1" | wc -c | tr -d " "`\s*/\1/p' \
	)
	archive_offset=$( \
		head --lines="$archive_header_length" "$archive_path" | \
		wc --bytes | \
		tr --delete ' ' \
	)

	printf '%s' "$archive_offset"
}

# Makeself - Get the size of the archive included in the given file
# USAGE: makeself_filesize $archive_path
# RETURN: the archive file size
makeself_filesize() {
	local archive_path
	archive_path="$1"

	local archive_filesize
	archive_filesize=$( \
		head --lines=200 "$archive_path" | \
		sed --silent 's/^\s*filesizes="\([0-9]\+\)"\s*/\1/p' \
	)

	printf '%s' "$archive_filesize"
}

# List the requirements to extract the contents of a MojoSetup installer
# USAGE: archive_requirements_mojosetup_list
archive_requirements_mojosetup_list() {
	# ShellCheck false-positive
	# Quote this to prevent word splitting.
	# shellcheck disable=SC2046
	printf '%s\n' \
		$(archive_requirements_makeself_list) \
		'unzip'
}

# Check the presence of required tools to handle a MojoSetup installer
# USAGE: archive_requirements_mojosetup_check
archive_requirements_mojosetup_check() {
	local commands_list required_command
	commands_list=$(archive_requirements_mojosetup_list)
	for required_command in $commands_list; do
		if ! command -v "$required_command" >/dev/null 2>&1; then
			error_dependency_not_found "$required_command"
			return 1
		fi
	done
}

# Extract the content of a MojoSetup installer
# USAGE: archive_extraction_mojosetup $archive $destination_directory $log_file
archive_extraction_mojosetup() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	# Fetch the archive properties
	local archive_makeself_offset archive_mojosetup_filesize archive_offset
	archive_makeself_offset=$(makeself_offset "$archive_path")
	archive_mojosetup_filesize=$(makeself_filesize "$archive_path")
	archive_offset=$((archive_makeself_offset + archive_mojosetup_filesize))
	## Arbitrary value, small values would increase the time spent on the dd calls.
	archive_block_size=4096

	# Extract the .zip archive containing the game data
	local archive_game_data archive_extraction_return_code
	archive_game_data="${destination_directory}/mojosetup-game-data.zip"
	## Silence ShellCheck false-positive
	## Consider using { cmd1; cmd2; } >> file instead of individual redirects.
	# shellcheck disable=SC2129
	printf 'dd if="%s" ibs="%s" obs="%s" skip="%sB" > "%s"\n' "$archive_path" "$archive_block_size" "$archive_block_size" "$archive_offset" "$archive_game_data" >> "$log_file"
	{
		dd if="$archive_path" ibs="$archive_block_size" obs="$archive_block_size" skip="${archive_offset}B" > "$archive_game_data" 2>> "$log_file"
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi

	# Extract the game data
	local archive_extraction_return_code
	printf 'unzip -o -d "%s" "%s"\n' "$destination_directory" "$archive_game_data" >> "$log_file"
	{
		## unzip -o: overwrite existing files without prompting.
		unzip -o -d "$destination_directory" "$archive_game_data" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
	rm "$archive_game_data"
}

# check the presence of required tools to handle a Windows Installer (.msi)
# USAGE: archive_dependencies_check_type_msi
archive_dependencies_check_type_msi() {
	if command -v 'msiextract' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'msiextract'
	return 1
}

# extract the content of a Windows Installer (.msi)
# USAGE: archive_extraction_msi $archive $destination_directory $log_file
archive_extraction_msi() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'msiextract' >/dev/null 2>&1; then
		archive_extraction_using_msiextract "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'msi'
		return 1
	fi
}
# check the presence of required tools to handle a NullSoft installer
# USAGE: archive_dependencies_check_type_nullsoft
archive_dependencies_check_type_nullsoft() {
	if command -v 'unar' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'unar'
	return 1
}

# extract the content of a NullSoft installer
# USAGE: archive_extraction_nullsoft $archive $destination_directory $log_file
archive_extraction_nullsoft() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'unar' >/dev/null 2>&1; then
		archive_extraction_using_unar "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'nullsoft-installer'
		return 1
	fi
}
# check the presence of required tools to handle a RAR archive
# USAGE: archive_dependencies_check_type_rar
archive_dependencies_check_type_rar() {
	if command -v 'unar' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'unar'
	return 1
}

# extract the content of a RAR archive
# USAGE: archive_extraction_rar $archive $destination_directory $log_file
archive_extraction_rar() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'unar' >/dev/null 2>&1; then
		archive_extraction_using_unar "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'rar'
		return 1
	fi
}
# check the presence of required tools to handle a tar archive
# USAGE: archive_dependencies_check_type_tar
archive_dependencies_check_type_tar() {
	if command -v 'tar' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'tar'
	return 1
}

# check the presence of required tools to handle a tar.bz2 archive
# USAGE: archive_dependencies_check_type_tarbz2
archive_dependencies_check_type_tarbz2() {
	archive_dependencies_check_type_tar
	if command -v 'bunzip2' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'bunzip2'
	return 1
}

# check the presence of required tools to handle a tar.gz archive
# USAGE: archive_dependencies_check_type_targz
archive_dependencies_check_type_targz() {
	archive_dependencies_check_type_tar
	if command -v 'gunzip' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'gunzip'
	return 1
}

# check the presence of required tools to handle a tar.xz archive
# USAGE: archive_dependencies_check_type_tarxz
archive_dependencies_check_type_tarxz() {
	archive_dependencies_check_type_tar
	if command -v 'unxz' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'unxz'
	return 1
}

# extract the content of a .tar archive
# USAGE: archive_extraction_tar $archive $destination_directory $log_file
archive_extraction_tar() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'tar' >/dev/null 2>&1; then
		archive_extraction_using_tar "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'tar'
		return 1
	fi
}
# check the presence of required tools to handle a .zip archive
# USAGE: archive_dependencies_check_type_zip
archive_dependencies_check_type_zip() {
	if command -v 'unzip' >/dev/null 2>&1; then
		return 0
	fi
	error_dependency_not_found 'unzip'
	return 1
}

# extract the content of a .zip archive
# USAGE: archive_extraction_zip $archive $destination_directory $log_file
archive_extraction_zip() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	if command -v 'unzip' >/dev/null 2>&1; then
		archive_extraction_using_unzip "$archive" "$destination_directory" "$log_file"
	else
		error_archive_no_extractor_found 'zip'
		return 1
	fi
}

# extract the content of an archive using 7za
# USAGE: archive_extraction_using_7za $archive $destination_directory $log_file
archive_extraction_using_7za() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	if [ -z "$extractor_options" ]; then
		extractor_options='-y'
	fi
	printf '7za x %s -o"%s" "%s"\n' "$extractor_options" "$destination_directory" "$archive_path" >> "$log_file"
	{
		7za x $extractor_options -o"$destination_directory" "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# extract the content of an archive using 7zr
# USAGE: archive_extraction_using_7zr $archive $destination_directory $log_file
archive_extraction_using_7zr() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	if [ -z "$extractor_options" ]; then
		extractor_options='-y'
	fi
	printf '7zr x %s -o"%s" "%s"\n' "$extractor_options" "$destination_directory" "$archive_path" >> "$log_file"
	{
		7zr x $extractor_options -o"$destination_directory" "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# extract the content of an archive using bsdtar
# USAGE: archive_extraction_using_bsdtar $archive $destination_directory $log_file
archive_extraction_using_bsdtar() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	printf 'bsdtar --verbose %s --directory "%s" --extract --file "%s"\n' "$extractor_options" "$destination_directory" "$archive_path" >> "$log_file"
	{
		bsdtar --verbose $extractor_options --directory "$destination_directory" --extract --file "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# extract the content of an archive using cabextract
# USAGE: archive_extraction_using_cabextract $archive $destination_directory $log_file
archive_extraction_using_cabextract() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	if [ -z "$extractor_options" ]; then
		extractor_options='-L'
	fi
	printf 'cabextract %s -d "%s" "%s"\n' "$extractor_options" "$destination_directory" "$archive_path" >> "$log_file"
	{
		cabextract $extractor_options -d "$destination_directory" "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# extract the content of an archive using dpkg-deb
# USAGE: archive_extraction_using_dpkgdeb $archive $destination_directory $log_file
archive_extraction_using_dpkgdeb() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	printf 'dpkg-deb --verbose %s --extract "%s" "%s"\n' "$extractor_options" "$archive_path" "$destination_directory" >> "$log_file"
	{
		dpkg-deb --verbose $extractor_options --extract "$archive_path" "$destination_directory" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# extract the content of an archive using innoextract
# USAGE: archive_extraction_using_innoextract $archive $destination_directory $log_file
archive_extraction_using_innoextract() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	if ! archive_extraction_using_innoextract_is_supported "$archive_path"; then
		error_innoextract_version_too_old "$archive_path"
		return 1
	fi

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	if [ -z "$extractor_options" ]; then
		extractor_options='--lowercase'
	fi
	printf 'innoextract %s --extract --output-dir "%s" "%s"\n' "$extractor_options" "$destination_directory" "$archive_path" >> "$log_file"
	{
		innoextract $extractor_options --extract --output-dir "$destination_directory" "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# check that the InnoSetup archive can be processed by the available innoextract version
# USAGE: archive_extraction_using_innoextract_is_supported $archive_path
# RETURNS: 0 if supported, 1 if unsupported
archive_extraction_using_innoextract_is_supported() {
	local archive_path
	archive_path="$1"

	# Use innoextract internal check
	if innoextract --list --silent "$archive_path" 2>&1 1>/dev/null | \
		head --lines=1 | \
		grep --ignore-case --quiet 'unexpected setup data version'
	then
		return 1
	fi

	# Check for GOG archives based on Galaxy file fragments, unsupported by innoextract < 1.7
	if innoextract --list "$archive_path" 2>/dev/null | \
		grep --quiet ' - "tmp/[0-9a-f]\{2\}/[0-9a-f]\{2\}/[0-9a-f]\{32\}" (.*)'
	then
		return 1
	fi

	return 0
}

# extract the content of an archive using lha
# USAGE: archive_extraction_using_lha $archive $destination_directory $log_file
archive_extraction_using_lha() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local archive_extraction_return_code
	# Due to its unusual command syntax, lha extractor has no support for ARCHIVE_xxx_EXTRACTOR_OPTIONS
	printf 'lha -ew="%s" "%s"\n' "$destination_directory" "$archive_path" >> "$log_file"
	{
		lha -ew="$destination_directory" "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# extract the content of an archive using msiextract
# USAGE: archive_extraction_using_msiextract $archive $destination_directory $log_file
archive_extraction_using_msiextract() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	printf 'msiextract %s --directory "%s" "%s"\n' "$extractor_options" "$destination_directory" "$archive_path" >> "$log_file"
	{
		msiextract $extractor_options --directory "$destination_directory" "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
	tolower "$destination_directory"
}

# extract the content of an archive using tar
# USAGE: archive_extraction_using_tar $archive $destination_directory $log_file
archive_extraction_using_tar() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	## Set default options based on archive type.
	if [ -z "$extractor_options" ]; then
		local archive_type
		archive_type=$(archive_type "$archive")
		case "$archive_type" in
			('tar.bz2')
				extractor_options='--bzip2'
			;;
			('tar.gz')
				extractor_options='--gzip'
			;;
			('tar.xz')
				extractor_options='--xz'
			;;
		esac
	fi
	printf 'tar --verbose %s --extract --file "%s" --directory "%s"\n' "$extractor_options" "$archive_path" "$destination_directory" >> "$log_file"
	{
		tar --verbose $extractor_options --extract --file "$archive_path" --directory "$destination_directory" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# extract the content of an archive using unar
# USAGE: archive_extraction_using_unar $archive $destination_directory $log_file
archive_extraction_using_unar() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	if [ -z "$extractor_options" ]; then
		extractor_options='-force-overwrite -no-directory'
	fi
	printf 'unar %s -output-directory "%s" "%s"\n' "$extractor_options" "$destination_directory" "$archive_path" >> "$log_file"
	{
		unar $extractor_options -output-directory "$destination_directory" "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# extract the content of an archive using unshield
# USAGE: archive_extraction_using_unshield $archive $destination_directory $log_file
archive_extraction_using_unshield() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	if [ -z "$extractor_options" ]; then
		extractor_options='-L'
	fi
	printf 'unshield %s -d "%s" x "%s"\n' "$extractor_options" "$destination_directory" "$archive_path" >> "$log_file"
	{
		unshield $extractor_options -d "$destination_directory" x "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# extract the content of an archive using unzip
# USAGE: archive_extraction_using_unzip $archive $destination_directory $log_file
archive_extraction_using_unzip() {
	local archive destination_directory log_file
	archive="$1"
	destination_directory="$2"
	log_file="$3"

	local archive_path
	archive_path=$(archive_path "$archive")

	local extractor_options archive_extraction_return_code
	extractor_options=$(archive_extractor_options "$archive")
	if [ -z "$extractor_options" ]; then
		## unzip -o: overwrite existing files without prompting.
		extractor_options='-o'
	fi
	printf 'unzip %s -d "%s" "%s"\n' "$extractor_options" "$destination_directory" "$archive_path" >> "$log_file"
	{
		unzip $extractor_options -d "$destination_directory" "$archive_path" >> "$log_file" 2>&1
		archive_extraction_return_code=$?
	} || true
	if [ $archive_extraction_return_code -ne 0 ]; then
		error_archive_extraction_failure "$archive"
		return 1
	fi
}

# Print the path to the log file for archive data extraction
# USAGE: archive_extraction_log_path
# RETURN: the absolute path to the log file,
#         the file might not exist yet, so the calling function should handle the directories and file creation
archive_extraction_log_path() {
	printf '%s/logs/archive-extraction.log' "$PLAYIT_WORKDIR"
}
# display the name of a file currently processed
# USAGE: information_file_in_use $file
information_file_in_use() {
	local file
	file="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Utilisation de %s\n'
		;;
		('en'|*)
			message='Using %s\n'
		;;
	esac
	print_message 'info' "$message"
}

# print data extraction message
# USAGE: information_archive_data_extraction $file
information_archive_data_extraction() {
	local file
	file="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Extraction des données de %s…\n'
		;;
		('en'|*)
			message='Extracting data from %s…\n'
		;;
	esac
	print_message 'info' "$message" \
		"$file"
}

# print hash computation message
# USAGE: info_archive_hash_computation $file_path
info_archive_hash_computation() {
	local file_path
	file_path="$1"

	local file_name
	file_name=$(basename "$file_path")

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Calcul de la somme de contrôle de %s…\n'
		;;
		('en'|*)
			message='Computing hashsum for %s…\n'
		;;
	esac
	print_message 'info' "$message" \
		"$file_name"
}

# display a list of archives, one per line, with their download URL if one is provided
# USAGE: information_archives_list $archive[…]
information_archives_list() {
	## TODO: The archive URL should be fetched using a dedicated function.
	local archive archive_name archive_url
	for archive in "$@"; do
		archive_name=$(archive_name "$archive")
		archive_url=$(get_value "${archive}_URL")
		if [ -n "$archive_url" ]; then
			print_message 'info' '%s — %s\n' \
				"$archive_name" \
				"$archive_url"
		else
			print_message 'info' '%s\n' \
				"$archive_name"
		fi
	done
}

# Warning - An optional icons archive is supported, but not available
# USAGE: warning_optional_archive_missing_icons $archive
warning_optional_archive_missing_icons() {
	local archive
	archive="$1"

	## TODO: The archive URL should be fetched using a dedicated function.
	local archive_name archive_url
	archive_name=$(archive_name "$archive")
	archive_url=$(get_value "${archive}_URL")

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Une archive proposant des icônes pour ce jeu est disponible, mais n’a pas été trouvée : %s\n'
			message="$message"'Elle peut être téléchargée depuis l’URL suivante : %s\n\n'
		;;
		('en'|*)
			message='An archive providing icons for this game is available, but could not be found: %s\n'
			message="$message"'It can be downloaded from the following URL: %s\n\n'
		;;
	esac
	print_message 'warning' "$message" \
		"$archive_name" \
		"$archive_url"
}

# Error - No archive supported for the current game script
error_no_archive_supported() {
	local game_script
	game_script=$(realpath "$0")

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Ce script semble ne prendre en charge aucune archive : %s\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de suivi : %s\n'
		;;
		('en'|*)
			message='This script seems to support no archive: %s\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$game_script" \
		"$PLAYIT_GAMES_BUG_TRACKER_URL"
}

# Error - No extractor is available to handle the given archive
# USAGE: error_archive_no_extractor_found $archive_type
error_archive_no_extractor_found() {
	local archive_type
	archive_type="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Ce script a essayé dʼextraire le contenu dʼune archive de type "%s", mais aucun outil approprié nʼa été trouvé.\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de gestion de bugs : %s\n'
		;;
		('en'|*)
			message='This script tried to extract the contents of a "%s" archive, but not appropriate tool could be found.\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$archive_type" \
		"$PLAYIT_GAMES_BUG_TRACKER_URL"
}

# Error - A required archive is not found
# List all the archives that could fulfill the requirement, with their download URL if one is provided
# USAGE: error_archive_not_found $archive[…]
error_archive_not_found() {
	# Get the path to the directory where the current archive is found
	local archives_path archives_path_full
	archives_path=$(archives_path_base)
	archives_path_full=$(realpath --no-symlinks "$archives_path")

	local messages_language message_1 message_2
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			if [ $# -eq 1 ]; then
				message_1='Le fichier suivant est introuvable dans %s :\n'
			else
				message_1='Aucun des fichiers suivants nʼest présent dans %s :\n'
			fi
			message_2='Vous devez télécharger le fichier requis avant de continuer.\n'
		;;
		('en'|*)
			if [ $# -eq 1 ]; then
				message_1='The following file could not be found in %s:\n'
			else
				message_1='None of the following files could be found in %s:\n'
			fi
			message_2='Please download the required file before proceeding.\n'
		;;
	esac
	print_message 'error' "$message_1" \
		"$archives_path_full"
	information_archives_list "$@" > /dev/stderr
	print_message 'info' "$message_2" > /dev/stderr
}

# Error - The type of the given archive could not be guessed
# USAGE: error_archive_type_not_set $archive
error_archive_type_not_set() {
	local archive
	archive="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='ARCHIVE_TYPE nʼest pas défini pour %s et nʼa pas pu être détecté automatiquement.\n'
		;;
		('en'|*)
			message='ARCHIVE_TYPE is not set for %s and could not be guessed.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$archive"
}

# Error - An integrity check failed
# USAGE: error_hashsum_mismatch $file_path
error_hashsum_mismatch() {
	local file_path
	file_path="$1"

	local file_name
	file_name=$(basename "$file_path")

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Somme de contrôle incohérente. %s nʼest pas le fichier attendu.\n'
			message="$message"'Utilisez --checksum=none pour forcer son utilisation.\n'
		;;
		('en'|*)
			message='Hashsum mismatch. %s is not the expected file.\n'
			message="$message"'Use --checksum=none to force its use.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$file_name"
}

# Error - The available version of innoextract is too old
# USAGE: error_innoextract_version_too_old $archive
error_innoextract_version_too_old() {
	local archive
	archive="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La version de innoextract disponible sur ce système est trop ancienne pour extraire les données de lʼarchive suivante : %s\n'
			message="$message"'Des instructions de mise-à-jour sont proposées :\n'
			message="$message"'- pour Debian : %s\n'
			message="$message"'- pour Ubuntu : %s\n'
		;;
		('en'|*)
			message='Available innoextract version is too old to extract data from the following archive: %s\n'
			message="$message"'Update instructions are proposed:\n'
			message="$message"'- for Debian: %s\n'
			message="$message"'- for Ubuntu: %s\n'
		;;
	esac
	## TODO: The innoextract upgrade instructions should be available from the Debian / Ubuntu documentations,
	##       not from the ./play.it one.
	print_message 'error' "$message" \
		"$archive" \
		'https://forge.dotslashplay.it/play.it/doc/-/wikis/user/distributions/debian#available-innoextract-version-is-too-old' \
		'https://forge.dotslashplay.it/play.it/doc/-/wikis/user/distributions/ubuntu#innoextract-version-is-too-old'
}

# Error - Invalid value used for archive type
# USAGE: error_archive_type_invalid $archive_type
error_archive_type_invalid() {
	local archive_type
	archive_type="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La valeur suivante ne correspond pas à un type dʼarchive connu : "%s"\n'
		;;
		('en'|*)
			message='The following value is not a valid archive type: "%s"\n'
		;;
	esac
	print_message 'error' "$message" \
		"$archive_type"
}

# Error - Invalid value used for archive extractor
# USAGE: error_archive_extractor_invalid $archive_extractor
error_archive_extractor_invalid() {
	local archive_extractor
	archive_extractor="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La valeur suivante ne correspond pas à un extracteur dʼarchive connu : "%s"\n'
		;;
		('en'|*)
			message='The following value is not a valid archive extractor: "%s"\n'
		;;
	esac
	print_message 'error' "$message" \
		"$archive_extractor"
}

# Error - Archive data extraction failed
# USAGE: error_archive_extraction_failure $archive
error_archive_extraction_failure() {
	local archive
	archive="$1"

	local archive_name
	archive_name=$(archive_name "$archive")

	local log_file
	log_file=$(archive_extraction_log_path)

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼextraction des données depuis lʼarchive suivante a échoué : %s\n'
			message="$message"'Vous pouvez obtenir plus de détails dans le fichier journal : %s\n'
		;;
		('en'|*)
			message='Data extraction from the following archive failed: %s\n'
			message="$message"'You can get more details from the following log file: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$archive_name" \
		"$log_file"
}

# Print the default path to the game data in the archive
# USAGE: content_path_default
# RETURN: a path relative to the archive root
content_path_default() {
	local content_path
	content_path=$(context_value 'CONTENT_PATH_DEFAULT')

	if [ -z "$content_path" ]; then
		error_missing_variable 'CONTENT_PATH_DEFAULT'
		return 1
	fi

	printf '%s' "$content_path"
}

# Print the path to the game data in the archive for a given identifier
# USAGE: content_path $content_id
# RETURN: a path relative to the archive root
#         or an empty path if none is set
content_path() {
	local content_id
	content_id="$1"

	# Use the context-specific content path if available
	local content_path
	content_path=$(context_value "CONTENT_${content_id}_PATH")
	## Try to parse legacy variables for old game scripts.
	if [ -z "$content_path" ]; then
		content_path=$(content_path_legacy "$content_id")
	fi

	# Fall back on the default path for the current game engine
	if [ -z "$content_path" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('visionaire')
				content_path=$(visionaire_content_path "$content_id")
			;;
		esac
	fi

	# Fall back to default content path if unset
	if [ -z "$content_path" ]; then
		# Do not fail if no default path is set
		content_path=$(content_path_default) 2>/dev/null || true
	fi

	printf '%s' "$content_path"
}

# Print the list of files to include from the archive for a given identifier
# USAGE: content_files $content_id
# RETURN: a list of paths relative to the path for the given identifier,
#         line breaks are used as separator between each item,
#         this list can include globbing patterns,
#         this list can be empty
content_files() {
	local content_id
	content_id="$1"

	local content_files
	content_files=$(context_value "CONTENT_${content_id}_FILES")
	## Try to parse legacy variables for old game scripts.
	if [ -z "$content_files" ]; then
		content_files=$(content_files_legacy "$content_id")
	fi

	# Fall back on the default files list for the current game engine
	if [ -z "$content_files" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unity3d')
				content_files=$(unity3d_content_files_default "$content_id")
			;;
			('unrealengine4')
				content_files=$(unrealengine4_content_files_default "$content_id")
			;;
			('visionaire')
				content_files=$(visionaire_content_files "$content_id")
			;;
		esac
	fi

	printf '%s' "$content_files"
}

# Fetch icon files, convert them to the expected format, include them in the given package.
#
# This function is the one that should be called from game scripts,
# it can take several applications as its arguments,
# and default to handle all applications if none are explicitely given.
#
# USAGE: content_inclusion_icons [$package [$application…]]
content_inclusion_icons() {
	# Do nothing if icons inclusion has been disabled.
	local option_icons
	option_icons=$(option_value 'icons')
	if [ "$option_icons" -eq 0 ]; then
		return 0
	fi

	# Get the package that should include the icons.
	local package
	if [ $# -ge 1 ]; then
		package="$1"
		shift 1
	else
		package=$(current_package)
	fi

	# If an optional icons archive has been provided, use that instead of the icons shipped with the game
	if archive_is_available 'ARCHIVE_ICONS'; then
		information_icons_inclusion
		content_inclusion_optional_icons_archive "$package"
		## Return early if an optional icons archive has been used.
		return 0
	fi

	# If no applications are explicitely listed,
	# try to fetch the icons for all applications.
	if [ "$#" -eq 0 ]; then
		applications_list=$(applications_list)
		## If content_inclusion_icons has been called with no argument, the applications list should not be empty.
		if [ -z "$applications_list" ]; then
			error_applications_list_empty
			return 1
		fi
		content_inclusion_icons "$package" $applications_list
		return 0
	fi

	information_icons_inclusion

	local application
	for application in "$@"; do
		icons_inclusion_single_application "$package" "$application"
	done
}

# Fetch files from the archive, and include them into the package skeleton.
# A list of default content identifiers is used.
# USAGE: content_inclusion_default
content_inclusion_default() {
	information_content_inclusion

	local packages_list
	packages_list=$(packages_list)

	local package unity3d_plugins
	for package in $packages_list; do
		unity3d_plugins=$(unity3d_plugins)
		if [ -n "$unity3d_plugins" ]; then
			content_inclusion_unity3d_plugins "$package"
		fi
		content_inclusion_default_libraries "$package"
		content_inclusion_default_fonts "$package"
		content_inclusion_default_game_data "$package"
		content_inclusion_default_documentation "$package"
	done

	# Including files used to apply tweaks to the WINE prefix should only be done in the packages including the game binaries.
	local package_architecture
	for package in $packages_list; do
		package_architecture=$(package_architecture "$package")
		case "$package_architecture" in
			('64'|'32')
				content_inclusion_wineprefix_tweaks "$package"
			;;
		esac
	done

	# content_inclusion_extra_libraries must be called in its own loop, at the end of the content inclusion process.
	# Since it can trigger an archive_extraction call leading to a set_standard_permissions call,
	# calling it earlier could lead to unwanted permissions reset.
	for package in $packages_list; do
		content_inclusion_extra_libraries "$package"
	done

	# Delete the remaining files extracted from archives but not included in any package
	## Skip the automatic clean-up for game scripts targeting ./play.it ≤ 2.25.
	if ! compatibility_level_is_at_least '2.26'; then
		return 0
	fi
	rm --force --recursive "${PLAYIT_WORKDIR}/gamedata"
}

# Fetch files from the archive, and include them into the package skeleton.
# A list of default content identifiers is used, limited to native libraries for a single package.
# USAGE: content_inclusion_default_libraries $package
content_inclusion_default_libraries() {
	local package
	package="$1"

	local package_suffix files_list
	package_suffix="${package#PKG_}"
	files_list=$(context_name "CONTENT_LIBS_${package_suffix}_FILES")
	if [ -z "$files_list" ]; then
		## Check if a default files list is set for the current engine,
		## return early otherwise.
		local content_files
		content_files=$(content_files "LIBS_${package_suffix}")
		if [ -z "$content_files" ]; then
			return 0
		fi
	fi

	local target_directory
	## On Arch Linux and Gentoo the libraries path can change based on the package architecture.
	target_directory=$(
		set_current_package "$package"
		path_libraries
	)
	content_inclusion "LIBS_${package_suffix}" "$package" "$target_directory"
	local index
	for index in $(seq 0 9); do
		files_list=$(context_name "CONTENT_LIBS${index}_${package_suffix}_FILES")
		if [ -z "$files_list" ]; then
			## Stop looping at the first unset files list.
			return 0
		fi
		content_inclusion "LIBS${index}_${package_suffix}" "$package" "$target_directory"
	done
}

# Fetch files from the archive, and include them into the package skeleton.
# A list of default content identifiers is used, limited to TTF fonts for a single package.
# USAGE: content_inclusion_default_fonts $package
content_inclusion_default_fonts() {
	local package
	package="$1"

	local package_suffix files_list
	package_suffix="${package#PKG_}"
	files_list=$(context_name "CONTENT_FONTS_${package_suffix}_FILES")
	if [ -z "$files_list" ]; then
		## Check if a default files list is set for the current engine,
		## return early otherwise.
		local content_files
		content_files=$(content_files "FONTS_${package_suffix}")
		if [ -z "$content_files" ]; then
			return 0
		fi
	fi

	local target_directory
	target_directory=$(path_fonts_ttf)
	content_inclusion "FONTS_${package_suffix}" "$package" "$target_directory"
	local index
	for index in $(seq 0 9); do
		files_list=$(context_name "CONTENT_FONTS${index}_${package_suffix}_FILES")
		if [ -z "$files_list" ]; then
			## Stop looping at the first unset files list.
			return 0
		fi
		content_inclusion "FONTS${index}_${package_suffix}" "$package" "$target_directory"
	done
}

# Fetch files from the archive, and include them into the package skeleton.
# A list of default content identifiers is used, limited to game data files for a single package.
# USAGE: content_inclusion_default_game_data $package
content_inclusion_default_game_data() {
	local package
	package="$1"

	local package_suffix files_list
	package_suffix="${package#PKG_}"
	files_list=$(context_name "CONTENT_GAME_${package_suffix}_FILES")
	if [ -z "$files_list" ]; then
		## Try to parse legacy variables for old game scripts.
		content_inclusion_default_game_data_legacy "$package"
		## Check if a default files list is set for the current engine,
		## return early otherwise.
		local content_files
		content_files=$(content_files "GAME_${package_suffix}")
		if [ -z "$content_files" ]; then
			return 0
		fi
	fi

	local target_directory
	target_directory=$(path_game_data)
	content_inclusion "GAME_${package_suffix}" "$package" "$target_directory"
	local index
	for index in $(seq 0 9); do
		files_list=$(context_name "CONTENT_GAME${index}_${package_suffix}_FILES")
		if [ -z "$files_list" ]; then
			## Stop looping at the first unset files list.
			return 0
		fi
		content_inclusion "GAME${index}_${package_suffix}" "$package" "$target_directory"
	done

}

# Fetch files from the archive, and include them into the package skeleton.
# A list of default content identifiers is used, limited to documentation files for a single package.
# USAGE: content_inclusion_default_documentation $package
content_inclusion_default_documentation() {
	local package
	package="$1"

	local package_suffix files_list
	package_suffix="${package#PKG_}"
	files_list=$(context_name "CONTENT_DOC_${package_suffix}_FILES")
	if [ -z "$files_list" ]; then
		## Try to parse legacy variables for old game scripts.
		content_inclusion_default_game_documentation_legacy "$package"
		## Check if a default files list is set for the current engine,
		## return early otherwise.
		local content_files
		content_files=$(content_files "DOC_${package_suffix}")
		if [ -z "$content_files" ]; then
			return 0
		fi
	fi

	local target_directory
	target_directory=$(path_documentation)
	content_inclusion "DOC_${package_suffix}" "$package" "$target_directory"
	local index
	for index in $(seq 0 9); do
		files_list=$(context_name "CONTENT_DOC${index}_${package_suffix}_FILES")
		if [ -z "$files_list" ]; then
			## Stop looping at the first unset files list.
			return 0
		fi
		content_inclusion "DOC${index}_${package_suffix}" "$package" "$target_directory"
	done
}

# Fetch files from the archive, and include them into the package skeleton.
# USAGE: content_inclusion $content_id $package $target_path
content_inclusion() {
	local content_id package target_path
	content_id="$1"
	package="$2"
	target_path="$3"

	information_content_inclusion

	# Check that the given package is valid
	if ! package_is_included_in_packages_list "$package"; then
		error_current_package_not_in_list "$package"
		return 1
	fi

	# Return early if the content source path is not set.
	local content_path
	content_path=$(content_path "$content_id")
	if [ -z "$content_path" ]; then
		return 0
	fi
	# Return early if the content source path does not exist.
	content_path_full="${PLAYIT_WORKDIR}/gamedata/${content_path}"
	if [ ! -e "$content_path_full" ]; then
		return 0
	fi

	# Debian - Handle huge files by splitting them in 9GB chunks,
	# and include the chunks in dedicated packages.
	if [ "$content_id" = "GAME_${package#PKG_}" ]; then
		local option_package
		option_package=$(option_value 'package')
		if [ "$option_package" = 'deb' ]; then
			local huge_files
			huge_files=$(huge_files_list "$package")
			if [ -n "$huge_files" ]; then
				content_inclusion_chunks "$package"
			fi
		fi
	fi

	# Set path to destination,
	# ensuring it is an absolute path.
	local package_path destination_path
	package_path=$(package_path "$package")
	destination_path=$(realpath --canonicalize-missing "${package_path}${target_path}")

	# Proceed with the actual files inclusion
	content_inclusion_include_paths "$content_id" "$destination_path"
}

# Convert a list of patterns to include into a series of find options
# USAGE: content_inclusion_find_options $content_identifier
# RETURN: a full find options string, using null-byte as separator
content_inclusion_find_options() {
	local content_identifier
	content_identifier="$1"

	local content_patterns_list
	content_patterns_list=$(content_files "$content_identifier")

	local content_pattern first_path_shown printf_format
	first_path_shown=0
	printf '.\0(\0'
	while read -r content_pattern; do
		## Skip empty lines.
		if [ -z "$content_pattern" ]; then
			continue
		fi
		if [ $first_path_shown -eq 0 ]; then
			printf_format='-path\0./%s'
			first_path_shown=1
		else
			printf_format='\0-o\0-path\0./%s'
		fi
		## Silence ShellCheck false-positive
		## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
		# shellcheck disable=SC2059
		printf -- "$printf_format" "${content_pattern#./}"
	done <<- EOL
	$(printf '%s' "$content_patterns_list")
	EOL
	printf '\0)\0-print0'
}

# Display the full list of paths to include, using a null-byte as the separator
# USAGE: content_inclusion_list_paths $content_identifier
# RETURN: a sorted list of paths, separated by null-bytes
content_inclusion_list_paths() {
	local content_identifier
	content_identifier="$1"

	local content_path content_path_full
	content_path=$(content_path "$content_identifier")
	content_path_full="${PLAYIT_WORKDIR}/gamedata/${content_path}"
	(
		cd "$content_path_full"
		content_inclusion_find_options "$content_identifier" | xargs --null find
	) | env --ignore-environment sort --zero-terminated
}

# Fetch a list of paths from the archives contents
# USAGE: content_inclusion_include_paths $content_identifier $destination_path
content_inclusion_include_paths() {
	local content_identifier destination_path
	content_identifier="$1"
	destination_path="$2"

	local content_path content_path_full paths_list_base64
	content_path=$(content_path "$content_identifier")
	content_path_full="${PLAYIT_WORKDIR}/gamedata/${content_path}"
	mkdir --parents "$destination_path"
	(
		cd "$content_path_full"
		## The list of paths is encoded in base64, because it uses null bytes as a separator and null bytes can not be stored in a variable.
		paths_list_base64=$(content_inclusion_list_paths "$content_identifier" | base64 --wrap=0)
		printf '%s' "$paths_list_base64" | base64 --decode | xargs --null --no-run-if-empty cp \
			--force \
			--link \
			--recursive \
			--no-dereference \
			--parents \
			--preserve=links \
			--target-directory "$destination_path"
		## Delete paths that have already been copied, so they will not end up duplicated in the packages.
		printf '%s' "$paths_list_base64" | base64 --decode | xargs --null --no-run-if-empty rm \
			--force \
			--recursive
	)
}

# Include required native libraries provided by extra archives
# USAGE: content_inclusion_extra_libraries $package
content_inclusion_extra_libraries() {
	local package
	package="$1"

	local libraries_required library_required
	libraries_required=$(dependencies_list_native_libraries "$package")
	while read -r library_required; do
		case "$library_required" in
			('libcurl.so.4+CURL_OPENSSL_3')
				content_inclusion_extra_libraries_libcurl3 "$package"
			;;
			('libFLAC.so.8')
				content_inclusion_extra_libraries_libflac8 "$package"
			;;
			('libgconf-2.so.4')
				content_inclusion_extra_libraries_libgconf2 "$package"
			;;
			('libidn.so.11')
				content_inclusion_extra_libraries_libidn11 "$package"
			;;
			('libpng12.so.0')
				content_inclusion_extra_libraries_libpng12 "$package"
			;;
			('libssl.so.1.0.0')
				content_inclusion_extra_libraries_libssl100 "$package"
			;;
			('libssl.so.1.1')
				content_inclusion_extra_libraries_libssl11 "$package"
			;;
		esac
	done <<- EOL
	$(printf '%s' "$libraries_required")
	EOL
}

# Include libcurl.so.3 and libcurl.so.4 including the CURL_OPENSSL_3 symbol
# USAGE: content_inclusion_extra_libraries_libcurl3 $package
content_inclusion_extra_libraries_libcurl3() {
	local package
	package="$1"

	# Set the list of files to include from the provided archive
	local package_architecture CONTENT_LIBCURL3_PATH CONTENT_LIBCURL3_FILES
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			## Silence ShellCheck false-positive
			## CONTENT_LIBCURL3_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBCURL3_PATH='x86_32'
		;;
		('64')
			## Silence ShellCheck false-positive
			## CONTENT_LIBCURL3_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBCURL3_PATH='x86_64'
		;;
	esac
	## Silence ShellCheck false-positive
	## CONTENT_LIBCURL3_FILES appears unused. Verify use (or export if used externally).
	# shellcheck disable=SC2034
	CONTENT_LIBCURL3_FILES='
	libcrypto.so.1.0.2
	libssl.so.1.0.2
	libcurl.so.3
	libcurl.so.4
	libcurl.so.4.4.0'

	# Proceed with the actual files inclusion
	local path_libraries
	path_libraries=$(
		set_current_package "$package"
		path_libraries
	)
	content_inclusion 'LIBCURL3' "$package" "$path_libraries"

	# Update the list of dependencies on native libraries
	local extra_native_libraries_required
	extra_native_libraries_required='
	libcom_err.so.2
	libc.so.6
	libdl.so.2
	libgssapi_krb5.so.2
	libidn2.so.0
	libk5crypto.so.3
	libkrb5.so.3
	libnghttp2.so.14
	libpsl.so.5
	libpthread.so.0
	librtmp.so.1
	libssh2.so.1
	libz.so.1'
	dependencies_add_native_libraries "$package" "$extra_native_libraries_required"
}

# Include libFLAC.so.8
# USAGE: content_inclusion_extra_libraries_libflac8 $package
content_inclusion_extra_libraries_libflac8() {
	# On Arch Linux, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			return 0
		;;
	esac

	local package
	package="$1"

	# Set the list of files to include from the provided archive
	local package_architecture CONTENT_LIBFLAC8_PATH CONTENT_LIBFLAC8_FILES
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			## Silence ShellCheck false-positive
			## CONTENT_LIBFLAC8_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBFLAC8_PATH='i386-linux-gnu'
		;;
		('64')
			## Silence ShellCheck false-positive
			## CONTENT_LIBFLAC8_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBFLAC8_PATH='x86_64-linux-gnu'
		;;
	esac
	## Silence ShellCheck false-positive
	## CONTENT_LIBFLAC8_FILES appears unused. Verify use (or export if used externally).
	# shellcheck disable=SC2034
	CONTENT_LIBFLAC8_FILES='
	libFLAC.so.8
	libFLAC.so.8.3.0'

	# Proceed with the actual files inclusion
	local path_libraries
	path_libraries=$(
		set_current_package "$package"
		path_libraries
	)
	content_inclusion 'LIBFLAC8' "$package" "$path_libraries"

	# Update the list of dependencies on native libraries
	local extra_native_libraries_required
	extra_native_libraries_required='
	libc.so.6
	libm.so.6
	libogg.so.0'
	dependencies_add_native_libraries "$package" "$extra_native_libraries_required"
}

# Include GConf 2 library
# USAGE: content_inclusion_extra_libraries_libgconf2 $package
content_inclusion_extra_libraries_libgconf2() {
	local package
	package="$1"

	# Set the list of files to include from the provided archive
	local package_architecture CONTENT_LIBGCONF2_PATH CONTENT_LIBGCONF2_FILES
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			## Silence ShellCheck false-positive
			## CONTENT_LIBGCONF2_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBGCONF2_PATH='i386-linux-gnu'
		;;
		('64')
			## Silence ShellCheck false-positive
			## CONTENT_LIBGCONF2_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBGCONF2_PATH='x86_64-linux-gnu'
		;;
	esac
	## Silence ShellCheck false-positive
	## CONTENT_LIBGCONF2_FILES appears unused. Verify use (or export if used externally).
	# shellcheck disable=SC2034
	CONTENT_LIBGCONF2_FILES='
	libgconf-2.so.4
	libgconf-2.so.4.1.5'

	# Proceed with the actual files inclusion
	local path_libraries
	path_libraries=$(
		set_current_package "$package"
		path_libraries
	)
	content_inclusion 'LIBGCONF2' "$package" "$path_libraries"

	# Update the list of dependencies on native libraries
	local extra_native_libraries_required
	extra_native_libraries_required='
	libc.so.6
	libdbus-1.so.3
	libdbus-glib-1.so.2
	libglib-2.0.so.0
	libgmodule-2.0.so.0
	libgobject-2.0.so.0'
	dependencies_add_native_libraries "$package" "$extra_native_libraries_required"
}

# Include GNU Libidn library
# USAGE: content_inclusion_extra_libraries_libidn11 $package
content_inclusion_extra_libraries_libidn11() {
	# On Arch Linux, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			return 0
		;;
	esac

	local package
	package="$1"

	# Set the list of files to include from the provided archive
	local package_architecture CONTENT_LIBIDN11_PATH CONTENT_LIBIDN11_FILES
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			## Silence ShellCheck false-positive
			## CONTENT_LIBIDN11_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBIDN11_PATH='i386-linux-gnu'
		;;
		('64')
			## Silence ShellCheck false-positive
			## CONTENT_LIBIDN11_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBIDN11_PATH='x86_64-linux-gnu'
		;;
	esac
	## Silence ShellCheck false-positive
	## CONTENT_LIBIDN11_FILES appears unused. Verify use (or export if used externally).
	# shellcheck disable=SC2034
	CONTENT_LIBIDN11_FILES='
	libidn.so.11
	libidn.so.11.6.16'

	# Proceed with the actual files inclusion
	local path_libraries
	path_libraries=$(
		set_current_package "$package"
		path_libraries
	)
	content_inclusion 'LIBIDN11' "$package" "$path_libraries"

	# Update the list of dependencies on native libraries
	local extra_native_libraries_required
	extra_native_libraries_required='
	libc.so.6'
	dependencies_add_native_libraries "$package" "$extra_native_libraries_required"
}

# Include PNG 1.2 libraries
# USAGE: content_inclusion_extra_libraries_libpng12 $package
content_inclusion_extra_libraries_libpng12() {
	# On Arch Linux and Gentoo, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'egentoo'|'gentoo')
			return 0
		;;
	esac

	local package
	package="$1"

	# Set the list of files to include from the provided archive
	local package_architecture CONTENT_LIBPNG12_PATH CONTENT_LIBPNG12_FILES
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			## Silence ShellCheck false-positive
			## CONTENT_LIBPNG12_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBPNG12_PATH='x86_32'
		;;
		('64')
			## Silence ShellCheck false-positive
			## CONTENT_LIBPNG12_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_LIBPNG12_PATH='x86_64'
		;;
	esac
	## Silence ShellCheck false-positive
	## CONTENT_LIBPNG12_FILES appears unused. Verify use (or export if used externally).
	# shellcheck disable=SC2034
	CONTENT_LIBPNG12_FILES='
	libpng12.so.0
	libpng12.so.0.50.0'

	# Proceed with the actual files inclusion
	local path_libraries
	path_libraries=$(
		set_current_package "$package"
		path_libraries
	)
	content_inclusion 'LIBPNG12' "$package" "$path_libraries"

	# Update the list of dependencies on native libraries
	local extra_native_libraries_required
	extra_native_libraries_required='
	libc.so.6
	libm.so.6
	libz.so.1'
	dependencies_add_native_libraries "$package" "$extra_native_libraries_required"
}

# Include OpenSSL 1.0.0 libraries
# USAGE: content_inclusion_extra_libraries_libssl100 $package
content_inclusion_extra_libraries_libssl100() {
	# On Arch Linux and Gentoo, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'egentoo'|'gentoo')
			return 0
		;;
	esac

	local package
	package="$1"

	# Set the list of files to include from the provided archive
	local package_architecture CONTENT_OPENSSL100_PATH CONTENT_OPENSSL100_FILES
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			## Silence ShellCheck false-positive
			## CONTENT_OPENSSL100_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_OPENSSL100_PATH='x86_32'
		;;
		('64')
			## Silence ShellCheck false-positive
			## CONTENT_OPENSSL100_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_OPENSSL100_PATH='x86_64'
		;;
	esac
	## Silence ShellCheck false-positive
	## CONTENT_OPENSSL100_FILES appears unused. Verify use (or export if used externally).
	# shellcheck disable=SC2034
	CONTENT_OPENSSL100_FILES='
	libcrypto.so.1.0.0
	libssl.so.1.0.0'

	# Proceed with the actual files inclusion
	local path_libraries
	path_libraries=$(
		set_current_package "$package"
		path_libraries
	)
	content_inclusion 'OPENSSL100' "$package" "$path_libraries"

	# Update the list of dependencies on native libraries
	local extra_native_libraries_required
	extra_native_libraries_required='
	libc.so.6
	libdl.so.2'
	dependencies_add_native_libraries "$package" "$extra_native_libraries_required"
}

# Include OpenSSL 1.1 libraries
# USAGE: content_inclusion_extra_libraries_libssl11 $package
content_inclusion_extra_libraries_libssl11() {
	# On Arch Linux and Gentoo, this library is still provided in the packages repositories
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch'|'egentoo'|'gentoo')
			return 0
		;;
	esac

	local package
	package="$1"

	# Set the list of files to include from the provided archive
	local package_architecture CONTENT_OPENSSL11_PATH CONTENT_OPENSSL11_FILES
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			## Silence ShellCheck false-positive
			## CONTENT_OPENSSL11_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_OPENSSL11_PATH='i386-linux-gnu'
		;;
		('64')
			## Silence ShellCheck false-positive
			## CONTENT_OPENSSL11_PATH appears unused. Verify use (or export if used externally).
			# shellcheck disable=SC2034
			CONTENT_OPENSSL11_PATH='x86_64-linux-gnu'
		;;
	esac
	## Silence ShellCheck false-positive
	## CONTENT_OPENSSL11_FILES appears unused. Verify use (or export if used externally).
	# shellcheck disable=SC2034
	CONTENT_OPENSSL11_FILES='
	libcrypto.so.1.1
	libssl.so.1.1'

	# Proceed with the actual files inclusion
	local path_libraries
	path_libraries=$(
		set_current_package "$package"
		path_libraries
	)
	content_inclusion 'OPENSSL11' "$package" "$path_libraries"

	# Update the list of dependencies on native libraries
	local extra_native_libraries_required
	extra_native_libraries_required='
	libc.so.6
	libdl.so.2
	libpthread.so.0'
	dependencies_add_native_libraries "$package" "$extra_native_libraries_required"
}

# Include the icons provided by an extra archive
# USAGE: content_inclusion_optional_icons_archive $package
content_inclusion_optional_icons_archive() {
	local package
	package="$1"

	# Proceed with the actual files inclusion
	local path_icons
	path_icons=$(path_icons)
	content_inclusion 'ICONS' "$package" "$path_icons"
}

# Include files required to apply tweaks to the WINE prefix
# USAGE: content_inclusion_wineprefix_tweaks $package
content_inclusion_wineprefix_tweaks() {
	local package
	package="$1"

	local wineprefix_tweaks wineprefix_tweak
	wineprefix_tweaks=$(wine_wineprefix_tweaks)
	while read -r wineprefix_tweak; do
		case "$wineprefix_tweak" in
			('mono')
				content_inclusion_wineprefix_tweaks_mono "$package"
			;;
		esac
	done <<- EOL
	$(printf '%s' "$wineprefix_tweaks")
	EOL
}

# Include the Mono .msi installer
# USAGE: content_inclusion_wineprefix_tweaks_mono $package
content_inclusion_wineprefix_tweaks_mono() {
	local package
	package="$1"

	local mono_installer_source package_path path_game_data mono_installer_name mono_installer_destination
	mono_installer_source=$(archive_path 'ARCHIVE_MONO')
	package_path=$(package_path "$package")
	path_game_data=$(path_game_data)
	mono_installer_name=$(archive_name 'ARCHIVE_MONO')
	mono_installer_destination="${package_path}${path_game_data}/wineprefix-tweaks/${mono_installer_name}"
	install -D --mode=644 "$mono_installer_source" "$mono_installer_destination"
}

# List huge files for the given package
# The paths should be relative to CONTENT_PATH_DEFAULT.
# USAGE: huge_files_list $package
# RETURN: a list of files,
#         one per line
huge_files_list() {
	local package
	package="$1"

	local huge_files
	huge_files=$(context_value "HUGE_FILES_${package#PKG_}")

	# Return early if no list is set for the given package
	if [ -z "$huge_files" ]; then
		return 0
	fi

	printf '%s' "$huge_files" | list_clean
}

# Split the given file into 9GB chunks
# USAGE: huge_file_split $file_path
huge_file_split() {
	local file_path
	file_path="$1"

	information_huge_file_split "$file_path"

	local content_path
	content_path=$(content_path_default)
	(
		cd "${PLAYIT_WORKDIR}/gamedata/${content_path}"
		split --bytes=9G --numeric-suffixes=1 --suffix-length=1 \
			"$file_path" \
			"${file_path}."
		rm --force "$file_path"
	)
}

# List the chunks generated from a given file
# USAGE: huge_file_chunks_list $file_path
# RETURN: a list of files,
#         one per line
huge_file_chunks_list() {
	local file_path
	file_path="$1"

	local content_path
	content_path=$(content_path_default)
	(
		cd "${PLAYIT_WORKDIR}/gamedata/${content_path}"
		find . -path "./${file_path}.?" | \
			sort | \
			sed 's#^\./##'
	)
}

# Print the commands concatenating chunks into a single file
# USAGE: huge_file_concatenate $file_path
huge_file_concatenate() {
	local file_path
	file_path="$1"

	local path_game
	path_game=$(path_game_data)

	cat <<- EOF
	# Rebuild a huge file from its chunks
	huge_file='${path_game}/${file_path}'
	EOF
	cat <<- 'EOF'
	for huge_file_chunk in "${huge_file}."*; do
	    if [ -e "$huge_file_chunk" ]; then
	        case "${LANG%_*}" in
	            ('fr')
	                message='Reconstruction de %s à partir de ses parties…\n'
	            ;;
	            ('en'|*)
	                message='Rebuilding %s from its chunks…\n'
	            ;;
	        esac
	        printf "$message" "$huge_file"
	        cat "${huge_file}."* > "$huge_file"
	        rm "${huge_file}."*
	        break
	    fi
	done
	EOF
}

# Print the commands deleting a single file that has been built from its chunks
# USAGE: huge_file_delete $file_path
huge_file_delete() {
	local file_path
	file_path="$1"

	local path_game
	path_game=$(path_game_data)

	cat <<- EOF
	# Delete a huge file that has been built from its chunks
	huge_file='${path_game}/${file_path}'
	EOF
	cat <<- 'EOF'
	rm --force "$huge_file"
	EOF
}

# Split huge files in chunks, and include them in dedicated packages
# USAGE: content_inclusion_chunks $package
content_inclusion_chunks() {
	local package
	package="$1"

	local huge_files
	huge_files=$(huge_files_list "$package")

	# Return early if no list is set for the given package
	if [ -z "$huge_files" ]; then
		return 0
	fi

	local huge_file chunks_list chunk_path chunks_counter
	chunks_counter=1
	while read -r huge_file; do
		# Split the current huge files into 9GB chunks
		huge_file_split "$huge_file"

		# Include each chunk into a dedicated new package
		chunks_list=$(huge_file_chunks_list "$huge_file")
		while read -r chunk_path; do
			content_inclusion_chunk_single "$package" "$chunk_path" "$chunks_counter"
			chunks_counter=$((chunks_counter + 1))
		done <<- EOL1
		$(printf '%s' "$chunks_list")
		EOL1

		# Set the postinst commands used to rebuild the file from its chunks
		## The file deletion is done when removing the chunks packages (see content_inclusion_chunk_single),
		## to avoid deleting it when reinstalling the main package without rebuilding it afterwards
		## because the chunks have been deleted already.
		local postinst_commands extra_postinst_commands
		postinst_commands=$(package_postinst_actions "$package")
		extra_postinst_commands=$(huge_file_concatenate "$huge_file")
		export "${package}_POSTINST_RUN"="$postinst_commands
		$extra_postinst_commands"
	done <<- EOL2
	$(printf '%s' "$huge_files")
	EOL2
}

# Include a single chunk into a new dedicated package
# USAGE: content_inclusion_chunk_single $package $chunk_path $chunks_counter
content_inclusion_chunk_single() {
	local package chunk_path chunks_counter
	package="$1"
	chunk_path="$2"
	chunks_counter="$3"

	# Compute the new package identifier and its content identifier
	local package_identifier package_suffix content_id
	package_identifier="${package}_CHUNK${chunks_counter}"
	package_suffix="${package_identifier#PKG_}"
	content_id="GAME_${package_suffix}"

	# Add the new package to the list of packages to build
	local packages_list packages_list_variable
	packages_list=$(packages_list)
	packages_list_variable=$(context_name 'PACKAGES_LIST')
	export "${packages_list_variable:-PACKAGES_LIST}=
	$package_identifier
	$packages_list"

	# Set the new package properties
	local package_id package_description prerm_commands
	package_id=$(package_id "$package")
	package_description=$(package_description "$package")
	prerm_commands=$(huge_file_delete "$huge_file")
	export "${package_identifier}_ID"="${package_id}-chunk${chunks_counter}"
	export "${package_identifier}_DESCRIPTION"="${package_description} - chunk ${chunks_counter}"
	export "${package_identifier}_PRERM_RUN"="$prerm_commands"

	# Add the new package to the list of dependencies of its parent
	dependencies_add_generic "$package" "${package_id}-chunk${chunks_counter}"

	# Include the chunk into the new package
	local path_game
	path_game=$(path_game_data)
	export "CONTENT_${content_id}_FILES"="$chunk_path"
	content_inclusion "$content_id" "$package_identifier" "$path_game"
}

# Information: Icons from the archives are included into packages paths
# USAGE: information_icons_inclusion
information_icons_inclusion() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Inclusion des icônes…\n'
		;;
		('en'|*)
			message='Including icons…\n'
		;;
	esac
	print_message 'info' "$message"
}

# Information: Files from the archives are included into packages paths
# USAGE: information_content_inclusion
information_content_inclusion() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Inclusion des fichiers du jeu…\n'
		;;
		('en'|*)
			message='Including game files…\n'
		;;
	esac
	print_message 'info_once' "$message"
}

# Information: A huge file is split into 9GB chunks
# USAGE: information_huge_file_split $path_file
information_huge_file_split() {
	local path_file
	path_file="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Découpage de %s en plusieurs fichiers…\n'
		;;
		('en'|*)
			message='Splitting %s into smaller chunks…\n'
		;;
	esac
	print_message 'info' "$message" \
		"$path_file"
}

# Print the list of all icon identifiers.
# USAGE: icons_list_all
# RETURN: a list of icons identifiers, one per line,
#         or an empty string if no icon seems to be set
icons_list_all() {
	local applications_list
	applications_list=$(applications_list)
	# Return early if there is no application set for the current game
	if [ -z "$applications_list" ]; then
		return 0
	fi

	local icons_list application application_icons_list
	icons_list=''
	for application in $applications_list; do
		application_icons_list=$(application_icons_list "$application")
		icons_list="$icons_list $application_icons_list"
	done

	if [ -n "$icons_list" ]; then
		printf '%s\n' $icons_list
	fi
}

# Print the list of icon identifiers for the given application.
# USAGE: application_icons_list $application
# RETURN: a space-separated list of icons identifiers,
#         or an empty string if no icon seems to be set
application_icons_list() {
	local application
	application="$1"

	# Use the value of APP_xxx_ICONS_LIST if it is set.
	local icons_list
	icons_list=$(context_value "${application}_ICONS_LIST")

	# Fall back on the default value of a single APP_xxx_ICON icon
	if [ -z "$icons_list" ]; then
		local default_icon
		default_icon=$(context_name "${application}_ICON")
		## If a value is explicitly set for APP_xxx_ICON,
		## it is considered to be the only icon for the current application.
		if [ -n "$default_icon" ]; then
			icons_list="$default_icon"
		fi
	fi

	## If no value is set for APP_xxx_ICON, try to guess one from the game engine.
	if [ -z "$icons_list" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unity3d')
				## It is expected that Unity3D games always come with a single icon.
				icons_list="${application}_ICON"
			;;
		esac
	fi

	## If no value is set for APP_xxx_ICON, try to guess one from the application type.
	if [ -z "$icons_list" ]; then
		local application_type
		application_type=$(application_type "$application")
		case "$application_type" in
			('wine')
				## If no value is explicitly set for the icon of a WINE application,
				## the game binary is used as a fallback icons source.
				icons_list="${application}_ICON"
			;;
		esac
	fi

	printf '%s' "$icons_list"
}

# Print the application identifier for the given icon
# USAGE: icon_application $icon
# RETURN: the application identifier
icon_application() {
	local icon
	icon="$1"

	# Look for an application identifier that share the same prefix than the given icon identifier.
	local application application_identifier applications_list
	applications_list=$(applications_list)
	## The applications list should not be empty.
	if [ -z "$applications_list" ]; then
		error_applications_list_empty
		return 1
	fi
	for application in $applications_list; do
		case "$icon" in
			("${application}_"*)
				application_identifier="$application"
				break
			;;
		esac
	done

	# Throw an error if no valid application identifier could be found.
	if [ -z "${application_identifier:-}" ]; then
		error_icon_application_not_found "$icon"
		return 1
	fi

	printf '%s' "$application_identifier"
}

# Print the path to the source file for the given icon
# USAGE: icon_path $icon
# RETURN: the path to the file used to extract icons from,
#         it is relative to CONTENT_PATH_DEFAULT
icon_path() {
	local icon
	icon="$1"

	local icon_path
	icon_path=$(context_value "$icon")

	## If no value is set for the icon path, try to guess one from the game engine.
	if [ -z "$icon_path" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unity3d')
				icon_path=$(unity3d_icon_path "$icon")
			;;
		esac
	fi

	## If no value is set for the icon path, try to guess one from the application type.
	if [ -z "$icon_path" ]; then
		local application application_type
		application=$(icon_application "$icon")
		application_type=$(application_type "$application")
		case "$application_type" in
			('wine')
				icon_path=$(icon_wine_path "$icon")
			;;
		esac
	fi

	# Check that the path to the icon is not empty
	if [ -z "$icon_path" ]; then
		error_icon_path_empty "$icon"
		return 1
	fi

	printf '%s' "$icon_path"
}

# Print the wrestool options string for the given .exe icon
# USAGE: icon_wrestool_options $icon
# RETURN: the options string to pass to wrestool
icon_wrestool_options() {
	local icon
	icon="$1"

	# Check that the given icon is a .exe file
	if ! icon_path "$icon" | grep --quiet '\.exe$'; then
		error_icon_unexpected_type "$icon" '*.exe'
		return 1
	fi

	local wrestool_options
	wrestool_options=$(get_value "${icon}_WRESTOOL_OPTIONS")

	# Fall back on a default value based on the game engine
	if [ -z "$wrestool_options" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unrealengine4')
				wrestool_options=$(unrealengine4_icon_wrestool_options_default)
			;;
			(*)
				wrestool_options='--type=14'
			;;
		esac
	fi

	printf '%s' "$wrestool_options"
}

# Fetch icons from the archive contents,
# convert them to PNG if they are not already in a supported format,
# include them in the given package.
#
# This function handles all icons for a given application.
#
# USAGE: icons_inclusion_single_application $package $application
icons_inclusion_single_application() {
	local package application
	package="$1"
	application="$2"

	local application_icons_list
	application_icons_list=$(application_icons_list "$application")
	# Throw an error if no icon was found for the given application.
	if [ -z "$application_icons_list" ]; then
		## Skip the error if an icons archives is supported, as a non-blocking warning has already been shown.
		if [ -n "${ARCHIVE_OPTIONAL_ICONS_NAME:-}" ]; then
			return 0
		fi
		error_no_icon_found "$application"
		return 1
	fi

	local icon
	for icon in $application_icons_list; do
		icons_inclusion_single_icon "$package" "$application" "$icon"
	done
}

# Compute the full path to the icon source file
# USAGE: icon_full_path $icon
# RETURN: the full path to the file that provides icons,
#         as an absolute path
icon_full_path() {
	local icon
	icon="$1"

	local content_path icon_path
	content_path=$(content_path_default)
	icon_path=$(icon_path "$icon")

	printf '%s/gamedata/%s/%s' "$PLAYIT_WORKDIR" "$content_path" "$icon_path"
}

# Fetch icon from the archive contents,
# convert it to PNG if it is not already in a supported format,
# include it in the given package.
#
# This function handles a single icon.
#
# USAGE: icons_inclusion_single_icon $package $application $icon
icons_inclusion_single_icon() {
	local package application icon
	package="$1"
	application="$2"
	icon="$3"

	# Check for the existence of the icons source file
	local icon_path
	icon_path=$(icon_full_path "$icon")
	if [ ! -f "$icon_path" ]; then
		error_icon_file_not_found "$icon_path"
		return 1
	fi

	icons_temporary_directory="${PLAYIT_WORKDIR}/icons"
	mkdir --parents "$icons_temporary_directory"
	icon_extract_png_from_file "$icon" "$icons_temporary_directory"
	icons_include_from_directory "$package" "$application" "$icons_temporary_directory"
	rmdir "$icons_temporary_directory"
}

# Convert the given file into .png icons
# USAGE: icon_extract_png_from_file $icon $destination
icon_extract_png_from_file() {
	local icon destination
	icon="$1"
	destination="$2"

	local icon_file icon_type
	icon_file=$(icon_full_path "$icon")
	icon_type=$(file_type "$icon_file")
	case "$icon_type" in
		( \
			'application/vnd.microsoft.portable-executable' | \
			'application/x-dosexec' \
		)
			icon_extract_png_from_exe "$icon" "$destination"
		;;
		('image/png')
			icon_copy_png "$icon" "$destination"
		;;
		('image/vnd.microsoft.icon')
			icon_extract_png_from_ico "$icon" "$destination"
		;;
		( \
			'image/bmp' | \
			'image/x-ms-bmp' \
		)
			icon_convert_bmp_to_png "$icon" "$destination"
		;;
		( \
			'image/x-xpixmap' | \
			'image/x-xpmi' \
		)
			icon_copy_xpm "$icon" "$destination"
		;;
		(*)
			error_icon_unsupported_type "$icon_file" "$icon_type"
			return 1
		;;
	esac
}

# Extract .png file(s) from the given .exe file
# USAGE: icon_extract_png_from_exe $icon $destination
icon_extract_png_from_exe() {
	local icon destination
	icon="$1"
	destination="$2"

	# Extract the .ico file(s) for the given .exe file
	icon_extract_ico_from_exe "$icon" "$destination"

	# Extract the .png file(s) from each previously extracted .ico file
	local inner_icon_file content_path
	content_path=$(content_path_default)
	for inner_icon_file in "$destination"/*.ico; do
		(
			inner_icon_file_name=$(basename "$inner_icon_file")
			inner_icon_file_temporary_path="${PLAYIT_WORKDIR}/gamedata/${content_path}/${inner_icon_file_name}"
			mv "$inner_icon_file" "$inner_icon_file_temporary_path"
			export TMP_INNER_ICON="$inner_icon_file_name"
			icon_extract_png_from_ico 'TMP_INNER_ICON' "$destination"
			rm "$inner_icon_file_temporary_path"
		)
	done
}

# Extract .ico file(s) from the given .exe file
# USAGE: icon_extract_ico_from_exe $icon $destination
icon_extract_ico_from_exe() {
	local icon destination
	icon="$1"
	destination="$2"

	local icon_file wrestool_options
	icon_file=$(icon_full_path "$icon")
	wrestool_options=$(icon_wrestool_options "$icon")
	## Silence ShellCheck false-positive
	## Double quote to prevent globbing and word splitting.
	# shellcheck disable=SC2086
	wrestool $wrestool_options --extract --output="$destination" "$icon_file" 2>/dev/null

	# Check that at least one .ico file has been extracted, throw an error otherwise
	local ico_files_number
	ico_files_number=$(find "$destination" -name '*.ico' | wc --lines)
	if [ "$ico_files_number" -lt 1 ]; then
		error_no_ico_file_extracted "$icon"
		return 1
	fi
}

# Convert the given .bmp file to .png
# USAGE: icon_convert_bmp_to_png $icon $destination
icon_convert_bmp_to_png() {
	local icon destination
	icon="$1"
	destination="$2"

	icon_convert_to_png "$icon" "$destination"
}

# Extract .png file(s) from the given .ico file
# USAGE: icon_extract_png_from_ico $icon $destination
icon_extract_png_from_ico() {
	local icon destination
	icon="$1"
	destination="$2"

	icon_convert_to_png "$icon" "$destination"
}

# Convert multiple icon formats supported by ImageMagick to .png
# USAGE: icon_convert_to_png $icon $destination
icon_convert_to_png() {
	local icon destination
	icon="$1"
	destination="$2"

	local icon_file file_name
	icon_file=$(icon_full_path "$icon")
	file_name=$(basename "$icon_file")
	convert "$icon_file" "${destination}/${file_name%.*}.png"
}

# Copy the given .png file to the given directory
# USAGE: icon_copy_png $icon $destination
icon_copy_png() {
	local icon destination
	icon="$1"
	destination="$2"

	local icon_file
	icon_file=$(icon_full_path "$icon")
	cp "$icon_file" "$destination"
}

# Copy the given .xpm file to the given directory
# USAGE: icon_copy_xpm $icon $destination
icon_copy_xpm() {
	local icon destination
	icon="$1"
	destination="$2"

	local icon_file
	icon_file=$(icon_full_path "$icon")
	cp "$icon_file" "$destination"
}

# Get icon files from the given directory and put them in the given package
# USAGE: icons_include_from_directory $package $application $directory
icons_include_from_directory() {
	local package application source_directory
	package="$1"
	application="$2"
	source_directory="$3"

	local package_path path_icons
	package_path=$(package_path "$package")
	path_icons=$(path_icons)

	local application_id
	application_id=$(
		set_current_package "$package"
		application_id "$application"
	)

	# Get the icons from the given source directory, then move them to the given package
	local source_file icon_resolution destination_directory destination_path
	for source_file in \
		"$source_directory"/*.png \
		"$source_directory"/*.xpm
	do
		## Skip the current pattern if it matched no file.
		if [ ! -e "$source_file" ]; then
			continue
		fi

		icon_resolution=$(icon_get_resolution "$source_file")
		destination_directory="${package_path}${path_icons}/${icon_resolution}/apps"
		destination_path="${destination_directory}/${application_id}.${source_file##*.}"
		mkdir --parents "$destination_directory"
		mv "$source_file" "$destination_path"
	done
}

# Return the resolution of the given image file
# USAGE: icon_get_resolution $file
# RETURNS: image resolution, using the format ${width}x${height}
icon_get_resolution() {
	local image_file
	image_file="$1"

	# `identify` should be available when this function is called.
	# Exits with an explicit error if it is missing
	if ! command -v 'identify' >/dev/null 2>&1; then
		error_unavailable_command 'icon_get_resolution' 'identify'
		return 1
	fi

	local image_resolution_string image_resolution
	image_resolution_string=$(identify "$image_file" | sed "s;^${image_file} ;;" | cut --delimiter=' ' --fields=2)
	image_resolution="${image_resolution_string%+0+0}"

	printf '%s' "$image_resolution"
	return 0
}

# Error - An icon file could not be found
# USAGE: error_icon_file_not_found $file
error_icon_file_not_found() {
	local file
	file="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le fichier dʼicône suivant est introuvable : %s\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de gestion de bugs : %s\n'
		;;
		('en'|*)
			message='The following icon file could not be found: %s\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$file" \
		"$PLAYIT_GAMES_BUG_TRACKER_URL"
}

# Error - The path to the given icon is not set
# USAGE: error_icon_path_empty $icon
error_icon_path_empty() {
	local icon
	icon="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='%s nʼest pas défini, mais il y a eu une tentative de récupérer le chemin de cette icône.\n'
		;;
		('en'|*)
			message='%s is not set, but there has been a request for this icon path.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$icon"
}

# Error - An icon file with an unsupported MIME type has been provided
# USAGE: error_icon_unsupported_type $icon_file $icon_type
error_icon_unsupported_type() {
	local icon_file icon_type
	icon_file="$1"
	icon_type="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le fichier dʼicône suivant est du type MIME "%s", qui nʼest pas pris en charge : %s\n'
		;;
		('en'|*)
			message='The following icon file is of the "%s" MIME type, that is not supported: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$icon_type" \
		"$icon_file"
}

# Error - No application identifier could be found related to the given icon identifier
# USAGE: error_icon_application_not_found $icon_identifier
error_icon_application_not_found() {
	local icon_identifier
	icon_identifier="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='L’identifiant d’icône fourni ne semble pas correspondre à une des applications prises en charge : %s\n'
		;;
		('en'|*)
			message='The given icon identifier does not seem to related to any of the supported applications: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$icon_identifier"
}

# Error - No .ico file has been extracted from the given .exe file
# USAGE: error_no_ico_file_extracted $icon_identifier
error_no_ico_file_extracted() {
	local icon_identifier
	icon_identifier="$1"

	local icon_file wrestool_options
	icon_file=$(icon_full_path "$icon_identifier")
	wrestool_options=$(icon_wrestool_options "$icon_identifier")

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Aucun fichier d’icône n’a été extrait du fichier suivant : "%s"\n'
			message="$message"'Les options suivantes ont été passées à wrestool : %s\n'
		;;
		('en'|*)
			message='No icon file has been extracted from the following file: "%s"\n'
			message="$message"'The following options have been passed to wrestool: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$icon_file" \
		"$wrestool_options"
}

# Error - No icon is set for the given application
# USAGE: error_no_icon_found $application
error_no_icon_found() {
	local application
	application="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Aucune icône n’est définie pour l’application suivante : %s\n'
		;;
		('en'|*)
			message='No icon is set for the following application: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$application"
}

# print installation instructions
# USAGE: print_instructions $pkg[…]
print_instructions() {
	# If no explicit list of packages has been passed, fall back on handling all packages
	if [ $# -eq 0 ]; then
		local packages_list
		packages_list=$(packages_list)
		print_instructions $packages_list
		return 0
	fi

	# Print the list of runtime commands that have been skipped
	local unknown_commands
	unknown_commands=$(dependencies_unknown_commands_list)
	if [ -n "$unknown_commands" ]; then
		warning_dependencies_unknown_commands
		# Clear list of skipped libraries dependencies,
		# so it will not be shown again.
		dependencies_unknown_commands_clear
	fi

	# Print the list of library dependencies that have been skipped
	local unknown_libraries
	unknown_libraries=$(dependencies_unknown_libraries_list)
	if [ -n "$unknown_libraries" ]; then
		warning_dependencies_unknown_libraries
		# Clear list of skipped libraries dependencies,
		# so it will not be shown again.
		dependencies_unknown_libraries_clear
	fi

	# Print the list of Mono library dependencies that have been skipped
	local unknown_mono_libraries
	unknown_mono_libraries=$(dependencies_unknown_mono_libraries_list)
	if [ -n "$unknown_mono_libraries" ]; then
		warning_dependencies_unknown_mono_libraries
		# Clear list of skipped Mono libraries dependencies,
		# so it will not be shown again.
		dependencies_unknown_mono_libraries_clear
	fi

	# Print the list of GStreamer media format dependencies that have been skipped
	local unknown_gstreamer_media_formats
	unknown_gstreamer_media_formats=$(dependencies_unknown_gstreamer_media_formats_list)
	if [ -n "$unknown_gstreamer_media_formats" ]; then
		warning_dependencies_unknown_gstreamer_media_formats
		# Clear list of skipped media format dependencies,
		# so it will not be shown again.
		dependencies_unknown_gstreamer_media_formats_clear
	fi

	# Sort packages by architecture
	local package package_architecture packages_list_32 packages_list_64 packages_list_all
	packages_list_32=''
	packages_list_64=''
	packages_list_all=''
	for package in "$@"; do
		package_architecture=$(package_architecture "$package")
		case "$package_architecture" in
			('32')
				packages_list_32="$packages_list_32 $package"
			;;
			('64')
				packages_list_64="$packages_list_64 $package"
			;;
			(*)
				packages_list_all="$packages_list_all $package"
			;;
		esac
	done

	if [ -s "$(dependency_gentoo_overlays_file)" ]; then
		information_required_gentoo_overlays
	fi

	local option_package
	option_package=$(option_value 'package')
	if [ "$option_package" = 'egentoo' ]; then
		info_local_overlay_gentoo
	fi

	local game_name
	game_name=$(game_name)
	information_installation_instructions_common "$game_name"

	# If both 32-bit and 64-bit binaries packages are available,
	# display instructions on how to install one build or the other.
	# If only a single architecture is available, display standard instructions.
	if [ -n "$packages_list_32" ] && [ -n "$packages_list_64" ]; then
		print_instructions_architecture_specific '32' $packages_list_all $packages_list_32
		print_instructions_architecture_specific '64' $packages_list_all $packages_list_64
	else
		local option_package
		option_package=$(option_value 'package')
		case $option_package in
			('arch')
				print_instructions_arch "$@"
			;;
			('deb')
				debian_install_instructions "$@"
			;;
			('gentoo')
				print_instructions_gentoo "$@"
			;;
			('egentoo')
				print_instructions_egentoo "$@"
			;;
		esac
	fi
	printf '\n'
}

# print installation instructions, for a given architecture
# USAGE: print_instructions_architecture_specific $pkg[…]
print_instructions_architecture_specific() {
	local architecture_variant
	architecture_variant="${1}-bit"
	information_installation_instructions_variant "$architecture_variant"
	shift 1

	local option_package
	option_package=$(option_value 'package')
	case $option_package in
		('arch')
			print_instructions_arch "$@"
		;;
		('deb')
			debian_install_instructions "$@"
		;;
		('gentoo')
			print_instructions_gentoo "$@"
		;;
		('egentoo')
			print_instructions_egentoo "$@"
		;;
	esac
}

# print common part of packages installation instructions
# USAGE: information_installation_instructions_common $game_name
information_installation_instructions_common() {
	local game_name
	game_name="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\nInstallez "%s" en lançant la série de commandes suivantes en root :\n'
		;;
		('en'|*)
			message='\nInstall "%s" by running the following commands as root:\n'
		;;
	esac
	print_message 'info' "$message" \
		"$game_name"
}

# print variant precision for packages installation instructions
# USAGE: information_installation_instructions_variant $variant
information_installation_instructions_variant() {
	local variant
	variant="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\nversion %s :\n'
		;;
		('en'|*)
			message='\n%s version:\n'
		;;
	esac
	print_message 'info' "$message" \
		"$variant"
}

# Display a list of unknown runtime commands from packages dependencies
# USAGE: warning_dependencies_unknown_commands
warning_dependencies_unknown_commands() {
	local messages_language message1 message2
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message1='Certaines dépendances de ce jeu ne sont pas encore prises en charge par ./play.it'
			message1="$message1"', voici la liste de celles qui ont été ignorées :\n'
			message2='Merci de signaler cette liste sur notre système de suivi :\n%s\n'
		;;
		('en'|*)
			message1='Some dependencies of this game are not supported by ./play.it yet'
			message1="$message1"', here are the ones that have been skipped:\n'
			message2='Please report this list on our issues tracker:\n%s\n'
		;;
	esac
	print_message 'warning' "$message1"
	local unknown_command
	while read -r unknown_command; do
		print_message 'info' '- %s\n' \
			"$unknown_command"
	done <<- EOL
	$(dependencies_unknown_commands_list)
	EOL
	print_message 'info' "$message2" \
		"$PLAYIT_BUG_TRACKER_URL"
}

# Display a list of unknown libraries from packages dependencies
# USAGE: warning_dependencies_unknown_libraries
warning_dependencies_unknown_libraries() {
	local messages_language message1 message2
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message1='Certaines dépendances de ce jeu ne sont pas encore prises en charge par ./play.it'
			message1="$message1"', voici la liste de celles qui ont été ignorées :\n'
			message2='Merci de signaler cette liste sur notre système de suivi :\n%s\n'
		;;
		('en'|*)
			message1='Some dependencies of this game are not supported by ./play.it yet'
			message1="$message1"', here are the ones that have been skipped:\n'
			message2='Please report this list on our issues tracker:\n%s\n'
		;;
	esac
	print_message 'warning' "$message1"
	local unkown_library
	while read -r unkown_library; do
		print_message 'info' '- %s\n' \
			"$unkown_library"
	done <<- EOL
	$(dependencies_unknown_libraries_list)
	EOL
	print_message 'info' "$message2" \
		"$PLAYIT_BUG_TRACKER_URL"
}

# Display a list of unknown Mono libraries from packages dependencies
# USAGE: warning_dependencies_unknown_mono_libraries
warning_dependencies_unknown_mono_libraries() {
	local messages_language message1 message2
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message1='Certaines dépendances Mono de ce jeu ne sont pas encore prises en charge par ./play.it'
			message1="$message1"', voici la liste de celles qui ont été ignorées :\n'
			message2='Merci de signaler cette liste sur notre système de suivi :\n%s\n'
		;;
		('en'|*)
			message1='Some Mono dependencies of this game are not supported by ./play.it yet'
			message1="$message1"', here are the ones that have been skipped:\n'
			message2='Please report this list on our issues tracker:\n%s\n'
		;;
	esac
	print_message 'warning' "$message1"
	local unkown_mono_library
	while read -r unkown_mono_library; do
		print_message 'info' '- %s\n' \
			"$unkown_mono_library"
	done <<- EOL
	$(dependencies_unknown_mono_libraries_list)
	EOL
	print_message 'info' "$message2" \
		"$PLAYIT_BUG_TRACKER_URL"
}

# Display a list of unknown GStreamer media formats from packages dependencies
# USAGE: warning_dependencies_unknown_gstreamer_media_formats
warning_dependencies_unknown_gstreamer_media_formats() {
	local messages_language message1 message2
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message1='Certains formats multimédia requis par ce jeu ne sont pas encore pris en charge par ./play.it'
			message1="$message1"', voici la liste de ceux qui ont été ignorés :\n'
			message2='Merci de signaler cette liste sur notre système de suivi :\n%s\n'
		;;
		('en'|*)
			message1='Some media formats required by this game are not supported by ./play.it yet'
			message1="$message1"', here are the ones that have been skipped:\n'
			message2='Please report this list on our issues tracker:\n%s\n'
		;;
	esac
	print_message 'warning' "$message1"
	local media_format
	while read -r media_format; do
		print_message 'info' '- %s\n' \
			"$media_format"
	done <<- EOL
	$(dependencies_unknown_gstreamer_media_formats_list)
	EOL
	print_message 'info' "$message2" \
		"$PLAYIT_BUG_TRACKER_URL"
}

# Write launcher scripts and menu entries.
#
# This function can take several applications as its arguments,
# and default to handle all applications if none are explicitely given.
#
# USAGE: launchers_generation [$package [$application…]]
launchers_generation() {
	local package
	if [ $# -ge 1 ]; then
		package="$1"
		shift 1
	else
		package=$(current_package)
	fi

	# If no applications are explicitely listed,
	# write launchers and menu entries for all applications.
	if [ "$#" -eq 0 ]; then
		applications_list=$(applications_list)
		## If launchers_generation has been called with no argument, the applications list should not be empty.
		if [ -z "$applications_list" ]; then
			error_applications_list_empty
			return 1
		fi
		launchers_generation "$package" $applications_list
		return 0
	fi

	information_launchers_generation

	local application
	for application in "$@"; do
		launcher_generation_checks "$package" "$application"
		launcher_write_script "$package" "$application"
		launcher_write_desktop "$package" "$application"
	done
}

# Run checks to ensure all required information for the launcher generation is set
# USAGE: launcher_generation_checks $package $application
launcher_generation_checks() {
	local package application
	package="$1"
	application="$2"

	# Ensure that the application type is set, or can be guessed
	local application_type
	application_type=$(
		set_current_package "$package"
		application_type "$application"
	)
	if [ -z "$application_type" ]; then
		error_no_application_type "$application"
		return 1
	fi

	# If the current application type relies on a game binary, ensure its path is set
	case "$application_type" in
		('custom')
			## Custom launchers might not rely on a provided binary.
		;;
		('renpy')
			## Ren'Py games do not rely on a provided binary.
		;;
		('scummvm')
			## ScummVM games do not rely on a provided binary,
			## but they expect a ScummVM game ID to be set.
			local application_scummid
			application_scummid=$(application_scummvm_scummid "$application")
			if [ -z "$application_scummid" ]; then
				error_application_scummid_invalid "$application" "$application_scummid"
				return 1
			fi
		;;
		(*)
			local application_exe
			application_exe=$(
				set_current_package "$package"
				application_exe "$application"
			)
			if [ -z "$application_exe" ]; then
				error_application_exe_empty "$application" 'launcher_generation_checks'
				return 1
			fi
		;;
	esac

	# If the current application type relies on a game binary, ensure that it can be found
	if [ -n "${application_exe:-}" ]; then
		## A dedicated function is used here to make it easier to override from game scripts.
		if ! launcher_target_presence_check "$package" "$application"; then
			error_launcher_missing_binary "$application_exe"
			return 1
		fi
	fi
}

# Check that the launcher target exists
# USAGE: launcher_target_presence_check $package $application
# RETURN: 0 if the game binary has been found,
#         1 if the game binary has not been found
launcher_target_presence_check() {
	local package application
	package="$1"
	application="$2"

	local application_exe application_exe_path
	application_exe=$(
		set_current_package "$package"
		application_exe "$application"
	)
	application_exe_path=$(
		set_current_package "$package"
		application_exe_path "$application_exe"
	)
	test -f "$application_exe_path"
}

# Write the launcher script for the given application.
# USAGE: launcher_write_script $package $application
launcher_write_script() {
	local package application
	package="$1"
	application="$2"

	local launcher_path launcher_directory
	launcher_path=$(launcher_path "$package" "$application")
	launcher_directory=$(dirname "$launcher_path")
	mkdir --parents "$launcher_directory"
	touch "$launcher_path"
	chmod 755 "$launcher_path"

	## The *_launcher functions are called on their own first, to avoid being spawned in a subshell. This prevents their return code from being lost.
	## The package context is always set to ensure context-sensitive values for runtime options are used.
	local application_type launcher_content
	application_type=$(
		set_current_package "$package"
		application_type "$application"
	)
	case "$application_type" in
		('custom')
			launcher_content=$(
				set_current_package "$package"
				custom_launcher "$application"
			)
			printf '%s' "$launcher_content" | snippet_clean > "$launcher_path"
		;;
		('dosbox')
			launcher_content=$(
				set_current_package "$package"
				dosbox_launcher "$application"
			)
			printf '%s' "$launcher_content" | snippet_clean > "$launcher_path"
			dependencies_add_command "$package" 'dosbox'
		;;
		('java')
			launcher_content=$(
				set_current_package "$package"
				java_launcher "$application"
			)
			printf '%s' "$launcher_content" | snippet_clean > "$launcher_path"
			dependencies_add_command "$package" 'java'
		;;
		('mono')
			launcher_content=$(
				set_current_package "$package"
				mono_launcher "$application"
			)
			printf '%s' "$launcher_content" | snippet_clean > "$launcher_path"
			dependencies_add_command "$package" 'mono'
		;;
		('native')
			launcher_content=$(
				set_current_package "$package"
				native_launcher "$application"
			)
			printf '%s' "$launcher_content" | snippet_clean > "$launcher_path"
			## Add execution permissions to the game binary file.
			local application_exe application_exe_path
			application_exe=$(
				set_current_package "$package"
				application_exe "$application"
			)
			application_exe_path=$(
				set_current_package "$package"
				application_exe_path "$application_exe"
			)
			chmod +x "$application_exe_path"
		;;
		('renpy')
			launcher_content=$(
				set_current_package "$package"
				renpy_launcher "$application"
			)
			printf '%s' "$launcher_content" | snippet_clean > "$launcher_path"
			dependencies_add_command "$package" 'renpy'
		;;
		('scummvm')
			launcher_content=$(
				set_current_package "$package"
				scummvm_launcher "$application"
			)
			printf '%s' "$launcher_content" | snippet_clean > "$launcher_path"
			dependencies_add_command "$package" 'scummvm'
		;;
		('wine')
			launcher_content=$(
				set_current_package "$package"
				wine_launcher "$application"
			)
			printf '%s' "$launcher_content" | snippet_clean > "$launcher_path"
			dependencies_add_command "$package" 'wine'
			## Add a package dependency on winetricks if the current game relies on some winetricks verbs.
			local winetricks_verbs
			winetricks_verbs=$(wine_winetricks_verbs)
			if [ -n "$winetricks_verbs" ]; then
				dependencies_add_command "$package" 'winetricks'
			fi
			## Add package dependencies on winetricks and rendering libraries if a non-default Direct3D renderer is required.
			local direct3d_renderer
			direct3d_renderer=$(wine_renderer_name)
			case "$direct3d_renderer" in
				('wined3d/'*)
					dependencies_add_command "$package" 'winetricks'
					local wined3d_backend
					wined3d_backend=$(printf '%s' "$direct3d_renderer" | cut --delimiter='/' --fields=2)
					case "$wined3d_backend" in
						('gl')
							dependencies_add_native_libraries "$package" 'libGL.so.1'
						;;
						('vulkan')
							dependencies_add_native_libraries "$package" 'libvulkan.so.1'
						;;
					esac
				;;
				('dxvk')
					dependencies_add_command "$package" 'winetricks'
					dependencies_add_native_libraries "$package" 'libvulkan.so.1'
				;;
				('vkd3d')
					dependencies_add_command "$package" 'winetricks'
					dependencies_add_native_libraries "$package" 'libvulkan.so.1'
				;;
			esac
		;;
	esac
}

# Print the path to the launcher script for the given application.
# USAGE: launcher_path $package $application
# RETURN: The absolute path to the launcher
launcher_path() {
	local package application
	package="$1"
	application="$2"

	local package_path path_binaries application_id
	package_path=$(package_path "$package")
	path_binaries=$(path_binaries)
	application_id=$(
		set_current_package "$package"
		application_id "$application"
	)

	printf '%s%s/%s' "$package_path" "$path_binaries" "$application_id"
}

# Print the headers common to all launcher scripts
# USAGE: launcher_headers
launcher_headers() {
	cat <<- EOF
	#!/bin/sh
	# script generated by ./play.it $LIBRARY_VERSION - https://www.dotslashplay.it/
	set -o errexit

	EOF
}

# Print the exit actions common to all launcher scripts
# USAGE: launcher_exit
launcher_exit() {
	cat <<- 'EOF'
	# Return the game exit code

	if [ -n "$game_exit_status" ]; then
	    exit "$game_exit_status"
	else
	    exit 0
	fi
	EOF
}

# Print the line starting the game
# USAGE: game_exec_line $application
# RETURN: the command to execute, including its command line options
game_exec_line() {
	local application
	application="$1"

	local application_type
	application_type=$(application_type "$application")
	case "$application_type" in
		('dosbox')
			dosbox_exec_line "$application"
		;;
		('java')
			java_exec_line "$application"
		;;
		('mono')
			mono_exec_line "$application"
		;;
		('native')
			native_exec_line "$application"
		;;
		('renpy')
			renpy_exec_line "$application"
		;;
		('scummvm')
			scummvm_exec_line "$application"
		;;
		('wine')
			wine_exec_line "$application"
		;;
	esac
}

# Write the XDG desktop file for the given application
# USAGE: launcher_write_desktop $package $application
launcher_write_desktop() {
	local package application
	package="$1"
	application="$2"

	local desktop_file desktop_directory
	desktop_file=$(launcher_desktop_filepath "$package" "$application")
	desktop_directory=$(dirname "$desktop_file")
	mkdir --parents "$desktop_directory"
	launcher_desktop "$application" > "$desktop_file"
}

# Print the content of the XDG desktop file for the given application
# USAGE: launcher_desktop $application
# RETURN: the full content of the XDG desktop file
launcher_desktop() {
	local application
	application="$1"

	local application_name application_category desktop_field_exec desktop_field_icon
	application_name=$(application_name "$application")
	application_category=$(application_category "$application")
	desktop_field_exec=$(desktop_field_exec "$application")
	desktop_field_icon=$(desktop_field_icon "$application")

	cat <<- EOF
	[Desktop Entry]
	Version=1.0
	Type=Application
	Name=$application_name
	Icon=$desktop_field_icon
	Exec=$desktop_field_exec
	Categories=$application_category
	EOF
}

# Print the full path to the XDG desktop file for the given application
# USAGE: launcher_desktop_filepath $package $application
# RETURN: an absolute file path
launcher_desktop_filepath() {
	local package application
	package="$1"
	application="$2"

	local application_id package_path path_xdg_desktop
	application_id=$(application_id "$application")
	package_path=$(package_path "$package")
	path_xdg_desktop=$(path_xdg_desktop)

	printf '%s/%s.desktop' \
		"${package_path}${path_xdg_desktop}" \
		"$application_id"
}

# Print the XDG desktop "Exec" field for the given application
# USAGE: desktop_field_exec $application
# RETURN: The "Exec" field content, including escaping if required
desktop_field_exec() {
	local application
	application="$1"

	local option_prefix application_id exec_field
	option_prefix=$(option_value 'prefix')
	application_id=$(application_id "$application")
	case "$option_prefix" in
		## Standard path, only the command name is required.
		('/usr'|'/usr/local')
			exec_field="$application_id"
		;;
		## Non-standard path including spaces, the full path enclosed in quotes is required.
		(*' '*)
			local path_binaries
			path_binaries=$(path_binaries)
			exec_field="'${path_binaries}/${application_id}'"
		;;
		## Non-standard path not including spaces, the full path is required.
		(*)
			local path_binaries
			path_binaries=$(path_binaries)
			exec_field="${path_binaries}/${application_id}"
		;;
	esac

	printf '%s' "$exec_field"
}

# Print the XDG desktop "Icon" field for the given application
# USAGE: desktop_field_icon $application
# RETURN: The "Icon" field content
desktop_field_icon() {
	local application
	application="$1"

	local icon_field
	icon_field=$(application_id "$application")

	printf '%s' "$icon_field"
}

# Enable the fake $HOME
# USAGE: fake_home_enable
fake_home_enable() {
	cat <<- 'EOF'
	# Enable the fake $HOME
	HOME_PATH_REAL="$HOME"
	HOME_PATH_FAKE=$(fake_home_path)
	export HOME="$HOME_PATH_FAKE"

	EOF
}

# Disable the fake $HOME
# USAGE: fake_home_disable
fake_home_disable() {
	cat <<- 'EOF'
	# Disable the fake $HOME
	export HOME="$HOME_PATH_REAL"

	EOF
}

# Print the paths relative to the fake $HOME that should be diverted to persistent storage
# USAGE: fake_home_persistent_directories
# RETURN: A list of path to directories,
#         separated by line breaks.
fake_home_persistent_directories() {
	local persistent_directories
	persistent_directories=$(context_value 'FAKE_HOME_PERSISTENT_DIRECTORIES')
	printf '%s' "$persistent_directories"
}

# Handle paths diversion from the fake $HOME to persistent storage
# USAGE: fake_home_persistent
fake_home_persistent() {
	local persistent_directories
	persistent_directories=$(fake_home_persistent_directories)
	cat <<- 'EOF'
	# Divert paths from the fake $HOME to persistent storage

	fake_home_path() {
	    # Fake $HOME path can be explicitely set using an environment variable
	    if [ -n "$PLAYIT_FAKE_HOME_PATH" ]; then
	        printf '%s' "$PLAYIT_FAKE_HOME_PATH"
	        return 0
	    fi
	    # Compute the default fake $HOME path if none has been explicitely set
	    printf '%s/play.it/home/%s' \
	        "${XDG_CACHE_HOME:="$HOME/.cache"}" \
	        "$GAME_ID"
	}
	FAKE_HOME_PATH=$(fake_home_path)

	## Divert paths set by the XDG Base Directory Specification
	## cf. https://specifications.freedesktop.org/basedir-spec/basedir-spec-0.8.html
	while read -r xdg_path_absolute; do
	    if printf '%s' "$xdg_path_absolute" | grep --quiet --regexp="^${HOME}/"; then
	        xdg_path_relative=$(printf '%s' "$xdg_path_absolute" | sed "s#^${HOME}/##")
	        persistent_path_diversion "$FAKE_HOME_PATH" "$HOME" "$xdg_path_relative"
	    fi
	done << EOL
	${XDG_CACHE_HOME:-${HOME}/.cache}
	${XDG_CONFIG_HOME:-${HOME}/.config}
	${XDG_DATA_HOME:-${HOME}/.local/share}
	${XDG_STATE_HOME:-${HOME}/.local/state}
	EOL
	unset xdg_path_absolute

	EOF
	cat <<- EOF
	## Divert paths specific to the current game
	FAKE_HOME_PERSISTENT_DIRECTORIES="$persistent_directories"
	EOF
	cat <<- 'EOF'
	while read -r directory; do
	    if [ -z "$directory" ]; then
	        continue
	    fi
	    persistent_path_diversion "$FAKE_HOME_PATH" "${USER_PERSISTENT_PATH}/fake-home" "$directory"
	done << EOL
	$(printf '%s' "$FAKE_HOME_PERSISTENT_DIRECTORIES")
	EOL
	unset directory

	EOF
}

# Print function computing the path to the game prefix
# USAGE: prefix_function_prefix_path
prefix_function_prefix_path() {
	cat <<- 'EOF'
	# Compute the path to game prefix for the current session
	prefix_path() {
	    # Prefix path can be explicitely set using an environment variable
	    if [ -n "$PLAYIT_PREFIX_PATH" ]; then
	        printf '%s' "$PLAYIT_PREFIX_PATH"
	        return 0
	    fi
	    # Compute the default prefix path if none has been explicitely set
	    printf '%s/play.it/prefixes/%s' \
	        "${XDG_CACHE_HOME:="$HOME/.cache"}" \
	        "$GAME_ID"
	}

	EOF
}

# Populate prefix with symbolic links to all game files
# USAGE: prefix_generate_links_farm
prefix_generate_links_farm() {
	cat <<- 'EOF'
	# Populate prefix with symbolic links to all game files
	prefix_generate_links_farm() {
	    ## Remove links to game directories
	    (
	        cd "$PATH_GAME"
	        find . -type d | while read -r directory; do
	            if [ -h "${PATH_PREFIX}/${directory}" ]; then
	                rm "${PATH_PREFIX}/${directory}"
	            fi
	        done
	        unset directory
	    )

	    ## Populate prefix with links to all game files.
	    cp \
	        --dereference --no-target-directory --recursive --remove-destination --symbolic-link \
	        "$PATH_GAME" "$PATH_PREFIX"

	    ## Remove dangling links and non-game empty directories.
	    (
	        cd "$PATH_PREFIX"
	        find . -type l | while read -r link; do
	            if [ ! -e "$link" ]; then
	                rm "$link"
	            fi
	        done
	        find . -depth -type d | while read -r directory; do
	            if [ ! -e "${PATH_GAME}/${directory}" ]; then
	                rmdir --ignore-fail-on-non-empty "$directory"
	            fi
	        done
	        unset link directory
	    )
	}

	EOF
}

# Print the functions used to generate a symlinks prefix
# USAGE: launcher_prefix_symlinks_functions
launcher_prefix_symlinks_functions() {
		cat <<- 'EOF'
		# Set userdir- and prefix-related functions
		EOF
		prefix_function_prefix_path
		prefix_generate_links_farm
}

# Print the actions used to build a symlinks prefix
# USAGE: launcher_prefix_symlinks_build
launcher_prefix_symlinks_build() {
	cat  <<- 'EOF'
	# Build user prefix

	PATH_PREFIX=$(prefix_path)
	mkdir --parents "$PATH_PREFIX"
	prefix_generate_links_farm

	EOF
}

# List the directories from the game prefix that should be diverted to a persistent path
# USAGE: persistent_list_directories
# RETURNS: a list of paths to directories, separated by line breaks
#          glob patterns can be included
persistent_list_directories() {
	local persistent_directories
	persistent_directories=$(context_value 'USER_PERSISTENT_DIRECTORIES')

	# Fall back on the legacy CONFIG_DIRS / DATA_DIRS variables.
	if [ -z "$persistent_directories" ]; then
		persistent_directories=$(persistent_list_directories_legacy)
	fi

	printf '%s' "$persistent_directories" | list_clean
}

# List the files from the game prefix that should be diverted to a persistent path
# USAGE: persistent_list_files
# RETURNS: a list of paths to files, separated by line breaks
#          glob patterns can be included
persistent_list_files() {
	local persistent_files
	persistent_files=$(context_value 'USER_PERSISTENT_FILES')

	# Fall back on the legacy CONFIG_FILES / DATA_FILES variables.
	if [ -z "$persistent_files" ]; then
		persistent_files=$(persistent_list_files_legacy)
	fi

	printf '%s' "$persistent_files" | list_clean
}

# Set path for persistent storage of user data, and populate the game prefix from the persistent storage
# USAGE: persistent_storage_initialization
persistent_storage_initialization() {
	cat <<- 'EOF'
	# Set path for persistent storage of user data
	persistent_user_path() {
	    ## The path can be explicitely set using an environment variable
	    if [ -n "$PLAYIT_PERSISTENT_USER_PATH" ]; then
	        printf '%s' "$PLAYIT_PERSISTENT_USER_PATH"
	        return 0
	    fi

	    ## Compute the default path if none has been explicitly set
	    printf '%s/games/%s' \
	        "${XDG_DATA_HOME:=$HOME/.local/share}" \
	        "$GAME_ID"
	}
	USER_PERSISTENT_PATH=$(persistent_user_path)
	mkdir --parents "$USER_PERSISTENT_PATH"

	# Populate the prefix from persistent files
	(
	    cd "$USER_PERSISTENT_PATH"
	    find -L . -type f ! -path './wine/*' | while read -r file; do
	        persistent_file="${USER_PERSISTENT_PATH}/${file}"
	        prefix_file="${PATH_PREFIX}/${file}"
	        if \
	            [ ! -e "$prefix_file" ] || \
	            [ "$(realpath "$prefix_file")" != "$(realpath "$persistent_file")" ]
	        then
	            mkdir --parents "$(dirname "$prefix_file")"
	            ln --symbolic --force --no-target-directory \
	                "$persistent_file" \
	                "$prefix_file"
	        fi
	    done
	    unset file persistent_file prefix_file
	)

	EOF
}

# Set the common actions required for directories and files diversion to persistent storage
# This is required for the diversions using one of the following variables:
# - USER_PERSISTENT_DIRECTORIES
# - USER_PERSISTENT_FILES
# USAGE: persistent_storage_common
persistent_storage_common() {
	local persistent_list_directories persistent_list_files
	persistent_list_directories=$(persistent_list_directories)
	persistent_list_files=$(persistent_list_files)

	# Return early if the current game script does not use paths diversion
	if \
		[ -z "$persistent_list_directories" ] && \
		[ -z "$persistent_list_files" ]
	then
		return 0
	fi

	cat <<- 'EOF'
	# Expand a path pattern into a list of existing paths.
	# If the pattern can not be expanded, it is printed as-is instead.
	expand_path_pattern() {
	    pattern="$1"

	    ## Silently skip empty patterns
	    if [ -z "$pattern" ]; then
	        return 0
	    fi

	    expanded_paths=$(find . -path "./${pattern#./}")
	    if [ -n "$expanded_paths" ]; then
	        printf '%s\n' "$expanded_paths"
	    else
	        printf '%s\n' "$pattern"
	    fi

	    unset pattern expanded_paths
	}

	EOF
}

# Set the action used to divert a given path to persistent storage
# This is required for the diversions using one of the following variables:
# - FAKE_HOME_PERSISTENT_DIRECTORIES
# - USER_PERSISTENT_DIRECTORIES
# - WINE_PERSISTENT_DIRECTORIES
# USAGE: persistent_path_diversion
persistent_path_diversion() {
	local fake_home_persistent_directories user_persistent_directories wine_persistent_directories
	fake_home_persistent_directories=$(fake_home_persistent_directories)
	user_persistent_directories=$(persistent_list_directories)
	wine_persistent_directories=$(wine_persistent_directories)

	# Return early if the current game script does not use paths diversion
	if \
		[ -z "$fake_home_persistent_directories" ] && \
		[ -z "$user_persistent_directories" ] && \
		[ -z "$wine_persistent_directories" ]
	then
		return 0
	fi

	cat <<- 'EOF'
	# Replace a given directory in a prefix by a link to another directory in persistent storage
	# USAGE: persistent_path_diversion $path_source $path_destination $directory
	persistent_path_diversion() {
	    path_source="$1"
	    path_destination="$2"
	    directory="$3"

	    ## If the target directory does not already exist in persistent storage,
	    ## copy it from the prefix (if existing) or create a new empty one.
	    if [ ! -e "${path_destination}/${directory}" ]; then
	        if [ -e "${path_source}/${directory}" ]; then
	            (
	                cd "$path_source"
	                cp --dereference --parents --recursive \
	                    "$directory" \
	                    "$path_destination"
	            )
	        else
	            mkdir --parents "${path_destination}/${directory}"
	        fi
	    fi

	    ## Replace the directory in the prefix by a link to the one in persistent storage.
	    if [ ! -h "${path_source}/${directory}" ]; then
	        directory_parent=$(dirname "${path_source}/${directory}")
	        rm --recursive --force "${path_source:?}/${directory}"
	        mkdir --parents "$directory_parent"
	        ln --symbolic "${path_destination}/${directory}" "${path_source}/${directory}"
	    fi
	    unset directory_parent

	    unset path_source path_destination directory
	}

	EOF
}

# Update directories diversions to persistent storage
# USAGE: persistent_storage_update_directories
persistent_storage_update_directories() {
	local persistent_list_directories
	persistent_list_directories=$(persistent_list_directories)

	# Return early if the current game script does not use directories diversion
	if [ -z "$persistent_list_directories" ]; then
		return 0
	fi

	cat <<- EOF
	# Update directories diversions to persistent storage
	USER_PERSISTENT_DIRECTORIES='${persistent_list_directories}'
	EOF
	cat <<- 'EOF'
	(
	    cd "$PATH_PREFIX"
	    while read -r directory_pattern; do
	        ## Skip empty patterns
	        if [ -z "$directory_pattern" ]; then
	            continue
	        fi

	        while read -r directory; do
	            persistent_path_diversion "$PATH_PREFIX" "$USER_PERSISTENT_PATH" "$directory"
	        done <<- EOL
	        $(expand_path_pattern "$directory_pattern")
	        EOL
	    done <<- EOL
	    $(printf '%s' "$USER_PERSISTENT_DIRECTORIES")
	    EOL
	    unset directory_pattern
	)

	EOF
}

# Update files diversions to persistent storage
# USAGE: persistent_storage_update_files
persistent_storage_update_files() {
	local persistent_list_files
	persistent_list_files=$(persistent_list_files)

	# Return early if the current game script does not use files diversion
	if [ -z "$persistent_list_files" ]; then
		return 0
	fi

	cat <<- EOF
	# Update files diversions to persistent storage
	USER_PERSISTENT_FILES='${persistent_list_files}'
	EOF
	cat <<- 'EOF'
	(
	    cd "$PATH_PREFIX"
	    while read -r file_pattern; do
	        ## Skip empty patterns
	        if [ -z "$file_pattern" ]; then
	            continue
	        fi

	        while read -r file; do
	            ## If the target file does not already exist in persistent storage,
	            ## copy it from the prefix (if existing).
	            if [ ! -e "${USER_PERSISTENT_PATH}/${file}" ]; then
	                if [ -e "$file" ]; then
	                    cp --dereference --parents \
	                        "$file" \
	                        "${USER_PERSISTENT_PATH}"
	                fi
	            fi

	            ## Replace the file in the prefix by a link to the one in persistent storage,
	            ## if such a file already exists in persistent storage.
	            if [ -e "${USER_PERSISTENT_PATH}/${file}" ]; then
	                file_parent=$(dirname "$file")
	                rm --force "$file"
	                mkdir --parents "$file_parent"
	                ln --symbolic "${USER_PERSISTENT_PATH}/${file}" "$file"
	            fi
	        done <<- EOL
	        $(expand_path_pattern "$file_pattern")
	        EOL
	        unset file
	    done <<- EOL
	    $(printf '%s' "$USER_PERSISTENT_FILES")
	    EOL
	    unset file_pattern
	)

	EOF
}

# Update persistent storage with files from the current prefix
# USAGE: persistent_storage_update_files_from_prefix
persistent_storage_update_files_from_prefix() {
	local persistent_list_files
	persistent_list_files=$(persistent_list_files)

	# Return early if the current game script does not use files diversion
	if [ -z "$persistent_list_files" ]; then
		return 0
	fi

	cat <<- 'EOF'
	# Update persistent storage with files from the current prefix
	(
	    cd "$PATH_PREFIX"
	    while read -r path_pattern; do
	        ## Skip empty patterns
	        if [ -z "$path_pattern" ]; then
	            continue
	        fi

	        while read -r path; do
	            if [ -f "$path" ] && [ ! -h "$path" ]; then
	                cp --parents --remove-destination "$path" "$USER_PERSISTENT_PATH"
	                rm --force "$path"
	                ln --symbolic "${USER_PERSISTENT_PATH}/${path}" "$path"
	            fi
	        done <<- EOL
	        $(expand_path_pattern "$path_pattern")
	        EOL
	        unset path
	    done <<- EOL
	    $(printf '%s' "$USER_PERSISTENT_FILES")
	    EOL
	    unset path_pattern
	)

	EOF
}

# Information: Launcher scripts and desktop entries are being written
# USAGE: information_launchers_generation
information_launchers_generation() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Génération des lanceurs…\n'
		;;
		('en'|*)
			message='Launchers generation…\n'
		;;
	esac
	print_message 'info_once' "$message"
}

# Error - A binary file is missing
# USAGE: error_launcher_missing_binary $binary
# CALLS: print_error
error_launcher_missing_binary() {
	local binary
	binary="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le fichier suivant est introuvable, mais la création dʼun lanceur pour celui-ci a été demandée : %s\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de gestion de bugs : %s\n'
		;;
		('en'|*)
			message='The following file can not be found, but a launcher targeting it should have been created: %s\n'
			message="$message"'Please report this issue on our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$binary" \
		"$PLAYIT_GAMES_BUG_TRACKER_URL"
}

# Error - The requested prefix type is not compatible with the given application type
# USAGE: error_launchers_prefix_type_unsupported $application
error_launchers_prefix_type_unsupported() {
	local application
	application="$1"

	local application_type prefix_type
	application_type=$(application_type "$application")
	if [ -z "$application_type" ]; then
		error_no_application_type "$application"
		return 1
	fi
	prefix_type=$(application_prefix_type "$application")

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le type de préfixe "%s" ne peut pas être utilisé pour une application du type "%s".\n'
			message="$message"'Merci de signaler cette erreur sur notre outil de suivi des problèmes : %s\n'
		;;
		('en'|*)
			message='Prefix type "%s" can not be used with application type "%s".\n'
			message="$message"'Please report this issue in our bug tracker: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$prefix_type" \
		"$application_type" \
		"$PLAYIT_GAMES_BUG_TRACKER_URL"
}

# Generate packages from the given list
# USAGE: packages_generation $package[…]
packages_generation() {
	# If not explicit packages list is given, generate all packages
	if [ $# -eq 0 ]; then
		local packages_list
		packages_list=$(packages_list)
		packages_generation $packages_list
		return 0
	fi

	information_packages_generation

	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			archlinux_packages_metadata "$@"
			archlinux_packages_build "$@"
		;;
		('deb')
			debian_packages_metadata "$@"
			debian_packages_build "$@"
		;;
		('gentoo')
			gentoo_packages_metadata "$@"
			gentoo_packages_build "$@"
		;;
		('egentoo')
			egentoo_packages_metadata "$@"
			egentoo_packages_build "$@"
		;;
	esac
}

# Print the full list of packages that should be built from the current archive
# If no value is set to PACKAGES_LIST or some archive-specific variant of PACKAGES_LIST,
# the following default value is returned: "PKG_MAIN".
# USAGE: packages_list
# RETURN: a list of package identifiers,
#         separated by line breaks
packages_list() {
	# WARNING - most context-related functions can not be used here,
	#           because current_package relies on the current function.

	local packages_list packages_list_variable
	packages_list_variable=$(context_name_archive 'PACKAGES_LIST')
	if [ -n "${packages_list_variable:-}" ]; then
		packages_list=$(get_value "$packages_list_variable")
	else
		packages_list="${PACKAGES_LIST:-}"
	fi

	# Fall back on the default packages list for the current game engine
	if [ -z "${packages_list:-}" ]; then
		## WARNING: game_engine can not be called here, bacause it relies on context_value.
		local game_engine game_engine_variable
		game_engine_variable=$(context_name_archive 'GAME_ENGINE')
		if [ -n "${game_engine_variable:-}" ]; then
			game_engine=$(get_value "$game_engine_variable")
		else
			game_engine="${GAME_ENGINE:-}"
		fi
		if [ -z "$game_engine" ]; then
			local visionaire_name visionaire_name_variable
			visionaire_name_variable=$(context_name_archive 'VISIONAIRE_NAME')
			if [ -n "$visionaire_name_variable" ]; then
				visionaire_name=$(get_value "$visionaire_name_variable")
			else
				visionaire_name="${VISIONAIRE_NAME:-}"
			fi
			if [ -n "$visionaire_name" ]; then
				game_engine='visionaire'
			fi
		fi
		case "$game_engine" in
			('visionaire')
				packages_list=$(visionaire_packages_list)
			;;
		esac
	fi

	# Fall back of the default packages list (a single package identified by "PKG_MAIN")
	if [ -z "${packages_list:-}" ]; then
		packages_list='PKG_MAIN'
	fi

	local package
	for package in $packages_list; do
		printf '%s\n' "$package"
	done
}

# Check if the given package is included in the list of packages that should be built
# USAGE: package_is_included_in_packages_list $package
# RETURN: 0 if the package is included, 1 if it is not
package_is_included_in_packages_list() {
	local package
	package="$1"

	local packages_list
	packages_list=$(packages_list)

	printf '%s' "$packages_list" | \
		grep --quiet --fixed-strings --word-regexp "$package"
}

# Print the list of the packages that would be generated from the given archive.
# USAGE: packages_print_list $archive
# RETURN: a list of package file names, one per line
packages_print_list() {
	local archive
	archive="$1"

	local option_package PLAYIT_CONTEXT_ARCHIVE
	option_package=$(option_value 'package')
	set_current_archive "$archive"
	case "$option_package" in
		('egentoo')
			local package_name
			package_name=$(egentoo_package_name)
			printf '%s\n' "$package_name"
		;;
		(*)
			local packages_list package package_name
			packages_list=$(packages_list)
			for package in $packages_list; do
				package_name=$(package_name "$package")
				printf '%s\n' "$package_name"
			done
		;;
	esac
}

# Print the id of the given package
# USAGE: package_id $package
# RETURNS: the package id, as a non-empty string
package_id() {
	local package
	package="$1"

	local package_id
	package_id=$(context_value "${package}_ID")

	# Fall back on the default package id for the current game engine
	if [ -z "$package_id" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('visionaire')
				package_id=$(visionaire_package_id "$package")
			;;
		esac
	fi

	# Fall back on the game id if no package id is explicitly set
	if [ -z "$package_id" ]; then
		## We need to explicitly set the context here,
		## because the value of GAME_ID might be specific to the current package.
		package_id=$(
			set_current_package "$package"
			game_id
		)
		## Include the expansion id if one is available.
		local expansion_id
		expansion_id=$(expansion_id)
		if [ -n "$expansion_id" ]; then
			package_id="${package_id}-${expansion_id}"
		fi
	fi

	# Check that the id fits the format restrictions.
	if ! printf '%s' "$package_id" | \
		grep --quiet --regexp='^[0-9a-z][-0-9a-z]\+[0-9a-z]$'
	then
		error_package_id_invalid "$package_id"
		return 1
	fi

	# Apply tweaks specific to the target package format.
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			package_id=$(archlinux_package_id "$package_id")
		;;
		('gentoo'|'egentoo')
			package_id=$(gentoo_package_id "$package_id")
		;;
	esac

	printf '%s' "$package_id"
}

# Print the architecture of the given package
# USAGE: package_architecture $package
# RETURNS: the package architecture, as one of the following values:
#          - 32
#          - 64
#          - all
package_architecture() {
	local package
	package="$1"

	local package_architecture
	package_architecture=$(context_value "${package}_ARCH")

	# If no architecture is explictly set for the given package, fall back to "all".
	if [ -z "$package_architecture" ]; then
		package_architecture='all'
	fi

	printf '%s' "$package_architecture"
}

# Print the desciption of the given package
# USAGE: package_description $package
# RETURNS: the package description, a non-empty string that should not include line breaks
package_description() {
	local package
	package="$1"

	local package_description
	package_description=$(context_value "${package}_DESCRIPTION")

	# Fall back on the default package description for the current game engine
	if [ -z "$package_description" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('visionaire')
				package_description=$(visionaire_package_description "$package")
			;;
		esac
	fi

	# Check that the package description does not span multiple lines
	if [ "$(printf '%s' "$package_description" | wc --lines)" -gt 0 ]; then
		error_variable_multiline "${package}_DESCRIPTION"
		return 1
	fi

	printf '%s' "$package_description"
}

# Print the file name of the given package
# USAGE: package_name $package
# RETURNS: the file name, as a string
package_name() {
	local package
	package="$1"

	local option_package package_name
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			package_name=$(package_name_archlinux "$package")
		;;
		('deb')
			package_name=$(package_name_debian "$package")
		;;
		('gentoo')
			package_name=$(package_name_gentoo "$package")
		;;
		('egentoo')
			package_name=$(egentoo_package_name)
		;;
	esac

	printf '%s' "$package_name"
}

# Get the path to the directory where the given package is prepared.
# USAGE: package_path $package
# RETURNS: path to a directory, it is not checked that it exists or is writable
package_path() {
	local package
	package="$1"

	assert_not_empty 'PLAYIT_WORKDIR' 'package_path'

	local option_package package_name package_path
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			package_path=$(package_path_archlinux "$package")
		;;
		('deb')
			package_path=$(package_path_debian "$package")
		;;
		('gentoo')
			package_path=$(package_path_gentoo "$package")
		;;
		('egentoo')
			package_path=$(package_path_egentoo "$package")
		;;
	esac

	printf '%s/packages/%s' "$PLAYIT_WORKDIR" "$package_path"
}

# Print the maintainer string
# USAGE: package_maintainer
# RETURNS: the package maintainer, as a non-empty string
package_maintainer() {
	local maintainer
	maintainer=''

	# Try to get a maintainer string from environment variables used by Debian tools.
	if ! variable_is_empty 'DEBEMAIL'; then
		if ! variable_is_empty 'DEBFULLNAME'; then
			maintainer="$DEBFULLNAME <${DEBEMAIL}>"
		else
			maintainer="$DEBEMAIL"
		fi
	fi
	if [ -n "$maintainer" ]; then
		printf '%s' "$maintainer"
		return 0
	fi

	# Try to get a maintainer string from /etc/makepkg.conf.
	if \
		[ -r '/etc/makepkg.conf' ] \
		&& grep --quiet '^PACKAGER=' '/etc/makepkg.conf'
	then
		if grep --quiet '^PACKAGER=".*"' '/etc/makepkg.conf'; then
			maintainer=$(sed --silent 's/^PACKAGER="\(.*\)"/\1/p' '/etc/makepkg.conf')
		elif grep --quiet "^PACKAGER='.*'" '/etc/makepkg.conf'; then
			maintainer=$(sed --silent "s/^PACKAGER='\\(.*\\)'/\\1/p" '/etc/makepkg.conf')
		else
			maintainer=$(sed --silent 's/^PACKAGER=\(.*\)/\1/p' '/etc/makepkg.conf')
		fi
	fi
	if [ -n "$maintainer" ]; then
		printf '%s' "$maintainer"
		return 0
	fi

	# Compute a maintainer e-mail from the current hostname and user name,
	# falling back to "user@localhost".
	local hostname
	if command -v 'hostname' >/dev/null 2>&1; then
		hostname=$(hostname)
	elif [ -r '/etc/hostname' ]; then
		hostname=$(cat '/etc/hostname')
	else
		hostname='localhost'
	fi
	local username
	if ! variable_is_empty 'USER'; then
		username="$USER"
	elif command -v 'whoami' >/dev/null 2>&1; then
		username=$(whoami)
	elif command -v 'id' >/dev/null 2>&1; then
		username=$(id --name --user)
	else
		username='user'
	fi
	printf '%s@%s' "$username" "$hostname"
}

# Print the package version string
# USAGE: package_version
# RETURNS: the package version, as a non-empty string
package_version() {
	# Get the version string for the current archive.
	local script_version_string archive package_version
	script_version_string=$(script_version)
	archive=$(current_archive)
	package_version=$(get_value "${archive}_VERSION")
	## Fall back on "1.0-1" if no version string is explicitly set.
	if [ -z "$package_version" ]; then
		package_version='1.0-1'
	fi
	package_version="${package_version}+${script_version_string}"

	# Portage does not like some of our version names
	# cf. https://devmanual.gentoo.org/ebuild-writing/file-format/index.html
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('gentoo'|'egentoo')
			package_version=$(gentoo_package_version "$package_version")
		;;
	esac

	printf '%s' "$package_version"
}

# Print the list of package names provided by the given package
# This list is used to ensure conflicting packages can not be installed at the same time.
# USAGE: package_provides $package
# RETURN: a list of provided package names,
#         one per line,
#         or an empty string
package_provides() {
	local package
	package="$1"

	local package_provides
	package_provides=$(context_value "${package}_PROVIDES")

	# Return early if there is no package name to print
	if [ -z "$package_provides" ]; then
		return 0
	fi

	printf '%s' "$package_provides" | list_clean
}

# Print the actions that should be run post-installation for the given package
# USAGE: package_postinst_actions $package
# RETURN: a list of actions, that can span over several lines,
#         the list can be empty
package_postinst_actions() {
	local package
	package="$1"

	local postinst_actions
	postinst_actions=$(get_value "${package}_POSTINST_RUN")

	# Return early if no action is set.
	if [ -z "$postinst_actions" ]; then
		return 0
	fi

	# Ensure the list of actions always end with a line break.
	printf '%s\n' "$postinst_actions"
}

# Print the actions that should be run pre-removal for the given package
# USAGE: package_prerm_actions $package
# RETURN: a list of actions, that can span over several lines,
#         the list can be empty
package_prerm_actions() {
	local package
	package="$1"

	local prerm_actions
	prerm_actions=$(get_value "${package}_PRERM_RUN")

	# Return early if no action is set.
	if [ -z "$prerm_actions" ]; then
		return 0
	fi

	# Ensure the list of actions always end with a line break.
	printf '%s\n' "$prerm_actions"
}

# Print the warning messages that should be displayed at the end of the given package installation
# USAGE: package_postinst_warnings $package
# RETURN: one or several messages, separated by line breaks,
#         the message can be empty
package_postinst_warnings() {
	local package
	package="$1"

	get_value "${package}_POSTINST_WARNINGS"
}

# Print the list of generic dependencies required by a given package
# USAGE: dependencies_list_generic $package
# RETURN: a list of generic dependcy keywords,
#         separated by line breaks
dependencies_list_generic() {
	local package
	package="$1"

	local dependencies_generic
	dependencies_generic=$(context_value "${package}_DEPS")

	# Generic dependencies are deprecated for compatibility levels ≥ 2.30
	if \
		[ -n "$dependencies_generic" ] && \
		compatibility_level_is_at_least '2.30'
	then
		warning_deprecated_variable "${package}_DEPS" "${package}_DEPENDENCIES_xxx"
	fi

	# Return early if the current package does not use legacy generic dependencies
	if [ -z "$dependencies_generic" ]; then
		return 0
	fi

	printf '%s' "$dependencies_generic" | sed 's/ /\n/g' | list_clean
}

# Add a dependency to the list of the given package.
# This function is used to update the generic dependencies list.
# USAGE: dependencies_add_generic $package $dependency
dependencies_add_generic() {
	local package dependency
	package="$1"
	dependency="$2"

	local current_dependencies
	current_dependencies=$(dependencies_list_generic "$package")

	local dependencies_variable_name
	dependencies_variable_name=$(context_name "${package}_DEPS")
	if [ -z "$dependencies_variable_name" ]; then
		dependencies_variable_name="${package}_DEPS"
	fi
	export $dependencies_variable_name="$current_dependencies $dependency"
}

# Print the list of commands required by a given package
# USAGE: dependencies_list_commands $package
# RETURNS: a list of commands,
#          one per line
dependencies_list_commands() {
	local package
	package="$1"

	local dependencies_commands
	dependencies_commands=$(context_value "${package}_DEPENDENCIES_COMMANDS")
	# Return early if the current package does not require any command
	if [ -z "$dependencies_commands" ]; then
		return 0
	fi

	printf '%s' "$dependencies_commands" | list_clean
}

# Add a command to the list of the given package.
# This function is used to update the commands dependencies list.
# USAGE: dependencies_add_command $package $dependency
dependencies_add_command() {
	local package dependency
	package="$1"
	dependency="$2"

	local current_dependencies
	current_dependencies=$(dependencies_list_commands "$package")

	local dependencies_variable_name
	dependencies_variable_name=$(context_name "${package}_DEPENDENCIES_COMMANDS")
	if [ -z "$dependencies_variable_name" ]; then
		dependencies_variable_name="${package}_DEPENDENCIES_COMMANDS"
	fi
	export $dependencies_variable_name="$current_dependencies
	$dependency"
}

# Print the path to a temporary files used for unknown commands listing
# USAGE: dependencies_unknown_commands_file
dependencies_unknown_commands_file() {
	printf '%s/unknown_commands_list' "$PLAYIT_WORKDIR"
}

# Print a list of unknown commands
# USAGE: dependencies_unknown_commands_list
dependencies_unknown_commands_list() {
	local unknown_commands_list
	unknown_commands_list=$(dependencies_unknown_commands_file)

	# Return early if there is no unknown command
	if [ ! -e "$unknown_commands_list" ]; then
		return 0
	fi

	list_clean < "$unknown_commands_list"
}

# Clear the list of unknown commands
# USAGE: dependencies_unknown_commands_clear
dependencies_unknown_commands_clear() {
	local unknown_commands_list
	unknown_commands_list=$(dependencies_unknown_commands_file)

	rm --force "$unknown_commands_list"
}

# Add a command to the list of unknown ones
# USAGE: dependencies_unknown_command_add $unknown_command
dependencies_unknown_command_add() {
	local unknown_command
	unknown_command="$1"

	local unknown_commands_list
	unknown_commands_list=$(dependencies_unknown_commands_file)

	# Do nothing if this command is already included in the list
	if \
		[ -e "$unknown_commands_list" ] \
		&& grep --quiet --fixed-strings --word-regexp "$unknown_command" "$unknown_commands_list"
	then
		return 0
	fi

	printf '%s\n' "$unknown_command" >> "$unknown_commands_list"
}

# Print the list of GStreamer decoders required by a given package
# USAGE: dependencies_list_gstreamer_decoders $package
# RETURNS: a list of GStreamer decoders,
#          one per line
dependencies_list_gstreamer_decoders() {
	local package
	package="$1"

	local gstreamer_decoders
	gstreamer_decoders=$(context_value "${package}_DEPENDENCIES_GSTREAMER_PLUGINS")

	# Fall back on the default list of decoders for the current game engine
	if [ -z "$gstreamer_decoders" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unrealengine4')
				gstreamer_decoders=$(unrealengine4_dependencies_list_gstreamer_decoders_default "$package")
			;;
		esac
	fi

	# Return early if the current package does not require any GStreamer decoder
	if [ -z "$gstreamer_decoders" ]; then
		return 0
	fi

	# Always return a list with no duplicate entry,
	# excluding empty lines.
	# Ignore grep error return if there is nothing to print.
	printf '%s' "$gstreamer_decoders" | list_clean
}

# Print the path to a temporary files used for unknown GStreamer media formats listing
# USAGE: dependencies_unknown_gstreamer_media_formats_file
dependencies_unknown_gstreamer_media_formats_file() {
	printf '%s/unknown_gstreamer_media_formats_list' "$PLAYIT_WORKDIR"
}

# Print a list of unknown GStreamer media formats
# USAGE: dependencies_unknown_gstreamer_media_formats_list
dependencies_unknown_gstreamer_media_formats_list() {
	local unknown_formats_list
	unknown_formats_list=$(dependencies_unknown_gstreamer_media_formats_file)

	# Return early if there is no unknown library
	if [ ! -e "$unknown_formats_list" ]; then
		return 0
	fi

	list_clean < "$unknown_formats_list"
}

# Clear the list of unknown GStreamer media formats
# USAGE: dependencies_unknown_gstreamer_media_formats_clear
dependencies_unknown_gstreamer_media_formats_clear() {
	local unknown_formats_list
	unknown_formats_list=$(dependencies_unknown_gstreamer_media_formats_file)

	rm --force "$unknown_formats_list"
}

# Add a GStreamer media format to the list of unknown ones
# USAGE: dependencies_unknown_gstreamer_media_formats_add $unknown_format
dependencies_unknown_gstreamer_media_formats_add() {
	local unknown_format unknown_formats_list
	unknown_format="$1"
	unknown_formats_list=$(dependencies_unknown_gstreamer_media_formats_file)

	# Do nothing if this format is already included in the list
	if \
		[ -e "$unknown_formats_list" ] \
		&& grep --quiet --fixed-strings --word-regexp "$unknown_format" "$unknown_formats_list"
	then
		return 0
	fi

	printf '%s\n' "$unknown_format" >> "$unknown_formats_list"
}
# Print the list of Mono libraries required by a given package
# USAGE: dependencies_list_mono_libraries $package
# RETURNS: a list of Mono library names,
#          one per line
dependencies_list_mono_libraries() {
	local package
	package="$1"

	# Distinct dependencies lists might be used based on source archive
	local dependencies_mono_libraries
	dependencies_mono_libraries=$(context_value "${package}_DEPENDENCIES_MONO_LIBRARIES")

	printf '%s' "$dependencies_mono_libraries" | list_clean
}

# Print the list of native packages providing the Mono libraries required by a given package
# USAGE: dependencies_list_mono_libraries_packages $package
# RETURNS: a list of native package names,
#          one per line
dependencies_list_mono_libraries_packages() {
	local package
	package="$1"

	# Return early if the current package requires no Mono library
	local required_mono_libraries library
	required_mono_libraries=$(dependencies_list_mono_libraries "$package")
	if [ -z "$required_mono_libraries" ]; then
		return 0
	fi

	# Return early when building packages for a system that does not provide Mono libraries in dedicated packages.
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			# Arch Linux provides all Mono libraries in a single "mono" package.
			printf '%s\n' 'mono'
			return 0
		;;
		('gentoo'|'egentoo')
			# Gentoo provides all Mono libraries in a single "dev-lang/mono" package.
			printf '%s\n' 'dev-lang/mono'
			return 0
		;;
	esac

	case "$option_package" in
		('deb')
			debian_dependencies_providing_mono_libraries $required_mono_libraries
			return 0
		;;
	esac
}

# Print the path to a temporary files used for unknown Mono libraries listing
# USAGE: dependencies_unknown_mono_libraries_file
dependencies_unknown_mono_libraries_file() {
	printf '%s/unknown_mono_libraries_list' "$PLAYIT_WORKDIR"
}

# Print a list of unknown Mono libraries
# USAGE: dependencies_unknown_mono_libraries_list
dependencies_unknown_mono_libraries_list() {
	local unknown_library unknown_libraries_list
	unknown_libraries_list=$(dependencies_unknown_mono_libraries_file)

	# Return early if there is no unknown library
	if [ ! -e "$unknown_libraries_list" ]; then
		return 0
	fi

	list_clean < "$unknown_libraries_list"
}

# Clear the list of unknown Mono libraries
# USAGE: dependencies_unknown_mono_libraries_clear
dependencies_unknown_mono_libraries_clear() {
	local unknown_library unknown_libraries_list
	unknown_libraries_list=$(dependencies_unknown_mono_libraries_file)

	rm --force "$unknown_libraries_list"
}

# Add a Mono library to the list of unknown ones
# USAGE: dependencies_unknown_mono_libraries_add $unknown_library
dependencies_unknown_mono_libraries_add() {
	local unknown_library unknown_libraries_list
	unknown_library="$1"
	unknown_libraries_list=$(dependencies_unknown_mono_libraries_file)

	# Do nothing if this library is already included in the list
	if \
		[ -e "$unknown_libraries_list" ] \
		&& grep --quiet --fixed-strings --word-regexp "$unknown_library" "$unknown_libraries_list"
	then
		return 0
	fi

	printf '%s\n' "$unknown_library" >> "$unknown_libraries_list"
}
# Print the list of native libraries required by a given package
# USAGE: dependencies_list_native_libraries $package
# RETURNS: a list of native library names,
#          one per line
dependencies_list_native_libraries() {
	local package
	package="$1"

	local dependencies_libraries
	dependencies_libraries=$(context_value "${package}_DEPENDENCIES_LIBRARIES")

	# Fall back on the default list of native libraries for the current game engine
	if [ -z "$dependencies_libraries" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('visionaire')
				dependencies_libraries=$(visionaire_package_dependencies_native_libraries "$package")
			;;
		esac
	fi

	printf '%s' "$dependencies_libraries" | list_clean
}

# Print the list of native libraries required by all packages
# USAGE: dependencies_list_native_libraries_all
# RETURNS: a list of native library names,
#          one per line
dependencies_list_native_libraries_all() {
	local packages_list package dependencies_libraries dependencies_libraries_all
	packages_list=$(packages_list)
	for package in $packages_list; do
		dependencies_libraries=$(dependencies_list_native_libraries "$package")
		dependencies_libraries_all="${dependencies_libraries_all:-}
		$dependencies_libraries"
	done

	printf '%s' "$dependencies_libraries_all" | list_clean
}

# Print the list of native packages providing the native libraries required by a given package
# USAGE: dependencies_list_native_libraries_packages $package
# RETURNS: a list of native package names,
#          one per line
dependencies_list_native_libraries_packages() {
	local package
	package="$1"

	local required_native_libraries option_package
	required_native_libraries=$(dependencies_list_native_libraries "$package")
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			local package_architecture
			package_architecture=$(package_architecture "$package")
			case "$package_architecture" in
				('32')
					archlinux_dependencies_providing_native_libraries_32bit $required_native_libraries
				;;
				(*)
					archlinux_dependencies_providing_native_libraries $required_native_libraries
				;;
			esac
		;;
		('deb')
			debian_dependencies_providing_native_libraries $required_native_libraries
		;;
		('gentoo'|'egentoo')
			local package_architecture
			package_architecture=$(package_architecture "$package")
			case "$package_architecture" in
				('32')
					gentoo_dependencies_providing_native_libraries_32bit "$package" $required_native_libraries
				;;
				(*)
					gentoo_dependencies_providing_native_libraries "$package" $required_native_libraries
				;;
			esac
		;;
	esac
}

# Print the path to a temporary files used for unknown libraries listing
# USAGE: dependencies_unknown_libraries_file
dependencies_unknown_libraries_file() {
	printf '%s/unknown_libraries_list' "$PLAYIT_WORKDIR"
}

# Print a list of unknown libraries
# USAGE: dependencies_unknown_libraries_list
dependencies_unknown_libraries_list() {
	local unknown_libraries_list
	unknown_libraries_list=$(dependencies_unknown_libraries_file)

	# Return early if there is no unknown library
	if [ ! -e "$unknown_libraries_list" ]; then
		return 0
	fi

	list_clean < "$unknown_libraries_list"
}

# Clear the list of unknown libraries
# USAGE: dependencies_unknown_libraries_clear
dependencies_unknown_libraries_clear() {
	local unknown_libraries_list
	unknown_libraries_list=$(dependencies_unknown_libraries_file)

	rm --force "$unknown_libraries_list"
}

# Add a library to the list of unknown ones
# USAGE: dependencies_unknown_libraries_add $unknown_library
dependencies_unknown_libraries_add() {
	local unknown_library unknown_libraries_list
	unknown_library="$1"
	unknown_libraries_list=$(dependencies_unknown_libraries_file)

	# Do nothing if this library is already included in the list
	if \
		[ -e "$unknown_libraries_list" ] \
		&& grep --quiet --fixed-strings --word-regexp "$unknown_library" "$unknown_libraries_list"
	then
		return 0
	fi

	printf '%s\n' "$unknown_library" >> "$unknown_libraries_list"
}

# Add a depdendency to the list of the given package.
# This function is used to update the native libraries dependencies list.
# USAGE: dependencies_add_native_libraries $package $dependency
dependencies_add_native_libraries() {
	local package dependency
	package="$1"
	dependency="$2"

	local current_dependencies
	current_dependencies=$(dependencies_list_native_libraries "$package")

	local dependencies_variable_name
	dependencies_variable_name=$(context_name "${package}_DEPENDENCIES_LIBRARIES")
	if [ -z "$dependencies_variable_name" ]; then
		dependencies_variable_name="${package}_DEPENDENCIES_LIBRARIES"
	fi
	export $dependencies_variable_name="$current_dependencies
	$dependency"
}

# Print the list of sibling packages required by a given package
# USAGE: dependencies_list_siblings $package
# RETURNS: a list of package identifiers,
#          one per line
dependencies_list_siblings() {
	local package
	package="$1"

	local dependencies_siblings
	dependencies_siblings=$(context_value "${package}_DEPENDENCIES_SIBLINGS")

	# Fall back on the default list of dependencies for the current game engine
	if [ -z "$dependencies_siblings" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('visionaire')
				dependencies_siblings=$(visionaire_package_dependencies_siblings  "$package")
			;;
		esac
	fi

	# Return early if the current package does not require any sibling package
	if [ -z "$dependencies_siblings" ]; then
		return 0
	fi

	printf '%s' "$dependencies_siblings" | list_clean
}

# Information: All packages are being built
# USAGE: information_packages_generation
information_packages_generation() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Géneration des paquets…\n'
		;;
		('en'|*)
			message='Packages generation…\n'
		;;
	esac
	print_message 'info' "$message"
}

# display a notification when trying to build a package that already exists
# USAGE: information_package_already_exists $file
information_package_already_exists() {
	local file
	file="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='%s existe déjà.\n'
		;;
		('en'|*)
			message='%s already exists.\n'
		;;
	esac
	print_message 'info' "$message" \
		 "$file"
}

# print package building message
# USAGE: information_package_building $file
information_package_building() {
	local file
	file="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Construction de %s…\n'
		;;
		('en'|*)
			message='Building %s…\n'
		;;
	esac
	print_message 'info' "$message" \
		"$file"
}

# Error - The provided package id uses an invalid format
# USAGE: error_package_id_invalid $package_id
error_package_id_invalid() {
	local package_id
	package_id="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼid de paquet fourni ne correspond pas au format attendu : "%s"\n'
			message="$message"'Cette valeur ne peut utiliser que des caractères du set [-a-z0-9],'
			message="$message"' et ne peut ni débuter ni sʼachever par un tiret.\n'
		;;
		('en'|*)
			message='The provided package id is not using the expected format: "%s"\n'
			message="$message"'The value should only include characters from the set [-a-z0-9],'
			message="$message"' and can not begin nor end with an hyphen.\n'
		;;
	esac
	print_message 'error' "$message" \
		"$package_id"
}

# Error - The generation of the given package failed.
# USAGE: error_package_generation_failed $package_name
error_package_generation_failed() {
	local package_name
	package_name="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La génération du paquet suivant a échoué : %s\n'
			message="$message"'Merci de signaler cet échec sur notre système de suivi : %s\n\n'
		;;
		('en'|*)
			message='The generation of the following package failed: %s\n'
			message="$message"'Please report this error on our bugs tracker: %s\n\n'
		;;
	esac
	print_message 'error' "$message" \
		"$package_name" \
		"$PLAYIT_BUG_TRACKER_URL"
}

# Error - The generation of the metadata for the given package failed.
# USAGE: error_package_metadata_generation_failed $package_name
error_package_metadata_generation_failed() {
	local package_name
	package_name="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La génération des méta-données du paquet suivant a échoué : %s\n'
		;;
		('en'|*)
			message='The generation of the following package metadata failed: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$package_name"
}

# Print install path for binaries.
# USAGE: path_binaries
path_binaries() {
	local install_prefix target_system path_structure
	install_prefix=$(option_value 'prefix')
	target_system=$(option_value 'package')
	case "$target_system" in
		('deb')
			# Debian uses /usr/games as the default path for game-related binaries.
			path_structure='%s/games'
		;;
		(*)
			# Non-Debian systems use /usr/bin as the default path for all binaries,
			# including game-related ones.
			path_structure='%s/bin'
		;;
	esac
	## Silence ShellCheck false-positive
	## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
	# shellcheck disable=SC2059
	printf "$path_structure" "$install_prefix"
}

# Print install path for XDG .desktop menu entries.
# USAGE: path_xdg_desktop
path_xdg_desktop() {
	# For convenience, XDG .desktop menu entries are always installed under the default install prefix.
	# If they could be installed under a custom path like /opt/${game_id},
	# they would not be picked up by applications menus without a manual intervention from the system administrator.
	printf '/usr/share/applications'
}

# Print install path for documentation files.
# USAGE: path_documentation
path_documentation() {
	local install_prefix
	install_prefix=$(option_value 'prefix')

	local option_package last_path_component
	option_package=$(option_value 'package')
	case "$option_package" in
		('deb'|'arch')
			last_path_component=$(game_id)
		;;
		('gentoo'|'egentoo')
			local game_id package_version
			game_id=$(game_id)
			package_version=$(package_version)
			last_path_component="${game_id}-${package_version}"
		;;
	esac

	printf '%s/share/doc/%s' "$install_prefix" "$last_path_component"
}

# Print install path for game files.
# USAGE: path_game_data
path_game_data() {
	local install_prefix game_id target_system path_structure
	install_prefix=$(option_value 'prefix')
	game_id=$(game_id)
	target_system=$(option_value 'package')
	case "$target_system" in
		('deb')
			# Debian uses /usr/share/games as the default path for game-related data files.
			path_structure='%s/share/games/%s'
		;;
		(*)
			# Non-Debian systems use /usr/share as the default path for all data files,
			# including game-related ones.
			path_structure='%s/share/%s'
		;;
	esac
	## Silence ShellCheck false-positive
	## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
	# shellcheck disable=SC2059
	printf "$path_structure" "$install_prefix" "$game_id"
}

# Print install path for icons.
# USAGE: path_icons
path_icons() {
	# Icons are always installed under the default install prefix.
	# launcher_desktop (src/30_launchers/00_common.sh) expects the icon to be available under either /usr or /usr/local.
	printf '/usr/share/icons/hicolor'
}

# Print install path for native libraries.
# USAGE: path_libraries
path_libraries() {
	local install_prefix game_id
	install_prefix=$(option_value 'prefix')
	game_id=$(game_id)

	local path_structure target_system
	## Set default value
	path_structure='%s/lib/games/%s'
	## Override default value if required
	target_system=$(option_value 'package')
	case "$target_system" in
		('arch')
			local package package_architecture
			package=$(current_package)
			package_architecture=$(package_architecture "$package")
			if [ "$package_architecture" = '32' ]; then
				path_structure='%s/lib32/games/%s'
			fi
		;;
		('gentoo'|'egentoo')
			local package package_architecture
			package=$(current_package)
			package_architecture=$(package_architecture "$package")
			if [ "$package_architecture" = '64' ]; then
				path_structure='%s/lib64/games/%s'
			fi
		;;
	esac

	## Silence ShellCheck false-positive
	## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
	# shellcheck disable=SC2059
	printf "$path_structure" "$install_prefix" "$game_id"
}

# Print the path to system libraries
# USAGE: path_libraries_system
path_libraries_system() {
	local path_libraries current_package package_architecture option_package
	current_package=$(current_package)
	package_architecture=$(package_architecture "$current_package")
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			case "$package_architecture" in
				('32')
					path_libraries='/usr/lib32'
				;;
				('64')
					path_libraries='/usr/lib'
				;;
			esac
		;;
		('deb')
			case "$package_architecture" in
				('32')
					path_libraries='/usr/lib/i386-linux-gnu'
				;;
				('64')
					path_libraries='/usr/lib/x86_64-linux-gnu'
				;;
			esac
		;;
		('gentoo'|'egentoo')
			case "$package_architecture" in
				('32')
					path_libraries='/usr/lib'
				;;
				('64')
					path_libraries='/usr/lib64'
				;;
			esac
		;;
	esac

	printf '%s' "$path_libraries"
}

# Print install path for TTF fonts.
# USAGE: path_fonts_ttf
path_fonts_ttf() {
	local game_id
	game_id=$(game_id)

	# Fonts are always installed under the default install prefix.
	printf '/usr/share/fonts/truetype/%s' "$game_id"
}

# List the LD_PRELOAD hacks that should be included for the current game
# USAGE: hacks_list
# RETURN: a list of hack identifiers, one per line,
#         or an empty string if the current game requires no hack
hacks_list() {
	local hacks_list
	hacks_list=$(context_value 'PRELOAD_HACKS_LIST')

	if [ -z "$hacks_list" ]; then
		return 0
	fi

	local hack
	for hack in $hacks_list; do
		printf '%s\n' "$hack"
	done
}

# Print the name of the given hack
# This is used to compute the file names of the source and the generated library.
# USAGE: hack_name $hack
# RETURN: the hack name, on a single line,
#         throw an error if it spans multiple lines,
#         throw an error if no name is set
hack_name() {
	local hack
	hack="$1"

	local hack_name
	hack_name=$(context_value "${hack}_NAME")

	# Throw an error if the name is empty
	if [ -z "$hack_name" ]; then
		error_missing_variable "${hack}_NAME"
		return 1
	fi

	# Throw an error if the name includes line breaks
	local line_breaks_number
	line_breaks_number=$(printf '%s' "$hack_name" | wc --lines)
	if [ "$line_breaks_number" -gt 0 ]; then
		error_variable_multiline "${hack}_NAME"
		return 1
	fi

	printf '%s' "$hack_name"
}

# Print the description of the given hack
# USAGE: hack_description $hack
# RETURN: the hack description, it can span over multiple lines,
#         throw an error if no description is set
hack_description() {
	local hack
	hack="$1"

	local hack_description
	hack_description=$(context_value "${hack}_DESCRIPTION")

	# Throw an error if the description is empty
	if [ -z "$hack_description" ]; then
		error_missing_variable "${hack}_DESCRIPTION"
		return 1
	fi

	printf '%s' "$hack_description"
}

# Print the identifier of the package that should inclide the given hack
# USAGE: hack_package $hack
# RETURN: a package identifier, falling back on the current package
hack_package() {
	local hack
	hack="$1"

	local hack_package
	hack_package=$(context_value "${hack}_PACKAGE")

	# Fall back on the current package identifier
	if [ -z "$hack_package" ]; then
		hack_package=$(current_package)
	fi

	printf '%s' "$hack_package"
}

# Print the content of the hack source
# USAGE: hack_source $hack
# RETURN: the hack source, usually spanning over multiple lines,
#         throw an error if no source is set
hack_source() {
	local hack
	hack="$1"

	local hack_source
	hack_source=$(context_value "${hack}_SOURCE")

	# Throw an error if the source is empty
	if [ -z "$hack_source" ]; then
		error_missing_variable "${hack}_SOURCE"
		return 1
	fi

	printf '%s' "$hack_source"
}

# Build the given LD_PRELOAD hack from its source
# USAGE: hack_build $hack
# RETURN: 0 if everything went well,
#         1 if the build failed
hack_build() {
	local hack
	hack="$1"

	# Write the source file
	local hack_path_source
	hack_path_source=$(hack_path_source "$hack")
	mkdir --parents "${PLAYIT_WORKDIR}/hacks"
	hack_source "$hack" > "$hack_path_source"

	# Prepare the compiler options string
	local gcc_options
	gcc_options='-shared -Wall -fPIC -ldl'
	local hack_package hack_package_architecture
	hack_package=$(hack_package "$hack")
	hack_package_architecture=$(package_architecture "$hack_package")
	if [ "$hack_package_architecture" = '32' ]; then
		gcc_options="$gcc_options -m32"
	fi

	# Build the .so library
	local hack_path_library hack_build_status
	hack_path_library=$(hack_path_library "$hack")
	{
		gcc $gcc_options "$hack_path_source" -o "$hack_path_library"
		hack_build_status=$?
	} || true
	if [ "$hack_build_status" -ne 0 ]; then
		error_hack_build_failure
		return 1
	fi
	rm "$hack_path_source"
}

# Print the path of the source file used for building the given hack
# USAGE: hack_path_source $hack
# RETURN: the absolute path to the hack source file
hack_path_source() {
	local hack
	hack="$1"

	local hack_name
	hack_name=$(hack_name "$hack")

	printf '%s/hacks/%s.c' "$PLAYIT_WORKDIR" "$hack_name"
}

# Print the path of the library built for the given hack
# USAGE: hack_path_library $hack
# RETURN: the absolute path to the hack library
hack_path_library() {
	local hack
	hack="$1"

	local hack_name
	hack_name=$(hack_name "$hack")

	printf '%s/hacks/%s.so' "$PLAYIT_WORKDIR" "$hack_name"
}

# Build and include all PRELOAD hacks
# USAGE: hacks_inclusion_default
# RETURN: 0 if all hacks could be built and included in the packages,
#         0 if no hacks are to be included,
#         1 otherwise
hacks_inclusion_default() {
	local packages_list package hacks_list hack
	packages_list=$(packages_list)
	for package in $packages_list; do
		hacks_list=$(hacks_included_in_package "$package")
		if [ -z "$hacks_list" ]; then
			continue
		fi
		for hack in $hacks_list; do
			hack_build "$hack"
			hack_inclusion "$hack"
		done
	done
}

# Include the hack library for the given hack into its target package
# USAGE: hack_inclusion $hack
# RETURN: 0 if the inclusion succeeded,
#         1 if the library could not be found
hack_inclusion() {
	local hack
	hack="$1"

	local hack_path_library
	hack_path_library=$(hack_path_library "$hack")
	if [ ! -f "$hack_path_library" ]; then
		# TODO: Display an explicit error message
		return 1
	fi

	local hack_package hack_package_path path_libraries
	hack_package=$(hack_package "$hack")
	hack_package_path=$(package_path "$hack_package")
	path_libraries=$(path_libraries)
	install -D --mode=644 --target-directory="${hack_package_path}${path_libraries}/preload-hacks" "$hack_path_library"
	rm "$hack_path_library"
}

# Print the pre-run actions required to use the given hack
# USAGE: hack_application_prerun $hack
# RETURN: the pre-run actions, as a string spanning multiple lines
hack_application_prerun() {
	local hack
	hack="$1"

	local hack_description hack_description_line
	hack_description=$(hack_description "$hack")
	while read -r hack_description_line; do
		cat <<- EOF
		# $hack_description_line
		EOF
	done <<- EOL
	$(printf '%s' "$hack_description")
	EOL

	local hack_name path_libraries
	hack_name=$(hack_name "$hack")
	path_libraries=$(path_libraries)
	cat <<- EOF
	export LD_PRELOAD="\${LD_PRELOAD}:${path_libraries}/preload-hacks/${hack_name}.so"
	EOF
}

# Print the list of hacks included in the given package
# USAGE: hacks_included_in_package $package
# RETURN: a list of hack identifiers, one per line
#         or an empty string if no hack is included in the current package
hacks_included_in_package() {
	local package
	package="$1"

	local hacks_list hack hack_package
	hacks_list=$(hacks_list)
	for hack in $hacks_list; do
		hack_package=$(hack_package "$hack")
		if [ "$hack_package" = "$package" ]; then
			printf '%s\n' "$hack"
		fi
	done
}

# Error - The build of a preload shim failed
# USAGE: error_hack_build_failure
error_hack_build_failure() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La construction d’une bibliothèque depuis ses sources a échoué.\n'
			message="$message"'Ce message devrait être précédé d’une erreur indiquant des en-têtes de compilation manquantes.\n'
		;;
		('en'|*)
			message='The building of a library from its sources failed.\n'
			message="$message"'This message should be preceded with an error indicating the missing compilation headers.\n'
		;;
	esac
	print_message 'error' "$message"
}

# Custom launcher - Throw an error if not overriden from the game script
# USAGE: custom_launcher $application
custom_launcher() {
	local application
	application="$1"

	error_custom_launcher_not_set "$application"
	return 1
}

# Error - The "custom_launcher" function has not been overriden by the game script
# USAGE: error_custom_launcher_not_set $application
error_custom_launcher_not_set() {
	local application
	application="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='L’application suivante nécessite que la fonction "custom_launcher" soit définie dans le script : %s\n'
		;;
		('en'|*)
			message='The following application expects the "custom_launcher" to be set by the game script: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$application"
}

# DOSBox - Print the DOSBox pre-run actions for the given application.
# These actions are run inside DOSBox.
# USAGE: dosbox_prerun $application
# RETURN: the pre-run actions, can span over multiple lines,
#         or an empty string if there are none
dosbox_prerun() {
	local application
	application="$1"

	get_value "${application}_DOSBOX_PRERUN"
}

# DOSBox - Print the DOSBox post-run actions for the given application.
# These actions are run inside DOSBox.
# USAGE: dosbox_postrun $application
# RETURN: the post-run actions, can span over multiple lines,
#         or an empty string if there are none
dosbox_postrun() {
	local application
	application="$1"

	get_value "${application}_DOSBOX_POSTRUN"
}

# DOSBox launcher - Print the script content
# USAGE: dosbox_launcher $application
dosbox_launcher() {
	local application
	application="$1"

	local prefix_type
	prefix_type=$(application_prefix_type "$application")
	case "$prefix_type" in
		('symlinks')
			launcher_headers
			dosbox_launcher_environment "$application"

			# Generate the game prefix
			launcher_prefix_symlinks_functions
			launcher_prefix_symlinks_build

			# Set up the paths diversion to persistent storage
			persistent_storage_initialization
			persistent_storage_common
			persistent_path_diversion
			persistent_storage_update_directories
			persistent_storage_update_files

			dosbox_launcher_run "$application"

			# Update persistent storage with files from the current prefix
			persistent_storage_update_files_from_prefix

			launcher_exit
		;;
		(*)
			error_launchers_prefix_type_unsupported "$application"
			return 1
		;;
	esac
}

# DOSBox launcher - Set the environment
# USAGE: dosbox_launcher_environment $application
dosbox_launcher_environment() {
	local application
	application="$1"

	local game_id path_game application_exe
	game_id=$(game_id)
	path_game=$(path_game_data)
	application_exe=$(application_exe_escaped "$application")

	cat <<- EOF
	# Set the environment

	GAME_ID='$game_id'
	PATH_GAME='$path_game'
	APP_EXE='$application_exe'

	EOF
}

# DOSBox launcher - Run DOSBox
# USAGE: dosbox_launcher_run $application
dosbox_launcher_run() {
	local application
	application="$1"

	local application_prerun application_postrun dosbox_instructions
	application_prerun=$(application_prerun "$application")
	application_postrun=$(application_postrun "$application")
	dosbox_instructions=$(dosbox_launcher_instructions "$application")

	cat <<- EOF
	# Run the game

	cd "\$PATH_PREFIX"

	$application_prerun

	## Do not exit on application failure,
	## to ensure post-run commands are run.
	set +o errexit

	## Silence ShellCheck false-positive
	## Argument mixes string and array. Use * or separate argument.
	# shellcheck disable=SC2145
	"\${PLAYIT_DOSBOX_BINARY:-dosbox}" -c "$dosbox_instructions"

	game_exit_status=\$?
	set -o errexit

	$application_postrun

	EOF
}

# DOSBox launcher - Run commands inside DOSBox
# USAGE: dosbox_launcher_instructions $application
dosbox_launcher_instructions() {
	local application
	application="$1"

	# Compute the command used to mount the disk image
	if [ -n "${GAME_IMAGE:-}" ]; then
		# Find the disk image path
		local packages_list path_game_data
		packages_list=$(packages_list)
		path_game_data=$(path_game_data)
		## Loop over the list of packages, one should include the disk image.
		local package package_path image_path disk_image
		for package in $packages_list; do
			package_path=$(package_path "$package")
			image_path="${package_path}${path_game_data}/${GAME_IMAGE}"
			if [ -e "$image_path" ]; then
				disk_image="$image_path"
				break
			fi
		done
		## Exit with a failure state if the disk image has not been found.
		if [ -z "${disk_image:-}" ]; then
			error_dosbox_disk_image_no_found "$GAME_IMAGE"
			return 1
		fi

		# Set the command used to mount the disk image, based on its type
		local mount_disk_image
		case "${GAME_IMAGE_TYPE:-iso}" in
			('cdrom')
				if [ -d "$disk_image" ]; then
					mount_disk_image="mount d $GAME_IMAGE -t cdrom"
				else
					mount_disk_image="imgmount d $GAME_IMAGE -t cdrom"
				fi
			;;
			('iso')
				mount_disk_image="imgmount d $GAME_IMAGE -t iso -fs iso"
			;;
		esac
	fi

	local dosbox_prerun dosbox_postrun
	dosbox_prerun=$(dosbox_prerun "$application")
	dosbox_postrun=$(dosbox_postrun "$application")

	cat <<- EOF
	mount c .
	c:
	EOF
	if [ -n "${mount_disk_image:-}" ]; then
		cat <<- EOF
		$mount_disk_image
		EOF
	fi
	if [ -n "$dosbox_prerun" ]; then
		cat <<- EOF
		$dosbox_prerun
		EOF
	fi
	game_exec_line "$application"
	if [ -n "$dosbox_postrun" ]; then
		cat <<- EOF
		$dosbox_postrun
		EOF
	fi
	cat <<- EOF
	exit
	EOF
}

# DOSBOX - Print the line starting the game
# USAGE: dosbox_exec_line $application
# RETURN: the command to execute, including its command line options
dosbox_exec_line() {
	local application
	application="$1"

	local application_options
	application_options=$(application_options "$application")
	cat <<- EOF
	\$APP_EXE $application_options \$@
	EOF
}

# Error - The DOSBox image disk was not found
# USAGE: error_dosbox_disk_image_no_found $disk_image
error_dosbox_disk_image_no_found() {
	local disk_image
	disk_image="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼimage de disque suivante nʼa pas été trouvée : %s\n'
		;;
		('en'|*)
			message='The following disk image could not be found: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$disk_image"
}

# print the Java options string for the given application
# USAGE: application_java_options $application
# RETURN: the options string on a single line,
#         or an empty string if no options are set
application_java_options() {
	# Check that the application uses the java type
	local application application_type
	application="$1"
	application_type=$(application_type "$application")
	if [ -z "$application_type" ]; then
		error_no_application_type "$application"
		return 1
	fi
	if [ "$application_type" != 'java' ]; then
		error_application_wrong_type 'application_java_options' "$application_type"
		return 1
	fi

	# Get the application Java options string from its identifier
	local application_java_options
	application_java_options=$(get_value "${application}_JAVA_OPTIONS")
	## Return early if no options string is set.
	if [ -z "$application_java_options" ]; then
		return 0
	fi

	# Check that the options string does not span multiple lines
	if [ "$(printf '%s' "$application_java_options" | wc --lines)" -gt 1 ]; then
		error_variable_multiline "${application}_JAVA_OPTIONS"
		return 1
	fi

	printf '%s' "$application_java_options"
}

# Java launcher - Print the script content
# USAGE: java_launcher $application
java_launcher() {
	local application
	application="$1"

	local prefix_type
	prefix_type=$(application_prefix_type "$application")
	case "$prefix_type" in
		('symlinks')
			launcher_headers
			java_launcher_environment "$application"

			# Generate the game prefix
			launcher_prefix_symlinks_functions
			launcher_prefix_symlinks_build

			# Set up the paths diversion to persistent storage
			persistent_storage_initialization
			persistent_storage_common
			persistent_path_diversion
			persistent_storage_update_directories
			persistent_storage_update_files

			java_launcher_run "$application"

			# Update persistent storage with files from the current prefix
			persistent_storage_update_files_from_prefix

			launcher_exit
		;;
		(*)
			error_launchers_prefix_type_unsupported "$application"
			return 1
		;;
	esac
}

# Java launcher - Set the environment
# USAGE: java_launcher_environment $application
java_launcher_environment() {
	local application
	application="$1"

	local game_id path_game application_exe
	game_id=$(game_id)
	path_game=$(path_game_data)
	application_exe=$(application_exe_escaped "$application")

	cat <<- EOF
	# Set the environment

	GAME_ID='$game_id'
	PATH_GAME='$path_game'
	APP_EXE='$application_exe'

	EOF
}

# Java launcher - Run Java
# USAGE: java_launcher_run $application
java_launcher_run() {
	local application
	application="$1"

	cat <<- 'EOF'
	# Run the game

	cd "$PATH_PREFIX"

	EOF

	# Set loading paths for libraries
	native_launcher_libraries

	application_prerun "$application"

	cat <<- 'EOF'
	## Do not exit on application failure,
	## to ensure post-run commands are run.
	set +o errexit

	EOF
	game_exec_line "$application"
	cat <<- 'EOF'

	game_exit_status=$?
	set -o errexit

	EOF

	application_postrun "$application"
}

# Java - Print the line starting the game
# USAGE: java_exec_line $application
# RETURN: the command to execute, including its command line options
java_exec_line() {
	local application
	application="$1"

	local application_java_options application_options
	application_java_options=$(application_java_options "$application")
	application_options=$(application_options "$application")
	cat <<- EOF
	java $application_java_options -jar "\$APP_EXE" $application_options "\$@"
	EOF
}

# Mono launcher - Print the script content
# USAGE: mono_launcher $application
mono_launcher() {
	local application
	application="$1"

	local prefix_type
	prefix_type=$(application_prefix_type "$application")
	case "$prefix_type" in
		('symlinks')
			launcher_headers
			mono_launcher_environment "$application"

			# Generate the game prefix
			launcher_prefix_symlinks_functions
			launcher_prefix_symlinks_build

			# Set up the paths diversion to persistent storage
			persistent_storage_initialization
			persistent_storage_common
			persistent_path_diversion
			persistent_storage_update_directories
			persistent_storage_update_files

			mono_launcher_run "$application"

			# Update persistent storage with files from the current prefix
			persistent_storage_update_files_from_prefix

			launcher_exit
		;;
		('none')
			launcher_headers
			mono_launcher_environment "$application"
			mono_launcher_run "$application"
			launcher_exit
		;;
		(*)
			error_launchers_prefix_type_unsupported "$application"
			return 1
		;;
	esac
}

# Mono launcher - Set the environment
# USAGE: mono_launcher_environment $application
mono_launcher_environment() {
	local application
	application="$1"

	local game_id path_game application_exe
	game_id=$(game_id)
	path_game=$(path_game_data)
	application_exe=$(application_exe_escaped "$application")

	cat <<- EOF
	# Set the environment

	GAME_ID='$game_id'
	PATH_GAME='$path_game'
	APP_EXE='$application_exe'

	EOF
}

# Mono launcher - Run Mono
# USAGE: mono_launcher_run $application
mono_launcher_run() {
	local application
	application="$1"

	local prefix_type execution_path
	prefix_type=$(application_prefix_type "$application")
	case "$prefix_type" in
		('symlinks')
			execution_path='$PATH_PREFIX'
		;;
		('none')
			execution_path='$PATH_GAME'
		;;
	esac
	cat <<- EOF
	# Run the game

	cd "$execution_path"

	EOF

	# Set loading paths for libraries
	native_launcher_libraries

	application_prerun "$application"

	# Apply common workarounds for Mono games
	mono_launcher_tweaks

	cat <<- 'EOF'
	## Do not exit on application failure,
	## to ensure post-run commands are run.
	set +o errexit

	EOF
	game_exec_line "$application"
	cat <<- 'EOF'

	game_exit_status=$?
	set -o errexit

	EOF

	application_postrun "$application"
}

# Mono launcher - Common workarounds
# USAGE: mono_launcher_tweaks
mono_launcher_tweaks() {
	cat <<- 'EOF'
	## Work around terminfo Mono bug,
	## cf. https://github.com/mono/mono/issues/6752
	export TERM="${TERM%-256color}"

	## Work around Mono unpredictable behaviour with non-US locales
	export LANG=C

	EOF
}

# Mono - Print the line starting the game
# USAGE: mono_exec_line $application
# RETURN: the command to execute, including its command line options
mono_exec_line() {
	local application
	application="$1"

	local application_options
	application_options=$(application_options "$application")
	cat <<- EOF
	mono "\$APP_EXE" $application_options "\$@"
	EOF
}

# Linux native launcher - Print the script content
# USAGE: native_launcher $application
native_launcher() {
	local application
	application="$1"

	local prefix_type
	prefix_type=$(application_prefix_type "$application")
	case "$prefix_type" in
		('symlinks')
			launcher_headers
			native_launcher_environment "$application"

			# Generate the game prefix
			launcher_prefix_symlinks_functions
			launcher_prefix_symlinks_build

			# Set up the paths diversion to persistent storage
			local fake_home_persistent_directories
			fake_home_persistent_directories=$(fake_home_persistent_directories)
			persistent_storage_initialization
			persistent_storage_common
			persistent_path_diversion
			persistent_storage_update_directories
			persistent_storage_update_files
			if [ -n "$fake_home_persistent_directories" ]; then
				fake_home_persistent
			fi

			native_launcher_run "$application"

			# Update persistent storage with files from the current prefix
			persistent_storage_update_files_from_prefix

			launcher_exit
		;;
		('none')
			launcher_headers
			native_launcher_environment "$application"
			native_launcher_run "$application"
			launcher_exit
		;;
		(*)
			error_launchers_prefix_type_unsupported "$application"
			return 1
		;;
	esac
}

# Linux native launcher - Set the environment
# USAGE: native_launcher_environment $application
native_launcher_environment() {
	local application
	application="$1"

	local game_id path_game application_exe
	game_id=$(game_id)
	path_game=$(path_game_data)
	application_exe=$(application_exe_escaped "$application")

	cat <<- EOF
	# Set the environment

	GAME_ID='$game_id'
	PATH_GAME='$path_game'
	APP_EXE='$application_exe'

	EOF
}

# Linux native launcher - Run the game binary
# USAGE: native_launcher_run $application
native_launcher_run() {
	local application
	application="$1"

	local prefix_type execution_path
	prefix_type=$(application_prefix_type "$application")
	case "$prefix_type" in
		('symlinks')
			execution_path='$PATH_PREFIX'
		;;
		('none')
			execution_path='$PATH_GAME'
		;;
	esac
	cat <<- EOF
	# Run the game

	cd "$execution_path"

	EOF

	# Set loading paths for libraries
	## This must be called before the engine specific tweaks, so they have access to the following variables:
	## - PLAYIT_LIBS_PATH_SYSTEM
	## - PLAYIT_LIBS_PATH_USER
	native_launcher_libraries

	local game_engine
	game_engine=$(game_engine)
	case "$game_engine" in
		('unity3d')
			# Start pulseaudio if it is available
			launcher_unity3d_pulseaudio_start

			# Work around crash on launch related to libpulse
			# Some Unity3D games crash on launch if libpulse-simple.so.0 is available but pulseaudio is not running
			launcher_unity3d_pulseaudio_hide_libpulse

			# Make a hard copy of the game binary in the current prefix,
			# otherwise the engine might follow the link and run the game from the system path.
			native_launcher_binary_copy

			# Work around Unity3D poor support for non-US locales
			launcher_unity3d_force_locale

			# Force the use of the system SDL library
			unity3d_tweak_sdl_native

			# Unity3D 4.x and 5.x - Disable the MAP_32BIT flag to prevent a crash one some Linux versions when running a 64-bit build
			local unity3d_version
			unity3d_version=$(unity3d_version)
			case "$unity3d_version" in
				('4.'*|'5.'*)
					local package package_architecture
					package=$(current_package)
					package_architecture=$(package_architecture "$package")
					if [ "$package_architecture" = '64' ]; then
						unity3d_disable_map32bit
					fi
				;;
			esac
		;;
		('visionaire')
			# Force the use of the system SDL library
			visionaire_tweak_sdl_native
		;;
		(*)
			# Make a hard copy of the game binary in the current prefix,
			# otherwise the engine might follow the link and run the game from the system path.
			local prefix_type
			prefix_type=$(application_prefix_type "$application")
			case "$prefix_type" in
				('symlinks')
					native_launcher_binary_copy
				;;
			esac
		;;
	esac

	# Enable a fake $HOME path
	local fake_home_persistent_directories
	fake_home_persistent_directories=$(fake_home_persistent_directories)
	if [ -n "$fake_home_persistent_directories" ]; then
		fake_home_enable
	fi

	application_prerun "$application"

	cat <<- 'EOF'

	## Do not exit on application failure,
	## to ensure post-run commands are run.
	set +o errexit

	EOF
	game_exec_line "$application"
	cat <<- 'EOF'

	game_exit_status=$?
	set -o errexit

	EOF

	application_postrun "$application"

	# Disable the fake $HOME path
	if [ -n "$fake_home_persistent_directories" ]; then
		fake_home_disable
	fi

	case "$game_engine" in
		('unity3d')
			# Stop pulseaudio if it has been started for this game session
			launcher_unity3d_pulseaudio_stop
		;;
	esac
}

# Linux native launcher - Copy the game binary into the game prefix
# USAGE: native_launcher_binary_copy
native_launcher_binary_copy() {
	cat <<- 'EOF'
	# Copy the game binary into the user prefix

	exe_destination="${PATH_PREFIX}/${APP_EXE}"
	if [ -h "$exe_destination" ]; then
	    exe_source=$(realpath "$exe_destination")
	    cp --remove-destination "$exe_source" "$exe_destination"
	fi
	unset exe_destination exe_source

	EOF
}

# Linux native launcher - Load shipped libraries
# USAGE: native_launcher_libraries
native_launcher_libraries() {
	local path_system path_user
	path_system=$(path_libraries)
	path_user='${HOME}/.local/lib/games/${GAME_ID}'

	cat <<- EOF
	# Set loading paths for libraries
	PLAYIT_LIBS_PATH_SYSTEM='$path_system'
	PLAYIT_LIBS_PATH_USER="$path_user"
	EOF
	cat <<- 'EOF'
	if [ -e "$PLAYIT_LIBS_PATH_SYSTEM" ]; then
	    LD_LIBRARY_PATH="${PLAYIT_LIBS_PATH_SYSTEM}:${LD_LIBRARY_PATH}"
	fi
	if [ -e "$PLAYIT_LIBS_PATH_USER" ]; then
	    LD_LIBRARY_PATH="${PLAYIT_LIBS_PATH_USER}:${LD_LIBRARY_PATH}"
	fi
	export LD_LIBRARY_PATH

	EOF
}

# Linux native - Print the line starting the game
# USAGE: native_exec_line $application
# RETURN: the command to execute, including its command line options
native_exec_line() {
	local application
	application="$1"

	local application_options
	application_options=$(application_options "$application")
	cat <<- EOF
	"./\$APP_EXE" $application_options "\$@"
	EOF
}

# Ren'Py - Print the launcher script content
# USAGE: renpy_launcher $application
renpy_launcher() {
	local application
	application="$1"

	local prefix_type
	prefix_type=$(application_prefix_type "$application")
	case "$prefix_type" in
		('none')
			launcher_headers
			renpy_launcher_run
			launcher_exit
		;;
		(*)
			error_launchers_prefix_type_unsupported "$application"
			return 1
		;;
	esac
}

# Ren'Py - Run Ren'Py
# USAGE: renpy_launcher_run
renpy_launcher_run() {
	cat <<- 'EOF'
	# Run the game

	EOF

	application_prerun "$application"
	game_exec_line "$application"
	application_postrun "$application"
}

# Ren'Py - Print the line starting the game
# USAGE: renpy_exec_line $application
# RETURN: the command to execute
renpy_exec_line() {
	## The application identifier is not actually used for Ren'Py games,
	## it is only passed for consistency with other *_exec_line functions.
	local application
	application="$1"

	local path_game
	path_game=$(path_game_data)

	cat <<- EOF
	renpy "$path_game"
	EOF
}

# Print the ScummVM id for the given application
# USAGE: application_scummvm_scummid $application
# RETURN: the ScummVM id, or an empty string if none is set
application_scummvm_scummid() {
	local application
	application="$1"

	# Get the application ScummVM id from its identifier
	local application_scummid
	application_scummid=$(context_value "${application}_SCUMMID")

	# Return early if no ScummVM id is set
	if [ -z "$application_scummid" ]; then
		return 0
	fi

	# Check that the id fits the ScummVM id format
	# Allowed formats are:
	# - "engine:game"
	# - "game"
	# The game field is allowed to include hyphen-minus, like in: "ags:gobliiins5-1"
	if ! printf '%s' "$application_scummid" | \
		grep --quiet --regexp='^\([0-9a-z]\+:\)\?[-0-9a-z]\+$'
	then
		error_application_scummid_invalid "$application" "$application_scummid"
		return 1
	fi

	printf '%s' "$application_scummid"
}

# ScummVM launcher - Print the script content
# USAGE: scummvm_launcher $application
scummvm_launcher() {
	local application
	application="$1"

	local prefix_type
	prefix_type=$(application_prefix_type "$application")
	case "$prefix_type" in
		('none')
			launcher_headers
			scummvm_launcher_environment "$application"
			scummvm_launcher_run
			launcher_exit
		;;
		(*)
			error_launchers_prefix_type_unsupported "$application"
			return 1
		;;
	esac
}

# ScummVM launcher - Set the environment
# USAGE: scummvm_launcher_environment $application
scummvm_launcher_environment() {
	local application
	application="$1"

	local path_game application_scummid
	path_game=$(path_game_data)
	application_scummid=$(application_scummvm_scummid "$application")

	cat <<- EOF
	# Set the environment

	PATH_GAME='$path_game'
	SCUMMVM_ID='$application_scummid'

	EOF
}

# ScummVM launcher - Run ScummVM
# USAGE: scummvm_launcher_run
scummvm_launcher_run() {
	cat <<- 'EOF'
	# Run the game

	EOF

	application_prerun "$application"
	game_exec_line "$application"
	application_postrun "$application"
}

# ScummVM - Print the line starting the game
# USAGE: scummvm_exec_line $application
# RETURN: the command to execute, including its command line options
scummvm_exec_line() {
	local application
	application="$1"

	local application_options
	application_options=$(application_options "$application")
	cat <<- EOF
	scummvm --path="\$PATH_GAME" $application_options "\$@" "\$SCUMMVM_ID"
	EOF
}

# Error - The provided ScummVM id uses an invalid format
# USAGE: error_application_scummid_invalid $application $application_scummid
error_application_scummid_invalid() {
	local application application_scummid
	application="$1"
	application_scummid="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼid ScummVM fourni pour lʼapplication %s ne semble pas correct : "%s"\n'
			message="$message"'Une liste de valeurs acceptées peut se trouver sur le site Web de ScummVM : \n%s\n'
		;;
		('en')
			message='The ScummVM id provided for application %s does not seem correct: "%s"\n'
			message="$message"'A list of valid values can be found on ScummVM website: \n%s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$application" \
		"$application_scummid" \
		'https://www.scummvm.org/compatibility/'
}

# WINE - Print the paths relative to the WINE prefix that should be diverted to persistent storage
# USAGE: wine_persistent_directories
# RETURN: A list of path to directories,
#         separated by line breaks.
wine_persistent_directories() {
	local persistent_directories
	persistent_directories=$(context_value 'WINE_PERSISTENT_DIRECTORIES')

	# Fall back on the default list of directories for the current game engine
	if [ -z "$persistent_directories" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unrealengine4')
				persistent_directories=$(unrealengine4_wine_persistent_directories_default)
			;;
		esac
	fi

	printf '%s' "$persistent_directories"
}

# WINE - Print the list of winetricks verbs that should be applied during the WINE prefix initialization.
# USAGE: wine_winetricks_verbs
# RETURN: A list of winetricks verbs,
#         the list can be empty.
wine_winetricks_verbs() {
	local winetricks_verbs
	winetricks_verbs=$(context_value 'WINE_WINETRICKS_VERBS')

	# Fall back on the legacy variable, for game scripts targeting ./play.it ≤ 2.25
	if \
		[ -z "$winetricks_verbs" ] && \
		! compatibility_level_is_at_least '2.26'
	then
		winetricks_verbs=$(context_value 'APP_WINETRICKS')
		if \
			[ -n "$winetricks_verbs" ] && \
			compatibility_level_is_at_least '2.25'
		then
			warning_deprecated_variable 'APP_WINETRICKS' 'WINE_WINETRICKS_VERBS'
		fi
	fi

	# Fall back on the default list of winetricks verbs for the current game engine
	if [ -z "$winetricks_verbs" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unrealengine4')
				winetricks_verbs=$(unrealengine4_wine_winetricks_verbs_default)
			;;
		esac
	fi

	# Append the verb used to set a virtual desktop, if required
	local virtual_desktop
	virtual_desktop=$(wine_virtual_desktop)
	case "$virtual_desktop" in
		('none') ;;
		('auto')
			winetricks_verbs="${winetricks_verbs:-} vd=\"\$(screen_resolution)\""
		;;
		(*)
			winetricks_verbs="${winetricks_verbs:-} vd=$virtual_desktop"
		;;
	esac

	printf '%s' "$winetricks_verbs"
}

# Compute the icon path from the APP_xxx_EXE value
# USAGE: icon_wine_path $icon
# RETURN: the icon path, it can include spaces,
#         or an empty string
icon_wine_path() {
	# Get the application identifier from the icon identifier.
	local icon application
	icon="$1"
	application=$(icon_application "$icon")

	# Check that the application uses the "wine" type.
	local application_type
	application_type=$(application_type "$application")
	if [ -z "$application_type" ]; then
		error_no_application_type "$application"
		return 1
	fi
	if [ "$application_type" != 'wine' ]; then
		error_application_wrong_type 'icon_wine_path' "$application_type"
		return 1
	fi

	# Print the path to the game binary.
	application_exe=$(application_exe "$application")
	## Check that application binary has been found
	if [ -z "$application_exe" ]; then
		error_application_exe_empty "$application" 'icon_wine_path'
		return 1
	fi
	printf '%s' "$application_exe"
}
# WINE launcher - Print the script content
# USAGE: wine_launcher $application
wine_launcher() {
	local application
	application="$1"

	local prefix_type
	prefix_type=$(application_prefix_type "$application")
	case "$prefix_type" in
		('symlinks')
			launcher_headers
			wine_launcher_environment "$application"

			# Generate the game prefix
			launcher_prefix_symlinks_functions
			launcher_prefix_symlinks_build

			# Set up the paths diversion to persistent storage
			persistent_storage_initialization
			persistent_storage_common
			persistent_path_diversion
			persistent_storage_update_directories
			persistent_storage_update_files

			# Generate the WINE prefix
			wine_launcher_wineprefix_environment
			wine_launcher_wineprefix_generate
			wine_launcher_wineprefix_persistent

			# Handle persistent storage of registry keys
			wine_launcher_regedit_environment
			wine_launcher_regedit_load

			wine_launcher_run "$application"

			# Handle persistent storage of registry keys
			wine_launcher_regedit_store

			# Update persistent storage with files from the current prefix
			persistent_storage_update_files_from_prefix

			launcher_exit
		;;
		(*)
			error_launchers_prefix_type_unsupported "$application"
			return 1
		;;
	esac
}

# WINE launcher - Set the environment
# USAGE: wine_launcher_environment $application
wine_launcher_environment() {
	local application
	application="$1"

	local game_id path_game application_exe
	game_id=$(game_id)
	path_game=$(path_game_data)
	application_exe=$(application_exe_escaped "$application")

	cat <<- EOF
	# Set the environment

	GAME_ID='$game_id'
	PATH_GAME='$path_game'
	APP_EXE='$application_exe'

	EOF
	cat <<- 'EOF'
	## Print the path to the `wine` command
	wine_command() {
	    if [ -z "$PLAYIT_WINE_CMD" ]; then
	        command -v wine
	        return 0
	    fi
	    printf '%s' "$PLAYIT_WINE_CMD"
	}
	wineboot_command() {
	    wine_command | sed 's#/wine$#/wineboot#'
	}

	EOF
	## The `wineserver` command is only used by winetricks
	local winetricks_verbs
	winetricks_verbs=$(wine_winetricks_verbs)
	if \
		[ -n "$winetricks_verbs" ] || \
		[ -n "${WINE_DIRECT3D_RENDERER:-}" ]
	then
		cat <<- 'EOF'
		wineserver_command() {
		    wine_command | sed 's#/wine$#/wineserver#'
		}

		EOF
	fi
	## Include the path to the `regedit` command only if it is going to be used
	if \
		[ -n "${APP_REGEDIT:-}" ] || \
		[ -n "${WINE_REGEDIT_PERSISTENT_KEYS:-}" ]
	then
		cat <<- 'EOF'
		regedit_command() {
		    wine_command | sed 's#/wine$#/regedit#'
		}

		EOF
	fi

	# Include the winetricks wrapper function only if it is going to be used
	local winetricks_verbs
	winetricks_verbs=$(wine_winetricks_verbs)
	if \
		[ -n "$winetricks_verbs" ] || \
		[ -n "${WINE_DIRECT3D_RENDERER:-}" ]
	then
		cat <<- 'EOF'
		## Apply winetricks verbs, spawning a terminal if required
		winetricks_wrapper() {
		    ## Export custom paths to WINE commands
		    ## so winetricks use them instead of the default paths
		    WINE=$(wine_command)
		    WINESERVER=$(wineserver_command)
		    WINEBOOT=$(wineboot_command)
		    export WINE WINESERVER WINEBOOT

		    ## Run winetricks, spawning a terminal if required
		    ## to ensure it is not silently running in the background
		    if [ -t 0 ] || command -v zenity kdialog >/dev/null; then
		        winetricks "$@"
		    elif command -v xterm >/dev/null; then
		        xterm -e winetricks "$@"
		    else
		        winetricks "$@"
		    fi

		    ## Wait a bit for lingering WINE processes to terminate
		    sleep 1s
		}

		EOF
	fi

	# Include the screen resolution detection function if it is required to set a virtual desktop.
	local virtual_desktop
	virtual_desktop=$(wine_virtual_desktop)
	if [ "$virtual_desktop" = 'auto' ]; then
		wine_snippet_screen_resolution
	fi
}

# WINE - Print the snippet handling the actual run of the game
# USAGE: wine_launcher_run $application
wine_launcher_run() {
	local application
	application="$1"

	cat <<- 'EOF'
	# Run the game

	cd "${WINEPREFIX}/drive_c/${GAME_ID}"

	EOF

	local game_engine
	game_engine=$(game_engine)
	case "$game_engine" in
		('visionaire')
			# Prevent the use of wayland SDL video driver
			visionaire_tweak_sdl_wine
		;;
	esac

	application_prerun "$application"

	cat <<- 'EOF'

	## Do not exit on application failure,
	## to ensure post-run commands are run.
	set +o errexit

	EOF
	game_exec_line "$application"
	cat <<- 'EOF'

	game_exit_status=$?
	set -o errexit

	EOF

	application_postrun "$application"
}

# WINE - Print the line starting the game
# USAGE: wine_exec_line $application
# RETURN: the command to execute, including its command line options
wine_exec_line() {
	local application
	application="$1"

	local application_options
	application_options=$(application_options "$application")
	cat <<- EOF
	\$(wine_command) "\$APP_EXE" $application_options "\$@"
	EOF
}

# WINE launcher - Set environment for registry keys persistent storage
# USAGE: wine_launcher_regedit_environment
wine_launcher_regedit_environment() {
	# Return early if no persistent registry keys are listed
	if [ -z "${WINE_REGEDIT_PERSISTENT_KEYS:-}" ]; then
		return 0
	fi

	cat <<- 'EOF'
	# Set environment for registry keys persistent storage

	USER_PERSISTENT_PATH_REGEDIT="${USER_PERSISTENT_PATH}/wine/regedit"
	REGEDIT_DUMPS_WINEPREFIX_PATH="${WINEPREFIX}/drive_c/${GAME_ID}/wine/regedit"
	EOF
	cat <<- EOF
	REGEDIT_PERSISTENT_KEYS='$WINE_REGEDIT_PERSISTENT_KEYS'

	EOF
	cat <<- 'EOF'
	## Convert registry key name to file path
	regedit_convert_key_to_path() {
	    printf '%s.reg' "$1" | \
	        sed 's#\\#/#g' | \
	        tr '[:upper:]' '[:lower:]'
	}

	EOF
}

# WINE launcher - Load registry keys during prefix initialization
# USAGE: regedit_initial
regedit_initial() {
	# Return early if there is no key to load during prefix initilization
	if [ -z "${APP_REGEDIT:-}" ]; then
		return 0
	fi

	cat <<- EOF
	    ## Load registry scripts
	    registry_scripts='$APP_REGEDIT'
	EOF
	cat <<- 'EOF'
	    (
	        cd "${WINEPREFIX}/drive_c/${GAME_ID}"
	        for registry_script in $registry_scripts; do
	            printf 'Loading registry script: %s\n' "$registry_script"
	            if [ ! -e "$registry_script" ]; then
	                printf '\n\033[1;31mError:\033[0m\n'
	                printf 'Failed to load required registry script: %s\n' "$registry_script"
	                exit 1
	            fi
	            set +o errexit
	            $(regedit_command) "$registry_script"
	            regedit_return_code=$?
	            set -o errexit
	            if [ $regedit_return_code -ne 0 ]; then
	                printf '\n\033[1;31mError:\033[0m\n'
	                printf 'Failed to load required registry script: %s\n' "$registry_script"
	                exit 1
	            fi
	            unset regedit_return_code
	        done
	        unset registry_script
	    )
	    unset registry_scripts

	EOF
}

# WINE launcher - Store registry keys in a persistent path
# USAGE: wine_launcher_regedit_store
wine_launcher_regedit_store() {
	# Return early if no persistent registry keys are listed
	if [ -z "${WINE_REGEDIT_PERSISTENT_KEYS:-}" ]; then
		return 0
	fi

	cat <<- 'EOF'
	# Store registry keys in a persistent path

	while read -r registry_key; do
	    if [ -z "$registry_key" ]; then
	        continue
	    fi
	    registry_dump="${REGEDIT_DUMPS_WINEPREFIX_PATH}/$(regedit_convert_key_to_path "$registry_key")"
	    registry_dump_directory=$(dirname "$registry_dump")
	    mkdir --parents "$registry_dump_directory"
	    printf 'Dumping registry key in "%s".\n' "$registry_dump"
	    $(regedit_command) -E "$registry_dump" "$registry_key"
	done << EOL
	$(printf '%s' "$REGEDIT_PERSISTENT_KEYS")
	EOL
	unset registry_key registry_dump registry_dump_directory

	mkdir --parents "$USER_PERSISTENT_PATH_REGEDIT"
	(
	    cd "$REGEDIT_DUMPS_WINEPREFIX_PATH"
	    find . -type f \
	        -exec cp --force --parents --target-directory="$USER_PERSISTENT_PATH_REGEDIT" {} +
	)

	EOF
}

# WINE launcher - Load registry keys from persistent dumps
# USAGE: wine_launcher_regedit_load
wine_launcher_regedit_load() {
	# Return early if no persistent registry keys are listed
	if [ -z "${WINE_REGEDIT_PERSISTENT_KEYS:-}" ]; then
		return 0
	fi

	cat <<- 'EOF'
	# Load registry keys from persistent dumps

	if [ -e "$USER_PERSISTENT_PATH_REGEDIT" ]; then
	    mkdir --parents "$REGEDIT_DUMPS_WINEPREFIX_PATH"
	    (
	        cd "$USER_PERSISTENT_PATH_REGEDIT"
	        find . -type f \
	            -exec cp --force --parents --target-directory="$REGEDIT_DUMPS_WINEPREFIX_PATH" {} +
	    )
	fi
	while read -r registry_key; do
	    if [ -z "$registry_key" ]; then
	        continue
	    fi
	    registry_dump="${REGEDIT_DUMPS_WINEPREFIX_PATH}/$(regedit_convert_key_to_path "$registry_key")"
	    if [ -e "$registry_dump" ]; then
	        printf 'Loading registry key from "%s".\n' "$registry_dump"
	        $(regedit_command) "$registry_dump"
	    fi
	done << EOL
	$(printf '%s' "$REGEDIT_PERSISTENT_KEYS")
	EOL
	unset registry_key registry_dump

	EOF
}

# Print the name of the renderer to use for Direct3D
# USAGE: wine_renderer_name
wine_renderer_name() {
	# Fetch the preferred renderer from the game script,
	# if it is explicitely set.
	local direct3d_renderer
	direct3d_renderer="${WINE_DIRECT3D_RENDERER:-}"

	# Fall back on the default renderer for the current game engine
	if [ -z "$direct3d_renderer" ]; then
		local game_engine
		game_engine=$(game_engine)
		case "$game_engine" in
			('unrealengine4')
				direct3d_renderer=$(unrealengine4_wine_renderer_name_default)
			;;
		esac
	fi

	# Fall back to using the default renderer
	if [ -z "$direct3d_renderer" ]; then
		direct3d_renderer='default'
	fi

	# Convert "wined3d" alias to "wined3d/gl"
	if [ "$direct3d_renderer" = 'wined3d' ]; then
		direct3d_renderer='wined3d/gl'
	fi

	# Check that an allowed value has been set
	case "$direct3d_renderer" in
		( \
			'default' | \
			'wined3d/gl' | \
			'wined3d/gdi' | \
			'wined3d/vulkan' | \
			'dxvk' | \
			'vkd3d' \
		)
			printf '%s' "$direct3d_renderer"
			return 0
		;;
		(*)
			error_unknown_wine_renderer "$direct3d_renderer"
			return 1
		;;
	esac
}

# WINE launcher - Set the correct Direct3D renderer
# USAGE: wine_launcher_renderer
wine_launcher_renderer() {
	local direct3d_renderer
	direct3d_renderer=$(wine_renderer_name)

	case "$direct3d_renderer" in
		('default')
			# Nothing to do here.
		;;
		( \
			'wined3d/gl' | \
			'wined3d/gdi' | \
			'wined3d/vulkan' \
		)
			local wined3d_backend
			wined3d_backend=$(printf '%s' "$direct3d_renderer" | cut --delimiter='/' --fields=2)
			wine_launcher_renderer_wined3d "$wined3d_backend"
		;;
		('dxvk')
			wine_launcher_renderer_dxvk
		;;
		('vkd3d')
			wine_launcher_renderer_vkd3d
		;;
	esac
}

# WINE launcher - Use WineD3D with a specific backend for Direct3D rendering
# USAGE: wine_launcher_renderer_wined3d $wined3d_backend
wine_launcher_renderer_wined3d() {
	local wined3d_backend
	wined3d_backend="$1"
	cat <<- EOF
	    ## Use WineD3D for Direct3D rendering, with the "$wined3d_backend" backend
	    wined3d_backend="$wined3d_backend"
	EOF
	cat <<- 'EOF'
	    if command -v winetricks >/dev/null 2>&1; then
	        winetricks_wrapper renderer=$wined3d_backend
	    else
	        message="\\033[1;33mWarning:\\033[0m\\n"
	        message="${message}WineD3D backend could not be set to ${wined3d_backend}.\\n"
	        message="${message}The game might run with display or performance issues.\\n"
	        printf "\\n${message}\\n"
	    fi
	    unset wined3d_backend

	    ## Wait a bit to ensure there is no lingering wine process
	    sleep 1s

	EOF
}

# WINE launcher - Use DXVK for Direct3D rendering
# USAGE: wine_launcher_renderer_dxvk
wine_launcher_renderer_dxvk() {
	cat <<- 'EOF'
	    ## Use DXVK for Direct3D 9/10/11 rendering
	    if \
	        command -v dxvk-setup >/dev/null 2>&1 && \
	        command -v wine-development >/dev/null 2>&1
	    then
	        ## Run dxvk-setup, spawning a terminal if required
	        ## to ensure it is not silently running in the background.
	        if [ ! -t 0 ] && command -v xterm >/dev/null; then
	            xterm -e dxvk-setup install --development
	        else
	            dxvk-setup install --development
	        fi
	    elif command -v winetricks >/dev/null 2>&1; then
	        winetricks_wrapper dxvk
	    else
	        message="\\033[1;33mWarning:\\033[0m\\n"
	        message="${message}DXVK patches could not be installed in the WINE prefix.\\n"
	        message="${message}The game might run with display or performance issues.\\n"
	        printf "\\n${message}\\n"
	    fi

	    ## Wait a bit to ensure there is no lingering wine process
	    sleep 1s

	EOF
}

# WINE launcher - Use vkd3d for Direct3D rendering
# USAGE: wine_launcher_renderer_vkd3d
wine_launcher_renderer_vkd3d() {
	cat <<- 'EOF'
	    ## Install vkd3d on first launch
	    if command -v winetricks >/dev/null 2>&1; then
	        winetricks_wrapper vkd3d
	    else
	        message="\\033[1;33mWarning:\\033[0m\\n"
	        message="${message}vkd3d patches could not be installed in the WINE prefix.\\n"
	        message="${message}The game might run with display or performance issues.\\n"
	        printf "\\n${message}\\n"
	    fi

	    ## Wait a bit to ensure there is no lingering wine process
	    sleep 1s

	EOF
}

# WINE - Get the WINE virtual desktop setting expected by the current game.
#
# The value should be one of:
# - none (default if no value is set)
# - auto (use the current screen resolution when the game is launched for the first time)
# - some specific resolution (example: "1280x1024")
#
# USAGE: wine_virtual_desktop
wine_virtual_desktop() {
	local virtual_desktop
	virtual_desktop=${WINE_VIRTUAL_DESKTOP:-none}

	case "$virtual_desktop" in
		('none'|'auto') ;;
		(*[0-9]x[0-9]*)
			if ! printf '%s' "$virtual_desktop" | grep --silent '^[0-9]\+x[0-9]\+$'; then
				error_wine_virtual_desktop_invalid "$virtual_desktop"
			fi
		;;
		(*)
			error_wine_virtual_desktop_invalid "$virtual_desktop"
		;;
	esac

	printf '%s' "$virtual_desktop"
}

# WINE - Get the display resolution of the current screen.
# USAGE: wine_snippet_screen_resolution
wine_snippet_screen_resolution() {
	cat <<- 'EOF'
	# Return the type of the current display server.
	# Supported return values:
	# - wayland/sway
	# - xorg
	# If no supported server is running, nothing is returned.
	display_server() {
	    if [ -n "${WAYLAND_DISPLAY:-}" ]; then
	        if [ -n "${SWAYSOCK:-}" ]; then
	            display_server='wayland/sway'
	        fi
	    elif [ -n "${DISPLAY:-}" ]; then
	        display_server='xorg'
	    fi
	    printf '%s' "${display_server:-}"
	    unset display_server
	}

	# Return the resolution of the current screen.
	# If no supported server is running, a fallback value is returned.
	screen_resolution() {
	    display_server=$(display_server)
	    case "$display_server" in
	        ('wayland/sway')
	            current_screen=$(
	                LANG=C swaymsg --pretty --type get_workspaces | \
	                    grep --after 1 '(focused)' | \
	                    sed --silent --expression='s/\s*Output: \(.*\)$/\1/p'
	            )
	            screen_resolution=$(
	                LANG=C swaymsg --pretty --type get_outputs | \
	                    grep --after 1 "$current_screen" | \
	                    sed --silent --expression='s/.*Current mode: \([0-9]\+x[0-9]\+\) @.*$/\1/p'
	            )
	            unset current_screen
	        ;;
	        ('xorg')
	            screen_resolution=$(
	                LANG=C xrandr | \
	                    sed --regexp-extended --silent --expression='s/.*primary.* ([0-9]+x[0-9]+).*/\1/p'
	            )
	        ;;
	        (*)
	            ## Fall back on a default resolution if an unsupported display server is in use.
	            screen_resolution='1024x768'
	        ;;
	    esac
	    printf '%s' "${screen_resolution:-}"
	    unset display_server screen_resolution
	}
	EOF
}

# WINE - Set default value for WINEDLLOVERRIDES
# USAGE: wine_dlloverrides_default
# RETURN: the default value to use for WINEDLLOVERRIDES,
#         if it is not set in the user environment.
wine_dlloverrides_default() {
	# A default value can be set from the game script, using the variable WINE_DLLOVERRIDES_DEFAULT
	local wine_dlloverrides
	wine_dlloverrides=$(context_value 'WINE_DLLOVERRIDES_DEFAULT')

	# Fall back on a default value
	if [ -z "$wine_dlloverrides" ]; then
		local wineprefix_tweaks
		wineprefix_tweaks=$(wine_wineprefix_tweaks)
		if printf '%s' "$wineprefix_tweaks" | grep --quiet --fixed-strings --line-regexp --regexp='mono'; then
			## If Mono is going to be used in the WINE prefix, "mscoree" should not be disabled.
			wine_dlloverrides='winemenubuilder.exe,mshtml='
		else
			wine_dlloverrides='winemenubuilder.exe,mscoree,mshtml='
		fi
	fi

	printf '%s' "$wine_dlloverrides"
}

# WINE launcher - Set the WINE prefix environment
# USAGE: wine_launcher_wineprefix_environment
wine_launcher_wineprefix_environment() {
	# Compute path to WINE prefix
	cat <<- 'EOF'
	# Compute the path to WINE prefix for the current session
	wineprefix_path() {
	    # Prefix path can be explicitely set using an environment variable
	    if [ -n "$WINEPREFIX" ]; then
	        printf '%s' "$WINEPREFIX"
	        return 0
	    fi
	    # Compute the default prefix path if none has been explicitely set
	    printf '%s/play.it/wine/%s' \
	        "${XDG_CACHE_HOME:="$HOME/.cache"}" \
	        "$GAME_ID"
	}

	EOF

	# Set compatibility links to legacy paths
	cat <<- 'EOF'
	# Set compatibility link to a legacy user path
	wineprefix_legacy_link() {
	    path_current="$1"
	    path_legacy="$2"

	    user_directory="${WINEPREFIX}/drive_c/users/${USER}"
	    (
	        cd "$user_directory"
	        if [ ! -e "$path_current" ]; then
	            path_current_parent=$(dirname "$path_current")
	            mkdir --parents "$path_current_parent"
	            mv "$path_legacy" "$path_current"
	        fi
	        path_legacy_parent=$(dirname "$path_legacy")
	        mkdir --parents "$path_legacy_parent"
	        ## Warning: the use of a link to an absolute path means that the WINE prefix can not be moved around without breaking the link.
	        ## Using a link to a relative path would be better, but harder to implement when $path_legacy includes several path components (like "Local Settings/Application Data").
	        ln --symbolic "${user_directory}/${path_current}" "$path_legacy"
	    )
	    unset user_directory path_current_parent

	    unset path_current path_legacy
	}

	EOF

	# Compute WINE prefix architecture
	local package package_architecture wine_architecture
	package=$(current_package)
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			wine_architecture='win32'
		;;
		('64')
			wine_architecture='win64'
		;;
	esac

	# Set variables used for WINE prefix
	local wine_dlloverrides_default
	wine_dlloverrides_default=$(wine_dlloverrides_default)
	cat <<- EOF
	# Set variables used for WINE prefix
	WINEARCH='$wine_architecture'
	WINEDEBUG="\${WINEDEBUG:=-all}"
	WINEPREFIX=\$(wineprefix_path)
	## Disable some WINE anti-features
	## - creation of desktop entries
	## - installation of Mono
	## - installation of Gecko
	WINEDLLOVERRIDES="\${WINEDLLOVERRIDES:=${wine_dlloverrides_default}}"
	## Work around WINE bug 41639 - Wine with freetype 2.7 causes font rendering issues
	## cf. https://bugs.winehq.org/show_bug.cgi?id=41639
	FREETYPE_PROPERTIES='truetype:interpreter-version=35'
	export WINEARCH WINEDEBUG WINEDLLOVERRIDES WINEPREFIX FREETYPE_PROPERTIES

	EOF
}

# WINE launcher - Generate the WINE prefix
# USAGE: wine_launcher_wineprefix_generate
wine_launcher_wineprefix_generate() {
	cat <<- 'EOF'
	# Generate the WINE prefix
	if ! [ -e "$WINEPREFIX" ]; then
	    mkdir --parents "$(dirname "$WINEPREFIX")"

	    ## Use LANG=C to avoid localized directory names
	    LANG=C $(wineboot_command) --init 2>/dev/null

	    ## Wait until the WINE prefix creation is complete
	    printf "Waiting for the WINE prefix initialization to complete, it might take a couple seconds…\\n"
	    while [ ! -f "${WINEPREFIX}/system.reg" ]; do
	        sleep 1s
	    done

	    ## Link game prefix into WINE prefix
	    ln --symbolic \
	        "$PATH_PREFIX" \
	        "${WINEPREFIX}/drive_c/${GAME_ID}"

	    ## Remove most links pointing outside of the WINE prefix
	    rm "$WINEPREFIX/dosdevices/z:"
	    find "$WINEPREFIX/drive_c/users/$(whoami)" -type l | while read -r directory; do
	        rm "$directory"
	        mkdir "$directory"
	    done
	    unset directory

	    ## Set links to legacy paths
	    wineprefix_legacy_link 'AppData/Roaming' 'Application Data'
	    wineprefix_legacy_link 'AppData/Local' 'Local Settings/Application Data'
	    wineprefix_legacy_link 'Documents' 'My Documents'

	EOF

	# If required, install Mono in the prefix
	local wineprefix_tweaks
	wineprefix_tweaks=$(wine_wineprefix_tweaks)
	if printf '%s' "$wineprefix_tweaks" | grep --quiet --fixed-strings --line-regexp --regexp='mono'; then
		wine_wineprefix_tweak_mono_install
	fi

	# Run initial winetricks call
	local winetricks_verbs
	winetricks_verbs=$(wine_winetricks_verbs)
	if [ -n "$winetricks_verbs" ]; then
		cat <<- EOF
		    ## Run initial winetricks call
		    winetricks_wrapper ${winetricks_verbs}

		EOF
	fi

	# Load registry scripts
	regedit_initial

	# Set Direct3D renderer
	wine_launcher_renderer

	cat <<- 'EOF'
	fi

	EOF
}

# WINE launcher - Handle paths diversion from WINE prefix to persistent storage
# USAGE: wine_launcher_wineprefix_persistent
wine_launcher_wineprefix_persistent() {
	local persistent_directories
	persistent_directories=$(wine_persistent_directories)
	if [ -n "$persistent_directories" ]; then
		cat <<- EOF
		# Divert paths from the WINE prefix to persistent storage
		WINE_PERSISTENT_DIRECTORIES="$persistent_directories"
		EOF
		cat <<- 'EOF'
		while read -r directory; do
		    if [ -z "$directory" ]; then
		        continue
		    fi
		    persistent_path_diversion "${WINEPREFIX}/drive_c" "${USER_PERSISTENT_PATH}/wineprefix" "$directory"
		done <<- EOL
		$(printf '%s' "$WINE_PERSISTENT_DIRECTORIES")
		EOL
		unset directory

		EOF
		return 0
	fi

	# Handle diversions using the deprecated APP_WINE_LINK_DIRS variable,
	# for game scripts targeting ./play.it < 2.24.
	if compatibility_level_is_at_least '2.24'; then
		return 0
	fi
	if [ -n "${APP_WINE_LINK_DIRS:-}" ]; then
		wine_launcher_wineprefix_persistent_legacy
	fi
}

# WINE - List the tweaks that should be applied to the WINE prefix
# USAGE: wine_wineprefix_tweaks
# RETURN: the list of tweaks, one per line
wine_wineprefix_tweaks() {
	local tweaks_list
	tweaks_list=${WINE_WINEPREFIX_TWEAKS:-}

	printf '%s' "$tweaks_list" | list_clean
}

# WINE - Print the snippet installing Mono in the WINE prefix
# USAGE: wine_wineprefix_tweak_mono_install
# RETURN: the code snippet, for inclusion in the game launcher script
wine_wineprefix_tweak_mono_install() {
	local mono_installer_name
	mono_installer_name=$(archive_name 'ARCHIVE_MONO')
	{
		cat <<- EOF
		    ## Install Mono in the WINE prefix.
		    \$(wine_command) "\${WINEPREFIX}/drive_c/\${GAME_ID}/wineprefix-tweaks/${mono_installer_name}"

		EOF
	}
}

# Error - The provided value for the WINE Direct3D renderer is invalid
# USAGE: error_unknown_wine_renderer $unknown_value
error_unknown_wine_renderer() {
	local unknown_value
	unknown_value="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La valeur suivante est invalide pour WINE_DIRECT3D_RENDERER : %s\n'
		;;
		('en'|*)
			message='The following value is not supported for WINE_DIRECT3D_RENDERER: %s\n'
		;;
	esac
	print_message 'error' "$message" \
		"$unknown_value"
}

# Error - The provided setting for the WINE virtual desktop is invalid.
# USAGE: error_wine_virtual_desktop_invalid $invalid_setting
error_wine_virtual_desktop_invalid() {
	local invalid_setting
	invalid_setting="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La valeur suivante est invalide pour WINE_VIRTUAL_DESKTOP : "%s"\n'
		;;
		('en'|*)
			message='The following value is not supported for WINE_VIRTUAL_DESKTOP: "%s"\n'
		;;
	esac
	print_message 'error' "$message" \
		"$invalid_setting"
}

# Print the path to the engine file including the Unity3D build version
# USAGE: unity3d_version_file $file_name
# RETURN: the path to the engine file including the build version string
unity3d_version_file() {
	local file_name
	file_name="$1"

	local unity3d_name engine_info_file
	unity3d_name=$(unity3d_name)
	engine_info_file="${unity3d_name}_Data/${file_name}"

	# Look for the engine file in the temporary path for archive content.
	local content_path engine_info_file_path
	content_path=$(content_path_default)
	engine_info_file_path="${PLAYIT_WORKDIR}/gamedata/${content_path}/${engine_info_file}"
	if [ -f "$engine_info_file_path" ]; then
		printf '%s' "$engine_info_file_path"
		return 0
	fi

	# Look for the engine file in the current package.
	local package package_path path_game_data
	package=$(current_package)
	package_path=$(package_path "$package")
	path_game_data=$(path_game_data)
	engine_info_file_path="${package_path}${path_game_data}/${engine_info_file}"
	if [ -f "$engine_info_file_path" ]; then
		printf '%s' "$engine_info_file_path"
		return 0
	fi

	# Look for the engine file in all packages.
	local packages_list
	packages_list=$(packages_list)
	for package in $packages_list; do
		package_path=$(package_path "$package")
		engine_info_file_path="${package_path}${path_game_data}/${engine_info_file}"
		if [ -f "$engine_info_file_path" ]; then
			printf '%s' "$engine_info_file_path"
			return 0
		fi
	done
}

# Print the Unity3D build version for the current game
# USAGE: unity3d_version
# RETURN: the build version string
unity3d_version() {
	# Find the engine file including the build version string.
	local engine_info_file_path
	## Recent Unity3D releases stores the build version string in a file named "globalgamemanagers".
	engine_info_file_path=$(unity3d_version_file 'globalgamemanagers')
	if [ -z "$engine_info_file_path" ]; then
		## Old Unity3D releases stores the build version string in a file named "mainData".
		engine_info_file_path=$(unity3d_version_file 'mainData')
	fi

	# Return early if the engine data file including the build version string could not be found.
	if [ -z "$engine_info_file_path" ]; then
		return 0
	fi

	strings "$engine_info_file_path" | head --lines=1
}

# Compute the file name of the game binary from the UNITY3D_NAME value
# USAGE: unity3d_application_exe_default application
# RETURN: the application file name,
#         or an empty string
unity3d_application_exe_default() {
	local application
	application="$1"

	# We can not rely on the application type here,
	# to avoid a loop between application_exe and application_type.
	local unity3d_name
	unity3d_name=$(unity3d_name)
	local package package_architecture
	package=$(current_package)
	package_architecture=$(package_architecture "$package")
	local filename_candidates_list filename_candidate filename_path filename_found
	case "$package_architecture" in
		('32')
			filename_candidates_list="
			${unity3d_name}.x86
			${unity3d_name}.exe
			${unity3d_name}"
		;;
		('64')
			filename_candidates_list="
			${unity3d_name}.x86_64
			${unity3d_name}.exe
			${unity3d_name}"
		;;
		(*)
			filename_candidates_list="
			${unity3d_name}.x86
			${unity3d_name}.x86_64
			${unity3d_name}.exe
			${unity3d_name}"
		;;
	esac
	## Use a while loop to avoid breaking on spaces in file name.
	while read -r filename_candidate; do
		## Skip empty lines.
		if [ -z "$filename_candidate" ]; then
			continue
		fi
		filename_path=$(application_exe_path "$filename_candidate")
		if [ -n "$filename_path" ]; then
			filename_found="$filename_candidate"
			break
		fi
	done <<- EOL
	$(printf '%s' "$filename_candidates_list")
	EOL

	## Throw an error if no binary has been found,
	## as this would cause other problems later in the script execution.
	if [ -z "${filename_found:-}" ]; then
		error_unity3d_binary_not_found
		return 1
	fi

	printf '%s' "$filename_found"
}

# Print the Unity3D name for the current game
# This function should not fail if no Unity3D name is set for the current game,
# so it can be used to automatically detect games using the "unity3d" type variant.
# USAGE: unity3d_name
# RETURN: the Unity3D name, a string that can include spaces,
#         or an empty string if none is set
unity3d_name() {
	context_value 'UNITY3D_NAME'
}

# Print the list of Unity3D plugins to include for the current game
# USAGE: unity3d_plugins
# RETURN: the list of plugins to include, one per line
unity3d_plugins() {
	context_value 'UNITY3D_PLUGINS'
}

# Include the shipped Unity3D plugins into the current package
# USAGE: content_inclusion_unity3d_plugins $package
content_inclusion_unity3d_plugins() {
	local package
	package="$1"

	# Set the plugins source path
	local unity3d_name content_path plugins_directory plugins_path
	unity3d_name=$(unity3d_name)
	content_path=$(content_path_default)
	plugins_directory="${content_path}/${unity3d_name}_Data/Plugins"
	plugins_path="${PLAYIT_WORKDIR}/gamedata/${plugins_directory}"

	local CONTENT_UNITY3D_PLUGINS_FILES target_directory
	## Silence ShellCheck false-positive
	## CONTENT_UNITY3D_PLUGINS_FILES appears unused. Verify use (or export if used externally).
	# shellcheck disable=SC2034
	CONTENT_UNITY3D_PLUGINS_FILES=$(unity3d_plugins)
	target_directory=$(
		set_current_package "$package"
		path_libraries
	)

	# Proceed with the actual files inclusion.
	local package_architecture architecture_string
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			architecture_string='x86'
		;;
		('64')
			architecture_string='x86_64'
		;;
		('all')
			# Return early if the current package should not include binaries.
			return 0
		;;
	esac
	if [ -d "${plugins_path}/${architecture_string}" ]; then
		local CONTENT_UNITY3D_PLUGINS_PATH
		## Silence ShellCheck false-positive
		## CONTENT_UNITY3D_PLUGINS_PATH appears unused. Verify use (or export if used externally).
		# shellcheck disable=SC2034
		CONTENT_UNITY3D_PLUGINS_PATH="${plugins_directory}/${architecture_string}"
		content_inclusion 'UNITY3D_PLUGINS' "$package" "$target_directory"

		# Delete the remaining plugins for the current architecture,
		# to prevent them from being included later.
		rm --force "${plugins_path}/${architecture_string}"/*
		rmdir --ignore-fail-on-non-empty --parents "${plugins_path}/${architecture_string}"
	fi

	# Some games include plugins in the "Plugins" directory,
	# without using an architecture sub-directory.
	if [ -d "$plugins_path" ]; then
		local CONTENT_UNITY3D_PLUGINS_PATH
		## Silence ShellCheck false-positive
		## CONTENT_UNITY3D_PLUGINS_FILES appears unused. Verify use (or export if used externally).
		# shellcheck disable=SC2034
		CONTENT_UNITY3D_PLUGINS_PATH="$plugins_directory"
		content_inclusion 'UNITY3D_PLUGINS' "$package" "$target_directory"
		rm --force "$plugins_path"/*.*
		rmdir --ignore-fail-on-non-empty --parents "$plugins_path"
	fi
}

# Unity3D - Print the list of files to include from the archive for a given identifier
# USAGE: unity3d_content_files_default $content_id
# RETURN: a list of paths relative to the path for the given identifier,
#         line breaks are used as separator between each item,
#         this list can include globbing patterns,
#         this list can be empty
unity3d_content_files_default() {
	local content_id
	content_id="$1"

	local unity3d_name
	unity3d_name=$(unity3d_name)

	local applications_list application application_type
	applications_list=$(applications_list)
	if [ -z "$applications_list" ]; then
		error_applications_list_empty
	fi
	application=$(printf '%s' "$applications_list" | head --lines=1)
	application_type=$(application_type "$application")

	local content_files
	case "$content_id" in
		('GAME_BIN')
			case "$application_type" in
				('native')
					content_files="
					MonoBleedingEdge
					${unity3d_name}_Data/Mono
					${unity3d_name}_Data/MonoBleedingEdge
					${unity3d_name}.x86_64
					${unity3d_name}.x86
					${unity3d_name}
					*.so"
				;;
				('wine')
					content_files="
					Mono
					MonoBleedingEdge
					${unity3d_name}_Data/Mono
					${unity3d_name}_Data/MonoBleedingEdge
					${unity3d_name}_Data/Plugins
					${unity3d_name}.exe
					baselib.dll
					GameAssembly.dll
					UnityPlayer.dll"
					## Include the lowercase variants of all paths.
					content_files="$content_files
					mono
					monobleedingedge
					${unity3d_name}_data/mono
					${unity3d_name}_data/monobleedingedge
					${unity3d_name}_data/plugins
					gameassembly.dll
					unityplayer.dll"
					## Game scripts targeting ./play.it < 2.28 might rely on more .dll files being included.
					if ! compatibility_level_is_at_least '2.28'; then
						content_files="$content_files
						*.dll"
					fi
				;;
			esac
		;;
		('GAME_BIN32')
			case "$application_type" in
				('native')
					content_files="
					MonoBleedingEdge/x86
					${unity3d_name}_Data/Mono/x86
					${unity3d_name}_Data/MonoBleedingEdge/x86
					${unity3d_name}.x86
					${unity3d_name}"
				;;
			esac
		;;
		('GAME_BIN64')
			case "$application_type" in
				('native')
					content_files="
					MonoBleedingEdge/x86_64
					${unity3d_name}_Data/Mono/x86_64
					${unity3d_name}_Data/MonoBleedingEdge/x86_64
					${unity3d_name}.x86_64
					${unity3d_name}"
				;;
			esac
		;;
		('GAME_DATA')
			case "$application_type" in
				('native')
					content_files="
					${unity3d_name}_Data"
				;;
				('wine')
					content_files="
					${unity3d_name}_Data
					${unity3d_name}_data"
				;;
			esac
		;;
	esac

	printf '%s' "${content_files:-}"
}

# Unity3D - Compute the icon path from the UNITY3D_NAME value
# USAGE: unity3d_icon_path
# RETURN: the path to the icon file
unity3d_icon_path() {
	local icon_path unity3d_name application application_type
	unity3d_name=$(unity3d_name)
	application=$(icon_application "$icon")
	application_type=$(application_type "$application")

	## Throw an error if no application type is found,
	## this is unexpected when relying on the default icon path for Unity3D games.
	if [ -z "$application_type" ]; then
		error_no_application_type "$application"
		return 1
	fi

	case "$application_type" in
		('native')
			icon_path="${unity3d_name}_Data/Resources/UnityPlayer.png"
		;;
		('wine')
			icon_path="${unity3d_name}.exe"
		;;
	esac

	printf '%s' "${icon_path:-}"
}

# Print the snippet starting pulseaudio if it is available
# USAGE: launcher_unity3d_pulseaudio_start
launcher_unity3d_pulseaudio_start() {
	cat <<- 'EOF'
	# Start pulseaudio if it is available
	pulseaudio_is_available() {
	    command -v pulseaudio >/dev/null 2>&1
	}
	if pulseaudio_is_available; then
	    if ! pulseaudio --check; then
	        touch .stop_pulseaudio_on_exit
	    fi
	    pulseaudio --start
	fi

	EOF
}

# Print the snippet stopping pulseaudio if it has been started for this game session
# USAGE: launcher_unity3d_pulseaudio_stop
launcher_unity3d_pulseaudio_stop() {
	cat <<- 'EOF'
	# Stop pulseaudio if it has been started for this game session
	if [ -e .stop_pulseaudio_on_exit ]; then
	    pulseaudio --kill
	    rm .stop_pulseaudio_on_exit
	fi

	EOF
}

# Print the snippet hiding libpulse-simple.so.0 if pulseaudio is not available
# USAGE: launcher_unity3d_pulseaudio_hide_libpulse
launcher_unity3d_pulseaudio_hide_libpulse() {
	cat <<- 'EOF'
	# Work around crash on launch related to libpulse
	# Some Unity3D games crash on launch if libpulse-simple.so.0 is available but pulseaudio is not running
	libpulse_null_link="${PLAYIT_LIBS_PATH_USER}/libpulse-simple.so.0"
	if pulseaudio_is_available; then
	    rm --force "$libpulse_null_link"
	else
	    mkdir --parents "$PLAYIT_LIBS_PATH_USER"
	    ln --force --symbolic /dev/null "$libpulse_null_link"
	fi
	unset libpulse_null_link

	EOF
}

# Print the snippet setting forcing the use of a US-like locale
# USAGE: launcher_unity3d_force_locale
# RETURN: the code snippet, a multi-lines string
launcher_unity3d_force_locale() {
	cat <<- 'EOF'
	# Work around Unity3D poor support for non-US locales
	export LANG=C

	EOF
}

# Unity3D - Force the use of the system SDL library, for use with native games
# USAGE: unity3d_tweak_sdl_native
unity3d_tweak_sdl_native() {
	local path_libraries
	path_libraries=$(path_libraries_system)
	cat <<- EOF
	# Force the use of the system SDL library
	export SDL_DYNAMIC_API="${path_libraries}/libSDL2-2.0.so.0"

	EOF
}

# Disable the MAP_32BIT flag to prevent a crash one some Linux versions when running a 64-bit build of Unity3D
# USAGE: unity3d_disable_map32bit
# RETURN: the code snippet, a multi-lines string, indented with four spaces
unity3d_disable_map32bit() {
	local hacks_list
	hacks_list=$(hacks_list)
	export PRELOAD_HACKS_LIST="$hacks_list
HACK_UNITY3D_DISABLE_MAP32BIT"

	local package
	package=$(current_package)

	export HACK_UNITY3D_DISABLE_MAP32BIT_NAME='disable-map32bit'
	export HACK_UNITY3D_DISABLE_MAP32BIT_PACKAGE="$package"
	export HACK_UNITY3D_DISABLE_MAP32BIT_DESCRIPTION='LD_PRELOAD shim disabling the MAP_32BIT flag
This prevents crashes on some Linux versions when running a 64-bit build of Unity3D.'
	export HACK_UNITY3D_DISABLE_MAP32BIT_SOURCE='
#define _GNU_SOURCE
#include <stdlib.h>
#include <dlfcn.h>
#include <sys/mman.h>

typedef void *(*orig_mmap_type)(void *addr, size_t length, int prot,
                                int flags, int fd, off_t offset);

void *mmap(void *addr, size_t length, int prot, int flags,
           int fd, off_t offset)
{
    static orig_mmap_type orig_mmap = NULL;
    if (orig_mmap == NULL)
        orig_mmap = (orig_mmap_type)dlsym(RTLD_NEXT, "mmap");

    flags &= ~MAP_32BIT;

    return orig_mmap(addr, length, prot, flags, fd, offset);
}
'

	hack_build 'HACK_UNITY3D_DISABLE_MAP32BIT'
	hack_inclusion 'HACK_UNITY3D_DISABLE_MAP32BIT'
}

# Error - Unity3D - No binary has been found
# USAGE: error_unity3d_binary_not_found
error_unity3d_binary_not_found() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Aucun exécutable nʼa été trouvé pour ce jeu Unity3D.\n'
			message="$message"'Cette erreur est probablement due à une valeur incorrecte assignée à CONTENT_PATH_DEFAULT.\n'
		;;
		('en'|*)
			message='No binary has been found for the current Unity3D game.\n'
			message="$message"'This is probably due to an incorrect value for the CONTENT_PATH_DEFAULT variable.\n'
		;;
	esac
	print_message 'error' "$message"
}

# Unreal Engine 4 - Print the Unreal Engine 4 name for the current game
# This function should not fail if no Unreal Engine 4 name is set for the current game,
# so it can be used to automatically detect games using the "unrealengine4" type variant.
# USAGE: unrealengine4_name
# RETURN: the Unreal Engine 4 name, a string that can include spaces,
#         or an empty string if none is set
unrealengine4_name() {
	context_value 'UNREALENGINE4_NAME'
}

# Unreal Engine 4 - Print the list of files to include from the archive for a given identifier
# USAGE: unrealengine4_content_files_default $content_id
# RETURN: a list of paths relative to the path for the given identifier,
#         line breaks are used as separator between each item,
#         this list can include globbing patterns,
#         this list can be empty
unrealengine4_content_files_default() {
	local content_id
	content_id="$1"

	local unrealengine4_name
	unrealengine4_name=$(unrealengine4_name)

	local applications_list application application_type
	applications_list=$(applications_list)
	if [ -z "$applications_list" ]; then
		error_applications_list_empty
	fi
	application=$(printf '%s' "$applications_list" | head --lines=1)
	application_type=$(application_type "$application")

	local content_files
	case "$content_id" in
		('GAME_BIN')
			case "$application_type" in
				('native')
					content_files="
					Engine
					${unrealengine4_name}/Binaries
					${unrealengine4_name}/Plugins"
				;;
				('wine')
					content_files="
					engine
					${unrealengine4_name}/binaries
					${unrealengine4_name}/plugins
					${unrealengine4_name}.exe"
				;;
			esac
		;;
		('GAME_DATA')
			case "$application_type" in
				('native')
					content_files="
					${unrealengine4_name}/Content"
				;;
				('wine')
					content_files="
					${unrealengine4_name}/content"
				;;
			esac
		;;
	esac

	printf '%s' "${content_files:-}"
}

# Unreal Engine 4 - Print the default wrestool options string
# USAGE: unrealengine4_icon_wrestool_options_default
# RETURN: the options string to pass to wrestool
unrealengine4_icon_wrestool_options_default() {
	printf '%s' '--type=14 --name=101'
}

# Unreal Engine 4 - Print a default list of required GStreamer decoders
# USAGE: unrealengine4_dependencies_list_gstreamer_decoders_default $package
# RETURNS: a list of GStreamer decoders, one per line,
#          the list can be empty.
unrealengine4_dependencies_list_gstreamer_decoders_default() {
	local package
	package="$1"

	# Return early if the current package should not depend on GStreamer plugins
	local package_architecture
	package_architecture=$(package_architecture "$package")
	if [ "$package_architecture" = 'all' ]; then
		return 0
	fi

	local applications_list application application_type
	applications_list=$(applications_list)
	if [ -z "$applications_list" ]; then
		error_applications_list_empty
	fi
	application=$(printf '%s' "$applications_list" | head --lines=1)
	application_type=$(application_type "$application")

	local gstreamer_decoders
	case "$application_type" in
		('wine')
			gstreamer_decoders='
			video/quicktime, variant=(string)iso'
		;;
	esac

	printf '%s' "${gstreamer_decoders:-}"
}

# Unreal Engine 4 - Print the name of the renderer to use for Direct3D
# USAGE: unrealengine4_wine_renderer_name_default
unrealengine4_wine_renderer_name_default() {
	printf '%s' 'dxvk'
}

# Unreal Engine 4 - Print the paths relative to the WINE prefix that should be diverted to persistent storage
# USAGE: unrealengine4_wine_persistent_directories_default
# RETURN: A list of path to directories,
#         separated by line breaks.
unrealengine4_wine_persistent_directories_default() {
	local unrealengine4_name
	unrealengine4_name=$(unrealengine4_name)

	printf 'users/${USER}/AppData/Local/%s/Saved' "$unrealengine4_name"
}

# Unreal Engine 4 - Print a default list of winetricks verb that should be applied during the WINE prefix initialization
# USAGE: unrealengine4_wine_winetricks_verbs_default
# RETURN: A list of winetricks verbs
unrealengine4_wine_winetricks_verbs_default() {
	printf '%s' 'vcrun2019'
}

# Visionaire - Get the list of applications
# USAGE: visionaire_applications_list
# RETURN: a list of application identifiers,
#         separated by line breaks
visionaire_applications_list() {
	printf '%s\n' \
		'APP_MAIN'
}

# Visionaire - Get the name of the game binary
# USAGE: visionaire_application_exe
# RETURN: the game binary file name,
#         or an empty string
visionaire_application_exe() {
	## WARNING: We can not rely on the application type here,
	##          to avoid a loop between application_exe and application_type.
	local application_exe visionaire_name filename_candidates_list filename_candidate filename_path
	visionaire_name=$(visionaire_name)
	filename_candidates_list="
	${visionaire_name}
	${visionaire_name}.exe"
	while read -r filename_candidate; do
		## Skip empty lines.
		if [ -z "$filename_candidate" ]; then
			continue
		fi
		filename_path=$(application_exe_path "$filename_candidate")
		if [ -n "$filename_path" ]; then
			application_exe="$filename_candidate"
			break
		fi
	done <<- EOL
	$(printf '%s' "$filename_candidates_list")
	EOL

	printf '%s' "${application_exe:-}"
}

# Visionaire - Print the Visionaire name for the current game
# This function should not fail if no Visionaire name is set for the current game,
# so it can be used to automatically detect games using the "visionaire" type variant.
# USAGE: visionaire_name
# RETURN: the Visionaire name, a string that can include spaces,
#         or an empty string if none is set
visionaire_name() {
	context_value 'VISIONAIRE_NAME'
}

# Visionaire - Print the default path to include files from
# USAGE: visionaire_content_path $content_id
# RETURN: a list of paths relative to the path for the given identifier,
#         line breaks are used as separator between each item,
#         this list can include globbing patterns,
#         this list can be empty
visionaire_content_path() {
	local content_id
	content_id="$1"

	local applications_list application application_type
	applications_list=$(applications_list)
	if [ -z "$applications_list" ]; then
		error_applications_list_empty
	fi
	application=$(printf '%s' "$applications_list" | head --lines=1)
	application_type=$(application_type "$application")

	local content_path content_path_default
	content_path_default=$(content_path_default)
	case "$content_id" in
		('LIBS_BIN')
			case "$application_type" in
				('native')
					content_path="${content_path_default}/libs64"
				;;
			esac
		;;
		('DOC_DATA')
			case "$application_type" in
				('native')
					content_path="${content_path_default}/documents"
				;;
				('wine')
					content_path="${content_path_default}/documents"
				;;
			esac
		;;
	esac

	printf '%s' "${content_path:-}"
}

# Visionaire - Print the list of files to include from the archive for a given identifier
# USAGE: visionaire_content_files $content_id
# RETURN: a list of paths relative to the path for the given identifier,
#         line breaks are used as separator between each item,
#         this list can include globbing patterns,
#         this list can be empty
visionaire_content_files() {
	local content_id
	content_id="$1"

	local visionaire_name
	visionaire_name=$(visionaire_name)

	local applications_list application application_type
	applications_list=$(applications_list)
	if [ -z "$applications_list" ]; then
		error_applications_list_empty
	fi
	application=$(printf '%s' "$applications_list" | head --lines=1)
	application_type=$(application_type "$application")

	local content_files
	case "$content_id" in
		('LIBS_BIN')
			case "$application_type" in
				('native')
					content_files='
					libavcodec.so.56
					libavdevice.so.56
					libavfilter.so.5
					libavformat.so.56
					libavutil.so.54
					libswresample.so.1
					libswscale.so.3'
				;;
			esac
		;;
		('GAME_BIN')
			case "$application_type" in
				('native')
					content_files="
					config.ini
					$visionaire_name"
				;;
				('wine')
					content_files="
					config.ini
					avcodec-*dll
					avformat-*.dll
					avutil-*.dll
					libsndfile-*.dll
					openal.dll
					openal32.dll
					sdl.dll
					sdl2.dll
					swresample-*.dll
					swscale-*.dll
					zlib1.dll
					${visionaire_name}.exe"
				;;
			esac
		;;
		('GAME_DATA')
			case "$application_type" in
				('native')
					content_files='
					characters
					lua
					scenes
					videos
					data.vis'
				;;
				('wine')
					content_files='
					characters
					lua
					scenes
					videos
					data.vis
					banner.jpg
					folder.jpg
					languages.xml'
				;;
			esac
		;;
		('DOC_DATA')
			case "$application_type" in
				('native')
					content_files='
					licenses'
				;;
				('wine')
					content_files='
					licenses'
				;;
			esac
		;;
	esac

	printf '%s' "${content_files:-}"
}

# Visionaire - Force the use of the system SDL library, for use with native games
# USAGE: visionaire_tweak_sdl_native
visionaire_tweak_sdl_native() {
	local path_libraries
	path_libraries=$(path_libraries_system)
	cat <<- EOF
	# Force the use of the system SDL library
	export SDL_DYNAMIC_API="${path_libraries}/libSDL2-2.0.so.0"

	EOF
}

# Visionaire - Prevent the use of wayland SDL video driver, for use with WINE games
# USAGE: visionaire_tweak_sdl_wine
visionaire_tweak_sdl_wine() {
	cat <<- 'EOF'
	# Prevent the use of wayland SDL video driver
	if [ "${SDL_VIDEODRIVER:-}" = 'wayland' ]; then
	    unset SDL_VIDEODRIVER
	fi

	EOF
}

# Visionaire - Get the default list of packages to build
# USAGE: visionaire_packages_list
# RETURN: a list of packages identifiers,
#         separated by line breaks
visionaire_packages_list() {
	printf '%s\n' \
		'PKG_BIN' \
		'PKG_DATA'
}

# Visionaire - Get the default package id for the given package
# USAGE: visionaire_package_id $package
# RETURN: a package id,
#         or an empty string if there is not default value for the given package
visionaire_package_id() {
	local package
	package="$1"

	local package_id game_id
	game_id=$(game_id)
	case "$package" in
		('PKG_DATA')
			package_id="${game_id}-data"
		;;
	esac

	printf '%s' "${package_id:-}"
}

# Visionaire - Get the default package description for the given package
# USAGE: visionaire_package_description $package
# RETURN: a package description,
#         or an empty string if there is not default value for the given package
visionaire_package_description() {
	local package
	package="$1"

	local package_description
	case "$package" in
		('PKG_DATA')
			package_description='data'
		;;
	esac

	printf '%s' "${package_description:-}"
}

# Visionaire - Get the list of dependencies on siblings for the given package
# USAGE: visionaire_package_dependencies_siblings $package
# RETURN: a list of package identifiers,
#         separated by line breaks
visionaire_package_dependencies_siblings() {
	local package
	package="$1"

	local package_dependencies
	case "$package" in
		('PKG_BIN')
			package_dependencies='
			PKG_DATA'
		;;
	esac

	printf '%s' "${package_dependencies:-}" | list_clean
}

# Visionaire - Get the list of native libraries dependencies for the given package
# USAGE: visionaire_package_dependencies_native_libraries $package
# RETURN: a list of native libraries,
#         separated by line breaks
visionaire_package_dependencies_native_libraries() {
	local package
	package="$1"

	local package_dependencies
	case "$package" in
		('PKG_BIN')
			package_dependencies='
			libc.so.6
			libdl.so.2
			libgcc_s.so.1
			libGL.so.1
			libm.so.6
			libopenal.so.1
			libpthread.so.0
			librt.so.1
			libstdc++.so.6'
		;;
	esac

	printf '%s' "${package_dependencies:-}" | list_clean
}

# Arch Linux - Print installation instructions
# USAGE: print_instructions_arch $package[…]
print_instructions_arch() {
	local option_output_dir string_format
	option_output_dir=$(option_value 'output-dir')
	if printf '%s' "$option_output_dir" | grep --quiet --fixed-strings ' '; then
		string_format=' "%s"'
	else
		string_format=' %s'
	fi

	printf 'pacman -U'

	local package package_name package_output
	for package in "$@"; do
		package_name=$(package_name "$package")
		package_output=$(realpath "${option_output_dir}/${package_name}")
		## Silence ShellCheck false-positive
		## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
		# shellcheck disable=SC2059
		printf "$string_format" "$package_output"
	done

	printf '\n'
}

# Arch Linux - Write the metadata for the listed packages
# USAGE: archlinux_packages_metadata $package[…]
archlinux_packages_metadata() {
	local package
	for package in "$@"; do
		archlinux_package_metadata_single "$package"
	done
}

# Arch Linux - Write the metadata for the given package
# USAGE: archlinux_package_metadata_single $package
archlinux_package_metadata_single() {
	local package
	package="$1"

	local package_path target
	package_path=$(package_path "$package")
	target="${package_path}/.PKGINFO"
	mkdir --parents "$(dirname "$target")"

	local \
		archlinux_field_pkgname \
		archlinux_field_pkgver \
		archlinux_field_packager \
		archlinux_field_builddate \
		archlinux_field_size \
		archlinux_field_arch \
		archlinux_field_pkgdesc \
		archlinux_field_depend \
		archlinux_field_provides
	archlinux_field_pkgname=$(archlinux_field_pkgname "$package")
	archlinux_field_pkgver=$(archlinux_field_pkgver "$package")
	archlinux_field_packager=$(archlinux_field_packager "$package")
	archlinux_field_builddate=$(archlinux_field_builddate "$package")
	archlinux_field_size=$(archlinux_field_size "$package")
	archlinux_field_arch=$(archlinux_field_arch "$package")
	archlinux_field_pkgdesc=$(archlinux_field_pkgdesc "$package")
	archlinux_field_depend=$(archlinux_field_depend "$package")
	archlinux_field_provides=$(archlinux_field_provides "$package")

	cat > "$target" <<- EOF
	# Generated by ./play.it $LIBRARY_VERSION
	pkgname = $archlinux_field_pkgname
	pkgver = $archlinux_field_pkgver
	packager = $archlinux_field_packager
	builddate = $archlinux_field_builddate
	size = $archlinux_field_size
	arch = $archlinux_field_arch
	pkgdesc = $archlinux_field_pkgdesc
	EOF
	if [ -n "$archlinux_field_depend" ]; then
		local field_depend_single
		while read -r field_depend_single; do
			cat >> "$target" <<- EOF
			depend = $field_depend_single
			EOF
		done <<- EOL
		$(printf '%s' "$archlinux_field_depend")
		EOL
	fi
	if [ -n "$archlinux_field_provides" ]; then
		local field_provides_single
		while read -r field_provides_single; do
			cat >> "$target" <<- EOF
			conflict = $field_provides_single
			provides = $field_provides_single
			EOF
		done <<- EOL
		$(printf '%s' "$archlinux_field_provides")
		EOL
	fi

	# Write the .INSTALL metadata file
	local install_contents
	install_contents=$(archlinux_script_install "$package")
	if [ -n "$install_contents" ]; then
		printf '%s' "$install_contents" > "${package_path}/.INSTALL"
	fi

	# Creates .MTREE
	local option_mtree
	option_mtree=$(option_value 'mtree')
	if [ "$option_mtree" -eq 1 ]; then
		package_archlinux_create_mtree "$package"
	fi
}

# Arch Linux - Build a list of packages
# USAGE: archlinux_packages_build $package[…]
archlinux_packages_build() {
	local package
	for package in "$@"; do
		archlinux_package_build_single "$package"
	done
}

# Arch Linux - Build a single package
# USAGE: archlinux_package_build_single $package
archlinux_package_build_single() {
	local package
	package="$1"

	local package_path
	package_path=$(package_path "$package")

	local option_output_dir package_name generated_package_path
	option_output_dir=$(option_value 'output-dir')
	package_name=$(package_name "$package")
	## The path to the generated package must be an absolute path,
	## because we do not run the tar call from the current directory.
	generated_package_path=$(realpath "${option_output_dir}/${package_name}")

	# Skip packages already existing,
	# unless called with --overwrite.
	local option_overwrite
	option_overwrite=$(option_value 'overwrite')
	if \
		[ "$option_overwrite" -eq 0 ] \
		&& [ -e "$generated_package_path" ]
	then
		information_package_already_exists "$package_name"
		return 0
	fi

	# Set basic tar options
	local tar_options
	tar_options='--create'
	if variable_is_empty 'PLAYIT_TAR_IMPLEMENTATION'; then
		guess_tar_implementation
	fi
	case "$PLAYIT_TAR_IMPLEMENTATION" in
		('gnutar')
			tar_options="$tar_options --group=root --owner=root"
		;;
		('bsdtar')
			tar_options="$tar_options --gname=root --uname=root"
		;;
		(*)
			error_unknown_tar_implementation
			return 1
		;;
	esac

	# Set compression setting
	local option_compression tar_compress_program
	option_compression=$(option_value 'compression')
	case "$option_compression" in
		('none')
			tar_compress_program=''
		;;
		('speed')
			tar_compress_program='zstd --fast=1'
		;;
		('size')
			tar_compress_program='zstd -19'
		;;
	esac

	# Run the actual package generation, using tar
	local package_generation_return_code
	information_package_building "$package_name"
	package_generation_return_code=$(
		cd "$package_path"
		local package_contents
		package_contents='.PKGINFO *'
		if [ -e '.INSTALL' ]; then
			package_contents=".INSTALL $package_contents"
		fi
		if [ -e '.MTREE' ]; then
			package_contents=".MTREE $package_contents"
		fi
		if [ -n "$tar_compress_program" ]; then
			{
				tar $tar_options --use-compress-program="$tar_compress_program" --file "$generated_package_path" $package_contents
				package_generation_return_code=$?
			} || true
		else
			{
				tar $tar_options --file "$generated_package_path" $package_contents
				package_generation_return_code=$?
			} || true
		fi
		printf '%s' "$package_generation_return_code"
	)

	if [ $package_generation_return_code -ne 0 ]; then
		error_package_generation_failed "$package_name"
		return 1
	fi
}

# creates .MTREE in package
# USAGE: package_archlinux_create_mtree $pkg_path
# RETURNS: nothing
package_archlinux_create_mtree() {
	local package
	package="$1"

	local package_path
	package_path=$(package_path "$package")

	info_package_mtree_computation "$package"
	(
		cd "$package_path"
		# shellcheck disable=SC2094
		env --ignore-environment find . -print0 | \
			env --ignore-environment bsdtar \
			--create \
			--file - \
			--files-from - \
			--format=mtree \
			--no-recursion \
			--null \
			--options='!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link' \
			--exclude .MTREE \
			| \
			env --ignore-environment gzip \
			--force \
			--no-name \
			--to-stdout \
			> .MTREE
	)
}

# Print the file name of the given package
# USAGE: package_name_archlinux $package
# RETURNS: the file name, as a string
package_name_archlinux() {
	local package
	package="$1"

	local package_id package_version package_architecture package_name
	package_id=$(package_id "$package")
	package_version=$(package_version)
	package_architecture=$(archlinux_field_arch "$package")
	package_name="${package_id}_${package_version}_${package_architecture}.pkg.tar"

	local option_compression
	option_compression=$(option_value 'compression')
	case $option_compression in
		('speed')
			package_name="${package_name}.zst"
		;;
		('size')
			package_name="${package_name}.zst"
		;;
	esac

	printf '%s' "$package_name"
}

# Get the path to the directory where the given package is prepared,
# relative to the directory where all packages are stored
# USAGE: package_path_archlinux $package
# RETURNS: relative path to a directory, as a string
package_path_archlinux() {
	local package
	package="$1"

	local package_name package_path
	package_name=$(package_name "$package")
	package_path="${package_name%.pkg.tar*}"

	printf '%s' "$package_path"
}

# Tweak the given package id to follow Arch Linux standards
# USAGE: archlinux_package_id $package_id
# RETURNS: the package id, as a non-empty string
archlinux_package_id() {
	local package_id
	package_id="$1"

	# Prepend "lib32-" to the ID of 32-bit packages.
	local package_architecture
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			package_id="lib32-${package_id}"
		;;
	esac

	printf '%s' "$package_id"
}

# Arch Linux - Print the value of the "pkgname" field
# USAGE: archlinux_field_pkgname $package
# RETURN: the field value
archlinux_field_pkgname() {
	local package
	package="$1"

	package_id "$package"
}

# Arch Linux - Print the value of the "pkgver" field
# USAGE: archlinux_field_pkgver $package
# RETURN: the field value
archlinux_field_pkgver() {
	## The package value is not actually used by this function,
	## but it is still passed for consistency with other archlinux_field_* functions.
	local package
	package="$1"

	package_version
}

# Arch Linux - Print the value of the "packager" field
# USAGE: archlinux_field_packager $package
# RETURN: the field value
archlinux_field_packager() {
	## The package value is not actually used by this function,
	## but it is still passed for consistency with other archlinux_field_* functions.
	local package
	package="$1"

	package_maintainer
}

# Arch Linux - Print the value of the "builddate" field
# USAGE: archlinux_field_builddate $package
# RETURN: the field value
archlinux_field_builddate() {
	## The package value is not actually used by this function,
	## but it is still passed for consistency with other archlinux_field_* functions.
	local package
	package="$1"

	date '+%s'
}

# Arch Linux - Print the value of the "size" field
# USAGE: archlinux_field_size $package
# RETURN: the field value
archlinux_field_size() {
	local package
	package="$1"

	local package_path
	package_path=$(package_path "$package")
	du --total --block-size=1 --summarize "$package_path" | \
		tail --lines=1 | \
		cut --fields=1
}

# Arch Linux - Print the value of the "arch" field
# USAGE: archlinux_field_arch $package
# RETURN: the field value
archlinux_field_arch() {
	local package
	package="$1"

	local package_architecture package_architecture_string
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32'|'64')
			package_architecture_string='x86_64'
		;;
		('all')
			package_architecture_string='any'
		;;
	esac

	printf '%s' "$package_architecture_string"
}

# Arch Linux - Print the value of the "pkgdesc" field
# USAGE: archlinux_field_pkgdesc $package
# RETURN: the field value
archlinux_field_pkgdesc() {
	local package
	package="$1"

	local game_name package_description script_version_string
	game_name=$(game_name)
	package_description=$(package_description "$package")
	script_version_string=$(script_version)

	printf '%s' "$game_name"
	if [ -n "$package_description" ]; then
		printf -- ' - %s' "$package_description"
	fi
	printf -- ' - ./play.it script version %s' "$script_version_string"
}

# Arch Linux - Print the value of the "depend" fields
# Each package name is displayed on its own line.
# USAGE: archlinux_field_depend $package
# RETURN: the field value
archlinux_field_depend() {
	local package
	package="$1"

	dependencies_archlinux_full_list "$package"
}

# Arch Linux - Print the value of the "provides" fields
# Each package name is displayed on its own line.
# These values are used for the "conflict" fields too.
# USAGE: archlinux_field_provides $package
# RETURN: the field value
archlinux_field_provides() {
	local package
	package="$1"

	local package_provides
	package_provides=$(package_provides "$package")

	local package_architecture
	package_architecture=$(package_architecture "$package")
	if [ "$package_architecture" = '32' ]; then
		local package_id package_name_32
		package_id=$(package_id "$package")
		package_name_32=$(printf '%s' "$package_id" | sed 's/^lib32-//')
		package_provides="$package_provides
		$package_name_32"
	fi

	# Return early if there is no package name provided
	if [ -z "$package_provides" ]; then
		return 0
	fi

	printf '%s\n' $package_provides
}

# Arch Linux - Print the contents of the .INSTALL script
# USAGE: archlinux_script_install $package
# RETURN: the contents of the .INSTALL file,
#         spanning over several lines
archlinux_script_install() {
	local package
	package="$1"

	# Print the definitions of post_install and post_upgrade.
	local postinst_actions postinst_warnings
	postinst_actions=$(package_postinst_actions "$package")
	postinst_warnings=$(package_postinst_warnings "$package")
	if [ -n "$postinst_actions" ] || [ -n "$postinst_warnings" ]; then
		cat <<- EOF
		post_install() {
		EOF
		## Include actions that should be run.
		if [ -n "$postinst_actions" ]; then
			printf '%s\n' "$postinst_actions"
		fi
		## Include warnings that should be displayed.
		if [ -n "$postinst_warnings" ]; then
			local warning_line
			while read -r warning_line; do
				printf 'printf "Warning: %%s\\n" "%s"\n' "$warning_line"
			done <<- EOL
			$(printf '%s' "$postinst_warnings")
			EOL
		fi
		cat <<- EOF
		}

		post_upgrade() {
		post_install
		}
		EOF
	fi

	# Print the definitions of pre_remove and pre_upgrade.
	local prerm_actions
	prerm_actions=$(package_prerm_actions "$package")
	if [ -n "$prerm_actions" ]; then
		cat <<- EOF
		pre_remove() {
		$prerm_actions
		}

		pre_upgrade() {
		pre_remove
		}
		EOF
	fi
}

# Arch Linux - Set list of generic dependencies (32-bit)
# USAGE: pkg_set_deps_arch32 $dep[…]
pkg_set_deps_arch32() {
	for dep in "$@"; do
		case $dep in
			('alsa')
				pkg_dep='
				lib32-alsa-lib
				lib32-alsa-plugins'
			;;
			('freetype')
				pkg_dep='lib32-freetype2'
			;;
			('gcc32')
				pkg_dep='
				gcc-multilib
				lib32-gcc-libs'
			;;
			('glibc')
				pkg_dep='lib32-glibc'
			;;
			('glu')
				pkg_dep='lib32-glu'
			;;
			('glx')
				pkg_dep='lib32-libgl'
			;;
			('gtk2')
				pkg_dep='lib32-gtk2'
			;;
			('json')
				pkg_dep='lib32-json-c'
			;;
			('libstdc++')
				pkg_dep='lib32-gcc-libs'
			;;
			('libudev1')
				pkg_dep='lib32-systemd'
			;;
			('libxrandr')
				pkg_dep='lib32-libxrandr'
			;;
			('nss')
				pkg_dep='lib32-nss'
			;;
			('openal')
				pkg_dep='lib32-openal'
			;;
			('sdl2')
				pkg_dep='lib32-sdl2'
			;;
			('xcursor')
				pkg_dep='lib32-libxcursor'
			;;
			( \
				'dosbox' | \
				'java' | \
				'mono' | \
				'pulseaudio' | \
				'scummvm' | \
				'wine' | \
				'winetricks' | \
				'xgamma' | \
				'xrandr' \
			)
				pkg_dep=$(archlinux_dependencies_single_command "$dep")
			;;
			( \
				'libgdk_pixbuf-2.0.so.0' | \
				'libc.so.6' | \
				'libglib-2.0.so.0' | \
				'libgobject-2.0.so.0' | \
				'libGLU.so.1' | \
				'libGL.so.1' | \
				'libgdk-x11-2.0.so.0' | \
				'libgtk-x11-2.0.so.0' | \
				'libasound.so.2' | \
				'libasound_module_'*'.so' | \
				'libmbedtls.so.12' | \
				'libpng16.so.16' | \
				'libpulse.so.0' | \
				'libpulse-simple.so.0' | \
				'libstdc++.so.6' | \
				'libudev.so.1' | \
				'libX11.so.6' | \
				'libopenal.so.1' | \
				'libSDL-1.2.so.0' | \
				'libSDL2-2.0.so.0' | \
				'libturbojpeg.so.0' | \
				'libuv.so.1' | \
				'libvorbisfile.so.3' | \
				'libz.so.1' \
			)
				pkg_dep=$(dependency_package_providing_library_arch32 "$dep")
			;;
			(*)
				pkg_dep="$dep"
			;;
		esac
		if variable_is_empty 'pkg_deps'; then
			pkg_deps="$pkg_dep"
		else
			pkg_deps="$pkg_deps $pkg_dep"
		fi
	done
}

# Arch Linux - Set list of generic dependencies (64-bit)
# set list or Arch Linux 64-bit dependencies from generic names
# USAGE: pkg_set_deps_arch64 $dep[…]
pkg_set_deps_arch64() {
	for dep in "$@"; do
		case $dep in
			('alsa')
				pkg_dep='
				alsa-lib
				alsa-plugins'
			;;
			('freetype')
				pkg_dep='freetype2'
			;;
			('gcc32')
				pkg_dep='
				gcc-multilib
				lib32-gcc-libs'
			;;
			('glibc')
				pkg_dep='glibc'
			;;
			('glu')
				pkg_dep='glu'
			;;
			('glx')
				pkg_dep='libgl'
			;;
			('gtk2')
				pkg_dep='gtk2'
			;;
			('json')
				pkg_dep='json-c'
			;;
			('libstdc++')
				pkg_dep='gcc-libs'
			;;
			('libudev1')
				pkg_dep='libudev.so=1-64'
			;;
			('libxrandr')
				pkg_dep='libxrandr'
			;;
			('nss')
				pkg_dep='nss'
			;;
			('openal')
				pkg_dep='openal'
			;;
			('sdl2')
				pkg_dep='sdl2'
			;;
			('xcursor')
				pkg_dep='libxcursor'
			;;
			( \
				'dosbox' | \
				'java' | \
				'mono' | \
				'pulseaudio' | \
				'scummvm' | \
				'wine' | \
				'winetricks' | \
				'xgamma' | \
				'xrandr' \
			)
				pkg_dep=$(archlinux_dependencies_single_command "$dep")
			;;
			( \
				'libgdk_pixbuf-2.0.so.0' | \
				'libc.so.6' | \
				'libglib-2.0.so.0' | \
				'libgobject-2.0.so.0' | \
				'libGLU.so.1' | \
				'libGL.so.1' | \
				'libgdk-x11-2.0.so.0' | \
				'libgtk-x11-2.0.so.0' | \
				'libasound.so.2' | \
				'libasound_module_'*'.so' | \
				'libmbedtls.so.12' | \
				'libpng16.so.16' | \
				'libpulse.so.0' | \
				'libpulse-simple.so.0' | \
				'libstdc++.so.6' | \
				'libudev.so.1' | \
				'libX11.so.6' | \
				'libopenal.so.1' | \
				'libSDL-1.2.so.0' | \
				'libSDL2-2.0.so.0' | \
				'libturbojpeg.so.0' | \
				'libuv.so.1' | \
				'libvorbisfile.so.3' | \
				'libz.so.1' \
			)
				pkg_dep=$(dependency_package_providing_library_arch "$dep")
			;;
			(*)
				pkg_dep="$dep"
			;;
		esac
		if variable_is_empty 'pkg_deps'; then
			pkg_deps="$pkg_dep"
		else
			pkg_deps="$pkg_deps $pkg_dep"
		fi
	done
}

# Arch Linux - List all dependencies for the given package
# USAGE: dependencies_archlinux_full_list $package
# RETURN: print a list of dependency strings,
#         one per line
dependencies_archlinux_full_list() {
	local package
	package="$1"

	local packages_list packages_list_full
	packages_list_full=''

	# Include generic dependencies
	local package_architecture generic_dependencies_command
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			generic_dependencies_command='pkg_set_deps_arch32'
		;;
		(*)
			generic_dependencies_command='pkg_set_deps_arch64'
		;;
	esac
	local dependencies_generic dependency_generic pkg_deps
	dependencies_generic=$(dependencies_list_generic "$package")
	while read -r dependency_generic; do
		# pkg_set_deps_arch sets a variable $pkg_deps instead of printing a value,
		# we prevent it from leaking by setting it to an empty value.
		pkg_deps=''
		"$generic_dependencies_command" $dependency_generic
		packages_list_full="$packages_list_full
		$pkg_deps"
	done <<- EOL
	$(printf '%s' "$dependencies_generic")
	EOL

	# Include dependencies on sibling package
	packages_list=$(archlinux_dependencies_siblings "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on commands
	packages_list=$(archlinux_dependencies_all_commands "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on native libraries
	packages_list=$(dependencies_list_native_libraries_packages "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on Mono libraries
	packages_list=$(dependencies_list_mono_libraries_packages "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on GStreamer plugins
	packages_list=$(archlinux_dependencies_gstreamer_all_formats "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	printf '%s' "$packages_list_full" | list_clean
}

# Arch Linux - Print the package names providing the commands required by the given package
# USAGE: archlinux_dependencies_all_commands $package
# RETURN: a list of Arch Linux package names,
#         one per line
archlinux_dependencies_all_commands() {
	local package
	package="$1"

	local required_commands
	required_commands=$(dependencies_list_commands "$package")
	# Return early if the current package does not require any command
	if [ -z "$required_commands" ]; then
		return 0
	fi

	local command packages_list required_packages
	packages_list=''
	while read -r command; do
		required_packages=$(archlinux_dependencies_single_command "$command")
		packages_list="$packages_list
		$required_packages"
	done <<- EOL
	$(printf '%s' "$required_commands")
	EOL

	printf '%s' "$packages_list" | list_clean
}

# Arch Linux - Print the package names providing the required command
# USAGE: archlinux_dependencies_single_command $required_command
# RETURN: a list of Arch Linux package names,
#         one per line
archlinux_dependencies_single_command() {
	local required_command
	required_command="$1"

	local package_names
	case "$required_command" in
		('corsix-th')
			package_names='
			corsix-th'
		;;
		('dos2unix')
			package_names='
			dos2unix'
		;;
		('dosbox')
			package_names='
			dosbox'
		;;
		('java')
			package_names='
			jre8-openjdk'
		;;
		('mono')
			package_names='
			mono'
		;;
		('mpv')
			package_names='
			mpv'
		;;
		('openmw-iniimporter')
			package_names='
			openmw'
		;;
		('openmw-launcher')
			package_names='
			openmw'
		;;
		('pulseaudio')
			package_names='
			pulseaudio'
		;;
		('renpy')
			package_names='
			renpy'
		;;
		('scummvm')
			package_names='
			scummvm'
		;;
		('sed')
			package_names='
			sed'
		;;
		('setxkbmap')
			package_names='
			xorg-setxkbmap'
		;;
		('vcmilauncher')
			package_names='
			vcmi'
		;;
		('wine')
			package_names='
			wine'
		;;
		('winetricks')
			package_names='
			winetricks
			xterm'
		;;
		('xgamma')
			package_names='
			xorg-xgamma'
		;;
		('xrandr')
			package_names='
			xorg-xrandr'
		;;
		(*)
			dependencies_unknown_command_add "$required_command"
			return 0
		;;
	esac

	printf '%s' "$package_names"
}

# Arch Linux - Print the package names providing the GStreamer plugins to decode the formats required by the given package
# USAGE: archlinux_dependencies_gstreamer_all_formats $package
# RETURN: a list of Arch Linux package names,
#         one per line
archlinux_dependencies_gstreamer_all_formats() {
	local package
	package="$1"

	local gstreamer_decoders
	gstreamer_decoders=$(dependencies_list_gstreamer_decoders "$package")
	# Return early if the current package does not require any GStreamer decoders
	if [ -z "$gstreamer_decoders" ]; then
		return 0
	fi

	local package_architecture command_dependencies_for_single_format
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			command_dependencies_for_single_format='archlinux_dependencies_gstreamer_single_format_32bit'
		;;
		(*)
			command_dependencies_for_single_format='archlinux_dependencies_gstreamer_single_format'
		;;
	esac

	local media_format packages_list required_packages
	packages_list=''
	while read -r media_format; do
		required_packages=$("$command_dependencies_for_single_format" "$media_format")
		packages_list="$packages_list
		$required_packages"
	done <<- EOL
	$(printf '%s' "$gstreamer_decoders")
	EOL

	printf '%s' "$packages_list" | list_clean
}

# Arch Linux - Print the package names providing the required GStreamer plugins to decode the given format
# USAGE: archlinux_dependency_providing_gstreamer_plugin $media_format
# RETURN: a list of Arch Linux package names,
#         one per line
archlinux_dependencies_gstreamer_single_format() {
	local media_format
	media_format="$1"

	local package_names
	case "$media_format" in
		('audioconvert')
			package_names='
			gst-plugins-base'
		;;
		('avidemux')
			package_names='
			gst-plugins-good'
		;;
		('decodebin')
			package_names='
			gst-plugins-base'
		;;
		('deinterlace')
			package_names='
			gst-plugins-good'
		;;
		('application/x-id3')
			package_names='
			gst-plugins-good'
		;;
		('audio/mpeg, mpegversion=(int)1, layer=(int)3')
			package_names='
			gst-plugins-good'
		;;
		('audio/x-wma, wmaversion=(int)1')
			package_names='
			gst-libav'
		;;
		('video/mpeg, systemstream=(boolean)true, mpegversion=(int)1')
			package_names='
			gst-plugins-ugly
			gst-plugins-bad'
		;;
		('video/quicktime, variant=(string)iso')
			package_names='
			gst-plugins-good
			gst-libav'
		;;
		('video/x-ms-asf')
			package_names='
			gst-plugins-ugly
			x-ms-asf'
		;;
		('video/x-msvideo')
			package_names='
			gst-plugins-good
			gst-libav'
		;;
		('video/x-wmv, wmvversion=(int)1')
			package_names='
			gst-libav'
		;;
		(*)
			dependencies_unknown_gstreamer_media_formats_add "$media_format"
			return 0
		;;
	esac

	printf '%s' "$package_names"
}

# Arch Linux - Print the package names providing the required GStreamer plugins to decode the given format (32-bit)
# USAGE: archlinux_dependency_providing_gstreamer_plugin_32bit $media_format
# RETURN: a list of Arch Linux package names,
#         one per line
archlinux_dependencies_gstreamer_single_format_32bit() {
	local media_format
	media_format="$1"

	local package_names
	case "$media_format" in
		('audioconvert')
			package_names='
			lib32-gst-plugins-base'
		;;
		('avidemux')
			package_names='
			lib32-gst-plugins-good'
		;;
		('decodebin')
			package_names='
			lib32-gst-plugins-base'
		;;
		('deinterlace')
			package_names='
			lib32-gst-plugins-good'
		;;
		('application/x-id3')
			package_names='
			lib32-gst-plugins-good'
		;;
		('audio/mpeg, mpegversion=(int)1, layer=(int)3')
			package_names='
			lib32-gst-plugins-good'
		;;
		('audio/x-wma, wmaversion=(int)1')
			package_names='
			lib32-gst-libav'
		;;
		('video/mpeg, systemstream=(boolean)true, mpegversion=(int)1')
			package_names='
			lib32-gst-plugins-ugly
			lib32-gst-plugins-bad'
		;;
		('video/quicktime, variant=(string)iso')
			package_names='
			lib32-gst-plugins-good
			lib32-gst-libav'
		;;
		('video/x-ms-asf')
			package_names='
			lib32-gst-plugins-ugly
			lib32-gst-libav'
		;;
		('video/x-msvideo')
			package_names='
			lib32-gst-plugins-good
			lib32-gst-libav'
		;;
		('video/x-wmv, wmvversion=(int)1')
			package_names='
			lib32-gst-libav'
		;;
		(*)
			dependencies_unknown_gstreamer_media_formats_add "$media_format"
			return 0
		;;
	esac

	printf '%s' "$package_names"
}

# Arch Linux - Print the package names providing the given native libraries
# USAGE: archlinux_dependencies_providing_native_libraries $library[…]
# RETURN: a list of Arch Linux package names,
#         one per line
archlinux_dependencies_providing_native_libraries() {
	local library packages_list package
	packages_list=''
	for library in "$@"; do
		package=$(dependency_package_providing_library_arch "$library")
		packages_list="$packages_list
		$package"
	done

	printf '%s' "$packages_list" | list_clean
}

# Arch Linux - Print the package names providing the given native libraries in a 32-bit build
# USAGE: archlinux_dependencies_providing_native_libraries_32bit $library[…]
# RETURN: a list of Arch Linux package names,
#         one per line
archlinux_dependencies_providing_native_libraries_32bit() {
	local library packages_list package
	packages_list=''
	for library in "$@"; do
		package=$(dependency_package_providing_library_arch32 "$library")
		packages_list="$packages_list
		$package"
	done

	printf '%s' "$packages_list" | list_clean
}

# Arch Linux - Print the package name providing the given native library
# USAGE: dependency_package_providing_library_arch $library
dependency_package_providing_library_arch() {
	local library package_name
	library="$1"
	case "$library" in
		('ld-linux.so.2')
			package_name='glibc'
		;;
		('ld-linux-x86-64.so.2')
			package_name='glibc'
		;;
		('liballeg.so.4.4')
			package_name='allegro4'
		;;
		('liballegro.so.5.2')
			package_name='allegro'
		;;
		('liballegro_acodec.so.5.2')
			package_name='allegro'
		;;
		('liballegro_audio.so.5.2')
			package_name='allegro'
		;;
		('liballegro_font.so.5.2')
			package_name='allegro'
		;;
		('liballegro_image.so.5.2')
			package_name='allegro'
		;;
		('liballegro_primitives.so.5.2')
			package_name='allegro'
		;;
		('liballegro_ttf.so.5.2')
			package_name='allegro'
		;;
		('libalut.so.0')
			package_name='freealut'
		;;
		('libasound.so.2')
			package_name='alsa-lib'
		;;
		('libasound_module_'*'.so')
			package_name='alsa-plugins'
		;;
		('libatspi.so.0')
			package_name='at-spi2-core'
		;;
		('libatk-1.0.so.0')
			package_name='atk'
		;;
		('libaudio.so.2')
			package_name='nas'
		;;
		('libboost_locale.so.1.74.0')
			# This library is not provided for Arch Linux
			unset package_name
		;;
		('libbz2.so.1.0'|'libbz2.so.1')
			package_name='bzip2'
		;;
		('libc.so.6')
			package_name='glibc'
		;;
		('libc++.so.1')
			package_name='libc++'
		;;
		('libc++abi.so.1')
			package_name='libc++abi'
		;;
		('libcairo.so.2')
			package_name='cairo'
		;;
		('libCg.so')
			package_name='nvidia-cg-toolkit'
		;;
		('libCgGL.so')
			package_name='nvidia-cg-toolkit'
		;;
		('libcom_err.so.2')
			package_name='e2fsprogs'
		;;
		('libcrypt.so.1')
			package_name='libxcrypt'
		;;
		('libcups.so.2')
			package_name='libcups'
		;;
		('libcurl.so.4')
			package_name='curl'
		;;
		('libcurl.so.4+CURL_OPENSSL_3')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/curl/
			return 0
		;;
		('libcurl-gnutls.so.4')
			package_name='libcurl-gnutls'
		;;
		('libdbus-1.so.3')
			package_name='dbus'
		;;
		('libdbus-glib-1.so.2')
			package_name='dbus-glib'
		;;
		('libdl.so.2')
			package_name='glibc'
		;;
		('libEGL.so.1')
			package_name='libglvnd'
		;;
		('libexpat.so.1')
			package_name='expat'
		;;
		('libFAudio.so.0')
			package_name='faudio'
		;;
		('libFLAC.so.8')
			package_name='flac1.3'
		;;
		('libfontconfig.so.1')
			package_name='fontconfig'
		;;
		('libfreeimage.so.3')
			package_name='freeimage'
		;;
		('libfreetype.so.6')
			package_name='freetype2'
		;;
		('libfribidi.so.0')
			package_name='fribidi'
		;;
		('libgcc_s.so.1')
			package_name='gcc-libs'
		;;
		('libgconf-2.so.4')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/gconf/
			return 0
		;;
		('libgcrypt.so.11')
			package_name='libgcrypt15'
		;;
		('libgdiplus.so')
			package_name='libgdiplus'
		;;
		('libgdk-3.so.0')
			package_name='gtk3'
		;;
		('libgdk_pixbuf-2.0.so.0')
			package_name='gdk-pixbuf2'
		;;
		('libgdk-x11-2.0.so.0')
			package_name='gtk2'
		;;
		('libgio-2.0.so.0')
			package_name='glib2'
		;;
		('libGL.so.1')
			package_name='libgl'
		;;
		('libGLEW.so.2.2')
			package_name='glew'
		;;
		('libglfw.so.3')
			package_name='glfw'
		;;
		('libglib-2.0.so.0')
			package_name='glib2'
		;;
		('libGLU.so.1')
			package_name='glu'
		;;
		('libGLX.so.0')
			package_name='libglvnd'
		;;
		('libgmodule-2.0.so.0')
			package_name='glib2'
		;;
		('libgobject-2.0.so.0')
			package_name='glib2'
		;;
		('libgomp.so.1')
			package_name='gcc-libs'
		;;
		('libgpg-error.so.0')
			package_name='libgpg-error'
		;;
		('libgssapi_krb5.so.2')
			package_name='krb5'
		;;
		('libgthread-2.0.so.0')
			package_name='glib2'
		;;
		('libgtk-x11-2.0.so.0')
			package_name='gtk2'
		;;
		('libgtk-3.so.0')
			package_name='gtk3'
		;;
		('libICE.so.6')
			package_name='libice'
		;;
		('libidn.so.11')
			package_name='libidn11'
		;;
		('libidn2.so.0')
			package_name='libidn2'
		;;
		('libIL.so.1')
			package_name='devil'
		;;
		('libjpeg.so.62')
			package_name='libjpeg6-turbo'
		;;
		('libk5crypto.so.3')
			package_name='krb5'
		;;
		('libkrb5.so.3')
			package_name='krb5'
		;;
		('liblcms2.so.2')
			package_name='lcms2'
		;;
		('liblua5.3.so.0')
			package_name='lua'
		;;
		('libluajit-5.1.so.2')
			package_name='luajit'
		;;
		('liblz4.so.1')
			package_name='lz4'
		;;
		('libm.so.6')
			package_name='glibc'
		;;
		('libmbedtls.so.12')
			package_name='mbedtls'
		;;
		('libminiupnpc.so.17')
			package_name='miniupnpc'
		;;
		('libminizip.so.1')
			package_name='minizip'
		;;
		('libmodplug.so.1')
			package_name='libmodplug'
		;;
		('libmpg123.so.0')
			package_name='mpg123'
		;;
		('libnghttp2.so.14')
			package_name='libnghttp2'
		;;
		('libnotify.so.4')
			package_name='libnotify'
		;;
		('libnspr4.so')
			package_name='nspr'
		;;
		('libnss3.so')
			package_name='nss'
		;;
		('libnssutil3.so')
			package_name='nss'
		;;
		('libogg.so.0')
			package_name='libogg'
		;;
		('libopenal.so.1')
			package_name='openal'
		;;
		('libOpenGL.so.0')
			package_name='libglvnd'
		;;
		('libopenmpt.so.0')
			package_name='libopenmpt'
		;;
		('libpango-1.0.so.0')
			package_name='pango'
		;;
		('libpangocairo-1.0.so.0')
			package_name='pango'
		;;
		('libpangoft2-1.0.so.0')
			package_name='pango'
		;;
		('libpcre.so.3')
			# This library is not provided for Arch Linux
			unset package_name
		;;
		('libphysfs.so.1')
			package_name='physfs'
		;;
		('libpixman-1.so.0')
			package_name='pixman'
		;;
		('libplc4.so')
			package_name='nspr'
		;;
		('libplds4.so')
			package_name='nspr'
		;;
		('libpng12.so.0')
			package_name='libpng12'
		;;
		('libpng16.so.16')
			package_name='libpng'
		;;
		('libpsl.so.5')
			package_name='libpsl'
		;;
		('libpthread.so.0')
			package_name='glibc'
		;;
		('libpulse.so.0')
			package_name='libpulse'
		;;
		('libpulse-simple.so.0')
			package_name='libpulse'
		;;
		('libresolv.so.2')
			package_name='glibc'
		;;
		('librt.so.1')
			package_name='glibc'
		;;
		('librtmp.so.1')
			package_name='rtmpdump'
		;;
		('libSDL-1.2.so.0')
			package_name='sdl'
		;;
		('libSDL_image-1.2.so.0')
			package_name='sdl_image'
		;;
		('libSDL_kitchensink.so.1')
			# This library is not provided for Arch Linux
			unset package_name
		;;
		('libSDL_mixer-1.2.so.0')
			package_name='sdl_mixer'
		;;
		('libSDL_sound-1.0.so.1')
			package_name='sdl_sound'
		;;
		('libSDL_ttf-2.0.so.0')
			package_name='sdl_ttf'
		;;
		('libSDL2-2.0.so.0')
			package_name='sdl2'
		;;
		('libSDL2_image-2.0.so.0')
			package_name='sdl2_image'
		;;
		('libSDL2_mixer-2.0.so.0')
			package_name='sdl2_mixer'
		;;
		('libSDL2_ttf-2.0.so.0')
			package_name='sdl2_ttf'
		;;
		('libsecret-1.so.0')
			package_name='libsecret'
		;;
		('libsigc-2.0.so.0')
			package_name='libsigc++'
		;;
		('libSM.so.6')
			package_name='libsm'
		;;
		('libsmime3.so')
			package_name='nss'
		;;
		('libsmpeg-0.4.so.0')
			package_name='smpeg'
		;;
		('libsodium.so.23')
			package_name='libsodium'
		;;
		('libssh2.so.1')
			package_name='libssh2'
		;;
		('libssl.so.1.0.0')
			package_name='openssl-1.0'
		;;
		('libssl.so.1.1')
			package_name='openssl-1.1'
		;;
		('libssl3.so')
			package_name='nss'
		;;
		('libstdc++.so.5')
			package_name='libstdc++5'
		;;
		('libstdc++.so.6')
			package_name='gcc-libs'
		;;
		('libtcmalloc_minimal.so.4')
			package_name='gperftools'
		;;
		('libtheora.so.0')
			package_name='libtheora'
		;;
		('libtheoradec.so.1')
			package_name='libtheora'
		;;
		('libtheoraenc.so.1')
			package_name='libtheora'
		;;
		('libthread_db.so.1')
			package_name='glibc'
		;;
		('libtiff.so.6')
			package_name='libtiff'
		;;
		('libturbojpeg.so.0')
			package_name='libjpeg-turbo'
		;;
		('libudev.so.0')
			package_name='libudev0-shim'
		;;
		('libudev.so.1')
			package_name='libudev.so=1-64'
		;;
		('libutil.so.1')
			package_name='glibc'
		;;
		('libuuid.so.1')
			package_name='util-linux-libs'
		;;
		('libuv.so.1')
			package_name='libuv'
		;;
		('libvorbis.so.0')
			package_name='libvorbis'
		;;
		('libvorbisenc.so.2')
			package_name='libvorbis'
		;;
		('libvorbisfile.so.3')
			package_name='libvorbis'
		;;
		('libvulkan.so.1')
			package_name='
			vulkan-icd-loader
			vulkan-driver'
		;;
		('libwayland-client.so.0')
			package_name='wayland'
		;;
		('libX11.so.6')
			package_name='libx11'
		;;
		('libX11-xcb.so.1')
			package_name='libx11'
		;;
		('libxcb.so.1')
			package_name='libxcb'
		;;
		('libxcb-randr.so.0')
			package_name='libxcb'
		;;
		('libXcomposite.so.1')
			package_name='libxcomposite'
		;;
		('libXcursor.so.1')
			package_name='libxcursor'
		;;
		('libXdamage.so.1')
			package_name='libxdamage'
		;;
		('libXext.so.6')
			package_name='libxext'
		;;
		('libXfixes.so.3')
			package_name='libxfixes'
		;;
		('libXft.so.2')
			package_name='libxft'
		;;
		('libXi.so.6')
			package_name='libxi'
		;;
		('libXinerama.so.1')
			package_name='libxinerama'
		;;
		('libxml2.so.2')
			package_name='libxml2'
		;;
		('libxmp.so.4')
			package_name='libxmp'
		;;
		('libXmu.so.6')
			package_name='libxmu'
		;;
		('libXrandr.so.2')
			package_name='libxrandr'
		;;
		('libXrender.so.1')
			package_name='libxrender'
		;;
		('libxslt.so.1')
			package_name='libxslt'
		;;
		('libXss.so.1')
			package_name='libxss'
		;;
		('libXt.so.6')
			package_name='libxt'
		;;
		('libXtst.so.6')
			package_name='libxtst'
		;;
		('libXxf86vm.so.1')
			package_name='libxxf86vm'
		;;
		('libyaml-0.so.2')
			package_name='libyaml'
		;;
		('libz.so.1')
			package_name='zlib'
		;;
	esac

	if [ -n "${package_name:-}" ]; then
		printf '%s' "$package_name"
		return 0
	fi

	dependencies_unknown_libraries_add "$library"
}

# Arch Linux - Print the package name providing the given native library in a 32-bit build
# USAGE: dependency_package_providing_library_arch32 $library
dependency_package_providing_library_arch32() {
	local library package_name
	library="$1"
	case "$library" in
		('ld-linux.so.2')
			package_name='lib32-glibc'
		;;
		('ld-linux-x86-64.so.2')
			package_name='lib32-glibc'
		;;
		('liballeg.so.4.4')
			package_name='lib32-allegro4'
		;;
		('liballegro.so.5.2')
			package_name='lib32-allegro'
		;;
		('liballegro_acodec.so.5.2')
			package_name='lib32-allegro'
		;;
		('liballegro_audio.so.5.2')
			package_name='lib32-allegro'
		;;
		('liballegro_font.so.5.2')
			package_name='lib32-allegro'
		;;
		('liballegro_image.so.5.2')
			package_name='lib32-allegro'
		;;
		('liballegro_primitives.so.5.2')
			package_name='lib32-allegro'
		;;
		('liballegro_ttf.so.5.2')
			package_name='lib32-allegro'
		;;
		('libalut.so.0')
			package_name='lib32-freealut'
		;;
		('libasound.so.2')
			package_name='lib32-alsa-lib'
		;;
		('libasound_module_'*'.so')
			package_name='lib32-alsa-plugins'
		;;
		('libatspi.so.0')
			package_name='lib32-at-spi2-core'
		;;
		('libatk-1.0.so.0')
			package_name='lib32-atk'
		;;
		('libaudio.so.2')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libboost_locale.so.1.74.0')
			# This library is not provided for Arch Linux
			unset package_name
		;;
		('libbz2.so.1.0'|'libbz2.so.1')
			package_name='lib32-bzip2'
		;;
		('libc.so.6')
			package_name='lib32-glibc'
		;;
		('libc++.so.1')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libc++abi.so.1')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libcairo.so.2')
			package_name='lib32-cairo'
		;;
		('libCg.so')
			package_name='lib32-nvidia-cg-toolkit'
		;;
		('libCgGL.so')
			package_name='lib32-nvidia-cg-toolkit'
		;;
		('libcom_err.so.2')
			package_name='lib32-e2fsprogs'
		;;
		('libcrypt.so.1')
			package_name='lib32-libxcrypt'
		;;
		('libcups.so.2')
			package_name='lib32-libcups'
		;;
		('libcurl.so.4')
			package_name='lib32-curl'
		;;
		('libcurl.so.4+CURL_OPENSSL_3')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/curl/
			return 0
		;;
		('libcurl-gnutls.so.4')
			package_name='lib32-libcurl-gnutls'
		;;
		('libdbus-1.so.3')
			package_name='lib32-dbus'
		;;
		('libdbus-glib-1.so.2')
			package_name='lib32-dbus-glib'
		;;
		('libdl.so.2')
			package_name='lib32-glibc'
		;;
		('libEGL.so.1')
			package_name='lib32-libglvnd'
		;;
		('libexpat.so.1')
			package_name='lib32-expat'
		;;
		('libFAudio.so.0')
			package_name='lib32-faudio'
		;;
		('libFLAC.so.8')
			package_name='lib32-flac1.3'
		;;
		('libfontconfig.so.1')
			package_name='lib32-fontconfig'
		;;
		('libfreeimage.so.3')
			package_name='lib32-freeimage'
		;;
		('libfreetype.so.6')
			package_name='lib32-freetype2'
		;;
		('libfribidi.so.0')
			package_name='lib32-fribidi'
		;;
		('libgcc_s.so.1')
			package_name='lib32-gcc-libs'
		;;
		('libgconf-2.so.4')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/gconf/
			return 0
		;;
		('libgcrypt.so.11')
			package_name='lib32-libgcrypt15'
		;;
		('libgdiplus.so')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libgdk-3.so.0')
			package_name='lib32-gtk3'
		;;
		('libgdk_pixbuf-2.0.so.0')
			package_name='lib32-gdk-pixbuf2'
		;;
		('libgdk-x11-2.0.so.0')
			package_name='lib32-gtk2'
		;;
		('libgio-2.0.so.0')
			package_name='lib32-glib2'
		;;
		('libGL.so.1')
			package_name='lib32-libgl'
		;;
		('libGLEW.so.2.2')
			package_name='lib32-glew'
		;;
		('libglfw.so.3')
			package_name='lib32-glfw'
		;;
		('libglib-2.0.so.0')
			package_name='lib32-glib2'
		;;
		('libGLU.so.1')
			package_name='lib32-glu'
		;;
		('libGLX.so.0')
			package_name='lib32-libglvnd'
		;;
		('libgmodule-2.0.so.0')
			package_name='lib32-glib2'
		;;
		('libgobject-2.0.so.0')
			package_name='lib32-glib2'
		;;
		('libgomp.so.1')
			package_name='lib32-gcc-libs'
		;;
		('libgpg-error.so.0')
			package_name='lib32-libgpg-error'
		;;
		('libgssapi_krb5.so.2')
			package_name='lib32-krb5'
		;;
		('libgthread-2.0.so.0')
			package_name='lib32-glib2'
		;;
		('libgtk-x11-2.0.so.0')
			package_name='lib32-gtk2'
		;;
		('libgtk-3.so.0')
			package_name='lib32-gtk3'
		;;
		('libICE.so.6')
			package_name='lib32-libice'
		;;
		('libidn.so.11')
			package_name='lib32-libidn11'
		;;
		('libidn2.so.0')
			package_name='lib32-libidn2'
		;;
		('libIL.so.1')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libjpeg.so.62')
			package_name='lib32-libjpeg6-turbo'
		;;
		('libk5crypto.so.3')
			package_name='lib32-krb5'
		;;
		('libkrb5.so.3')
			package_name='lib32-krb5'
		;;
		('liblcms2.so.2')
			package_name='lib32-lcms2'
		;;
		('liblua5.3.so.0')
			package_name='lib32-lua'
		;;
		('libluajit-5.1.so.2')
			package_name='lib32-luajit'
		;;
		('liblz4.so.1')
			package_name='lib32-lz4'
		;;
		('libm.so.6')
			package_name='lib32-glibc'
		;;
		('libmbedtls.so.12')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libminiupnpc.so.17')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libminizip.so.1')
			package_name='lib32-minizip'
		;;
		('libmodplug.so.1')
			package_name='lib32-libmodplug'
		;;
		('libmpg123.so.0')
			package_name='lib32-mpg123'
		;;
		('libnghttp2.so.14')
			package_name='lib32-libnghttp2'
		;;
		('libnotify.so.4')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libnspr4.so')
			package_name='lib32-nspr'
		;;
		('libnss3.so')
			package_name='lib32-nss'
		;;
		('libnssutil3.so')
			package_name='lib32-nss'
		;;
		('libogg.so.0')
			package_name='lib32-libogg'
		;;
		('libopenal.so.1')
			package_name='lib32-openal'
		;;
		('libOpenGL.so.0')
			package_name='lib32-libglvnd'
		;;
		('libopenmpt.so.0')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libpango-1.0.so.0')
			package_name='lib32-pango'
		;;
		('libpangocairo-1.0.so.0')
			package_name='lib32-pango'
		;;
		('libpangoft2-1.0.so.0')
			package_name='lib32-pango'
		;;
		('libpcre.so.3')
			# This library is not provided for Arch Linux
			unset package_name
		;;
		('libphysfs.so.1')
			package_name='lib32-physfs'
		;;
		('libpixman-1.so.0')
			package_name='lib32-pixman'
		;;
		('libplc4.so')
			package_name='lib32-nspr'
		;;
		('libplds4.so')
			package_name='lib32-nspr'
		;;
		('libpng12.so.0')
			package_name='lib32-libpng12'
		;;
		('libpng16.so.16')
			package_name='lib32-libpng'
		;;
		('libpsl.so.5')
			package_name='lib32-libpsl'
		;;
		('libpthread.so.0')
			package_name='lib32-glibc'
		;;
		('libpulse.so.0')
			package_name='lib32-libpulse'
		;;
		('libpulse-simple.so.0')
			package_name='lib32-libpulse'
		;;
		('libresolv.so.2')
			package_name='lib32-glibc'
		;;
		('librt.so.1')
			package_name='lib32-glibc'
		;;
		('librtmp.so.1')
			package_name='lib32-rtmpdump'
		;;
		('libSDL-1.2.so.0')
			package_name='lib32-sdl'
		;;
		('libSDL_image-1.2.so.0')
			package_name='lib32-sdl_image'
		;;
		('libSDL_kitchensink.so.1')
			# This library is not provided for Arch Linux
			unset package_name
		;;
		('libSDL_mixer-1.2.so.0')
			package_name='lib32-sdl_mixer'
		;;
		('libSDL_sound-1.0.so.1')
			package_name='lib32-sdl_sound'
		;;
		('libSDL_ttf-2.0.so.0')
			package_name='lib32-sdl_ttf'
		;;
		('libSDL2-2.0.so.0')
			package_name='lib32-sdl2'
		;;
		('libSDL2_image-2.0.so.0')
			package_name='lib32-sdl2_image'
		;;
		('libSDL2_mixer-2.0.so.0')
			package_name='lib32-sdl2_mixer'
		;;
		('libSDL2_ttf-2.0.so.0')
			package_name='lib32-sdl2_ttf'
		;;
		('libsecret-1.so.0')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libsigc-2.0.so.0')
			package_name='lib32-libsigc++'
		;;
		('libSM.so.6')
			package_name='lib32-libsm'
		;;
		('libsmime3.so')
			package_name='lib32-nss'
		;;
		('libsmpeg-0.4.so.0')
			package_name='lib32-smpeg'
		;;
		('libsodium.so.23')
			package_name='lib32-libsodium'
		;;
		('libssh2.so.1')
			package_name='lib32-libssh2'
		;;
		('libssl.so.1.0.0')
			package_name='lib32-openssl-1.0'
		;;
		('libssl.so.1.1')
			package_name='lib32-openssl-1.1'
		;;
		('libssl3.so')
			package_name='lib32-nss'
		;;
		('libstdc++.so.5')
			package_name='lib32-libstdc++5'
		;;
		('libstdc++.so.6')
			package_name='lib32-gcc-libs'
		;;
		('libtcmalloc_minimal.so.4')
			package_name='lib32-gperftools'
		;;
		('libtheora.so.0')
			package_name='lib32-libtheora'
		;;
		('libtheoradec.so.1')
			package_name='lib32-libtheora'
		;;
		('libtheoraenc.so.1')
			package_name='lib32-libtheora'
		;;
		('libthread_db.so.1')
			package_name='lib32-glibc'
		;;
		('libtiff.so.6')
			package_name='lib32-libtiff'
		;;
		('libturbojpeg.so.0')
			package_name='lib32-libjpeg-turbo'
		;;
		('libudev.so.0')
			package_name='lib32-libudev0-shim'
		;;
		('libudev.so.1')
			package_name='lib32-systemd'
		;;
		('libutil.so.1')
			package_name='lib32-glibc'
		;;
		('libuuid.so.1')
			package_name='lib32-util-linux'
		;;
		('libuv.so.1')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libvorbis.so.0')
			package_name='lib32-libvorbis'
		;;
		('libvorbisenc.so.2')
			package_name='lib32-libvorbis'
		;;
		('libvorbisfile.so.3')
			package_name='lib32-libvorbis'
		;;
		('libvulkan.so.1')
			package_name='
			lib32-vulkan-icd-loader
			lib32-vulkan-driver'
		;;
		('libwayland-client.so.0')
			package_name='lib32-wayland'
		;;
		('libX11.so.6')
			package_name='lib32-libx11'
		;;
		('libX11-xcb.so.1')
			package_name='lib32-libx11'
		;;
		('libxcb.so.1')
			package_name='lib32-libxcb'
		;;
		('libxcb-randr.so.0')
			package_name='lib32-libxcb'
		;;
		('libXcomposite.so.1')
			package_name='lib32-libxcomposite'
		;;
		('libXcursor.so.1')
			package_name='lib32-libxcursor'
		;;
		('libXdamage.so.1')
			package_name='lib32-libxdamage'
		;;
		('libXext.so.6')
			package_name='lib32-libxext'
		;;
		('libXfixes.so.3')
			package_name='lib32-libxfixes'
		;;
		('libXft.so.2')
			package_name='lib32-libxft'
		;;
		('libXi.so.6')
			package_name='lib32-libxi'
		;;
		('libXinerama.so.1')
			package_name='lib32-libxinerama'
		;;
		('libxml2.so.2')
			package_name='lib32-libxml2'
		;;
		('libxmp.so.4')
			package_name='lib32-libxmp-git'
		;;
		('libXmu.so.6')
			package_name='lib32-libxmu'
		;;
		('libXrandr.so.2')
			package_name='lib32-libxrandr'
		;;
		('libXrender.so.1')
			package_name='lib32-libxrender'
		;;
		('libxslt.so.1')
			package_name='lib32-libxslt'
		;;
		('libXss.so.1')
			package_name='lib32-libxss'
		;;
		('libXt.so.6')
			package_name='lib32-libxt'
		;;
		('libXtst.so.6')
			package_name='lib32-libxtst'
		;;
		('libXxf86vm.so.1')
			package_name='lib32-libxxf86vm'
		;;
		('libyaml-0.so.2')
			# This library is not provided in a 32-bit build for Arch Linux
			unset package_name
		;;
		('libz.so.1')
			package_name='lib32-zlib'
		;;
	esac

	if [ -n "${package_name:-}" ]; then
		printf '%s' "$package_name"
		return 0
	fi

	dependencies_unknown_libraries_add "$library"
}

# Arch Linux - Print the sinling package names required by the given package
# USAGE: archlinux_dependencies_siblings $package
# RETURN: a list of package names,
#         one per line
archlinux_dependencies_siblings() {
	local package
	package="$1"

	local required_siblings
	required_siblings=$(dependencies_list_siblings "$package")
	# Return early if the current package does not require any sibling
	if [ -z "$required_siblings" ]; then
		return 0
	fi

	local sibling dependencies_siblings_list required_packages
	while read -r sibling; do
		required_package=$(package_id "$sibling")
		dependencies_siblings_list="$dependencies_siblings_list
		$required_package"
	done <<- EOL
	$(printf '%s' "$required_siblings")
	EOL

	printf '%s' "$dependencies_siblings_list" | list_clean
}

# print mtree computation message
# USAGE: info_package_mtree_computation $package
info_package_mtree_computation() {
	local package
	package="$1"

	local package_path
	package_path=$(package_path "$package")

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Création du fichier .MTREE pour %s…\n'
		;;
		('en'|*)
			message='Creating .MTREE file for %s…\n'
		;;
	esac
	print_message 'info' "$message" \
		"$package_path"
}
# Debian - Print installation instructions
# USAGE: debian_install_instructions $package[…]
debian_install_instructions() {
	if [ "${PLAYIT_DEBIAN_OLD_DEB_FORMAT:-0}" -eq 1 ]; then
		debian_install_instructions_dpkg "$@"
	else
		debian_install_instructions_apt "$@"
	fi
}

# Debian - Print installation instructions, using apt
# USAGE: debian_install_instructions_apt $package[…]
debian_install_instructions_apt() {
	local option_output_dir string_format
	option_output_dir=$(option_value 'output-dir')
	if printf '%s' "$option_output_dir" | grep --quiet --fixed-strings ' '; then
		string_format=' "%s"'
	else
		string_format=' %s'
	fi

	printf 'apt install'

	local package package_name package_output string_format
	for package in "$@"; do
		package_name=$(package_name "$package")
		package_output=$(realpath "${option_output_dir}/${package_name}")
		## Silence ShellCheck false-positive
		## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
		# shellcheck disable=SC2059
		printf "$string_format" "$package_output"
	done

	printf '\n'
}

# Debian - Print installation instructions, using dpkg
# USAGE: debian_install_instructions_dpkg $package[…]
debian_install_instructions_dpkg() {
	local option_output_dir string_format
	option_output_dir=$(option_value 'output-dir')
	if printf '%s' "$option_output_dir" | grep --quiet --fixed-strings ' '; then
		string_format=' "%s"'
	else
		string_format=' %s'
	fi

	printf 'dpkg --install'

	local package package_name package_output string_format
	for package in "$@"; do
		package_name=$(package_name "$package")
		package_output=$(realpath "${option_output_dir}/${package_name}")
		## Silence ShellCheck false-positive
		## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
		# shellcheck disable=SC2059
		printf "$string_format" "$package_output"
	done

	printf '\n'
	printf 'apt-get install --fix-broken\n\n'

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Les éventuelles erreurs de dépendances suite à la commande dpkg peuvent être ignorées,'
			message="$message"' elles seront corrigées par la commande apt-get à entrer ensuite.\n'
		;;
		('en'|*)
			message='Potential errors related to dependencies after the dpkg command can be ignored,'
			message="$message"' they will be fixed by the command apt-get to run afterwards.\n'
		;;
	esac
	print_message 'info' "$message"
}

# Debian - Write the metadata for the listed packages
# USAGE: debian_packages_metadata $package[…]
debian_packages_metadata() {
	local package
	for package in "$@"; do
		debian_package_metadata_single "$package"
	done
}

# Debian - Write the metadata for the given package
# USAGE: debian_package_metadata_single $package
debian_package_metadata_single() {
	local package
	package="$1"

	# Create metadata directory.
	local package_path control_directory
	package_path=$(package_path "$package")
	control_directory="${package_path}/DEBIAN"
	mkdir --parents "$control_directory"

	# Write main metadata file (DEBIAN/control).
	local control_file
	control_file="${control_directory}/control"
	debian_package_metadata_control "$package" > "$control_file"

	# Write postinst/prerm scripts, enforce correct permissions.
	local postinst_actions postinst_warnings
	postinst_actions=$(package_postinst_actions "$package")
	postinst_warnings=$(package_postinst_warnings "$package")
	if [ -n "$postinst_actions" ] || [ -n "$postinst_warnings" ]; then
		debian_script_postinst "$package" > "${control_directory}/postinst"
		chmod 755 "${control_directory}/postinst"
	fi
	local prerm_actions
	prerm_actions=$(package_prerm_actions "$package")
	if [ -n "$prerm_actions" ]; then
		debian_script_prerm "$package" > "${control_directory}/prerm"
		chmod 755 "${control_directory}/prerm"
	fi
}

# Print the content of the DEBIAN/control metadata file for the given package
# USAGE: debian_package_metadata_control $package
# RETURN: the contents of the DEBIAN/control file,
#         spanning over multiple lines
debian_package_metadata_control() {
	local package
	package="$1"

	local \
		debian_field_package \
		debian_field_source \
		debian_field_version \
		debian_field_architecture \
		debian_field_maintainer \
		debian_field_installedsize \
		debian_field_provides \
		debian_field_depends \
		debian_field_description
	debian_field_package=$(debian_field_package "$package")
	debian_field_source=$(debian_field_source "$package")
	debian_field_version=$(debian_field_version "$package")
	debian_field_architecture=$(debian_field_architecture "$package")
	debian_field_maintainer=$(debian_field_maintainer "$package")
	debian_field_installedsize=$(debian_field_installedsize "$package")
	debian_field_provides=$(debian_field_provides "$package")
	debian_field_depends=$(debian_field_depends "$package")
	debian_field_description=$(debian_field_description "$package")

	cat <<- EOF
	Package: $debian_field_package
	Source: $debian_field_source
	Version: $debian_field_version
	Architecture: $debian_field_architecture
	Multi-Arch: foreign
	Maintainer: $debian_field_maintainer
	Installed-Size: $debian_field_installedsize
	Section: non-free/games
	EOF
	if [ -n "$debian_field_provides" ]; then
		cat <<- EOF
		Conflicts: $debian_field_provides
		Provides: $debian_field_provides
		Replaces: $debian_field_provides
		EOF
	fi
	if [ -n "$debian_field_depends" ]; then
		cat <<- EOF
		Depends: $debian_field_depends
		EOF
	fi
	cat <<- EOF
	Description: $debian_field_description
	EOF
}

# Debian - Build a list of packages
# USAGE: debian_packages_build $package[…]
debian_packages_build() {
	local package
	for package in "$@"; do
		debian_package_build_single "$package"
	done
}

# Debian - Build a single package
# USAGE: debian_package_build_single $package
debian_package_build_single() {
	local package
	package="$1"

	local package_path
	package_path=$(package_path "$package")

	local option_output_dir package_name generated_package_path
	option_output_dir=$(option_value 'output-dir')
	package_name=$(package_name "$package")
	generated_package_path="${option_output_dir}/${package_name}"

	# Skip packages already existing,
	# unless called with --overwrite.
	local option_overwrite
	option_overwrite=$(option_value 'overwrite')
	if \
		[ "$option_overwrite" -eq 0 ] \
		&& [ -e "$generated_package_path" ]
	then
		information_package_already_exists "$package_name"
		return 0
	fi

	# Set the common dpkg-deb options
	local dpkg_options
	## Create all files and directories with owner:group = root:root.
	dpkg_options='--root-owner-group'

	# Use old .deb format if the package is going over the size limit for the modern format
	local package_size
	package_size=$(debian_field_installedsize "$package")
	if [ "$package_size" -gt 9700000 ]; then
		warning_debian_size_limit "$package"
		export PLAYIT_DEBIAN_OLD_DEB_FORMAT=1
		dpkg_options="${dpkg_options:-} --deb-format=0.939000"
	fi

	# Set compression setting
	local option_compression
	option_compression=$(option_value 'compression')
	case "$option_compression" in
		('none')
			dpkg_options="${dpkg_options:-} -Znone"
		;;
		('speed')
			dpkg_options="${dpkg_options:-} -Zgzip"
		;;
		('size')
			if [ "${PLAYIT_DEBIAN_OLD_DEB_FORMAT:-0}" -eq 1 ]; then
				## Old .deb format 0.939000 is not compatible with xz compression.
				dpkg_options="${dpkg_options:-} -Zgzip"
			else
				dpkg_options="${dpkg_options:-} -Zxz"
			fi
		;;
		('auto')
			if [ "${PLAYIT_DEBIAN_OLD_DEB_FORMAT:-0}" -eq 1 ]; then
				## Old .deb format 0.939000 is not compatible with xz compression.
				dpkg_options="${dpkg_options:-} -Zgzip"
			fi
		;;
	esac

	# Run the actual package generation, using dpkg-deb
	local TMPDIR package_generation_return_code
	information_package_building "$package_name"
	## We need to explicitly export TMPDIR, or dpkg-deb will not pick it up.
	export TMPDIR="$PLAYIT_WORKDIR"
	{
		dpkg-deb ${dpkg_options:-} --build "$package_path" "$generated_package_path" 1>/dev/null
		package_generation_return_code=$?
	} || true
	if [ $package_generation_return_code -ne 0 ]; then
		error_package_generation_failed "$package_name"
		return 1
	fi
}

# Print the file name of the given package
# USAGE: package_name_debian $package
# RETURNS: the file name, as a string
package_name_debian() {
	local package
	package="$1"

	local package_id package_version package_architecture package_name
	package_id=$(package_id "$package")
	package_version=$(package_version)
	package_architecture=$(debian_field_architecture "$package")
	package_name="${package_id}_${package_version}_${package_architecture}.deb"

	printf '%s' "$package_name"
}

# Get the path to the directory where the given package is prepared,
# relative to the directory where all packages are stored
# USAGE: package_path_debian $package
# RETURNS: relative path to a directory, as a string
package_path_debian() {
	local package
	package="$1"

	local package_name package_path
	package_name=$(package_name "$package")
	package_path="${package_name%.deb}"

	printf '%s' "$package_path"
}

# Debian - Print the content of the "Package" field
# USAGE: debian_field_package $package
# RETURN: the field content
debian_field_package() {
	local package
	package="$1"

	package_id "$package"
}

# Debian - Print the content of the "Source" field
# USAGE: debian_field_source $package
# RETURN: the field content
debian_field_source() {
	## The package argument is not actually used,
	## but is passed for consistency with other debian_field_* functions.
	local package
	package="$1"

	## Including the version should make upgrades easier when using `apt install --with-source (…)`.
	local game_id package_version
	game_id=$(game_id)
	package_version=$(package_version)

	printf '%s (%s)' "$game_id" "$package_version"
}

# Debian - Print the content of the "Version" field
# USAGE: debian_field_version $package
# RETURN: the field content
debian_field_version() {
	## The package argument is not actually used,
	## but is passed for consistency with other debian_field_* functions.
	local package
	package="$1"

	package_version
}

# Debian - Print the content of the "Architecture" field
# USAGE: debian_field_architecture $package
# RETURN: the field content
debian_field_architecture() {
	local package
	package="$1"

	local package_architecture package_architecture_string
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			package_architecture_string='i386'
		;;
		('64')
			package_architecture_string='amd64'
		;;
		('all')
			package_architecture_string='all'
		;;
	esac

	printf '%s' "$package_architecture_string"
}

# Debian - Print the content of the "Maintainer" field
# USAGE: debian_field_maintainer $package
# RETURN: the field content
debian_field_maintainer() {
	## The package argument is not actually used,
	## but is passed for consistency with other debian_field_* functions.
	local package
	package="$1"

	package_maintainer
}

# Debian - Print the content of the "Installed-Size" field
# USAGE: debian_field_installedsize $package
# RETURN: the field content
debian_field_installedsize() {
	local package
	package="$1"

	# Compute the package size, in kilobytes
	local package_path
	package_path=$(package_path "$package")
	du --total --block-size=1K --summarize "$package_path" | \
		tail --lines=1 | \
		cut --fields=1
}

# Debian - Print the content of the "Provides" field
# This value is used for the fields "Conflicts" and "Replaces" too.
# USAGE: debian_field_provides $package
# RETURN: the field content
debian_field_provides() {
	local package
	package="$1"

	local package_provides
	package_provides=$(package_provides "$package")

	# Return early if there is no package name provided
	if [ -z "$package_provides" ]; then
		return 0
	fi

	local provides_list package_name
	while read -r package_name; do
		if [ -z "${provides_list:-}" ]; then
			provides_list="$package_name"
		else
			provides_list="$provides_list, $package_name"
		fi
	done <<- EOL
	$(printf '%s' "$package_provides")
	EOL

	printf '%s' "$provides_list"
}

# Debian - Print the content of the "Depends" field
# USAGE: debian_field_depends $package
# RETURN: the field content
debian_field_depends() {
	local package
	package="$1"

	local dependencies_list first_item_displayed dependency_string
	dependencies_list=$(dependencies_debian_full_list "$package")
	first_item_displayed=0
	while read -r dependency_string; do
		if [ -z "$dependency_string" ]; then
			continue
		fi
		if [ "$first_item_displayed" -eq 0 ]; then
			printf '%s' "$dependency_string"
			first_item_displayed=1
		else
			printf ', %s' "$dependency_string"
		fi
	done <<- EOF
	$(printf '%s' "$dependencies_list")
	EOF
}

# Debian - Print the content of the "Description" field
# USAGE: debian_field_description $package
# RETURN: the field content,
#         spanning over multiple lines
debian_field_description() {
	local package
	package="$1"

	local game_name package_description script_version_string
	game_name=$(game_name)
	package_description=$(package_description "$package")
	script_version_string=$(script_version)

	printf '%s' "$game_name"
	if [ -n "$package_description" ]; then
		printf -- ' - %s' "$package_description"
	fi
	printf '\n ./play.it script version %s' "$script_version_string"
}

# Debian - Print the content of the DEBIAN/postinst script for the given package
# USAGE: debian_script_postinst $package
# RETURN: the contents of the DEBIAN/postinst file,
#         spanning over multiple lines
debian_script_postinst() {
	local package
	package="$1"

	cat <<- EOF
	#!/bin/sh
	set -o errexit

	EOF
	## Include actions that should be run.
	package_postinst_actions "$package"
	## Include warnings that should be displayed.
	local warning_messages
	warning_messages=$(package_postinst_warnings "$package")
	if [ -n "$warning_messages" ]; then
		local warning_line
		while read -r warning_line; do
			printf 'printf "Warning: %%s\\n" "%s"\n' "$warning_line"
		done <<- EOL
		$(printf '%s' "$warning_messages")
		EOL
	fi
	cat <<- EOF

	exit 0
	EOF
}

# Debian - Print the content of the DEBIAN/prerm script for the given package
# USAGE: debian_script_prerm $package
# RETURN: the contents of the DEBIAN/prerm file,
#         spanning over multiple lines
debian_script_prerm() {
	local package
	package="$1"

	cat <<- EOF
	#!/bin/sh
	set -o errexit

	EOF
	## Include actions that should be run.
	package_prerm_actions "$package"
	cat <<- EOF

	exit 0
	EOF
}

# Print the list of packages required to provide the given generic dependency keyword on Debian
# USAGE: pkg_set_deps_deb $dependency_keyword [$package]
pkg_set_deps_deb() {
	# The optional second argument is only used with the "wine" dependency keyword.
	local dependency_keyword package
	dependency_keyword="$1"
	package="$2"

	case "$dependency_keyword" in
		('alsa')
			printf 'libasound2-plugins'
		;;
		('freetype')
			printf 'libfreetype6'
		;;
		('gcc32')
			printf 'gcc-multilib:amd64 | gcc'
		;;
		('glibc')
			printf 'libc6'
		;;
		('glu')
			printf 'libglu1-mesa | libglu1'
		;;
		('glx')
			printf 'libgl1 | libgl1-mesa-glx, libglx-mesa0 | libglx-vendor | libgl1-mesa-glx'
		;;
		('gtk2')
			printf 'libgtk2.0-0'
		;;
		('json')
			printf 'libjson-c3 | libjson-c2 | libjson0'
		;;
		('libstdc++')
			printf 'libstdc++6'
		;;
		('libudev1')
			printf 'libudev1'
		;;
		('libxrandr')
			printf 'libxrandr2'
		;;
		('nss')
			printf 'libnss3'
		;;
		('openal')
			printf 'libopenal1'
		;;
		('sdl2')
			printf 'libsdl2-2.0-0'
		;;
		('xcursor')
			printf 'libxcursor1'
		;;
		( \
			'dosbox' | \
			'java' | \
			'mono' | \
			'pulseaudio' | \
			'scummvm' | \
			'wine' | \
			'winetricks' | \
			'xgamma' | \
			'xrandr' \
		)
			debian_dependencies_single_command "$package" "$dependency_keyword"
		;;
		( \
			'libgdk_pixbuf-2.0.so.0' | \
			'libc.so.6' | \
			'libglib-2.0.so.0' | \
			'libgobject-2.0.so.0' | \
			'libGLU.so.1' | \
			'libGL.so.1' | \
			'libgdk-x11-2.0.so.0' | \
			'libgtk-x11-2.0.so.0' | \
			'libasound.so.2' | \
			'libasound_module_'*'.so' | \
			'libmbedtls.so.12' | \
			'libpng16.so.16' | \
			'libpulse.so.0' | \
			'libpulse-simple.so.0' | \
			'libstdc++.so.6' | \
			'libudev.so.1' | \
			'libX11.so.6' | \
			'libopenal.so.1' | \
			'libSDL-1.2.so.0' | \
			'libSDL2-2.0.so.0' | \
			'libturbojpeg.so.0' | \
			'libuv.so.1' | \
			'libvorbisfile.so.3' | \
			'libz.so.1' \
		)
			dependency_package_providing_library_deb "$dependency_keyword"
		;;
		(*)
			# Unknown dependency keywords are assumed to be litteral package names.
			printf '%s' "$dependency_keyword"
		;;
	esac
}

# Debian - List all dependencies for the given package
# USAGE: dependencies_debian_full_list $package
# RETURN: print a list of dependency strings,
#         one per line
dependencies_debian_full_list() {
	local package
	package="$1"

	local packages_list packages_list_full
	packages_list_full=''

	# Include generic dependencies
	local dependencies_generic dependency_generic dependency_package
	dependencies_generic=$(dependencies_list_generic "$package")
	while read -r dependency_generic; do
		dependency_package=$(pkg_set_deps_deb "$dependency_generic" "$package")
		packages_list_full="$packages_list_full
		$dependency_package"
	done <<- EOL
	$(printf '%s' "$dependencies_generic")
	EOL

	# Include dependencies on sibling package
	packages_list=$(debian_dependencies_siblings "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on commands
	packages_list=$(debian_dependencies_all_commands "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on native libraries
	packages_list=$(dependencies_list_native_libraries_packages "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on Mono libraries
	packages_list=$(dependencies_list_mono_libraries_packages "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on GStreamer plugins
	packages_list=$(debian_dependencies_gstreamer_all_formats "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	printf '%s' "$packages_list_full" | list_clean
}

# Debian - Print the package names providing the commands required by the given package
# USAGE: debian_dependencies_all_commands $package
# RETURN: a list of Debian package names,
#         one per line
debian_dependencies_all_commands() {
	local package
	package="$1"

	local required_commands
	required_commands=$(dependencies_list_commands "$package")
	# Return early if the current package does not require any command
	if [ -z "$required_commands" ]; then
		return 0
	fi

	local command packages_list required_packages
	packages_list=''
	while read -r command; do
		required_packages=$(debian_dependencies_single_command "$package" "$command")
		packages_list="$packages_list
		$required_packages"
	done <<- EOL
	$(printf '%s' "$required_commands")
	EOL

	printf '%s' "$packages_list" | list_clean
}

# Debian - Print the package names providing the required command
# USAGE: debian_dependencies_single_command $package $required_command
# RETURN: a list of Debian package names,
#         one per line
debian_dependencies_single_command() {
	local package required_command
	package="$1"
	required_command="$2"

	local package_names
	case "$required_command" in
		('corsix-th')
			package_names='
			corsix-th'
		;;
		('dos2unix')
			package_names='
			dos2unix'
		;;
		('dosbox')
			package_names='
			dosbox'
		;;
		('java')
			package_names='
			default-jre | java-runtime'
		;;
		('mono')
			package_names='
			mono-runtime'
		;;
		('mpv')
			package_names='
			mpv:amd64 | mpv'
		;;
		('openmw-iniimporter')
			package_names='
			openmw-launcher'
		;;
		('openmw-launcher')
			package_names='
			openmw-launcher'
		;;
		('pulseaudio')
			package_names='
			pulseaudio:amd64 | pulseaudio'
		;;
		('renpy')
			package_names='
			renpy'
		;;
		('scummvm')
			package_names='
			scummvm'
		;;
		('sed')
			# The Debian policy advises against adding dependencies on packages that are part of the required set.
			package_names=''
		;;
		('setxkbmap')
			package_names='
			x11-xkb-utils'
		;;
		('vcmilauncher')
			package_names='
			vcmi'
		;;
		('wine')
			local package_architecture
			package_architecture=$(package_architecture "$package")
			case "$package_architecture" in
				('32')
					package_names='
					wine32 | wine32-development | wine-stable-i386 | wine-devel-i386 | wine-staging-i386
					wine:amd64 | wine'
				;;
				('64')
					package_names='wine64 | wine64-development | wine-stable-amd64 | wine-devel-amd64 | wine-staging-amd64
					wine'
				;;
			esac
		;;
		('winetricks')
			package_names='
			winetricks
			xterm:amd64 | xterm | zenity:amd64 | zenity | kdialog:amd64 | kdialog'
		;;
		('xgamma')
			package_names='
			x11-xserver-utils:amd64 | x11-xserver-utils'
		;;
		('xrandr')
			package_names='
			x11-xserver-utils:amd64 | x11-xserver-utils'
		;;
		(*)
			dependencies_unknown_command_add "$required_command"
			return 0
		;;
	esac

	printf '%s' "$package_names"
}

# Debian - Print the package names providing the GStreamer plugins to decode the formats required by the given package
# USAGE: debian_dependencies_gstreamer_all_formats $package
# RETURN: a list of Debian package names,
#         one per line
debian_dependencies_gstreamer_all_formats() {
	local package
	package="$1"

	local gstreamer_decoders
	gstreamer_decoders=$(dependencies_list_gstreamer_decoders "$package")
	# Return early if the current package does not require any GStreamer plugin
	if [ -z "$gstreamer_decoders" ]; then
		return 0
	fi

	local media_format packages_list required_packages
	packages_list=''
	while read -r media_format; do
		required_packages=$(debian_dependencies_gstreamer_single_format "$media_format")
		packages_list="$packages_list
		$required_packages"
	done <<- EOL
	$(printf '%s' "$gstreamer_decoders")
	EOL

	printf '%s' "$packages_list" | list_clean
}

# Debian - Print the package names providing the required GStreamer plugins to decode the given format
# USAGE: debian_dependency_providing_gstreamer_plugin $media_format
# RETURN: a list of Debian package names,
#         one per line
debian_dependencies_gstreamer_single_format() {
	local media_format
	media_format="$1"

	local package_names
	case "$media_format" in
		('audioconvert')
			package_names='
			gstreamer1.0-plugins-base'
		;;
		('avidemux')
			package_names='
			gstreamer1.0-plugins-good'
		;;
		('decodebin')
			package_names='
			gstreamer1.0-plugins-base'
		;;
		('deinterlace')
			package_names='
			gstreamer1.0-plugins-good'
		;;
		('application/x-id3')
			package_names='
			gstreamer1.0-plugins-good'
		;;
		('audio/mpeg, mpegversion=(int)1, layer=(int)3')
			package_names='
			gstreamer1.0-plugins-good'
		;;
		('audio/x-wma, wmaversion=(int)1')
			package_names='
			gstreamer1.0-libav'
		;;
		('video/mpeg, systemstream=(boolean)true, mpegversion=(int)1')
			package_names='
			gstreamer1.0-plugins-ugly
			gstreamer1.0-plugins-bad'
		;;
		('video/quicktime, variant=(string)iso')
			package_names='
			gstreamer1.0-plugins-good
			gstreamer1.0-libav'
		;;
		('video/x-ms-asf')
			package_names='
			gstreamer1.0-plugins-ugly
			gstreamer1.0-libav'
		;;
		('video/x-msvideo')
			package_names='
			gstreamer1.0-plugins-good
			gstreamer1.0-libav'
		;;
		('video/x-wmv, wmvversion=(int)1')
			package_names='
			gstreamer1.0-libav'
		;;
		(*)
			dependencies_unknown_gstreamer_media_formats_add "$media_format"
			return 0
		;;
	esac

	printf '%s' "$package_names"
}

# Debian - Print the package names providing the given Mono libraries
# WARNING - Unknown Mono libraries are silently omitted.
# USAGE: debian_dependencies_providing_mono_libraries $library[…]
# RETURN: a list of Debian package names,
#         one per line
debian_dependencies_providing_mono_libraries() {
	local library packages_list package
	packages_list=''
	for library in "$@"; do
		package=$(debian_dependency_providing_mono_library "$library")
		packages_list="$packages_list
		$package"
	done

	printf '%s' "$packages_list" | list_clean
}

# Debian - Print the package name providing the given Mono library
# WARNING - Unknown Mono libraries are silently omitted.
# USAGE: debian_dependency_providing_mono_library $library
# RETURN: a single Debian package name,
#         followed by a line break
debian_dependency_providing_mono_library() {
	local library
	library="$1"

	local package_name
	case "$library" in
		('mscorlib.dll')
			package_name='libmono-corlib4.5-cil'
		;;
		('I18N.dll')
			package_name='libmono-i18n4.0-cil'
		;;
		('I18N.West.dll')
			package_name='libmono-i18n-west4.0-cil'
		;;
		('Microsoft.CSharp.dll')
			package_name='libmono-microsoft-csharp4.0-cil'
		;;
		('Mono.CSharp.dll')
			package_name='libmono-csharp4.0c-cil'
		;;
		('Mono.Posix.dll')
			package_name='libmono-posix4.0-cil'
		;;
		('Mono.Security.dll')
			package_name='libmono-security4.0-cil'
		;;
		('OpenTK.dll')
			package_name='libopentk1.1-cil'
		;;
		('OpenTK.Compatibility.dll')
			package_name='libopentk1.1-cil'
		;;
		('OpenTK.GLControl.dll')
			package_name='libopentk1.1-cil'
		;;
		('System.dll')
			package_name='libmono-system4.0-cil'
		;;
		('System.ComponentModel.DataAnnotations.dll')
			package_name='libmono-system-componentmodel-dataannotations4.0-cil'
		;;
		('System.Configuration.dll')
			package_name='libmono-system-configuration4.0-cil'
		;;
		('System.Configuration.Install.dll')
			package_name='libmono-system-configuration-install4.0-cil'
		;;
		('System.Core.dll')
			package_name='libmono-system-core4.0-cil'
		;;
		('System.Data.dll')
			package_name='libmono-system-data4.0-cil'
		;;
		('System.Design.dll')
			package_name='libmono-system-design4.0-cil'
		;;
		('System.Drawing.dll')
			package_name='libmono-system-drawing4.0-cil'
		;;
		('System.IO.Compression.dll')
			package_name='libmono-system-io-compression4.0-cil'
		;;
		('System.IO.Compression.FileSystem.dll')
			package_name='libmono-system-io-compression-filesystem4.0-cil'
		;;
		('System.Management.dll')
			package_name='libmono-system-management4.0-cil'
		;;
		('System.Net.dll')
			package_name='libmono-system-net4.0-cil'
		;;
		('System.Net.Http.dll')
			package_name='libmono-system-net-http4.0-cil'
		;;
		('System.Numerics.dll')
			package_name='libmono-system-numerics4.0-cil'
		;;
		('System.Runtime.Serialization.dll')
			package_name='libmono-system-runtime-serialization4.0-cil'
		;;
		('System.Security.dll')
			package_name='libmono-system-security4.0-cil'
		;;
		('System.Transactions.dll')
			package_name='libmono-system-transactions4.0-cil'
		;;
		('System.Web.dll')
			package_name='libmono-system-web4.0-cil'
		;;
		('System.Web.Extensions.dll')
			package_name='libmono-system-web-extensions4.0-cil'
		;;
		('System.Web.Http.dll')
			package_name='libmono-system-web-http4.0-cil'
		;;
		('System.Web.Services.dll')
			package_name='libmono-system-web-services4.0-cil'
		;;
		('System.Windows.Forms.dll')
			package_name='libmono-system-windows-forms4.0-cil'
		;;
		('System.Xml.dll')
			package_name='libmono-system-xml4.0-cil'
		;;
		('System.Xml.Linq.dll')
			package_name='libmono-system-xml-linq4.0-cil'
		;;
		('WindowsBase.dll')
			package_name='libmono-windowsbase4.0-cil'
		;;
	esac

	if [ -n "${package_name:-}" ]; then
		printf '%s' "$package_name"
		return 0
	fi

	dependencies_unknown_mono_libraries_add "$library"
}

# Debian - Print the package names providing the given native libraries
# USAGE: debian_dependencies_providing_native_libraries $library[…]
# RETURN: a list of Debian package names,
#         one per line
debian_dependencies_providing_native_libraries() {
	local library packages_list package
	packages_list=''
	for library in "$@"; do
		package=$(dependency_package_providing_library_deb "$library")
		packages_list="$packages_list
		$package"
	done

	printf '%s' "$packages_list" | list_clean
}

# Debian - Print the package name providing the given native library
# USAGE: dependency_package_providing_library_deb $library
dependency_package_providing_library_deb() {
	local library package_name
	library="$1"
	case "$library" in
		('ld-linux.so.2')
			package_name='libc6'
		;;
		('ld-linux-x86-64.so.2')
			package_name='libc6'
		;;
		('liballeg.so.4.4')
			package_name='liballegro4.4'
		;;
		('liballegro.so.5.2')
			package_name='liballegro5.2'
		;;
		('liballegro_acodec.so.5.2')
			package_name='liballegro-acodec5.2'
		;;
		('liballegro_audio.so.5.2')
			package_name='liballegro-audio5.2'
		;;
		('liballegro_font.so.5.2')
			package_name='liballegro5.2'
		;;
		('liballegro_image.so.5.2')
			package_name='liballegro-image5.2'
		;;
		('liballegro_primitives.so.5.2')
			package_name='liballegro5.2'
		;;
		('liballegro_ttf.so.5.2')
			package_name='liballegro-ttf5.2'
		;;
		('libalut.so.0')
			package_name='libalut0'
		;;
		('libasound.so.2')
			package_name='libasound2'
		;;
		('libasound_module_'*'.so')
			package_name='libasound2-plugins'
		;;
		('libatspi.so.0')
			package_name='libatspi2.0-0'
		;;
		('libatk-1.0.so.0')
			package_name='libatk1.0-0'
		;;
		('libaudio.so.2')
			package_name='libaudio2'
		;;
		('libboost_locale.so.1.74.0')
			package_name='libboost-locale1.74.0'
		;;
		('libbz2.so.1.0'|'libbz2.so.1')
			package_name='libbz2-1.0'
		;;
		('libc.so.6')
			package_name='libc6'
		;;
		('libc++.so.1')
			package_name='libc++1'
		;;
		('libc++abi.so.1')
			package_name='libc++abi1'
		;;
		('libcairo.so.2')
			package_name='libcairo2'
		;;
		('libCg.so')
			package_name='libcg'
		;;
		('libCgGL.so')
			package_name='libcggl'
		;;
		('libcom_err.so.2')
			package_name='libcom-err2'
		;;
		('libcrypt.so.1')
			package_name='libcrypt1'
		;;
		('libcups.so.2')
			package_name='libcups2'
		;;
		('libcurl.so.4')
			package_name='libcurl4'
		;;
		('libcurl.so.4+CURL_OPENSSL_3')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/curl/
			return 0
		;;
		('libcurl-gnutls.so.4')
			package_name='libcurl3-gnutls'
		;;
		('libdbus-1.so.3')
			package_name='libdbus-1-3'
		;;
		('libdbus-glib-1.so.2')
			package_name='libdbus-glib-1-2'
		;;
		('libdl.so.2')
			package_name='libc6'
		;;
		('libEGL.so.1')
			package_name='libegl1'
		;;
		('libexpat.so.1')
			package_name='libexpat1'
		;;
		('libFAudio.so.0')
			package_name='libfaudio0'
		;;
		('libFLAC.so.8')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/flac/
			return 0
		;;
		('libfontconfig.so.1')
			package_name='libfontconfig1'
		;;
		('libfreeimage.so.3')
			package_name='libfreeimage3'
		;;
		('libfreetype.so.6')
			package_name='libfreetype6'
		;;
		('libfribidi.so.0')
			package_name='libfribidi0'
		;;
		('libgcc_s.so.1')
			package_name='libgcc-s1'
		;;
		('libgconf-2.so.4')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/gconf/
			return 0
		;;
		('libgcrypt.so.11')
			# This old library is no longer available from Debian.
			unset package_name
		;;
		('libgdiplus.so')
			package_name='libgdiplus'
		;;
		('libgdk-3.so.0')
			package_name='libgtk-3-0'
		;;
		('libgdk_pixbuf-2.0.so.0')
			package_name='libgdk-pixbuf-2.0-0 | libgdk-pixbuf2.0-0'
		;;
		('libgdk-x11-2.0.so.0')
			package_name='libgtk2.0-0'
		;;
		('libgio-2.0.so.0')
			package_name='libglib2.0-0'
		;;
		('libGL.so.1')
			package_name='
			libgl1 | libgl1-mesa-glx
			libglx-mesa0 | libglx-vendor | libgl1-mesa-glx'
		;;
		('libGLEW.so.2.2')
			package_name='libglew2.2'
		;;
		('libglfw.so.3')
			package_name='libglfw3 | libglfw3-wayland'
		;;
		('libglib-2.0.so.0')
			package_name='libglib2.0-0'
		;;
		('libGLU.so.1')
			package_name='libglu1-mesa | libglu1'
		;;
		('libGLX.so.0')
			package_name='libglx0'
		;;
		('libgmodule-2.0.so.0')
			package_name='libglib2.0-0'
		;;
		('libgobject-2.0.so.0')
			package_name='libglib2.0-0'
		;;
		('libgomp.so.1')
			package_name='libgomp1'
		;;
		('libgpg-error.so.0')
			package_name='libgpg-error0'
		;;
		('libgssapi_krb5.so.2')
			package_name='libgssapi-krb5-2'
		;;
		('libgthread-2.0.so.0')
			package_name='libglib2.0-0'
		;;
		('libgtk-x11-2.0.so.0')
			package_name='libgtk2.0-0'
		;;
		('libgtk-3.so.0')
			package_name='libgtk-3-0'
		;;
		('libICE.so.6')
			package_name='libice6'
		;;
		('libidn.so.11')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/libidn/
			return 0
		;;
		('libidn2.so.0')
			package_name='libidn2-0'
		;;
		('libIL.so.1')
			package_name='libdevil1c2'
		;;
		('libjpeg.so.62')
			package_name='libjpeg62-turbo | libjpeg62'
		;;
		('libk5crypto.so.3')
			package_name='libk5crypto3'
		;;
		('libkrb5.so.3')
			package_name='libkrb5-3'
		;;
		('liblcms2.so.2')
			package_name='liblcms2-2'
		;;
		('liblua5.3.so.0')
			package_name='liblua5.3-0'
		;;
		('libluajit-5.1.so.2')
			package_name='libluajit-5.1-2'
		;;
		('liblz4.so.1')
			package_name='liblz4-1'
		;;
		('libm.so.6')
			package_name='libc6'
		;;
		('libmbedtls.so.12')
			package_name='libmbedtls12'
		;;
		('libminiupnpc.so.17')
			package_name='libminiupnpc17'
		;;
		('libminizip.so.1')
			package_name='libminizip1'
		;;
		('libmodplug.so.1')
			package_name='libmodplug1'
		;;
		('libmpg123.so.0')
			package_name='libmpg123-0'
		;;
		('libnghttp2.so.14')
			package_name='libnghttp2-14'
		;;
		('libnotify.so.4')
			package_name='libnotify4'
		;;
		('libnspr4.so')
			package_name='libnspr4'
		;;
		('libnss3.so')
			package_name='libnss3'
		;;
		('libnssutil3.so')
			package_name='libnss3'
		;;
		('libogg.so.0')
			package_name='libogg0'
		;;
		('libopenal.so.1')
			package_name='libopenal1'
		;;
		('libOpenGL.so.0')
			package_name='libopengl0'
		;;
		('libopenmpt.so.0')
			package_name='libopenmpt0'
		;;
		('libpango-1.0.so.0')
			package_name='libpango-1.0-0'
		;;
		('libpangocairo-1.0.so.0')
			package_name='libpangocairo-1.0-0'
		;;
		('libpangoft2-1.0.so.0')
			package_name='libpangoft2-1.0-0'
		;;
		('libpcre.so.3')
			package_name='libpcre3'
		;;
		('libphysfs.so.1')
			package_name='libphysfs1'
		;;
		('libpixman-1.so.0')
			package_name='libpixman-1-0'
		;;
		('libplc4.so')
			package_name='libnspr4'
		;;
		('libplds4.so')
			package_name='libnspr4'
		;;
		('libpng12.so.0')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/libpng/
			return 0
		;;
		('libpng16.so.16')
			package_name='libpng16-16'
		;;
		('libpsl.so.5')
			package_name='libpsl5'
		;;
		('libpthread.so.0')
			package_name='libc6'
		;;
		('libpulse.so.0')
			package_name='libpulse0'
		;;
		('libpulse-simple.so.0')
			package_name='libpulse0'
		;;
		('libresolv.so.2')
			package_name='libc6'
		;;
		('librt.so.1')
			package_name='libc6'
		;;
		('librtmp.so.1')
			package_name='librtmp1'
		;;
		('libSDL-1.2.so.0')
			package_name='libsdl1.2debian'
		;;
		('libSDL_image-1.2.so.0')
			package_name='libsdl-image1.2'
		;;
		('libSDL_kitchensink.so.1')
			package_name='libsdl-kitchensink1'
		;;
		('libSDL_mixer-1.2.so.0')
			package_name='libsdl-mixer1.2'
		;;
		('libSDL_sound-1.0.so.1')
			package_name='libsdl-sound1.2'
		;;
		('libSDL_ttf-2.0.so.0')
			package_name='libsdl-ttf2.0-0'
		;;
		('libSDL2-2.0.so.0')
			package_name='libsdl2-2.0-0'
		;;
		('libSDL2_image-2.0.so.0')
			package_name='libsdl2-image-2.0-0'
		;;
		('libSDL2_mixer-2.0.so.0')
			package_name='libsdl2-mixer-2.0-0'
		;;
		('libSDL2_ttf-2.0.so.0')
			package_name='libsdl2-ttf-2.0-0'
		;;
		('libsecret-1.so.0')
			package_name='libsecret-1-0'
		;;
		('libsigc-2.0.so.0')
			package_name='libsigc++-2.0-0v5'
		;;
		('libSM.so.6')
			package_name='libsm6'
		;;
		('libsmime3.so')
			package_name='libnss3'
		;;
		('libsmpeg-0.4.so.0')
			package_name='libsmpeg0'
		;;
		('libsodium.so.23')
			package_name='libsodium23'
		;;
		('libssh2.so.1')
			package_name='libssh2-1'
		;;
		('libssl.so.1.0.0')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/openssl/
			return 0
		;;
		('libssl.so.1.1')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/openssl/
			return 0
		;;
		('libssl3.so')
			package_name='libnss3'
		;;
		('libstdc++.so.5')
			package_name='libstdc++5'
		;;
		('libstdc++.so.6')
			package_name='libstdc++6'
		;;
		('libtcmalloc_minimal.so.4')
			package_name='libtcmalloc-minimal4'
		;;
		('libtheora.so.0')
			package_name='libtheora0'
		;;
		('libtheoradec.so.1')
			package_name='libtheora0'
		;;
		('libtheoraenc.so.1')
			package_name='libtheora0'
		;;
		('libthread_db.so.1')
			package_name='libc6'
		;;
		('libtiff.so.6')
			package_name='libtiff6'
		;;
		('libturbojpeg.so.0')
			package_name='libturbojpeg0'
		;;
		('libudev.so.0')
			package_name='libudev0'
		;;
		('libudev.so.1')
			package_name='libudev1'
		;;
		('libutil.so.1')
			package_name='libc6'
		;;
		('libuuid.so.1')
			package_name='libuuid1'
		;;
		('libuv.so.1')
			package_name='libuv1'
		;;
		('libvorbis.so.0')
			package_name='libvorbis0a'
		;;
		('libvorbisenc.so.2')
			package_name='libvorbisenc2'
		;;
		('libvorbisfile.so.3')
			package_name='libvorbisfile3'
		;;
		('libvulkan.so.1')
			package_name='
			libvulkan1
			mesa-vulkan-drivers | vulkan-icd'
		;;
		('libwayland-client.so.0')
			package_name='libwayland-client0'
		;;
		('libX11.so.6')
			package_name='libx11-6'
		;;
		('libX11-xcb.so.1')
			package_name='libx11-xcb1'
		;;
		('libxcb.so.1')
			package_name='libxcb1'
		;;
		('libxcb-randr.so.0')
			package_name='libxcb-randr0'
		;;
		('libXcomposite.so.1')
			package_name='libxcomposite1'
		;;
		('libXcursor.so.1')
			package_name='libxcursor1'
		;;
		('libXdamage.so.1')
			package_name='libxdamage1'
		;;
		('libXext.so.6')
			package_name='libxext6'
		;;
		('libXfixes.so.3')
			package_name='libxfixes3'
		;;
		('libXft.so.2')
			package_name='libxft2'
		;;
		('libXi.so.6')
			package_name='libxi6'
		;;
		('libXinerama.so.1')
			package_name='libxinerama1'
		;;
		('libxml2.so.2')
			package_name='libxml2'
		;;
		('libxmp.so.4')
			package_name='libxmp4'
		;;
		('libXmu.so.6')
			package_name='libxmu6'
		;;
		('libXrandr.so.2')
			package_name='libxrandr2'
		;;
		('libXrender.so.1')
			package_name='libxrender1'
		;;
		('libxslt.so.1')
			package_name='libxslt1.1'
		;;
		('libXss.so.1')
			package_name='libxss1'
		;;
		('libXt.so.6')
			package_name='libxt6'
		;;
		('libXtst.so.6')
			package_name='libxtst6'
		;;
		('libXxf86vm.so.1')
			package_name='libxxf86vm1'
		;;
		('libyaml-0.so.2')
			package_name='libyaml-0-2'
		;;
		('libz.so.1')
			package_name='zlib1g'
		;;
	esac

	if [ -n "${package_name:-}" ]; then
		printf '%s' "$package_name"
		return 0
	fi

	dependencies_unknown_libraries_add "$library"
}

# Debian - Print the sinling package names required by the given package
# USAGE: debian_dependencies_siblings $package
# RETURN: a list of package names,
#         one per line
debian_dependencies_siblings() {
	local package
	package="$1"

	local required_siblings
	required_siblings=$(dependencies_list_siblings "$package")
	# Return early if the current package does not require any sibling package
	if [ -z "$required_siblings" ]; then
		return 0
	fi

	local sibling dependencies_siblings_list required_package
	while read -r sibling; do
		required_package=$(package_id "$sibling")
		dependencies_siblings_list="${dependencies_siblings_list:-}
		$required_package"
	done <<- EOL
	$(printf '%s' "$required_siblings")
	EOL

	printf '%s' "${dependencies_siblings_list:-}" | list_clean
}

# Warning: A .deb package is going over the 9GB size limit
# USAGE: warning_debian_size_limit $package
warning_debian_size_limit() {
	local package
	package="$1"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le paquet suivant est trop gros pour le format .deb moderne : %s\n'
			message="$message"'Merci de signaler cet avertissement sur notre système de suivi : %s\n\n'
		;;
		('en'|*)
			message='The following package is too big for .deb modern format: %s\n'
			message="$message"'Please report this warning on our issues tracker: %s\n\n'
		;;
	esac
	print_message 'warning' "$message" \
		"$package" \
		"$PLAYIT_GAMES_BUG_TRACKER_URL"
}
# print installation instructions for Gentoo Linux with ebuilds
# USAGE: print_instructions_egentoo $pkg[…]
print_instructions_egentoo() {
	info_package_to_distfiles

	local pkg pkg_path ebuild_path package_id
	for pkg in "$@"; do
		pkg_path=$(realpath "$(get_value "${pkg}_PKG")")
		ebuild_path="$(basename "${pkg_path%%.*}").ebuild"
		package_id=$(package_id "$pkg")

		printf 'mkdir -p ${OVERLAY_PATH}/games-playit/%s\n' \
			"$package_id"
		printf 'mv %s ${OVERLAY_PATH}/games-playit/%s/\n' \
			"$ebuild_path" "$package_id"
		printf 'ebuild ${OVERLAY_PATH}/games-playit/%s/%s manifest\n' \
			"$package_id" "$ebuild_path"
		printf 'emerge games-playit/%s\n' \
			"$package_id"
	done
}

# Gentoo - Print installation instructions
# USAGE: print_instructions_gentoo $package[…]
print_instructions_gentoo() {
	local option_output_dir string_format
	option_output_dir=$(option_value 'output-dir')
	if printf '%s' "$option_output_dir" | grep --quiet --fixed-strings ' '; then
		string_format=' "%s"'
	else
		string_format=' %s'
	fi

	printf 'quickunpkg --'

	local package package_name package_output
	for package in "$@"; do
		package_name=$(package_name "$package")
		package_output=$(realpath "${option_output_dir}/${package_name}")
		## Silence ShellCheck false-positive
		## Don't use variables in the printf format string. Use printf "..%s.." "$foo".
		# shellcheck disable=SC2059
		printf "$string_format" "$package_output"
	done

	printf ' # https://downloads.dotslashplay.it/resources/gentoo/ '
	information_installation_instructions_gentoo_comment
	printf '\n'
}

# Gentoo ("egentoo" variant) - Write the metadata for the listed packages
# USAGE: egentoo_packages_metadata $package[…]
egentoo_packages_metadata() {
	local ebuild_path
	local inherits

	inherits="xdg"

	local option_output_dir package_id package_name
	option_output_dir=$(option_value 'output-dir')
	package_id="$(egentoo_package_id)"
	package_name="$(egentoo_package_name)"
	mkdir --parents "${option_output_dir}/overlay/games-playit/${package_id}"
	ebuild_path=$(realpath "${option_output_dir}/overlay/games-playit/${package_id}/${package_name}.ebuild")

	local \
		egentoo_field_keywords \
		egentoo_field_srcuri \
		egentoo_field_rdepend \
		egentoo_field_bdepend
	egentoo_field_keywords=$(egentoo_field_keywords "$@")
	egentoo_field_srcuri=$(egentoo_field_srcuri "$@")
	egentoo_field_rdepend=$(egentoo_field_rdepend "$@")
	egentoo_field_bdepend=$(egentoo_field_bdepend "$@")

	local install_cp_options
	install_cp_options='--link --no-dereference --recursive --verbose'
	cat > "$ebuild_path" << EOF
# Copyright 1999-2021 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

EAPI=7
RESTRICT="fetch strip binchecks"

inherit $inherits

KEYWORDS="$egentoo_field_keywords"
DESCRIPTION="Ebuild automatically generated with ./play.it"
HOMEPAGE="https://forge.dotslashplay.it/play.it"
SRC_URI="$egentoo_field_srcuri"
SLOT="0"

RDEPEND="$egentoo_field_rdepend"
BDEPEND="$egentoo_field_bdepend"

S=\${WORKDIR}

pkg_nofetch() {
	elog "Please move \$SRC_URI"
	elog "to your distfiles folder."
}

src_install() {
	if test -d \$S/data; then
		cp $install_cp_options \$S/data/* \$D || die
	fi

	if use x86 && test -d \$S/x86; then
		cp $install_cp_options \$S/x86/* \$D || die
	elif use amd64; then
		if test -d \$S/amd64; then
			cp $install_cp_options \$S/amd64/* \$D || die
		elif test -d \$S/x86; then
			cp $install_cp_options \$S/x86/* \$D || die
		fi
	fi
}
EOF
}

# Append the required extension to the given package name,
# relying on BINPKG_COMPRESS environment variable.
# USAGE: egentoo_package_filename_auto package_filename
# RETURN: the given package name, with the correct filename extension added,
#         following the current value of BINPKG_COMPRESS
egentoo_package_filename_auto() {
	local package_filename
	package_filename="$1"

	# "zstd" is the default value for BINKPG_COMPRESS,
	# according to make.conf(5) manpage.
	# cf. https://dev.gentoo.org/~zmedico/portage/doc/man/make.conf.5.html
	local package_filename
	case "${BINPKG_COMPRESS:-zstd}" in
		('bzip2')
			package_filename="${package_filename}.bz2"
		;;
		('gzip')
			package_filename="${package_filename}.gz"
		;;
		('lz4')
			package_filename="${package_filename}.lz4"
		;;
		('lzip')
			package_filename="${package_filename}.lz"
		;;
		('lzop')
			package_filename="${package_filename}.lzop"
		;;
		('xz')
			package_filename="${package_filename}.xz"
		;;
		('zstd')
			package_filename="${package_filename}.zst"
		;;
	esac

	printf '%s' "$package_filename"
}

# Print the command that should be used for the generated .tar archive compression,
# relying on BINPKG_COMPRESS environment variable.
# USAGE: egentoo_package_compression_command_auto
# RETURN: the command to use for the generated archive compression,
#         following the current value of BINPKG_COMPRESS
egentoo_package_compression_command_auto() {
	# "zstd" is the default value for BINKPG_COMPRESS,
	# according to make.conf(5) manpage.
	# cf. https://dev.gentoo.org/~zmedico/portage/doc/man/make.conf.5.html
	local compression_command
	case "${BINPKG_COMPRESS:-zstd}" in
		('bzip2')
			compression_command='bzip2'
		;;
		('gzip')
			compression_command='gzip'
		;;
		('lz4')
			compression_command='lz4'
		;;
		('lzip')
			compression_command='lzip'
		;;
		('lzop')
			compression_command='lzop'
		;;
		('xz')
			compression_command='xz'
		;;
		('zstd')
			compression_command='zstd'
		;;
	esac

	printf '%s' "$compression_command"
}

# Gentoo ("egentoo" variant) - Build dummy package
# USAGE: egentoo_packages_build $package[…]
egentoo_packages_build() {
	local option_output_dir package_name package_filename
	option_output_dir=$(option_value 'output-dir')
	mkdir --parents "${option_output_dir}/packages"
	package_name="$(egentoo_package_name)"
	package_filename=$(realpath "${option_output_dir}/packages/${package_name}.tar")

	# Set base tar archiving command and options
	local tar_command tar_options
	tar_command='tar'
	tar_options='--create -P'
	if variable_is_empty 'PLAYIT_TAR_IMPLEMENTATION'; then
		guess_tar_implementation
	fi
	case "$PLAYIT_TAR_IMPLEMENTATION" in
		('gnutar')
			tar_options="$tar_options --group=root --owner=root"
		;;
		('bsdtar')
			tar_options="$tar_options --gname=root --uname=root"
		;;
		(*)
			error_unknown_tar_implementation
			return 1
		;;
	esac

	# Set compression command and options
	local option_compression compression_command compression_options
	option_compression=$(option_value 'compression')
	case "$option_compression" in
		('speed')
			package_filename="${package_filename}.gz"
			compression_command='gzip'
			compression_options=''
		;;
		('size')
			package_filename="${package_filename}.bz2"
			compression_command='bzip2'
			compression_options=''
		;;
		('auto')
			package_filename=$(egentoo_package_filename_auto "$package_filename")
			compression_command=$(egentoo_package_compression_command_auto)
			compression_options="${BINPKG_COMPRESS_FLAGS:-}"
		;;
	esac
	compression_options="$compression_options --stdout --quiet"

	local option_overwrite package
	option_overwrite=$(option_value 'overwrite')
	if [ -e "$package_filename" ] && [ "$option_overwrite" -eq 0 ]; then
		information_package_already_exists "$(basename "$package_filename")"
		for package in "$@"; do
			eval "${package}"_PKG=\""$package_filename"\"
			export "${package}"_PKG
		done
		return 0
	else
		rm -f "$package_filename"
	fi

	local packages_paths package_path package_architecture
	packages_paths=''
	for package in "$@"; do
		package_path=$(package_path "$package")
		packages_paths="$packages_paths $package_path"
		package_architecture=$(package_architecture "$package")
		case "$package_architecture" in
			('64') tar_options="$tar_options --xform=s:^${package_path}:./amd64:x" ;;
			('32') tar_options="$tar_options --xform=s:^${package_path}:./x86:x" ;;
			(*)    tar_options="$tar_options --xform=s:^${package_path}:./data:x" ;;
		esac
		export "${package}_PKG=$package_filename"
	done

	# Run the actual package generation, using tar
	local package_generation_return_code
	information_package_building "$(basename "$package_filename")"
	if [ -z "$compression_command" ]; then
		{
			# shellcheck disable=SC2046
			"$tar_command" $tar_options --file "$package_filename" $packages_paths
			package_generation_return_code=$?
		} || true
	else
		{
			"$tar_command" $tar_options $packages_paths | "$compression_command" $compression_options > "$package_filename"
			package_generation_return_code=$?
		} || true
	fi

	if [ $package_generation_return_code -ne 0 ]; then
		error_package_generation_failed "$package_name"
		return 1
	fi
}

# Get the path to the directory where the given package is prepared,
# relative to the directory where all packages are stored
# USAGE: package_path_egentoo $package
# RETURNS: relative path to a directory, as a string
package_path_egentoo() {
	local package
	package="$1"

	local package_id package_version package_architecture package_path
	package_id=$(package_id "$package")
	package_version=$(package_version)
	package_architecture=$(gentoo_package_architecture_string "$package")
	package_path="${package_id}_${package_version}_${package_architecture}"

	printf '%s' "$package_path"
}

# id of the single package built on egentoo
# USAGE: egentoo_package_id
egentoo_package_id() {
	game_id
}

# Prints the name of the single package built using the egentoo variant
# USAGE: egentoo_package_name
egentoo_package_name() {
	local package_id package_version
	package_id=$(egentoo_package_id)
	package_version=$(package_version)
	printf '%s-%s' "$package_id" "$package_version"
}

# Gentoo ("gentoo" variant) - Write the metadata for the listed packages
# USAGE: gentoo_packages_metadata $package[…]
gentoo_packages_metadata() {
	local package
	for package in "$@"; do
		gentoo_package_metadata_single "$package"
	done
}

# Gentoo ("gentoo" variant) - Write the metadata for the given package
# USAGE: gentoo_package_metadata_single $package
gentoo_package_metadata_single() {
	local package
	package="$1"

	local package_id overlay_path
	overlay_path="${PLAYIT_WORKDIR}/${package}/gentoo-overlay"

	mkdir --parents "${overlay_path}/metadata"
	cat > "${overlay_path}/metadata/layout.conf" <<- EOF
	masters = gentoo
	EOF

	mkdir --parents "${overlay_path}/profiles"
	cat > "${overlay_path}/profiles/categories" <<- EOF
	games-playit
	EOF

	local package_id package_path
	package_id=$(package_id "$package")
	package_path=$(package_path "$package")
	mkdir --parents "${overlay_path}/games-playit/${package_id}/files"
	ln --symbolic --force --no-target-directory \
		"$package_path" \
		"${overlay_path}/games-playit/${package_id}/files/install"

	# Write the ebuild file
	local ebuild_path
	ebuild_path=$(gentoo_ebuild_path "$package")
	gentoo_ebuild_content "$package" > "$ebuild_path"

}

# Gentoo - Print path to ebuild file for the given package
# USAGE: gentoo_ebuild_path $package
# RETURN: the path to the .ebuild file
gentoo_ebuild_path() {
	local package
	package="$1"

	local package_id package_name
	package_id=$(package_id "$package")
	package_name=$(package_name "$package")

	printf '%s/%s/gentoo-overlay/games-playit/%s/%s.ebuild' "$PLAYIT_WORKDIR" "$package" "$package_id" "${package_name%.tbz2}"
}

# Gentoo ("gentoo" variant) - Print the contents of the ebuild file
# USAGE: gentoo_ebuild_content $package
# RETURN: the file contents,
#         spanning over multiple lines
gentoo_ebuild_content() {
	local package
	package="$1"

	local \
		gentoo_field_keywords \
		gentoo_field_description \
		gentoo_field_rdepend
	gentoo_field_keywords=$(gentoo_field_keywords "$package")
	gentoo_field_description=$(gentoo_field_description "$package")
	gentoo_field_rdepend=$(gentoo_field_rdepend "$package")

	cat <<- EOF
	EAPI=7
	RESTRICT="fetch strip binchecks"
	KEYWORDS="$gentoo_field_keywords"
	DESCRIPTION="$gentoo_field_description"
	SLOT="0"
	RDEPEND="$gentoo_field_rdepend"
	EOF
	cat <<- 'EOF'
	src_unpack() {
		mkdir --parents "$S"
	}
	src_install() {
		cp --recursive --link $FILESDIR/install/* $ED/
	}
	EOF

	# Print the pkg_postinst function, if some postinst actions are set
	gentoo_script_postinst "$package"

	# Print the pkg_prerm function, if some prerm actions are set
	gentoo_script_prerm "$package"
}

# Gentoo ("gentoo" variant) - Build a list of packages
# USAGE: gentoo_packages_build $package[…]
gentoo_packages_build() {
	local package
	for package in "$@"; do
		gentoo_package_build_single "$package"
	done
}

# Gentoo ("gentoo" variant) - Build a single package
# USAGE: gentoo_package_build_single $package
gentoo_package_build_single() {
	local package
	package="$1"

	# Set the path where the package should be generated.
	local option_output_dir package_name generated_package_path generated_package_directory
	option_output_dir=$(option_value 'output-dir')
	package_name=$(package_name "$package")
	generated_package_path="${option_output_dir}/${package_name}"
	generated_package_directory=$(dirname "$generated_package_path")

	# Skip packages already existing,
	# unless called with --overwrite.
	local option_overwrite
	option_overwrite=$(option_value 'overwrite')
	if \
		[ "$option_overwrite" -eq 0 ] \
		&& [ -e "$generated_package_path" ]
	then
		information_package_already_exists "$package_name"
		return 0
	fi

	# Set compression setting
	local option_compression binpkg_compress
	option_compression=$(option_value 'compression')
	case "$option_compression" in
		('speed')
			binpkg_compress='gzip'
		;;
		('size')
			binpkg_compress='bzip2'
		;;
		('auto')
			binpkg_compress=''
		;;
	esac

	# Run the actual package generation, using ebuild
	local ebuild_path metadata_generation_return_code package_generation_return_code
	information_package_building "$package_name"
	mkdir --parents "${PLAYIT_WORKDIR}/portage-tmpdir"
	ebuild_path=$(gentoo_ebuild_path "$package")
	{
		ebuild "$ebuild_path" manifest 1>/dev/null
		metadata_generation_return_code=$?
	} || true

	if [ $metadata_generation_return_code -ne 0 ]; then
		error_package_metadata_generation_failed "$package_name"
		return 1
	fi

	## The following variables must be exported, otherwise ebuild would not pick them up.
	PORTAGE_TMPDIR="${PLAYIT_WORKDIR}/portage-tmpdir"
	PKGDIR="${PLAYIT_WORKDIR}/gentoo-pkgdir"
	export PORTAGE_TMPDIR PKGDIR
	if [ -n "$binpkg_compress" ]; then
		{
			## The following variable must be exported, otherwise ebuild would not pick it up.
			BINPKG_COMPRESS="$binpkg_compress"
			export BINPKG_COMPRESS
			fakeroot -- ebuild "$ebuild_path" package 1>/dev/null
			package_generation_return_code=$?
		} || true
	else
		{
			fakeroot -- ebuild "$ebuild_path" package 1>/dev/null
			package_generation_return_code=$?
		} || true
	fi

	if [ $package_generation_return_code -ne 0 ]; then
		error_package_generation_failed "$package_name"
		return 1
	fi

	mkdir --parents "$generated_package_directory"
	mv "${PLAYIT_WORKDIR}/gentoo-pkgdir/games-playit/${package_name}" "$generated_package_path"
	rm --recursive "${PLAYIT_WORKDIR}/portage-tmpdir"
}

# Print the file name of the given package
# USAGE: package_name_gentoo $package
# RETURNS: the file name, as a string
package_name_gentoo() {
	local package
	package="$1"

	local package_id package_version package_name
	package_id=$(package_id "$package")
	package_version=$(package_version)
	package_name="${package_id}-${package_version}.tbz2"

	# Avoid paths collisions when building multiple architecture variants for a same package
	local packages_list current_package current_package_id
	packages_list=$(packages_list)
	for current_package in $packages_list; do
		current_package_id=$(package_id "$current_package")
		if \
			[ "$current_package" != "$package" ] \
			&& [ "$current_package_id" = "$package_id" ]
		then
			local package_architecture
			package_architecture=$(gentoo_package_architecture_string "$package")
			package_name="${package_architecture}/${package_name}"
			break
		fi
	done

	printf '%s' "$package_name"
}

# Get the path to the directory where the given package is prepared,
# relative to the directory where all packages are stored
# USAGE: package_path_gentoo $package
# RETURNS: relative path to a directory, as a string
package_path_gentoo() {
	local package
	package="$1"

	local package_name package_path
	package_name=$(package_name "$package")
	package_path="${package_name%.tbz2}"

	printf '%s' "$package_path"
}

# Tweak the given package version string to ensure it is compatible with portage
# USAGE: gentoo_package_version $package_version
# RETURNS: the package version, as a non-empty string
gentoo_package_version() {
	local package_version
	package_version="$1"

	set +o errexit
	package_version=$(
		printf '%s' "$package_version" | \
			grep --extended-regexp --only-matching '^([0-9]{1,18})(\.[0-9]{1,18})*[a-z]?'
	)
	set -o errexit

	if [ -z "$package_version" ]; then
		package_version='1.0'
	fi

	local script_version_string
	script_version_string=$(script_version)

	printf '%s_p%s' "$package_version" "$(printf '%s' "$script_version_string" | sed 's/\.//g')"
}

# Print the architecture string of the given package, in the format expected by portage
# USAGE: gentoo_package_architecture_string $package
# RETURNS: the package architecture, as one of the following values:
#          - x86
#          - amd64
#          - data (dummy value)
gentoo_package_architecture_string() {
	local package
	package="$1"

	local package_architecture package_architecture_string
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			package_architecture_string='x86'
		;;
		('64')
			package_architecture_string='amd64'
		;;
		('all')
			# We could put anything here, it should not be used for package metadata.
			package_architecture_string='data'
		;;
	esac

	printf '%s' "$package_architecture_string"
}

# Tweak the given package id to ensure compatibility with portage
# USAGE: gentoo_package_id $package_id
# RETURNS: the package id, as a non-empty string
gentoo_package_id() {
	local package_id
	package_id="$1"

	# Avoid mixups between numbers in package id and version number.
	printf '%s' "$package_id" | sed 's/-/_/g'
}

# Gentoo ("egentoo" variant) - Print the content of the "KEYWORDS" field
# USAGE: egentoo_field_keywords $package…
# RETURN: the field value
egentoo_field_keywords() {
	local keywords package package_architecture
	keywords='x86 amd64'
	for package in "$@"; do
		package_architecture=$(package_architecture "$package")
		case "$package_architecture" in
			('32')
				printf '%s' '-* x86 amd64'
				return 0
			;;
			('64')
				keywords='-* amd64'
			;;
		esac
	done

	printf '%s' "$keywords"
}

# Gentoo ("egentoo" variant) - Print the content of the "SRC_URI" field
# USAGE: egentoo_field_srcuri $package…
# RETURN: the field value
egentoo_field_srcuri() {
	## The list of packages is ignored by this function,
	## it is only passed for consistency with other egentoo_field_* functions.

	package_filename="$(egentoo_package_name).tar"

	local option_compression
	option_compression=$(option_value 'compression')
	case $option_compression in
		('speed')
			package_filename="${package_filename}.gz"
		;;
		('size')
			package_filename="${package_filename}.bz2"
		;;
	esac

	printf '%s' "$package_filename"
}

# Gentoo ("egentoo" variant) - Print the content of the "RDEPEND" field
# USAGE: egentoo_field_rdepend $package…
# RETURN: the field value
egentoo_field_rdepend() {
	local package package_architecture package_64bits package_32bits package_data
	package_64bits=''
	package_32bits=''
	package_data=''
	for package in "$@"; do
		package_architecture=$(package_architecture "$package")
		case "$package_architecture" in
			('32')
				package_32bits="$package"
			;;
			('64')
				package_64bits="$package"
			;;
			(*)
				package_data="$package"
			;;
		esac
	done

	if [ -n "$package_64bits" ]; then
		gentoo_field_rdepend "$package_64bits"
	elif [ -n "$package_32bits" ]; then
		gentoo_field_rdepend "$package_32bits"
	elif [ -n "$package_data" ]; then
		gentoo_field_rdepend "$package_data"
	fi
}

# Gentoo ("egentoo" variant) - Print the content of the "BDEPEND" field
# USAGE: egentoo_field_bdepend $package…
# RETURN: the field value
egentoo_field_bdepend() {
	local field_bdepend
	## FIXME: $build_deps is not set anywhere, so this field will always be empty.
	field_bdepend=${build_deps:-}

	printf '%s' "$field_bdepend"
}

# Gentoo - Print the content of the "KEYWORDS" field
# USAGE: gentoo_field_keywords $package
# RETURN: the field value
gentoo_field_keywords() {
	local package
	package="$1"

	local package_architecture ebuild_keywords
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			ebuild_keywords='-* x86 amd64'
		;;
		('64')
			ebuild_keywords='-* amd64'
		;;
		(*)
			ebuild_keywords='x86 amd64' # data packages
		;;
	esac

	printf '%s' "$ebuild_keywords"
}

# Gentoo - Print the content of the "DESCRIPTION" field
# USAGE: gentoo_field_description $package
# RETURN: the field value
gentoo_field_description() {
	local package
	package="$1"

	local game_name package_description script_version_string
	game_name=$(game_name)
	package_description=$(package_description "$package")
	script_version_string=$(script_version)

	printf '%s' "$game_name"
	if [ -n "$package_description" ]; then
		printf -- ' - %s' "$package_description"
	fi
	printf -- ' - ./play.it script version %s' "$script_version_string"
}

# Gentoo - Print the content of the "RDEPEND" field
# USAGE: gentoo_field_rdepend $package
# RETURN: the field value
gentoo_field_rdepend() {
	local package
	package="$1"

	local dependencies_list first_item_displayed dependency_string
	dependencies_list=$(dependencies_gentoo_full_list "$package")
	first_item_displayed=0
	while IFS= read -r dependency_string; do
		if [ -z "$dependency_string" ]; then
			continue
		fi
		# Gentoo policy is that dependencies should be displayed one per line,
		# and indentation is to be done using tabulations.
		if [ "$first_item_displayed" -eq 0 ]; then
			printf '%s' "$dependency_string"
			first_item_displayed=1
		else
			printf '\n\t%s' "$dependency_string"
		fi
	done <<- EOL
	$(printf '%s' "$dependencies_list")
	EOL

	# Return early when building package in the "egentoo" format,
	# as we have no good way to set conflicts with this format.
	local option_package
	option_package=$(option_value 'package')
	if [ "$option_package" = 'egentoo' ]; then
		return 0
	fi

	local package_conflicts package_conflict
	package_conflicts=$(package_provides "$package")

	# Return early if the current package has no "provides" field.
	if [ -z "$package_conflicts" ]; then
		return 0
	fi

	# Gentoo has no notion of "provided" package,
	# so we need to loop over all supported archives
	# to get the name of all packages providing a given package id.
	local archives_list packages_list
	archives_list=$(archives_return_list)
	packages_list=$(
		for archive in $archives_list; do
			set_current_archive "$archive"
			packages_list
		done | list_clean
	)

	# For each conflict of the current package,
	# find all potential packages that would provide this package id.
	local package_current package_current_id package_current_provides package_current_provide
	for package_conflict in $package_conflicts; do
		for package_current in $packages_list; do
			# Skip the package we are writing metadata for,
			# so it does not end up conflicting with itself.
			if [ "$package_current" = "$package" ]; then
				continue
			fi
			package_current_provides=$(package_provides "$package_current")
			for package_current_provide in $package_current_provides; do
				if [ "$package_current_provide" = "$package_conflict" ]; then
					package_current_id=$(package_id "$package_current")
					if [ "$first_item_displayed" -eq 0 ]; then
						printf '!games-play.it/%s' "$package_current_id"
						first_item_displayed=1
					else
						printf '\n\t!games-play.it/%s' "$package_current_id"
					fi
				fi
			done
		done
	done
}

# Gentoo - Print the pkg_postinst function
# USAGE: gentoo_script_postinst $package
# RETURN: the defintion of the pkg_postinst function,
#         spanning over several lines
gentoo_script_postinst() {
	local package
	package="$1"

	local postinst_actions postinst_warnings
	postinst_actions=$(package_postinst_actions "$package")
	postinst_warnings=$(package_postinst_warnings "$package")
	if [ -n "$postinst_actions" ] || [ -n "$postinst_warnings" ]; then
		cat <<- EOF
		pkg_postinst() {
		EOF

		# Include actions that should be run.
		if [ -n "$postinst_actions" ]; then
			printf '%s\n' "$postinst_actions"
		fi

		# Include warnings that should be displayed.
		if [ -n "$postinst_warnings" ]; then
			local warning_line
			while read -r warning_line; do
				printf 'ewarn "%s"\n' "$warning_line"
			done <<- EOL
			$(printf '%s' "$postinst_warnings")
			EOL
		fi

		cat <<- EOF
		}
		EOF
	fi
}

# Gentoo - Print the pkg_prerm function
# USAGE: gentoo_script_prerm $package
# RETURN: the defintion of the pkg_prerm function,
#         spanning over several lines
gentoo_script_prerm() {
	local package
	package="$1"

	local prerm_actions
	prerm_actions=$(package_prerm_actions "$package")
	if [ -n "$prerm_actions" ]; then
		cat <<- EOF
		pkg_prerm() {
		$prerm_actions
		}
		EOF
	fi
}

# Gentoo - Set list of generic dependencies
# USAGE: pkg_set_deps_gentoo $package $dep[…]
pkg_set_deps_gentoo() {
	local package
	package="$1"
	shift

	local package_architecture architecture_suffix
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			architecture_suffix='[abi_x86_32]'
		;;
		('64')
			architecture_suffix=''
		;;
	esac
	local pkg_dep
	for dep in "$@"; do
		pkg_dep=''
		case $dep in
			('alsa')
				pkg_dep="media-libs/alsa-lib$architecture_suffix media-plugins/alsa-plugins$architecture_suffix"
			;;
			('freetype')
				pkg_dep="media-libs/freetype$architecture_suffix"
			;;
			('gcc32')
				pkg_dep='' #gcc (in @system) should be multilib unless it is a no-multilib profile, in which case the 32 bits libraries wouldn't work
			;;
			('glibc')
				pkg_dep="sys-libs/glibc"
				if [ "$package_architecture" = '32' ]; then
					pkg_dep="$pkg_dep amd64? ( sys-libs/glibc[multilib] )"
				fi
			;;
			('glu')
				pkg_dep="virtual/glu$architecture_suffix"
			;;
			('glx')
				pkg_dep="virtual/opengl$architecture_suffix"
			;;
			('gtk2')
				pkg_dep="x11-libs/gtk+:2$architecture_suffix"
			;;
			('json')
				pkg_dep="dev-libs/json-c$architecture_suffix"
			;;
			('libstdc++')
				pkg_dep='' #maybe this should be virtual/libstdc++, otherwise, it is included in gcc, which should be in @system
			;;
			('libudev1')
				pkg_dep="virtual/libudev$architecture_suffix"
			;;
			('libxrandr')
				pkg_dep="x11-libs/libXrandr$architecture_suffix"
			;;
			('nss')
				pkg_dep="dev-libs/nss$architecture_suffix"
			;;
			('openal')
				pkg_dep="media-libs/openal$architecture_suffix"
			;;
			('sdl2')
				pkg_dep="media-libs/libsdl2$architecture_suffix"
			;;
			('xcursor')
				pkg_dep="x11-libs/libXcursor$architecture_suffix"
			;;
			( \
				'dosbox' | \
				'java' | \
				'mono' | \
				'pulseaudio' | \
				'scummvm' | \
				'wine' | \
				'winetricks' | \
				'xgamma' | \
				'xrandr' \
			)
				gentoo_dependencies_single_command "$package" "$dependency_keyword"
			;;
			( \
				'libasound.so.2' | \
				'libasound_module_'*'.so' | \
				'libc.so.6' | \
				'libgdk_pixbuf-2.0.so.0' | \
				'libgdk-x11-2.0.so.0' | \
				'libGL.so.1' | \
				'libglib-2.0.so.0' | \
				'libGLU.so.1' | \
				'libgobject-2.0.so.0' | \
				'libgtk-x11-2.0.so.0' | \
				'libmbedtls.so.12' | \
				'libpng16.so.16' | \
				'libpulse.so.0' | \
				'libpulse-simple.so.0' | \
				'libopenal.so.1' | \
				'libSDL-1.2.so.0' | \
				'libSDL2-2.0.so.0' | \
				'libstdc++.so.6' | \
				'libturbojpeg.so.0' | \
				'libuv.so.1' | \
				'libudev.so.1' | \
				'libvorbisfile.so.3' | \
				'libX11.so.6' | \
				'libz.so.1' \
			)
				case "$package_architecture" in
					('32')
						pkg_dep=$(dependency_package_providing_library_gentoo32 "$dep" "$package")
					;;
					(*)
						pkg_dep=$(dependency_package_providing_library_gentoo "$dep" "$package")
					;;
				esac
			;;
		esac
		if [ -n "$pkg_dep" ]; then
			if variable_is_empty 'pkg_deps'; then
				pkg_deps="$pkg_dep"
			else
				pkg_deps="$pkg_deps $pkg_dep"
			fi
		fi
	done
}

# Gentoo - List all dependencies for the given package
# USAGE: dependencies_gentoo_full_list $package
dependencies_gentoo_full_list() {
	local package
	package="$1"

	# Include generic dependencies
	local dependencies_generic dependency_generic
	dependencies_generic=$(dependencies_list_generic "$package")
	while read -r dependency_generic; do
		# pkg_set_deps_gentoo sets a variable $pkg_deps instead of printing a value,
		# we prevent it from leaking by setting it to an empty value.
		pkg_deps=''
		pkg_set_deps_gentoo $package $dependency_generic
		printf '%s\n' "$pkg_deps"
	done <<- EOL
	$(printf '%s' "$dependencies_generic")
	EOL

	local packages_list packages_list_full
	packages_list_full=''

	# Include dependencies on sibling package (not when building "egentoo" merged packages)
	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('gentoo')
			packages_list=$(gentoo_dependencies_siblings "$package")
			packages_list_full="$packages_list_full
			$packages_list"
		;;
	esac

	# Include dependencies on commands
	packages_list=$(gentoo_dependencies_all_commands "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on native libraries
	packages_list=$(dependencies_list_native_libraries_packages "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on Mono libraries
	packages_list=$(dependencies_list_mono_libraries_packages "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	# Include dependencies on GStreamer plugins
	packages_list=$(gentoo_dependencies_gstreamer_all_formats "$package")
	packages_list_full="$packages_list_full
	$packages_list"

	printf '%s' "$packages_list_full" | list_clean
}

# Gentoo - Print the path to a temporary file used for additional overlays listing
# USAGE: dependency_gentoo_overlays_file
dependency_gentoo_overlays_file() {
	printf '%s/overlays' "$PLAYIT_WORKDIR"
}

# Gentoo - Add an overlay to the list of additional overlays
# USAGE: dependency_gentoo_overlays_add $overlay
dependency_gentoo_overlays_add() {
	local overlay overlays_file
	overlay="$1"
	overlays_file="$(dependency_gentoo_overlays_file)"

	# Do nothing if this overlay is already included in the list
	if test -e "$overlays_file" \
		&& grep --quiet --fixed-strings --word-regexp "$overlay" < "$overlays_file"
	then
		return 0
	fi

	printf '%s\n' "$overlay" >> "$overlays_file"
}

# Gentoo - Print gentoo libdir name
# USAGE: dependency_gentoo_libdir $arch_string
# Note: This prints the name (ie. “lib”) not the path (ie. “/usr/lib”)
dependency_gentoo_libdir() {
	local arch_string="$1"
	if command -v portageq >/dev/null 2>&1; then
		print '%s' "$(portageq envvar "LIBDIR_${arch_string}")"
	else
		case "$arch_string" in
			('amd64')
				printf '%s' 'lib64'
				;;
			('x86')
				printf '%s' 'lib'
				;;
			('x32')
				printf '%s' 'libx32'
				;;
			(*)
				error_unknown_gentoo_architecture_string "$arch_string" 'dependency_gentoo_libdir'
				return 1
				;;
		esac
	fi
	return 0
}

# Gentoo - Link library installed in non-standard libdir to the game’s libdir
# USAGE: dependencies_gentoo_link $libname $libdir $package
# Note: $libdir is the library’s directory, not the game’s one!
dependencies_gentoo_link() {
	local libname libdir package game_libdir
	libname="$1"
	libdir="$2"
	package="$3"
	game_libdir=$(
		set_current_package "$package"
		path_libraries
	)

	local package_path library_destination
	package_path=$(package_path "$package")
	library_destination="${package_path}${game_libdir}"

	mkdir --parents "$library_destination"
	ln -sft "$library_destination" "${libdir}/${libname}"
}

# Gentoo - Print the package names providing the commands required by the given package
# USAGE: gentoo_dependencies_all_commands $package
# RETURN: a list of Gentoo package names,
#         one per line
gentoo_dependencies_all_commands() {
	local package
	package="$1"

	local required_commands
	required_commands=$(dependencies_list_commands "$package")
	# Return early if the current package does not require any command
	if [ -z "$required_commands" ]; then
		return 0
	fi

	local command packages_list required_packages
	packages_list=''
	while read -r command; do
		required_packages=$(gentoo_dependencies_single_command "$package" "$command")
		packages_list="$packages_list
		$required_packages"
	done <<- EOL
	$(printf '%s' "$required_commands")
	EOL

	printf '%s' "$packages_list" | list_clean
}

# Gentoo - Print the package names providing the required command
# USAGE: gentoo_dependencies_single_command $package $required_command
# RETURN: a list of Gentoo package names,
#         one per line
gentoo_dependencies_single_command() {
	local package required_command
	package="$1"
	required_command="$2"

	local package_names
	case "$required_command" in
		('corsix-th')
			package_names='
			games-simulation/corsix-th'
		;;
		('dos2unix')
			package_names='
			app-text/dos2unix'
		;;
		('dosbox')
			package_names='
			games-emulation/dosbox'
		;;
		('java')
			package_names='
			virtual/jre'
		;;
		('mono')
			package_names='
			dev-lang/mono'
		;;
		('mpv')
			package_names='
			media-video/mpv'
		;;
		('openmw-iniimporter')
			package_names='
			games-engines/openmw'
		;;
		('openmw-launcher')
			package_names='
			games-engines/openmw'
		;;
		('pulseaudio')
			package_names='
			media-sound/pulseaudio'
		;;
		('renpy')
			package_names='
			games-engines/renpy'
		;;
		('scummvm')
			package_names='
			games-engines/scummvm[mp3,truetype,opengl,vorbis,theora]'
		;;
		('sed')
			package_names='
			sys-apps/sed'
		;;
		('setxkbmap')
			package_names='
			x11-apps/setxkbmap'
		;;
		('vcmilauncher')
			dependency_gentoo_overlays_add 'https://cgit.gentoo.org/proj/gamerlay.git'
			package_names='
			games-strategy/vcmi'
		;;
		('wine')
			local package_architecture
			package_architecture=$(package_architecture "$package")
			case "$package_architecture" in
				('32')
					package_names='
					virtual/wine[abi_x86_32]'
				;;
				('64')
					package_names='
					virtual/wine[abi_x86_64]'
				;;
			esac
		;;
		('winetricks')
			## TODO - Add an OR dependency on one of these packages:
			## - x11-terms/xterm
			## - gnome-extra/zenity
			## - kde-apps/kdialog
			## This dependency must be set on a single line.
			package_names='
			app-emulation/winetricks'
		;;
		('xgamma')
			package_names='
			x11-apps/xgamma'
		;;
		('xrandr')
			package_names='
			x11-apps/xrandr'
		;;
		(*)
			dependencies_unknown_command_add "$required_command"
			return 0
		;;
	esac

	printf '%s' "$package_names"
}

# Gentoo - Print the package names providing the GStreamer plugins to decode the formats required by the given package
# USAGE: gentoo_dependencies_gstreamer_all_formats $package
# RETURN: a list of Gentoo package names,
#         one per line
gentoo_dependencies_gstreamer_all_formats() {
	local package
	package="$1"

	local gstreamer_decoders
	gstreamer_decoders=$(dependencies_list_gstreamer_decoders "$package")
	# Return early if the current package does not require any GStreamer plugin
	if [ -z "$gstreamer_decoders" ]; then
		return 0
	fi

	local package_architecture command_dependencies_for_single_format
	package_architecture=$(package_architecture "$package")
	case "$package_architecture" in
		('32')
			command_dependencies_for_single_format='gentoo_dependencies_gstreamer_single_format_32bit'
		;;
		(*)
			command_dependencies_for_single_format='gentoo_dependencies_gstreamer_single_format'
		;;
	esac

	local media_format packages_list required_packages
	packages_list=''
	while read -r media_format; do
		required_packages=$("$command_dependencies_for_single_format" "$media_format")
		packages_list="$packages_list
		$required_packages"
	done <<- EOL
	$(printf '%s' "$gstreamer_decoders")
	EOL

	printf '%s' "$packages_list" | list_clean
}

# Gentoo - Print the package names providing the required GStreamer plugins to decode the given format
# USAGE: gentoo_dependency_providing_gstreamer_plugin $media_format
# RETURN: a list of Gentoo package names,
#         one per line
gentoo_dependencies_gstreamer_single_format() {
	local media_format
	media_format="$1"

	local package_names
	case "$media_format" in
		('audioconvert')
			package_names='
			media-libs/gst-plugins-base'
		;;
		('avidemux')
			package_names='
			media-libs/gst-plugins-good'
		;;
		('decodebin')
			package_names='
			media-libs/gst-plugins-base'
		;;
		('deinterlace')
			package_names='
			media-libs/gst-plugins-good'
		;;
		('application/x-id3')
			package_names='
			media-libs/gst-plugins-good'
		;;
		('audio/mpeg, mpegversion=(int)1, layer=(int)3')
			package_names='
			media-libs/gst-plugins-good'
		;;
		('audio/x-wma, wmaversion=(int)1')
			package_names='
			media-plugins/gst-plugins-libav'
		;;
		('video/mpeg, systemstream=(boolean)true, mpegversion=(int)1')
			package_names='
			media-libs/gst-plugins-ugly
			media-libs/gst-plugins-bad'
		;;
		('video/quicktime, variant=(string)iso')
			package_names='
			media-libs/gst-plugins-good
			media-plugins/gst-plugins-libav'
		;;
		('video/x-ms-asf')
			package_names='
			media-libs/gst-plugins-ugly
			media-plugins/gst-plugins-libav'
		;;
		('video/x-msvideo')
			package_names='
			media-libs/gst-plugins-good
			media-plugins/gst-plugins-libav'
		;;
		('video/x-wmv, wmvversion=(int)1')
			package_names='
			media-plugins/gst-plugins-libav'
		;;
		(*)
			dependencies_unknown_gstreamer_media_formats_add "$media_format"
			return 0
		;;
	esac

	printf '%s' "$package_names"
}

# Gentoo - Print the package names providing the required GStreamer plugins to decode the given format (32-bit)
# USAGE: gentoo_dependency_providing_gstreamer_plugin_32bit $media_format
# RETURN: a list of Gentoo package names,
#         one per line
gentoo_dependencies_gstreamer_single_format_32bit() {
	local media_format
	media_format="$1"

	local package_names
	case "$media_format" in
		('audioconvert')
			package_names='
			media-libs/gst-plugins-base[abi_x86_32]'
		;;
		('avidemux')
			package_names='
			media-libs/gst-plugins-good[abi_x86_32]'
		;;
		('decodebin')
			package_names='
			media-libs/gst-plugins-base[abi_x86_32]'
		;;
		('deinterlace')
			package_names='
			media-libs/gst-plugins-good[abi_x86_32]'
		;;
		('application/x-id3')
			package_names='
			media-libs/gst-plugins-good[abi_x86_32]'
		;;
		('audio/mpeg, mpegversion=(int)1, layer=(int)3')
			package_names='
			media-libs/gst-plugins-good[abi_x86_32]'
		;;
		('audio/x-wma, wmaversion=(int)1')
			package_names='
			media-plugins/gst-plugins-libav[abi_x86_32]'
		;;
		('video/mpeg, systemstream=(boolean)true, mpegversion=(int)1')
			package_names='
			media-libs/gst-plugins-ugly[abi_x86_32]
			media-libs/gst-plugins-bad[abi_x86_32]'
		;;
		('video/quicktime, variant=(string)iso')
			package_names='
			media-libs/gst-plugins-good[abi_x86_32]
			media-plugins/gst-plugins-libav[abi_x86_32]'
		;;
		('video/x-ms-asf')
			package_names='
			media-libs/gst-plugins-ugly[abi_x86_32]
			media-plugins/gst-plugins-libav[abi_x86_32]'
		;;
		('video/x-msvideo')
			package_names='
			media-libs/gst-plugins-good[abi_x86_32]
			media-plugins/gst-plugins-libav[abi_x86_32]'
		;;
		('video/x-wmv, wmvversion=(int)1')
			package_names='
			media-plugins/gst-plugins-libav[abi_x86_32]'
		;;
		(*)
			dependencies_unknown_gstreamer_media_formats_add "$media_format"
			return 0
		;;
	esac

	printf '%s' "$package_names"
}

# Gentoo - Print the package names providing the given native libraries
# USAGE: gentoo_dependencies_providing_native_libraries $package $library[…]
# RETURN: a list of Gentoo package names,
#         one per line
gentoo_dependencies_providing_native_libraries() {
	local package
	package="$1"
	shift 1

	local library native_packages_list native_package
	native_packages_list=''
	for library in "$@"; do
		native_package=$(dependency_package_providing_library_gentoo "$library" "$package")
		native_packages_list="$native_packages_list
		$native_package"
	done

	printf '%s' "$native_packages_list" | list_clean
}

# Gentoo - Print the package names providing the given native libraries in a 32-bit build
# USAGE: gentoo_dependencies_providing_native_libraries_32bit $package $library[…]
# RETURN: a list of Gentoo package names,
#         one per line
gentoo_dependencies_providing_native_libraries_32bit() {
	local package
	package="$1"
	shift 1

	local library native_packages_list native_package
	native_packages_list=''
	for library in "$@"; do
		native_package=$(dependency_package_providing_library_gentoo32 "$library" "$package")
		native_packages_list="$native_packages_list
		$native_package"
	done

	printf '%s' "$native_packages_list" | list_clean
}

# Gentoo - Print the package name providing the given native library
# USAGE: dependency_package_providing_library_gentoo $library $package
dependency_package_providing_library_gentoo() {
	local library package package_name pkg_overlay
	library="$1"
	package="$2"
	case "$library" in
		('ld-linux.so.2')
			package_name='sys-libs/glibc'
		;;
		('ld-linux-x86-64.so.2')
			package_name='sys-libs/glibc'
		;;
		('liballeg.so.4.4')
			package_name='media-libs/allegro'
		;;
		('liballegro.so.5.2')
			package_name='media-libs/allegro'
		;;
		('liballegro_acodec.so.5.2')
			package_name='media-libs/allegro'
		;;
		('liballegro_audio.so.5.2')
			package_name='media-libs/allegro'
		;;
		('liballegro_font.so.5.2')
			package_name='media-libs/allegro'
		;;
		('liballegro_image.so.5.2')
			package_name='media-libs/allegro'
		;;
		('liballegro_primitives.so.5.2')
			package_name='media-libs/allegro'
		;;
		('liballegro_ttf.so.5.2')
			package_name='media-libs/allegro'
		;;
		('libalut.so.0')
			package_name='media-libs/freealut'
		;;
		('libasound.so.2')
			package_name='media-libs/alsa-lib'
		;;
		('libasound_module_'*'.so')
			package_name='media-plugins/alsa-plugins'
		;;
		('libatspi.so.0')
			package_name='app-accessibility/at-spi2-core'
		;;
		('libatk-1.0.so.0')
			package_name='dev-libs/atk'
		;;
		('libaudio.so.2')
			package_name='media-libs/nas'
		;;
		('libboost_locale.so.1.74.0')
			# This library is not provided for Gentoo
			unset package_name
		;;
		('libbz2.so.1.0'|'libbz2.so.1')
			package_name='app-arch/bzip2'
		;;
		('libc.so.6')
			package_name='sys-libs/glibc'
		;;
		('libc++.so.1')
			package_name='sys-libs/libcxx'
		;;
		('libc++abi.so.1')
			package_name='sys-libs/libcxxabi'
		;;
		('libcairo.so.2')
			package_name='x11-libs/cairo'
		;;
		('libCg.so')
			# This library is not provided for Gentoo
			unset package_name
		;;
		('libCgGL.so')
			# This library is not provided for Gentoo
			unset package_name
		;;
		('libcom_err.so.2')
			package_name='sys-libs/e2fsprogs-libs'
		;;
		('libcrypt.so.1')
			package_name='sys-libs/libxcrypt'
		;;
		('libcups.so.2')
			package_name='net-print/cups'
		;;
		('libcurl.so.4')
			package_name='net-misc/curl'
		;;
		('libcurl.so.4+CURL_OPENSSL_3')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/curl/
			return 0
		;;
		('libcurl-gnutls.so.4')
			package_name='net-libs/libcurl-debian'
			pkg_overlay='steam-overlay'
			dependencies_gentoo_link 'libcurl-gnutls.so.4' "/usr/$(dependency_gentoo_libdir 'amd64')/debiancompat" "$package"
			;;
		('libdbus-1.so.3')
			package_name='sys-apps/dbus'
		;;
		('libdbus-glib-1.so.2')
			package_name='dev-libs/dbus-glib'
		;;
		('libdl.so.2')
			package_name='sys-libs/glibc'
		;;
		('libEGL.so.1')
			package_name='media-libs/libglvnd'
		;;
		('libexpat.so.1')
			package_name='dev-libs/expat'
		;;
		('libFAudio.so.0')
			package_name='app-emulation/faudio'
		;;
		('libFLAC.so.8')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/flac/
			return 0
		;;
		('libfontconfig.so.1')
			package_name='media-libs/fontconfig'
		;;
		('libfreeimage.so.3')
			package_name='media-libs/freeimage'
		;;
		('libfreetype.so.6')
			package_name='media-libs/freetype'
		;;
		('libfribidi.so.0')
			package_name='dev-libs/fribidi'
		;;
		('libgcc_s.so.1')
			package_name='sys-devel/gcc'
		;;
		('libgconf-2.so.4')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/gconf/
			return 0
		;;
		('libgcrypt.so.11')
			package_name='dev-libs/libgcrypt-compat'
		;;
		('libgdiplus.so')
			package_name='dev-dotnet/libgdiplus'
		;;
		('libgdk-3.so.0')
			package_name='x11-libs/gtk+:3'
		;;
		('libgdk_pixbuf-2.0.so.0')
			package_name='x11-libs/gdk-pixbuf:2'
		;;
		('libgdk-x11-2.0.so.0')
			package_name='x11-libs/gtk+:2'
		;;
		('libgio-2.0.so.0')
			package_name='dev-libs/glib:2'
		;;
		('libGL.so.1')
			package_name='virtual/opengl'
		;;
		('libGLEW.so.2.2')
			package_name='media-libs/glew'
		;;
		('libglfw.so.3')
			package_name='media-libs/glfw'
		;;
		('libglib-2.0.so.0')
			package_name='dev-libs/glib:2'
		;;
		('libGLU.so.1')
			package_name='virtual/glu'
		;;
		('libGLX.so.0')
			package_name='media-libs/libglvnd'
		;;
		('libgmodule-2.0.so.0')
			package_name='dev-libs/glib:2'
		;;
		('libgobject-2.0.so.0')
			package_name='dev-libs/glib:2'
		;;
		('libgomp.so.1')
			package_name='sys-devel/gcc'
		;;
		('libgpg-error.so.0')
			package_name='dev-libs/libgpg-error'
		;;
		('libgssapi_krb5.so.2')
			package_name='app-crypt/mit-krb5'
		;;
		('libgthread-2.0.so.0')
			package_name='dev-libs/glib:2'
		;;
		('libgtk-x11-2.0.so.0')
			package_name='x11-libs/gtk+:2'
		;;
		('libgtk-3.so.0')
			package_name='x11-libs/gtk+:3'
		;;
		('libICE.so.6')
			package_name='x11-libs/libICE'
		;;
		('libidn.so.11')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/libidn/
			return 0
		;;
		('libidn2.so.0')
			package_name='net-dns/libidn2'
		;;
		('libIL.so.1')
			package_name='media-libs/devil'
		;;
		('libjpeg.so.62')
			package_name='media-libs/libjpeg-turbo'
		;;
		('libk5crypto.so.3')
			package_name='app-crypt/mit-krb5'
		;;
		('libkrb5.so.3')
			package_name='app-crypt/mit-krb5'
		;;
		('liblcms2.so.2')
			package_name='media-libs/lcms'
		;;
		('liblua5.3.so.0')
			package_name='dev-lang/lua'
		;;
		('libluajit-5.1.so.2')
			package_name='dev-lang/luajit'
		;;
		('liblz4.so.1')
			package_name='app-arch/lz4'
		;;
		('libm.so.6')
			package_name='sys-libs/glibc'
		;;
		('libmbedtls.so.12')
			package_name='net-libs/mbedtls:0/12'
		;;
		('libminiupnpc.so.17')
			package_name='net-libs/miniupnpc'
		;;
		('libminizip.so.1')
			package_name='sys-libs/zlib'
		;;
		('libmodplug.so.1')
			package_name='media-libs/libmodplug'
		;;
		('libmpg123.so.0')
			package_name='media-sound/mpg123'
		;;
		('libnghttp2.so.14')
			package_name='net-libs/nghttp2'
		;;
		('libnotify.so.4')
			package_name='x11-libs/libnotify'
		;;
		('libnspr4.so')
			package_name='dev-libs/nspr'
		;;
		('libnss3.so')
			package_name='dev-libs/nss'
		;;
		('libnssutil3.so')
			package_name='dev-libs/nss'
		;;
		('libogg.so.0')
			package_name='media-libs/libogg'
		;;
		('libopenal.so.1')
			package_name='media-libs/openal'
		;;
		('libOpenGL.so.0')
			package_name='media-libs/libglvnd'
		;;
		('libopenmpt.so.0')
			package_name='media-libs/libopenmpt'
		;;
		('libpango-1.0.so.0')
			package_name='x11-libs/pango'
		;;
		('libpangocairo-1.0.so.0')
			package_name='x11-libs/pango'
		;;
		('libpangoft2-1.0.so.0')
			package_name='x11-libs/pango'
		;;
		('libpcre.so.3')
			package_name='dev-libs/libpcre-debian'
		;;
		('libphysfs.so.1')
			package_name='dev-games/physfs'
		;;
		('libpixman-1.so.0')
			package_name='x11-libs/pixman'
		;;
		('libplc4.so')
			package_name='dev-libs/nspr'
		;;
		('libplds4.so')
			package_name='dev-libs/nspr'
		;;
		('libpng12.so.0')
			package_name='media-libs/libpng-compat:1.2'
		;;
		('libpng16.so.16')
			package_name='media-libs/libpng:0/16'
		;;
		('libpsl.so.5')
			package_name='net-libs/libpsl'
		;;
		('libpthread.so.0')
			package_name='sys-libs/glibc'
		;;
		('libpulse.so.0')
			package_name='media-sound/pulseaudio'
		;;
		('libpulse-simple.so.0')
			package_name='media-sound/pulseaudio'
		;;
		('libresolv.so.2')
			package_name='sys-libs/glibc'
		;;
		('librt.so.1')
			package_name='sys-libs/glibc'
		;;
		('librtmp.so.1')
			package_name='media-video/rtmpdump'
		;;
		('libSDL-1.2.so.0')
			package_name='media-libs/libsdl[opengl]'
		;;
		('libSDL_image-1.2.so.0')
			package_name='media-libs/sdl-image'
		;;
		('libSDL_kitchensink.so.1')
			# This library is not provided for Gentoo
			unset package_name
		;;
		('libSDL_mixer-1.2.so.0')
			package_name='media-libs/sdl-mixer'
		;;
		('libSDL_sound-1.0.so.1')
			package_name='media-libs/sdl-sound'
		;;
		('libSDL_ttf-2.0.so.0')
			package_name='media-libs/sdl-ttf'
		;;
		('libSDL2-2.0.so.0')
			package_name='media-libs/libsdl2[opengl]'
		;;
		('libSDL2_image-2.0.so.0')
			# Most games will require at least jpeg and png
			# Maybe we should add gif and tiff to that list?
			package_name='media-libs/sdl2-image[jpeg,png]'
		;;
		('libSDL2_mixer-2.0.so.0')
			# Most games will require at least one of flac, mp3, vorbis or wav USE flags,
			# it should better to require them all instead of not requiring any
			# and having non-fonctionnal sound in some games.
			package_name='media-libs/sdl2-mixer[flac,mp3,vorbis,wav]'
		;;
		('libSDL2_ttf-2.0.so.0')
			package_name='media-libs/sdl2-ttf'
		;;
		('libsecret-1.so.0')
			package_name='app-crypt/libsecret'
		;;
		('libsigc-2.0.so.0')
			package_name='dev-libs/libsigc++'
		;;
		('libSM.so.6')
			package_name='x11-libs/libSM'
		;;
		('libsmime3.so')
			package_name='dev-libs/nss'
		;;
		('libsmpeg-0.4.so.0')
			package_name='media-libs/smpeg'
		;;
		('libsodium.so.23')
			package_name='dev-libs/libsodium'
		;;
		('libssh2.so.1')
			package_name='net-libs/libssh2'
		;;
		('libssl.so.1.0.0')
			package_name='dev-libs/openssl-compat:1.0.0'
		;;
		('libssl.so.1.1')
			package_name='dev-libs/openssl-compat:1.1.1'
		;;
		('libssl3.so')
			package_name='dev-libs/nss'
		;;
		('libstdc++.so.5')
			package_name='sys-libs/libstdc++-v3'
		;;
		('libstdc++.so.6')
			package_name='sys-devel/gcc'
		;;
		('libtcmalloc_minimal.so.4')
			package_name='dev-util/google-perftools'
		;;
		('libtheora.so.0')
			package_name='media-libs/libtheora'
		;;
		('libtheoradec.so.1')
			package_name='media-libs/libtheora'
		;;
		('libtheoraenc.so.1')
			package_name='media-libs/libtheora'
		;;
		('libthread_db.so.1')
			package_name='sys-libs/glibc'
		;;
		('libtiff.so.6')
			package_name='media-libs/tiff'
		;;
		('libturbojpeg.so.0')
			package_name='media-libs/libjpeg-turbo'
		;;
		('libudev.so.0')
			package_name='sys-libs/libudev-compat'
		;;
		('libudev.so.1')
			package_name='virtual/libudev'
		;;
		('libutil.so.1')
			package_name='sys-libs/glibc'
		;;
		('libuuid.so.1')
			package_name='sys-apps/util-linux'
		;;
		('libuv.so.1')
			package_name='dev-libs/libuv:0/1'
		;;
		('libvorbis.so.0')
			package_name='media-libs/libvorbis'
		;;
		('libvorbisenc.so.2')
			package_name='media-libs/libvorbis'
		;;
		('libvorbisfile.so.3')
			package_name='media-libs/libvorbis'
		;;
		('libvulkan.so.1')
			package_name='media-libs/vulkan-loader'
		;;
		('libwayland-client.so.0')
			package_name='dev-libs/wayland'
		;;
		('libX11.so.6')
			package_name='x11-libs/libX11'
		;;
		('libX11-xcb.so.1')
			package_name='x11-libs/libX11'
		;;
		('libxcb.so.1')
			package_name='x11-libs/libxcb'
		;;
		('libxcb-randr.so.0')
			package_name='x11-libs/libxcb'
		;;
		('libXcomposite.so.1')
			package_name='x11-libs/libXcomposite'
		;;
		('libXcursor.so.1')
			package_name='x11-libs/libXcursor'
		;;
		('libXdamage.so.1')
			package_name='x11-libs/libXdamage'
		;;
		('libXext.so.6')
			package_name='x11-libs/libXext'
		;;
		('libXfixes.so.3')
			package_name='x11-libs/libXfixes'
		;;
		('libXft.so.2')
			package_name='x11-libs/libXft'
		;;
		('libXi.so.6')
			package_name='x11-libs/libXi'
		;;
		('libXinerama.so.1')
			package_name='x11-libs/libXinerama'
		;;
		('libxml2.so.2')
			package_name='dev-libs/libxml2'
		;;
		('libxmp.so.4')
			package_name='media-libs/libxmp'
		;;
		('libXmu.so.6')
			package_name='x11-libs/libXmu'
		;;
		('libXrandr.so.2')
			package_name='x11-libs/libXrandr'
		;;
		('libXrender.so.1')
			package_name='x11-libs/libXrender'
		;;
		('libxslt.so.1')
			package_name='dev-libs/libxslt'
		;;
		('libXss.so.1')
			package_name='x11-libs/libXScrnSaver'
		;;
		('libXt.so.6')
			package_name='x11-libs/libXt'
		;;
		('libXtst.so.6')
			package_name='x11-libs/libXtst'
		;;
		('libXxf86vm.so.1')
			package_name='x11-libs/libXxf86vm'
		;;
		('libyaml-0.so.2')
			package_name='dev-libs/libyaml'
		;;
		('libz.so.1')
			package_name='sys-libs/zlib:0/1'
		;;
	esac

	if [ -n "${package_name:-}" ]; then
		printf '%s' "$package_name"
		if [ -n "${pkg_overlay:-}" ]; then
			dependency_gentoo_overlays_add "$pkg_overlay"
		fi
		return 0
	fi

	dependencies_unknown_libraries_add "$library"
}

# Gentoo - Print the package name providing the given native library in a 32-bit build
# USAGE: dependency_package_providing_library_gentoo32 $library $package
dependency_package_providing_library_gentoo32() {
	local library package package_name pkg_overlay
	library="$1"
	package="$2"
	case "$library" in
		('ld-linux.so.2')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('ld-linux-x86-64.so.2')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('liballeg.so.4.4')
			package_name='media-libs/allegro[abi_x86_32]'
		;;
		('liballegro.so.5.2')
			package_name='media-libs/allegro[abi_x86_32]'
		;;
		('liballegro_acodec.so.5.2')
			package_name='media-libs/allegro[abi_x86_32]'
		;;
		('liballegro_audio.so.5.2')
			package_name='media-libs/allegro[abi_x86_32]'
		;;
		('liballegro_font.so.5.2')
			package_name='media-libs/allegro[abi_x86_32]'
		;;
		('liballegro_image.so.5.2')
			package_name='media-libs/allegro[abi_x86_32]'
		;;
		('liballegro_primitives.so.5.2')
			package_name='media-libs/allegro[abi_x86_32]'
		;;
		('liballegro_ttf.so.5.2')
			package_name='media-libs/allegro[abi_x86_32]'
		;;
		('libalut.so.0')
			package_name='media-libs/freealut[abi_x86_32]'
		;;
		('libasound.so.2')
			package_name='media-libs/alsa-lib[abi_x86_32]'
		;;
		('libasound_module_'*'.so')
			package_name='media-plugins/alsa-plugins[abi_x86_32]'
		;;
		('libatspi.so.0')
			package_name='app-accessibility/at-spi2-core[abi_x86_32]'
		;;
		('libatk-1.0.so.0')
			package_name='dev-libs/atk[abi_x86_32]'
		;;
		('libaudio.so.2')
			package_name='media-libs/nas[abi_x86_32]'
		;;
		('libboost_locale.so.1.74.0')
			# This library is not provided for Gentoo
			unset package_name
		;;
		('libbz2.so.1.0'|'libbz2.so.1')
			package_name='app-arch/bzip2[abi_x86_32]'
		;;
		('libc.so.6')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('libc++.so.1')
			package_name='sys-libs/libcxx[abi_x86_32]'
		;;
		('libc++abi.so.1')
			package_name='sys-libs/libcxxabi[abi_x86_32]'
		;;
		('libcairo.so.2')
			package_name='x11-libs/cairo[abi_x86_32]'
		;;
		('libCg.so')
			# This library is not provided for Gentoo
			unset package_name
		;;
		('libCgGL.so')
			# This library is not provided for Gentoo
			unset package_name
		;;
		('libcom_err.so.2')
			package_name='sys-libs/e2fsprogs-libs[abi_x86_32]'
		;;
		('libcrypt.so.1')
			package_name='sys-libs/libxcrypt[abi_x86_32]'
		;;
		('libcups.so.2')
			package_name='net-print/cups[abi_x86_32]'
		;;
		('libcurl.so.4')
			package_name='net-misc/curl[abi_x86_32]'
		;;
		('libcurl.so.4+CURL_OPENSSL_3')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/curl/
			return 0
		;;
		('libcurl-gnutls.so.4')
			package_name='net-libs/libcurl-debian[abi_x86_32]'
			pkg_overlay='steam-overlay'
			dependencies_gentoo_link 'libcurl-gnutls.so.4' "/usr/$(dependency_gentoo_libdir 'x86')/debiancompat" "$package"
			;;
		('libdbus-1.so.3')
			package_name='sys-apps/dbus[abi_x86_32]'
		;;
		('libdbus-glib-1.so.2')
			package_name='dev-libs/dbus-glib[abi_x86_32]'
		;;
		('libdl.so.2')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('libEGL.so.1')
			package_name='media-libs/libglvnd[abi_x86_32]'
		;;
		('libexpat.so.1')
			package_name='dev-libs/expat[abi_x86_32]'
		;;
		('libFAudio.so.0')
			package_name='app-emulation/faudio[abi_x86_32]'
		;;
		('libFLAC.so.8')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/flac/
			return 0
		;;
		('libfontconfig.so.1')
			package_name='media-libs/fontconfig[abi_x86_32]'
		;;
		('libfreeimage.so.3')
			package_name='media-libs/freeimage[abi_x86_32]'
		;;
		('libfreetype.so.6')
			package_name='media-libs/freetype[abi_x86_32]'
		;;
		('libfribidi.so.0')
			package_name='dev-libs/fribidi[abi_x86_32]'
		;;
		('libgcc_s.so.1')
			package_name='sys-devel/gcc[abi_x86_32]'
		;;
		('libgconf-2.so.4')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/gconf/
			return 0
		;;
		('libgcrypt.so.11')
			package_name='dev-libs/libgcrypt-compat[abi_x86_32]'
		;;
		('libgdiplus.so')
			package_name='dev-dotnet/libgdiplus[abi_x86_32]'
		;;
		('libgdk-3.so.0')
			package_name='x11-libs/gtk+:3[abi_x86_32]'
		;;
		('libgdk_pixbuf-2.0.so.0')
			package_name='x11-libs/gdk-pixbuf:2[abi_x86_32]'
		;;
		('libgdk-x11-2.0.so.0')
			package_name='x11-libs/gtk+:2[abi_x86_32]'
		;;
		('libgio-2.0.so.0')
			package_name='dev-libs/glib:2[abi_x86_32]'
		;;
		('libGL.so.1')
			package_name='virtual/opengl[abi_x86_32]'
		;;
		('libGLEW.so.2.2')
			package_name='media-libs/glew[abi_x86_32]'
		;;
		('libglfw.so.3')
			package_name='media-libs/glfw[abi_x86_32]'
		;;
		('libglib-2.0.so.0')
			package_name='dev-libs/glib:2[abi_x86_32]'
		;;
		('libGLU.so.1')
			package_name='virtual/glu[abi_x86_32]'
		;;
		('libGLX.so.0')
			package_name='media-libs/libglvnd[abi_x86_32]'
		;;
		('libgmodule-2.0.so.0')
			package_name='dev-libs/glib:2[abi_x86_32]'
		;;
		('libgobject-2.0.so.0')
			package_name='dev-libs/glib:2[abi_x86_32]'
		;;
		('libgomp.so.1')
			package_name='sys-devel/gcc[abi_x86_32]'
		;;
		('libgpg-error.so.0')
			package_name='dev-libs/libgpg-error[abi_x86_32]'
		;;
		('libgssapi_krb5.so.2')
			package_name='app-crypt/mit-krb5[abi_x86_32]'
		;;
		('libgthread-2.0.so.0')
			package_name='dev-libs/glib:2[abi_x86_32]'
		;;
		('libgtk-x11-2.0.so.0')
			package_name='x11-libs/gtk+:2[abi_x86_32]'
		;;
		('libgtk-3.so.0')
			package_name='x11-libs/gtk+:3[abi_x86_32]'
		;;
		('libICE.so.6')
			package_name='x11-libs/libICE[abi_x86_32]'
		;;
		('libidn.so.11')
			# This native library is provided by an extra archive:
			# https://downloads.dotslashplay.it/resources/libidn/
			return 0
		;;
		('libidn2.so.0')
			package_name='net-dns/libidn2[abi_x86_32]'
		;;
		('libIL.so.1')
			package_name='media-libs/devil[abi_x86_32]'
		;;
		('libjpeg.so.62')
			package_name='media-libs/libjpeg-turbo[abi_x86_32]'
		;;
		('libk5crypto.so.3')
			package_name='app-crypt/mit-krb5[abi_x86_32]'
		;;
		('libkrb5.so.3')
			package_name='app-crypt/mit-krb5[abi_x86_32]'
		;;
		('liblcms2.so.2')
			package_name='media-libs/lcms[abi_x86_32]'
		;;
		('liblua5.3.so.0')
			package_name='dev-lang/lua[abi_x86_32]'
		;;
		('libluajit-5.1.so.2')
			package_name='dev-lang/luajit[abi_x86_32]'
		;;
		('liblz4.so.1')
			package_name='app-arch/lz4[abi_x86_32]'
		;;
		('libm.so.6')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('libmbedtls.so.12')
			package_name='net-libs/mbedtls:0/12[abi_x86_32]'
		;;
		('libminiupnpc.so.17')
			package_name='net-libs/miniupnpc[abi_x86_32]'
		;;
		('libminizip.so.1')
			package_name='sys-libs/zlib[abi_x86_32]'
		;;
		('libmodplug.so.1')
			package_name='media-libs/libmodplug[abi_x86_32]'
		;;
		('libmpg123.so.0')
			package_name='media-sound/mpg123[abi_x86_32]'
		;;
		('libnghttp2.so.14')
			package_name='net-libs/nghttp2[abi_x86_32]'
		;;
		('libnotify.so.4')
			package_name='x11-libs/libnotify[abi_x86_32]'
		;;
		('libnspr4.so')
			package_name='dev-libs/nspr[abi_x86_32]'
		;;
		('libnss3.so')
			package_name='dev-libs/nss[abi_x86_32]'
		;;
		('libnssutil3.so')
			package_name='dev-libs/nss[abi_x86_32]'
		;;
		('libogg.so.0')
			package_name='media-libs/libogg[abi_x86_32]'
		;;
		('libopenal.so.1')
			package_name='media-libs/openal[abi_x86_32]'
		;;
		('libOpenGL.so.0')
			package_name='media-libs/libglvnd[abi_x86_32]'
		;;
		('libopenmpt.so.0')
			package_name='media-libs/libopenmpt[abi_x86_32]'
		;;
		('libpango-1.0.so.0')
			package_name='x11-libs/pango[abi_x86_32]'
		;;
		('libpangocairo-1.0.so.0')
			package_name='x11-libs/pango[abi_x86_32]'
		;;
		('libpangoft2-1.0.so.0')
			package_name='x11-libs/pango[abi_x86_32]'
		;;
		('libpcre.so.3')
			package_name='dev-libs/libpcre-debian[abi_x86_32]'
		;;
		('libphysfs.so.1')
			package_name='dev-games/physfs[abi_x86_32]'
		;;
		('libpixman-1.so.0')
			package_name='x11-libs/pixman[abi_x86_32]'
		;;
		('libplc4.so')
			package_name='dev-libs/nspr[abi_x86_32]'
		;;
		('libplds4.so')
			package_name='dev-libs/nspr[abi_x86_32]'
		;;
		('libpng12.so.0')
			package_name='media-libs/libpng-compat:1.2[abi_x86_32]'
		;;
		('libpng16.so.16')
			package_name='media-libs/libpng:0/16[abi_x86_32]'
		;;
		('libpsl.so.5')
			package_name='net-libs/libpsl[abi_x86_32]'
		;;
		('libpthread.so.0')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('libpulse.so.0')
			package_name='media-sound/pulseaudio[abi_x86_32]'
		;;
		('libpulse-simple.so.0')
			package_name='media-sound/pulseaudio[abi_x86_32]'
		;;
		('libresolv.so.2')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('librt.so.1')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('librtmp.so.1')
			package_name='media-video/rtmpdump[abi_x86_32]'
		;;
		('libSDL-1.2.so.0')
			package_name='media-libs/libsdl[abi_x86_32,opengl]'
		;;
		('libSDL_image-1.2.so.0')
			package_name='media-libs/sdl-image[abi_x86_32]'
		;;
		('libSDL_kitchensink.so.1')
			# This library is not provided for Gentoo
			unset package_name
		;;
		('libSDL_mixer-1.2.so.0')
			package_name='media-libs/sdl-mixer[abi_x86_32]'
		;;
		('libSDL_sound-1.0.so.1')
			package_name='media-libs/sdl-sound[abi_x86_32]'
		;;
		('libSDL_ttf-2.0.so.0')
			package_name='media-libs/sdl-ttf[abi_x86_32]'
		;;
		('libSDL2-2.0.so.0')
			package_name='media-libs/libsdl2[abi_x86_32,opengl]'
		;;
		('libSDL2_image-2.0.so.0')
			# Most games will require at least jpeg and png
			# Maybe we should add gif and tiff to that list?
			package_name='media-libs/sdl2-image[jpeg,png,abi_x86_32]'
		;;
		('libSDL2_mixer-2.0.so.0')
			# Most games will require at least one of flac, mp3, vorbis or wav USE flags,
			# it should better to require them all instead of not requiring any
			# and having non-fonctionnal sound in some games.
			package_name='media-libs/sdl2-mixer[flac,mp3,vorbis,wav,abi_x86_32]'
		;;
		('libSDL2_ttf-2.0.so.0')
			package_name='media-libs/sdl2-ttf[abi_x86_32]'
		;;
		('libsecret-1.so.0')
			package_name='app-crypt/libsecret[abi_x86_32]'
		;;
		('libsigc-2.0.so.0')
			package_name='dev-libs/libsigc++[abi_x86_32]'
		;;
		('libSM.so.6')
			package_name='x11-libs/libSM[abi_x86_32]'
		;;
		('libsmime3.so')
			package_name='dev-libs/nss[abi_x86_32]'
		;;
		('libsmpeg-0.4.so.0')
			package_name='media-libs/smpeg[abi_x86_32]'
		;;
		('libsodium.so.23')
			package_name='dev-libs/libsodium[abi_x86_32]'
		;;
		('libssh2.so.1')
			package_name='net-libs/libssh2[abi_x86_32]'
		;;
		('libssl.so.1.0.0')
			package_name='dev-libs/openssl-compat:1.0.0[abi_x86_32]'
		;;
		('libssl.so.1.1')
			package_name='dev-libs/openssl-compat:1.1.1[abi_x86_32]'
		;;
		('libssl3.so')
			package_name='dev-libs/nss[abi_x86_32]'
		;;
		('libstdc++.so.5')
			package_name='sys-libs/libstdc++-v3[abi_x86_32]'
		;;
		('libstdc++.so.6')
			package_name='sys-devel/gcc amd64? ( sys-devel/gcc[multilib] )'
		;;
		('libtcmalloc_minimal.so.4')
			package_name='dev-util/google-perftools[abi_x86_32]'
		;;
		('libtheora.so.0')
			package_name='media-libs/libtheora[abi_x86_32]'
		;;
		('libtheoradec.so.1')
			package_name='media-libs/libtheora[abi_x86_32]'
		;;
		('libtheoraenc.so.1')
			package_name='media-libs/libtheora[abi_x86_32]'
		;;
		('libthread_db.so.1')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('libtiff.so.6')
			package_name='media-libs/tiff[abi_x86_32]'
		;;
		('libturbojpeg.so.0')
			package_name='media-libs/libjpeg-turbo[abi_x86_32]'
		;;
		('libudev.so.0')
			package_name='sys-libs/libudev-compat[abi_x86_32]'
		;;
		('libudev.so.1')
			package_name='virtual/libudev[abi_x86_32]'
		;;
		('libutil.so.1')
			package_name='sys-libs/glibc amd64? ( sys-libs/glibc[multilib] )'
		;;
		('libuuid.so.1')
			package_name='sys-apps/util-linux[abi_x86_32]'
		;;
		('libuv.so.1')
			package_name='dev-libs/libuv:0/1[abi_x86_32]'
		;;
		('libvorbis.so.0')
			package_name='media-libs/libvorbis[abi_x86_32]'
		;;
		('libvorbisenc.so.2')
			package_name='media-libs/libvorbis[abi_x86_32]'
		;;
		('libvorbisfile.so.3')
			package_name='media-libs/libvorbis[abi_x86_32]'
		;;
		('libvulkan.so.1')
			package_name='media-libs/vulkan-loader[abi_x86_32]'
		;;
		('libwayland-client.so.0')
			package_name='dev-libs/wayland[abi_x86_32]'
		;;
		('libX11.so.6')
			package_name='x11-libs/libX11[abi_x86_32]'
		;;
		('libX11-xcb.so.1')
			package_name='x11-libs/libX11[abi_x86_32]'
		;;
		('libxcb.so.1')
			package_name='x11-libs/libxcb[abi_x86_32]'
		;;
		('libxcb-randr.so.0')
			package_name='x11-libs/libxcb[abi_x86_32]'
		;;
		('libXcomposite.so.1')
			package_name='x11-libs/libXcomposite[abi_x86_32]'
		;;
		('libXcursor.so.1')
			package_name='x11-libs/libXcursor[abi_x86_32]'
		;;
		('libXdamage.so.1')
			package_name='x11-libs/libXdamage[abi_x86_32]'
		;;
		('libXext.so.6')
			package_name='x11-libs/libXext[abi_x86_32]'
		;;
		('libXfixes.so.3')
			package_name='x11-libs/libXfixes[abi_x86_32]'
		;;
		('libXft.so.2')
			package_name='x11-libs/libXft[abi_x86_32]'
		;;
		('libXi.so.6')
			package_name='x11-libs/libXi[abi_x86_32]'
		;;
		('libXinerama.so.1')
			package_name='x11-libs/libXinerama[abi_x86_32]'
		;;
		('libxml2.so.2')
			package_name='dev-libs/libxml2[abi_x86_32]'
		;;
		('libxmp.so.4')
			package_name='media-libs/libxmp[abi_x86_32]'
		;;
		('libXmu.so.6')
			package_name='x11-libs/libXmu[abi_x86_32]'
		;;
		('libXrandr.so.2')
			package_name='x11-libs/libXrandr[abi_x86_32]'
		;;
		('libXrender.so.1')
			package_name='x11-libs/libXrender[abi_x86_32]'
		;;
		('libxslt.so.1')
			package_name='dev-libs/libxslt[abi_x86_32]'
		;;
		('libXss.so.1')
			package_name='x11-libs/libXScrnSaver[abi_x86_32]'
		;;
		('libXt.so.6')
			package_name='x11-libs/libXt[abi_x86_32]'
		;;
		('libXtst.so.6')
			package_name='x11-libs/libXtst[abi_x86_32]'
		;;
		('libXxf86vm.so.1')
			package_name='x11-libs/libXxf86vm[abi_x86_32]'
		;;
		('libyaml-0.so.2')
			package_name='dev-libs/libyaml[abi_x86_32]'
		;;
		('libz.so.1')
			package_name='sys-libs/zlib:0/1[abi_x86_32]'
		;;
	esac

	if [ -n "${package_name:-}" ]; then
		printf '%s' "$package_name"
		if [ -n "${pkg_overlay:-}" ]; then
			dependency_gentoo_overlays_add "$pkg_overlay"
		fi
		return 0
	fi

	dependencies_unknown_libraries_add "$library"
}

# Gentoo - Print the sinling package names required by the given package
# USAGE: gentoo_dependencies_siblings $package
# RETURN: a list of package names,
#         one per line
gentoo_dependencies_siblings() {
	local package
	package="$1"

	local required_siblings
	required_siblings=$(dependencies_list_siblings "$package")
	# Return early if the current package does not require any sibling package
	if [ -z "$required_siblings" ]; then
		return 0
	fi

	local sibling dependencies_siblings_list required_package
	while read -r sibling; do
		required_package=$(package_id "$sibling")
		dependencies_siblings_list="${dependencies_siblings_list:-}
		$required_package"
	done <<- EOL
	$(printf '%s' "$required_siblings")
	EOL

	printf '%s' "${dependencies_siblings_list:-}" | list_clean
}

# print notification about required overlays when building Gentoo packages
# USAGE: information_required_gentoo_overlays $overlays
information_required_gentoo_overlays() {
	local overlays_file
	overlays_file=$(dependency_gentoo_overlays_file)

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Vous pourriez avoir besoin des overlays suivants pour installer ces paquets :'
		;;
		('en'|*)
			message='You may need the following overlays to install these packages:'
		;;
	esac
	print_message 'info' '\n%s\n' \
		"$message"
	print_message 'info' '\t%s\n' \
		"$(cat "$overlays_file")"
}

# add comment to packages installation instructions on Gentoo
# USAGE: information_installation_instructions_gentoo_comment
information_installation_instructions_gentoo_comment() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='ou mettez les paquets dans un PKGDIR (dans un dossier nommé games-playit) et emergez-les'
		;;
		('en'|*)
			message='or put the packages in a PKGDIR (in a folder named games-playit) and emerge them'
		;;
	esac
	print_message 'info' "$message"
}

# inform the need of a local overlay on gentoo for ebuilds
# USAGE: info_local_overlay_gentoo
info_local_overlay_gentoo() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='\nUn overlay local est nécessaire pour utiliser les ebuilds générés par ./play.it\n'
			message="$message"'Dans la suite OVERLAY_PATH correspond au chemin de votre overlay local\n'
		;;
		('en'|*)
			message='\nA local overlay is needed to use the ebuilds generated by ./play.it\n'
			message="$message"'In what comes next, OVERLAY_PATH is the path to your local overlay\n'
		;;
	esac
	print_message 'info' "$message"
}

# inform the need to move the packages to a distfile on egentoo
# USAGE: info_package_to_distfiles
info_package_to_distfiles() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Déplacez les paquets créés dans votre disfile\n'
			message="$message"'puis exécutez les instructions suivantes :\n'
			;;
		('en'|*)
			message='Move the generated packages into your distfile\n'
			message="$message"'then run the following commands:\n'
			;;
	esac
	print_message 'info' "$message"
}

# Error - An unknown architecture string is used
# USAGE: error_unknown_gentoo_architecture_string $arch_string $caller
error_unknown_gentoo_architecture_string() {
	local arch_string caller
	arch_string="$1"
	caller="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼarchitecture « %s », utilisée dans %s, est inconnue sous Gentoo.\n'
			;;
		('en'|*)
			message='“%s” architecture, used in %s, is unknown on Gentoo.\n'
			;;
	esac
	print_message 'error' "$message" \
		"$arch_string" \
		"$caller"
}

# Warning: A deprecated function has been called.
# USAGE: warning_deprecated_function $old_function $new_function
warning_deprecated_function() {
	local old_function new_function
	old_function="$1"
	new_function="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La fonction suivante est dépréciée : %s\n'
			message="$message"'Cette nouvelle fonction devrait être utilisée à sa place : %s\n\n'
		;;
		('en'|*)
			message='The following function is deprecated: %s\n'
			message="$message"'This new function should be used instead: %s\n\n'
		;;
	esac

	# Print the message on the standard error output,
	# to avoid messing up the regular output of the function that triggered this warning.
	print_message 'warning_once' "$message" \
		"$old_function" \
		"$new_function" \
		> /dev/stderr
}

# Warning: A deprecated variable is set.
# USAGE: warning_deprecated_variable $old_variable $new_variable
warning_deprecated_variable() {
	local old_variable new_variable
	old_variable="$1"
	new_variable="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La variable suivante est dépréciée : %s\n'
			message="$message"'Cette nouvelle variable devrait être utilisée à sa place : %s\n\n'
		;;
		('en'|*)
			message='The following variable is deprecated: %s\n'
			message="$message"'This new variable should be used instead: %s\n\n'
		;;
	esac

	# Print the message on the standard error output,
	# to avoid messing up the regular output of the variable that triggered this warning.
	print_message 'warning_once' "$message" \
		"$old_variable" \
		"$new_variable" \
		> /dev/stderr
}

# Warning: An option is set to a deprecated value.
# USAGE: warning_option_value_deprecated $option_name $option_value
warning_option_value_deprecated() {
	local option_name option_value
	option_name="$1"
	option_value="$2"

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La valeur suivante est dépréciée pour lʼoption "%s", et ne sera plus acceptée dans une future version : "%s"\n\n'
		;;
		('en'|*)
			message='The following value is deprecated for option "%s", and will no longer be supported with some future update: "%s"\n\n'
		;;
	esac

	# Print the message on the standard error output,
	# to avoid messing up the regular output of the variable that triggered this warning.
	print_message 'warning_once' "$message" \
		"$option_name" \
		"$option_value" \
		> /dev/stderr
}

# Warning: An archive has a deprecated type set.
# USAGE: warning_archive_type_deprecated $archive
warning_archive_type_deprecated() {
	local archive
	archive="$1"

	local archive_type game_name
	archive_type=$(archive_type "$archive")
	game_name=$(game_name)

	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='La prise en charge de "%s" utilise un type dʼarchive déprécié : %s\n\n'
		;;
		('en'|*)
			message='Support for "%s" is using an obsolete archive type: %s\n\n'
		;;
	esac

	# Print the message on the standard error output,
	# to avoid messing up the regular output of the variable that triggered this warning.
	print_message 'warning_once' "$message" \
		"$game_name" \
		"$archive_type" \
		> /dev/stderr
}

# Warning - The legacy variable has been use to set the current archive.
# USAGE: warning_context_legacy_archive
warning_context_legacy_archive() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Lʼarchive actuelle a été définie en utilisant la variable dépréciée $ARCHIVE.\n'
			message="$message"'La fonction set_current_archive devrait être utilisée à sa place.\n'
		;;
		('en'|*)
			message='The current archive has been set using the deprecated variable $ARCHIVE.\n'
			message="$message"'The function set_current_archive should be used instead.\n'
		;;
	esac

	# Print the message on the standard error output,
	# to avoid messing up the regular output of the function that triggered this warning.
	print_message 'warning_once' "$message" > /dev/stderr
}

# Warning - The legacy variable has been use to set the current package.
# USAGE: warning_context_legacy_package
warning_context_legacy_package() {
	local messages_language message
	messages_language=$(messages_language)
	case "$messages_language" in
		('fr')
			message='Le paquet actuel a été défini en utilisant la variable dépréciée $PKG.\n'
			message="$message"'La fonction set_current_package devrait être utilisée à sa place.\n'
		;;
		('en'|*)
			message='The current package has been set using the deprecated variable $PKG.\n'
			message="$message"'The function set_current_package should be used instead.\n'
		;;
	esac

	# Print the message on the standard error output,
	# to avoid messing up the regular output of the function that triggered this warning.
	print_message 'warning_once' "$message" > /dev/stderr
}

# Keep compatibility with 2.27 and older

icons_inclusion() {
	if compatibility_level_is_at_least '2.28'; then
		warning_deprecated_function 'icons_inclusion' 'content_inclusion_icons'
	fi

	local package
	package=$(current_package)
	content_inclusion_icons "$package" "$@"
}

launchers_write() {
	if compatibility_level_is_at_least '2.28'; then
		warning_deprecated_function 'launchers_write' 'launchers_generation'
	fi

	local package
	package=$(current_package)
	launchers_generation "$package" "$@"
}

# Keep compatibility with 2.23 and older

wine_launcher_wineprefix_persistent_legacy() {
	if compatibility_level_is_at_least '2.23'; then
		warning_deprecated_variable 'APP_WINE_LINK_DIRS' 'WINE_PERSISTENT_DIRECTORIES'
	fi

	cat <<- EOF
	# Move files that should be diverted to persistent paths to the game directory
	APP_WINE_LINK_DIRS="$APP_WINE_LINK_DIRS"
	EOF
	cat <<- 'EOF'
	printf '%s' "$APP_WINE_LINK_DIRS" | grep ':' | while read -r line; do
	    prefix_dir="$PATH_PREFIX/${line%%:*}"
	    wine_dir="$WINEPREFIX/drive_c/${line#*:}"
	    mkdir --parents "$prefix_dir"
	    if [ ! -h "$wine_dir" ]; then
	        if [ -d "$wine_dir" ]; then
	            # A basic recursive cp will not work due to the presence of symbolic links to directories in the destination.
	            (
	                cd "$prefix_dir"
	                find . -type l
	            ) | while read -r link; do
	                if [ -e "${wine_dir}/${link}" ]; then
	                    cp --no-clobber --recursive "${wine_dir}/${link}"/* "${prefix_dir}/${link}"/
	                    rm --force --recursive "${wine_dir:?}/${link}"
	                fi
	            done
	            cp --no-clobber --no-target-directory --recursive "$wine_dir" "$prefix_dir"
	            rm --force --recursive "$wine_dir"
	        fi
	        if [ ! -d "$prefix_dir" ]; then
	            mkdir --parents "$prefix_dir"
	        fi
	        mkdir --parents "$(dirname "$wine_dir")"
	        ln --symbolic "$prefix_dir" "$wine_dir"
	    fi
	done

	EOF
}

write_metadata() {
	if compatibility_level_is_at_least '2.24'; then
		warning_deprecated_function 'write_metadata' 'packages_generation'
	fi

	# If not explicit packages list is given, write metadata for all packages
	if [ $# -eq 0 ]; then
		local packages_list
		packages_list=$(packages_list)
		write_metadata $packages_list
	fi

	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			archlinux_packages_metadata "$@"
		;;
		('deb')
			debian_packages_metadata "$@"
		;;
		('gentoo')
			gentoo_packages_metadata "$@"
		;;
		('egentoo')
			egentoo_packages_metadata "$@"
		;;
	esac
}

build_pkg() {
	if compatibility_level_is_at_least '2.24'; then
		warning_deprecated_function 'build_pkg' 'packages_generation'
	fi

	# If not explicit packages list is given, build all packages
	if [ $# -eq 0 ]; then
		local packages_list
		packages_list=$(packages_list)
		build_pkg $packages_list
	fi

	local option_package
	option_package=$(option_value 'package')
	case "$option_package" in
		('arch')
			archlinux_packages_build "$@"
		;;
		('deb')
			debian_packages_build "$@"
		;;
		('gentoo')
			gentoo_packages_build "$@"
		;;
		('egentoo')
			egentoo_packages_build "$@"
		;;
	esac
}

# Keep compatibility with 2.18 and older

content_path_legacy() {
	local content_id
	content_id="$1"

	local content_path_legacy
	content_path_legacy=$(context_value "ARCHIVE_${content_id}_PATH")
	if [ -z "$content_path_legacy" ]; then
		## Return early if no path is set.
		return 0
	fi

	if compatibility_level_is_at_least '2.18'; then
		warning_deprecated_variable \
			"ARCHIVE_${content_id}_PATH" \
			"CONTENT_${content_id}_PATH"
	fi

	printf '%s' "$content_path_legacy"
}

content_files_legacy() {
	local content_id
	content_id="$1"

	local content_files_legacy
	content_files_legacy=$(context_value "ARCHIVE_${content_id}_FILES")
	if [ -z "$content_files_legacy" ]; then
		## Return early if no files list is set.
		return 0
	fi

	if compatibility_level_is_at_least '2.18'; then
		warning_deprecated_variable \
			"ARCHIVE_${content_id}_FILES" \
			"CONTENT_${content_id}_FILES"
	fi

	# Legacy variable could use spaces as a delimiter,
	# line breaks are expected instead.
	local file_pattern
	for file_pattern in $content_files_legacy; do
		printf '%s\n' "$file_pattern"
	done
}

content_inclusion_default_game_data_legacy() {
	local package
	package="$1"

	local package_suffix files_list
	package_suffix="${package#PKG_}"
	files_list=$(context_name "ARCHIVE_GAME_${package_suffix}_FILES")
	if [ -z "$files_list" ]; then
		## Return early if no files list is set.
		return 0
	fi

	local target_directory
	target_directory=$(path_game_data)
	content_inclusion "GAME_${package_suffix}" "$package" "$target_directory"
	local index
	for index in $(seq 0 9); do
		files_list=$(context_name "ARCHIVE_GAME${index}_${package_suffix}_FILES")
		if [ -z "$files_list" ]; then
			## Stop looping at the first unset files list.
			return 0
		fi
		content_inclusion "GAME${index}_${package_suffix}" "$package" "$target_directory"
	done
}

content_inclusion_default_game_documentation_legacy() {
	local package
	package="$1"

	local package_suffix files_list
	package_suffix="${package#PKG_}"
	files_list=$(context_name "ARCHIVE_DOC_${package_suffix}_FILES")
	if [ -z "$files_list" ]; then
		## Return early if no files list is set.
		return 0
	fi

	local target_directory
	target_directory=$(path_game_data)
	content_inclusion "DOC_${package_suffix}" "$package" "$target_directory"
	local index
	for index in $(seq 0 9); do
		files_list=$(context_name "ARCHIVE_DOC${index}_${package_suffix}_FILES")
		if [ -z "$files_list" ]; then
			## Stop looping at the first unset files list.
			return 0
		fi
		content_inclusion "DOC${index}_${package_suffix}" "$package" "$target_directory"
	done
}

# Keep compatibility with 2.17 and older

prepare_package_layout() {
	if compatibility_level_is_at_least '2.18'; then
		warning_deprecated_function 'prepare_package_layout' 'content_inclusion_default'
	fi

	content_inclusion_default
}

# Keep compatibility with 2.16 and older

icons_get_from_package() {
	if compatibility_level_is_at_least '2.28'; then
		warning_deprecated_function 'icons_get_from_package' 'content_inclusion_icons'
	elif compatibility_level_is_at_least '2.17'; then
		warning_deprecated_function 'icons_get_from_package' 'icons_inclusion'
	fi

	local package package_path path_game_data icon_source_directory_legacy
	package=$(current_package)
	package_path=$(package_path "$package")
	path_game_data=$(path_game_data)
	icon_source_directory_legacy="${package_path}${path_game_data}"

	# Get the archive inner path, falling back on a default value.
	local content_path icon_source_directory_new
	if [ -z "${CONTENT_PATH_DEFAULT:-}" ]; then
		local CONTENT_PATH_DEFAULT
		CONTENT_PATH_DEFAULT='.'
	fi
	content_path=$(content_path_default)
	icon_source_directory_new="${PLAYIT_WORKDIR}/gamedata/${content_path}"

	local application
	for application in "$@"; do
		local application_icons_list
		application_icons_list=$(application_icons_list "$application")
		if [ -z "$application_icons_list" ]; then
			continue
		fi
		local icon
		for icon in $application_icons_list; do
			local icon_path icon_full_path_legacy icon_full_path_new
			icon_path=$(icon_path "$icon")
			## Compute icon legacy path.
			icon_full_path_legacy="${icon_source_directory_legacy}/${icon_path}"
			## Compute icon expected path for new function.
			icon_full_path_new="${icon_source_directory_new}/${icon_path}"
			## Set compatibility link.
			icon_full_path_legacy_canonical=$(realpath --canonicalize-missing "$icon_full_path_legacy")
			icon_full_path_new_canonical=$(realpath --canonicalize-missing "$icon_full_path_new")
			if [ "$icon_full_path_legacy_canonical" != "$icon_full_path_new_canonical" ]; then
				mkdir --parents "$(dirname "$icon_full_path_new")"
				ln --symbolic "$icon_full_path_legacy" "$icon_full_path_new"
			fi
			## Call new function.
			icons_inclusion_single_icon "$package" "$application" "$icon"
			## Remove compatibility link.
			if [ "$icon_full_path_legacy_canonical" != "$icon_full_path_new_canonical" ]; then
				rm "$icon_full_path_new"
				## Do not try to delete "…/gamedata/.", rmdir would fail with "Invalid argument".
				if [ "$content_path" != '.' ]; then
					rmdir --parents --ignore-fail-on-non-empty "$(dirname "$icon_full_path_new")"
				fi
			fi
		done
	done
}

persistent_list_directories_legacy() {
	set +o noglob
	if [ -n "${CONFIG_DIRS:-}" ]; then
		if compatibility_level_is_at_least '2.17'; then
			warning_deprecated_variable 'CONFIG_DIRS' 'USER_PERSISTENT_DIRECTORIES'
		fi
		local legacy_directory
		for legacy_directory in $CONFIG_DIRS; do
			printf '%s\n' "$legacy_directory"
		done
	fi
	if [ -n "${DATA_DIRS:-}" ]; then
		if compatibility_level_is_at_least '2.17'; then
			warning_deprecated_variable 'DATA_DIRS' 'USER_PERSISTENT_DIRECTORIES'
		fi
		local legacy_directory
		for legacy_directory in $DATA_DIRS; do
			printf '%s\n' "$legacy_directory"
		done
	fi
	set -o noglob
}

persistent_list_files_legacy() {
	set +o noglob
	if [ -n "${CONFIG_FILES:-}" ]; then
		if compatibility_level_is_at_least '2.17'; then
			warning_deprecated_variable 'CONFIG_FILES' 'USER_PERSISTENT_FILES'
		fi
		local legacy_file
		for legacy_file in $CONFIG_FILES; do
			printf '%s\n' "$legacy_file"
		done
	fi
	if [ -n "${DATA_FILES:-}" ]; then
		if compatibility_level_is_at_least '2.17'; then
			warning_deprecated_variable 'DATA_FILES' 'USER_PERSISTENT_FILES'
		fi
		local legacy_file
		for legacy_file in $DATA_FILES; do
			printf '%s\n' "$legacy_file"
		done
	fi
	set -o noglob
}

# Keep compatibility with 2.15 and older

extract_data_from() {
	if compatibility_level_is_at_least '2.16'; then
		warning_deprecated_function 'extract_data_from' 'archive_extraction'
	fi

	local archive archive_path_from_environment archive_path_from_environment_real
	archive=$(current_archive)
	archive_path_from_environment=$(archive_path "$archive")
	archive_path_from_environment_real=$(realpath --canonicalize-existing "$archive_path_from_environment")

	local archive_path_from_parameters archive_path_from_parameters_real
	archive_path_from_parameters="$1"
	archive_path_from_parameters_real=$(realpath --canonicalize-existing "$archive_path_from_parameters")

	if [ "${archive_path_from_environment_real:-}" != "${archive_path_from_parameters_real:-}" ]; then
		local messages_language message game_name
		game_name=$(game_name)
		messages_language=$(messages_language)
		case "$messages_language" in
			('fr')
				message='La prise en charge de "%s" utilise du code obsolète qui nʼest plus fonctionnel.\n'
				message="$message"'La fonction ayant déclenché cette erreur est : %s\n'
				message="$message"'Merci de signaler cette erreur sur notre système de suivi : %s\n'
			;;
			('en'|*)
				message='Support for "%s" is relying on obsolete code that no longer works.\n'
				message="$message"'The function that triggered this error is: %s\n'
				message="$message"'Please report this error on our issues tracker: %s\n'
			;;
		esac
		print_message 'error' "$message" \
			"$game_name" \
			'extract_data_from' \
			"$PLAYIT_GAMES_BUG_TRACKER_URL"
		return 1
	fi

	archive_extraction "$archive"
}

if [ "$(basename "$0")" != 'libplayit2.sh' ] && [ -z "$LIB_ONLY" ]; then

	# Exit immediately on error
	set -o errexit

	# Exit if the current process has been spawned by the root user
	## This check can be skipped by setting the following environment variable:
	## PLAYIT_OPTION_RUN_AS_ROOT=1
	if \
		[ "${PLAYIT_OPTION_RUN_AS_ROOT:-0}" -eq 0 ] && \
		check_is_running_as_root
	then
		error_run_as_root
		exit 1
	fi

	# Error out (and exit) when trying to expand an unset variable
	## Only for game scripts targeting ./play.it ≥ 2.23
	if compatibility_level_is_at_least '2.23'; then
		set -o nounset
	fi

	# Set input field separator to default value (space, tab, newline)
	unset IFS

	# Force umask to ensure all paths are created with correct permissions
	umask 0022

	# Check early if ./play.it has been called in games listing mode,
	# since most of the initialization steps are not required in this mode.

	option_list_supported_games=$(option_value 'list-supported-games')
	if [ "${option_list_supported_games:-0}" -eq 1 ]; then
		## Set the default package, used by the context system when no explicit package context is set.
		## It is required by the context system, but is usually set later, when the current archive has been set already.
		packages_list=$(packages_list)
		default_package=$(printf '%s' "$packages_list" | head --lines=1)
		set_default_package "$default_package"
		unset packages_list default_package

		games_list_supported
		exit 0
	fi
	unset option_list_supported_games

	# Unset variables that we do not want to import from the user environment

	unset SOURCE_ARCHIVE_PATH
	unset PLAYIT_WORKDIR

	# Set URLs for error messages

	PLAYIT_GAMES_BUG_TRACKER_URL='https://forge.dotslashplay.it/play.it/games/issues'
	PLAYIT_BUG_TRACKER_URL='https://forge.dotslashplay.it/play.it/scripts/issues'

	# Check the current library version against the expected compatibility level

	COMPATIBILITY_LEVEL=$(compatibility_level)
	COMPATIBILITY_LEVEL_MAJOR=$(printf '%s' "$COMPATIBILITY_LEVEL" | cut --delimiter='.' --fields=1)
	LIBRARY_VERSION_MAJOR=$(printf '%s' "$LIBRARY_VERSION" | cut --delimiter='.' --fields=1)
	if [ "$LIBRARY_VERSION_MAJOR" -lt "$COMPATIBILITY_LEVEL_MAJOR" ]; then
		error_incompatible_versions
		exit 1
	elif [ "$LIBRARY_VERSION_MAJOR" -eq "$COMPATIBILITY_LEVEL_MAJOR" ]; then
		COMPATIBILITY_LEVEL_MINOR=$(printf '%s' "$COMPATIBILITY_LEVEL" | cut --delimiter='.' --fields=2)
		LIBRARY_VERSION_MINOR=$(printf '%s' "$LIBRARY_VERSION" | cut --delimiter='.' --fields=2)
		if [ "$LIBRARY_VERSION_MINOR" -lt "$COMPATIBILITY_LEVEL_MINOR" ]; then
			error_incompatible_versions
			exit 1
		fi
	fi
	unset \
		COMPATIBILITY_LEVEL \
		COMPATIBILITY_LEVEL_MAJOR COMPATIBILITY_LEVEL_MINOR \
		LIBRARY_VERSION_MAJOR LIBRARY_VERSION_MINOR

	# Set options

	## Set hardcoded defaults
	options_init_default

	## Load defaults from the configuration file
	config_file_path=$(find_configuration_file "$@")
	load_configuration_file "$config_file_path"

	## Set options from the command-line
	parse_arguments "$@"

	# Display the help message,
	# if called with --help.

	option_help=$(option_value 'help')
	if [ "$option_help" -eq 1 ]; then
		help
		exit 0
	fi
	unset option_help

	# Display the version string,
	# if called with --version.

	option_version=$(option_value 'version')
	if [ "$option_version" -eq 1 ]; then
		printf '%s\n' "$LIBRARY_VERSION"
		exit 0
	fi
	unset option_version

	# If called with --list-supported-games,
	# print the list of games supported by the current game script,
	# then exit early.

	option_list_supported_games=$(option_value 'list-supported-games')
	if [ "$option_list_supported_games" -eq 1 ]; then
		games_list_supported
		exit 0
	fi
	unset option_list_supported_games

	# Check the validity of all options

	options_validity_check
	options_compatibility_check

	# Set the base archive that is going to be used.

	archive_initialize_base

	# Set the default package, used by the context system when no explicit package context is set

	packages_list=$(packages_list)
	default_package=$(printf '%s' "$packages_list" | head --lines=1)
	set_default_package "$default_package"
	unset packages_list default_package

	# Set the path to the temporary directory used by ./play.it.
	# This can not be set earlier, because GAME_ID (required to compute this path) might be unset before the call to archive_initialize_base.

	set_temp_directories

	# Display the path to the game script,
	# if called with --show-game-script.

	option_show_game_script=$(option_value 'show-game-script')
	if [ "$option_show_game_script" -eq 1 ]; then
		printf '%s\n' "$(realpath "$0")"
		## Delete temporary files
		rm --force --recursive "${PLAYIT_WORKDIR:-}"
		exit 0
	fi
	unset option_show_game_script

	# If called with --list-packages,
	# print the list of packages that would be generated from the given archive
	# then exit early.

	option_list_packages=$(option_value 'list-packages')
	if [ "$option_list_packages" -eq 1 ]; then
		archive=$(current_archive)
		packages_print_list "$archive"
		## Delete temporary files
		rm --force --recursive "${PLAYIT_WORKDIR:-}"
		exit 0
	fi
	unset option_list_packages

	# If called with --list-requirements,
	# print the list of commands required to run the current game script
	# then exit early.

	option_list_requirements=$(option_value 'list-requirements')
	if [ "$option_list_requirements" -eq 1 ]; then
		requirements_list
		## Delete temporary files
		rm --force --recursive "${PLAYIT_WORKDIR:-}"
		exit 0
	fi
	unset option_list_requirements

	# Check the list of packages that would be built,
	# exit early if all are already there.

	option_overwrite=$(option_value 'overwrite')
	if [ "$option_overwrite" -eq 0 ]; then
		option_output_dir=$(option_value 'output-dir')
		archive=$(current_archive)
		generated_packages_list=$(packages_print_list "$archive")
		missing_package=0
		while read -r generated_package; do
			if [ ! -e "${option_output_dir}/${generated_package}" ]; then
				missing_package=1
				break
			fi
		done <<- EOL
		$(printf '%s' "$generated_packages_list")
		EOL
		if [ "$missing_package" -eq 0 ]; then
			info_all_packages_already_built
			## Delete temporary files
			rm --force --recursive "${PLAYIT_WORKDIR:-}"
			exit 0
		fi
		unset option_output_dir generated_packages_list generated_package missing_package
	fi
	unset option_overwrite

	# Check the presence of required tools

	{
		check_deps
		check_deps_status=$?
	} || true
	if [ $check_deps_status -ne 0 ]; then
		## Delete temporary files
		rm --force --recursive "${PLAYIT_WORKDIR:-}"
		exit 1
	fi
	unset check_deps_status
	archive=$(current_archive)
	{
		archive_dependencies_check "$archive"
		archive_dependencies_check_status=$?
	} || true
	if [ $archive_dependencies_check_status -ne 0 ]; then
		## Delete temporary files
		rm --force --recursive "${PLAYIT_WORKDIR:-}"
		exit 1
	fi
	unset archive archive_dependencies_check_status

	# Check for the presence of extra required archives

	{
		archives_required_extra_presence_check
		archives_required_extra_presence_check_status=$?
	} || true
	if [ $archives_required_extra_presence_check_status -ne 0 ]; then
		## Delete temporary files
		rm --force --recursive "${PLAYIT_WORKDIR:-}"
		exit 1
	fi
	unset archives_required_extra_presence_check_status

	# Check for the presence of extra optional archives

	archives_optional_extra_presence_check

	# Check the archives integrity

	{
		archives_integrity_check
		archives_integrity_check_status=$?
	} || true
	if [ $archives_integrity_check_status -ne 0 ]; then
		## Delete temporary files
		rm --force --recursive "${PLAYIT_WORKDIR:-}"
		exit 1
	fi
	unset archives_integrity_check_status
fi

