-
Notifications
You must be signed in to change notification settings - Fork 0
LibreShockwave
Welcome to the LibreShockwave wiki!
https://uoxyc.github.io/LibreShockwave/web/index.html
https://uoxyc.github.io/LibreShockwave/web/test-harness.html
https://uoxyc.github.io/LibreShockwave/debugger/debug-harness.html
LibreShockwave is a C++20 project for parsing Macromedia/Adobe Director and Shockwave files (.dir, .dxr, .dcr, .cct, .cst).
It 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.
-
CMake 3.20 or newer
-
A C++20 compiler
-
zlib development headers, or a zlib-compatible zlib-ng package
-
Optional: Ninja for faster incremental builds
-
Optional: Qt6 Widgets (or Qt5 Widgets) for the desktop debugger
-
Optional: Emscripten for the browser/WASM target
-
Optional: Node.js and npm for browser/WASM verification
Use the build script to print the dependency commands for your Linux distribution:
./build.sh --depsThe script knows the native package names for Debian, Ubuntu, Linux Mint, Pop!_OS, Fedora, RHEL, Rocky Linux, AlmaLinux, CentOS Stream, Arch Linux, Manjaro, openSUSE, and Alpine Linux.
To let the script install native dependencies for the detected distribution:
./build.sh --install-depsAutomatic installation is optional; by default the script only builds.
./build.shBy default this configures a Debug build, builds the C++ tests and native probe tools, and runs ctest.
Useful variants:
./build.sh --release
./build.sh --generator Ninja
./build.sh --jobs 8
./build.sh --target libreshockwave_probe --no-tests
./build.sh --cleanThe main library target is LibreShockwave::libreshockwave.
Asset catalogue extractor
Extract a directory of Director/Shockwave files into the same asset layout used by the v31 asset catalogue:
./cmake-build-debug/cpp/libreshockwave_asset_extractor path/to/dcr-files path/to/outputThe output contains one directory per input file with manifest.tsv,
file_info.tsv, and decoded bitmaps/, text/, sounds/, palettes/, and
raw_chunks/, and scripts/ subdirectories. The output directory is optional;
if omitted, the generated catalogue is written into the input directory.
The scripts/ directory contains native decompiled Lingo source (.ls) and
resolved bytecode listings (.lsasm). Script assets are also added to the
per-file manifest.tsv when they can be associated with a cast member.
- RIFX, XFIR, RIFF, and FFIR containers
- Big-endian and little-endian Director files
- Afterburner-compressed files (
.dcr,.cct) - Director versions 4 through 12
- Cast members: bitmaps, text, scripts, sounds, shapes, palettes, fonts, and Shockwave3D metadata
- Lingo bytecode with symbol, global, property, and handler resolution
- Score and timeline data, including frames, channels, labels, palettes, tempos, and behavior intervals
- File metadata, including stage dimensions, tempo, version, movie type, endian mode, and external cast paths
- Bitmaps: 1/2/4/8/16/32-bit depths, palette support, native alpha, matte data, and ARGB output buffers
- Text: field and rich text cast members through STXT and XMED data
- Sound: MP3 extraction, PCM WAV conversion, and IMA ADPCM decoding
- Palettes: built-in Director palettes and custom CLUT chunks
- Fonts: PFR1 font parsing and conversion to TrueType (
.ttf) bytes
LibreShockwave includes a Qt desktop debugger and a browser/WASM debugger harness. Both expose movie playback, Lingo bytecode and decompiled code, breakpoints, stepping, call-stack and variable inspection, and watch expressions.
The desktop debugger is built only when Qt6 Widgets or Qt5 Widgets is available. Build its target explicitly:
./build.sh --target libreshockwave_debugger --no-tests
./cmake-build-debug/cpp/libreshockwave_debugger_app/libreshockwave_debugger path/to/movie.dcr
Use --release and cmake-build-release for a Release build. The executable is placed under <build-dir>/cpp/libreshockwave_debugger_app/ when a custom build directory is used.
The command-line argument is optional. Pass a movie or recording as the positional argument; --play starts ordinary playback or recording replay after loading (including asynchronous HTTP(S) movies):
libreshockwave_debugger --play path/to/movie.dcr
libreshockwave_debugger --play path/to/session.lswdebugYou can also open local .dir, .dcr, .dxr, .cct, and .cst files from File → Open Movie, or enter an HTTP(S) movie URL with File → Open URL. Network-dependent movies can receive their key=value external parameters from Parameters → Edit Parameters; parameter changes take effect after reloading the movie. The window remembers recent movies, parameters, layout, and breakpoints per movie.
The debugger workflow is:
- Load a movie and press Play.
- Expand a cast in Movies & Scripts, select a script and handler, then use the Bytecode or Decompiled code view.
- Click a code gutter line to toggle a breakpoint, or press
F9while paused to toggle one at the current instruction. - When execution pauses, inspect the call stack, locals, properties, globals, and watches. Enter an expression in the Watches panel to evaluate it in the paused context.
Playback and stepping shortcuts are:
| Key | Action |
|---|---|
F5 |
Continue |
Esc |
Pause |
F9 |
Toggle breakpoint |
F10 |
Step over |
F11 |
Step into |
Shift+F11 |
Step out |
Play & Record saves stage mouse and keyboard input as a .lswdebug file. Open that file with File → Open Debug Recording to reload its movie and external parameters and replay the captured input.
When vendoring the repository inside another CMake project:
add_subdirectory(path/to/LibreShockwave)
target_link_libraries(my_tool PRIVATE LibreShockwave::libreshockwave)
target_compile_features(my_tool PRIVATE cxx_std_20)The C++ API currently loads from memory. Read the file bytes, then call DirectorFile::load.
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <stdexcept>
#include <vector>
#include "libreshockwave/DirectorFile.hpp"
std::vector<std::uint8_t> readFile(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
if (!input) {
throw std::runtime_error("Unable to open file");
}
return {
std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>()
};
}
int main(int argc, char** argv) {
const std::filesystem::path moviePath = argc > 1 ? argv[1] : "movie.dcr";
auto file = libreshockwave::DirectorFile::load(readFile(moviePath));
file->setBasePath(moviePath.parent_path().string());
std::cout << file->stageWidth() << "x" << file->stageHeight()
<< " tempo=" << file->tempo()
<< " cast members=" << file->castMembers().size()
<< '\n';
}for (const auto& member : file->castMembers()) {
if (member->isBitmap()) {
// Decode with file->decodeBitmap(member).
}
if (member->isScript()) {
// Resolve with file->getScriptForCastMember(member).
}
if (member->isSound()) {
// Read linked snd chunks.
}
if (member->isText()) {
// Read linked STXT/XMED text.
}
}#include <cstdint>
#include <vector>
// #define STB_IMAGE_WRITE_IMPLEMENTATION
// #include "stb_image_write.h"
for (const auto& member : file->castMembers()) {
if (!member->isBitmap()) {
continue;
}
if (auto bitmap = file->decodeBitmap(member)) {
const int width = bitmap->width();
const int height = bitmap->height();
const auto& argbPixels = bitmap->pixels();
// Example uses stb_image_write.h (https://github.com/nothings/stb).
std::vector<std::uint8_t> rgbaPixels;
rgbaPixels.reserve(static_cast<size_t>(width) * height * 4);
for (const auto argb : argbPixels) {
rgbaPixels.push_back((argb >> 16) & 0xFF);
rgbaPixels.push_back((argb >> 8) & 0xFF);
rgbaPixels.push_back(argb & 0xFF);
rgbaPixels.push_back((argb >> 24) & 0xFF);
}
stbi_write_png(
(member->name() + ".png").c_str(),
width,
height,
4,
rgbaPixels.data(),
width * 4);
}
}for (const auto& member : file->castMembers()) {
auto text = file->getTextForMember(member);
if (text && !text->text().empty()) {
std::cout << member->name() << ": " << text->text() << '\n';
}
}#include <filesystem>
#include <fstream>
#include <memory>
#include "libreshockwave/audio/SoundConverter.hpp"
#include "libreshockwave/chunks/SoundChunk.hpp"
#include "libreshockwave/format/ChunkType.hpp"
for (const auto& member : file->castMembers()) {
if (!member->isSound()) {
continue;
}
for (const auto& chunk : file->getLinkedChunksForMember(
member,
libreshockwave::format::fourCC(libreshockwave::format::ChunkType::snd_))) {
auto sound = std::dynamic_pointer_cast<libreshockwave::chunks::SoundChunk>(chunk);
if (!sound) {
continue;
}
const auto bytes = sound->isMp3()
? libreshockwave::audio::SoundConverter::extractMp3(*sound).value_or(std::vector<std::uint8_t>{})
: libreshockwave::audio::SoundConverter::toWav(*sound);
std::ofstream output(member->name() + (sound->isMp3() ? ".mp3" : ".wav"), std::ios::binary);
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
}
}Director files can embed PFR1 font data inside XMED chunks. LibreShockwave can parse PFR1 data and convert it to standard TrueType bytes.
#include <filesystem>
#include <fstream>
#include <memory>
#include "libreshockwave/chunks/RawChunk.hpp"
#include "libreshockwave/font/Pfr1Font.hpp"
#include "libreshockwave/font/Pfr1TtfConverter.hpp"
#include "libreshockwave/format/ChunkType.hpp"
for (const auto& member : file->castMembers()) {
for (const auto& chunk : file->getLinkedChunksForMember(
member,
libreshockwave::format::fourCC(libreshockwave::format::ChunkType::XMED))) {
auto raw = std::dynamic_pointer_cast<libreshockwave::chunks::RawChunk>(chunk);
if (!raw || raw->data().size() < 4) {
continue;
}
const auto& data = raw->data();
if (data[0] != 'P' || data[1] != 'F' || data[2] != 'R' || data[3] != '1') {
continue;
}
auto font = libreshockwave::font::Pfr1Font::parse(data);
auto ttf = libreshockwave::font::Pfr1TtfConverter::convert(*font, font->fontName);
std::ofstream output(member->name() + ".ttf", std::ios::binary);
output.write(reinterpret_cast<const char*>(ttf.data()), static_cast<std::streamsize>(ttf.size()));
}
}for (const auto& script : file->scripts()) {
auto names = file->getScriptNamesForScript(script);
for (const auto& global : script->getGlobalNames(names.get())) {
std::cout << "global " << global << '\n';
}
for (const auto& handler : script->handlers()) {
std::cout << "handler " << script->getHandlerName(handler, names.get()) << '\n';
for (const auto& instruction : handler.instructions) {
std::cout << " " << instruction.offset << ": "
<< instruction.toString() << '\n';
}
}
}if (auto score = file->scoreChunk()) {
std::cout << "frames=" << score->getFrameCount()
<< " channels=" << score->getChannelCount()
<< '\n';
for (const auto& interval : score->frameIntervals()) {
std::cout << "interval "
<< interval.primary.startFrame
<< "-"
<< interval.primary.endFrame
<< '\n';
}
}
if (auto labels = file->frameLabelsChunk()) {
for (const auto& label : labels->labels()) {
std::cout << label.frameNum.value() << ": " << label.label << '\n';
}
}#include "libreshockwave/player/Player.hpp"
libreshockwave::player::Player player(file);
player.setExternalParams({
{"sw1", "external.variables.txt=http://example.com/vars.txt"},
{"sw2", "connection.info.host=127.0.0.1"}
});
player.play();
for (int frame = 0; frame < 10 && player.tick(); ++frame) {
auto rendered = player.frameSnapshot().renderFrame();
const auto& argbPixels = rendered.pixels();
// rendered.width(), rendered.height(), and argbPixels describe the frame.
}
player.shutdown();player.setDebugEnabled(true);
player.setErrorListener([](std::string_view message, std::string_view detail) {
std::cerr << "Lingo error: " << message << '\n' << detail << '\n';
});
auto callStack = player.formatLingoCallStack();For bytecode-level debugging, attach a libreshockwave::player::debug::DebugControllerApi implementation with player.setDebugController(...).
Build the Emscripten target from an Emscripten-enabled shell:
./build.sh --wasm --releaseThe generated browser player is assembled under:
cmake-build-wasm/cpp/wasm-dist/
When testing in a browser, serve that output directory over HTTP instead of opening files directly from disk:
cd cmake-build-wasm/cpp/wasm-dist
python3 -m http.server 8098 --bind 127.0.0.1http://127.0.0.1:8098/
If you use another web server, serve the files from the same directory. The current WASM build is single-threaded and does not use Emscripten pthreads or shared memory, so SharedArrayBuffer cross-origin isolation headers are not required.
Direct remote movie and asset URLs still use normal browser fetch() behavior, so those servers must either allow CORS or be served through the same origin.
The browser target exposes LibreShockwavePlayer from libreshockwave-cpp-player.js.
<canvas id="stage" width="640" height="480"></canvas>
<script src="libreshockwave-cpp-player.js"></script>
<script>
const player = LibreShockwavePlayer.create("stage", {
params: {
sw1: "external.variables.txt=http://example.com/vars.txt"
},
debugPlayback: true,
onLoad(info) {
console.log(info.width + "x" + info.height);
},
onError(message) {
console.error(message);
}
});
player.load("movie.dcr");
</script>The following files must be served from the same directory unless basePath is supplied:
| File | Purpose |
|---|---|
index.html |
Ready-made browser player page |
libreshockwave-cpp-player.js |
Main browser player API |
libreshockwave-cpp-worker.js |
Worker wrapper for the WASM runtime |
libreshockwave-cpp-wasm.js |
Emscripten module loader |
libreshockwave-cpp-wasm.wasm |
Compiled runtime |
cpp/
CMakeLists.txt
include/libreshockwave/ Public C++ headers
src/ Runtime, SDK, VM, and player sources
apps/
tools/ Native probes and browser fixture checker
wasm/ WASM bridge entry points
resources/fonts/ Bundled runtime font assets
tests/ C++ regression and contract tests
web/
... Browser player and worker assets
docs/
rendering-rules.md Renderer behavior notes
inks.txt Director ink behavior reference
node --check web/libreshockwave-cpp-player.js
node --check web/libreshockwave-cpp-worker.js
node --check cpp/apps/tools/browser_fixture_check.js
node --check cpp/apps/tools/browser_index_check.js
./build.sh
git diff --checkRun fixture probes that match the risk of the change before saving a C++ port slice.
LibreShockwave could not have been done without these projects:
See LICENCE.