Skip to content

Commit

Permalink
-Bundled the (now no longer) global variables for the path glob patte…
Browse files Browse the repository at this point in the history
…rns and the file

offset into a separate hash. Users wishing to tweak their contents will
now have to scroll to the bottom of the file. :(

-Added a routine that checks for commandline options without a
corresponding value.

-Feature Creep: Extended the script's functions to work for Shadowrun
Returns and Shadowrun Dragonfall. Updated the help text to reflect the
changes.
  • Loading branch information
Van-Ziegelstein committed Jun 2, 2018
1 parent a924cc5 commit 6f4809a
Showing 1 changed file with 120 additions and 96 deletions.
216 changes: 120 additions & 96 deletions shadow_tune.pl
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
#!/bin/perl


use strict;
use warnings;
use File::Copy;
use Fcntl qw( SEEK_SET SEEK_END );

#A hard coded offset including some safe space at which the script will start loading the content of the resources.assets file. Since the entire file is about 2G, slurping it whole would be excessive.
my $sound_meta_start = 2002710000;


#The predefined paths where the script searches for the Shadowrun Hong Kong install.
my @install_dirs = (

"~/.local/share/Steam/steamapps/common/Shadowrun Hong Kong",
"~/.steam/steam/SteamApps/common/Shadowrun Hong Kong",
"~/{Steam,Games,GOG}/{,Steam/,GOG/,Shadowrun/}Shadowrun Hong Kong",
"~/{steam,games,gog}/{,steam/,gog/,shadowrun/}Shadowrun Hong Kong",
"~/.wine{,32,64,_steam,_shadowrun}/drive_c/{GOG Games,Program Files/Steam/steamapps/common}/Shadowrun Hong Kong"

);


sub find_game_resources {

my $sr_resources;
my $op_params = $_[0];

die "Invalid game selection. Either of returns|dragonfall|hongkong must be specified.\n" unless $op_params->{edition} =~ s/^(returns|dragonfall|hongkong)$/\u$1/i;
$op_params->{edition} =~ s/Hongkong/Hong\ Kong/;

my $srhk_resources;

PATH_TRIAL: foreach my $path_expr (@install_dirs) {
PATH_TRIAL: foreach my $path_expr (@{$op_params->{install_dirs}}) {

foreach my $path (glob(qq/"$path_expr"/)) {

if (-d ($path .= "/SRHK_Data")) {
if ($path =~ /Shadowrun\s$op_params->{edition}/) {

$srhk_resources = $path;
$sr_resources = glob(qq|"$path/*_Data"|);
last PATH_TRIAL;

}
Expand All @@ -41,69 +29,71 @@ sub find_game_resources {

}

die "Unable to locate Shadowrun Hong Kong game assets.\n" unless $srhk_resources;
print "Found: $srhk_resources\n";
return $srhk_resources;
die "Unable to locate Shadowrun $op_params->{edition} game assets.\n" unless $sr_resources && -d $sr_resources;
print "Found: $sr_resources\n";
return $sr_resources;

}


sub asset_dump {

my $delimiter = qr/\x00*\x02\x00{3}\x0E\00{7}\x02\x00{3}/;
my $assets_content = $_[0];
my $verbose = $_[1];
my $op_params = $_[0];
my $assets_content = $_[1];
my $sound_meta_start = $_[2];

my @track_list;

print "Parsing resources.assets...\n";

while ($assets_content =~ /(HongKong-[\w-]+|TESTSTINGER)$delimiter/g) {
while ($assets_content =~ /([\w-]+)$delimiter/g) {

my($tracksize, $resS_offset) = unpack("V2", substr($assets_content, $+[0], 8));
my $size_offset = $sound_meta_start + $+[0];

print "\n$1\n", "-"x45, "\nSize: $tracksize\nSize data offset: $size_offset\nTrack resS offset: $resS_offset\n" if $verbose == 1;
print "\n$1\n", "-"x45, "\nSize: $tracksize\nSize data offset: $size_offset\nTrack resS offset: $resS_offset\n" if $op_params->{verbose} == 1;

push(@track_list, {"name" => $1, "size_offset" => $size_offset, "size" => $tracksize, "track_offset" => $resS_offset});

}

print "\n" if $verbose == 1;
print "\n" if $op_params->{verbose} == 1;
return @track_list;
}


sub resS_dump {

my $ogg_first_page = qr/OggS\x00\x02/;
my $resS_content = $_[0];
my $verbose = $_[1];
my $op_params = $_[0];
my $resS_content = $_[1];
my $track_num = 0;
my @offset_list;

print "Parsing resources.assets.reS replacment file...\n";
print "\n" if $verbose == 1;
print "\n" if $op_params->{verbose} == 1;

while ($resS_content =~ /$ogg_first_page/g) {

print "Track ", ++$track_num, " offset: $-[0]\n" if $verbose == 1;
print "Track ", ++$track_num, " offset: $-[0]\n" if $op_params->{verbose} == 1;
push(@offset_list, $-[0]);

}

print "\n" if $verbose == 1;
print "\n" if $op_params->{verbose} == 1;
return @offset_list;
}


sub asset_update {

my $current_tracklist = $_[0]->{"track_list"};
my $new_track_offsets = $_[0]->{"new_offsets"};
my $resS_end = $_[0]->{"resS_end"};
my $assets_file = $_[1];
my $verbose = $_[2];
my $op_params = $_[0];
my $current_tracklist = $_[1]->{"track_list"};
my $new_track_offsets = $_[1]->{"new_offsets"};
my $resS_end = $_[1]->{"resS_end"};
my $assets_file = $_[2];

die "Number of replacement offsets does not match original track number.\n" unless @{$new_track_offsets} == @{$current_tracklist};

print "Remapping offset and size values in resources.assets...\n";
Expand All @@ -115,45 +105,44 @@ sub asset_update {
$track->{"track_offset"} = shift(@{$new_track_offsets});
$track->{"size"} = $new_track_offsets->[0] - $track->{"track_offset"};

print "\n$track->{qq/name/}\n", "-"x45, "\nNew size: $track->{qq/size/}\nNew resS offset: $track->{qq/track_offset/}\n" if $verbose == 1;
print "\n$track->{qq/name/}\n", "-"x45, "\nNew size: $track->{qq/size/}\nNew resS offset: $track->{qq/track_offset/}\n" if $op_params->{verbose} == 1;

seek($assets_file, $track->{"size_offset"}, SEEK_SET);
print $assets_file pack("VV", $track->{"size"}, $track->{"track_offset"});

}

print "\n" if $verbose == 1;
print "\n" if $op_params->{verbose} == 1;

}


sub swap_music_files {

my $srhk_resources = $_[0];
my $replacement_resS_file = $_[1];
my $verbose = $_[2];
my $op_params = $_[0];
my $sound_meta_start = $op_params->{meta_offsets}{$op_params->{edition}};
my %offset_meta;


open(my $assets_file, "+<:raw", "$srhk_resources/resources.assets") or die "resources.assets file missing or access restricted.\n";
open(my $assets_file, "+<:raw", "$op_params->{sr_resources}/resources.assets") or die "resources.assets file missing or access restricted.\n";

seek($assets_file, $sound_meta_start, SEEK_SET);
my $assets_content = do { local $/ = undef; <$assets_file>; };
$offset_meta{"track_list"} = [ asset_dump($assets_content, $verbose) ];
open(my $new_resS, "<:raw", "$replacement_resS_file") or die "Unable to open resources.assets.resS replacment.\n";
$offset_meta{"track_list"} = [ asset_dump($op_params, $assets_content, $sound_meta_start) ];

open(my $new_resS, "<:raw", "$op_params->{new_resS_file}") or die "Unable to open resources.assets.resS replacment.\n";

my $new_resS_content = do { local $/ = undef; <$new_resS>; };
seek($new_resS, 0, SEEK_END);
$offset_meta{"resS_end"} = tell($new_resS);
close($new_resS);
$offset_meta{"new_offsets"} = [ resS_dump($new_resS_content, $verbose) ];
open(my $current_resS, ">", "$srhk_resources/resources.assets.resS") or die "Unable to update resources.assets.resS.\n";
$offset_meta{"new_offsets"} = [ resS_dump($op_params, $new_resS_content) ];

open(my $current_resS, ">", "$op_params->{sr_resources}/resources.assets.resS") or die "Unable to update resources.assets.resS.\n";
print $current_resS $new_resS_content;
close($current_resS);

asset_update(\%offset_meta, $assets_file, $verbose);
asset_update($op_params, \%offset_meta, $assets_file);

close($assets_file);

Expand All @@ -162,31 +151,28 @@ sub swap_music_files {

sub music_replace {

my $srhk_resources;
my $verbose = $_[1];
my $new_resS_file = $_[0];

die "You must give a valid path to a new resources.assets.reS file.\n" unless $new_resS_file && -s glob(qq/"$new_resS_file"/);

$srhk_resources = find_game_resources();

while (-e "$srhk_resources/resources.assets.resS.bak") {
my $op_params = $_[0];

die "You must give a valid path to a new resources.assets.reS file.\n" unless $op_params->{new_resS_file} && -s glob(qq/"$op_params->{new_resS_file}"/);

$op_params->{sr_resources} = find_game_resources($op_params);

print "A backup file for resources.assets.resS is already present. Are you sure you want to continue with the replacement? (y/n) ";
while (-e "$op_params->{sr_resources}/resources.assets.resS.bak") {

chomp(my $user_choice = <STDIN>);
print "A backup file for resources.assets.resS is already present. Are you sure you want to continue with the replacement? (y/n) ";
chomp(my $user_choice = <STDIN>);

last if $user_choice =~ /y/i;
exit 0 if $user_choice =~ /n/i;
print "\n";

}

move("$srhk_resources/resources.assets.resS", "$srhk_resources/resources.assets.resS.bak") or die "Backup file creation failed.\n";
print "Created backup: $srhk_resources/resources.assets.resS.bak\n";
swap_music_files($srhk_resources, $new_resS_file, $verbose);
move("$op_params->{sr_resources}/resources.assets.resS", "$op_params->{sr_resources}/resources.assets.resS.bak") or die "Backup file creation failed.\n";

print "Created backup: $op_params->{sr_resources}/resources.assets.resS.bak\n";

swap_music_files($op_params);

print "Done\n";

Expand All @@ -195,75 +181,113 @@ sub music_replace {

sub music_restore {

my $srhk_resources;
my $verbose = $_[0];
my $op_params = $_[0];

$srhk_resources = find_game_resources();
$op_params->{sr_resources} = find_game_resources($op_params);

$op_params->{new_resS_file} = "$op_params->{sr_resources}/resources.assets.resS.bak";

die "No backup file found in $srhk_resources\n" unless -s "$srhk_resources/resources.assets.resS.bak";
die "No backup file found in $op_params->{sr_resources}\n" unless -s "$op_params->{new_resS_file}";

swap_music_files($srhk_resources, "$srhk_resources/resources.assets.resS.bak", $verbose);
swap_music_files($op_params);

unlink "$srhk_resources/resources.assets.resS.bak" or warn "Failed to delete backup file.\n";
unlink "$op_params->{sr_resources}/resources.assets.resS.bak" or warn "Failed to delete backup file.\n";

print "Done\n";

}


sub get_option {

shift @ARGV;
die "Error, option without a value detected.\n" unless @ARGV != 0 && $ARGV[0] !~ /-{1,2}\w/;

}


sub help_dialogue {

print "\nThis is a small tool for modders/users who wish to tinker with Shadowrun Hong Kong's sound files.\n",
print "\nThis is a small tool for modders/users who wish to tinker with the sound files of Harebrained Schemes' Shadowrun games.\n",
"Its main purpose is to automate the replacement of the vanilla soundtrack.\n",
"The script has two operation modes:\n\n",
"swap: replace the existing resources.assets.resS file with a new one and update the metadata in resources.assets. The format of the command is:\n",
"shadow_tune.pl -swap -n <path-to-new-resources.assets.reS-file> [-i <path-to-shadowrun-install-folder>] [-v]\n",
"The file provided via the -n option should be the new resources.assets.reS file containing the music tracks (in ogg vorbis format) that the user wishes to use.\n",
"The script can execute two kinds of operations:\n\n",
"---Swap---\n",
"Replace the existing resources.assets.resS file with a new one and update the metadata in resources.assets. The format of the command is:\n",
"shadow_tune.pl -swap <path-to-new-resources.assets.reS-file> [-e returns|dragonfall|hongkong ] [-i <path-to-shadowrun-install-folder>] [-v]\n",
"The file provided via the -swap option should be the new resources.assets.reS file containing the music tracks (in ogg vorbis format) that the user wishes to use.\n",
"Before replacing the original, the script will make a backup copy of the resources.assets.reS file that can later be used for the restore operation.\n\n",
"restore: revert back to the state prior to the sound modification. The format of the command is:\n",
"shadow_tune.pl -restore [-i <path-to-shadowrun-install-folder>] [-v]\n",
"---Restore---\n",
"Revert back to the state prior to the sound modification. The format of the command is:\n",
"shadow_tune.pl -restore [-e returns|dragonfall|hongkong ] [-i <path-to-shadowrun-install-folder>] [-v]\n",
"This operation will fail if the script can't locate the backup copy mentioned above.\n\n",
"With both modes, the script will try to locate the directory where Shadowrun was installed.\n",
"In case this process fails, there's the optional -i commandline parameter, which lets you manually set the path. Note that this should just be the path to the root directory of the installation.\n\n",
"The -e option can be used to specify the Shadowrun game to operate on. If omitted, the script will assume that its operations are to be caried out on the files of Shadowrun Returns.\n\n",
"Regardless which operation is chosen, the script will try to locate the directory where the specified Shadowrun game is installed. (By default the directory of Shadowrun Returns.)\n",
"In case this process fails, there's the optional -i commandline parameter, which lets you manually set the path. Note that this should just be the path to the root directory of the Shadowrun Game that is to be modified.\n\n",
"More verbose output can be obtained via the -v parameter.\n\n",
"And of course --help prints this stuff.\n\n";
}



if ( @ARGV != 0) {

my $new_resS_file;
my $verbose = 0;
my $operation = 0;
my %op_params = (
edition => "Returns",
new_resS_file => undef,
verbose => 0,

#Some path glob patterns that are used by the script to locate the Shadowrun games. Currently only Linux
#specific patterns have been tested.
install_dirs => [
"~/.local/share/Steam/steamapps/common/Shadowrun*",
"~/.steam/steam/SteamApps/common/Shadowrun*",
"~/{Steam,Games,GOG}/{,Steam/,GOG/,Shadowrun/}Shadowrun*",
"~/{steam,games,gog}/{,steam/,gog/,shadowrun/}Shadowrun*",
"~/.wine{,32,64,_steam,_shadowrun}/drive_c/{GOG Games,Program Files/Steam/steamapps/common}/Shadowrun*"
],

#Hardcoded offsets for the respective Shadowrun game at which the script will start loading the resources.assets file
#into memory. Its size varies between the games, with that of Shadowrun Returns being around 600 Megabytes and that of Hong Kong
#almost 2 Gigabytes. In all cases, slurping it whole might impose a noticeable penalty on performance.
meta_offsets => {
Returns => 624000000,
Dragonfall => 1794000000,
"Hong Kong" => 2002710000

}

);

until (@ARGV == 0) {

if ($ARGV[0] =~ /--help/i) { help_dialogue; exit 0; }

elsif ($ARGV[0] =~ /-swap/i) { $operation = 1; }
elsif ($ARGV[0] =~ /-swap/i) { $operation = 1;
get_option();
chomp($op_params{new_resS_file} = $ARGV[0]);
}

elsif ($ARGV[0] =~ /-restore/i) { $operation = 2; }

elsif ($ARGV[0] =~ /-n/i) { shift; chomp($new_resS_file = $ARGV[0]); }

elsif ($ARGV[0] =~ /-i/i) { shift;
elsif ($ARGV[0] =~ /-i/i) { get_option();
chomp($ARGV[0]);
$ARGV[0] =~ s/\/$//;
unshift(@install_dirs, $ARGV[0]);
unshift(@{$op_params{install_dirs}}, $ARGV[0]);
}

elsif ($ARGV[0] =~ /-v/i) { $verbose = 1; }
elsif ($ARGV[0] =~ /-e/i) { get_option(); chomp($op_params{edition} = "\L$ARGV[0]"); }

elsif ($ARGV[0] =~ /-v/i) { $op_params{verbose} = 1; }

else { die "That option is unsupported. Type --help for more info...\n";}
else { die "Unsupported commandline parameter. Type --help for more info...\n";}

shift;

}

if ($operation == 1) { music_replace($new_resS_file, $verbose); }
if ($operation == 1) { music_replace(\%op_params); }

elsif ($operation == 2) { music_restore($verbose); }
elsif ($operation == 2) { music_restore(\%op_params); }

else { die "One of the operation modes (-swap/-restore) must be specified.\n"; }

Expand Down

0 comments on commit 6f4809a

Please sign in to comment.