diff --git a/src/virtual-machine.js b/src/virtual-machine.js
index cf82327f5..a078b98a8 100644
--- a/src/virtual-machine.js
+++ b/src/virtual-machine.js
@@ -212,7 +212,7 @@ class VirtualMachine extends EventEmitter {
         }
 
         const validationPromise = new Promise((resolve, reject) => {
-            validate(input, (error, res) => {
+            validate(input, false /* this is not a single sprite */, (error, res) => {
                 if (error) {
                     reject(error);
                 }
@@ -379,23 +379,64 @@ class VirtualMachine extends EventEmitter {
     }
 
     /**
-     * Add a single sprite from the "Sprite2" (i.e., SB2 sprite) format.
-     * @param {string} json JSON string representing the sprite.
-     * @returns {Promise} Promise that resolves after the sprite is added
+     * Add a sprite, this could be .sprite2 or .sprite3. Unpack and validate
+     * such a file first.
+     * @param {string | object} input A json string, object, or ArrayBuffer representing the project to load.
+     * @return {!Promise} Promise that resolves after targets are installed.
      */
-    addSprite2 (json) {
-        // Validate & parse
-        if (typeof json !== 'string') {
-            log.error('Failed to parse sprite. Non-string supplied to addSprite2.');
-            return;
-        }
-        json = JSON.parse(json);
-        if (typeof json !== 'object') {
-            log.error('Failed to parse sprite. JSON supplied to addSprite2 is not an object.');
-            return;
+    addSprite (input) {
+        if (typeof input === 'object' && !(input instanceof ArrayBuffer) &&
+          !ArrayBuffer.isView(input)) {
+            // If the input is an object and not any ArrayBuffer
+            // or an ArrayBuffer view (this includes all typed arrays and DataViews)
+            // turn the object into a JSON string, because we suspect
+            // this is a project.json as an object
+            // validate expects a string or buffer as input
+            // TODO not sure if we need to check that it also isn't a data view
+            input = JSON.stringify(input);
         }
 
-        return sb2.deserialize(json, this.runtime, true)
+        const validationPromise = new Promise((resolve, reject) => {
+            validate(input, true /* single sprite */, (error, res) => {
+                if (error) {
+                    reject(error);
+                }
+                if (res) {
+                    resolve(res);
+                }
+            });
+        });
+
+        return validationPromise
+            .then(validatedInput => {
+                const projectVersion = validatedInput[0].projectVersion;
+                if (projectVersion === 2) {
+                    return this.addSprite2(validatedInput[0], validatedInput[1]);
+                }
+                if (projectVersion === 3) {
+                    return this.addSprite3(validatedInput[0], validatedInput[1]);
+                }
+                return Promise.reject('Unable to verify sprite version.');
+            })
+            .catch(error => {
+                // Intentionally rejecting here (want errors to be handled by caller)
+                if (error.hasOwnProperty('validationError')) {
+                    return Promise.reject(JSON.stringify(error));
+                }
+                return Promise.reject(error);
+            });
+    }
+
+    /**
+     * Add a single sprite from the "Sprite2" (i.e., SB2 sprite) format.
+     * @param {string} json JSON string representing the sprite.
+     * @param {?ArrayBuffer} zip Optional zip of assets being referenced by json
+     * @returns {Promise} Promise that resolves after the sprite is added
+     */
+    addSprite2 (json, zip) {
+        // Validate & parse
+
+        return sb2.deserialize(json, this.runtime, true, zip)
             .then(({targets, extensions}) =>
                 this.installTargets(targets, extensions, false));
     }
@@ -403,26 +444,14 @@ class VirtualMachine extends EventEmitter {
     /**
      * Add a single sb3 sprite.
      * @param {string} target JSON string representing the sprite/target.
+     * @param {?ArrayBuffer} zip Optional zip of assets being referenced by target json
      * @returns {Promise} Promise that resolves after the sprite is added
      */
-    addSprite3 (target) {
+    addSprite3 (target, zip) {
         // Validate & parse
-        if (typeof target !== 'string') {
-            log.error('Failed to parse sprite. Non-string supplied to addSprite3.');
-            return;
-        }
-        target = JSON.parse(target);
-        if (typeof target !== 'object') {
-            log.error('Failed to parse sprite. JSON supplied to addSprite3 is not an object.');
-            return;
-        }
-
-        const jsonFormatted = {
-            targets: [target]
-        };
 
         return sb3
-            .deserialize(jsonFormatted, this.runtime, null)
+            .deserialize(target, this.runtime, zip)
             .then(({targets, extensions}) => this.installTargets(targets, extensions, false));
     }
 
@@ -614,7 +643,7 @@ class VirtualMachine extends EventEmitter {
         canvas.height = bitmap.height;
         const context = canvas.getContext('2d');
         context.putImageData(bitmap, 0, 0);
-        
+
         // Divide by resolution because the renderer's definition of the rotation center
         // is the rotation center divided by the bitmap resolution
         this.runtime.renderer.updateBitmapSkin(