package funkin.backend.utils; import funkin.backend.FunkinSprite; import funkin.backend.system.ErrorCode; import funkin.backend.FunkinSprite.XMLAnimType; import flixel.util.FlxColor; import haxe.xml.Access; import flixel.util.typeLimit.OneOfTwo; import funkin.backend.system.interfaces.IOffsetCompatible; using StringTools; typedef TextFormat = { text:String, format:Dynamic } /** * Class made to make XML parsing easier. */ class XMLUtil { /** * Applies a property XML node to an object. * @param object Object to which the xml property will be applied * @param property `property` node. */ public static function applyXMLProperty(object:Dynamic, property:Access):ErrorCode { if (!property.has.name || !property.has.type || !property.has.value) { Logs.trace('Failed to apply XML property: XML Element is missing name, type, or value attributes.', WARNING); return MISSING_PROPERTY; } var keys = property.att.name.split("."); var o = object; var isPath = false; while(keys.length > 1) { isPath = true; o = Reflect.getProperty(o, keys.shift()); // TODO: support arrays } var value:Dynamic = switch(property.att.type.toLowerCase()) { case "f" | "float" | "number": Std.parseFloat(property.att.value); case "i" | "int" | "integer" | "color": Std.parseInt(property.att.value); case "s" | "string" | "str" | "text": property.att.value; case "b" | "bool" | "boolean": property.att.value.toLowerCase() == "true"; default: return TYPE_INCORRECT; } if (value == null) return VALUE_NULL; if (object is IXMLEvents) { cast(object, IXMLEvents).onPropertySet(property.att.name, value); } try { Reflect.setProperty(o, keys[0], value); } catch(e) { var str = 'Failed to apply XML property: $e on ${Type.getClass(object)}'; if(isPath) { str += ' (Path: ${property.att.name})'; } Logs.trace(str, WARNING); return REFLECT_ERROR; } return OK; } /** * Overrides a sprite based on a XML node. */ public static function loadSpriteFromXML(spr:FunkinSprite, node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT):FunkinSprite { if (parentFolder == null) parentFolder = ""; spr.name = node.getAtt("name"); spr.antialiasing = true; spr.loadSprite(Paths.image('$parentFolder${node.getAtt("sprite").getDefault(spr.name)}', null, true)); spr.spriteAnimType = defaultAnimType; if (node.has.type) { spr.spriteAnimType = XMLAnimType.fromString(node.att.type, spr.spriteAnimType); } if(node.has.x) { var x:Null = Std.parseFloat(node.att.x); if (x.isNotNull()) spr.x = x; } if(node.has.y) { var y:Null = Std.parseFloat(node.att.y); if (y.isNotNull()) spr.y = y; } if (node.has.scroll) { var scroll:Null = Std.parseFloat(node.att.scroll); if (scroll.isNotNull()) spr.scrollFactor.set(scroll, scroll); } if (node.has.scrollx) { var scroll:Null = Std.parseFloat(node.att.scrollx); if (scroll.isNotNull()) spr.scrollFactor.x = scroll; } if (node.has.scrolly) { var scroll:Null = Std.parseFloat(node.att.scrolly); if (scroll.isNotNull()) spr.scrollFactor.y = scroll; } if (node.has.skewx) { var skew:Null = Std.parseFloat(node.att.skewx); if (skew.isNotNull()) spr.skew.x = skew; } if (node.has.skewy) { var skew:Null = Std.parseFloat(node.att.skewy); if (skew.isNotNull()) spr.skew.y = skew; } if (node.has.antialiasing) spr.antialiasing = node.att.antialiasing == "true"; if (node.has.width) { var width:Null = Std.parseFloat(node.att.width); if (width.isNotNull()) spr.width = width; } if (node.has.height) { var height:Null = Std.parseFloat(node.att.height); if (height.isNotNull()) spr.height = height; } if (node.has.scale) { var scale:Null = Std.parseFloat(node.att.scale); if (scale.isNotNull()) spr.scale.set(scale, scale); } if (node.has.scalex) { var scale:Null = Std.parseFloat(node.att.scalex); if (scale.isNotNull()) spr.scale.x = scale; } if (node.has.scaley) { var scale:Null = Std.parseFloat(node.att.scaley); if (scale.isNotNull()) spr.scale.y = scale; } if (node.has.graphicSize) { var graphicSize:Null = Std.parseInt(node.att.graphicSize); if (graphicSize.isNotNull()) spr.setGraphicSize(graphicSize, graphicSize); } if (node.has.graphicSizex) { var graphicSizex:Null = Std.parseInt(node.att.graphicSizex); if (graphicSizex.isNotNull()) spr.setGraphicSize(graphicSizex); } if (node.has.graphicSizey) { var graphicSizey:Null = Std.parseInt(node.att.graphicSizey); if (graphicSizey.isNotNull()) spr.setGraphicSize(0, graphicSizey); } if (node.has.updateHitbox && node.att.updateHitbox == "true") spr.updateHitbox(); if (node.has.zoomfactor) spr.zoomFactor = Std.parseFloat(node.getAtt("zoomfactor")).getDefault(spr.zoomFactor); if (node.has.alpha) spr.alpha = Std.parseFloat(node.getAtt("alpha")).getDefault(spr.alpha); if(node.has.color) spr.color = FlxColor.fromString(node.getAtt("color")).getDefault(0xFFFFFFFF); if (node.has.playOnCountdown) spr.skipNegativeBeats = node.att.playOnCountdown == "true"; if (node.has.beatInterval) spr.beatInterval = Std.parseInt(node.att.beatInterval); if (node.has.beatOffset) spr.beatOffset = Std.parseInt(node.att.beatOffset); if(node.hasNode.anim) { for(anim in node.nodes.anim) addXMLAnimation(spr, anim); } else { if (spr.frames != null && spr.frames.frames != null) { addAnimToSprite(spr, { name: "idle", anim: null, fps: 24, loop: spr.spriteAnimType == LOOP, animType: spr.spriteAnimType, x: 0, y: 0, indices: [for(i in 0...spr.frames.frames.length) i], forced: (node.has.forced && node.att.forced == "true") || (!node.has.forced && spr.spriteAnimType == BEAT) }); } } return spr; } /** * Creates a new sprite based on a XML node. */ public static inline function createSpriteFromXML(node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT, ?cl:Class, ?args:Array):FunkinSprite { if(cl == null) cl = FunkinSprite; if(args == null) args = []; return loadSpriteFromXML(Type.createInstance(cl, args), node, parentFolder, defaultAnimType); } public static function extractAnimFromXML(anim:Access, animType:XMLAnimType = NONE, loop:Bool = false):AnimData { var animData:AnimData = { name: null, anim: null, fps: 24, loop: loop, animType: animType, x: 0, y: 0, indices: [], forced: false, }; if (anim.has.name) animData.name = anim.att.name; if (anim.has.type) animData.animType = XMLAnimType.fromString(anim.att.type, animData.animType); if (anim.has.anim) animData.anim = anim.att.anim; if (anim.has.fps) animData.fps = Std.parseFloat(anim.att.fps).getDefault(animData.fps); if (anim.has.x) animData.x = Std.parseFloat(anim.att.x).getDefault(animData.x); if (anim.has.y) animData.y = Std.parseFloat(anim.att.y).getDefault(animData.y); if (anim.has.loop) animData.loop = anim.att.loop == "true"; if (anim.has.forced) animData.forced = anim.att.forced == "true"; if (anim.has.indices) { var indicesSplit = anim.att.indices.split(","); for(indice in indicesSplit) { var i = Std.parseInt(indice.trim()); if (i != null) animData.indices.push(i); } } return animData; } /** * Adds an XML animation to `sprite`. * @param sprite Destination sprite * @param anim Animation (Must be a `anim` XML node) */ public static function addXMLAnimation(sprite:FlxSprite, anim:Access, loop:Bool = false):ErrorCode { var animType:XMLAnimType = NONE; if (sprite is FunkinSprite) { animType = cast(sprite, FunkinSprite).spriteAnimType; } return addAnimToSprite(sprite, extractAnimFromXML(anim, animType, loop)); } public static function addAnimToSprite(sprite:FlxSprite, animData:AnimData):ErrorCode { if (animData.name != null) { if (animData.fps <= 0 #if web || animData.fps == null #end) animData.fps = 24; if (sprite is FunkinSprite && cast(sprite, FunkinSprite).animateAtlas != null) { var animateAnim = cast(sprite, FunkinSprite).animateAtlas.anim; if(animData.anim == null) return MISSING_PROPERTY; if (animData.indices.length > 0) animateAnim.addBySymbolIndices(animData.name, animData.anim, animData.indices, animData.fps, animData.loop); else animateAnim.addBySymbol(animData.name, animData.anim, animData.fps, animData.loop); } else { if (animData.anim == null && animData.indices.length > 0) sprite.animation.add(animData.name, animData.indices, animData.fps, animData.loop); else if (animData.indices.length > 0) sprite.animation.addByIndices(animData.name, animData.anim, animData.indices, "", animData.fps, animData.loop); else sprite.animation.addByPrefix(animData.name, animData.anim, animData.fps, animData.loop); } if (sprite is IOffsetCompatible) cast(sprite, IOffsetCompatible).addOffset(animData.name, animData.x, animData.y); if (sprite is FunkinSprite) { var xmlSpr = cast(sprite, FunkinSprite); switch(animData.animType) { case BEAT: xmlSpr.beatAnims.push({ name: animData.name, forced: animData.forced.getDefault(false) }); case LOOP: xmlSpr.animation.play(animData.name, animData.forced.getDefault(false)); default: // nothing } xmlSpr.animDatas.set(animData.name, animData); } return OK; } return MISSING_PROPERTY; } public static inline function fixXMLText(text:String) { var v:String; return [for(l in text.split("\n")) if ((v = l.trim()) != "") v].join("\n"); } /** * WARNING: will edit directly the node! */ public static function fixSpacingInNode(node:Access):Access { var arr = [for(x in node.x) x]; for(i => n in arr) { if(n.nodeType == PCData) { if(i == 0) n.nodeValue = n.nodeValue.ltrim(); if(i == arr.length - 1) n.nodeValue = n.nodeValue.rtrim(); if(n.nodeValue.contains("\n")) { var a = n.nodeValue.split("\n"); n.nodeValue = [for(i => x in a) i == 0 ? x.rtrim() : i == arr.length - 1 ? x.ltrim() : x.trim()].join("\n"); } } } return node; } public static function getTextFormats(_node:OneOfTwo, currentFormat:Dynamic = null, parsedSegments:Array = null):Array { var node:Xml = cast _node; if (currentFormat == null) currentFormat = {}; if (parsedSegments == null) parsedSegments = []; for (child in node) { switch (child.nodeType) { case Element: if (child.nodeName == "format") { var format:Dynamic = Reflect.copy(currentFormat); @:privateAccess for (key => name in child.attributeMap) { Reflect.setField(format, key, name); } getTextFormats(child, format, parsedSegments); } case PCData: parsedSegments.push({ text: child.nodeValue, format: Reflect.copy(currentFormat) }); default: // Ignore other node types } } return parsedSegments; } } typedef AnimData = { var name:String; var anim:String; var fps:Float; var loop:Bool; var x:Float; var y:Float; var indices:Array; var animType:XMLAnimType; var ?forced:Bool; } typedef BeatAnim = { var name:String; var forced:Bool; } interface IXMLEvents { public function onPropertySet(property:String, value:Dynamic):Void; }