{"article":{"slug":"getting-old-macromedia-director-games-to-run-on-modern-hardware","title":"Getting old Macromedia Director Games to run on modern Hardware","subtitle":null,"summary":"WerWolv revives German point-and-click adventure games from the early 2000s, such as \"Findus bei den Mucklas\", by reverse engineering the CD-check copy protection that Macromedia Director games shared, patching it so the games run from copies on modern Windows, and notes many similar titles used the same mechanism.","content_type":"blog_post","language":"en","canonical_url":"https://werwolv.net/posts/macromedia_copy_protection/","author":{"name":"WerWolv","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"WerWolv","url":"https://werwolv.net/","listing_slug":null,"listing":null},"topics":[{"name":"Reverse Engineering","slug":"reverse-engineering","url":"https://listedarticles.com/topics/reverse-engineering"},{"name":"Retro Computing","slug":"retro-computing","url":"https://listedarticles.com/topics/retro-computing"},{"name":"Games","slug":"games","url":"https://listedarticles.com/topics/games"}],"about_listings":[],"cover_image_url":null,"license":"CC-BY-NC-SA-4.0","word_count":2806,"reading_minutes":12,"published_at":"2026-10-11T02:08:36.980Z","added_at":"2026-10-11T02:08:36.980Z","updated_at":"2026-10-11T02:08:36.980Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/getting-old-macromedia-director-games-to-run-on-modern-hardware","markdown_url":"https://listedarticles.com/articles/getting-old-macromedia-director-games-to-run-on-modern-hardware.md","example":false,"citation":"WerWolv, WerWolv. \"Getting old Macromedia Director Games to run on modern Hardware.\" 11 Oct 2026. https://werwolv.net/posts/macromedia_copy_protection/ (CC-BY-NC-SA-4.0)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://werwolv.net/posts/macromedia_copy_protection/"},"body_markdown":"# Introduction\n\nWhen I was a kid in the early 2000s, I spent a lot of time playing games that I borrowed from our local library. Many of them were German point-and-click adventure games, since those were the games that ran on my dad’s Windows 95 computer. Even back then, I was interested in tech and always tried to figure out how I could keep playing these games after giving them back to the library, but I, of course, never managed to do it.\nA few days ago, I stumbled across one of those failed attempts again: a set of CDs onto which I tried to burn a copy of “Findus bei den Mucklas.” I figured that maybe now was the time where I finally have the skills to do it!\n\n# Getting a Disk Image\n\nObviously, the CD I burned back then was unusable, both because CD burners at the time often weren’t able to properly read or write copy-protected CDs and because I haven’t had a computer with a disc drive in the past 10 years.\n\n![disappointment.png](/assets/limited_by_the_technology_of_my_time.CTomc4_G.png)\n\ndisappointment.png\n\nThankfully, some helpful soul uploaded a set of full disk images for that and all the other games in the series to [![](https://www.google.com/s2/favicons?domain=archive.org&sz=128)archive.org](https://archive.org/details/cd-01_202304). It’s not piracy if it’s on there, right?\n\n## Unpacking the Image\n\nThe game came as a set of three files: a `.ccd`, a `.cue` and a `.img` file. Those are apparently produced by the old [![](https://www.google.com/s2/favicons?domain=en.wikipedia.org&sz=128)CloneCD program](https://en.wikipedia.org/wiki/CloneCD) and contain the raw data from the disc, plus additional out-of-band information such as track metadata. Games from that time often came with audio tracks as well, so you could just pop these CDs into a CD player and listen to the soundtrack or other bundled media.\n\nLooking at the `.cue` file shows that there’s a MODE1 trackThese are tracks meant for storing regular data, such as a file system. They are usually skipped by audio players. at the start containing the game files, followed by four `AUDIO` tracks:\n\nCD01.cue\n\n```\nFILE \"CD01.img\" BINARY\n\nTRACK 1 MODE1/2352\n\nINDEX 1 00:00:00\n\nTRACK 2 AUDIO\n\nINDEX 1 45:08:37\n\nTRACK 3 AUDIO\n\nINDEX 1 46:38:44\n\nTRACK 4 AUDIO\n\nINDEX 1 47:08:50\n\nTRACK 5 AUDIO\n\nINDEX 1 47:11:58\n```\n\nTo access the data, I just used the old `ccd2iso` tool, which failed when it hit the first `AUDIO` track but was still able to convert the start of the file into a regular ISO that could be mounted.\n\n# Copy Protection 1\n\nMounting the file and trying to copy the data over, I ran into what I believe to be the first copy protection measure:\n\n![Duplicate files popup while extracting the archive](/assets/duplicate_files.CKhQrlWW.png)\n\nDuplicate files popup while extracting the archive\n\nSome of the files seem to be in the disk image multiple times: once as the real copy and once as a second copy that was either 0 bytes or 256 bytes long and contained random garbage.\nMy guess is that they did that on purpose to trick some tools into displaying only the fake decoy copy of the file instead of the real one by creating deliberately malformed directory entries on the disk. Windows seems to be able to always read the correct file here (otherwise it wouldn’t work there either of course) but with the different tools I tried, each one of them showed a different combination of files in the end.\n\nUsing a combination of `PeaZip`, `Ark`, `Dolphin` and `7z`, I ultimately managed to extract the right files from the ISO, but it was mostly manual labour: choosing the files that were the right size and skipping the decoy ones.\n\n![Final files that were extracted from the ISO image](/assets/extracted_files.BPaeBJGH.png)\n\nFinal files that were extracted from the ISO image\n\nAt this point, just running the installer seemed easiest, and it surprisingly worked. I ran it using `WINEPREFIX=$(pwd)/prefix wine \"Installiere Findus4.exe\"` to make it install into a separate Wine prefix, from which I could copy the installed data afterwards.\n\n# Copy Protection 2 - Electric Boogaloo\n\nExecuting `Findus4.exe` now, of course, didn’t just work, but instead showed a nice error popup saying that the CD was missing.\n\n![Missing CD Error popup](/assets/missing_cd.Bywsoz1T.png)\n\nMissing CD Error popup\n\nSimply searching for that message using `rg -a \"Lege die CD\"` showed that they just read it from the `Pettson3.ini` file. *They apparently didn’t even bother renaming that file from the previous game.*\n\nPettson3.ini\n\n```\n[CDROM]\n\nCDROM=?\n\n[GAME1]\n\nVind=0\n\n[ALERT1]\n\nMESSG=Lege die CD Findus4 ein und versuche es noch einmal.\n\n[SCREEN]\n\nFULLSCREEN=1\n```\n\nSearching for a few strings ultimately led to `messg` being found in `Findus4.exe`. My first instinct was, of course, to just throw the binary into Ghidra and start looking for it. Surprisingly, the string didn’t show up anywhere using the `Search -> For Strings...` option, which usually means that the string isn’t actually part of the PE data but some sort of extra payload appended to the end of the executable file.\n\n## Extracting the Embedded Script\n\nAfter some more digging, I found that these games are all `Macromedia Director Projector` executables. These were generated by Macromedia Director and basically contain the whole runtime for interpreting Director files, with a `.dxr` file concatenated onto the end. That file is what gets executed when the program runs. It made sense that the CD check would be part of the embedded Director file, because why would the runtime read from a config file called `Pettson3.ini`?\n\nDuring my research, I stumbled across [![](https://www.google.com/s2/favicons?domain=github.com&sz=128)ProjectorRays](https://github.com/ProjectorRays/ProjectorRays), which is a disassembler and decompiler for exactly these files. They have the file signature `RIFX` or `XFIR`, depending on the endianness of the target system, which makes them pretty easy to identify. Using ImHex, I found a few occurrences of those strings in the binary. Based on the ProjectorRays source code, the format of these `.dxr` files starts with something like this:\n\ndirector.hexpat\n\n```\nenum RifxType : u32 {\n\nMV93 = le u32(\"MV93\"),\n\nMV95 = le u32(\"MV95\"),\n\nFGDM = le u32(\"FGDM\"),\n\nFGDC = le u32(\"FGDC\"),\n\nAPPL = le u32(\"APPL\")\n\n};\n\nstruct Content {\n\ntype::Size<u32> length;\n\nRifxType type;\n\n// ...\n\n};\n\nstruct DirectorFile {\n\nchar magic[4];\n\nif (magic == \"RIFX\")\n\nbe Content content [[inline]];\n\nelse if (magic == \"XFIR\")\n\nle Content content [[inline]];\n\nelse\n\nstd::error(\"Invalid Director File!\");\n\n};\n```\n\nOnly two of the `XFIR` occurrences had a valid type after them: one was `APPL` at address `0x00222151`, which, according to ScummVM, identifies a Director application wrapper (the executable file we are working with), and the other was `MV93` at address `0x002222F0`, which seems to be the regular, uncompressed Director movie script.\n\nI initially tried to copy that chunk out of the executable and process it separately using ProjectorRays, but it turned out that all the offsets used inside the appended file are absolute and account for the `.dxr` file not starting at the beginning of the executable. Instead, I ended up adding an `--offset` option to the CLI that lets you seek forward to the actual offset of the embedded binary. The PR can be found here in case it’s useful to somebody else: [![](https://www.google.com/s2/favicons?domain=github.com&sz=128)ProjectorRays#63](https://github.com/ProjectorRays/ProjectorRays/pull/63).\n\n## Disassembling the Director File\n\nFinally, after all this trouble, we can dump the embedded Director file’s contents using the following command.\n\nTerminal window\n\n```\n./projectorrays decompile Findus4.exe -o output --dump-scripts --offset 0x002222F0\n\noutput\n\n├── Findus4\n\n│   └── casts\n\n│       └── Internal\n\n│           ├── MovieScript 1.lasm\n\n│           └── MovieScript 1.ls\n\n└── Findus4.dir\n```\n\nTo my surprise, this generated both a disassembly file (the `.lasm` file) **and** a decompiled version of the script (the `.ls` file), which still contains all the original function and variable names!\n\n![Decompiled Director Script](/assets/decompiled_script.DEUGyPJA.png)\n\nDecompiled Director Script\n\n## Analyzing the Script\n\nThe generated file is only around 150 lines long and contains the game’s initialization logic. It’s written in a programming language called [![](https://www.google.com/s2/favicons?domain=en.wikipedia.org&sz=128)Lingo](https://en.wikipedia.org/wiki/Lingo_(programming_language)), which is what Macromedia Director uses. Even without knowing the language, it’s pretty straightforward to read. It didn’t take long to find the function from which that `\"messg\"` string originally came. As seen here, they implemented a primitive `.ini` file parser to parse the `Pettson3.ini` file and get a few config values out of it, one being the error message displayed when no CD can be found.\n\nMovieScript 1.lasm\n\n```\n-- ...\n\non analyseIniText iniText\n\nrepeat with mening = 1 to iniText.line.count\n\nReddString = StripString(iniText.line[mening])\n\nif ReddString contains \"[\" then\n\nReddString = StripString(iniText.line[mening + 1])\n\nantalBokstaver = ReddString.char.count\n\nif ReddString contains \"cdrom\" then\n\ncdromenhet = ReddString.char[7..antalBokstaver]\n\nend if\n\nif ReddString contains \"Vind\" then\n\nantalBokstaver = ReddString.char.count\n\nvindspelAktivt = integer(ReddString.char[6])\n\nend if\n\nif ReddString contains \"messg\" then\n\nantalBokstaver = ReddString.char.count\n\nalertmessage = ReddString.char[7..antalBokstaver]\n\nend if\n\nif ReddString contains \"fullscreen\" then\n\nantalBokstaver = ReddString.char.count\n\nfullscreen = integer(ReddString.char[12])\n\nend if\n\nend if\n\nend repeat\n\nresultat = [#cdromenhet: cdromenhet, #vindspelAktivt: vindspelAktivt, #alertmessage: alertmessage, #screen: fullscreen]\n\nreturn resultat\n\nend\n\n-- ...\n```\n\n#### \n\nI always find it super fun to read these old scripts and look at the weird mix of broken English, identifiers written in foreign languages (Swedish in this case, I think?) and probably extremely buggy code.\n\nA bit further down, we can also find the function where the error text is actually being used:\n\nMovieScript 1.ls\n\n```\non checkpccdrompath\n\nseperator = the last char in the applicationPath\n\ndrivelista = [\"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\", \"k\", \"l\", \"m\", \"n\", \"o\", \"p\", \"q\", \"r\", \"s\", \"t\", \"u\", \"v\", \"w\", \"x\", \"y\", \"z\"]\n\nRetvar = VOID\n\nrepeat with Drive in drivelista\n\niscdrom = baDiskInfo(Drive, \"type\")\n\nif iscdrom = \"CD-ROM\" then\n\nPath = Drive & \":\" & seperator & \"p3media\"\n\nif checkoncd(Path) then\n\nRetvar = Drive\n\nexit repeat\n\nend if\n\nend if\n\nend repeat\n\nreturn Retvar\n\nend\n\n-- ... Part of setSearchPaths function\n\ndriveletter = checkpccdrompath()\n\nif not voidp(driveletter) then\n\nini[#cdromenhet] = driveletter & \":\"\n\nhdMedia = mp & \"p3media\"\n\nhdljud = mp & \"p3media\\p3ljud\"\n\nhdcast = mp & \"p3media\\p3cast\"\n\ncdMedia = string(ini.cdromenhet & \"\\p3media\")\n\ncdLjud = string(ini.cdromenhet & \"\\p3media\\p3ljud\")\n\ncdCast = string(ini.cdromenhet & \"\\p3media\\p3cast\")\n\nthe searchPaths = [hdMedia, hdljud, hdcast, cdMedia, cdLjud, cdCast]\n\nelse\n\nalert(string(ini.alertmessage))\n\nquit()\n\nend if\n\n-- ...\n```\n\nAgain, pretty straightforward code. They just loop over all possible Windows drives, trying to find one that contains a CD-ROM with the `p3media` folder on it. If one is found, the search paths are set up. Otherwise, the error message we saw is displayed and the game calls `quit()`.\n\n## Patching the Script\n\nAnalyzing the `setSearchPaths` function a bit, I figured the easiest way to avoid executing the `quit()` branch while still allowing it to set up the search paths correctly would be to patch the `if` to jump to the next instruction instead of the `else` branch if the check failed. That would result in the CD paths being a bit weird, but I figured it would probably still work, since the game lets you do a “Full Install” where it copies all the required data to the hard disk and presumably loads the files from there.\n\nIn the assembly file, the instructions we want to patch are the following:\n\nMovieScript 1.lasm\n\n```\n< 95 00 60       > [246] jmpifz [342] ......... if not voidp(driveletter) then / else\n\n< 4C 08          > [249] getlocal 8 ........... <ini>\n```\n\nProjectorRays sadly doesn’t yet support assembling the code back, but thankfully, the Lingo bytecode is incredibly simple, so we can do it manually with ImHex. Every instruction starts with a single opcode byte, followed by a big-endian integer that’s either 1, 2 or 4 bytes long. To make the manual patching a bit easier, I also made ProjectorRays output the bytes that make up each instruction (also submitted upstream to ProjectorRays).\n\nNow for the patching: `jmpifz` has the opcode byte `0x95`, followed by the big-endian 16-bit relative offset `0x0060`, or 96 bytes, indicating where to jump. To simply jump to the next instruction, we need to jump 3 bytes forward (from offset 246 to offset 249), so we want to patch the instruction bytes to be `< 95 00 03 >`. Searching for the original byte sequence yielded only a single result at address `0x002252E8`, which I overwrote with the new instruction.\n\nAnd with that, the game’s intro video was playing! 🎉\n\n![The game's intro video playing](/assets/game_starting.B9PgIx08.png)\n\nThe game's intro video playing\n\n# Copy Protection 3 - Rise of the Machines\n\nSadly, there was more to it.\n\n![disappointment2.png](/assets/copy_protection.C1PIXWDL.png)\n\ndisappointment2.png\n\nAgain, simply searching for that string yielded two results, and I decided to start with `intro.dxr` for now.\n\nGrepping Error Message 2\n\n```\n➜  rg -a 'Disc Error !'\n\nP3media/intro.dxr\n\n...\n\nP3media/huset.dxr\n\n...\n```\n\nThrowing it into ProjectorRays again resulted in a few more scripts, one of which was straightforwardly called `BehaviorScript 36 - Call Copy-X CD protection DLL (OPTGRAPH.DLL).ls`. *I didn’t need that much handholding, devs, but thanks either way.*\n\nInside was, as the filename suggested, some code that loads the `optgraph.dll` located next to the game executable and calls its `initdisplay()` function. It then checks the return value of that function, and if it doesn’t match some magic value, the game is once again aborted.\n\nBehaviorScript 36 - Call Copy-X CD protection DLL (OPTGRAPH.DLL).ls\n\n```\non exitFrame me\n\nif the platform contains \"Mac\" then\n\nnothing()\n\nelse\n\nif Ok = 0 then\n\nalert(\"Disc Error !\")\n\navslutaSpelet()\n\nelse\n\nif getReturnVal(me) <> 26079537 then\n\nalert(\"Disc Error !\")\n\navslutaSpelet()\n\nend if\n\nend if\n\nend if\n\nend\n```\n\nInterestingly, they didn’t even bother implementing the copy protection for Mac…\n\nTo get past this check, we have a few options. We could make the game believe that it’s running on a Mac, patch the comparison against the magic number or patch the DLL to make it always return that constant. Given that the identical check is implemented again in `huset.dxr` and that I didn’t really want to reverse engineer the runtime and patch the platform detection, I decided to patch `optgraph.dll` instead.\n\n## Patching the Support DLL\n\nOpening the DLL in Ghidra showed that it essentially contained only that one function and nothing else.\n\n`initdisplay()` itself is a bit of a convoluted mess where they send a bunch of low-level commands to the CD drive to somehow construct that magic value from it.\n\nGhidra Decompile - OPTGRAPH.DLL\n\n```\n// ...\n\nFUN_10001e50(&local_44,0,0x10);\n\nlocal_3c = 3;\n\nmciSendCommandA(local_c,0x814,0x100);\n\nlocal_3c = 1;\n\nlocal_2c = local_40;\n\nmciSendCommandA(local_c,0x814,0x102,&local_44);\n\nlocal_28 = (uint *)(local_40 & 0xff);\n\nlocal_3c = 1;\n\nlocal_48 = (char *)(local_40 >> 8 & 0xff);\n\nlocal_38 = 4;\n\nlocal_40._0_2_ = local_40._1_2_;\n\nmciSendCommandA(local_c,0x814,0x112,&local_44);\n\n// ...\n```\n\nI didn’t spend too much time reversing any of this but instead just used Ghidra’s wonderful `Patch Instruction` option to replace the first few instructions with a plain and boring `MOV` and `RET`.\n\nGhidra Assembly Listing - OPTGRAPH.DLL\n\n```\n**************************************************************\n\n*                          FUNCTION                          *\n\n**************************************************************\n\nint __stdcall initdisplay(void)\n\nassume FS_OFFSET = 0xffdff000\n\nint               EAX:4          <RETURN>\n\n0x1060  1  initdisplay\n\ninitdisplay\n\n10001060 b8 31 f1        MOV        EAX, 26079537\n\n8d 01\n\n10001065 c3              RET\n\n10001066 05              ??         05h\n\n10001067 00              ??         00h\n\n10001068 00              ??         00h\n```\n\nAfter struggling for an embarrassingly long time to get Ghidra to properly export the DLL again, I finally managed to do it through `File -> Export -> Original File`.\n\nThis seemed to have been the last thing needed, since the game now gets all the way to the menu screen!\n\n![Start menu screen of the game](/assets/start_menu.BS9j7iUD.png)\n\nStart menu screen of the game\n\n## Writing an auto patcher\n\nAfter I was done, I wrote a quick Python script that can apply the patches discussed in this post. This exact script will only work for the German version of the Game that I linked above. However, it shouldn’t be hard to adapt to other versions as well (probably only requires fixing the hash and the offset)\n\npatch\\_findus4.py\n\n```\n#!/usr/bin/env python3\n\nimport hashlib\n\nimport sys\n\nfrom pathlib import Path\n\ngame_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()\n\npatches = [\n\n(\n\ngame_dir / \"Findus4.exe\",\n\n\"b33312ff9086c1cbc09d9cde6baa0eff4220a1ed354c9a3211ff6787fff46e39\",\n\n0x2252E8,\n\nbytes.fromhex(\"95 00 60\"),\n\nbytes.fromhex(\"95 00 03\"),\n\n),\n\n(\n\ngame_dir / \"optgraph.dll\",\n\n\"9897563fd6447976e2780ba0711f6e4e0a5a98a5c5cbecc4cb9bd608d856bd50\",\n\n0x1060,\n\nbytes.fromhex(\"55 8B EC 81 EC F4\"),\n\nbytes.fromhex(\"B8 31 F1 8D 01 C3\"),\n\n),\n\n]\n\nverified = []\n\nfor path, expected_hash, offset, original, replacement in patches:\n\ndata = bytearray(path.read_bytes())\n\nactual_hash = hashlib.sha256(data).hexdigest()\n\nif actual_hash != expected_hash:\n\nraise SystemExit(f\"Unexpected SHA-256 for {path}: {actual_hash}\")\n\nif data[offset : offset + len(original)] != original:\n\nraise SystemExit(f\"Unexpected bytes at 0x{offset:X} in {path}\")\n\ndata[offset : offset + len(replacement)] = replacement\n\nverified.append((path, data))\n\nfor path, data in verified:\n\npath.chmod(path.stat().st_mode | 0o200)\n\npath.write_bytes(data)\n\nprint(f\"Patched {path}\")\n```\n\n# Wrapping Up\n\nAfter spending the evening playing through that game again, I also looked through a few other, similar games that I used to play. It turns out that a surprisingly large number of them used Macromedia and had essentially the same basic copy-protection mechanism. Sadly, there’s no easy way to make a common patch that works for all of these games, but with a little bit of effort, the same process can be applied to them as well.\n\nBeing able to play these games again after over 20 years, and maybe even having my future kid play them, makes me really happy :)\n\nI really hope this post can bring the same joy to others as well!\n\n---\n\nAuthor: WerWolv\n\nPost: Getting old Macromedia Director Games to run on modern Hardware\n\nLink: <https://werwolv.net/posts/macromedia_copy_protection/>\n\nSource: [@WerWolv/Blog | posts//index.mdx](https://github.dev/WerWolv/Blog/blob/main/src/content/posts//index.mdx)\n\nLicense: [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)\n","body_html":"<h1 id=\"introduction\">Introduction</h1>\n<p>When I was a kid in the early 2000s, I spent a lot of time playing games that I borrowed from our local library. Many of them were German point-and-click adventure games, since those were the games that ran on my dad’s Windows 95 computer. Even back then, I was interested in tech and always tried to figure out how I could keep playing these games after giving them back to the library, but I, of course, never managed to do it.\nA few days ago, I stumbled across one of those failed attempts again: a set of CDs onto which I tried to burn a copy of “Findus bei den Mucklas.” I figured that maybe now was the time where I finally have the skills to do it!</p>\n<h1 id=\"getting-a-disk-image\">Getting a Disk Image</h1>\n<p>Obviously, the CD I burned back then was unusable, both because CD burners at the time often weren’t able to properly read or write copy-protected CDs and because I haven’t had a computer with a disc drive in the past 10 years.</p>\n<p>disappointment.png</p>\n<p>disappointment.png</p>\n<p>Thankfully, some helpful soul uploaded a set of full disk images for that and all the other games in the series to <a href=\"https://archive.org/details/cd-01_202304\" rel=\"nofollow ugc noopener\"><img src=\"https://www.google.com/s2/favicons?domain=archive.org&amp;sz=128\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" />archive.org</a>. It’s not piracy if it’s on there, right?</p>\n<h2 id=\"unpacking-the-image\">Unpacking the Image</h2>\n<p>The game came as a set of three files: a <code>.ccd</code>, a <code>.cue</code> and a <code>.img</code> file. Those are apparently produced by the old <a href=\"https://en.wikipedia.org/wiki/CloneCD\" rel=\"nofollow ugc noopener\"><img src=\"https://www.google.com/s2/favicons?domain=en.wikipedia.org&amp;sz=128\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" />CloneCD program</a> and contain the raw data from the disc, plus additional out-of-band information such as track metadata. Games from that time often came with audio tracks as well, so you could just pop these CDs into a CD player and listen to the soundtrack or other bundled media.</p>\n<p>Looking at the <code>.cue</code> file shows that there’s a MODE1 trackThese are tracks meant for storing regular data, such as a file system. They are usually skipped by audio players. at the start containing the game files, followed by four <code>AUDIO</code> tracks:</p>\n<p>CD01.cue</p>\n<pre><code>FILE &quot;CD01.img&quot; BINARY\n\nTRACK 1 MODE1/2352\n\nINDEX 1 00:00:00\n\nTRACK 2 AUDIO\n\nINDEX 1 45:08:37\n\nTRACK 3 AUDIO\n\nINDEX 1 46:38:44\n\nTRACK 4 AUDIO\n\nINDEX 1 47:08:50\n\nTRACK 5 AUDIO\n\nINDEX 1 47:11:58</code></pre>\n<p>To access the data, I just used the old <code>ccd2iso</code> tool, which failed when it hit the first <code>AUDIO</code> track but was still able to convert the start of the file into a regular ISO that could be mounted.</p>\n<h1 id=\"copy-protection-1\">Copy Protection 1</h1>\n<p>Mounting the file and trying to copy the data over, I ran into what I believe to be the first copy protection measure:</p>\n<p>Duplicate files popup while extracting the archive</p>\n<p>Duplicate files popup while extracting the archive</p>\n<p>Some of the files seem to be in the disk image multiple times: once as the real copy and once as a second copy that was either 0 bytes or 256 bytes long and contained random garbage.\nMy guess is that they did that on purpose to trick some tools into displaying only the fake decoy copy of the file instead of the real one by creating deliberately malformed directory entries on the disk. Windows seems to be able to always read the correct file here (otherwise it wouldn’t work there either of course) but with the different tools I tried, each one of them showed a different combination of files in the end.</p>\n<p>Using a combination of <code>PeaZip</code>, <code>Ark</code>, <code>Dolphin</code> and <code>7z</code>, I ultimately managed to extract the right files from the ISO, but it was mostly manual labour: choosing the files that were the right size and skipping the decoy ones.</p>\n<p>Final files that were extracted from the ISO image</p>\n<p>Final files that were extracted from the ISO image</p>\n<p>At this point, just running the installer seemed easiest, and it surprisingly worked. I ran it using <code>WINEPREFIX=$(pwd)/prefix wine &quot;Installiere Findus4.exe&quot;</code> to make it install into a separate Wine prefix, from which I could copy the installed data afterwards.</p>\n<h1 id=\"copy-protection-2-electric-boogaloo\">Copy Protection 2 - Electric Boogaloo</h1>\n<p>Executing <code>Findus4.exe</code> now, of course, didn’t just work, but instead showed a nice error popup saying that the CD was missing.</p>\n<p>Missing CD Error popup</p>\n<p>Missing CD Error popup</p>\n<p>Simply searching for that message using <code>rg -a &quot;Lege die CD&quot;</code> showed that they just read it from the <code>Pettson3.ini</code> file. <em>They apparently didn’t even bother renaming that file from the previous game.</em></p>\n<p>Pettson3.ini</p>\n<pre><code>[CDROM]\n\nCDROM=?\n\n[GAME1]\n\nVind=0\n\n[ALERT1]\n\nMESSG=Lege die CD Findus4 ein und versuche es noch einmal.\n\n[SCREEN]\n\nFULLSCREEN=1</code></pre>\n<p>Searching for a few strings ultimately led to <code>messg</code> being found in <code>Findus4.exe</code>. My first instinct was, of course, to just throw the binary into Ghidra and start looking for it. Surprisingly, the string didn’t show up anywhere using the <code>Search -&gt; For Strings...</code> option, which usually means that the string isn’t actually part of the PE data but some sort of extra payload appended to the end of the executable file.</p>\n<h2 id=\"extracting-the-embedded-script\">Extracting the Embedded Script</h2>\n<p>After some more digging, I found that these games are all <code>Macromedia Director Projector</code> executables. These were generated by Macromedia Director and basically contain the whole runtime for interpreting Director files, with a <code>.dxr</code> file concatenated onto the end. That file is what gets executed when the program runs. It made sense that the CD check would be part of the embedded Director file, because why would the runtime read from a config file called <code>Pettson3.ini</code>?</p>\n<p>During my research, I stumbled across <a href=\"https://github.com/ProjectorRays/ProjectorRays\" rel=\"nofollow ugc noopener\"><img src=\"https://www.google.com/s2/favicons?domain=github.com&amp;sz=128\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" />ProjectorRays</a>, which is a disassembler and decompiler for exactly these files. They have the file signature <code>RIFX</code> or <code>XFIR</code>, depending on the endianness of the target system, which makes them pretty easy to identify. Using ImHex, I found a few occurrences of those strings in the binary. Based on the ProjectorRays source code, the format of these <code>.dxr</code> files starts with something like this:</p>\n<p>director.hexpat</p>\n<pre><code>enum RifxType : u32 {\n\nMV93 = le u32(&quot;MV93&quot;),\n\nMV95 = le u32(&quot;MV95&quot;),\n\nFGDM = le u32(&quot;FGDM&quot;),\n\nFGDC = le u32(&quot;FGDC&quot;),\n\nAPPL = le u32(&quot;APPL&quot;)\n\n};\n\nstruct Content {\n\ntype::Size&lt;u32&gt; length;\n\nRifxType type;\n\n// ...\n\n};\n\nstruct DirectorFile {\n\nchar magic[4];\n\nif (magic == &quot;RIFX&quot;)\n\nbe Content content [[inline]];\n\nelse if (magic == &quot;XFIR&quot;)\n\nle Content content [[inline]];\n\nelse\n\nstd::error(&quot;Invalid Director File!&quot;);\n\n};</code></pre>\n<p>Only two of the <code>XFIR</code> occurrences had a valid type after them: one was <code>APPL</code> at address <code>0x00222151</code>, which, according to ScummVM, identifies a Director application wrapper (the executable file we are working with), and the other was <code>MV93</code> at address <code>0x002222F0</code>, which seems to be the regular, uncompressed Director movie script.</p>\n<p>I initially tried to copy that chunk out of the executable and process it separately using ProjectorRays, but it turned out that all the offsets used inside the appended file are absolute and account for the <code>.dxr</code> file not starting at the beginning of the executable. Instead, I ended up adding an <code>--offset</code> option to the CLI that lets you seek forward to the actual offset of the embedded binary. The PR can be found here in case it’s useful to somebody else: <a href=\"https://github.com/ProjectorRays/ProjectorRays/pull/63\" rel=\"nofollow ugc noopener\"><img src=\"https://www.google.com/s2/favicons?domain=github.com&amp;sz=128\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" />ProjectorRays#63</a>.</p>\n<h2 id=\"disassembling-the-director-file\">Disassembling the Director File</h2>\n<p>Finally, after all this trouble, we can dump the embedded Director file’s contents using the following command.</p>\n<p>Terminal window</p>\n<pre><code>./projectorrays decompile Findus4.exe -o output --dump-scripts --offset 0x002222F0\n\noutput\n\n├── Findus4\n\n│   └── casts\n\n│       └── Internal\n\n│           ├── MovieScript 1.lasm\n\n│           └── MovieScript 1.ls\n\n└── Findus4.dir</code></pre>\n<p>To my surprise, this generated both a disassembly file (the <code>.lasm</code> file) <strong>and</strong> a decompiled version of the script (the <code>.ls</code> file), which still contains all the original function and variable names!</p>\n<p>Decompiled Director Script</p>\n<p>Decompiled Director Script</p>\n<h2 id=\"analyzing-the-script\">Analyzing the Script</h2>\n<p>The generated file is only around 150 lines long and contains the game’s initialization logic. It’s written in a programming language called <a href=\"https://en.wikipedia.org/wiki/Lingo_(programming_language)\" rel=\"nofollow ugc noopener\"><img src=\"https://www.google.com/s2/favicons?domain=en.wikipedia.org&amp;sz=128\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" />Lingo</a>, which is what Macromedia Director uses. Even without knowing the language, it’s pretty straightforward to read. It didn’t take long to find the function from which that <code>&quot;messg&quot;</code> string originally came. As seen here, they implemented a primitive <code>.ini</code> file parser to parse the <code>Pettson3.ini</code> file and get a few config values out of it, one being the error message displayed when no CD can be found.</p>\n<p>MovieScript 1.lasm</p>\n<pre><code>-- ...\n\non analyseIniText iniText\n\nrepeat with mening = 1 to iniText.line.count\n\nReddString = StripString(iniText.line[mening])\n\nif ReddString contains &quot;[&quot; then\n\nReddString = StripString(iniText.line[mening + 1])\n\nantalBokstaver = ReddString.char.count\n\nif ReddString contains &quot;cdrom&quot; then\n\ncdromenhet = ReddString.char[7..antalBokstaver]\n\nend if\n\nif ReddString contains &quot;Vind&quot; then\n\nantalBokstaver = ReddString.char.count\n\nvindspelAktivt = integer(ReddString.char[6])\n\nend if\n\nif ReddString contains &quot;messg&quot; then\n\nantalBokstaver = ReddString.char.count\n\nalertmessage = ReddString.char[7..antalBokstaver]\n\nend if\n\nif ReddString contains &quot;fullscreen&quot; then\n\nantalBokstaver = ReddString.char.count\n\nfullscreen = integer(ReddString.char[12])\n\nend if\n\nend if\n\nend repeat\n\nresultat = [#cdromenhet: cdromenhet, #vindspelAktivt: vindspelAktivt, #alertmessage: alertmessage, #screen: fullscreen]\n\nreturn resultat\n\nend\n\n-- ...</code></pre>\n<p>#### </p>\n<p>I always find it super fun to read these old scripts and look at the weird mix of broken English, identifiers written in foreign languages (Swedish in this case, I think?) and probably extremely buggy code.</p>\n<p>A bit further down, we can also find the function where the error text is actually being used:</p>\n<p>MovieScript 1.ls</p>\n<pre><code>on checkpccdrompath\n\nseperator = the last char in the applicationPath\n\ndrivelista = [&quot;d&quot;, &quot;e&quot;, &quot;f&quot;, &quot;g&quot;, &quot;h&quot;, &quot;i&quot;, &quot;j&quot;, &quot;k&quot;, &quot;l&quot;, &quot;m&quot;, &quot;n&quot;, &quot;o&quot;, &quot;p&quot;, &quot;q&quot;, &quot;r&quot;, &quot;s&quot;, &quot;t&quot;, &quot;u&quot;, &quot;v&quot;, &quot;w&quot;, &quot;x&quot;, &quot;y&quot;, &quot;z&quot;]\n\nRetvar = VOID\n\nrepeat with Drive in drivelista\n\niscdrom = baDiskInfo(Drive, &quot;type&quot;)\n\nif iscdrom = &quot;CD-ROM&quot; then\n\nPath = Drive &amp; &quot;:&quot; &amp; seperator &amp; &quot;p3media&quot;\n\nif checkoncd(Path) then\n\nRetvar = Drive\n\nexit repeat\n\nend if\n\nend if\n\nend repeat\n\nreturn Retvar\n\nend\n\n-- ... Part of setSearchPaths function\n\ndriveletter = checkpccdrompath()\n\nif not voidp(driveletter) then\n\nini[#cdromenhet] = driveletter &amp; &quot;:&quot;\n\nhdMedia = mp &amp; &quot;p3media&quot;\n\nhdljud = mp &amp; &quot;p3media\\p3ljud&quot;\n\nhdcast = mp &amp; &quot;p3media\\p3cast&quot;\n\ncdMedia = string(ini.cdromenhet &amp; &quot;\\p3media&quot;)\n\ncdLjud = string(ini.cdromenhet &amp; &quot;\\p3media\\p3ljud&quot;)\n\ncdCast = string(ini.cdromenhet &amp; &quot;\\p3media\\p3cast&quot;)\n\nthe searchPaths = [hdMedia, hdljud, hdcast, cdMedia, cdLjud, cdCast]\n\nelse\n\nalert(string(ini.alertmessage))\n\nquit()\n\nend if\n\n-- ...</code></pre>\n<p>Again, pretty straightforward code. They just loop over all possible Windows drives, trying to find one that contains a CD-ROM with the <code>p3media</code> folder on it. If one is found, the search paths are set up. Otherwise, the error message we saw is displayed and the game calls <code>quit()</code>.</p>\n<h2 id=\"patching-the-script\">Patching the Script</h2>\n<p>Analyzing the <code>setSearchPaths</code> function a bit, I figured the easiest way to avoid executing the <code>quit()</code> branch while still allowing it to set up the search paths correctly would be to patch the <code>if</code> to jump to the next instruction instead of the <code>else</code> branch if the check failed. That would result in the CD paths being a bit weird, but I figured it would probably still work, since the game lets you do a “Full Install” where it copies all the required data to the hard disk and presumably loads the files from there.</p>\n<p>In the assembly file, the instructions we want to patch are the following:</p>\n<p>MovieScript 1.lasm</p>\n<pre><code>&lt; 95 00 60       &gt; [246] jmpifz [342] ......... if not voidp(driveletter) then / else\n\n&lt; 4C 08          &gt; [249] getlocal 8 ........... &lt;ini&gt;</code></pre>\n<p>ProjectorRays sadly doesn’t yet support assembling the code back, but thankfully, the Lingo bytecode is incredibly simple, so we can do it manually with ImHex. Every instruction starts with a single opcode byte, followed by a big-endian integer that’s either 1, 2 or 4 bytes long. To make the manual patching a bit easier, I also made ProjectorRays output the bytes that make up each instruction (also submitted upstream to ProjectorRays).</p>\n<p>Now for the patching: <code>jmpifz</code> has the opcode byte <code>0x95</code>, followed by the big-endian 16-bit relative offset <code>0x0060</code>, or 96 bytes, indicating where to jump. To simply jump to the next instruction, we need to jump 3 bytes forward (from offset 246 to offset 249), so we want to patch the instruction bytes to be <code>&lt; 95 00 03 &gt;</code>. Searching for the original byte sequence yielded only a single result at address <code>0x002252E8</code>, which I overwrote with the new instruction.</p>\n<p>And with that, the game’s intro video was playing! 🎉</p>\n<p>The game&#39;s intro video playing</p>\n<p>The game&#39;s intro video playing</p>\n<h1 id=\"copy-protection-3-rise-of-the-machines\">Copy Protection 3 - Rise of the Machines</h1>\n<p>Sadly, there was more to it.</p>\n<p>disappointment2.png</p>\n<p>disappointment2.png</p>\n<p>Again, simply searching for that string yielded two results, and I decided to start with <code>intro.dxr</code> for now.</p>\n<p>Grepping Error Message 2</p>\n<pre><code>➜  rg -a &#39;Disc Error !&#39;\n\nP3media/intro.dxr\n\n...\n\nP3media/huset.dxr\n\n...</code></pre>\n<p>Throwing it into ProjectorRays again resulted in a few more scripts, one of which was straightforwardly called <code>BehaviorScript 36 - Call Copy-X CD protection DLL (OPTGRAPH.DLL).ls</code>. <em>I didn’t need that much handholding, devs, but thanks either way.</em></p>\n<p>Inside was, as the filename suggested, some code that loads the <code>optgraph.dll</code> located next to the game executable and calls its <code>initdisplay()</code> function. It then checks the return value of that function, and if it doesn’t match some magic value, the game is once again aborted.</p>\n<p>BehaviorScript 36 - Call Copy-X CD protection DLL (OPTGRAPH.DLL).ls</p>\n<pre><code>on exitFrame me\n\nif the platform contains &quot;Mac&quot; then\n\nnothing()\n\nelse\n\nif Ok = 0 then\n\nalert(&quot;Disc Error !&quot;)\n\navslutaSpelet()\n\nelse\n\nif getReturnVal(me) &lt;&gt; 26079537 then\n\nalert(&quot;Disc Error !&quot;)\n\navslutaSpelet()\n\nend if\n\nend if\n\nend if\n\nend</code></pre>\n<p>Interestingly, they didn’t even bother implementing the copy protection for Mac…</p>\n<p>To get past this check, we have a few options. We could make the game believe that it’s running on a Mac, patch the comparison against the magic number or patch the DLL to make it always return that constant. Given that the identical check is implemented again in <code>huset.dxr</code> and that I didn’t really want to reverse engineer the runtime and patch the platform detection, I decided to patch <code>optgraph.dll</code> instead.</p>\n<h2 id=\"patching-the-support-dll\">Patching the Support DLL</h2>\n<p>Opening the DLL in Ghidra showed that it essentially contained only that one function and nothing else.</p>\n<p><code>initdisplay()</code> itself is a bit of a convoluted mess where they send a bunch of low-level commands to the CD drive to somehow construct that magic value from it.</p>\n<p>Ghidra Decompile - OPTGRAPH.DLL</p>\n<pre><code>// ...\n\nFUN_10001e50(&amp;local_44,0,0x10);\n\nlocal_3c = 3;\n\nmciSendCommandA(local_c,0x814,0x100);\n\nlocal_3c = 1;\n\nlocal_2c = local_40;\n\nmciSendCommandA(local_c,0x814,0x102,&amp;local_44);\n\nlocal_28 = (uint *)(local_40 &amp; 0xff);\n\nlocal_3c = 1;\n\nlocal_48 = (char *)(local_40 &gt;&gt; 8 &amp; 0xff);\n\nlocal_38 = 4;\n\nlocal_40._0_2_ = local_40._1_2_;\n\nmciSendCommandA(local_c,0x814,0x112,&amp;local_44);\n\n// ...</code></pre>\n<p>I didn’t spend too much time reversing any of this but instead just used Ghidra’s wonderful <code>Patch Instruction</code> option to replace the first few instructions with a plain and boring <code>MOV</code> and <code>RET</code>.</p>\n<p>Ghidra Assembly Listing - OPTGRAPH.DLL</p>\n<pre><code>**************************************************************\n\n*                          FUNCTION                          *\n\n**************************************************************\n\nint __stdcall initdisplay(void)\n\nassume FS_OFFSET = 0xffdff000\n\nint               EAX:4          &lt;RETURN&gt;\n\n0x1060  1  initdisplay\n\ninitdisplay\n\n10001060 b8 31 f1        MOV        EAX, 26079537\n\n8d 01\n\n10001065 c3              RET\n\n10001066 05              ??         05h\n\n10001067 00              ??         00h\n\n10001068 00              ??         00h</code></pre>\n<p>After struggling for an embarrassingly long time to get Ghidra to properly export the DLL again, I finally managed to do it through <code>File -&gt; Export -&gt; Original File</code>.</p>\n<p>This seemed to have been the last thing needed, since the game now gets all the way to the menu screen!</p>\n<p>Start menu screen of the game</p>\n<p>Start menu screen of the game</p>\n<h2 id=\"writing-an-auto-patcher\">Writing an auto patcher</h2>\n<p>After I was done, I wrote a quick Python script that can apply the patches discussed in this post. This exact script will only work for the German version of the Game that I linked above. However, it shouldn’t be hard to adapt to other versions as well (probably only requires fixing the hash and the offset)</p>\n<p>patch_findus4.py</p>\n<pre><code>#!/usr/bin/env python3\n\nimport hashlib\n\nimport sys\n\nfrom pathlib import Path\n\ngame_dir = Path(sys.argv[1]) if len(sys.argv) &gt; 1 else Path.cwd()\n\npatches = [\n\n(\n\ngame_dir / &quot;Findus4.exe&quot;,\n\n&quot;b33312ff9086c1cbc09d9cde6baa0eff4220a1ed354c9a3211ff6787fff46e39&quot;,\n\n0x2252E8,\n\nbytes.fromhex(&quot;95 00 60&quot;),\n\nbytes.fromhex(&quot;95 00 03&quot;),\n\n),\n\n(\n\ngame_dir / &quot;optgraph.dll&quot;,\n\n&quot;9897563fd6447976e2780ba0711f6e4e0a5a98a5c5cbecc4cb9bd608d856bd50&quot;,\n\n0x1060,\n\nbytes.fromhex(&quot;55 8B EC 81 EC F4&quot;),\n\nbytes.fromhex(&quot;B8 31 F1 8D 01 C3&quot;),\n\n),\n\n]\n\nverified = []\n\nfor path, expected_hash, offset, original, replacement in patches:\n\ndata = bytearray(path.read_bytes())\n\nactual_hash = hashlib.sha256(data).hexdigest()\n\nif actual_hash != expected_hash:\n\nraise SystemExit(f&quot;Unexpected SHA-256 for {path}: {actual_hash}&quot;)\n\nif data[offset : offset + len(original)] != original:\n\nraise SystemExit(f&quot;Unexpected bytes at 0x{offset:X} in {path}&quot;)\n\ndata[offset : offset + len(replacement)] = replacement\n\nverified.append((path, data))\n\nfor path, data in verified:\n\npath.chmod(path.stat().st_mode | 0o200)\n\npath.write_bytes(data)\n\nprint(f&quot;Patched {path}&quot;)</code></pre>\n<h1 id=\"wrapping-up\">Wrapping Up</h1>\n<p>After spending the evening playing through that game again, I also looked through a few other, similar games that I used to play. It turns out that a surprisingly large number of them used Macromedia and had essentially the same basic copy-protection mechanism. Sadly, there’s no easy way to make a common patch that works for all of these games, but with a little bit of effort, the same process can be applied to them as well.</p>\n<p>Being able to play these games again after over 20 years, and maybe even having my future kid play them, makes me really happy :)</p>\n<p>I really hope this post can bring the same joy to others as well!</p>\n<hr />\n<p>Author: WerWolv</p>\n<p>Post: Getting old Macromedia Director Games to run on modern Hardware</p>\n<p>Link: <a href=\"https://werwolv.net/posts/macromedia_copy_protection/\" rel=\"nofollow ugc noopener\">https://werwolv.net/posts/macromedia_copy_protection/</a></p>\n<p>Source: <a href=\"https://github.dev/WerWolv/Blog/blob/main/src/content/posts//index.mdx\" rel=\"nofollow ugc noopener\">@WerWolv/Blog | posts//index.mdx</a></p>\n<p>License: <a href=\"https://creativecommons.org/licenses/by-nc-sa/4.0/\" rel=\"nofollow ugc noopener\">CC BY-NC-SA 4.0</a></p>","headings":[{"level":1,"text":"Introduction","id":"introduction"},{"level":1,"text":"Getting a Disk Image","id":"getting-a-disk-image"},{"level":2,"text":"Unpacking the Image","id":"unpacking-the-image"},{"level":1,"text":"Copy Protection 1","id":"copy-protection-1"},{"level":1,"text":"Copy Protection 2 - Electric Boogaloo","id":"copy-protection-2-electric-boogaloo"},{"level":2,"text":"Extracting the Embedded Script","id":"extracting-the-embedded-script"},{"level":2,"text":"Disassembling the Director File","id":"disassembling-the-director-file"},{"level":2,"text":"Analyzing the Script","id":"analyzing-the-script"},{"level":2,"text":"Patching the Script","id":"patching-the-script"},{"level":1,"text":"Copy Protection 3 - Rise of the Machines","id":"copy-protection-3-rise-of-the-machines"},{"level":2,"text":"Patching the Support DLL","id":"patching-the-support-dll"},{"level":2,"text":"Writing an auto patcher","id":"writing-an-auto-patcher"},{"level":1,"text":"Wrapping Up","id":"wrapping-up"}]}}