implement texture atlas support for the stage editor
This commit is contained in:
+1
-1
Submodule assets updated: aa81b51551...d9ebd43f6b
@@ -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<StageEditorAssetFile> = [];
|
||||
|
||||
public var selectedShader:InverseDotsShader;
|
||||
|
||||
@@ -22,7 +25,10 @@ class StageEditorObject extends FunkinSprite
|
||||
*/
|
||||
public var startingAnimation:String = "";
|
||||
|
||||
public var animDatas:Map<String, AnimationData> = [];
|
||||
/**
|
||||
* Stored list of animation data for easier exports.
|
||||
*/
|
||||
public var animationDatas:Map<String, AnimationData> = [];
|
||||
|
||||
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<Float>, indices:Array<Int>, 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
|
||||
|
||||
@@ -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> = [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<CharacterType, FlxTypedGroup<BaseCharacter>> = [];
|
||||
public var charCamOffsets:Map<CharacterType, Array<Float>> = DEFAULT_CAMERA_OFFSETS.copy();
|
||||
public var charPos:Map<CharacterType, Array<Float>> = DEFAULT_POSITIONS.copy();
|
||||
public var bitmaps:Map<String, BitmapData> = []; // used for optimizing the file size!!!
|
||||
|
||||
/**
|
||||
* An array of files used by the sprites.
|
||||
*/
|
||||
public var allFiles(get, never):Array<StageEditorAssetFile>;
|
||||
|
||||
function get_allFiles()
|
||||
{
|
||||
var result:Array<StageEditorAssetFile> = [];
|
||||
for (sprite in spriteArray)
|
||||
{
|
||||
result = result.concat(sprite.usedFiles);
|
||||
}
|
||||
|
||||
return result.distinct();
|
||||
}
|
||||
|
||||
var charDeselectShader:Grayscale = new Grayscale();
|
||||
var floorLines:Array<FlxSprite> = [];
|
||||
@@ -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<String> = [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<String> = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<String> = 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<String> = [
|
||||
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<StageEditorObject> = stageEditorState.spriteArray;
|
||||
spr.zIndex = sprArray.length == 0 ? 0 : (sprArray[sprArray.length - 1].zIndex + 1);
|
||||
|
||||
stageEditorState.selectedSprite = spr;
|
||||
|
||||
@@ -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<String> = FileUtil.readDir(atlasPath);
|
||||
|
||||
var baseAtlasPath:String = Path.withoutDirectory(atlasPath);
|
||||
var animJson:String = FileUtil.readStringFromPath(Path.join([atlasPath, 'Animation.json']));
|
||||
var spritemapFiles:Array<StageEditorState.StageEditorAssetFile> = [];
|
||||
|
||||
// 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<SpritemapInput> = [
|
||||
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
|
||||
@@ -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<StageEditorState.StageEditorAssetFile> = data.neededFiles.find(f -> f.name.endsWith('.png'));
|
||||
var frameData:Null<StageEditorState.StageEditorAssetFile> = 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<StageEditorState.StageEditorAssetFile> = data.neededFiles.find(f -> f.name.endsWith('.png'));
|
||||
var frameData:Null<StageEditorState.StageEditorAssetFile> = 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<SpritemapInput> = [];
|
||||
|
||||
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("</TextureAtlas>"))
|
||||
{
|
||||
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 += "</TextureAtlas>";
|
||||
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<StageEditorState.StageEditorAssetFile>;
|
||||
}
|
||||
#end
|
||||
|
||||
@@ -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<String, String> = [];
|
||||
|
||||
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<Entry>();
|
||||
|
||||
// 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<String, String> = [];
|
||||
var entries:Array<Entry> = FileUtil.readZIPFromBytes(zip);
|
||||
var stageData:Null<StageData> = null;
|
||||
var allFiles:Array<StageEditorAssetFile> = [];
|
||||
|
||||
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<StageEditorAssetFile> = 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<StageEditorAssetFile> = [];
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<String>
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -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<String> = 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<StageEditorAssetFile> = 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 + ".");
|
||||
|
||||
Reference in New Issue
Block a user