moved utils (update your scripts!!!!)
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
package funkin.utils;
|
||||
|
||||
import funkin.system.Song;
|
||||
import funkin.system.Conductor;
|
||||
import flixel.system.FlxSoundGroup;
|
||||
import animateatlas.AtlasFrameMaker;
|
||||
import haxe.Json;
|
||||
import funkin.menus.StoryMenuState.WeekData;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import haxe.io.Path;
|
||||
import haxe.xml.Access;
|
||||
import flixel.input.keyboard.FlxKey;
|
||||
import lime.utils.Assets;
|
||||
import flixel.animation.FlxAnimation;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.FlxG;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.FlxObject;
|
||||
import flixel.FlxCamera;
|
||||
import flixel.util.FlxAxes;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import flash.geom.ColorTransform;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class CoolUtil
|
||||
{
|
||||
/*
|
||||
* Returns `v` if not null, `defaultValue` otherwise.
|
||||
* @param v The value
|
||||
* @param defaultValue The default value
|
||||
* @return The return value
|
||||
*/
|
||||
public static inline function getDefault<T>(v:Null<T>, defaultValue:T):Null<T> {
|
||||
return (v == null || isNaN(v)) ? defaultValue : v;
|
||||
}
|
||||
|
||||
public static function parseJson(assetPath:String) {
|
||||
return Json.parse(Assets.getText(assetPath));
|
||||
}
|
||||
|
||||
public inline static function parseJsonString(str:String)
|
||||
return Json.parse(str);
|
||||
|
||||
public static inline function isNaN(v:Dynamic) {
|
||||
if (v is Float || v is Int)
|
||||
return Math.isNaN(cast(v, Float));
|
||||
return false;
|
||||
}
|
||||
|
||||
public static inline function last<T>(array:Array<T>):T {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
||||
public static function setFieldDefault<T>(v:Dynamic, name:String, defaultValue:T):T {
|
||||
if (Reflect.hasField(v, name)) {
|
||||
var f:Null<Dynamic> = Reflect.field(v, name);
|
||||
if (f != null)
|
||||
return cast f;
|
||||
}
|
||||
Reflect.setField(v, name, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public static inline function addZeros(str:String, num:Int) {
|
||||
while(str.length < num) str = '0${str}';
|
||||
return str;
|
||||
}
|
||||
|
||||
public static function getSizeString(size:Float):String {
|
||||
var labels = ["B", "KB", "MB", "GB", "TB"];
|
||||
var rSize:Float = size;
|
||||
var label:Int = 0;
|
||||
while(rSize > 1024 && label < labels.length-1) {
|
||||
label++;
|
||||
rSize /= 1024;
|
||||
}
|
||||
return '${Std.int(rSize) + "." + addZeros(Std.string(Std.int((rSize % 1) * 100)), 2)}${labels[label]}';
|
||||
}
|
||||
|
||||
public static inline function fpsLerp(v1:Float, v2:Float, ratio:Float):Float {
|
||||
return FlxMath.lerp(v1, v2, getFPSRatio(ratio));
|
||||
}
|
||||
/**
|
||||
* Lerps from color1 into color2 (Shortcut to `FlxColor.interpolate`)
|
||||
* @param color1 Color 1
|
||||
* @param color2 Color 2
|
||||
* @param ratio Ratio
|
||||
* @param fpsSensitive Whenever the ratio should be case sentivive (adapted when game is running at 120 instead of 60)
|
||||
*/
|
||||
public static inline function lerpColor(color1:FlxColor, color2:FlxColor, ratio:Float, fpsSensitive:Bool = false) {
|
||||
if (!fpsSensitive)
|
||||
ratio = getFPSRatio(ratio);
|
||||
return FlxColor.interpolate(color1, color2, ratio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a lerp ratio based on current FPS to keep a stable speed on higher framerate.
|
||||
* @param ratio Ratio
|
||||
* @return FPS-Modified Ratio
|
||||
*/
|
||||
public static inline function getFPSRatio(ratio:Float):Float {
|
||||
return FlxMath.bound(ratio * 60 * FlxG.elapsed, 0, 1);
|
||||
}
|
||||
/**
|
||||
* Tries to get a color from a `Dynamic` variable.
|
||||
* @param c `Dynamic` color.
|
||||
* @return The result color, or `null` if invalid.
|
||||
*/
|
||||
public static function getColorFromDynamic(c:Dynamic):Null<FlxColor> {
|
||||
// -1
|
||||
if (c is Int) return c;
|
||||
|
||||
// -1.0
|
||||
if (c is Float) return Std.int(c);
|
||||
|
||||
// "#FFFFFF"
|
||||
if (c is String) return FlxColor.fromString(c);
|
||||
|
||||
// [255, 255, 255]
|
||||
if (c is Array) {
|
||||
var r:Int = 0;
|
||||
var g:Int = 0;
|
||||
var b:Int = 0;
|
||||
var a:Int = 255;
|
||||
var array:Array<Dynamic> = cast c;
|
||||
for(k=>e in array) {
|
||||
if (e is Int || e is Float) {
|
||||
switch(k) {
|
||||
case 0: r = Std.int(e);
|
||||
case 1: g = Std.int(e);
|
||||
case 2: b = Std.int(e);
|
||||
case 3: a = Std.int(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return FlxColor.fromRGB(r, g, b, a);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function playMenuSong(fadeIn:Bool = false) {
|
||||
if (FlxG.sound.music == null || !FlxG.sound.music.playing)
|
||||
{
|
||||
playMusic(Paths.music('freakyMenu'), 102, fadeIn ? 0 : 1);
|
||||
if (fadeIn)
|
||||
FlxG.sound.music.fadeIn(4, 0, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
public static function preloadCharacter(name:String, ?spriteName:String) {
|
||||
if (name == null) return;
|
||||
if (spriteName == null)
|
||||
spriteName = name;
|
||||
Assets.getText(Paths.xml('characters/$name'));
|
||||
Paths.getFrames('characters/$spriteName');
|
||||
}
|
||||
|
||||
public static function playMusic(path:String, DefaultBPM:Int = 102, Volume:Int = 1, Looped:Bool = true, ?Group:FlxSoundGroup) {
|
||||
Conductor.reset();
|
||||
FlxG.sound.playMusic(path, Volume, Looped, Group);
|
||||
|
||||
var infoPath = '${Path.withoutExtension(path)}.ini';
|
||||
if (Assets.exists(infoPath)) {
|
||||
var musicInfo = IniUtil.parseAsset(infoPath, [
|
||||
"BPM" => null
|
||||
]);
|
||||
var parsedBPM:Null<Float> = Std.parseFloat(musicInfo["BPM"]);
|
||||
Conductor.changeBPM(parsedBPM == null ? DefaultBPM : parsedBPM);
|
||||
} else
|
||||
Conductor.changeBPM(DefaultBPM);
|
||||
}
|
||||
/**
|
||||
* Plays a specified Menu SFX.
|
||||
* @param menuSFX Menu SFX to play
|
||||
* @param volume At which volume it should play
|
||||
*/
|
||||
public static function playMenuSFX(menuSFX:Int = 0, volume:Float = 1) {
|
||||
FlxG.sound.play(Paths.sound(switch(menuSFX) {
|
||||
case 1: 'menu/confirm';
|
||||
case 2: 'menu/cancel';
|
||||
default: 'menu/scroll';
|
||||
}), volume);
|
||||
}
|
||||
|
||||
public static function coolTextFile(path:String):Array<String>
|
||||
{
|
||||
return [for(e in Assets.getText(path).trim().split('\n')) e.trim()];
|
||||
}
|
||||
|
||||
public static inline function numberArray(max:Int, ?min = 0):Array<Int>
|
||||
{
|
||||
return [for (i in min...max) i];
|
||||
}
|
||||
|
||||
public static function switchAnimFrames(anim1:FlxAnimation, anim2:FlxAnimation) {
|
||||
if (anim1 == null || anim2 == null) return;
|
||||
var old = anim1.frames;
|
||||
anim1.frames = anim2.frames;
|
||||
anim2.frames = old;
|
||||
}
|
||||
|
||||
public static function setUnstretchedGraphicSize(sprite:FlxSprite, width:Int, height:Int, fill:Bool = true, maxScale:Float = 0) {
|
||||
sprite.setGraphicSize(width, height);
|
||||
sprite.updateHitbox();
|
||||
var nScale = (fill ? Math.max : Math.min)(sprite.scale.x, sprite.scale.y);
|
||||
if (maxScale > 0 && nScale > maxScale) nScale = maxScale;
|
||||
sprite.scale.set(nScale, nScale);
|
||||
}
|
||||
|
||||
public static inline function keyToString(key:Null<FlxKey>):String {
|
||||
return switch(key) {
|
||||
case null | 0 | NONE: "---";
|
||||
case LEFT: "←";
|
||||
case DOWN: "↓";
|
||||
case UP: "↑";
|
||||
case RIGHT: "→";
|
||||
case ESCAPE: "ESC";
|
||||
case BACKSPACE: "[←]";
|
||||
case NUMPADZERO: "#0";
|
||||
case NUMPADONE: "#1";
|
||||
case NUMPADTWO: "#2";
|
||||
case NUMPADTHREE: "#3";
|
||||
case NUMPADFOUR: "#4";
|
||||
case NUMPADFIVE: "#5";
|
||||
case NUMPADSIX: "#6";
|
||||
case NUMPADSEVEN: "#7";
|
||||
case NUMPADEIGHT: "#8";
|
||||
case NUMPADNINE: "#9";
|
||||
case NUMPADPLUS: "#+";
|
||||
case NUMPADMINUS: "#-";
|
||||
case NUMPADPERIOD: "#.";
|
||||
case ZERO: "0";
|
||||
case ONE: "1";
|
||||
case TWO: "2";
|
||||
case THREE: "3";
|
||||
case FOUR: "4";
|
||||
case FIVE: "5";
|
||||
case SIX: "6";
|
||||
case SEVEN: "7";
|
||||
case EIGHT: "8";
|
||||
case NINE: "9";
|
||||
case PERIOD: ".";
|
||||
default: key.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static function cameraCenter(obj:FlxObject, cam:FlxCamera, axes:FlxAxes = XY) {
|
||||
switch(axes) {
|
||||
case XY:
|
||||
obj.setPosition((cam.width - obj.width) / 2, (cam.height - obj.height) / 2);
|
||||
case X:
|
||||
obj.x = (cam.width - obj.width) / 2;
|
||||
case Y:
|
||||
obj.y = (cam.height - obj.height) / 2;
|
||||
case NONE:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static function loadWeek(weekData:WeekData, difficulty:String = "normal") {
|
||||
PlayState.storyWeek = weekData;
|
||||
PlayState.storyPlaylist = [for(e in weekData.songs) e.name];
|
||||
PlayState.isStoryMode = true;
|
||||
PlayState.campaignScore = 0;
|
||||
__loadSong(PlayState.storyPlaylist[0], difficulty);
|
||||
}
|
||||
public static function loadSong(name:String, difficulty:String = "normal") {
|
||||
PlayState.campaignScore = 0;
|
||||
PlayState.isStoryMode = false;
|
||||
__loadSong(name, difficulty);
|
||||
}
|
||||
public static function __loadSong(name:String, difficulty:String) {
|
||||
PlayState.difficulty = difficulty;
|
||||
|
||||
PlayState.SONG = Song.loadFromJson(name, difficulty);
|
||||
}
|
||||
public static function setSpriteSize(sprite:FlxSprite, width:Float, height:Float) {
|
||||
sprite.scale.set(width / sprite.frameWidth, height / sprite.frameHeight);
|
||||
sprite.updateHitbox();
|
||||
}
|
||||
|
||||
public static inline function getAtt(xml:Access, name:String) {
|
||||
if (!xml.has.resolve(name)) return null;
|
||||
return xml.att.resolve(name);
|
||||
}
|
||||
|
||||
public static function loadFrames(path:String, Unique:Bool = false, Key:String = null, SkipAtlasCheck:Bool = false):FlxFramesCollection {
|
||||
var noExt = Path.withoutExtension(path);
|
||||
|
||||
if (Assets.exists('$noExt/1.png')) {
|
||||
// MULTIPLE SPRITESHEETS!!
|
||||
|
||||
var graphic = FlxG.bitmap.add("flixel/images/logo/default.png", false, '$noExt/mult');
|
||||
var frames = FlxAtlasFrames.findFrame(graphic);
|
||||
if (frames != null)
|
||||
return frames;
|
||||
|
||||
trace("no frames yet for multiple atlases!!");
|
||||
var spritesheets = [];
|
||||
var cur = 1;
|
||||
var finalFrames = new FlxFramesCollection(graphic, ATLAS);
|
||||
while(Assets.exists('$noExt/$cur.png')) {
|
||||
spritesheets.push(loadFrames('$noExt/$cur.png'));
|
||||
cur++;
|
||||
}
|
||||
for(frames in spritesheets)
|
||||
if (frames != null && frames.frames != null)
|
||||
for(f in frames.frames)
|
||||
if (f != null) {
|
||||
finalFrames.frames.push(f);
|
||||
f.parent = frames.parent;
|
||||
}
|
||||
return finalFrames;
|
||||
} else if (!SkipAtlasCheck && Assets.exists('$noExt/Animation.json')
|
||||
&& Assets.exists('$noExt/spritemap.json')
|
||||
&& Assets.exists('$noExt/spritemap.png')) {
|
||||
return AtlasFrameMaker.construct(noExt);
|
||||
} else if (Assets.exists('$noExt.xml')) {
|
||||
return Paths.getSparrowAtlasAlt(noExt);
|
||||
} else if (Assets.exists('$noExt.txt')) {
|
||||
return Paths.getPackerAtlasAlt(noExt);
|
||||
}
|
||||
|
||||
var graph:FlxGraphic = FlxG.bitmap.add(path, Unique, Key);
|
||||
if (graph == null)
|
||||
return null;
|
||||
return graph.imageFrame;
|
||||
}
|
||||
|
||||
public static function loadAnimatedGraphic(spr:FlxSprite, path:String) {
|
||||
spr.frames = loadFrames(path);
|
||||
|
||||
if (spr.frames != null && spr.frames.frames != null) {
|
||||
spr.animation.add("idle", [for(i in 0...spr.frames.frames.length) i], 24, true);
|
||||
spr.animation.play("idle");
|
||||
}
|
||||
|
||||
return spr;
|
||||
}
|
||||
|
||||
public static inline function copyColorTransform(color1:ColorTransform, color2:ColorTransform) {
|
||||
color1.alphaMultiplier = color2.alphaMultiplier;
|
||||
color1.alphaOffset = color2.alphaOffset;
|
||||
color1.blueMultiplier = color2.blueMultiplier;
|
||||
color1.blueOffset = color2.blueOffset;
|
||||
color1.greenMultiplier = color2.greenMultiplier;
|
||||
color1.greenOffset = color2.greenOffset;
|
||||
color1.redMultiplier = color2.redMultiplier;
|
||||
color1.redOffset = color2.redOffset;
|
||||
}
|
||||
|
||||
public static function resetSprite(spr:FlxSprite, x:Float, y:Float) {
|
||||
spr.reset(x, y);
|
||||
spr.alpha = 1;
|
||||
spr.visible = true;
|
||||
spr.active = true;
|
||||
spr.antialiasing = FlxSprite.defaultAntialiasing;
|
||||
spr.rotOffset.set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the macro class created by hscript-improved for an abstract / enum
|
||||
*/
|
||||
public static function getMacroAbstractClass(className:String) {
|
||||
return Type.resolveClass('${className}_HSC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the content of an array
|
||||
*/
|
||||
public static function clear<T>(array:Array<T>):Array<T> {
|
||||
while(array.length > 0)
|
||||
array.shift();
|
||||
return array;
|
||||
}
|
||||
|
||||
public static function pushGroup<T>(array:Array<T>, ...args:T):Array<T> {
|
||||
for(a in args)
|
||||
array.push(a);
|
||||
return array;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package funkin.utils;
|
||||
|
||||
import openfl.utils.Assets;
|
||||
|
||||
/**
|
||||
* DOESNT SUPPORT CATEGORIES YET!!
|
||||
*/
|
||||
class IniUtil {
|
||||
public static inline function parseAsset(assetPath:String, ?defaultVariables:Map<String, String>)
|
||||
return parseString(Assets.getText(assetPath), defaultVariables);
|
||||
|
||||
public static function parseString(data:String, ?defaultVariables:Map<String, String>) {
|
||||
var trimmed:String;
|
||||
var splitContent = [for(e in data.split("\n")) if ((trimmed = e.trim()) != "") trimmed];
|
||||
|
||||
var finalMap:Map<String, String> = [];
|
||||
if (defaultVariables != null)
|
||||
for(k=>e in defaultVariables)
|
||||
finalMap[k] = e;
|
||||
|
||||
for(line in splitContent) {
|
||||
// comment
|
||||
if (line.startsWith(";")) continue;
|
||||
// categories; not supported yet
|
||||
if (line.startsWith("[") && line.endsWith("]")) continue;
|
||||
|
||||
var index = line.indexOf("=");
|
||||
var name = line.substr(0, index).trim();
|
||||
var value = line.substr(index+1).trim();
|
||||
|
||||
if (value.startsWith("\"") && value.endsWith("\""))
|
||||
value = value.substr(1, value.length - 2);
|
||||
|
||||
if (value.length == 0 || name.length == 0)
|
||||
continue;
|
||||
|
||||
finalMap[name] = value;
|
||||
}
|
||||
return finalMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package funkin.utils;
|
||||
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.text.FlxText;
|
||||
|
||||
class MarkdownUtil {
|
||||
public static function applyMarkdownText(text:FlxText, str:String) {
|
||||
text.textField.htmlText = Markdown.markdownToHtml(prepareMarkdown(str));
|
||||
var changes:Array<FlxTextFormatMarkerPair> = [
|
||||
createAdvancedMarkerPair("{title}", 0xFFFFFFFF, text.size * 2, false, true, true),
|
||||
createAdvancedMarkerPair("{subtitle}", 0xFFFFFFFF, text.size * 1.5, false, true, true),
|
||||
createAdvancedMarkerPair("{subsubtitle}", 0xFFFFFFFF, text.size * 1.25, false, true, true),
|
||||
createAdvancedMarkerPair("{ident1}", 0xFFFFFFFF, null, false, false, false, text.size * 2, true),
|
||||
createAdvancedMarkerPair("{ident2}", 0xFFFFFFFF, null, false, false, false, text.size * 4, true),
|
||||
createAdvancedMarkerPair("{ident3}", 0xFFFFFFFF, null, false, false, false, text.size * 6, true),
|
||||
createAdvancedMarkerPair("{ident4}", 0xFFFFFFFF, null, false, false, false, text.size * 8, true),
|
||||
new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFFFFF00, false, true), "{o}"),
|
||||
new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFFF0000, false, true), "{r}"),
|
||||
new FlxTextFormatMarkerPair(new FlxTextFormat(0xFF00FF08, false, true), "{g}"),
|
||||
new FlxTextFormatMarkerPair(new FlxTextFormat(0xFF7D7D7D, false, true), "{gray}"),
|
||||
new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFFFFFFF, false, true, 0xFF222222), "{italic}"),
|
||||
new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFFFFFFF, false, true, 0xFF666666), "{bold}"),
|
||||
new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFFFFFFF, false, true, 0xFF888888), "{bold-italic}"),
|
||||
];
|
||||
|
||||
text.applyMarkup(
|
||||
parseWarnings(text.textField.text),
|
||||
changes
|
||||
);
|
||||
@:privateAccess {
|
||||
// hacky fix for the text cutting when bigger text formats are used
|
||||
text._regen = true;
|
||||
text.regenGraphic();
|
||||
text._regen = true;
|
||||
text.regenGraphic();
|
||||
}
|
||||
}
|
||||
|
||||
public static function createAdvancedMarkerPair(marker:String, color:FlxColor, size:Null<Float>, bold:Bool = false, italic:Bool = false, ?underline:Bool = false, ?blockIndent:Int, ?bullet:Bool) {
|
||||
@:privateAccess {
|
||||
var format = new FlxTextFormat(color, bold, italic);
|
||||
format.format.size = size == null ? null : Std.int(size);
|
||||
format.format.underline = underline;
|
||||
format.format.blockIndent = blockIndent;
|
||||
format.format.bullet = bullet;
|
||||
return new FlxTextFormatMarkerPair(format, marker);
|
||||
}
|
||||
}
|
||||
|
||||
public static function parseWarnings(text:String) {
|
||||
text = text.replace(" ", " ");
|
||||
text = parseEmote(text, "⚠", "{o}(!)", "{o}");
|
||||
text = parseEmote(text, "❌", "{r}(X)", "{r}");
|
||||
text = parseEmote(text, "✔", "{g}(+)", "{g}");
|
||||
return text;
|
||||
}
|
||||
|
||||
public static function prepareMarkdown(text:String) {
|
||||
text = "\n" + text;
|
||||
text = parseEmote(text, "\n- ", "\n{ident1} • ", "{ident1}");
|
||||
text = parseEmote(text, "\n - ", "\n{ident2} • ", "{ident2}");
|
||||
text = parseEmote(text, "\n - ", "\n{ident3} • ", "{ident3}");
|
||||
text = parseEmote(text, "\n - ", "\n{ident4} • ", "{ident4}");
|
||||
text = parseEmote(text, "\n> ", "\n{gray}", "{gray}");
|
||||
text = parseEmote(text, "\n### ", "\n{subsubtitle}", "{subsubtitle}");
|
||||
text = parseEmote(text, "\n## ", "\n{subtitle}", "{subtitle}");
|
||||
text = parseEmote(text, "\n# ", "\n{title}", "{title}");
|
||||
text = parseEmote(text, "***", "{bold-italic}", "{bold-italic}", "***");
|
||||
text = parseEmote(text, "**", "{bold}", "{bold}", "**");
|
||||
text = parseEmote(text, "*", "{italic}", "{italic}", "*");
|
||||
text = parseEmote(text, "_", "{italic}", "{italic}", "*");
|
||||
text = parseEmote(text, "__", "{bold}", "{bold}", "*");
|
||||
text.substr("\n".length);
|
||||
return text;
|
||||
}
|
||||
|
||||
public static function parseEmote(text:String, emote:String, beginning:String, end:String, emoteEnd:String = "\n") {
|
||||
var index:Int;
|
||||
while((index = text.indexOf(emote)) >= 0) {
|
||||
var nextIndex = text.indexOf(emoteEnd, index + emote.length);
|
||||
if (nextIndex < 0)
|
||||
nextIndex = text.length;
|
||||
text = text.substr(0, index) + beginning + text.substring(index + emote.length, nextIndex) + end + (emote == emoteEnd ? "" : emoteEnd) + text.substring(nextIndex + emoteEnd.length);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package funkin.utils;
|
||||
|
||||
#if ALLOW_MULTITHREADING
|
||||
class ThreadUtil {
|
||||
/**
|
||||
* Creates a new Thread with an error handler.
|
||||
* @param func Function to execute
|
||||
* @param autoRestart Whenever the thread should auto restart itself after crashing.
|
||||
*/
|
||||
public static function createSafe(func:Void->Void, autoRestart:Bool = false) {
|
||||
if (autoRestart) {
|
||||
return sys.thread.Thread.create(function() {
|
||||
while(true) {
|
||||
try {
|
||||
func();
|
||||
} catch(e) {
|
||||
trace(e.details());
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return sys.thread.Thread.create(function() {
|
||||
try {
|
||||
func();
|
||||
} catch(e) {
|
||||
trace(e.details());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
@@ -0,0 +1,184 @@
|
||||
package funkin.utils;
|
||||
|
||||
import funkin.system.FunkinSprite;
|
||||
import funkin.system.ErrorCode;
|
||||
import funkin.system.FunkinSprite.XMLAnimType;
|
||||
import flixel.math.FlxMath;
|
||||
import funkin.interfaces.IBeatReceiver;
|
||||
import haxe.xml.Access;
|
||||
import funkin.interfaces.IOffsetCompatible;
|
||||
import flixel.FlxSprite;
|
||||
|
||||
using StringTools;
|
||||
/**
|
||||
* 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 misses name, type, or value attributes.', WARNING);
|
||||
return MISSING_PROPERTY;
|
||||
}
|
||||
|
||||
var value:Dynamic = switch(property.att.type.toLowerCase()) {
|
||||
case "float" | "number": Std.parseFloat(property.att.value);
|
||||
case "int" | "integer" | "color": Std.parseInt(property.att.value);
|
||||
case "string" | "str" | "text": property.att.value;
|
||||
case "bool" | "boolean": property.att.value.toLowerCase() == "true";
|
||||
default: return TYPE_INCORRECT;
|
||||
}
|
||||
if (value == null) return VALUE_NULL;
|
||||
|
||||
try {
|
||||
Reflect.setProperty(object, property.att.name, value);
|
||||
} catch(e) {
|
||||
Logs.trace('Failed to apply XML property: $e on ${Type.getClass(object)}', WARNING);
|
||||
return REFLECT_ERROR;
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sprite based on a XML node.
|
||||
*/
|
||||
public static function createSpriteFromXML(node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT, ?cl:Class<FunkinSprite>):FunkinSprite {
|
||||
if (parentFolder == null) parentFolder = "";
|
||||
|
||||
var spr:FunkinSprite = cl != null ? Type.createInstance(cl, []) : new FunkinSprite();
|
||||
spr.name = node.getAtt("name");
|
||||
spr.antialiasing = true;
|
||||
|
||||
spr.loadAnimatedGraphic(Paths.image('$parentFolder${node.getAtt("sprite")}', null, true));
|
||||
|
||||
spr.spriteAnimType = defaultAnimType;
|
||||
if (node.has.type)
|
||||
spr.spriteAnimType = XMLAnimType.fromString(node.att.type, spr.spriteAnimType);
|
||||
|
||||
var x:Null<Float> = node.has.x ? Std.parseFloat(node.att.x) : null;
|
||||
var y:Null<Float> = node.has.y ? Std.parseFloat(node.att.y) : null;
|
||||
if (x != null) spr.x = x;
|
||||
if (y != null) spr.y = y;
|
||||
if (node.has.scroll) {
|
||||
var scroll:Null<Float> = Std.parseFloat(node.att.scroll);
|
||||
if (scroll != null) spr.scrollFactor.set(scroll, scroll);
|
||||
} else {
|
||||
if (node.has.scrollx) {
|
||||
var scroll:Null<Float> = Std.parseFloat(node.att.scrollx);
|
||||
if (scroll != null) spr.scrollFactor.x = scroll;
|
||||
}
|
||||
if (node.has.scrolly) {
|
||||
var scroll:Null<Float> = Std.parseFloat(node.att.scrolly);
|
||||
if (scroll != null) spr.scrollFactor.y = scroll;
|
||||
}
|
||||
}
|
||||
if (node.has.antialiasing) spr.antialiasing = node.att.antialiasing == "true";
|
||||
if (node.has.scale) {
|
||||
var scale:Null<Float> = Std.parseFloat(node.att.scale);
|
||||
if (scale != null) spr.scale.set(scale, scale);
|
||||
}
|
||||
if (node.has.updateHitbox && node.att.updateHitbox == "true") spr.updateHitbox();
|
||||
|
||||
spr.zoomFactor = Std.parseFloat(node.getAtt("zoomfactor")).getDefault(spr.zoomFactor);
|
||||
|
||||
for(anim in node.nodes.anim)
|
||||
addXMLAnimation(spr, anim);
|
||||
|
||||
return spr;
|
||||
}
|
||||
|
||||
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: []
|
||||
};
|
||||
|
||||
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.parseInt(anim.att.fps);
|
||||
if (anim.has.x) animData.x = Std.parseFloat(anim.att.x);
|
||||
if (anim.has.y) animData.y = Std.parseFloat(anim.att.y);
|
||||
if (anim.has.loop) animData.loop = anim.att.loop == "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 && animData.anim != 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.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.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(animData.name);
|
||||
case LOOP:
|
||||
xmlSpr.animation.play(animData.name);
|
||||
default:
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
return MISSING_PROPERTY;
|
||||
}
|
||||
}
|
||||
|
||||
typedef AnimData = {
|
||||
var name:String;
|
||||
var anim:String;
|
||||
var fps:Int;
|
||||
var loop:Bool;
|
||||
var x:Float;
|
||||
var y:Float;
|
||||
var indices:Array<Int>;
|
||||
var animType:XMLAnimType;
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package funkin.utils;
|
||||
|
||||
#if (!macro && sys)
|
||||
import openfl.display.BitmapData;
|
||||
#end
|
||||
|
||||
import haxe.Exception;
|
||||
import haxe.Json;
|
||||
import haxe.crypto.Crc32;
|
||||
import haxe.zip.Writer;
|
||||
import haxe.zip.Tools;
|
||||
import haxe.zip.Entry;
|
||||
import haxe.zip.Uncompress;
|
||||
import haxe.zip.Reader;
|
||||
import haxe.zip.Compress;
|
||||
import sys.FileSystem;
|
||||
import sys.io.File;
|
||||
import sys.thread.Thread;
|
||||
|
||||
using StringTools;
|
||||
|
||||
// import ZipUtils; ZipUtils.uncompressZip(ZipUtils.openZip("E:\\Desktop\\test\\termination lua.ycemod"), "E:\\Desktop\\test\\uncompressed\\");
|
||||
// import ZipUtils; var e = ZipUtils.createZipFile("gjnsdghs.ycemod"); ZipUtils.writeFolderToZip(e, "./mods/Friday Night Funkin'/", "Friday Night Funkin'/"); e.flush(); e.close();
|
||||
|
||||
class ZipUtil {
|
||||
public static var bannedNames:Array<String> = [".git", ".gitignore", ".github", ".vscode", ".gitattributes", "readme.txt"];
|
||||
|
||||
/**
|
||||
* [Description] Uncompresses `zip` into the `destFolder` folder
|
||||
* @param zip
|
||||
* @param destFolder
|
||||
*/
|
||||
public static function uncompressZip(zip:Reader, destFolder:String, ?prefix:String, ?prog:ZipProgress):ZipProgress {
|
||||
// we never know
|
||||
FileSystem.createDirectory(destFolder);
|
||||
|
||||
var fields = zip.read();
|
||||
|
||||
try {
|
||||
if (prefix != null) {
|
||||
var f = fields;
|
||||
fields = new List<Entry>();
|
||||
for(field in f) {
|
||||
if (field.fileName.startsWith(prefix)) {
|
||||
fields.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prog == null)
|
||||
prog = new ZipProgress();
|
||||
prog.fileCount = fields.length;
|
||||
for(k=>field in fields) {
|
||||
prog.curFile = k;
|
||||
var isFolder = field.fileName.endsWith("/") && field.fileSize == 0;
|
||||
if (isFolder) {
|
||||
FileSystem.createDirectory('${destFolder}/${field.fileName}');
|
||||
} else {
|
||||
var split = [for(e in field.fileName.split("/")) e.trim()];
|
||||
split.pop();
|
||||
FileSystem.createDirectory('${destFolder}/${split.join("/")}');
|
||||
|
||||
var data = unzip(field);
|
||||
File.saveBytes('${destFolder}/${field.fileName}', data);
|
||||
}
|
||||
}
|
||||
prog.curFile = fields.length;
|
||||
prog.done = true;
|
||||
} catch(e) {
|
||||
prog.done = true;
|
||||
prog.error = e;
|
||||
}
|
||||
return prog;
|
||||
}
|
||||
|
||||
#if (!macro && sys)
|
||||
public static function uncompressZipAsync(zip:Reader, destFolder:String, ?prog:ZipProgress, ?prefix:String):ZipProgress {
|
||||
if (prog == null)
|
||||
prog = new ZipProgress();
|
||||
Thread.create(function() {
|
||||
uncompressZip(zip, destFolder, prefix, prog);
|
||||
});
|
||||
return prog;
|
||||
}
|
||||
#end
|
||||
|
||||
/**
|
||||
* [Description] Returns a `zip.Reader` instance from path.
|
||||
* @param zipPath
|
||||
* @return Reader
|
||||
*/
|
||||
public static function openZip(zipPath:String):Reader {
|
||||
return new ZipReader(File.read(zipPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* [Description] Copy of haxe's Zip unzip function cause lime replaced it.
|
||||
* @param f Zip entry
|
||||
*/
|
||||
public static function unzip(f:Entry) {
|
||||
if (!f.compressed)
|
||||
return f.data;
|
||||
var c = new haxe.zip.Uncompress(-15);
|
||||
var s = haxe.io.Bytes.alloc(f.fileSize);
|
||||
var r = c.execute(f.data, 0, s, 0);
|
||||
c.close();
|
||||
if (!r.done || r.read != f.data.length || r.write != f.fileSize)
|
||||
throw "Invalid compressed data for " + f.fileName;
|
||||
f.compressed = false;
|
||||
f.dataSize = f.fileSize;
|
||||
f.data = s;
|
||||
return f.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Description] Creates a ZIP file at the specified location and returns the Writer.
|
||||
* @param path
|
||||
* @return Writer
|
||||
*/
|
||||
public static function createZipFile(path:String):ZipWriter {
|
||||
var output = File.write(path);
|
||||
return new ZipWriter(output);
|
||||
}
|
||||
|
||||
/**
|
||||
[Description] Writes the entirety of a folder to a zip file.
|
||||
@param zip ZIP file to write to
|
||||
@param path Folder path
|
||||
@param prefix (Additional) allows you to set a prefix in the zip itself.
|
||||
**/
|
||||
public static function writeFolderToZip(zip:ZipWriter, path:String, ?prefix:String, ?prog:ZipProgress, ?whitelist:Array<String>):ZipProgress {
|
||||
if (prefix == null) prefix = "";
|
||||
if (whitelist == null) whitelist = [];
|
||||
if (prog == null) prog = new ZipProgress();
|
||||
|
||||
try {
|
||||
var curPath:Array<String> = ['$path'];
|
||||
var destPath:Array<String> = [];
|
||||
if (prefix != "") {
|
||||
prefix = prefix.replace("\\", "/");
|
||||
while(prefix.charAt(0) == "/") prefix = prefix.substr(1);
|
||||
while(prefix.charAt(prefix.length-1) == "/") prefix = prefix.substr(0, prefix.length-1);
|
||||
destPath.push(prefix);
|
||||
}
|
||||
|
||||
var files:Array<StrNameLabel> = [];
|
||||
|
||||
var doFolder:Void->Void = null;
|
||||
(doFolder = function() {
|
||||
var path = curPath.join("/");
|
||||
var zipPath = destPath.join("/");
|
||||
for(e in FileSystem.readDirectory(path)) {
|
||||
if (bannedNames.contains(e.toLowerCase()) && !whitelist.contains(e.toLowerCase())) continue;
|
||||
if (FileSystem.isDirectory('$path/$e')) {
|
||||
// is directory, so loop into that function again
|
||||
for(p in [curPath, destPath]) p.push(e);
|
||||
doFolder();
|
||||
for(p in [curPath, destPath]) p.pop();
|
||||
} else {
|
||||
// is file, put it in the list
|
||||
var zipPath = '$zipPath/$e';
|
||||
while(zipPath.charAt(0) == "/") zipPath = zipPath.substr(1);
|
||||
files.push(new StrNameLabel('$path/$e', zipPath));
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
prog.fileCount = files.length;
|
||||
for(k=>file in files) {
|
||||
prog.curFile = k;
|
||||
|
||||
var fileContent = File.getBytes(file.name);
|
||||
var fileInfo = FileSystem.stat(file.name);
|
||||
var entry:Entry = {
|
||||
fileName: file.label,
|
||||
fileSize: fileInfo.size,
|
||||
fileTime: Date.now(),
|
||||
dataSize: 0,
|
||||
data: fileContent,
|
||||
crc32: Crc32.make(fileContent),
|
||||
compressed: false
|
||||
};
|
||||
Tools.compress(entry, 1);
|
||||
zip.writeFile(entry);
|
||||
}
|
||||
zip.writeCDR();
|
||||
} catch(e) {
|
||||
prog.error = e;
|
||||
}
|
||||
prog.done = true;
|
||||
return prog;
|
||||
}
|
||||
|
||||
public static function writeFolderToZipAsync(zip:ZipWriter, path:String, ?prefix:String):ZipProgress {
|
||||
var zipProg = new ZipProgress();
|
||||
Thread.create(function() {
|
||||
writeFolderToZip(zip, path, prefix, zipProg);
|
||||
});
|
||||
return zipProg;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Description] Converts an `Array<Entry>` to a `List<Entry>`.
|
||||
* @param array
|
||||
* @return List<Entry>
|
||||
*/
|
||||
public static function arrayToList(array:Array<Entry>):List<Entry> {
|
||||
var list = new List<Entry>();
|
||||
for(e in array) list.push(e);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
class ZipProgress {
|
||||
public var error:Exception = null;
|
||||
|
||||
public var curFile:Int = 0;
|
||||
public var fileCount:Int = 0;
|
||||
public var done:Bool = false;
|
||||
public var percentage(get, null):Float;
|
||||
|
||||
private function get_percentage() {
|
||||
return fileCount <= 0 ? 0 : curFile / fileCount;
|
||||
}
|
||||
|
||||
public function new() {}
|
||||
}
|
||||
|
||||
class ZipReader extends Reader {
|
||||
public var files:List<Entry>;
|
||||
|
||||
public override function read() {
|
||||
if (files != null) return files;
|
||||
try {
|
||||
var files = super.read();
|
||||
return this.files = files;
|
||||
} catch(e) {
|
||||
}
|
||||
return new List<Entry>();
|
||||
}
|
||||
}
|
||||
|
||||
class ZipWriter extends Writer {
|
||||
public function flush() {
|
||||
o.flush();
|
||||
}
|
||||
|
||||
public function writeFile(entry:Entry) {
|
||||
writeEntryHeader(entry);
|
||||
o.writeFullBytes(entry.data, 0, entry.data.length);
|
||||
}
|
||||
|
||||
public function close() {
|
||||
o.close();
|
||||
}
|
||||
}
|
||||
|
||||
class StrNameLabel {
|
||||
public var name:String;
|
||||
public var label:String;
|
||||
|
||||
public function new(name:String, label:String) {
|
||||
this.name = name;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user