#!/bin/sh

##################
# fnaify 1.2
#
# created 2017-12-27 by Thomas Frohwein (thfr)
# portability fixes by Mariusz Zaborski (oshogbo)
#################
# Script to get FNA-based games ready to run on BSD
#
# FNA is a reimplementation of the Microsoft XNA Game Studio 4.0 Refresh libraries.
# Thanks to the great work by Ethan Lee (flibitijibibo) games using FNA are
# highly portable and can even run on OpenBSD.
# Please refer to https://fna-xna.github.io/ for more information about FNA
##################
# License: ISC license (see LICENSE in same directory)
##################
# Requirements:
#
# - SDL2 library that identifies as 'Linux' rather than 'OpenBSD'
#   (needs to be compiled this way; patch to recognize OpenBSD in
#   progress upstream)
# - mono
# - some additional libraries, like mojoshader, theorafile, FAudio, etc,
#   depending on the particular game
##########
# KNOWN BUGS:
#
###########
# TODO:
# - with Bastion, check for openal even if not in lib folders or .config files
# - check assembly version for Bastion?
# - add warning about non-OpenAL Bastion version
#	incompatible GOG version: Hash Algoritm: 0x00008004
#		Version:       1.0.0.0
# - use ':' as separator for $depdir instead of newline
#########

#########
# Argument parsing and Usage
#########

USAGE="Usage: `basename $0` [-vh] [-d <dependency dirs>] [<gamedir>]\n\
\n\
-d: replace directories for finding dependencies\n\
    NOTE: separate multiple entries with ':' (default:
    /usr/local/share/fnaify-libs:/usr/local/lib:/usr/X11R6/lib)\n\
-h: print usage information\n\
-v: verbose output\n\
\n\
<gamedir> is optional and defaults to \$PWD if not specified.\
"

gamedir="$PWD"

while [ $# -gt 0 ]; do
	case "$1" in
		-d) depdir="$2"; shift;;
		-h) echo "$USAGE"; exit 0;;
		-v) FNAIFY_DEBUG=1;;
		-*) echo "$USAGE"; exit 1;;
		*) gamedir=$(readlink -f "$1");;
	esac
	shift
done

cd "$gamedir"

# /usr/X11R6/lib is location of libfreetype.so on OpenBSD
# FIXME: $gamedir in here may be an issue if there are Linux libs in there...

if [ -z $depdir ]; then
depdir="/usr/local/lib:/usr/X11R6/lib:$gamedir"
fi

if [ -d "/usr/local/share/fnaify-libs" ]; then
depdir="/usr/local/share/fnaify-libs:$depdir"
fi

#########
# VARIABLE AND FUNCTION DEFINITIONS
#########

SAVEIFS=$IFS
fna_warning=0

debug_echo()
{
	if [ -z "$FNAIFY_DEBUG" ]; then
		return
	fi

	if [ "${1}" = '-n' ]; then
		printf "$2"
	else
		printf "$1\n"
	fi
}

# FIXME: replace with something using wc -c
debug_printdash()
{
	if [ -z "$FNAIFY_DEBUG" ]; then
		return
	fi

	printdash $*
}

# printdash: print $1 number of dashes in one line, followed by newline
# FIXME: replace with something using wc -c
printdash()
{
	c=1
	while [ $c -le $1 ]
	do
		echo -n -
		# $((...)) for arithmetic substitution
		c=$((c+1))
	done
	echo ""
}

# inarray: check if $1 is in array $2 (with simple grep)
inarray()
{
	firstarg="$1"
	shift 1
	echo "$*" | grep -q "$firstarg"
}

# validlib: returns 0 unless $1 is in ignoredarray, then returns 1
# FIXME: this won't be able to deal with whitespace at the moment
validlib()
{
	libnam="$(trunklibnam $1)"
	if [ -n "$(echo \"$ignoredarray\" | grep $libnam)" ]; then
		return 1
	elif [ -n "$(echo \"$libnam\" | grep '\.dll[ \t]*')" ]; then
		return 1
	else
		return 0
	fi
}

