Secret of Evermore (Bugfixed)

Secret of Evermore is a game for the Super Nintendo released in 1995. It continues to be a somewhat controversial title in the otherwise spotless Squaresoft SNES library… but I like it, and my wife plays it as a “comfort game” whenever she is feeling sick.

IPS is an antiquated binary patch file format, used to provide a “diff” of raw bytes that should be applied over an existing file. IPS patches for SNES games are widespread, and often do things like enable cheats, alter graphics, translate text, etc. In some cases, people have found bugs or glitches in the original code, and release an IPS patch to fix it. Often these are found from the work of speedrunners, who spot a glitch and exploit it to break the game in some way. The SNES hackers then identify the code problem behind the bug, and patch the raw binary code to close the hole. There’s a whole black art to crafting binary bugfixes – the space for a fix is severely limited, and if the new code is too big you have to find additional unused code area elsewhere to jump into (or optimize a different routine to make some free space!)

For some games, more than one bugfix patch is available. Managing these with the ubiquitous LunarIPS tool is a pain – you have to generate a bunch of intermediate ROMs, and the patches may conflict / overwrite one another without indication of the problem. There are better IPS patchers around, but I didn’t want to go on a research quest to find one. Besides, IPS is a pretty simple format – why not just write my own patcher?

I wanted a way to take a binary file, apply a complete “patch set” to it, and return the resulting bin. I wanted it to check for conflicts in patches, and give a descriptive message of which exact patches were colliding. And then I didn’t want to just serve up a cooked file, for copyright and maintainability reasons. “XYZ (Bugfixed)” hacks are too frequently outdated, as new patches are released. So I put a simple PHP frontend before it and a folder full of patches server-side.

The tool is here: https://greg-kennedy.com/SecretOfEvermore/

Users can upload a file. If the SHA-1 matches, it gets patched and they download the fixed version. This tool is for Secret of Evermore, with all the patches (i.e. “hard work”) done by Assassin17.

I may stand up sites for other games as I run across them, or merge these into a single “bugfixer” tool if it gets too out of control.

The patch.pl script follows.

#!/usr/bin/env perl
use strict;
use warnings;

##############################################
# Applies (multiple) IPS patches to a file.
##############################################

##############################################
# Types
use constant
{
  COPY => 0,
  RLE => 1
};

##############################################
# Helper functions

# Safe read N bytes from fh
sub _rd
{
  my ($fh, $size) = @_;

  my $buffer;
  my $count = read ($fh, $buffer, $size);

  die "Short read on file: expected $size, got $count" unless $size == $count;

  return $buffer;
}

##############################################
# Main processing

# Print usage
die "Usage: $0 in.bin out.bin patch.ips <patch2.ips ...>" unless scalar @ARGV > 2;

# Check some input path stuff
my $in_filename = shift @ARGV;
my $out_filename = shift @ARGV;

# Set a verbose variable if it's OK to write to stdout
my $verbose = ($out_filename ne '-' ? 1 : 0);

# Patch processing: Open and parse each patch
my %patch;
my %truncate;

