diff --git a/bin/hxswfml.n b/bin/hxswfml.n new file mode 100644 index 00000000..398b0a61 Binary files /dev/null and b/bin/hxswfml.n differ diff --git a/command/data/scaffold/flambe.yaml b/command/data/scaffold/flambe.yaml index c3094938..ae0cbaec 100644 --- a/command/data/scaffold/flambe.yaml +++ b/command/data/scaffold/flambe.yaml @@ -23,6 +23,12 @@ default_platform: flash orientation: portrait fullscreen: true +# Whether to embed assets in to SWF's. If not, assets will be loaded over HTTP. +embed_assets: false + +width: 640 +height: 480 + # Additional flags to pass to the Haxe compiler. # haxe_flags: -lib nape -D foobar diff --git a/command/index.js b/command/index.js index cdbee2a8..d59dc25c 100644 --- a/command/index.js +++ b/command/index.js @@ -10,8 +10,10 @@ var os = require("os"); var path = require("path"); var spawn = require("child_process").spawn; var wrench = require("wrench"); +var xmldom = require("xmldom"); var DATA_DIR = __dirname + "/data/"; +var BIN_DIR = __dirname + "/../bin/"; var CACHE_DIR = "build/.cache/"; var HAXE_COMPILER_PORT = 6000; @@ -154,18 +156,88 @@ exports.build = function (config, platforms, opts) { return Q(); }; - var prepareAssets = function (dest) { + var generateAssetXml = function (relAssetDir, hxswfmlDoc) { + forEachFileIn(relAssetDir, function (file) { + var filePath = relAssetDir + "/" + file; + if(fs.lstatSync(filePath).isDirectory()) { + generateAssetXml(filePath, hxswfmlDoc); + } else { + var dataType = ""; + var extension = filePath.substring(filePath.lastIndexOf(".") + 1).toLowerCase(); + switch (extension) { + case "png": + case "jpg": + case "jpeg": + case "jxr": + case "gif": + dataType = "bitmap"; + break; + case "mp3": + dataType = "sound"; + break; + default: + dataType = "bytearray"; + break; + } + + var el = hxswfmlDoc.createElement(dataType); + el.setAttribute("file", filePath); + // create a valid AS3 class name from the asset url by replacing all non digits, non word characters, and leading non-letters with dollar signs + var className = filePath.substring(filePath.indexOf("/") + 1, filePath.lastIndexOf(".")).replace(/[^\d|\w|\$]|^[^A-za-z]/g, '$'); + el.setAttribute("class", className); + hxswfmlDoc.documentElement.appendChild(el); + } + }); + }; + + var prepareEmbeddedAssetLibrary = function () { + var hxswfmlDoc = new xmldom.DOMParser().parseFromString(""); + hxswfmlDoc.documentElement.setAttribute("width", get(config, "width")); + hxswfmlDoc.documentElement.setAttribute("height", get(config, "height")); + assetPaths.forEach(function (assetPath) { + generateAssetXml(assetPath, hxswfmlDoc); + }); + hxswfmlDoc.documentElement.appendChild(hxswfmlDoc.createElement("frame")); + + var xmlPath = CACHE_DIR+"swf/hxswfml_asset_lib_def.xml"; + fs.writeFileSync(xmlPath, new xmldom.XMLSerializer().serializeToString(hxswfmlDoc)); + + return hxswfml(["xml2lib", xmlPath, "libs/library.swf"]); + }; + + var prepareAssets = function (dest, platform) { + var assetFlags = ["--macro", "flambe.platform.ManifestBuilder.use(\""+dest+"\")"]; + wrench.rmdirSyncRecursive(dest, true); + if (fs.existsSync("libs/library.swf")) { + fs.unlinkSync("libs/library.swf"); + } + // TODO(bruno): Filter out certain formats based on the platform - return copyDirs(assetPaths, dest) - .then(function () { - return ["--macro", "flambe.platform.ManifestBuilder.use(\""+dest+"\")"]; + var promise = copyDirs(assetPaths, dest); + if (platform == "flash" && get(config, "embed_assets")) { + wrench.mkdirSyncRecursive(CACHE_DIR+"swf"); + assetFlags.push("-D", "embed_assets"); + + promise = promise.then(function () { + return prepareEmbeddedAssetLibrary() + }); + } + + return promise.then(function () { + return assetFlags; }); }; var swfFlags = function (air) { // Flags common to all swf-based targets (flash, android, ios) - var flags = ["--flash-strict", "-swf-header", "640:480:60:000000"]; + var flags = ["--flash-strict"]; + if (!get(config, "embed_assets")) { + // when assets are embedded, we take the header from their swf library so that + // we can also absorb their timeline with -D flash-use-stage, which is incompatible with -swf-header + var swfHeader = get(config, "width") + ":" + get(config, "height") + ":60:000000"; + flags.push("-swf-header", swfHeader); + } if (debug) flags.push("-D", "fdb", "-D", "advanced-telemetry"); else flags.push("-D", "native_trace"); @@ -223,7 +295,7 @@ exports.build = function (config, platforms, opts) { var outputDir = "build/web"; return prepareWeb(outputDir) - .then(function () { return prepareAssets(outputDir+"/assets") }) + .then(function () { return prepareAssets(outputDir+"/assets", "html") }) .then(function (assetFlags) { console.log("Building: " + outputDir); return buildJS({ @@ -236,29 +308,28 @@ exports.build = function (config, platforms, opts) { var buildFlash = function () { var swf = "build/web/targets/main-flash.swf"; - var flashFlags = swfFlags(false).concat([ - "-swf-version", SWF_VERSION, "-swf", swf]); return prepareWeb() - .then(function () { return prepareAssets("build/web/assets") }) + .then(function () { return prepareAssets("build/web/assets", "flash") }) .then(function (assetFlags) { console.log("Building: " + swf); + var flashFlags = swfFlags(false).concat([ + "-swf-version", SWF_VERSION, "-swf", swf]); return haxe(commonFlags.concat(assetFlags).concat(flashFlags)); }); }; - var buildAir = function (flags) { + var buildAir = function (flags, platform) { var airFlags = swfFlags(true).concat(["-swf-version", "11.7", "-D", "air"]); wrench.mkdirSyncRecursive(CACHE_DIR+"air"); - return prepareAssets(CACHE_DIR+"air/assets") + return prepareAssets(CACHE_DIR+"air/assets", platform) .then(function (assetFlags) { return haxe(commonFlags.concat(assetFlags).concat(airFlags).concat(flags)); }); }; var generateAirXml = function (swf, output) { - var xmldom = require("xmldom"); var xml = "\n" + " "+get(config, "id")+"\n" + @@ -349,7 +420,7 @@ exports.build = function (config, platforms, opts) { var cert = CACHE_DIR+"air/certificate-android.p12"; var xml = CACHE_DIR+"air/config-android.xml"; - return buildAir(["-D", "android", "-swf", CACHE_DIR+"air/"+swf]) + return buildAir(["-D", "android", "-swf", CACHE_DIR+"air/"+swf], "android") .then(function () { // Generate a dummy certificate if it doesn't exist if (!fs.existsSync(cert)) { @@ -387,7 +458,7 @@ exports.build = function (config, platforms, opts) { var mobileProvision = "certs/ios.mobileprovision"; var xml = CACHE_DIR+"air/config-ios.xml"; - return buildAir(["-D", "ios", "-D", "no-flash-override", "-swf", CACHE_DIR+"air/"+swf]) + return buildAir(["-D", "ios", "-D", "no-flash-override", "-swf", CACHE_DIR+"air/"+swf], "ios") .then(function () { var pathOptions = generateAirXml(swf, xml); @@ -414,7 +485,7 @@ exports.build = function (config, platforms, opts) { var outputDir = "build/firefox"; wrench.mkdirSyncRecursive(outputDir+"/targets"); - return prepareAssets(outputDir+"/assets") + return prepareAssets(outputDir+"/assets", "firefox") .then(function (assetFlags) { console.log("Building: " + outputDir); return buildJS({ @@ -563,6 +634,11 @@ var haxelib = function (flags, opts) { }; exports.haxelib = haxelib; +var hxswfml = function (flags, opts) { + return exec("neko " + BIN_DIR + "hxswfml.n", flags, opts); +}; +exports.hxswfml = hxswfml; + var adt = function (flags, opts) { return exec("adt", flags, opts); }; diff --git a/src/flambe/platform/flash/FlashEmbeddedAssetPackLoader.hx b/src/flambe/platform/flash/FlashEmbeddedAssetPackLoader.hx new file mode 100644 index 00000000..bc4c2848 --- /dev/null +++ b/src/flambe/platform/flash/FlashEmbeddedAssetPackLoader.hx @@ -0,0 +1,78 @@ +// +// Flambe - Rapid game development +// https://github.com/aduros/flambe/blob/master/LICENSE.txt + +package flambe.platform.flash; + +import flambe.display.Texture; +import flash.display.Bitmap; +import flash.display.Loader; +import flash.errors.Error; +import flash.events.ErrorEvent; +import flash.events.Event; +import flash.events.IEventDispatcher; +import flash.events.IOErrorEvent; +import flash.events.ProgressEvent; +import flash.events.SecurityErrorEvent; +import flash.filesystem.File; +import flash.media.Sound; +import flash.net.URLLoader; +import flash.net.URLRequest; +import flash.system.Capabilities; +import flash.system.LoaderContext; +import flash.utils.ByteArray; + +import flambe.asset.AssetEntry; +import flambe.asset.Manifest; +import flambe.util.Assert; + +class FlashEmbeddedAssetPackLoader extends BasicAssetPackLoader +{ + public function new (platform :FlashPlatform, manifest :Manifest) + { + super(platform, manifest); + } + + override private function loadEntry (url :String, entry :AssetEntry) + { + var asset : Dynamic; + // create a valid AS3 class name from the asset url, replace all non digits, non word characters, and leading non-letters with dollar signs + var className = ~/[^\d|\w|\$]|^[^A-za-z]/g.replace(url.substring(url.indexOf("/") + 1, url.lastIndexOf(".")), "$"); + + if (Type.resolveClass(className) != null) { + var resInst = Type.createInstance(Type.resolveClass(className), []); // an instance of the resource + + switch (entry.format) { + case JXR, PNG, JPG, GIF: + asset = _platform.getRenderer().createTextureFromImage(resInst.bitmapData); + resInst.bitmapData.dispose(); + + case MP3: + var sound :Sound = cast resInst; + asset = new FlashSound(sound); + + case Data: + var data :ByteArray = cast resInst; + asset = new BasicFile(data.toString()); + + default: + // Should never happen + Assert.fail("Unsupported format", ["format", entry.format]); + return; + } + + if (asset != null) { + handleLoad(entry, asset); + } else { + // Assume this is a failed createTexture() + trace("failed to create texture..."); + handleTextureError(entry); + } + } + } + + override private function getAssetFormats (fn :Array -> Void) + { + fn([JXR, PNG, JPG, GIF, MP3, Data]); + } +} diff --git a/src/flambe/platform/flash/FlashPlatform.hx b/src/flambe/platform/flash/FlashPlatform.hx index 60479408..cac88ea3 100644 --- a/src/flambe/platform/flash/FlashPlatform.hx +++ b/src/flambe/platform/flash/FlashPlatform.hx @@ -101,7 +101,11 @@ class FlashPlatform public function loadAssetPack (manifest :Manifest) :Promise { - return new FlashAssetPackLoader(this, manifest).promise; + #if embed_assets + return new FlashEmbeddedAssetPackLoader(this, manifest).promise; + #else + return new FlashAssetPackLoader(this, manifest).promise; + #end } public function getStage () :StageSystem