package funkin.backend.utils; #if sys import sys.FileSystem; #end import flixel.text.FlxText; import funkin.backend.utils.XMLUtil.TextFormat; import flixel.util.typeLimit.OneOfTwo; import flixel.util.typeLimit.OneOfThree; import flixel.tweens.FlxTween; import flixel.system.frontEnds.SoundFrontEnd; import flixel.sound.FlxSound; import funkin.backend.system.Conductor; import flixel.sound.FlxSoundGroup; import haxe.Json; import haxe.io.Path; import haxe.io.Bytes; import haxe.xml.Access; import flixel.input.keyboard.FlxKey; import lime.utils.Assets; import flixel.animation.FlxAnimation; import flixel.input.keyboard.FlxKey; import flixel.sound.FlxSound; import flixel.sound.FlxSoundGroup; import flixel.system.frontEnds.SoundFrontEnd; import flixel.text.FlxText; import flixel.tweens.FlxTween; import flixel.util.FlxAxes; import flixel.util.FlxColor; import flixel.util.typeLimit.OneOfTwo; import funkin.backend.system.Conductor; import funkin.backend.utils.XMLUtil.TextFormat; import haxe.CallStack; import haxe.Json; import haxe.io.Bytes; import haxe.io.Path; import haxe.xml.Access; import lime.utils.Assets; import openfl.geom.ColorTransform; using StringTools; @:allow(funkin.game.PlayState) class CoolUtil { public static function getLastExceptionStack():String { return CallStack.toString(CallStack.exceptionStack()); } /* * Returns `v` if not null * @param v The value * @return A bool value */ public static inline function isNotNull(v:Null):Bool { return v != null && !isNaN(v); } /* * 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(v:Null, defaultValue:T):T { return (v == null || isNaN(v)) ? defaultValue : v; } /** * Shortcut to parse JSON from an Asset path * @param assetPath Path to the JSON asset. */ public static function parseJson(assetPath:String) { return Json.parse(Assets.getText(assetPath)); } /** * Deletes a folder recursively * @param delete Path to the folder. */ @:noUsing public static function deleteFolder(delete:String) { #if sys if (!FileSystem.exists(delete)) return; var files:Array = FileSystem.readDirectory(delete); for(file in files) { if (FileSystem.isDirectory(delete + "/" + file)) { deleteFolder(delete + "/" + file); FileSystem.deleteDirectory(delete + "/" + file); } else { try FileSystem.deleteFile(delete + "/" + file) catch(e) Logs.trace("Could not delete " + delete + "/" + file, WARNING); } } #end } /** * Safe saves a file (even adding eventual missing folders) and shows a warning box instead of making the program crash * @param path Path to save the file at. * @param content Content of the file to save (as String or Bytes). */ @:noUsing public static function safeSaveFile(path:String, content:OneOfTwo, showErrorBox:Bool = true) { #if sys try { addMissingFolders(Path.directory(path)); if(content is Bytes) sys.io.File.saveBytes(path, content); else sys.io.File.saveContent(path, content); } catch(e) { var errMsg:String = 'Error while trying to save the file: ${Std.string(e).replace('\n', ' ')}'; Logs.traceColored([Logs.logText(errMsg, RED)], ERROR); if(showErrorBox) funkin.backend.utils.NativeAPI.showMessageBox("Codename Engine Warning", errMsg, MSG_WARNING); } #end } /** * Gets file attributes from a file or a folder adding eventual missing folders in the path * (WARNING: Only works on `windows` for now. On other platforms the attributes' value it's always going to be `0` -thanks to the wrapper you can also use `isNothing` for checking- but still creates eventual missing folders if the platforms allows it to). * @param path Path to the file or folder * @param useAbsol If it should use the absolute path (By default it's `true` but if it's `false` you can use files outside from this program's directory for example) * @return The attributes through the `FileAttributeWrapper` */ @:noUsing public static inline function safeGetAttributes(path:String, useAbsol:Bool = true):FileAttributeWrapper { addMissingFolders(Path.directory(path)); var result = NativeAPI.getFileAttributes(path, useAbsol); if(result.isNothing) Logs.trace('The file where it has been tried to get the attributes from, might be corrupted or inexistent (code: ${result.getValue()})', WARNING); return result; } /** * Sets file attributes to a file or a folder adding eventual missing folders in the path * (WARNING: Only works on `windows` for now. On other platforms the return code it's always going to be `0` but still creates eventual missing folders if the platforms allows it to). * @param path Path to the file or folder * @param attrib The attribute(s) to set (WARNING: There are some non settable attributes, such as the `COMPRESSED` one) * @param useAbsol If it should use the absolute path (By default it's `true` but if it's `false` you can use files outside from this program's directory for example) * @return The result code: `0` means that it failed setting */ @:noUsing public static inline function safeSetAttributes(path:String, attrib:OneOfThree, useAbsol:Bool = true):Int { // yes, i'm aware that FileAttribute is also an Int so need to include it too, but at least like this we don't have to make cast sometimes while passing the arguments - Nex addMissingFolders(Path.directory(path)); var result = NativeAPI.setFileAttributes(path, attrib, useAbsol); if(result == 0) Logs.trace('Failed to set attributes to $path with a code of: $result', WARNING); return result; } /** * Adds one (or more) file attributes to a file or a folder adding eventual missing folders in the path * (WARNING: Only works on `windows` for now. On other platforms the return code it's always going to be `0` but still creates eventual missing folders if the platforms allows it to). * @param path Path to the file or folder * @param attrib The attribute(s) to add (WARNING: There are some non settable attributes, such as the `COMPRESSED` one) * @param useAbsol If it should use the absolute path (By default it's `true` but if it's `false` you can use files outside from this program's directory for example) * @return The result code: `0` means that it failed setting */ @:noUsing public static inline function safeAddAttributes(path:String, attrib:OneOfTwo, useAbsol:Bool = true):Int { addMissingFolders(Path.directory(path)); var result = NativeAPI.addFileAttributes(path, attrib, useAbsol); if(result == 0) Logs.trace('Failed to add attributes to $path with a code of: $result', WARNING); return result; } /** * Removes one (or more) file attributes to a file or a folder adding eventual missing folders in the path * (WARNING: Only works on `windows` for now. On other platforms the return code it's always going to be `0` but still creates eventual missing folders if the platforms allows it to). * @param path Path to the file or folder * @param attrib The attribute(s) to remove (WARNING: There are some non settable attributes, such as the `COMPRESSED` one) * @param useAbsol If it should use the absolute path (By default it's `true` but if it's `false` you can use files outside from this program's directory for example) * @return The result code: `0` means that it failed setting */ @:noUsing public static inline function safeRemoveAttributes(path:String, attrib:OneOfTwo, useAbsol:Bool = true):Int { addMissingFolders(Path.directory(path)); var result = NativeAPI.removeFileAttributes(path, attrib, useAbsol); if(result == 0) Logs.trace('Failed to remove attributes to $path with a code of: $result', WARNING); return result; } /** * Creates eventual missing folders to the specified `path` * * WARNING: eventual files in `path` will be considered as folders! Just to make possible folders be named as `songs.json` for example * * @param path Path to check. * @return The initial Path. */ @:noUsing public static function addMissingFolders(path:String):String { #if sys var folders:Array = path.split("/"); var currentPath:String = ""; for (folder in folders) { currentPath += folder + "/"; if (!FileSystem.exists(currentPath)) FileSystem.createDirectory(currentPath); } #end return path; } /** * Shortcut to parse a JSON string * @param str Path to the JSON string * @return Parsed JSON */ public inline static function parseJsonString(str:String) return Json.parse(str); /** * Whenever a value is NaN or not. * @param v Value */ public static inline function isNaN(v:Dynamic) { if (v is Float || v is Int) return Math.isNaN(cast(v, Float)); return false; } /** * Returns the last of an Array * @param array Array * @return T Last element */ public static inline function last(array:Array):T { return array[array.length - 1]; } /** * Sets a field's default value, and returns it. In case it already exists, returns the existing one. * @param v Dynamic to set the default value to * @param name Name of the value * @param defaultValue Default value * @return T New/old value. */ public static function setFieldDefault(v:Dynamic, name:String, defaultValue:T):T { if (Reflect.hasField(v, name)) { var f:Null = Reflect.field(v, name); if (f != null) return cast f; } Reflect.setField(v, name, defaultValue); return defaultValue; } /** * Add several zeros at the beginning of a string, so that `2` becomes `02`. * @param str String to add zeros * @param num The length required */ public static inline function addZeros(str:String, num:Int) { while(str.length < num) str = '0${str}'; return str; } /** * Add several zeros at the end of a string, so that `2` becomes `20`, useful for ms. * @param str String to add zeros * @param num The length required */ public static inline function addEndZeros(str:String, num:Int) { while(str.length < num) str = '${str}0'; return str; } /** * Returns a string representation of a size, following this format: `1.02 GB`, `134.00 MB` * @param size Size to convert to string * @return String Result string representation */ 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]}'; } /** * Replaces in a string any kind of IP with `[Your IP]` making the string safer to trace. * @param msg String to check and edit * @return String Result without any kind of IP */ public static inline function removeIP(msg:String):String { return ~/\d+.\d+.\d+.\d+/.replace(msg, "[Your IP]"); // For now its just IPs but who knows in the future.. - Nex } /** * Alternative linear interpolation function for each frame use, without worrying about framerate changes. * @param v1 Begin value * @param v2 End value * @param ratio Ratio * @return Float Final value */ @:noUsing 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 fps sensitive (adapted when game is running at 120 instead of 60) */ @:noUsing 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 */ @:noUsing 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 { // -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 = 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; } /** * Plays the main menu theme. * @param fadeIn */ @:noUsing public static function playMenuSong(fadeIn:Bool = false) { if (FlxG.sound.music == null || !FlxG.sound.music.playing) { playMusic(Paths.music('freakyMenu'), true, fadeIn ? 0 : 1, true, 102); FlxG.sound.music.persist = true; if (fadeIn) FlxG.sound.music.fadeIn(4, 0, 0.7); } } /** * Preloads a character. * @param name Character name * @param spriteName (Optional) sprite name. */ @:noUsing 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'); } /** * Plays music, while resetting the Conductor, and taking info from INI in count. * @param path Path to the music * @param Persist Whenever the music should persist while switching states * @param DefaultBPM Default BPM of the music (102) * @param Volume Volume of the music (1) * @param Looped Whenever the music loops (true) * @param Group A group that this music belongs to (default) */ @:noUsing public static function playMusic(path:String, Persist:Bool = false, Volume:Int = 1, Looped:Bool = true, DefaultBPM:Int = 102, ?Group:FlxSoundGroup) { Conductor.reset(); FlxG.sound.playMusic(path, Volume, Looped, Group); if (FlxG.sound.music != null) { FlxG.sound.music.persist = Persist; } var infoPath = '${Path.withoutExtension(path)}.ini'; if (Assets.exists(infoPath)) { var musicInfo = IniUtil.parseAsset(infoPath, [ "BPM" => null, "TimeSignature" => "4/4" ]); var timeSignParsed:Array> = musicInfo["TimeSignature"] == null ? [] : [for(s in musicInfo["TimeSignature"].split("/")) Std.parseFloat(s)]; var beatsPerMeasure:Float = 4; var stepsPerBeat:Float = 4; // Check later, i dont think timeSignParsed can contain null, only nan if (timeSignParsed.length == 2 && !timeSignParsed.contains(null)) { beatsPerMeasure = timeSignParsed[0] == null || timeSignParsed[0] <= 0 ? 4 : cast timeSignParsed[0]; stepsPerBeat = timeSignParsed[1] == null || timeSignParsed[1] <= 0 ? 4 : cast timeSignParsed[1]; } var bpm:Null = Std.parseFloat(musicInfo["BPM"]).getDefault(DefaultBPM); Conductor.changeBPM(bpm, beatsPerMeasure, stepsPerBeat); } else Conductor.changeBPM(DefaultBPM); } /** * Plays a specified Menu SFX. * @param menuSFX Menu SFX to play * @param volume At which volume it should play */ @:noUsing public static inline function playMenuSFX(menuSFX:CoolSfx = SCROLL, volume:Float = 1) { FlxG.sound.play(Paths.sound(switch(menuSFX) { case CONFIRM: 'menu/confirm'; case CANCEL: 'menu/cancel'; case SCROLL: 'menu/scroll'; case CHECKED: 'menu/checkboxChecked'; case UNCHECKED: 'menu/checkboxUnchecked'; case WARNING: 'menu/warningMenu'; default: 'menu/scroll'; }), volume); } /** * Allows you to split a text file from a path, into a "cool text file", AKA a list. Allows for comments. For example, * `# comment` * `test1` * ` ` * `test2` * will return `["test1", "test2"]` * @param path * @return Array */ @:noUsing public static function coolTextFile(path:String):Array { var trim:String; return [for(line in Assets.getText(path).split("\n")) if ((trim = line.trim()) != "" && !trim.startsWith("#")) trim]; } /** * Returns an array of number from min to max. Equivalent of `[for (i in min...max) i]`. * @param max Max value * @param min Minimal value (0) * @return Array Final array */ @:noUsing public static inline function numberArray(max:Int, ?min:Int = 0):Array { return [for (i in min...max) i]; } /** * Switches frames from 2 FlxAnimations. * @param anim1 First animation * @param anim2 Second animation */ @:noUsing 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; } /** * Allows you to set a graphic size (ex: 150x150), with proper hitbox without a stretched sprite. * @param sprite Sprite to apply the new graphic size to * @param width Width * @param height Height * @param fill Whenever the sprite should fill instead of shrinking (true) * @param maxScale Maximum scale (0 / none) */ public static inline 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); } /** * Returns a simple string representation of a FlxKey. Used in Controls options. * @param key Key * @return Simple representation */ public static inline function keyToString(key:Null):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(); } } /** * Centers an object in a camera's field, basically `screenCenter()` but `camera.width` and `camera.height` are used instead of `FlxG.width` and `FlxG.height`. * @param obj Sprite to center * @param cam Camera * @param axes Axes (XY) */ public static inline 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: } } /** * Equivalent of `setGraphicSize`, except that it can accept floats and automatically updates the hitbox. * @param sprite Sprite to set the size of * @param width Width * @param height Height */ public static inline function setSpriteSize(sprite:FlxSprite, width:Float, height:Float) { sprite.scale.set(width / sprite.frameWidth, height / sprite.frameHeight); sprite.updateHitbox(); } /** * Gets an XML attribute from an `Access` abstract, without throwing an exception if invalid. * Example: `xml.getAtt("test").getDefault("Hello, World!");` * @param xml XML to get the attribute from * @param name Name of the attribute */ public static inline function getAtt(xml:Access, name:String) { if (!xml.has.resolve(name)) return null; return xml.att.resolve(name); } /** * Sets automatically all the compatible formats to a text. * * WARNING: These are dependant from the font, so if the font doesn't support for example the `bold` format it won't work! * @param text Text to set the format for * @param formats Array of the formats (to get the formats from a node, you can use `XMLUtil.getTextFormats(node)`) */ public static function autoSetFormat(text:FlxText, formats:Array) { var i = 0; @:privateAccess for(format in formats) { var fmtt = format.format; var start = i; var end = i + format.text.length; i = end; if(Reflect.fields(fmtt).length == 0) continue; var fmt = new FlxTextFormat(); fmt.format.color = Reflect.hasField(fmtt, "color") ? FlxColor.fromString(fmtt.color) : text.color; fmt.format.font = Reflect.hasField(fmtt, "font") ? Paths.font(fmtt.font) : text.font; fmt.format.size = Reflect.hasField(fmtt, "size") ? Std.parseInt(fmtt.size) : text.size; fmt.format.italic = Reflect.hasField(fmtt, "italic") ? fmtt.italic == "true" : text.italic; fmt.format.bold = Reflect.hasField(fmtt, "bold") ? fmtt.bold == "true" : text.bold; fmt.borderColor = Reflect.hasField(fmtt, "borderColor") ? FlxColor.fromString(fmtt.borderColor) : text.borderColor; fmt.format.align = Reflect.hasField(fmtt, "align") ? TextFormatAlign.fromString(fmtt.align) : FlxTextAlign.toOpenFL(text.alignment); if(Reflect.hasField(fmtt, "leading")) fmt.format.leading = Std.parseInt(fmtt.leading); if(Reflect.hasField(fmtt, "kerning")) fmt.format.kerning = fmtt.kerning == "true"; if(Reflect.hasField(fmtt, "blockIndent")) fmt.format.blockIndent = Std.parseInt(fmtt.blockIndent); if(Reflect.hasField(fmtt, "bullet")) fmt.format.bullet = fmtt.bullet == "true"; if(Reflect.hasField(fmtt, "indent")) fmt.format.indent = Std.parseInt(fmtt.indent); if(Reflect.hasField(fmtt, "leftMargin")) fmt.format.leftMargin = Std.parseInt(fmtt.leftMargin); if(Reflect.hasField(fmtt, "letterSpacing")) fmt.format.letterSpacing = Std.parseFloat(fmtt.letterSpacing); if(Reflect.hasField(fmtt, "rightMargin")) fmt.format.rightMargin = Std.parseInt(fmtt.rightMargin); if(Reflect.hasField(fmtt, "tabStops")) fmt.format.tabStops = [for(x in cast(fmtt.tabStops, String).split(",")) Std.parseInt(x)]; if(Reflect.hasField(fmtt, "underline")) fmt.format.underline = fmtt.underline == "true"; text.addFormat(fmt, start, end); } return text; } /** * Loads an animated graphic, and automatically animates it. * @param spr Sprite to load the graphic for * @param path Path to the graphic */ public static function loadAnimatedGraphic(spr:FlxSprite, path:String) { spr.frames = Paths.getFrames(path, true); 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; } /** * Copies a color transform from color1 to color2 * @param color1 Color transform to copy to * @param color2 Color transform to copy from */ 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; } /** * Resets an FlxSprite * @param spr Sprite to reset * @param x New X position * @param y New Y position */ public static function resetSprite(spr:FlxSprite, x:Float, y:Float) { spr.reset(x, y); spr.alpha = 1; spr.visible = true; spr.active = true; spr.acceleration.set(); spr.velocity.set(); spr.drag.set(); spr.antialiasing = FlxSprite.defaultAntialiasing; spr.frameOffset.set(); FlxTween.cancelTweensOf(spr); } /** * Gets the macro class created by hscript-improved for an abstract / enum */ @:noUsing public static inline function getMacroAbstractClass(className:String) { return Type.resolveClass('${className}_HSC'); } /** * Basically indexOf, but starts from the end. * @param array Array to scan * @param element Element * @return Index, or -1 if unsuccessful. */ public static inline function indexOfFromLast(array:Array, element:T):Int { /*var i = array.length - 1; while(i >= 0) { if (array[i] == element) break; i--; } return i;*/ return array.lastIndexOf(element); } /** * Clears the content of an array */ public static inline function clear(array:Array):Array { // while(array.length > 0) // array.shift(); array.resize(0); return array; } /** * Push an entire group into an array. * @param array Array to push the group into * @param ...args Group entries * @return Array */ public static inline function pushGroup(array:Array, ...args:T):Array { for(a in args) array.push(a); return array; } /** * Opens an URL in the browser. * @param url */ @:noUsing public static inline function openURL(url:String) { #if linux Sys.command('/usr/bin/xdg-open', [url]); #else FlxG.openURL(url); #end } /** * Converts a timestamp to a readable format such as `01:22` (`mm:ss`) */ public static inline function timeToStr(time:Float) return '${Std.string(Std.int(time / 60000)).addZeros(2)}:${Std.string(Std.int(time / 1000) % 60).addZeros(2)}.${Std.string(Std.int(time % 1000)).addZeros(3)}'; /** * Stops a sound, set its time to 0 then play it again. * @param sound Sound to replay. */ public static inline function replay(sound:FlxSound) { sound.stop(); sound.time = 0; sound.play(); } /** * Equivalent of `Math.max`, except doesn't require a Int -> Float -> Int conversion. * @param p1 * @param p2 * @return return p1 < p2 ? p2 : p1 */ @:noUsing public static inline function maxInt(p1:Int, p2:Int) return p1 < p2 ? p2 : p1; /** * Equivalent of `Math.floor`, except doesn't require a Int -> Float -> Int conversion. * @param e Value to get the floor of. */ public static inline function floorInt(e:Float) { var r = Std.int(e); if (e < 0 && r != e) r--; return r; } @:noUsing public static inline function quantize(Value:Float, Quant:Float) { return Math.fround(Value * Quant) / Quant; } /** * Sets a SoundFrontEnd's music to a FlxSound. * Example: `FlxG.sound.setMusic(music);` * @param frontEnd SoundFrontEnd to set the music of * @param music Music */ public static inline function setMusic(frontEnd:SoundFrontEnd, music:FlxSound) { if (frontEnd.music != null) @:privateAccess frontEnd.destroySound(frontEnd.music); frontEnd.list.remove(music); frontEnd.music = music; } @:noUsing public static inline function flxeaseFromString(mainEase:String, suffix:String) return Reflect.field(FlxEase, mainEase + (mainEase == "linear" ? "" : suffix)); public static function sortAlphabetically(array:Array, ?lowercase:Bool=false) { array.sort(function(a1, a2):Int { if(lowercase) { a1 = a1.toLowerCase(); a2 = a2.toLowerCase(); } if (a1 < a2) return -1; if (a1 > a2) return 1; return 0; }); return array; } public static inline function pushOnce(array:Array, element:T) { #if (haxe >= "4.0.0") if (!array.contains(element)) array.push(element); #else if (array.indexOf(element) == -1) array.push(element); #end } #if !(haxe >= "4.0.0") public static inline function contains(array:Array, element:T) { return array.indexOf(element) != -1; } #end /* * Returns the filename of a path, without the extension. * @param path Path to get the filename from * @return Filename */ @:noUsing public static inline function getFilename(file:String) { var file = new haxe.io.Path(file); return file.file; } /** * Converts a string of "1..3,5,7..9,8..5" into an array of numbers like [1,2,3,5,7,8,9,8,7,6,5] * @param input String to parse * @return Array of numbers */ public static function parseNumberRange(input:String):Array { var result:Array = []; var parts:Array = input.split(","); for (part in parts) { part = part.trim(); var idx = part.indexOf(".."); if (idx != -1) { var start = Std.parseInt(part.substring(0, idx).trim()); var end = Std.parseInt(part.substring(idx + 2).trim()); if(start == null || end == null) { continue; } if (start < end) { for (j in start...end + 1) { result.push(j); } } else { for (j in end...start + 1) { result.push(start + end - j); } } } else { var num = Std.parseInt(part); if (num != null) { result.push(num); } } } return result; } /** * Converts an array of numbers into a string of ranges. * Example: [1,2,3,5,7,8,9,8,7,6,5] -> "1..3,5,7..9,8..5" * @param numbers Array of numbers * @return String representing the ranges */ public static function formatNumberRange(numbers:Array, seperator:String = ","):String { if (numbers.length == 0) return ""; var result:Array = []; var i = 0; while (i < numbers.length) { var start = numbers[i]; var end = start; var direction = 0; // 0: no sequence, 1: increasing, -1: decreasing if (i + 1 < numbers.length) { // detect direction of sequence if (numbers[i + 1] == end + 1) { direction = 1; } else if (numbers[i + 1] == end - 1) { direction = -1; } } if(direction != 0) { while (i + 1 < numbers.length && (numbers[i + 1] == end + direction)) { end = numbers[i + 1]; i++; } } if (start == end) { // no direction result.push('${start}'); } else if (start + direction == end) { // 1 step increment result.push('${start},${end}'); } else { // store as range result.push('${start}..${end}'); } i++; } return result.join(seperator); } public static function deepFlatten(arr:Array, ?result:Array):Array { if(arr == null) return []; if(result == null) result = []; for (e in arr) { if (Std.isOfType(e, Array)) { deepFlatten(e, result); } else { result.push(e); } } return result; } } /** * SFXs to play using `playMenuSFX`. */ enum abstract CoolSfx(Int) from Int { var SCROLL = 0; var CONFIRM = 1; var CANCEL = 2; var CHECKED = 3; var UNCHECKED = 4; var WARNING = 5; }