#!/usr/bin/perl -w

=head1 NAME

detads - decompile TADS 2 .GAM files

=head1 SYNOPSIS

B<detads> [B<-c>] [B<-s> I<file>] I<file>

=head1 DESCRIPTION

detads is a Perl script which (mostly) decompiles TADS 2 game files.
The decompiled TADS 2 source code is printed to stdout.

=head1 OPTIONS

=over 4

=item B<-c>

Decompile for the more C-like syntax (activated by #pragma C+ in
TADS source).  Default is to decompile for the default syntax.

=item B<-s> I<file>

Input a symbols file.  The I<file> is a Perl script which can set
several global variables, which control the names given to identifiers.
For example:

    $objs[23] = 'redHerring';
    $objs[35] = 'myFunction';
    $props[312] = 'dontAnnoyMe';
    $propArgs[312] = ['actor', 'maxAnnoyance'];
    $actions[40] = 'Sneezeat';

Note that it is not necessary (or desirable) to set names for builtin
properties, objects and functions used directly by the builtin parser,
or action methods (such as verDoSneezeat or doSneezeat in the above
example).

=item I<file>

The TADS game file to be decompiled.

=back

=head1 BUGS

Sometimes, because values don't get popped off the runtime stack in
certain situations, the decompiler outputs errors.  For example:

    f: function(arg1) {
        local local1, local2;

        g(arg1 + local1);
        // ERROR: nonempty stack
        // local1 := 1
        ...
    }

This comes from the code:

    f: function(arg1) {
        local local1 := 1, local2;

        g(arg1 + local1);
        ...
    }

Similarly, output such as:

    local3 := 4;
    // ERROR: nonempty stack
    // local2 := foo;
    // local1 := 0;
    while (local1 < 10) {
        ...
        local1++;
    }

comes from the code:

    for (local1 := 0, local2 := foo, local3 := 4; local1 < 10; local1++) {
        ...
    }

Objects and functions will almost certainly not be output in the order
they were defined in the original source code.

The decompiler cannot detect `for' statements (as hinted above), or
`modify' or `replace' directives.

=head1 COPYRIGHT AND LICENSE

Copyright 2003 by Daniel Schepler.

This program is free software; you may redistribute it and/or modify
it under the terms of the GNU General Public License, version 2 or
later.

=head1 AUTHOR

Daniel Schepler <schepler@math.berkeley.edu>

=cut

use Fcntl qw(SEEK_SET);
use integer;

sub ord1($;$) {
    my $str = shift;
    my $ofs = shift || 0;

    return ord(substr($str, $ofs));
}

sub read1(*) {
    my $FH = shift;

    return ord(getc($FH));
}

sub ord2($;$) {
    my $str = shift;
    my $ofs = shift || 0;
    my $c1 = ord1($str, $ofs);
    my $c2 = ord1($str, $ofs + 1);

    return ($c2 << 8) | $c1;
}

sub ord2s($;$) {
    my $str = shift;
    my $ofs = shift || 0;
    my $result = ord2($str, $ofs);
    $result -= 0x10000 if $result >= 0x8000;
    return $result;
}

sub read2(*) {
    my $FH = shift;
    my $str;

    read($FH, $str, 2);
    return ord2($str);
}

sub ord4($;$) {
    my $str = shift;
    my $ofs = shift || 0;

    my $c1 = ord1($str, $ofs);
    my $c2 = ord1($str, $ofs + 1);
    my $c3 = ord1($str, $ofs + 2);
    my $c4 = ord1($str, $ofs + 3);
    $c4 -= 256 if $c4 >= 128;	# In case of a signed value

    return ($c4 << 24) | ($c3 << 16) | ($c2 << 8) | $c1;
}

sub read4(*) {
    my $FH = shift;
    my $str;

    read($FH, $str, 4);
    return ord4($str);
}

$xorseed = 0x3f;
$xorinc = 0x40;

sub decode($) {
    my $block = shift;

    return $block unless $crypt;

    my $len = length($block);
    my $mask = $xorseed;
    my $maskstr;

    for (my $i = 0; $i < $len; $i++) {
	$maskstr .= chr($mask);
	$mask = ($mask + $xorinc) % 256;
    }

    return $block ^ $maskstr;
}

# Datatype numbers
sub DAT_NUMBER() { return 1; }
sub DAT_OBJECT() { return 2; }
sub DAT_SSTRING() { return 3; }
sub DAT_BASEPTR() { return 4; }
sub DAT_NIL() { return 5; }
sub DAT_CODE() { return 6; }
sub DAT_LIST() { return 7; }
sub DAT_TRUE() { return 8; }
sub DAT_DSTRING() { return 9; }
sub DAT_FNADDR() { return 10; }
sub DAT_TPL() { return 11; }
sub DAT_PROPNUM() { return 13; }
sub DAT_DEMAND() { return 14; }
sub DAT_SYN() { return 15; }
sub DAT_REDIR() { return 16; }
sub DAT_TPL2() { return 17; }

# Print a value, returning the length it occupied
sub valueStr($$$) {
  my $block = shift;
  my $pos = shift;
  my $type = shift;

  if ($type == DAT_NUMBER) {
      return (ord4($block, $pos), $pos + 4);
  }
  elsif ($type == DAT_OBJECT || $type == DAT_FNADDR) {
      return (($type == DAT_FNADDR ? "&" : "") . objStr(ord2($block, $pos)),
	      $pos + 2);
  }
  elsif ($type == DAT_PROPNUM) {
      return ("&" . propStr(ord2($block, $pos)), $pos + 2);
  }
  elsif ($type == DAT_SSTRING || $type == DAT_DSTRING) {
      my ($str, $newpos) = stringStr($block, $pos,
				     $type == DAT_SSTRING ? "'" : '"');
      return ($str, $newpos);
  }
  elsif ($type == DAT_LIST) {
      my ($str, $newpos) = listStr($block, $pos);
      return ($str, $newpos);
  }
  elsif ($type == DAT_NIL) {
      return ("nil", $pos);
  }
  elsif ($type == DAT_TRUE) {
      return ("true", $pos);
  }
  else {
      warn "Unexpected data type: $type";
      return ("", length($block));
  }
}

sub listStr($$) {
  my $block = shift;
  my $pos = shift;
  my $result = "[";

  my $len = ord2($block, $pos);
  my $endpos = $pos + $len;
  my $str;

  $pos += 2;
  while ($pos < $endpos) {
    my $type = ord1($block, $pos);
    $pos++;
    ($str, $pos) = valueStr($block, $pos, $type);
    $result .= $str;
    $result .= ", " if $pos < $endpos;
  }
  $result .= "]";
  return ($result, $endpos);
}