# trunklibnam: truncate the name of the library (and remove '-2.0') to match OpenBSD
trunklibnam()
{
	libnam="$1"
	# remove trailing version numbers and dots
	libnam="$(echo "$libnam" | sed -n -E "s/[\.0-9]*$//p")"
	libnam="$(echo "$libnam" | sed -E "s/(libSDL2[^-]*)-2\.0(\.so.*)/\1\2/")"
	echo "$libnam"
}

libraryname()
{
        version="$1"

	debug_echo ""
	if [ ! -e "$gamedir/$version" ]; then
		debug_echo "Couldn't find library directory $gamedir/$version"
	else
		debug_echo "Entering library directory $gamedir/$version"
		for file in $(ls "$gamedir/$version"); do
			# sort out libs (e.g. steam) that need to be ignored
			validlib $file
			if [ $? -eq 1 ]; then
				debug_echo "\tignored file: $file"
				continue
			fi
			debug_echo -n "\tfound library file: $file"
			file=$(trunklibnam "$file")
			debug_echo -n " -> $file"
			inarray $file ${needlibarray}
			if [ $? -eq 0 ]; then
				debug_echo " - already in array"
			elif [ $? -eq 1 ]; then
				needlibarray="$needlibarray$file "
				debug_echo ""
			else
				echo "\n\t - ERROR: inarray returned with unexpected error"
				echo ""
				exit 1
			fi
		done
		debug_echo "Done with library directory $gamedir/$version"
	fi
}

#ignoredarray: array of lib names to ignore for the configuration checking
ignoredarray="libCSteamworks.so
libsteam_api.so
libSteamworksNative.so
libcef.so
libXNAWebRenderer.so
libSteamWrapper.so
libParisSteam.so
steamwrapper.so
libCommunityExpressSW.so
libXNAFileDialog.so
libfmod.so
libfmodstudio.so
libtiny_jpeg.so
libGalaxy64.so
libGalaxyCSharpGlue.so
libGalaxy.so"

#monofilearray: array of mono files that need to be removed from the game folder
monofilearray="System.dll
Mono.Posix.dll
Mono.Security.dll
System.Configuration.dll
System.Core.dll
System.Data.dll
System.Xml.dll
System.Security.dll
System.Runtime.Serialization.dll
mscorlib.dll
System.Drawing.dll
monoconfig
System.Xml.Linq.dll
WindowsBase.dll
monomachineconfig
I18N.CJK.dll
I18N.MidEast.dll
I18N.Other.dll
I18N.Rare.dll
I18N.West.dll
I18N.dll
Microsoft.CSharp.dll
Mono.CSharp.dll
System.ServiceModel.dll"

#needlibarray[*] is the array that will hold the names of needed libraries
# define here so that special cases (Atom Zombie Smasher, MidBoss) can
# add to this early
needlibarray=""

#######################################################################

debug_echo "Dependency directory string: $depdir"

#######
# Identify mono and FNA/MonoGame framework files first, warn/break if errors
#######

# if FNAIFY_MONO isn't set, default to just 'mono', assuming it's in the path
if [ -z "$FNAIFY_MONO" ]; then
	FNAIFY_MONO="mono"
fi

# check version of framework library (FNA.dll or MonoGame.Framework.dll)
debug_echo "Checking mono and version of framework library..."
"$FNAIFY_MONO" --version 2>/dev/null >/dev/null
if [ "$?" -gt 0 ]; then
	echo "error calling mono - aborting... Please make sure that mono is in path or set it in FNAIFY_MONO environment variable"
	exit 1
fi

# does any of the framework files exist in $gamedir?
if [ -e "$gamedir/FNA.dll" ]; then
	debug_echo "\tFound FNA.dll"
