{"id":41628205,"url":"https://github.com/quackster/libreshockwave","last_synced_at":"2026-05-25T07:01:22.725Z","repository":{"id":332990249,"uuid":"1135396586","full_name":"Quackster/LibreShockwave","owner":"Quackster","description":"An open-source SDK, decompiler and web player for Adobe/Macromedia Shockwave","archived":false,"fork":false,"pushed_at":"2026-05-11T05:19:48.000Z","size":21361,"stargazers_count":36,"open_issues_count":1,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2026-05-11T08:43:30.446Z","etag":null,"topics":["adobe","director","dissassembler","java","macromedia","reverse-engineering","shockwave"],"latest_commit_sha":null,"homepage":"https://libreshockwave.net","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Quackster.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-16T03:20:30.000Z","updated_at":"2026-05-10T14:12:51.000Z","dependencies_parsed_at":"2026-03-15T09:00:17.413Z","dependency_job_id":"b5163680-0070-4854-abab-8518a42a6c61","html_url":"https://github.com/Quackster/LibreShockwave","commit_stats":null,"previous_names":["quackster/libreshockwave"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/Quackster/LibreShockwave","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Quackster%2FLibreShockwave","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Quackster%2FLibreShockwave/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Quackster%2FLibreShockwave/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Quackster%2FLibreShockwave/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Quackster","download_url":"https://codeload.github.com/Quackster/LibreShockwave/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Quackster%2FLibreShockwave/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33464012,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-25T06:32:55.349Z","status":"ssl_error","status_checked_at":"2026-05-25T06:32:35.322Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["adobe","director","dissassembler","java","macromedia","reverse-engineering","shockwave"],"created_at":"2026-01-24T14:22:22.156Z","updated_at":"2026-05-25T07:01:22.718Z","avatar_url":"https://github.com/Quackster.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LibreShockwave\n\n[![Website](https://img.shields.io/badge/Website-libreshockwave.net-orange?logo=googlechrome\u0026logoColor=white)](https://libreshockwave.net)\n\nA Java project for parsing Macromedia/Adobe Director and Shockwave files (.dir, .dxr, .dcr, .cct, .cst).\n\nIt **won't** *just* be an emulator, the goal is to eventually become a full software suite and ecosystem, with a Director player, alongside a replacement for Director MX (2004, 11.5, etc) as an open source replacement for Macromedia/Adobe Shockwave.\n\n## Requirements\n\n- Java 21 or later\n\n## Building\n\n```bash\n./gradlew build\n```\n\n## Supported Formats\n\n- RIFX container (big-endian and little-endian)\n- Afterburner-compressed files (.dcr, .cct)\n- Director versions 4 through 12\n\n## Capabilities\n\n### Reading\n- Cast members (bitmaps, text, scripts, sounds, shapes, palettes, fonts)\n- Lingo bytecode with symbol resolution\n- Score/timeline data (frames, channels, labels, behaviour intervals)\n- File metadata (stage dimensions, tempo, version)\n\n### Asset Extraction\n- Bitmaps: 1/2/4/8/16/32-bit depths, palette support, PNG export\n- Text: Field (type 3) and Text (type 12) cast members via STXT chunks\n- Sound: PCM to WAV conversion, MP3 extraction, IMA ADPCM decoding\n- Palettes: Built-in Director palettes and custom CLUT chunks\n- Fonts: PFR1 (Portable Font Resource) extraction from XMED chunks, export to TrueType (.ttf)\n\n## Player \u0026 Lingo VM\n\n\u003e **Note:** The Lingo VM, desktop player, and WASM player are under active development and are not production-ready. Expect missing features, incomplete Lingo coverage, and breaking changes.\n\nLibreShockwave includes a Lingo bytecode virtual machine and player that can load and run Director movies. The VM executes compiled Lingo scripts, handles score playback, sprite rendering, and external cast loading — bringing `.dcr` and `.dir` files back to life.\n\n**[Try the live demo →](https://libre.oldskooler.org/)** — a nightly build of the web player is deployed and ready to use. Load any `.dcr` or `.dir` file to test it in your browser.\n\nThe player is available in two forms:\n- **Desktop** (`player-swing`) — Swing-based UI with an integrated Lingo debugger\n- **Web** (`player-wasm`) — Compiled to WebAssembly via TeaVM, runs in any modern browser\n\nAll player functionality is decoupled from the SDK and VM via the `player-core` module, which provides platform-independent playback logic (score traversal, event dispatch, sprite management, bitmap decoding).\n\n\u003cimg width=\"1920\" height=\"1200\" alt=\"image\" src=\"https://github.com/user-attachments/assets/ea55ad95-bc72-4994-89e6-d0259bf0cc6b\" /\u003e\n\n## Editor\n\nThe `editor` module is a Swing-based Director MX 2004–style authoring environment. It provides an IDE-like interface with dockable panels for inspecting, editing, and playing back Director movies — bringing back the classic Director workflow as open source software.\n\nPanels include: Stage, Score, Cast, Script Editor, Property Inspector, Behavior Inspector, Color Palettes, Library Palette, Paint, Text/Field Editors, Vector Shape, Markers, Message Window, and a Bytecode Debugger. Layouts are persisted to `~/.libreshockwave/layout.json`.\n\n![](https://cdn.discordapp.com/attachments/1481540357849354272/1482322819680309258/image.png?ex=69b68834\u0026is=69b536b4\u0026hm=78debce49e047fe4e4e2c1ea7ca875cc7174ba95c7f89c873dd34a9438c28f29\u0026)\n\n### Running\n\n```bash\n./gradlew :editor:run\n```\n\n## Using player-core as a Library\n\nThe `player-core` module provides platform-independent playback logic with no UI dependencies. Use it to build custom players (JavaFX, headless renderer, server-side processor, etc.).\n\n### Dependency\n\n```groovy\nimplementation project(':player-core')  // transitively includes :vm and :sdk\n```\n\n### Minimal Example\n\n```java\nimport com.libreshockwave.DirectorFile;\nimport com.libreshockwave.bitmap.Bitmap;\nimport com.libreshockwave.player.Player;\nimport com.libreshockwave.player.render.FrameSnapshot;\n\nDirectorFile file = DirectorFile.load(Path.of(\"movie.dcr\"));\nPlayer player = new Player(file);\nplayer.play();\n\n// Game loop\nwhile (player.tick()) {\n    FrameSnapshot snap = player.getFrameSnapshot();\n    Bitmap frame = snap.renderFrame();              // composites all sprites with ink effects\n    BufferedImage image = frame.toBufferedImage();   // ready to draw or save\n}\n\nplayer.shutdown();\n```\n\nEach call to `tick()` advances one frame and returns `true` while the movie is still playing. `renderFrame()` composites all sprites (bitmap, text, shape) with ink effects into a single image using pure software rendering — no AWT dependency required.\n\n\u003cdetails\u003e\n\u003csummary\u003eCustom networking\u003c/summary\u003e\n\nFor environments without `java.net.http` (e.g. WASM, Android), pass a `NetProvider` to the constructor:\n\n```java\nPlayer player = new Player(file, new NetBuiltins.NetProvider() {\n    public int preloadNetThing(String url) { /* start async fetch, return task ID */ }\n    public int postNetText(String url, String postData) { /* POST, return task ID */ }\n    public boolean netDone(Integer taskId) { /* true when complete */ }\n    public String netTextResult(Integer taskId) { /* response body */ }\n    public int netError(Integer taskId) { /* 0 = OK, negative = error */ }\n    public String getStreamStatus(Integer taskId) { /* \"Connecting\", \"Complete\", etc. */ }\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eExternal parameters\u003c/summary\u003e\n\nShockwave movies read `\u003cPARAM\u003e` tags from the embedding HTML. Pass these before calling `play()`:\n\n```java\nplayer.setExternalParams(Map.of(\n    \"sw1\", \"external.variables.txt=http://example.com/vars.txt\",\n    \"sw2\", \"connection.info.host=127.0.0.1\"\n));\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eEvent listeners\u003c/summary\u003e\n\n```java\n// Player events (enterFrame, mouseDown, etc.)\nplayer.setEventListener(event -\u003e {\n    System.out.println(event.event() + \" at frame \" + event.frame());\n});\n\n// Notified when an external cast finishes loading\nplayer.setCastLoadedListener(() -\u003e {\n    System.out.println(\"A cast finished loading\");\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eError handling\u003c/summary\u003e\n\n```java\n// Listen for Lingo script errors\nplayer.setErrorListener((message, exception) -\u003e {\n    System.err.println(\"Lingo error: \" + message);\n\n    // The exception carries the Lingo call stack at the point of the error\n    String callStack = exception.formatLingoCallStack();\n    if (callStack != null) {\n        System.err.println(callStack);\n    }\n\n    // Or inspect individual frames\n    for (var frame : exception.getLingoCallStack()) {\n        System.out.println(frame.handlerName() + \" in \" + frame.scriptName()\n            + \" [bytecode \" + frame.bytecodeIndex() + \"]\");\n    }\n});\n```\n\nYou can also get the call stack at any time during execution (e.g. from a TraceListener or breakpoint):\n\n```java\n// Get the live Lingo call stack (empty list when no handlers are executing)\nList\u003cLingoVM.CallStackFrame\u003e stack = player.getLingoCallStack();\n\n// Or as a formatted string\nString formatted = player.formatLingoCallStack();\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eDebug playback\u003c/summary\u003e\n\nDebug playback controls `put` output, error call stacks, and diagnostic logging. It is **enabled by default**.\n\n```java\n// Disable debug output (suppresses put/error logging to stderr)\nDebugConfig.setDebugPlaybackEnabled(false);\n\n// Re-enable\nDebugConfig.setDebugPlaybackEnabled(true);\n```\n\nFor bytecode-level debugging (breakpoints, stepping, watch expressions), use the desktop player's built-in debugger or attach a `DebugControllerApi`:\n\n```java\nDebugController debugger = new DebugController();\nplayer.setDebugController(debugger);\n\n// Add a breakpoint (scriptId, handlerName, bytecodeOffset)\ndebugger.addBreakpoint(42, \"enterFrame\", 0);\n\n// Step controls (when paused at a breakpoint)\ndebugger.stepInto();\ndebugger.stepOver();\ndebugger.stepOut();\ndebugger.continueExecution();\n\n// Inspect state when paused\nDebugSnapshot snap = debugger.getCurrentSnapshot();\nsnap.locals();     // local variables\nsnap.globals();    // global variables\nsnap.stack();      // operand stack\nsnap.callStack();  // call frames\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eLifecycle\u003c/summary\u003e\n\n| Method | Description |\n|--------|-------------|\n| `play()` | Prepare the movie and begin playback |\n| `tick()` | Advance one frame; returns `false` when the movie has stopped |\n| `pause()` | Pause playback (keeps state) |\n| `resume()` | Resume after pause |\n| `stop()` | Stop playback and reset to frame 1 |\n| `shutdown()` | Release all resources (thread pools, caches) |\n\n\u003c/details\u003e\n\n## Usage\n\n### Loading a File\n\n```java\nimport com.libreshockwave.DirectorFile;\nimport java.nio.file.Path;\n\n// From file path\nDirectorFile file = DirectorFile.load(Path.of(\"movie.dcr\"));\n\n// From byte array\nDirectorFile file = DirectorFile.load(bytes);\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eAccessing metadata\u003c/summary\u003e\n\n```java\nDirectorFile file = DirectorFile.load(Path.of(\"movie.dcr\"));\n\nfile.isAfterburner();                    // true if compressed\nfile.getEndian();                        // BIG_ENDIAN (Mac) or LITTLE_ENDIAN (Windows)\nfile.getStageWidth();                    // stage width in pixels\nfile.getStageHeight();                   // stage height in pixels\nfile.getTempo();                         // frames per second\nfile.getConfig().directorVersion();      // internal version number\nfile.getChannelCount();                  // sprite channels (48-1000 depending on version)\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eIterating cast members\u003c/summary\u003e\n\n```java\nfor (CastMemberChunk member : file.getCastMembers()) {\n    int id = member.id();\n    String name = member.name();\n\n    if (member.isBitmap()) { /* ... */ }\n    if (member.isScript()) { /* ... */ }\n    if (member.isSound()) { /* ... */ }\n    if (member.isField()) { /* old-style text */ }\n    if (member.isText()) { /* rich text */ }\n    if (member.hasTextContent()) { /* either field or text */ }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eExtracting bitmaps\u003c/summary\u003e\n\n```java\nfor (CastMemberChunk member : file.getCastMembers()) {\n    if (!member.isBitmap()) continue;\n\n    file.decodeBitmap(member).ifPresent(bitmap -\u003e {\n        BufferedImage image = bitmap.toBufferedImage();\n        ImageIO.write(image, \"PNG\", new File(member.name() + \".png\"));\n    });\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eExtracting text\u003c/summary\u003e\n\n```java\nKeyTableChunk keyTable = file.getKeyTable();\n\nfor (CastMemberChunk member : file.getCastMembers()) {\n    if (!member.hasTextContent()) continue;\n\n    for (KeyTableChunk.KeyTableEntry entry : keyTable.getEntriesForOwner(member.id())) {\n        if (entry.fourccString().equals(\"STXT\")) {\n            Chunk chunk = file.getChunk(entry.sectionId());\n            if (chunk instanceof TextChunk textChunk) {\n                String text = textChunk.text();\n            }\n            break;\n        }\n    }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eExtracting sounds\u003c/summary\u003e\n\n```java\nimport com.libreshockwave.audio.SoundConverter;\n\nfor (CastMemberChunk member : file.getCastMembers()) {\n    if (!member.isSound()) continue;\n\n    for (KeyTableChunk.KeyTableEntry entry : keyTable.getEntriesForOwner(member.id())) {\n        if (entry.fourccString().equals(\"snd \")) {\n            SoundChunk sound = (SoundChunk) file.getChunk(entry.sectionId());\n\n            if (sound.isMp3()) {\n                byte[] mp3 = SoundConverter.extractMp3(sound);\n            } else {\n                byte[] wav = SoundConverter.toWav(sound);\n            }\n            break;\n        }\n    }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eExtracting fonts (PFR1 → TTF)\u003c/summary\u003e\n\nDirector files can embed fonts as PFR1 (Portable Font Resource) data inside XMED chunks attached to OLE-type cast members. LibreShockwave can parse these and convert them to standard TrueType (.ttf) files.\n\n```java\nimport com.libreshockwave.font.Pfr1Font;\nimport com.libreshockwave.font.Pfr1TtfConverter;\n\n// Find XMED chunks with PFR1 data\nKeyTableChunk keyTable = file.getKeyTable();\nint xmedFourcc = ChunkType.XMED.getFourCC();\n\nfor (CastMemberChunk member : file.getCastMembers()) {\n    var entry = keyTable.findEntry(member.id(), xmedFourcc);\n    if (entry == null) continue;\n\n    Chunk chunk = file.getChunk(entry.sectionId());\n    if (!(chunk instanceof RawChunk raw)) continue;\n\n    byte[] data = raw.data();\n    if (data == null || data.length \u003c 4) continue;\n    if (data[0] != 'P' || data[1] != 'F' || data[2] != 'R' || data[3] != '1') continue;\n\n    // Parse PFR1 and convert to TTF\n    Pfr1Font font = Pfr1Font.parse(data);\n    byte[] ttfBytes = Pfr1TtfConverter.convert(font, font.fontName);\n    Files.write(Path.of(member.name() + \".ttf\"), ttfBytes);\n}\n```\n\nThe player automatically detects PFR1 fonts when cast libraries load, converts them to TTF in memory, and registers them for pixel-perfect text rendering.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eAccessing scripts and bytecode\u003c/summary\u003e\n\n```java\nScriptNamesChunk names = file.getScriptNames();\n\nfor (ScriptChunk script : file.getScripts()) {\n    // Script-level declarations\n    List\u003cString\u003e globals = script.getGlobalNames(names);\n    List\u003cString\u003e properties = script.getPropertyNames(names);\n\n    for (ScriptChunk.Handler handler : script.handlers()) {\n        String handlerName = names.getName(handler.nameId());\n        int argCount = handler.argCount();\n        int localCount = handler.localCount();\n\n        // Argument and local variable names\n        for (int id : handler.argNameIds()) {\n            String argName = names.getName(id);\n        }\n        for (int id : handler.localNameIds()) {\n            String localName = names.getName(id);\n        }\n\n        // Bytecode instructions\n        for (ScriptChunk.Handler.Instruction instr : handler.instructions()) {\n            int offset = instr.offset();\n            Opcode opcode = instr.opcode();\n            int argument = instr.argument();\n        }\n    }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eAggregating globals and properties\u003c/summary\u003e\n\n```java\n// All unique globals across all scripts\nSet\u003cString\u003e allGlobals = file.getAllGlobalNames();\n\n// All unique properties across all scripts\nSet\u003cString\u003e allProperties = file.getAllPropertyNames();\n\n// Detailed info per script\nfor (DirectorFile.ScriptInfo info : file.getScriptInfoList()) {\n    info.scriptId();\n    info.scriptName();\n    info.scriptType();\n    info.globals();\n    info.properties();\n    info.handlers();\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eReading score data\u003c/summary\u003e\n\n```java\nif (file.hasScore()) {\n    ScoreChunk score = file.getScoreChunk();\n    int frames = score.getFrameCount();\n    int channels = score.getChannelCount();\n\n    // Frame labels\n    FrameLabelsChunk labels = file.getFrameLabelsChunk();\n    if (labels != null) {\n        for (FrameLabelsChunk.FrameLabel label : labels.labels()) {\n            int frameNum = label.frameNum();\n            String labelName = label.label();\n        }\n    }\n\n    // Behaviour intervals\n    for (ScoreChunk.FrameInterval interval : score.frameIntervals()) {\n        int start = interval.startFrame();\n        int end = interval.endFrame();\n        int scriptId = interval.scriptId();\n    }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eAccessing raw chunks\u003c/summary\u003e\n\n```java\n// All chunk metadata\nfor (DirectorFile.ChunkInfo info : file.getAllChunkInfo()) {\n    int id = info.id();\n    ChunkType type = info.type();\n    int offset = info.offset();\n    int length = info.length();\n}\n\n// Specific chunk by ID\nChunk chunk = file.getChunk(42);\n\n// Type-safe chunk access\nfile.getChunk(42, BitmapChunk.class).ifPresent(bitmap -\u003e {\n    byte[] data = bitmap.data();\n});\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eExternal cast files\u003c/summary\u003e\n\n```java\nfor (String castPath : file.getExternalCastPaths()) {\n    Path resolved = baseDir.resolve(castPath);\n    if (Files.exists(resolved)) {\n        DirectorFile castFile = DirectorFile.load(resolved);\n    }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eSaving files\u003c/summary\u003e\n\n```java\n// Load compressed/protected file\nDirectorFile file = DirectorFile.load(Path.of(\"protected.dcr\"));\n\n// Save as unprotected RIFX (decompiles scripts automatically)\nfile.save(Path.of(\"unprotected.dir\"));\n\n// Or get bytes\nbyte[] rifxData = file.saveToBytes();\n```\n\n\u003c/details\u003e\n\n## Web Player (player-wasm)\n\nThe `player-wasm` module compiles the player for the browser using [TeaVM](https://teavm.org/) v0.13's standard WebAssembly backend. It produces a `.wasm` file with a JavaScript library that runs in all modern browsers.\n\nWASM is a pure computation engine with **zero `@Import` annotations** — JS owns networking (`fetch`), canvas rendering, and the animation loop. All WASM execution runs in a **Web Worker** so slow Lingo scripts never block the main thread.\n\n### Building\n\n```bash\n./gradlew :player-wasm:generateWasm\n```\n\nThis compiles the Java player to WebAssembly and assembles all files (WASM binary, JS runtime, HTML, CSS) into a single serveable directory at `player-wasm/build/dist/`.\n\n### Running locally\n\n```bash\n./gradlew :player-wasm:generateWasm\nnpx serve --cors player-wasm/build/dist\n# Open http://localhost:3000\n```\n\nFor the SharedArrayBuffer/Atomics frame transport, serve `player-wasm/build/dist/`\nwith cross-origin isolation enabled:\n\n```http\nCross-Origin-Opener-Policy: same-origin\nCross-Origin-Embedder-Policy: require-corp\nCross-Origin-Resource-Policy: same-origin\n```\n\nThe dist directory includes a `_headers` file for hosts that support it. On\nother servers, configure equivalent headers for `index.html`, JavaScript,\nCSS, and `.wasm` assets. Without these headers the player falls back to normal\ntransferable frame buffers.\n\n### Deploying\n\nCopy the contents of `player-wasm/build/dist/` to your web server. The included `index.html` is a ready-made player page with file picker, URL bar, transport controls, and a params editor.\n\n### Embedding in Any Web Page\n\nInclude `shockwave-lib.js` and add a `\u003ccanvas\u003e`. That's it.\n\n```html\n\u003ccanvas id=\"stage\" width=\"640\" height=\"480\"\u003e\u003c/canvas\u003e\n\u003cscript src=\"shockwave-lib.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  var player = LibreShockwave.create(\"stage\", { websocketMode: \"ws\" });\n  player.load(\"http://example.com/movie.dcr\");\n\u003c/script\u003e\n```\n\nSet `websocketMode` to `ws` or `wss` when you want to explicitly control MUS\nWebSocket scheme selection instead of relying on the runtime protocol\ndetection.\n\nThe following files must be served from the same directory as the script:\n\n| File | Purpose |\n|------|---------|\n| `shockwave-lib.js` | Player library (the only `\u003cscript\u003e` you need) |\n| `shockwave-worker.js` | Web Worker — runs the WASM engine off the main thread |\n| `player-wasm.wasm` | Compiled player engine |\n| `player-wasm.wasm-runtime.js` | TeaVM runtime (loaded by the worker automatically) |\n\n\u003cdetails\u003e\n\u003csummary\u003eJavaScript API\u003c/summary\u003e\n\n```js\n// Create a player on a \u003ccanvas\u003e element (by ID or element reference)\nvar player = LibreShockwave.create(\"my-canvas\", {\n    basePath:      \"/wasm/\",                 // where the WASM files live (auto-detected by default)\n    params:        { sw1: \"key=value\" },     // Shockwave \u003cPARAM\u003e tags\n    websocketMode: \"wss\",                    // force MUS websocket scheme (optional)\n    autoplay:      true,                     // start playing after load (default: true)\n    remember:      true,                     // persist params in localStorage (default: false)\n    debugPlayback: true,                     // enable put/error logging (default: true)\n    onLoad:        function(info) {},        // { width, height, frameCount, tempo }\n    onError:       function(msg) {},         // error message string\n    onFrame:       function(frame, total) {} // called each frame\n});\n\n// Load a movie\nplayer.load(\"http://localhost/movie.dcr\");  // from URL\nplayer.loadFile(fileInput.files[0]);        // from \u003cinput type=\"file\"\u003e\n\n// Playback\nplayer.play();\nplayer.pause();\nplayer.stop();\nplayer.goToFrame(10);\nplayer.stepForward();\nplayer.stepBackward();\n\n// External parameters (Shockwave PARAM tags)\nplayer.setParam(\"sw1\", \"external.variables.txt=http://localhost/gamedata/external_variables.txt\");\nplayer.setParams({ sw1: \"...\", sw2: \"...\" });\n\n// State\nplayer.getCurrentFrame();  // current frame number\nplayer.getFrameCount();    // total frames\n\n// Debugging\nplayer.setDebugPlayback(true);   // enable/disable put output \u0026 error stack traces\nplayer.getCallStack().then(function(stack) {\n    console.log(stack);          // Lingo call stack (async, returns Promise\u003cstring\u003e)\n});\n\n// Reset (terminates worker, creates fresh WASM instance)\nplayer.reset().then(function() {\n    player.load(\"http://localhost/movie.dcr\");  // load on a clean slate\n});\n\n// Clean up\nplayer.destroy();\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eArchitecture\u003c/summary\u003e\n\n```\nMain thread (shockwave-lib.js)            Worker (shockwave-worker.js)\n──────────────────────────────            ────────────────────────────\nfetch() .dcr file\n  postMessage('loadMovie', bytes)  →      WasmEntry.loadMovie()\n                                            → Player + QueuedNetProvider created\n  postMessage('preloadCasts')      →      preloadCasts() + pumpNetworkCollect()\n                                            → fetch() cast files\n  ← postMessage('castsDone')\n\n  postMessage('play')              →      WasmEntry.play()\n\nrequestAnimationFrame loop:\n  postMessage('tick')              →      WasmEntry.tick()\n                                            → Lingo VM executes (may be slow — OK\n                                              because it's off the main thread)\n                                            → pumpNetworkCollect()\n                                              → fetch() any queued URLs\n                                              → deliverFetchResult()\n                                            → getFrameDataJson()\n                                            → getBitmapData() for new members\n  ← postMessage('frame', fd, bitmaps)\n  createImageBitmap() + cache\n  Canvas 2D drawImage()\n```\n\n**Key design decisions:**\n- No `@Import` — WASM never calls JS; JS polls for pending network requests\n- Web Worker — WASM tick runs off the main thread; Lingo scripts that take\n  hundreds of ms during loading never cause `requestAnimationFrame` violations\n- Worker owns networking — `fetch()` runs in the worker; no relay through main thread\n- Zero-copy bitmap transfer — RGBA bytes sent from worker via `Transferable`;\n  main thread caches `ImageBitmap` objects; only new/changed members are sent each frame\n- Single rendering path — sprite JSON + bitmap cache (no pixel buffer fallback)\n- Fallback URLs in JSON — worker handles retry logic (.cct → .cst on 404)\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eModule structure\u003c/summary\u003e\n\n```\nplayer-wasm/\n  build.gradle                          # TeaVM plugin config + assembleWasm task\n  src/main/java/.../wasm/\n    WasmEntry.java                      # All @Export methods (single entry point)\n    WasmPlayer.java                     # Player wrapper (deferred play, tick resilience)\n    QueuedNetProvider.java              # Polling-based NetProvider (no @Import)\n    SpriteDataExporter.java             # Frame data JSON + bitmap cache\n  src/main/resources/web/\n    index.html                          # Player page with toolbar and transport controls\n    shockwave-lib.js                    # Main-thread library: creates Worker, renders canvas\n    shockwave-worker.js                 # Web Worker: WASM engine + network pump\n    libreshockwave.css                  # Styling\n```\n\n\u003c/details\u003e\n\n### Development Notes\n\n\u003cdetails\u003e\n\u003csummary\u003eTeaVM 0.13 WASM code-gen bugs (5 known issues)\u003c/summary\u003e\n\n#### TeaVM string switch — never use Java keywords as `case` labels\n\nTeaVM 0.13 silently miscompiles `switch` statements that use Java keywords as string case labels. The case body is never entered at runtime even though equality holds in plain Java:\n\n```java\n// BROKEN in TeaVM WASM — case \"char\" is silently skipped:\nreturn switch (chunkType) {\n    case \"char\" -\u003e str.substring(start - 1, end);\n    case \"word\" -\u003e ...\n};\n\n// CORRECT — use if-else instead:\nif (\"char\".equals(chunkType)) {\n    return str.substring(start - 1, end);\n} else if (\"word\".equals(chunkType)) {\n    ...\n}\n```\n\nAffected keywords include `char`, `int`, `void`, `class`, `new`, `return`, and any other Java reserved word. Use `if-else` chains with `.equals()` whenever a string variable might match a keyword at runtime.\n\n#### TeaVM nested `yield switch` — avoid nested switch expressions\n\nTeaVM 0.13 silently fails on nested `switch` expressions that use `yield`. When a `yield switch(...) { ... }` appears inside an outer switch expression, the inner switch crashes at WASM runtime with no error:\n\n```java\n// BROKEN in TeaVM WASM — inner switch silently throws:\nreturn switch (method) {\n    case \"count\" -\u003e {\n        yield switch (chunkType) {\n            case \"char\" -\u003e Datum.of(text.length());\n            case \"word\" -\u003e Datum.of(trimmed.split(\"\\\\s+\").length);\n            default -\u003e Datum.ZERO;\n        };\n    }\n    default -\u003e Datum.VOID;\n};\n\n// CORRECT — use if-else instead of the inner switch:\nreturn switch (method) {\n    case \"count\" -\u003e {\n        if (\"char\".equals(chunkType)) {\n            yield Datum.of(text.length());\n        } else if (\"word\".equals(chunkType)) {\n            yield Datum.of(trimmed.split(\"\\\\s+\").length);\n        } else {\n            yield Datum.ZERO;\n        }\n    }\n    default -\u003e Datum.VOID;\n};\n```\n\nThe outer switch expression with `yield` works fine — only the **nesting** of a second switch expression inside it causes the issue.\n\n#### TeaVM `int[]` array element assignment reordering\n\nTeaVM 0.13 reorders element assignments when writing to `int[]` arrays. Both array initializers (`new int[] { a, b }`) and explicit index assignment (`arr[0] = a; arr[1] = b;`) can have their values swapped in the compiled WASM output:\n\n```java\n// BROKEN in TeaVM WASM — elements get swapped:\nint offset = reader.readU16();\nint frame = reader.readU16();\nint[] entry = new int[2];\nentry[0] = offset;  // may contain frame at runtime\nentry[1] = frame;   // may contain offset at runtime\nlist.add(entry);\n\n// ALSO BROKEN — initializer has same problem:\nlist.add(new int[] { offset, frame });\n\n// CORRECT — use parallel List\u003cInteger\u003e instead:\nList\u003cInteger\u003e offsets = new ArrayList\u003c\u003e();\nList\u003cInteger\u003e frames = new ArrayList\u003c\u003e();\noffsets.add(offset);\nframes.add(frame);\n```\n\nUse parallel `List\u003cInteger\u003e` or separate variables instead of multi-element `int[]` arrays when the element order matters.\n\n#### TeaVM chained method calls in `HashMap.put()`/`get()` arguments\n\nTeaVM 0.13 garbles values when chained method calls appear as arguments to `HashMap.put()` or `HashMap.get()`. The compiled WASM produces wrong keys and/or values:\n\n```java\n// BROKEN in TeaVM WASM — garbled HashMap keys/values:\nmap.put(label.getName().toLowerCase(), label.getFrame().value());\n\n// CORRECT — extract to local variables first:\nString key = label.getName().toLowerCase();\nint value = label.getFrame().value();\nmap.put(key, value);\n```\n\nAlways extract chained method call results into local variables before passing them as arguments to collection methods like `put()`, `get()`, `add()`, etc.\n\n#### TeaVM `%n` format specifier not supported\n\nTeaVM 0.13 does not support the `%n` (platform newline) format specifier in `String.format()` and `System.out.printf()`. Using `%n` throws `UnknownFormatConversionException` at runtime, which silently crashes any code path that contains it:\n\n```java\n// BROKEN in TeaVM WASM — throws UnknownFormatConversionException:\nSystem.out.printf(\"value=%d%n\", x);\n\n// CORRECT — use literal \\n instead:\nSystem.out.printf(\"value=%d\\n\", x);\n```\n\nAlways use `\\n` instead of `%n` in format strings. This applies to all code reachable from WASM, including debug logging behind feature flags.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eMultiuser Xtra — WebSocket proxy setup\u003c/summary\u003e\n\nDirector movies that use the **Multiuser Xtra** need a WebSocket-to-TCP proxy because browsers can't open raw TCP sockets. [websockify](https://github.com/novnc/websockify) bridges WebSocket connections from the browser to the TCP server.\n\nEach TCP port your movie connects to needs its own websockify instance. For example, Habbo connects to a game server (port 30001) and a MUS server (port 38101).\n\n#### Installing websockify\n\n```bash\npip install websockify\n```\n\n#### Quick start\n\n```bash\n# Proxy WebSocket :30001 → TCP 127.0.0.1:30087\npython -m websockify --verbose --traffic 30001 127.0.0.1:30087\n```\n\nReplace ports as needed. The first argument is the WebSocket listen port, the second is the TCP target (`host:port`).\n\n#### Running as a service (Linux — systemd)\n\nCreate `/etc/systemd/system/websockify-game.service`:\n\n```ini\n[Unit]\nDescription=websockify game proxy (WS :30001 → TCP :30087)\nAfter=network.target\n\n[Service]\nExecStart=/usr/bin/python3 -m websockify 30001 127.0.0.1:30087\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=multi-user.target\n```\n\nThen enable and start:\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable websockify-game\nsudo systemctl start websockify-game\n```\n\nRepeat with a second unit file for each additional port (e.g. `websockify-mus.service` for the MUS connection).\n\u003c/details\u003e\n\n### Known Limitations\n\n- No mouse/keyboard event forwarding to Lingo VM (planned)\n- No Lingo debugger in WASM (available in desktop player)\n- 32-bit JPEG-based bitmaps (ediM+ALFA) render as placeholders\n\n## Tools\n\n### Running Tests\n\n```bash\n# All unit tests\n./gradlew :sdk:test :vm:test\n```\n\n#### SDK Tests (`sdk/src/test/`)\n\n| Class | Description |\n|-------|-------------|\n| `DirectorFileTest` | Integration tests for loading and parsing Director files |\n| `DcrFileTest` | DCR file parsing with external cast support |\n| `SdkFeatureTest` | Comprehensive SDK feature tests (local files + HTTP) |\n| `Bitmap32BitTest` | 32-bit bitmap decoding |\n| `BitmapExtractTest` | Bitmap extraction by name from Director files |\n| `ScriptTypeTest` | Script type identification and classification |\n| `SoundExtractionTest` | Sound extraction (PCM → WAV, MP3) |\n| `Pfr1ToTtfTest` | PFR1 font → TTF conversion + pixel comparison against reference |\n| `Pfr1ParseTest` | PFR1 binary format parser diagnostics |\n\n```bash\n# Run SDK integration tests (JavaExec, not JUnit)\n./gradlew :sdk:runTests           # DirectorFileTest\n./gradlew :sdk:runDcrTests        # DcrFileTest (optional: -Pfile=path/to/file.dcr)\n./gradlew :sdk:runFeatureTests    # SdkFeatureTest\n```\n\n#### VM Tests (`vm/src/test/`)\n\n| Class | Description |\n|-------|-------------|\n| `LingoVMTest` | VM execution, stack operations, control flow (21 tests) |\n| `OpcodeTest` | Opcode encoding/decoding and argument handling (51 tests) |\n| `DatumTest` | Datum type conversions and equality (11 tests) |\n| `ScriptInstanceTest` | Script instance lifecycle, properties, list operations (48 tests) |\n\n```bash\n# Run VM tests via JavaExec\n./gradlew :vm:runVmTests\n```\n\n## Architecture\n\n### Modules\n\n| Module | Description |\n|--------|-------------|\n| `sdk` | Core library for parsing Director/Shockwave files |\n| `vm` | Lingo bytecode virtual machine |\n| `player-core` | Platform-independent playback engine (score, events, rendering data) |\n| `player-wasm` | Browser player compiled to WebAssembly via TeaVM |\n| `editor` | Swing-based Director MX 2004–style authoring environment |\n\n\u003cdetails\u003e\n\u003csummary\u003eSDK packages\u003c/summary\u003e\n\n- `com.libreshockwave` - Main `DirectorFile` class\n- `com.libreshockwave.chunks` - Chunk type parsers (CASt, Lscr, BITD, STXT, etc.)\n- `com.libreshockwave.bitmap` - Bitmap decoding and palette handling\n- `com.libreshockwave.audio` - Sound conversion utilities\n- `com.libreshockwave.lingo` - Opcode definitions and decompiler\n- `com.libreshockwave.io` - Binary readers/writers\n- `com.libreshockwave.format` - File format utilities (Afterburner, chunk types)\n- `com.libreshockwave.cast` - Cast member type definitions\n- `com.libreshockwave.font` - PFR1 font parser, TTF converter, bitmap font rasterizer\n\n\u003c/details\u003e\n\n## References\n\nThis implementation draws from:\n\n- [dirplayer-rs](https://github.com/igorlira/dirplayer-rs) by Igor Lira\n- [ProjectorRays](https://github.com/ProjectorRays/ProjectorRays) by Debby Servilla\n- ScummVM Director engine documentation\n\n## Licence\n\nThis project uses a split licence model:\n\n- **`sdk/` and `vm/`** — Licensed under the [Apache License 2.0](sdk/LICENSE)\n- **`player-core/`, `player-swing/`, `player-simulator/`, and `player-wasm/`** — Licensed under the [GNU General Public License v3.0](player-core/LICENSE)\n\nSee the `LICENSE` file in each module directory for full terms.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquackster%2Flibreshockwave","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquackster%2Flibreshockwave","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquackster%2Flibreshockwave/lists"}