A Java library for parsing Macromedia/Adobe Director and Shockwave files (.dir, .dxr, .dcr, .cct, .cst).
- Java 21 or later
./gradlew build- RIFX container (big-endian and little-endian)
- Afterburner-compressed files (.dcr, .cct)
- Director versions 4 through 12
- Cast members (bitmaps, text, scripts, sounds, shapes, palettes, fonts)
- Lingo bytecode with symbol resolution
- Score/timeline data (frames, channels, labels, behaviour intervals)
- File metadata (stage dimensions, tempo, version)
- Bitmaps: 1/2/4/8/16/32-bit depths, palette support, PNG export
- Text: Field (type 3) and Text (type 12) cast members via STXT chunks
- Sound: PCM to WAV conversion, MP3 extraction, IMA ADPCM decoding
- Palettes: Built-in Director palettes and custom CLUT chunks
- Save to uncompressed RIFX format
- Remove protection from protected files
- Decompile and embed Lingo source into cast members
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.
LibreShockwave 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.
The player is available in two forms:
- Desktop (
player) — Swing-based UI with an integrated Lingo debugger - Web (
player-wasm) — Compiled to WebAssembly via TeaVM, runs in any modern browser
All 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).
The 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.).
implementation project(':player-core') // transitively includes :vm and :sdkimport com.libreshockwave.DirectorFile;
import com.libreshockwave.player.Player;
import com.libreshockwave.player.render.FrameSnapshot;
import com.libreshockwave.player.render.RenderSprite;
DirectorFile file = DirectorFile.load(Path.of("movie.dcr"));
Player player = new Player(file);
player.play();
// Game loop
while (player.tick()) {
FrameSnapshot snap = player.getFrameSnapshot();
for (RenderSprite sprite : snap.sprites()) {
if (!sprite.isVisible() || sprite.getBakedBitmap() == null) continue;
Bitmap bmp = sprite.getBakedBitmap(); // fully composited (ink + color applied)
int x = sprite.getX();
int y = sprite.getY();
// draw bmp at (x, y) with your rendering backend
}
}
player.shutdown();Each call to tick() advances one frame and returns true while the movie is still playing. getFrameSnapshot() returns the current frame's state with pre-baked bitmaps for all sprite types (bitmap, text, shape).
Custom networking
For environments without java.net.http (e.g. WASM, Android), pass a NetProvider to the constructor:
Player player = new Player(file, new NetBuiltins.NetProvider() {
public int preloadNetThing(String url) { /* start async fetch, return task ID */ }
public int postNetText(String url, String postData) { /* POST, return task ID */ }
public boolean netDone(Integer taskId) { /* true when complete */ }
public String netTextResult(Integer taskId) { /* response body */ }
public int netError(Integer taskId) { /* 0 = OK, negative = error */ }
public String getStreamStatus(Integer taskId) { /* "Connecting", "Complete", etc. */ }
});External parameters
Shockwave movies read <PARAM> tags from the embedding HTML. Pass these before calling play():
player.setExternalParams(Map.of(
"sw1", "external.variables.txt=http://example.com/vars.txt",
"sw2", "connection.info.host=127.0.0.1"
));Event listeners
// Player events (enterFrame, mouseDown, etc.)
player.setEventListener(event -> {
System.out.println(event.event() + " at frame " + event.frame());
});
// Notified when an external cast finishes loading
player.setCastLoadedListener(() -> {
System.out.println("A cast finished loading");
});Lifecycle
| Method | Description |
|---|---|
play() |
Prepare the movie and begin playback |
tick() |
Advance one frame; returns false when the movie has stopped |
pause() |
Pause playback (keeps state) |
resume() |
Resume after pause |
stop() |
Stop playback and reset to frame 1 |
shutdown() |
Release all resources (thread pools, caches) |
A GUI tool for browsing and extracting assets from Director files (available on the releases page).
import com.libreshockwave.DirectorFile;
import java.nio.file.Path;
// From file path
DirectorFile file = DirectorFile.load(Path.of("movie.dcr"));
// From byte array
DirectorFile file = DirectorFile.load(bytes);Accessing metadata
DirectorFile file = DirectorFile.load(Path.of("movie.dcr"));
file.isAfterburner(); // true if compressed
file.getEndian(); // BIG_ENDIAN (Mac) or LITTLE_ENDIAN (Windows)
file.getStageWidth(); // stage width in pixels
file.getStageHeight(); // stage height in pixels
file.getTempo(); // frames per second
file.getConfig().directorVersion(); // internal version number
file.getChannelCount(); // sprite channels (48-1000 depending on version)Iterating cast members
for (CastMemberChunk member : file.getCastMembers()) {
int id = member.id();
String name = member.name();
if (member.isBitmap()) { /* ... */ }
if (member.isScript()) { /* ... */ }
if (member.isSound()) { /* ... */ }
if (member.isField()) { /* old-style text */ }
if (member.isText()) { /* rich text */ }
if (member.hasTextContent()) { /* either field or text */ }
}Extracting bitmaps
for (CastMemberChunk member : file.getCastMembers()) {
if (!member.isBitmap()) continue;
file.decodeBitmap(member).ifPresent(bitmap -> {
BufferedImage image = bitmap.toBufferedImage();
ImageIO.write(image, "PNG", new File(member.name() + ".png"));
});
}Extracting text
KeyTableChunk keyTable = file.getKeyTable();
for (CastMemberChunk member : file.getCastMembers()) {
if (!member.hasTextContent()) continue;
for (KeyTableChunk.KeyTableEntry entry : keyTable.getEntriesForOwner(member.id())) {
if (entry.fourccString().equals("STXT")) {
Chunk chunk = file.getChunk(entry.sectionId());
if (chunk instanceof TextChunk textChunk) {
String text = textChunk.text();
}
break;
}
}
}Extracting sounds
import com.libreshockwave.audio.SoundConverter;
for (CastMemberChunk member : file.getCastMembers()) {
if (!member.isSound()) continue;
for (KeyTableChunk.KeyTableEntry entry : keyTable.getEntriesForOwner(member.id())) {
if (entry.fourccString().equals("snd ")) {
SoundChunk sound = (SoundChunk) file.getChunk(entry.sectionId());
if (sound.isMp3()) {
byte[] mp3 = SoundConverter.extractMp3(sound);
} else {
byte[] wav = SoundConverter.toWav(sound);
}
break;
}
}
}Accessing scripts and bytecode
ScriptNamesChunk names = file.getScriptNames();
for (ScriptChunk script : file.getScripts()) {
// Script-level declarations
List<String> globals = script.getGlobalNames(names);
List<String> properties = script.getPropertyNames(names);
for (ScriptChunk.Handler handler : script.handlers()) {
String handlerName = names.getName(handler.nameId());
int argCount = handler.argCount();
int localCount = handler.localCount();
// Argument and local variable names
for (int id : handler.argNameIds()) {
String argName = names.getName(id);
}
for (int id : handler.localNameIds()) {
String localName = names.getName(id);
}
// Bytecode instructions
for (ScriptChunk.Handler.Instruction instr : handler.instructions()) {
int offset = instr.offset();
Opcode opcode = instr.opcode();
int argument = instr.argument();
}
}
}Aggregating globals and properties
// All unique globals across all scripts
Set<String> allGlobals = file.getAllGlobalNames();
// All unique properties across all scripts
Set<String> allProperties = file.getAllPropertyNames();
// Detailed info per script
for (DirectorFile.ScriptInfo info : file.getScriptInfoList()) {
info.scriptId();
info.scriptName();
info.scriptType();
info.globals();
info.properties();
info.handlers();
}Reading score data
if (file.hasScore()) {
ScoreChunk score = file.getScoreChunk();
int frames = score.getFrameCount();
int channels = score.getChannelCount();
// Frame labels
FrameLabelsChunk labels = file.getFrameLabelsChunk();
if (labels != null) {
for (FrameLabelsChunk.FrameLabel label : labels.labels()) {
int frameNum = label.frameNum();
String labelName = label.label();
}
}
// Behaviour intervals
for (ScoreChunk.FrameInterval interval : score.frameIntervals()) {
int start = interval.startFrame();
int end = interval.endFrame();
int scriptId = interval.scriptId();
}
}Accessing raw chunks
// All chunk metadata
for (DirectorFile.ChunkInfo info : file.getAllChunkInfo()) {
int id = info.id();
ChunkType type = info.type();
int offset = info.offset();
int length = info.length();
}
// Specific chunk by ID
Chunk chunk = file.getChunk(42);
// Type-safe chunk access
file.getChunk(42, BitmapChunk.class).ifPresent(bitmap -> {
byte[] data = bitmap.data();
});External cast files
for (String castPath : file.getExternalCastPaths()) {
Path resolved = baseDir.resolve(castPath);
if (Files.exists(resolved)) {
DirectorFile castFile = DirectorFile.load(resolved);
}
}Saving files
// Load compressed/protected file
DirectorFile file = DirectorFile.load(Path.of("protected.dcr"));
// Save as unprotected RIFX (decompiles scripts automatically)
file.save(Path.of("unprotected.dir"));
// Or get bytes
byte[] rifxData = file.saveToBytes();The player-wasm module compiles the player for the browser using TeaVM v0.13's standard WebAssembly backend. It produces a .wasm file with a JavaScript library that runs in all modern browsers.
WASM is a pure computation engine with zero @Import annotations — JS owns networking (fetch), canvas rendering, and the animation loop. No Web Worker required.
./gradlew :player-wasm:generateWasmThis 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/.
./gradlew :player-wasm:generateWasm
npx serve player-wasm/build/dist
# Open http://localhost:3000Copy 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.
Include shockwave-lib.js and add a <canvas>. That's it.
<canvas id="stage" width="640" height="480"></canvas>
<script src="shockwave-lib.js"></script>
<script>
var player = LibreShockwave.create("stage");
player.load("http://example.com/movie.dcr");
</script>The following files must be served from the same directory as the script:
| File | Purpose |
|---|---|
shockwave-lib.js |
Player library (the only <script> you need) |
player-wasm.wasm |
Compiled player engine |
player-wasm.wasm-runtime.js |
TeaVM runtime (loaded automatically) |
JavaScript API
// Create a player on a <canvas> element (by ID or element reference)
var player = LibreShockwave.create("my-canvas", {
basePath: "/wasm/", // where the WASM files live (auto-detected by default)
params: { sw1: "key=value" }, // Shockwave <PARAM> tags
autoplay: true, // start playing after load (default: true)
remember: true, // persist params in localStorage (default: false)
onLoad: function(info) {}, // { width, height, frameCount, tempo }
onError: function(msg) {}, // error message string
onFrame: function(frame, total) {} // called each frame
});
// Load a movie
player.load("http://localhost/movie.dcr"); // from URL
player.loadFile(fileInput.files[0]); // from <input type="file">
// Playback
player.play();
player.pause();
player.stop();
player.goToFrame(10);
player.stepForward();
player.stepBackward();
// External parameters (Shockwave PARAM tags)
player.setParam("sw1", "external.variables.txt=http://localhost/gamedata/external_variables.txt");
player.setParams({ sw1: "...", sw2: "..." });
// State
player.getCurrentFrame(); // current frame number
player.getFrameCount(); // total frames
// Clean up
player.destroy();Architecture
JS (shockwave-lib.js) WASM (@Export methods)
────────────────────── ────────────────────
fetch() .dcr file
→ loadMovie(bytes, basePath) → WasmEntry.loadMovie()
→ WasmPlayer.loadMovie()
→ QueuedNetProvider created
→ Player.preloadAllCasts()
requestAnimationFrame loop:
→ tick() → WasmEntry.tick()
→ getPendingFetchJson() → QueuedNetProvider.serializePendingRequests()
→ drainPendingFetches() → QueuedNetProvider.drainPendingRequests()
→ fetch(url) for each request
→ deliverFetchResult(id, data) → QueuedNetProvider.onFetchComplete()
→ CastLibManager.setExternalCastDataByUrl()
→ getFrameDataJson() → SpriteDataExporter.exportFrameData()
→ getBitmapData(memberId) → SpriteDataExporter.getBitmapRGBA()
→ Canvas 2D drawImage()
Key design decisions:
- No
@Import— WASM never calls JS; JS polls for pending network requests - No Web Worker —
tick()is fast enough for the main thread - Single rendering path — sprite JSON + bitmap fetch (no pixel buffer fallback)
- Fallback URLs in JSON — JS handles retry logic (.cct → .cst on 404)
Module structure
player-wasm/
build.gradle # TeaVM plugin config (standard WASM target)
src/main/java/.../wasm/
WasmEntry.java # All @Export methods (single entry point)
WasmPlayer.java # Player wrapper (deferred play, tick resilience)
QueuedNetProvider.java # Polling-based NetProvider (no @Import)
SpriteDataExporter.java # Frame data JSON + bitmap cache
src/main/resources/web/
index.html # Player page with toolbar and transport controls
shockwave-lib.js # Embeddable player library (~350 lines)
libreshockwave.css # Styling
- No mouse/keyboard event forwarding to Lingo VM (planned)
- No Lingo debugger in WASM (available in desktop player)
- 32-bit JPEG-based bitmaps (ediM+ALFA) render as placeholders
./gradlew :sdk:extractCasts# Unit tests per module
./gradlew :sdk:test
./gradlew :vm:test
./gradlew :player-core:test
# SDK integration / feature tests
./gradlew :sdk:runTests
./gradlew :sdk:runFeatureTests
# Build the WASM player (output in player-wasm/build/dist/)
./gradlew :player-wasm:generateWasm| Module | Description |
|---|---|
sdk |
Core library for parsing Director/Shockwave files |
vm |
Lingo bytecode virtual machine |
player-core |
Platform-independent playback engine (score, events, rendering data) |
player |
Desktop player with Swing UI and debugger |
player-wasm |
Browser player compiled to WebAssembly via TeaVM |
cast-extractor |
GUI tool for extracting assets from Director files |
SDK packages
com.libreshockwave- MainDirectorFileclasscom.libreshockwave.chunks- Chunk type parsers (CASt, Lscr, BITD, STXT, etc.)com.libreshockwave.bitmap- Bitmap decoding and palette handlingcom.libreshockwave.audio- Sound conversion utilitiescom.libreshockwave.lingo- Opcode definitions and decompilercom.libreshockwave.io- Binary readers/writerscom.libreshockwave.format- File format utilities (Afterburner, chunk types)com.libreshockwave.cast- Cast member type definitions
This implementation draws from:
- dirplayer-rs by Igor Lira
- ProjectorRays by Debby Servilla
- ScummVM Director engine documentation
Licensed under the Apache License, Version 2.0. See LICENSE for details.
