#!/usr/bin/perl -w =head1 NAME detads - decompile TADS 2 .GAM files =head1 SYNOPSIS B [B<-c>] [B<-s> I] I =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 Input a symbols file. The I 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 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 =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]"; } }