@props = (undef, "doAction", "verb", "noun", "adjective", "preposition",
	  "article", "plural", "sdesc", "thedesc", "doDefault",
	  "ioDefault", "ioAction", "location", "value",
	  "roomAction", "actorAction", "contents", "tpl",
	  "prepDefault", "verActor", "validDo", "validIo",
	  "lookAround", "roomCheck", "statusLine", "locationOK",
	  "isVisible", "cantReach", "isHim", "isHer", "action",
	  "validDoList", "validIoList", "iobjGen", "dobjGen",
	  "nilPrep", "rejectMultiDobj", "moveInto", "construct",
	  "destruct", "validActor", "preferredActor", "isEquivalent",
	  "adesc", "multisdesc", "tpl2", "anyvalue",
	  "newNumbered", "unknown", "parseUnknownDobj",
	  "parseUnknownIobj", "dobjCheck", "iobjCheck", "verbAction",
	  "disambigDobj", "disambigIobj", "prefixdesc", "isThem");

sub propStr($) {
  my $n = shift;

  if ($n == 0) {
    return "noprop";
  }
  elsif ($n > 0 && defined $props[$n]) {
    return $props[$n];
  }
  else {
    return "prop$n";
  }
}

@objs = ();

sub objStr($) {
  my $n = shift;

  if ($n == 65535) {
    return "nullobj";
  }
  elsif (defined $objs[$n]) {
    return $objs[$n];
  }
  else {
    return "obj$n";
  }
}

sub builtinStr($) {
    my $n = shift;
  my @builtins = ("say", "car", "cdr", "length", "randomize", "rand",
		  "substr", "cvtstr", "cvtnum", "upper", "lower",
		  "caps", "find", "getarg", "datatype", "setdaemon",
		  "setfuse", "setversion", "notify", "unnotify",
		  "yorn", "remfuse", "remdaemon", "incturn", "quit",
		  "save", "restore", "logging", "input", "setit",
		  "askfile", "setscore", "firstobj", "nextobj",
		  "isclass", "restart", "debugTrace", "undo", "defined",
		  "proptype", "outhide", "runfuses", "rundaemons",
		  "gettime", "getfuse", "intersect", "inputkey",
		  "objwords", "addword", "delword", "getwords",
		  "nocaps", "skipturn", "clearscreen", "firstsc",
		  "verbinfo", "fopen", "fclose", "fwrite", "fread",
		  "fseek", "fseekeof", "ftell", "outcapture",
		  "systemInfo", "morePrompt", "parserSetMe",
		  "parserGetMe", "reSearch", "reGetGroup", "inputevent",
		  "timeDelay", "setOutputFilter", "execCommand",
		  "parserGetObj", "parseNounList", "parserTokenize",
		  "parserGetTokTypes", "parserDictLookup",
		  "parserResolveObjects", "parserReplaceCommand",
		  "exitobj", "inputdialog", "resourceExists");

  if ($n < 0 || $n > $#builtins) {
    return "builtin$n";
  }
  else {
    return $builtins[$n];
  }
}

@propArgs = (undef, undef, undef, undef, undef, undef,
	     undef, undef, undef, undef,
	     ['actor', 'prep', 'iobj'], # doDefault
	     ['actor', 'prep'], # ioDefault
	     undef, undef, undef,
	     ['actor', 'verb', 'dobj', 'prep', 'iobj'], # roomAction
	     ['verb', 'dobj', 'prep', 'iobj'], # actorAction
	     undef, undef, undef, undef,
	     ['actor', 'obj', 'seqno'], # validDo
	     ['actor', 'obj', 'seqno'], # validIo
	     ['verbosity'],	# lookAround
	     ['verb'],	# roomCheck
	     undef, undef,
	     ['vantage'],	# isVisible
	     ['actor', 'dolist', 'iolist', 'prep'], # cantReach
	     undef, undef,
	     ['actor'],	# action
	     ['actor', 'prep', 'iobj'], # validDoList
	     ['actor', 'prep', 'dobj'], # validIoList
	     ['actor', 'verb', 'dobj', 'prep'], # iobjGen
	     ['actor', 'verb', 'iobj', 'prep'], # dobjGen
	     ['prep'],	# rejectMultiDobj
	     ['dest'],	# moveInto
	     undef, undef, undef, undef, undef, undef, undef, undef,
	     ['num'],		# anyvalue
	     ['actor', 'verb', 'num'], # newNumbered
	     undef,
	     ['actor', 'prep', 'iobj', 'wordlist'], # parseUnknownDobj
	     ['actor', 'prep', 'iobj', 'wordlist'], # parseUnknownIobj
	     ['actor', 'prep', 'iobj', 'prep'], # dobjCheck
	     ['actor', 'prep', 'iobj', 'prep'], # iobjCheck
	     ['actor', 'dobj', 'prep', 'iobj'], # verbAction
	     ['actor', 'prep', 'iobj', 'verprop', 'wordlist', 'objlist',
	       'flaglist', 'numberWanted', 'isAmbiguous', 'silent'],
				# disambigDobj
	     ['actor', 'prep', 'dobj', 'verprop', 'wordlist', 'objlist',
	       'flaglist', 'numberWanted', 'isAmbiguous', 'silent'],
				# disambigIobj
	     ['show', 'current_index', 'count', 'multi_flags'], # prefixdesc
	     undef
);
@funcArgs = ();

sub localStr($$) {
    my $num = shift;
    my $propNum = shift;	# Negative for function

    if ($num < 0) {
	my $argList = ($propNum < 0 ? $funcArgs[-$propNum] :
		       $propArgs[$propNum]);
	if (defined $argList && -1 - $num <= $#{$argList}) {
	    return $argList->[-1 - $num];
	}
	else {
	    return "arg" . (-$num);
	}
    }
    else {
	return "local$num";
    }
}

sub stringStr($$$) {
    my $block = shift;
    my $pos = shift;
    my $delim = shift;

    my $strlen = ord2($block, $pos);
    my $str = substr($block, $pos + 2, $strlen - 2);
    $str =~ s/$delim/\\$delim/g;
    return ($delim . $str . $delim, $pos + $strlen);
}

sub indentStr($) {
    my $level = shift;
    return ("\t" x ($level / 2)) . ("    " x ($level % 2));
}

sub funcArgs($) {
    my $num = shift;
    return "" if $num == 0;

    my @args = splice(@stack, -$num);
    splice(@precstack, -$num);
    return "(" . join(", ", reverse(@args)) . ")";
}

# The possible precedence levels:
# 14: atoms (numbers or symbols)
# 13: . []; function calls
# 12: ++ --
# 11: unary operators (-, not, ~, delete, new)
# 10: * / %
# 9: + -
# 8: << >>
# 7: comparisons
# 6: &
# 5: ^
# 4: |
# 3: and
# 2: or
# 1: ?:
# 0: assignments

sub doBinaryOp($$) {
    my $op = shift;
    my $prec = shift;

    my $arg2 = pop(@stack);
    $arg2 = "($arg2)" if pop(@precstack) <= $prec;
    my $arg1 = pop(@stack);
    $arg1 = "($arg1)" if pop(@precstack) < $prec;

    push(@stack, "$arg1 $op $arg2");
    push(@precstack, $prec);
}