for (my $i = 0; $i < scalar @ARGV; $i ++)
{
  my $filename = $ARGV[$i];

  if ($verbose) { print "Reading patch data from $filename\n" }

  open (my $fh, '<', $filename) or die "Could not open file $filename: $!";
  binmode($fh);

  die "Error: IPS signature not found" unless _rd($fh, 5) eq 'PATCH';

  # Ready to begin parsing bytes out of this patch.
  my %self;
  while (!eof $fh)
  {
    # read 3 byte tag
    my $offset = unpack ('N', "\0" . _rd($fh, 3));

    if ($offset == 0x00454f46) # 'END'
    {
      # some ips patches have a 3-byte "truncate to X" value
      if (!eof $fh)
      {
        my $trunc = unpack ('N', "\0" . _rd($fh, 3));
        if (exists $truncate{length} && $truncate{length} != $trunc) {
          die "Error: IPS patch $filename truncate $trunc conflicts with IPS patch " . $ARGV[$truncate{file}] . " truncate " . $truncate{length}
        }
        %truncate = ( file => $i, length => $trunc );

        if (!eof $fh) { die "Error: extraneous bytes at end of IPS patch $filename" }
      }
    } else {
      my $length = unpack ('n', _rd($fh, 2));

      if ($verbose) { printf(" Block 0x%08x, ", $offset); }

      my %patch_data;
      if ($length)
      {
        # straight paste
        if ($verbose) { printf(" COPY: length %d\n", $length); }
        %patch_data = ( type => COPY, length => $length, data => _rd($fh, $length), file => $i );
      } else {
        # RLE
        $length = unpack ('n', _rd($fh, 2));
        my $data = _rd($fh, 1);

        if ($verbose) { printf(" RLE: length %d, data 0x%02x\n", $length, $data); }
        %patch_data = ( type => RLE, length => $length, data => $data, file => $i );
      }

      # ensure no collision between new patch data and existing cumulative patch info
      my $end = $offset + $length;

      foreach my $existing_offset (keys %patch)
      {
        my $existing_end = $existing_offset + $patch{$existing_offset}{length};

        if (($offset > $existing_offset && $offset < $existing_end) ||
            ($end > $existing_offset && $end < $existing_end) ||
            ($existing_offset > $offset && $existing_offset < $end) ||
            ($existing_end > $offset && $existing_end < $end)) {
          die "Error: IPS patch $filename, range $offset-$end conflicts with IPS patch " . $ARGV[$patch{$existing_offset}{file}] . ", range $existing_offset-$existing_end";
        }
      }

      # looks OK, push into master patchset
      $patch{$offset} = \%patch_data;
    }
  }
}

# Open the input file
if ($verbose) { print "Reading input data from $in_filename\n" }
my $f_in;
if ($in_filename eq '-') {
  $f_in = *STDIN;
} else {
  open ($f_in, '<', $in_filename) or die "Could not open $in_filename: $!";
}
binmode $f_in;

# also need the input filesize
my $size = (stat $f_in)[7];
if ($verbose) { print " Size == $size\n" }

# Open the output file
if ($verbose) { print "Writing to output file $out_filename\n" }
my $f_out;
if ($out_filename eq '-') {
  $f_out = *STDOUT;
} else {
  open ($f_out, '>', $out_filename) or die "Could not open $out_filename: $!";
}
binmode $f_out;

# Finally, patch and write.
my $offset = 0;

foreach my $patch_offset (sort { $a <=> $b } keys %patch)
{
  # compute distance from current offset to start of the next patch
  my $length = $patch_offset - $offset;

  # copy the intervening bytes from in to out
  if ($verbose) { printf (" Copying %d bytes from input to 0x%08x\n", $length, $offset) }

  seek ($f_in, $offset, 0);
  print $f_out _rd($f_in, $length);
  $offset += $length;

  # now patch this portion
  if ($patch{$patch_offset}{type} == COPY)
  {
    if ($verbose) { printf (" Patching (COPY) %d bytes from %s to 0x%08x\n", $patch{$patch_offset}{length}, $ARGV[$patch{$patch_offset}{file}], $patch_offset) }

    print $f_out $patch{$patch_offset}{data};
    $offset += $patch{$patch_offset}{length};
  } else {
    if ($verbose) { printf (" Patching (RLE) %d bytes from %s to 0x%08x\n", $patch{$patch_offset}{length}, $ARGV[$patch{$patch_offset}{file}], $patch_offset) }

    print $f_out ($patch{$patch_offset}{data} x $patch{$patch_offset}{length});
    $offset += $patch{$patch_offset}{length};
  }
}

# copy the final bytes from in to out
my $length;
if (exists $truncate{length}) {
  $length = $truncate{length} - $offset;
} else {
  $length = $size - $offset;
}
if ($verbose) { printf (" Copying %d final bytes from input to 0x%08x\n", $length, $offset) }

seek ($f_in, $offset, 0);
print $f_out _rd($f_in, $length);

# All done, close up files
close ($f_out);
close ($f_in);

Leave a Reply

Your email address will not be published. Required fields are marked *