elif [ -e "$gamedir/MonoGame.Framework.dll" ]; then
	debug_echo "\tFound MonoGame.Framework.dll"
else
	# Atom Zombie Smasher is the only compatible game I know of that
	# doesn't have any of these framework files...
	ls "$gamedir" | grep -iqm 1 "atomzombiesmasher"
	if [ $? -eq 0 ]; then
		debug_echo "Atom Zombie Smasher requires libatomstb.so instead of FNA.dll or MonoGame.Framework.dll"
	else
		echo "ERROR: Could not find framework library (FNA.dll or MonoGame.Framework.dll) in $gamedir"
		exit 1
	fi
fi

# check if monodis can be called
monodis 2>/dev/null
if [ "$?" -eq 127 ]; then
	echo "WARNING: Can't run monodis; unable to check for compatibility of FNA/MonoGame framewowk file."
else
	# The following code assumes that only one of FNA.dll and MonoGame.Framework.dll is present
	if [ -e "$gamedir/FNA.dll" ]; then
		fnaversion=`monodis --assembly "$gamedir/FNA.dll" | grep "Version" | tr -d [:alpha:] | tr -d " " | tr -d \:`
		fnamajor=`echo "$fnaversion" | sed -n -E "s/\..*//p"`
		fnaminor=`echo "$fnaversion" | sed -n -E "s/[0-9]+\.([0-9]+)\.[0-9]+\.[0-9]+/\1/p"`
		debug_echo -n "\tFNA.dll version $fnaversion, "
		debug_echo -n "major: $fnamajor, "
		debug_echo "minor: $fnaminor"
		if [ $fnamajor -lt 16 ] || ( [ $fnamajor -eq 16 ] && [ $fnaminor -lt 5 ] )
		then
			fna_warning=1
		fi
	elif [ -e "$gamedir/MonoGame.Framework.dll" ]; then
		mgversion=`monodis --assembly "$gamedir/MonoGame.Framework.dll" | grep "Version" | tr -d [:alpha:] | tr -d " " | tr -d \:`
		debug_echo "\tMonoGame.Framework.dll version $mgversion"
	fi
fi

#######
# MAIN SCRIPT
#######

echo ""

# scriptfilearray: array of files that are identified as a possible launchscript
debug_echo "Trying to identify the launch script file automatically..."

scount=0
scriptfilearray=""
sfile=""
IFS="
"
for sfile in $(cd "$gamedir" && find . -maxdepth 1 -type f | grep -Ev "\.[^/]|mono.*config$" | cut -f 2 -d "/"); do
	debug_echo "\tfound candidate for launch script file: $sfile"
	if [ -z "${scriptfilearray}" ]; then
		scriptfilearray="$sfile"
	else
		scriptfilearray="$scriptfilearray $sfile"
	fi
	scount=$((scount + 1))
done
IFS=$SAVEIFS

if [ $scount -gt 1 ]; then
	i=1
	for fsc in $scriptfilearray; do
		echo "$i: ${fsc}"
		i=$((i + 1))
	done
	input_script=0
	# "\(Expression\) - Parentheses for grouping must be escaped with a backslash \"
	# -- https://en.wikipedia.org/wiki/Test_(Unix)
	# check for range of numbers, number of characters, and presence of non-digit characters
	while [ \( $(echo "$input_script" | wc -c) -gt 2 \) -o \( -z "$(echo "$input_script" | grep [1-9])" \) -o \( $input_script -gt $((i - 1)) \) ]; do
		echo -n "Enter number of the file to use as the launch script: "
		read input_script
	done
	# http://www.etalabs.net/sh_tricks.html - Working with arrays
	set -- ${scriptfilearray}
	input_script="\$$input_script"
	# hmm, eval... simplest way to pick an array element it seems.
	# http://mywiki.wooledge.org/BashFAQ/048
	# NOTE: this only works for up to 9 candidate files
	scriptfile=$(eval echo "$input_script")
