diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e0313a4117b8bb03fa7a10acf2a0a1bd591b246c..a02f6f8d8f1b287707842273b042448faed0223b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -247,8 +247,6 @@ if(SRB2_CONFIG_ENABLE_WEBM_MOVIES)
 	target_compile_definitions(SRB2SDL2 PRIVATE -DSRB2_CONFIG_ENABLE_WEBM_MOVIES)
 endif()
 
-target_link_libraries(SRB2SDL2 PRIVATE acsvm)
-
 set(SRB2_HAVE_THREADS ON)
 target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_THREADS)
 
diff --git a/src/acs/CMakeLists.txt b/src/acs/CMakeLists.txt
index e684675af0b4da61f5ad870b3bcf3f57108beb56..fda2e72f9da6f1689b2fc98f2490fe13b2e65c18 100644
--- a/src/acs/CMakeLists.txt
+++ b/src/acs/CMakeLists.txt
@@ -10,3 +10,11 @@ target_sources(SRB2SDL2 PRIVATE
 	interface.cpp
 	interface.h
 )
+
+target_include_directories(SRB2SDL2 PRIVATE vm) # This sucks
+
+set(ACSVM_NOFLAGS ON)
+set(ACSVM_SHARED OFF)
+add_subdirectory(vm)
+
+target_link_libraries(SRB2SDL2 PRIVATE acsvm)
diff --git a/src/acs/acsvm.hpp b/src/acs/acsvm.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4ae2ee62425a4b44fcc55b615fec68a830c07591
--- /dev/null
+++ b/src/acs/acsvm.hpp
@@ -0,0 +1,48 @@
+// DR. ROBOTNIK'S RING RACERS
+//-----------------------------------------------------------------------------
+// Copyright (C) 2016 by James Haley, David Hill, et al. (Team Eternity)
+// Copyright (C) 2022 by Sally "TehRealSalt" Cochenour
+// Copyright (C) 2022 by Kart Krew
+//
+// This program is free software distributed under the
+// terms of the GNU General Public License, version 2.
+// See the 'LICENSE' file for more details.
+//-----------------------------------------------------------------------------
+/// \file  acsvm.hpp
+/// \brief ACSVM include file
+
+#ifndef __SRB2_ACSVM_HPP__
+#define __SRB2_ACSVM_HPP__
+
+#include <ACSVM/Action.hpp>
+#include <ACSVM/Array.hpp>
+#include <ACSVM/BinaryIO.hpp>
+#include <ACSVM/CallFunc.hpp>
+#include <ACSVM/Code.hpp>
+#include <ACSVM/CodeData.hpp>
+#include <ACSVM/CodeList.hpp>
+#include <ACSVM/Environment.hpp>
+#include <ACSVM/Error.hpp>
+#include <ACSVM/Function.hpp>
+#include <ACSVM/HashMap.hpp>
+#include <ACSVM/HashMapFixed.hpp>
+#include <ACSVM/ID.hpp>
+#include <ACSVM/Init.hpp>
+#include <ACSVM/Jump.hpp>
+#include <ACSVM/List.hpp>
+#include <ACSVM/Module.hpp>
+#include <ACSVM/PrintBuf.hpp>
+#include <ACSVM/Scope.hpp>
+#include <ACSVM/Script.hpp>
+#include <ACSVM/Serial.hpp>
+#include <ACSVM/Stack.hpp>
+#include <ACSVM/Store.hpp>
+#include <ACSVM/String.hpp>
+#include <ACSVM/Thread.hpp>
+#include <ACSVM/Tracer.hpp>
+#include <ACSVM/Types.hpp>
+#include <ACSVM/Vector.hpp>
+
+#include <Util/Floats.hpp>
+
+#endif //__SRB2_ACSVM_HPP__
diff --git a/src/acs/call-funcs.cpp b/src/acs/call-funcs.cpp
index c0e91be7643031125fc111ba947ec4e90f835d48..4cbdcdeccfa0495c1b1d4b969fa3ca4147d59056 100644
--- a/src/acs/call-funcs.cpp
+++ b/src/acs/call-funcs.cpp
@@ -14,16 +14,7 @@
 #include <algorithm>
 #include <cctype>
 
-#include <ACSVM/Code.hpp>
-#include <ACSVM/CodeData.hpp>
-#include <ACSVM/Environment.hpp>
-#include <ACSVM/Error.hpp>
-#include <ACSVM/Module.hpp>
-#include <ACSVM/Scope.hpp>
-#include <ACSVM/Script.hpp>
-#include <ACSVM/Serial.hpp>
-#include <ACSVM/Thread.hpp>
-#include <Util/Floats.hpp>
+#include "acsvm.hpp"
 
 extern "C" {
 #include "../doomtype.h"
diff --git a/src/acs/call-funcs.hpp b/src/acs/call-funcs.hpp
index dff479bd35c6c5be7129903ba24b5315dbf62412..b5136832fd9747e345fd0f765b9456b8269ac337 100644
--- a/src/acs/call-funcs.hpp
+++ b/src/acs/call-funcs.hpp
@@ -14,16 +14,7 @@
 #ifndef __SRB2_ACS_CALL_FUNCS_HPP__
 #define __SRB2_ACS_CALL_FUNCS_HPP__
 
-#include <ACSVM/Code.hpp>
-#include <ACSVM/CodeData.hpp>
-#include <ACSVM/Environment.hpp>
-#include <ACSVM/Error.hpp>
-#include <ACSVM/Module.hpp>
-#include <ACSVM/Scope.hpp>
-#include <ACSVM/Script.hpp>
-#include <ACSVM/Serial.hpp>
-#include <ACSVM/Thread.hpp>
-#include <Util/Floats.hpp>
+#include "acsvm.hpp"
 
 /*--------------------------------------------------
 	bool CallFunc_???(ACSVM::Thread *thread, const ACSVM::Word *argV, ACSVM::Word argC);
diff --git a/src/acs/environment.cpp b/src/acs/environment.cpp
index 38e74a17260a06e309941faa2f9b1fdcd8e37dc9..c51c222e11df8b012f139c0349e4fa8f38a0475d 100644
--- a/src/acs/environment.cpp
+++ b/src/acs/environment.cpp
@@ -14,16 +14,7 @@
 #include <algorithm>
 #include <vector>
 
-#include <ACSVM/Code.hpp>
-#include <ACSVM/CodeData.hpp>
-#include <ACSVM/Environment.hpp>
-#include <ACSVM/Error.hpp>
-#include <ACSVM/Module.hpp>
-#include <ACSVM/Scope.hpp>
-#include <ACSVM/Script.hpp>
-#include <ACSVM/Serial.hpp>
-#include <ACSVM/Thread.hpp>
-#include <Util/Floats.hpp>
+#include "acsvm.hpp"
 
 extern "C" {
 #include "../doomtype.h"
diff --git a/src/acs/environment.hpp b/src/acs/environment.hpp
index 5d080ad355098995c1f2eccd7e97b4eaf09cf656..fa6dbe03391054bf0c9d7c4a372ba413f18cdb47 100644
--- a/src/acs/environment.hpp
+++ b/src/acs/environment.hpp
@@ -14,16 +14,7 @@
 #ifndef __SRB2_ACS_ENVIRONMENT_HPP__
 #define __SRB2_ACS_ENVIRONMENT_HPP__
 
-#include <ACSVM/Code.hpp>
-#include <ACSVM/CodeData.hpp>
-#include <ACSVM/Environment.hpp>
-#include <ACSVM/Error.hpp>
-#include <ACSVM/Module.hpp>
-#include <ACSVM/Scope.hpp>
-#include <ACSVM/Script.hpp>
-#include <ACSVM/Serial.hpp>
-#include <ACSVM/Thread.hpp>
-#include <Util/Floats.hpp>
+#include "acsvm.hpp"
 
 namespace srb2::acs {
 
diff --git a/src/acs/interface.cpp b/src/acs/interface.cpp
index 864c1fa3f9a7b22754b3d7ea2c8af5a8f43a7b42..61b880961d5907a2995acd7afd1366fe7fbafd6c 100644
--- a/src/acs/interface.cpp
+++ b/src/acs/interface.cpp
@@ -16,17 +16,7 @@
 #include <ostream>
 #include <vector>
 
-#include <ACSVM/Action.hpp>
-#include <ACSVM/Code.hpp>
-#include <ACSVM/CodeData.hpp>
-#include <ACSVM/Environment.hpp>
-#include <ACSVM/Error.hpp>
-#include <ACSVM/Module.hpp>
-#include <ACSVM/Scope.hpp>
-#include <ACSVM/Script.hpp>
-#include <ACSVM/Serial.hpp>
-#include <ACSVM/Thread.hpp>
-#include <Util/Floats.hpp>
+#include "acsvm.hpp"
 
 extern "C" {
 #include "interface.h"
diff --git a/src/acs/stream.hpp b/src/acs/stream.hpp
index ebe908e1d689fb640e950ca5b2f6ea760469560d..456a72532435916a3a1248216e64ab6d2d3333dc 100644
--- a/src/acs/stream.hpp
+++ b/src/acs/stream.hpp
@@ -19,16 +19,7 @@
 
 #include <streambuf>
 
-#include <ACSVM/Code.hpp>
-#include <ACSVM/CodeData.hpp>
-#include <ACSVM/Environment.hpp>
-#include <ACSVM/Error.hpp>
-#include <ACSVM/Module.hpp>
-#include <ACSVM/Scope.hpp>
-#include <ACSVM/Script.hpp>
-#include <ACSVM/Serial.hpp>
-#include <ACSVM/Thread.hpp>
-#include <Util/Floats.hpp>
+#include "acsvm.hpp"
 
 extern "C" {
 #include "../doomtype.h"
diff --git a/src/acs/thread.cpp b/src/acs/thread.cpp
index 5ebc7234ccacbdd999636f892d768c8d60dcf9d6..88494e5b30c38a9c1591ff55ec1221fc7d8cc732 100644
--- a/src/acs/thread.cpp
+++ b/src/acs/thread.cpp
@@ -11,17 +11,7 @@
 /// \file  thread.cpp
 /// \brief Action Code Script: Thread definition
 
-#include <ACSVM/Code.hpp>
-#include <ACSVM/CodeData.hpp>
-#include <ACSVM/Environment.hpp>
-#include <ACSVM/Error.hpp>
-#include <ACSVM/Module.hpp>
-#include <ACSVM/Scope.hpp>
-#include <ACSVM/Script.hpp>
-#include <ACSVM/Serial.hpp>
-#include <ACSVM/Thread.hpp>
-#include <ACSVM/BinaryIO.hpp>
-#include <Util/Floats.hpp>
+#include "acsvm.hpp"
 
 #include "thread.hpp"
 
diff --git a/src/acs/thread.hpp b/src/acs/thread.hpp
index eff31eb29a5f01170c87518bb7e411e02841f04a..5535ac556a6be39fc4466a95f7e1dd2c0448ccb8 100644
--- a/src/acs/thread.hpp
+++ b/src/acs/thread.hpp
@@ -14,16 +14,7 @@
 #ifndef __SRB2_ACS_THREAD_HPP__
 #define __SRB2_ACS_THREAD_HPP__
 
-#include <ACSVM/Code.hpp>
-#include <ACSVM/CodeData.hpp>
-#include <ACSVM/Environment.hpp>
-#include <ACSVM/Error.hpp>
-#include <ACSVM/Module.hpp>
-#include <ACSVM/Scope.hpp>
-#include <ACSVM/Script.hpp>
-#include <ACSVM/Serial.hpp>
-#include <ACSVM/Thread.hpp>
-#include <Util/Floats.hpp>
+#include "acsvm.hpp"
 
 extern "C" {
 #include "../doomtype.h"
diff --git a/src/acs/vm/ACSVM/Action.cpp b/src/acs/vm/ACSVM/Action.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d6aa857ba833c1373239a3f856bef4770feb0648
--- /dev/null
+++ b/src/acs/vm/ACSVM/Action.cpp
@@ -0,0 +1,90 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Deferred Action classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Action.hpp"
+
+#include "Environment.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // ScriptAction move constructor
+   //
+   ScriptAction::ScriptAction(ScriptAction &&a) :
+      action{std::move(a.action)},
+      argV  {std::move(a.argV)},
+      id    {std::move(a.id)},
+      link  {this, std::move(a.link)},
+      name  {std::move(a.name)}
+   {
+   }
+
+   //
+   // ScriptAction constructor
+   //
+   ScriptAction::ScriptAction(ScopeID id_, ScriptName name_, Action action_, Vector<Word> &&argV_) :
+      action{action_},
+      argV  {std::move(argV_)},
+      id    {id_},
+      link  {this},
+      name  {name_}
+   {
+   }
+
+   //
+   // ScriptAction destructor
+   //
+   ScriptAction::~ScriptAction()
+   {
+   }
+
+   //
+   // ScriptAction::lockStrings
+   //
+   void ScriptAction::lockStrings(Environment *env) const
+   {
+      if(name.s) ++name.s->lock;
+
+      for(auto &arg : argV)
+         ++env->getString(arg)->lock;
+   }
+
+   //
+   // ScriptAction::refStrings
+   //
+   void ScriptAction::refStrings(Environment *env) const
+   {
+      if(name.s) name.s->ref = true;
+
+      for(auto &arg : argV)
+         env->getString(arg)->ref = true;
+   }
+
+   //
+   // ScriptAction::unlockStrings
+   //
+   void ScriptAction::unlockStrings(Environment *env) const
+   {
+      if(name.s) --name.s->lock;
+
+      for(auto &arg : argV)
+         --env->getString(arg)->lock;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Action.hpp b/src/acs/vm/ACSVM/Action.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a2cef539418fd8e60c7dc8b98155da7b600382cc
--- /dev/null
+++ b/src/acs/vm/ACSVM/Action.hpp
@@ -0,0 +1,83 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Deferred Action classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Action_H__
+#define ACSVM__Action_H__
+
+#include "List.hpp"
+#include "Script.hpp"
+#include "Vector.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ScopeID
+   //
+   class ScopeID
+   {
+   public:
+      ScopeID() = default;
+      ScopeID(Word global_, Word hub_, Word map_) :
+         global{global_}, hub{hub_}, map{map_} {}
+
+      bool operator == (ScopeID const &id) const
+         {return global == id.global && hub == id.hub && map == id.map;}
+      bool operator != (ScopeID const &id) const
+         {return global != id.global || hub != id.hub || map != id.map;}
+
+      Word global;
+      Word hub;
+      Word map;
+   };
+
+   //
+   // ScriptAction
+   //
+   // Represents a deferred Script action.
+   //
+   class ScriptAction
+   {
+   public:
+      enum Action
+      {
+         Start,
+         StartForced,
+         Stop,
+         Pause,
+      };
+
+
+      ScriptAction(ScriptAction &&action);
+      ScriptAction(ScopeID id, ScriptName name, Action action, Vector<Word> &&argV);
+      ~ScriptAction();
+
+      void lockStrings(Environment *env) const;
+
+      void refStrings(Environment *env) const;
+
+      void unlockStrings(Environment *env) const;
+
+      Action                 action;
+      Vector<Word>           argV;
+      ScopeID                id;
+      ListLink<ScriptAction> link;
+      ScriptName             name;
+   };
+}
+
+#endif//ACSVM__Action_H__
+
diff --git a/src/acs/vm/ACSVM/Array.cpp b/src/acs/vm/ACSVM/Array.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..510f847269482145f2d12f9d6107a83a20687196
--- /dev/null
+++ b/src/acs/vm/ACSVM/Array.cpp
@@ -0,0 +1,214 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Array class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Array.hpp"
+
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "Serial.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Static Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // FreeData (Word)
+   //
+   static void FreeData(Word &)
+   {
+   }
+
+   //
+   // FreeData
+   //
+   template<typename T>
+   static void FreeData(T *&data)
+   {
+      if(!data) return;
+
+      for(auto &itr : *data)
+         FreeData(itr);
+
+      delete[] data;
+      data = nullptr;
+   }
+
+   //
+   // ReadData (Word)
+   //
+   static void ReadData(std::istream &in, Word &out)
+   {
+      out = ReadVLN<Word>(in);
+   }
+
+   //
+   // ReadData
+   //
+   template<typename T>
+   static void ReadData(std::istream &in, T *&out)
+   {
+      if(in.get())
+      {
+         if(!out) out = new T[1]{};
+
+         for(auto &itr : *out)
+            ReadData(in, itr);
+      }
+      else
+         FreeData(out);
+   }
+
+   //
+   // RefStringsData (Word)
+   //
+   static void RefStringsData(Environment *env, Word const &data, void (*ref)(String *))
+   {
+      ref(env->getString(data));
+   }
+
+   //
+   // RefStringsData
+   //
+   template<typename T>
+   static void RefStringsData(Environment *env, T *data, void (*ref)(String *))
+   {
+      if(data) for(auto &itr : *data)
+         RefStringsData(env, itr, ref);
+   }
+
+   //
+   // WriteData (Word)
+   //
+   static void WriteData(std::ostream &out, Word const &in)
+   {
+      WriteVLN(out, in);
+   }
+
+   //
+   // WriteData
+   //
+   template<typename T>
+   static void WriteData(std::ostream &out, T *const &in)
+   {
+      if(in)
+      {
+         out.put('\1');
+
+         for(auto &itr : *in)
+            WriteData(out, itr);
+      }
+      else
+         out.put('\0');
+   }
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Array::operator [Word]
+   //
+   Word &Array::operator [] (Word idx)
+   {
+      if(!data) data = new Data[1]{};
+      Bank *&bank = (*data)[idx / (BankSize * SegmSize * PageSize)];
+
+      if(!bank) bank = new Bank[1]{};
+      Segm *&segm = (*bank)[idx / (SegmSize * PageSize) % BankSize];
+
+      if(!segm) segm = new Segm[1]{};
+      Page *&page = (*segm)[idx / PageSize % SegmSize];
+
+      if(!page) page = new Page[1]{};
+      return (*page)[idx % PageSize];
+   }
+
+   //
+   // Array::find
+   //
+   Word Array::find(Word idx) const
+   {
+      if(!data) return 0;
+      Bank *&bank = (*data)[idx / (BankSize * SegmSize * PageSize)];
+
+      if(!bank) return 0;
+      Segm *&segm = (*bank)[idx / (SegmSize * PageSize) % BankSize];
+
+      if(!segm) return 0;
+      Page *&page = (*segm)[idx / PageSize % SegmSize];
+
+      if(!page) return 0;
+      return (*page)[idx % PageSize];
+   }
+
+   //
+   // Array::clear
+   //
+   void Array::clear()
+   {
+      FreeData(data);
+   }
+
+   //
+   // Array::loadState
+   //
+   void Array::loadState(Serial &in)
+   {
+      in.readSign(Signature::Array);
+      ReadData(in, data);
+      in.readSign(~Signature::Array);
+   }
+
+   //
+   // Array::lockStrings
+   //
+   void Array::lockStrings(Environment *env) const
+   {
+      RefStringsData(env, data, [](String *s){++s->lock;});
+   }
+
+   //
+   // Array::refStrings
+   //
+   void Array::refStrings(Environment *env) const
+   {
+      RefStringsData(env, data, [](String *s){s->ref = true;});
+   }
+
+   //
+   // Array::saveState
+   //
+   void Array::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::Array);
+      WriteData(out, data);
+      out.writeSign(~Signature::Array);
+   }
+
+   //
+   // Array::unlockStrings
+   //
+   void Array::unlockStrings(Environment *env) const
+   {
+      RefStringsData(env, data, [](String *s){--s->lock;});
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Array.hpp b/src/acs/vm/ACSVM/Array.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a71f3b58a0d79418a5f24db08fdf36a4808d6fae
--- /dev/null
+++ b/src/acs/vm/ACSVM/Array.hpp
@@ -0,0 +1,71 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Array class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Array_H__
+#define ACSVM__Array_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Array
+   //
+   // Sparse-allocation array of 2**32 Words.
+   //
+   class Array
+   {
+   public:
+      Array() : data{nullptr} {}
+      Array(Array const &) = delete;
+      Array(Array &&array) : data{array.data} {array.data = nullptr;}
+      ~Array() {clear();}
+
+      Word &operator [] (Word idx);
+
+      void clear();
+
+      // If idx is allocated, returns that Word. Otherwise, returns 0.
+      Word find(Word idx) const;
+
+      void loadState(Serial &in);
+
+      void lockStrings(Environment *env) const;
+
+      void refStrings(Environment *env) const;
+
+      void saveState(Serial &out) const;
+
+      void unlockStrings(Environment *env) const;
+
+   private:
+      static constexpr std::size_t PageSize = 256;
+      static constexpr std::size_t SegmSize = 256;
+      static constexpr std::size_t BankSize = 256;
+      static constexpr std::size_t DataSize = 256;
+
+      using Page = Word [PageSize];
+      using Segm = Page*[SegmSize];
+      using Bank = Segm*[BankSize];
+      using Data = Bank*[DataSize];
+
+      Data *data;
+   };
+}
+
+#endif//ACSVM__Array_H__
+
diff --git a/src/acs/vm/ACSVM/BinaryIO.cpp b/src/acs/vm/ACSVM/BinaryIO.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..be229bad7550b1f13a30623421ced532412068c0
--- /dev/null
+++ b/src/acs/vm/ACSVM/BinaryIO.cpp
@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Binary data reading/writing primitives.
+//
+//-----------------------------------------------------------------------------
+
+#include "BinaryIO.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // ReadLE4
+   //
+   std::uint_fast32_t ReadLE4(std::istream &in)
+   {
+      Byte buf[4];
+      for(auto &b : buf) b = in.get();
+      return ReadLE4(buf);
+   }
+
+   //
+   // WriteLE4
+   //
+   void WriteLE4(std::ostream &out, std::uint_fast32_t in)
+   {
+      Byte buf[4];
+      WriteLE4(buf, in);
+      for(auto b : buf) out.put(b);
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/BinaryIO.hpp b/src/acs/vm/ACSVM/BinaryIO.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..56aeea84b07d91c2b793733095c3748d466b859d
--- /dev/null
+++ b/src/acs/vm/ACSVM/BinaryIO.hpp
@@ -0,0 +1,118 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Binary data reading/writing primitives.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__BinaryIO_H__
+#define ACSVM__BinaryIO_H__
+
+#include "Types.hpp"
+
+#include <istream>
+#include <ostream>
+#include <climits>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   std::uint_fast8_t  ReadLE1(Byte const *data);
+   std::uint_fast16_t ReadLE2(Byte const *data);
+   std::uint_fast32_t ReadLE4(Byte const *data);
+   std::uint_fast32_t ReadLE4(std::istream &in);
+
+   template<typename T>
+   T ReadVLN(std::istream &in);
+
+   void WriteLE4(Byte *out, std::uint_fast32_t in);
+   void WriteLE4(std::ostream &out, std::uint_fast32_t in);
+
+   template<typename T>
+   void WriteVLN(std::ostream &out, T in);
+
+   //
+   // ReadLE1
+   //
+   inline std::uint_fast8_t ReadLE1(Byte const *data)
+   {
+      return static_cast<std::uint_fast8_t>(data[0]);
+   }
+
+   //
+   // ReadLE2
+   //
+   inline std::uint_fast16_t ReadLE2(Byte const *data)
+   {
+      return
+         (static_cast<std::uint_fast16_t>(data[0]) << 0) |
+         (static_cast<std::uint_fast16_t>(data[1]) << 8);
+   }
+
+   //
+   // ReadLE4
+   //
+   inline std::uint_fast32_t ReadLE4(Byte const *data)
+   {
+      return
+         (static_cast<std::uint_fast32_t>(data[0]) <<  0) |
+         (static_cast<std::uint_fast32_t>(data[1]) <<  8) |
+         (static_cast<std::uint_fast32_t>(data[2]) << 16) |
+         (static_cast<std::uint_fast32_t>(data[3]) << 24);
+   }
+
+   //
+   // ReadVLN
+   //
+   template<typename T>
+   T ReadVLN(std::istream &in)
+   {
+      T out{0};
+
+      unsigned char c;
+      while(((c = in.get()) & 0x80) && in)
+         out = (out << 7) + (c & 0x7F);
+      out = (out << 7) + c;
+
+      return out;
+   }
+
+   //
+   // WriteLE4
+   //
+   inline void WriteLE4(Byte *out, std::uint_fast32_t in)
+   {
+      out[0] = (in >>  0) & 0xFF;
+      out[1] = (in >>  8) & 0xFF;
+      out[2] = (in >> 16) & 0xFF;
+      out[3] = (in >> 24) & 0xFF;
+   }
+
+   //
+   // WriteVLN
+   //
+   template<typename T>
+   void WriteVLN(std::ostream &out, T in)
+   {
+      constexpr std::size_t len = (sizeof(T) * CHAR_BIT + 6) / 7;
+      char buf[len], *ptr = buf + len;
+
+      *--ptr = static_cast<char>(in & 0x7F);
+      while((in >>= 7))
+         *--ptr = static_cast<char>(in & 0x7F) | 0x80;
+
+      out.write(ptr, (buf + len) - ptr);
+   }
+}
+
+#endif//ACSVM__BinaryIO_H__
+
diff --git a/src/acs/vm/ACSVM/CMakeLists.txt b/src/acs/vm/ACSVM/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..29760733fb97ed9ff32d6a74f6c947f1a8500678
--- /dev/null
+++ b/src/acs/vm/ACSVM/CMakeLists.txt
@@ -0,0 +1,82 @@
+##-----------------------------------------------------------------------------
+##
+## Copyright (C) 2015-2017 David Hill
+##
+## See COPYING for license information.
+##
+##-----------------------------------------------------------------------------
+##
+## CMake file for acsvm.
+##
+##-----------------------------------------------------------------------------
+
+
+##----------------------------------------------------------------------------|
+## Environment Configuration                                                  |
+##
+
+include_directories(.)
+
+
+##----------------------------------------------------------------------------|
+## Targets                                                                    |
+##
+
+##
+## acsvm
+##
+add_library(acsvm ${ACSVM_SHARED_DECL}
+   Action.cpp
+   Action.hpp
+   Array.cpp
+   Array.hpp
+   BinaryIO.cpp
+   BinaryIO.hpp
+   CallFunc.cpp
+   CallFunc.hpp
+   Code.hpp
+   CodeData.cpp
+   CodeData.hpp
+   CodeList.hpp
+   Environment.cpp
+   Environment.hpp
+   Error.cpp
+   Error.hpp
+   Function.cpp
+   Function.hpp
+   HashMap.hpp
+   HashMapFixed.hpp
+   ID.hpp
+   Init.cpp
+   Init.hpp
+   Jump.cpp
+   Jump.hpp
+   Module.cpp
+   Module.hpp
+   ModuleACS0.cpp
+   ModuleACSE.cpp
+   PrintBuf.cpp
+   PrintBuf.hpp
+   Scope.cpp
+   Scope.hpp
+   Script.cpp
+   Script.hpp
+   Serial.cpp
+   Serial.hpp
+   Stack.hpp
+   Store.hpp
+   String.cpp
+   String.hpp
+   Thread.cpp
+   Thread.hpp
+   ThreadExec.cpp
+   Tracer.cpp
+   Tracer.hpp
+   Types.hpp
+   Vector.hpp
+)
+
+ACSVM_INSTALL_LIB(acsvm)
+
+## EOF
+
diff --git a/src/acs/vm/ACSVM/CallFunc.cpp b/src/acs/vm/ACSVM/CallFunc.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..142865e2da71c62b5f8c86985e20cc4e398ea288
--- /dev/null
+++ b/src/acs/vm/ACSVM/CallFunc.cpp
@@ -0,0 +1,480 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Internal CallFunc functions.
+//
+//-----------------------------------------------------------------------------
+
+#include "CallFunc.hpp"
+
+#include "Action.hpp"
+#include "Code.hpp"
+#include "Environment.hpp"
+#include "Module.hpp"
+#include "Scope.hpp"
+#include "Thread.hpp"
+
+#include <cctype>
+#include <cinttypes>
+
+
+//----------------------------------------------------------------------------|
+// Static Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // PrintArray
+   //
+   static void PrintArray(Thread *thread, Word const *argv, Word argc, Array const &arr)
+   {
+      Word idx = argv[0] + (argc > 2 ? argv[2] : 0);
+      Word len = argc > 3 ? argv[3] : -1;
+
+      thread->env->printArray(thread->printBuf, arr, idx, len);
+   }
+
+   //
+   // StrCaseCmp
+   //
+   static int StrCaseCmp(String *l, String *r, Word n)
+   {
+      for(char const *ls = l->str, *rs = r->str;; ++ls, ++rs)
+      {
+         char lc = std::toupper(*ls), rc = std::toupper(*rs);
+         if(lc != rc) return lc < rc ? -1 : 1;
+         if(!lc || !n--) return 0;
+      }
+   }
+
+   //
+   // StrCmp
+   //
+   static int StrCmp(String *l, String *r, Word n)
+   {
+      for(char const *ls = l->str, *rs = r->str;; ++ls, ++rs)
+      {
+         char lc = *ls, rc = *rs;
+         if(lc != rc) return lc < rc ? -1 : 1;
+         if(!lc || !n--) return 0;
+      }
+   }
+
+   //
+   // StrCpyArray
+   //
+   static bool StrCpyArray(Thread *thread, Word const *argv, Array &dst)
+   {
+      Word    dstOff = argv[0] + argv[2];
+      Word    dstLen = argv[3];
+      String *src = thread->scopeMap->getString(argv[4]);
+      Word    srcIdx = argv[5];
+
+      if(srcIdx > src->len) return false;
+
+      for(Word dstIdx = dstOff;;)
+      {
+         if(dstIdx - dstOff == dstLen) return false;
+         if(!(dst[dstIdx++] = src->str[srcIdx++])) return true;
+      }
+   }
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // void Nop()
+   //
+   bool CallFunc_Func_Nop(Thread *, Word const *, Word)
+   {
+      return false;
+   }
+
+   //
+   // [[noreturn]] void Kill()
+   //
+   bool CallFunc_Func_Kill(Thread *thread, Word const *, Word)
+   {
+      thread->env->printKill(thread, static_cast<Word>(KillType::UnknownFunc), 0);
+      thread->state = ThreadState::Stopped;
+      return true;
+   }
+
+   //======================================================
+   // Printing Functions
+   //
+
+   //
+   // void PrintChar(char c)
+   //
+   bool CallFunc_Func_PrintChar(Thread *thread, Word const *argv, Word)
+   {
+      thread->printBuf.reserve(1);
+      thread->printBuf.put(static_cast<char>(argv[0]));
+      return false;
+   }
+
+   //
+   // str PrintEndStr()
+   //
+   bool CallFunc_Func_PrintEndStr(Thread *thread, Word const *, Word)
+   {
+      char const *data = thread->printBuf.data();
+      std::size_t size = thread->printBuf.size();
+      String     *str  = thread->env->getString(data, size);
+      thread->printBuf.drop();
+      thread->dataStk.push(~str->idx);
+      return false;
+   }
+
+   //
+   // void PrintFixD(fixed d)
+   //
+   bool CallFunc_Func_PrintFixD(Thread *thread, Word const *argv, Word)
+   {
+      // %E worst case: -3.276800e+04 == 13
+      // %F worst case: -32767.999985 == 13
+      // %G worst case: -1.52588e-05  == 12
+      // %G should be maximally P+6 + extra exponent digits.
+      thread->printBuf.reserve(12);
+      thread->printBuf.format("%G", static_cast<std::int32_t>(argv[0]) / 65536.0);
+      return false;
+   }
+
+   //
+   // void PrintGblArr(int idx, int arr, int off = 0, int len = -1)
+   //
+   bool CallFunc_Func_PrintGblArr(Thread *thread, Word const *argv, Word argc)
+   {
+      PrintArray(thread, argv, argc, thread->scopeGbl->arrV[argv[1]]);
+      return false;
+   }
+
+   //
+   // void PrintHubArr(int idx, int arr, int off = 0, int len = -1)
+   //
+   bool CallFunc_Func_PrintHubArr(Thread *thread, Word const *argv, Word argc)
+   {
+      PrintArray(thread, argv, argc, thread->scopeHub->arrV[argv[1]]);
+      return false;
+   }
+
+   //
+   // void PrintIntB(int b)
+   //
+   bool CallFunc_Func_PrintIntB(Thread *thread, Word const *argv, Word)
+   {
+      // %B worst case: 11111111111111111111111111111111 == 32
+      char buf[32], *end = buf+32, *itr = end;
+      for(Word b = argv[0]; b; b >>= 1) *--itr = '0' + (b & 1);
+      thread->printBuf.reserve(end - itr);
+      thread->printBuf.put(itr, end - itr);
+      return false;
+   }
+
+   //
+   // void PrintIntD(int d)
+   //
+   bool CallFunc_Func_PrintIntD(Thread *thread, Word const *argv, Word)
+   {
+      // %d worst case: -2147483648 == 11
+      thread->printBuf.reserve(11);
+      thread->printBuf.format("%" PRId32, static_cast<std::int32_t>(argv[0]));
+      return false;
+   }
+
+   //
+   // void PrintIntX(int x)
+   //
+   bool CallFunc_Func_PrintIntX(Thread *thread, Word const *argv, Word)
+   {
+      // %d worst case: FFFFFFFF == 8
+      thread->printBuf.reserve(8);
+      thread->printBuf.format("%" PRIX32, static_cast<std::uint32_t>(argv[0]));
+      return false;
+   }
+
+   //
+   // void PrintLocArr(int idx, int arr, int off = 0, int len = -1)
+   //
+   bool CallFunc_Func_PrintLocArr(Thread *thread, Word const *argv, Word argc)
+   {
+      PrintArray(thread, argv, argc, thread->localArr[argv[1]]);
+      return false;
+   }
+
+   //
+   // void PrintModArr(int idx, int arr, int off = 0, int len = -1)
+   //
+   bool CallFunc_Func_PrintModArr(Thread *thread, Word const *argv, Word argc)
+   {
+      PrintArray(thread, argv, argc, *thread->scopeMod->arrV[argv[1]]);
+      return false;
+   }
+
+   //
+   // void PrintPush()
+   //
+   bool CallFunc_Func_PrintPush(Thread *thread, Word const *, Word)
+   {
+      thread->printBuf.push();
+      return false;
+   }
+
+   //
+   // void PrintString(str s)
+   //
+   bool CallFunc_Func_PrintString(Thread *thread, Word const *argv, Word)
+   {
+      String *s = thread->scopeMap->getString(argv[0]);
+      thread->printBuf.reserve(s->len0);
+      thread->printBuf.put(s->str, s->len0);
+      return false;
+   }
+
+   //======================================================
+   // Script Functions
+   //
+
+   //
+   // int ScrPauseS(str name, int map)
+   //
+   bool CallFunc_Func_ScrPauseS(Thread *thread, Word const *argV, Word)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+      ScopeID scope{thread->scopeGbl->id, thread->scopeHub->id, argV[1]};
+      if(!scope.map) scope.map = thread->scopeMap->id;
+
+      thread->dataStk.push(thread->scopeMap->scriptPause(name, scope));
+      return false;
+   }
+
+   //
+   // int ScrStartS(str name, int map, ...)
+   //
+   bool CallFunc_Func_ScrStartS(Thread *thread, Word const *argV, Word argC)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+      ScopeID scope{thread->scopeGbl->id, thread->scopeHub->id, argV[1]};
+      if(!scope.map) scope.map = thread->scopeMap->id;
+
+      thread->dataStk.push(thread->scopeMap->scriptStart(name, scope, {argV+2, argC-2}));
+      return false;
+   }
+
+   //
+   // int ScrStartSD(str name, int map, int arg0, int arg1, int lock)
+   //
+   bool CallFunc_Func_ScrStartSD(Thread *thread, Word const *argV, Word)
+   {
+      if(!thread->env->checkLock(thread, argV[4], true))
+      {
+         thread->dataStk.push(0);
+         return false;
+      }
+
+      return CallFunc_Func_ScrStartS(thread, argV, 4);
+   }
+
+   //
+   // int ScrStartSF(str name, int map, ...)
+   //
+   bool CallFunc_Func_ScrStartSF(Thread *thread, Word const *argV, Word argC)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+      ScopeID scope{thread->scopeGbl->id, thread->scopeHub->id, argV[1]};
+      if(!scope.map) scope.map = thread->scopeMap->id;
+
+      thread->dataStk.push(thread->scopeMap->scriptStartForced(name, scope, {argV+2, argC-2}));
+      return false;
+   }
+
+   //
+   // int ScrStartSL(str name, int map, int arg0, int arg1, int lock)
+   //
+   bool CallFunc_Func_ScrStartSL(Thread *thread, Word const *argV, Word)
+   {
+      if(!thread->env->checkLock(thread, argV[4], false))
+      {
+         thread->dataStk.push(0);
+         return false;
+      }
+
+      return CallFunc_Func_ScrStartS(thread, argV, 4);
+   }
+
+   //
+   // int ScrStartSR(str name, ...)
+   //
+   bool CallFunc_Func_ScrStartSR(Thread *thread, Word const *argV, Word argC)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+
+      thread->dataStk.push(thread->scopeMap->scriptStartResult(name, {argV+1, argC-1}));
+      return false;
+   }
+
+   //
+   // int ScrStopS(str name, int map)
+   //
+   bool CallFunc_Func_ScrStopS(Thread *thread, Word const *argV, Word)
+   {
+      String *name = thread->scopeMap->getString(argV[0]);
+      ScopeID scope{thread->scopeGbl->id, thread->scopeHub->id, argV[1]};
+      if(!scope.map) scope.map = thread->scopeMap->id;
+
+      thread->dataStk.push(thread->scopeMap->scriptStop(name, scope));
+      return false;
+   }
+
+   //======================================================
+   // String Functions
+   //
+
+   //
+   // int GetChar(str s, int i)
+   //
+   bool CallFunc_Func_GetChar(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(thread->scopeMap->getString(argv[0])->get(argv[1]));
+      return false;
+   }
+
+   //
+   // int StrCaseCmp(str l, str r, int n = -1)
+   //
+   bool CallFunc_Func_StrCaseCmp(Thread *thread, Word const *argv, Word argc)
+   {
+      String *l = thread->scopeMap->getString(argv[0]);
+      String *r = thread->scopeMap->getString(argv[1]);
+      Word    n = argc > 2 ? argv[2] : -1;
+
+      thread->dataStk.push(StrCaseCmp(l, r, n));
+      return false;
+   }
+
+   //
+   // int StrCmp(str l, str r, int n = -1)
+   //
+   bool CallFunc_Func_StrCmp(Thread *thread, Word const *argv, Word argc)
+   {
+      String *l = thread->scopeMap->getString(argv[0]);
+      String *r = thread->scopeMap->getString(argv[1]);
+      Word    n = argc > 2 ? argv[2] : -1;
+
+      thread->dataStk.push(StrCmp(l, r, n));
+      return false;
+   }
+
+   //
+   // int StrCpyGblArr(int idx, int dst, int dstOff, int dstLen, str src, int srcOff)
+   //
+   bool CallFunc_Func_StrCpyGblArr(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(StrCpyArray(thread, argv, thread->scopeGbl->arrV[argv[1]]));
+      return false;
+   }
+
+   //
+   // int StrCpyHubArr(int idx, int dst, int dstOff, int dstLen, str src, int srcOff)
+   //
+   bool CallFunc_Func_StrCpyHubArr(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(StrCpyArray(thread, argv, thread->scopeHub->arrV[argv[1]]));
+      return false;
+   }
+
+   //
+   // int StrCpyLocArr(int idx, int dst, int dstOff, int dstLen, str src, int srcOff)
+   //
+   bool CallFunc_Func_StrCpyLocArr(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(StrCpyArray(thread, argv, thread->localArr[argv[1]]));
+      return false;
+   }
+
+   //
+   // int StrCpyModArr(int idx, int dst, int dstOff, int dstLen, str src, int srcOff)
+   //
+   bool CallFunc_Func_StrCpyModArr(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(StrCpyArray(thread, argv, *thread->scopeMod->arrV[argv[1]]));
+      return false;
+   }
+
+   //
+   // str StrLeft(str s, int len)
+   //
+   bool CallFunc_Func_StrLeft(Thread *thread, Word const *argv, Word)
+   {
+      String *str = thread->scopeMap->getString(argv[0]);
+      Word    len = argv[1];
+
+      if(len < str->len)
+         str = thread->env->getString(str->str, len);
+
+      thread->dataStk.push(~str->idx);
+      return false;
+   }
+
+   //
+   // int StrLen(str s)
+   //
+   bool CallFunc_Func_StrLen(Thread *thread, Word const *argv, Word)
+   {
+      thread->dataStk.push(thread->scopeMap->getString(argv[0])->len0);
+      return false;
+   }
+
+   //
+   // str StrMid(str s, int idx, int len)
+   //
+   bool CallFunc_Func_StrMid(Thread *thread, Word const *argv, Word)
+   {
+      String *str = thread->scopeMap->getString(argv[0]);
+      Word    idx = argv[1];
+      Word    len = argv[2];
+
+      if(idx < str->len)
+      {
+         if(len < str->len - idx)
+            str = thread->env->getString(str->str + idx, len);
+         else
+            str = thread->env->getString(str->str + idx, str->len - idx);
+      }
+      else
+         str = thread->env->getString("", static_cast<std::size_t>(0));
+
+      thread->dataStk.push(~str->idx);
+      return false;
+   }
+
+   //
+   // str StrRight(str s, int len)
+   //
+   bool CallFunc_Func_StrRight(Thread *thread, Word const *argv, Word)
+   {
+      String *str = thread->scopeMap->getString(argv[0]);
+      Word    len = argv[1];
+
+      if(len < str->len)
+         str = thread->env->getString(str->str + str->len - len, len);
+
+      thread->dataStk.push(~str->idx);
+      return false;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/CallFunc.hpp b/src/acs/vm/ACSVM/CallFunc.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8813e85462a93546ae1f327d77df1b4e528160d4
--- /dev/null
+++ b/src/acs/vm/ACSVM/CallFunc.hpp
@@ -0,0 +1,31 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Internal CallFunc functions.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__CallFunc_H__
+#define ACSVM__CallFunc_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   #define ACSVM_FuncList(name) \
+      bool (CallFunc_Func_##name)(Thread *thread, Word const *argv, Word argc);
+   #include "CodeList.hpp"
+}
+
+#endif//ACSVM__CallFunc_H__
+
diff --git a/src/acs/vm/ACSVM/Code.hpp b/src/acs/vm/ACSVM/Code.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a3b16cc7617eda1f06d8fbf272046bf7d1852082
--- /dev/null
+++ b/src/acs/vm/ACSVM/Code.hpp
@@ -0,0 +1,91 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Code classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Code_H__
+#define ACSVM__Code_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Code
+   //
+   // Internal codes.
+   //
+   enum class Code
+   {
+      #define ACSVM_CodeList(name, ...) name,
+      #include "CodeList.hpp"
+
+      None
+   };
+
+   //
+   // CodeACS0
+   //
+   // ACS0 codes.
+   //
+   enum class CodeACS0
+   {
+      #define ACSVM_CodeListACS0(name, idx, ...) name = idx,
+      #include "CodeList.hpp"
+
+      None
+   };
+
+   //
+   // Func
+   //
+   // Internal CallFunc indexes.
+   //
+   enum class Func
+   {
+      #define ACSVM_FuncList(name) name,
+      #include "CodeList.hpp"
+
+      None
+   };
+
+   //
+   // FuncACS0
+   //
+   // ACS0 CallFunc indexes.
+   //
+   enum class FuncACS0
+   {
+      #define ACSVM_FuncListACS0(name, idx, ...) name = idx,
+      #include "CodeList.hpp"
+
+      None
+   };
+
+   //
+   // KillType
+   //
+   enum class KillType
+   {
+      None,
+      OutOfBounds,
+      UnknownCode,
+      UnknownFunc,
+      BranchLimit,
+   };
+}
+
+#endif//ACSVM__Code_H__
+
diff --git a/src/acs/vm/ACSVM/CodeData.cpp b/src/acs/vm/ACSVM/CodeData.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5a05ed4ae07b657374cada59e5f93feb18204a42
--- /dev/null
+++ b/src/acs/vm/ACSVM/CodeData.cpp
@@ -0,0 +1,205 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// CodeData classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "CodeData.hpp"
+
+#include "Code.hpp"
+
+#include <algorithm>
+#include <cstring>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // CodeDataACS0 constructor
+   //
+   CodeDataACS0::CodeDataACS0(char const *args_, Code transCode_,
+      Word stackArgC_, Word transFunc_) :
+      code     {CodeACS0::None},
+      args     {args_},
+      argc     {CountArgs(args_)},
+      stackArgC{stackArgC_},
+      transCode{transCode_},
+      transFunc{transFunc_}
+   {
+   }
+
+   //
+   // CodeDataACS0 constructor
+   //
+   CodeDataACS0::CodeDataACS0(char const *args_, Word stackArgC_, Word transFunc_) :
+      code     {CodeACS0::None},
+      args     {args_},
+      argc     {CountArgs(args_)},
+      stackArgC{stackArgC_},
+      transCode{argc ? Code::CallFunc_Lit : Code::CallFunc},
+      transFunc{transFunc_}
+   {
+   }
+
+   //
+   // CodeDataACS0 constructor
+   //
+   CodeDataACS0::CodeDataACS0(CodeACS0 code_, char const *args_,
+      Code transCode_, Word stackArgC_, Func transFunc_) :
+      code     {code_},
+      args     {args_},
+      argc     {CountArgs(args_)},
+      stackArgC{stackArgC_},
+      transCode{transCode_},
+      transFunc{transFunc_ != Func::None ? static_cast<Word>(transFunc_) : 0}
+   {
+   }
+
+   //
+   // CodeDataACS0::CountArgs
+   //
+   std::size_t CodeDataACS0::CountArgs(char const *args)
+   {
+      std::size_t argc = 0;
+
+      for(; *args; ++args) switch(*args)
+      {
+      case 'B':
+      case 'H':
+      case 'W':
+      case 'b':
+      case 'h':
+         ++argc;
+         break;
+      }
+
+      return argc;
+   }
+
+   //
+   // FuncDataACS0 copy constructor
+   //
+   FuncDataACS0::FuncDataACS0(FuncDataACS0 const &data) :
+      transFunc{data.transFunc},
+
+      transCodeV{new TransCode[data.transCodeC]},
+      transCodeC{data.transCodeC}
+   {
+      std::copy(data.transCodeV, data.transCodeV + transCodeC, transCodeV);
+   }
+
+   //
+   // FuncDataACS0 move constructor
+   //
+   FuncDataACS0::FuncDataACS0(FuncDataACS0 &&data) :
+      transFunc{data.transFunc},
+
+      transCodeV{data.transCodeV},
+      transCodeC{data.transCodeC}
+   {
+      data.transCodeV = nullptr;
+      data.transCodeC = 0;
+   }
+
+   //
+   // FuncDataACS0 constructor
+   //
+   FuncDataACS0::FuncDataACS0(FuncACS0 func_, Func transFunc_,
+      std::initializer_list<TransCode> transCodes) :
+      func{func_},
+
+      transFunc{transFunc_ != Func::None ? static_cast<Word>(transFunc_) : 0},
+
+      transCodeV{new TransCode[transCodes.size()]},
+      transCodeC{transCodes.size()}
+   {
+      std::copy(transCodes.begin(), transCodes.end(), transCodeV);
+   }
+
+   //
+   // FuncDataACS0 constructor
+   //
+   FuncDataACS0::FuncDataACS0(Word transFunc_) :
+      func{FuncACS0::None},
+
+      transFunc{transFunc_},
+
+      transCodeV{nullptr},
+      transCodeC{0}
+   {
+   }
+
+   //
+   // FuncDataACS0 constructor
+   //
+   FuncDataACS0::FuncDataACS0(Word transFunc_,
+      std::initializer_list<TransCode> transCodes) :
+      func{FuncACS0::None},
+
+      transFunc{transFunc_},
+
+      transCodeV{new TransCode[transCodes.size()]},
+      transCodeC{transCodes.size()}
+   {
+      std::copy(transCodes.begin(), transCodes.end(), transCodeV);
+   }
+
+   //
+   // FuncDataACS0 constructor
+   //
+   FuncDataACS0::FuncDataACS0(Word transFunc_,
+      std::unique_ptr<TransCode[]> &&transCodeV_, std::size_t transCodeC_) :
+      func{FuncACS0::None},
+
+      transFunc{transFunc_},
+
+      transCodeV{transCodeV_.release()},
+      transCodeC{transCodeC_}
+   {
+   }
+
+   //
+   // FuncDataACS0 destructor
+   //
+   FuncDataACS0::~FuncDataACS0()
+   {
+      delete[] transCodeV;
+   }
+
+   //
+   // FuncDataACS0::operator = FuncDataACS0
+   //
+   FuncDataACS0 &FuncDataACS0::operator = (FuncDataACS0 &&data)
+   {
+      std::swap(transFunc, data.transFunc);
+
+      std::swap(transCodeV, data.transCodeV);
+      std::swap(transCodeC, data.transCodeC);
+
+      return *this;
+   }
+
+   //
+   // FuncDataACS0::getTransCode
+   //
+   Code FuncDataACS0::getTransCode(Word argc) const
+   {
+      for(auto itr = transCodeV, end = itr + transCodeC; itr != end; ++itr)
+         if(itr->first == argc) return itr->second;
+
+      return Code::CallFunc;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/CodeData.hpp b/src/acs/vm/ACSVM/CodeData.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..71b6f635afeb61483496229d8a5baaf7c69a07ca
--- /dev/null
+++ b/src/acs/vm/ACSVM/CodeData.hpp
@@ -0,0 +1,132 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// CodeData classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__CodeData_H__
+#define ACSVM__CodeData_H__
+
+#include "Types.hpp"
+
+#include <initializer_list>
+#include <memory>
+#include <utility>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // CodeData
+   //
+   // Internal code description.
+   //
+   class CodeData
+   {
+   public:
+      Code code;
+
+      Word argc;
+   };
+
+   //
+   // CodeDataACS0
+   //
+   // ACS0 code description.
+   //
+   class CodeDataACS0
+   {
+   public:
+      CodeDataACS0(char const *args, Code transCode, Word stackArgC,
+         Word transFunc = 0);
+      CodeDataACS0(char const *args, Word stackArgC, Word transFunc);
+      CodeDataACS0(CodeACS0 code, char const *args, Code transCode,
+         Word stackArgC, Func transFunc);
+
+      // Code index. If not an internally recognized code, is set to None.
+      CodeACS0 code;
+
+      // String describing the code's arguments.
+      //    A - Previous value is MapReg index.
+      //    a - Previous Value is MapArr index.
+      //    B - Single byte.
+      //    b - Single byte if compressed, full word otherwise.
+      //    G - Previous value is GblReg index.
+      //    g - Previous Value is GblArr index.
+      //    H - Half word.
+      //    h - Half word if compressed, full word otherwise.
+      //    J - Previous value is jump index.
+      //    L - Previous value is LocReg index.
+      //    l - Previous Value is LocArr index.
+      //    O - Previous value is ModReg index.
+      //    o - Previous Value is ModArr index.
+      //    S - Previous value is string index.
+      //    U - Previous value is HubReg index.
+      //    u - Previous Value is HubArr index.
+      //    W - Full word.
+      char const *args;
+      std::size_t argc;
+
+      // Stack argument count.
+      Word stackArgC;
+
+      // Internal code to translate to.
+      Code transCode;
+
+      // CallFunc index to translate to.
+      Word transFunc;
+
+   private:
+      static std::size_t CountArgs(char const *args);
+   };
+
+   //
+   // FuncDataACS0
+   //
+   // ACS0 CallFunc description.
+   //
+   class FuncDataACS0
+   {
+   public:
+      using TransCode = std::pair<Word, Code>;
+
+      FuncDataACS0(FuncDataACS0 const &);
+      FuncDataACS0(FuncDataACS0 &&data);
+      FuncDataACS0(FuncACS0 func, Func transFunc,
+         std::initializer_list<TransCode> transCodes);
+      FuncDataACS0(Word transFunc);
+      FuncDataACS0(Word transFunc, std::initializer_list<TransCode> transCodes);
+      FuncDataACS0(Word transFunc, std::unique_ptr<TransCode[]> &&transCodeV,
+         std::size_t transCodeC);
+      ~FuncDataACS0();
+
+      FuncDataACS0 &operator = (FuncDataACS0 const &) = delete;
+      FuncDataACS0 &operator = (FuncDataACS0 &&data);
+
+      // Internal code to translate to.
+      Code getTransCode(Word argc) const;
+
+      // CallFunc index. If not internally recognized, is set to None.
+      FuncACS0 func;
+
+      // CallFunc index to translate to.
+      Word transFunc;
+
+   private:
+      TransCode  *transCodeV;
+      std::size_t transCodeC;
+   };
+}
+
+#endif//ACSVM__CodeData_H__
+
diff --git a/src/acs/vm/ACSVM/CodeList.hpp b/src/acs/vm/ACSVM/CodeList.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4a4635c36e616b9c520cd57f40f97853f51c24e8
--- /dev/null
+++ b/src/acs/vm/ACSVM/CodeList.hpp
@@ -0,0 +1,454 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// List of all codes.
+//
+//-----------------------------------------------------------------------------
+
+
+#ifdef ACSVM_CodeList
+
+ACSVM_CodeList(Nop,          0)
+ACSVM_CodeList(Kill,         2)
+
+// Binary operator codes.
+#define ACSVM_CodeList_BinaryOpSet(name) \
+   ACSVM_CodeList(name,           0) \
+   ACSVM_CodeList(name##_GblArr,  1) \
+   ACSVM_CodeList(name##_GblReg,  1) \
+   ACSVM_CodeList(name##_HubArr,  1) \
+   ACSVM_CodeList(name##_HubReg,  1) \
+   ACSVM_CodeList(name##_LocArr,  1) \
+   ACSVM_CodeList(name##_LocReg,  1) \
+   ACSVM_CodeList(name##_ModArr,  1) \
+   ACSVM_CodeList(name##_ModReg,  1)
+ACSVM_CodeList_BinaryOpSet(AddU)
+ACSVM_CodeList_BinaryOpSet(AndU)
+ACSVM_CodeList_BinaryOpSet(DivI)
+ACSVM_CodeList_BinaryOpSet(ModI)
+ACSVM_CodeList_BinaryOpSet(MulU)
+ACSVM_CodeList_BinaryOpSet(OrIU)
+ACSVM_CodeList_BinaryOpSet(OrXU)
+ACSVM_CodeList_BinaryOpSet(ShLU)
+ACSVM_CodeList_BinaryOpSet(ShRI)
+ACSVM_CodeList_BinaryOpSet(SubU)
+#undef ACSVM_CodeList_BinaryOpSet
+ACSVM_CodeList(CmpI_GE,      0)
+ACSVM_CodeList(CmpI_GT,      0)
+ACSVM_CodeList(CmpI_LE,      0)
+ACSVM_CodeList(CmpI_LT,      0)
+ACSVM_CodeList(CmpU_EQ,      0)
+ACSVM_CodeList(CmpU_NE,      0)
+ACSVM_CodeList(DivX,         0)
+ACSVM_CodeList(LAnd,         0)
+ACSVM_CodeList(LOrI,         0)
+ACSVM_CodeList(MulX,         0)
+
+// Call codes.
+ACSVM_CodeList(Call_Lit,     1)
+ACSVM_CodeList(Call_Stk,     0)
+ACSVM_CodeList(CallFunc,     2)
+ACSVM_CodeList(CallFunc_Lit, 0)
+ACSVM_CodeList(CallSpec,     2)
+ACSVM_CodeList(CallSpec_Lit, 0)
+ACSVM_CodeList(CallSpec_R1,  2)
+ACSVM_CodeList(Retn,         0)
+
+// Drop codes.
+ACSVM_CodeList(Drop_GblArr,  1)
+ACSVM_CodeList(Drop_GblReg,  1)
+ACSVM_CodeList(Drop_HubArr,  1)
+ACSVM_CodeList(Drop_HubReg,  1)
+ACSVM_CodeList(Drop_LocArr,  1)
+ACSVM_CodeList(Drop_LocReg,  1)
+ACSVM_CodeList(Drop_ModArr,  1)
+ACSVM_CodeList(Drop_ModReg,  1)
+ACSVM_CodeList(Drop_Nul,     0)
+ACSVM_CodeList(Drop_ScrRet,  0)
+
+// Jump codes.
+ACSVM_CodeList(Jcnd_Lit,     2)
+ACSVM_CodeList(Jcnd_Nil,     1)
+ACSVM_CodeList(Jcnd_Tab,     1)
+ACSVM_CodeList(Jcnd_Tru,     1)
+ACSVM_CodeList(Jump_Lit,     1)
+ACSVM_CodeList(Jump_Stk,     0)
+
+// Push codes.
+ACSVM_CodeList(Pfun_Lit,     1)
+ACSVM_CodeList(Pstr_Stk,     0)
+ACSVM_CodeList(Push_GblArr,  1)
+ACSVM_CodeList(Push_GblReg,  1)
+ACSVM_CodeList(Push_HubArr,  1)
+ACSVM_CodeList(Push_HubReg,  1)
+ACSVM_CodeList(Push_Lit,     1)
+ACSVM_CodeList(Push_LitArr,  0)
+ACSVM_CodeList(Push_LocArr,  1)
+ACSVM_CodeList(Push_LocReg,  1)
+ACSVM_CodeList(Push_ModArr,  1)
+ACSVM_CodeList(Push_ModReg,  1)
+ACSVM_CodeList(Push_StrArs,  0)
+
+// Script control codes.
+ACSVM_CodeList(ScrDelay,     0)
+ACSVM_CodeList(ScrDelay_Lit, 1)
+ACSVM_CodeList(ScrHalt,      0)
+ACSVM_CodeList(ScrRestart,   0)
+ACSVM_CodeList(ScrTerm,      0)
+ACSVM_CodeList(ScrWaitI,     0)
+ACSVM_CodeList(ScrWaitI_Lit, 1)
+ACSVM_CodeList(ScrWaitS,     0)
+ACSVM_CodeList(ScrWaitS_Lit, 1)
+
+// Stack control codes.
+ACSVM_CodeList(Copy,         0)
+ACSVM_CodeList(Swap,         0)
+
+// Unary operator codes.
+#define ACSVM_CodeList_UnaryOpSet(name) \
+   ACSVM_CodeList(name##_GblArr,  1) \
+   ACSVM_CodeList(name##_GblReg,  1) \
+   ACSVM_CodeList(name##_HubArr,  1) \
+   ACSVM_CodeList(name##_HubReg,  1) \
+   ACSVM_CodeList(name##_LocArr,  1) \
+   ACSVM_CodeList(name##_LocReg,  1) \
+   ACSVM_CodeList(name##_ModArr,  1) \
+   ACSVM_CodeList(name##_ModReg,  1)
+ACSVM_CodeList_UnaryOpSet(DecU)
+ACSVM_CodeList_UnaryOpSet(IncU)
+#undef ACSVM_CodeList_UnaryOpSet
+ACSVM_CodeList(InvU,         0)
+ACSVM_CodeList(NegI,         0)
+ACSVM_CodeList(NotU,         0)
+
+#undef ACSVM_CodeList
+#endif
+
+
+#ifdef ACSVM_CodeListACS0
+
+ACSVM_CodeListACS0(Nop,            0, "",       Nop,          0, None)
+ACSVM_CodeListACS0(ScrTerm,        1, "",       ScrTerm,      0, None)
+ACSVM_CodeListACS0(ScrHalt,        2, "",       ScrHalt,      0, None)
+ACSVM_CodeListACS0(Push_Lit,       3, "W",      Push_Lit,     0, None)
+ACSVM_CodeListACS0(CallSpec_1,     4, "b",      CallSpec,     1, None)
+ACSVM_CodeListACS0(CallSpec_2,     5, "b",      CallSpec,     2, None)
+ACSVM_CodeListACS0(CallSpec_3,     6, "b",      CallSpec,     3, None)
+ACSVM_CodeListACS0(CallSpec_4,     7, "b",      CallSpec,     4, None)
+ACSVM_CodeListACS0(CallSpec_5,     8, "b",      CallSpec,     5, None)
+ACSVM_CodeListACS0(CallSpec_1L,    9, "bW",     CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_2L,   10, "bWW",    CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_3L,   11, "bWWW",   CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_4L,   12, "bWWWW",  CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_5L,   13, "bWWWWW", CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(AddU,          14, "",       AddU,         2, None)
+ACSVM_CodeListACS0(SubU,          15, "",       SubU,         2, None)
+ACSVM_CodeListACS0(MulU,          16, "",       MulU,         2, None)
+ACSVM_CodeListACS0(DivI,          17, "",       DivI,         2, None)
+ACSVM_CodeListACS0(ModI,          18, "",       ModI,         2, None)
+ACSVM_CodeListACS0(CmpU_EQ,       19, "",       CmpU_EQ,      2, None)
+ACSVM_CodeListACS0(CmpU_NE,       20, "",       CmpU_NE,      2, None)
+ACSVM_CodeListACS0(CmpI_LT,       21, "",       CmpI_LT,      2, None)
+ACSVM_CodeListACS0(CmpI_GT,       22, "",       CmpI_GT,      2, None)
+ACSVM_CodeListACS0(CmpI_LE,       23, "",       CmpI_LE,      2, None)
+ACSVM_CodeListACS0(CmpI_GE,       24, "",       CmpI_GE,      2, None)
+ACSVM_CodeListACS0(Drop_LocReg,   25, "bL",     Drop_LocReg,  1, None)
+ACSVM_CodeListACS0(Drop_ModReg,   26, "bO",     Drop_ModReg,  1, None)
+ACSVM_CodeListACS0(Drop_HubReg,   27, "bU",     Drop_HubReg,  1, None)
+ACSVM_CodeListACS0(Push_LocReg,   28, "bL",     Push_LocReg,  1, None)
+ACSVM_CodeListACS0(Push_ModReg,   29, "bO",     Push_ModReg,  1, None)
+ACSVM_CodeListACS0(Push_HubReg,   30, "bU",     Push_HubReg,  1, None)
+ACSVM_CodeListACS0(AddU_LocReg,   31, "bL",     AddU_LocReg,  1, None)
+ACSVM_CodeListACS0(AddU_ModReg,   32, "bO",     AddU_ModReg,  1, None)
+ACSVM_CodeListACS0(AddU_HubReg,   33, "bU",     AddU_HubReg,  1, None)
+ACSVM_CodeListACS0(SubU_LocReg,   34, "bL",     SubU_LocReg,  1, None)
+ACSVM_CodeListACS0(SubU_ModReg,   35, "bO",     SubU_ModReg,  1, None)
+ACSVM_CodeListACS0(SubU_HubReg,   36, "bU",     SubU_HubReg,  1, None)
+ACSVM_CodeListACS0(MulU_LocReg,   37, "bL",     MulU_LocReg,  1, None)
+ACSVM_CodeListACS0(MulU_ModReg,   38, "bO",     MulU_ModReg,  1, None)
+ACSVM_CodeListACS0(MulU_HubReg,   39, "bU",     MulU_HubReg,  1, None)
+ACSVM_CodeListACS0(DivI_LocReg,   40, "bL",     DivI_LocReg,  1, None)
+ACSVM_CodeListACS0(DivI_ModReg,   41, "bO",     DivI_ModReg,  1, None)
+ACSVM_CodeListACS0(DivI_HubReg,   42, "bU",     DivI_HubReg,  1, None)
+ACSVM_CodeListACS0(ModI_LocReg,   43, "bL",     ModI_LocReg,  1, None)
+ACSVM_CodeListACS0(ModI_ModReg,   44, "bO",     ModI_ModReg,  1, None)
+ACSVM_CodeListACS0(ModI_HubReg,   45, "bU",     ModI_HubReg,  1, None)
+ACSVM_CodeListACS0(IncU_LocReg,   46, "bL",     IncU_LocReg,  1, None)
+ACSVM_CodeListACS0(IncU_ModReg,   47, "bO",     IncU_ModReg,  1, None)
+ACSVM_CodeListACS0(IncU_HubReg,   48, "bU",     IncU_HubReg,  1, None)
+ACSVM_CodeListACS0(DecU_LocReg,   49, "bL",     DecU_LocReg,  1, None)
+ACSVM_CodeListACS0(DecU_ModReg,   50, "bO",     DecU_ModReg,  1, None)
+ACSVM_CodeListACS0(DecU_HubReg,   51, "bU",     DecU_HubReg,  1, None)
+ACSVM_CodeListACS0(Jump_Lit,      52, "WJ",     Jump_Lit,     0, None)
+ACSVM_CodeListACS0(Jcnd_Tru,      53, "WJ",     Jcnd_Tru,     1, None)
+ACSVM_CodeListACS0(Drop_Nul,      54, "",       Drop_Nul,     1, None)
+ACSVM_CodeListACS0(ScrDelay,      55, "",       ScrDelay,     1, None)
+ACSVM_CodeListACS0(ScrDelay_Lit,  56, "W",      ScrDelay_Lit, 0, None)
+
+ACSVM_CodeListACS0(ScrRestart,    69, "",       ScrRestart,   0, None)
+ACSVM_CodeListACS0(LAnd,          70, "",       LAnd,         2, None)
+ACSVM_CodeListACS0(LOrI,          71, "",       LOrI,         2, None)
+ACSVM_CodeListACS0(AndU,          72, "",       AndU,         2, None)
+ACSVM_CodeListACS0(OrIU,          73, "",       OrIU,         2, None)
+ACSVM_CodeListACS0(OrXU,          74, "",       OrXU,         2, None)
+ACSVM_CodeListACS0(NotU,          75, "",       NotU,         1, None)
+ACSVM_CodeListACS0(ShLU,          76, "",       ShLU,         2, None)
+ACSVM_CodeListACS0(ShRI,          77, "",       ShRI,         2, None)
+ACSVM_CodeListACS0(NegI,          78, "",       NegI,         1, None)
+ACSVM_CodeListACS0(Jcnd_Nil,      79, "WJ",     Jcnd_Nil,     1, None)
+
+ACSVM_CodeListACS0(ScrWaitI,      81, "",       ScrWaitI,     1, None)
+ACSVM_CodeListACS0(ScrWaitI_Lit,  82, "W",      ScrWaitI_Lit, 0, None)
+
+ACSVM_CodeListACS0(Jcnd_Lit,      84, "WWJ",    Jcnd_Lit,     1, None)
+ACSVM_CodeListACS0(PrintPush,     85, "",       CallFunc,     0, PrintPush)
+
+ACSVM_CodeListACS0(PrintString,   87, "",       CallFunc,     1, PrintString)
+ACSVM_CodeListACS0(PrintIntD,     88, "",       CallFunc,     1, PrintIntD)
+ACSVM_CodeListACS0(PrintChar,     89, "",       CallFunc,     1, PrintChar)
+
+ACSVM_CodeListACS0(MulX,         136, "",       MulX,         2, None)
+ACSVM_CodeListACS0(DivX,         137, "",       DivX,         2, None)
+
+ACSVM_CodeListACS0(PrintFixD,    157, "",       CallFunc,     1, PrintFixD)
+
+ACSVM_CodeListACS0(Push_LitB,    167, "B",      Push_Lit,     0, None)
+ACSVM_CodeListACS0(CallSpec_1LB, 168, "BB",     CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_2LB, 169, "BBB",    CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_3LB, 170, "BBBB",   CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_4LB, 171, "BBBBB",  CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_5LB, 172, "BBBBBB", CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(ScrDelay_LB,  173, "B",      ScrDelay_Lit, 0, None)
+ACSVM_CodeListACS0(Push_LitArrB, 175, "",       Push_LitArr,  0, None)
+ACSVM_CodeListACS0(Push_Lit2B,   176, "BB",     Push_LitArr,  0, None)
+ACSVM_CodeListACS0(Push_Lit3B,   177, "BBB",    Push_LitArr,  0, None)
+ACSVM_CodeListACS0(Push_Lit4B,   178, "BBBB",   Push_LitArr,  0, None)
+ACSVM_CodeListACS0(Push_Lit5B,   179, "BBBBB",  Push_LitArr,  0, None)
+
+ACSVM_CodeListACS0(Drop_GblReg,  181, "bG",     Drop_GblReg,  1, None)
+ACSVM_CodeListACS0(Push_GblReg,  182, "bG",     Push_GblReg,  0, None)
+ACSVM_CodeListACS0(AddU_GblReg,  183, "bG",     AddU_GblReg,  1, None)
+ACSVM_CodeListACS0(SubU_GblReg,  184, "bG",     SubU_GblReg,  1, None)
+ACSVM_CodeListACS0(MulU_GblReg,  185, "bG",     MulU_GblReg,  1, None)
+ACSVM_CodeListACS0(DivI_GblReg,  186, "bG",     DivI_GblReg,  1, None)
+ACSVM_CodeListACS0(ModI_GblReg,  187, "bG",     ModI_GblReg,  1, None)
+ACSVM_CodeListACS0(IncU_GblReg,  188, "bG",     IncU_GblReg,  1, None)
+ACSVM_CodeListACS0(DecU_GblReg,  189, "bG",     DecU_GblReg,  1, None)
+
+ACSVM_CodeListACS0(Call_Lit,     203, "b",      Call_Lit,     0, None)
+ACSVM_CodeListACS0(Call_Nul,     204, "b",      Call_Lit,     0, None)
+ACSVM_CodeListACS0(Retn_Nul,     205, "",       Retn,         0, None)
+ACSVM_CodeListACS0(Retn_Stk,     206, "",       Retn,         0, None)
+ACSVM_CodeListACS0(Push_ModArr,  207, "bo",     Push_ModArr,  1, None)
+ACSVM_CodeListACS0(Drop_ModArr,  208, "bo",     Drop_ModArr,  2, None)
+ACSVM_CodeListACS0(AddU_ModArr,  209, "bo",     AddU_ModArr,  2, None)
+ACSVM_CodeListACS0(SubU_ModArr,  210, "bo",     SubU_ModArr,  2, None)
+ACSVM_CodeListACS0(MulU_ModArr,  211, "bo",     MulU_ModArr,  2, None)
+ACSVM_CodeListACS0(DivI_ModArr,  212, "bo",     DivI_ModArr,  2, None)
+ACSVM_CodeListACS0(ModI_ModArr,  213, "bo",     ModI_ModArr,  2, None)
+ACSVM_CodeListACS0(IncU_ModArr,  214, "bo",     IncU_ModArr,  2, None)
+ACSVM_CodeListACS0(DecU_ModArr,  215, "bo",     DecU_ModArr,  2, None)
+ACSVM_CodeListACS0(Copy,         216, "",       Copy,         1, None)
+ACSVM_CodeListACS0(Swap,         217, "",       Swap,         2, None)
+
+ACSVM_CodeListACS0(Pstr_Stk,     225, "",       Pstr_Stk,     1, None)
+ACSVM_CodeListACS0(Push_HubArr,  226, "bu",     Push_HubArr,  1, None)
+ACSVM_CodeListACS0(Drop_HubArr,  227, "bu",     Drop_HubArr,  2, None)
+ACSVM_CodeListACS0(AddU_HubArr,  228, "bu",     AddU_HubArr,  2, None)
+ACSVM_CodeListACS0(SubU_HubArr,  229, "bu",     SubU_HubArr,  2, None)
+ACSVM_CodeListACS0(MulU_HubArr,  230, "bu",     MulU_HubArr,  2, None)
+ACSVM_CodeListACS0(DivI_HubArr,  231, "bu",     DivI_HubArr,  2, None)
+ACSVM_CodeListACS0(ModI_HubArr,  232, "bu",     ModI_HubArr,  2, None)
+ACSVM_CodeListACS0(IncU_HubArr,  233, "bu",     IncU_HubArr,  2, None)
+ACSVM_CodeListACS0(DecU_HubArr,  234, "bu",     DecU_HubArr,  2, None)
+ACSVM_CodeListACS0(Push_GblArr,  235, "bg",     Push_GblArr,  1, None)
+ACSVM_CodeListACS0(Drop_GblArr,  236, "bg",     Drop_GblArr,  2, None)
+ACSVM_CodeListACS0(AddU_GblArr,  237, "bg",     AddU_GblArr,  2, None)
+ACSVM_CodeListACS0(SubU_GblArr,  238, "bg",     SubU_GblArr,  2, None)
+ACSVM_CodeListACS0(MulU_GblArr,  239, "bg",     MulU_GblArr,  2, None)
+ACSVM_CodeListACS0(DivI_GblArr,  240, "bg",     DivI_GblArr,  2, None)
+ACSVM_CodeListACS0(ModI_GblArr,  241, "bg",     ModI_GblArr,  2, None)
+ACSVM_CodeListACS0(IncU_GblArr,  242, "bg",     IncU_GblArr,  2, None)
+ACSVM_CodeListACS0(DecU_GblArr,  243, "bg",     DecU_GblArr,  2, None)
+
+ACSVM_CodeListACS0(StrLen,       253, "",       CallFunc,     1, StrLen)
+
+ACSVM_CodeListACS0(Jcnd_Tab,     256, "",       Jcnd_Tab,     1, None)
+ACSVM_CodeListACS0(Drop_ScrRet,  257, "",       Drop_ScrRet,  1, None)
+
+ACSVM_CodeListACS0(CallSpec_5R1, 263, "b",      CallSpec_R1,  5, None)
+
+ACSVM_CodeListACS0(PrintModArr,  273, "",       CallFunc,     2, PrintModArr)
+ACSVM_CodeListACS0(PrintHubArr,  274, "",       CallFunc,     2, PrintHubArr)
+ACSVM_CodeListACS0(PrintGblArr,  275, "",       CallFunc,     2, PrintGblArr)
+
+ACSVM_CodeListACS0(AndU_LocReg,  291, "bL",     AndU_LocReg,  1, None)
+ACSVM_CodeListACS0(AndU_ModReg,  292, "bO",     AndU_ModReg,  1, None)
+ACSVM_CodeListACS0(AndU_HubReg,  293, "bU",     AndU_HubReg,  1, None)
+ACSVM_CodeListACS0(AndU_GblReg,  294, "bG",     AndU_GblReg,  1, None)
+ACSVM_CodeListACS0(AndU_ModArr,  295, "bo",     AndU_ModArr,  2, None)
+ACSVM_CodeListACS0(AndU_HubArr,  296, "bu",     AndU_HubArr,  2, None)
+ACSVM_CodeListACS0(AndU_GblArr,  297, "bg",     AndU_GblArr,  2, None)
+ACSVM_CodeListACS0(OrXU_LocReg,  298, "bL",     OrXU_LocReg,  1, None)
+ACSVM_CodeListACS0(OrXU_ModReg,  299, "bO",     OrXU_ModReg,  1, None)
+ACSVM_CodeListACS0(OrXU_HubReg,  300, "bU",     OrXU_HubReg,  1, None)
+ACSVM_CodeListACS0(OrXU_GblReg,  301, "bG",     OrXU_GblReg,  1, None)
+ACSVM_CodeListACS0(OrXU_ModArr,  302, "bo",     OrXU_ModArr,  2, None)
+ACSVM_CodeListACS0(OrXU_HubArr,  303, "bu",     OrXU_HubArr,  2, None)
+ACSVM_CodeListACS0(OrXU_GblArr,  304, "bg",     OrXU_GblArr,  2, None)
+ACSVM_CodeListACS0(OrIU_LocReg,  305, "bL",     OrIU_LocReg,  1, None)
+ACSVM_CodeListACS0(OrIU_ModReg,  306, "bO",     OrIU_ModReg,  1, None)
+ACSVM_CodeListACS0(OrIU_HubReg,  307, "bU",     OrIU_HubReg,  1, None)
+ACSVM_CodeListACS0(OrIU_GblReg,  308, "bG",     OrIU_GblReg,  1, None)
+ACSVM_CodeListACS0(OrIU_ModArr,  309, "bo",     OrIU_ModArr,  2, None)
+ACSVM_CodeListACS0(OrIU_HubArr,  310, "bu",     OrIU_HubArr,  2, None)
+ACSVM_CodeListACS0(OrIU_GblArr,  311, "bg",     OrIU_GblArr,  2, None)
+ACSVM_CodeListACS0(ShLU_LocReg,  312, "bL",     ShLU_LocReg,  1, None)
+ACSVM_CodeListACS0(ShLU_ModReg,  313, "bO",     ShLU_ModReg,  1, None)
+ACSVM_CodeListACS0(ShLU_HubReg,  314, "bU",     ShLU_HubReg,  1, None)
+ACSVM_CodeListACS0(ShLU_GblReg,  315, "bG",     ShLU_GblReg,  1, None)
+ACSVM_CodeListACS0(ShLU_ModArr,  316, "bo",     ShLU_ModArr,  2, None)
+ACSVM_CodeListACS0(ShLU_HubArr,  317, "bu",     ShLU_HubArr,  2, None)
+ACSVM_CodeListACS0(ShLU_GblArr,  318, "bg",     ShLU_GblArr,  2, None)
+ACSVM_CodeListACS0(ShRI_LocReg,  319, "bL",     ShRI_LocReg,  1, None)
+ACSVM_CodeListACS0(ShRI_ModReg,  320, "bO",     ShRI_ModReg,  1, None)
+ACSVM_CodeListACS0(ShRI_HubReg,  321, "bU",     ShRI_HubReg,  1, None)
+ACSVM_CodeListACS0(ShRI_GblReg,  322, "bG",     ShRI_GblReg,  1, None)
+ACSVM_CodeListACS0(ShRI_ModArr,  323, "bo",     ShRI_ModArr,  2, None)
+ACSVM_CodeListACS0(ShRI_HubArr,  324, "bu",     ShRI_HubArr,  2, None)
+ACSVM_CodeListACS0(ShRI_GblArr,  325, "bg",     ShRI_GblArr,  2, None)
+
+ACSVM_CodeListACS0(InvU,         330, "",       InvU,         1, None)
+
+ACSVM_CodeListACS0(PrintIntB,    349, "",       CallFunc,     1, PrintIntB)
+ACSVM_CodeListACS0(PrintIntX,    350, "",       CallFunc,     1, PrintIntX)
+ACSVM_CodeListACS0(CallFunc,     351, "bh",     CallFunc,     0, None)
+ACSVM_CodeListACS0(PrintEndStr,  352, "",       CallFunc,     0, PrintEndStr)
+ACSVM_CodeListACS0(PrintModArrR, 353, "",       CallFunc,     4, PrintModArr)
+ACSVM_CodeListACS0(PrintHubArrR, 354, "",       CallFunc,     4, PrintHubArr)
+ACSVM_CodeListACS0(PrintGblArrR, 355, "",       CallFunc,     4, PrintGblArr)
+ACSVM_CodeListACS0(StrCpyModArr, 356, "",       CallFunc,     6, StrCpyModArr)
+ACSVM_CodeListACS0(StrCpyHubArr, 357, "",       CallFunc,     6, StrCpyHubArr)
+ACSVM_CodeListACS0(StrCpyGblArr, 358, "",       CallFunc,     6, StrCpyGblArr)
+ACSVM_CodeListACS0(Pfun_Lit,     359, "b",      Pfun_Lit,     0, None)
+ACSVM_CodeListACS0(Call_Stk,     360, "",       Call_Stk,     1, None)
+ACSVM_CodeListACS0(ScrWaitS,     361, "",       ScrWaitS,     1, None)
+
+ACSVM_CodeListACS0(Jump_Stk,     363, "",       Jump_Stk,     1, None)
+ACSVM_CodeListACS0(Drop_LocArr,  364, "bl",     Drop_LocArr,  2, None)
+ACSVM_CodeListACS0(Push_LocArr,  365, "bl",     Push_LocArr,  1, None)
+ACSVM_CodeListACS0(AddU_LocArr,  366, "bl",     AddU_LocArr,  2, None)
+ACSVM_CodeListACS0(SubU_LocArr,  367, "bl",     SubU_LocArr,  2, None)
+ACSVM_CodeListACS0(MulU_LocArr,  368, "bl",     MulU_LocArr,  2, None)
+ACSVM_CodeListACS0(DivI_LocArr,  369, "bl",     DivI_LocArr,  2, None)
+ACSVM_CodeListACS0(ModI_LocArr,  370, "bl",     ModI_LocArr,  2, None)
+ACSVM_CodeListACS0(IncU_LocArr,  371, "bl",     IncU_LocArr,  2, None)
+ACSVM_CodeListACS0(DecU_LocArr,  372, "bl",     DecU_LocArr,  2, None)
+ACSVM_CodeListACS0(AndU_LocArr,  373, "bl",     AndU_LocArr,  2, None)
+ACSVM_CodeListACS0(OrXU_LocArr,  374, "bl",     OrXU_LocArr,  2, None)
+ACSVM_CodeListACS0(OrIU_LocArr,  375, "bl",     OrIU_LocArr,  2, None)
+ACSVM_CodeListACS0(ShLU_LocArr,  376, "bl",     ShLU_LocArr,  2, None)
+ACSVM_CodeListACS0(ShRI_LocArr,  377, "bl",     ShRI_LocArr,  2, None)
+ACSVM_CodeListACS0(PrintLocArr,  378, "",       CallFunc,     2, PrintLocArr)
+ACSVM_CodeListACS0(PrintLocArrR, 379, "",       CallFunc,     4, PrintLocArr)
+ACSVM_CodeListACS0(StrCpyLocArr, 380, "",       CallFunc,     6, StrCpyLocArr)
+
+ACSVM_CodeListACS0(CallSpec_6,   500, "b",      CallSpec,     6, None)
+ACSVM_CodeListACS0(CallSpec_7,   501, "b",      CallSpec,     7, None)
+ACSVM_CodeListACS0(CallSpec_8,   502, "b",      CallSpec,     8, None)
+ACSVM_CodeListACS0(CallSpec_9,   503, "b",      CallSpec,     9, None)
+ACSVM_CodeListACS0(CallSpec_10,  504, "b",      CallSpec,    10, None)
+ACSVM_CodeListACS0(CallSpec_6L,  505, "bWWWWWW",      CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_7L,  506, "bWWWWWWWW",    CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_8L,  507, "bWWWWWWWWW",   CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_9L,  508, "bWWWWWWWWWW",  CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_10L, 509, "bWWWWWWWWWWW", CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_6LB, 510, "BBBBBBB",     CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_7LB, 511, "BBBBBBBB",    CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_8LB, 512, "BBBBBBBBB",   CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_9LB, 513, "BBBBBBBBBB",  CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(CallSpec_10LB,514, "BBBBBBBBBBB", CallSpec_Lit, 0, None)
+ACSVM_CodeListACS0(Push_Lit6B,   515, "BBBBBB",     Push_LitArr, 0, None)
+ACSVM_CodeListACS0(Push_Lit7B,   516, "BBBBBBB",    Push_LitArr, 0, None)
+ACSVM_CodeListACS0(Push_Lit8B,   517, "BBBBBBBB",   Push_LitArr, 0, None)
+ACSVM_CodeListACS0(Push_Lit9B,   518, "BBBBBBBBB",  Push_LitArr, 0, None)
+ACSVM_CodeListACS0(Push_Lit10B,  519, "BBBBBBBBBB", Push_LitArr, 0, None)
+ACSVM_CodeListACS0(CallSpec_10R1,520, "b",      CallSpec_R1, 10, None)
+
+#undef ACSVM_CodeListACS0
+#endif
+
+
+#ifdef ACSVM_FuncList
+
+ACSVM_FuncList(Nop)
+ACSVM_FuncList(Kill)
+
+// Printing functions.
+ACSVM_FuncList(PrintChar)
+ACSVM_FuncList(PrintEndStr)
+ACSVM_FuncList(PrintFixD)
+ACSVM_FuncList(PrintGblArr)
+ACSVM_FuncList(PrintHubArr)
+ACSVM_FuncList(PrintIntB)
+ACSVM_FuncList(PrintIntD)
+ACSVM_FuncList(PrintIntX)
+ACSVM_FuncList(PrintLocArr)
+ACSVM_FuncList(PrintModArr)
+ACSVM_FuncList(PrintPush)
+ACSVM_FuncList(PrintString)
+
+// Script functions.
+ACSVM_FuncList(ScrPauseS)
+ACSVM_FuncList(ScrStartS)
+ACSVM_FuncList(ScrStartSD) // Locked Door
+ACSVM_FuncList(ScrStartSF) // Forced
+ACSVM_FuncList(ScrStartSL) // Locked
+ACSVM_FuncList(ScrStartSR) // Result
+ACSVM_FuncList(ScrStopS)
+
+// String functions.
+ACSVM_FuncList(GetChar)
+ACSVM_FuncList(StrCaseCmp)
+ACSVM_FuncList(StrCmp)
+ACSVM_FuncList(StrCpyGblArr)
+ACSVM_FuncList(StrCpyHubArr)
+ACSVM_FuncList(StrCpyLocArr)
+ACSVM_FuncList(StrCpyModArr)
+ACSVM_FuncList(StrLeft)
+ACSVM_FuncList(StrLen)
+ACSVM_FuncList(StrMid)
+ACSVM_FuncList(StrRight)
+
+#undef ACSVM_FuncList
+#endif
+
+
+#ifdef ACSVM_FuncListACS0
+
+ACSVM_FuncListACS0(GetChar, 15, GetChar, {{2, Code::Push_StrArs}})
+
+ACSVM_FuncListACS0(ScrStartS,  39, ScrStartS,  {})
+ACSVM_FuncListACS0(ScrPauseS,  40, ScrPauseS,  {})
+ACSVM_FuncListACS0(ScrStopS,   41, ScrStopS,   {})
+ACSVM_FuncListACS0(ScrStartSL, 42, ScrStartSL, {})
+ACSVM_FuncListACS0(ScrStartSD, 43, ScrStartSD, {})
+ACSVM_FuncListACS0(ScrStartSR, 44, ScrStartSR, {})
+ACSVM_FuncListACS0(ScrStartSF, 45, ScrStartSF, {})
+
+ACSVM_FuncListACS0(StrCmp,     63, StrCmp,     {})
+ACSVM_FuncListACS0(StrCaseCmp, 64, StrCaseCmp, {})
+ACSVM_FuncListACS0(StrLeft,    65, StrLeft,    {})
+ACSVM_FuncListACS0(StrRight,   66, StrRight,   {})
+ACSVM_FuncListACS0(StrMid,     67, StrMid,     {})
+
+#undef ACSVM_FuncListACS0
+#endif
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Environment.cpp b/src/acs/vm/ACSVM/Environment.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6461ec5140059e3dab85f2f75af673e89634a2a2
--- /dev/null
+++ b/src/acs/vm/ACSVM/Environment.cpp
@@ -0,0 +1,908 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Environment class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Environment.hpp"
+
+#include "Action.hpp"
+#include "BinaryIO.hpp"
+#include "CallFunc.hpp"
+#include "Code.hpp"
+#include "CodeData.hpp"
+#include "Function.hpp"
+#include "HashMap.hpp"
+#include "Module.hpp"
+#include "PrintBuf.hpp"
+#include "Scope.hpp"
+#include "Script.hpp"
+#include "Serial.hpp"
+#include "Thread.hpp"
+
+#include <iostream>
+#include <list>
+#include <unordered_map>
+#include <vector>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Environment::PrivData
+   //
+   struct Environment::PrivData
+   {
+      using FuncName = std::pair<ModuleName, String *>;
+      using FuncElem = HashMapElem<FuncName, Word>;
+
+      struct NameEqual
+      {
+         bool operator () (ModuleName const *l, ModuleName const *r) const
+            {return *l == *r;}
+      };
+
+      struct NameHash
+      {
+         std::size_t operator () (ModuleName const *name) const
+            {return name->hash();}
+      };
+
+      struct FuncNameHash
+      {
+         std::size_t operator () (FuncName const &name) const
+            {return name.first.hash() + name.second->hash;}
+      };
+
+
+      // Reserve index 0 as no function.
+      std::vector<Function *> functionByIdx{nullptr};
+
+      HashMapKeyExt<FuncName, Word, FuncNameHash> functionByName{16, 16};
+
+      HashMapKeyMem<ModuleName, Module, &Module::name, &Module::hashLink> modules;
+
+      HashMapKeyMem<Word, GlobalScope, &GlobalScope::id, &GlobalScope::hashLink> scopes;
+
+      std::vector<CallFunc> tableCallFunc
+      {
+         #define ACSVM_FuncList(name) \
+            CallFunc_Func_##name,
+         #include "CodeList.hpp"
+
+         CallFunc_Func_Nop
+      };
+
+      std::unordered_map<Word, CodeDataACS0> tableCodeDataACS0
+      {
+         #define ACSVM_CodeListACS0(name, code, args, transCode, stackArgC, transFunc) \
+            {code, {CodeACS0::name, args, Code::transCode, stackArgC, Func::transFunc}},
+         #include "CodeList.hpp"
+      };
+
+      std::unordered_map<Word, FuncDataACS0> tableFuncDataACS0
+      {
+         #define ACSVM_FuncListACS0(name, func, transFunc, ...) \
+            {func, {FuncACS0::name, Func::transFunc, __VA_ARGS__}},
+         #include "CodeList.hpp"
+      };
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Environment constructor
+   //
+   Environment::Environment() :
+      branchLimit  {0},
+      scriptLocRegC{ScriptLocRegCDefault},
+
+      funcV{nullptr},
+      funcC{0},
+
+      pd{new PrivData}
+   {
+      funcV = pd->functionByIdx.data();
+      funcC = pd->functionByIdx.size();
+   }
+
+   //
+   // Environment destructor
+   //
+   Environment::~Environment()
+   {
+      pd->functionByName.free();
+      pd->modules.free();
+      pd->scopes.free();
+
+      while(scriptAction.next->obj)
+         delete scriptAction.next->obj;
+
+      delete pd;
+
+      // Deallocate threads. Do this after scopes have been destructed.
+      while(threadFree.next->obj)
+         delete threadFree.next->obj;
+   }
+
+   //
+   // Environment::addCallFunc
+   //
+   Word Environment::addCallFunc(CallFunc func)
+   {
+      pd->tableCallFunc.push_back(func);
+      return pd->tableCallFunc.size() - 1;
+   }
+
+   //
+   // Environment::addCodeDataACS0
+   //
+   void Environment::addCodeDataACS0(Word code, CodeDataACS0 &&data)
+   {
+      auto itr = pd->tableCodeDataACS0.find(code);
+      if(itr == pd->tableCodeDataACS0.end())
+         pd->tableCodeDataACS0.emplace(code, std::move(data));
+      else
+         itr->second = std::move(data);
+   }
+
+   //
+   // Environment::addFuncDataACS0
+   //
+   void Environment::addFuncDataACS0(Word func, FuncDataACS0 &&data)
+   {
+      auto itr = pd->tableFuncDataACS0.find(func);
+      if(itr == pd->tableFuncDataACS0.end())
+         pd->tableFuncDataACS0.emplace(func, std::move(data));
+      else
+         itr->second = std::move(data);
+   }
+
+   //
+   // Environment::allocThread
+   //
+   Thread *Environment::allocThread()
+   {
+      return new Thread(this);
+   }
+
+   //
+   // Environment::callFunc
+   //
+   bool Environment::callFunc(Thread *thread, Word func, Word const *argV, Word argC)
+   {
+      return pd->tableCallFunc[func](thread, argV, argC);
+   }
+
+   //
+   // Environment::callSpec
+   //
+   Word Environment::callSpec(Thread *thread, Word spec, Word const *argV, Word argC)
+   {
+      if(thread->scopeMap->clampCallSpec && thread->module->isACS0)
+      {
+         Vector<Word> argTmp{argV, argC};
+         for(auto &arg : argTmp) arg &= 0xFF;
+         return callSpecImpl(thread, spec, argTmp.data(), argTmp.size());
+      }
+      else
+         return callSpecImpl(thread, spec, argV, argC);
+   }
+
+   //
+   // Environment::callSpecImpl
+   //
+   Word Environment::callSpecImpl(Thread *, Word, Word const *, Word)
+   {
+      return 0;
+   }
+
+   //
+   // Environment::checkLock
+   //
+   bool Environment::checkLock(Thread *, Word, bool)
+   {
+      return false;
+   }
+
+   //
+   // Environment::checkTag
+   //
+   bool Environment::checkTag(Word, Word)
+   {
+      return false;
+   }
+
+   //
+   // Environment::collectStrings
+   //
+   void Environment::collectStrings()
+   {
+      stringTable.collectBegin();
+      refStrings();
+      stringTable.collectEnd();
+   }
+
+   //
+   // Environment::countActiveThread
+   //
+   std::size_t Environment::countActiveThread() const
+   {
+      std::size_t n = 0;
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            n += scope.countActiveThread();
+      }
+
+      return n;
+   }
+
+   //
+   // Environment::deferAction
+   //
+   void Environment::deferAction(ScriptAction &&action)
+   {
+      (new ScriptAction(std::move(action)))->link.insert(&scriptAction);
+   }
+
+   //
+   // Environment::exec
+   //
+   void Environment::exec()
+   {
+      // Delegate deferred script actions.
+      for(auto itr = scriptAction.begin(), end = scriptAction.end(); itr != end;)
+      {
+         auto scope = pd->scopes.find(itr->id.global);
+         if(scope && scope->active)
+            itr++->link.relink(&scope->scriptAction);
+         else
+            ++itr;
+      }
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            scope.exec();
+      }
+   }
+
+   //
+   // Environment::findCodeDataACS0
+   //
+   CodeDataACS0 const *Environment::findCodeDataACS0(Word code)
+   {
+      auto itr = pd->tableCodeDataACS0.find(code);
+      return itr == pd->tableCodeDataACS0.end() ? nullptr : &itr->second;
+   }
+
+   //
+   // Environment::findFuncDataACS0
+   //
+   FuncDataACS0 const *Environment::findFuncDataACS0(Word func)
+   {
+      auto itr = pd->tableFuncDataACS0.find(func);
+      return itr == pd->tableFuncDataACS0.end() ? nullptr : &itr->second;
+   }
+
+   //
+   // Environment::findModule
+   //
+   Module *Environment::findModule(ModuleName const &name) const
+   {
+      return pd->modules.find(name);
+   }
+
+   //
+   // Environment::freeFunction
+   //
+   void Environment::freeFunction(Function *func)
+   {
+      // Null every reference to this function in every Module.
+      // O(N*M) is not very nice, but that can be fixed if/when it comes up.
+      for(auto &module : pd->modules)
+      {
+         for(Function *&funcItr : module.functionV)
+         {
+            if(funcItr == func)
+               funcItr = nullptr;
+         }
+      }
+
+      pd->functionByIdx[func->idx] = nullptr;
+      delete func;
+   }
+
+   //
+   // Environment::freeGlobalScope
+   //
+   void Environment::freeGlobalScope(GlobalScope *scope)
+   {
+      pd->scopes.unlink(scope);
+      delete scope;
+   }
+
+   //
+   // Environment::freeModule
+   //
+   void Environment::freeModule(Module *module)
+   {
+      pd->modules.unlink(module);
+      delete module;
+   }
+
+   //
+   // Environment::freeThread
+   //
+   void Environment::freeThread(Thread *thread)
+   {
+      thread->link.relink(&threadFree);
+   }
+
+   //
+   // Environment::getCodeData
+   //
+   CodeData const *Environment::getCodeData(Code code)
+   {
+      switch(code)
+      {
+         #define ACSVM_CodeList(name, argc) case Code::name: \
+            {static CodeData const data{Code::name, argc}; return &data;}
+         #include "CodeList.hpp"
+
+      default:
+      case Code::None:
+         static CodeData const dataNone{Code::None, 0};
+         return &dataNone;
+      }
+   }
+
+   //
+   // Environment::getFreeThread
+   //
+   Thread *Environment::getFreeThread()
+   {
+      if(threadFree.next->obj)
+      {
+         Thread *thread = threadFree.next->obj;
+         thread->link.unlink();
+         return thread;
+      }
+      else
+         return allocThread();
+   }
+
+   //
+   // Environment::getFunction
+   //
+   Function *Environment::getFunction(Module *module, String *funcName)
+   {
+      if(funcName)
+      {
+         PrivData::FuncName namePair{module->name, funcName};
+         auto idx = pd->functionByName.find(namePair);
+
+         if(!idx)
+         {
+            #if SIZE_MAX > UINT32_MAX
+            if(pd->functionByIdx.size() > UINT32_MAX)
+               throw std::bad_alloc();
+            #endif
+
+            idx = new PrivData::FuncElem{std::move(namePair),
+               static_cast<Word>(pd->functionByIdx.size())};
+            pd->functionByName.insert(idx);
+
+            pd->functionByIdx.emplace_back();
+            funcV = pd->functionByIdx.data();
+            funcC = pd->functionByIdx.size();
+         }
+
+         auto &ptr = pd->functionByIdx[idx->val];
+
+         if(!ptr)
+            ptr = new Function{module, funcName, idx->val};
+
+         return ptr;
+      }
+      else
+         return new Function{module, nullptr, 0};
+   }
+
+   //
+   // Environment::getGlobalScope
+   //
+   GlobalScope *Environment::getGlobalScope(Word id)
+   {
+      if(auto *scope = pd->scopes.find(id))
+         return scope;
+
+      auto scope = new GlobalScope(this, id);
+      pd->scopes.insert(scope);
+      return scope;
+   }
+
+   //
+   // Environment::getModule
+   //
+   Module *Environment::getModule(ModuleName const &name)
+   {
+      auto module = pd->modules.find(name);
+
+      if(!module)
+      {
+         module = new Module{this, name};
+         pd->modules.insert(module);
+         loadModule(module);
+      }
+      else
+      {
+         if(!module->loaded)
+            loadModule(module);
+      }
+
+      return module;
+   }
+
+   //
+   // Environment::getModuleName
+   //
+   ModuleName Environment::getModuleName(char const *str)
+   {
+      return getModuleName(str, std::strlen(str));
+   }
+
+   //
+   // Environment::getModuleName
+   //
+   ModuleName Environment::getModuleName(char const *str, std::size_t len)
+   {
+      return {getString(str, len), nullptr, 0};
+   }
+
+   //
+   // Environment::hasActiveThread
+   //
+   bool Environment::hasActiveThread() const
+   {
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active && scope.hasActiveThread())
+            return true;
+      }
+
+      return false;
+   }
+
+   //
+   // Environment::loadFunctions
+   //
+   void Environment::loadFunctions(Serial &in)
+   {
+      // Function index map.
+      pd->functionByName.free();
+      for(std::size_t n = ReadVLN<std::size_t>(in); n--;)
+      {
+         ModuleName name = readModuleName(in);
+         String    *str  = &stringTable[ReadVLN<Word>(in)];
+         Word       idx  = ReadVLN<Word>(in);
+
+         pd->functionByName.insert(new PrivData::FuncElem{{name, str}, idx});
+      }
+
+      // Function vector.
+      auto oldTable = pd->functionByIdx;
+
+      pd->functionByIdx.clear();
+      pd->functionByIdx.resize(ReadVLN<std::size_t>(in), nullptr);
+      funcV = pd->functionByIdx.data();
+      funcC = pd->functionByIdx.size();
+
+      // Reset function indexes.
+      for(Function *&func : oldTable)
+      {
+         if(func)
+         {
+            auto idx = pd->functionByName.find({func->module->name, func->name});
+            func->idx = idx ? idx->val : 0;
+            pd->functionByIdx[func->idx] = func;
+         }
+      }
+   }
+
+   //
+   // Environment::loadGlobalScopes
+   //
+   void Environment::loadGlobalScopes(Serial &in)
+   {
+      // Clear existing scopes.
+      pd->scopes.free();
+
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+         getGlobalScope(ReadVLN<Word>(in))->loadState(in);
+   }
+
+   //
+   // Environment::loadScriptActions
+   //
+   void Environment::loadScriptActions(Serial &in)
+   {
+      readScriptActions(in, scriptAction);
+   }
+
+   //
+   // Environment::loadState
+   //
+   void Environment::loadState(Serial &in)
+   {
+      in.readSign(Signature::Environment);
+
+      loadStringTable(in);
+      loadFunctions(in);
+      loadGlobalScopes(in);
+      loadScriptActions(in);
+
+      in.readSign(~Signature::Environment);
+   }
+
+   //
+   // Environment::loadStringTable
+   //
+   void Environment::loadStringTable(Serial &in)
+   {
+      StringTable oldTable{std::move(stringTable)};
+      stringTable.loadState(in);
+      resetStrings();
+   }
+
+   //
+   // Environment::printArray
+   //
+   void Environment::printArray(PrintBuf &buf, Array const &array, Word index, Word limit)
+   {
+      PrintArrayChar(buf, array, index, limit);
+   }
+
+   //
+   // Environment::printKill
+   //
+   void Environment::printKill(Thread *thread, Word type, Word data)
+   {
+      std::cerr << "ACSVM ERROR: Kill " << type << ':' << data
+         << " at " << (thread->codePtr - thread->module->codeV.data() - 1) << '\n';
+   }
+
+   //
+   // Environment::readModuleName
+   //
+   ModuleName Environment::readModuleName(Serial &in) const
+   {
+      auto s = readString(in);
+      auto i = ReadVLN<std::size_t>(in);
+
+      return {s, nullptr, i};
+   }
+
+   //
+   // Environment::readScript
+   //
+   Script *Environment::readScript(Serial &in) const
+   {
+      auto idx = ReadVLN<std::size_t>(in);
+      return &findModule(readModuleName(in))->scriptV[idx];
+   }
+
+   //
+   // Environment::readScriptAction
+   //
+   ScriptAction *Environment::readScriptAction(Serial &in) const
+   {
+      auto action = static_cast<ScriptAction::Action>(ReadVLN<int>(in));
+
+      Vector<Word> argV;
+      argV.alloc(ReadVLN<std::size_t>(in));
+      for(auto &arg : argV)
+         arg = ReadVLN<Word>(in);
+
+      ScopeID id;
+      id.global = ReadVLN<Word>(in);
+      id.hub    = ReadVLN<Word>(in);
+      id.map    = ReadVLN<Word>(in);
+
+      ScriptName name = readScriptName(in);
+
+      return new ScriptAction{id, name, action, std::move(argV)};
+   }
+
+   //
+   // Environment::readScriptActions
+   //
+   void Environment::readScriptActions(Serial &in, ListLink<ScriptAction> &out) const
+   {
+      // Clear existing actions.
+      while(out.next->obj)
+         delete out.next->obj;
+
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+         readScriptAction(in)->link.insert(&out);
+   }
+
+   //
+   // Environment::readScriptName
+   //
+   ScriptName Environment::readScriptName(Serial &in) const
+   {
+      String *s = in.in->get() ? &stringTable[ReadVLN<Word>(in)] : nullptr;
+      Word    i = ReadVLN<Word>(in);
+      return {s, i};
+   }
+
+   //
+   // Environment::readString
+   //
+   String *Environment::readString(Serial &in) const
+   {
+      if(auto idx = ReadVLN<std::size_t>(in))
+         return &stringTable[idx - 1];
+      else
+         return nullptr;
+   }
+
+   //
+   // Environment::refStrings
+   //
+   void Environment::refStrings()
+   {
+      for(auto &action : scriptAction)
+         action.refStrings(this);
+
+      for(auto &funcIdx : pd->functionByName)
+      {
+         funcIdx.key.first.s->ref = true;
+         funcIdx.key.second->ref  = true;
+      }
+
+      for(auto &module : pd->modules)
+         module.refStrings();
+
+      for(auto &scope : pd->scopes)
+         scope.refStrings();
+   }
+
+   //
+   // Environment::resetStrings
+   //
+   void Environment::resetStrings()
+   {
+      for(auto &funcIdx : pd->functionByName)
+      {
+         funcIdx.key.first.s = getString(funcIdx.key.first.s);
+         funcIdx.key.second  = getString(funcIdx.key.second);
+      }
+
+      for(auto &module : pd->modules)
+         module.resetStrings();
+   }
+
+   //
+   // Environment::saveFunctions
+   //
+   void Environment::saveFunctions(Serial &out) const
+   {
+      WriteVLN(out, pd->functionByName.size());
+      for(auto &funcIdx : pd->functionByName)
+      {
+         writeModuleName(out, funcIdx.key.first);
+         WriteVLN(out, funcIdx.key.second->idx);
+         WriteVLN(out, funcIdx.val);
+      }
+
+      WriteVLN(out, pd->functionByIdx.size());
+   }
+
+   //
+   // Environment::saveGlobalScopes
+   //
+   void Environment::saveGlobalScopes(Serial &out) const
+   {
+      WriteVLN(out, pd->scopes.size());
+      for(auto &scope : pd->scopes)
+      {
+         WriteVLN(out, scope.id);
+         scope.saveState(out);
+      }
+   }
+
+   //
+   // Environment::saveScriptActions
+   //
+   void Environment::saveScriptActions(Serial &out) const
+   {
+      writeScriptActions(out, scriptAction);
+   }
+
+   //
+   // Environment::saveState
+   //
+   void Environment::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::Environment);
+
+      saveStringTable(out);
+      saveFunctions(out);
+      saveGlobalScopes(out);
+      saveScriptActions(out);
+
+      out.writeSign(~Signature::Environment);
+   }
+
+   //
+   // Environment::saveStringTable
+   //
+   void Environment::saveStringTable(Serial &out) const
+   {
+      stringTable.saveState(out);
+   }
+
+   //
+   // Environment::writeModuleName
+   //
+   void Environment::writeModuleName(Serial &out, ModuleName const &in) const
+   {
+      writeString(out, in.s);
+      WriteVLN(out, in.i);
+   }
+
+   //
+   // Environment::writeScript
+   //
+   void Environment::writeScript(Serial &out, Script *in) const
+   {
+      WriteVLN(out, in - in->module->scriptV.data());
+      writeModuleName(out, in->module->name);
+   }
+
+   //
+   // Environment::writeScriptAction
+   //
+   void Environment::writeScriptAction(Serial &out, ScriptAction const *in) const
+   {
+      WriteVLN<int>(out, in->action);
+
+      WriteVLN(out, in->argV.size());
+      for(auto &arg : in->argV)
+         WriteVLN(out, arg);
+
+      WriteVLN(out, in->id.global);
+      WriteVLN(out, in->id.hub);
+      WriteVLN(out, in->id.map);
+
+      writeScriptName(out, in->name);
+   }
+
+   //
+   // Environment::writeScriptActions
+   //
+   void Environment::writeScriptActions(Serial &out,
+      ListLink<ScriptAction> const &in) const
+   {
+      WriteVLN(out, in.size());
+
+      for(auto &action : in)
+         writeScriptAction(out, &action);
+   }
+
+   //
+   // Environment::writeScriptName
+   //
+   void Environment::writeScriptName(Serial &out, ScriptName const &in) const
+   {
+      if(in.s)
+      {
+         out.out->put('\1');
+         WriteVLN(out, in.s->idx);
+      }
+      else
+         out.out->put('\0');
+
+      WriteVLN(out, in.i);
+   }
+
+   //
+   // Environment::writeString
+   //
+   void Environment::writeString(Serial &out, String const *in) const
+   {
+      if(in)
+         WriteVLN<std::size_t>(out, in->idx + 1);
+      else
+         WriteVLN<std::size_t>(out, 0);
+   }
+
+   //
+   // Environment::PrintArrayChar
+   //
+   void Environment::PrintArrayChar(PrintBuf &buf, Array const &array, Word index, Word limit)
+   {
+      // Calculate output length and end index.
+      std::size_t len = 0;
+      Word        end;
+      for(Word &itr = end = index; itr - index != limit; ++itr)
+      {
+         Word c = array.find(itr);
+         if(!c) break;
+         ++len;
+      }
+
+      // Acquire output buffer.
+      buf.reserve(len);
+      char *s = buf.getBuf(len);
+
+      // Truncate elements to char.
+      for(Word itr = index; itr != end; ++itr)
+         *s++ = array.find(itr);
+   }
+
+   //
+   // Environment::PrintArrayUTF8
+   //
+   void Environment::PrintArrayUTF8(PrintBuf &buf, Array const &array, Word index, Word limit)
+   {
+      // Calculate output length and end index.
+      std::size_t len = 0;
+      Word        end;
+      for(Word &itr = end = index; itr - index != limit; ++itr)
+      {
+         Word c = array.find(itr);
+         if(!c) break;
+         if(c > 0x10FFFF) c = 0xFFFD;
+
+              if(c <= 0x007F) len += 1;
+         else if(c <= 0x07FF) len += 2;
+         else if(c <= 0xFFFF) len += 3;
+         else                 len += 4;
+      }
+
+      // Acquire output buffer.
+      buf.reserve(len);
+      char *s = buf.getBuf(len);
+
+      // Convert UTF-32 sequence to UTF-8.
+      for(Word itr = index; itr != end; ++itr)
+      {
+         Word c = array.find(itr);
+         if(c > 0x10FFFF) c = 0xFFFD;
+
+         if(c <= 0x7F)   {*s++ = 0x00 | (c >>  0); goto put0;}
+         if(c <= 0x7FF)  {*s++ = 0xC0 | (c >>  6); goto put1;}
+         if(c <= 0xFFFF) {*s++ = 0xE0 | (c >> 12); goto put2;}
+                         {*s++ = 0xF0 | (c >> 18); goto put3;}
+
+         put3: *s++ = 0x80 | ((c >> 12) & 0x3F);
+         put2: *s++ = 0x80 | ((c >>  6) & 0x3F);
+         put1: *s++ = 0x80 | ((c >>  0) & 0x3F);
+         put0:;
+      }
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Environment.hpp b/src/acs/vm/ACSVM/Environment.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..975a5af88375b9880e29ac9b1a6b1fb5f6c7a296
--- /dev/null
+++ b/src/acs/vm/ACSVM/Environment.hpp
@@ -0,0 +1,203 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Environment class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Environment_H__
+#define ACSVM__Environment_H__
+
+#include "List.hpp"
+#include "String.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Environment
+   //
+   // Represents an entire ACS environment.
+   //
+   class Environment
+   {
+   public:
+      Environment();
+      virtual ~Environment();
+
+      Word addCallFunc(CallFunc func);
+
+      void addCodeDataACS0(Word code, CodeDataACS0 &&data);
+      void addFuncDataACS0(Word func, FuncDataACS0 &&data);
+
+      virtual bool callFunc(Thread *thread, Word func, Word const *argV, Word argC);
+      Word callSpec(Thread *thread, Word spec, Word const *argV, Word argC);
+
+      // Function to check if a lock can be opened. Default behavior is to
+      // always return false.
+      virtual bool checkLock(Thread *thread, Word lock, bool door);
+
+      // Function to check tags. Must return true to indicate script should
+      // continue. Default behavior is to always return false.
+      virtual bool checkTag(Word type, Word tag);
+
+      void collectStrings();
+
+      std::size_t countActiveThread() const;
+
+      void deferAction(ScriptAction &&action);
+
+      virtual void exec();
+
+      CodeDataACS0 const *findCodeDataACS0(Word code);
+      FuncDataACS0 const *findFuncDataACS0(Word func);
+
+      Module *findModule(ModuleName const &name) const;
+
+      // Used by Module when unloading.
+      void freeFunction(Function *func);
+
+      void freeGlobalScope(GlobalScope *scope);
+
+      void freeModule(Module *module);
+
+      void freeThread(Thread *thread);
+
+      CodeData const *getCodeData(Code code);
+
+      Thread *getFreeThread();
+
+      Function *getFunction(Word idx) {return idx < funcC ? funcV[idx] : nullptr;}
+
+      Function *getFunction(Module *module, String *name);
+
+      GlobalScope *getGlobalScope(Word id);
+
+      // Gets the named module, loading it if needed.
+      Module *getModule(ModuleName const &name);
+
+      ModuleName getModuleName(char const *str);
+      virtual ModuleName getModuleName(char const *str, std::size_t len);
+
+      // Called to translate script type from ACS0 script number.
+      // Default behavior is to modulus 1000 the name.
+      virtual std::pair<Word /*type*/, Word /*name*/> getScriptTypeACS0(Word name)
+         {return {name / 1000, name % 1000};}
+
+      // Called to translate script type from ACSE SPTR.
+      // Default behavior is to return the type as-is.
+      virtual Word getScriptTypeACSE(Word type) {return type;}
+
+      String *getString(Word idx) {return &stringTable[~idx];}
+
+      String *getString(char const *first, char const *last)
+         {return &stringTable[{first, last}];}
+
+      String *getString(char const *str)
+         {return getString(str, std::strlen(str));}
+
+      String *getString(char const *str, std::size_t len)
+         {return &stringTable[{str, len}];}
+
+      String *getString(StringData const *data)
+         {return data ? &stringTable[*data] : nullptr;}
+
+      // Returns true if any contained scope is active and has an active thread.
+      bool hasActiveThread() const;
+
+      virtual void loadState(Serial &in);
+
+      // Prints an array to a print buffer. Default behavior is PrintArrayChar.
+      virtual void printArray(PrintBuf &buf, Array const &array, Word index, Word limit);
+
+      // Function to print Kill instructions. Default behavior is to print
+      // message to stderr.
+      virtual void printKill(Thread *thread, Word type, Word data);
+
+      // Deserializes a ModuleName. Default behavior is to load s and i.
+      virtual ModuleName readModuleName(Serial &in) const;
+
+      Script *readScript(Serial &in) const;
+      ScriptAction *readScriptAction(Serial &in) const;
+      void readScriptActions(Serial &in, ListLink<ScriptAction> &out) const;
+      ScriptName readScriptName(Serial &in) const;
+      String *readString(Serial &in) const;
+
+      virtual void refStrings();
+
+      virtual void resetStrings();
+
+      virtual void saveState(Serial &out) const;
+
+      // Serializes a ModuleName. Default behavior is to save s and i.
+      virtual void writeModuleName(Serial &out, ModuleName const &name) const;
+
+      void writeScript(Serial &out, Script *in) const;
+      void writeScriptAction(Serial &out, ScriptAction const *in) const;
+      void writeScriptActions(Serial &out, ListLink<ScriptAction> const &in) const;
+      void writeScriptName(Serial &out, ScriptName const &in) const;
+      void writeString(Serial &out, String const *in) const;
+
+      StringTable stringTable;
+
+      // Number of branches allowed per call to Thread::exec. Default of 0
+      // means no limit.
+      Word branchLimit;
+
+      // Default number of script variables. Default is 20.
+      Word scriptLocRegC;
+
+
+      // Prints an array to a print buffer, truncating elements of the array to
+      // fit char.
+      static void PrintArrayChar(PrintBuf &buf, Array const &array, Word index, Word limit);
+
+      // Prints an array to a print buffer, converting the array as a UTF-32
+      // sequence into a UTF-8 sequence.
+      static void PrintArrayUTF8(PrintBuf &buf, Array const &array, Word index, Word limit);
+
+      static constexpr Word ScriptLocRegCDefault = 20;
+
+   protected:
+      virtual Thread *allocThread();
+
+      // Called by callSpec after processing arguments. Default behavior is to
+      // do nothing and return 0.
+      virtual Word callSpecImpl(Thread *thread, Word spec, Word const *argV, Word argC);
+
+      virtual void loadModule(Module *module) = 0;
+
+      ListLink<ScriptAction> scriptAction;
+      ListLink<Thread>       threadFree;
+
+      Function  **funcV;
+      std::size_t funcC;
+
+   private:
+      struct PrivData;
+
+      void loadFunctions(Serial &in);
+      void loadGlobalScopes(Serial &in);
+      void loadScriptActions(Serial &in);
+      void loadStringTable(Serial &in);
+
+      void saveFunctions(Serial &out) const;
+      void saveGlobalScopes(Serial &out) const;
+      void saveScriptActions(Serial &out) const;
+      void saveStringTable(Serial &out) const;
+
+      PrivData *pd;
+   };
+}
+
+#endif//ACSVM__Environment_H__
+
diff --git a/src/acs/vm/ACSVM/Error.cpp b/src/acs/vm/ACSVM/Error.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..85de5cbbd14421282882db4417605322f757591d
--- /dev/null
+++ b/src/acs/vm/ACSVM/Error.cpp
@@ -0,0 +1,56 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Error classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Error.hpp"
+
+#include "BinaryIO.hpp"
+
+#include <cctype>
+#include <cstdio>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // SerialSignError constructor
+   //
+   SerialSignError::SerialSignError(Signature sig_, Signature got_)
+   {
+      auto sig = static_cast<std::uint32_t>(sig_);
+      auto got = static_cast<std::uint32_t>(got_);
+
+      WriteLE4(reinterpret_cast<Byte *>(buf + SigS), sig);
+      WriteLE4(reinterpret_cast<Byte *>(buf + GotS), got);
+      for(auto i : {SigS+0, SigS+1, SigS+2, SigS+3, GotS+0, GotS+1, GotS+2, GotS+3})
+         if(!std::isprint(buf[i]) && !std::isprint(buf[i] = ~buf[i])) buf[i] = ' ';
+
+      for(int i = 8; i--;) buf[Sig + i] = "0123456789ABCDEF"[sig & 0xF], sig >>= 4;
+      for(int i = 8; i--;) buf[Got + i] = "0123456789ABCDEF"[got & 0xF], got >>= 4;
+
+      msg = buf;
+   }
+
+   //
+   // SerialSignError destructor
+   //
+   SerialSignError::~SerialSignError()
+   {
+      delete[] msg;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Error.hpp b/src/acs/vm/ACSVM/Error.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..6d217a11158b531c005e5f0517486821e6ab642a
--- /dev/null
+++ b/src/acs/vm/ACSVM/Error.hpp
@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Error classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Error_H__
+#define ACSVM__Error_H__
+
+#include "Types.hpp"
+
+#include <stdexcept>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ReadError
+   //
+   // Generic exception class for errors occurring during bytecode reading.
+   //
+   class ReadError : public std::exception
+   {
+   public:
+      ReadError(char const *msg_ = "ACSVM::ReadError") : msg{msg_} {}
+
+      virtual char const *what() const noexcept {return msg;}
+
+      char const *const msg;
+   };
+
+   //
+   // SerialError
+   //
+   // Generic exception for errors during serialization.
+   //
+   class SerialError : public std::exception
+   {
+   public:
+      SerialError(char const *msg_) : msg{const_cast<char *>(msg_)} {}
+
+      virtual char const *what() const noexcept {return msg;}
+
+   protected:
+      SerialError() : msg{nullptr} {}
+
+      char *msg;
+   };
+
+   //
+   // SerialSignError
+   //
+   // Thrown due to signature mismatch.
+   //
+   class SerialSignError : public SerialError
+   {
+   public:
+      SerialSignError(Signature sig, Signature got);
+      ~SerialSignError();
+
+   private:
+      static constexpr std::size_t Sig = 29, SigS = 39;
+      static constexpr std::size_t Got = 49, GotS = 59;
+
+      char buf[sizeof("signature mismatch: expected XXXXXXXX (XXXX) got XXXXXXXX (XXXX)")] =
+                      "signature mismatch: expected XXXXXXXX (XXXX) got XXXXXXXX (XXXX)";
+   };
+}
+
+#endif//ACSVM__Error_H__
+
diff --git a/src/acs/vm/ACSVM/Function.cpp b/src/acs/vm/ACSVM/Function.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..53e5e8f6c65d47475b706e5bb0c3ba0895213a35
--- /dev/null
+++ b/src/acs/vm/ACSVM/Function.cpp
@@ -0,0 +1,48 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Function class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Function.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Function constructor
+   //
+   Function::Function(Module *module_, String *name_, Word idx_) :
+      module{module_},
+      name  {name_},
+      idx   {idx_},
+
+      argC   {0},
+      codeIdx{0},
+      locArrC{0},
+      locRegC{0},
+
+      flagRet{false}
+   {
+   }
+
+   //
+   // Function destructor
+   //
+   Function::~Function()
+   {
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Function.hpp b/src/acs/vm/ACSVM/Function.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..2186d537ba7aa6f26484beda728f03adf417d88e
--- /dev/null
+++ b/src/acs/vm/ACSVM/Function.hpp
@@ -0,0 +1,48 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Function class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Function_H__
+#define ACSVM__Function_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Function
+   //
+   class Function
+   {
+   public:
+      Function(Module *module, String *name, Word idx);
+      ~Function();
+
+      Module *module;
+      String *name;
+      Word    idx;
+
+      Word    argC;
+      Word    codeIdx;
+      Word    locArrC;
+      Word    locRegC;
+
+      bool flagRet : 1;
+   };
+}
+
+#endif//ACSVM__Function_H__
+
diff --git a/src/acs/vm/ACSVM/HashMap.hpp b/src/acs/vm/ACSVM/HashMap.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b509e5119e10171b37c627be4442758aed461bf0
--- /dev/null
+++ b/src/acs/vm/ACSVM/HashMap.hpp
@@ -0,0 +1,278 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// HashMap class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__HashMap_H__
+#define ACSVM__HashMap_H__
+
+#include "List.hpp"
+#include "Types.hpp"
+#include "Vector.hpp"
+
+#include <functional>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // HashMapGetKeyMem
+   //
+   // Used for HashMaps for which the Key is a member of T.
+   //
+   template<typename Key, typename T, Key const T::*KeyMem>
+   struct HashMapGetKeyMem
+   {
+      static Key const &Get(T *obj) {return obj->*KeyMem;}
+   };
+
+   //
+   // HashMapGetKeyObj
+   //
+   // Used for HashMaps for which the Key is a base class or the same as T.
+   //
+   template<typename Key, typename T>
+   struct HashMapGetKeyObj
+   {
+      static Key const &Get(T *obj) {return *obj;}
+   };
+
+   //
+   // HashMap
+   //
+   // Stores objects of type T that can be found by keys of type Key.
+   //
+   // GetKeyMem must be HashMapGetKeyMem or HashMapGetKeyObj.
+   //
+   // This class does not manage the lifetimes of the contained objects, and
+   // objects must be unlinked by the unlink function before being destructed.
+   // Although an exception is made to the latter if the clear function is
+   // called before any other.
+   //
+   template<typename Key, typename T, typename GetKey, ListLink<T> T::*LinkMem,
+      typename Hash = std::hash<Key>, typename KeyEqual = std::equal_to<Key>>
+   class HashMap
+   {
+   private:
+      //
+      // Iterator
+      //
+      template<typename Obj>
+      class IteratorBase
+      {
+      public:
+         //
+         // operator ++
+         //
+         IteratorBase<Obj> &operator ++ ()
+         {
+            for(;;)
+            {
+               // Traverse to next link.
+               link = link->next;
+
+               // If it has an object, we are done.
+               if(link->obj) break;
+
+               // Otherwise, we are at the current chain's head. So increment
+               // to the next chain. If at the last chain, we are done.
+               if(++link == last) break;
+            }
+
+            return *this;
+         }
+
+         IteratorBase<Obj> operator ++ (int) {auto i = *this; ++*this; return i;}
+
+         Obj &operator * () const {return *link->obj;}
+         Obj *operator -> () const {return link->obj;}
+
+         bool operator == (IteratorBase<Obj> const &iter) const
+            {return iter.link == link;}
+         bool operator != (IteratorBase<Obj> const &iter) const
+            {return iter.link != link;}
+
+
+         friend class HashMap;
+
+      private:
+         IteratorBase(ListLink<T> *link_, ListLink<T> *last_) :
+            link{link_}, last{last_} {if(link != last) ++*this;}
+
+         ListLink<T> *link, *last;
+      };
+
+   public:
+      using const_iterator = IteratorBase<T const>;
+      using iterator       = IteratorBase<T>;
+      using size_type      = std::size_t;
+
+
+      HashMap() : chainV{16}, objC{0}, growC{16} {}
+      HashMap(size_type count, size_type growC_) :
+         chainV{count}, objC{0}, growC{growC_} {}
+      ~HashMap() {clear();}
+
+      // begin
+      iterator begin() {return {chainV.begin(), chainV.end()};}
+
+      //
+      // clear
+      //
+      void clear()
+      {
+         for(auto &chain : chainV)
+         {
+            while(auto obj = chain.next->obj)
+               (obj->*LinkMem).unlink();
+         }
+
+         objC = 0;
+      }
+
+      // end
+      iterator end() {return {chainV.end(), chainV.end()};}
+
+      //
+      // find
+      //
+      T *find(Key const &key)
+      {
+         for(auto itr = chainV[hasher(key) % chainV.size()].next; itr->obj; itr = itr->next)
+         {
+            if(equal(key, GetKey::Get(itr->obj)))
+               return itr->obj;
+         }
+
+         return nullptr;
+      }
+
+      //
+      // free
+      //
+      // Unlinks and deletes all contained objects.
+      //
+      void free()
+      {
+         for(auto &chain : chainV)
+         {
+            while(auto obj = chain.next->obj)
+               (obj->*LinkMem).unlink(), delete obj;
+         }
+
+         objC = 0;
+      }
+
+      //
+      // insert
+      //
+      void insert(T *obj)
+      {
+         if(objC >= chainV.size())
+            resize(chainV.size() + chainV.size() / 2 + growC);
+
+         ++objC;
+         (obj->*LinkMem).insert(&chainV[hasher(GetKey::Get(obj)) % chainV.size()]);
+      }
+
+      //
+      // resize
+      //
+      // Reallocates to count chains.
+      //
+      void resize(size_type count)
+      {
+         auto oldChainV = std::move(chainV);
+         chainV.alloc(count);
+
+         for(auto &chain : oldChainV)
+         {
+            while(auto obj = chain.next->obj)
+               (obj->*LinkMem).relink(&chainV[hasher(GetKey::Get(obj)) % chainV.size()]);
+         }
+      }
+
+      // size
+      size_type size() const {return objC;}
+
+      //
+      // unlink
+      //
+      void unlink(T *obj)
+      {
+         --objC;
+         (obj->*LinkMem).unlink();
+      }
+
+   private:
+      Vector<ListLink<T>> chainV;
+      Hash                hasher;
+      KeyEqual            equal;
+
+      size_type objC;
+      size_type growC;
+   };
+
+   //
+   // HashMapKeyMem
+   //
+   // Convenience typedef for HashMapGetKeyMem-based HashMaps.
+   //
+   template<typename Key, typename T, Key const T::*KeyMem,
+      ListLink<T> T::*LinkMem, typename Hash = std::hash<Key>,
+      typename KeyEqual = std::equal_to<Key>>
+   using HashMapKeyMem = HashMap<Key, T, HashMapGetKeyMem<Key, T, KeyMem>,
+      LinkMem, Hash, KeyEqual>;
+
+   //
+   // HashMapKeyObj
+   //
+   // Convenience typedef for HashMapGetKeyObj-based HashMaps.
+   //
+   template<typename Key, typename T, ListLink<T> T::*LinkMem,
+      typename Hash = std::hash<Key>, typename KeyEqual = std::equal_to<Key>>
+   using HashMapKeyObj = HashMap<Key, T, HashMapGetKeyObj<Key, T>, LinkMem,
+      Hash, KeyEqual>;
+
+   //
+   // HashMapElem
+   //
+   // Wraps a type with a key and link for use in HashMap.
+   //
+   template<typename Key, typename T>
+   class HashMapElem
+   {
+   public:
+      HashMapElem(Key const &key_, T const &val_) :
+         key{key_}, val{val_}, link{this} {}
+
+      Key key;
+      T   val;
+
+      ListLink<HashMapElem<Key, T>> link;
+   };
+
+   //
+   // HashMapKeyExt
+   //
+   // Convenience typedef for HashMapElem-based HashMaps.
+   //
+   template<typename Key, typename T, typename Hash = std::hash<Key>,
+      typename KeyEqual = std::equal_to<Key>>
+   using HashMapKeyExt = HashMapKeyMem<Key, HashMapElem<Key, T>,
+      &HashMapElem<Key, T>::key, &HashMapElem<Key, T>::link, Hash, KeyEqual>;
+}
+
+#endif//ACSVM__HashMap_H__
+
diff --git a/src/acs/vm/ACSVM/HashMapFixed.hpp b/src/acs/vm/ACSVM/HashMapFixed.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..0886d9b97dfa2cc477c1829c646fda12d0390df0
--- /dev/null
+++ b/src/acs/vm/ACSVM/HashMapFixed.hpp
@@ -0,0 +1,144 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// HashMapFixed class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__HashMapFixed_H__
+#define ACSVM__HashMapFixed_H__
+
+#include "Types.hpp"
+
+#include <functional>
+#include <new>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // HashMapFixed
+   //
+   // Non-resizable hash map.
+   //
+   template<typename Key, typename T, typename Hash = std::hash<Key>>
+   class HashMapFixed
+   {
+   public:
+      struct Elem
+      {
+         Key   key;
+         T     val;
+         Elem *next;
+      };
+
+      using iterator   = Elem *;
+      using size_type  = std::size_t;
+      using value_type = Elem;
+
+
+      HashMapFixed() : hasher{}, table{nullptr}, elemV{nullptr}, elemC{0} {}
+      ~HashMapFixed() {free();}
+
+      //
+      // alloc
+      //
+      void alloc(size_type count)
+      {
+         if(elemV) free();
+
+         if(!count) return;
+
+         size_type sizeRaw = sizeof(Elem) * count + sizeof(Elem *) * count;
+
+         elemC = count;
+         elemV = static_cast<Elem *>(::operator new(sizeRaw));
+      }
+
+      // begin
+      iterator begin() {return elemV;}
+
+      //
+      // build
+      //
+      void build()
+      {
+         // Initialize table.
+         table = reinterpret_cast<Elem **>(elemV + elemC);
+         for(Elem **elem = table + elemC; elem != table;)
+            *--elem = nullptr;
+
+         // Insert elements.
+         for(Elem &elem : *this)
+         {
+            size_type hash = hasher(elem.key) % elemC;
+
+            elem.next = table[hash];
+            table[hash] = &elem;
+         }
+      }
+
+      // empty
+      bool empty() const {return !elemC;}
+
+      // end
+      iterator end() {return elemV + elemC;}
+
+      //
+      // find
+      //
+      T *find(Key const &key)
+      {
+         if(!table) return nullptr;
+
+         for(Elem *elem = table[hasher(key) % elemC]; elem; elem = elem->next)
+         {
+            if(elem->key == key)
+               return &elem->val;
+         }
+
+         return nullptr;
+      }
+
+      //
+      // free
+      //
+      void free()
+      {
+         if(table)
+         {
+            for(Elem *elem = elemV + elemC; elem != elemV;)
+               (--elem)->~Elem();
+
+            table = nullptr;
+         }
+
+         ::operator delete(elemV);
+
+         elemV = nullptr;
+         elemC = 0;
+      }
+
+      // size
+      size_type size() const {return elemC;}
+
+   private:
+      Hash hasher;
+
+      Elem    **table;
+      Elem     *elemV;
+      size_type elemC;
+   };
+}
+
+#endif//ACSVM__HashMapFixed_H__
+
diff --git a/src/acs/vm/ACSVM/ID.hpp b/src/acs/vm/ACSVM/ID.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..fbb113cce0e457519b7be27ac3b749d43257e90d
--- /dev/null
+++ b/src/acs/vm/ACSVM/ID.hpp
@@ -0,0 +1,50 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Numeric identifiers.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__ID_H__
+#define ACSVM__ID_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   constexpr std::uint32_t MakeID(char c0, char c1, char c2, char c3);
+   constexpr std::uint32_t MakeID(char const (&s)[5]);
+
+   //
+   // MakeID
+   //
+   constexpr std::uint32_t MakeID(char c0, char c1, char c2, char c3)
+   {
+      return
+         (static_cast<std::uint32_t>(c0) <<  0) |
+         (static_cast<std::uint32_t>(c1) <<  8) |
+         (static_cast<std::uint32_t>(c2) << 16) |
+         (static_cast<std::uint32_t>(c3) << 24);
+   }
+
+   //
+   // MakeID
+   //
+   constexpr std::uint32_t MakeID(char const (&s)[5])
+   {
+      return MakeID(s[0], s[1], s[2], s[3]);
+   }
+}
+
+#endif//ACSVM__ID_H__
+
diff --git a/src/acs/vm/ACSVM/Init.cpp b/src/acs/vm/ACSVM/Init.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..66e65bc8113d0b0701e1567aa5d8026ea621599f
--- /dev/null
+++ b/src/acs/vm/ACSVM/Init.cpp
@@ -0,0 +1,149 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Initializer handling.
+//
+//-----------------------------------------------------------------------------
+
+#include "Init.hpp"
+
+#include "Array.hpp"
+#include "Function.hpp"
+#include "Module.hpp"
+#include "String.hpp"
+
+#include <vector>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ArrayInit::PrivData
+   //
+   struct ArrayInit::PrivData
+   {
+      std::vector<WordInit> initV;
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+
+   //
+   // ArrayInit constructor
+   //
+   ArrayInit::ArrayInit() :
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // ArrayInit destructor
+   //
+   ArrayInit::~ArrayInit()
+   {
+      delete pd;
+   }
+
+   //
+   // ArrayInit::apply
+   //
+   void ArrayInit::apply(Array &arr, Module *module)
+   {
+      Word idx = 0;
+      for(WordInit &init : pd->initV)
+      {
+         Word value = init.getValue(module);
+         if(value) arr[idx] = value;
+         ++idx;
+      }
+   }
+
+   //
+   // ArrayInit::finish
+   //
+   void ArrayInit::finish()
+   {
+      // Clear out trailing zeroes.
+      while(!pd->initV.empty() && !pd->initV.back())
+         pd->initV.pop_back();
+
+      // Shrink vector.
+      pd->initV.shrink_to_fit();
+
+      // TODO: Break up initialization data into nonzero ranges.
+   }
+
+   //
+   // ArrayInit::reserve
+   //
+   void ArrayInit::reserve(Word count)
+   {
+      pd->initV.resize(count, 0);
+   }
+
+   //
+   // ArrayInit::setTag
+   //
+   void ArrayInit::setTag(Word idx, InitTag tag)
+   {
+      if(idx >= pd->initV.size())
+         pd->initV.resize(idx + 1, 0);
+
+      pd->initV[idx].tag = tag;
+   }
+
+   //
+   // ArrayInit::setVal
+   //
+   void ArrayInit::setVal(Word idx, Word val)
+   {
+      if(idx >= pd->initV.size())
+         pd->initV.resize(idx + 1, 0);
+
+      pd->initV[idx].val = val;
+   }
+
+   //
+   // WordInit::getValue
+   //
+   Word WordInit::getValue(Module *module) const
+   {
+      switch(tag)
+      {
+      case InitTag::Integer:
+         return val;
+
+      case InitTag::Function:
+         if(val < module->functionV.size() && module->functionV[val])
+            return module->functionV[val]->idx;
+         else
+            return val;
+
+      case InitTag::String:
+         if(val < module->stringV.size())
+            return ~module->stringV[val]->idx;
+         else
+            return val;
+      }
+
+      return val;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Init.hpp b/src/acs/vm/ACSVM/Init.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..aecbf700b9673b807173ac088d2ff24c60d2599d
--- /dev/null
+++ b/src/acs/vm/ACSVM/Init.hpp
@@ -0,0 +1,80 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Initializer handling.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Init_H__
+#define ACSVM__Init_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // InitTag
+   //
+   enum class InitTag
+   {
+      Integer,
+      Function,
+      String,
+   };
+
+   //
+   // ArrayInit
+   //
+   class ArrayInit
+   {
+   public:
+      ArrayInit();
+      ~ArrayInit();
+
+      void apply(Array &arr, Module *module);
+
+      void finish();
+
+      void reserve(Word count);
+
+      void setTag(Word idx, InitTag tag);
+      void setVal(Word idx, Word    val);
+
+   private:
+      struct PrivData;
+
+      PrivData *pd;
+   };
+
+   //
+   // WordInit
+   //
+   class WordInit
+   {
+   public:
+      WordInit() = default;
+      WordInit(Word val_) : val{val_}, tag{InitTag::Integer} {}
+      WordInit(Word val_, InitTag tag_) : val{val_}, tag{tag_} {}
+
+      explicit operator bool () const
+         {return val || tag != InitTag::Integer;}
+
+      Word getValue(Module *module) const;
+
+      Word    val;
+      InitTag tag;
+   };
+}
+
+#endif//ACSVM__Init_H__
+
diff --git a/src/acs/vm/ACSVM/Jump.cpp b/src/acs/vm/ACSVM/Jump.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fa6efed3789239195369ed473fa416290a4e302c
--- /dev/null
+++ b/src/acs/vm/ACSVM/Jump.cpp
@@ -0,0 +1,44 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Jump class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Jump.hpp"
+
+#include "BinaryIO.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // JumpMap::loadJumps
+   //
+   void JumpMap::loadJumps(Byte const *data, std::size_t count)
+   {
+      table.alloc(count);
+      std::size_t iter = 0;
+
+      for(auto &jump : table)
+      {
+         Word caseVal = ReadLE4(data + iter); iter += 4;
+         Word codeIdx = ReadLE4(data + iter); iter += 4;
+         new(&jump) HashMapFixed<Word, Word>::Elem{caseVal, codeIdx, nullptr};
+      }
+
+      table.build();
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Jump.hpp b/src/acs/vm/ACSVM/Jump.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..1656de037aa2952cda4eeff771334854d7624502
--- /dev/null
+++ b/src/acs/vm/ACSVM/Jump.hpp
@@ -0,0 +1,49 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Jump class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Jump_H__
+#define ACSVM__Jump_H__
+
+#include "HashMapFixed.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Jump
+   //
+   // Dynamic jump target.
+   //
+   class Jump
+   {
+   public:
+      Word codeIdx;
+   };
+
+   //
+   // JumpMap
+   //
+   class JumpMap
+   {
+   public:
+      void loadJumps(Byte const *data, std::size_t count);
+
+      HashMapFixed<Word, Word> table;
+   };
+}
+
+#endif//ACSVM__Jump_H__
+
diff --git a/src/acs/vm/ACSVM/List.hpp b/src/acs/vm/ACSVM/List.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..3c65a2a350a9cb91b4b0f8bffac1038ebd0fb6c0
--- /dev/null
+++ b/src/acs/vm/ACSVM/List.hpp
@@ -0,0 +1,114 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Linked list handling.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__List_H__
+#define ACSVM__List_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ListLink
+   //
+   template<typename T>
+   class ListLink
+   {
+   private:
+      //
+      // IteratorBase
+      //
+      template<typename Obj>
+      class IteratorBase
+      {
+      public:
+         IteratorBase<Obj> &operator ++ () {link = link->next; return *this;}
+         IteratorBase<Obj> operator ++ (int) {auto i = *this; ++*this; return i;}
+
+         Obj &operator * () const {return *link->obj;}
+         Obj *operator -> () const {return link->obj;}
+
+         bool operator == (IteratorBase<Obj> const &iter) const
+            {return iter.link == link;}
+         bool operator != (IteratorBase<Obj> const &iter) const
+            {return iter.link != link;}
+
+
+         friend class ListLink;
+
+      private:
+         IteratorBase(ListLink<T> const *link_) : link{link_} {}
+
+         ListLink<T> const *link;
+      };
+
+   public:
+      ListLink() : obj{nullptr}, prev{this}, next{this} {}
+      ListLink(ListLink<T> const &) = delete;
+      ListLink(T *obj_) : obj{obj_}, prev{this}, next{this} {}
+      ListLink(T *obj_, ListLink<T> &&link) :
+         obj{obj_}, prev{link.prev}, next{link.next}
+         {prev->next = next->prev = this; link.prev = link.next = &link;}
+      ~ListLink() {unlink();}
+
+      // begin
+      IteratorBase<T>       begin()       {return next;}
+      IteratorBase<T const> begin() const {return next;}
+
+      // end
+      IteratorBase<T>       end()       {return this;}
+      IteratorBase<T const> end() const {return this;}
+
+      //
+      // insert
+      //
+      void insert(ListLink<T> *head)
+      {
+         (prev = head->prev)->next = this;
+         (next = head      )->prev = this;
+      }
+
+      void relink(ListLink<T> *head) {unlink(); insert(head);}
+
+      //
+      // size
+      //
+      std::size_t size() const
+      {
+         std::size_t count = 0;
+         for(auto const &o : *this) (void)o, ++count;
+         return count;
+      }
+
+      //
+      // unlink
+      //
+      void unlink()
+      {
+         prev->next = next;
+         next->prev = prev;
+
+         prev = next = this;
+      }
+
+      T *const obj;
+      ListLink<T> *prev, *next;
+   };
+}
+
+#endif//ACSVM__List_H__
+
diff --git a/src/acs/vm/ACSVM/Module.cpp b/src/acs/vm/ACSVM/Module.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..95626be7b5ac878c268d346e2704e7d752cc34e7
--- /dev/null
+++ b/src/acs/vm/ACSVM/Module.cpp
@@ -0,0 +1,139 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Module class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Module.hpp"
+
+#include "Array.hpp"
+#include "Environment.hpp"
+#include "Function.hpp"
+#include "Init.hpp"
+#include "Jump.hpp"
+#include "Script.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // ModuleName::hash
+   //
+   std::size_t ModuleName::hash() const
+   {
+      return s->hash + std::hash<void*>()(p) + i;
+   }
+
+   //
+   // Module constructor
+   //
+   Module::Module(Environment *env_, ModuleName const &name_) :
+      env{env_},
+      name{name_},
+
+      hashLink{this},
+
+      isACS0{false},
+      loaded{false}
+   {
+   }
+
+   //
+   // Module destructor
+   //
+   Module::~Module()
+   {
+      reset();
+   }
+
+   //
+   // Module::refStrings
+   //
+   void Module::refStrings() const
+   {
+      if(name.s) name.s->ref = true;
+
+      for(auto &s : arrImpV)   if(s) s->ref = true;
+      for(auto &s : arrNameV)  if(s) s->ref = true;
+      for(auto &s : funcNameV) if(s) s->ref = true;
+      for(auto &s : regImpV)   if(s) s->ref = true;
+      for(auto &s : regNameV)  if(s) s->ref = true;
+      for(auto &s : scrNameV)  if(s) s->ref = true;
+      for(auto &s : stringV)   if(s) s->ref = true;
+
+      for(auto &func : functionV)
+         if(func && func->name) func->name->ref = true;
+
+      for(auto &scr : scriptV)
+         if(scr.name.s) scr.name.s->ref = true;
+   }
+
+   //
+   // Module::reset
+   //
+   void Module::reset()
+   {
+      // Unload locally defined functions from env.
+      for(Function *&func : functionV)
+      {
+         if(func && func->module == this)
+            env->freeFunction(func);
+      }
+
+      arrImpV.free();
+      arrInitV.free();
+      arrNameV.free();
+      arrSizeV.free();
+      codeV.free();
+      funcNameV.free();
+      functionV.free();
+      importV.free();
+      jumpV.free();
+      jumpMapV.free();
+      regImpV.free();
+      regInitV.free();
+      regNameV.free();
+      scrNameV.free();
+      scriptV.free();
+      stringV.free();
+
+      isACS0 = false;
+      loaded = false;
+   }
+
+   //
+   // Module::resetStrings
+   //
+   void Module::resetStrings()
+   {
+      name.s = env->getString(name.s);
+
+      for(auto &s : arrImpV)   s = env->getString(s);
+      for(auto &s : arrNameV)  s = env->getString(s);
+      for(auto &s : funcNameV) s = env->getString(s);
+      for(auto &s : regImpV)   s = env->getString(s);
+      for(auto &s : regNameV)  s = env->getString(s);
+      for(auto &s : scrNameV)  s = env->getString(s);
+      for(auto &s : stringV)   s = env->getString(s);
+
+      for(auto &func : functionV)
+         if(func) func->name = env->getString(func->name);
+
+      for(auto &scr : scriptV)
+         scr.name.s = env->getString(scr.name.s);
+   }
+}
+
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Module.hpp b/src/acs/vm/ACSVM/Module.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..b2b46e6132443842ea8d9165f9a76e9d9378e305
--- /dev/null
+++ b/src/acs/vm/ACSVM/Module.hpp
@@ -0,0 +1,178 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Module class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Module_H__
+#define ACSVM__Module_H__
+
+#include "ID.hpp"
+#include "List.hpp"
+#include "Vector.hpp"
+
+#include <functional>
+#include <memory>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ModuleName
+   //
+   // Stores a Module's name. Name semantics are user-defined and must provide
+   // a (user-defined) mapping from name to bytecode data. The names are used
+   // internally only for determining if a specific module has already been
+   // loaded. That is, two ModuleNames should compare equal if and only if they
+   // designate the same bytecode data.
+   //
+   class ModuleName
+   {
+   public:
+      ModuleName(String *s_, void *p_, std::size_t i_) : s{s_}, p{p_}, i{i_} {}
+
+      bool operator == (ModuleName const &name) const
+         {return s == name.s && p == name.p && i == name.i;}
+      bool operator != (ModuleName const &name) const
+         {return s != name.s || p != name.p || i != name.i;}
+
+      std::size_t hash() const;
+
+      // String value. May be null.
+      String *s;
+
+      // Arbitrary pointer value.
+      void *p;
+
+      // Arbitrary integer value.
+      std::size_t i;
+   };
+
+   //
+   // Module
+   //
+   // Represents an ACS bytecode module.
+   //
+   class Module
+   {
+   public:
+      Module(Environment *env, ModuleName const &name);
+      ~Module();
+
+      void readBytecode(Byte const *data, std::size_t size);
+
+      void refStrings() const;
+
+      void reset();
+
+      void resetStrings();
+
+      Environment *env;
+      ModuleName   name;
+
+      Vector<String *>   arrImpV;
+      Vector<ArrayInit>  arrInitV;
+      Vector<String *>   arrNameV;
+      Vector<Word>       arrSizeV;
+      Vector<Word>       codeV;
+      Vector<String *>   funcNameV;
+      Vector<Function *> functionV;
+      Vector<Module *>   importV;
+      Vector<Jump>       jumpV;
+      Vector<JumpMap>    jumpMapV;
+      Vector<String *>   regImpV;
+      Vector<WordInit>   regInitV;
+      Vector<String *>   regNameV;
+      Vector<String *>   scrNameV;
+      Vector<Script>     scriptV;
+      Vector<String *>   stringV;
+
+      ListLink<Module> hashLink;
+
+      bool isACS0;
+      bool loaded;
+
+
+      static std::pair<
+         std::unique_ptr<Byte[]> /*data*/,
+         std::size_t             /*size*/>
+      DecryptStringACSE(Byte const *data, std::size_t size, std::size_t iter);
+
+      static std::unique_ptr<char[]> ParseStringACS0(Byte const *first,
+         Byte const *last, std::size_t len);
+
+      static std::tuple<
+         Byte const * /*begin*/,
+         Byte const * /*end*/,
+         std::size_t  /*len*/>
+      ScanStringACS0(Byte const *data, std::size_t size, std::size_t iter);
+
+   private:
+      bool chunkIterACSE(Byte const *data, std::size_t size,
+         bool (Module::*chunker)(Byte const *, std::size_t, Word));
+
+      void chunkStrTabACSE(Vector<String *> &strV,
+         Byte const *data, std::size_t size, bool junk);
+
+      bool chunkerACSE_AIMP(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_AINI(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_ARAY(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_ASTR(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_ATAG(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_FARY(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_FNAM(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_FUNC(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_JUMP(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_LOAD(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_MEXP(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_MIMP(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_MINI(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_MSTR(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SARY(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SFLG(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SNAM(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SPTR8(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SPTR12(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_STRE(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_STRL(Byte const *data, std::size_t size, Word chunkName);
+      bool chunkerACSE_SVCT(Byte const *data, std::size_t size, Word chunkName);
+
+      void readBytecodeACS0(Byte const *data, std::size_t size);
+      void readBytecodeACSE(Byte const *data, std::size_t size,
+         bool compressed, std::size_t iter = 4);
+
+      void readChunksACSE(Byte const *data, std::size_t size, bool fakeACS0);
+
+      void readCodeACS0(Byte const *data, std::size_t size, bool compressed);
+
+      String *readStringACS0(Byte const *data, std::size_t size, std::size_t iter);
+
+      void setScriptNameTypeACSE(Script *scr, Word nameInt, Word type);
+   };
+}
+
+namespace std
+{
+   //
+   // hash<::ACSVM::ModuleName>
+   //
+   template<>
+   struct hash<::ACSVM::ModuleName>
+   {
+      size_t operator () (::ACSVM::ModuleName const &name) const
+         {return name.hash();}
+   };
+}
+
+#endif//ACSVM__Module_H__
+
diff --git a/src/acs/vm/ACSVM/ModuleACS0.cpp b/src/acs/vm/ACSVM/ModuleACS0.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7ae16ac2f2efd1ce0bd4d16b1a8a3f4342984907
--- /dev/null
+++ b/src/acs/vm/ACSVM/ModuleACS0.cpp
@@ -0,0 +1,333 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Module class bytecode reading.
+//
+//-----------------------------------------------------------------------------
+
+#include "Module.hpp"
+
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "Error.hpp"
+#include "Jump.hpp"
+#include "Script.hpp"
+#include "Tracer.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Module::reaBytecode
+   //
+   void Module::readBytecode(Byte const *data, std::size_t size)
+   {
+      try
+      {
+         if(size < 4) throw ReadError();
+
+         switch(ReadLE4(data))
+         {
+         case MakeID("ACS\0"):
+            readBytecodeACS0(data, size);
+            break;
+
+         case MakeID("ACSE"):
+            readBytecodeACSE(data, size, false);
+            break;
+
+         case MakeID("ACSe"):
+            readBytecodeACSE(data, size, true);
+            break;
+         }
+      }
+      catch(...)
+      {
+         // If an exception occurs before module is fully loaded, reset it.
+         if(!loaded)
+            reset();
+
+         throw;
+      }
+   }
+
+   //
+   // Module::readBytecodeACS0
+   //
+   void Module::readBytecodeACS0(Byte const *data, std::size_t size)
+   {
+      std::size_t iter;
+
+      // Read table index.
+      if(size < 8) throw ReadError();
+      iter = ReadLE4(data + 4);
+      if(iter > size) throw ReadError();
+
+      // Check for ACSE header behind indicated table.
+      if(iter >= 8)
+      {
+         switch(ReadLE4(data + (iter - 4)))
+         {
+         case MakeID("ACSE"):
+            return readBytecodeACSE(data, size, false, iter - 8);
+
+         case MakeID("ACSe"):
+            return readBytecodeACSE(data, size, true, iter - 8);
+         }
+      }
+
+      // Mark as ACS0.
+      isACS0 = true;
+
+      // Read script table.
+
+      // Read script count.
+      if(size - iter < 4) throw ReadError();
+      scriptV.alloc(ReadLE4(data + iter), this); iter += 4;
+
+      // Read scripts.
+      if(size - iter < scriptV.size() * 12) throw ReadError();
+      for(Script &scr : scriptV)
+      {
+         scr.name.i  = ReadLE4(data + iter); iter += 4;
+         scr.codeIdx = ReadLE4(data + iter); iter += 4;
+         scr.argC    = ReadLE4(data + iter); iter += 4;
+
+         std::tie(scr.type, scr.name.i) = env->getScriptTypeACS0(scr.name.i);
+      }
+
+      // Read string table.
+
+      // Read string count.
+      if(size - iter < 4) throw ReadError();
+      stringV.alloc(ReadLE4(data + iter)); iter += 4;
+
+      // Read strings.
+      if(size - iter < stringV.size() * 4) throw ReadError();
+      for(String *&str : stringV)
+      {
+         str = readStringACS0(data, size, ReadLE4(data + iter)); iter += 4;
+      }
+
+      // Read code.
+      readCodeACS0(data, size, false);
+
+      loaded = true;
+   }
+
+   //
+   // Module::readCodeACS0
+   //
+   void Module::readCodeACS0(Byte const *data, std::size_t size, bool compressed)
+   {
+      TracerACS0 tracer{env, data, size, compressed};
+
+      // Trace code paths from this module.
+      tracer.trace(this);
+
+      codeV.alloc(tracer.codeC);
+      jumpMapV.alloc(tracer.jumpMapC);
+
+      tracer.translate(this);
+   }
+
+   //
+   // Module::readStringACS0
+   //
+   String *Module::readStringACS0(Byte const *data, std::size_t size, std::size_t iter)
+   {
+      Byte const *begin, *end;
+      std::size_t len;
+      std::tie(begin, end, len) = ScanStringACS0(data, size, iter);
+
+      // If result length is same as input length, no processing is needed.
+      if(static_cast<std::size_t>(end - begin) == len)
+      {
+         // Byte is always unsigned char, which is allowed to alias with char.
+         return env->getString(
+            reinterpret_cast<char const *>(begin),
+            reinterpret_cast<char const *>(end));
+      }
+      else
+         return env->getString(ParseStringACS0(begin, end, len).get(), len);
+   }
+
+   //
+   // Module::ParseStringACS0
+   //
+   std::unique_ptr<char[]> Module::ParseStringACS0(Byte const *first,
+      Byte const *last, std::size_t len)
+   {
+      std::unique_ptr<char[]> buf{new char[len + 1]};
+      char                   *bufItr = buf.get();
+
+      for(Byte const *s = first; s != last;)
+      {
+         if(*s == '\\')
+         {
+            if(++s == last)
+               break;
+
+            switch(*s)
+            {
+            case 'a': *bufItr++ += '\a';   ++s; break;
+            case 'b': *bufItr++ += '\b';   ++s; break;
+            case 'c': *bufItr++ += '\x1C'; ++s; break; // ZDoom color escape
+            case 'f': *bufItr++ += '\f';   ++s; break;
+            case 'r': *bufItr++ += '\r';   ++s; break;
+            case 'n': *bufItr++ += '\n';   ++s; break;
+            case 't': *bufItr++ += '\t';   ++s; break;
+            case 'v': *bufItr++ += '\v';   ++s; break;
+
+            case '0': case '1': case '2': case '3':
+            case '4': case '5': case '6': case '7':
+               for(unsigned int i = 3, c = 0; i-- && s != last; ++s)
+               {
+                  switch(*s)
+                  {
+                  case '0': c = c * 8 + 00; continue;
+                  case '1': c = c * 8 + 01; continue;
+                  case '2': c = c * 8 + 02; continue;
+                  case '3': c = c * 8 + 03; continue;
+                  case '4': c = c * 8 + 04; continue;
+                  case '5': c = c * 8 + 05; continue;
+                  case '6': c = c * 8 + 06; continue;
+                  case '7': c = c * 8 + 07; continue;
+                  }
+
+                  *bufItr++ = c;
+                  break;
+               }
+               break;
+
+            case 'X': case 'x':
+               ++s;
+               for(unsigned int i = 2, c = 0; i-- && s != last; ++s)
+               {
+                  switch(*s)
+                  {
+                  case '0': c = c * 16 + 0x0; continue;
+                  case '1': c = c * 16 + 0x1; continue;
+                  case '2': c = c * 16 + 0x2; continue;
+                  case '3': c = c * 16 + 0x3; continue;
+                  case '4': c = c * 16 + 0x4; continue;
+                  case '5': c = c * 16 + 0x5; continue;
+                  case '6': c = c * 16 + 0x6; continue;
+                  case '7': c = c * 16 + 0x7; continue;
+                  case '8': c = c * 16 + 0x8; continue;
+                  case '9': c = c * 16 + 0x9; continue;
+                  case 'A': c = c * 16 + 0xA; continue;
+                  case 'B': c = c * 16 + 0xB; continue;
+                  case 'C': c = c * 16 + 0xC; continue;
+                  case 'D': c = c * 16 + 0xD; continue;
+                  case 'E': c = c * 16 + 0xE; continue;
+                  case 'F': c = c * 16 + 0xF; continue;
+                  case 'a': c = c * 16 + 0xa; continue;
+                  case 'b': c = c * 16 + 0xb; continue;
+                  case 'c': c = c * 16 + 0xc; continue;
+                  case 'd': c = c * 16 + 0xd; continue;
+                  case 'e': c = c * 16 + 0xe; continue;
+                  case 'f': c = c * 16 + 0xf; continue;
+                  }
+
+                  *bufItr++ = c;
+                  break;
+               }
+               break;
+
+            default:
+               *bufItr++ = *s++;
+               break;
+            }
+         }
+         else
+            *bufItr++ = *s++;
+      }
+
+      *bufItr++ = '\0';
+
+      return buf;
+   }
+
+   //
+   // Module::ScanStringACS0
+   //
+   std::tuple<
+      Byte const * /*begin*/,
+      Byte const * /*end*/,
+      std::size_t  /*len*/>
+   Module::ScanStringACS0(Byte const *data, std::size_t size, std::size_t iter)
+   {
+      if(iter > size) throw ReadError();
+
+      Byte const *begin = data + iter;
+      Byte const *end   = data + size;
+      Byte const *s     = begin;
+      std::size_t len   = 0;
+
+      while(s != end && *s)
+      {
+         if(*s++ == '\\')
+         {
+            if(s == end || !*s)
+               break;
+
+            switch(*s++)
+            {
+            case '0': case '1': case '2': case '3':
+            case '4': case '5': case '6': case '7':
+               for(int i = 2; i-- && s != end; ++s)
+               {
+                  switch(*s)
+                  {
+                  case '0': case '1': case '2': case '3':
+                  case '4': case '5': case '6': case '7':
+                     continue;
+                  }
+
+                  break;
+               }
+               break;
+
+            case 'X': case 'x':
+               for(int i = 2; i-- && s != end; ++s)
+               {
+                  switch(*s)
+                  {
+                  case '0': case '1': case '2': case '3': case '4':
+                  case '5': case '6': case '7': case '8': case '9':
+                  case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+                  case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+                     continue;
+                  }
+
+                  break;
+               }
+               break;
+
+            default:
+               break;
+            }
+         }
+
+         ++len;
+      }
+
+      // If not terminated by a null, string is malformed.
+      if(s == end) throw ReadError();
+
+      return std::make_tuple(begin, s, len);
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/ModuleACSE.cpp b/src/acs/vm/ACSVM/ModuleACSE.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a38a29f5be3f26aa8652f1d6fdfedd7c997883e1
--- /dev/null
+++ b/src/acs/vm/ACSVM/ModuleACSE.cpp
@@ -0,0 +1,860 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Module class bytecode reading.
+//
+//-----------------------------------------------------------------------------
+
+#include "Module.hpp"
+
+#include "Array.hpp"
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "Error.hpp"
+#include "Function.hpp"
+#include "Init.hpp"
+#include "Jump.hpp"
+#include "Script.hpp"
+
+#include <algorithm>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Module::chunkIterACSE
+   //
+   bool Module::chunkIterACSE(Byte const *data, std::size_t size,
+      bool (Module::*chunker)(Byte const *, std::size_t, Word))
+   {
+      std::size_t iter = 0;
+
+      while(iter != size)
+      {
+         // Need space for header.
+         if(size - iter < 8) throw ReadError();
+
+         // Read header.
+         Word chunkName = ReadLE4(data + iter + 0);
+         Word chunkSize = ReadLE4(data + iter + 4);
+
+         // Consume header.
+         iter += 8;
+
+         // Need space for payload.
+         if(size - iter < chunkSize) throw ReadError();
+
+         // Read payload.
+         if((this->*chunker)(data + iter, chunkSize, chunkName))
+            return true;
+
+         // Consume payload.
+         iter += chunkSize;
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkStrTabACSE
+   //
+   void Module::chunkStrTabACSE(Vector<String *> &strV,
+      Byte const *data, std::size_t size, bool junk)
+   {
+      std::size_t iter = 0;
+
+      if(junk)
+      {
+         if(size < 12) throw ReadError();
+
+         /*junk   = ReadLE4(data + iter);*/ iter += 4;
+         strV.alloc(ReadLE4(data + iter));  iter += 4;
+         /*junk   = ReadLE4(data + iter);*/ iter += 4;
+      }
+      else
+      {
+         if(size < 4) throw ReadError();
+
+         strV.alloc(ReadLE4(data + iter)); iter += 4;
+      }
+
+      if(size - iter < strV.size() * 4) throw ReadError();
+      for(String *&str : strV)
+      {
+         str = readStringACS0(data, size, ReadLE4(data + iter)); iter += 4;
+      }
+   }
+
+   //
+   // Module::chunkerACSE_AIMP
+   //
+   bool Module::chunkerACSE_AIMP(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("AIMP")) return false;
+
+      if(size < 4) throw ReadError();
+
+      // Chunk starts with a number of entries. However, that is redundant with
+      // just checking for the end of the chunk as in MIMP, so do that.
+
+      // Determine highest index.
+      Word arrC = 0;
+      for(std::size_t iter = 4; iter != size;)
+      {
+         if(size - iter < 8) throw ReadError();
+
+         Word idx = ReadLE4(data + iter);   iter += 4;
+         /*   len = LeadLE4(data + iter);*/ iter += 4;
+
+         arrC = std::max<Word>(arrC, idx + 1);
+
+         Byte const *next;
+         std::tie(std::ignore, next, std::ignore) = ScanStringACS0(data, size, iter);
+
+         iter = next - data + 1;
+      }
+
+      // Read imports.
+      arrImpV.alloc(arrC);
+      for(std::size_t iter = 4; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter);   iter += 4;
+         /*   len = LeadLE4(data + iter);*/ iter += 4;
+
+         Byte const *next;
+         std::size_t len;
+         std::tie(std::ignore, next, len) = ScanStringACS0(data, size, iter);
+
+         std::unique_ptr<char[]> str = ParseStringACS0(data + iter, next, len);
+
+         arrImpV[idx] = env->getString(str.get(), len);
+
+         iter = next - data + 1;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_AINI
+   //
+   bool Module::chunkerACSE_AINI(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("AINI")) return false;
+
+      if(size < 4 || size % 4) throw ReadError();
+
+      Word idx = ReadLE4(data);
+
+      // Silently ignore out of bounds initializers.
+      if(idx >= arrInitV.size()) return false;
+
+      auto &init = arrInitV[idx];
+      for(std::size_t iter = 4; iter != size; iter += 4)
+         init.setVal(iter / 4 - 1, ReadLE4(data + iter));
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_ARAY
+   //
+   bool Module::chunkerACSE_ARAY(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("ARAY")) return false;
+
+      if(size % 8) throw ReadError();
+
+      Word arrC = 0;
+
+      // Determine highest index.
+      for(std::size_t iter = 0; iter != size; iter += 8)
+         arrC = std::max<Word>(arrC, ReadLE4(data + iter) + 1);
+
+      arrNameV.alloc(arrC);
+      arrInitV.alloc(arrC);
+      arrSizeV.alloc(arrC);
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter); iter += 4;
+         Word len = ReadLE4(data + iter); iter += 4;
+
+         arrInitV[idx].reserve(len);
+         arrSizeV[idx] = len;
+
+         // Use names from MEXP.
+         if(idx < regNameV.size())
+         {
+            arrNameV[idx] = regNameV[idx];
+            regNameV[idx] = nullptr;
+         }
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_ASTR
+   //
+   bool Module::chunkerACSE_ASTR(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("ASTR")) return false;
+
+      if(size % 4) throw ReadError();
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter); iter += 4;
+
+         // Silently ignore out of bounds initializers.
+         if(idx >= arrInitV.size()) continue;
+
+         auto &init = arrInitV[idx];
+         for(Word i = 0, e = arrSizeV[idx]; i != e; ++i)
+            init.setTag(i, InitTag::String);
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_ATAG
+   //
+   bool Module::chunkerACSE_ATAG(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("ATAG")) return false;
+
+      if(size < 5 || data[0]) throw ReadError();
+
+      Word idx = ReadLE4(data + 1);
+
+      // Silently ignore out of bounds initializers.
+      if(idx >= arrInitV.size()) return false;
+
+      auto &init = arrInitV[idx];
+      for(std::size_t iter = 5; iter != size; ++iter)
+      {
+         switch(data[iter])
+         {
+         case 0: init.setTag(iter - 5, InitTag::Integer);  break;
+         case 1: init.setTag(iter - 5, InitTag::String);   break;
+         case 2: init.setTag(iter - 5, InitTag::Function); break;
+         }
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_FARY
+   //
+   bool Module::chunkerACSE_FARY(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("FARY")) return false;
+
+      if(size < 2 || (size - 2) % 4) throw ReadError();
+
+      Word        idx  = ReadLE2(data);
+      std::size_t arrC = (size - 2) / 4;
+
+      if(idx < functionV.size() && functionV[idx])
+         functionV[idx]->locArrC = arrC;
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_FNAM
+   //
+   bool Module::chunkerACSE_FNAM(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("FNAM")) return false;
+
+      chunkStrTabACSE(funcNameV, data, size, false);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_FUNC
+   //
+   bool Module::chunkerACSE_FUNC(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("FUNC")) return false;
+
+      if(size % 8) throw ReadError();
+
+      // Read functions.
+      functionV.alloc(size / 8);
+
+      std::size_t iter = 0;
+      for(Function *&func : functionV)
+      {
+         Word idx     = iter / 8;
+         Word argC    = ReadLE1(data + iter); iter += 1;
+         Word locRegC = ReadLE1(data + iter); iter += 1;
+         Word flags   = ReadLE2(data + iter); iter += 2;
+         Word codeIdx = ReadLE4(data + iter); iter += 4;
+
+         // Ignore undefined functions for now.
+         if(!codeIdx) continue;
+
+         String   *funcName = idx < funcNameV.size() ? funcNameV[idx] : nullptr;
+         Function *function = env->getFunction(this, funcName);
+
+         function->argC    = argC;
+         function->locRegC = locRegC;
+         function->flagRet = flags & 0x0001;
+         function->codeIdx = codeIdx;
+
+         func = function;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_JUMP
+   //
+   bool Module::chunkerACSE_JUMP(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("JUMP")) return false;
+
+      if(size % 4) throw ReadError();
+
+      // Read jumps.
+      jumpV.alloc(size / 4);
+
+      std::size_t iter = 0;
+      for(Jump &jump : jumpV)
+      {
+         jump.codeIdx = ReadLE4(data + iter); iter += 4;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_LOAD
+   //
+   bool Module::chunkerACSE_LOAD(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("LOAD")) return false;
+
+      // Count imports.
+      std::size_t importC = 0;
+      for(Byte const *iter = data, *end = data + size; iter != end; ++ iter)
+         if(!*iter) ++importC;
+
+      importV.alloc(importC);
+
+      for(std::size_t iter = 0, i = 0; iter != size;)
+      {
+         Byte const *next;
+         std::size_t len;
+         std::tie(std::ignore, next, len) = ScanStringACS0(data, size, iter);
+
+         std::unique_ptr<char[]> str = ParseStringACS0(data + iter, next, len);
+
+         auto loadName = env->getModuleName(str.get(), len);
+         if(loadName != name)
+            importV[i++] = env->getModule(std::move(loadName));
+         else
+            importV[i++] = this;
+
+         iter = next - data + 1;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_MEXP
+   //
+   bool Module::chunkerACSE_MEXP(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("MEXP")) return false;
+
+      chunkStrTabACSE(regNameV, data, size, false);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_MIMP
+   //
+   bool Module::chunkerACSE_MIMP(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("MIMP")) return false;
+
+      // Determine highest index.
+      Word regC = 0;
+      for(std::size_t iter = 0; iter != size;)
+      {
+         if(size - iter < 4) throw ReadError();
+
+         Word idx = ReadLE4(data + iter); iter += 4;
+
+         regC = std::max<Word>(regC, idx + 1);
+
+         Byte const *next;
+         std::tie(std::ignore, next, std::ignore) = ScanStringACS0(data, size, iter);
+
+         iter = next - data + 1;
+      }
+
+      // Read imports.
+      regImpV.alloc(regC);
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter); iter += 4;
+
+         Byte const *next;
+         std::size_t len;
+         std::tie(std::ignore, next, len) = ScanStringACS0(data, size, iter);
+
+         std::unique_ptr<char[]> str = ParseStringACS0(data + iter, next, len);
+
+         regImpV[idx] = env->getString(str.get(), len);
+
+         iter = next - data + 1;
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_MINI
+   //
+   bool Module::chunkerACSE_MINI(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("MINI")) return false;
+
+      if(size % 4 || size < 4) throw ReadError("bad MINI size");
+
+      Word idx  = ReadLE4(data);
+      Word regC = idx + size / 4 - 1;
+
+      if(regC > regInitV.size())
+         regInitV.realloc(regC);
+
+      for(std::size_t iter = 4; iter != size; iter += 4)
+         regInitV[idx++] = ReadLE4(data + iter);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_MSTR
+   //
+   bool Module::chunkerACSE_MSTR(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("MSTR")) return false;
+
+      if(size % 4) throw ReadError();
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word idx = ReadLE4(data + iter); iter += 4;
+
+         // Silently ignore out of bounds initializers.
+         if(idx < regInitV.size())
+            regInitV[idx].tag = InitTag::String;
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_SARY
+   //
+   bool Module::chunkerACSE_SARY(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SARY")) return false;
+
+      if(size < 2 || (size - 2) % 4) throw ReadError();
+
+      Word        nameInt = ReadLE2(data);
+      std::size_t arrC    = (size - 2) / 4;
+
+      if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+
+      for(Script &scr : scriptV)
+         if(scr.name.i == nameInt) scr.locArrC = arrC;
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_SFLG
+   //
+   bool Module::chunkerACSE_SFLG(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SFLG")) return false;
+
+      if(size % 4) throw ReadError();
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word nameInt = ReadLE2(data + iter); iter += 2;
+         Word flags   = ReadLE2(data + iter); iter += 2;
+
+         bool flagNet    = !!(flags & 0x0001);
+         bool flagClient = !!(flags & 0x0002);
+
+         if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+
+         for(Script &scr : scriptV)
+         {
+            if(scr.name.i == nameInt)
+            {
+               scr.flagClient = flagClient;
+               scr.flagNet    = flagNet;
+            }
+         }
+      }
+
+      return false;
+   }
+
+   //
+   // Module::chunkerACSE_SNAM
+   //
+   bool Module::chunkerACSE_SNAM(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SNAM")) return false;
+
+      chunkStrTabACSE(scrNameV, data, size, false);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_SPTR8
+   //
+   // Reads 8-byte SPTR chunk.
+   //
+   bool Module::chunkerACSE_SPTR8(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SPTR")) return false;
+
+      if(size % 8) throw ReadError();
+
+      // Read scripts.
+      scriptV.alloc(size / 8, this);
+
+      std::size_t iter = 0;
+      for(Script &scr : scriptV)
+      {
+         Word nameInt = ReadLE2(data + iter); iter += 2;
+         Word type    = ReadLE1(data + iter); iter += 1;
+         scr.argC     = ReadLE1(data + iter); iter += 1;
+         scr.codeIdx  = ReadLE4(data + iter); iter += 4;
+
+         if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+         setScriptNameTypeACSE(&scr, nameInt, type);
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_SPTR12
+   //
+   // Reads 12-byte SPTR chunk.
+   //
+   bool Module::chunkerACSE_SPTR12(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SPTR")) return false;
+
+      if(size % 12) throw ReadError();
+
+      // Read scripts.
+      scriptV.alloc(size / 12, this);
+
+      std::size_t iter = 0;
+      for(Script &scr : scriptV)
+      {
+         Word nameInt = ReadLE2(data + iter); iter += 2;
+         Word type    = ReadLE2(data + iter); iter += 2;
+         scr.codeIdx  = ReadLE4(data + iter); iter += 4;
+         scr.argC     = ReadLE4(data + iter); iter += 4;
+
+         if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+         setScriptNameTypeACSE(&scr, nameInt, type);
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_STRE
+   //
+   bool Module::chunkerACSE_STRE(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("STRE")) return false;
+
+      std::size_t iter = 0;
+
+      if(size < 12) throw ReadError();
+
+      /*junk      = ReadLE4(data + iter);*/ iter += 4;
+      stringV.alloc(ReadLE4(data + iter));  iter += 4;
+      /*junk      = ReadLE4(data + iter);*/ iter += 4;
+
+      if(size - iter < stringV.size() * 4) throw ReadError();
+      for(String *&str : stringV)
+      {
+         std::size_t offset = ReadLE4(data + iter); iter += 4;
+
+         // Decrypt string.
+         std::unique_ptr<Byte[]> buf;
+         std::size_t             len;
+         std::tie(buf, len) = DecryptStringACSE(data, size, offset);
+
+         // Scan string.
+         Byte const *bufEnd;
+         std::tie(std::ignore, bufEnd, len) = ScanStringACS0(buf.get(), len, 0);
+
+         // Parse string.
+         str = env->getString(ParseStringACS0(buf.get(), bufEnd, len).get(), len);
+      }
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_STRL
+   //
+   bool Module::chunkerACSE_STRL(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("STRL")) return false;
+
+      chunkStrTabACSE(stringV, data, size, true);
+
+      return true;
+   }
+
+   //
+   // Module::chunkerACSE_SVCT
+   //
+   bool Module::chunkerACSE_SVCT(Byte const *data, std::size_t size, Word chunkName)
+   {
+      if(chunkName != MakeID("SVCT")) return false;
+
+      if(size % 4) throw ReadError();
+
+      for(std::size_t iter = 0; iter != size;)
+      {
+         Word nameInt = ReadLE2(data + iter); iter += 2;
+         Word regC    = ReadLE2(data + iter); iter += 2;
+
+         if(nameInt & 0x8000) nameInt |= 0xFFFF0000;
+
+         for(Script &scr : scriptV)
+         {
+            if(scr.name.i == nameInt)
+               scr.locRegC = regC;
+         }
+      }
+
+      return false;
+   }
+
+   //
+   // Module::readBytecodeACSE
+   //
+   void Module::readBytecodeACSE(Byte const *data, std::size_t size,
+      bool compressed, std::size_t offset)
+   {
+      std::size_t iter = offset;
+
+      // Find table start.
+      if(iter > size || size - iter < 4) throw ReadError();
+      iter = ReadLE4(data + iter);
+      if(iter > size) throw ReadError();
+
+      // Read chunks.
+      if(offset == 4)
+      {
+         readChunksACSE(data + iter, size - iter, false);
+      }
+      else
+      {
+         if(iter <= offset)
+            readChunksACSE(data + iter, offset - iter, true);
+         else
+            readChunksACSE(data + iter, size - iter, true);
+      }
+
+      // Read code.
+      readCodeACS0(data, size, compressed);
+
+      loaded = true;
+   }
+
+   //
+   // Module::readChunksACSE
+   //
+   void Module::readChunksACSE(Byte const *data, std::size_t size, bool fakeACS0)
+   {
+      // MEXP - Module Variable/Array Export
+      chunkIterACSE(data, size, &Module::chunkerACSE_MEXP);
+
+      // ARAY - Module Arrays
+      chunkIterACSE(data, size, &Module::chunkerACSE_ARAY);
+
+      // AINI - Module Array Init
+      chunkIterACSE(data, size, &Module::chunkerACSE_AINI);
+
+      // FNAM - Function Names
+      chunkIterACSE(data, size, &Module::chunkerACSE_FNAM);
+
+      // FUNC - Functions
+      chunkIterACSE(data, size, &Module::chunkerACSE_FUNC);
+
+      // FARY - Function Arrays
+      chunkIterACSE(data, size, &Module::chunkerACSE_FARY);
+
+      // JUMP - Dynamic Jump Targets
+      chunkIterACSE(data, size, &Module::chunkerACSE_JUMP);
+
+      // MINI - Module Variable Init
+      chunkIterACSE(data, size, &Module::chunkerACSE_MINI);
+
+      // SNAM - Script Names
+      chunkIterACSE(data, size, &Module::chunkerACSE_SNAM);
+
+      // SPTR - Script Pointers
+      if(fakeACS0)
+         chunkIterACSE(data, size, &Module::chunkerACSE_SPTR8);
+      else
+         chunkIterACSE(data, size, &Module::chunkerACSE_SPTR12);
+
+      // SARY - Script Arrays
+      chunkIterACSE(data, size, &Module::chunkerACSE_SARY);
+
+      // SFLG - Script Flags
+      chunkIterACSE(data, size, &Module::chunkerACSE_SFLG);
+
+      // SVCT - Script Variable Count
+      chunkIterACSE(data, size, &Module::chunkerACSE_SVCT);
+
+      // STRE - Encrypted String Literals
+      if(!chunkIterACSE(data, size, &Module::chunkerACSE_STRE))
+      {
+         // STRL - String Literals
+         chunkIterACSE(data, size, &Module::chunkerACSE_STRL);
+      }
+
+      // LOAD - Library Loading
+      chunkIterACSE(data, size, &Module::chunkerACSE_LOAD);
+
+      // Process function imports.
+      for(auto &func : functionV)
+      {
+         if(func) continue;
+
+         std::size_t idx = &func - functionV.data();
+
+         if(idx >= funcNameV.size()) continue;
+
+         auto &funcName = funcNameV[idx];
+
+         if(!funcName) continue;
+
+         for(auto &import : importV)
+         {
+            for(auto &funcImp : import->functionV)
+            {
+               if(funcImp && funcImp->name == funcName)
+               {
+                  func = funcImp;
+                  goto func_found;
+               }
+            }
+         }
+
+      func_found:;
+      }
+
+      // AIMP - Module Array Import
+      chunkIterACSE(data, size, &Module::chunkerACSE_AIMP);
+
+      // MIMP - Module Variable Import
+      chunkIterACSE(data, size, &Module::chunkerACSE_MIMP);
+
+      // ASTR - Module Array Strings
+      chunkIterACSE(data, size, &Module::chunkerACSE_ASTR);
+
+      // ATAG - Module Array Tagging
+      chunkIterACSE(data, size, &Module::chunkerACSE_ATAG);
+
+      // MSTR - Module Variable Strings
+      chunkIterACSE(data, size, &Module::chunkerACSE_MSTR);
+
+      for(auto &init : arrInitV)
+         init.finish();
+   }
+
+   //
+   // Module::setScriptNameTypeACSE
+   //
+   void Module::setScriptNameTypeACSE(Script *scr, Word nameInt, Word type)
+   {
+      // If high bit is set, script is named.
+      if((scr->name.i = nameInt) & 0x80000000)
+      {
+         // Fetch name.
+         Word nameIdx = ~scr->name.i;
+         if(nameIdx < scrNameV.size())
+            scr->name.s = scrNameV[nameIdx];
+      }
+
+      scr->type = env->getScriptTypeACSE(type);
+   }
+
+   //
+   // Module::DecryptStringACSE
+   //
+   std::pair<
+      std::unique_ptr<Byte[]> /*data*/,
+      std::size_t             /*size*/>
+   Module::DecryptStringACSE(Byte const *data, std::size_t size, std::size_t iter)
+   {
+      Word const key = iter * 157135;
+
+      // Calculate length. Start at 1 for null terminator.
+      std::size_t len = 1;
+      for(std::size_t i = iter, n = 0;; ++i, ++n, ++len)
+      {
+         if(i == size) throw ReadError();
+
+         Byte c = static_cast<Byte>(data[i] ^ (n / 2 + key));
+         if(!c) break;
+      }
+
+      // Decrypt data.
+      std::unique_ptr<Byte[]> buf{new Byte[len]};
+      for(std::size_t i = iter, n = 0;; ++i, ++n)
+      {
+         Byte c = static_cast<Byte>(data[i] ^ (n / 2 + key));
+         if(!(buf[n] = c)) break;
+      }
+
+      return {std::move(buf), len};
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/PrintBuf.cpp b/src/acs/vm/ACSVM/PrintBuf.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..928585433cfc8bc92bca357cc8e6c2cd89bd7de5
--- /dev/null
+++ b/src/acs/vm/ACSVM/PrintBuf.cpp
@@ -0,0 +1,148 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// PrintBuf class.
+//
+//-----------------------------------------------------------------------------
+
+#include "PrintBuf.hpp"
+
+#include "BinaryIO.hpp"
+
+#include <cstdio>
+#include <cstdlib>
+#include <new>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // PrintBuf constructor
+   //
+   PrintBuf::PrintBuf() :
+      buffer{nullptr},
+      bufEnd{nullptr},
+      bufBeg{nullptr},
+      bufPtr{nullptr}
+   {
+   }
+
+   //
+   // PrintBuf destructor
+   //
+   PrintBuf::~PrintBuf()
+   {
+      std::free(buffer);
+   }
+
+   //
+   // PrintBuf::drop
+   //
+   void PrintBuf::drop()
+   {
+      if(bufBeg != buffer)
+      {
+         bufPtr = bufBeg - 4;
+         bufBeg = bufPtr - ReadLE4(reinterpret_cast<Byte *>(bufPtr));
+      }
+      else
+         bufPtr = bufBeg;
+   }
+
+   //
+   // PrintBuf::format
+   //
+   void PrintBuf::format(char const *fmt, ...)
+   {
+      va_list arg;
+      va_start(arg, fmt);
+      formatv(fmt, arg);
+      va_end(arg);
+   }
+
+   //
+   // PrintBuf::formatv
+   //
+   void PrintBuf::formatv(char const *fmt, va_list arg)
+   {
+      bufPtr += std::vsprintf(bufPtr, fmt, arg);
+   }
+
+   //
+   // PrintBuf::getLoadBuf
+   //
+   char *PrintBuf::getLoadBuf(std::size_t countFull, std::size_t count)
+   {
+      if(static_cast<std::size_t>(bufEnd - buffer) <= countFull)
+      {
+         char *bufNew;
+         if(!(bufNew = static_cast<char *>(std::realloc(buffer, countFull + 1))))
+            throw std::bad_alloc();
+
+         buffer = bufNew;
+         bufEnd = buffer + countFull + 1;
+      }
+
+      bufPtr = buffer + countFull;
+      bufBeg = bufPtr - count;
+
+      return buffer;
+   }
+
+   //
+   // PrintBuf::push
+   //
+   void PrintBuf::push()
+   {
+      reserve(4);
+      WriteLE4(reinterpret_cast<Byte *>(bufPtr), bufPtr - bufBeg);
+      bufBeg = bufPtr += 4;
+   }
+
+   //
+   // PrintBuf::reserve
+   //
+   void PrintBuf::reserve(std::size_t count)
+   {
+      if(static_cast<std::size_t>(bufEnd - bufPtr) > count)
+         return;
+
+      // Allocate extra to anticipate further reserves. +1 for null.
+      count = count * 2 + 1;
+
+      std::size_t idxEnd = bufEnd - buffer;
+      std::size_t idxBeg = bufBeg - buffer;
+      std::size_t idxPtr = bufPtr - buffer;
+
+      // Check for size overflow.
+      if(SIZE_MAX - idxEnd < count)
+         throw std::bad_alloc();
+
+      // Check that the current segment won't pass the push limit.
+      if(UINT32_MAX - (idxEnd - idxBeg) < count)
+         throw std::bad_alloc();
+
+      idxEnd += count;
+
+      char *bufNew;
+      if(!(bufNew = static_cast<char *>(std::realloc(buffer, idxEnd))))
+         throw std::bad_alloc();
+
+      buffer = bufNew;
+      bufEnd = buffer + idxEnd;
+      bufBeg = buffer + idxBeg;
+      bufPtr = buffer + idxPtr;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/PrintBuf.hpp b/src/acs/vm/ACSVM/PrintBuf.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8cbf64d4828e6f8613b6093123c070f88a14be34
--- /dev/null
+++ b/src/acs/vm/ACSVM/PrintBuf.hpp
@@ -0,0 +1,74 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// PrintBuf class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__PrintBuf_H__
+#define ACSVM__PrintBuf_H__
+
+#include "Types.hpp"
+
+#include <cstdarg>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // PrintBuf
+   //
+   class PrintBuf
+   {
+   public:
+      PrintBuf();
+      ~PrintBuf();
+
+      void clear() {bufBeg = bufPtr = buffer;}
+
+      char const *data() const {return *bufPtr = '\0', bufBeg;}
+      char const *dataFull() const {return buffer;}
+
+      void drop();
+
+      // Formats using sprintf. Does not reserve space.
+      void format(char const *fmt, ...);
+      void formatv(char const *fmt, std::va_list arg);
+
+      // Returns a pointer to count chars to write into. The caller must write
+      // to the entire returned buffer. Does not reserve space.
+      char *getBuf(std::size_t count)
+         {char *s = bufPtr; bufPtr += count; return s;}
+
+      // Prepares the buffer to be deserialized.
+      char *getLoadBuf(std::size_t countFull, std::size_t count);
+
+      void push();
+
+      // Writes literal characters. Does not reserve space.
+      void put(char c) {*bufPtr++ = c;}
+      void put(char const *s) {while(*s) *bufPtr++ = *s++;}
+      void put(char const *s, std::size_t n) {while(n--) *bufPtr++ = *s++;}
+
+      // Ensures at least count chars are available for writing into.
+      void reserve(std::size_t count);
+
+      std::size_t size() const {return bufPtr - bufBeg;}
+      std::size_t sizeFull() const {return bufPtr - buffer;}
+
+   private:
+      char *buffer, *bufEnd, *bufBeg, *bufPtr;
+   };
+}
+
+#endif//ACSVM__PrintBuf_H__
+
diff --git a/src/acs/vm/ACSVM/Scope.cpp b/src/acs/vm/ACSVM/Scope.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7f56a7e6fc57528eac81357b31513e834b7cf456
--- /dev/null
+++ b/src/acs/vm/ACSVM/Scope.cpp
@@ -0,0 +1,1299 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2020 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Scope classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Scope.hpp"
+
+#include "Action.hpp"
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "HashMap.hpp"
+#include "HashMapFixed.hpp"
+#include "Init.hpp"
+#include "Module.hpp"
+#include "Script.hpp"
+#include "Serial.hpp"
+#include "Thread.hpp"
+
+#include <algorithm>
+#include <unordered_set>
+#include <vector>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // GlobalScope::PrivData
+   //
+   struct GlobalScope::PrivData
+   {
+      HashMapKeyMem<Word, HubScope, &HubScope::id, &HubScope::hashLink> scopes;
+   };
+
+   //
+   // HubScope::PrivData
+   //
+   struct HubScope::PrivData
+   {
+      HashMapKeyMem<Word, MapScope, &MapScope::id, &MapScope::hashLink> scopes;
+   };
+
+   //
+   // MapScope::PrivData
+   //
+   struct MapScope::PrivData
+   {
+      HashMapFixed<Module *, ModuleScope> scopes;
+
+      HashMapFixed<Word,     Script *> scriptInt;
+      HashMapFixed<String *, Script *> scriptStr;
+
+      HashMapFixed<Script *, Thread *> scriptThread;
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Objects                                                             |
+//
+
+namespace ACSVM
+{
+   constexpr std::size_t ModuleScope::ArrC;
+   constexpr std::size_t ModuleScope::RegC;
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // GlobalScope constructor
+   //
+   GlobalScope::GlobalScope(Environment *env_, Word id_) :
+      env{env_},
+      id {id_},
+
+      arrV{},
+      regV{},
+
+      hashLink{this},
+
+      active{false},
+
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // GlobalScope destructor
+   //
+   GlobalScope::~GlobalScope()
+   {
+      reset();
+      delete pd;
+   }
+
+   //
+   // GlobalScope::countActiveThread
+   //
+   std::size_t GlobalScope::countActiveThread() const
+   {
+      std::size_t n = 0;
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            n += scope.countActiveThread();
+      }
+
+      return n;
+   }
+
+   //
+   // GlobalScope::exec
+   //
+   void GlobalScope::exec()
+   {
+      // Delegate deferred script actions.
+      for(auto itr = scriptAction.begin(), end = scriptAction.end(); itr != end;)
+      {
+         auto scope = pd->scopes.find(itr->id.global);
+         if(scope && scope->active)
+            itr++->link.relink(&scope->scriptAction);
+         else
+            ++itr;
+      }
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            scope.exec();
+      }
+   }
+
+   //
+   // GlobalScope::freeHubScope
+   //
+   void GlobalScope::freeHubScope(HubScope *scope)
+   {
+      pd->scopes.unlink(scope);
+      delete scope;
+   }
+
+   //
+   // GlobalScope::getHubScope
+   //
+   HubScope *GlobalScope::getHubScope(Word scopeID)
+   {
+      if(auto *scope = pd->scopes.find(scopeID))
+         return scope;
+
+      auto scope = new HubScope(this, scopeID);
+      pd->scopes.insert(scope);
+      return scope;
+   }
+
+   //
+   // GlobalScope::hasActiveThread
+   //
+   bool GlobalScope::hasActiveThread() const
+   {
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active && scope.hasActiveThread())
+            return true;
+      }
+
+      return false;
+   }
+
+   //
+   // GlobalScope::loadState
+   //
+   void GlobalScope::loadState(Serial &in)
+   {
+      reset();
+
+      in.readSign(Signature::GlobalScope);
+
+      for(auto &arr : arrV)
+         arr.loadState(in);
+
+      for(auto &reg : regV)
+         reg = ReadVLN<Word>(in);
+
+      env->readScriptActions(in, scriptAction);
+
+      active = in.in->get() != '\0';
+
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+         getHubScope(ReadVLN<Word>(in))->loadState(in);
+
+      in.readSign(~Signature::GlobalScope);
+   }
+
+   //
+   // GlobalScope::lockStrings
+   //
+   void GlobalScope::lockStrings() const
+   {
+      for(auto &arr : arrV) arr.lockStrings(env);
+      for(auto &reg : regV) ++env->getString(reg)->lock;
+
+      for(auto &action : scriptAction)
+         action.lockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.lockStrings();
+   }
+
+   //
+   // GlobalScope::refStrings
+   //
+   void GlobalScope::refStrings() const
+   {
+      for(auto &arr : arrV) arr.refStrings(env);
+      for(auto &reg : regV) env->getString(reg)->ref = true;
+
+      for(auto &action : scriptAction)
+         action.refStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.refStrings();
+   }
+
+   //
+   // GlobalScope::reset
+   //
+   void GlobalScope::reset()
+   {
+      while(scriptAction.next->obj)
+         delete scriptAction.next->obj;
+
+      pd->scopes.free();
+   }
+
+   //
+   // GlobalScope::saveState
+   //
+   void GlobalScope::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::GlobalScope);
+
+      for(auto &arr : arrV)
+         arr.saveState(out);
+
+      for(auto &reg : regV)
+         WriteVLN(out, reg);
+
+      env->writeScriptActions(out, scriptAction);
+
+      out.out->put(active ? '\1' : '\0');
+
+      WriteVLN(out, pd->scopes.size());
+      for(auto &scope : pd->scopes)
+      {
+         WriteVLN(out, scope.id);
+         scope.saveState(out);
+      }
+
+      out.writeSign(~Signature::GlobalScope);
+   }
+
+   //
+   // GlobalScope::unlockStrings
+   //
+   void GlobalScope::unlockStrings() const
+   {
+      for(auto &arr : arrV) arr.unlockStrings(env);
+      for(auto &reg : regV) --env->getString(reg)->lock;
+
+      for(auto &action : scriptAction)
+         action.unlockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.unlockStrings();
+   }
+
+   //
+   // HubScope constructor
+   //
+   HubScope::HubScope(GlobalScope *global_, Word id_) :
+      env   {global_->env},
+      global{global_},
+      id    {id_},
+
+      arrV{},
+      regV{},
+
+      hashLink{this},
+
+      active{false},
+
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // HubScope destructor
+   //
+   HubScope::~HubScope()
+   {
+      reset();
+      delete pd;
+   }
+
+   //
+   // HubScope::countActiveThread
+   //
+   std::size_t HubScope::countActiveThread() const
+   {
+      std::size_t n = 0;
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            n += scope.countActiveThread();
+      }
+
+      return n;
+   }
+
+   //
+   // HubScope::exec
+   //
+   void HubScope::exec()
+   {
+      // Delegate deferred script actions.
+      for(auto itr = scriptAction.begin(), end = scriptAction.end(); itr != end;)
+      {
+         auto scope = pd->scopes.find(itr->id.global);
+         if(scope && scope->active)
+            itr++->link.relink(&scope->scriptAction);
+         else
+            ++itr;
+      }
+
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active)
+            scope.exec();
+      }
+   }
+
+   //
+   // HubScope::freeMapScope
+   //
+   void HubScope::freeMapScope(MapScope *scope)
+   {
+      pd->scopes.unlink(scope);
+      delete scope;
+   }
+
+   //
+   // HubScope::getMapScope
+   //
+   MapScope *HubScope::getMapScope(Word scopeID)
+   {
+      if(auto *scope = pd->scopes.find(scopeID))
+         return scope;
+
+      auto scope = new MapScope(this, scopeID);
+      pd->scopes.insert(scope);
+      return scope;
+   }
+
+   //
+   // HubScope::hasActiveThread
+   //
+   bool HubScope::hasActiveThread() const
+   {
+      for(auto &scope : pd->scopes)
+      {
+         if(scope.active && scope.hasActiveThread())
+            return true;
+      }
+
+      return false;
+   }
+
+   //
+   // HubScope::loadState
+   //
+   void HubScope::loadState(Serial &in)
+   {
+      reset();
+
+      in.readSign(Signature::HubScope);
+
+      for(auto &arr : arrV)
+         arr.loadState(in);
+
+      for(auto &reg : regV)
+         reg = ReadVLN<Word>(in);
+
+      env->readScriptActions(in, scriptAction);
+
+      active = in.in->get() != '\0';
+
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+         getMapScope(ReadVLN<Word>(in))->loadState(in);
+
+      in.readSign(~Signature::HubScope);
+   }
+
+   //
+   // HubScope::lockStrings
+   //
+   void HubScope::lockStrings() const
+   {
+      for(auto &arr : arrV) arr.lockStrings(env);
+      for(auto &reg : regV) ++env->getString(reg)->lock;
+
+      for(auto &action : scriptAction)
+         action.lockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.lockStrings();
+   }
+
+   //
+   // HubScope::refStrings
+   //
+   void HubScope::refStrings() const
+   {
+      for(auto &arr : arrV) arr.refStrings(env);
+      for(auto &reg : regV) env->getString(reg)->ref = true;
+
+      for(auto &action : scriptAction)
+         action.refStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.refStrings();
+   }
+
+   //
+   // HubScope::reset
+   //
+   void HubScope::reset()
+   {
+      while(scriptAction.next->obj)
+         delete scriptAction.next->obj;
+
+      pd->scopes.free();
+   }
+
+   //
+   // HubScope::saveState
+   //
+   void HubScope::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::HubScope);
+
+      for(auto &arr : arrV)
+         arr.saveState(out);
+
+      for(auto &reg : regV)
+         WriteVLN(out, reg);
+
+      env->writeScriptActions(out, scriptAction);
+
+      out.out->put(active ? '\1' : '\0');
+
+      WriteVLN(out, pd->scopes.size());
+      for(auto &scope : pd->scopes)
+      {
+         WriteVLN(out, scope.id);
+         scope.saveState(out);
+      }
+
+      out.writeSign(~Signature::HubScope);
+   }
+
+   //
+   // HubScope::unlockStrings
+   //
+   void HubScope::unlockStrings() const
+   {
+      for(auto &arr : arrV) arr.unlockStrings(env);
+      for(auto &reg : regV) --env->getString(reg)->lock;
+
+      for(auto &action : scriptAction)
+         action.unlockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.unlockStrings();
+   }
+
+   //
+   // MapScope constructor
+   //
+   MapScope::MapScope(HubScope *hub_, Word id_) :
+      env{hub_->env},
+      hub{hub_},
+      id {id_},
+
+      hashLink{this},
+
+      module0{nullptr},
+
+      active       {false},
+      clampCallSpec{false},
+
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // MapScope destructor
+   //
+   MapScope::~MapScope()
+   {
+      reset();
+      delete pd;
+   }
+
+   //
+   // MapScope::addModules
+   //
+   void MapScope::addModules(Module *const *moduleV, std::size_t moduleC)
+   {
+      module0 = moduleC ? moduleV[0] : nullptr;
+
+      // Find all associated modules.
+
+      struct
+      {
+         std::unordered_set<Module *> set;
+         std::vector<Module *>        vec;
+
+         void add(Module *module)
+         {
+            if(!set.insert(module).second) return;
+
+            vec.push_back(module);
+            for(auto &import : module->importV)
+               add(import);
+         }
+      } modules;
+
+      for(auto itr = moduleV, end = itr + moduleC; itr != end; ++itr)
+         modules.add(*itr);
+
+      // Count scripts.
+
+      std::size_t scriptThrC = 0;
+      std::size_t scriptIntC = 0;
+      std::size_t scriptStrC = 0;
+
+      for(auto &module : modules.vec)
+      {
+         for(auto &script : module->scriptV)
+         {
+            ++scriptThrC;
+            if(script.name.s)
+               ++scriptStrC;
+            else
+               ++scriptIntC;
+         }
+      }
+
+      // Create lookup tables.
+
+      pd->scopes.alloc(modules.vec.size());
+      pd->scriptInt.alloc(scriptIntC);
+      pd->scriptStr.alloc(scriptStrC);
+      pd->scriptThread.alloc(scriptThrC);
+
+      auto scopeItr     = pd->scopes.begin();
+      auto scriptIntItr = pd->scriptInt.begin();
+      auto scriptStrItr = pd->scriptStr.begin();
+      auto scriptThrItr = pd->scriptThread.begin();
+
+      for(auto &module : modules.vec)
+      {
+         using ElemScope = HashMapFixed<Module *, ModuleScope>::Elem;
+
+         new(scopeItr++) ElemScope{module, {this, module}, nullptr};
+
+         for(auto &script : module->scriptV)
+         {
+            using ElemInt = HashMapFixed<Word,     Script *>::Elem;
+            using ElemStr = HashMapFixed<String *, Script *>::Elem;
+            using ElemThr = HashMapFixed<Script *, Thread *>::Elem;
+
+            new(scriptThrItr++) ElemThr{&script, nullptr, nullptr};
+
+            if(script.name.s)
+               new(scriptStrItr++) ElemStr{script.name.s, &script, nullptr};
+            else
+               new(scriptIntItr++) ElemInt{script.name.i, &script, nullptr};
+         }
+      }
+
+      pd->scopes.build();
+      pd->scriptInt.build();
+      pd->scriptStr.build();
+      pd->scriptThread.build();
+
+      for(auto &scope : pd->scopes)
+         scope.val.import();
+   }
+
+   //
+   // MapScope::countActiveThread
+   //
+   std::size_t MapScope::countActiveThread() const
+   {
+      return threadActive.size();
+   }
+
+   //
+   // MapScope::exec
+   //
+   void MapScope::exec()
+   {
+      // Execute deferred script actions.
+      while(scriptAction.next->obj)
+      {
+         ScriptAction *action = scriptAction.next->obj;
+         Script       *script = findScript(action->name);
+
+         if(script) switch(action->action)
+         {
+         case ScriptAction::Start:
+            scriptStart(script, {action->argV.data(), action->argV.size()});
+            break;
+
+         case ScriptAction::StartForced:
+            scriptStartForced(script, {action->argV.data(), action->argV.size()});
+            break;
+
+         case ScriptAction::Stop:
+            scriptStop(script);
+            break;
+
+         case ScriptAction::Pause:
+            scriptPause(script);
+            break;
+         }
+
+         delete action;
+      }
+
+      // Execute running threads.
+      for(auto itr = threadActive.begin(), end = threadActive.end(); itr != end;)
+      {
+         itr->exec();
+         if(itr->state == ThreadState::Inactive)
+            freeThread(&*itr++);
+         else
+            ++itr;
+      }
+   }
+
+   //
+   // MapScope::findScript
+   //
+   Script *MapScope::findScript(ScriptName name)
+   {
+      return name.s ? findScript(name.s) : findScript(name.i);
+   }
+
+   //
+   // MapScope::findScript
+   //
+   Script *MapScope::findScript(String *name)
+   {
+      if(Script **script = pd->scriptStr.find(name))
+         return *script;
+      else
+         return nullptr;
+   }
+
+   //
+   // MapScope::findScript
+   //
+   Script *MapScope::findScript(Word name)
+   {
+      if(Script **script = pd->scriptInt.find(name))
+         return *script;
+      else
+         return nullptr;
+   }
+
+   //
+   // MapScope::freeThread
+   //
+   void MapScope::freeThread(Thread *thread)
+   {
+      auto itr = pd->scriptThread.find(thread->script);
+      if(itr  && *itr == thread)
+         *itr = nullptr;
+
+      env->freeThread(thread);
+   }
+
+   //
+   // MapScope::getModuleScope
+   //
+   ModuleScope *MapScope::getModuleScope(Module *module)
+   {
+      return pd->scopes.find(module);
+   }
+
+   //
+   // MapScope::getString
+   //
+   String *MapScope::getString(Word idx) const
+   {
+      if(idx & 0x80000000)
+         return &env->stringTable[~idx];
+
+      if(!module0 || idx >= module0->stringV.size())
+         return &env->stringTable.getNone();
+
+      return module0->stringV[idx];
+   }
+
+   //
+   // MapScope::hasActiveThread
+   //
+   bool MapScope::hasActiveThread() const
+   {
+      for(auto &thread : threadActive)
+      {
+         if(thread.state != ThreadState::Inactive)
+            return true;
+      }
+
+      return false;
+   }
+
+   //
+   // MapScope::hasModules
+   //
+   bool MapScope::hasModules() const
+   {
+      return !pd->scopes.empty();
+   }
+
+   //
+   // MapScope::isScriptActive
+   //
+   bool MapScope::isScriptActive(Script *script)
+   {
+      auto itr = pd->scriptThread.find(script);
+      return itr && *itr && (*itr)->state != ThreadState::Inactive;
+   }
+
+   //
+   // MapScope::loadModules
+   //
+   void MapScope::loadModules(Serial &in)
+   {
+      auto count = ReadVLN<std::size_t>(in);
+      std::vector<Module *> modules;
+      modules.reserve(count);
+
+      for(auto n = count; n--;)
+         modules.emplace_back(env->getModule(env->readModuleName(in)));
+
+      addModules(modules.data(), modules.size());
+
+      for(auto &module : modules)
+         pd->scopes.find(module)->loadState(in);
+   }
+
+   //
+   // MapScope::loadState
+   //
+   void MapScope::loadState(Serial &in)
+   {
+      reset();
+
+      in.readSign(Signature::MapScope);
+
+      env->readScriptActions(in, scriptAction);
+      active = in.in->get() != '\0';
+      loadModules(in);
+      loadThreads(in);
+
+      in.readSign(~Signature::MapScope);
+   }
+
+   //
+   // MapScope::loadThreads
+   //
+   void MapScope::loadThreads(Serial &in)
+   {
+      for(auto n = ReadVLN<std::size_t>(in); n--;)
+      {
+         Thread *thread = env->getFreeThread();
+         thread->link.insert(&threadActive);
+         thread->loadState(in);
+
+         if(in.in->get())
+         {
+            auto scrThread = pd->scriptThread.find(thread->script);
+            if(scrThread)
+               *scrThread = thread;
+         }
+      }
+   }
+
+   //
+   // MapScope::lockStrings
+   //
+   void MapScope::lockStrings() const
+   {
+      for(auto &action : scriptAction)
+         action.lockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.val.lockStrings();
+
+      for(auto &thread : threadActive)
+         thread.lockStrings();
+   }
+
+   //
+   // MapScope::refStrings
+   //
+   void MapScope::refStrings() const
+   {
+      for(auto &action : scriptAction)
+         action.refStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.val.refStrings();
+
+      for(auto &thread : threadActive)
+         thread.refStrings();
+   }
+
+   //
+   // MapScope::reset
+   //
+   void MapScope::reset()
+   {
+      // Stop any remaining threads and return them to free list.
+      while(threadActive.next->obj)
+      {
+         threadActive.next->obj->stop();
+         env->freeThread(threadActive.next->obj);
+      }
+
+      while(scriptAction.next->obj)
+         delete scriptAction.next->obj;
+
+      active = false;
+
+      pd->scopes.free();
+
+      pd->scriptInt.free();
+      pd->scriptStr.free();
+      pd->scriptThread.free();
+   }
+
+   //
+   // MapScope::saveModules
+   //
+   void MapScope::saveModules(Serial &out) const
+   {
+      WriteVLN(out, pd->scopes.size());
+
+      for(auto &scope : pd->scopes)
+         env->writeModuleName(out, scope.key->name);
+
+      for(auto &scope : pd->scopes)
+         scope.val.saveState(out);
+   }
+
+   //
+   // MapScope::saveState
+   //
+   void MapScope::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::MapScope);
+
+      env->writeScriptActions(out, scriptAction);
+      out.out->put(active ? '\1' : '\0');
+      saveModules(out);
+      saveThreads(out);
+
+      out.writeSign(~Signature::MapScope);
+   }
+
+   //
+   // MapScope::saveThreads
+   //
+   void MapScope::saveThreads(Serial &out) const
+   {
+      WriteVLN(out, threadActive.size());
+      for(auto &thread : threadActive)
+      {
+         thread.saveState(out);
+
+         auto scrThread = pd->scriptThread.find(thread.script);
+         out.out->put(scrThread && *scrThread == &thread ? '\1' : '\0');
+      }
+   }
+
+   //
+   // MapScope::scriptPause
+   //
+   bool MapScope::scriptPause(Script *script)
+   {
+      auto itr = pd->scriptThread.find(script);
+      if(!itr || !*itr)
+         return false;
+
+      switch((*itr)->state.state)
+      {
+      case ThreadState::Inactive:
+      case ThreadState::Paused:
+      case ThreadState::Stopped:
+         return false;
+
+      default:
+         (*itr)->state = ThreadState::Paused;
+         return true;
+      }
+   }
+
+   //
+   // MapScope::scriptPause
+   //
+   bool MapScope::scriptPause(ScriptName name, ScopeID scope)
+   {
+      if(scope != ScopeID{hub->global->id, hub->id, id})
+      {
+         env->deferAction({scope, name, ScriptAction::Pause, {}});
+         return true;
+      }
+
+      if(Script *script = findScript(name))
+         return scriptPause(script);
+      else
+         return false;
+   }
+
+   //
+   // MapScope::scriptStart
+   //
+   bool MapScope::scriptStart(Script *script, ScriptStartInfo const &info)
+   {
+      auto itr = pd->scriptThread.find(script);
+      if(!itr)
+         return false;
+
+      if(Thread *&thread = *itr)
+      {
+         switch(thread->state.state)
+         {
+         case ThreadState::Paused:
+            thread->state = ThreadState::Running;
+            return true;
+
+         default:
+            return false;
+         }
+      }
+      else
+      {
+         thread = env->getFreeThread();
+         thread->start(script, this, info.info, info.argV, info.argC);
+         if(info.func) info.func(thread);
+         if(info.funcc) info.funcc(thread);
+         return true;
+      }
+   }
+
+   //
+   // MapScope::scriptStart
+   //
+   bool MapScope::scriptStart(ScriptName name, ScopeID scope, ScriptStartInfo const &info)
+   {
+      if(scope != ScopeID{hub->global->id, hub->id, id})
+      {
+         env->deferAction({scope, name, ScriptAction::Start, {info.argV, info.argC}});
+         return true;
+      }
+
+      if(Script *script = findScript(name))
+         return scriptStart(script, info);
+      else
+         return false;
+   }
+
+   //
+   // MapScope::scriptStartForced
+   //
+   bool MapScope::scriptStartForced(Script *script, ScriptStartInfo const &info)
+   {
+      Thread *thread = env->getFreeThread();
+
+      thread->start(script, this, info.info, info.argV, info.argC);
+      if(info.func) info.func(thread);
+      if(info.funcc) info.funcc(thread);
+      return true;
+   }
+
+   //
+   // MapScope::scriptStartForced
+   //
+   bool MapScope::scriptStartForced(ScriptName name, ScopeID scope, ScriptStartInfo const &info)
+   {
+      if(scope != ScopeID{hub->global->id, hub->id, id})
+      {
+         env->deferAction({scope, name, ScriptAction::StartForced, {info.argV, info.argC}});
+         return true;
+      }
+
+      if(Script *script = findScript(name))
+         return scriptStartForced(script, info);
+      else
+         return false;
+   }
+
+   //
+   // MapScope::scriptStartResult
+   //
+   Word MapScope::scriptStartResult(Script *script, ScriptStartInfo const &info)
+   {
+      Thread *thread = env->getFreeThread();
+
+      thread->start(script, this, info.info, info.argV, info.argC);
+      if(info.func) info.func(thread);
+      if(info.funcc) info.funcc(thread);
+      thread->exec();
+
+      Word result = thread->result;
+      if(thread->state == ThreadState::Inactive)
+         freeThread(thread);
+      return result;
+   }
+
+   //
+   // MapScope::scriptStartResult
+   //
+   Word MapScope::scriptStartResult(ScriptName name, ScriptStartInfo const &info)
+   {
+      if(Script *script = findScript(name))
+         return scriptStartResult(script, info);
+      else
+         return 0;
+   }
+
+   //
+   // MapScope::scriptStartType
+   //
+   Word MapScope::scriptStartType(Word type, ScriptStartInfo const &info)
+   {
+      Word result = 0;
+
+      for(auto &script : pd->scriptThread)
+      {
+         if(script.key->type == type)
+            result += scriptStart(script.key, info);
+      }
+
+      return result;
+   }
+
+   //
+   // MapScope::scriptStartTypeForced
+   //
+   Word MapScope::scriptStartTypeForced(Word type, ScriptStartInfo const &info)
+   {
+      Word result = 0;
+
+      for(auto &script : pd->scriptThread)
+      {
+         if(script.key->type == type)
+            result += scriptStartForced(script.key, info);
+      }
+
+      return result;
+   }
+
+   //
+   // MapScope::scriptStop
+   //
+   bool MapScope::scriptStop(Script *script)
+   {
+      auto itr = pd->scriptThread.find(script);
+      if(!itr || !*itr)
+         return false;
+
+      switch((*itr)->state.state)
+      {
+      case ThreadState::Inactive:
+      case ThreadState::Stopped:
+         return false;
+
+      default:
+         (*itr)->state = ThreadState::Stopped;
+         (*itr)        = nullptr;
+         return true;
+      }
+   }
+
+   //
+   // MapScope::scriptStop
+   //
+   bool MapScope::scriptStop(ScriptName name, ScopeID scope)
+   {
+      if(scope != ScopeID{hub->global->id, hub->id, id})
+      {
+         env->deferAction({scope, name, ScriptAction::Stop, {}});
+         return true;
+      }
+
+      if(Script *script = findScript(name))
+         return scriptStop(script);
+      else
+         return false;
+   }
+
+   //
+   // MapScope::unlockStrings
+   //
+   void MapScope::unlockStrings() const
+   {
+      for(auto &action : scriptAction)
+         action.unlockStrings(env);
+
+      for(auto &scope : pd->scopes)
+         scope.val.unlockStrings();
+
+      for(auto &thread : threadActive)
+         thread.unlockStrings();
+   }
+
+   //
+   // ModuleScope constructor
+   //
+   ModuleScope::ModuleScope(MapScope *map_, Module *module_) :
+      env   {map_->env},
+      map   {map_},
+      module{module_},
+
+      selfArrV{},
+      selfRegV{}
+   {
+      // Set arrays and registers to refer to this scope's by default.
+      for(std::size_t i = 0; i != ArrC; ++i) arrV[i] = &selfArrV[i];
+      for(std::size_t i = 0; i != RegC; ++i) regV[i] = &selfRegV[i];
+
+      // Apply initialization data from module.
+
+      for(std::size_t i = 0; i != ArrC; ++i)
+      {
+         if(i < module->arrInitV.size())
+            module->arrInitV[i].apply(selfArrV[i], module);
+      }
+
+      for(std::size_t i = 0; i != RegC; ++i)
+      {
+         if(i < module->regInitV.size())
+            selfRegV[i] = module->regInitV[i].getValue(module);
+      }
+   }
+
+   //
+   // ModuleScope destructor
+   //
+   ModuleScope::~ModuleScope()
+   {
+   }
+
+   //
+   // ModuleScope::import
+   //
+   void ModuleScope::import()
+   {
+      for(std::size_t i = 0, e = std::min<std::size_t>(ArrC, module->arrImpV.size()); i != e; ++i)
+      {
+         String *arrName = module->arrImpV[i];
+         if(!arrName) continue;
+
+         for(auto &imp : module->importV)
+         {
+            for(auto &impName : imp->arrNameV)
+            {
+               if(impName == arrName)
+               {
+                  std::size_t impIdx = &impName - imp->arrNameV.data();
+                  if(impIdx >= ArrC) continue;
+                  arrV[i] = &map->getModuleScope(imp)->selfArrV[impIdx];
+                  goto arr_found;
+               }
+            }
+         }
+
+      arr_found:;
+      }
+
+      for(std::size_t i = 0, e = std::min<std::size_t>(RegC, module->regImpV.size()); i != e; ++i)
+      {
+         String *regName = module->regImpV[i];
+         if(!regName) continue;
+
+         for(auto &imp : module->importV)
+         {
+            for(auto &impName : imp->regNameV)
+            {
+               if(impName == regName)
+               {
+                  std::size_t impIdx = &impName - imp->regNameV.data();
+                  if(impIdx >= RegC) continue;
+                  regV[i] = &map->getModuleScope(imp)->selfRegV[impIdx];
+                  goto reg_found;
+               }
+            }
+         }
+
+      reg_found:;
+      }
+   }
+
+   //
+   // ModuleScope::loadState
+   //
+   void ModuleScope::loadState(Serial &in)
+   {
+      in.readSign(Signature::ModuleScope);
+
+      for(auto &arr : selfArrV)
+         arr.loadState(in);
+
+      for(auto &reg : selfRegV)
+         reg = ReadVLN<Word>(in);
+
+      in.readSign(~Signature::ModuleScope);
+   }
+
+   //
+   // ModuleScope::lockStrings
+   //
+   void ModuleScope::lockStrings() const
+   {
+      for(auto &arr : selfArrV) arr.lockStrings(env);
+      for(auto &reg : selfRegV) ++env->getString(reg)->lock;
+   }
+
+   //
+   // ModuleScope::refStrings
+   //
+   void ModuleScope::refStrings() const
+   {
+      for(auto &arr : selfArrV) arr.refStrings(env);
+      for(auto &reg : selfRegV) env->getString(reg)->ref = true;
+   }
+
+   //
+   // ModuleScope::saveState
+   //
+   void ModuleScope::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::ModuleScope);
+
+      for(auto &arr : selfArrV)
+         arr.saveState(out);
+
+      for(auto &reg : selfRegV)
+         WriteVLN(out, reg);
+
+      out.writeSign(~Signature::ModuleScope);
+   }
+
+   //
+   // ModuleScope::unlockStrings
+   //
+   void ModuleScope::unlockStrings() const
+   {
+      for(auto &arr : selfArrV) arr.unlockStrings(env);
+      for(auto &reg : selfRegV) --env->getString(reg)->lock;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Scope.hpp b/src/acs/vm/ACSVM/Scope.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..761e65b2bb5ba76a4171f82fc17b1a5b07c94215
--- /dev/null
+++ b/src/acs/vm/ACSVM/Scope.hpp
@@ -0,0 +1,285 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Scope classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Scope_H__
+#define ACSVM__Scope_H__
+
+#include "Array.hpp"
+#include "List.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   extern "C" using MapScope_ScriptStartFuncC = void (*)(void *);
+
+   //
+   // GlobalScope
+   //
+   class GlobalScope
+   {
+   public:
+      static constexpr std::size_t ArrC = 256;
+      static constexpr std::size_t RegC = 256;
+
+
+      GlobalScope(GlobalScope const &) = delete;
+      GlobalScope(Environment *env, Word id);
+      ~GlobalScope();
+
+      std::size_t countActiveThread() const;
+
+      void exec();
+
+      void freeHubScope(HubScope *scope);
+
+      HubScope *getHubScope(Word id);
+
+      bool hasActiveThread() const;
+
+      void lockStrings() const;
+
+      void loadState(Serial &in);
+
+      void refStrings() const;
+
+      void reset();
+
+      void saveState(Serial &out) const;
+
+      void unlockStrings() const;
+
+      Environment *const env;
+      Word         const id;
+
+      Array arrV[ArrC];
+      Word  regV[RegC];
+
+      ListLink<GlobalScope>  hashLink;
+      ListLink<ScriptAction> scriptAction;
+
+      bool active;
+
+   private:
+      struct PrivData;
+
+      PrivData *pd;
+   };
+
+   //
+   // HubScope
+   //
+   class HubScope
+   {
+   public:
+      static constexpr std::size_t ArrC = 256;
+      static constexpr std::size_t RegC = 256;
+
+
+      HubScope(HubScope const &) = delete;
+      HubScope(GlobalScope *global, Word id);
+      ~HubScope();
+
+      std::size_t countActiveThread() const;
+
+      void exec();
+
+      void freeMapScope(MapScope *scope);
+
+      MapScope *getMapScope(Word id);
+
+      bool hasActiveThread() const;
+
+      void lockStrings() const;
+
+      void loadState(Serial &in);
+
+      void refStrings() const;
+
+      void reset();
+
+      void saveState(Serial &out) const;
+
+      void unlockStrings() const;
+
+      Environment *const env;
+      GlobalScope *const global;
+      Word         const id;
+
+      Array arrV[ArrC];
+      Word  regV[RegC];
+
+      ListLink<HubScope>     hashLink;
+      ListLink<ScriptAction> scriptAction;
+
+      bool active;
+
+   private:
+      struct PrivData;
+
+      PrivData *pd;
+   };
+
+   //
+   // MapScope
+   //
+   class MapScope
+   {
+   public:
+      using ScriptStartFunc = void (*)(Thread *);
+      using ScriptStartFuncC = MapScope_ScriptStartFuncC;
+
+      //
+      // ScriptStartInfo
+      //
+      class ScriptStartInfo
+      {
+      public:
+         ScriptStartInfo() :
+            argV{nullptr}, func{nullptr}, funcc{nullptr}, info{nullptr}, argC{0} {}
+         ScriptStartInfo(Word const *argV_, std::size_t argC_,
+            ThreadInfo const *info_ = nullptr, ScriptStartFunc func_ = nullptr) :
+            argV{argV_}, func{func_}, funcc{nullptr}, info{info_}, argC{argC_} {}
+         ScriptStartInfo(Word const *argV_, std::size_t argC_,
+            ThreadInfo const *info_, ScriptStartFuncC func_) :
+            argV{argV_}, func{nullptr}, funcc{func_}, info{info_}, argC{argC_} {}
+
+         Word       const *argV;
+         ScriptStartFunc   func;
+         ScriptStartFuncC  funcc;
+         ThreadInfo const *info;
+         std::size_t       argC;
+      };
+
+
+      MapScope(MapScope const &) = delete;
+      MapScope(HubScope *hub, Word id);
+      ~MapScope();
+
+      void addModules(Module *const *moduleV, std::size_t moduleC);
+
+      std::size_t countActiveThread() const;
+
+      void exec();
+
+      Script *findScript(ScriptName name);
+      Script *findScript(String *name);
+      Script *findScript(Word name);
+
+      ModuleScope *getModuleScope(Module *module);
+
+      String *getString(Word idx) const;
+
+      bool hasActiveThread() const;
+
+      bool hasModules() const;
+
+      bool isScriptActive(Script *script);
+
+      void loadState(Serial &in);
+
+      void lockStrings() const;
+
+      void refStrings() const;
+
+      void reset();
+
+      void saveState(Serial &out) const;
+
+      bool scriptPause(Script *script);
+      bool scriptPause(ScriptName name, ScopeID scope);
+      bool scriptStart(Script *script, ScriptStartInfo const &info);
+      bool scriptStart(ScriptName name, ScopeID scope, ScriptStartInfo const &info);
+      bool scriptStartForced(Script *script, ScriptStartInfo const &info);
+      bool scriptStartForced(ScriptName name, ScopeID scope, ScriptStartInfo const &info);
+      Word scriptStartResult(Script *script, ScriptStartInfo const &info);
+      Word scriptStartResult(ScriptName name, ScriptStartInfo const &info);
+      Word scriptStartType(Word type, ScriptStartInfo const &info);
+      Word scriptStartTypeForced(Word type, ScriptStartInfo const &info);
+      bool scriptStop(Script *script);
+      bool scriptStop(ScriptName name, ScopeID scope);
+
+      void unlockStrings() const;
+
+      Environment *const env;
+      HubScope    *const hub;
+      Word         const id;
+
+      ListLink<MapScope>     hashLink;
+      ListLink<ScriptAction> scriptAction;
+      ListLink<Thread>       threadActive;
+
+      // Used for untagged string lookup.
+      Module *module0;
+
+      bool active;
+      bool clampCallSpec;
+
+   protected:
+      void freeThread(Thread *thread);
+
+   private:
+      struct PrivData;
+
+      void loadModules(Serial &in);
+      void loadThreads(Serial &in);
+
+      void saveModules(Serial &out) const;
+      void saveThreads(Serial &out) const;
+
+      PrivData *pd;
+   };
+
+   //
+   // ModuleScope
+   //
+   class ModuleScope
+   {
+   public:
+      static constexpr std::size_t ArrC = 256;
+      static constexpr std::size_t RegC = 256;
+
+
+      ModuleScope(ModuleScope const &) = delete;
+      ModuleScope(MapScope *map, Module *module);
+      ~ModuleScope();
+
+      void import();
+
+      void loadState(Serial &in);
+
+      void lockStrings() const;
+
+      void refStrings() const;
+
+      void saveState(Serial &out) const;
+
+      void unlockStrings() const;
+
+      Environment *const env;
+      MapScope    *const map;
+      Module      *const module;
+
+      Array *arrV[ArrC];
+      Word  *regV[RegC];
+
+   private:
+      Array selfArrV[ArrC];
+      Word  selfRegV[RegC];
+   };
+}
+
+#endif//ACSVM__Scope_H__
+
diff --git a/src/acs/vm/ACSVM/Script.cpp b/src/acs/vm/ACSVM/Script.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..15d2d94a732457b417d5a24b6361f80dcc18d3a9
--- /dev/null
+++ b/src/acs/vm/ACSVM/Script.cpp
@@ -0,0 +1,54 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Script class.
+//
+//-----------------------------------------------------------------------------
+
+#include "Script.hpp"
+
+#include "Environment.hpp"
+#include "Module.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Fumnctions                                                          |
+//
+
+namespace ACSVM
+{
+   //
+   // Script constructor
+   //
+   Script::Script(Module *module_) :
+      module{module_},
+
+      name{},
+
+      argC   {0},
+      codeIdx{0},
+      flags  {0},
+      locArrC{0},
+      locRegC{module->env->scriptLocRegC},
+      type   {0},
+
+      flagClient{false},
+      flagNet   {false}
+   {
+   }
+
+   //
+   // Script destructor
+   //
+   Script::~Script()
+   {
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Script.hpp b/src/acs/vm/ACSVM/Script.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..33f8f490569ffc841e50af6ae49e5e36fd3fa3f3
--- /dev/null
+++ b/src/acs/vm/ACSVM/Script.hpp
@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Script class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Script_H__
+#define ACSVM__Script_H__
+
+#include "Types.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // ScriptName
+   //
+   class ScriptName
+   {
+   public:
+      ScriptName() : s{nullptr}, i{0} {}
+      ScriptName(String *s_) : s{s_}, i{0} {}
+      ScriptName(String *s_, Word i_) : s{s_}, i{i_} {}
+      ScriptName(Word i_) : s{nullptr}, i{i_} {}
+
+      String *s;
+      Word    i;
+   };
+
+   //
+   // Script
+   //
+   class Script
+   {
+   public:
+      explicit Script(Module *module);
+      ~Script();
+
+      Module *const module;
+
+      ScriptName name;
+
+      Word argC;
+      Word codeIdx;
+      Word flags;
+      Word locArrC;
+      Word locRegC;
+      Word type;
+
+      bool flagClient : 1;
+      bool flagNet    : 1;
+   };
+}
+
+#endif//ACSVM__Script_H__
+
diff --git a/src/acs/vm/ACSVM/Serial.cpp b/src/acs/vm/ACSVM/Serial.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..83edc0528be18b6f49003032a59ad0304e58533d
--- /dev/null
+++ b/src/acs/vm/ACSVM/Serial.cpp
@@ -0,0 +1,98 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Serialization.
+//
+//-----------------------------------------------------------------------------
+
+#include "Serial.hpp"
+
+#include "BinaryIO.hpp"
+#include "Error.hpp"
+
+#include <cstring>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Serial::loadHead
+   //
+   void Serial::loadHead()
+   {
+      char buf[6] = {};
+      in->read(buf, 6);
+
+      if(std::memcmp(buf, "ACSVM\0", 6))
+         throw SerialError{"invalid file signature"};
+
+      version = ReadVLN<unsigned int>(*in);
+
+      auto flags = ReadVLN<std::uint_fast32_t>(*in);
+      signs = flags & 0x0001;
+   }
+
+   //
+   // Serial::loadTail
+   //
+   void Serial::loadTail()
+   {
+      readSign(~Signature::Serial);
+   }
+
+   //
+   // Serial::readSign
+   //
+   void Serial::readSign(Signature sign)
+   {
+      if(!signs) return;
+
+      auto got = static_cast<Signature>(ReadLE4(*in));
+
+      if(sign != got)
+         throw SerialSignError{sign, got};
+   }
+
+   //
+   // Serial::saveHead
+   //
+   void Serial::saveHead()
+   {
+      out->write("ACSVM\0", 6);
+      WriteVLN(*out, 0);
+
+      std::uint_fast32_t flags = 0;
+      if(signs) flags |= 0x0001;
+      WriteVLN(*out, flags);
+   }
+
+   //
+   // Serial::saveTail
+   //
+   void Serial::saveTail()
+   {
+      writeSign(~Signature::Serial);
+   }
+
+   //
+   // Serial::writeSign
+   //
+   void Serial::writeSign(Signature sign)
+   {
+      if(!signs) return;
+
+      WriteLE4(*out, static_cast<std::uint32_t>(sign));
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Serial.hpp b/src/acs/vm/ACSVM/Serial.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..98e1673724d71d443615510e131f1ffc8e1a0bb5
--- /dev/null
+++ b/src/acs/vm/ACSVM/Serial.hpp
@@ -0,0 +1,89 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Serialization.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Serial_H__
+#define ACSVM__Serial_H__
+
+#include "ID.hpp"
+
+#include <istream>
+#include <ostream>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Signature
+   //
+   enum class Signature : std::uint32_t
+   {
+      Array       = MakeID("ARAY"),
+      Environment = MakeID("ENVI"),
+      GlobalScope = MakeID("GBLs"),
+      HubScope    = MakeID("HUBs"),
+      MapScope    = MakeID("MAPs"),
+      ModuleScope = MakeID("MODs"),
+      Serial      = MakeID("SERI"),
+      Thread      = MakeID("THRD"),
+   };
+
+   //
+   // Serial
+   //
+   class Serial
+   {
+   public:
+      Serial(std::istream &in_) : in{&in_} {}
+      Serial(std::ostream &out_) : out{&out_},
+         version{0}, signs{false} {}
+
+      operator std::istream & () {return *in;}
+      operator std::ostream & () {return *out;}
+
+      void loadHead();
+      void loadTail();
+
+      void readSign(Signature sign);
+
+      void saveHead();
+      void saveTail();
+
+      void writeSign(Signature sign);
+
+      union
+      {
+         std::istream *const in;
+         std::ostream *const out;
+      };
+
+      unsigned int version;
+      bool         signs;
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   constexpr Signature operator ~ (Signature sign)
+      {return static_cast<Signature>(~static_cast<std::uint32_t>(sign));}
+}
+
+#endif//ACSVM__Serial_H__
+
diff --git a/src/acs/vm/ACSVM/Stack.hpp b/src/acs/vm/ACSVM/Stack.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9fb72cc53e4e033357dab1e9396e5006bef93460
--- /dev/null
+++ b/src/acs/vm/ACSVM/Stack.hpp
@@ -0,0 +1,110 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Stack class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Stack_H__
+#define ACSVM__Stack_H__
+
+#include <climits>
+#include <new>
+#include <utility>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Stack
+   //
+   // Stack container.
+   //
+   template<typename T>
+   class Stack
+   {
+   public:
+      Stack() : stack{nullptr}, stkEnd{nullptr}, stkPtr{nullptr} {}
+      ~Stack() {clear(); ::operator delete(stack);}
+
+      // operator []
+      T &operator [] (std::size_t idx) {return *(stkPtr - idx);}
+
+      // begin
+      T       *begin()       {return stack;}
+      T const *begin() const {return stack;}
+
+      // clear
+      void clear() {while(stkPtr != stack) (--stkPtr)->~T();}
+
+      // drop
+      void drop() {(--stkPtr)->~T();}
+      void drop(std::size_t n) {while(n--) (--stkPtr)->~T();}
+
+      // empty
+      bool empty() const {return stkPtr == stack;}
+
+      // end
+      T       *end()       {return stkPtr;}
+      T const *end() const {return stkPtr;}
+
+      // push
+      void push(T const &value) {new(stkPtr++) T(          value );}
+      void push(T      &&value) {new(stkPtr++) T(std::move(value));}
+
+      //
+      // reserve
+      //
+      void reserve(std::size_t count)
+      {
+         if(static_cast<std::size_t>(stkEnd - stkPtr) >= count)
+            return;
+
+         // Save pointers as indexes.
+         std::size_t idxEnd = stkEnd - stack;
+         std::size_t idxPtr = stkPtr - stack;
+
+         // Calculate new array size.
+         if(SIZE_MAX / sizeof(T) - idxEnd < count * 2)
+            throw std::bad_alloc();
+
+         idxEnd += count * 2;
+
+         // Allocate and initialize new array.
+         T *stackNew = static_cast<T *>(::operator new(idxEnd * sizeof(T)));
+         for(T *itrNew = stackNew, *itr = stack, *end = stkPtr; itr != end;)
+         {
+            new(itrNew++) T(std::move(*itr++));
+            itr->~T();
+         }
+
+         // Free old array.
+         ::operator delete(stack);
+
+         // Restore pointers.
+         stack  = stackNew;
+         stkPtr = stack + idxPtr;
+         stkEnd = stack + idxEnd;
+      }
+
+      // size
+      std::size_t size() const {return stkPtr - stack;}
+
+   private:
+      T *stack;
+      T *stkEnd;
+      T *stkPtr;
+   };
+}
+
+#endif//ACSVM__Stack_H__
+
diff --git a/src/acs/vm/ACSVM/Store.hpp b/src/acs/vm/ACSVM/Store.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..713b4a4b8d573a6d8e6587a78fef0a60083f8568
--- /dev/null
+++ b/src/acs/vm/ACSVM/Store.hpp
@@ -0,0 +1,150 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Store class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Store_H__
+#define ACSVM__Store_H__
+
+#include "Types.hpp"
+
+#include <new>
+#include <utility>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Store
+   //
+   // Manages storage area for locals.
+   //
+   template<typename T>
+   class Store
+   {
+   public:
+      Store() : store{nullptr}, storeEnd{nullptr}, active{nullptr}, activeEnd{nullptr} {}
+      ~Store() {clear(); ::operator delete(store);}
+
+      // operator []
+      T &operator [] (std::size_t idx) {return active[idx];}
+
+      //
+      // alloc
+      //
+      void alloc(std::size_t count)
+      {
+         // Possibly reallocate underlying storage.
+         if(static_cast<std::size_t>(storeEnd - activeEnd) < count)
+         {
+            // Save pointers as indexes.
+            std::size_t activeIdx    = active    - store;
+            std::size_t activeEndIdx = activeEnd - store;
+            std::size_t storeEndIdx  = storeEnd  - store;
+
+            // Calculate new array size.
+            if(SIZE_MAX / sizeof(T) - storeEndIdx < count * 2)
+               throw std::bad_alloc();
+
+            storeEndIdx += count * 2;
+
+            // Allocate and initialize new array.
+            T *storeNew = static_cast<T *>(::operator new(storeEndIdx * sizeof(T)));
+            for(T *out = storeNew, *in = store, *end = activeEnd; in != end; ++out, ++in)
+            {
+               new(out) T(std::move(*in));
+               in->~T();
+            }
+
+            // Free old array.
+            ::operator delete(store);
+
+            // Restore pointers.
+            store     = storeNew;
+            active    = store + activeIdx;
+            activeEnd = store + activeEndIdx;
+            storeEnd  = store + storeEndIdx;
+         }
+
+         active = activeEnd;
+         while(count--) new(activeEnd++) T{};
+      }
+
+      //
+      // allocLoad
+      //
+      // Allocates storage for loading from saved state. countFull elements are
+      // value-initialized and count elements are made available. That is, they
+      // should correspond to a prior call to sizeFull and size, respectively.
+      //
+      void allocLoad(std::size_t countFull, std::size_t count)
+      {
+         clear();
+         alloc(countFull);
+         active = activeEnd - count;
+      }
+
+      // begin
+      T       *begin()       {return active;}
+      T const *begin() const {return active;}
+
+      // beginFull
+      T       *beginFull()       {return store;}
+      T const *beginFull() const {return store;}
+
+      //
+      // clear
+      //
+      void clear()
+      {
+         while(activeEnd != store)
+           (--activeEnd)->~T();
+
+         active = store;
+      }
+
+      // dataFull
+      T const *dataFull() const {return store;}
+
+      // end
+      T       *end()       {return activeEnd;}
+      T const *end() const {return activeEnd;}
+
+      //
+      // free
+      //
+      // count must be the size (in elements) of the previous allocation.
+      //
+      void free(std::size_t count)
+      {
+        while(activeEnd != active)
+           (--activeEnd)->~T();
+
+        active -= count;
+      }
+
+      // size
+      std::size_t size() const {return activeEnd - active;}
+
+      // sizeFull
+      std::size_t sizeFull() const {return activeEnd - store;}
+
+   private:
+      T *store,  *storeEnd;
+      T *active, *activeEnd;
+   };
+}
+
+#endif//ACSVM__Store_H__
+
diff --git a/src/acs/vm/ACSVM/String.cpp b/src/acs/vm/ACSVM/String.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d0b0b59524d82ce6cef0a09556bff67f90e0e8c1
--- /dev/null
+++ b/src/acs/vm/ACSVM/String.cpp
@@ -0,0 +1,354 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// String classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "String.hpp"
+
+#include "BinaryIO.hpp"
+#include "HashMap.hpp"
+
+#include <new>
+#include <vector>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // StringTable::PrivData
+   //
+   struct StringTable::PrivData
+   {
+      std::vector<Word> freeIdx;
+
+      HashMapKeyObj<StringData, String, &String::link> stringByData{64, 64};
+      std::vector<String *>                            stringByIdx;
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // String constructor
+   //
+   String::String(StringData const &data, Word idx_) :
+      StringData{data}, lock{0}, idx{idx_}, len0(std::strlen(str)), link{this}
+   {
+   }
+
+   //
+   // String destructor
+   //
+   String::~String()
+   {
+   }
+
+   //
+   // String::Delete
+   //
+   void String::Delete(String *str)
+   {
+      str->~String();
+      operator delete(str);
+   }
+
+   //
+   // String::New
+   //
+   String *String::New(StringData const &data, Word idx)
+   {
+      String *str = static_cast<String *>(operator new(sizeof(String) + data.len + 1));
+      char   *buf = reinterpret_cast<char *>(str + 1);
+
+      memcpy(buf, data.str, data.len);
+      buf[data.len] = '\0';
+
+      return new(str) String{{buf, data.len, data.hash}, idx};
+   }
+
+   //
+   // String::Read
+   //
+   String *String::Read(std::istream &in, Word idx)
+   {
+      std::size_t len = ReadVLN<std::size_t>(in);
+
+      String *str = static_cast<String *>(operator new(sizeof(String) + len + 1));
+      char   *buf = reinterpret_cast<char *>(str + 1);
+
+      in.read(buf, len);
+      buf[len] = '\0';
+
+      return new(str) String{{buf, len, StrHash(buf, len)}, idx};
+   }
+
+   //
+   // String::Write
+   //
+   void String::Write(std::ostream &out, String *in)
+   {
+      WriteVLN(out, in->len);
+      out.write(in->str, in->len);
+   }
+
+   //
+   // StringTable constructor
+   //
+   StringTable::StringTable() :
+      strV{nullptr},
+      strC{0},
+
+      strNone{String::New({"", 0, 0}, 0)},
+
+      pd{new PrivData}
+   {
+   }
+
+   //
+   // StringTable move constructor
+   //
+   StringTable::StringTable(StringTable &&table) :
+      strV{table.strV},
+      strC{table.strC},
+
+      strNone{table.strNone},
+
+      pd{table.pd}
+   {
+      table.strV = nullptr;
+      table.strC = 0;
+
+      table.strNone = nullptr;
+
+      table.pd = nullptr;
+   }
+
+   //
+   // StringTable destructor
+   //
+   StringTable::~StringTable()
+   {
+      if(!pd) return;
+
+      clear();
+
+      delete pd;
+
+      String::Delete(strNone);
+   }
+
+   //
+   // StringTable::operator [StringData]
+   //
+   String &StringTable::operator [] (StringData const &data)
+   {
+      if(auto str = pd->stringByData.find(data)) return *str;
+
+      Word idx;
+      if(pd->freeIdx.empty())
+      {
+         // Index has to fit within Word size.
+         // If size_t has an equal or lesser max, then the check is redundant,
+         // and some compilers warn about that kind of tautological comparison.
+         #if SIZE_MAX > UINT32_MAX
+         if(pd->stringByIdx.size() > UINT32_MAX)
+            throw std::bad_alloc();
+         #endif
+
+         idx = pd->stringByIdx.size();
+         pd->stringByIdx.emplace_back(strNone);
+         strV = pd->stringByIdx.data();
+         strC = pd->stringByIdx.size();
+      }
+      else
+      {
+         idx = pd->freeIdx.back();
+         pd->freeIdx.pop_back();
+      }
+
+      String *str = String::New(data, idx);
+      pd->stringByIdx[idx] = str;
+      pd->stringByData.insert(str);
+      return *str;
+   }
+
+   //
+   // StringTable::clear
+   //
+   void StringTable::clear()
+   {
+      for(auto &str : pd->stringByIdx)
+      {
+         if(str != strNone)
+            String::Delete(str);
+      }
+
+      pd->freeIdx.clear();
+      pd->stringByData.clear();
+      pd->stringByIdx.clear();
+
+      strV = nullptr;
+      strC = 0;
+   }
+
+   //
+   // StringTable::collectBegin
+   //
+   void StringTable::collectBegin()
+   {
+      for(auto &str : pd->stringByData)
+         str.ref = false;
+   }
+
+   //
+   // StringTable.collectEnd
+   //
+   void StringTable::collectEnd()
+   {
+      for(auto itr = pd->stringByData.begin(), end = pd->stringByData.end(); itr != end;)
+      {
+         if(!itr->ref && !itr->lock)
+         {
+            String &str = *itr++;
+            pd->stringByIdx[str.idx] = strNone;
+            pd->freeIdx.push_back(str.idx);
+            pd->stringByData.unlink(&str);
+            String::Delete(&str);
+         }
+         else
+            ++itr;
+      }
+   }
+
+   //
+   // StringTable::loadState
+   //
+   void StringTable::loadState(std::istream &in)
+   {
+      if(pd)
+      {
+         clear();
+      }
+      else
+      {
+         pd      = new PrivData;
+         strNone = String::New({"", 0, 0}, 0);
+      }
+
+      auto count = ReadVLN<std::size_t>(in);
+
+      pd->stringByIdx.resize(count);
+      strV = pd->stringByIdx.data();
+      strC = pd->stringByIdx.size();
+
+      for(std::size_t idx = 0; idx != count; ++idx)
+      {
+         if(in.get())
+         {
+            String *str = String::Read(in, idx);
+            str->lock = ReadVLN<std::size_t>(in);
+            pd->stringByIdx[idx] = str;
+            pd->stringByData.insert(str);
+         }
+         else
+         {
+            pd->stringByIdx[idx] = strNone;
+            pd->freeIdx.emplace_back(idx);
+         }
+      }
+   }
+
+   //
+   // StringTable::saveState
+   //
+   void StringTable::saveState(std::ostream &out) const
+   {
+      WriteVLN(out, pd->stringByIdx.size());
+
+      for(String *&str : pd->stringByIdx)
+      {
+         if(str != strNone)
+         {
+            out << '\1';
+
+            String::Write(out, str);
+            WriteVLN(out, str->lock);
+         }
+         else
+            out << '\0';
+      }
+   }
+
+   //
+   // StringTable::size
+   //
+   std::size_t StringTable::size() const
+   {
+      return pd->stringByData.size();
+   }
+
+   //
+   // StrDup
+   //
+   std::unique_ptr<char[]> StrDup(char const *str)
+   {
+      return StrDup(str, std::strlen(str));
+   }
+
+   //
+   // StrDup
+   //
+   std::unique_ptr<char[]> StrDup(char const *str, std::size_t len)
+   {
+      std::unique_ptr<char[]> dup{new char[len + 1]};
+      std::memcpy(dup.get(), str, len);
+      dup[len] = '\0';
+
+      return dup;
+   }
+
+   //
+   // StrHash
+   //
+   std::size_t StrHash(char const *str)
+   {
+      std::size_t hash = 0;
+
+      if(str) while(*str)
+         hash = hash * 5 + static_cast<unsigned char>(*str++);
+
+      return hash;
+   }
+
+   //
+   // StrHash
+   //
+   std::size_t StrHash(char const *str, std::size_t len)
+   {
+      std::size_t hash = 0;
+
+      while(len--)
+         hash = hash * 5 + static_cast<unsigned char>(*str++);
+
+      return hash;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/String.hpp b/src/acs/vm/ACSVM/String.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..7c46e4f9205bb4572c71e30cedeff6d8d962e0c2
--- /dev/null
+++ b/src/acs/vm/ACSVM/String.hpp
@@ -0,0 +1,159 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// String classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__String_H__
+#define ACSVM__String_H__
+
+#include "List.hpp"
+#include "Types.hpp"
+
+#include <cstring>
+#include <functional>
+#include <memory>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   std::size_t StrHash(char const *str, std::size_t len);
+
+   //
+   // StringData
+   //
+   // Stores basic string information. Does not manage the storage for the
+   // string data.
+   //
+   class StringData
+   {
+   public:
+      StringData(char const *first, char const *last) :
+         str{first}, len(last - first), hash{StrHash(str, len)} {}
+      StringData(char const *str_, std::size_t len_) :
+         str{str_}, len{len_}, hash{StrHash(str, len)} {}
+      StringData(char const *str_, std::size_t len_, std::size_t hash_) :
+         str{str_}, len{len_}, hash{hash_} {}
+
+      bool operator == (StringData const &r) const
+         {return hash == r.hash && len == r.len && !std::memcmp(str, r.str, len);}
+
+      char const *const str;
+      std::size_t const len;
+      std::size_t const hash;
+   };
+
+   //
+   // String
+   //
+   // Indexed string data.
+   //
+   class String : public StringData
+   {
+   public:
+      std::size_t lock;
+
+      Word const idx;  // Index into table.
+      Word const len0; // Null-terminated length.
+
+      bool ref;
+
+      char get(std::size_t i) const {return i < len ? str[i] : '\0';}
+
+
+      friend class StringTable;
+
+   private:
+      String(StringData const &data, Word idx);
+      ~String();
+
+      ListLink<String> link;
+
+
+      static void Delete(String *str);
+
+      static String *New(StringData const &data, Word idx);
+
+      static String *Read(std::istream &in, Word idx);
+
+      static void Write(std::ostream &out, String *in);
+   };
+
+   //
+   // StringTable
+   //
+   class StringTable
+   {
+   public:
+      StringTable();
+      StringTable(StringTable &&table);
+      ~StringTable();
+
+      String &operator [] (Word idx) const
+         {return idx < strC ? *strV[idx] : *strNone;}
+      String &operator [] (StringData const &data);
+
+      void clear();
+
+      void collectBegin();
+      void collectEnd();
+
+      String &getNone() {return *strNone;}
+
+      void loadState(std::istream &in);
+
+      void saveState(std::ostream &out) const;
+
+      std::size_t size() const;
+
+   private:
+      struct PrivData;
+
+      String    **strV;
+      std::size_t strC;
+
+      String *strNone;
+
+      PrivData *pd;
+   };
+}
+
+namespace std
+{
+   //
+   // hash<::ACSVM::StringData>
+   //
+   template<>
+   struct hash<::ACSVM::StringData>
+   {
+      size_t operator () (::ACSVM::StringData const &data) const
+         {return data.hash;}
+   };
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   std::unique_ptr<char[]> StrDup(char const *str);
+   std::unique_ptr<char[]> StrDup(char const *str, std::size_t len);
+
+   std::size_t StrHash(char const *str);
+   std::size_t StrHash(char const *str, std::size_t len);
+}
+
+#endif//ACSVM__String_H__
+
diff --git a/src/acs/vm/ACSVM/Thread.cpp b/src/acs/vm/ACSVM/Thread.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c4d19a7a8c30a070656f35a3732a6804ba627340
--- /dev/null
+++ b/src/acs/vm/ACSVM/Thread.cpp
@@ -0,0 +1,292 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Thread classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Thread.hpp"
+
+#include "Array.hpp"
+#include "BinaryIO.hpp"
+#include "Environment.hpp"
+#include "Module.hpp"
+#include "Scope.hpp"
+#include "Script.hpp"
+#include "Serial.hpp"
+
+#include <algorithm>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Thread constructor
+   //
+   Thread::Thread(Environment *env_) :
+      env{env_},
+
+      link{this},
+
+      codePtr {nullptr},
+      module  {nullptr},
+      scopeGbl{nullptr},
+      scopeHub{nullptr},
+      scopeMap{nullptr},
+      scopeMod{nullptr},
+      script  {nullptr},
+      delay   {0},
+      result  {0}
+   {
+   }
+
+   //
+   // Thread destructor
+   //
+   Thread::~Thread()
+   {
+   }
+
+   //
+   // Thread::getInfo
+   //
+   ThreadInfo const *Thread::getInfo() const
+   {
+      return nullptr;
+   }
+
+   //
+   // Thread::loadState
+   //
+   void Thread::loadState(Serial &in)
+   {
+      std::size_t count, countFull;
+
+      in.readSign(Signature::Thread);
+
+      module   = env->getModule(env->readModuleName(in));
+      codePtr  = &module->codeV[ReadVLN<std::size_t>(in)];
+      scopeGbl = env->getGlobalScope(ReadVLN<Word>(in));
+      scopeHub = scopeGbl->getHubScope(ReadVLN<Word>(in));
+      scopeMap = scopeHub->getMapScope(ReadVLN<Word>(in));
+      scopeMod = scopeMap->getModuleScope(module);
+      script   = env->readScript(in);
+      delay    = ReadVLN<Word>(in);
+      result   = ReadVLN<Word>(in);
+
+      count = ReadVLN<std::size_t>(in);
+      callStk.clear(); callStk.reserve(count + CallStkSize);
+      while(count--)
+         callStk.push(readCallFrame(in));
+
+      count = ReadVLN<std::size_t>(in);
+      dataStk.clear(); dataStk.reserve(count + DataStkSize);
+      while(count--)
+         dataStk.push(ReadVLN<Word>(in));
+
+      countFull = ReadVLN<std::size_t>(in);
+      count     = ReadVLN<std::size_t>(in);
+      localArr.allocLoad(countFull, count);
+      for(auto itr = localArr.beginFull(), end = localArr.end(); itr != end; ++itr)
+         itr->loadState(in);
+
+      countFull = ReadVLN<std::size_t>(in);
+      count     = ReadVLN<std::size_t>(in);
+      localReg.allocLoad(countFull, count);
+      for(auto itr = localReg.beginFull(), end = localReg.end(); itr != end; ++itr)
+         *itr = ReadVLN<Word>(in);
+
+      countFull = ReadVLN<std::size_t>(in);
+      count     = ReadVLN<std::size_t>(in);
+      in.in->read(printBuf.getLoadBuf(countFull, count), countFull);
+
+      state.state = static_cast<ThreadState::State>(ReadVLN<int>(in));
+      state.data = ReadVLN<Word>(in);
+      state.type = ReadVLN<Word>(in);
+
+      in.readSign(~Signature::Thread);
+   }
+
+   //
+   // Thread::lockStrings
+   //
+   void Thread::lockStrings() const
+   {
+      for(auto &data : dataStk)
+         ++env->getString(data)->lock;
+
+      for(auto arr = localArr.beginFull(), end = localArr.end(); arr != end; ++arr)
+         arr->lockStrings(env);
+
+      for(auto reg = localReg.beginFull(), end = localReg.end(); reg != end; ++reg)
+         ++env->getString(*reg)->lock;
+
+      if(state == ThreadState::WaitScrS)
+         ++env->getString(state.data)->lock;
+   }
+
+   //
+   // Thread::readCallFrame
+   //
+   CallFrame Thread::readCallFrame(Serial &in) const
+   {
+      CallFrame out;
+
+      out.module   = env->getModule(env->readModuleName(in));
+      out.scopeMod = scopeMap->getModuleScope(out.module);
+      out.codePtr  = &out.module->codeV[ReadVLN<std::size_t>(in)];
+      out.locArrC  = ReadVLN<std::size_t>(in);
+      out.locRegC  = ReadVLN<std::size_t>(in);
+
+      return out;
+   }
+
+   //
+   // Thread::refStrings
+   //
+   void Thread::refStrings() const
+   {
+      for(auto &data : dataStk)
+         env->getString(data)->ref = true;
+
+      for(auto arr = localArr.beginFull(), end = localArr.end(); arr != end; ++arr)
+         arr->refStrings(env);
+
+      for(auto reg = localReg.beginFull(), end = localReg.end(); reg != end; ++reg)
+         env->getString(*reg)->ref = true;
+
+      if(state == ThreadState::WaitScrS)
+         env->getString(state.data)->ref = true;
+   }
+
+   //
+   // Thread::saveState
+   //
+   void Thread::saveState(Serial &out) const
+   {
+      out.writeSign(Signature::Thread);
+
+      env->writeModuleName(out, module->name);
+      WriteVLN(out, codePtr - module->codeV.data());
+      WriteVLN(out, scopeGbl->id);
+      WriteVLN(out, scopeHub->id);
+      WriteVLN(out, scopeMap->id);
+      env->writeScript(out, script);
+      WriteVLN(out, delay);
+      WriteVLN(out, result);
+
+      WriteVLN(out, callStk.size());
+      for(auto &call : callStk)
+         writeCallFrame(out, call);
+
+      WriteVLN(out, dataStk.size());
+      for(auto &data : dataStk)
+         WriteVLN(out, data);
+
+      WriteVLN(out, localArr.sizeFull());
+      WriteVLN(out, localArr.size());
+      for(auto itr = localArr.beginFull(), end = localArr.end(); itr != end; ++itr)
+         itr->saveState(out);
+
+      WriteVLN(out, localReg.sizeFull());
+      WriteVLN(out, localReg.size());
+      for(auto itr = localReg.beginFull(), end = localReg.end(); itr != end; ++itr)
+         WriteVLN(out, *itr);
+
+      WriteVLN(out, printBuf.sizeFull());
+      WriteVLN(out, printBuf.size());
+      out.out->write(printBuf.dataFull(), printBuf.sizeFull());
+
+      WriteVLN<int>(out, state.state);
+      WriteVLN(out, state.data);
+      WriteVLN(out, state.type);
+
+      out.writeSign(~Signature::Thread);
+   }
+
+   //
+   // Thread::start
+   //
+   void Thread::start(Script *script_, MapScope *map, ThreadInfo const *,
+      Word const *argV, Word argC)
+   {
+      link.insert(&map->threadActive);
+
+      script  = script_;
+      module  = script->module;
+      codePtr = &module->codeV[script->codeIdx];
+
+      scopeMod = map->getModuleScope(module);
+      scopeMap = map;
+      scopeHub = scopeMap->hub;
+      scopeGbl = scopeHub->global;
+
+      callStk.reserve(CallStkSize);
+      dataStk.reserve(DataStkSize);
+      localArr.alloc(script->locArrC);
+      localReg.alloc(script->locRegC);
+
+      std::copy(argV, argV + std::min<Word>(argC, script->argC), &localReg[0]);
+
+      delay  = 0;
+      result = 0;
+      state  = ThreadState::Running;
+   }
+
+   //
+   // Thread::stop
+   //
+   void Thread::stop()
+   {
+      // Release execution resources.
+      callStk.clear();
+      dataStk.clear();
+      localArr.clear();
+      localReg.clear();
+      printBuf.clear();
+
+      // Set state.
+      state = ThreadState::Inactive;
+   }
+
+   //
+   // Thread::unlockStrings
+   //
+   void Thread::unlockStrings() const
+   {
+      for(auto &data : dataStk)
+         --env->getString(data)->lock;
+
+      for(auto arr = localArr.beginFull(), end = localArr.end(); arr != end; ++arr)
+         arr->unlockStrings(env);
+
+      for(auto reg = localReg.beginFull(), end = localReg.end(); reg != end; ++reg)
+         --env->getString(*reg)->lock;
+
+      if(state == ThreadState::WaitScrS)
+         --env->getString(state.data)->lock;
+   }
+
+   //
+   // Thread::writeCallFrame
+   //
+   void Thread::writeCallFrame(Serial &out, CallFrame const &in) const
+   {
+      env->writeModuleName(out, in.module->name);
+      WriteVLN(out, in.codePtr - in.module->codeV.data());
+      WriteVLN(out, in.locArrC);
+      WriteVLN(out, in.locRegC);
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Thread.hpp b/src/acs/vm/ACSVM/Thread.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..06d4b34d0d11173888ad51eb21f668862ff22c4b
--- /dev/null
+++ b/src/acs/vm/ACSVM/Thread.hpp
@@ -0,0 +1,157 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Thread classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Thread_H__
+#define ACSVM__Thread_H__
+
+#include "List.hpp"
+#include "PrintBuf.hpp"
+#include "Stack.hpp"
+#include "Store.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // CallFrame
+   //
+   // Stores a call frame for execution.
+   //
+   class CallFrame
+   {
+   public:
+      Word  const *codePtr;
+      Module      *module;
+      ModuleScope *scopeMod;
+      std::size_t  locArrC;
+      std::size_t  locRegC;
+   };
+
+   //
+   // ThreadState
+   //
+   class ThreadState
+   {
+   public:
+      enum State
+      {
+         Inactive, // Inactive thread.
+         Running,  // Running.
+         Stopped,  // Will go inactive on next exec.
+         Paused,   // Paused by instruction.
+         WaitScrI, // Waiting on a numbered script.
+         WaitScrS, // Waiting on a named script.
+         WaitTag,  // Waiting on tagged object.
+      };
+
+
+      ThreadState() : state{Inactive}, data{0}, type{0} {}
+      ThreadState(State state_) :
+         state{state_}, data{0}, type{0} {}
+      ThreadState(State state_, Word data_) :
+         state{state_}, data{data_}, type{0} {}
+      ThreadState(State state_, Word data_, Word type_) :
+         state{state_}, data{data_}, type{type_} {}
+
+      bool operator == (State s) const {return state == s;}
+      bool operator != (State s) const {return state != s;}
+
+      State state;
+
+      // Extra state data. Used by:
+      //    WaitScrI - Script number.
+      //    WaitScrS - Script name index.
+      //    WaitTag  - Tag number.
+      Word data;
+
+      // Extra state data. Used by:
+      //    WaitTag - Tag type.
+      Word type;
+   };
+
+   //
+   // ThreadInfo
+   //
+   // Derived classes can be used to pass extra information to started threads.
+   //
+   class ThreadInfo
+   {
+   public:
+      virtual ~ThreadInfo() {}
+   };
+
+   //
+   // Thread
+   //
+   class Thread
+   {
+   public:
+      Thread(Environment *env);
+      virtual ~Thread();
+
+      void exec();
+
+      virtual ThreadInfo const *getInfo() const;
+
+      virtual void loadState(Serial &in);
+
+      virtual void lockStrings() const;
+
+      virtual void refStrings() const;
+
+      virtual void saveState(Serial &out) const;
+
+      virtual void start(Script *script, MapScope *map, ThreadInfo const *info,
+         Word const *argV, Word argC);
+
+      virtual void stop();
+
+      virtual void unlockStrings() const;
+
+      Environment *const env;
+
+      ListLink<Thread> link;
+
+      Stack<CallFrame> callStk;
+      Stack<Word>      dataStk;
+      Store<Array>     localArr;
+      Store<Word>      localReg;
+      PrintBuf         printBuf;
+      ThreadState      state;
+
+      Word  const *codePtr; // Instruction pointer.
+      Module      *module;  // Current execution Module.
+      GlobalScope *scopeGbl;
+      HubScope    *scopeHub;
+      MapScope    *scopeMap;
+      ModuleScope *scopeMod;
+      Script      *script;  // Current execution Script.
+      Word         delay;   // Execution delay tics.
+      Word         result;  // Code-defined thread result.
+
+
+      static constexpr std::size_t CallStkSize =   8;
+      static constexpr std::size_t DataStkSize = 256;
+
+   private:
+      CallFrame readCallFrame(Serial &in) const;
+
+      void writeCallFrame(Serial &out, CallFrame const &in) const;
+   };
+}
+
+#endif//ACSVM__Thread_H__
+
diff --git a/src/acs/vm/ACSVM/ThreadExec.cpp b/src/acs/vm/ACSVM/ThreadExec.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6754800a4e3cd5594088c3a709932cd52db07e45
--- /dev/null
+++ b/src/acs/vm/ACSVM/ThreadExec.cpp
@@ -0,0 +1,643 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Thread execution.
+//
+//-----------------------------------------------------------------------------
+
+#include "Thread.hpp"
+
+#include "Array.hpp"
+#include "Code.hpp"
+#include "Environment.hpp"
+#include "Function.hpp"
+#include "Jump.hpp"
+#include "Module.hpp"
+#include "Scope.hpp"
+#include "Script.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Macros                                                                     |
+//
+
+//
+// ACSVM_DynamicGoto
+//
+// If nonzero, enables use of dynamic goto labels in core interpreter loop.
+// Currently, only gcc syntax is supported.
+//
+#ifndef ACSVM_DynamicGoto
+#if defined(__GNUC__)
+#define ACSVM_DynamicGoto 1
+#else
+#define ACSVM_DynamicGoto 0
+#endif
+#endif
+
+//
+// BranchTo
+//
+#define BranchTo(target) \
+   do \
+   { \
+      codePtr = &module->codeV[(target)]; \
+      CountBranch(); \
+   } \
+   while(0)
+
+//
+// CountBranch
+//
+// Used to limit the number of branches to prevent infinite no-delay loops.
+//
+#define CountBranch() \
+   if(branches && !--branches) \
+   { \
+      env->printKill(this, static_cast<Word>(KillType::BranchLimit), 0); \
+      goto thread_stop; \
+   } \
+   else \
+      ((void)0)
+
+//
+// DeclCase
+//
+#if ACSVM_DynamicGoto
+#define DeclCase(name) case_Code##name
+#else
+#define DeclCase(name) case static_cast<Word>(Code::name)
+#endif
+
+//
+// NextCase
+//
+#if ACSVM_DynamicGoto
+#define NextCase() goto *cases[*codePtr++]
+#else
+#define NextCase() goto next_case
+#endif
+
+//
+// Op_*
+//
+
+#define Op_AddU(lop) (dataStk.drop(), (lop) += dataStk[0])
+#define Op_AndU(lop) (dataStk.drop(), (lop) &= dataStk[0])
+#define Op_CmpI_GE(lop) (dataStk.drop(), OpFunc_CmpI_GE(lop, dataStk[0]))
+#define Op_CmpI_GT(lop) (dataStk.drop(), OpFunc_CmpI_GT(lop, dataStk[0]))
+#define Op_CmpI_LE(lop) (dataStk.drop(), OpFunc_CmpI_LE(lop, dataStk[0]))
+#define Op_CmpI_LT(lop) (dataStk.drop(), OpFunc_CmpI_LT(lop, dataStk[0]))
+#define Op_CmpU_EQ(lop) (dataStk.drop(), OpFunc_CmpU_EQ(lop, dataStk[0]))
+#define Op_CmpU_NE(lop) (dataStk.drop(), OpFunc_CmpU_NE(lop, dataStk[0]))
+#define Op_DecU(lop) (--(lop))
+#define Op_DivI(lop) (dataStk.drop(), OpFunc_DivI(lop, dataStk[0]))
+#define Op_DivX(lop) (dataStk.drop(), OpFunc_DivX(lop, dataStk[0]))
+#define Op_Drop(lop) (dataStk.drop(), (lop) = dataStk[0])
+#define Op_IncU(lop) (++(lop))
+#define Op_LAnd(lop) (dataStk.drop(), OpFunc_LAnd(lop, dataStk[0]))
+#define Op_LOrI(lop) (dataStk.drop(), OpFunc_LOrI(lop, dataStk[0]))
+#define Op_ModI(lop) (dataStk.drop(), OpFunc_ModI(lop, dataStk[0]))
+#define Op_MulU(lop) (dataStk.drop(), (lop) *= dataStk[0])
+#define Op_MulX(lop) (dataStk.drop(), OpFunc_MulX(lop, dataStk[0]))
+#define Op_OrIU(lop) (dataStk.drop(), (lop) |= dataStk[0])
+#define Op_OrXU(lop) (dataStk.drop(), (lop) ^= dataStk[0])
+#define Op_ShLU(lop) (dataStk.drop(), (lop) <<= dataStk[0] & 31)
+#define Op_ShRI(lop) (dataStk.drop(), OpFunc_ShRI(lop, dataStk[0]))
+#define Op_SubU(lop) (dataStk.drop(), (lop) -= dataStk[0])
+
+//
+// OpSet
+//
+#define OpSet(op) \
+   DeclCase(op##_GblArr): \
+      Op_##op(scopeGbl->arrV[*codePtr++][dataStk[1]]); dataStk.drop(); \
+      NextCase(); \
+   DeclCase(op##_GblReg): \
+      Op_##op(scopeGbl->regV[*codePtr++]); \
+      NextCase(); \
+   DeclCase(op##_HubArr): \
+      Op_##op(scopeHub->arrV[*codePtr++][dataStk[1]]); dataStk.drop(); \
+      NextCase(); \
+   DeclCase(op##_HubReg): \
+      Op_##op(scopeHub->regV[*codePtr++]); \
+      NextCase(); \
+   DeclCase(op##_LocArr): \
+      Op_##op(localArr[*codePtr++][dataStk[1]]); dataStk.drop(); \
+      NextCase(); \
+   DeclCase(op##_LocReg): \
+      Op_##op(localReg[*codePtr++]); \
+      NextCase(); \
+   DeclCase(op##_ModArr): \
+      Op_##op((*scopeMod->arrV[*codePtr++])[dataStk[1]]); dataStk.drop(); \
+      NextCase(); \
+   DeclCase(op##_ModReg): \
+      Op_##op(*scopeMod->regV[*codePtr++]); \
+      NextCase()
+
+
+//----------------------------------------------------------------------------|
+// Static Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // OpFunc_CmpI_GE
+   //
+   static inline void OpFunc_CmpI_GE(Word &lop, Word rop)
+   {
+      lop = static_cast<SWord>(lop) >= static_cast<SWord>(rop);
+   }
+
+   //
+   // OpFunc_CmpI_GT
+   //
+   static inline void OpFunc_CmpI_GT(Word &lop, Word rop)
+   {
+      lop = static_cast<SWord>(lop) > static_cast<SWord>(rop);
+   }
+
+   //
+   // OpFunc_CmpI_LE
+   //
+   static inline void OpFunc_CmpI_LE(Word &lop, Word rop)
+   {
+      lop = static_cast<SWord>(lop) <= static_cast<SWord>(rop);
+   }
+
+   //
+   // OpFunc_CmpI_LT
+   //
+   static inline void OpFunc_CmpI_LT(Word &lop, Word rop)
+   {
+      lop = static_cast<SWord>(lop) < static_cast<SWord>(rop);
+   }
+
+   //
+   // OpFunc_CmpU_EQ
+   //
+   static inline void OpFunc_CmpU_EQ(Word &lop, Word rop)
+   {
+      lop = lop == rop;
+   }
+
+   //
+   // OpFunc_CmpU_NE
+   //
+   static inline void OpFunc_CmpU_NE(Word &lop, Word rop)
+   {
+      lop = lop != rop;
+   }
+
+   //
+   // OpFunc_DivI
+   //
+   static inline void OpFunc_DivI(Word &lop, Word rop)
+   {
+      lop = rop ? static_cast<SWord>(lop) / static_cast<SWord>(rop) : 0;
+   }
+
+   //
+   // OpFunc_DivX
+   //
+   static inline void OpFunc_DivX(Word &lop, Word rop)
+   {
+      if(rop)
+         lop = (SDWord(SWord(lop)) << 16) / SWord(rop);
+      else
+         lop = 0;
+   }
+
+   //
+   // OpFunc_LAnd
+   //
+   static inline void OpFunc_LAnd(Word &lop, Word rop)
+   {
+      lop = lop && rop;
+   }
+
+   //
+   // OpFunc_LOrI
+   //
+   static inline void OpFunc_LOrI(Word &lop, Word rop)
+   {
+      lop = lop || rop;
+   }
+
+   //
+   // OpFunc_ModI
+   //
+   static inline void OpFunc_ModI(Word &lop, Word rop)
+   {
+      lop = rop ? static_cast<SWord>(lop) % static_cast<SWord>(rop) : 0;
+   }
+
+   //
+   // OpFunc_MulX
+   //
+   static inline void OpFunc_MulX(Word &lop, Word rop)
+   {
+      lop = DWord(SDWord(SWord(lop)) * SWord(rop)) >> 16;
+   }
+
+   //
+   // OpFunc_ShRI
+   //
+   static inline void OpFunc_ShRI(Word &lop, Word rop)
+   {
+      // TODO: Implement this without relying on sign-extending shift.
+      lop = static_cast<SWord>(lop) >> (rop & 31);
+   }
+}
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // Thread::exec
+   //
+   void Thread::exec()
+   {
+      if(delay && --delay)
+         return;
+
+      auto branches = env->branchLimit;
+
+   exec_intr:
+      switch(state.state)
+      {
+      case ThreadState::Inactive: return;
+      case ThreadState::Stopped:  goto thread_stop;
+      case ThreadState::Paused:   return;
+
+      case ThreadState::Running:
+         if(delay)
+            return;
+         break;
+
+      case ThreadState::WaitScrI:
+         if(scopeMap->isScriptActive(scopeMap->findScript(state.data)))
+            return;
+         state = ThreadState::Running;
+         break;
+
+      case ThreadState::WaitScrS:
+         if(scopeMap->isScriptActive(scopeMap->findScript(scopeMap->getString(state.data))))
+            return;
+         state = ThreadState::Running;
+         break;
+
+      case ThreadState::WaitTag:
+         if(!module->env->checkTag(state.type, state.data))
+            return;
+         state = ThreadState::Running;
+         break;
+      }
+
+      #if ACSVM_DynamicGoto
+      static void const *const cases[] =
+      {
+         #define ACSVM_CodeList(name, ...) &&case_Code##name,
+         #include "CodeList.hpp"
+      };
+      #endif
+
+      #if ACSVM_DynamicGoto
+      NextCase();
+      #else
+      next_case: switch(*codePtr++)
+      #endif
+      {
+      DeclCase(Nop):
+         NextCase();
+
+      DeclCase(Kill):
+         module->env->printKill(this, codePtr[0], codePtr[1]);
+         goto thread_stop;
+
+         //================================================
+         // Binary operator codes.
+         //
+
+         OpSet(AddU);
+         OpSet(AndU);
+         OpSet(DivI);
+         OpSet(ModI);
+         OpSet(MulU);
+         OpSet(OrIU);
+         OpSet(OrXU);
+         OpSet(ShLU);
+         OpSet(ShRI);
+         OpSet(SubU);
+
+      DeclCase(AddU): Op_AddU(dataStk[1]); NextCase();
+      DeclCase(AndU): Op_AndU(dataStk[1]); NextCase();
+      DeclCase(CmpI_GE): Op_CmpI_GE(dataStk[1]); NextCase();
+      DeclCase(CmpI_GT): Op_CmpI_GT(dataStk[1]); NextCase();
+      DeclCase(CmpI_LE): Op_CmpI_LE(dataStk[1]); NextCase();
+      DeclCase(CmpI_LT): Op_CmpI_LT(dataStk[1]); NextCase();
+      DeclCase(CmpU_EQ): Op_CmpU_EQ(dataStk[1]); NextCase();
+      DeclCase(CmpU_NE): Op_CmpU_NE(dataStk[1]); NextCase();
+      DeclCase(DivI): Op_DivI(dataStk[1]); NextCase();
+      DeclCase(DivX): Op_DivX(dataStk[1]); NextCase();
+      DeclCase(LAnd): Op_LAnd(dataStk[1]); NextCase();
+      DeclCase(LOrI): Op_LOrI(dataStk[1]); NextCase();
+      DeclCase(ModI): Op_ModI(dataStk[1]); NextCase();
+      DeclCase(MulU): Op_MulU(dataStk[1]); NextCase();
+      DeclCase(MulX): Op_MulX(dataStk[1]); NextCase();
+      DeclCase(OrIU): Op_OrIU(dataStk[1]); NextCase();
+      DeclCase(OrXU): Op_OrXU(dataStk[1]); NextCase();
+      DeclCase(ShLU): Op_ShLU(dataStk[1]); NextCase();
+      DeclCase(ShRI): Op_ShRI(dataStk[1]); NextCase();
+      DeclCase(SubU): Op_SubU(dataStk[1]); NextCase();
+
+         //================================================
+         // Call codes.
+         //
+
+      DeclCase(Call_Lit):
+         {
+            Function *func;
+
+            func = *codePtr < module->functionV.size() ? module->functionV[*codePtr] : nullptr;
+            ++codePtr;
+
+         do_call:
+            if(!func) {BranchTo(0); NextCase();}
+
+            // Reserve stack space.
+            callStk.reserve(CallStkSize);
+            dataStk.reserve(DataStkSize);
+
+            // Push call frame.
+            callStk.push({codePtr, module, scopeMod, localArr.size(), localReg.size()});
+
+            // Apply function data.
+            codePtr      = &func->module->codeV[func->codeIdx];
+            module       = func->module;
+            scopeMod     = scopeMap->getModuleScope(module);
+            localArr.alloc(func->locArrC);
+            localReg.alloc(func->locRegC);
+
+            // Read arguments.
+            dataStk.drop(func->argC);
+            memcpy(&localReg[0], &dataStk[0], func->argC * sizeof(Word));
+
+            NextCase();
+
+      DeclCase(Call_Stk):
+            dataStk.drop();
+            func = env->getFunction(dataStk[0]);
+            goto do_call;
+         }
+
+      DeclCase(CallFunc):
+         {
+            Word argc = *codePtr++;
+            Word func = *codePtr++;
+            dataStk.drop(argc);
+            if(env->callFunc(this, func, &dataStk[0], argc))
+               goto exec_intr;
+         }
+         NextCase();
+
+      DeclCase(CallFunc_Lit):
+         {
+            Word        argc = *codePtr++;
+            Word        func = *codePtr++;
+            Word const *argv =  codePtr;
+            codePtr += argc;
+            if(env->callFunc(this, func, argv, argc))
+               goto exec_intr;
+         }
+         NextCase();
+
+      DeclCase(CallSpec):
+         {
+            Word argc = *codePtr++;
+            Word spec = *codePtr++;
+            dataStk.drop(argc);
+            env->callSpec(this, spec, &dataStk[0], argc);
+         }
+         NextCase();
+
+      DeclCase(CallSpec_Lit):
+         {
+            Word        argc = *codePtr++;
+            Word        spec = *codePtr++;
+            Word const *argv =  codePtr;
+            codePtr += argc;
+            env->callSpec(this, spec, argv, argc);
+         }
+         NextCase();
+
+      DeclCase(CallSpec_R1):
+         {
+            Word argc = *codePtr++;
+            Word spec = *codePtr++;
+            dataStk.drop(argc);
+            dataStk.push(env->callSpec(this, spec, &dataStk[0], argc));
+         }
+         NextCase();
+
+      DeclCase(Retn):
+         // If no call frames left, terminate the thread.
+         if(callStk.empty())
+            goto thread_stop;
+
+         // Apply call frame.
+         codePtr     = callStk[1].codePtr;
+         module      = callStk[1].module;
+         scopeMod    = callStk[1].scopeMod;
+         localArr.free(callStk[1].locArrC);
+         localReg.free(callStk[1].locRegC);
+
+         // Drop call frame.
+         callStk.drop();
+
+         NextCase();
+
+         //================================================
+         // Drop codes.
+         //
+
+         OpSet(Drop);
+
+      DeclCase(Drop_Nul):
+        dataStk.drop();
+        NextCase();
+
+      DeclCase(Drop_ScrRet):
+        dataStk.drop();
+        result = dataStk[0];
+        NextCase();
+
+         //================================================
+         // Jump codes.
+         //
+
+      DeclCase(Jcnd_Lit):
+        if(dataStk[1] == *codePtr++)
+        {
+           dataStk.drop();
+           BranchTo(*codePtr);
+        }
+        else
+           ++codePtr;
+        NextCase();
+
+      DeclCase(Jcnd_Nil):
+         if(dataStk.drop(), dataStk[0])
+            ++codePtr;
+         else
+            BranchTo(*codePtr);
+         NextCase();
+
+      DeclCase(Jcnd_Tab):
+         if(auto jump = module->jumpMapV[*codePtr++].table.find(dataStk[1]))
+         {
+            dataStk.drop();
+            BranchTo(*jump);
+         }
+         NextCase();
+
+      DeclCase(Jcnd_Tru):
+         if(dataStk.drop(), dataStk[0])
+            BranchTo(*codePtr);
+         else
+            ++codePtr;
+         NextCase();
+
+      DeclCase(Jump_Lit):
+        BranchTo(*codePtr);
+        NextCase();
+
+      DeclCase(Jump_Stk):
+         dataStk.drop();
+         BranchTo(dataStk[0] < module->jumpV.size() ? module->jumpV[dataStk[0]].codeIdx : 0);
+         NextCase();
+
+         //================================================
+         // Push codes.
+         //
+
+      DeclCase(Pfun_Lit):
+         if(*codePtr < module->functionV.size())
+            dataStk.push(module->functionV[*codePtr]->idx);
+         else
+            dataStk.push(0);
+         ++codePtr;
+         NextCase();
+
+      DeclCase(Pstr_Stk):
+         if(dataStk[1] < module->stringV.size())
+            dataStk[1] = ~module->stringV[dataStk[1]]->idx;
+         NextCase();
+
+      DeclCase(Push_GblArr): dataStk[1] = scopeGbl->arrV[*codePtr++].find(dataStk[1]); NextCase();
+      DeclCase(Push_GblReg): dataStk.push(scopeGbl->regV[*codePtr++]); NextCase();
+      DeclCase(Push_HubArr): dataStk[1] = scopeHub->arrV[*codePtr++].find(dataStk[1]); NextCase();
+      DeclCase(Push_HubReg): dataStk.push(scopeHub->regV[*codePtr++]); NextCase();
+      DeclCase(Push_Lit):    dataStk.push(*codePtr++); NextCase();
+      DeclCase(Push_LitArr): for(auto i = *codePtr++; i--;) dataStk.push(*codePtr++); NextCase();
+      DeclCase(Push_LocArr): dataStk[1] = localArr[*codePtr++].find(dataStk[1]); NextCase();
+      DeclCase(Push_LocReg): dataStk.push(localReg[*codePtr++]); NextCase();
+      DeclCase(Push_ModArr): dataStk[1] = scopeMod->arrV[*codePtr++]->find(dataStk[1]); NextCase();
+      DeclCase(Push_ModReg): dataStk.push(*scopeMod->regV[*codePtr++]); NextCase();
+
+      DeclCase(Push_StrArs):
+         dataStk.drop();
+         dataStk[1] = scopeMap->getString(dataStk[1])->get(dataStk[0]);
+         NextCase();
+
+         //================================================
+         // Script control codes.
+         //
+
+      DeclCase(ScrDelay):
+         dataStk.drop();
+         delay = dataStk[0];
+         goto exec_intr;
+
+      DeclCase(ScrDelay_Lit):
+         delay = *codePtr++;
+         goto exec_intr;
+
+      DeclCase(ScrHalt):
+         state = ThreadState::Paused;
+         goto exec_intr;
+
+      DeclCase(ScrRestart):
+         BranchTo(script->codeIdx);
+         NextCase();
+
+      DeclCase(ScrTerm):
+         goto thread_stop;
+
+      DeclCase(ScrWaitI):
+         dataStk.drop();
+         state = {ThreadState::WaitScrI, dataStk[0]};
+         goto exec_intr;
+
+      DeclCase(ScrWaitI_Lit):
+         state = {ThreadState::WaitScrI, *codePtr++};
+         goto exec_intr;
+
+      DeclCase(ScrWaitS):
+         dataStk.drop();
+         state = {ThreadState::WaitScrS, dataStk[0]};
+         goto exec_intr;
+
+      DeclCase(ScrWaitS_Lit):
+         state = {ThreadState::WaitScrS, *codePtr++};
+         goto exec_intr;
+
+         //================================================
+         // Stack control codes.
+         //
+
+      DeclCase(Copy):
+         {auto temp = dataStk[1]; dataStk.push(temp);}
+         NextCase();
+
+      DeclCase(Swap):
+         std::swap(dataStk[2], dataStk[1]);
+         NextCase();
+
+         //================================================
+         // Unary operator codes.
+         //
+
+         OpSet(DecU);
+         OpSet(IncU);
+
+      DeclCase(InvU):
+         dataStk[1] = ~dataStk[1];
+         NextCase();
+
+      DeclCase(NegI):
+         dataStk[1] = ~dataStk[1] + 1;
+         NextCase();
+
+      DeclCase(NotU):
+         dataStk[1] = !dataStk[1];
+         NextCase();
+      }
+
+   thread_stop:
+      stop();
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Tracer.cpp b/src/acs/vm/ACSVM/Tracer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..901b09352dcd6592bb1c7e780645acf5f89bff51
--- /dev/null
+++ b/src/acs/vm/ACSVM/Tracer.cpp
@@ -0,0 +1,635 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Tracer classes.
+//
+//-----------------------------------------------------------------------------
+
+#include "Tracer.hpp"
+
+#include "BinaryIO.hpp"
+#include "Code.hpp"
+#include "CodeData.hpp"
+#include "Environment.hpp"
+#include "Error.hpp"
+#include "Function.hpp"
+#include "Jump.hpp"
+#include "Module.hpp"
+#include "Script.hpp"
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   //
+   // TracerACS0 constructor
+   //
+   TracerACS0::TracerACS0(Environment *env_, Byte const *data_,
+      std::size_t size_, bool compressed_) :
+      env       {env_},
+      codeFound {new Byte[size_]{}},
+      codeIndex {new Word[size_]{}},
+      codeC     {0},
+      jumpC     {0},
+      jumpMapC  {0},
+      data      {data_},
+      size      {size_},
+      compressed{compressed_}
+   {
+   }
+
+   //
+   // TracerACS0 destructor
+   //
+   TracerACS0::~TracerACS0()
+   {
+   }
+
+   //
+   // TracerACS0::getArgBytes
+   //
+   std::size_t TracerACS0::getArgBytes(CodeDataACS0 const *opData, std::size_t iter)
+   {
+      std::size_t argBytes;
+
+      switch(opData->code)
+      {
+      case CodeACS0::Push_LitArrB:
+         if(size - iter < 1) throw ReadError();
+
+         return data[iter] + 1;
+
+      case CodeACS0::Jcnd_Tab:
+         // Calculate alignment.
+         argBytes = ((iter + 3) & ~static_cast<std::size_t>(3)) - iter;
+         if(size - iter < argBytes) throw ReadError();
+
+         // Read the number of cases.
+         if(size - iter - argBytes < 4) throw ReadError();
+         argBytes += ReadLE4(data + iter + argBytes) * 8 + 4;
+
+         return argBytes;
+
+      default:
+         argBytes = 0;
+         for(char const *s = opData->args; *s; ++s) switch(*s)
+         {
+         case 'B': argBytes += 1; break;
+         case 'H': argBytes += 2; break;
+         case 'W': argBytes += 4; break;
+         case 'b': argBytes += compressed ? 1 : 4; break;
+         case 'h': argBytes += compressed ? 2 : 4; break;
+         }
+         return argBytes;
+      }
+   }
+
+   //
+   // TracerACS0::readCallFunc
+   //
+   std::pair<Word /*argc*/, Word /*func*/> TracerACS0::readCallFunc(std::size_t iter)
+   {
+      Word argc, func;
+      if(compressed)
+      {
+         argc = ReadLE1(data + iter + 0);
+         func = ReadLE2(data + iter + 1);
+      }
+      else
+      {
+         argc = ReadLE4(data + iter + 0);
+         func = ReadLE4(data + iter + 4);
+      }
+      return {argc, func};
+   }
+
+   //
+   // TracerACS0::readOpACS0
+   //
+   std::tuple<
+      Word                 /*opCode*/,
+      CodeDataACS0 const * /*opData*/,
+      std::size_t          /*opSize*/>
+   TracerACS0::readOpACS0(std::size_t iter)
+   {
+      if(compressed)
+      {
+         if(size - iter < 1) throw ReadError();
+
+         std::size_t opSize = 1;
+         Word        opCode = data[iter];
+
+         if(opCode >= 240)
+         {
+            if(size - iter < 2) throw ReadError();
+            ++opSize;
+            opCode = 240 + ((opCode - 240) << 8) + data[iter + 1];
+         }
+
+         return std::make_tuple(opCode, env->findCodeDataACS0(opCode), opSize);
+      }
+      else
+      {
+         if(size - iter < 4) throw ReadError();
+
+         Word opCode = ReadLE4(data + iter);
+
+         return std::make_tuple(opCode, env->findCodeDataACS0(opCode), 4);
+      }
+   }
+
+   //
+   // TracerACS0::setFound
+   //
+   bool TracerACS0::setFound(std::size_t first, std::size_t last)
+   {
+      Byte *begin = &codeFound[first];
+      Byte *end   = &codeFound[last];
+
+      std::size_t found = 0;
+
+      for(Byte *itr = begin; itr != end; ++itr)
+         found += *itr;
+
+      if(found)
+      {
+         if(found != last - first) throw ReadError();
+         return false;
+      }
+
+      for(Byte *itr = begin; itr != end; ++itr)
+         *itr = true;
+
+      return true;
+   }
+
+   //
+   // TracerACS0::trace
+   //
+   void TracerACS0::trace(Module *module)
+   {
+      // Add Kill to catch branches to zero.
+      codeC += 1 + env->getCodeData(Code::Kill)->argc;
+
+      // Trace from entry points.
+
+      for(Function *&func : module->functionV)
+         if(func && func->module == module) trace(func->codeIdx);
+
+      for(Jump &jump : module->jumpV)
+         trace(jump.codeIdx);
+
+      for(Script &scr : module->scriptV)
+         trace(scr.codeIdx);
+
+      // Add Kill to catch execution past end.
+      codeC += 1 + env->getCodeData(Code::Kill)->argc;
+   }
+
+   //
+   // TracerACS0::trace
+   //
+   void TracerACS0::trace(std::size_t iter)
+   {
+      for(std::size_t next;; iter = next)
+      {
+         // If at the end of the file, terminate tracer. Reaching here will
+         // result in a Kill, but the bytecode is otherwise well formed.
+         if(iter == size) return;
+
+         // Whereas if iter is out of bounds, bytecode is malformed.
+         if(iter > size) throw ReadError();
+
+         // Read op.
+         CodeDataACS0 const *opData;
+         std::size_t         opSize;
+         std::tie(std::ignore, opData, opSize) = readOpACS0(iter);
+
+         // If no translation available, terminate trace.
+         if(!opData)
+         {
+            // Mark as found, so that the translator generates a KILL.
+            setFound(iter, iter + opSize);
+            codeC += 1 + env->getCodeData(Code::Kill)->argc;
+            return;
+         }
+
+         std::size_t opSizeFull = opSize + getArgBytes(opData, iter + opSize);
+
+         // If this op goes out of bounds, bytecode is malformed.
+         if(size - iter < opSizeFull) throw ReadError();
+
+         next = iter + opSizeFull;
+
+         // If this op already found, terminate trace.
+         if(!setFound(iter, next))
+            return;
+
+         // Get data for translated op.
+         CodeData const *opTran = env->getCodeData(opData->transCode);
+
+         // Count internal size of op.
+         switch(opData->code)
+         {
+            // -> Call(F) Drop_Nul()
+         case CodeACS0::Call_Nul:
+            codeC += 3;
+            break;
+
+         case CodeACS0::CallSpec_1L:
+         case CodeACS0::CallSpec_2L:
+         case CodeACS0::CallSpec_3L:
+         case CodeACS0::CallSpec_4L:
+         case CodeACS0::CallSpec_5L:
+         case CodeACS0::CallSpec_6L:
+         case CodeACS0::CallSpec_7L:
+         case CodeACS0::CallSpec_8L:
+         case CodeACS0::CallSpec_9L:
+         case CodeACS0::CallSpec_10L:
+         case CodeACS0::CallSpec_1LB:
+         case CodeACS0::CallSpec_2LB:
+         case CodeACS0::CallSpec_3LB:
+         case CodeACS0::CallSpec_4LB:
+         case CodeACS0::CallSpec_5LB:
+         case CodeACS0::CallSpec_6LB:
+         case CodeACS0::CallSpec_7LB:
+         case CodeACS0::CallSpec_8LB:
+         case CodeACS0::CallSpec_9LB:
+         case CodeACS0::CallSpec_10LB:
+         case CodeACS0::Push_Lit2B:
+         case CodeACS0::Push_Lit3B:
+         case CodeACS0::Push_Lit4B:
+         case CodeACS0::Push_Lit5B:
+         case CodeACS0::Push_Lit6B:
+         case CodeACS0::Push_Lit7B:
+         case CodeACS0::Push_Lit8B:
+         case CodeACS0::Push_Lit9B:
+         case CodeACS0::Push_Lit10B:
+            codeC += opData->argc + 2;
+            break;
+
+         case CodeACS0::Push_LitArrB:
+            codeC += data[iter + opSize] + 2;
+            break;
+
+            // -> Push_Lit(0) Retn()
+         case CodeACS0::Retn_Nul:
+            codeC += 3;
+            break;
+
+         case CodeACS0::CallFunc:
+            {
+               Word argc, func;
+               std::tie(argc, func) = readCallFunc(iter + opSize);
+
+               FuncDataACS0 const *opFunc = env->findFuncDataACS0(func);
+
+               if(!opFunc)
+               {
+                  codeC += 1 + env->getCodeData(Code::Kill)->argc;
+                  return;
+               }
+
+               opTran = env->getCodeData(opFunc->getTransCode(argc));
+            }
+
+         default:
+            if(opTran->code == Code::CallFunc_Lit)
+               codeC += opData->argc + 1 + 2;
+            else
+               codeC += opTran->argc + 1;
+
+            if(opTran->code == Code::Kill)
+               return;
+
+            break;
+         }
+
+         // Special handling for branching ops.
+         switch(opData->code)
+         {
+         case CodeACS0::Jcnd_Nil:
+         case CodeACS0::Jcnd_Tru:
+            ++jumpC;
+            trace(ReadLE4(data + iter + opSize));
+            break;
+
+         case CodeACS0::Jcnd_Lit:
+            ++jumpC;
+            trace(ReadLE4(data + iter + opSize + 4));
+            break;
+
+         case CodeACS0::Jcnd_Tab:
+            {
+               std::size_t count, jumpIter;
+
+               jumpIter = (iter + opSize + 3) & ~static_cast<std::size_t>(3);
+               count = ReadLE4(data + jumpIter); jumpIter += 4;
+
+               ++jumpMapC;
+
+               // Trace all of the jump targets.
+               for(; count--; jumpIter += 8)
+                  trace(ReadLE4(data + jumpIter + 4));
+            }
+            break;
+
+         case CodeACS0::Jump_Lit:
+            ++jumpC;
+            next = ReadLE4(data + iter + opSize);
+            break;
+
+         case CodeACS0::Jump_Stk:
+            // The target of this jump will get traced when the dynamic jump
+            // targets get traced.
+            return;
+
+         case CodeACS0::Retn_Stk:
+         case CodeACS0::Retn_Nul:
+         case CodeACS0::ScrTerm:
+            return;
+
+         default:
+            break;
+         }
+      }
+   }
+
+   //
+   // TracerACS0::translate
+   //
+   void TracerACS0::translate(Module *module)
+   {
+      std::unique_ptr<Word*[]> jumps{new uint32_t *[jumpC]};
+
+      Word  *codeItr    = module->codeV.data();
+      Word **jumpItr    = jumps.get();
+      auto   jumpMapItr = module->jumpMapV.data();
+
+      // Add Kill to catch branches to zero.
+      *codeItr++ = static_cast<Word>(Code::Kill);
+      *codeItr++ = static_cast<Word>(KillType::OutOfBounds);
+      *codeItr++ = 0;
+
+      for(std::size_t iter = 0, next; iter != size; iter = next)
+      {
+         // If no code at this index, skip it.
+         if(!codeFound[iter])
+         {
+            next = iter + 1;
+            continue;
+         }
+
+         // Record jump target.
+         codeIndex[iter] = codeItr - module->codeV.data();
+
+         // Read op.
+         Word                opCode;
+         CodeDataACS0 const *opData;
+         std::size_t         opSize;
+         std::tie(opCode, opData, opSize) = readOpACS0(iter);
+
+         // If no translation available, generate Kill.
+         if(!opData)
+         {
+            *codeItr++ = static_cast<Word>(Code::Kill);
+            *codeItr++ = static_cast<Word>(KillType::UnknownCode);
+            *codeItr++ = opCode;
+            next = iter + opSize;
+            continue;
+         }
+
+         // Calculate next index.
+         next = iter + opSize + getArgBytes(opData, iter + opSize);
+
+         // Get data for translated op.
+         CodeData const *opTran = env->getCodeData(opData->transCode);
+
+         // Generate internal op.
+         switch(opData->code)
+         {
+         case CodeACS0::Call_Nul:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            if(compressed)
+               *codeItr++ = ReadLE1(data + iter + opSize);
+            else
+               *codeItr++ = ReadLE4(data + iter + opSize);
+            *codeItr++ = static_cast<Word>(Code::Drop_Nul);
+            break;
+
+         case CodeACS0::CallSpec_1:
+         case CodeACS0::CallSpec_2:
+         case CodeACS0::CallSpec_3:
+         case CodeACS0::CallSpec_4:
+         case CodeACS0::CallSpec_5:
+         case CodeACS0::CallSpec_6:
+         case CodeACS0::CallSpec_7:
+         case CodeACS0::CallSpec_8:
+         case CodeACS0::CallSpec_9:
+         case CodeACS0::CallSpec_10:
+         case CodeACS0::CallSpec_5R1:
+         case CodeACS0::CallSpec_10R1:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            *codeItr++ = opData->stackArgC;
+            goto trans_args;
+
+         case CodeACS0::CallSpec_1L:
+         case CodeACS0::CallSpec_1LB:
+         case CodeACS0::CallSpec_2L:
+         case CodeACS0::CallSpec_2LB:
+         case CodeACS0::CallSpec_3L:
+         case CodeACS0::CallSpec_3LB:
+         case CodeACS0::CallSpec_4L:
+         case CodeACS0::CallSpec_4LB:
+         case CodeACS0::CallSpec_5L:
+         case CodeACS0::CallSpec_5LB:
+         case CodeACS0::CallSpec_6L:
+         case CodeACS0::CallSpec_6LB:
+         case CodeACS0::CallSpec_7L:
+         case CodeACS0::CallSpec_7LB:
+         case CodeACS0::CallSpec_8L:
+         case CodeACS0::CallSpec_8LB:
+         case CodeACS0::CallSpec_9L:
+         case CodeACS0::CallSpec_9LB:
+         case CodeACS0::CallSpec_10L:
+         case CodeACS0::CallSpec_10LB:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            *codeItr++ = opData->argc - 1;
+            goto trans_args;
+
+         case CodeACS0::Jcnd_Tab:
+            {
+               std::size_t count, jumpIter;
+
+               jumpIter = (iter + opSize + 3) & ~static_cast<std::size_t>(3);
+               count = ReadLE4(data + jumpIter); jumpIter += 4;
+
+               *codeItr++ = static_cast<Word>(opData->transCode);
+               *codeItr++ = jumpMapItr - module->jumpMapV.data();
+
+               (jumpMapItr++)->loadJumps(data + jumpIter, count);
+            }
+            break;
+
+         case CodeACS0::Push_LitArrB:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            iter += opSize;
+            for(std::size_t n = *codeItr++ = data[iter++]; n--;)
+               *codeItr++ = data[iter++];
+            break;
+
+         case CodeACS0::Push_Lit2B:
+         case CodeACS0::Push_Lit3B:
+         case CodeACS0::Push_Lit4B:
+         case CodeACS0::Push_Lit5B:
+         case CodeACS0::Push_Lit6B:
+         case CodeACS0::Push_Lit7B:
+         case CodeACS0::Push_Lit8B:
+         case CodeACS0::Push_Lit9B:
+         case CodeACS0::Push_Lit10B:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            *codeItr++ = opData->argc;
+            goto trans_args;
+
+         case CodeACS0::Retn_Nul:
+            *codeItr++ = static_cast<Word>(Code::Push_Lit);
+            *codeItr++ = static_cast<Word>(0);
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            break;
+
+         case CodeACS0::CallFunc:
+            {
+               Word argc, func;
+               std::tie(argc, func) = readCallFunc(iter + opSize);
+
+               FuncDataACS0 const *opFunc = env->findFuncDataACS0(func);
+
+               if(!opFunc)
+               {
+                  *codeItr++ = static_cast<Word>(Code::Kill);
+                  *codeItr++ = static_cast<Word>(KillType::UnknownFunc);
+                  *codeItr++ = func;
+                  continue;
+               }
+
+               opTran = env->getCodeData(opFunc->getTransCode(argc));
+
+               *codeItr++ = static_cast<Word>(opTran->code);
+               if(opTran->code == Code::Kill)
+               {
+                  *codeItr++ = static_cast<Word>(KillType::UnknownFunc);
+                  *codeItr++ = func;
+                  continue;
+               }
+               else if(opTran->code == Code::CallFunc)
+               {
+                  *codeItr++ = argc;
+                  *codeItr++ = opFunc->transFunc;
+               }
+            }
+            break;
+
+         default:
+            *codeItr++ = static_cast<Word>(opData->transCode);
+            if(opTran->code == Code::Kill)
+            {
+               *codeItr++ = static_cast<Word>(KillType::UnknownCode);
+               *codeItr++ = opCode;
+               continue;
+            }
+            else if(opTran->code == Code::CallFunc)
+            {
+               *codeItr++ = opData->stackArgC;
+               *codeItr++ = opData->transFunc;
+            }
+            else if(opTran->code == Code::CallFunc_Lit)
+            {
+               *codeItr++ = opData->argc;
+               *codeItr++ = opData->transFunc;
+            }
+
+         trans_args:
+            // Convert arguments.
+            iter += opSize;
+            for(char const *a = opData->args; *a; ++a) switch(*a)
+            {
+            case 'B': *codeItr++ = ReadLE1(data + iter); iter += 1; break;
+            case 'H': *codeItr++ = ReadLE2(data + iter); iter += 2; break;
+            case 'W': *codeItr++ = ReadLE4(data + iter); iter += 4; break;
+
+            case 'J':
+               *jumpItr++ = codeItr - 1;
+               break;
+
+            case 'S':
+               if(*(codeItr - 1) < module->stringV.size())
+                  *(codeItr - 1) = ~module->stringV[*(codeItr - 1)]->idx;
+               break;
+
+            case 'b':
+               if(compressed)
+                  {*codeItr++ = ReadLE1(data + iter); iter += 1;}
+               else
+                  {*codeItr++ = ReadLE4(data + iter); iter += 4;}
+               break;
+
+            case 'h':
+               if(compressed)
+                  {*codeItr++ = ReadLE2(data + iter); iter += 2;}
+               else
+                  {*codeItr++ = ReadLE4(data + iter); iter += 4;}
+               break;
+            }
+
+            break;
+         }
+      }
+
+      // Add Kill to catch execution past end.
+      *codeItr++ = static_cast<Word>(Code::Kill);
+      *codeItr++ = static_cast<Word>(KillType::OutOfBounds);
+      *codeItr++ = 1;
+
+      // Translate jumps. Has to be done after code in order to jump forward.
+      while(jumpItr != jumps.get())
+      {
+         codeItr = *--jumpItr;
+
+         if(*codeItr < size)
+            *codeItr = codeIndex[*codeItr];
+         else
+            *codeItr = 0;
+      }
+
+      // Translate entry points.
+
+      for(Function *&func : module->functionV)
+      {
+         if(func && func->module == module)
+            func->codeIdx = func->codeIdx < size ? codeIndex[func->codeIdx] : 0;
+      }
+
+      for(Jump &jump : module->jumpV)
+         jump.codeIdx = jump.codeIdx < size ? codeIndex[jump.codeIdx] : 0;
+
+      for(JumpMap &jumpMap : module->jumpMapV)
+      {
+         for(auto &jump : jumpMap.table)
+            jump.val = jump.val < size ? codeIndex[jump.val] : 0;
+      }
+
+      for(Script &scr : module->scriptV)
+         scr.codeIdx = scr.codeIdx < size ? codeIndex[scr.codeIdx] : 0;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/ACSVM/Tracer.hpp b/src/acs/vm/ACSVM/Tracer.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d0918946a1501796244d01514ff390751a5fe423
--- /dev/null
+++ b/src/acs/vm/ACSVM/Tracer.hpp
@@ -0,0 +1,76 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Tracer classes.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Tracer_H__
+#define ACSVM__Tracer_H__
+
+#include "Types.hpp"
+
+#include <memory>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // TracerACS0
+   //
+   // Traces input bytecode in ACS0 format for code paths, and then
+   // translates discovered codes.
+   //
+   class TracerACS0
+   {
+   public:
+      TracerACS0(Environment *env, Byte const *data, std::size_t size, bool compressed);
+      ~TracerACS0();
+
+      void trace(Module *module);
+
+      void translate(Module *module);
+
+      Environment *env;
+
+      std::unique_ptr<Byte[]> codeFound;
+      std::unique_ptr<Word[]> codeIndex;
+      std::size_t             codeC;
+
+      std::size_t jumpC;
+
+      std::size_t jumpMapC;
+
+   private:
+      std::size_t getArgBytes(CodeDataACS0 const *opData, std::size_t iter);
+
+      std::pair<Word /*argc*/, Word /*func*/> readCallFunc(std::size_t iter);
+
+      std::tuple<
+         Word                 /*opCode*/,
+         CodeDataACS0 const * /*opData*/,
+         std::size_t          /*opSize*/>
+      readOpACS0(std::size_t iter);
+
+      bool setFound(std::size_t first, std::size_t last);
+
+      void trace(std::size_t iter);
+
+      // Bytecode information.
+      Byte const *data;
+      std::size_t size;
+      bool        compressed;
+   };
+}
+
+#endif//ACSVM__Tracer_H__
+
diff --git a/src/acs/vm/ACSVM/Types.hpp b/src/acs/vm/ACSVM/Types.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..8da5445d570d176a0897071b8e86b9d1cfa70871
--- /dev/null
+++ b/src/acs/vm/ACSVM/Types.hpp
@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2017 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Common typedefs and class forward declarations.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Types_H__
+#define ACSVM__Types_H__
+
+#include <cinttypes>
+#include <cstddef>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   // Host platform byte.
+   // Should this be uint8_t? Short answer: No.
+   // Long answer: char is the smallest addressable unit in the implementation.
+   // That is, it is by definition the byte of the target platform. And it is
+   // required to be at least 8 bits wide, which is the only real requirement
+   // for loading ACS bytecode.
+   // Furthermore, uint8_t is only defined if the implementation has a type
+   // with exactly 8 data bits and no padding bits. So even if you wanted to
+   // unilaterally declare bytes to be 8 bits, the only type that can possibly
+   // satisfy uint8_t is unsigned char. If CHAR_BIT is not 8, then there can be
+   // no uint8_t.
+   using Byte = unsigned char;
+
+   using DWord = std::uint64_t;
+   using SDWord = std::int64_t;
+   using SWord = std::int32_t;
+   using Word = std::uint32_t;
+
+   enum class Code;
+   enum class CodeACS0;
+   enum class Func;
+   enum class FuncACS0;
+   enum class InitTag;
+   enum class Signature : std::uint32_t;
+   class Array;
+   class ArrayInit;
+   class CodeData;
+   class CodeDataACS0;
+   class Environment;
+   class FuncDataACS0;
+   class Function;
+   class GlobalScope;
+   class HubScope;
+   class Jump;
+   class JumpMap;
+   class MapScope;
+   class Module;
+   class ModuleName;
+   class ModuleScope;
+   class PrintBuf;
+   class ScopeID;
+   class Script;
+   class ScriptAction;
+   class ScriptName;
+   class Serial;
+   class String;
+   class Thread;
+   class ThreadInfo;
+   class ThreadState;
+   class WordInit;
+
+   using CallFunc = bool (*)(Thread *thread, Word const *argv, Word argc);
+}
+
+#endif//ACSVM__Types_H__
+
diff --git a/src/acs/vm/ACSVM/Vector.hpp b/src/acs/vm/ACSVM/Vector.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..966bc6ea9a45f805bd99d164d84359671c5327c4
--- /dev/null
+++ b/src/acs/vm/ACSVM/Vector.hpp
@@ -0,0 +1,140 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (C) 2015-2016 David Hill
+//
+// See COPYING for license information.
+//
+//-----------------------------------------------------------------------------
+//
+// Vector class.
+//
+//-----------------------------------------------------------------------------
+
+#ifndef ACSVM__Vector_H__
+#define ACSVM__Vector_H__
+
+#include "Types.hpp"
+
+#include <new>
+#include <utility>
+
+
+//----------------------------------------------------------------------------|
+// Types                                                                      |
+//
+
+namespace ACSVM
+{
+   //
+   // Vector
+   //
+   // Runtime sized array.
+   //
+   template<typename T>
+   class Vector
+   {
+   public:
+      using const_iterator = T const *;
+      using iterator       = T *;
+      using size_type      = std::size_t;
+
+
+      Vector() : dataV{nullptr}, dataC{0} {}
+      Vector(Vector<T> const &) = delete;
+      Vector(Vector<T> &&v) : dataV{v.dataV}, dataC{v.dataC}
+         {v.dataV = nullptr; v.dataC = 0;}
+      Vector(size_type count) : dataV{nullptr}, dataC{0} {alloc(count);}
+
+      Vector(T const *v, size_type c)
+      {
+         dataC = c;
+         dataV = static_cast<T *>(::operator new(sizeof(T) * dataC));
+
+         for(T *itr = dataV, *last = itr + dataC; itr != last; ++itr)
+            new(itr) T{*v++};
+      }
+
+      ~Vector() {free();}
+
+      T &operator [] (size_type i) {return dataV[i];}
+
+      Vector<T> &operator = (Vector<T> &&v) {swap(v); return *this;}
+
+      //
+      // alloc
+      //
+      template<typename... Args>
+      void alloc(size_type count, Args const &...args)
+      {
+         if(dataV) free();
+
+         dataC = count;
+         dataV = static_cast<T *>(::operator new(sizeof(T) * dataC));
+
+         for(T *itr = dataV, *last = itr + dataC; itr != last; ++itr)
+            new(itr) T{args...};
+      }
+
+      // begin
+            iterator begin()       {return dataV;}
+      const_iterator begin() const {return dataV;}
+
+      // data
+      T *data() {return dataV;}
+
+      // end
+            iterator end()       {return dataV + dataC;}
+      const_iterator end() const {return dataV + dataC;}
+
+      //
+      // free
+      //
+      void free()
+      {
+         if(!dataV) return;
+
+         for(T *itr = dataV + dataC; itr != dataV;)
+            (--itr)->~T();
+
+         ::operator delete(dataV);
+         dataV = nullptr;
+         dataC = 0;
+      }
+
+      //
+      // realloc
+      //
+      template<typename... Args>
+      void realloc(size_type count, Args const &...args)
+      {
+         if(count == dataC) return;
+
+         Vector<T> old{std::move(*this)};
+
+         dataC = count;
+         dataV = static_cast<T *>(::operator new(sizeof(T) * dataC));
+
+         T *itr = begin(), *last = end(), *oldItr = old.begin();
+         T *mid = count > old.size() ? dataV + old.size() : last;
+
+         while(itr != mid)
+            new(itr++) T{std::move(*oldItr++)};
+         while(itr != last)
+            new(itr++) T{args...};
+      }
+
+      // size
+      size_type size() const {return dataC;}
+
+      // swap
+      void swap(Vector<T> &v)
+         {std::swap(dataV, v.dataV); std::swap(dataC, v.dataC);}
+
+   private:
+      T        *dataV;
+      size_type dataC;
+   };
+}
+
+#endif//ACSVM__Vector_H__
+
diff --git a/src/acs/vm/CMakeLists.txt b/src/acs/vm/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3116012c67ec1e89d47c5e3afbb3e17ad523c889
--- /dev/null
+++ b/src/acs/vm/CMakeLists.txt
@@ -0,0 +1,193 @@
+##-----------------------------------------------------------------------------
+##
+## Copyright (C) 2015-2016 David Hill
+##
+## See COPYING for license information.
+##
+##-----------------------------------------------------------------------------
+##
+## Root CMake file.
+##
+##-----------------------------------------------------------------------------
+
+cmake_minimum_required(VERSION 2.6)
+
+cmake_policy(SET CMP0017 NEW)
+
+project(acsvm)
+
+include(CheckCCompilerFlag)
+include(CheckCXXCompilerFlag)
+
+
+##----------------------------------------------------------------------------|
+## Functions                                                                  |
+##
+
+##
+## ACSVM_INSTALL_EXE
+##
+function(ACSVM_INSTALL_EXE name)
+   if(ACSVM_INSTALL_EXE)
+      install(TARGETS ${name}
+         RUNTIME DESTINATION bin
+         LIBRARY DESTINATION lib
+         ARCHIVE DESTINATION lib
+      )
+   endif()
+endfunction()
+
+##
+## ACSVM_INSTALL_LIB
+##
+function(ACSVM_INSTALL_LIB name)
+   if(ACSVM_INSTALL_LIB)
+      if(ACSVM_INSTALL_API)
+         install(TARGETS ${name}
+            RUNTIME DESTINATION bin
+            LIBRARY DESTINATION lib
+            ARCHIVE DESTINATION lib
+         )
+      elseif(ACSVM_SHARED)
+         install(TARGETS ${name}
+            RUNTIME DESTINATION bin
+            LIBRARY DESTINATION lib
+         )
+      endif()
+   endif()
+endfunction()
+
+##
+## ACSVM_TRY_C_FLAG
+##
+function(ACSVM_TRY_C_FLAG flag name)
+   CHECK_C_COMPILER_FLAG(${flag} ACSVM_C_FLAG_${name})
+
+   if(ACSVM_C_FLAG_${name})
+      set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${flag}" PARENT_SCOPE)
+   endif()
+endfunction()
+
+##
+## ACSVM_TRY_CXX_FLAG
+##
+function(ACSVM_TRY_CXX_FLAG flag name)
+   CHECK_CXX_COMPILER_FLAG(${flag} ACSVM_CXX_FLAG_${name})
+
+   if(ACSVM_CXX_FLAG_${name})
+      set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${flag}" PARENT_SCOPE)
+   endif()
+endfunction()
+
+
+##----------------------------------------------------------------------------|
+## Environment Detection                                                      |
+##
+
+set(ACSVM_SHARED_DEFAULT ON)
+
+if(NOT ACSVM_NOFLAGS)
+   if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
+      ACSVM_TRY_C_FLAG(-Wall    Wall)
+      ACSVM_TRY_C_FLAG(-Wextra  Wextra)
+
+      ACSVM_TRY_C_FLAG(-std=c11 STD_C)
+   endif()
+
+   if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
+      ACSVM_TRY_CXX_FLAG(-Wall    Wall)
+      ACSVM_TRY_CXX_FLAG(-Wextra  Wextra)
+
+      ACSVM_TRY_CXX_FLAG(-std=c++11 STD_CXX)
+   endif()
+endif()
+
+if(MSVC)
+   # Disable shared by default, as the source does not contain the needed
+   # declaration annotations to make that work under MSVC.
+   set(ACSVM_SHARED_DEFAULT OFF)
+endif()
+
+
+##----------------------------------------------------------------------------|
+## Variables                                                                  |
+##
+
+##
+## ACSVM_INSTALL_API
+##
+if(NOT DEFINED ACSVM_INSTALL_API)
+   set(ACSVM_INSTALL_API ON CACHE BOOL "Install ACSVM headers.")
+endif()
+
+##
+## ACSVM_INSTALL_DOC
+##
+if(NOT DEFINED ACSVM_INSTALL_DOC)
+   set(ACSVM_INSTALL_DOC ON CACHE BOOL "Install ACSVM documentation.")
+endif()
+
+##
+## ACSVM_INSTALL_EXE
+##
+if(NOT DEFINED ACSVM_INSTALL_EXE)
+   set(ACSVM_INSTALL_EXE ON CACHE BOOL "Install ACSVM executables.")
+endif()
+
+##
+## ACSVM_INSTALL_LIB
+##
+if(NOT DEFINED ACSVM_INSTALL_LIB)
+   set(ACSVM_INSTALL_LIB ON CACHE BOOL "Install ACSVM libraries.")
+endif()
+
+##
+## ACSVM_SHARED
+##
+## If true (or equiavalent), libraries will be built as SHARED. Otherwise,
+## they are built as STATIC.
+##
+if(NOT DEFINED ACSVM_SHARED)
+   set(ACSVM_SHARED ${ACSVM_SHARED_DEFAULT} CACHE BOOL
+      "Build libraries as shared objects.")
+endif()
+
+##
+## ACSVM_SHARED_DECL
+##
+## Used internally for convenience in add_library commands.
+##
+if(ACSVM_SHARED)
+   set(ACSVM_SHARED_DECL SHARED)
+else()
+   set(ACSVM_SHARED_DECL STATIC)
+endif()
+
+
+##----------------------------------------------------------------------------|
+## Environment Configuration                                                  |
+##
+
+include_directories(.)
+
+
+##----------------------------------------------------------------------------|
+## Targets                                                                    |
+##
+
+add_subdirectory(ACSVM)
+
+if(EXISTS "${CMAKE_SOURCE_DIR}/CAPI")
+   add_subdirectory(CAPI)
+endif()
+
+if(EXISTS "${CMAKE_SOURCE_DIR}/Exec")
+   add_subdirectory(Exec)
+endif()
+
+if(EXISTS "${CMAKE_SOURCE_DIR}/Util")
+   add_subdirectory(Util)
+endif()
+
+## EOF
+
diff --git a/src/acs/vm/COPYING b/src/acs/vm/COPYING
new file mode 100644
index 0000000000000000000000000000000000000000..4362b49151d7b34ef83b3067a8f9c9f877d72a0e
--- /dev/null
+++ b/src/acs/vm/COPYING
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/src/acs/vm/README b/src/acs/vm/README
new file mode 100644
index 0000000000000000000000000000000000000000..bb24555639c4cd2db9be2627d5f9ee7b5f34145b
--- /dev/null
+++ b/src/acs/vm/README
@@ -0,0 +1,121 @@
+ACS Virtual Machine (ACSVM)
+
+ACS VM library and standalone interpreter. Intended to be suitable for use in
+Doom-based video game engines to implement Hexen ACS or ZDoom ACS bytecode
+execution. It is focused on being usable for implementing existing extensions
+and new functions (whether through new instructions or CallFunc indexes) while
+having performance suitable to the complex ACS-based mods that have come into
+existence.
+
+
+===============================================================================
+Integrating ACSVM
+===============================================================================
+
+Although it can be used as an external library, ACSVM is also written so that
+it can be integrated directly into the repository of projects using it without
+needing to change any of the ACSVM files.
+
+It is enough to copy ACSVM's root into the root of the target repository, such
+that acsvm/CMakeLists.txt exists. Only the subdirectories of desired components
+(usually just ACSVM) need to be included. The CMakeLists.txt will automatically
+disable the omitted components.
+
+In your root CMakeLists.txt, all that is needed is:
+  set(ACSVM_NOFLAGS ON)
+  set(ACSVM_SHARED OFF)
+  add_subdirectory(acsvm)
+And the enabled components (again, usually just acsvm) will be available for
+use in target_link_libraries.
+
+
+===============================================================================
+Usage Overview
+===============================================================================
+
+===========================================================
+Getting Started
+===========================================================
+
+To use ACSVM, you will need to define a class that inherits from
+ACSVM::Environment. By overriding the various virtuals you can configure the
+different aspects of ACS loading and interpretation. But the absolute minimal
+usage only requires overriding loadModule:
+  class Env : public ACSVM::Environment
+  {
+  protected:
+    virtual void loadModule(ACSVM::Module *module);
+  };
+
+Which is implemented by using the module's name to locate the corresponding
+bytecode and passing that to module->readBytecode. The default behavior of
+getModuleName is to just set the ModuleName's string. This can be used to
+implement bytecode directly from files:
+  void Env::loadModule(ACSVM::Module *module)
+  {
+    std::ifstream in{module->name.s->str,
+       std::ios_base::in | std::ios_base::binary};
+
+    if(!in) throw ACSVM::ReadError("file open failure");
+
+    std::vector<ACSVM::Byte> data;
+
+    for(int c; c = in.get(), in;)
+      data.push_back(c);
+
+    module->readBytecode(data.data(), data.size());
+  }
+In a Doom engine, this would most likely use lumps, instead. Either by doing
+the lookup in loadModule, or by overriding getModuleName to turn the input
+string into a lump number.
+
+To actually initialize the environment and load some modules, you can use:
+  void EnvInit(Environment &env, char const *const *namev, std::size_t namec)
+  {
+    // Load modules.
+    std::vector<ACSVM::Module *> modules;
+    for(std::size_t i = 1; i < namec; ++i)
+      modules.push_back(env.getModule(env.getModuleName(namev[i])));
+
+    // Create and activate scopes.
+    ACSVM::GlobalScope *global = env.getGlobalScope(0);  global->active = true;
+    ACSVM::HubScope    *hub    = global->getHubScope(0); hub   ->active = true;
+    ACSVM::MapScope    *map    = hub->getMapScope(0);    map   ->active = true;
+
+    // Register modules with map scope.
+    map->addModules(modules.data(), modules.size());
+
+    // Start Open scripts.
+    map->scriptStartType(1, {});
+  }
+
+And then a simple interpreter loop:
+   while(env.hasActiveThread())
+   {
+      std::chrono::duration<double> rate{1.0 / 35};
+      auto time = std::chrono::steady_clock::now() + rate;
+
+      env.exec();
+
+      std::this_thread::sleep_until(time);
+   }
+Note that if you already have a game loop, you only need to call env.exec once
+per simulation frame.
+
+Finally, you will need to register instruction and callfunc functions to
+actually interface with the larger environment. At the least, it is useful to
+implement the EndPrint (86) instruction:
+  bool CF_EndPrint(ACSVM::Thread *thread, ACSVM::Word const *, ACSVM::Word)
+  {
+    std::cout << thread->printBuf.data() << '\n';
+    thread->printBuf.drop();
+    return false;
+  }
+
+  Environment::Environment()
+  {
+    addCodeDataACS0(86, {"", 0, addCallFunc(CF_EndPrint)});
+  }
+Most of the other ACS printing logic is already handled by ACSVM, so this is
+enough to display simple Print messages.
+
diff --git a/src/acs/vm/Util/CMakeLists.txt b/src/acs/vm/Util/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..081e078d2c053ccfaf5482e6e50f78c16804342b
--- /dev/null
+++ b/src/acs/vm/Util/CMakeLists.txt
@@ -0,0 +1,38 @@
+##-----------------------------------------------------------------------------
+##
+## Copyright (C) 2015 David Hill
+##
+## See COPYING for license information.
+##
+##-----------------------------------------------------------------------------
+##
+## CMake file for acsvm-util.
+##
+##-----------------------------------------------------------------------------
+
+
+##----------------------------------------------------------------------------|
+## Environment Configuration                                                  |
+##
+
+include_directories(.)
+
+
+##----------------------------------------------------------------------------|
+## Targets                                                                    |
+##
+
+##
+## acsvm-capi
+##
+add_library(acsvm-util ${ACSVM_SHARED_DECL}
+   Floats.cpp
+   Floats.hpp
+)
+
+target_link_libraries(acsvm-util acsvm)
+
+ACSVM_INSTALL_LIB(acsvm-util)
+
+## EOF
+
diff --git a/src/acs/vm/Util/Floats.cpp b/src/acs/vm/Util/Floats.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7436e6ea4eb39b1b9ace76fce436d36271364015
--- /dev/null
+++ b/src/acs/vm/Util/Floats.cpp
@@ -0,0 +1,89 @@
+//----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//----------------------------------------------------------------------------
+//
+// Floating-point utilities.
+//
+//----------------------------------------------------------------------------
+
+#include "Floats.hpp"
+
+#include "ACSVM/Thread.hpp"
+
+#include <cstdio>
+
+
+//----------------------------------------------------------------------------|
+// Macros                                                                     |
+//
+
+//
+// DoubleOp
+//
+#define DoubleOp(name, op) \
+   bool CF_##name##F_W2(Thread *thread, Word const *argV, Word) \
+   { \
+      double l = WordsToFloat<double, 2>({{argV[0], argV[1]}}); \
+      double r = WordsToFloat<double, 2>({{argV[2], argV[3]}}); \
+      for(auto w : FloatToWords<2>(l op r)) thread->dataStk.push(w); \
+      return false; \
+   }
+
+//
+// FloatOp
+//
+#define FloatOp(name, op) \
+   bool CF_##name##F_W1(Thread *thread, Word const *argV, Word) \
+   { \
+      float l = WordsToFloat<float, 1>({{argV[0]}}); \
+      float r = WordsToFloat<float, 1>({{argV[1]}}); \
+      for(auto w : FloatToWords<1>(l op r)) thread->dataStk.push(w); \
+      return false; \
+   }
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   FloatOp(Add, +);
+   FloatOp(Div, /);
+   FloatOp(Mul, *);
+   FloatOp(Sub, -);
+
+   DoubleOp(Add, +);
+   DoubleOp(Div, /);
+   DoubleOp(Mul, *);
+   DoubleOp(Sub, -);
+
+   //
+   // void PrintDouble(double f)
+   //
+   bool CF_PrintDouble(Thread *thread, Word const *argV, Word)
+   {
+      double f = WordsToFloat<double, 2>({{argV[0], argV[1]}});
+      thread->printBuf.reserve(std::snprintf(nullptr, 0, "%f", f));
+      thread->printBuf.format("%f", f);
+      return false;
+   }
+
+   //
+   // void PrintFloat(float f)
+   //
+   bool CF_PrintFloat(Thread *thread, Word const *argV, Word)
+   {
+      float f = WordsToFloat<float, 1>({{argV[0]}});
+      thread->printBuf.reserve(std::snprintf(nullptr, 0, "%f", f));
+      thread->printBuf.format("%f", f);
+      return false;
+   }
+}
+
+// EOF
+
diff --git a/src/acs/vm/Util/Floats.hpp b/src/acs/vm/Util/Floats.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..94cd9b25ebb6b3a1a82af7d9aec6cc3413315442
--- /dev/null
+++ b/src/acs/vm/Util/Floats.hpp
@@ -0,0 +1,186 @@
+//----------------------------------------------------------------------------
+//
+// Copyright (C) 2015 David Hill
+//
+// See COPYING for license information.
+//
+//----------------------------------------------------------------------------
+//
+// Floating-point utilities.
+//
+//----------------------------------------------------------------------------
+
+#ifndef ACSVM__Util__Floats_H__
+#define ACSVM__Util__Floats_H__
+
+#include "../ACSVM/Types.hpp"
+
+#include <array>
+#include <cmath>
+
+
+//----------------------------------------------------------------------------|
+// Extern Functions                                                           |
+//
+
+namespace ACSVM
+{
+   bool CF_AddF_W1(Thread *thread, Word const *argV, Word argC);
+   bool CF_DivF_W1(Thread *thread, Word const *argV, Word argC);
+   bool CF_MulF_W1(Thread *thread, Word const *argV, Word argC);
+   bool CF_SubF_W1(Thread *thread, Word const *argV, Word argC);
+   bool CF_AddF_W2(Thread *thread, Word const *argV, Word argC);
+   bool CF_DivF_W2(Thread *thread, Word const *argV, Word argC);
+   bool CF_MulF_W2(Thread *thread, Word const *argV, Word argC);
+   bool CF_SubF_W2(Thread *thread, Word const *argV, Word argC);
+
+   bool CF_PrintDouble(Thread *thread, Word const *argV, Word argC);
+   bool CF_PrintFloat(Thread *thread, Word const *argV, Word argC);
+
+   //
+   // FloatExpBitDefault
+   //
+   template<std::size_t N>
+   constexpr std::size_t FloatExpBitDefault();
+
+   template<> constexpr std::size_t FloatExpBitDefault<1>() {return  8;}
+   template<> constexpr std::size_t FloatExpBitDefault<2>() {return 11;}
+   template<> constexpr std::size_t FloatExpBitDefault<4>() {return 15;}
+
+   //
+   // FloatToWords
+   //
+   template<std::size_t N, std::size_t ExpBit = FloatExpBitDefault<N>(), typename FltT>
+   std::array<Word, N> FloatToWords(FltT const &f)
+   {
+      static_assert(N >= 1, "N must be at least 1.");
+      static_assert(ExpBit >= 2, "ExpBit must be at least 2.");
+      static_assert(ExpBit <= 30, "ExpBit must be at most 30.");
+
+
+      constexpr int ExpMax = (1 << (ExpBit - 1)) - 1;
+      constexpr int ExpMin = -ExpMax - 1;
+      constexpr int ExpOff = ExpMax - 1;
+
+
+      std::array<Word, N> w{};
+      Word &wHi = std::get<N - 1>(w);
+      Word &wLo = std::get<0>(w);
+
+      // Convert sign.
+      bool sigRaw = std::signbit(f);
+      wHi = static_cast<Word>(sigRaw) << 31;
+
+      // Convert special values.
+      switch(std::fpclassify(f))
+      {
+      case FP_INFINITE:
+         // Convert to +/-INFINITY.
+         wHi |= (0xFFFFFFFFu << (31 - ExpBit)) & 0x7FFFFFFF;
+         return w;
+
+      case FP_NAN:
+         // Convert to NAN.
+         wHi |= 0x7FFFFFFF;
+         for(auto itr = &wLo, end = &wHi; itr != end; ++itr)
+            *itr = 0xFFFFFFFF;
+         return w;
+
+      case FP_SUBNORMAL:
+         // TODO: Subnormals.
+      case FP_ZERO:
+         // Convert to +/-0.
+         return w;
+      }
+
+      int  expRaw = 0;
+      FltT manRaw = std::ldexp(std::frexp(std::fabs(f), &expRaw), N * 32 - ExpBit);
+
+      // Check for exponent overflow.
+      if(expRaw > ExpMax)
+      {
+         // Overflow to +/-INFINITY.
+         wHi |= (0xFFFFFFFFu << (32 - ExpBit)) & 0x7FFFFFFF;
+         return w;
+      }
+
+      // Check for exponent underflow.
+      if(expRaw < ExpMin)
+      {
+         // Underflow to +/-0.
+         return w;
+      }
+
+      // Convert exponent.
+      wHi |= static_cast<Word>(expRaw + ExpOff) << (32 - ExpBit - 1);
+
+      // Convert mantissa.
+      for(int i = 0, e = N - 1; i != e; ++i)
+         w[i] = static_cast<Word>(std::fmod(std::ldexp(manRaw, -i * 32), 4294967296.0));
+      wHi |= static_cast<Word>(std::ldexp(manRaw, -static_cast<int>(N - 1) * 32))
+         & ~(0xFFFFFFFFu << (31 - ExpBit));
+
+      return w;
+   }
+
+   //
+   // WordsToFloat
+   //
+   template<typename FltT, std::size_t N, std::size_t ExpBit = FloatExpBitDefault<N>()>
+   FltT WordsToFloat(std::array<Word, N> const &w)
+   {
+      static_assert(N >= 1, "N must be at least 1.");
+      static_assert(ExpBit >= 2, "ExpBit must be at least 2.");
+      static_assert(ExpBit <= 30, "ExpBit must be at most 30.");
+
+
+      constexpr int ExpMax = (1 << (ExpBit - 1)) - 1;
+      constexpr int ExpMin = -ExpMax - 1;
+      constexpr int ExpOff = ExpMax;
+
+      constexpr Word ManMask = 0xFFFFFFFFu >> (ExpBit + 1);
+
+
+      Word const &wHi = std::get<N - 1>(w);
+      Word const &wLo = std::get<0>(w);
+
+      bool sig = !!(wHi & 0x80000000);
+      int  exp = static_cast<int>((wHi & 0x7FFFFFFF) >> (31 - ExpBit)) - ExpOff;
+
+      // INFINITY or NAN.
+      if(exp > ExpMax)
+      {
+         // Check for NAN.
+         for(auto itr = &wLo, end = &wHi; itr != end; ++itr)
+            if(*itr) return NAN;
+         if(wHi & ManMask) return NAN;
+
+         return sig ? -INFINITY : +INFINITY;
+      }
+
+      // Zero or subnormal.
+      if(exp < ExpMin)
+      {
+         // TODO: Subnormals.
+
+         return sig ? -0.0f : +0.0f;
+      }
+
+      // Convert mantissa.
+      FltT f = 0;
+      for(auto itr = &wHi, end = &wLo; itr != end;)
+         f = ldexp(f + *--itr, -32);
+      f = ldexp(f + (wHi & ManMask) + (ManMask + 1), -static_cast<int>(31 - ExpBit));
+
+      // Convert exponent.
+      f = ldexp(f, exp);
+
+      // Convert sign.
+      f = sig ? -f : +f;
+
+      return f;
+   }
+}
+
+#endif//ACSVM__Util__Floats_H__
+
diff --git a/src/acs/vm/doc/ACSVM.txt b/src/acs/vm/doc/ACSVM.txt
new file mode 100644
index 0000000000000000000000000000000000000000..17837859be145e38bc0e15f07dff41c1865ec0a9
--- /dev/null
+++ b/src/acs/vm/doc/ACSVM.txt
@@ -0,0 +1,1212 @@
+###############################################################################
+ACSVM Library Specification
+###############################################################################
+
+===============================================================================
+Types <ACSVM/ACSVM/Types.hpp>
+===============================================================================
+
+Synopsis:
+  using Byte = unsigned char;
+
+  using DWord = std::uint64_t;
+  using SDWord = std::int64_t;
+  using SWord = std::int32_t;
+  using Word = std::uint32_t;
+
+  using CallFunc = bool (*)(Thread *thread, Word const *argv, Word argc);
+
+===============================================================================
+Deferred Actions <ACSVM/ACSVM/Action.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ScopeID
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Action.hpp>
+  class ScopeID
+  {
+  public:
+    ScopeID() = default;
+    ScopeID(Word global, Word hub, Word map);
+
+    bool operator == (ScopeID const &id) const;
+    bool operator != (ScopeID const &id) const;
+
+    Word global;
+    Word hub;
+    Word map;
+  };
+
+===============================================================================
+Arrays <ACSVM/ACSVM/Array.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Array
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Array.hpp>
+  class Array
+  {
+  public:
+    Array();
+    Array(Array const &) = delete;
+    Array(Array &&array);
+    ~Array();
+
+    Word &operator [] (Word idx);
+
+    void clear();
+
+    Word find(Word idx) const;
+
+    void lockStrings(Environment *env) const;
+
+    void unlockStrings(Environment *env) const;
+  };
+
+===============================================================================
+Codes <ACSVM/ACSVM/Code.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Code
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Code.hpp>
+  enum class Code
+  {
+      /* ... */
+      None
+  };
+
+===========================================================
+ACSVM::Func
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Code.hpp>
+  enum class Func
+  {
+    /* ... */
+    None
+  };
+
+===========================================================
+ACSVM::KillType
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Code.hpp>
+  enum class KillType
+  {
+    None,
+    OutOfBounds,
+    UnknownCode,
+    UnknownFunc,
+    BranchLimit,
+  };
+
+===============================================================================
+Code Data <ACSVM/ACSVM/CodeData.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::CodeDataACS0
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/CodeData.hpp>
+  class CodeDataACS0
+  {
+  public:
+    CodeDataACS0(char const *args, Code transCode, Word stackArgC,
+      Word transFunc = 0);
+    CodeDataACS0(char const *args, Word stackArgC, Word transFunc);
+
+    char const *args;
+    std::size_t argc;
+
+    Word stackArgC;
+
+    Code transCode;
+
+    Word transFunc;
+  };
+
+===========================================================
+ACSVM::FuncDataACS0
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/CodeData.hpp>
+  class FuncDataACS0
+  {
+  public:
+    using TransCode = std::pair<Word, Code>;
+
+
+    FuncDataACS0(FuncDataACS0 const &data);
+    FuncDataACS0(FuncDataACS0 &&data);
+    FuncDataACS0(Word transFunc);
+    FuncDataACS0(Word transFunc, std::initializer_list<TransCode> transCodes);
+    ~FuncDataACS0();
+
+    FuncDataACS0 &operator = (FuncDataACS0 const &) = delete;
+    FuncDataACS0 &operator = (FuncDataACS0 &&data);
+
+    Word transFunc;
+  };
+
+===============================================================================
+Environment <ACSVM/ACSVM/Environment.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Environment
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Environment.hpp>
+  class Environment
+  {
+  public:
+    Environment();
+    virtual ~Environment();
+
+    Word addCallFunc(CallFunc func);
+
+    void addCodeDataACS0(Word code, CodeDataACS0 &&data);
+
+    void addFuncDataACS0(Word func, FuncDataACS0 &&data);
+
+    virtual bool checkLock(Thread *thread, Word lock, bool door);
+
+    virtual bool checkTag(Word type, Word tag);
+
+    void collectStrings();
+
+    virtual void exec();
+
+    void freeGlobalScope(GlobalScope *scope);
+
+    void freeModule(Module *module);
+
+    GlobalScope *getGlobalScope(Word id);
+
+    Module *getModule(ModuleName const &name);
+
+    ModuleName getModuleName(char const *str);
+    virtual ModuleName getModuleName(char const *str, std::size_t len);
+
+    String *getString(Word idx);
+    String *getString(char const *first, char const *last);
+    String *getString(char const *str);
+    String *getString(char const *str, std::size_t len);
+    String *getString(StringData const *data);
+
+    bool hasActiveThread() const;
+
+    virtual void loadState(Serial &in);
+
+    virtual void printArray(PrintBuf &buf, Array const &array, Word index,
+      Word limit);
+
+    virtual void printKill(Thread *thread, Word type, Word data);
+
+    virtual ModuleName readModuleName(Serial &in) const;
+
+    String *readString(Serial &in) const;
+
+    virtual void refStrings();
+
+    virtual void resetStrings();
+
+    virtual void saveState(Serial &out) const;
+
+    virtual void writeModuleName(Serial &out, ModuleName const &name) const;
+
+    void writeString(Serial &out, String const *in) const;
+
+    StringTable stringTable;
+
+    Word scriptLocRegC;
+
+
+    static void PrintArrayChar(PrintBuf &buf, Array const &array, Word index,
+      Word limit);
+
+    static void PrintArrayUTF8(PrintBuf &buf, Array const &array, Word index,
+      Word limit);
+
+    static constexpr Word ScriptLocRegCDefault;
+
+  protected:
+    virtual Thread *allocThread();
+
+    virtual Word callSpecImpl(Thread *thread, Word spec, Word const *argV,
+      Word argC);
+
+    virtual void loadModule(Module *module) = 0;
+  };
+
+Description:
+  Represents an execution environment.
+
+-----------------------------------------------------------
+ACSVM::Environment::Environment
+-----------------------------------------------------------
+
+Synopsis:
+  Environment();
+
+Description:
+  Constructs the Environment object.
+
+-----------------------------------------------------------
+ACSVM::Environment::~Environment
+-----------------------------------------------------------
+
+Synopsis:
+  ~Environment();
+
+Description:
+  Destructs the Environment object.
+
+-----------------------------------------------------------
+ACSVM::Environment::addCallFunc
+-----------------------------------------------------------
+
+Synopsis:
+  Word addCallFunc(CallFunc func);
+
+Description:
+  Adds a function callback for scripts and returns its index.
+
+Returns:
+  Added function's index.
+
+-----------------------------------------------------------
+ACSVM::Environment::addCodeDataACS0
+-----------------------------------------------------------
+
+Synopsis:
+  void addCodeDataACS0(Word code, CodeDataACS0 &&data);
+
+Description:
+  Adds a translation from instruction code for ACS0 and derived bytecode.
+
+-----------------------------------------------------------
+ACSVM::Environment::addFuncDataACS0
+-----------------------------------------------------------
+
+Synopsis:
+  void addFuncDataACS0(Word func, FuncDataACS0 &&data);
+
+Description:
+  Adds a translation from callfunc func for ACS0 and derived bytecode.
+
+-----------------------------------------------------------
+ACSVM::Environment::checkLock
+-----------------------------------------------------------
+
+Synopsis:
+  virtual bool checkLock(Thread *thread, Word lock, bool door);
+
+Description:
+  Called to check if a given lock number can be used from a given thread. The
+  lock number has no internal semantics, and is passed from the user source
+  unaltered.
+
+  The base implementation always return false.
+
+Returns:
+  True if the lock is open for that thread, false otherwise.
+
+-----------------------------------------------------------
+ACSVM::Environment::checkTag
+-----------------------------------------------------------
+
+Synopsis:
+  virtual bool checkTag(Word type, Word tag);
+
+Description:
+  Called to check if a given tag is inactive. The tag type and number both have
+  no internal semantics, and are passed from the user source unaltered.
+
+  The base implementation always returns false.
+
+Returns:
+  True if the tag is inactive, false otherwise.
+
+-----------------------------------------------------------
+ACSVM::Environment::collectStrings
+-----------------------------------------------------------
+
+Synopsis:
+  void collectStrings();
+
+Description:
+  Performs a full scan of the environment and frees strings that are no longer
+  in use.
+
+-----------------------------------------------------------
+ACSVM::Environment::exec
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void exec();
+
+Description:
+  Performs a single execution cycle. Deferred script actions will be applied,
+  and active threads will execute until they terminate or enter a wait state.
+
+-----------------------------------------------------------
+ACSVM::Environment::freeGlobalScope
+-----------------------------------------------------------
+
+Synopsis:
+  void freeGlobalScope(GlobalScope *scope);
+
+Description:
+  Destructs and deallocates a contained GlobalScope object.
+
+-----------------------------------------------------------
+ACSVM::Environment::freeModule
+-----------------------------------------------------------
+
+Synopsis:
+  void freeModule(Module *module);
+
+Description:
+  Destructs and deallocates a contained Module object.
+
+  If any other modules reference the freed module, they must also be freed.
+
+-----------------------------------------------------------
+ACSVM::Environment::getGlobalScope
+-----------------------------------------------------------
+
+Synopsis:
+  GlobalScope *getGlobalScope(Word id);
+
+Description:
+  Retrieves a GlobalScope object by its identifier number. If it does not
+  exist, it will be created.
+
+Returns:
+  GlobalScope object with given id.
+
+-----------------------------------------------------------
+ACSVM::Environment::getModule
+-----------------------------------------------------------
+
+Synopsis:
+  Module *getModule(ModuleName const &name);
+
+Description:
+  Retrieves a Module object by name. If it does not exist or is not loaded, it
+  will be created and loaded as needed.
+
+Returns:
+  Module object with given name.
+
+-----------------------------------------------------------
+ACSVM::Environment::getModuleName
+-----------------------------------------------------------
+
+Synopsis:
+  ModuleName getModuleName(char const *str);
+  virtual ModuleName getModuleName(char const *str, std::size_t len);
+
+Description:
+  Generates a ModuleName from an input string. The first form calls the second,
+  using the null-terminated length of the input string.
+
+  The base implementation converts the input string into a String object for
+  ModuleName::s, leaving the other ModuleName fields set to 0.
+
+Returns:
+  ModuleName object formed from input string.
+
+-----------------------------------------------------------
+ACSVM::Environment::getScriptType
+-----------------------------------------------------------
+
+Synopsis:
+  virtual std::pair<Word, Word> getScriptTypeACS0(Word name);
+  virtual Word getScriptTypeACSE(Word type);
+
+Description:
+  Translates a bytecode script type into an internal type.
+
+  First form takes the script number and must return the type and name.
+
+  The base implementation of the first form translates by dividing by 1000. The
+  second form returns the type unaltered.
+
+Returns:
+  Translated script type or (type, name) pair.
+
+-----------------------------------------------------------
+ACSVM::Environment::getString
+-----------------------------------------------------------
+
+Synopsis:
+  String *getString(Word idx);
+  String *getString(char const *first, char const *last);
+  String *getString(char const *str);
+  String *getString(char const *str, std::size_t len);
+  String *getString(StringData const *data);
+
+Description:
+  First form returns a String object as if by calling (&stringTable[~idx]).
+
+  Second, third, and fourth forms create a StringData from input to find or
+  create an entry in stringTable.
+
+  Fifth form uses the supplied StringData object to find or create an entry in
+  stringTable. If data is null, null is returned. This is intended primarily
+  for resetting strings after deserialization.
+
+Returns:
+  First form returns the String object with the given index. All other forms
+  return a String object with the same data as the input.
+
+  Fifth form will return null if input is null, and non-null otherwise. All
+  other forms never return null.
+
+-----------------------------------------------------------
+ACSVM::Environment::hasActiveThread
+-----------------------------------------------------------
+
+Synopsis:
+  bool hasActiveThread() const;
+
+Description:
+  Checks for any active threads. A thread is considered active if it has any
+  state other than ThreadState::Inactive. So this will include threads that are
+  delayed, waiting for a condition (script or tag), or set to stop during the
+  next execution cycle.
+
+Returns:
+  True if there are any active threads, false otherwise.
+
+-----------------------------------------------------------
+ACSVM::Environment::loadState
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void loadState(Serial &in);
+
+Description:
+  Restores the environment state from a previously serialized instance. If in
+  does not contain a byte stream generated by a previous call to saveState, the
+  behavior is undefined.
+
+-----------------------------------------------------------
+ACSVM::Environment::printArray
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void printArray(PrintBuf &buf, Array const &array, Word index,
+    Word limit);
+
+Description:
+  Called to write a null-terminated character subsequence from an Array object
+  to a print buffer.
+
+  The base implementation calls PrintArrayChar.
+
+-----------------------------------------------------------
+ACSVM::Environment::printKill
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void printKill(Thread *thread, Word type, Word data);
+
+Description:
+  Called when a thread encounters an error and must terminate. This includes
+  executing Code::Kill or calling Func::Kill. When calling by ACSVM itself, the
+  type parameter has a value from the KillType enumeration.
+
+  This function is expected to return normally, and the caller will handle
+  thread termination.
+
+  The base implementation prints kill information to std::cerr.
+
+-----------------------------------------------------------
+ACSVM::Environment::readModuleName
+-----------------------------------------------------------
+
+Synopsis:
+  virtual ModuleName readModuleName(Serial &in) const;
+
+Description:
+  Called to read a ModuleName from a serialized environment.
+
+  The base implementation reads the s and i members, leaving the p member null.
+
+Returns:
+  Deserialized ModuleName.
+
+-----------------------------------------------------------
+ACSVM::Environment::readString
+-----------------------------------------------------------
+
+Synopsis:
+  String *readString(Serial &in) const;
+
+Description:
+  Reads a String by index from a serialized Environment. If the written String
+  pointer was null, this function returns a null pointer.
+
+Returns:
+  Deserialized String.
+
+-----------------------------------------------------------
+ACSVM::Environment::refStrings
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void refStrings();
+
+Description:
+  Called by collectStrings to mark contained strings as referenced.
+
+  The base implementation marks strings of all contained objects, as well as
+  performs an exhaustive scan of VM memory for string indexes.
+
+-----------------------------------------------------------
+ACSVM::Environment::resetStrings
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void resetStrings();
+
+Description:
+  Called by loadState after reading the new StringTable to reset any String
+  pointers to the corresponding entry in the read table.
+
+  The base implementation resets strings of all contained objects.
+
+-----------------------------------------------------------
+ACSVM::Environment::saveState
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void saveState(Serial &out) const;
+
+Description:
+  Serializes the environment state, which can be restored with a call to
+  loadState.
+
+-----------------------------------------------------------
+ACSVM::Environment::writeModuleName
+-----------------------------------------------------------
+
+Synopsis:
+  virtual void writeModuleName(Serial &out,
+    ModuleName const &name) const;
+
+Description:
+  Called to write a ModuleName.
+
+  The base implementation writes the s and i members.
+
+-----------------------------------------------------------
+ACSVM::Environment::writeString
+-----------------------------------------------------------
+
+Synopsis:
+  void writeString(Serial &out, String const *in) const;
+
+Description:
+  Writes a String by index. If in is null, a null pointer will be returned by
+  the corresponding call to readString.
+
+===============================================================================
+Errors <ACSVM/ACSVM/Error.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ReadError
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Error.hpp>
+  class ReadError : public std::exception
+  {
+  public:
+    ReadError(char const *msg = "ACSVM::ReadError");
+
+    virtual char const *what() const noexcept;
+  };
+
+===============================================================================
+Modules <ACSVM/ACSVM/Module.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ModuleName
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Module.hpp>
+  class ModuleName
+  {
+  public:
+    ModuleName(String *s, void *p, std::size_t i);
+
+    bool operator == (ModuleName const &name) const;
+    bool operator != (ModuleName const &name) const;
+
+    std::size_t hash() const;
+
+    String     *s;
+    void       *p;
+    std::size_t i;
+  };
+
+===========================================================
+ACSVM::Module
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Module.hpp>
+  class Module
+  {
+  public:
+    void readBytecode(Byte const *data, std::size_t size);
+
+    Environment *env;
+    ModuleName   name;
+
+    bool isACS0;
+    bool loaded;
+
+
+    static constexpr std::uint32_t ChunkID(char c0, char c1, char c2, char c3);
+
+    static constexpr std::uint32_t ChunkID(char const (&s)[5]);
+  };
+
+===============================================================================
+Print Buffers <ACSVM/ACSVM/PrintBuf.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::PrintBuf
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/PrintBuf.hpp>
+  class PrintBuf
+  {
+  public:
+    PrintBuf();
+    ~PrintBuf();
+
+    void clear();
+
+    char const *data() const;
+    char const *dataFull() const;
+
+    void drop();
+
+    void format(char const *fmt, ...);
+    void formatv(char const *fmt, std::va_list arg);
+
+    char *getBuf(std::size_t count);
+
+    char *getLoadBuf(std::size_t countFull, std::size_t count);
+
+    void push();
+
+    void put(char c);
+    void put(char const *s);
+    void put(char const *s, std::size_t n);
+
+    void reserve(std::size_t count);
+
+    std::size_t size() const;
+    std::size_t sizeFull() const;
+  };
+
+===============================================================================
+Scopes <ACSVM/ACSVM/Scope.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::GlobalScope
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class GlobalScope
+  {
+  public:
+    static constexpr std::size_t ArrC = 256;
+    static constexpr std::size_t RegC = 256;
+
+
+    void freeHubScope(HubScope *scope);
+
+    HubScope *getHubScope(Word id);
+
+    bool hasActiveThread() const;
+
+    void lockStrings() const;
+
+    void reset();
+
+    void unlockStrings() const;
+
+    Environment *const env;
+    Word         const id;
+
+    Array arrV[ArrC];
+    Word  regV[RegC];
+
+    bool active;
+  };
+
+===========================================================
+ACSVM::HubScope
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class HubScope
+  {
+  public:
+    static constexpr std::size_t ArrC = 256;
+    static constexpr std::size_t RegC = 256;
+
+
+    void freeMapScope(MapScope *scope);
+
+    MapScope *getMapScope(Word id);
+
+    bool hasActiveThread() const;
+
+    void lockStrings() const;
+
+    void reset();
+
+    void unlockStrings() const;
+
+    Environment *const env;
+    GlobalScope *const global;
+    Word         const id;
+
+    Array arrV[ArrC];
+    Word  regV[RegC];
+
+    bool active;
+  };
+
+===========================================================
+ACSVM::MapScope
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class MapScope
+  {
+  public:
+    using ScriptStartFunc = void (*)(Thread *);
+    using ScriptStartFuncC = MapScope_ScriptStartFuncC;
+
+    class ScriptStartInfo;
+
+
+    void addModules(Module *const *moduleV, std::size_t moduleC);
+
+    Script *findScript(ScriptName name);
+
+    ModuleScope *getModuleScope(Module *module);
+
+    String *getString(Word idx) const;
+
+    bool hasActiveThread() const;
+
+    bool hasModules() const;
+
+    bool isScriptActive(Script *script);
+
+   void loadState(Serial &in);
+
+    void lockStrings() const;
+
+    void reset();
+
+    void saveState(Serial &out) const;
+
+    bool scriptPause(Script *script);
+    bool scriptPause(ScriptName name, ScopeID scope);
+    bool scriptStart(Script *script, ScriptStartInfo const &info);
+    bool scriptStart(ScriptName name, ScopeID scope,
+      ScriptStartInfo const &info);
+    bool scriptStartForced(Script *script, ScriptStartInfo const &info);
+    bool scriptStartForced(ScriptName name, ScopeID scope,
+      ScriptStartInfo const &info);
+    Word scriptStartResult(Script *script, ScriptStartInfo const &info);
+    Word scriptStartResult(ScriptName name, ScriptStartInfo const &info);
+    Word scriptStartType(Word type, ScriptStartInfo const &info);
+    Word scriptStartTypeForced(Word type, ScriptStartInfo const &info);
+    bool scriptStop(Script *script);
+    bool scriptStop(ScriptName name, ScopeID scope);
+
+    void unlockStrings() const;
+
+    Environment *const env;
+    HubScope    *const hub;
+    Word         const id;
+
+    bool active;
+    bool clampCallSpec;
+  };
+
+===========================================================
+ACSVM::MapScope::ScriptStartInfo
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class ScriptStartInfo
+  {
+  public:
+    ScriptStartInfo();
+    ScriptStartInfo(Word const *argV, std::size_t argC,
+       ThreadInfo const *info = nullptr, ScriptStartFunc func = nullptr);
+    ScriptStartInfo(Word const *argV, std::size_t argC,
+       ThreadInfo const *info, ScriptStartFuncC func);
+
+    Word       const *argV;
+    ScriptStartFunc   func;
+    ScriptStartFuncC  funcc;
+    ThreadInfo const *info;
+    std::size_t       argC;
+  };
+
+===========================================================
+ACSVM::ModuleScope
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Scope.hpp>
+  class ModuleScope
+  {
+  public:
+    static constexpr std::size_t ArrC = 256;
+    static constexpr std::size_t RegC = 256;
+
+
+    void lockStrings() const;
+
+    void unlockStrings() const;
+
+    Environment *const env;
+    MapScope    *const map;
+    Module      *const module;
+
+    Array *arrV[ArrC];
+    Word  *regV[RegC];
+  };
+
+===============================================================================
+Scripts <ACSVM/ACSVM/Script.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ScriptName
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Script.hpp>
+  class ScriptName
+  {
+  public:
+    ScriptName();
+    ScriptName(String *s);
+    ScriptName(String *s, Word i);
+    ScriptName(Word i);
+
+    String *s;
+    Word    i;
+  };
+
+===========================================================
+ACSVM::Script
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Script.hpp>
+  class Script
+  {
+  public:
+    explicit Script(Module *module);
+    ~Script();
+
+    Module *const module;
+
+    ScriptName name;
+
+    Word argC;
+    Word codeIdx;
+    Word flags;
+    Word locArrC;
+    Word locRegC;
+    Word type;
+
+    bool flagClient : 1;
+    bool flagNet    : 1;
+  };
+
+===============================================================================
+Stacks <ACSVM/ACSVM/Stack.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Stack
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Stack.hpp>
+  template<typename T>
+  class Stack
+  {
+  public:
+    Stack();
+    ~Stack();
+
+    T &operator [] (std::size_t idx);
+
+    T       *begin();
+    T const *begin() const;
+
+    void clear();
+
+    void drop();
+    void drop(std::size_t n);
+
+    bool empty() const;
+
+    T       *end();
+    T const *end() const;
+
+    void push(T const &value);
+    void push(T      &&value);
+
+    void reserve(std::size_t count);
+
+    std::size_t size() const;
+  };
+
+===============================================================================
+Locals Storage <ACSVM/ACSVM/Store.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::Store
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Store.hpp>
+  template<typename T>
+  class Store
+  {
+  public:
+    Store();
+    ~Store();
+
+    T &operator [] (std::size_t idx);
+
+    void alloc(std::size_t count);
+
+    void allocLoad(std::size_t countFull, std::size_t count);
+
+    T       *begin();
+    T const *begin() const;
+
+    T       *beginFull();
+    T const *beginFull() const;
+
+    void clear();
+
+    T const *dataFull() const;
+
+    T       *end();
+    T const *end() const;
+
+    void free(std::size_t count);
+
+    std::size_t size() const;
+
+    std::size_t sizeFull() const;
+  };
+
+===============================================================================
+Strings <ACSVM/ACSVM/String.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::StringData
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/String.hpp>
+  class StringData
+  {
+  public:
+    StringData(char const *first, char const *last);
+    StringData(char const *str, std::size_t len);
+    StringData(char const *str, std::size_t len, std::size_t hash);
+
+    bool operator == (StringData const &r) const;
+
+    char const *const str;
+    std::size_t const len;
+    std::size_t const hash;
+  };
+
+===========================================================
+ACSVM::String
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/String.hpp>
+  class String : public StringData
+  {
+  public:
+    std::size_t lock;
+
+    Word const idx;
+    Word const len0;
+
+    bool ref;
+
+    char get(std::size_t i) const;
+  };
+
+===========================================================
+ACSVM::StringTable
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/String.hpp>
+  class StringTable
+  {
+  public:
+    StringTable();
+    StringTable(StringTable &&table);
+    ~StringTable();
+
+    String &operator [] (Word idx) const;
+    String &operator [] (StringData const &data);
+
+    void clear();
+
+    void collectBegin();
+    void collectEnd();
+
+    String &getNone();
+
+    std::size_t size() const;
+  };
+
+===============================================================================
+Threads <ACSVM/ACSVM/Thread.hpp>
+===============================================================================
+
+===========================================================
+ACSVM::ThreadState
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Thread.hpp>
+  class ThreadState
+  {
+  public:
+    enum State
+    {
+      Inactive,
+      Running,
+      Stopped,
+      Paused,
+      WaitScrI,
+      WaitScrS,
+      WaitTag,
+    };
+
+
+    ThreadState();
+    ThreadState(State state);
+    ThreadState(State state, Word data);
+    ThreadState(State state, Word data, Word type);
+
+    bool operator == (State s) const;
+    bool operator != (State s) const;
+
+    State state;
+    Word data;
+    Word type;
+  };
+
+===========================================================
+ACSVM::ThreadInfo
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Thread.hpp>
+  class ThreadInfo
+  {
+  public:
+    virtual ~ThreadInfo() {}
+  };
+
+===========================================================
+ACSVM::Thread
+===========================================================
+
+Synopsis:
+  #include <ACSVM/ACSVM/Thread.hpp>
+  class Thread
+  {
+  public:
+    virtual ThreadInfo const *getInfo() const;
+
+    virtual void lockStrings() const;
+
+    virtual void unlockStrings() const;
+
+    Environment *const env;
+
+    Stack<CallFrame> callStk;
+    Stack<Word>      dataStk;
+    Store<Array>     localArr;
+    Store<Word>      localReg;
+    PrintBuf         printBuf;
+    ThreadState      state;
+
+    Word  const *codePtr;
+    Module      *module;
+    GlobalScope *scopeGbl;
+    HubScope    *scopeHub;
+    MapScope    *scopeMap;
+    ModuleScope *scopeMod;
+    Script      *script;
+    Word         delay;
+    Word         result;
+  };
+
+###############################################################################
+EOF
+###############################################################################
+
diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt
index 2dd78d8c0fa053c5526e5ad34dabe1e72b13f84d..5fc7bf5d60c87c7719eaffb9fac168ac0ee04479 100644
--- a/thirdparty/CMakeLists.txt
+++ b/thirdparty/CMakeLists.txt
@@ -22,7 +22,6 @@ include("cpm-discordrpc.cmake")
 include("cpm-xmp-lite.cmake")
 include("cpm-fmt.cmake")
 include("cpm-imgui.cmake")
-include("cpm-acsvm.cmake")
 
 if (SRB2_CONFIG_ENABLE_WEBM_MOVIES)
 	if (NOT "${SRB2_CONFIG_SYSTEM_LIBRARIES}")
diff --git a/thirdparty/cpm-acsvm.cmake b/thirdparty/cpm-acsvm.cmake
deleted file mode 100644
index 2318050bbb9ab029bb0d513e0b207f7f4a2ab8b3..0000000000000000000000000000000000000000
--- a/thirdparty/cpm-acsvm.cmake
+++ /dev/null
@@ -1,77 +0,0 @@
-CPMAddPackage(
-	NAME acsvm
-	VERSION 0
-	URL "https://github.com/DavidPH/ACSVM/archive/7011af443dd03e8592d7810b0b91f46c49bdde59.zip"
-	EXCLUDE_FROM_ALL ON
-	DOWNLOAD_ONLY YES
-)
-
-if(acsvm_ADDED)
-	# Sal -- While ACSVM can be built as a shared library, a lot of its options are
-	# tied to directories existing, because the project suggests just copying it into
-	# your own project directly. I don't want us to do that, so I made our own target.
-	set(
-		acsvm_SOURCES
-
-		ACSVM/Action.cpp
-		ACSVM/Action.hpp
-		ACSVM/Array.cpp
-		ACSVM/Array.hpp
-		ACSVM/BinaryIO.cpp
-		ACSVM/BinaryIO.hpp
-		ACSVM/CallFunc.cpp
-		ACSVM/CallFunc.hpp
-		ACSVM/Code.hpp
-		ACSVM/CodeData.cpp
-		ACSVM/CodeData.hpp
-		ACSVM/CodeList.hpp
-		ACSVM/Environment.cpp
-		ACSVM/Environment.hpp
-		ACSVM/Error.cpp
-		ACSVM/Error.hpp
-		ACSVM/Function.cpp
-		ACSVM/Function.hpp
-		ACSVM/HashMap.hpp
-		ACSVM/HashMapFixed.hpp
-		ACSVM/ID.hpp
-		ACSVM/Init.cpp
-		ACSVM/Init.hpp
-		ACSVM/Jump.cpp
-		ACSVM/Jump.hpp
-		ACSVM/Module.cpp
-		ACSVM/Module.hpp
-		ACSVM/ModuleACS0.cpp
-		ACSVM/ModuleACSE.cpp
-		ACSVM/PrintBuf.cpp
-		ACSVM/PrintBuf.hpp
-		ACSVM/Scope.cpp
-		ACSVM/Scope.hpp
-		ACSVM/Script.cpp
-		ACSVM/Script.hpp
-		ACSVM/Serial.cpp
-		ACSVM/Serial.hpp
-		ACSVM/Stack.hpp
-		ACSVM/Store.hpp
-		ACSVM/String.cpp
-		ACSVM/String.hpp
-		ACSVM/Thread.cpp
-		ACSVM/Thread.hpp
-		ACSVM/ThreadExec.cpp
-		ACSVM/Tracer.cpp
-		ACSVM/Tracer.hpp
-		ACSVM/Types.hpp
-		ACSVM/Vector.hpp
-
-		Util/Floats.cpp
-		Util/Floats.hpp
-	)
-	list(TRANSFORM acsvm_SOURCES PREPEND "${acsvm_SOURCE_DIR}/")
-	add_library(acsvm "${SRB2_INTERNAL_LIBRARY_TYPE}" ${acsvm_SOURCES})
-
-	target_compile_features(acsvm PRIVATE cxx_std_11)
-
-	target_include_directories(acsvm INTERFACE "${acsvm_SOURCE_DIR}")
-
-	target_link_libraries(acsvm PRIVATE acsvm::acsvm)
-	add_library(acsvm::acsvm ALIAS acsvm)
-endif()