[Scummvm-git-logs] scummvm master -> 88239fb184d475c1ce8add9ceb6a271802d2c019

sev- noreply at scummvm.org
Tue May 14 20:32:28 UTC 2024


This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
88239fb184 DIRECTOR: Added ProjectorRays' Lingo Decompiler


Commit: 88239fb184d475c1ce8add9ceb6a271802d2c019
    https://github.com/scummvm/scummvm/commit/88239fb184d475c1ce8add9ceb6a271802d2c019
Author: Eugene Sandulenko (sev at scummvm.org)
Date: 2024-05-14T22:32:02+02:00

Commit Message:
DIRECTOR: Added ProjectorRays' Lingo Decompiler

Hash id 5f23e27fcca9030b42f024d656f00b84c81d3cfc

Changed paths:
  A engines/director/lingo/lingodec/ast.cpp
  A engines/director/lingo/lingodec/ast.h
  A engines/director/lingo/lingodec/codewriter.cpp
  A engines/director/lingo/lingodec/codewriter.h
  A engines/director/lingo/lingodec/context.cpp
  A engines/director/lingo/lingodec/context.h
  A engines/director/lingo/lingodec/enums.h
  A engines/director/lingo/lingodec/handler.cpp
  A engines/director/lingo/lingodec/handler.h
  A engines/director/lingo/lingodec/names.cpp
  A engines/director/lingo/lingodec/names.h
  A engines/director/lingo/lingodec/resolver.h
  A engines/director/lingo/lingodec/script.cpp
  A engines/director/lingo/lingodec/script.h
    engines/director/lingo/lingo.cpp
    engines/director/module.mk
    engines/director/movie.cpp
    engines/director/util.cpp
    engines/director/util.h


