diff --git a/plugins/clay/tools/src/backend/tools/ClayBackendTools.hx b/plugins/clay/tools/src/backend/tools/ClayBackendTools.hx index 66163c8af..b8bd41f64 100644 --- a/plugins/clay/tools/src/backend/tools/ClayBackendTools.hx +++ b/plugins/clay/tools/src/backend/tools/ClayBackendTools.hx @@ -202,10 +202,35 @@ class ClayBackendTools implements tools.spec.BackendTools { } + public function getDstAssetsPath(cwd:String, target:tools.BuildTarget, variant:String):String { + + var outTargetPath = target.outPath('clay', cwd, context.debug, variant); + + return switch (target.name) { + case 'mac': + Path.join([cwd, 'project', 'mac', context.project.app.name + '.app', 'Contents', 'Resources', 'assets']); + case 'ios': + Path.join([cwd, 'project', 'ios', 'project', 'assets', 'assets']); + case 'android': + Path.join([cwd, 'project', 'android', 'app', 'src', 'main', 'assets', 'assets']); + case 'windows' | 'linux' | 'web': + Path.join([cwd, 'project', target.name, 'assets']); + default: + Path.join([outTargetPath, 'assets']); + }; + + } + + public function getTransformedAssetsPath(cwd:String, target:tools.BuildTarget, variant:String):String { + + var outTargetPath = target.outPath('clay', cwd, context.debug, variant); + return Path.join([outTargetPath, 'transformedAssets']); + + } + public function transformAssets(cwd:String, assets:Array, target:tools.BuildTarget, variant:String, listOnly:Bool, ?dstAssetsPath:String):Array { var newAssets:Array = []; - var outTargetPath = target.outPath('clay', cwd, context.debug, variant); var validDstPaths:Map = new Map(); var assetsChanged = false; @@ -217,18 +242,7 @@ class ClayBackendTools implements tools.spec.BackendTools { var premultiplyAlpha = (target.name != 'web'); if (dstAssetsPath == null) { - switch (target.name) { - case 'mac': - dstAssetsPath = Path.join([cwd, 'project', 'mac', context.project.app.name + '.app', 'Contents', 'Resources', 'assets']); - case 'ios': - dstAssetsPath = Path.join([cwd, 'project', 'ios', 'project', 'assets', 'assets']); - case 'android': - dstAssetsPath = Path.join([cwd, 'project', 'android', 'app', 'src', 'main', 'assets', 'assets']); - case 'windows' | 'linux' | 'web': - dstAssetsPath = Path.join([cwd, 'project', target.name, 'assets']); - default: - dstAssetsPath = Path.join([outTargetPath, 'assets']); - } + dstAssetsPath = getDstAssetsPath(cwd, target, variant); } // Add/update missing assets diff --git a/plugins/font/ceramic.yml b/plugins/font/ceramic.yml new file mode 100644 index 000000000..34d44b2cd --- /dev/null +++ b/plugins/font/ceramic.yml @@ -0,0 +1,4 @@ +plugin: + name: Font + tools: tools.FontPlugin + diff --git a/plugins/font/tools/src/tools/FontPlugin.hx b/plugins/font/tools/src/tools/FontPlugin.hx new file mode 100644 index 000000000..30a6ec066 --- /dev/null +++ b/plugins/font/tools/src/tools/FontPlugin.hx @@ -0,0 +1,72 @@ +package tools; + +import haxe.io.Path; +import sys.FileSystem; +import sys.io.File; +import tools.Context; +import tools.Helpers.*; + +using StringTools; + +@:keep +class FontPlugin { + +/// Tools + + public function new() {} + + public function init(context:Context):Void { + + // Add tasks + context.addTask('font', new tools.tasks.font.Font()); + + // Add assets transformers + context.assetsTransformers.push({ + transform: transformAssets + }); + + } + + public function transformAssets(assets:Array, transformedAssetsPath:String, changedPaths:Array):Array { + + var result:Array = []; + + for (asset in assets) { + final lowerCaseName = asset.name.toLowerCase(); + if (lowerCaseName.endsWith('.ttf') || lowerCaseName.endsWith('.otf')) { + // Transform TTF/OTF to MSDF Bitmap Font + + // Compute destination .fnt path + var baseName = asset.name.substring(0, asset.name.length - 4); + var fntName = baseName + '.fnt'; + var pngName = baseName + '.png'; + var dstFntPath = Path.join([transformedAssetsPath, fntName]); + var dstPngPath = Path.join([transformedAssetsPath, pngName]); + + if (!Files.haveSameLastModified(asset.absolutePath, dstFntPath) || !Files.haveSameLastModified(asset.absolutePath, dstPngPath)) { + FontUtils.createBitmapFont({ + fontPath: asset.absolutePath, + outputPath: transformedAssetsPath, + msdf: true, + quiet: true + }); + Files.setToSameLastModified(asset.absolutePath, dstFntPath); + Files.setToSameLastModified(asset.absolutePath, dstPngPath); + + changedPaths.push(dstFntPath); + changedPaths.push(dstPngPath); + } + + result.push(new tools.Asset(fntName, transformedAssetsPath)); + result.push(new tools.Asset(pngName, transformedAssetsPath)); + } + else { + result.push(asset); + } + } + + return result; + + } + +} diff --git a/tools/src/tools/tasks/Font.hx b/plugins/font/tools/src/tools/FontUtils.hx similarity index 72% rename from tools/src/tools/tasks/Font.hx rename to plugins/font/tools/src/tools/FontUtils.hx index e1b6fa0aa..cfbeed66c 100644 --- a/tools/src/tools/tasks/Font.hx +++ b/plugins/font/tools/src/tools/FontUtils.hx @@ -1,4 +1,4 @@ -package tools.tasks; +package tools; import haxe.Json; import haxe.io.Path; @@ -8,61 +8,45 @@ import tools.Helpers.*; using StringTools; -class Font extends Task { +class FontUtils { + + public static function createBitmapFont(options:{ + fontPath:String, + ?outputPath:String, + ?charset:String, + ?quiet:Bool, + ?charsetFile:String, + ?msdf:Bool, + ?msdfRange:Int, + ?bold:Bool, + ?italic:Bool, + ?padding:Int, + ?size:Float, + ?offsetX:Float, + ?offsetY:Float + }) { + + var cwd = context.cwd; + + var fontPath:String = options.fontPath; + var outputPath:String = options.outputPath; + var charset:String = options.charset; + var quiet:Bool = options.quiet ?? false; + var charsetFile:String = options.charsetFile; + var msdf:Bool = options.msdf ?? false; + var msdfRange:Int = options.msdfRange ?? 2; + var bold:Bool = options.bold ?? false; + var italic:Bool = options.italic ?? false; + var padding:Int = options.padding ?? 2; + var size:Float = options.size ?? 42; + var offsetX:Float = options.offsetX ?? 0; + var offsetY:Float = options.offsetY ?? 0; -/// Lifecycle - - override public function help(cwd:String):Array> { - - return [ - ['--font ', 'The ttf/otf font file to convert'], - ['--out ', 'The output directory'], - ['--msdf', 'If used, export with multichannel distance field'], - ['--msdf-range', 'Sets the distance field range in pixels (default: 2)'], - ['--size ', 'The font size to export (default: 42)'], - ['--factor ', 'A precision factor (advanced usage, default: 4)'], - ['--charset', 'Characters to use as charset'], - ['--charset-file', 'A text file containing characters to use as charset'], - ['--offset-x', 'Move every character by this X offset'], - ['--offset-y', 'Move every character by this Y offset'] - ]; - - } - - override public function info(cwd:String):String { - - return "Utility to convert ttf/otf font to bitmap font compatible with ceramic"; - - } - - override function run(cwd:String, args:Array):Void { - - var fontPath = extractArgValue(args, 'font'); - var outputPath = extractArgValue(args, 'out'); - var charset = extractArgValue(args, 'charset'); - var charsetFile = extractArgValue(args, 'charset-file'); - var msdf = extractArgFlag(args, 'msdf'); - var msdfRange = extractArgValue(args, 'msdf-range') != null ? Math.round(Std.parseFloat(extractArgValue(args, 'msdf-range'))) : 2; - var bold = extractArgFlag(args, 'bold'); - var italic = extractArgFlag(args, 'italic'); - var outputInTmp = extractArgFlag(args, 'output-in-tmp'); - var padding:Int = extractArgValue(args, 'padding') != null ? Math.round(Std.parseFloat(extractArgValue(args, 'padding'))) : 2; - var size:Float = extractArgValue(args, 'size') != null ? Std.parseFloat(extractArgValue(args, 'size')) : 42; - var offsetX:Float = extractArgValue(args, 'offset-x') != null ? Std.parseFloat(extractArgValue(args, 'offset-x')) : 0; - var offsetY:Float = extractArgValue(args, 'offset-y') != null ? Std.parseFloat(extractArgValue(args, 'offset-y')) : 0; - - if (fontPath == null) { - fail('--font argument is required'); - } + if (!Path.isAbsolute(fontPath)) + fontPath = Path.normalize(Path.join([cwd, fontPath])); - if (outputPath == null) { - if (outputInTmp) { - outputPath = TempDirectory.tempDir('font') ?? Path.join([cwd, '.tmp']); - } - else { - outputPath = cwd; - } - } + if (!Path.isAbsolute(outputPath)) + outputPath = Path.normalize(Path.join([cwd, outputPath])); if (charset == null) { if (charsetFile != null) { @@ -77,12 +61,6 @@ class Font extends Task { } } - if (!Path.isAbsolute(fontPath)) - fontPath = Path.normalize(Path.join([cwd, fontPath])); - - if (!Path.isAbsolute(outputPath)) - outputPath = Path.normalize(Path.join([cwd, outputPath])); - var tmpDir = TempDirectory.tempDir('font') ?? Path.join([cwd, '.tmp']); if (FileSystem.exists(tmpDir)) { Files.deleteRecursive(tmpDir); @@ -119,7 +97,7 @@ class Font extends Task { var tmpImagePath = Path.join([tmpDir, '$rawName.png']); var tmpJsonPath = Path.join([tmpDir, '$rawName.json']); - command(msdfAtlasGen, [ + final result = command(msdfAtlasGen, [ '-charset', tmpCharsetPath, '-font', tmpFontPath, '-type', msdf ? 'msdf' : 'softmask', @@ -131,7 +109,7 @@ class Font extends Task { ].concat(msdf ? [ '-pxrange', '$msdfRange', ] : []), { - mute: outputInTmp + mute: quiet }); // Make the image transparent, if not using msdf @@ -254,11 +232,9 @@ class Font extends Task { // Remove temporary files Files.deleteRecursive(tmpDir); - // Print output path, if using "output in tmp" - if (outputInTmp) { - print(outputPath); - } + // Return `true` if the command didn't fail + return result.status == 0; } -} \ No newline at end of file +} diff --git a/plugins/font/tools/src/tools/tasks/font/Font.hx b/plugins/font/tools/src/tools/tasks/font/Font.hx new file mode 100644 index 000000000..3652795f6 --- /dev/null +++ b/plugins/font/tools/src/tools/tasks/font/Font.hx @@ -0,0 +1,80 @@ +package tools.tasks.font; + +import haxe.Json; +import haxe.io.Path; +import sys.FileSystem; +import sys.io.File; +import tools.Helpers.*; + +using StringTools; + +class Font extends Task { + +/// Lifecycle + + override public function help(cwd:String):Array> { + + return [ + ['--font ', 'The ttf/otf font file to convert'], + ['--out ', 'The output directory'], + ['--msdf', 'If used, export with multichannel distance field'], + ['--msdf-range', 'Sets the distance field range in pixels (default: 2)'], + ['--size ', 'The font size to export (default: 42)'], + ['--factor ', 'A precision factor (advanced usage, default: 4)'], + ['--charset', 'Characters to use as charset'], + ['--charset-file', 'A text file containing characters to use as charset'], + ['--offset-x', 'Move every character by this X offset'], + ['--offset-y', 'Move every character by this Y offset'] + ]; + + } + + override public function info(cwd:String):String { + + return "Utility to convert ttf/otf font to bitmap font compatible with ceramic"; + + } + + override function run(cwd:String, args:Array):Void { + + var fontPath = extractArgValue(args, 'font'); + var outputPath = extractArgValue(args, 'out'); + var charset = extractArgValue(args, 'charset'); + var quiet = extractArgFlag(args, 'quiet'); + var charsetFile = extractArgValue(args, 'charset-file'); + var msdf = extractArgFlag(args, 'msdf'); + var msdfRange = extractArgValue(args, 'msdf-range') != null ? Math.round(Std.parseFloat(extractArgValue(args, 'msdf-range'))) : 2; + var bold = extractArgFlag(args, 'bold'); + var italic = extractArgFlag(args, 'italic'); + var padding:Int = extractArgValue(args, 'padding') != null ? Math.round(Std.parseFloat(extractArgValue(args, 'padding'))) : 2; + var size:Float = extractArgValue(args, 'size') != null ? Std.parseFloat(extractArgValue(args, 'size')) : 42; + var offsetX:Float = extractArgValue(args, 'offset-x') != null ? Std.parseFloat(extractArgValue(args, 'offset-x')) : 0; + var offsetY:Float = extractArgValue(args, 'offset-y') != null ? Std.parseFloat(extractArgValue(args, 'offset-y')) : 0; + + if (fontPath == null) { + fail('--font argument is required'); + } + + if (outputPath == null) { + outputPath = cwd; + } + + FontUtils.createBitmapFont({ + fontPath: fontPath, + outputPath: outputPath, + charset: charset, + quiet: quiet, + charsetFile: charsetFile, + msdf: msdf, + msdfRange: msdfRange, + bold: bold, + italic: italic, + padding: padding, + size: size, + offsetX: offsetX, + offsetY: offsetY + }); + + } + +} \ No newline at end of file diff --git a/plugins/headless/tools/src/backend/tools/HeadlessBackendTools.hx b/plugins/headless/tools/src/backend/tools/HeadlessBackendTools.hx index e8adb21f2..0f79251e4 100644 --- a/plugins/headless/tools/src/backend/tools/HeadlessBackendTools.hx +++ b/plugins/headless/tools/src/backend/tools/HeadlessBackendTools.hx @@ -143,13 +143,27 @@ class HeadlessBackendTools implements tools.spec.BackendTools { } + public function getDstAssetsPath(cwd:String, target:tools.BuildTarget, variant:String):String { + + var hxmlProjectPath = target.outPath('headless', cwd, context.debug, variant); + return Path.join([hxmlProjectPath, 'assets']); + + } + + public function getTransformedAssetsPath(cwd:String, target:tools.BuildTarget, variant:String):String { + + var hxmlProjectPath = target.outPath('headless', cwd, context.debug, variant); + return Path.join([hxmlProjectPath, 'transformedAssets']); + + } + public function transformAssets(cwd:String, assets:Array, target:tools.BuildTarget, variant:String, listOnly:Bool, ?dstAssetsPath:String):Array { var newAssets:Array = []; var hxmlProjectPath = target.outPath('headless', cwd, context.debug, variant); var validDstPaths:Map = new Map(); if (dstAssetsPath == null) { - dstAssetsPath = Path.join([hxmlProjectPath, 'assets']); + dstAssetsPath = getDstAssetsPath(cwd, target, variant); } var assetsPrefix = ''; diff --git a/plugins/mac/tools/src/tools/tasks/mac/Compile.hx b/plugins/mac/tools/src/tools/tasks/mac/Compile.hx index fd692d0d5..d4b110e50 100644 --- a/plugins/mac/tools/src/tools/tasks/mac/Compile.hx +++ b/plugins/mac/tools/src/tools/tasks/mac/Compile.hx @@ -2,6 +2,7 @@ package tools.tasks.mac; import haxe.io.Path; import sys.FileSystem; +import sys.io.File; import tools.Helpers.*; using StringTools; @@ -80,7 +81,7 @@ class Compile extends tools.Task { } } - if (allBinaries.length > 0) { + if (allBinaries.length > 1) { // Need to merge archs with lipo print('Create universal binary'); command('lipo', [ @@ -88,6 +89,12 @@ class Compile extends tools.Task { '-output', Path.join([outTargetPath, 'cpp', baseBinary]) ])); } + else if (allBinaries.length == 1) { + FileSystem.rename( + allBinaries[0], + Path.join([outTargetPath, 'cpp', baseBinary]) + ); + } } diff --git a/plugins/unity/tools/src/backend/tools/UnityBackendTools.hx b/plugins/unity/tools/src/backend/tools/UnityBackendTools.hx index c38526b68..236c3a142 100644 --- a/plugins/unity/tools/src/backend/tools/UnityBackendTools.hx +++ b/plugins/unity/tools/src/backend/tools/UnityBackendTools.hx @@ -142,6 +142,20 @@ class UnityBackendTools implements tools.spec.BackendTools { } + public function getDstAssetsPath(cwd:String, target:tools.BuildTarget, variant:String):String { + + var hxmlProjectPath = target.outPath('unity', cwd, context.debug, variant); + return Path.join([hxmlProjectPath, 'assets']); + + } + + public function getTransformedAssetsPath(cwd:String, target:tools.BuildTarget, variant:String):String { + + var hxmlProjectPath = target.outPath('unity', cwd, context.debug, variant); + return Path.join([hxmlProjectPath, 'transformedAssets']); + + } + public function transformAssets(cwd:String, assets:Array, target:tools.BuildTarget, variant:String, listOnly:Bool, ?dstAssetsPath:String):Array { var nonTxtExtensions = [ @@ -163,10 +177,9 @@ class UnityBackendTools implements tools.spec.BackendTools { ]; var newAssets:Array = []; - var hxmlProjectPath = target.outPath('unity', cwd, context.debug, variant); var validDstPaths:Map = new Map(); if (dstAssetsPath == null) { - dstAssetsPath = Path.join([hxmlProjectPath, 'assets']); + dstAssetsPath = getDstAssetsPath(cwd, target, variant); } var unityProjectPath = UnityProject.resolveUnityProjectPath(cwd, context.project); diff --git a/plugins/web/tools/src/tools/tasks/web/Web.hx b/plugins/web/tools/src/tools/tasks/web/Web.hx index 0646e3da2..95b868931 100644 --- a/plugins/web/tools/src/tools/tasks/web/Web.hx +++ b/plugins/web/tools/src/tools/tasks/web/Web.hx @@ -258,6 +258,12 @@ class Web extends tools.Task { ); } + if (Sys.systemName() == 'Windows') { + proc.env.set('CERAMIC_CLI', Path.join([context.ceramicToolsPath, 'ceramic.exe'])); + } else { + proc.env.set('CERAMIC_CLI', Path.join([context.ceramicToolsPath, 'ceramic'])); + } + var out = new SplitStream('\n'.code, line -> { line = formatLineOutput(outTargetPath, line); stdoutWrite(line + "\n"); diff --git a/runtime/src/ceramic/Assets.hx b/runtime/src/ceramic/Assets.hx index 8095d9771..4e63823ef 100644 --- a/runtime/src/ceramic/Assets.hx +++ b/runtime/src/ceramic/Assets.hx @@ -1101,7 +1101,7 @@ class Assets extends Entity { var realPathKey = realAssetPath(key, runtimeAssets); newLastModifiedByRealAssetPath.set(realPathKey, value); if (lastModifiedByRealAssetPath.exists(realPathKey)) { - if (value > lastModifiedByRealAssetPath.get(realPathKey)) { + if (value != lastModifiedByRealAssetPath.get(realPathKey)) { incrementReloadCount(realPathKey); } } @@ -1199,6 +1199,8 @@ class Assets extends Entity { static function incrementReloadCount(realAssetPath:String) { + realAssetPath = Path.normalize(realAssetPath); + if (Assets.reloadCountByRealAssetPath == null) Assets.reloadCountByRealAssetPath = new Map(); @@ -1213,6 +1215,8 @@ class Assets extends Entity { public static function getReloadCount(realAssetPath:String):Int { + realAssetPath = Path.normalize(realAssetPath); + if (Assets.reloadCountByRealAssetPath == null || !Assets.reloadCountByRealAssetPath.exists(realAssetPath)) return 0; diff --git a/runtime/src/ceramic/AtlasAsset.hx b/runtime/src/ceramic/AtlasAsset.hx index 25a4d0e54..5a3e5c651 100644 --- a/runtime/src/ceramic/AtlasAsset.hx +++ b/runtime/src/ceramic/AtlasAsset.hx @@ -251,7 +251,7 @@ class AtlasAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload atlas (file has changed)'); load(); } diff --git a/runtime/src/ceramic/BinaryAsset.hx b/runtime/src/ceramic/BinaryAsset.hx index 410f61cf1..9efdcc5a9 100644 --- a/runtime/src/ceramic/BinaryAsset.hx +++ b/runtime/src/ceramic/BinaryAsset.hx @@ -76,7 +76,7 @@ class BinaryAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload binary (file has changed)'); load(); } diff --git a/runtime/src/ceramic/DatabaseAsset.hx b/runtime/src/ceramic/DatabaseAsset.hx index f52d37cc0..f18bf579e 100644 --- a/runtime/src/ceramic/DatabaseAsset.hx +++ b/runtime/src/ceramic/DatabaseAsset.hx @@ -78,7 +78,7 @@ class DatabaseAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload database (file has changed)'); load(); } diff --git a/runtime/src/ceramic/FontAsset.hx b/runtime/src/ceramic/FontAsset.hx index 2e6e540be..79acbb33d 100644 --- a/runtime/src/ceramic/FontAsset.hx +++ b/runtime/src/ceramic/FontAsset.hx @@ -3,6 +3,8 @@ package ceramic; import ceramic.Path; import ceramic.Shortcuts.*; +using StringTools; + class FontAsset extends Asset { /// Events @@ -17,6 +19,8 @@ class FontAsset extends Asset { @observe public var font:BitmapFont = null; + var transformedPath:String = null; + /// Lifecycle override public function new(name:String, ?variant:String, ?options:AssetOptions #if ceramic_debug_entity_allocs , ?pos:haxe.PosInfos #end) { @@ -53,20 +57,31 @@ class FontAsset extends Asset { return; } - log.info('Load font $path'); + // TTF/OTF fonts don't exist at runtime, so if the name is that, + // we resolve the converted bitmap font .fnt path instead + var actualPath = path; + + var lowerCasePath = actualPath.toLowerCase(); + if (lowerCasePath.endsWith('.ttf') || lowerCasePath.endsWith('.otf')) { + actualPath = actualPath.substring(0, actualPath.length - 4) + '.fnt'; + } + log.info('Load font $actualPath'); + if (transformedPath != null) { + actualPath = Path.join([transformedPath, actualPath]); + } // Use runtime assets if provided assets.runtimeAssets = runtimeAssets; var asset = new TextAsset(name); asset.handleTexturesDensityChange = false; - asset.path = path; + asset.path = actualPath; assets.addAsset(asset); assets.onceComplete(this, function(success) { var text = asset.text; - var relativeFontPath = Path.directory(path); + var relativeFontPath = Path.directory(actualPath); if (relativeFontPath == '') relativeFontPath = '.'; if (text != null) { @@ -93,6 +108,7 @@ class FontAsset extends Asset { asset.handleTexturesDensityChange = false; asset.path = pathInfo.path; + assets.addAsset(asset); assetList.push(asset); @@ -233,9 +249,37 @@ class FontAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload font (file has changed)'); - load(); + + var lowerCasePath = path.toLowerCase(); + if (owner?.runtimeAssets != null && lowerCasePath.endsWith('.ttf') || lowerCasePath.endsWith('.otf')) { + var actualPath = path; + owner.runtimeAssets.requestTransformedDir(transformedDir -> { + Platform.runCeramic([ + 'assets', + '--filter', path, + '--from', owner.runtimeAssets.path, + '--to', transformedDir, + '--list-changed' + ], + (code, out, err) -> { + if (code == 0) { + transformedPath = transformedDir; + var changed:Array = Json.parse(out.trim()); + for (absolutePath in changed) { + log.debug('Updated on the fly: $absolutePath'); + Assets.incrementReloadCount(absolutePath); + } + load(); + } + }); + }); + } + else { + load(); + } + } } diff --git a/runtime/src/ceramic/FragmentsAsset.hx b/runtime/src/ceramic/FragmentsAsset.hx index 9ae2c1730..ab49f5c0c 100644 --- a/runtime/src/ceramic/FragmentsAsset.hx +++ b/runtime/src/ceramic/FragmentsAsset.hx @@ -195,7 +195,7 @@ class FragmentsAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload fragments (file has changed)'); load(); } diff --git a/runtime/src/ceramic/ImageAsset.hx b/runtime/src/ceramic/ImageAsset.hx index d4cfd40a2..b30c82399 100644 --- a/runtime/src/ceramic/ImageAsset.hx +++ b/runtime/src/ceramic/ImageAsset.hx @@ -291,7 +291,7 @@ class ImageAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload texture (file has changed)'); load(); } diff --git a/runtime/src/ceramic/Platform.hx b/runtime/src/ceramic/Platform.hx index 89356d5db..3becd38e7 100644 --- a/runtime/src/ceramic/Platform.hx +++ b/runtime/src/ceramic/Platform.hx @@ -301,6 +301,79 @@ class Platform { } + /** + * Executes the given Ceramic command asynchronously. + * This will work only on desktop platforms that have a valid Ceramic installation available. + * When the app has been launched by Ceramic itself, the same CLI tool will be used. + * Otherwise, it will look for a `ceramic` command available in `PATH` + * @param cmd The command to run + * @param args (optional) The command arguments, which will be automatically escaped + * @param callback The callback, called when the command has finished (or failed) + */ + public static function runCeramic(args:Array, callback:(code:Int, stdout:String, stderr:String)->Void):Void { + + var cmd = Platform.getEnv('CERAMIC_CLI'); + if (cmd == null) + cmd = isWindows() ? 'ceramic.exe' : 'ceramic'; + + log.debug('ceramic ' + args.join(' ')); + exec(cmd, args, callback); + + } + + public static function getEnv(key:String):String { + + #if ((web && ceramic_use_electron) || node || nodejs || hxnodejs) + try { + return js.Syntax.code('process.env[{0}]', key); + } + catch (e:Any) { + return null; + } + #elseif (sys || cs) + return Sys.getEnv(key); + #else + return null; + #end + + } + + public static function isWindows():Bool { + + #if ((web && ceramic_use_electron) || node || nodejs || hxnodejs) + var isWindows:Bool = false; + if (_isWindowsValue == 0) { + try { + #if (node || nodejs || hxnodejs) + isWindows = js.Syntax.code('process.platform == "win32"'); + #else + final os:Dynamic = nodeRequire('os'); + isWindows = os.platform() == 'win32'; + #end + } + catch (e:Any) { + isWindows = false; + } + _isWindowsValue = (isWindows ? 1 : -1); + } + else { + isWindows = (_isWindowsValue == 1); + } + return isWindows; + #elseif windows + return true; + #elseif sys + var isWindows:Bool = false; + if (_isWindowsValue == 0) { + _isWindowsValue = ((Sys.systemName() == 'Windows') ? 1 : -1); + } + return (_isWindowsValue == 1); + #else + return false; + #end + + } + /** * Executes the given command asynchronously * @param cmd The command to run @@ -317,19 +390,8 @@ class Platform { args = []; } - var isWindows:Bool = false; - if (_execIsWindowsValue == 0) { - #if (node || nodejs || hxnodejs) - isWindows = js.Syntax.code('process.platform == "win32"'); - #else - final os:Dynamic = nodeRequire('os'); - isWindows = os.platform() == 'win32'; - #end - _execIsWindowsValue = (isWindows ? 1 : -1); - } - else { - isWindows = (_execIsWindowsValue == 1); - } + final isWindows = isWindows(); + var options = { shell: isWindows ? (StringTools.endsWith(cmd, '.exe') ? false : true) : false }; @@ -388,7 +450,7 @@ class Platform { } - private static var _execIsWindowsValue:Int = 0; + private static var _isWindowsValue:Int = 0; private static var _execDidLogError:Bool = false; } diff --git a/runtime/src/ceramic/RuntimeAssets.hx b/runtime/src/ceramic/RuntimeAssets.hx index fbedd01f1..e31bb61e9 100644 --- a/runtime/src/ceramic/RuntimeAssets.hx +++ b/runtime/src/ceramic/RuntimeAssets.hx @@ -15,6 +15,44 @@ import sys.FileSystem; */ class RuntimeAssets { + var transformedDir:String = null; + var didQueryTransformedDir:Int = 0; + var pendingTransformedDirCallbacks:Array<(transformedDir:String)->Void> = null; + public function requestTransformedDir(callback:(transformedDir:String)->Void):Void { + if (didQueryTransformedDir == 2) { + app.onceImmediate(() -> callback(transformedDir)); + } + else if (didQueryTransformedDir == 1) { + if (pendingTransformedDirCallbacks == null) { + pendingTransformedDirCallbacks = []; + } + pendingTransformedDirCallbacks.push(callback); + } + else { + didQueryTransformedDir = 1; + Platform.runCeramic(['tmp', 'dir'], (code, out, err) -> { + if (code == 0) { + var result = out.trim(); + if (result.length > 0 && Files.exists(result)) { + transformedDir = result; + } + } + else { + app.logger.error('Failed to resolve tmp dir: $code / $err'); + } + didQueryTransformedDir = 2; + callback(transformedDir); + if (pendingTransformedDirCallbacks != null) { + var transformedDirCallbacks = pendingTransformedDirCallbacks; + pendingTransformedDirCallbacks = null; + for (cb in transformedDirCallbacks) { + cb(transformedDir); + } + } + }); + } + } + var allAssets:Array = null; var allAssetDirs:Array = null; diff --git a/runtime/src/ceramic/ShaderAsset.hx b/runtime/src/ceramic/ShaderAsset.hx index d4cb1efbd..5a39fcbc3 100644 --- a/runtime/src/ceramic/ShaderAsset.hx +++ b/runtime/src/ceramic/ShaderAsset.hx @@ -192,7 +192,7 @@ class ShaderAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload shader (fragment shader has changed)'); load(); } @@ -208,7 +208,7 @@ class ShaderAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload shader (vertex shader has changed)'); load(); } diff --git a/runtime/src/ceramic/SoundAsset.hx b/runtime/src/ceramic/SoundAsset.hx index 6846b3558..f6af67055 100644 --- a/runtime/src/ceramic/SoundAsset.hx +++ b/runtime/src/ceramic/SoundAsset.hx @@ -129,7 +129,7 @@ class SoundAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload sound (file has changed)'); load(); } diff --git a/runtime/src/ceramic/TextAsset.hx b/runtime/src/ceramic/TextAsset.hx index f168017ac..b332da190 100644 --- a/runtime/src/ceramic/TextAsset.hx +++ b/runtime/src/ceramic/TextAsset.hx @@ -75,7 +75,7 @@ class TextAsset extends Asset { newTime = newFiles.get(path); } - if (newTime > previousTime) { + if (newTime != previousTime) { log.info('Reload text (file has changed)'); load(); } diff --git a/runtime/src/ceramic/WatchDirectory.hx b/runtime/src/ceramic/WatchDirectory.hx index d7ae5e52e..ec1ba4913 100644 --- a/runtime/src/ceramic/WatchDirectory.hx +++ b/runtime/src/ceramic/WatchDirectory.hx @@ -221,7 +221,7 @@ class WatchDirectory extends Entity { didChange = true; break; } - else if (mtime > previousFilesModificationTime.get(path)) { + else if (mtime != previousFilesModificationTime.get(path)) { // Modification time has changed didChange = true; break; diff --git a/tools/src/tools/Context.hx b/tools/src/tools/Context.hx index d8be694da..28884d5e9 100644 --- a/tools/src/tools/Context.hx +++ b/tools/src/tools/Context.hx @@ -90,6 +90,12 @@ class Context { /** A flag to tell whether one asset or more have changed since last asset pass */ public var assetsChanged:Bool; + /** Assets transformers, such as converters to transform TTF/OTF to Bitmap font and so on... */ + public var assetsTransformers:Array; + + /** List of temporary directories that have been created and not cleaned up automatically */ + public var tempDirs:Array; + /** A flag to tell whether one icon or more have changed since last icon pass */ public var iconsChanged:Bool; diff --git a/tools/src/tools/Helpers.hx b/tools/src/tools/Helpers.hx index 1e87817e3..d6d4a008e 100644 --- a/tools/src/tools/Helpers.hx +++ b/tools/src/tools/Helpers.hx @@ -249,7 +249,7 @@ class Helpers { } if (Sys.systemName() == 'Windows') { - return command(Path.join([context.ceramicToolsPath, 'ceramic.cmd']), actualArgs, { cwd: cwd, mute: mute }); + return command(Path.join([context.ceramicToolsPath, 'ceramic.exe']), actualArgs, { cwd: cwd, mute: mute }); } else { return command(Path.join([context.ceramicToolsPath, 'ceramic']), actualArgs, { cwd: cwd, mute: mute }); } @@ -719,6 +719,12 @@ class Helpers { final proc = new Process(name, args, options.cwd); + if (Sys.systemName() == 'Windows') { + proc.env.set('CERAMIC_CLI', Path.join([context.ceramicToolsPath, 'ceramic.exe'])); + } else { + proc.env.set('CERAMIC_CLI', Path.join([context.ceramicToolsPath, 'ceramic'])); + } + proc.inherit_file_descriptors = false; var stdout = new SplitStream('\n'.code, line -> { @@ -774,7 +780,6 @@ class Helpers { // Handle Windows, again... if (Sys.systemName() == 'Windows') { - // npm if (name == 'npm' || name == 'node' || name == 'ceramic' || name == 'haxe' || name == 'haxelib' || name == 'neko') { name = name + '.cmd'; } @@ -782,6 +787,12 @@ class Helpers { final proc = new Process(name, args, options.cwd); + if (Sys.systemName() == 'Windows') { + proc.env.set('CERAMIC_CLI', Path.join([context.ceramicToolsPath, 'ceramic.exe'])); + } else { + proc.env.set('CERAMIC_CLI', Path.join([context.ceramicToolsPath, 'ceramic'])); + } + if (options.detached) { proc.detach_process = true; } diff --git a/tools/src/tools/Tools.hx b/tools/src/tools/Tools.hx index 0b24e5a9a..763523fde 100644 --- a/tools/src/tools/Tools.hx +++ b/tools/src/tools/Tools.hx @@ -52,6 +52,8 @@ class Tools { isEmbeddedInElectron: false, ceramicVersion: null, assetsChanged: false, + assetsTransformers: [], + tempDirs: [], iconsChanged: false, printSplitLines: (args.indexOf('--print-split-lines') != -1), haxePaths: [], @@ -103,11 +105,12 @@ class Tools { context.addTask('info', new tools.tasks.Info()); context.addTask('libs', new tools.tasks.Libs()); context.addTask('hxml', new tools.tasks.Hxml()); - - context.addTask('font', new tools.tasks.Font()); + context.addTask('assets', new tools.tasks.Assets()); context.addTask('haxe server', new tools.tasks.HaxeServer()); + context.addTask('tmp dir', new tools.tasks.TmpDir()); + context.addTask('plugin hxml', new tools.tasks.plugin.PluginHxml()); context.addTask('plugin list', new tools.tasks.plugin.ListPlugins()); diff --git a/tools/src/tools/spec/BackendTools.hx b/tools/src/tools/spec/BackendTools.hx index 5b858671a..449e4cd61 100644 --- a/tools/src/tools/spec/BackendTools.hx +++ b/tools/src/tools/spec/BackendTools.hx @@ -33,6 +33,12 @@ interface BackendTools { /** Run backend framework (dependency) update/install **/ function runUpdate(cwd:String, args:Array):Void; + /** Get the destination assets path from the given info */ + function getDstAssetsPath(cwd:String, target:tools.BuildTarget, variant:String):String; + + /** Get the transformed assets path from the given info */ + function getTransformedAssetsPath(cwd:String, target:tools.BuildTarget, variant:String):String; + /** Transform and get assets for the given backend and build target */ function transformAssets(cwd:String, assets:Array, target:tools.BuildTarget, variant:String, listOnly:Bool, ?dstAssetsPath:String):Array; diff --git a/tools/src/tools/spec/TransformAssets.hx b/tools/src/tools/spec/TransformAssets.hx new file mode 100644 index 000000000..fcfd750a8 --- /dev/null +++ b/tools/src/tools/spec/TransformAssets.hx @@ -0,0 +1,7 @@ +package tools.spec; + +typedef TransformAssets = { + + function transform(assets:Array, transformedAssetsPath:String, changedPaths:Array):Array; + +} diff --git a/tools/src/tools/tasks/Assets.hx b/tools/src/tools/tasks/Assets.hx index 5d76c2f13..a6d1e7c2b 100644 --- a/tools/src/tools/tasks/Assets.hx +++ b/tools/src/tools/tasks/Assets.hx @@ -20,26 +20,46 @@ class Assets extends tools.Task { override public function info(cwd:String):String { - return "Transform/copy project's assets for " + context.backend.name + " backend and given target."; + if (context.backend == null) { + return "Transform/copy assets."; + } + else { + return "Transform/copy project's assets for " + context.backend.name + " backend and given target."; + } } override function run(cwd:String, args:Array):Void { + var filter = extractArgValue(args, 'filter'); + var regex = filter != null ? Glob.toEReg(filter) : null; + + var noBackendTransform = (context.backend == null) || extractArgFlag(args, 'no-backend-transform'); + var fromArg = extractArgValue(args, 'from', true); var toArg = extractArgValue(args, 'to', true); var processIcons = false; + var isProjectAssets = false; + var fromPath = null; var toPath = null; var project = null; + var changedPaths:Array = []; + var listChanged = extractArgFlag(args, 'list-changed'); + // We are either processing assets for current project // or with provided source and destination if (fromArg == null || toArg == null) { + isProjectAssets = true; processIcons = true; project = ensureCeramicProject(cwd, args, App); + + // Never filter when doing project assets + filter = null; + regex = null; } else { fromPath = fromArg; @@ -62,10 +82,10 @@ class Assets extends tools.Task { } } - var availableTargets = context.backend.getBuildTargets(); - var targetName = getTargetName(args, availableTargets); + var availableTargets = context.backend != null ? context.backend.getBuildTargets() : []; + var targetName = context.backend != null ? getTargetName(args, availableTargets) : null; - if (targetName == null) { + if (targetName == null && context.backend != null) { fail('You must specify a target to transform/copy assets to.'); } @@ -81,7 +101,7 @@ class Assets extends tools.Task { } - if (target == null) { + if (target == null && context.backend != null) { fail('Unknown target: $targetName'); } @@ -100,52 +120,103 @@ class Assets extends tools.Task { // Add assets if (FileSystem.exists(assetsPath)) { for (name in Files.getFlatDirectory(assetsPath)) { - assets.push(new tools.Asset(name, assetsPath)); - names.set(name, true); + if (regex == null || regex.match(name)) { + assets.push(new tools.Asset(name, assetsPath)); + names.set(name, true); + } } } - // Add extra asset paths - if (project != null) { - var extraAssets:Array = project.app.assets; - if (extraAssets != null) { - for (extraAssetsPath in extraAssets) { - if (FileSystem.exists(extraAssetsPath) && FileSystem.isDirectory(extraAssetsPath)) { - for (name in Files.getFlatDirectory(extraAssetsPath)) { - if (!names.exists(name)) { - assets.push(new tools.Asset(name, extraAssetsPath)); - names.set(name, true); + // Compute destination assets path + var dstAssetsPath = toPath; + var transformedAssetsPath = null; + if (!noBackendTransform) { + if (dstAssetsPath == null) { + dstAssetsPath = context.backend.getDstAssetsPath( + cwd, + target, + context.variant + ); + } + if (transformedAssetsPath == null) { + transformedAssetsPath = context.backend.getTransformedAssetsPath( + cwd, + target, + context.variant + ); + } + } + else if (transformedAssetsPath == null && !isProjectAssets && toPath != null) { + transformedAssetsPath = toPath; + } + else { + transformedAssetsPath = TempDirectory.tempDir('transformedAssets'); + context.tempDirs.push(transformedAssetsPath); + } + + // If no specific path is specified, that means we are + // transforming project's assets, so let's involve every extra asset path + // including the ones provided by plugins + if (isProjectAssets) { + + print('Update project assets'); + + // Add extra asset paths + if (project != null) { + var extraAssets:Array = project.app.assets; + if (extraAssets != null) { + for (extraAssetsPath in extraAssets) { + if (FileSystem.exists(extraAssetsPath) && FileSystem.isDirectory(extraAssetsPath)) { + for (name in Files.getFlatDirectory(extraAssetsPath)) { + if (!names.exists(name)) { + if (regex == null || regex.match(name)) { + assets.push(new tools.Asset(name, extraAssetsPath)); + names.set(name, true); + } + } } } } } } - } - // Add ceramic default assets (if not overrided by project or plugins assets) - if (FileSystem.exists(ceramicAssetsPath)) { - for (name in Files.getFlatDirectory(ceramicAssetsPath)) { - if (!names.exists(name)) { - assets.push(new tools.Asset(name, ceramicAssetsPath)); + // Add ceramic default assets (if not overrided by project or plugins assets) + if (FileSystem.exists(ceramicAssetsPath)) { + for (name in Files.getFlatDirectory(ceramicAssetsPath)) { + if (!names.exists(name)) { + if (regex == null || regex.match(name)) { + assets.push(new tools.Asset(name, ceramicAssetsPath)); + } + } } } } + else { + // In other situations, we are explicitly processing assets from and to specific paths + } - print('Update project assets'); - - // Transform/copy assets - var transformedAssets = context.backend.transformAssets( - cwd, - assets, - target, - context.variant, - listOnly, - toPath - ); + // Transform assets with high level transformers + if (!FileSystem.exists(transformedAssetsPath)) { + FileSystem.createDirectory(transformedAssetsPath); + } + var transformedAssets = assets; + for (transformer in context.assetsTransformers) { + transformedAssets = transformer.transform(transformedAssets, transformedAssetsPath, changedPaths); + } - if (transformedAssets.length > 0) { + if (!noBackendTransform) { + // Transform/copy assets with backend + transformedAssets = context.backend.transformAssets( + cwd, + transformedAssets, + target, + context.variant, + listOnly, + toPath + ); + } - var dstAssetsPath = transformedAssets[0].rootDirectory; + if (isProjectAssets && transformedAssets.length > 0 && dstAssetsPath != null) { // Add _assets.json listing // @@ -196,7 +267,7 @@ class Assets extends tools.Task { } } - if (context.assetsChanged || context.iconsChanged) { + if (isProjectAssets && context.backend != null && (context.assetsChanged || context.iconsChanged)) { // Invalidate project files last modified times because assets or icons have changed // in order to ensure build will be reprocessed again var outPath = target.outPath(context.backend.name, cwd, context.debug, context.variant); @@ -210,6 +281,10 @@ class Assets extends tools.Task { } } + if (listChanged) { + print(Json.stringify(changedPaths)); + } + } } \ No newline at end of file diff --git a/tools/src/tools/tasks/TmpDir.hx b/tools/src/tools/tasks/TmpDir.hx new file mode 100644 index 000000000..096dd5cf9 --- /dev/null +++ b/tools/src/tools/tasks/TmpDir.hx @@ -0,0 +1,19 @@ +package tools.tasks; + +import tools.Helpers.*; + +class TmpDir extends tools.Task { + + override public function info(cwd:String):String { + + return "Create and return a temporary directory"; + + } + + override function run(cwd:String, args:Array):Void { + + print(TempDirectory.tempDir('ceramic')); + + } + +}