elif [ $scount -eq 1 ]; then
	scriptfile="${scriptfilearray}"
else
	echo "WARNING: failed to identify a pre-existing launch script."
	echo -n "Please enter a name for the launch script to be created: "
	read scriptfile
fi

debug_echo "Identified the following file as the launch script: $scriptfile"
debug_echo ""

# path and file variable definitions
fullscriptpath="$gamedir/$scriptfile"
IFS="
"
exefile=""
nexefile=0
for xfile in $(cd "$gamedir" && find . -maxdepth 1 -type f -iname "*.exe" | cut -f 2 -d "/"); do
	exefile="$exefile$xfile:"
	nexefile=$((nexefile + 1))
done
IFS=$SAVEIFS

# configfilesarray: array of files in gamedir ending in '.config'
configfilesarray=""
nconfigfilesarray=0
debug_echo "Identifying config files..."
IFS="
"
for cfile in $(ls "$gamedir" | grep "\.config$"); do
	debug_echo "\tfound config file: $cfile"
	configfilesarray="$configfilesarray$cfile "
	nconfigfilesarray=$((nconfigfilesarray + 1))
done
IFS=$SAVEIFS
debug_echo "Done identifying config files."
debug_echo ""

####
# identify required libraries
####
# - will check 3 sources: lib64, lib, and .config files
# - filenames not whitespace-safe, but should not be used in such files anyway
####

debug_echo "Identifying libraries required by the game..."
debug_printdash 45

#for MidBoss, add SDL2_image_compact to needlib
MidBoss=0
ls "$gamedir" | grep -iqm 1 "midboss"
if [ $? -eq 0 ]; then
	MidBoss=1
	needlibarray="${needlibarray}libSDL2_image_compact.so "
fi

# get library names from lib64 folder
libraryname "lib64"
libraryname "lib"

# get library names from .config files
debug_echo "Obtaining library names from the following config files"

# check that configfilesarray isn't empty
if [ $nconfigfilesarray -lt 1 ]; then
	debug_echo "No config files found."
else
	cfile=""	# empty the variable because it has been used before
	for cfile in $configfilesarray; do
		debug_echo "\t$cfile"
		linuxlines=$(grep "os\=\"linux" "$gamedir/$cfile")
		for libstring in $(echo "$linuxlines" | sed -n -E "s/.*target=\"([^\"]+).*/\1/p"); do
			# Fix where library name includes directory information
			# remove "./" at the beginning of librarynames
			libstring=$(echo "$libstring" | sed -E 's/^.\///')
			# remove directories at the start of lib name
			libstring=$(echo "$libstring" | sed -E 's/^.*\///')
			debug_echo -n "\t\tFound library string: $libstring"
			# sort out libs (e.g. steam) that need to be ignored
			validlib $libstring
			if [ $? -eq 1 ]; then
				debug_echo " - ignored"
				continue
			fi
			# truncate/fix SDL2 names{,s}
			libstring=$(trunklibnam "$libstring")
			debug_echo -n " -> $libstring"
			# check if libstring is already in needlibarray.
			# add to needlibarray only if not.
			inarray $libstring $needlibarray
			if [ $? -eq 0 ]; then
				debug_echo " - already in array"
			elif [ $? -eq 1 ]; then
				needlibarray="$needlibarray$libstring "
				debug_echo " - added to array"
			else
				echo "\n\t - ERROR: inarray returned with unexpected error\n"
				exit 1
			fi
		done
	done
	debug_echo "Done with identifying libraries in config files"
fi
debug_echo "Done with identification of needed libraries."

# Fix libpngXX.so filename
debug_echo -n "Fixing libpng filenames if present..."
needlibarray=$(echo "${needlibarray}" | sed -E "s/(libpng)..(\.so.*)/\1\2/")
debug_echo " done.\n"

