# Copyright (c) 2022-2025 Thomas Frohwein
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

package IndieRunner::Engine::Mono;

=head1 NAME

IndieRunner::Engine::Mono - Mono engine module

=head1 DESCRIPTION

Module to set up and launch games made with Mono. There are games that use just this engine module, but it also serves as the baseline for other Mono-based engines.

=head1 METHODS

=over 8

=cut

use v5.36;
use version 0.77; our $VERSION = version->declare('v0.0.1');

use parent 'IndieRunner::Engine';

use Carp;
use Readonly;

use IndieRunner::Engine::Mono::Dllmap;
use IndieRunner::Engine::Mono::Iomap;
use IndieRunner::Helpers qw( match_bin_file );

# OpenBSD:	/usr/local/bin/mono
# Gentoo:	/usr/bin/mono
Readonly my $MONO_BIN => 'mono';

Readonly my @MONO_GLOBS => (
	'I18N{,.*}.dll',
	'Microsoft.*.dll',
	'Mono.*.dll',
	'System{,.*}.dll',
	'libMonoPosixHelper.so{,.x86{,_64}}',
	'monoconfig',
	'monomachineconfig',
	'mscorlib.dll',
	);

Readonly my @MONO_GLOB_EXCLUDE => (	# literal (partial) match, not glob or RE
	'System.Data.SQLite.dll',	# SpaceChem
	'System.Data.HashFunction.Core.dll',	# Stardew Valley
	'System.Data.HashFunction.Interfaces.dll',	# Stardew Valley
	'System.Data.HashFunction.xxHash.dll',	# Stardew Valley
	);

Readonly my %QUIRKS_ARGS => {
	'-disableweb'	=> [ 'Hacknet.exe', ],
	'-noSound'	=> [ 'ScourgeBringer.exe', ],
	};

Readonly my %QUIRKS_ENV => {
	'MONO_FORCE_COMPAT=1'	=> [
		'Blueberry.exe',
		'Shenzhen.exe',
		'ThePit.exe',
		],
	};

=item get_mono_files($custom_suffix)

Collect and return an array of Mono files that are often bundled and interfere with execution of the native Mono binary.
C<$custom_suffix> is an optional argument, allowing to select files with a suffix like "_".

=cut

sub get_mono_files ( $custom_suffix = '' ) {
	my @mono_files;
	my @match;

	foreach my $g ( @MONO_GLOBS ) {
		@match = glob( $g . $custom_suffix );	# $custom_suffix: e.g. '_'
		next unless @match;

		if ( -f $match[0] ) {		# check that globbed files exist
			push( @mono_files, @match );
		}
	}

	return @mono_files;
}

=item get_assembly_version($assembly_file)

Takes a mono assembly as the argument, and returns the assembly version as returned from L<monodis(1)>.

=cut

sub get_assembly_version ( $assembly_file ) {
        my $monodis_info = qx( monodis --assembly $assembly_file );
        if ( $monodis_info =~ /\nVersion:\s+([0-9\.]+)/ ) {
	        return $1;
        }
        else {
		return '';
        }
}

=item get_bin()

Return the Mono binary.

=cut

sub get_bin ( $self ) {
	return $MONO_BIN;
}

=item setup()

Removes Mono-related libraries and config files that interfere with execution of the system Mono binary if necessary.
Also performs the replacement for L<mono(1)>'s previous MONO_IOMAP functionality.

=cut

sub setup ( $self ) {
	$self->SUPER::setup();

	if ( $$self{ ir_obj }->get_use_rigg ) {
		# restore any system Mono assemblies
		foreach my $f ( get_mono_files('_') ) {
			$$self{ mode_obj }->restore( $f );
		}
	}
	else {
		# remove system Mono assemblies, except @MONO_GLOB_EXCLUDE
		foreach my $f ( get_mono_files() ) {
			$$self{ mode_obj }->remove( $f )
				unless grep { /\Q$f\E/ } @MONO_GLOB_EXCLUDE;
		}
	}

	if ( $$self{ ir_obj }->get_use_rigg ) {
		# restore any config files
		foreach my $c ( glob '*.config_' ) {
			$$self{ mode_obj }->restore( $c );
		}
	}
	else {
		# remove config files that do dllmap
		foreach my $c ( glob '*.config' ) {
			$$self{ mode_obj }->remove( $c )
				if ( match_bin_file( 'dllmap', $c ) );
		}
	}

	# replacement for mono's lost MONO_IOMAP
	my %iomaps = IndieRunner::Engine::Mono::Iomap::iomap_symlink();
	while ( ( my $newfile, my $oldfile ) = each ( %iomaps ) ) {
		$$self{ mode_obj }->insert( $oldfile, $newfile );
	}

	# XXX: for 'SSGame.exe': mkdir -p ~/.local/share/SSDD
}


