diff --git a/assets b/assets index aa81b5155..d9ebd43f6 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit aa81b515516001df5a8ec4c8d6f86e73a6f99a09 +Subproject commit d9ebd43f6b3c8fa00efd6bde482d119a8677e4a7 diff --git a/source/funkin/ui/debug/stageeditor/StageEditorObject.hx b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx index ef96c719f..47c69f407 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorObject.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorObject.hx @@ -1,19 +1,22 @@ package funkin.ui.debug.stageeditor; #if FEATURE_STAGE_EDITOR +import flixel.graphics.frames.FlxFramesCollection; import funkin.data.animation.AnimationData; -import funkin.graphics.FunkinSprite; +import funkin.play.stage.Bopper; +import funkin.util.assets.FlxAnimationUtil; import funkin.graphics.shaders.InverseDotsShader; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorAssetFile; /** * Contains all the Logic needed for Stage Editor. Only for Stage Editor, as in the gameplay StageProps and Boppers will be used. */ -class StageEditorObject extends FunkinSprite +class StageEditorObject extends Bopper { /** - * The internal Name of the Object. + * The files used on this asset that will be saved later. */ - public var name:String = "Unnamed"; + public var usedFiles:Array = []; public var selectedShader:InverseDotsShader; @@ -22,7 +25,10 @@ class StageEditorObject extends FunkinSprite */ public var startingAnimation:String = ""; - public var animDatas:Map = []; + /** + * Stored list of animation data for easier exports. + */ + public var animationDatas:Map = []; override public function new() { @@ -32,6 +38,26 @@ class StageEditorObject extends FunkinSprite shader = selectedShader; } + override function set_frames(frames:FlxFramesCollection) + { + // Remove files associated with this sprite when resetting its frames. + usedFiles.resize(0); + + // Since animations get reset when changing frames, we also have to reset the animation data stored. + animationDatas.clear(); + startingAnimation = ''; + + return super.set_frames(frames); + } + + override function destroy() + { + // Destroy the frame collection to prevent multiple texture atlas objects using the same frame collection instance. + this.frames.destroy(); + + super.destroy(); + } + /** * Whether the Object is currently being modified in the Stage Editor. */ @@ -41,85 +67,32 @@ class StageEditorObject extends FunkinSprite { this.isDebugged = value; - if (value == false) // plays upon starting yippee!!! - playAnim(startingAnimation, true); + if (!value) + { + update_shouldAlternate(); // Update the alternating animation now in case the animation list was changed. + playAnimation(startingAnimation, true); + } else { - if (animation.curAnim != null) - { - animation.stop(); - offset.set(); - updateHitbox(); - } + if (animation.curAnim != null) animation.stop(); } return value; } - public function playAnim(name:String, restart:Bool = false, reversed:Bool = false):Void + public function addAnimation(data:AnimationData) { - if (!animation.getNameList().contains(name)) return; - - animation.play(name, restart, reversed, 0); - - if (animDatas.exists(name)) offset.set(animDatas[name].offsets[0], animDatas[name].offsets[1]); + if (this.isAnimate) + { + FlxAnimationUtil.addTextureAtlasAnimation(this, data); + } else - offset.set(); - } - - /** - * On which beat should it dance? - */ - public var danceEvery:Float = 0; - - /** - * Internal, handles danceLeft and danceRight. - */ - var _danced:Bool = true; - - public function dance(restart:Bool = false):Void - { - if (isDebugged) return; - - var idle = animation.getNameList().contains("idle"); - var dancing = animation.getNameList().contains("danceLeft") && animation.getNameList().contains("danceRight"); - - if (!idle && !dancing) return; - - if (dancing) { - if (_danced) playAnim("danceRight", restart); - else - playAnim("danceLeft", restart); - - _danced = !_danced; + FlxAnimationUtil.addAtlasAnimation(this, data); } - else if (idle) - { - playAnim("idle", restart); - } - } - public function addAnim(name:String, prefix:String, offsets:Array, indices:Array, frameRate:Int = 24, looped:Bool = true, flipX:Bool = false, - flipY:Bool = false) - { - if (indices.length > 0) animation.addByIndices(name, prefix, indices, "", frameRate, looped, flipX, flipY); - else - animation.addByPrefix(name, prefix, frameRate, looped, flipX, flipY); - - if (animation.getNameList().contains(name)) // sometimes the animation doesnt add - { - animDatas.set(name, { - name: name, - prefix: prefix, - offsets: offsets, - looped: looped, - frameRate: frameRate, - flipX: flipX, - flipY: flipY, - frameIndices: indices - }); - } + setAnimationOffsets(data.name, data.offsets[0], data.offsets[1]); + animationDatas.set(data.name, data); } } #end diff --git a/source/funkin/ui/debug/stageeditor/StageEditorState.hx b/source/funkin/ui/debug/stageeditor/StageEditorState.hx index eb72a1eac..743f7a6c6 100644 --- a/source/funkin/ui/debug/stageeditor/StageEditorState.hx +++ b/source/funkin/ui/debug/stageeditor/StageEditorState.hx @@ -12,6 +12,7 @@ import flixel.FlxSprite; import flixel.util.FlxColor; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.addons.display.FlxGridOverlay; +import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.BaseCharacter; import funkin.play.character.BaseCharacter.CharacterType; import funkin.data.character.CharacterData.CharacterDataParser; @@ -51,6 +52,7 @@ import funkin.util.logging.CrashHandler; import funkin.graphics.shaders.Grayscale; import funkin.data.stage.StageRegistry; import funkin.graphics.FunkinCamera; +import haxe.io.Bytes; /** * Da Stage Editor woo!! @@ -77,7 +79,6 @@ class StageEditorState extends UIState public static final MAX_Z_INDEX:Int = 10000; public static final CHARACTER_COLORS:Array = [FlxColor.RED, FlxColor.PURPLE, FlxColor.CYAN]; // FCUK IVE TURNED INTO AN AMERICAN public static final TIME_BEFORE_ANIM_STOP:Float = 3.0; - public static var instance:StageEditorState = null; // unused lol // the other shit:tm: var menubar:MenuBar; @@ -227,7 +228,22 @@ class StageEditorState extends UIState public var charGroups:Map> = []; public var charCamOffsets:Map> = DEFAULT_CAMERA_OFFSETS.copy(); public var charPos:Map> = DEFAULT_POSITIONS.copy(); - public var bitmaps:Map = []; // used for optimizing the file size!!! + + /** + * An array of files used by the sprites. + */ + public var allFiles(get, never):Array; + + function get_allFiles() + { + var result:Array = []; + for (sprite in spriteArray) + { + result = result.concat(sprite.usedFiles); + } + + return result.distinct(); + } var charDeselectShader:Grayscale = new Grayscale(); var floorLines:Array = []; @@ -280,12 +296,9 @@ class StageEditorState extends UIState override public function create():Void { WindowManager.instance.reset(); - instance = this; FlxG.sound.music?.stop(); WindowUtil.setWindowTitle("Friday Night Funkin\' Stage Editor"); - AssetDataHandler.init(this); - camGame = new FunkinCamera(); camHUD = new FlxCamera(); camHUD.bgColor.alpha = 0; @@ -420,7 +433,7 @@ class StageEditorState extends UIState { if (!allowInput || welcomeDialog != null) return; - var data = BitmapData.fromFile(path); + var data:Bytes = FileUtil.readBytesFromPath(path); if (data != null) { @@ -534,22 +547,23 @@ class StageEditorState extends UIState { if (testingMode) { - if (conductorInUse.currentBeat % 2 == 0) - { - for (char in getCharacters()) char?.dance(true); - } - - for (asset in spriteArray) - { - if (asset.danceEvery > 0 && conductorInUse.currentBeat % asset.danceEvery == 0) asset.dance(true); - } - if (conductorInUse.currentBeat % 8 == 0 && !FlxG.keys.pressed.SHIFT) curTestChar++; } return super.beatHit(); } + override public function dispatchEvent(event:funkin.modding.events.ScriptEvent) + { + super.dispatchEvent(event); + + // Dispatch the Conductor events to the stage props and characters to make them dance on beat in the testing mode. + if (!testingMode || !(event is funkin.modding.events.ScriptEvent.SongTimeScriptEvent)) return; + + for (prop in spriteArray) ScriptEventDispatcher.callEvent(prop, event); + for (char in getCharacters()) ScriptEventDispatcher.callEvent(char, event); + } + override public function update(elapsed:Float):Void { // Save the stage if exiting through the F4 keybind, as it moves you to the Main Menu. @@ -1508,25 +1522,27 @@ class StageEditorState extends UIState case 'copy object': if (selectedSprite == null) return; - copiedSprite = selectedSprite.toData(true); + copiedSprite = selectedSprite.toData(); case 'paste object': if (copiedSprite == null) return; saved = false; - var spr = new StageEditorObject().fromData(copiedSprite); - var objNames = [for (a in spriteArray) a.name]; - - if (objNames.contains(spr.name)) + // Change the sprite name here instead of later since the texture atlas frame collections are saved by the prop name. + var dataCopy:Dynamic = Reflect.copy(copiedSprite); + var objNames:Array = [for (a in spriteArray) a.name]; + if (objNames.contains(dataCopy.name)) { var i = 1; - while (objNames.contains(spr.name + ' (' + i + ')')) + while (objNames.contains(dataCopy.name + ' (' + i + ')')) i++; - spr.name += ' (' + i + ')'; + dataCopy.name += ' (' + i + ')'; } + var spr:StageEditorObject = new StageEditorObject().fromData(dataCopy); + add(spr); selectedSprite = spr; updateArray(); @@ -1598,48 +1614,30 @@ class StageEditorState extends UIState undoArray = []; redoArray = []; updateArray(); - removeUnusedBitmaps(); + allFiles.resize(0); } - public function removeUnusedBitmaps() + /** + * Get a stored file based on the name or the data. + */ + public function getFile(?name:String, ?data:Bytes) { - var usedBitmaps:Array = []; - - for (asset in spriteArray) + for (file in allFiles) { - var data = asset.toData(false); - if (data.assetPath.startsWith('#')) continue; // the simple graphics - - usedBitmaps.push(data.assetPath); + if (data != null && file.data == data) return file; + if (name != null && file.name == name) return file; } - for (name => bit in bitmaps) - { - if (usedBitmaps.contains(name)) continue; - bitmaps.remove(name); - } + return null; } - public function addBitmap(newBitmap:BitmapData, ?name:String):String + /** + * Creates a file to be used for the assets. + */ + public function createFile(name:String, data:Bytes) { - // first we check for existing bitmaps so we dont like add an extra one - for (name => bitmap in bitmaps) - { - if (bitmap == newBitmap) return name; - } - - if (name != null && !bitmaps.exists(name)) - { - bitmaps.set(name, newBitmap); - return name; - } - - var id:Int = 0; - while (bitmaps.exists('image' + id)) - id++; - - bitmaps.set('image' + id, newBitmap); - return 'image' + id; + if (getFile(name, data) != null) return getFile(name, data); + return {name: name, data: data}; } override function destroy():Void @@ -1745,3 +1743,16 @@ typedef StageEditorParams = */ var ?targetDadChar:String; }; + +typedef StageEditorAssetFile = +{ + /** + * The name of the file. + */ + var name:String; + + /** + * The content of the file, decoded into bytes. + */ + var data:Bytes; +} diff --git a/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx b/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx index 9a742f008..cec4332e6 100644 --- a/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx +++ b/source/funkin/ui/debug/stageeditor/components/NewObjDialog.hx @@ -3,7 +3,9 @@ package funkin.ui.debug.stageeditor.components; #if FEATURE_STAGE_EDITOR import haxe.ui.containers.dialogs.Dialog; import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorAssetFile; import openfl.display.BitmapData; +import haxe.io.Bytes; import haxe.ui.notifications.NotificationType; import haxe.ui.notifications.NotificationManager; @@ -13,14 +15,14 @@ class NewObjDialog extends Dialog public var bitmapName:Null = null; var stageEditorState:StageEditorState; - var bitmap:BitmapData; + var data:Bytes; - override public function new(state:StageEditorState, img:BitmapData = null) + override public function new(state:StageEditorState, data:Bytes = null) { super(); stageEditorState = state; - bitmap = img; + this.data = data; field.onChange = function(_) { @@ -39,8 +41,8 @@ class NewObjDialog extends Dialog if (button == '{{Create}}') { - var objNames = [ - for (obj in StageEditorState.instance.spriteArray) obj.name + var objNames:Array = [ + for (obj in stageEditorState.spriteArray) obj.name ]; if (field.text == '' || field.text == null || objNames.contains(field.text)) @@ -57,10 +59,12 @@ class NewObjDialog extends Dialog { var spr = new StageEditorObject(); - if (bitmap != null) + if (data != null) { - var bitToLoad = stageEditorState.addBitmap(bitmap, bitmapName); - spr.loadGraphic(stageEditorState.bitmaps[bitToLoad]); + var file:StageEditorAssetFile = stageEditorState.createFile(bitmapName, data); + spr.loadGraphic(BitmapData.fromBytes(file.data)); + + spr.usedFiles.push(file); } else spr.loadGraphic(AssetDataHandler.getDefaultGraphic()); @@ -68,7 +72,7 @@ class NewObjDialog extends Dialog spr.name = field.text; spr.screenCenter(); - var sprArray = stageEditorState.spriteArray; + var sprArray:Array = stageEditorState.spriteArray; spr.zIndex = sprArray.length == 0 ? 0 : (sprArray[sprArray.length - 1].zIndex + 1); stageEditorState.selectedSprite = spr; diff --git a/source/funkin/ui/debug/stageeditor/components/TextureAtlasSettingsDialog.hx b/source/funkin/ui/debug/stageeditor/components/TextureAtlasSettingsDialog.hx new file mode 100644 index 000000000..ce439b243 --- /dev/null +++ b/source/funkin/ui/debug/stageeditor/components/TextureAtlasSettingsDialog.hx @@ -0,0 +1,109 @@ +package funkin.ui.debug.stageeditor.components; + +#if FEATURE_STAGE_EDITOR +import animate.FlxAnimateFrames; +import funkin.util.FileUtil; +import funkin.util.SerializerUtil; +import haxe.io.Bytes; +import haxe.io.Path; +import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.components.CheckBox; +import haxe.ui.components.DropDown; +import openfl.display.BitmapData; + +@:build(haxe.ui.macros.ComponentMacros.build('assets/exclude/data/ui/stage-editor/dialogs/texture-atlas-settings.xml')) +class TextureAtlasSettingsDialog extends Dialog +{ + var atlasPath:String; + var stageEditorState:StageEditorState; + var taSwfMode:CheckBox; + var taCacheOnLoad:CheckBox; + var taFilterQuality:DropDown; + var taApplyStageMatrix:CheckBox; + var taUseRenderTexture:CheckBox; + + override public function new(state:StageEditorState, atlasPath:String) + { + super(); + + stageEditorState = state; + this.atlasPath = atlasPath; + + buttons = DialogButton.CANCEL | DialogButton.APPLY; + + destroyOnClose = true; + } + + override public function validateDialog(button:DialogButton, fn:Bool->Void) + { + var done:Bool = true; + + if (button == '{{apply}}') + { + var linkedObj:StageEditorObject = stageEditorState.selectedSprite; + var files:Array = FileUtil.readDir(atlasPath); + + var baseAtlasPath:String = Path.withoutDirectory(atlasPath); + var animJson:String = FileUtil.readStringFromPath(Path.join([atlasPath, 'Animation.json'])); + var spritemapFiles:Array = []; + + // Texture atlases could have multiple spritemaps so we have to locate and load them all. + var i:Int = 1; + while (files.containsAllExact(['spritemap$i.png', 'spritemap$i.json'])) + { + var joinedPath:String = Path.join([baseAtlasPath, 'spritemap$i']); + spritemapFiles.push({name: '$joinedPath.png', data: FileUtil.readBytesFromPath(Path.join([atlasPath, 'spritemap$i.png']))}); + spritemapFiles.push({name: '$joinedPath.json', data: FileUtil.readBytesFromPath(Path.join([atlasPath, 'spritemap$i.json']))}); + i++; + } + + @:privateAccess + var spritemaps:Array = [ + for (i in 0...Std.int(spritemapFiles.length / 2)) { + source: BitmapData.fromBytes(spritemapFiles[i * 2].data), + json: SerializerUtil.sanitizeJSON(spritemapFiles[i * 2 + 1].data.toString()) + } + ]; + + linkedObj.applyStageMatrix = taApplyStageMatrix.selected; + linkedObj.useRenderTexture = taUseRenderTexture.selected; + + var settings:FlxAnimateSettings = { + swfMode: taSwfMode.selected, + cacheOnLoad: taCacheOnLoad.selected, + filterQuality: cast taFilterQuality.selectedIndex + }; + + linkedObj.frames = FlxAnimateFrames.fromAnimate(animJson, spritemaps, null, linkedObj.name, true, settings); + + @:privateAccess + cast(linkedObj.frames, FlxAnimateFrames)._settings = settings; // Settings are temporary, but we need to save them for exporting. + + // Unlike sparrow/packer assets, whose spritesheets can be loaded even when there's no animations attached to the sprite, + // texture atlas objects need to have at least one animation before they could properly load, as boppers (animated assets) + // are only created if an object's `danceEvery` isn't zero or the object has animations. + var baseSymbol:String = cast(linkedObj.frames, FlxAnimateFrames).timeline.libraryItem.name; + linkedObj.addAnimation({ + name: 'Idle', + prefix: baseSymbol, + animType: 'symbol', + offsets: [0, 0], + frameIndices: [], + looped: false, + flipX: false, + flipY: false, + frameRate: Std.int(linkedObj.anim.getDefaultFramerate()) + }); + linkedObj.startingAnimation = 'Idle'; + + linkedObj.usedFiles.push({name: Path.join([baseAtlasPath, 'Animation.json']), data: Bytes.ofString(animJson)}); + linkedObj.usedFiles = linkedObj.usedFiles.concat(spritemapFiles); + + stageEditorState.notifyChange('Texture Atlas Loading Done', 'Finished loading the Texture Atlas for the object ${linkedObj.name}.'); + stageEditorState.updateDialog(OBJECT_ANIMS); + } + + fn(done); + } +} +#end diff --git a/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx index 54d165264..095b4b371 100644 --- a/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx +++ b/source/funkin/ui/debug/stageeditor/handlers/AssetDataHandler.hx @@ -1,12 +1,14 @@ package funkin.ui.debug.stageeditor.handlers; #if FEATURE_STAGE_EDITOR +import animate.FlxAnimateFrames; import flixel.graphics.frames.FlxAtlasFrames; -import openfl.display.BitmapData; -import flixel.FlxSprite; import flixel.util.FlxColor; -import openfl.display.BlendMode; +import flixel.FlxSprite; import funkin.data.stage.StageData.StageDataProp; +import haxe.io.Path; +import openfl.display.BitmapData; +import openfl.display.BlendMode; using StringTools; @@ -15,24 +17,17 @@ using StringTools; */ class AssetDataHandler { - static var state:StageEditorState; - - public static function init(state:StageEditorState) - { - AssetDataHandler.state = state; - } - /** * Turns an Object into Data. * @param obj the Object whose data to read. * @param useBitmaps Whether to Save object's BitmapData directly. * @return Data of the Object */ - public static function toData(obj:StageEditorObject, useBitmaps:Bool = false):StageEditorObjectData + public static function toData(obj:StageEditorObject):StageEditorObjectData { var outputData:StageEditorObjectData = { name: obj.name, - assetPath: "", + assetPath: '', position: [obj.x, obj.y], zIndex: obj.zIndex, isPixel: !obj.antialiasing, @@ -40,35 +35,46 @@ class AssetDataHandler alpha: obj.alpha, danceEvery: obj.animation.getNameList().length > 0 ? obj.danceEvery : 0, scroll: [obj.scrollFactor.x, obj.scrollFactor.y], - animations: [for (n => d in obj.animDatas) d], + animations: [ + for (n => d in obj.animationDatas) d + ], startingAnimation: obj.startingAnimation, - animType: "sparrow", // automatically making sparrow atlases yeah + animType: getAnimType(obj), angle: obj.angle, flipX: obj.flipX, flipY: obj.flipY, blend: obj.blend == null ? "" : Std.string(obj.blend), color: obj.color.toWebString(), - animData: "" + neededFiles: obj.usedFiles.clone() } - if (useBitmaps) + if (obj.usedFiles.length > 0) { - outputData.bitmap = obj.pixels.clone(); - outputData.animData = obj.generateXML(); - return outputData; - } - - for (name => bit in state.bitmaps) - { - if (areTheseBitmapsEqual(bit, obj.pixels)) + if (outputData.animType == 'animateatlas') { - outputData.assetPath = name; - outputData.animData = obj.generateXML(name); - return outputData; + outputData.assetPath = Path.directory(obj.usedFiles[0].name); + + // Additionally set the atlas settings here. + var frames:FlxAnimateFrames = cast obj.frames; + + @:privateAccess + outputData.atlasSettings = { + useRenderTexture: obj.useRenderTexture, + applyStageMatrix: obj.applyStageMatrix, + swfMode: frames._settings?.swfMode ?? false, + cacheOnLoad: frames._settings?.cacheOnLoad ?? false, + filterQuality: frames._settings?.filterQuality ?? animate.FlxAnimateFrames.FilterQuality.MEDIUM + } + } + else + { + outputData.assetPath = Path.withoutExtension(obj.usedFiles[0].name); } } - - outputData.assetPath = "#FFFFFF"; + else // Solid color + { + outputData.assetPath = obj.color.toWebString(); + } return outputData; } @@ -80,44 +86,68 @@ class AssetDataHandler */ public static function fromData(object:StageEditorObject, data:StageEditorObjectData) { - if (data.bitmap != null) + if (data.animations != null && data.animations.length > 0) { - if (data.animations != null && data.animations.length > 0) + switch (data.animType) { - var bitToLoad = state.addBitmap(data.bitmap.clone(), data.name); - object.frames = FlxAtlasFrames.fromSparrow(state.bitmaps[bitToLoad], data.animData); - } - else if (areTheseBitmapsEqual(data.bitmap, getDefaultGraphic())) - { - object.loadGraphic(getDefaultGraphic()); - } - else - { - var bitToLoad = state.addBitmap(data.bitmap.clone(), data.name); - object.loadGraphic(state.bitmaps[bitToLoad]); + case 'sparrow': + var spritesheet:Null = data.neededFiles.find(f -> f.name.endsWith('.png')); + var frameData:Null = data.neededFiles.find(f -> f.name.endsWith('.xml')); + + if (spritesheet != null && frameData != null) + { + object.frames = FlxAtlasFrames.fromSparrow(BitmapData.fromBytes(spritesheet.data), frameData.data.toString()); + } + + case 'packer': + var spritesheet:Null = data.neededFiles.find(f -> f.name.endsWith('.png')); + var frameData:Null = data.neededFiles.find(f -> f.name.endsWith('.txt')); + + if (spritesheet != null && frameData != null) + { + object.frames = FlxAtlasFrames.fromSpriteSheetPacker(BitmapData.fromBytes(spritesheet.data), frameData.data.toString()); + } + + case 'animateatlas': + var animateJson:String = data.neededFiles.find(f -> f.name.endsWith('/Animation.json'))?.data?.toString() ?? ''; + var spritemaps:Array = []; + + var totalSpritemaps:Int = Std.int((data.neededFiles.length - 1) / 2); + for (i in 0...totalSpritemaps) + { + var name:String = '/spritemap${i + 1}'; + spritemaps.push({ + source: BitmapData.fromBytes(data.neededFiles.find(f -> f.name.endsWith('$name.png'))?.data), + json: data.neededFiles.find(f -> f.name.endsWith('$name.json'))?.data?.toString() ?? '' + }); + } + + object.applyStageMatrix = data.atlasSettings?.applyStageMatrix ?? false; + object.useRenderTexture = data.atlasSettings?.useRenderTexture ?? false; + + var settings:FlxAnimateSettings = { + swfMode: data.atlasSettings?.swfMode ?? false, + cacheOnLoad: data.atlasSettings?.cacheOnLoad ?? false, + filterQuality: cast data.atlasSettings?.filterQuality ?? animate.FlxAnimateFrames.FilterQuality.MEDIUM + }; + + object.frames = FlxAnimateFrames.fromAnimate(animateJson, spritemaps, null, data.name, true, settings); + + @:privateAccess + cast(object.frames, FlxAnimateFrames)._settings = settings; // Settings are temporary, but we need to save them for exporting. + default: + // Do nothing. } } + else if (data.assetPath.startsWith("#")) + { + object.loadGraphic(getDefaultGraphic()); + object.color = FlxColor.fromString(data.assetPath); + } else - { - if (data.animations != null && data.animations.length > 0) // considering we're unpacking we might as well just do this instead of switch - { - if (data.animData.contains("")) - { - object.frames = FlxAtlasFrames.fromSparrow(state.bitmaps[data.assetPath].clone(), data.animData); - } - else - { - object.frames = FlxAtlasFrames.fromSpriteSheetPacker(state.bitmaps[data.assetPath].clone(), data.animData); - } - } - else if (data.assetPath.startsWith("#")) - { - object.loadGraphic(getDefaultGraphic()); - object.color = FlxColor.fromString(data.assetPath); - } - else - object.loadGraphic(state.bitmaps[data.assetPath].clone()); - } + object.loadGraphic(BitmapData.fromBytes(data.neededFiles[0].data)); + + object.usedFiles = data.neededFiles; object.name = data.name; object.setPosition(data.position[0], data.position[1]); @@ -126,18 +156,26 @@ class AssetDataHandler object.alpha = data.alpha; object.danceEvery = data.danceEvery; object.scrollFactor.set(data.scroll[0], data.scroll[1]); - object.startingAnimation = data.startingAnimation; + object.startingAnimation = data.startingAnimation ?? ""; object.angle = data.angle; object.blend = blendFromString(data.blend); if (!data.assetPath.startsWith("#")) object.color = FlxColor.fromString(data.color); for (anim in data.animations) { - object.addAnim(anim.name, anim.prefix, anim.offsets ?? [0, 0], anim.frameIndices ?? [], anim.frameRate ?? 24, anim.looped ?? false, anim.flipX ?? false, - anim.flipY ?? false); + object.addAnimation(anim); } - if (object.animation.getNameList().contains(data.startingAnimation)) object.startingAnimation = data.startingAnimation; + if (object.animation.getNameList().contains(data.startingAnimation)) + { + object.startingAnimation = data.startingAnimation; + object.playAnimation(object.startingAnimation); + + flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() + { + if (object?.animation?.curAnim != null) object.animation.stop(); + }); + } switch (data.scale) { @@ -149,16 +187,25 @@ class AssetDataHandler } object.updateHitbox(); - object.playAnim(object.startingAnimation); - - flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() - { - if (object?.animation?.curAnim != null) object.animation.stop(); - }); - return object; } + /** + * Get the animation type of an object depending on the frames and animation count. + */ + public static function getAnimType(object:StageEditorObject) + { + if (object.isAnimate) return 'animateatlas'; + + // For packer we check every file for the object to see if it has a .txt file. + for (file in object.usedFiles) + { + if (file.name.endsWith(".txt")) return 'packer'; + } + + return 'sparrow'; // Sparrow is the default value, even for static sprites. + } + /** * Returns a default BitmapData to be used for all the props. * @return BitmapData @@ -175,7 +222,6 @@ class AssetDataHandler */ public static function blendFromString(blend:String):BlendMode { - // originally this was a MASSIVE and I do mean MASSIVE switch case, though then I found out that blendmode already has one implemented @:privateAccess return BlendMode.fromString(blend.toLowerCase().trim()); } @@ -199,32 +245,11 @@ class AssetDataHandler xml += ""; return xml; } - - // I am aware OpenFL has it's own compare bitmap function, though I find this to be better ngl - - static function areTheseBitmapsEqual(bitmap1:BitmapData, bitmap2:BitmapData) - { - if (bitmap1.width != bitmap2.width || bitmap1.height != bitmap2.height) return false; - - var bytes1 = bitmap1.image.data; - var bytes2 = bitmap2.image.data; - - for (i in 0...bytes1.length) - { - if (bytes1[i] != bytes2[i]) - { - return false; - } - } - - return true; - } } typedef StageEditorObjectData = { > StageDataProp, - var animData:String; - var ?bitmap:BitmapData; + var neededFiles:Array; } #end diff --git a/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx index 7497b069c..245efe89d 100644 --- a/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx +++ b/source/funkin/ui/debug/stageeditor/handlers/StageDataHandler.hx @@ -10,8 +10,10 @@ import funkin.play.character.BaseCharacter; import funkin.data.stage.StageData; import funkin.data.stage.StageData.StageDataCharacter; import funkin.data.stage.StageRegistry; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorAssetFile; import openfl.utils.Assets as OpenFLAssets; import lime.utils.Assets as LimeAssets; +import haxe.io.Path; using StringTools; @@ -28,11 +30,9 @@ class StageDataHandler endData.directory = state.stageFolder; // step 1 phase 1: object data - var xmlMap:Map = []; - for (obj in state.spriteArray) { - var data = obj.toData(false); + var data = obj.toData(); endData.props.push({ name: data.name, assetPath: data.assetPath.startsWith("#") ? data.color : data.assetPath, @@ -50,10 +50,9 @@ class StageDataHandler flipY: data.flipY, angle: data.angle, blend: data.blend, - color: data.assetPath.startsWith("#") ? "#FFFFFF" : data.color + color: data.assetPath.startsWith("#") ? "#FFFFFF" : data.color, + atlasSettings: data.atlasSettings }); - - if (!xmlMap.exists(data.assetPath) && data.animData != "") xmlMap.set(data.assetPath, data.animData); } // step 1 phase 2: character data @@ -99,45 +98,23 @@ class StageDataHandler // step 2: saving everything to entryList var entryList = new Array(); - // step 2 phase 1: images - state.removeUnusedBitmaps(); - for (name => img in state.bitmaps) + // step 2 phase 1: assets + for (asset in state.allFiles) { - var bytes = img?.image?.encode(PNG); - if (bytes == null) continue; - var entry:Entry = { - fileName: name + ".png", - fileSize: bytes.length, + fileName: asset.name, + fileSize: asset.data.length, fileTime: Date.now(), compressed: false, - dataSize: bytes.length, - data: bytes, - crc32: null // apparently fileutil.hx does not like crc32, idk why but i dont even know what crc32 is - } - - entryList.push(entry); - } - - // step 2 phase 2: xmls - for (path => xml in xmlMap) - { - var bytes = Bytes.ofString(xml); - - var entry:Entry = { - fileName: path + ".xml", - fileSize: bytes.length, - fileTime: Date.now(), - compressed: false, - dataSize: bytes.length, - data: bytes, + dataSize: asset.data.length, + data: asset.data, crc32: null } entryList.push(entry); } - // step 2 phase 3: the main data + // step 2 phase 2: the main data var stageBytes = Bytes.ofString(endData.serialize()); entryList.push({ fileName: formatStageId(endData.name) + ".json", @@ -156,28 +133,24 @@ class StageDataHandler public static function unpackShitFromZip(state:StageEditorState, zip:Bytes) { state.clearAssets(); - state.bitmaps.clear(); - var entries = FileUtil.readZIPFromBytes(zip); - var stageData:StageData = new StageData(); - - var xmls:Map = []; + var entries:Array = FileUtil.readZIPFromBytes(zip); + var stageData:Null = null; + var allFiles:Array = []; for (stuff in entries) { - var ext = stuff.fileName.split(".")[1]; + var ext:String = stuff.fileName.split(".")[1]; - switch (ext) + // A json file can either be a texture atlas file or the stage data. + // Texture atlas files are found in a folder with other atlas assets, so the stage data doesn't have a slash in the name + if (ext == "json" && !stuff.fileName.contains("/")) { - case "png": - var data = BitmapData.fromBytes(stuff.data); - state.bitmaps.set(stuff.fileName.replace(".png", ""), data); - - case "xml": - xmls.set(stuff.fileName.replace(".xml", ""), stuff.data.toString()); - - case "json": - stageData = StageRegistry.instance.parseEntryDataRaw(stuff.data.toString(), stuff.fileName); + stageData = StageRegistry.instance.parseEntryDataRaw(stuff.data.toString(), stuff.fileName); + } + else + { + allFiles.push({name: stuff.fileName, data: stuff.data}); } } @@ -199,12 +172,14 @@ class StageDataHandler // objects for (objData in stageData.props) { - // make the data and roll with it + var assets:Array = allFiles.filter((f) -> Path.withoutExtension(f.name) == objData.assetPath); + var spr = new StageEditorObject(); spr.fromData({ name: objData.name ?? "Unnamed", assetPath: objData.assetPath, animations: objData.animations.copy(), + animType: objData.animType, scale: objData.scale, position: objData.position, alpha: objData.alpha, @@ -218,7 +193,8 @@ class StageDataHandler flipX: objData.flipX, flipY: objData.flipY, startingAnimation: objData.startingAnimation, - animData: xmls[objData.assetPath] ?? "" + neededFiles: assets, + atlasSettings: objData.atlasSettings }); state.add(spr); @@ -271,15 +247,8 @@ class StageDataHandler public static function loadFromDataRaw(state:StageEditorState, data:StageData) { state.clearAssets(); - state.bitmaps.clear(); - - if (data == null) - { - loadDummyData(state); - return; - } @:privateAccess - if (!LimeAssets.libraryPaths.exists(data.directory)) + if (data == null || !LimeAssets.libraryPaths.exists(data.directory)) { loadDummyData(state); return; @@ -305,16 +274,39 @@ class StageDataHandler for (objData in data.props) { var spr = new StageEditorObject(); - if (!objData.assetPath.startsWith("#")) state.bitmaps.set(objData.assetPath, Assets.getBitmapData(Paths.image(objData.assetPath))); + var neededFiles:Array = []; - var usePacker:Bool = objData.animType == "packer"; - var animPath:String = Paths.file("images/" + objData.assetPath + (usePacker ? ".txt" : ".xml")); - var animText:String = Assets.exists(animPath) ? Assets.getText(animPath) : ""; + // Add all assets related to the object to the allFiles array. + if (!objData.assetPath.startsWith("#")) + { + if (objData.animType == "animateatlas") + { + var checkFor:String = Paths.stripLibrary(Paths.animateAtlas(objData.assetPath, state.stageFolder)); + for (file in Assets.list()) + { + if (!file.startsWith(checkFor)) continue; + + var validName:String = objData.assetPath + file.substring(checkFor.length); + neededFiles.push(state.createFile(validName, Assets.getBytes('${state.stageFolder}:$file'))); + } + } + else + { + neededFiles.push(state.createFile('${objData.assetPath}.png', Assets.getBytes(Paths.image(objData.assetPath)))); + + var animFile:String = '${objData.assetPath}${objData.animType == "packer" ? ".txt" : ".xml"}'; + if (Assets.exists(Paths.file('images/$animFile'))) + { + neededFiles.push(state.createFile(animFile, Assets.getBytes(Paths.file('images/$animFile')))); + } + } + } spr.fromData({ name: objData.name ?? "Unnamed", assetPath: objData.assetPath, animations: objData.animations.copy(), + animType: objData.animType, scale: objData.scale, position: objData.position, alpha: objData.alpha, @@ -328,13 +320,14 @@ class StageDataHandler flipX: objData.flipX, flipY: objData.flipY, startingAnimation: objData.startingAnimation, - animData: animText + neededFiles: neededFiles, + atlasSettings: objData.atlasSettings }); state.add(spr); + state.updateArray(); } - state.updateArray(); state.updateMarkerPos(); } diff --git a/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx index c71cbe2e3..6f7773a07 100644 --- a/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx +++ b/source/funkin/ui/debug/stageeditor/handlers/UndoRedoHandler.hx @@ -138,7 +138,7 @@ class UndoRedoHandler case OBJECT_DELETED: finalAction.data = { ID: state.selectedSprite.ID, - data: state.selectedSprite.toData(true) + data: state.selectedSprite.toData() } case OBJECT_ROTATED: diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectAnimsToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectAnimsToolbox.hx index 4eb2b497f..589459965 100644 --- a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectAnimsToolbox.hx +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectAnimsToolbox.hx @@ -1,6 +1,7 @@ package funkin.ui.debug.stageeditor.toolboxes; #if FEATURE_STAGE_EDITOR +import animate.FlxAnimateFrames; import haxe.ui.components.Button; import haxe.ui.components.CheckBox; import haxe.ui.components.DropDown; @@ -40,11 +41,18 @@ class StageEditorObjectAnimsToolbox extends StageEditorDefaultToolbox { if (objFrameList.selectedIndex == -1) return; objAnimPrefix.text = objFrameList.selectedItem.name; + + // Remove the indicators from the prefix text if the selected item is from an atlas sprite. + if (objFrameList.selectedItem.isLabel != null) + { + var isLabel:Bool = objFrameList.selectedItem.isLabel ?? false; + objAnimPrefix.text = objAnimPrefix.text.replace(' ${isLabel ? "(Label)" : "(Symbol)"}', ''); + } } objAnims.onChange = function(_) { - var animData = linkedObj?.animDatas[ + var animData = linkedObj?.animationDatas[ objAnims.selectedItem?.text ?? "" ]; @@ -104,8 +112,7 @@ class StageEditorObjectAnimsToolbox extends StageEditorDefaultToolbox if (linkedObj.startingAnimation == daAnim) linkedObj.startingAnimation = ""; linkedObj.animation.remove(daAnim); - linkedObj.animDatas.remove(daAnim); - linkedObj.offset.set(); + linkedObj.animationDatas.remove(daAnim); state.notifyChange("Animation Deletion Done", "Animation " + objAnims.selectedItem.text @@ -143,13 +150,30 @@ class StageEditorObjectAnimsToolbox extends StageEditorDefaultToolbox } // Otherwise, update them accordingly. - - if (previousFrames !=[ - for (f in linkedObj.frames.frames) f.name - ]) updateFrameList(); + if (previousFrames != getObjectFrameList()) updateFrameList(); if (previousAnims != linkedObj.animation.getNameList().copy()) updateAnimList(); } + /** + * Get a formatted list of frames for the object. + */ + function getObjectFrameList():Array + { + if (linkedObj == null) return []; + + if (linkedObj.frames is FlxAnimateFrames) + { + var frames:FlxAnimateFrames = cast linkedObj.frames; + + @:privateAccess + return linkedObj.getFrameLabelList().concat(frames.dictionary.keys().array()); + } + + return[ + for (f in linkedObj.frames.frames) f.name + ]; + } + function updateFrameList() { previousFrames = []; @@ -157,11 +181,30 @@ class StageEditorObjectAnimsToolbox extends StageEditorDefaultToolbox if (linkedObj == null) return; - for (fname in linkedObj.frames.frames) + if (linkedObj.frames is FlxAnimateFrames) { - if (fname != null) objFrameList.dataSource.add({name: fname.name}); + for (fname in linkedObj.getFrameLabelList()) + { + objFrameList.dataSource.add({name: '$fname (Label)', isLabel: true}); + previousFrames.push(fname); + } - previousFrames.push(fname.name); + var frames:FlxAnimateFrames = cast linkedObj.frames; + + @:privateAccess + for (fname in frames.dictionary.keys()) + { + objFrameList.dataSource.add({name: '$fname (Symbol)', isLabel: false}); + previousFrames.push(fname); + } + } + else + { + for (fname in linkedObj.frames.frames) + { + if (fname != null) objFrameList.dataSource.add({name: fname.name}); + previousFrames.push(fname.name); + } } } @@ -198,9 +241,24 @@ class StageEditorObjectAnimsToolbox extends StageEditorDefaultToolbox } var shouldDoIndices:Bool = (indices.length > 0 && !indices.contains(null)); + var isSymbol:Bool = false; - linkedObj.addAnim(objAnimName.text, objAnimPrefix.text, [objAnimOffsetX.pos, objAnimOffsetY.pos], (shouldDoIndices ? indices : []), - Std.int(objAnimFramerate.pos), objAnimLooped.selected, objAnimFlipX.selected, objAnimFlipY.selected); + if (linkedObj.frames is FlxAnimateFrames) + { + isSymbol = cast(linkedObj.frames, FlxAnimateFrames).existsSymbol(objAnimPrefix.text); + } + + linkedObj.addAnimation({ + name: objAnimName.text, + prefix: objAnimPrefix.text, + offsets: [objAnimOffsetX.pos, objAnimOffsetY.pos], + looped: objAnimLooped.selected, + frameIndices: (shouldDoIndices ? indices : []), + frameRate: Std.int(objAnimFramerate.pos), + flipX: objAnimFlipX.selected, + flipY: objAnimFlipY.selected, + animType: isSymbol ? 'symbol' : 'framelabel' + }); if (linkedObj.animation.getByName(objAnimName.text) == null) { @@ -209,7 +267,7 @@ class StageEditorObjectAnimsToolbox extends StageEditorDefaultToolbox } if (objAnimStart.selected) linkedObj.startingAnimation = objAnimName.text; - linkedObj.playAnim(objAnimName.text); + linkedObj.playAnimation(objAnimName.text); stageEditorState.notifyChange("Animation Saving Done", "Animation " + objAnimName.text + " has been saved to the Object " + linkedObj.name + "."); updateAnimList(); diff --git a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectGraphicToolbox.hx b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectGraphicToolbox.hx index bab545ecb..a5de618d7 100644 --- a/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectGraphicToolbox.hx +++ b/source/funkin/ui/debug/stageeditor/toolboxes/StageEditorObjectGraphicToolbox.hx @@ -3,13 +3,15 @@ package funkin.ui.debug.stageeditor.toolboxes; #if FEATURE_STAGE_EDITOR import flixel.graphics.frames.FlxAtlasFrames; import funkin.ui.debug.stageeditor.handlers.AssetDataHandler; +import funkin.ui.debug.stageeditor.StageEditorState.StageEditorAssetFile; +import funkin.ui.debug.stageeditor.components.TextureAtlasSettingsDialog; import funkin.util.FileUtil; +import haxe.io.Bytes; +import haxe.io.Path; import haxe.ui.components.Button; import haxe.ui.components.Image; import haxe.ui.components.NumberStepper; import haxe.ui.components.TextArea; -import haxe.ui.containers.dialogs.Dialogs.FileDialogTypes; -import haxe.ui.containers.dialogs.Dialogs; import haxe.ui.ToolkitAssets; import openfl.display.BitmapData; import haxe.ui.events.UIEvent; @@ -21,6 +23,7 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox var objImage:Image; var objLoad:Button; var objLoadNet:Button; + var objLoadTextureAtlas:Button; var objReset:Button; var objResetFrames:Button; var objFrameTxt:TextArea; @@ -50,16 +53,12 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox if (imageInfo == null) return; objImage.resource = imageInfo.data; - linkedObj.frame = imageInfo.data; - // This checks if the same image had already been loaded, so that we don't add it twice. - // Kind of hacky but it is what it is. - var name:String = haxe.io.Path.withoutExtension(selectedFile.name); - var bitToLoad:String = state.addBitmap(linkedObj.updateFramePixels(), name); - linkedObj.loadGraphic(state.bitmaps[bitToLoad]); + var file:StageEditorAssetFile = state.createFile(selectedFile.name, selectedFile.bytes); + linkedObj.loadGraphic(BitmapData.fromBytes(file.data)); linkedObj.updateHitbox(); - state.removeUnusedBitmaps(); + linkedObj.usedFiles.push(file); refresh(); objImageWidth.pos = objImageWidth.max; @@ -77,11 +76,11 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox state.createURLDialog(function(bytes:lime.utils.Bytes) { - var bitToLoad:String = state.addBitmap(BitmapData.fromBytes(bytes), linkedObj.name); - linkedObj.loadGraphic(state.bitmaps[bitToLoad]); + var file:StageEditorAssetFile = state.createFile('${linkedObj.name}.png', bytes); + linkedObj.loadGraphic(BitmapData.fromBytes(file.data)); linkedObj.updateHitbox(); - state.removeUnusedBitmaps(); + linkedObj.usedFiles.push(file); // Update the image preview. refresh(); @@ -90,6 +89,34 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox }); } + // Callback for loading a texture atlas. + objLoadTextureAtlas.onClick = function(_) + { + if (linkedObj == null) return; + + FileUtil.browseForDirectory("Open Exported Texture Atlas", function(path:String) + { + var files:Array = FileUtil.readDir(path); + + if (!files.containsAllExact(["Animation.json", "spritemap1.json", "spritemap1.png"])) + { + state.notifyChange('Texture Atlas Loading Error', 'The folder $path doesn\'t contain required assets for a Texture Atlas.', true); + return; + } + + if (files.contains('metadata.json')) + { + state.notifyChange('Texture Atlas Loading Error', 'The Texture Atlas from the folder $path doesn\'t have inlined assets.', true); + return; + } + + var dialog:TextureAtlasSettingsDialog = new TextureAtlasSettingsDialog(state, path); + dialog.showDialog(); + + /**/ + }); + } + // Callback for resetting the image. objReset.onClick = function(_) { @@ -98,9 +125,6 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox linkedObj.loadGraphic(AssetDataHandler.getDefaultGraphic()); linkedObj.updateHitbox(); - // remove unused bitmaps - state.removeUnusedBitmaps(); - // Update the image preview. refresh(); stageEditorState.updateDialog(OBJECT_ANIMS); @@ -111,7 +135,11 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox { if (linkedObj == null) return; + var file:Null = linkedObj.usedFiles.find(f -> f.name.endsWith(".png")); linkedObj.loadGraphic(linkedObj.graphic); + + if (file != null) linkedObj.usedFiles.push(file); + refresh(); stageEditorState.updateDialog(OBJECT_ANIMS); } @@ -124,7 +152,6 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox if (selectedFile != null && selectedFile.bytes != null) { objFrameTxt.text = selectedFile.bytes.toString(); - state.notifyChange("Frame Text Loaded", "The Text File " + selectedFile.name + " has been loaded."); } }); @@ -141,6 +168,7 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox { if (linkedObj == null) return; + var file:StageEditorAssetFile = state.createFile('${linkedObj.name}.png', linkedObj.pixels.image.encode()); linkedObj.loadGraphic(linkedObj.graphic, true, Std.int(objImageWidth.pos), Std.int(objImageHeight.pos)); linkedObj.updateHitbox(); @@ -153,6 +181,12 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox linkedObj.frames.frames[i].name = 'Frame$i'; } + linkedObj.usedFiles.push(file); + + // Add a generated XML file to the files, so that this sprite can be used in-game. + var name:String = Path.withoutExtension(linkedObj.usedFiles[0].name); + linkedObj.usedFiles.push(state.createFile('$name.xml', Bytes.ofString(AssetDataHandler.generateXML(linkedObj, name)))); + // Refresh the display. refresh(); @@ -185,13 +219,15 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox } /** - * Set the linked object's frames based on its graphic and loaded text. + * Set the linked object's frames based on its graphic and loaded text. Only for sparrow and packer atlases. * @param usePacker */ function setObjFrames(usePacker:Bool) { if (linkedObj == null || objFrameTxt.text == null || objFrameTxt.text.length == 0) return; + var file:StageEditorAssetFile = stageEditorState.createFile('${linkedObj.name}.png', linkedObj.pixels.image.encode()); + try { if (usePacker) @@ -209,9 +245,13 @@ class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox return; } - linkedObj.animDatas.clear(); - linkedObj.animation.destroyAnimations(); linkedObj.updateHitbox(); + + linkedObj.usedFiles.push(file); + + var name:String = Path.withoutExtension(linkedObj.usedFiles[0].name); + linkedObj.usedFiles.push(stageEditorState.createFile('$name.${usePacker ? "txt" : "xml"}', Bytes.ofString(objFrameTxt.text))); + refresh(); stageEditorState.notifyChange("Frame Setup Done", "Finished the Frame Setup for the Object " + linkedObj.name + ".");