# Check if the libraries are available on the system (/usr/local/lib).
# If not, break and inform user which libraries need to be installed.
echo -n "Checking installed libraries... "
debug_echo ""

# missinglibs[*] accumulates missing library names to inform user
missinglibs=""
for needlib in ${needlibarray}; do
	IFS=":"
	if $(ls $depdir | grep -q "$needlib")
	then
		IFS=$SAVEIFS
		debug_echo "\tfound library for: $needlib"
	else
		IFS=$SAVEIFS
		debug_echo "\tNot found: $needlib"
		missinglibs="$missinglibs$needlib "
	fi
done
echo "done."

echo -n "Result of configuration testing: "
if [ -n "${missinglibs}" ]; then
	echo "FAILED"
	printdash 39
	
	echo -n - -
	echo " Could not find the following libraries:\n
	\n\
	${missinglibs}"
	echo ""
	exit 1
fi
echo "SUCCESS"
debug_printdash 40

####
# replace all occurences of 'linux' in .config files (dllmap) with 'openbsd,freebsd,netbsd'
#    (this won't be needed after fna 18.01+ rolled out to all games... if this will ever happen...)

echo ""
echo "Adjusting config files for BSD..."

# create backup .linux files of the original .config files
debug_echo -n "\t(creating copy of original config files with suffix '.linux')... "
IFS="
"
for file in $(ls "$gamedir" | grep "\.config$"); do
	if [ ! -e "$gamedir/$file.linux" ]; then
		cp -p "$gamedir/$file" "$gamedir/$file.linux"
	fi
done
IFS=$SAVEIFS
debug_echo "done."

# replace all terms in *.config
IFS="
"
for file in $(ls "$gamedir" | grep "\.config$"); do
	# someone may be able to provide a more compact solution for this section
	# remove "./" at the beginning of any target="..."
	sed -i -E 's/target="\.\//target="/g' "$gamedir/$file"
	# remove directory lib{,64} at the start of any target="..."
	sed -i -E 's/target="lib(64)?\//target="/g' "$gamedir/$file"
	sed -i -E 's/os="linux/os="openbsd,freebsd,netbsd/g' "$gamedir/$file"
	# remove suffix numbers
	sed -i -E "s/\.so[\.0-9]*\"/.so\"/g" "$gamedir/$file"
	# fix SDL2 naming by removing the '-2.0'
	sed -i -E "s/-2\.0\.so/.so/g" "$gamedir/$file"
	# for MidBoss, replace SDL2_image with SDL2_image_compact
	if [ $MidBoss -eq 1 ]; then
		sed -i -E 's/libSDL2_image\./libSDL2_image_compact./g' "$gamedir/$file"
	fi
done
IFS=$SAVEIFS
debug_echo "Config files adjusted."

# Move interfering mono files out of the way
debug_echo "Moving some bundled dll files into linux-files subfolder... "
for file in $monofilearray; do
	if [ -e "$gamedir/$file" ]; then
		debug_echo "\tFound bundled mono file: $file"
		mkdir -p "$gamedir/linux-files"
		mv "$gamedir/$file" "$gamedir/linux-files/"
	fi
done
if [ $fna_warning -eq 1 ]; then
	echo "\tMoving FNA.dll out of the way because it is likely incompatible"
	mkdir -p "$gamedir/linux-files"
	mv "$gamedir/FNA.dll" "$gamedir/FNA.dll.config" \
		"$gamedir/FNA.dll.config.linux" "$gamedir/linux-files/"
fi
debug_echo " done."

# create wrapper script in the game folder, set correct .exe in script, and set to executable