diff --git a/engines/director/lingo/lingo.cpp b/engines/director/lingo/lingo.cpp
index dc1ec563c00..8c8ce36f24f 100644
--- a/engines/director/lingo/lingo.cpp
+++ b/engines/director/lingo/lingo.cpp
@@ -336,6 +336,8 @@ Symbol Lingo::getHandler(const Common::String &name) {
 	if (_state->context && _state->context->_functionHandlers.contains(name))
 		return _state->context->_functionHandlers[name];
 
+	warning("getHandler('%s'), movie is: %s", name.c_str(), g_director->getCurrentMovie()->getMacName().c_str());
+
 	sym = g_director->getCurrentMovie()->getHandler(name);
 	if (sym.type != VOIDSYM)
 		return sym;
diff --git a/engines/director/lingo/lingodec/ast.cpp b/engines/director/lingo/lingodec/ast.cpp
new file mode 100644
index 00000000000..49aa410ac6f
--- /dev/null
+++ b/engines/director/lingo/lingodec/ast.cpp
@@ -0,0 +1,1169 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#include "common/util.h"
+#include "./ast.h"
+#include "./codewriter.h"
+#include "./handler.h"
+#include "./names.h"
+#include "./script.h"
+
+namespace LingoDec {
+
+/* Datum */
+
+int Datum::toInt() {
+	switch (type) {
+	case kDatumInt:
+		return i;
+	case kDatumFloat:
+		return f;
+	default:
+		break;
+	}
+	return 0;
+}
+
+void Datum::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	switch (type) {
+	case kDatumVoid:
+		code.write("VOID");
+		return;
+	case kDatumSymbol:
+		code.write("#" + s);
+		return;
+	case kDatumVarRef:
+		code.write(s);
+		return;
+	case kDatumString:
+		if (s.size() == 0) {
+			code.write("EMPTY");
+			return;
+		}
+		if (s.size() == 1) {
+			switch (s[0]) {
+			case '\x03':
+				code.write("ENTER");
+				return;
+			case '\x08':
+				code.write("BACKSPACE");
+				return;
+			case '\t':
+				code.write("TAB");
+				return;
+			case '\r':
+				code.write("RETURN");
+				return;
+			case '"':
+				code.write("QUOTE");
+				return;
+			default:
+				break;
+			}
+		}
+		if (sum) {
+			code.write("\"" + Common::toPrintable(s) + "\"");
+			return;
+		}
+		code.write("\"" + s + "\"");
+		return;
+	case kDatumInt:
+		code.write(Common::String::format("%d", i));
+		return;
+	case kDatumFloat:
+		code.write(Common::String::format("%g", f));
+		return;
+	case kDatumList:
+	case kDatumArgList:
+	case kDatumArgListNoRet:
+		{
+			if (type == kDatumList)
+				code.write("[");
+			for (size_t ii = 0; ii < l.size(); ii++) {
+				if (ii > 0)
+					code.write(", ");
+				l[ii]->writeScriptText(code, dot, sum);
+			}
+			if (type == kDatumList)
+				code.write("]");
+		}
+		return;
+	case kDatumPropList:
+		{
+			code.write("[");
+			if (l.size() == 0) {
+				code.write(":");
+			} else {
+				for (size_t ii = 0; ii < l.size(); ii += 2) {
+					if (ii > 0)
+						code.write(", ");
+					l[ii]->writeScriptText(code, dot, sum);
+					code.write(": ");
+					l[ii + 1]->writeScriptText(code, dot, sum);
+				}
+			}
+			code.write("]");
+		}
+		return;
+	}
+}
+
+/* AST */
+
+void AST::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	root->writeScriptText(code, dot, sum);
+}
+
+void AST::addStatement(Common::SharedPtr<Node> statement) {
+	currentBlock->addChild(Common::move(statement));
+}
+
+void AST::enterBlock(BlockNode *block) {
+	currentBlock = block;
+}
+
+void AST::exitBlock() {
+	auto ancestorStatement = currentBlock->ancestorStatement();
+	if (!ancestorStatement) {
+		currentBlock = nullptr;
+		return;
+	}
+
+	auto block = ancestorStatement->parent;
+	if (!block || block->type != kBlockNode) {
+		currentBlock = nullptr;
+		return;
+	}
+
+	currentBlock = static_cast<BlockNode *>(block);
+}
+
+/* Node */
+
+Common::SharedPtr<Datum> Node::getValue() {
+	return Common::SharedPtr<Datum>(new Datum());
+}
+
+Node *Node::ancestorStatement() {
+	Node *ancestor = parent;
+	while (ancestor && !ancestor->isStatement) {
+		ancestor = ancestor->parent;
+	}
+	return ancestor;
+}
+
+LoopNode *Node::ancestorLoop() {
+	Node *ancestor = parent;
+	while (ancestor && !ancestor->isLoop) {
+		ancestor = ancestor->parent;
+	}
+	return static_cast<LoopNode *>(ancestor);
+}
+
+bool Node::hasSpaces(bool) {
+	return true;
+}
+
+/* ErrorNode */
+
+void ErrorNode::writeScriptText(CodeWriter &code, bool, bool) const {
+	code.write("ERROR");
+}
+
+bool ErrorNode::hasSpaces(bool) {
+	return false;
+}
+
+/* CommentNode */
+
+void CommentNode::writeScriptText(CodeWriter &code, bool, bool) const {
+	code.write("-- ");
+	code.write(text);
+}
+
+/* LiteralNode */
+
+void LiteralNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	value->writeScriptText(code, dot, sum);
+}
+
+Common::SharedPtr<Datum> LiteralNode::getValue() {
+	return value;
+}
+
+bool LiteralNode::hasSpaces(bool) {
+	return false;
+}
+
+/* BlockNode */
+
+void BlockNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	for (const auto &child : children) {
+		child->writeScriptText(code, dot, sum);
+		code.writeLine();
+	}
+}
+
+void BlockNode::addChild(Common::SharedPtr<Node> child) {
+	child->parent = this;
+	children.push_back(Common::move(child));
+}
+
+/* HandlerNode */
+
+void HandlerNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	if (handler->isGenericEvent) {
+		block->writeScriptText(code, dot, sum);
+	} else {
+		Script *script = handler->script;
+		bool isMethod = script->isFactory();
+		if (isMethod) {
+			code.write("method ");
+		} else {
+			code.write("on ");
+		}
+		code.write(handler->name);
+		if (handler->argumentNames.size() > 0) {
+			code.write(" ");
+			for (size_t i = 0; i < handler->argumentNames.size(); i++) {
+				if (i > 0)
+					code.write(", ");
+				code.write(handler->argumentNames[i]);
+			}
+		}
+		code.writeLine();
+		code.indent();
+		if (isMethod && script->propertyNames.size() > 0 && handler == &script->handlers[0]) {
+			code.write("instance ");
+			for (size_t i = 0; i < script->propertyNames.size(); i++) {
+				if (i > 0)
+					code.write(", ");
+				code.write(script->propertyNames[i]);
+			}
+			code.writeLine();
+		}
+		if (handler->globalNames.size() > 0) {
+			code.write("global ");
+			for (size_t i = 0; i < handler->globalNames.size(); i++) {
+				if (i > 0)
+					code.write(", ");
+				code.write(handler->globalNames[i]);
+			}
+			code.writeLine();
+		}
+		block->writeScriptText(code, dot, sum);
+		code.unindent();
+		if (!isMethod) {
+			code.writeLine("end");
+		}
+	}
+}
+
+/* ExitStmtNode */
+
+void ExitStmtNode::writeScriptText(CodeWriter &code, bool, bool) const {
+	code.write("exit");
+}
+
+/* InverseOpNode */
+
+void InverseOpNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("-");
+
+	bool parenOperand = operand->hasSpaces(dot);
+	if (parenOperand) {
+		code.write("(");
+	}
+	operand->writeScriptText(code, dot, sum);
+	if (parenOperand) {
+		code.write(")");
+	}
+}
+
+/* NotOpNode */
+
+void NotOpNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("not ");
+
+	bool parenOperand = operand->hasSpaces(dot);
+	if (parenOperand) {
+		code.write("(");
+	}
+	operand->writeScriptText(code, dot, sum);
+	if (parenOperand) {
+		code.write(")");
+	}
+}
+
+/* BinaryOpNode */
+
+void BinaryOpNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	unsigned int precedence = getPrecedence();
+	bool parenLeft = false;
+	bool parenRight = false;
+	if (precedence) {
+		if (left->type == kBinaryOpNode) {
+			auto leftBinaryOpNode = static_cast<BinaryOpNode *>(left.get());
+			parenLeft = (leftBinaryOpNode->getPrecedence() != precedence);
+		}
+		parenRight = (right->type == kBinaryOpNode);
+	}
+
+	if (parenLeft) {
+		code.write("(");
+	}
+	left->writeScriptText(code, dot, sum);
+	if (parenLeft) {
+		code.write(")");
+	}
+
+	code.write(" ");
+	code.write(StandardNames::getName(StandardNames::binaryOpNames, opcode));
+	code.write(" ");
+
+	if (parenRight) {
+		code.write("(");
+	}
+	right->writeScriptText(code, dot, sum);
+	if (parenRight) {
+		code.write(")");
+	}
+}
+
+unsigned int BinaryOpNode::getPrecedence() const {
+	switch (opcode) {
+	case kOpMul:
+	case kOpDiv:
+	case kOpMod:
+		return 1;
+	case kOpAdd:
+	case kOpSub:
+		return 2;
+	case kOpLt:
+	case kOpLtEq:
+	case kOpNtEq:
+	case kOpEq:
+	case kOpGt:
+	case kOpGtEq:
+		return 3;
+	case kOpAnd:
+		return 4;
+	case kOpOr:
+		return 5;
+	default:
+		break;
+	}
+	return 0;
+}
+
+/* ChunkExprNode */
+
+void ChunkExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write(StandardNames::getName(StandardNames::chunkTypeNames, type));
+	code.write(" ");
+	first->writeScriptText(code, dot, sum);
+	if (!(last->type == kLiteralNode && last->getValue()->type == kDatumInt && last->getValue()->i == 0)) {
+		code.write(" to ");
+		last->writeScriptText(code, dot, sum);
+	}
+	code.write(" of ");
+	string->writeScriptText(code, false, sum); // we want the string to always be verbose
+}
+
+/* ChunkHiliteStmtNode */
+
+void ChunkHiliteStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("hilite ");
+	chunk->writeScriptText(code, dot, sum);
+}
+
+/* ChunkDeleteStmtNode */
+
+void ChunkDeleteStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("delete ");
+	chunk->writeScriptText(code, dot, sum);
+}
+
+/* SpriteIntersectsExprNode */
+
+void SpriteIntersectsExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("sprite ");
+
+	bool parenFirstSprite = (firstSprite->type == kBinaryOpNode);
+	if (parenFirstSprite) {
+		code.write("(");
+	}
+	firstSprite->writeScriptText(code, dot, sum);
+	if (parenFirstSprite) {
+		code.write(")");
+	}
+
+	code.write(" intersects ");
+
+	bool parenSecondSprite = (secondSprite->type == kBinaryOpNode);
+	if (parenSecondSprite) {
+		code.write("(");
+	}
+	secondSprite->writeScriptText(code, dot, sum);
+	if (parenSecondSprite) {
+		code.write(")");
+	}
+}
+
+/* SpriteWithinExprNode */
+
+void SpriteWithinExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("sprite ");
+
+	bool parenFirstSprite = (firstSprite->type == kBinaryOpNode);
+	if (parenFirstSprite) {
+		code.write("(");
+	}
+	firstSprite->writeScriptText(code, dot, sum);
+	if (parenFirstSprite) {
+		code.write(")");
+	}
+
+	code.write(" within ");
+
+	bool parenSecondSprite = (secondSprite->type == kBinaryOpNode);
+	if (parenSecondSprite) {
+		code.write("(");
+	}
+	secondSprite->writeScriptText(code, dot, sum);
+	if (parenSecondSprite) {
+		code.write(")");
+	}
+}
+
+/* MemberExprNode */
+
+void MemberExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	bool hasCastID = castID && !(castID->type == kLiteralNode && castID->getValue()->type == kDatumInt && castID->getValue()->i == 0);
+	code.write(type);
+	if (dot) {
+		code.write("(");
+		memberID->writeScriptText(code, dot, sum);
+		if (hasCastID) {
+			code.write(", ");
+			castID->writeScriptText(code, dot, sum);
+		}
+		code.write(")");
+	} else {
+		code.write(" ");
+
+		bool parenMemberID = (memberID->type == kBinaryOpNode);
+		if (parenMemberID) {
+			code.write("(");
+		}
+		memberID->writeScriptText(code, dot, sum);
+		if (parenMemberID) {
+			code.write(")");
+		}
+
+		if (hasCastID) {
+			code.write(" of castLib ");
+
+			bool parenCastID = (castID->type == kBinaryOpNode);
+			if (parenCastID) {
+				code.write("(");
+			}
+			castID->writeScriptText(code, dot, sum);
+			if (parenCastID) {
+				code.write(")");
+			}
+		}
+	}
+}
+
+bool MemberExprNode::hasSpaces(bool dot) {
+	return !dot;
+}
+
+/* VarNode */
+
+void VarNode::writeScriptText(CodeWriter &code, bool, bool) const {
+	code.write(varName);
+}
+
+bool VarNode::hasSpaces(bool) {
+	return false;
+}
+
+/* AssignmentStmtNode */
+
+void AssignmentStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	if (!dot || forceVerbose) {
+		code.write("set ");
+		variable->writeScriptText(code, false, sum); // we want the variable to always be verbose
+		code.write(" to ");
+		value->writeScriptText(code, dot, sum);
+	} else {
+		variable->writeScriptText(code, dot, sum);
+		code.write(" = ");
+		value->writeScriptText(code, dot, sum);
+	}
+}
+
+/* IfStmtNode */
+
+void IfStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("if ");
+	condition->writeScriptText(code, dot, sum);
+	code.write(" then");
+	if (sum) {
+		if (hasElse) {
+			code.write(" / else");
+		}
+	} else {
+		code.writeLine();
+		code.indent();
+		block1->writeScriptText(code, dot, sum);
+		code.unindent();
+		if (hasElse) {
+			code.writeLine("else");
+			code.indent();
+			block2->writeScriptText(code, dot, sum);
+			code.unindent();
+		}
+		code.write("end if");
+	}
+}
+
+/* RepeatWhileStmtNode */
+
+void RepeatWhileStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("repeat while ");
+	condition->writeScriptText(code, dot, sum);
+	if (!sum) {
+		code.writeLine();
+		code.indent();
+		block->writeScriptText(code, dot, sum);
+		code.unindent();
+		code.write("end repeat");
+	}
+}
+
+/* RepeatWithInStmtNode */
+
+void RepeatWithInStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("repeat with ");
+	code.write(varName);
+	code.write(" in ");
+	list->writeScriptText(code, dot, sum);
+	if (!sum) {
+		code.writeLine();
+		code.indent();
+		block->writeScriptText(code, dot, sum);
+		code.unindent();
+		code.write("end repeat");
+	}
+}
+
+/* RepeatWithToStmtNode */
+
+void RepeatWithToStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("repeat with ");
+	code.write(varName);
+	code.write(" = ");
+	start->writeScriptText(code, dot, sum);
+	if (up) {
+		code.write(" to ");
+	} else {
+		code.write(" down to ");
+	}
+	end->writeScriptText(code, dot, sum);
+	if (!sum) {
+		code.writeLine();
+		code.indent();
+		block->writeScriptText(code, dot, sum);
+		code.unindent();
+		code.write("end repeat");
+	}
+}
+
+/* CaseLabelNode */
+
+void CaseLabelNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	if (sum) {
+		code.write("(case) ");
+		if (parent->type == kCaseLabelNode) {
+			auto parentLabel = static_cast<CaseLabelNode *>(parent);
+			if (parentLabel->nextOr.get() == this) {
+				code.write("..., ");
+			}
+		}
+
+		bool parenValue = value->hasSpaces(dot);
+		if (parenValue) {
+			code.write("(");
+		}
+		value->writeScriptText(code, dot, sum);
+		if (parenValue) {
+			code.write(")");
+		}
+
+		if (nextOr) {
+			code.write(", ...");
+		} else {
+			code.write(":");
+		}
+	} else {
+		bool parenValue = value->hasSpaces(dot);
+		if (parenValue) {
+			code.write("(");
+		}
+		value->writeScriptText(code, dot, sum);
+		if (parenValue) {
+			code.write(")");
+		}
+
+		if (nextOr) {
+			code.write(", ");
+			nextOr->writeScriptText(code, dot, sum);
+		} else {
+			code.writeLine(":");
+			code.indent();
+			block->writeScriptText(code, dot, sum);
+			code.unindent();
+		}
+		if (nextLabel) {
+			nextLabel->writeScriptText(code, dot, sum);
+		}
+	}
+}
+
+/* OtherwiseNode */
+
+void OtherwiseNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	if (sum) {
+		code.write("(case) otherwise:");
+	} else {
+		code.writeLine("otherwise:");
+		code.indent();
+		block->writeScriptText(code, dot, sum);
+		code.unindent();
+	}
+}
+
+/* EndCaseNode */
+
+void EndCaseNode::writeScriptText(CodeWriter &code, bool, bool) const {
+	code.write("end case");
+}
+
+/* CaseStmtNode */
+
+void CaseStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("case ");
+	value->writeScriptText(code, dot, sum);
+	code.write(" of");
+	if (sum) {
+		if (!firstLabel) {
+			if (otherwise) {
+				code.write(" / otherwise:");
+			} else {
+				code.write(" / end case");
+			}
+		}
+	} else {
+		code.writeLine();
+		code.indent();
+		if (firstLabel) {
+			firstLabel->writeScriptText(code, dot, sum);
+		}
+		if (otherwise) {
+			otherwise->writeScriptText(code, dot, sum);
+		}
+		code.unindent();
+		code.write("end case");
+	}
+}
+
+void CaseStmtNode::addOtherwise() {
+	otherwise = Common::SharedPtr<OtherwiseNode>(new OtherwiseNode());
+	otherwise->parent = this;
+	otherwise->block->endPos = endPos;
+}
+
+/* TellStmtNode */
+
+void TellStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("tell ");
+	window->writeScriptText(code, dot, sum);
+	if (!sum) {
+		code.writeLine();
+		code.indent();
+		block->writeScriptText(code, dot, sum);
+		code.unindent();
+		code.write("end tell");
+	}
+}
+
+/* SoundCmdStmtNode */
+
+void SoundCmdStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("sound ");
+	code.write(cmd);
+	if (argList->getValue()->l.size() > 0) {
+		code.write(" ");
+		argList->writeScriptText(code, dot, sum);
+	}
+}
+
+/* PlayCmdStmtNode */
+
+void PlayCmdStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	auto &rawArgs = argList->getValue()->l;
+
+	code.write("play");
+
+	if (rawArgs.size() == 0) {
+		code.write(" done");
+		return;
+	}
+
+	auto &frame = rawArgs[0];
+	if (rawArgs.size() == 1) {
+		code.write(" frame ");
+		frame->writeScriptText(code, dot, sum);
+		return;
+	}
+
+	auto &movie = rawArgs[1];
+	if (!(frame->type == kLiteralNode && frame->getValue()->type == kDatumInt && frame->getValue()->i == 1)) {
+		code.write(" frame ");
+		frame->writeScriptText(code, dot, sum);
+		code.write(" of");
+	}
+	code.write(" movie ");
+	movie->writeScriptText(code, dot, sum);
+}
+
+/* CallNode */
+
+bool CallNode::noParens() const {
+	if (isStatement) {
+		// TODO: Make a complete list of commonly paren-less commands
+		if (name == "put")
+			return true;
+		if (name == "return")
+			return true;
+	}
+
+	return false;
+}
+
+bool CallNode::isMemberExpr() const {
+	if (isExpression) {
+		size_t nargs = argList->getValue()->l.size();
+		if (name == "cast" && (nargs == 1 || nargs == 2))
+			return true;
+		if (name == "member" && (nargs == 1 || nargs == 2))
+			return true;
+		if (name == "script" && (nargs == 1 || nargs == 2))
+			return true;
+		if (name == "castLib" && nargs == 1)
+			return true;
+		if (name == "window" && nargs == 1)
+			return true;
+	}
+
+	return false;
+}
+
+void CallNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	if (isExpression && argList->getValue()->l.size() == 0) {
+		if (name == "pi") {
+			code.write("PI");
+			return;
+		}
+		if (name == "space") {
+			code.write("SPACE");
+			return;
+		}
+		if (name == "void") {
+			code.write("VOID");
+			return;
+		}
+	}
+
+	if (!dot && isMemberExpr()) {
+		/**
+		 * In some cases, member expressions such as `member 1 of castLib 1` compile
+		 * to the function call `member(1, 1)`. However, this doesn't parse correctly
+		 * in pre-dot-syntax versions of Director, and `put(member(1, 1))` does not
+		 * compile. Therefore, we rewrite these expressions to the verbose syntax when
+		 * in verbose mode.
+		 */
+		code.write(name);
+		code.write(" ");
+
+		auto memberID = argList->getValue()->l[0];
+		bool parenMemberID = (memberID->type == kBinaryOpNode);
+		if (parenMemberID) {
+			code.write("(");
+		}
+		memberID->writeScriptText(code, dot, sum);
+		if (parenMemberID) {
+			code.write(")");
+		}
+
+		if (argList->getValue()->l.size() == 2) {
+			code.write(" of castLib ");
+
+			auto castID = argList->getValue()->l[1];
+			bool parenCastID = (castID->type == kBinaryOpNode);
+			if (parenCastID) {
+				code.write("(");
+			}
+			castID->writeScriptText(code, dot, sum);
+			if (parenCastID) {
+				code.write(")");
+			}
+		}
+		return;
+	}
+
+	code.write(name);
+	if (noParens()) {
+		code.write(" ");
+		argList->writeScriptText(code, dot, sum);
+	} else {
+		code.write("(");
+		argList->writeScriptText(code, dot, sum);
+		code.write(")");
+	}
+}
+
+bool CallNode::hasSpaces(bool dot) {
+	if (!dot && isMemberExpr())
+		return true;
+
+	if (noParens())
+		return true;
+
+	return false;
+}
+
+/* ObjCallNode */
+
+void ObjCallNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	auto &rawArgs = argList->getValue()->l;
+
+	auto &obj = rawArgs[0];
+	bool parenObj = obj->hasSpaces(dot);
+	if (parenObj) {
+		code.write("(");
+	}
+	obj->writeScriptText(code, dot, sum);
+	if (parenObj) {
+		code.write(")");
+	}
+
+	code.write(".");
+	code.write(name);
+	code.write("(");
+	for (size_t i = 1; i < rawArgs.size(); i++) {
+		if (i > 1)
+			code.write(", ");
+		rawArgs[i]->writeScriptText(code, dot, sum);
+	}
+	code.write(")");
+}
+
+bool ObjCallNode::hasSpaces(bool) {
+	return false;
+}
+
+/* ObjCallV4Node */
+
+void ObjCallV4Node::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	obj->writeScriptText(code, dot, sum);
+	code.write("(");
+	argList->writeScriptText(code, dot, sum);
+	code.write(")");
+}
+
+bool ObjCallV4Node::hasSpaces(bool) {
+	return false;
+}
+
+/* TheExprNode */
+
+void TheExprNode::writeScriptText(CodeWriter &code, bool, bool) const {
+	code.write("the ");
+	code.write(prop);
+}
+
+/* LastStringChunkExprNode */
+
+void LastStringChunkExprNode::writeScriptText(CodeWriter &code, bool, bool sum) const {
+	code.write("the last ");
+	code.write(StandardNames::getName(StandardNames::chunkTypeNames, type));
+	code.write(" in ");
+
+	bool parenObj = (obj->type == kBinaryOpNode);
+	if (parenObj) {
+		code.write("(");
+	}
+	obj->writeScriptText(code, false, sum); // we want the object to always be verbose
+	if (parenObj) {
+		code.write(")");
+	}
+}
+
+/* StringChunkCountExprNode */
+
+void StringChunkCountExprNode::writeScriptText(CodeWriter &code, bool, bool sum) const {
+	code.write("the number of ");
+	code.write(StandardNames::getName(StandardNames::chunkTypeNames, type)); // we want the object to always be verbose
+	code.write("s in ");
+
+	bool parenObj = (obj->type == kBinaryOpNode);
+	if (parenObj) {
+		code.write("(");
+	}
+	obj->writeScriptText(code, false, sum);
+	if (parenObj) {
+		code.write(")");
+	}
+}
+
+/* MenuPropExprNode */
+
+void MenuPropExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("the ");
+	code.write(StandardNames::getName(StandardNames::menuPropertyNames, prop));
+	code.write(" of menu ");
+
+	bool parenMenuID = (menuID->type == kBinaryOpNode);
+	if (parenMenuID) {
+		code.write("(");
+	}
+	menuID->writeScriptText(code, dot, sum);
+	if (parenMenuID) {
+		code.write(")");
+	}
+}
+
+/* MenuItemPropExprNode */
+
+void MenuItemPropExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("the ");
+	code.write(StandardNames::getName(StandardNames::menuItemPropertyNames, prop));
+	code.write(" of menuItem ");
+
+	bool parenItemID = (itemID->type == kBinaryOpNode);
+	if (parenItemID) {
+		code.write("(");
+	}
+	itemID->writeScriptText(code, dot, sum);
+	if (parenItemID) {
+		code.write(")");
+	}
+
+	code.write(" of menu ");
+
+	bool parenMenuID = (menuID->type == kBinaryOpNode);
+	if (parenMenuID) {
+		code.write("(");
+	}
+	menuID->writeScriptText(code, dot, sum);
+	if (parenMenuID) {
+		code.write(")");
+	}
+}
+
+/* SoundPropExprNode */
+
+void SoundPropExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("the ");
+	code.write(StandardNames::getName(StandardNames::soundPropertyNames, prop));
+	code.write(" of sound ");
+
+	bool parenSoundID = (soundID->type == kBinaryOpNode);
+	if (parenSoundID) {
+		code.write("(");
+	}
+	soundID->writeScriptText(code, dot, sum);
+	if (parenSoundID) {
+		code.write(")");
+	}
+}
+
+/* SpritePropExprNode */
+
+void SpritePropExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("the ");
+	code.write(StandardNames::getName(StandardNames::spritePropertyNames, prop));
+	code.write(" of sprite ");
+
+	bool parenSpriteID = (spriteID->type == kBinaryOpNode);
+	if (parenSpriteID) {
+		code.write("(");
+	}
+	spriteID->writeScriptText(code, dot, sum);
+	if (parenSpriteID) {
+		code.write(")");
+	}
+}
+
+/* ThePropExprNode */
+
+void ThePropExprNode::writeScriptText(CodeWriter &code, bool, bool sum) const {
+	code.write("the ");
+	code.write(prop);
+	code.write(" of ");
+
+	bool parenObj = (obj->type == kBinaryOpNode);
+	if (parenObj) {
+		code.write("(");
+	}
+	obj->writeScriptText(code, false, sum); // we want the object to always be verbose
+	if (parenObj) {
+		code.write(")");
+	}
+}
+
+/* ObjPropExprNode */
+
+void ObjPropExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	if (dot) {
+		bool parenObj = obj->hasSpaces(dot);
+		if (parenObj) {
+			code.write("(");
+		}
+		obj->writeScriptText(code, dot, sum);
+		if (parenObj) {
+			code.write(")");
+		}
+
+		code.write(".");
+		code.write(prop);
+	} else {
+		code.write("the ");
+		code.write(prop);
+		code.write(" of ");
+
+		bool parenObj = (obj->type == kBinaryOpNode);
+		if (parenObj) {
+			code.write("(");
+		}
+		obj->writeScriptText(code, dot, sum);
+		if (parenObj) {
+			code.write(")");
+		}
+	}
+}
+
+bool ObjPropExprNode::hasSpaces(bool dot) {
+	return !dot;
+}
+
+/* ObjBracketExprNode */
+
+void ObjBracketExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	bool parenObj = obj->hasSpaces(dot);
+	if (parenObj) {
+		code.write("(");
+	}
+	obj->writeScriptText(code, dot, sum);
+	if (parenObj) {
+		code.write(")");
+	}
+
+	code.write("[");
+	prop->writeScriptText(code, dot, sum);
+	code.write("]");
+}
+
+bool ObjBracketExprNode::hasSpaces(bool) {
+	return false;
+}
+
+/* ObjPropIndexExprNode */
+
+void ObjPropIndexExprNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	bool parenObj = obj->hasSpaces(dot);
+	if (parenObj) {
+		code.write("(");
+	}
+	obj->writeScriptText(code, dot, sum);
+	if (parenObj) {
+		code.write(")");
+	}
+
+	code.write(".");
+	code.write(prop);
+	code.write("[");
+	index->writeScriptText(code, dot, sum);
+	if (index2) {
+		code.write("..");
+		index2->writeScriptText(code, dot, sum);
+	}
+	code.write("]");
+}
+
+bool ObjPropIndexExprNode::hasSpaces(bool) {
+	return false;
+}
+
+/* ExitRepeatStmtNode */
+
+void ExitRepeatStmtNode::writeScriptText(CodeWriter &code, bool, bool) const {
+	code.write("exit repeat");
+}
+
+/* NextRepeatStmtNode */
+
+void NextRepeatStmtNode::writeScriptText(CodeWriter &code, bool, bool) const {
+	code.write("next repeat");
+}
+
+/* PutStmtNode */
+
+void PutStmtNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("put ");
+	value->writeScriptText(code, dot, sum);
+	code.write(" ");
+	code.write(StandardNames::getName(StandardNames::putTypeNames, type));
+	code.write(" ");
+	variable->writeScriptText(code, false, sum); // we want the variable to always be verbose
+}
+
+/* WhenStmtNode */
+
+void WhenStmtNode::writeScriptText(CodeWriter &code, bool, bool) const {
+	code.write("when ");
+	code.write(StandardNames::getName(StandardNames::whenEventNames, event));
+	code.write(" then");
+
+	code.doIndentation = false;
+	for (size_t i = 0; i < script.size(); i++) {
+		char ch = script[i];
+		if (ch == '\r') {
+			if (i != script.size() - 1) {
+				code.writeLine();
+			}
+		} else {
+			code.write(ch);
+		}
+	}
+	code.doIndentation = true;
+}
+
+/* NewObjNode */
+
+void NewObjNode::writeScriptText(CodeWriter &code, bool dot, bool sum) const {
+	code.write("new ");
+	code.write(objType);
+	code.write("(");
+	objArgs->writeScriptText(code, dot, sum);
+	code.write(")");
+}
+
+} // namespace LingoDec
diff --git a/engines/director/lingo/lingodec/ast.h b/engines/director/lingo/lingodec/ast.h
new file mode 100644
index 00000000000..391b0427fb1
--- /dev/null
+++ b/engines/director/lingo/lingodec/ast.h
@@ -0,0 +1,860 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef LINGODEC_AST_H
+#define LINGODEC_AST_H
+
+#include "common/array.h"
+#include "common/ptr.h"
+#include "common/str.h"
+#include "common/util.h"
+#include "./codewriter.h"
+#include "./enums.h"
+
+namespace LingoDec {
+
+struct CaseLabelNode;
+struct Handler;
+struct LoopNode;
+struct Node;
+struct RepeatWithInStmtNode;
+
+/* Datum */
+
+struct Datum {
+	DatumType type;
+	int i;
+	double f;
+	Common::String s;
+	Common::Array<Common::SharedPtr<Node>> l;
+
+	Datum() {
+		type = kDatumVoid;
+	}
+	Datum(int val) {
+		type = kDatumInt;
+		i = val;
+	}
+	Datum(double val) {
+		type = kDatumFloat;
+		f = val;
+	}
+	Datum(DatumType t, Common::String val) {
+		type = t;
+		s = val;
+	}
+	Datum(DatumType t, Common::Array<Common::SharedPtr<Node>> val) {
+		type = t;
+		l = val;
+	}
+
+	int toInt();
+	void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* Node */
+
+struct Node {
+	NodeType type;
+	bool isExpression;
+	bool isStatement;
+	bool isLabel;
+	bool isLoop;
+	Node *parent;
+
+	Node(NodeType t) : type(t), isExpression(false), isStatement(false), isLabel(false), isLoop(false), parent(nullptr) {}
+	virtual ~Node() = default;
+	virtual void writeScriptText(CodeWriter&, bool, bool) const {}
+	virtual Common::SharedPtr<Datum> getValue();
+	Node *ancestorStatement();
+	LoopNode *ancestorLoop();
+	virtual bool hasSpaces(bool dot);
+};
+
+/* ExprNode */
+
+struct ExprNode : Node {
+	ExprNode(NodeType t) : Node(t) {
+		isExpression = true;
+	}
+	virtual ~ExprNode() = default;
+};
+
+/* StmtNode */
+
+struct StmtNode : Node {
+	StmtNode(NodeType t) : Node(t) {
+		isStatement = true;
+	}
+	virtual ~StmtNode() = default;
+};
+
+/* LabelNode */
+
+struct LabelNode : Node {
+	LabelNode(NodeType t) : Node(t) {
+		isLabel = true;
+	}
+	virtual ~LabelNode() = default;
+};
+
+/* LoopNode */
+
+struct LoopNode : StmtNode {
+	uint32_t startIndex;
+
+	LoopNode(NodeType t, uint32_t startIndex) : StmtNode(t), startIndex(startIndex) {
+		isLoop = true;
+	}
+	virtual ~LoopNode() = default;
+};
+
+/* ErrorNode */
+
+struct ErrorNode : ExprNode {
+	ErrorNode() : ExprNode(kErrorNode) {}
+	virtual ~ErrorNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual bool hasSpaces(bool dot);
+};
+
+/* CommentNode */
+
+struct CommentNode : Node {
+	Common::String text;
+
+	CommentNode(Common::String t) : Node(kCommentNode), text(t) {}
+	virtual ~CommentNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* LiteralNode */
+
+struct LiteralNode : ExprNode {
+	Common::SharedPtr<Datum> value;
+
+	LiteralNode(Common::SharedPtr<Datum> d) : ExprNode(kLiteralNode) {
+		value = Common::move(d);
+	}
+	virtual ~LiteralNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual Common::SharedPtr<Datum> getValue();
+	virtual bool hasSpaces(bool dot);
+};
+
+/* BlockNode */
+
+struct BlockNode : Node {
+	Common::Array<Common::SharedPtr<Node>> children;
+
+	// for use during translation:
+	uint32_t endPos;
+	CaseLabelNode *currentCaseLabel;
+
+	BlockNode() : Node(kBlockNode), endPos(-1), currentCaseLabel(nullptr) {}
+	virtual ~BlockNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	void addChild(Common::SharedPtr<Node> child);
+};
+
+/* HandlerNode */
+
+struct HandlerNode : Node {
+	Handler *handler;
+	Common::SharedPtr<BlockNode> block;
+
+	HandlerNode(Handler *h)
+		: Node(kHandlerNode), handler(h) {
+		block = Common::SharedPtr<BlockNode>(new BlockNode());
+		block->parent = this;
+	}
+	virtual ~HandlerNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* ExitStmtNode */
+
+struct ExitStmtNode : StmtNode {
+	ExitStmtNode() : StmtNode(kExitStmtNode) {}
+	virtual ~ExitStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* InverseOpNode */
+
+struct InverseOpNode : ExprNode {
+	Common::SharedPtr<Node> operand;
+
+	InverseOpNode(Common::SharedPtr<Node> o) : ExprNode(kInverseOpNode) {
+		operand = Common::move(o);
+		operand->parent = this;
+	}
+	virtual ~InverseOpNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* NotOpNode */
+
+struct NotOpNode : ExprNode {
+	Common::SharedPtr<Node> operand;
+
+	NotOpNode(Common::SharedPtr<Node> o) : ExprNode(kNotOpNode) {
+		operand = Common::move(o);
+		operand->parent = this;
+	}
+	virtual ~NotOpNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* BinaryOpNode */
+
+struct BinaryOpNode : ExprNode {
+	OpCode opcode;
+	Common::SharedPtr<Node> left;
+	Common::SharedPtr<Node> right;
+
+	BinaryOpNode(OpCode op, Common::SharedPtr<Node> a, Common::SharedPtr<Node> b)
+		: ExprNode(kBinaryOpNode), opcode(op) {
+		left = Common::move(a);
+		left->parent = this;
+		right = Common::move(b);
+		right->parent = this;
+	}
+	virtual ~BinaryOpNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual unsigned int getPrecedence() const;
+};
+
+/* ChunkExprNode */
+
+struct ChunkExprNode : ExprNode {
+	ChunkExprType type;
+	Common::SharedPtr<Node> first;
+	Common::SharedPtr<Node> last;
+	Common::SharedPtr<Node> string;
+
+	ChunkExprNode(ChunkExprType t, Common::SharedPtr<Node> a, Common::SharedPtr<Node> b, Common::SharedPtr<Node> s)
+		: ExprNode(kChunkExprNode), type(t) {
+		first = Common::move(a);
+		first->parent = this;
+		last = Common::move(b);
+		last->parent = this;
+		string = Common::move(s);
+		string->parent = this;
+	}
+	virtual ~ChunkExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* ChunkHiliteStmtNode */
+
+struct ChunkHiliteStmtNode : StmtNode {
+	Common::SharedPtr<Node> chunk;
+
+	ChunkHiliteStmtNode(Common::SharedPtr<Node> c) : StmtNode(kChunkHiliteStmtNode) {
+		chunk = Common::move(c);
+		chunk->parent = this;
+	}
+	virtual ~ChunkHiliteStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* ChunkDeleteStmtNode */
+
+struct ChunkDeleteStmtNode : StmtNode {
+	Common::SharedPtr<Node> chunk;
+
+	ChunkDeleteStmtNode(Common::SharedPtr<Node> c) : StmtNode(kChunkDeleteStmtNode) {
+		chunk = Common::move(c);
+		chunk->parent = this;
+	}
+	virtual ~ChunkDeleteStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* SpriteIntersectsExprNode */
+
+struct SpriteIntersectsExprNode : ExprNode {
+	Common::SharedPtr<Node> firstSprite;
+	Common::SharedPtr<Node> secondSprite;
+
+	SpriteIntersectsExprNode(Common::SharedPtr<Node> a, Common::SharedPtr<Node> b)
+		: ExprNode(kSpriteIntersectsExprNode) {
+		firstSprite = Common::move(a);
+		firstSprite->parent = this;
+		secondSprite = Common::move(b);
+		secondSprite->parent = this;
+	}
+	virtual ~SpriteIntersectsExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* SpriteWithinExprNode */
+
+struct SpriteWithinExprNode : ExprNode {
+	Common::SharedPtr<Node> firstSprite;
+	Common::SharedPtr<Node> secondSprite;
+
+	SpriteWithinExprNode(Common::SharedPtr<Node> a, Common::SharedPtr<Node> b)
+		: ExprNode(kSpriteWithinExprNode) {
+		firstSprite = Common::move(a);
+		firstSprite->parent = this;
+		secondSprite = Common::move(b);
+		secondSprite->parent = this;
+	}
+	virtual ~SpriteWithinExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* MemberExprNode */
+
+struct MemberExprNode : ExprNode {
+	Common::String type;
+	Common::SharedPtr<Node> memberID;
+	Common::SharedPtr<Node> castID;
+
+	MemberExprNode(Common::String type, Common::SharedPtr<Node> memberID, Common::SharedPtr<Node> castID)
+		: ExprNode(kMemberExprNode), type(type) {
+		this->memberID = Common::move(memberID);
+		this->memberID->parent = this;
+		if (castID) {
+			this->castID = Common::move(castID);
+			this->castID->parent = this;
+		}
+	}
+	virtual ~MemberExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual bool hasSpaces(bool dot);
+};
+
+/* VarNode */
+
+struct VarNode : ExprNode {
+	Common::String varName;
+
+	VarNode(Common::String v) : ExprNode(kVarNode), varName(v) {}
+	virtual ~VarNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual bool hasSpaces(bool dot);
+};
+
+/* AssignmentStmtNode */
+
+struct AssignmentStmtNode : StmtNode {
+	Common::SharedPtr<Node> variable;
+	Common::SharedPtr<Node> value;
+	bool forceVerbose;
+
+	AssignmentStmtNode(Common::SharedPtr<Node> var, Common::SharedPtr<Node> val, bool forceVerbose = false)
+		: StmtNode(kAssignmentStmtNode), forceVerbose(forceVerbose) {
+		variable = Common::move(var);
+		variable->parent = this;
+		value = Common::move(val);
+		value->parent = this;
+	}
+
+	virtual ~AssignmentStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* IfStmtNode */
+
+struct IfStmtNode : StmtNode {
+	bool hasElse;
+	Common::SharedPtr<Node> condition;
+	Common::SharedPtr<BlockNode> block1;
+	Common::SharedPtr<BlockNode> block2;
+
+	IfStmtNode(Common::SharedPtr<Node> c) : StmtNode(kIfStmtNode), hasElse(false) {
+		condition = Common::move(c);
+		condition->parent = this;
+		block1 = Common::SharedPtr<BlockNode>(new BlockNode());
+		block1->parent = this;
+		block2 = Common::SharedPtr<BlockNode>(new BlockNode());
+		block2->parent = this;
+	}
+	virtual ~IfStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* RepeatWhileStmtNode */
+
+struct RepeatWhileStmtNode : LoopNode {
+	Common::SharedPtr<Node> condition;
+	Common::SharedPtr<BlockNode> block;
+
+	RepeatWhileStmtNode(uint32_t startIndex, Common::SharedPtr<Node> c)
+		: LoopNode(kRepeatWhileStmtNode, startIndex) {
+		condition = Common::move(c);
+		condition->parent = this;
+		block = Common::SharedPtr<BlockNode>(new BlockNode());
+		block->parent = this;
+	}
+	virtual ~RepeatWhileStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* RepeatWithInStmtNode */
+
+struct RepeatWithInStmtNode : LoopNode {
+	Common::String varName;
+	Common::SharedPtr<Node> list;
+	Common::SharedPtr<BlockNode> block;
+
+	RepeatWithInStmtNode(uint32_t startIndex, Common::String v, Common::SharedPtr<Node> l)
+		: LoopNode(kRepeatWithInStmtNode, startIndex) {
+		varName = v;
+		list = Common::move(l);
+		list->parent = this;
+		block = Common::SharedPtr<BlockNode>(new BlockNode());
+		block->parent = this;
+	}
+	virtual ~RepeatWithInStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* RepeatWithToStmtNode */
+
+struct RepeatWithToStmtNode : LoopNode {
+	Common::String varName;
+	Common::SharedPtr<Node> start;
+	bool up;
+	Common::SharedPtr<Node> end;
+	Common::SharedPtr<BlockNode> block;
+
+	RepeatWithToStmtNode(uint32_t startIndex, Common::String v, Common::SharedPtr<Node> s, bool up, Common::SharedPtr<Node> e)
+		: LoopNode(kRepeatWithToStmtNode, startIndex), up(up) {
+		varName = v;
+		start = Common::move(s);
+		start->parent = this;
+		end = Common::move(e);
+		end->parent = this;
+		block = Common::SharedPtr<BlockNode>(new BlockNode());
+		block->parent = this;
+	}
+	virtual ~RepeatWithToStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* CaseLabelNode */
+
+struct CaseLabelNode : LabelNode {
+	Common::SharedPtr<Node> value;
+	CaseExpect expect;
+
+	Common::SharedPtr<CaseLabelNode> nextOr;
+
+	Common::SharedPtr<CaseLabelNode> nextLabel;
+	Common::SharedPtr<BlockNode> block;
+
+	CaseLabelNode(Common::SharedPtr<Node> v, CaseExpect e) : LabelNode(kCaseLabelNode), expect(e) {
+		value = Common::move(v);
+		value->parent = this;
+	}
+	virtual ~CaseLabelNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* OtherwiseNode */
+
+struct OtherwiseNode : LabelNode {
+	Common::SharedPtr<BlockNode> block;
+
+	OtherwiseNode() : LabelNode(kOtherwiseNode) {
+		block = Common::SharedPtr<BlockNode>(new BlockNode());
+		block->parent = this;
+	}
+	virtual ~OtherwiseNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* EndCaseNode */
+
+struct EndCaseNode : LabelNode {
+	EndCaseNode() : LabelNode(kEndCaseNode) {}
+	virtual ~EndCaseNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* CaseStmtNode */
+
+struct CaseStmtNode : StmtNode {
+	Common::SharedPtr<Node> value;
+	Common::SharedPtr<CaseLabelNode> firstLabel;
+	Common::SharedPtr<OtherwiseNode> otherwise;
+
+	// for use during translation:
+	int32_t endPos = -1;
+	int32_t potentialOtherwisePos = -1;
+
+	CaseStmtNode(Common::SharedPtr<Node> v) : StmtNode(kCaseStmtNode) {
+		value = Common::move(v);
+		value->parent = this;
+	}
+	virtual ~CaseStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	void addOtherwise();
+};
+
+/* TellStmtNode */
+
+struct TellStmtNode : StmtNode {
+	Common::SharedPtr<Node> window;
+	Common::SharedPtr<BlockNode> block;
+
+	TellStmtNode(Common::SharedPtr<Node> w) : StmtNode(kTellStmtNode) {
+		window = Common::move(w);
+		window->parent = this;
+		block = Common::SharedPtr<BlockNode>(new BlockNode());
+		block->parent = this;
+	}
+	virtual ~TellStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* SoundCmdStmtNode */
+
+struct SoundCmdStmtNode : StmtNode {
+	Common::String cmd;
+	Common::SharedPtr<Node> argList;
+
+	SoundCmdStmtNode(Common::String c, Common::SharedPtr<Node> a) : StmtNode(kSoundCmdStmtNode) {
+		cmd = c;
+		argList = Common::move(a);
+		argList->parent = this;
+	}
+	virtual ~SoundCmdStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* PlayCmdStmtNode */
+
+struct PlayCmdStmtNode : StmtNode {
+	Common::SharedPtr<Node> argList;
+
+	PlayCmdStmtNode(Common::SharedPtr<Node> a) : StmtNode(kPlayCmdStmtNode) {
+		argList = Common::move(a);
+		argList->parent = this;
+	}
+	virtual ~PlayCmdStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* CallNode */
+
+struct CallNode : Node {
+	Common::String name;
+	Common::SharedPtr<Node> argList;
+
+	CallNode(Common::String n, Common::SharedPtr<Node> a) : Node(kCallNode) {
+		name = n;
+		argList = Common::move(a);
+		argList->parent = this;
+		if (argList->getValue()->type == kDatumArgListNoRet)
+			isStatement = true;
+		else
+			isExpression = true;
+	}
+	virtual ~CallNode() = default;
+	bool noParens() const;
+	bool isMemberExpr() const;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual bool hasSpaces(bool dot);
+};
+
+/* ObjCallNode */
+
+struct ObjCallNode : Node {
+	Common::String name;
+	Common::SharedPtr<Node> argList;
+
+	ObjCallNode(Common::String n, Common::SharedPtr<Node> a) : Node(kObjCallNode) {
+		name = n;
+		argList = Common::move(a);
+		argList->parent = this;
+		if (argList->getValue()->type == kDatumArgListNoRet)
+			isStatement = true;
+		else
+			isExpression = true;
+	}
+	virtual ~ObjCallNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual bool hasSpaces(bool dot);
+};
+
+/* ObjCallV4Node */
+
+struct ObjCallV4Node : Node {
+	Common::SharedPtr<Node> obj;
+	Common::SharedPtr<Node> argList;
+
+	ObjCallV4Node(Common::SharedPtr<Node> o, Common::SharedPtr<Node> a) : Node(kObjCallV4Node) {
+		obj = o;
+		argList = Common::move(a);
+		argList->parent = this;
+		if (argList->getValue()->type == kDatumArgListNoRet)
+			isStatement = true;
+		else
+			isExpression = true;
+	}
+	virtual ~ObjCallV4Node() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual bool hasSpaces(bool dot);
+};
+
+/* TheExprNode */
+
+struct TheExprNode : ExprNode {
+	Common::String prop;
+
+	TheExprNode(Common::String p) : ExprNode(kTheExprNode), prop(p) {}
+	virtual ~TheExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* LastStringChunkExprNode */
+
+struct LastStringChunkExprNode : ExprNode {
+	ChunkExprType type;
+	Common::SharedPtr<Node> obj;
+
+	LastStringChunkExprNode(ChunkExprType t, Common::SharedPtr<Node> o)
+		: ExprNode(kLastStringChunkExprNode), type(t) {
+		obj = Common::move(o);
+		obj->parent = this;
+	}
+	virtual ~LastStringChunkExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* StringChunkCountExprNode */
+
+struct StringChunkCountExprNode : ExprNode {
+	ChunkExprType type;
+	Common::SharedPtr<Node> obj;
+
+	StringChunkCountExprNode(ChunkExprType t, Common::SharedPtr<Node> o)
+		: ExprNode(kStringChunkCountExprNode), type(t) {
+		obj = Common::move(o);
+		obj->parent = this;
+	}
+	virtual ~StringChunkCountExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* MenuPropExprNode */
+
+struct MenuPropExprNode : ExprNode {
+	Common::SharedPtr<Node> menuID;
+	unsigned int prop;
+
+	MenuPropExprNode(Common::SharedPtr<Node> m, unsigned int p)
+		: ExprNode(kMenuPropExprNode), prop(p) {
+		menuID = Common::move(m);
+		menuID->parent = this;
+	}
+	virtual ~MenuPropExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* MenuItemPropExprNode */
+
+struct MenuItemPropExprNode : ExprNode {
+	Common::SharedPtr<Node> menuID;
+	Common::SharedPtr<Node> itemID;
+	unsigned int prop;
+
+	MenuItemPropExprNode(Common::SharedPtr<Node> m, Common::SharedPtr<Node> i, unsigned int p)
+		: ExprNode(kMenuItemPropExprNode), prop(p) {
+		menuID = Common::move(m);
+		menuID->parent = this;
+		itemID = Common::move(i);
+		itemID->parent = this;
+	}
+	virtual ~MenuItemPropExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* SoundPropExprNode */
+
+struct SoundPropExprNode : ExprNode {
+	Common::SharedPtr<Node> soundID;
+	unsigned int prop;
+
+	SoundPropExprNode(Common::SharedPtr<Node> s, unsigned int p)
+		: ExprNode(kSoundPropExprNode), prop(p) {
+		soundID = Common::move(s);
+		soundID->parent = this;
+	}
+	virtual ~SoundPropExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* SpritePropExprNode */
+
+struct SpritePropExprNode : ExprNode {
+	Common::SharedPtr<Node> spriteID;
+	unsigned int prop;
+
+	SpritePropExprNode(Common::SharedPtr<Node> s, unsigned int p)
+		: ExprNode(kSpritePropExprNode), prop(p) {
+		spriteID = Common::move(s);
+		spriteID->parent = this;
+	}
+	virtual ~SpritePropExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* ThePropExprNode */
+
+struct ThePropExprNode : ExprNode {
+	Common::SharedPtr<Node> obj;
+	Common::String prop;
+
+	ThePropExprNode(Common::SharedPtr<Node> o, Common::String p)
+		: ExprNode(kThePropExprNode), prop(p) {
+		obj = Common::move(o);
+		obj->parent = this;
+	}
+	virtual ~ThePropExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* ObjPropExprNode */
+
+struct ObjPropExprNode : ExprNode {
+	Common::SharedPtr<Node> obj;
+	Common::String prop;
+
+	ObjPropExprNode(Common::SharedPtr<Node> o, Common::String p)
+		: ExprNode(kObjPropExprNode), prop(p) {
+		obj = Common::move(o);
+		obj->parent = this;
+	}
+	virtual ~ObjPropExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual bool hasSpaces(bool dot);
+};
+
+/* ObjBracketExprNode */
+
+struct ObjBracketExprNode : ExprNode {
+	Common::SharedPtr<Node> obj;
+	Common::SharedPtr<Node> prop;
+
+	ObjBracketExprNode(Common::SharedPtr<Node> o, Common::SharedPtr<Node> p)
+		: ExprNode(kObjBracketExprNode) {
+		obj = Common::move(o);
+		obj->parent = this;
+		prop = Common::move(p);
+		prop->parent = this;
+	}
+	virtual ~ObjBracketExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual bool hasSpaces(bool dot);
+};
+
+/* ObjPropIndexExprNode */
+
+struct ObjPropIndexExprNode : ExprNode {
+	Common::SharedPtr<Node> obj;
+	Common::String prop;
+	Common::SharedPtr<Node> index;
+	Common::SharedPtr<Node> index2;
+
+	ObjPropIndexExprNode(Common::SharedPtr<Node> o, Common::String p, Common::SharedPtr<Node> i, Common::SharedPtr<Node> i2)
+		: ExprNode(kObjPropIndexExprNode), prop(p) {
+		obj = Common::move(o);
+		obj->parent = this;
+		index = Common::move(i);
+		index->parent = this;
+		if (i2) {
+			index2 = Common::move(i2);
+			index2->parent = this;
+		}
+	}
+	virtual ~ObjPropIndexExprNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	virtual bool hasSpaces(bool dot);
+};
+
+/* ExitRepeatStmtNode */
+
+struct ExitRepeatStmtNode : StmtNode {
+	ExitRepeatStmtNode() : StmtNode(kExitRepeatStmtNode) {}
+	virtual ~ExitRepeatStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* NextRepeatStmtNode */
+
+struct NextRepeatStmtNode : StmtNode {
+	NextRepeatStmtNode() : StmtNode(kNextRepeatStmtNode) {}
+	virtual ~NextRepeatStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* PutStmtNode */
+
+struct PutStmtNode : StmtNode {
+	PutType type;
+	Common::SharedPtr<Node> variable;
+	Common::SharedPtr<Node> value;
+
+	PutStmtNode(PutType t, Common::SharedPtr<Node> var, Common::SharedPtr<Node> val)
+		: StmtNode(kPutStmtNode), type(t) {
+		variable = Common::move(var);
+		variable->parent = this;
+		value = Common::move(val);
+		value->parent = this;
+	}
+	virtual ~PutStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* WhenStmtNode */
+
+struct WhenStmtNode : StmtNode {
+	int event;
+	Common::String script;
+
+	WhenStmtNode(int e, Common::String s)
+		: StmtNode(kWhenStmtNode), event(e), script(s) {}
+	virtual ~WhenStmtNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* NewObjNode */
+
+struct NewObjNode : ExprNode {
+	Common::String objType;
+	Common::SharedPtr<Node> objArgs;
+
+	NewObjNode(Common::String o, Common::SharedPtr<Node> args) : ExprNode(kNewObjNode), objType(o), objArgs(args) {}
+	virtual ~NewObjNode() = default;
+	virtual void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+};
+
+/* AST */
+
+struct AST {
+	Common::SharedPtr<HandlerNode> root;
+	BlockNode *currentBlock;
+
+	AST(Handler *handler){
+		root = Common::SharedPtr<HandlerNode>(new HandlerNode(handler));
+		currentBlock = root->block.get();
+	}
+
+	void writeScriptText(CodeWriter &code, bool dot, bool sum) const;
+	void addStatement(Common::SharedPtr<Node> statement);
+	void enterBlock(BlockNode *block);
+	void exitBlock();
+};
+
+} // namespace LingoDec
+
+#endif // LINGODEC_AST_H
diff --git a/engines/director/lingo/lingodec/codewriter.cpp b/engines/director/lingo/lingodec/codewriter.cpp
new file mode 100644
index 00000000000..fa20eae8883
--- /dev/null
+++ b/engines/director/lingo/lingodec/codewriter.cpp
@@ -0,0 +1,75 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#include "./codewriter.h"
+
+namespace LingoDec {
+
+void CodeWriter::write(Common::String str) {
+	if (str.empty())
+		return;
+
+	writeIndentation();
+	_res += str;
+	_lineWidth += str.size();
+	_size += str.size();
+}
+
+void CodeWriter::write(char ch) {
+	writeIndentation();
+	_res += ch;
+	_lineWidth += 1;
+	_size += 1;
+}
+
+void CodeWriter::writeLine(Common::String str) {
+	if (str.empty()) {
+		_res += _lineEnding;
+	} else {
+		writeIndentation();
+		_res += str;
+		_res += _lineEnding;
+	}
+	_indentationWritten = false;
+	_lineWidth = 0;
+	_size += str.size() + _lineEnding.size();
+}
+
+void CodeWriter::writeLine() {
+	_res += _lineEnding;
+	_indentationWritten = false;
+	_lineWidth = 0;
+	_size += _lineEnding.size();
+}
+
+void CodeWriter::indent() {
+	_indentationLevel += 1;
+}
+
+void CodeWriter::unindent() {
+	if (_indentationLevel > 0) {
+		_indentationLevel -= 1;
+	}
+}
+
+Common::String CodeWriter::str() const {
+	return _res;
+}
+
+void CodeWriter::writeIndentation() {
+	if (_indentationWritten || !doIndentation)
+		return;
+
+	for (int i = 0; i < _indentationLevel; i++) {
+		_res += _indentation;
+	}
+
+	_indentationWritten = true;
+	_lineWidth = _indentationLevel * _indentation.size();
+	_size += _lineWidth;
+}
+
+} // namespace Common
diff --git a/engines/director/lingo/lingodec/codewriter.h b/engines/director/lingo/lingodec/codewriter.h
new file mode 100644
index 00000000000..81e0f8d7209
--- /dev/null
+++ b/engines/director/lingo/lingodec/codewriter.h
@@ -0,0 +1,51 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef LINGODEC_CODEWRITER_H
+#define LINGODEC_CODEWRITER_H
+
+#include "common/str.h"
+
+namespace LingoDec {
+
+class CodeWriter {
+protected:
+	Common::String _res;
+
+	Common::String _lineEnding;
+	Common::String _indentation;
+
+	int _indentationLevel = 0;
+	bool _indentationWritten = false;
+	size_t _lineWidth = 0;
+	size_t _size = 0;
+
+public:
+	bool doIndentation = true;
+
+public:
+	CodeWriter(Common::String lineEnding, Common::String indentation = "  ")
+		: _lineEnding(lineEnding), _indentation(indentation) {}
+
+	void write(Common::String str);
+	void write(char ch);
+	void writeLine(Common::String str);
+	void writeLine();
+
+	void indent();
+	void unindent();
+
+	Common::String str() const;
+	size_t lineWidth() const { return _lineWidth; }
+	size_t size() const { return _size; }
+
+protected:
+	void writeIndentation();
+};
+
+} // namespace Common
+
+#endif // LINGODEC_CODEWRITER_H
diff --git a/engines/director/lingo/lingodec/context.cpp b/engines/director/lingo/lingodec/context.cpp
new file mode 100644
index 00000000000..e2307bbb6a0
--- /dev/null
+++ b/engines/director/lingo/lingodec/context.cpp
@@ -0,0 +1,84 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#include "common/stream.h"
+#include "common/util.h"
+#include "./context.h"
+#include "./names.h"
+#include "./resolver.h"
+#include "./script.h"
+
+namespace LingoDec {
+
+struct ScriptContextMapEntry;
+
+/* ScriptContext */
+
+void ScriptContext::read(Common::SeekableReadStream &stream) {
+	// Lingo scripts are always big endian regardless of file endianness
+	unknown0 = stream.readSint32BE();
+	unknown1 = stream.readSint32BE();
+	entryCount = stream.readUint32BE();
+	entryCount2 = stream.readUint32BE();
+	entriesOffset = stream.readUint16BE();
+	unknown2 = stream.readSint16BE();
+	unknown3 = stream.readSint32BE();
+	unknown4 = stream.readSint32BE();
+	unknown5 = stream.readSint32BE();
+	lnamSectionID = stream.readSint32BE();
+	validCount = stream.readUint16BE();
+	flags = stream.readUint16BE();
+	freePointer = stream.readSint16BE();
+
+	stream.seek(entriesOffset);
+	sectionMap.resize(entryCount);
+	for (auto &entry : sectionMap) {
+		entry.read(stream);
+	}
+
+	lnam = resolver->getScriptNames(lnamSectionID);
+	for (uint32_t i = 1; i <= entryCount; i++) {
+		auto section = sectionMap[i - 1];
+		if (section.sectionID > -1) {
+			Script *script = resolver->getScript(section.sectionID);
+			script->setContext(this);
+			scripts[i] = script;
+		}
+	}
+
+	for (auto it = scripts.begin(); it != scripts.end(); ++it) {
+		Script *script = it->second;
+		if (script->isFactory()) {
+			Script *parent = scripts[script->parentNumber + 1];
+			parent->factories.push_back(script);
+		}
+	}
+}
+
+bool ScriptContext::validName(int id) const {
+	return lnam->validName(id);
+}
+
+Common::String ScriptContext::getName(int id) const {
+	return lnam->getName(id);
+}
+
+void ScriptContext::parseScripts() {
+	for (auto it = scripts.begin(); it != scripts.end(); ++it) {
+		it->second->parse();
+	}
+}
+
+/* ScriptContextMapEntry */
+
+void ScriptContextMapEntry::read(Common::SeekableReadStream &stream) {
+	unknown0 = stream.readSint32BE();
+	sectionID = stream.readSint32BE();
+	unknown1 = stream.readUint16BE();
+	unknown2 = stream.readUint16BE();
+}
+
+} // namespace LingoDec
diff --git a/engines/director/lingo/lingodec/context.h b/engines/director/lingo/lingodec/context.h
new file mode 100644
index 00000000000..a8846eb07b3
--- /dev/null
+++ b/engines/director/lingo/lingodec/context.h
@@ -0,0 +1,70 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef LINGODEC_CONTEXT_H
+#define LINGODEC_CONTEXT_H
+
+#include "common/array.h"
+#include "common/stablemap.h"
+
+namespace Common {
+class SeekableReadStream;
+}
+
+namespace LingoDec {
+
+class ChunkResolver;
+struct Script;
+struct ScriptContextMapEntry;
+struct ScriptNames;
+
+/* ScriptContext */
+
+struct ScriptContext {
+	int32_t unknown0;
+	int32_t unknown1;
+	uint32_t entryCount;
+	uint32_t entryCount2;
+	uint16_t entriesOffset;
+	int16_t unknown2;
+	int32_t unknown3;
+	int32_t unknown4;
+	int32_t unknown5;
+	int32_t lnamSectionID;
+	uint16_t validCount;
+	uint16_t flags;
+	int16_t freePointer;
+
+	unsigned int version;
+	ChunkResolver *resolver;
+	ScriptNames *lnam;
+	Common::Array<ScriptContextMapEntry> sectionMap;
+	Common::StableMap<uint32_t, Script *> scripts;
+
+	ScriptContext(unsigned int version, ChunkResolver *resolver) : version(version),
+																   resolver(resolver),
+																   lnam(nullptr) {}
+
+	void read(Common::SeekableReadStream &stream);
+	bool validName(int id) const;
+	Common::String getName(int id) const;
+	void parseScripts();
+};
+
+/* ScriptContextMapEntry */
+
+struct ScriptContextMapEntry {
+	int32_t unknown0;
+	int32_t sectionID;
+	uint16_t unknown1;
+	uint16_t unknown2;
+
+	void read(Common::SeekableReadStream &stream);
+};
+
+} // namespace LingoDec
+
+#endif // LINGODEC_CONTEXT_H
diff --git a/engines/director/lingo/lingodec/enums.h b/engines/director/lingo/lingodec/enums.h
new file mode 100644
index 00000000000..3dd65fae1e4
--- /dev/null
+++ b/engines/director/lingo/lingodec/enums.h
@@ -0,0 +1,216 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef LINGODEC_ENUMS_H
+#define LINGODEC_ENUMS_H
+
+namespace LingoDec {
+
+enum OpCode {
+	// single-byte
+	kOpRet				= 0x01,
+	kOpRetFactory		= 0x02,
+	kOpPushZero			= 0x03,
+	kOpMul				= 0x04,
+	kOpAdd				= 0x05,
+	kOpSub				= 0x06,
+	kOpDiv				= 0x07,
+	kOpMod				= 0x08,
+	kOpInv				= 0x09,
+	kOpJoinStr			= 0x0a,
+	kOpJoinPadStr		= 0x0b,
+	kOpLt				= 0x0c,
+	kOpLtEq				= 0x0d,
+	kOpNtEq				= 0x0e,
+	kOpEq				= 0x0f,
+	kOpGt				= 0x10,
+	kOpGtEq				= 0x11,
+	kOpAnd				= 0x12,
+	kOpOr				= 0x13,
+	kOpNot				= 0x14,
+	kOpContainsStr		= 0x15,
+	kOpContains0Str		= 0x16,
+	kOpGetChunk			= 0x17,
+	kOpHiliteChunk		= 0x18,
+	kOpOntoSpr			= 0x19,
+	kOpIntoSpr			= 0x1a,
+	kOpGetField			= 0x1b,
+	kOpStartTell		= 0x1c,
+	kOpEndTell			= 0x1d,
+	kOpPushList			= 0x1e,
+	kOpPushPropList		= 0x1f,
+	kOpSwap				= 0x21,
+
+	// multi-byte
+	kOpPushInt8			= 0x41,
+	kOpPushArgListNoRet	= 0x42,
+	kOpPushArgList		= 0x43,
+	kOpPushCons			= 0x44,
+	kOpPushSymb			= 0x45,
+	kOpPushVarRef		= 0x46,
+	kOpGetGlobal2		= 0x48,
+	kOpGetGlobal		= 0x49,
+	kOpGetProp			= 0x4a,
+	kOpGetParam			= 0x4b,
+	kOpGetLocal			= 0x4c,
+	kOpSetGlobal2		= 0x4e,
+	kOpSetGlobal		= 0x4f,
+	kOpSetProp			= 0x50,
+	kOpSetParam			= 0x51,
+	kOpSetLocal			= 0x52,
+	kOpJmp				= 0x53,
+	kOpEndRepeat		= 0x54,
+	kOpJmpIfZ			= 0x55,
+	kOpLocalCall		= 0x56,
+	kOpExtCall			= 0x57,
+	kOpObjCallV4		= 0x58,
+	kOpPut				= 0x59,
+	kOpPutChunk			= 0x5a,
+	kOpDeleteChunk		= 0x5b,
+	kOpGet				= 0x5c,
+	kOpSet				= 0x5d,
+	kOpGetMovieProp		= 0x5f,
+	kOpSetMovieProp		= 0x60,
+	kOpGetObjProp		= 0x61,
+	kOpSetObjProp		= 0x62,
+	kOpTellCall			= 0x63,
+	kOpPeek				= 0x64,
+	kOpPop				= 0x65,
+	kOpTheBuiltin		= 0x66,
+	kOpObjCall			= 0x67,
+	kOpPushChunkVarRef	= 0x6d,
+	kOpPushInt16		= 0x6e,
+	kOpPushInt32		= 0x6f,
+	kOpGetChainedProp	= 0x70,
+	kOpPushFloat32		= 0x71,
+	kOpGetTopLevelProp	= 0x72,
+	kOpNewObj			= 0x73
+};
+
+enum DatumType {
+	kDatumVoid,
+	kDatumSymbol,
+	kDatumVarRef,
+	kDatumString,
+	kDatumInt,
+	kDatumFloat,
+	kDatumList,
+	kDatumArgList,
+	kDatumArgListNoRet,
+	kDatumPropList
+};
+
+enum ChunkExprType {
+	kChunkChar	= 0x01,
+	kChunkWord	= 0x02,
+	kChunkItem	= 0x03,
+	kChunkLine	= 0x04
+};
+
+enum PutType {
+	kPutInto	= 0x01,
+	kPutAfter	= 0x02,
+	kPutBefore	= 0x03
+};
+
+enum NodeType {
+	kNoneNode,
+	kErrorNode,
+	kTempNode,
+	kCommentNode,
+	kLiteralNode,
+	kBlockNode,
+	kHandlerNode,
+	kExitStmtNode,
+	kInverseOpNode,
+	kNotOpNode,
+	kBinaryOpNode,
+	kChunkExprNode,
+	kChunkHiliteStmtNode,
+	kChunkDeleteStmtNode,
+	kSpriteIntersectsExprNode,
+	kSpriteWithinExprNode,
+	kMemberExprNode,
+	kVarNode,
+	kAssignmentStmtNode,
+	kIfStmtNode,
+	kRepeatWhileStmtNode,
+	kRepeatWithInStmtNode,
+	kRepeatWithToStmtNode,
+	kCaseStmtNode,
+	kCaseLabelNode,
+	kOtherwiseNode,
+	kEndCaseNode,
+	kTellStmtNode,
+	kSoundCmdStmtNode,
+	kPlayCmdStmtNode,
+	kCallNode,
+	kObjCallNode,
+	kObjCallV4Node,
+	kTheExprNode,
+	kLastStringChunkExprNode,
+	kStringChunkCountExprNode,
+	kMenuPropExprNode,
+	kMenuItemPropExprNode,
+	kSoundPropExprNode,
+	kSpritePropExprNode,
+	kThePropExprNode,
+	kObjPropExprNode,
+	kObjBracketExprNode,
+	kObjPropIndexExprNode,
+	kExitRepeatStmtNode,
+	kNextRepeatStmtNode,
+	kPutStmtNode,
+	kWhenStmtNode,
+	kNewObjNode
+};
+
+enum BytecodeTag {
+	kTagNone,
+	kTagSkip,
+	kTagRepeatWhile,
+	kTagRepeatWithIn,
+	kTagRepeatWithTo,
+	kTagRepeatWithDownTo,
+	kTagNextRepeatTarget,
+	kTagEndCase
+};
+
+enum CaseExpect {
+	kCaseExpectEnd,
+	kCaseExpectOr,
+	kCaseExpectNext,
+	kCaseExpectOtherwise
+};
+
+enum ScriptFlag {
+	kScriptFlagUnused		= (1 << 0x0),
+	kScriptFlagFuncsGlobal	= (1 << 0x1),
+	kScriptFlagVarsGlobal	= (1 << 0x2),	// Occurs in event scripts (which have no local vars). Correlated with use of alternate global var opcodes.
+	kScriptFlagUnk3			= (1 << 0x3),
+	kScriptFlagFactoryDef	= (1 << 0x4),
+	kScriptFlagUnk5			= (1 << 0x5),
+	kScriptFlagUnk6			= (1 << 0x6),
+	kScriptFlagUnk7			= (1 << 0x7),
+	kScriptFlagHasFactory	= (1 << 0x8),
+	kScriptFlagEventScript	= (1 << 0x9),
+	kScriptFlagEventScript2	= (1 << 0xa),
+	kScriptFlagUnkB			= (1 << 0xb),
+	kScriptFlagUnkC			= (1 << 0xc),
+	kScriptFlagUnkD			= (1 << 0xd),
+	kScriptFlagUnkE			= (1 << 0xe),
+	kScriptFlagUnkF			= (1 << 0xf)
+};
+
+enum LiteralType {
+	kLiteralString	= 1,
+	kLiteralInt		= 4,
+	kLiteralFloat	= 9
+};
+
+}
+
+#endif // LINGODEC_ENUMS_H
diff --git a/engines/director/lingo/lingodec/handler.cpp b/engines/director/lingo/lingodec/handler.cpp
new file mode 100644
index 00000000000..b4bfc1b85c3
--- /dev/null
+++ b/engines/director/lingo/lingodec/handler.cpp
@@ -0,0 +1,1305 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#include "common/stream.h"
+#include "common/util.h"
+#include "./ast.h"
+#include "./codewriter.h"
+#include "./handler.h"
+#include "./names.h"
+#include "./script.h"
+
+namespace LingoDec {
+
+/* Handler */
+
+void Handler::readRecord(Common::SeekableReadStream &stream) {
+	nameID = stream.readSint16BE();
+	vectorPos = stream.readUint16BE();
+	compiledLen = stream.readUint32BE();
+	compiledOffset = stream.readUint32BE();
+	argumentCount = stream.readUint16BE();
+	argumentOffset = stream.readUint32BE();
+	localsCount = stream.readUint16BE();
+	localsOffset = stream.readUint32BE();
+	globalsCount = stream.readUint16BE();
+	globalsOffset = stream.readUint32BE();
+	unknown1 = stream.readUint32BE();
+	unknown2 = stream.readUint16BE();
+	lineCount = stream.readUint16BE();
+	lineOffset = stream.readUint32BE();
+	// yet to implement
+	if (script->version >= 850)
+		stackHeight = stream.readUint32BE();
+}
+
+void Handler::readData(Common::SeekableReadStream &stream) {
+	stream.seek(compiledOffset);
+	while (stream.pos() < compiledOffset + compiledLen) {
+		uint32_t pos = stream.pos() - compiledOffset;
+		uint8_t op = stream.readByte();
+		OpCode opcode = static_cast<OpCode>(op >= 0x40 ? 0x40 + op % 0x40 : op);
+		// argument can be one, two or four bytes
+		int32_t obj = 0;
+		if (op >= 0xc0) {
+			// four bytes
+			obj = stream.readSint32BE();
+		} else if (op >= 0x80) {
+			// two bytes
+			if (opcode == kOpPushInt16 || opcode == kOpPushInt8) {
+				// treat pushint's arg as signed
+				// pushint8 may be used to push a 16-bit int in older Lingo
+				obj = stream.readSint16BE();
+			} else {
+				obj = stream.readUint16BE();
+			}
+		} else if (op >= 0x40) {
+			// one byte
+			if (opcode == kOpPushInt8) {
+				// treat pushint's arg as signed
+				obj = stream.readSByte();
+			} else {
+				obj = stream.readByte();
+			}
+		}
+		Bytecode bytecode(op, obj, pos);
+		bytecodeArray.push_back(bytecode);
+		bytecodePosMap[pos] = bytecodeArray.size() - 1;
+	}
+
+	argumentNameIDs = readVarnamesTable(stream, argumentCount, argumentOffset);
+	localNameIDs = readVarnamesTable(stream, localsCount, localsOffset);
+	globalNameIDs = readVarnamesTable(stream, globalsCount, globalsOffset);
+}
+
+Common::Array<int16_t> Handler::readVarnamesTable(Common::SeekableReadStream &stream, uint16_t count, uint32_t offset) {
+	stream.seek(offset);
+	Common::Array<int16_t> nameIDs;
+	nameIDs.resize(count);
+	for (size_t i = 0; i < count; i++) {
+		nameIDs[i] = stream.readUint16BE();
+	}
+	return nameIDs;
+}
+
+void Handler::readNames() {
+	if (!isGenericEvent) {
+		name = getName(nameID);
+	}
+	for (size_t i = 0; i < argumentNameIDs.size(); i++) {
+		if (i == 0 && script->isFactory())
+			continue;
+		argumentNames.push_back(getName(argumentNameIDs[i]));
+	}
+	for (auto nameID_ : localNameIDs) {
+		if (validName(nameID_)) {
+			localNames.push_back(getName(nameID_));
+		}
+	}
+	for (auto nameID_ : globalNameIDs) {
+		if (validName(nameID_)) {
+			globalNames.push_back(getName(nameID_));
+		}
+	}
+}
+
+bool Handler::validName(int id) const {
+	return script->validName(id);
+}
+
+Common::String Handler::getName(int id) const {
+	return script->getName(id);
+}
+
+Common::String Handler::getArgumentName(int id) const {
+	if (-1 < id && (unsigned)id < argumentNameIDs.size())
+		return getName(argumentNameIDs[id]);
+	return Common::String::format("UNKNOWN_ARG_%d", id);
+}
+
+Common::String Handler::getLocalName(int id) const {
+	if (-1 < id && (unsigned)id < localNameIDs.size())
+		return getName(localNameIDs[id]);
+	return Common::String::format("UNKNOWN_LOCAL_%d", id);
+}
+
+Common::SharedPtr<Node> Handler::pop() {
+	if (stack.empty())
+		return Common::SharedPtr<Node>(new ErrorNode());
+
+	auto res = stack.back();
+	stack.pop_back();
+	return res;
+}
+
+int Handler::variableMultiplier() {
+	if (script->version >= 850)
+		return 1;
+	if (script->version >= 500)
+		return 8;
+	return 6;
+}
+
+Common::SharedPtr<Node> Handler::readVar(int varType) {
+	Common::SharedPtr<Node> castID;
+	if (varType == 0x6 && script->version >= 500) // field cast ID
+		castID = pop();
+	Common::SharedPtr<Node> id = pop();
+
+	switch (varType) {
+	case 0x1: // global
+	case 0x2: // global
+	case 0x3: // property/instance
+		return id;
+	case 0x4: // arg
+		{
+			Common::String name_ = getArgumentName(id->getValue()->i / variableMultiplier());
+			auto ref = Common::SharedPtr<Datum>(new Datum(kDatumVarRef, name_));
+			return Common::SharedPtr<Node>(new LiteralNode(Common::move(ref)));
+		}
+	case 0x5: // local
+		{
+			Common::String name_ = getLocalName(id->getValue()->i / variableMultiplier());
+			auto ref = Common::SharedPtr<Datum>(new Datum(kDatumVarRef, name_));
+			return Common::SharedPtr<Node>(new LiteralNode(Common::move(ref)));
+		}
+	case 0x6: // field
+		return Common::SharedPtr<Node>(new MemberExprNode("field", Common::move(id), Common::move(castID)));
+	default:
+		warning(Common::String::format("findVar: unhandled var type %d", varType).c_str());
+		break;
+	}
+	return Common::SharedPtr<Node>(new ErrorNode());
+}
+
+Common::String Handler::getVarNameFromSet(const Bytecode &bytecode) {
+	Common::String varName;
+	switch (bytecode.opcode) {
+	case kOpSetGlobal:
+	case kOpSetGlobal2:
+		varName = getName(bytecode.obj);
+		break;
+	case kOpSetProp:
+		varName = getName(bytecode.obj);
+		break;
+	case kOpSetParam:
+		varName = getArgumentName(bytecode.obj / variableMultiplier());
+		break;
+	case kOpSetLocal:
+		varName = getLocalName(bytecode.obj / variableMultiplier());
+		break;
+	default:
+		varName = "ERROR";
+		break;
+	}
+	return varName;
+}
+
+Common::SharedPtr<Node> Handler::readV4Property(int propertyType, int propertyID) {
+	switch (propertyType) {
+	case 0x00:
+		{
+			if (propertyID <= 0x0b) { // movie property
+				auto propName = StandardNames::getName(StandardNames::moviePropertyNames, propertyID);
+				return Common::SharedPtr<Node>(new TheExprNode(propName));
+			} else { // last chunk
+				auto string = pop();
+				auto chunkType = static_cast<ChunkExprType>(propertyID - 0x0b);
+				return Common::SharedPtr<Node>(new LastStringChunkExprNode(chunkType, Common::move(string)));
+			}
+		}
+		break;
+	case 0x01: // number of chunks
+		{
+			auto string = pop();
+			return Common::SharedPtr<Node>(new StringChunkCountExprNode(static_cast<ChunkExprType>(propertyID), Common::move(string)));
+		}
+		break;
+	case 0x02: // menu property
+		{
+			auto menuID = pop();
+			return Common::SharedPtr<Node>(new MenuPropExprNode(Common::move(menuID), propertyID));
+		}
+		break;
+	case 0x03: // menu item property
+		{
+			auto menuID = pop();
+			auto itemID = pop();
+			return Common::SharedPtr<Node>(new MenuItemPropExprNode(Common::move(menuID), Common::move(itemID), propertyID));
+		}
+		break;
+	case 0x04: // sound property
+		{
+			auto soundID = pop();
+			return Common::SharedPtr<Node>(new SoundPropExprNode(Common::move(soundID), propertyID));
+		}
+		break;
+	case 0x05: // resource property - unused?
+		return Common::SharedPtr<Node>(new CommentNode("ERROR: Resource property"));
+	case 0x06: // sprite property
+		{
+			auto spriteID = pop();
+			return Common::SharedPtr<Node>(new SpritePropExprNode(Common::move(spriteID), propertyID));
+		}
+		break;
+	case 0x07: // animation property
+		return Common::SharedPtr<Node>(new TheExprNode(StandardNames::getName(StandardNames::animationPropertyNames, propertyID)));
+	case 0x08: // animation 2 property
+		if (propertyID == 0x02 && script->version >= 500) { // the number of castMembers supports castLib selection from Director 5.0
+			auto castLib = pop();
+			if (!(castLib->type == kLiteralNode && castLib->getValue()->type == kDatumInt && castLib->getValue()->toInt() == 0)) {
+				auto castLibNode = Common::SharedPtr<Node>(new MemberExprNode("castLib", castLib, nullptr));
+				return Common::SharedPtr<Node>(new ThePropExprNode(castLibNode, StandardNames::getName(StandardNames::animation2PropertyNames, propertyID)));
+			}
+		}
+		return Common::SharedPtr<Node>(new TheExprNode(StandardNames::getName(StandardNames::animation2PropertyNames, propertyID)));
+	case 0x09: // generic cast member
+	case 0x0a: // chunk of cast member
+	case 0x0b: // field
+	case 0x0c: // chunk of field
+	case 0x0d: // digital video
+	case 0x0e: // bitmap
+	case 0x0f: // sound
+	case 0x10: // button
+	case 0x11: // shape
+	case 0x12: // movie
+	case 0x13: // script
+	case 0x14: // scriptText
+	case 0x15: // chunk of scriptText
+		{
+			auto propName = StandardNames::getName(StandardNames::memberPropertyNames, propertyID);
+			Common::SharedPtr<Node> castID;
+			if (script->version >= 500) {
+				castID = pop();
+			}
+			auto memberID = pop();
+			Common::String prefix;
+			if (propertyType == 0x0b || propertyType == 0x0c) {
+				prefix = "field";
+			} else if (propertyType == 0x14 || propertyType == 0x15) {
+				prefix = "script";
+			} else {
+				prefix = (script->version >= 500) ? "member" : "cast";
+			}
+			auto member = Common::SharedPtr<Node>(new MemberExprNode(prefix, Common::move(memberID), Common::move(castID)));
+			Common::SharedPtr<Node> entity;
+			if (propertyType == 0x0a || propertyType == 0x0c || propertyType == 0x15) {
+				entity = readChunkRef(Common::move(member));
+			} else {
+				entity = member;
+			}
+			return Common::SharedPtr<Node>(new ThePropExprNode(Common::move(entity), propName));
+		}
+		break;
+	default:
+		break;
+	}
+	return Common::SharedPtr<Node>(new CommentNode(Common::String::format("ERROR: Unknown property type %d", propertyType)));
+}
+
+Common::SharedPtr<Node> Handler::readChunkRef(Common::SharedPtr<Node> string) {
+	auto lastLine = pop();
+	auto firstLine = pop();
+	auto lastItem = pop();
+	auto firstItem = pop();
+	auto lastWord = pop();
+	auto firstWord = pop();
+	auto lastChar = pop();
+	auto firstChar = pop();
+
+	if (!(firstLine->type == kLiteralNode && firstLine->getValue()->type == kDatumInt && firstLine->getValue()->toInt() == 0))
+		string = Common::SharedPtr<Node>(new ChunkExprNode(kChunkLine, Common::move(firstLine), Common::move(lastLine), Common::move(string)));
+	if (!(firstItem->type == kLiteralNode && firstItem->getValue()->type == kDatumInt && firstItem->getValue()->toInt() == 0))
+		string = Common::SharedPtr<Node>(new ChunkExprNode(kChunkItem, Common::move(firstItem), Common::move(lastItem), Common::move(string)));
+	if (!(firstWord->type == kLiteralNode && firstWord->getValue()->type == kDatumInt && firstWord->getValue()->toInt() == 0))
+		string = Common::SharedPtr<Node>(new ChunkExprNode(kChunkWord, Common::move(firstWord), Common::move(lastWord), Common::move(string)));
+	if (!(firstChar->type == kLiteralNode && firstChar->getValue()->type == kDatumInt && firstChar->getValue()->toInt() == 0))
+		string = Common::SharedPtr<Node>(new ChunkExprNode(kChunkChar, Common::move(firstChar), Common::move(lastChar), Common::move(string)));
+
+	return string;
+}
+
+void Handler::tagLoops() {
+	// Tag any jmpifz which is a loop with the loop type
+	// (kTagRepeatWhile, kTagRepeatWithIn, kTagRepeatWithTo, kTagRepeatWithDownTo).
+	// Tag the instruction which `next repeat` jumps to with kTagNextRepeatTarget.
+	// Tag any instructions which are internal loop logic with kTagSkip, so that
+	// they will be skipped during translation.
+
+	for (uint32_t startIndex = 0; startIndex < bytecodeArray.size(); startIndex++) {
+		// All loops begin with jmpifz...
+		auto &jmpifz = bytecodeArray[startIndex];
+		if (jmpifz.opcode != kOpJmpIfZ)
+			continue;
+
+		// ...and end with endrepeat.
+		uint32_t jmpPos = jmpifz.pos + jmpifz.obj;
+		uint32_t endIndex = bytecodePosMap[jmpPos];
+		auto &endRepeat = bytecodeArray[endIndex - 1];
+		if (endRepeat.opcode != kOpEndRepeat || (endRepeat.pos - endRepeat.obj) > jmpifz.pos)
+			continue;
+
+		BytecodeTag loopType = identifyLoop(startIndex, endIndex);
+		bytecodeArray[startIndex].tag = loopType;
+
+		if (loopType == kTagRepeatWithIn) {
+			for (uint32_t i = startIndex - 7, end = startIndex - 1; i <= end; i++)
+				bytecodeArray[i].tag = kTagSkip;
+			for (uint32_t i = startIndex + 1, end = startIndex + 5; i <= end; i++)
+				bytecodeArray[i].tag = kTagSkip;
+			bytecodeArray[endIndex - 3].tag = kTagNextRepeatTarget; // pushint8 1
+			bytecodeArray[endIndex - 3].ownerLoop = startIndex;
+			bytecodeArray[endIndex - 2].tag = kTagSkip; // add
+			bytecodeArray[endIndex - 1].tag = kTagSkip; // endrepeat
+			bytecodeArray[endIndex - 1].ownerLoop = startIndex;
+			bytecodeArray[endIndex].tag = kTagSkip; // pop 3
+		} else if (loopType == kTagRepeatWithTo || loopType == kTagRepeatWithDownTo) {
+			uint32_t conditionStartIndex = bytecodePosMap[endRepeat.pos - endRepeat.obj];
+			bytecodeArray[conditionStartIndex - 1].tag = kTagSkip; // set
+			bytecodeArray[conditionStartIndex].tag = kTagSkip; // get
+			bytecodeArray[startIndex - 1].tag = kTagSkip; // lteq / gteq
+			bytecodeArray[endIndex - 5].tag = kTagNextRepeatTarget; // pushint8 1 / pushint8 -1
+			bytecodeArray[endIndex - 5].ownerLoop = startIndex;
+			bytecodeArray[endIndex - 4].tag = kTagSkip; // get
+			bytecodeArray[endIndex - 3].tag = kTagSkip; // add
+			bytecodeArray[endIndex - 2].tag = kTagSkip; // set
+			bytecodeArray[endIndex - 1].tag = kTagSkip; // endrepeat
+			bytecodeArray[endIndex - 1].ownerLoop = startIndex;
+		} else if (loopType == kTagRepeatWhile) {
+			bytecodeArray[endIndex - 1].tag = kTagNextRepeatTarget; // endrepeat
+			bytecodeArray[endIndex - 1].ownerLoop = startIndex;
+		}
+	}
+}
+
+bool Handler::isRepeatWithIn(uint32_t startIndex, uint32_t endIndex) {
+	if (startIndex < 7 || startIndex > bytecodeArray.size() - 6)
+		return false;
+	if (!(bytecodeArray[startIndex - 7].opcode == kOpPeek && bytecodeArray[startIndex - 7].obj == 0))
+		return false;
+	if (!(bytecodeArray[startIndex - 6].opcode == kOpPushArgList && bytecodeArray[startIndex - 6].obj == 1))
+		return false;
+	if (!(bytecodeArray[startIndex - 5].opcode == kOpExtCall && getName(bytecodeArray[startIndex - 5].obj) == "count"))
+		return false;
+	if (!(bytecodeArray[startIndex - 4].opcode == kOpPushInt8 && bytecodeArray[startIndex - 4].obj == 1))
+		return false;
+	if (!(bytecodeArray[startIndex - 3].opcode == kOpPeek && bytecodeArray[startIndex - 3].obj == 0))
+		return false;
+	if (!(bytecodeArray[startIndex - 2].opcode == kOpPeek && bytecodeArray[startIndex - 2].obj == 2))
+		return false;
+	if (!(bytecodeArray[startIndex - 1].opcode == kOpLtEq))
+		return false;
+	// if (!(bytecodeArray[startIndex].opcode == kOpJmpIfZ))
+	//     return false;
+	if (!(bytecodeArray[startIndex + 1].opcode == kOpPeek && bytecodeArray[startIndex + 1].obj == 2))
+		return false;
+	if (!(bytecodeArray[startIndex + 2].opcode == kOpPeek && bytecodeArray[startIndex + 2].obj == 1))
+		return false;
+	if (!(bytecodeArray[startIndex + 3].opcode == kOpPushArgList && bytecodeArray[startIndex + 3].obj == 2))
+		return false;
+	if (!(bytecodeArray[startIndex + 4].opcode == kOpExtCall && getName(bytecodeArray[startIndex + 4].obj) == "getAt"))
+		return false;
+	if (!(bytecodeArray[startIndex + 5].opcode == kOpSetGlobal || bytecodeArray[startIndex + 5].opcode == kOpSetProp
+			|| bytecodeArray[startIndex + 5].opcode == kOpSetParam || bytecodeArray[startIndex + 5].opcode == kOpSetLocal))
+		return false;
+
+	if (endIndex < 3)
+		return false;
+	if (!(bytecodeArray[endIndex - 3].opcode == kOpPushInt8 && bytecodeArray[endIndex - 3].obj == 1))
+		return false;
+	if (!(bytecodeArray[endIndex - 2].opcode == kOpAdd))
+		return false;
+	// if (!(bytecodeArray[startIndex - 1].opcode == kOpEndRepeat))
+	//     return false;
+	if (!(bytecodeArray[endIndex].opcode == kOpPop && bytecodeArray[endIndex].obj == 3))
+		return false;
+
+	return true;
+}
+
+BytecodeTag Handler::identifyLoop(uint32_t startIndex, uint32_t endIndex) {
+	if (isRepeatWithIn(startIndex, endIndex))
+		return kTagRepeatWithIn;
+
+	if (startIndex < 1)
+		return kTagRepeatWhile;
+
+	bool up;
+	switch (bytecodeArray[startIndex - 1].opcode) {
+	case kOpLtEq:
+		up = true;
+		break;
+	case kOpGtEq:
+		up = false;
+		break;
+	default:
+		return kTagRepeatWhile;
+	}
+
+	auto &endRepeat = bytecodeArray[endIndex - 1];
+	uint32_t conditionStartIndex = bytecodePosMap[endRepeat.pos - endRepeat.obj];
+
+	if (conditionStartIndex < 1)
+		return kTagRepeatWhile;
+
+	OpCode getOp;
+	switch (bytecodeArray[conditionStartIndex - 1].opcode) {
+	case kOpSetGlobal:
+		getOp = kOpGetGlobal;
+		break;
+	case kOpSetGlobal2:
+		getOp = kOpGetGlobal2;
+		break;
+	case kOpSetProp:
+		getOp = kOpGetProp;
+		break;
+	case kOpSetParam:
+		getOp = kOpGetParam;
+		break;
+	case kOpSetLocal:
+		getOp = kOpGetLocal;
+		break;
+	default:
+		return kTagRepeatWhile;
+	}
+	OpCode setOp = bytecodeArray[conditionStartIndex - 1].opcode;
+	int32_t varID = bytecodeArray[conditionStartIndex - 1].obj;
+
+	if (!(bytecodeArray[conditionStartIndex].opcode == getOp && bytecodeArray[conditionStartIndex].obj == varID))
+		return kTagRepeatWhile;
+
+	if (endIndex < 5)
+		return kTagRepeatWhile;
+	if (up) {
+		if (!(bytecodeArray[endIndex - 5].opcode == kOpPushInt8 && bytecodeArray[endIndex - 5].obj == 1))
+			return kTagRepeatWhile;
+	} else {
+		if (!(bytecodeArray[endIndex - 5].opcode == kOpPushInt8 && bytecodeArray[endIndex - 5].obj == -1))
+			return kTagRepeatWhile;
+	}
+	if (!(bytecodeArray[endIndex - 4].opcode == getOp && bytecodeArray[endIndex - 4].obj == varID))
+		return kTagRepeatWhile;
+	if (!(bytecodeArray[endIndex - 3].opcode == kOpAdd))
+		return kTagRepeatWhile;
+	if (!(bytecodeArray[endIndex - 2].opcode == setOp && bytecodeArray[endIndex - 2].obj == varID))
+		return kTagRepeatWhile;
+
+	return up ? kTagRepeatWithTo : kTagRepeatWithDownTo;
+}
+
+void Handler::parse() {
+	tagLoops();
+	stack.clear();
+	uint32_t i = 0;
+	while (i < bytecodeArray.size()) {
+		auto &bytecode = bytecodeArray[i];
+		uint32_t pos = bytecode.pos;
+		// exit last block if at end
+		while (pos == ast.currentBlock->endPos) {
+			auto exitedBlock = ast.currentBlock;
+			auto ancestorStmt = ast.currentBlock->ancestorStatement();
+			ast.exitBlock();
+			if (ancestorStmt) {
+				if (ancestorStmt->type == kIfStmtNode) {
+					auto ifStatement = static_cast<IfStmtNode *>(ancestorStmt);
+					if (ifStatement->hasElse && exitedBlock == ifStatement->block1.get()) {
+						ast.enterBlock(ifStatement->block2.get());
+					}
+				} else if (ancestorStmt->type == kCaseStmtNode) {
+					auto caseStmt = static_cast<CaseStmtNode *>(ancestorStmt);
+					auto caseLabel = ast.currentBlock->currentCaseLabel;
+					if (caseLabel) {
+						if (caseLabel->expect == kCaseExpectOtherwise) {
+							ast.currentBlock->currentCaseLabel = nullptr;
+							caseStmt->addOtherwise();
+							size_t otherwiseIndex = bytecodePosMap[caseStmt->potentialOtherwisePos];
+							bytecodeArray[otherwiseIndex].translation = Common::SharedPtr<Node>(caseStmt->otherwise);
+							ast.enterBlock(caseStmt->otherwise->block.get());
+						} else if (caseLabel->expect == kCaseExpectEnd) {
+							ast.currentBlock->currentCaseLabel = nullptr;
+						}
+					}
+				}
+			}
+		}
+		auto translateSize = translateBytecode(bytecode, i);
+		i += translateSize;
+	}
+}
+
+uint32_t Handler::translateBytecode(Bytecode &bytecode, uint32_t index) {
+	if (bytecode.tag == kTagSkip || bytecode.tag == kTagNextRepeatTarget) {
+		// This is internal loop logic. Skip it.
+		return 1;
+	}
+
+	Common::SharedPtr<Node> translation = nullptr;
+	BlockNode *nextBlock = nullptr;
+
+	switch (bytecode.opcode) {
+	case kOpRet:
+	case kOpRetFactory:
+		if (index == bytecodeArray.size() - 1) {
+			return 1; // end of handler
+		}
+		translation = Common::SharedPtr<Node>(new ExitStmtNode());
+		break;
+	case kOpPushZero:
+		translation = Common::SharedPtr<Node>(new LiteralNode(Common::SharedPtr<Datum>(new Datum(0))));
+		break;
+	case kOpMul:
+	case kOpAdd:
+	case kOpSub:
+	case kOpDiv:
+	case kOpMod:
+	case kOpJoinStr:
+	case kOpJoinPadStr:
+	case kOpLt:
+	case kOpLtEq:
+	case kOpNtEq:
+	case kOpEq:
+	case kOpGt:
+	case kOpGtEq:
+	case kOpAnd:
+	case kOpOr:
+	case kOpContainsStr:
+	case kOpContains0Str:
+		{
+			auto b = pop();
+			auto a = pop();
+			translation = Common::SharedPtr<Node>(new BinaryOpNode(bytecode.opcode, Common::move(a), Common::move(b)));
+		}
+		break;
+	case kOpInv:
+		{
+			auto x = pop();
+			translation = Common::SharedPtr<Node>(new InverseOpNode(Common::move(x)));
+		}
+		break;
+	case kOpNot:
+		{
+			auto x = pop();
+			translation = Common::SharedPtr<Node>(new NotOpNode(Common::move(x)));
+		}
+		break;
+	case kOpGetChunk:
+		{
+			auto string = pop();
+			translation = readChunkRef(Common::move(string));
+		}
+		break;
+	case kOpHiliteChunk:
+		{
+			Common::SharedPtr<Node> castID;
+			if (script->version >= 500)
+				castID = pop();
+			auto fieldID = pop();
+			auto field = Common::SharedPtr<Node>(new MemberExprNode("field", Common::move(fieldID), Common::move(castID)));
+			auto chunk = readChunkRef(Common::move(field));
+			if (chunk->type == kCommentNode) { // error comment
+				translation = chunk;
+			} else {
+				translation = Common::SharedPtr<Node>(new ChunkHiliteStmtNode(Common::move(chunk)));
+			}
+		}
+		break;
+	case kOpOntoSpr:
+		{
+			auto secondSprite = pop();
+			auto firstSprite = pop();
+			translation = Common::SharedPtr<Node>(new SpriteIntersectsExprNode(Common::move(firstSprite), Common::move(secondSprite)));
+		}
+		break;
+	case kOpIntoSpr:
+		{
+			auto secondSprite = pop();
+			auto firstSprite = pop();
+			translation = Common::SharedPtr<Node>(new SpriteWithinExprNode(Common::move(firstSprite), Common::move(secondSprite)));
+		}
+		break;
+	case kOpGetField:
+		{
+			Common::SharedPtr<Node> castID;
+			if (script->version >= 500)
+				castID = pop();
+			auto fieldID = pop();
+			translation = Common::SharedPtr<Node>(new MemberExprNode("field", Common::move(fieldID), Common::move(castID)));
+		}
+		break;
+	case kOpStartTell:
+		{
+			auto window = pop();
+			auto tellStmt = Common::SharedPtr<TellStmtNode>(new TellStmtNode(Common::move(window)));
+			translation = tellStmt;
+			nextBlock = tellStmt->block.get();
+		}
+		break;
+	case kOpEndTell:
+		{
+			ast.exitBlock();
+			return 1;
+		}
+		break;
+	case kOpPushList:
+		{
+			auto list = pop();
+			list->getValue()->type = kDatumList;
+			translation = list;
+		}
+		break;
+	case kOpPushPropList:
+		{
+			auto list = pop();
+			list->getValue()->type = kDatumPropList;
+			translation = list;
+		}
+		break;
+	case kOpSwap:
+		if (stack.size() >= 2) {
+			std::swap(stack[stack.size() - 1], stack[stack.size() - 2]);
+		} else {
+			warning("kOpSwap: Stack too small!");
+		}
+		return 1;
+	case kOpPushInt8:
+	case kOpPushInt16:
+	case kOpPushInt32:
+		{
+			auto i = Common::SharedPtr<Datum>(new Datum(bytecode.obj));
+			translation = Common::SharedPtr<Node>(new LiteralNode(Common::move(i)));
+		}
+		break;
+	case kOpPushFloat32:
+		{
+			auto f = Common::SharedPtr<Datum>(new Datum(*(float *)(&bytecode.obj)));
+			translation = Common::SharedPtr<Node>(new LiteralNode(Common::move(f)));
+		}
+		break;
+	case kOpPushArgListNoRet:
+		{
+			auto argCount = bytecode.obj;
+			Common::Array<Common::SharedPtr<Node>> args;
+			args.resize(argCount);
+			while (argCount) {
+				argCount--;
+				args[argCount] = pop();
+			}
+			auto argList = Common::SharedPtr<Datum>(new Datum(kDatumArgListNoRet, args));
+			translation = Common::SharedPtr<Node>(new LiteralNode(Common::move(argList)));
+		}
+		break;
+	case kOpPushArgList:
+		{
+			auto argCount = bytecode.obj;
+			Common::Array<Common::SharedPtr<Node>> args;
+			args.resize(argCount);
+			while (argCount) {
+				argCount--;
+				args[argCount] = pop();
+			}
+			auto argList = Common::SharedPtr<Datum>(new Datum(kDatumArgList, args));
+			translation = Common::SharedPtr<Node>(new LiteralNode(Common::move(argList)));
+		}
+		break;
+	case kOpPushCons:
+		{
+			int literalID = bytecode.obj / variableMultiplier();
+			if (-1 < literalID && (unsigned)literalID < script->literals.size()) {
+				translation = Common::SharedPtr<Node>(new LiteralNode(script->literals[literalID].value));
+			} else {
+				translation = Common::SharedPtr<Node>(new ErrorNode());
+			}
+			break;
+		}
+	case kOpPushSymb:
+		{
+			auto sym = Common::SharedPtr<Datum>(new Datum(kDatumSymbol, getName(bytecode.obj)));
+			translation = Common::SharedPtr<Node>(new LiteralNode(Common::move(sym)));
+		}
+		break;
+	case kOpPushVarRef:
+		{
+			auto ref = Common::SharedPtr<Datum>(new Datum(kDatumVarRef, getName(bytecode.obj)));
+			translation = Common::SharedPtr<Node>(new LiteralNode(Common::move(ref)));
+		}
+		break;
+	case kOpGetGlobal:
+	case kOpGetGlobal2:
+		{
+			auto name_ = getName(bytecode.obj);
+			translation = Common::SharedPtr<Node>(new VarNode(name_));
+		}
+		break;
+	case kOpGetProp:
+		translation = Common::SharedPtr<Node>(new VarNode(getName(bytecode.obj)));
+		break;
+	case kOpGetParam:
+		translation = Common::SharedPtr<Node>(new VarNode(getArgumentName(bytecode.obj / variableMultiplier())));
+		break;
+	case kOpGetLocal:
+		translation = Common::SharedPtr<Node>(new VarNode(getLocalName(bytecode.obj / variableMultiplier())));
+		break;
+	case kOpSetGlobal:
+	case kOpSetGlobal2:
+		{
+			auto varName = getName(bytecode.obj);
+			auto var = Common::SharedPtr<Node>(new VarNode(varName));
+			auto value = pop();
+			translation = Common::SharedPtr<Node>(new AssignmentStmtNode(Common::move(var), Common::move(value)));
+		}
+		break;
+	case kOpSetProp:
+		{
+			auto var = Common::SharedPtr<Node>(new VarNode(getName(bytecode.obj)));
+			auto value = pop();
+			translation = Common::SharedPtr<Node>(new AssignmentStmtNode(Common::move(var), Common::move(value)));
+		}
+		break;
+	case kOpSetParam:
+		{
+			auto var = Common::SharedPtr<Node>(new VarNode(getArgumentName(bytecode.obj / variableMultiplier())));
+			auto value = pop();
+			translation = Common::SharedPtr<Node>(new AssignmentStmtNode(Common::move(var), Common::move(value)));
+		}
+		break;
+	case kOpSetLocal:
+		{
+			auto var = Common::SharedPtr<Node>(new VarNode(getLocalName(bytecode.obj / variableMultiplier())));
+			auto value = pop();
+			translation = Common::SharedPtr<Node>(new AssignmentStmtNode(Common::move(var), Common::move(value)));
+		}
+		break;
+	case kOpJmp:
+		{
+			uint32_t targetPos = bytecode.pos + bytecode.obj;
+			size_t targetIndex = bytecodePosMap[targetPos];
+			auto &targetBytecode = bytecodeArray[targetIndex];
+			auto ancestorLoop = ast.currentBlock->ancestorLoop();
+			if (ancestorLoop) {
+				if (bytecodeArray[targetIndex - 1].opcode == kOpEndRepeat && bytecodeArray[targetIndex - 1].ownerLoop == ancestorLoop->startIndex) {
+					translation = Common::SharedPtr<Node>(new ExitRepeatStmtNode());
+					break;
+				} else if (bytecodeArray[targetIndex].tag == kTagNextRepeatTarget && bytecodeArray[targetIndex].ownerLoop == ancestorLoop->startIndex) {
+					translation = Common::SharedPtr<Node>(new NextRepeatStmtNode());
+					break;
+				}
+			}
+			auto &nextBytecode = bytecodeArray[index + 1];
+			auto ancestorStatement = ast.currentBlock->ancestorStatement();
+			if (ancestorStatement && nextBytecode.pos == ast.currentBlock->endPos) {
+				if (ancestorStatement->type == kIfStmtNode) {
+					auto ifStmt = static_cast<IfStmtNode *>(ancestorStatement);
+					if (ast.currentBlock == ifStmt->block1.get()) {
+						ifStmt->hasElse = true;
+						ifStmt->block2->endPos = targetPos;
+						return 1; // if statement amended, nothing to push
+					}
+				} else if (ancestorStatement->type == kCaseStmtNode) {
+					auto caseStmt = static_cast<CaseStmtNode *>(ancestorStatement);
+					caseStmt->potentialOtherwisePos = bytecode.pos;
+					caseStmt->endPos = targetPos;
+					targetBytecode.tag = kTagEndCase;
+					return 1;
+				}
+			}
+			if (targetBytecode.opcode == kOpPop && targetBytecode.obj == 1) {
+				// This is a case statement starting with 'otherwise'
+				auto value = pop();
+				auto caseStmt = Common::SharedPtr<CaseStmtNode>(new CaseStmtNode(Common::move(value)));
+				caseStmt->endPos = targetPos;
+				targetBytecode.tag = kTagEndCase;
+				caseStmt->addOtherwise();
+				translation = caseStmt;
+				nextBlock = caseStmt->otherwise->block.get();
+				break;
+			}
+			translation = Common::SharedPtr<Node>(new CommentNode("ERROR: Could not identify jmp"));
+		}
+		break;
+	case kOpEndRepeat:
+		// This should normally be tagged kTagSkip or kTagNextRepeatTarget and skipped.
+		translation = Common::SharedPtr<Node>(new CommentNode("ERROR: Stray endrepeat"));
+		break;
+	case kOpJmpIfZ:
+		{
+			uint32_t endPos = bytecode.pos + bytecode.obj;
+			uint32_t endIndex = bytecodePosMap[endPos];
+			switch (bytecode.tag) {
+			case kTagRepeatWhile:
+				{
+					auto condition = pop();
+					auto loop = Common::SharedPtr<RepeatWhileStmtNode>(new RepeatWhileStmtNode(index, Common::move(condition)));
+					loop->block->endPos = endPos;
+					translation = loop;
+					nextBlock = loop->block.get();
+				}
+				break;
+			case kTagRepeatWithIn:
+				{
+					auto list = pop();
+					Common::String varName = getVarNameFromSet(bytecodeArray[index + 5]);
+					auto loop = Common::SharedPtr<RepeatWithInStmtNode>(new RepeatWithInStmtNode(index, varName, Common::move(list)));
+					loop->block->endPos = endPos;
+					translation = loop;
+					nextBlock = loop->block.get();
+				}
+				break;
+			case kTagRepeatWithTo:
+			case kTagRepeatWithDownTo:
+				{
+					bool up = (bytecode.tag == kTagRepeatWithTo);
+					auto end = pop();
+					auto start = pop();
+					auto endRepeat = bytecodeArray[endIndex - 1];
+					uint32_t conditionStartIndex = bytecodePosMap[endRepeat.pos - endRepeat.obj];
+					Common::String varName = getVarNameFromSet(bytecodeArray[conditionStartIndex - 1]);
+					auto loop = Common::SharedPtr<RepeatWithToStmtNode>(new RepeatWithToStmtNode(index, varName, Common::move(start), up, Common::move(end)));
+					loop->block->endPos = endPos;
+					translation = loop;
+					nextBlock = loop->block.get();
+				}
+				break;
+			default:
+				{
+					auto condition = pop();
+					auto ifStmt = Common::SharedPtr<IfStmtNode>(new IfStmtNode(Common::move(condition)));
+					ifStmt->block1->endPos = endPos;
+					translation = ifStmt;
+					nextBlock = ifStmt->block1.get();
+				}
+				break;
+			}
+		}
+		break;
+	case kOpLocalCall:
+		{
+			auto argList = pop();
+			translation = Common::SharedPtr<Node>(new CallNode(script->handlers[bytecode.obj].name, Common::move(argList)));
+		}
+		break;
+	case kOpExtCall:
+	case kOpTellCall:
+		{
+			Common::String name_ = getName(bytecode.obj);
+			auto argList = pop();
+			bool isStatement = (argList->getValue()->type == kDatumArgListNoRet);
+			auto &rawArgList = argList->getValue()->l;
+			size_t nargs = rawArgList.size();
+			if (isStatement && name_ == "sound" && nargs > 0 && rawArgList[0]->type == kLiteralNode && rawArgList[0]->getValue()->type == kDatumSymbol) {
+				Common::String cmd = rawArgList[0]->getValue()->s;
+				rawArgList.erase(rawArgList.begin());
+				translation = Common::SharedPtr<Node>(new SoundCmdStmtNode(cmd, Common::move(argList)));
+			} else if (isStatement && name_ == "play" && nargs <= 2) {
+				translation = Common::SharedPtr<Node>(new PlayCmdStmtNode(Common::move(argList)));
+			} else {
+				translation = Common::SharedPtr<Node>(new CallNode(name_, Common::move(argList)));
+			}
+		}
+		break;
+	case kOpObjCallV4:
+		{
+			auto object = readVar(bytecode.obj);
+			auto argList = pop();
+			auto &rawArgList = argList->getValue()->l;
+			if (rawArgList.size() > 0) {
+				// first arg is a symbol
+				// replace it with a variable
+				rawArgList[0] = Common::SharedPtr<Node>(new VarNode(rawArgList[0]->getValue()->s));
+			}
+			translation = Common::SharedPtr<Node>(new ObjCallV4Node(Common::move(object), Common::move(argList)));
+		}
+		break;
+	case kOpPut:
+		{
+			PutType putType = static_cast<PutType>((bytecode.obj >> 4) & 0xF);
+			uint32_t varType = bytecode.obj & 0xF;
+			auto var = readVar(varType);
+			auto val = pop();
+			translation = Common::SharedPtr<Node>(new PutStmtNode(putType, Common::move(var), Common::move(val)));
+		}
+		break;
+	case kOpPutChunk:
+		{
+			PutType putType = static_cast<PutType>((bytecode.obj >> 4) & 0xF);
+			uint32_t varType = bytecode.obj & 0xF;
+			auto var = readVar(varType);
+			auto chunk = readChunkRef(Common::move(var));
+			auto val = pop();
+			if (chunk->type == kCommentNode) { // error comment
+				translation = chunk;
+			} else {
+				translation = Common::SharedPtr<Node>(new PutStmtNode(putType, Common::move(chunk), Common::move(val)));
+			}
+		}
+		break;
+	case kOpDeleteChunk:
+		{
+			auto var = readVar(bytecode.obj);
+			auto chunk = readChunkRef(Common::move(var));
+			if (chunk->type == kCommentNode) { // error comment
+				translation = chunk;
+			} else {
+				translation = Common::SharedPtr<Node>(new ChunkDeleteStmtNode(Common::move(chunk)));
+			}
+		}
+		break;
+	case kOpGet:
+		{
+			int propertyID = pop()->getValue()->toInt();
+			translation = readV4Property(bytecode.obj, propertyID);
+		}
+		break;
+	case kOpSet:
+		{
+			int propertyID = pop()->getValue()->toInt();
+			auto value = pop();
+			if (bytecode.obj == 0x00 && 0x01 <= propertyID && propertyID <= 0x05 && value->getValue()->type == kDatumString) {
+				// This is either a `set eventScript to "script"` or `when event then script` statement.
+				// If the script starts with a space, it's probably a when statement.
+				// If the script contains a line break, it's definitely a when statement.
+				Common::String script_ = value->getValue()->s;
+				if (script_.size() > 0 && (script_[0] == ' ' || script_.find('\r') != Common::String::npos)) {
+					translation = Common::SharedPtr<Node>(new WhenStmtNode(propertyID, script_));
+				}
+			}
+			if (!translation) {
+				auto prop = readV4Property(bytecode.obj, propertyID);
+				if (prop->type == kCommentNode) { // error comment
+					translation = prop;
+				} else {
+					translation = Common::SharedPtr<Node>(new AssignmentStmtNode(Common::move(prop), Common::move(value), true));
+				}
+			}
+		}
+		break;
+	case kOpGetMovieProp:
+		translation = Common::SharedPtr<Node>(new TheExprNode(getName(bytecode.obj)));
+		break;
+	case kOpSetMovieProp:
+		{
+			auto value = pop();
+			auto prop = Common::SharedPtr<TheExprNode>(new TheExprNode(getName(bytecode.obj)));
+			translation = Common::SharedPtr<Node>(new AssignmentStmtNode(Common::move(prop), Common::move(value)));
+		}
+		break;
+	case kOpGetObjProp:
+	case kOpGetChainedProp:
+		{
+			auto object = pop();
+			translation = Common::SharedPtr<Node>(new ObjPropExprNode(Common::move(object), getName(bytecode.obj)));
+		}
+		break;
+	case kOpSetObjProp:
+		{
+			auto value = pop();
+			auto object = pop();
+			auto prop = Common::SharedPtr<ObjPropExprNode>(new ObjPropExprNode(Common::move(object), getName(bytecode.obj)));
+			translation = Common::SharedPtr<Node>(new AssignmentStmtNode(Common::move(prop), Common::move(value)));
+		}
+		break;
+	case kOpPeek:
+		{
+			// This op denotes the beginning of a 'repeat with ... in list' statement or a case in a cases statement.
+
+			// In a 'repeat with ... in list' statement, this peeked value is the list.
+			// In a cases statement, this is the switch expression.
+
+			auto prevLabel = ast.currentBlock->currentCaseLabel;
+
+			// This must be a case. Find the comparison against the switch expression.
+			auto originalStackSize = stack.size();
+			uint32_t currIndex = index + 1;
+			Bytecode *currBytecode = &bytecodeArray[currIndex];
+			do {
+				translateBytecode(*currBytecode, currIndex);
+				currIndex += 1;
+				currBytecode = &bytecodeArray[currIndex];
+			} while (
+				currIndex < bytecodeArray.size()
+				&& !(stack.size() == originalStackSize + 1 && (currBytecode->opcode == kOpEq || currBytecode->opcode == kOpNtEq))
+			);
+			if (currIndex >= bytecodeArray.size()) {
+				bytecode.translation = Common::SharedPtr<Node>(new CommentNode("ERROR: Expected eq or nteq!"));
+				ast.addStatement(bytecode.translation);
+				return currIndex - index + 1;
+			}
+
+			// If the comparison is <>, this is followed by another, equivalent case.
+			// (e.g. this could be case1 in `case1, case2: statement`)
+			bool notEq = (currBytecode->opcode == kOpNtEq);
+			Common::SharedPtr<Node> caseValue = pop(); // This is the value the switch expression is compared against.
+
+			currIndex += 1;
+			currBytecode = &bytecodeArray[currIndex];
+			if (currIndex >= bytecodeArray.size() || currBytecode->opcode != kOpJmpIfZ) {
+				bytecode.translation = Common::SharedPtr<Node>(new CommentNode("ERROR: Expected jmpifz!"));
+				ast.addStatement(bytecode.translation);
+				return currIndex - index + 1;
+			}
+
+			auto &jmpifz = *currBytecode;
+			auto jmpPos = jmpifz.pos + jmpifz.obj;
+			size_t targetIndex = bytecodePosMap[jmpPos];
+			auto &targetBytecode = bytecodeArray[targetIndex];
+			auto &prevFromTarget = bytecodeArray[targetIndex - 1];
+			CaseExpect expect;
+			if (notEq) {
+				expect = kCaseExpectOr; // Expect an equivalent case after this one.
+			} else if (targetBytecode.opcode == kOpPeek) {
+				expect = kCaseExpectNext; // Expect a different case after this one.
+			} else if (targetBytecode.opcode == kOpPop
+					&& targetBytecode.obj == 1
+					&& (prevFromTarget.opcode != kOpJmp || prevFromTarget.pos + prevFromTarget.obj == targetBytecode.pos)) {
+				expect = kCaseExpectEnd; // Expect the end of the switch statement.
+			} else {
+				expect = kCaseExpectOtherwise; // Expect an 'otherwise' block.
+			}
+
+			auto currLabel = Common::SharedPtr<CaseLabelNode>(new CaseLabelNode(Common::move(caseValue), expect));
+			jmpifz.translation = currLabel;
+			ast.currentBlock->currentCaseLabel = currLabel.get();
+
+			if (!prevLabel) {
+				auto peekedValue = pop();
+				auto caseStmt = Common::SharedPtr<CaseStmtNode>(new CaseStmtNode(Common::move(peekedValue)));
+				caseStmt->firstLabel = currLabel;
+				currLabel->parent = caseStmt.get();
+				bytecode.translation = caseStmt;
+				ast.addStatement(caseStmt);
+			} else if (prevLabel->expect == kCaseExpectOr) {
+				prevLabel->nextOr = currLabel;
+				currLabel->parent = prevLabel;
+			} else if (prevLabel->expect == kCaseExpectNext) {
+				prevLabel->nextLabel = currLabel;
+				currLabel->parent = prevLabel;
+			}
+
+			// The block doesn't start until the after last equivalent case,
+			// so don't create a block yet if we're expecting an equivalent case.
+			if (currLabel->expect != kCaseExpectOr) {
+				currLabel->block = Common::SharedPtr<BlockNode>(new BlockNode());
+				currLabel->block->parent = currLabel.get();
+				currLabel->block->endPos = jmpPos;
+				ast.enterBlock(currLabel->block.get());
+			}
+
+			return currIndex - index + 1;
+		}
+		break;
+	case kOpPop:
+		{
+			// Pop instructions in 'repeat with in' loops are tagged kTagSkip and skipped.
+			if (bytecode.tag == kTagEndCase) {
+				// We've already recognized this as the end of a case statement.
+				// Attach an 'end case' node for the summary only.
+				bytecode.translation = Common::SharedPtr<EndCaseNode>();
+				return 1;
+			}
+			if (bytecode.obj == 1 && stack.size() == 1) {
+				// We have an unused value on the stack, so this must be the end
+				// of a case statement with no labels.
+				auto value = pop();
+				translation = Common::SharedPtr<Node>(new CaseStmtNode(Common::move(value)));
+				break;
+			}
+			// Otherwise, this pop instruction occurs before a 'return' within
+			// a case statement. No translation needed.
+			return 1;
+		}
+		break;
+	case kOpTheBuiltin:
+		{
+			pop(); // empty arglist
+			translation = Common::SharedPtr<Node>(new TheExprNode(getName(bytecode.obj)));
+		}
+		break;
+	case kOpObjCall:
+		{
+			Common::String method = getName(bytecode.obj);
+			auto argList = pop();
+			auto &rawArgList = argList->getValue()->l;
+			size_t nargs = rawArgList.size();
+			if (method == "getAt" && nargs == 2)  {
+				// obj.getAt(i) => obj[i]
+				auto obj = rawArgList[0];
+				auto prop = rawArgList[1];
+				translation = Common::SharedPtr<Node>(new ObjBracketExprNode(Common::move(obj), Common::move(prop)));
+			} else if (method == "setAt" && nargs == 3) {
+				// obj.setAt(i) => obj[i] = val
+				auto obj = rawArgList[0];
+				auto prop = rawArgList[1];
+				auto val = rawArgList[2];
+				Common::SharedPtr<Node> propExpr = Common::SharedPtr<Node>(new ObjBracketExprNode(Common::move(obj), Common::move(prop)));
+				translation = Common::SharedPtr<Node>(new AssignmentStmtNode(Common::move(propExpr), Common::move(val)));
+			} else if ((method == "getProp" || method == "getPropRef") && (nargs == 3 || nargs == 4) && rawArgList[1]->getValue()->type == kDatumSymbol) {
+				// obj.getProp(#prop, i) => obj.prop[i]
+				// obj.getProp(#prop, i, i2) => obj.prop[i..i2]
+				auto obj = rawArgList[0];
+				Common::String propName  = rawArgList[1]->getValue()->s;
+				auto i = rawArgList[2];
+				auto i2 = (nargs == 4) ? rawArgList[3] : nullptr;
+				translation = Common::SharedPtr<Node>(new ObjPropIndexExprNode(Common::move(obj), propName, Common::move(i), Common::move(i2)));
+			} else if (method == "setProp" && (nargs == 4 || nargs == 5) && rawArgList[1]->getValue()->type == kDatumSymbol) {
+				// obj.setProp(#prop, i, val) => obj.prop[i] = val
+				// obj.setProp(#prop, i, i2, val) => obj.prop[i..i2] = val
+				auto obj = rawArgList[0];
+				Common::String propName  = rawArgList[1]->getValue()->s;
+				auto i = rawArgList[2];
+				auto i2 = (nargs == 5) ? rawArgList[3] : nullptr;
+				auto propExpr = Common::SharedPtr<ObjPropIndexExprNode>(new ObjPropIndexExprNode(Common::move(obj), propName, Common::move(i), Common::move(i2)));
+				auto val = rawArgList[nargs - 1];
+				translation = Common::SharedPtr<Node>(new AssignmentStmtNode(Common::move(propExpr), Common::move(val)));
+			} else if (method == "count" && nargs == 2 && rawArgList[1]->getValue()->type == kDatumSymbol) {
+				// obj.count(#prop) => obj.prop.count
+				auto obj = rawArgList[0];
+				Common::String propName  = rawArgList[1]->getValue()->s;
+				auto propExpr = Common::SharedPtr<ObjPropExprNode>(new ObjPropExprNode(Common::move(obj), propName));
+				translation = Common::SharedPtr<Node>(new ObjPropExprNode(Common::move(propExpr), "count"));
+			} else if ((method == "setContents" || method == "setContentsAfter" || method == "setContentsBefore") && nargs == 2) {
+				// var.setContents(val) => put val into var
+				// var.setContentsAfter(val) => put val after var
+				// var.setContentsBefore(val) => put val before var
+				PutType putType;
+				if (method == "setContents") {
+					putType = kPutInto;
+				} else if (method == "setContentsAfter") {
+					putType = kPutAfter;
+				} else {
+					putType = kPutBefore;
+				}
+				auto var = rawArgList[0];
+				auto val = rawArgList[1];
+				translation = Common::SharedPtr<Node>(new PutStmtNode(putType, Common::move(var), Common::move(val)));
+			} else if (method == "hilite" && nargs == 1) {
+				// chunk.hilite() => hilite chunk
+				auto chunk = rawArgList[0];
+				translation = Common::SharedPtr<Node>(new ChunkHiliteStmtNode(chunk));
+			} else if (method == "delete" && nargs == 1) {
+				// chunk.delete() => delete chunk
+				auto chunk = rawArgList[0];
+				translation = Common::SharedPtr<Node>(new ChunkDeleteStmtNode(chunk));
+			} else {
+				translation = Common::SharedPtr<Node>(new ObjCallNode(method, Common::move(argList)));
+			}
+		}
+		break;
+	case kOpPushChunkVarRef:
+		translation = readVar(bytecode.obj);
+		break;
+	case kOpGetTopLevelProp:
+		{
+			auto name_ = getName(bytecode.obj);
+			translation = Common::SharedPtr<VarNode>(new VarNode(name_));
+		}
+		break;
+	case kOpNewObj:
+		{
+			auto objType = getName(bytecode.obj);
+			auto objArgs = pop();
+			translation = Common::SharedPtr<NewObjNode>(new NewObjNode(objType, Common::move(objArgs)));
+		}
+		break;
+	default:
+		{
+			auto commentText = StandardNames::getOpcodeName(bytecode.opID);
+			if (bytecode.opcode >= 0x40)
+				commentText += Common::String::format(" %d", bytecode.obj);
+			translation = Common::SharedPtr<CommentNode>(new CommentNode(commentText));
+			stack.clear(); // Clear stack so later bytecode won't be too screwed up
+		}
+	}
+
+	if (!translation)
+		translation = Common::SharedPtr<ErrorNode>(new ErrorNode());
+
+	bytecode.translation = translation;
+	if (translation->isExpression) {
+		stack.push_back(Common::move(translation));
+	} else {
+		ast.addStatement(Common::move(translation));
+	}
+
+	if (nextBlock)
+		ast.enterBlock(nextBlock);
+
+	return 1;
+}
+
+Common::String posToString(int32_t pos) {
+	return Common::String::format("[%3d]", pos);
+}
+
+void Handler::writeBytecodeText(CodeWriter &code, bool dotSyntax) const {
+	bool isMethod = script->isFactory();
+
+	if (!isGenericEvent) {
+		if (isMethod) {
+			code.write("method ");
+		} else {
+			code.write("on ");
+		}
+		code.write(name);
+		if (argumentNames.size() > 0) {
+			code.write(" ");
+			for (size_t i = 0; i < argumentNames.size(); i++) {
+				if (i > 0)
+					code.write(", ");
+				code.write(argumentNames[i]);
+			}
+		}
+		code.writeLine();
+		code.indent();
+	}
+	for (auto &bytecode : bytecodeArray) {
+		code.write(posToString(bytecode.pos));
+		code.write(" ");
+		code.write(StandardNames::getOpcodeName(bytecode.opID));
+		switch (bytecode.opcode) {
+		case kOpJmp:
+		case kOpJmpIfZ:
+			code.write(" ");
+			code.write(posToString(bytecode.pos + bytecode.obj));
+			break;
+		case kOpEndRepeat:
+			code.write(" ");
+			code.write(posToString(bytecode.pos - bytecode.obj));
+			break;
+		case kOpPushFloat32:
+			code.write(" ");
+			code.write(Common::String::format("%g", (*(const float *)(&bytecode.obj))));
+			break;
+		default:
+			if (bytecode.opID > 0x40) {
+				code.write(" ");
+				code.write(Common::String::format("%d", bytecode.obj));
+			}
+			break;
+		}
+		if (bytecode.translation) {
+			code.write(" ...");
+			while (code.lineWidth() < 49) {
+				code.write(".");
+			}
+			code.write(" ");
+			if (bytecode.translation->isExpression) {
+				code.write("<");
+			}
+			bytecode.translation->writeScriptText(code, dotSyntax, true);
+			if (bytecode.translation->isExpression) {
+				code.write(">");
+			}
+		}
+		code.writeLine();
+	}
+	if (!isGenericEvent) {
+		code.unindent();
+		if (!isMethod) {
+			code.writeLine("end");
+		}
+	}
+}
+
+} // namespace LingoDec
diff --git a/engines/director/lingo/lingodec/handler.h b/engines/director/lingo/lingodec/handler.h
new file mode 100644
index 00000000000..f5ad04831ae
--- /dev/null
+++ b/engines/director/lingo/lingodec/handler.h
@@ -0,0 +1,110 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef LINGODEC_HANDLER_H
+#define LINGODEC_HANDLER_H
+
+#include "common/array.h"
+#include "common/stablemap.h"
+#include "common/str.h"
+#include "./enums.h"
+
+namespace Common {
+class SeekableReadStream;
+}
+
+namespace LingoDec {
+
+struct AST;
+struct Bytecode;
+class CodeWriter;
+struct Node;
+struct Script;
+
+/* Handler */
+
+struct Handler {
+	int16_t nameID = 0;
+	uint16_t vectorPos = 0;
+	uint32_t compiledLen = 0;
+	uint32_t compiledOffset = 0;
+	uint16_t argumentCount = 0;
+	uint32_t argumentOffset = 0;
+	uint16_t localsCount = 0;
+	uint32_t localsOffset = 0;
+	uint16_t globalsCount = 0;
+	uint32_t globalsOffset = 0;
+	uint32_t unknown1 = 0;
+	uint16_t unknown2 = 0;
+	uint16_t lineCount = 0;
+	uint32_t lineOffset = 0;
+	uint32_t stackHeight = 0;
+
+	Common::Array<int16_t> argumentNameIDs;
+	Common::Array<int16_t> localNameIDs;
+	Common::Array<int16_t> globalNameIDs;
+
+	Script *script = nullptr;
+	Common::Array<Bytecode> bytecodeArray;
+	Common::StableMap<uint32_t, size_t> bytecodePosMap;
+	Common::Array<Common::String> argumentNames;
+	Common::Array<Common::String> localNames;
+	Common::Array<Common::String> globalNames;
+	Common::String name;
+
+	Common::Array<Common::SharedPtr<Node>> stack;
+	AST ast;
+
+	bool isGenericEvent = false;
+
+	Handler(): ast(this) {}
+
+	void setScript(Script *s) {
+		script = s;
+	}
+
+	void readRecord(Common::SeekableReadStream &stream);
+	void readData(Common::SeekableReadStream &stream);
+	Common::Array<int16_t> readVarnamesTable(Common::SeekableReadStream &stream, uint16_t count, uint32_t offset);
+	void readNames();
+	bool validName(int id) const;
+	Common::String getName(int id) const;
+	Common::String getArgumentName(int id) const;
+	Common::String getLocalName(int id) const;
+	Common::SharedPtr<Node> pop();
+	int variableMultiplier();
+	Common::SharedPtr<Node> readVar(int varType);
+	Common::String getVarNameFromSet(const Bytecode &bytecode);
+	Common::SharedPtr<Node> readV4Property(int propertyType, int propertyID);
+	Common::SharedPtr<Node> readChunkRef(Common::SharedPtr<Node> string);
+	void tagLoops();
+	bool isRepeatWithIn(uint32_t startIndex, uint32_t endIndex);
+	BytecodeTag identifyLoop(uint32_t startIndex, uint32_t endIndex);
+	void parse();
+	uint32_t translateBytecode(Bytecode &bytecode, uint32_t index);
+	void writeBytecodeText(CodeWriter &code, bool dotSyntax) const;
+};
+
+/* Bytecode */
+
+struct Bytecode {
+	uint8_t opID;
+	OpCode opcode;
+	int32_t obj;
+	uint32_t pos;
+	BytecodeTag tag;
+	uint32_t ownerLoop;
+	Common::SharedPtr<Node> translation;
+
+	Bytecode(uint8_t op, int32_t o, uint32_t p)
+		: opID(op), obj(o), pos(p), tag(kTagNone), ownerLoop(UINT32_MAX) {
+		opcode = static_cast<OpCode>(op >= 0x40 ? 0x40 + op % 0x40 : op);
+	}
+};
+
+}
+
+#endif // LINGODEC_HANDLER_H
diff --git a/engines/director/lingo/lingodec/names.cpp b/engines/director/lingo/lingodec/names.cpp
new file mode 100644
index 00000000000..3b28d81fcde
--- /dev/null
+++ b/engines/director/lingo/lingodec/names.cpp
@@ -0,0 +1,343 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#include "common/str.h"
+#include "common/stream.h"
+#include "common/util.h"
+#include "./enums.h"
+#include "./names.h"
+
+namespace LingoDec {
+
+template<class Key, class Val>
+static Common::StableMap<Key, Val> toMap(std::initializer_list<Common::Pair<Key, Val>> items) {
+	Common::StableMap<Key, Val> stableMap;
+	for(auto item : items) {
+		stableMap.insert(Common::Pair<Key, Val>(item.first, item.second));
+	}
+	return stableMap;
+}
+
+/* StandardNames */
+
+Common::StableMap<unsigned int, Common::String> StandardNames::opcodeNames = toMap<unsigned int, Common::String>({
+	// single-byte
+	{ kOpRet,				"ret" },
+	{ kOpRetFactory,		"retfactory" },
+	{ kOpPushZero,			"pushzero" },
+	{ kOpMul,				"mul" },
+	{ kOpAdd,				"add" },
+	{ kOpSub,				"sub" },
+	{ kOpDiv,				"div" },
+	{ kOpMod,				"mod" },
+	{ kOpInv,				"inv" },
+	{ kOpJoinStr,			"joinstr" },
+	{ kOpJoinPadStr,		"joinpadstr" },
+	{ kOpLt,				"lt" },
+	{ kOpLtEq,				"lteq" },
+	{ kOpNtEq,				"nteq" },
+	{ kOpEq,				"eq" },
+	{ kOpGt,				"gt" },
+	{ kOpGtEq,				"gteq" },
+	{ kOpAnd,				"and" },
+	{ kOpOr,				"or" },
+	{ kOpNot,				"not" },
+	{ kOpContainsStr,		"containsstr" },
+	{ kOpContains0Str,		"contains0str" },
+	{ kOpGetChunk,			"getchunk" },
+	{ kOpHiliteChunk,		"hilitechunk" },
+	{ kOpOntoSpr,			"ontospr" },
+	{ kOpIntoSpr,			"intospr" },
+	{ kOpGetField,			"getfield" },
+	{ kOpStartTell,			"starttell" },
+	{ kOpEndTell,			"endtell" },
+	{ kOpPushList,			"pushlist" },
+	{ kOpPushPropList,		"pushproplist" },
+	{ kOpSwap,				"swap" },
+
+	// multi-byte
+	{ kOpPushInt8,			"pushint8" },
+	{ kOpPushArgListNoRet,	"pusharglistnoret" },
+	{ kOpPushArgList,		"pusharglist" },
+	{ kOpPushCons,			"pushcons" },
+	{ kOpPushSymb,			"pushsymb" },
+	{ kOpPushVarRef,		"pushvarref" },
+	{ kOpGetGlobal2,		"getglobal2" },
+	{ kOpGetGlobal,			"getglobal" },
+	{ kOpGetProp,			"getprop" },
+	{ kOpGetParam,			"getparam" },
+	{ kOpGetLocal,			"getlocal" },
+	{ kOpSetGlobal2,		"setglobal2" },
+	{ kOpSetGlobal,			"setglobal" },
+	{ kOpSetProp,			"setprop" },
+	{ kOpSetParam,			"setparam" },
+	{ kOpSetLocal,			"setlocal" },
+	{ kOpJmp,				"jmp" },
+	{ kOpEndRepeat,			"endrepeat" },
+	{ kOpJmpIfZ,			"jmpifz" },
+	{ kOpLocalCall,			"localcall" },
+	{ kOpExtCall,			"extcall" },
+	{ kOpObjCallV4,			"objcallv4" },
+	{ kOpPut,				"put" },
+	{ kOpPutChunk,			"putchunk" },
+	{ kOpDeleteChunk,		"deletechunk" },
+	{ kOpGet,				"get" },
+	{ kOpSet,				"set" },
+	{ kOpGetMovieProp,		"getmovieprop" },
+	{ kOpSetMovieProp,		"setmovieprop" },
+	{ kOpGetObjProp,		"getobjprop" },
+	{ kOpSetObjProp,		"setobjprop" },
+	{ kOpTellCall,			"tellcall" },
+	{ kOpPeek,				"peek" },
+	{ kOpPop,				"pop" },
+	{ kOpTheBuiltin,		"thebuiltin" },
+	{ kOpObjCall,			"objcall" },
+	{ kOpPushChunkVarRef,	"pushchunkvarref" },
+	{ kOpPushInt16,			"pushint16" },
+	{ kOpPushInt32,			"pushint32" },
+	{ kOpGetChainedProp,	"getchainedprop" },
+	{ kOpPushFloat32,		"pushfloat32" },
+	{ kOpGetTopLevelProp,	"gettoplevelprop" },
+	{ kOpNewObj,			"newobj" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::binaryOpNames = toMap<unsigned int, Common::String>({
+	{ kOpMul,			"*" },
+	{ kOpAdd,			"+" },
+	{ kOpSub,			"-" },
+	{ kOpDiv,			"/" },
+	{ kOpMod,			"mod" },
+	{ kOpJoinStr,		"&" },
+	{ kOpJoinPadStr,	"&&" },
+	{ kOpLt,			"<" },
+	{ kOpLtEq,			"<=" },
+	{ kOpNtEq,			"<>" },
+	{ kOpEq,			"=" },
+	{ kOpGt,			">" },
+	{ kOpGtEq,			">=" },
+	{ kOpAnd,			"and" },
+	{ kOpOr,			"or" },
+	{ kOpContainsStr,	"contains" },
+	{ kOpContains0Str,	"starts" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::chunkTypeNames = toMap<unsigned int, Common::String>({
+	{ kChunkChar, "char" },
+	{ kChunkWord, "word" },
+	{ kChunkItem, "item" },
+	{ kChunkLine, "line" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::putTypeNames = toMap<unsigned int, Common::String>({
+	{ kPutInto,		"into" },
+	{ kPutAfter,	"after" },
+	{ kPutBefore,	"before" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::moviePropertyNames = toMap<unsigned int, Common::String>({
+	{ 0x00, "floatPrecision" },
+	{ 0x01, "mouseDownScript" },
+	{ 0x02, "mouseUpScript" },
+	{ 0x03, "keyDownScript" },
+	{ 0x04, "keyUpScript" },
+	{ 0x05, "timeoutScript" },
+	{ 0x06, "short time" },
+	{ 0x07, "abbr time" },
+	{ 0x08, "long time" },
+	{ 0x09, "short date" },
+	{ 0x0a, "abbr date" },
+	{ 0x0b, "long date" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::whenEventNames = toMap<unsigned int, Common::String>({
+	{ 0x01, "mouseDown" },
+	{ 0x02, "mouseUp" },
+	{ 0x03, "keyDown" },
+	{ 0x04, "keyUp" },
+	{ 0x05, "timeOut" },
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::menuPropertyNames = toMap<unsigned int, Common::String>({
+	{ 0x01, "name" },
+	{ 0x02, "number of menuItems" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::menuItemPropertyNames = toMap<unsigned int, Common::String>({
+	{ 0x01, "name" },
+	{ 0x02, "checkMark" },
+	{ 0x03, "enabled" },
+	{ 0x04, "script" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::soundPropertyNames = toMap<unsigned int, Common::String>({
+	{ 0x01, "volume" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::spritePropertyNames = toMap<unsigned int, Common::String>({
+	{ 0x01, "type" },
+	{ 0x02, "backColor" },
+	{ 0x03, "bottom" },
+	{ 0x04, "castNum" },
+	{ 0x05, "constraint" },
+	{ 0x06, "cursor" },
+	{ 0x07, "foreColor" },
+	{ 0x08, "height" },
+	{ 0x09, "immediate" },
+	{ 0x0a, "ink" },
+	{ 0x0b, "left" },
+	{ 0x0c, "lineSize" },
+	{ 0x0d, "locH" },
+	{ 0x0e, "locV" },
+	{ 0x0f, "movieRate" },
+	{ 0x10, "movieTime" },
+	{ 0x11, "pattern" },
+	{ 0x12, "puppet" },
+	{ 0x13, "right" },
+	{ 0x14, "startTime" },
+	{ 0x15, "stopTime" },
+	{ 0x16, "stretch" },
+	{ 0x17, "top" },
+	{ 0x18, "trails" },
+	{ 0x19, "visible" },
+	{ 0x1a, "volume" },
+	{ 0x1b, "width" },
+	{ 0x1c, "blend" },
+	{ 0x1d, "scriptNum" },
+	{ 0x1e, "moveableSprite" },
+	{ 0x1f, "editableText" },
+	{ 0x20, "scoreColor" },
+	{ 0x21, "loc" },
+	{ 0x22, "rect" },
+	{ 0x23, "memberNum" },
+	{ 0x24, "castLibNum" },
+	{ 0x25, "member" },
+	{ 0x26, "scriptInstanceList" },
+	{ 0x27, "currentTime" },
+	{ 0x28, "mostRecentCuePoint" },
+	{ 0x29, "tweened" },
+	{ 0x2a, "name" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::animationPropertyNames = toMap<unsigned int, Common::String>({
+	{ 0x01, "beepOn" },
+	{ 0x02, "buttonStyle" },
+	{ 0x03, "centerStage" },
+	{ 0x04, "checkBoxAccess" },
+	{ 0x05, "checkboxType" },
+	{ 0x06, "colorDepth" },
+	{ 0x07, "colorQD" },
+	{ 0x08, "exitLock" },
+	{ 0x09, "fixStageSize" },
+	{ 0x0a, "fullColorPermit" },
+	{ 0x0b, "imageDirect" },
+	{ 0x0c, "doubleClick" },
+	{ 0x0d, "key" },
+	{ 0x0e, "lastClick" },
+	{ 0x0f, "lastEvent" },
+	{ 0x10, "keyCode" },
+	{ 0x11, "lastKey" },
+	{ 0x12, "lastRoll"},
+	{ 0x13, "timeoutLapsed" },
+	{ 0x14, "multiSound" },
+	{ 0x15, "pauseState" },
+	{ 0x16, "quickTimePresent" },
+	{ 0x17, "selEnd" },
+	{ 0x18, "selStart" },
+	{ 0x19, "soundEnabled" },
+	{ 0x1a, "soundLevel" },
+	{ 0x1b, "stageColor" },
+	// 0x1c indicates dontPassEvent was called.
+	// It doesn't seem to have a Lingo-accessible name.
+	{ 0x1d, "switchColorDepth" },
+	{ 0x1e, "timeoutKeyDown" },
+	{ 0x1f, "timeoutLength" },
+	{ 0x20, "timeoutMouse" },
+	{ 0x21, "timeoutPlay" },
+	{ 0x22, "timer" },
+	{ 0x23, "preLoadRAM" },
+	{ 0x24, "videoForWindowsPresent" },
+	{ 0x25, "netPresent" },
+	{ 0x26, "safePlayer" },
+	{ 0x27, "soundKeepDevice" },
+	{ 0x28, "soundMixMedia" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::animation2PropertyNames = toMap<unsigned int, Common::String>({
+	{ 0x01, "perFrameHook" },
+	{ 0x02, "number of castMembers" },
+	{ 0x03, "number of menus" },
+	{ 0x04, "number of castLibs" },
+	{ 0x05, "number of xtras" }
+});
+
+Common::StableMap<unsigned int, Common::String> StandardNames::memberPropertyNames = toMap<unsigned int, Common::String>({
+	{ 0x01, "name" },
+	{ 0x02, "text" },
+	{ 0x03, "textStyle" },
+	{ 0x04, "textFont" },
+	{ 0x05, "textHeight" },
+	{ 0x06, "textAlign" },
+	{ 0x07, "textSize" },
+	{ 0x08, "picture" },
+	{ 0x09, "hilite" },
+	{ 0x0a, "number" },
+	{ 0x0b, "size" },
+	{ 0x0c, "loop" },
+	{ 0x0d, "duration" },
+	{ 0x0e, "controller" },
+	{ 0x0f, "directToStage" },
+	{ 0x10, "sound" },
+	{ 0x11, "foreColor" },
+	{ 0x12, "backColor" },
+	{ 0x13, "type" }
+});
+
+Common::String StandardNames::getOpcodeName(uint8_t id) {
+	if (id >= 0x40)
+		id = 0x40 + id % 0x40;
+	auto it = opcodeNames.find(id);
+	if (it == opcodeNames.end()){
+		return Common::String::format("unk%02X" , id);
+	}
+	return it->second;
+}
+
+Common::String StandardNames::getName(const Common::StableMap<unsigned int, Common::String> &nameMap, unsigned int id) {
+	auto it = nameMap.find(id);
+	if (it == nameMap.end())
+		return "ERROR";
+	return it->second;
+}
+
+/* ScriptNames */
+
+void ScriptNames::read(Common::SeekableReadStream &stream) {
+	// Lingo scripts are always big endian regardless of file endianness
+	unknown0 = stream.readSint32BE();
+	unknown1 = stream.readSint32BE();
+	len1 = stream.readUint32BE();
+	len2 = stream.readUint32BE();
+	namesOffset = stream.readUint16BE();
+	namesCount = stream.readUint16BE();
+
+	stream.seek(namesOffset);
+	names.resize(namesCount);
+	for (auto &name : names) {
+		name = stream.readPascalString();
+	}
+}
+
+bool ScriptNames::validName(int id) const {
+	return -1 < id && (unsigned)id < names.size();
+}
+
+Common::String ScriptNames::getName(int id) const {
+	if (validName(id))
+		return names[id];
+	return Common::String::format("UNKNOWN_NAME_%d", id);
+}
+
+}
diff --git a/engines/director/lingo/lingodec/names.h b/engines/director/lingo/lingodec/names.h
new file mode 100644
index 00000000000..ae6226de5e0
--- /dev/null
+++ b/engines/director/lingo/lingodec/names.h
@@ -0,0 +1,63 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef LINGODEC_NAMES_H
+#define LINGODEC_NAMES_H
+
+#include "common/array.h"
+#include "common/stablemap.h"
+
+namespace Common {
+class SeekableReadStream;
+class String;
+}
+
+namespace LingoDec {
+
+/* StandardNames */
+
+struct StandardNames {
+	static Common::StableMap<unsigned int, Common::String> opcodeNames;
+	static Common::StableMap<unsigned int, Common::String> binaryOpNames;
+	static Common::StableMap<unsigned int, Common::String> chunkTypeNames;
+	static Common::StableMap<unsigned int, Common::String> putTypeNames;
+	static Common::StableMap<unsigned int, Common::String> moviePropertyNames;
+	static Common::StableMap<unsigned int, Common::String> whenEventNames;
+	static Common::StableMap<unsigned int, Common::String> timeNames;
+	static Common::StableMap<unsigned int, Common::String> menuPropertyNames;
+	static Common::StableMap<unsigned int, Common::String> menuItemPropertyNames;
+	static Common::StableMap<unsigned int, Common::String> soundPropertyNames;
+	static Common::StableMap<unsigned int, Common::String> spritePropertyNames;
+	static Common::StableMap<unsigned int, Common::String> animationPropertyNames;
+	static Common::StableMap<unsigned int, Common::String> animation2PropertyNames;
+	static Common::StableMap<unsigned int, Common::String> memberPropertyNames;
+
+	static Common::String getOpcodeName(uint8_t id);
+	static Common::String getName(const Common::StableMap<unsigned int, Common::String> &nameMap, unsigned int id);
+};
+
+/* ScriptNames */
+
+struct ScriptNames {
+	int32_t unknown0;
+	int32_t unknown1;
+	uint32_t len1;
+	uint32_t len2;
+	uint16_t namesOffset;
+	uint16_t namesCount;
+	Common::Array<Common::String> names;
+
+	unsigned int version;
+
+	ScriptNames(unsigned int version) : version(version) {}
+	void read(Common::SeekableReadStream &stream);
+	bool validName(int id) const;
+	Common::String getName(int id) const;
+};
+
+} // namespace LingoDec
+
+#endif // LINGODEC_NAMES_H
diff --git a/engines/director/lingo/lingodec/resolver.h b/engines/director/lingo/lingodec/resolver.h
new file mode 100644
index 00000000000..2bdbdfe0fe3
--- /dev/null
+++ b/engines/director/lingo/lingodec/resolver.h
@@ -0,0 +1,25 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef LINGODEC_RESOLVER_H
+#define LINGODEC_RESOLVER_H
+
+namespace LingoDec {
+
+struct Script;
+struct ScriptNames;
+
+class ChunkResolver {
+public:
+	ChunkResolver() {}
+	virtual ~ChunkResolver() {}
+	virtual Script *getScript(int32_t id) = 0;
+	virtual ScriptNames *getScriptNames(int32_t id) = 0;
+};
+
+}
+
+#endif // LINGODEC_RESOLVER_H
diff --git a/engines/director/lingo/lingodec/script.cpp b/engines/director/lingo/lingodec/script.cpp
new file mode 100644
index 00000000000..ffbf9d64804
--- /dev/null
+++ b/engines/director/lingo/lingodec/script.cpp
@@ -0,0 +1,255 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#include "common/stream.h"
+#include "./ast.h"
+#include "./codewriter.h"
+#include "./context.h"
+#include "./handler.h"
+#include "./script.h"
+
+double readAppleFloat80(void *ptr);
+
+namespace LingoDec {
+
+/* Script */
+
+Script::Script(unsigned int version) :
+	version(version),
+	context(nullptr) {}
+
+Script::~Script() = default;
+
+void Script::read(Common::SeekableReadStream &stream) {
+	// Lingo scripts are always big endian regardless of file endianness
+	stream.seek(8);
+	/*  8 */ totalLength = stream.readUint32BE();
+	/* 12 */ totalLength2 = stream.readUint32BE();
+	/* 16 */ headerLength = stream.readUint16BE();
+	/* 18 */ scriptNumber = stream.readUint16BE();
+	/* 20 */ unk20 = stream.readSint16BE();
+	/* 22 */ parentNumber = stream.readSint16BE();
+
+	stream.seek(38);
+	/* 38 */ scriptFlags = stream.readUint32BE();
+	/* 42 */ unk42 = stream.readSint16BE();
+	/* 44 */ castID = stream.readSint32BE();
+	/* 48 */ factoryNameID = stream.readSint16BE();
+	/* 50 */ handlerVectorsCount = stream.readUint16BE();
+	/* 52 */ handlerVectorsOffset = stream.readUint32BE();
+	/* 56 */ handlerVectorsSize = stream.readUint32BE();
+	/* 60 */ propertiesCount = stream.readUint16BE();
+	/* 62 */ propertiesOffset = stream.readUint32BE();
+	/* 66 */ globalsCount = stream.readUint16BE();
+	/* 68 */ globalsOffset = stream.readUint32BE();
+	/* 72 */ handlersCount = stream.readUint16BE();
+	/* 74 */ handlersOffset = stream.readUint32BE();
+	/* 78 */ literalsCount = stream.readUint16BE();
+	/* 80 */ literalsOffset = stream.readUint32BE();
+	/* 84 */ literalsDataCount = stream.readUint32BE();
+	/* 88 */ literalsDataOffset = stream.readUint32BE();
+
+	propertyNameIDs = readVarnamesTable(stream, propertiesCount, propertiesOffset);
+	globalNameIDs = readVarnamesTable(stream, globalsCount, globalsOffset);
+
+	handlers.resize(handlersCount);
+	for (auto &handler : handlers) {
+		handler.setScript(this);
+	}
+	if ((scriptFlags & LingoDec::kScriptFlagEventScript) && handlersCount > 0) {
+		handlers[0].isGenericEvent = true;
+	}
+
+	stream.seek(handlersOffset);
+	for (auto &handler : handlers) {
+		handler.readRecord(stream);
+	}
+	for (auto &handler : handlers) {
+		handler.readData(stream);
+	}
+
+	stream.seek(literalsOffset);
+	literals.resize(literalsCount);
+	for (auto &literal : literals) {
+		literal.readRecord(stream, version);
+	}
+	for (auto &literal : literals) {
+		literal.readData(stream, literalsDataOffset);
+	}
+}
+
+Common::Array<int16_t> Script::readVarnamesTable(Common::SeekableReadStream &stream, uint16_t count, uint32_t offset) {
+	stream.seek(offset);
+	Common::Array<int16_t> nameIDs(count);
+	for (uint16_t i = 0; i < count; i++) {
+		nameIDs[i] = stream.readSint16BE();
+	}
+	return nameIDs;
+}
+
+bool Script::validName(int id) const {
+	return context->validName(id);
+}
+
+Common::String Script::getName(int id) const {
+	return context->getName(id);
+}
+
+void Script::setContext(ScriptContext *ctx) {
+	this->context = ctx;
+	if (factoryNameID != -1) {
+		factoryName = getName(factoryNameID);
+	}
+	for (auto nameID : propertyNameIDs) {
+		if (validName(nameID)) {
+			Common::String name = getName(nameID);
+			if (isFactory() && name == "me")
+				continue;
+			propertyNames.push_back(name);
+		}
+	}
+	for (auto nameID : globalNameIDs) {
+		if (validName(nameID)) {
+			globalNames.push_back(getName(nameID));
+		}
+	}
+	for (auto &handler : handlers) {
+		handler.readNames();
+	}
+}
+
+void Script::parse() {
+	for (auto &handler : handlers) {
+		handler.parse();
+	}
+}
+
+void Script::writeVarDeclarations(CodeWriter &code) const {
+	if (!isFactory()) {
+		if (propertyNames.size() > 0) {
+			code.write("property ");
+			for (size_t i = 0; i < propertyNames.size(); i++) {
+				if (i > 0)
+					code.write(", ");
+				code.write(propertyNames[i]);
+			}
+			code.writeLine();
+		}
+	}
+	if (globalNames.size() > 0) {
+		code.write("global ");
+		for (size_t i = 0; i < globalNames.size(); i++) {
+			if (i > 0)
+				code.write(", ");
+			code.write(globalNames[i]);
+		}
+		code.writeLine();
+	}
+}
+
+void Script::writeScriptText(CodeWriter &code, bool dotSyntax) const {
+	size_t origSize = code.size();
+	writeVarDeclarations(code);
+	if (isFactory()) {
+		if (code.size() != origSize) {
+			code.writeLine();
+		}
+		code.write("factory ");
+		code.writeLine(factoryName);
+	}
+	for (size_t i = 0; i < handlers.size(); i++) {
+		if ((!isFactory() || i > 0) && code.size() != origSize) {
+			code.writeLine();
+		}
+		handlers[i].ast.writeScriptText(code, dotSyntax, false);
+	}
+	for (auto factory : factories) {
+		if (code.size() != origSize) {
+			code.writeLine();
+		}
+		factory->writeScriptText(code, dotSyntax);
+	}
+}
+
+Common::String Script::scriptText(const char *lineEnding, bool dotSyntax) const {
+	CodeWriter code(lineEnding);
+	writeScriptText(code, dotSyntax);
+	return code.str();
+}
+
+void Script::writeBytecodeText(CodeWriter &code, bool dotSyntax) const {
+	size_t origSize = code.size();
+	writeVarDeclarations(code);
+	if (isFactory()) {
+		if (code.size() != origSize) {
+			code.writeLine();
+		}
+		code.write("factory ");
+		code.writeLine(factoryName);
+	}
+	for (size_t i = 0; i < handlers.size(); i++) {
+		if ((!isFactory() || i > 0) && code.size() != origSize) {
+			code.writeLine();
+		}
+		handlers[i].writeBytecodeText(code, dotSyntax);
+	}
+	for (auto factory : factories) {
+		if (code.size() != origSize) {
+			code.writeLine();
+		}
+		factory->writeBytecodeText(code, dotSyntax);
+	}
+}
+
+Common::String Script::bytecodeText(const char *lineEnding, bool dotSyntax) const {
+	CodeWriter code(lineEnding);
+	writeBytecodeText(code, dotSyntax);
+	return code.str();
+}
+
+bool Script::isFactory() const {
+	return (scriptFlags & LingoDec::kScriptFlagFactoryDef);
+}
+
+/* LiteralStore */
+
+void LiteralStore::readRecord(Common::SeekableReadStream &stream, int version) {
+	if (version >= 500)
+		type = static_cast<LiteralType>(stream.readUint32BE());
+	else
+		type = static_cast<LiteralType>(stream.readUint16BE());
+	offset = stream.readUint32BE();
+}
+
+void LiteralStore::readData(Common::SeekableReadStream &stream, uint32_t startOffset) {
+	if (type == kLiteralInt) {
+		value = Common::SharedPtr<LingoDec::Datum>(new LingoDec::Datum((int)offset));
+	} else {
+		stream.seek(startOffset + offset);
+		auto length = stream.readUint32BE();
+		if (type == kLiteralString) {
+			char *buf = new char[length];
+			stream.read(buf, length - 1);
+			buf[length - 1] = '\0';
+			value = Common::SharedPtr<LingoDec::Datum>(new LingoDec::Datum(LingoDec::kDatumString, Common::String(buf)));
+			delete[] buf;
+		} else if (type == kLiteralFloat) {
+			double floatVal = 0.0;
+			if (length == 8) {
+				floatVal = stream.readDoubleBE();
+			} else if (length == 10) {
+				uint8_t buf[10];
+				stream.read(buf, 10);
+				floatVal = readAppleFloat80(buf);
+			}
+			value = Common::SharedPtr<LingoDec::Datum>(new LingoDec::Datum(floatVal));
+		} else {
+			value = Common::SharedPtr<LingoDec::Datum>(new LingoDec::Datum());
+		}
+	}
+}
+
+} // namespace LingoDec
diff --git a/engines/director/lingo/lingodec/script.h b/engines/director/lingo/lingodec/script.h
new file mode 100644
index 00000000000..e53d53bf591
--- /dev/null
+++ b/engines/director/lingo/lingodec/script.h
@@ -0,0 +1,96 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef LINGODEC_SCRIPT_H
+#define LINGODEC_SCRIPT_H
+
+#include "common/ptr.h"
+#include "common/str.h"
+#include "./enums.h"
+
+namespace Common {
+class ReadStream;
+}
+
+namespace LingoDec {
+
+class CodeWriter;
+struct Datum;
+struct Handler;
+struct ScriptContext;
+
+/* LiteralStore */
+
+struct LiteralStore {
+	LiteralType type;
+	uint32_t offset;
+	Common::SharedPtr<Datum> value;
+
+	void readRecord(Common::SeekableReadStream &stream, int version);
+	void readData(Common::SeekableReadStream &stream, uint32_t startOffset);
+};
+
+/* Script */
+
+struct Script {
+	/*  8 */ uint32_t totalLength;
+	/* 12 */ uint32_t totalLength2;
+	/* 16 */ uint16_t headerLength;
+	/* 18 */ uint16_t scriptNumber;
+	/* 20 */ int16_t unk20;
+	/* 22 */ int16_t parentNumber;
+
+	/* 38 */ uint32_t scriptFlags;
+	/* 42 */ int16_t unk42;
+	/* 44 */ int32_t castID;
+	/* 48 */ int16_t factoryNameID;
+	/* 50 */ uint16_t handlerVectorsCount;
+	/* 52 */ uint32_t handlerVectorsOffset;
+	/* 56 */ uint32_t handlerVectorsSize;
+	/* 60 */ uint16_t propertiesCount;
+	/* 62 */ uint32_t propertiesOffset;
+	/* 66 */ uint16_t globalsCount;
+	/* 68 */ uint32_t globalsOffset;
+	/* 72 */ uint16_t handlersCount;
+	/* 74 */ uint32_t handlersOffset;
+	/* 78 */ uint16_t literalsCount;
+	/* 80 */ uint32_t literalsOffset;
+	/* 84 */ uint32_t literalsDataCount;
+	/* 88 */ uint32_t literalsDataOffset;
+
+	Common::Array<int16_t> propertyNameIDs;
+	Common::Array<int16_t> globalNameIDs;
+
+	Common::String factoryName;
+	Common::Array<Common::String> propertyNames;
+	Common::Array<Common::String> globalNames;
+	Common::Array<Handler> handlers;
+	Common::Array<LiteralStore> literals;
+	Common::Array<Script *> factories;
+
+	unsigned int version;
+	ScriptContext *context;
+
+	Script(unsigned int version);
+	~Script();
+	void read(Common::SeekableReadStream &stream);
+	Common::Array<int16_t> readVarnamesTable(Common::SeekableReadStream &stream, uint16_t count, uint32_t offset);
+	bool validName(int id) const;
+	Common::String getName(int id) const;
+	void setContext(ScriptContext *ctx);
+	void parse();
+	void writeVarDeclarations(CodeWriter &code) const;
+	void writeScriptText(CodeWriter &code, bool dotSyntax) const;
+	Common::String scriptText(const char *lineEnding, bool dotSyntax) const;
+	void writeBytecodeText(CodeWriter &code, bool dotSyntax) const;
+	Common::String bytecodeText(const char *lineEnding, bool dotSyntax) const;
+
+	bool isFactory() const;
+};
+
+} // namespace LingoDec
+
+#endif // LINGODEC_SCRIPT_H
diff --git a/engines/director/module.mk b/engines/director/module.mk
index 0019d1e0500..36d494a4411 100644
--- a/engines/director/module.mk
+++ b/engines/director/module.mk
@@ -53,6 +53,12 @@ MODULE_OBJS = \
 	lingo/lingo-preprocessor.o \
 	lingo/lingo-the.o \
 	lingo/lingo-utils.o \
+	lingo/lingodec/ast.o \
+	lingo/lingodec/context.o \
+	lingo/lingodec/codewriter.o \
+	lingo/lingodec/handler.o \
+	lingo/lingodec/names.o \
+	lingo/lingodec/script.o \
 	lingo/xlibs/aiff.o \
 	lingo/xlibs/applecdxobj.o \
 	lingo/xlibs/askuser.o \
diff --git a/engines/director/movie.cpp b/engines/director/movie.cpp
index 7d16b72f052..1e673070114 100644
--- a/engines/director/movie.cpp
+++ b/engines/director/movie.cpp
@@ -607,12 +607,23 @@ ScriptContext *Movie::getScriptContext(ScriptType type, CastMemberID id) {
 
 Symbol Movie::getHandler(const Common::String &name) {
 	for (auto &it : _casts) {
+		if (name == "waitSignal") {
+			for (auto &i : it._value->_lingoArchive->functionHandlers)
+				debug("%s", i._key.c_str());
+		}
+
 		if (it._value->_lingoArchive->functionHandlers.contains(name))
 			return it._value->_lingoArchive->functionHandlers[name];
 	}
 
-	if (_sharedCast && _sharedCast->_lingoArchive->functionHandlers.contains(name))
+	if (_sharedCast && _sharedCast->_lingoArchive->functionHandlers.contains(name)) {
+		if (name == "waitSignal") {
+			for (auto &i : _sharedCast->_lingoArchive->functionHandlers)
+				debug("sh: %s", i._key.c_str());
+		}
+
 		return _sharedCast->_lingoArchive->functionHandlers[name];
+	}
 
 	return Symbol();
 }
diff --git a/engines/director/util.cpp b/engines/director/util.cpp
index 1d43798e6c4..c301f2524c8 100644
--- a/engines/director/util.cpp
+++ b/engines/director/util.cpp
@@ -1651,6 +1651,8 @@ void DirectorEngine::delayMillis(uint32 delay) {
 	_system->delayMillis(delay);
 }
 
+} // End of namespace Director
+
 double readAppleFloat80(byte *ptr) {
 	// Floats in an "80 bit IEEE Standard 754 floating
 	// point number (Standard Apple Numeric Environment [SANE] data type
@@ -1661,5 +1663,3 @@ double readAppleFloat80(byte *ptr) {
 
 	return Common::XPFloat(signAndExponent, mantissa).toDouble(Common::XPFloat::kSemanticsSANE);
 }
-
-} // End of namespace Director
diff --git a/engines/director/util.h b/engines/director/util.h
index 59f4f8d8e91..82acc17127a 100644
--- a/engines/director/util.h
+++ b/engines/director/util.h
@@ -70,8 +70,6 @@ Common::Path dumpFactoryName(const char *prefix, const char *name, const char *e
 
 bool isButtonSprite(SpriteType spriteType);
 
-double readAppleFloat80(byte *ptr);
-
 class RandomState {
 public:
 	uint32 _seed;
@@ -139,4 +137,6 @@ inline void lerpPalette(byte *target, byte *palA, int palALength, byte *palB, in
 
 } // End of namespace Director
 
+double readAppleFloat80(byte *ptr);
+
 #endif




More information about the Scummvm-git-logs mailing list