sub doUnaryOp($$) {
    my $op = shift;
    my $prec = shift;

    my $arg = pop(@stack);
    $arg = "($arg)" if pop(@precstack) < $prec;

    push(@stack, "$op $arg");
    push(@precstack, $prec);
}

sub checkStack($) {
    my $indentLevel = shift;
    my $nonempty = 0;

    foreach my $elt (@stack) {
	$nonempty = 1 unless $elt eq "*SAYEXPR*";
    }
    if ($nonempty) {
	if ($#lines == -1) {
	    push(@lines, "");
	    push(@labels, -1);
	}
	$lines[$#lines] .= "\n" . indentStr($indentLevel) .
	    "// ERROR: nonempty stack";
	while ($#stack >= 0) {
	    $elt = pop(@stack);
	    pop(@precstack);
	    next if $elt eq "*SAYEXPR*";
	    $lines[$#lines] .= "\n" . indentStr($indentLevel) .
		"// $elt";
	}
    }
    else {
	@stack = ();
    }
}

$cmode = 0;

sub decompileBlock {
    my $block = shift;
    my $propNum = shift;
    my $startpos = shift;
    my $endpos = shift;
    my $breakLabel = shift;
    my $continueLabel = shift;
    my $indentLevel = shift;

    my $pos = $startpos;
    my $reachable = 1;
    my $linepos = $pos;
    while ($pos < $endpos || ($endpos == -1 &&
			      ($reachable || $pos <= $maxLabel))) {
	my $opcode = ord1($block, $pos);
	my $str;

	# Only a few opcodes don't fall through to the next
	$reachable = 1;
	$pos++;
	if ($opcode == 1) {	# pushnum
	    push(@stack, ord4($block, $pos));
	    push(@precstack, 14);
	    $pos += 4;
	}
	elsif ($opcode == 2) {	# pushobj
	    push(@stack, objStr(ord2($block, $pos)));
	    push(@precstack, 14);
	    $pos += 2;
	}
	elsif ($opcode == 3) {	# neg
	    doUnaryOp("-", 11);
	}
	elsif ($opcode == 4) {	# not
	    doUnaryOp("not", 11);
	}
	elsif ($opcode == 5) {	# add
	    doBinaryOp("+", 9);
	}
	elsif ($opcode == 6) {	# sub
	    doBinaryOp("-", 9);
	}
	elsif ($opcode == 7) {	# mul
	    doBinaryOp("*", 10);
	}
	elsif ($opcode == 8) {	# div
	    doBinaryOp("/", 10);
	}
	elsif ($opcode == 9) {	# and
	    doBinaryOp($cmode ? "&&" : "and", 3);
	}
	elsif ($opcode == 10) {	# or
	    doBinaryOp($cmode ? "||" : "or", 2);
	}
	elsif ($opcode == 11) {	# eq
	    doBinaryOp($cmode ? "==" : "=", 7);
	}
	elsif ($opcode == 12) {	# ne
	    doBinaryOp($cmode ? "!=" : "<>", 7);
	}
	elsif ($opcode == 13) {	# gt
	    doBinaryOp(">", 7);
	}
	elsif ($opcode == 14) {	# ge
	    doBinaryOp(">=", 7);
	}
	elsif ($opcode == 15) {	# lt
	    doBinaryOp("<", 7);
	}
	elsif ($opcode == 16) {	# le
	    doBinaryOp("<=", 7);
	}
	elsif ($opcode == 17) {	# call
	    my $obj = objStr(ord2($block, $pos + 1));
	    push(@stack, $obj . funcArgs(ord1($block, $pos)));
	    $stack[$#stack] .= "()" if ord1($block, $pos) == 0;
	    push(@precstack, 13);
	    $pos += 3;
	}
	elsif ($opcode == 18) {	# getp
	    my $prop = propStr(ord2($block, $pos + 1));
	    my $obj = pop(@stack);
	    $obj = "($obj)" if pop(@precstack) < 13;
	    push(@stack, "$obj.$prop" . funcArgs(ord1($block, $pos)));
	    push(@precstack, 13);
	    $pos += 3;
	}
	elsif ($opcode == 20) {	# getlcl
	    my $num = ord2s($block, $pos);
	    $pos += 2;
	    push(@stack, localStr($num, $propNum));
	    push(@precstack, 14);
	}
	elsif ($opcode == 22) {	# return
	    $pos += 2;
	    $reachable = 0;

	    # Check to see if it will be the last; if so, don't bother
	    # printing the statement
	    if ($endpos != -1 || $pos <= $maxLabel) {
		push(@lines, indentStr($indentLevel) . "return;");
	    }
	    # But do make the label available for the top-level
	    # decompile, in case it's needed
	    push(@labels, $linepos);
	    $linepos = $pos;
	    checkStack($indentLevel);
	}
	elsif ($opcode == 23) {	# retval
	    $pos += 2;
	    $reachable = 0;

	    push(@lines, indentStr($indentLevel) . "return " .
		 pop(@stack) . ";");
	    pop(@precstack);
	    push(@labels, $linepos);
	    $linepos = $pos;
	    checkStack($indentLevel);
	}
	elsif ($opcode == 25) {	# discard
	    my $expr = pop(@stack);
	    pop(@precstack);

	    unless ($expr eq "*SAYEXPR*") { # See "builtin" case below
		push(@lines, indentStr($indentLevel) . "$expr;");
		push(@labels, $linepos);
	    }
	    $linepos = $pos;
	    checkStack($indentLevel);
	}
	elsif ($opcode == 26) {	# jmp
	    my $dest = $pos + ord2s($block, $pos);
	    $pos += 2;

	    if ($dest == $breakLabel) {
		push(@lines, indentStr($indentLevel) . "break;");
	    }
	    elsif ($dest == $continueLabel) {
		push(@lines, indentStr($indentLevel) . "continue;");
	    }
	    else {
		push(@lines, indentStr($indentLevel) . "goto label$dest;");
		$labelNeeded{$dest} = 1;
		$maxLabel = $dest if $dest > $maxLabel;
	    }

	    push(@labels, $linepos);
	    $linepos = $pos;
	    checkStack($indentLevel);
	}
	elsif ($opcode == 27) {	# jf
	    my $dest = $pos + ord2s($block, $pos);
	    $pos += 2;

	    if ($dest < $startpos || ($endpos != -1 && $dest > $endpos)) {
		# Oops, it's a jump outside this block
		push(@lines, indentStr($indentLevel) . "if (not (" .
		     pop(@stack) . ")) goto label$dest;");
		pop(@precstack);
		push(@labels, $linepos);
		$labelNeeded{$dest} = 1;
		$maxLabel = $dest if $dest > $maxLabel;
		$linepos = $pos;
		checkStack($indentLevel);
	    }
	    elsif (ord1($block, $dest - 3) == 26 && # jmp
		($dest - 2) + ord2s($block, $dest - 2) == $linepos) {
		# A while statement
		push(@lines, indentStr($indentLevel) .
		     "while (" . pop(@stack) . ") {");
		pop(@precstack);
		push(@labels, $linepos);

		decompileBlock($block, $propNum, $pos, $dest - 3,
			       $dest, $linepos, $indentLevel + 1);

		push(@lines, indentStr($indentLevel) . "}");
		push(@labels, $dest - 3);

		$pos = $dest;
		$linepos = $pos;
		checkStack($indentLevel);
	    }
	    elsif ($dest > $pos + 3 && # Decompile "if (cond) break;" correctly
		   ord1($block, $dest - 3) == 26 &&
		   ord2s($block, $dest - 2) > 0 &&
		   ($endpos == -1 ||
		    ($dest - 2) + ord2s($block, $dest - 2) <= $endpos)) {
		# An if/else statement -- or ?: expression
		my $endElse = $dest - 2 + ord2s($block, $dest - 2);
		my $cond = pop(@stack);
		my $condprec = pop(@precstack);

		if ($linepos == $startpos && $endElse == $endpos &&
		    $#lines >= 0 && $lines[$#lines] eq
		    (indentStr($indentLevel - 1) . "} else {")) {
		    # Contract to "else if (...)" -- the contents of
		    # an else statement shouldn't be a ?: expression
		    # without even a discard
		    $lines[$#lines] = indentStr($indentLevel - 1) .
			"} else if ($cond) {";
		    decompileBlock($block, $propNum, $pos, $dest - 3,
				   $breakLabel, $continueLabel,
				   $indentLevel);
		    checkStack($indentLevel);
		    push(@lines, indentStr($indentLevel - 1) . "} else {");
		    push(@labels, $dest - 3);

		    decompileBlock($block, $propNum, $dest, $endElse,
				   $breakLabel, $continueLabel,
				   $indentLevel);
		    # The outer decompile will provide the }
		    $linepos = $endElse;
		    checkStack($indentLevel);
		}
		else {
		    push(@lines, indentStr($indentLevel) .
			 "if ($cond) {");
		    push(@labels, $linepos);

		    my $oldline = $#lines;
		    decompileBlock($block, $propNum, $pos, $dest - 3,
				   $breakLabel, $continueLabel,
				   $indentLevel + 1);
		    if ($#lines == $oldline) {
			# No output -- assume it's a ?: expression
			pop(@lines); # Remove "if" line
			pop(@labels);
			$cond = "($cond)" if $condprec <= 1;

			my $trueexpr = pop(@stack);
			$trueexpr = "($trueexpr)" if pop(@precstack) <= 1;

			decompileBlock($block, $propNum, $dest, $endElse,
				       $breakLabel, $continueLabel,
				       $indentLevel + 1);
			my $falseexpr = pop(@stack);
			$falseexpr = "($falseexpr)" if pop(@precstack) <= 1;

			push(@stack, "$cond ? $trueexpr : $falseexpr");
			push(@precstack, 1);
		    }
		    else {
			checkStack($indentLevel);
			push(@lines, indentStr($indentLevel) .
			     "} else {");
			push(@labels, $dest - 3);

			decompileBlock($block, $propNum, $dest, $endElse,
				       $breakLabel, $continueLabel,
				       $indentLevel + 1);
			$lines[$#lines] .= "\n" . indentStr($indentLevel) .
			    "}";
			$linepos = $endElse;
			checkStack($indentLevel);
		    }
		}

		$pos = $endElse;
	    }
	    else {
		# A plain if statement
		if ($linepos == $startpos && $dest == $endpos &&
		    $#lines >= 0 && $lines[$#lines] eq
		    (indentStr($indentLevel - 1) . "} else {")) {
		    # Contract to "else if (...)"
		    $lines[$#lines] = indentStr($indentLevel - 1) .
			"} else if (" . pop(@stack) . ") {";
		    pop(@precstack);
		    checkStack($indentLevel);

		    decompileBlock($block, $propNum, $pos, $dest,
				   $breakLabel, $continueLabel,
				   $indentLevel);
		    # The outer decompile will provide the }
		}
		else {
		    push(@lines, indentStr($indentLevel) .
			 "if (" . pop(@stack) . ") {");
		    pop(@precstack);
		    push(@labels, $linepos);
		    checkStack($indentLevel);

		    decompileBlock($block, $propNum, $pos, $dest,
				   $breakLabel, $continueLabel,
				   $indentLevel + 1);

		    $lines[$#lines] .= "\n" . indentStr($indentLevel) . "}";
		}
		$pos = $dest;
		$linepos = $pos;
		checkStack($indentLevel);
	    }
	}
	elsif ($opcode == 28) {	# pushself
	    push(@stack, "self");
	    push(@precstack, 14);
	}
	elsif ($opcode == 29) {	# say
	    ($str, $pos) = stringStr($block, $pos, '"');
	    # Try to combine with a previous string ending with
	    # printing an expression
	    if ($#lines >= 0 && substr($lines[$#lines], -4) eq '>>";') {
		$lines[$#lines] = substr($lines[$#lines], 0, -2) .
		    substr($str, 1) . ";";
	    }
	    else {
		push(@lines, indentStr($indentLevel) . "$str;");
		push(@labels, $linepos);
	    }
	    $linepos = $pos;
	}
	elsif ($opcode == 30) {	# builtin
	    # Special case: say with 2 arguments is used to implement
	    # "<< expr >>".  The pop of the result isn't reliable, so
	    # push a special token.
	    if (builtinStr(ord2($block, $pos + 1)) eq "say" &&
		ord1($block, $pos) == 2) {
		my $expr = pop(@stack);
		pop(@precstack);
		pop(@stack);	# Usually nil for second argument
		pop(@precstack);

		# Try to combine with a previous string
		if ($#lines >= 0 && substr($lines[$#lines], -2) eq '";') {
		    $lines[$#lines] = substr($lines[$#lines], 0, -2) .
			"<< $expr >>\";";
		}
		else {
		    push(@lines, indentStr($indentLevel) .
			 "\"<< $expr >>\";");
		    push(@labels, $linepos);
		}
		push(@stack, "*SAYEXPR*");
		push(@precstack, 14);
	    }
	    else {
		push(@stack, builtinStr(ord2($block, $pos + 1)) .
		     funcArgs(ord1($block, $pos)));
		$stack[$#stack] .= "()" if ord1($block, $pos) == 0;
		push(@precstack, 13);
	    }
	    $pos += 3;
	}
	elsif ($opcode == 31) {	# pushstr
	    my $str;
	    ($str, $pos) = stringStr($block, $pos, "'");
	    push(@stack, $str);
	    push(@precstack, 14);
	}
	elsif ($opcode == 32) {	# pushlst
	    my $str;
	    ($str, $pos) = listStr($block, $pos);
	    push(@stack, $str);
	    push(@precstack, 14);
	}
	elsif ($opcode == 33) {	# pushnil
	    push(@stack, "nil");
	    push(@precstack, 14);
	}
	elsif ($opcode == 34) {	# pushtrue
	    push(@stack, "true");
	    push(@precstack, 14);
	}
	elsif ($opcode == 35) {	# pushfn
	    push(@stack, "&" . objStr(ord2($block, $pos)));
	    push(@precstack, 14);
	    $pos += 2;
	}
	elsif ($opcode == 40) {	# ptrgetp
	    my $prop = pop(@stack);
	    pop(@precstack);	# It needs parentheses in all cases
	    my $obj = pop(@stack);
	    $obj = "($obj)" if pop(@precstack) < 13;

	    push(@stack, "$obj.($prop)" . funcArgs(ord1($block, $pos)));
	    push(@precstack, 13);
	    $pos++;
	}
	elsif ($opcode == 41) {	# pass
	    my $prop = propStr(ord2($block, $pos));
	    $pos += 2;
	    push(@lines, indentStr($indentLevel) . "pass $prop;");
	    push(@labels, $linepos);
	    $linepos = $pos;
	    checkStack($indentLevel);
	}
	elsif ($opcode == 42) {	# exit
	    push(@lines, indentStr($indentLevel) . "exit;");
	    push(@labels, $linepos);
	    $linepos = $pos;
	    checkStack($indentLevel);
	}
	elsif ($opcode == 43) {	# abort
	    push(@lines, indentStr($indentLevel) . "abort;");
	    push(@labels, $linepos);
	    $linepos = $pos;
	    checkStack($indentLevel);
	}
	elsif ($opcode == 44) {	# askdo
	    push(@lines, indentStr($indentLevel) . "askdo;");
	    push(@labels, $linepos);
	    $linepos = $pos;
	    checkStack($indentLevel);
	}
	elsif ($opcode == 45) {	# askio
	    push(@lines, indentStr($indentLevel) . "askio(" .
		 propStr(ord2($block, $pos)) . ");");
	    push(@labels, $linepos);
	    $pos += 2;
	    $linepos = $pos;
	    checkStack($indentLevel);
	}
	elsif ($opcode == 46) {	# expinh
	    push(@stack, "inherited " . objStr(ord2($block, $pos + 3)) .
		 "." . propStr(ord2($block, $pos + 1)) .
		 funcArgs(ord1($block, $pos)));
	    push(@precstack, 13);
	    $pos += 5;
	}
	elsif ($opcode == 59) {	# jt
	    # Used to implement a do/while statement
	    my $dest = $pos + ord2s($block, $pos);
	    $pos += 2;
	    if ($dest >= $pos || $dest < $startpos) {
		# Oops, it's a jump forward or to before this block...
	      noline:
		push(@lines, indentStr($indentLevel) . "if (" .
		     pop(@stack) . ") goto label$dest;");
		pop(@precstack);
		push(@labels, $linepos);
		$labelNeeded{$dest} = 1;
		$linepos = $pos;
		checkStack($indentLevel);
	    }
	    else {
		# Search for the beginning
		my $line = 0;
		$line++ while ($line < $#labels && $labels[$line] < $dest);
		goto noline unless $labels[$line] == $dest;
		my $cond = pop(@stack);
		pop(@precstack);

		# Delete everything inside the block, and do it over
		splice(@lines, $line);
		splice(@labels, $line);
		decompileBlock($block, $propNum, $dest, $linepos,
			       $pos, $linepos, $indentLevel + 1);

		$lines[$line] = indentStr($indentLevel) . "do {\n" .
		    $lines[$line];
		push(@lines, indentStr($indentLevel) . "} while ($cond);");
		push(@labels, $linepos);
		$linepos = $pos;
		checkStack($indentLevel);
	    }
	}
	elsif ($opcode == 60) {	# getpself
	    my $prop = propStr(ord2($block, $pos + 1));
	    push(@stack, "self.$prop" . funcArgs(ord1($block, $pos)));
	    push(@precstack, 13);
	    $pos += 3;
	}
	elsif ($opcode == 62) {	# getpobj
	    my $obj = objStr(ord2($block, $pos + 1));
	    my $prop = propStr(ord2($block, $pos + 3));
	    push(@stack, "$obj.$prop" . funcArgs(ord1($block, $pos)));
	    push(@precstack, 13);
	    $pos += 5;
	}
	elsif ($opcode == 64) {	# index
	    my $arg2 = pop(@stack);
	    pop(@precstack);
	    my $arg1 = pop(@stack);
	    $arg1 = "($arg1)" if pop(@precstack) < 13;
	    push(@stack, $arg1 . "[" . $arg2 . "]");
	    push(@precstack, 13);
	}
	elsif ($opcode == 67) {	# pushpn
	    push(@stack, "&" . propStr(ord2($block, $pos)));
	    push(@precstack, 14);
	    $pos += 2;
	}
	elsif ($opcode == 68) {	# jst
	    # Used to implement an || operation; call decompileBlock
	    # recursively to get the other argument
	    my $dest = $pos + ord2s($block, $pos);
	    $pos += 2;
	    decompileBlock($block, $propNum, $pos, $dest,
			   $breakLabel, $continueLabel, $indentLevel);

	    doBinaryOp($cmode ? "||" : "or", 2);

	    $pos = $dest;
	}
	elsif ($opcode == 69) {	# jsf
	    # Used to implement an && operation; call decompileBlock
	    # recursively to get the other argument
	    my $dest = $pos + ord2s($block, $pos);
	    $pos += 2;
	    decompileBlock($block, $propNum, $pos, $dest,
			   $breakLabel, $continueLabel, $indentLevel);

	    doBinaryOp($cmode ? "&&" : "and", 3);

	    $pos = $dest;
	}
	elsif ($opcode == 71) {	# inherit
	    my $prop = propStr(ord2($block, $pos + 1));
	    push(@stack, "inherited.$prop" .
		 funcArgs(ord1($block, $pos)));
	    push(@precstack, 13);
	    $pos += 3;
	}
	elsif ($opcode == 74) {	# cons
	    my @args = splice(@stack, -ord2($block, $pos));
	    splice(@precstack, -ord2($block, $pos));

	    push(@stack, "[" . join(", ", reverse(@args)) . "]");
	    push(@precstack, 14);
	    $pos += 2;
	}
	elsif ($opcode == 75) {	# switch
	    my $swtable = $pos + ord2s($block, $pos);
	    $pos += 2;

	    my $swlen = ord2($block, $swtable);
	    my $swpos = $swtable + 2;
	    my @swcases = ();
	    my @swdests = ();
	    for (my $i = 0; $i < $swlen; $i++) {
		my $str;
		my $swopcode = ord1($block, $swpos);
		$swpos++;

		if ($swopcode == 1) { # pushint
		    $str = ord4($block, $swpos);
		    $swpos += 4;
		}
		elsif ($swopcode == 2) { # pushobj
		    $str = objStr(ord2($block, $swpos));
		    $swpos += 2;
		}
		elsif ($swopcode == 31) { # pushstr
		    ($str, $swpos) = stringStr($block, $swpos, "'");
		}
		else {
		    die "Unimplemented or invalid opcode $swopcode for switch";
		}
		push(@swcases, $str);
		push(@swdests, $swpos + ord2s($block, $swpos));
		$swpos += 2;
	    }
	    push(@swdests, $swpos + ord2s($block, $swpos)); # default
	    $swpos += 2;

	    push(@lines, indentStr($indentLevel) . "switch (" .
		 pop(@stack) . ") {");
	    pop(@precstack);
	    push(@labels, $linepos);
	    checkStack($indentLevel);
	    for (my $i = 0; $i <= $#swcases; $i++) {
		$lines[$#lines] .= "\n" . indentStr($indentLevel) .
		    "case $swcases[$i]:";
		if ($swdests[$i + 1] > $swtable) {
		    decompileBlock($block, $propNum,
				   $swdests[$i], $swtable - 3,
				   $swpos, $continueLabel, $indentLevel + 1);
		}
		elsif ($swdests[$i + 1] > $swdests[$i]) {
		    decompileBlock($block, $propNum,
				   $swdests[$i], $swdests[$i + 1],
				   $swpos, $continueLabel, $indentLevel + 1);
		}
		checkStack($indentLevel);
	    }

	    if ($swdests[$#swdests] < $swtable) {
		$lines[$#lines] .= "\n" . indentStr($indentLevel) .
		    "default:";
		decompileBlock($block, $propNum,
			       $swdests[$#swdests], $swtable - 3,
			       $swpos, $continueLabel, $indentLevel + 1);
		# $swtable - 3 skips the implicit "break" statement
		checkStack($indentLevel);
	    }

	    $lines[$#lines] .= "\n" . indentStr($indentLevel) . "}";
	    $pos = $swpos;
	    $linepos = $pos;
	}
	elsif ($opcode == 76) {	# argc
	    push(@stack, "argc");
	    push(@precstack, 14);
	}
	elsif ($opcode == 83) {	# mod
	    doBinaryOp("%", 10);
	}
	elsif ($opcode == 84) {	# band
	    doBinaryOp("&", 6);
	}
	elsif ($opcode == 85) {	# bor
	    doBinaryOp("|", 4);
	}
	elsif ($opcode == 86) {	# xor
	    doBinaryOp("^", 5);
	}
	elsif ($opcode == 87) {	# bnot
	    doUnaryOp("~", 11);
	}
	elsif ($opcode == 88) {	# shl
	    doBinaryOp("<<", 8);
	}
	elsif ($opcode == 89) {	# shr
	    doBinaryOp(">>", 8);
	}
	elsif ($opcode == 90) {	# new
	    doUnaryOp("new", 11);
	}
	elsif ($opcode == 91) {	# delete
	    doUnaryOp("delete", 11);
	}
	elsif (($opcode & 0xc0) == 0xc0) { # Assignment
	    my $ext = 0;
	    if (($opcode & 0x1c) == 0x1c) {
		$ext = ord1($block, $pos);
		$pos++;
	    }

	    my $dest;
	    if (($opcode & 3) == 0) {
		my $num = ord2s($block, $pos);
		$pos += 2;
		$dest = localStr($num, $propNum);
	    }
	    elsif (($opcode & 3) == 1) {
		my $prop = propStr(ord2s($block, $pos));
		$pos += 2;
		my $obj = pop(@stack);
		$obj = "($obj)" if pop(@precstack) < 13;
		$dest = "$obj.$prop";
	    }
	    elsif (($opcode & 3) == 2) {
		my $index = pop(@stack);
		pop(@precstack);
		my $list = pop(@stack);
		$list = "($list)" if pop(@precstack) < 13;
		$dest = $list . "[" . $index . "]";
	    }
	    elsif (($opcode & 3) == 3) {
		my $prop = pop(@stack);
		pop(@precstack);
		my $obj = pop(@stack);
		$obj = "($obj)" if pop(@precstack) < 13;
		$dest = "$obj.($prop)";
	    }

	    if (($opcode & 0x1c) == 0x14 || ($opcode & 0x1c) == 0x18) {
		my $op = (($opcode & 0x1c) == 0x14 ? "++" : "--");
		if ($opcode & 0x20) {
		    push(@stack, "$op$dest");
		}
		else {
		    push(@stack, "$dest$op");
		}
		push(@precstack, 12);
	    }
	    else {
		my $op;
		$op = ($cmode ? "=" : ":=") if ($opcode & 0x1c) == 0x00;
		$op = "+=" if ($opcode & 0x1c) == 0x04;
		$op = "-=" if ($opcode & 0x1c) == 0x08;
		$op = "*=" if ($opcode & 0x1c) == 0x0c;
		$op = "/=" if ($opcode & 0x1c) == 0x10;
		$op = "%=" if $ext == 1;
		$op = "&=" if $ext == 2;
		$op = "|=" if $ext == 3;
		$op = "^=" if $ext == 4;
		$op = "<<=" if $ext == 5;
		$op = ">>=" if $ext == 6;
		my $val = pop(@stack);

		if (substr($val, 0, 10) eq "*NEWLIST* ") {
		    push(@stack, substr($val, 10));
		    # Keep same precedence
		}
		else {
		    pop(@precstack);
		    # Assignment is lowest precedence, and right associative
		    push(@stack, "$dest $op $val");
		    push(@precstack, 0);
		}
	    }

	    # Mark a list assignment for the later assignment back
	    # into the appropriate lvalue
	    if (($opcode & 3) == 2) {
		$stack[$#stack] = "*NEWLIST* $stack[$#stack]";
	    }
	}
	else {
	    print join("\n", @lines), "\n";
	    die "Unimplemented or invalid opcode $opcode";
	}
    }
    push(@labels, $pos) if $endpos == -1;
}

sub decompile($$) {
    my $block = shift;
    my $propNum = shift;

    my $pos = 0;
    if (ord1($block, $pos) == 77) { # chkargc
	my $numargs = ord1($block, $pos + 1);
	unless ($numargs == 0) {
	    print "(";
	    for (my $i = 1; $i <= ($numargs & 127); $i++) {
		print localStr(-$i, $propNum);
		print ", " if $i < ($numargs & 127);
	    }
	    print ", " if $numargs > 128;
	    print "..." if $numargs >= 128;
	    print ")";
	}
	$pos += 2;
    }
    print " =" if $propNum > 0;
    print " {\n";
    if (ord1($block, $pos) == 24) { # enter
	my $numlocals = ord2($block, $pos + 1);
	unless ($numlocals == 0) {
	    print indentStr($propNum > 0 ? 2 : 1), "local ";
	    for (my $i = 1; $i <= $numlocals; $i++) {
		print localStr($i, $propNum);
		print ", " if $i < $numlocals;
	    }
	    print ";\n\n";
	}
	$pos += 3;
    }

    # Preliminary setup
    %labelNeeded = ();
    @lines = ();
    @labels = ();
    @stack = ();
    @precstack = ();
    $maxLabel = 0;
    decompileBlock($block, $propNum, $pos, -1, -1, -1, $propNum > 0 ? 2 : 1);

    for (my $i = 0; $i <= $#lines; $i++) {
	my $label = $labels[$i];
	print "label$label:\n" if $labelNeeded{$label};
	print $lines[$i], "\n";
    }

    my $label = $labels[$#lines + 1];
    print "label$label:\n" if $labelNeeded{$label};
    print indentStr($propNum > 0 ? 1 : 0), "}";
}

sub processXSI(*$) {
    my $FH = shift;

    $xorseed = read1($FH);
    $xorinc = read1($FH);
}

sub processFMTSTR(*$) {
    my $FH = shift;
    my $len = shift;

    my $block;
    $len = read2($FH);
    read($FH, $block, $len);
    $block = decode($block);

    my $pos = 0;
    while ($pos < $len) {
	my $prop = ord2($block, $pos);
	my $str;
	($str, $pos) = stringStr($block, $pos + 2, "'");
	print "formatstring $str ", propStr($prop), ";\n";
    }
    print "\n";
}

sub processCMPD(*$) {
    my $FH = shift;
    my $len = shift;

    my $block;
    $len = read2($FH);
    read($FH, $block, $len);
    $block = decode($block);

    my $pos = 0;
    my $i = 0;
    my $str;

    while ($pos < $len) {
	print "compoundWord" if $i % 3 == 0;
	($str, $pos) = stringStr($block, $pos, "'");
	print " $str";
	print ";\n" if $i % 3 == 2;
	$i++;
    }
    print "\n";
}

sub printSpecwords($$$) {
    my $block = shift;
    my $pos = shift;
    my $flags = shift;

    my $i = 0, $result = 0;;

    print "    ";
    while (substr($block, $pos, 1) eq $flags) {
	print " = " if $i > 0;
	$strlen = ord1($block, $pos + 1);
	$str = substr($block, $pos + 2, $strlen);
	$str =~ s/\'/\\\'/g;
	print "'$str'";
	$pos += $strlen + 2;
	$result += $strlen + 2;
	$i++;
    }
    return $result;
}

sub processSPECWORD(*$) {
    my $FH = shift;
    my $len = shift;

    my $block;
    $len = read2($FH);
    read($FH, $block, $len);
    $block = decode($block);

    my $pos = 0;

    print "specialWords\n";
    foreach $c ('O', ',', '.', 'A', 'B', 'X', 'N', 'P', 'I', 'T',
		'M', 'R', 'Y') {
	$pos += printSpecwords($block, $pos, $c);
	print ",\n" unless $c eq 'Y';
	print ";\n\n" if $c eq 'Y';
    }
}

$actnum = 0;
@actions = ();

sub assignVerb($$$$) {
    my $verIoProp = shift;
    my $ioProp = shift;
    my $verDoProp = shift;
    my $doProp = shift;

    unless (defined $props[$doProp]) {
	my $actname = $actions[$actnum] || ("Action$actnum");
	$actnum++;
	$props[$verDoProp] = "verDo$actname";
	$propArgs[$verDoProp] =
	  ($verIoProp != 0 ? ['actor', 'iobj'] : ['actor']);;
	$props[$doProp] = "do$actname";
	$propArgs[$doProp] = $propArgs[$verDoProp];
	if ($verIoProp != 0) {
	    $props[$verIoProp] = "verIo$actname";
	    $propArgs[$verIoProp] = ['actor'];
	    $props[$ioProp] = "io$actname";
	    $propArgs[$ioProp] = ['actor', 'dobj'];
	}
    }
}

@objblocks = ();
@objtypes = ();

sub preprocessOBJ(*$) {
    my $FH = shift;
    my $len = shift;
    my $block;

    read($FH, $block, $len);

    # Search for tpl2 properties; and while we're at it, save the
    # decoded object data
    my $pos = 0;

    while ($pos < $len) {
	my $type = ord1($block, $pos);
	my $n = ord2($block, $pos + 1);
	my $sizeuse = ord2($block, $pos + 5);
	my $objblock = decode(substr($block, $pos + 7, $sizeuse));
	$objblocks[$n] = $objblock;
	$objtypes[$n] = $type;

	if ($type == 2) {
	    my $flags = ord2($objblock, 2);
	    my $nsc = ord2($objblock, 4);
	    my $nprop = ord2($objblock, 6);
	    my $pos = 14 + 2 * $nsc;
	    $pos += 2 * $nprop if $flags & 2;

	    for (my $i = 0; $i < $nprop; $i++) {
		my $type = ord1($objblock, $pos + 2);
		my $size = ord2($objblock, $pos + 3);

		if ($type == DAT_TPL2) {
		    my $num = ord1($objblock, $pos + 6);
		    my $intpos = $pos + 7;
		    for (my $j = 0; $j < $num; $j++) {
			assignVerb(ord2($objblock, $intpos + 2),
				   ord2($objblock, $intpos + 4),
				   ord2($objblock, $intpos + 6),
				   ord2($objblock, $intpos + 8));
			$intpos += 16;
		    }
		}
		$pos += 6 + $size;
	    }
	}
	$pos += 7 + $sizeuse;
    }
}

sub preprocessREQ(*$) {
    my $FH = shift;
    my $len = shift;

    my @reqnames = ("Me", "takeVerb", "strObj", "numObj", "pardon",
		    "againVerb", "init", "preparse", "parseError",
		    "commandPrompt", "parseDisambig", "parseError2",
		    "parseDefault", "parseAskobj", "preparseCmd",
		    "parseAskobjActor", "parseErrorParam", "commandAfterRead",
		    "initRestore", "parseUnknownVerb", "parseNounPhrase",
		    "postAction", "endCommand", "preCommand",
		    "parseAskobjIndirect");
    my @reqargs = (undef, undef, undef, undef, undef, undef, undef,
		   ['cmd'],	# preparse
		   ['num', 'str'], # parseError
		   ['type'],	# commandPrompt
		   ['nameString', 'objList'], # parseDisambig
		   ['verb', 'dobj', 'prep', 'iobj'], # parseError2
		   ['obj', 'prep'], # parseDefault
		   ['verb'],	# parseAskobj
		   ['wordList'], # preparseCmd
		   ['actor', 'verb'], # parseAskobjActor
		   ['num', 'str'], # parseErrorParam
		   ['type'],	# commandAfterRead
		   undef,
		   ['actor', 'wordlist', 'typelist', 'errnum'],
				# parseUnknownVerb
		   ['wordlist', 'typelist', 'currentIndex',
		    'complainOnNoMatch', 'isActorCheck'], # parseNounPhrase
		   ['actor', 'verb', 'dobj', 'prep', 'iobj', 'status'],
				# postAction
		   ['actor', 'verb', 'dobj_list', 'prep', 'iobj', 'status'],
				# endCommand
		   ['actor', 'verb', 'dobj_list', 'prep', 'iobj'],
				# preCommand
		   ['actor', 'verb', 'prep', 'objectList']
				# parseAskobjIndirect
		  );

    foreach my $i (0..$#reqnames) {
	my $name = $reqnames[$i];
	my $args = $reqargs[$i];

	return if $len <= 0;
	my $obj = read2($FH);
	if ($obj != 65535) {
	    $objs[$obj] = $name;
	    $funcArgs[$obj] = $args if defined $args;
	}
	$len -= 2;
    }
}

@vocab = ();

sub preprocessVOC(*$) {
    my $FH = shift;
    my $len = shift;

    my $pos = 0;

    while ($pos < $len) {
	my $len1 = read2($FH);
	my $len2 = read2($FH);
	my $prpnum = read2($FH);
	my $objnum = read2($FH);
	my $classflg = read2($FH);

	my $block;
	read($FH, $block, $len1 + $len2);

	unless ($classflg & 2) { # Skip if inherited
	    $block = decode($block);
	    my $str = substr($block, 0, $len1);
	    $str .= " " . substr($block, $len1, $len2) if $len2 != 0;
	    $str =~ s/\'/\\\'/g;

	    # Construct references as needed
	    $vocab[$objnum] = [] unless defined $vocab[$objnum];
	    $vocab[$objnum]->[$prpnum] = []
		unless defined $vocab[$objnum]->[$prpnum];

	    push(@{$vocab[$objnum]->[$prpnum]}, "'$str'");
	}
	$pos += 10 + $len1 + $len2;
    }
}

sub dumpObj($$) {
    my $block = shift;
    my $n = shift;

    my $flags = ord2($block, 2);
    my $nsc = ord2($block, 4);
    print "class " if $flags & 1;
    print objStr($n), ": ";
    for (my $i = 0; $i < $nsc; $i++) {
	print ", " if $i > 0;
	print objStr(ord2($block, 14 + 2 * $i));
    }
    print "object" if $nsc == 0;
    print "\n";

    # Dump vocabulary
    if (defined $vocab[$n]) {
	for (my $i = 0; $i <= $#{$vocab[$n]}; $i++) {
	    next unless defined $vocab[$n]->[$i];
	    print indentStr(1), propStr($i), " = ",
	    join(" ", @{$vocab[$n]->[$i]}), "\n";
	}
    }

    my $nprop = ord2($block, 6);
    my $pos = 14 + 2 * $nsc;
    $pos += 2 * $nprop if $flags & 2;

    for (my $i = 0; $i < $nprop; $i++) {
	my $num = ord2($block, $pos);
	my $type = ord1($block, $pos + 2);
	my $size = ord2($block, $pos + 3);

	if ($type == DAT_CODE) {
	    print indentStr(1), propStr($num);
	    decompile(substr($block, $pos + 6, $size), $num);
	    print "\n";
	}
	elsif ($type == DAT_SYN) {
	    my $synType = substr(propStr($num), 0, 2);
	    if ($synType eq 'do' || $synType eq 'io') {
		print "    ", $synType, "Synonym('",
		substr(propStr(ord2($block, $pos + 6)), 2), "') = '",
		substr(propStr($num), 2), "'\n";
	    }
	}
	elsif ($type == DAT_REDIR) {
	    my $synType = substr(propStr($num), 0, 2);
	    if ($synType eq 'do' || $synType eq 'io') {
		print "    ", propStr($num), " -> ",
		objStr(ord2($block, $pos + 6)), "\n";
	    }
	}
	elsif ($type == DAT_TPL2) {
	    my $num = ord1($block, $pos + 6);
	    my $intpos = $pos + 7;

	    for (my $j = 0; $j < $num; $j++) {
		if (ord2($block, $intpos + 2) != 0) {
		    print "    ioAction";
		}
		else {
		    print "    doAction";
		}
		print "(", objStr(ord2($block, $intpos)), ")"
		    if ord2($block, $intpos) != 65535;
		print " = '";
		# Strip off the 'do' from the doAction property name
		print substr(propStr(ord2($block, $intpos + 8)), 2), "'\n";
		$intpos += 16;
	    }
	}
	elsif ($type != DAT_DEMAND) {
	    my ($str) = valueStr($block, $pos + 6, $type);
	    print "    ", propStr($num), " = $str\n";
	}
	$pos += 6 + $size;
    }
    print ";\n";
}

for (;;) {
    if ($#ARGV >= 1 && $ARGV[0] eq '-s') {
	unless ($return = do $ARGV[1]) {
	    warn "couldn't parse file: $@" if $@;
	    warn "couldn't do $ARGV[1]: $!" unless defined $return;
	    warn "couldn't run $ARGV[1]" unless $return;
	}
	splice(@ARGV, 0, 2);
    }
    elsif ($#ARGV >= 0 && $ARGV[0] eq '-c') {
	$cmode = 1;
	shift(@ARGV);
    }
    else {
	last;
    }
}
if ($#ARGV != 0) {
    die "Usage: detads [-s symbols] [-c] file.gam\n";
}

open(GAM, $ARGV[0]) || die("Couldn't find file $ARGV[0]");
# Assume for now that it is a TADS game; skip to the header flags
seek(GAM, 20, SEEK_SET);
$flags = read1(GAM);
$crypt = $flags & 8;
seek(GAM, 48, SEEK_SET);

print "#pragma C+;\n\n" if $cmode;

for (;;) {
    $namelen = read1(GAM);
    read(GAM, $name, $namelen);
    $nextofs = read4(GAM);
    $curofs = tell(GAM);
    $sectlen = $nextofs - $curofs;
    last if $name eq '$EOF';

    processXSI(GAM, $sectlen) if $name eq 'XSI';
    preprocessOBJ(GAM, $sectlen) if $name eq 'OBJ';
    preprocessREQ(GAM, $sectlen) if $name eq 'REQ';
    processFMTSTR(GAM, $sectlen) if $name eq 'FMTSTR';
    processCMPD(GAM, $sectlen) if $name eq 'CMPD';
    processSPECWORD(GAM, $sectlen) if $name eq 'SPECWORD';
    preprocessVOC(GAM, $sectlen) if $name eq 'VOC';

    seek(GAM, $nextofs, SEEK_SET);
}

close(GAM);

for ($n = 0; $n <= $#objblocks; $n++) {
    next unless defined $objblocks[$n];
    if ($objtypes[$n] == 1) {
	print objStr($n), ": function";
	decompile($objblocks[$n], -$n);
	print "\n";
    }
    elsif ($objtypes[$n] == 2) {
	dumpObj($objblocks[$n], $n);
    }
    else {
	die "Unsupported object type $objtypes[$n]";
    }
}
