#!/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 <
[email protected]>
=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]";
}
}