echo ""
if [ $nexefile -gt 1 ]; then
	i=1
	while [ $i -le $nexefile ]; do
		echo "$i: $(echo $exefile | cut -f $i -d ':')"
		i=$((i + 1))
	done
	input_exe=0
	# "\(Expression\) - Parentheses for grouping must be escaped with a backslash \"
	# -- https://en.wikipedia.org/wiki/Test_(Unix)
	while [ \( $(echo "$input_exe" | wc -c) -gt 2 \) -o \( -z "$(echo "$input_exe" | grep [1-9])" \) -o \( $input_exe -gt $((i - 1)) \) ]; do
		echo -n "Enter number of .exe file to choose for wrapper script: "
		read input_exe
	done
	# http://www.etalabs.net/sh_tricks.html - Working with arrays
	#set -- ${exefile}
	#exefile="\$$input_exe"
	# hmm, eval... simplest way to pick an array element it seems.
	# http://mywiki.wooledge.org/BashFAQ/048
	# NOTE: this only works for up to 9 candidate files
	selectexe="$(echo $exefile | cut -f $input_exe -d ':')"
elif [ $nexefile -eq 1 ]; then
	selectexe="$exefile"
else
	echo "ERROR: no .exe file found\n"
	exit 1
fi
echo ""

# trim trailing whitespace from $selectexe
selectexe="$(echo "$selectexe" | sed -E "s/[ \t]$//")"

echo "Replacing launcher script with BSD version..."
# if not exists, make backup of original script for linux
debug_echo -n "\t(creating copy of original launcher script with suffix '.linux')... "
if [ ! -e "$fullscriptpath.linux" ]; then
	cp -p "$fullscriptpath" "$fullscriptpath.linux"
fi
debug_echo "done."

exe_flags=""
exe_env=""

if [ $fna_warning -eq 1 ]; then
	debug_echo "Setting MONO_PATH to find FNA.dll in /usr/local/share/fnaify-libs"
	exe_env="${exe_env}MONO_PATH=/usr/local/share/fnaify-libs "
fi

if $(ls "$gamedir" | grep -iqm 1 hacknet); then
	debug_echo "Setting flag '-disableweb' for HackNet"
	exe_flags="-disableweb"
fi

if $(ls "$gamedir" | grep -iqm 1 wizorb); then
	debug_echo "Setting 'MONO_IOMAP=all' for WizOrb"
	exe_env="${exe_env}MONO_IOMAP=all "
fi

# The content of the wrapper script
cat <<EOF > "$fullscriptpath"
#!/bin/sh

# fnaify wrapper script for launching FNA games on BSD

# Move to the game's location in case it was invoked from elsewhere
cd "\`dirname "\$0"\`"

# run mono with LD_LIBRARY_PATH to find native BSD libraries
LD_LIBRARY_PATH="$(echo "$depdir" | sed -E 's/:$//')" $exe_env exec "$FNAIFY_MONO" "$(echo "$selectexe" | tr -d '\:')" $exe_flags \$*
EOF

chmod +x "$fullscriptpath"

debug_echo "Launcher script replaced.\n"
debug_echo "SETUP COMPLETE"
debug_printdash 14
echo ""
echo "You should now be able to start the game by running:"
echo ""
echo "\$ ./$scriptfile"
echo ""

if $(ls "$gamedir" | grep -iqm 1 shuggy); then
	cat <<EOF
WARNING: version of FNA.dll is likely incompatible and has been moved from the
game directory to the linux-files subdirectory. This particular game has been
found to have bugs with the most recent FNA versions. FNA version 17.12 is
presumed to work with this game and can be obtained from upstream:

https://github.com/FNA-XNA/FNA/releases/tag/17.12

EOF
elif [ $fna_warning -eq 1 ]; then
	echo "WARNING: version of FNA.dll potentially incompatble!"
	echo "Original FNA files have been moved to linux-files subdirectory"
	echo "The launch script has been set up to run with a more recent FNA.dll"
	echo "located in /usr/local/share/fnaify-libs."
	echo ""
	echo "If no FNA.dll exists in the following path for dependencies, you may need"
	echo "to obtain a working FNA.dll version 16.5 or greater to run the game:"
	echo ""
	echo "$depdir"
	echo ""
fi
