1. --!movie
  2.  
  3. --****************************************************************************
  4. -- Decompiler
  5. -- @author Valentin Schmidt
  6. -- @version 0.4
  7. --****************************************************************************
  8.  
  9. -- We only use a single global, so we can restore it easily if a loaded movie called
  10. -- "clearGlobals" in its "prepareMovie" handler (which unfortunately can't be blocked).
  11. global $ -- Decompile "decompiler.exe" to see what framework "$" is about ;-)
  12.  
  13. ----------------------------------------
  14. --
  15. ----------------------------------------
  16. on startMovie
  17.     _player.debugPlaybackEnabled = TRUE
  18.  
  19.     -- libs
  20.     $.import("cast")
  21.     $.import("drop")
  22.     $.import("file")
  23.     $.import("filesystem")
  24.     $.import("regex")
  25.     $.import("shell")
  26.  
  27.     _player.itemDelimiter = "."
  28.     $[#version] = float(_player.productVersion.item[1..2]) -- version of current Director runtime
  29.  
  30.     -- movie props
  31.     _movie.stage.rect = rect(0, 0, 480, 320)
  32.     _movie.centerStage = TRUE
  33.     _movie.stage.title = "Drop .dcr/.dxr/.dir/.cct/.cxt/.cst/Projector file(s) into window"
  34.     _movie.stage.titlebarOptions.visible = TRUE
  35.     _movie.stage.bgColor = rgb("#cccccc")
  36.     _movie.puppetTempo(30)
  37.     makeUI()
  38.     _movie.updateStage()
  39.     _movie.stage.visible = TRUE
  40.  
  41.     $.drop.setCallback(#dropEvent)
  42.     $.drop.start()
  43.  
  44.     $[#alerthook] = $.include("data/alert.ls").new()
  45.  
  46.     bin = $.PATH & "bin" & $.PD & "projectorrays.exe"
  47.     if bin contains SPACE then bin = QUOTE&bin&QUOTE
  48.     $[#bin] = bin
  49.  
  50.     $[#magic_macos] = []
  51.     $.magic_macos.append([74, 111, 121, 33])   -- 4A 6F 79 21 = "Joy!" (D10- macOS projector)
  52.     $.magic_macos.append([202, 254, 186, 190]) -- CA FE BA BE (D11+ Projector Resources)
  53.     $.magic_macos.append([206, 250, 237, 254]) -- CE FA ED FE (D11+ Projector Intel Resources)
  54.     $.magic_macos.append([82, 73, 70, 88])     -- 52 49 46 58 = "RIFX" (raw data fork of classic mac app)
  55.  
  56.     the floatPrecision = 1 -- for printing versions
  57. end
  58.  
  59. ----------------------------------------
  60. --
  61. ----------------------------------------
  62. on makeUI
  63.     sn = 1
  64.     y = 10
  65.  
  66.     m = new(#button)
  67.     m.buttonType = #checkBox
  68.     m.name = "fixes"
  69.     m.rect = rect(0, 0, 460, 0)
  70.     m.text = "Apply heuristic fixes to decompiled code"
  71.     m.hilite = TRUE
  72.     _movie.puppetSprite(sn, TRUE)
  73.     sprite(sn).ink = 36
  74.     sprite(sn).loc = point(10, y)
  75.     sprite(sn).member = m
  76.     sn = sn + 1
  77.     y = y + 25
  78.  
  79.     m = new(#button)
  80.     m.buttonType = #checkBox
  81.     m.name = "fix_d4"
  82.     m.rect = rect(0, 0, 460, 0)
  83.     m.text = "Fix decompiled scripts without handlers (Director 4)"
  84.     _movie.puppetSprite(sn, TRUE)
  85.     sprite(sn).ink = 36
  86.     sprite(sn).loc = point(10, y)
  87.     sprite(sn).member = m
  88.     sn = sn + 1
  89.     y = y + 25
  90.  
  91.     m = new(#button)
  92.     m.buttonType = #checkBox
  93.     m.name = "no_dir_decompile"
  94.     m.rect = rect(0, 0, 460, 0)
  95.     m.text = "Don't decompile scripts of unprotected files (.dir and .cst)"
  96.     _movie.puppetSprite(sn, TRUE)
  97.     sprite(sn).ink = 36
  98.     sprite(sn).loc = point(10, y)
  99.     sprite(sn).member = m
  100.     sn = sn + 1
  101.     y = y + 25
  102.  
  103.     m = new(#button)
  104.     m.buttonType = #checkBox
  105.     m.name = "keep"
  106.     m.rect = rect(0, 0, 460, 0)
  107.     m.text = "Keep decompiled scripts (in TMP folder)"
  108.     _movie.puppetSprite(sn, TRUE)
  109.     sprite(sn).ink = 36
  110.     sprite(sn).loc = point(10, y)
  111.     sprite(sn).member = m
  112.     sn = sn + 1
  113.     y = y + 25
  114.  
  115.     m = new(#button)
  116.     m.buttonType = #checkBox
  117.     m.name = "version_only"
  118.     m.rect = rect(0, 0, 460, 0)
  119.     m.text = "Only print Director file version"
  120.     _movie.puppetSprite(sn, TRUE)
  121.     sprite(sn).ink = 36
  122.     sprite(sn).loc = point(10, y)
  123.     sprite(sn).member = m
  124.     sn = sn + 1
  125.  
  126.     m = new(#button)
  127.     m.buttonType = #pushButton
  128.     m.name = "run"
  129.     m.rect = rect(0, 0, 200, 0)
  130.     m.alignment = "center"
  131.     m.text = "Try to run a movie..."
  132.     m.scriptText = "on mouseUp"&RETURN&"run"&RETURN&"end"
  133.     _movie.puppetSprite(sn, TRUE)
  134.     sprite(sn).ink = 36
  135.     sprite(sn).loc = point(140, 290)
  136.     sprite(sn).member = m
  137.     sn = sn + 1
  138. end
  139.  
  140. ----------------------------------------
  141. --
  142. ----------------------------------------
  143. on run
  144.     fn = $.file.selectFileOpen("Select movie", "Director movies (.dir .dxr .dcr),*.dir;*.dxr;*.dcr", "")
  145.     if fn <> "" then
  146.         w = window().new(fn)
  147.         w.open()
  148.     end if
  149. end
  150.  
  151. ----------------------------------------
  152. -- @callback
  153. ----------------------------------------
  154. on dropEvent (drop_data)
  155.  
  156.     repeat with fn in drop_data[#folders]
  157.         _player.itemDelimiter = "."
  158.         if the last item of fn = "app" then
  159.             bn = $.file.getBaseName(fn)
  160.             fn_bin = fn & $.PD & "Contents" & $.PD & "MacOS" & $.PD & bn
  161.             if $.file.exists(fn_bin) then
  162.                 msg "----------------------------------------"
  163.                 msg "Unpacking macOS projector" && QUOTE & $.file.getFileName(fn) & QUOTE & " ..."
  164.                 msg "----------------------------------------"
  165.                 output_dir = $.file.getFilePath(fn) & $.PD & bn & "_contents"
  166.                 $.filesystem.folderCreate(output_dir)
  167.                 unpackProjector(fn_bin, output_dir, TRUE)
  168.                 msg "Done" & RETURN
  169.             end if
  170.             next repeat
  171.         end if
  172.     end repeat
  173.  
  174.     repeat with fn in drop_data[#files]
  175.         ext = $.file.getFileType(fn)
  176.         if ext = "exe" then
  177.             msg "----------------------------------------"
  178.             msg "Unpacking Windows projector" && QUOTE & $.file.getFileName(fn) & QUOTE & " ..."
  179.             msg "----------------------------------------"
  180.             output_dir = $.file.getFilePath(fn) & $.PD & $.file.getBaseName(fn) & "_contents"
  181.             $.filesystem.folderCreate(output_dir)
  182.             unpackProjector(fn, output_dir, FALSE)
  183.             msg "Done" & RETURN
  184.             next repeat
  185.         end if
  186.  
  187.         if not ["dcr", "dxr", "dir", "cct", "cxt", "cst"].getPos(ext) then
  188.  
  189.             -- check if it's a macOS binary
  190.             fp = $.file.fopen(fn, "rb")
  191.             magic = $.file.freadbytes(fp, 4)
  192.             $.file.fclose(fp)
  193.             is_macos_bin = FALSE
  194.             repeat with m in $.magic_macos
  195.                 if magic[1]=m[1] and magic[2]=m[2] and magic[3]=m[3] and magic[4]=m[4] then
  196.                     is_macos_bin = TRUE
  197.                     exit repeat
  198.                 end if
  199.             end repeat
  200.             if is_macos_bin then
  201.                 msg "----------------------------------------"
  202.                 msg "Unpacking macOS projector" && QUOTE & $.file.getFileName(fn) & QUOTE & " ..."
  203.                 msg "----------------------------------------"
  204.                 output_dir = $.file.getFilePath(fn) & $.PD & $.file.getBaseName(fn) & "_contents"
  205.                 $.filesystem.folderCreate(output_dir)
  206.                 unpackProjector(fn, output_dir, TRUE)
  207.                 msg "Done" & RETURN
  208.                 next repeat
  209.             end if
  210.  
  211.             _player.alert("Unsupported file extension:" && ext)
  212.             next repeat
  213.         end if
  214.  
  215.         if member("version_only").hilite then
  216.             checkVersion(fn, ext)
  217.             next repeat
  218.         end if
  219.  
  220.         msg "----------------------------------------"
  221.         msg "Decompiling" && QUOTE & $.file.getFileName(fn) & QUOTE & " ..."
  222.         msg "----------------------------------------"
  223.  
  224.         tmp_dir = getEnvVar("TMP") & $.PD & $.file.getBaseName(fn)
  225.         $.filesystem.folderDeleteRecursive(tmp_dir)
  226.         $.filesystem.folderCreate(tmp_dir)
  227.  
  228.         if ["cct", "cxt", "cst"].getPos(ext) then
  229.             fn_dest = fn & ".cst"
  230.             $.filesystem.fileDelete(fn_dest)
  231.             unprotectCastLib(fn, fn_dest, tmp_dir, member("fixes").hilite, member("fix_d4").hilite)
  232.         else
  233.             fn_dest = fn & ".dir"
  234.             $.filesystem.fileDelete(fn_dest)
  235.             unprotectMovie(fn, fn_dest, tmp_dir, member("fixes").hilite, member("fix_d4").hilite)
  236.         end if
  237.  
  238.         $.shell.shell_setcurrentdir($.PATH)
  239.         if not member("keep").hilite then
  240.             $.filesystem.folderDeleteRecursive(tmp_dir)
  241.         end if
  242.  
  243.         if $.filesystem.fileExists(fn_dest) then
  244.             msg "File processed successfully" & RETURN
  245.         else
  246.             msg "Error: failed to process file" & RETURN
  247.         end if
  248.     end repeat
  249.     _player.alertHook = 0
  250. end
  251.  
  252. ----------------------------------------
  253. --
  254. ----------------------------------------
  255. on unprotectCastLib (fn, fn_dest, script_dir, apply_fixes, fix_d4)
  256.     ext = $.file.getFileType(fn)
  257.  
  258.     -- version check
  259.     if not checkVersion(fn, ext) then return
  260.  
  261.     decompile = ext <> "cst" or not member("no_dir_decompile").hilite
  262.  
  263.     if decompile then
  264.         $.shell.shell_setcurrentdir(script_dir)
  265.         cmd = $.bin && QUOTE & fn & QUOTE
  266.         res = $.shell.shell_cmd(cmd).line[1]
  267.         scripts = $.filesystem.folderList(script_dir)
  268.     end if
  269.  
  270.     _player.alertHook = $.alerthook
  271.     cn = $.cast.attachCastLib("dummy", fn)
  272.  
  273.     if decompile then
  274.         _player.itemDelimiter = "_"
  275.         repeat with scr in scripts
  276.             bn = scr.char[1..scr.length-3]
  277.             mn = integer(bn.item[2])
  278.             -- custom projectorrays.exe uses 32000+ for scripts it failed to assign to members
  279.             if mn > 32000 then next repeat
  280.  
  281.             code = $.file.getString(script_dir & $.PD & scr, "windows-1252")
  282.             code = cleanCode(code, apply_fixes)
  283.  
  284.             msg "Loading script member("&mn&")"
  285.  
  286.             mem = member(mn, cn)
  287.             if mem.linked and mem.type = #script then
  288.                 a = mem.name
  289.                 b = mem.scriptType
  290.                 c = mem.scriptSyntax
  291.                 m = new(#script, mem)
  292.                 m.name = a
  293.                 m.scriptType = b
  294.                 m.scriptSyntax = c
  295.             end if
  296.             mem.scriptText = code
  297.         end repeat
  298.     end if
  299.  
  300.     castLib(cn).save(fn_dest)
  301.     $.cast.detachCastLib(cn)
  302.  
  303.     _player.alertHook = 0
  304. end
  305.  
  306. ----------------------------------------
  307. --
  308. ----------------------------------------
  309. on unprotectMovie (fn, fn_dest, script_dir, apply_fixes, fix_d4)
  310.     ext = $.file.getFileType(fn)
  311.  
  312.     -- version check
  313.     if not checkVersion(fn, ext) then return
  314.  
  315.     decompile = ext <> "dir" or not member("no_dir_decompile").hilite
  316.     --decompile = not member("no_dir_decompile").hilite
  317.  
  318.     if decompile then
  319.         $.shell.shell_setcurrentdir(script_dir)
  320.         cmd = $.bin && QUOTE & fn & QUOTE
  321.         res = $.shell.shell_cmd(cmd).line[1]
  322.         scripts = $.filesystem.folderList(script_dir)
  323.     end if
  324.  
  325.     -- load movie as MIAW
  326.     tmp = $ -- protect against "clearglobals()" call
  327.     w = window().new("tmp")
  328.     _player.alertHook = $.alerthook
  329.     w.fileName = fn
  330.     w.visible = FALSE
  331.     w.movie.pause()
  332.     $ = tmp
  333.  
  334.     -- check for external castlibs
  335.     external_cst = []
  336.     repeat with i = 2 to w.movie.castlib.count
  337.         fn_cst = w.movie.castlib[i].filename
  338.         if fn_cst <> "" and fn_cst <> w.movie.path & w.movie.name then
  339.             external_cst.append(i)
  340.             if ext <> "dir" then
  341.                 $.filesystem.folderCreate(script_dir & $.PD & string(i))
  342.                 $.shell.shell_setcurrentdir(script_dir & $.PD & string(i))
  343.                 msg "Decompiling external cast" && QUOTE & fn_cst & QUOTE
  344.                 cmd = $.bin && QUOTE & fn_cst & QUOTE
  345.                 res = $.shell.shell_cmd(cmd).line[1]
  346.                 v = integer(res.word[3])
  347.                 if v = 1200 then
  348.                     movie_version = 11.5
  349.                 else
  350.                     movie_version = (v/10)/10. -- ignore final digit: 404 => 4.0
  351.                 end if
  352.                 msg res && "(" & movie_version & ")"
  353.                 $.shell.shell_setcurrentdir(script_dir)
  354.             end if
  355.         end if
  356.     end repeat
  357.  
  358.     if decompile then
  359.  
  360.         -- Director 4 fix: replace UNKNOWN_NAME_ handlers either with exitFrame or mouseUp
  361.         if fix_d4 then
  362.             frame_scripts = []
  363.             cntf = w.movie.lastFrame
  364.             w.movie.go(cntf)
  365.             repeat with fn = 1 to cntf
  366.                 w.movie.go(fn)
  367.                 fs = w.movie.frameScript -- unique memberNum
  368.                 if fs=0 then next repeat
  369.                 mn = fs mod 131072 -- 0x20000
  370.                 cn = fs / 131072 + 1
  371.                 frame_scripts.append(w.movie.castlib[cn].member[mn])
  372.                 fn = w.movie.sprite[0].endFrame
  373.             end repeat
  374.         end if
  375.  
  376.         _player.itemDelimiter = "_"
  377.         scripts = $.filesystem.folderList(script_dir)
  378.         repeat with scr in scripts
  379.             if the last char of scr = $.PD then -- scripts of external castlib in subfolder
  380.  
  381.                 delete the last char of scr
  382.                 cn = integer(scr)
  383.  
  384.                 cst_scripts = $.filesystem.folderList(script_dir & $.PD & scr)
  385.                 repeat with cst_scr in cst_scripts
  386.  
  387.                     code = $.file.getString(script_dir & $.PD & scr & $.PD & cst_scr, "windows-1252")
  388.                     code = cleanCode(code, apply_fixes)
  389.  
  390.                     bn = cst_scr.char[1..cst_scr.length-3]
  391.                     mn = integer(bn.item[2])
  392.                     if mn > 32000 then next repeat -- custom projectorrays.exe uses 32000+ for scripts it failed to assign to members
  393.                     msg "Loading script member("&mn&", "&cn&")"
  394.                     tell w
  395.                         mem = member(mn, cn)
  396.                         if mem.linked and mem.type = #script then
  397.                             a = mem.name
  398.                             b = mem.scriptType
  399.                             c = mem.scriptSyntax
  400.                             m = new(#script, mem)
  401.                             m.name = a
  402.                             m.scriptType = b
  403.                             m.scriptSyntax = c
  404.                         end if
  405.                         mem.scriptText = code
  406.                         if fix_d4 then
  407.                             if mem.script.handlers().count = 1 then
  408.                                 if frame_scripts.getPos(mem) then
  409.                                     mem.scriptText = $.regex.replace("^on UNKNOWN_NAME_.*$", "on exitFrame", mem.scriptText, "m")
  410.                                 else
  411.                                     mem.scriptText = $.regex.replace("^on UNKNOWN_NAME_.*$", "on mouseUp", mem.scriptText, "m")
  412.                                 end if
  413.                             end if
  414.                         end if
  415.                     end tell
  416.                 end repeat
  417.             else
  418.                 bn = scr.char[1..scr.length-3]
  419.                 cn = integer(bn.item[1])
  420.                 mn = integer(bn.item[2])
  421.                 if cn = 0 then cn = 1
  422.                 if mn > 32000 then next repeat --
  423.  
  424.                 code = $.file.getString(script_dir & $.PD & scr, "windows-1252")
  425.                 code = cleanCode(code, apply_fixes)
  426.  
  427.                 msg "Loading script member("&mn&", "&cn&")"
  428.                 tell w
  429.                     mem = member(mn, cn)
  430.                     if mem.linked and mem.type = #script then
  431.                         a = mem.name
  432.                         b = mem.scriptType
  433.                         c = mem.scriptSyntax
  434.                         m = new(#script, mem)
  435.                         m.name = a
  436.                         m.scriptType = b
  437.                         m.scriptSyntax = c
  438.                     end if
  439.                     mem.scriptText = code
  440.                     if fix_d4 then
  441.                         if mem.script.handlers().count = 1 then
  442.                             if frame_scripts.getPos(mem) then
  443.                                 mem.scriptText = $.regex.replace("^on UNKNOWN_NAME_.*$", "on exitFrame", mem.scriptText, "m")
  444.                             else
  445.                                 mem.scriptText = $.regex.replace("^on UNKNOWN_NAME_.*$", "on mouseUp", mem.scriptText, "m")
  446.                             end if
  447.                         end if
  448.                     end if
  449.                 end tell
  450.             end if
  451.         end repeat
  452.  
  453.     end if
  454.  
  455.     repeat with n in external_cst
  456.         w.movie.castlib[n].save(w.movie.castlib[n].filename & ".cst")
  457.     end repeat
  458.     w.movie.saveMovie(fn_dest)
  459.  
  460.     tmp = $
  461.     w.fileName = $.PATH & "data" & $.PD & "dummy.dir" -- prevents calling stopMovie in loaded movie
  462.     w.close()
  463.     w.forget()
  464.     $ = tmp
  465. end
  466.  
  467. ----------------------------------------
  468. --
  469. ----------------------------------------
  470. on checkVersion (fn, ext)
  471.     cmd = $.bin && "--no-decompile" && QUOTE & fn & QUOTE
  472.     res = $.shell.shell_cmd(cmd).line[1]
  473.     v = integer(res.word[3])
  474.     if v = 1200 then
  475.         movie_version = 11.5
  476.     else
  477.         movie_version = (v/10)/10. -- ignore final digit: 404 => 4.0
  478.     end if
  479.     msg res && "(" & movie_version & ")"
  480.     version_mayor = bitOr($.version, 0)
  481.     if movie_version > $.version then
  482.         _player.alert("File too new!"&RETURN&RETURN&\
  483.         "This decompiler only supports Director files"&RETURN&\
  484.         "up to version" && version_mayor & ".x."&RETURN&RETURN&\
  485.         "Use the D" & (version_mayor+1) & " decompiler version to handle this file.")
  486.         return FALSE
  487.     else if ext = "dcr" or ext = "cct" then
  488.         if movie_version >= version_mayor then
  489.             msg = "File too new!"&RETURN&RETURN&\
  490.             "This decompiler only supports compressed Director files"&RETURN&\
  491.             "up to version" && (version_mayor-1) & ".x."
  492.             if version_mayor < 12 then
  493.                 put RETURN & RETURN & "Use the D" & (version_mayor+1) & " decompiler version to handle this file." after msg
  494.             end if
  495.             _player.alert(msg)
  496.             return FALSE
  497.         end if
  498.     end if
  499.     return TRUE
  500. end
  501.  
  502. ----------------------------------------
  503. --
  504. ----------------------------------------
  505. on cleanCode (code, apply_fixes)
  506.     code = $.regex.replace("\x0D\x0A", RETURN, code)
  507.     if apply_fixes then code = fixCode(code)
  508.     return code
  509. end
  510.  
  511. ----------------------------------------
  512. -- Applies rules based on regular expressions that try to improve the code that ProjectorRays creates.
  513. -- Dear user: comment out rules that fail and/or add new rules that make sense.
  514. ----------------------------------------
  515. on fixCode (code)
  516.     if voidP($[#regList]) then
  517.         $[#regList] = []
  518.         $[#replList] = []
  519.  
  520.         _addRule($.regex.re("sound\(#(\w+), (.+)\)"), "sound($2).$1()")
  521.         _addRule($.regex.re("(the [A-Za-z]+)\."), "($1).")
  522.         _addRule($.regex.re("([A-Za-z0-9]+)\.char\[(\d+)\].delete\(\)"), "delete char $2 of $1")
  523.         _addRule($.regex.re("([A-Za-z0-9]+)\.line\[(\d+)\].delete\(\)"), "delete line $2 of $1")
  524.         _addRule($.regex.re("the ERROR of sprite"), "the member of sprite")
  525.         _addRule($.regex.re("\.count\(#(\w+)\)"), ".$1.count")
  526.         _addRule($.regex.re("(.+=.+):$", "gm"), "($1):")
  527.         _addRule($.regex.re("open\((.+),(.+)\)"), "_player.open($1,$2)")
  528.         _addRule($.regex.re(" the ERROR"), " ERROR")
  529.         _addRule($.regex.re("set the keyDownScript to "&QUOTE&" nothing\x0D"&QUOTE), "set the keyDownScript to EMPTY")
  530.         _addRule($.regex.re("set the timeoutScript to "&QUOTE&" nothing\x0D"&QUOTE), "set the timeoutScript to EMPTY")
  531.         _addRule($.regex.re("set the keyDownScript to "&QUOTE&" (\w+)\x0D"&QUOTE), "set the keyDownScript to "&QUOTE&"$1"&QUOTE)
  532.         _addRule($.regex.re("(window\(.+\))\.windowType"), "$1().type")
  533.  
  534.         q = QUOTE&"&QUOTE&"&QUOTE
  535.         _addRule($.regex.re("set the timeoutScript to "&QUOTE&" go movie "&QUOTE&"(.+)"&QUOTE&"\x0D"&QUOTE),\
  536.         "set the timeoutScript to "&QUOTE&"go movie "&q&"$1"&q&" "&QUOTE)
  537.     end if
  538.     code = $.regex.re_replace_multi($.regList, $.replList, code)
  539.  
  540.     -- prepend line "global ERROR" to scripts that contain ERROR, so they (might) still compile.
  541.     if count($.regex.match("ERROR", code, "")) then put "global ERROR"&RETURN before code
  542.     return code
  543. end
  544.  
  545. ----------------------------------------
  546. --
  547. ----------------------------------------
  548. on _addRule (reg, repl)
  549.     $.regList.append(reg)
  550.     $.replList.append(repl)
  551. end
  552.  
  553. ----------------------------------------
  554. --
  555. ----------------------------------------
  556. on bytesOffset (needle, haystack, start_pos)
  557.     if voidP(start_pos) then start_pos = 1
  558.     if stringP(needle) then
  559.         s = needle
  560.         needle = []
  561.         len = s.length
  562.         repeat with i = 1 to len
  563.             if s.char[i] = "." then
  564.                 needle[i] = -1
  565.             else
  566.                 needle[i] = chartonum(s.char[i])
  567.             end if
  568.         end repeat
  569.     end if
  570.     cnt = haystack.length - needle.count + 1
  571.     cnt2 = needle.count
  572.     repeat with i = start_pos to cnt
  573.         ok = TRUE
  574.         repeat with j = 1 to cnt2
  575.             if needle[j] < 0 then next repeat
  576.             if haystack[i+j-1]<>needle[j] then
  577.                 ok = FALSE
  578.                 exit repeat
  579.             end if
  580.         end repeat
  581.         if ok then return i
  582.     end repeat
  583.     return 0
  584. end
  585.  
  586. ----------------------------------------
  587. --
  588. ----------------------------------------
  589. on unpackProjector (exe_file, output_dir, is_mac)
  590.     if the last char of output_dir<>$.PD then put $.PD after output_dir
  591.     strip = 32 -- 0 for raw data fork
  592.  
  593.     fp = $.file.fopen(exe_file, "rb")
  594.     exe_size = $.file.fsize(fp) - strip
  595.     $.file.fseek(fp, strip)
  596.     data = $.file.freadbytes(fp, exe_size)
  597.     $.file.fclose(fp)
  598.  
  599.     if exe_size<1 then return _player.alert("Couldn't open "&exe_file&"!")
  600.  
  601.     if is_mac then
  602.         is_bigendian = TRUE
  603.         start_pos = bytesOffset("RIFX....APPL", data)
  604.         if start_pos = 0 then
  605.             is_bigendian = FALSE
  606.             start_pos = bytesOffset("XFIR....LPPA", data)
  607.         end if
  608.     else
  609.         is_bigendian = FALSE
  610.         start_pos = bytesOffset("XFIR....LPPA", data)
  611.         if start_pos = 0 then
  612.             is_bigendian = TRUE
  613.             start_pos = bytesOffset("RIFX....APPL", data)
  614.         end if
  615.     end if
  616.     if start_pos = 0 then
  617.         return _player.alert("Couldn't identify "&exe_file&" as Director projector!")
  618.     end if
  619.  
  620.     endian = [#littleEndian, #bigEndian][is_bigendian+1]
  621.     data.endian = endian
  622.  
  623.     -- find XFIR and RIFF chunks
  624.     res = []
  625.     res2 = []
  626.     xres = []
  627.  
  628.     start_pos = start_pos + 12
  629.     if is_bigendian then
  630.         repeat with i = start_pos to exe_size
  631.             c = data[i]
  632.             if c = 82 then -- R
  633.                 data.position = i + 1
  634.                 s1 = data.readRawString(3)
  635.                 if s1 = "IFX" then
  636.                     data.position = data.position + 4
  637.                     s2 = data.readRawString(4)
  638.                     if s2 = "MV93" or s2 = "MC95" then --
  639.                         res.add([i, s2])
  640.                         i = i+8
  641.                     else if s2 = "FGDM" OR s2 = "FGDC" then
  642.                         res2.add([i, s2])
  643.                         i = i + 8
  644.                     end if
  645.                     i = i+3
  646.                 else if s1 = "IFF" then
  647.                     data.position = data.position + 4
  648.                     s2 = data.readRawString(8)
  649.                     if s2 = "XtraFILE" then
  650.                         xres.add(i)
  651.                         i = i + 12
  652.                     end if
  653.                     i = i + 3
  654.                 end if
  655.             end if
  656.         end repeat
  657.     else
  658.         repeat with i = start_pos to exe_size
  659.             c = data[i]
  660.             if c = 88 then -- X
  661.                 data.position = i + 1
  662.                 s1 = data.readRawString(3)
  663.                 if s1 = "FIR" then
  664.                     data.position = data.position + 4
  665.                     s2 = data.readRawString(4)
  666.                     if s2 = "39VM" then
  667.                         res.add([i, s2])
  668.                         i = i + 8
  669.                     else if s2 = "MDGF" OR s2 = "CDGF" then
  670.                         res2.add([i, s2])
  671.                         i = i + 8
  672.                     end if
  673.                 end if
  674.                 i = i + 3
  675.             else if c = 82 then -- R --> RIFF....XtraFILE
  676.                 data.position = i + 1
  677.                 s1 = data.readRawString(3)
  678.                 if s1 = "IFF" then
  679.                     data.position = data.position + 4
  680.                     s2 = data.readRawString(8)
  681.                     if s2 = "XtraFILE" then
  682.                         xres.add(i)
  683.                         i = i + 12
  684.                     end if
  685.                     i = i + 3
  686.                 end if
  687.             end if
  688.         end repeat
  689.     end if
  690.  
  691.     if res.count = 0 then
  692.         if res2.count = 0 then
  693.             return _player.alert("Nothing found to extract!")
  694.         else
  695.             compressFlag = 1
  696.             res = res2
  697.         end if
  698.     else
  699.         compressFlag = 0
  700.     end if
  701.  
  702.     --  header-template
  703.     header = bytearray()
  704.     header.endian = endian
  705.     header.writeRawString(["XFIR", "RIFX"][is_bigendian+1], 4)
  706.     header.writeInt32(exe_size - 8 + strip)
  707.     header.writeRawString(["39VMpami", "MV93imap"][is_bigendian+1], 8)
  708.     header.writeInt32(24)
  709.     header.writeInt32(1)
  710.     header.writeInt32(0) --> size
  711.     header.writeInt32(1923)
  712.  
  713.     -- extract file names from DICT
  714.     dir_names = []
  715.     x32_names = []
  716.  
  717.     -- find Dict chunk (littleEndian: tciD, bigEndian: Dict)
  718.     pos = res[1][1] -- position of first XFIR/RIFX
  719.     needle = [[116,99,105,68], [68,105,99,116]][is_bigendian+1] -- tciD / Dict
  720.     repeat with i = pos - 3 down to 1
  721.         if data[i] = needle[1] then
  722.             if data[i + 1] = needle[2] then
  723.                 if data[i + 2] = needle[3] then
  724.                     if data[i + 3] = needle[4] then
  725.                         exit repeat
  726.                     end if
  727.                 end if
  728.             end if
  729.         end if
  730.     end repeat
  731.  
  732.     if i > 1 then -- found!
  733.         data.position = i
  734.         dict = data.readByteArray(pos-i)
  735.         dict.endian = endian
  736.         dict.position = 25
  737.         cnt = dict.readInt32()
  738.  
  739.         -- no idea what's going on here
  740.         if cnt > 65535 then
  741.             dict.endian = [#bigEndian, #littleEndian][is_bigendian+1]
  742.             dict.position = 25
  743.             cnt = dict.readInt32()
  744.             dict.endian = endian
  745.         end if
  746.  
  747.         if cnt = 1 then
  748.             dir_names.add("main.dxr")
  749.         else
  750.             pt = cnt*8 + [65, 68][is_bigendian+1]
  751.             repeat with i = 1 to cnt
  752.                 len = dict[pt] - [0, 3][is_bigendian+1]
  753.                 dict.position = pt + 4
  754.                 fn = dict.readRawString(len)
  755.                 if fn contains "Xtras:" or ["x32", "cpio"].getPos($.file.getFileType(fn)) then
  756.                     x32_names.add($.file.getFileName(fn))
  757.                 else
  758.                     dir_names.add($.file.getFileName(fn))
  759.                 end if
  760.                 if i<cnt then
  761.                     -- find next
  762.                     pt = pt + 4 + len
  763.                     repeat while (dict[pt] = 0)
  764.                         pt = pt + 1
  765.                     end repeat
  766.                 end if
  767.             end repeat
  768.         end if
  769.     end if
  770.  
  771.     if compressFlag then -- compressed files
  772.         msg "Files in projector are compressed"
  773.  
  774.         file_num = 0
  775.         repeat with r in res
  776.             pos = r[1]
  777.             file_num = file_num + 1
  778.             fn = dir_names[file_num]
  779.  
  780.             ext = $.file.getFileType(fn)
  781.             if ext = "" then -- just guessing
  782.                 put ".cct" after fn
  783.             else if ext = "cst" then
  784.                 fn = fn.char[1..fn.length - 3] & "cct"
  785.             else
  786.                 fn = fn.char[1..fn.length - 3] & "dcr"
  787.             end if
  788.  
  789.             msg "Extracting " & QUOTE & fn & QUOTE & " ..."
  790.  
  791.             data.position = pos + 4
  792.             chunk_size = data.readInt32()
  793.             fp = $.file.fopen(output_dir & fn, "wb")
  794.             data.position = pos
  795.             chunk = data.readByteArray(chunk_size + 8)
  796.             $.file.fwritebytes(fp, chunk)
  797.             $.file.fclose(fp)
  798.         end repeat
  799.  
  800.     else -- non-compressed files
  801.         msg "Files in projector are not compressed"
  802.  
  803.         file_num = 0
  804.         repeat with r in res
  805.             pos = r[1] + 32 + strip
  806.             file_num = file_num + 1
  807.             header.position = 25
  808.             header.writeInt32(pos - 1 + 12)
  809.             fn = dir_names[file_num] -- err
  810.  
  811.             ext = $.file.getFileType(fn)
  812.             if ext = "" then -- just guessing
  813.                 put ".cxt" after fn
  814.             else if ext = "cst" then
  815.                 fn = fn.char[1..fn.length - 3] & "cxt"
  816.             else
  817.                 fn = fn.char[1..fn.length - 3] & "dxr"
  818.             end if
  819.  
  820.             msg "Extracting " & QUOTE & fn & QUOTE & " ..."
  821.  
  822.             fp = $.file.fopen(output_dir & fn, "wb")
  823.             $.file.fwritebytes(fp, header)
  824.             $.file.fwritebytes(fp, data)
  825.             $.file.fclose(fp)
  826.         end repeat
  827.     end if
  828.  
  829.     -- extract xtras
  830.     file_num = 0
  831.     data.endian = #bigEndian -- always bigEndian
  832.     repeat with pos in xres
  833.         data.position = pos + 4
  834.         chunk_size = data.readInt32()
  835.         data.position = pos + 48
  836.         chunk = data.readBytearray(chunk_size - 40)
  837.         file_num = file_num + 1
  838.         fn = x32_names[file_num]
  839.         msg "Extracting " & QUOTE & fn & QUOTE & " ..."
  840.         chunk.uncompress()
  841.         $.file.putBytes(output_dir & fn, chunk)
  842.     end repeat
  843. end
  844.  
  845. ----------------------------------------
  846. -- prints to message window without adding quotes
  847. -- (based on CommandLine xtra)
  848. ----------------------------------------
  849. on msg (str)
  850.     printMsg(str)
  851.     printMsg(RETURN)
  852. end
  853.  
[raw code]