=item get_env_ref()

Build the array for L<mono(1)> execution, consisting of LD_LIBRARY_PATH, MONO_PATH, and SDL_PLATFORM.

=cut

sub get_env_ref ( $self ) {
	my @ld_library_path = (
				'/usr/local/lib',
				'/usr/X11R6/lib',
				'/usr/local/lib/steamworks-nosteam',
			      );
	my @mono_path = (
			  '/usr/local/share/FNA',
			  '/usr/local/lib/steamworks-nosteam',
			);
	my @env = (
		    'SDL_PLATFORM=Linux',
		  );
	if ( not $$self{ ir_obj }->get_use_rigg ) {
		push @env,
			( 'LD_LIBRARY_PATH=' . join( ':', @ld_library_path ),
			  'MONO_CONFIG=' . IndieRunner::Engine::Mono::Dllmap::get_dllmap_target(),
			  'MONO_PATH=' . join( ':', @mono_path ),
			);
	}

	# quirks
	foreach my $k ( keys %QUIRKS_ENV ) {
		foreach my $l ( @{ $QUIRKS_ENV{ $k } } ) {
			if ( -e $l ) {
				push @env, $k;
				last;
			}
		}
	}

	return \@env;
}

=item get_args_ref()

Build the arguments array, including heuristic for the .exe assembly and the game name.

=cut

sub get_args_ref ( $self ) {
	# heuristic to figure out the binary name from game_name
	my @args;
	my $game_file;
	my $first_word = (split( /[[:blank:]]/, $$self{ game_name } ) )[0];
	if ( -f $$self{ game_name } . '.exe' ) {
		$game_file = $$self{ game_name } . '.exe';
	}
	elsif ( -f 'PanzerPaladin.exe' ) {	# XXX: make this process a list
		$game_file = 'PanzerPaladin.exe';
	}
	elsif ( -f ( $$self{ game_name } =~ s/[[:blank:]]//gr ) . '.exe' ) {
		$game_file = ( $$self{ game_name } =~ s/[[:blank:]]//gr ) . '.exe';
	}
	else {
		if ( length( $first_word ) > 3 and my @candidate_files = glob '*' ) {
			if ( @candidate_files = grep { /^\Q$first_word\E.*\.exe$/i }
						@candidate_files ) {
				# heuristic: return shortest file
				my $shortest = $candidate_files[0];
				foreach my $f ( @candidate_files ) {
					$shortest = $f if length( $f ) < length( $shortest );
				}
				$game_file = $shortest;
			}

		}
	}
	($game_file) = glob '*.exe' unless $game_file;	# just pick the first .exe

	confess "Failed to identify game file for Mono, based on detected game name \"$$self{ game_name }\". Aborting." if ( not $game_file );
	push @args, $game_file;

	# quirks
	foreach my $k ( keys %QUIRKS_ARGS ) {
		foreach my $l ( @{ $QUIRKS_ARGS{ $k } } ) {
			if ( -e $l ) {
				push @args, $k;
				last;
			}
		}
	}

	return \@args;
}

1;

__END__

=back

=head1 SEE ALSO

L<IndieRunner::Engine>,
L<IndieRunner::Engine::FNA>,
L<IndieRunner::Engine::MonoGame>,
L<IndieRunner::Engine::XNA>,
L<IndieRunner::Engine::Mono::Iomap>.

=head1 AUTHOR

Thomas Frohwein E<lt>thfr@cpan.orgE<gt>.

=head1 COPYRIGHT

Copyright 2022-2025 by Thomas Frohwein E<lt>thfr@cpan.orgE<gt>.

This program is free software; you can redistribute it and/or modify it under the ISC license.
