Merge remote-tracking branch 'dev/main'

This commit is contained in:
⍚~Nex
2024-02-27 18:01:34 +01:00
110 changed files with 1944 additions and 23012 deletions
+13 -5
View File
@@ -13,6 +13,7 @@ import flixel.FlxG;
* Help from Ne_Eo
* @see https://twitter.com/Ne_Eo_Twitch
*/
@:access(away3d.core.managers.Stage3DManager)
class Flx3DUtil
{
#if THREE_D_SUPPORT
@@ -25,18 +26,25 @@ class Flx3DUtil
return FlxG.stage.stage3Ds.length;
}
/**
* Returns the total amount of 3D stages that are in use
* @return Int
*/
public static inline function getUsed3D():Int
{
return Stage3DManager._numStageProxies;
}
/**
* Returns if a Stage3D is available
* @return Bool
*/
public static inline function is3DAvailable():Bool
{
@:privateAccess {
if (Stage3DManager._stageProxies == null)
return true;
if (Stage3DManager._stageProxies == null)
return true;
return Stage3DManager._numStageProxies < Stage3DManager._stageProxies.length;
}
return Stage3DManager._numStageProxies < Stage3DManager._stageProxies.length;
}
/**
+1 -1
View File
@@ -36,7 +36,7 @@ class Flx3DView extends FlxView3D {
var meshes:Array<Mesh> = [];
public function new(x:Float = 0, y:Float = 0, width:Int = -1, height:Int = -1) {
if (!Flx3DUtil.is3DAvailable())
throw "[Flx3DView] 3D is not available on this platform. Stages in use: " + Flx3DUtil.getTotal3D() + ", Max stages allowed: " + FlxG.stage.stage3Ds.length + ".";
throw "[Flx3DView] 3D is not available on this platform. Stages in use: " + Flx3DUtil.getUsed3D() + ", Max stages allowed: " + Flx3DUtil.getTotal3D() + ".";
super(x, y, width, height);
__cur3DStageID = __3DIDS++;
}
+1 -1
View File
@@ -123,7 +123,7 @@ class MusicBeatState extends FlxState implements IBeatReceiver
var path = Paths.script('data/states/${scriptName}/LIB_$i');
var script = Script.create(path);
if (script is DummyScript) continue;
script.fileName = '$i:$scriptName';
script.remappedNames.set(script.fileName, '$i:${script.fileName}');
stateScripts.add(script);
script.load();
}
+1 -1
View File
@@ -110,7 +110,7 @@ class MusicBeatSubstate extends FlxSubState implements IBeatReceiver
var path = Paths.script('data/states/${scriptName}/LIB_$i');
var script = Script.create(path);
if (script is DummyScript) continue;
script.fileName = '$i:$scriptName';
script.remappedNames.set(script.fileName, '$i:${script.fileName}');
stateScripts.add(script);
script.load();
}
@@ -6,9 +6,6 @@ import lime.utils.AssetLibrary;
class AssetsLibraryList extends AssetLibrary {
public var libraries:Array<AssetLibrary> = [];
public var rootLibs:Array<AssetLibrary> = [];
public var assetsLibs:Array<AssetLibrary> = [];
@:allow(funkin.backend.system.Main)
@:allow(funkin.backend.system.MainState)
private var __defaultLibraries:Array<AssetLibrary> = [];
@@ -19,7 +16,6 @@ class AssetsLibraryList extends AssetLibrary {
libraries.remove(lib);
return lib;
}
public function existsSpecific(id:String, type:String, source:AssetSource = BOTH) {
if (!id.startsWith("assets/") && exists('assets/$id', type))
return true;
@@ -98,29 +94,23 @@ class AssetsLibraryList extends AssetLibrary {
return content;
}
public function getSpecificAsset(id:String, type:String, source:AssetSource = BOTH, ?avoidRoot:Bool = false):Dynamic {
public function getSpecificAsset(id:String, type:String, source:AssetSource = BOTH):Dynamic {
try {
if (!id.startsWith("assets/")) {
var ass = getSpecificAsset('assets/$id', type, source);
if (ass != null)
if (ass != null) {
return ass;
} else {
for (lib in assetsLibs) {
if (shouldSkipLib(libraries.indexOf(lib), source)) continue;
var asset = lib.getAsset(id, type);
if (asset != null)
return asset;
}
}
for(k=>e in libraries) {
if (shouldSkipLib(k, source)) continue;
if (!avoidRoot) {
for (lib in rootLibs) {
if (shouldSkipLib(libraries.indexOf(lib), source)) continue;
var asset = lib.getAsset(id, type);
if (asset != null)
@:privateAccess
if (e.exists(id, e.types.get(id))) {
var asset = e.getAsset(id, type);
if (asset != null) {
return asset;
}
}
}
return null;
@@ -140,9 +130,6 @@ class AssetsLibraryList extends AssetLibrary {
public override inline function getAsset(id:String, type:String):Dynamic
return getSpecificAsset(id, type, BOTH);
public inline function getAssetSafe(id:String, type:String):Dynamic
return getSpecificAsset(id, type, BOTH, true);
public override function isLocal(id:String, type:String) {
return true;
}
@@ -171,17 +158,8 @@ class AssetsLibraryList extends AssetLibrary {
addLibrary(d);
}
public function addLibrary(lib:AssetLibrary) @:privateAccess {
public function addLibrary(lib:AssetLibrary) {
libraries.insert(0, lib);
var finalLib:AssetLibrary = lib;
if (finalLib is openfl.utils.AssetLibrary)
finalLib = cast(finalLib, openfl.utils.AssetLibrary).__proxy;
if (finalLib is IModsAssetLibrary)
assetsLibs.insert(0, finalLib);
else
rootLibs.insert(0, finalLib);
return lib;
}
}
+17
View File
@@ -302,6 +302,23 @@ class Paths
return content;
*/
}
// Used in Script.hx
@:noCompletion public static function getFilenameFromLibFile(path:String) {
var file = new haxe.io.Path(path);
if(file.file.startsWith("LIB_")) {
return file.dir + "." + file.ext;
}
return path;
}
@:noCompletion public static function getLibFromLibFile(path:String) {
var file = new haxe.io.Path(path);
if(file.file.startsWith("LIB_")) {
return file.file.substr(4);
}
return "";
}
}
class ScriptPathInfo {
+29 -3
View File
@@ -18,6 +18,19 @@ class Chart {
*/
public inline static var defaultColor:FlxColor = 0xFF9271FD;
public static function loadEventsJson(songName:String) {
var path = Paths.file('songs/${songName.toLowerCase()}/events.json');
var data:Array<ChartEvent> = null;
if (Assets.exists(path)) {
try {
data = Json.parse(Assets.getText(path)).events;
} catch(e) {
Logs.trace('Failed to load song event data for ${songName} ($path): ${Std.string(e)}', ERROR);
}
}
return data;
}
public static function loadChartMeta(songName:String, difficulty:String = "normal", fromMods:Bool = true) {
var metaPath = Paths.file('songs/${songName.toLowerCase()}/meta.json');
var metaDiffPath = Paths.file('songs/${songName.toLowerCase()}/meta-${difficulty.toLowerCase()}.json');
@@ -117,6 +130,7 @@ class Chart {
3 => "Alt Animation Toggle",
];
if (data.events == null) data.events = [];
for (event in cast(data.events, Array<Dynamic>)) {
if (Reflect.hasField(event, "type")) {
if(event.type != null)
@@ -145,6 +159,16 @@ class Chart {
}
base.meta = loadedMeta;
}
/**
* events.json LOADING
*/
#if REGION
var extraEvents:Array<ChartEvent> = loadEventsJson(songName);
if (extraEvents != null)
base.events = base.events.concat(extraEvents);
#end
return base;
}
@@ -172,7 +196,7 @@ class Chart {
public static function save(songFolderPath:String, chart:ChartData, difficulty:String = "normal", ?saveSettings:ChartSaveSettings):ChartData {
if (saveSettings == null) saveSettings = {};
var filteredChart = filterChartForSaving(chart, saveSettings.saveMetaInChart);
var filteredChart = filterChartForSaving(chart, saveSettings.saveMetaInChart, saveSettings.saveEventsInChart);
var meta = filteredChart.meta;
#if sys
@@ -191,15 +215,16 @@ class Chart {
return filteredChart;
}
public static function filterChartForSaving(chart:ChartData, ?saveMetaInChart:Null<Bool>):ChartData {
public static function filterChartForSaving(chart:ChartData, ?saveMetaInChart:Null<Bool>, ?saveEventsInChart:Null<Bool>):ChartData {
var data = Reflect.copy(chart); // make a copy of the chart to leave the OG intact
if (saveMetaInChart != true) {
data.meta = null;
} else {
data.meta = Reflect.copy(chart.meta); // also make a copy of the metadata to leave the OG intact.
if (data.meta != null) data.meta.parsedColor = null;
if(data.meta != null && Reflect.hasField(data.meta, "parsedColor")) Reflect.deleteField(data.meta, "parsedColor");
}
data.events = saveEventsInChart != true ? null : Reflect.copy(chart.events); // same here once again
data.fromMods = null;
var sortedData:Dynamic = {};
@@ -215,5 +240,6 @@ class Chart {
typedef ChartSaveSettings = {
var ?overrideExistingMeta:Bool;
var ?saveMetaInChart:Bool;
var ?saveEventsInChart:Bool;
var ?prettyPrint:Bool;
}
@@ -94,7 +94,7 @@ class GlobalScript {
var path = Paths.script('data/global/LIB_$i');
var script = Script.create(path);
if (script is DummyScript) continue;
script.fileName = '$i:global.hx';
script.remappedNames.set(script.fileName, '$i:${script.fileName}');
scripts.add(script);
script.load();
}
+13 -14
View File
@@ -1,21 +1,17 @@
package funkin.backend.scripting;
import haxe.io.Path;
import hscript.Expr.ClassDecl;
import hscript.Expr.ModuleDecl;
import hscript.Expr.Error;
import hscript.Parser;
import openfl.Assets;
import lime.utils.AssetType;
import hscript.*;
class HScript extends Script {
public var interp:Interp;
public var parser:Parser;
public var expr:Expr;
public var decls:Array<ModuleDecl> = null;
public var code:String;
public var folderlessPath:String;
public var code:String = null;
//public var folderlessPath:String;
var __importedPaths:Array<String>;
public static function initParser() {
@@ -30,10 +26,12 @@ class HScript extends Script {
interp = new Interp();
// We use getAssetSafe to prevent crash from empty file (null which would check root (big crash)) -lunar
code = Assets.exists(path) ? Paths.assetsTree.getAssetSafe(path, TEXT) : null;
try {
if(Assets.exists(rawPath)) code = Assets.getText(rawPath);
} catch(e) Logs.trace('Error while reading $path: ${Std.string(e)}', ERROR);
parser = initParser();
folderlessPath = Path.directory(path);
//folderlessPath = Path.directory(path);
__importedPaths = [path];
interp.errorHandler = _errorHandler;
@@ -48,14 +46,13 @@ class HScript extends Script {
}));
funkin.backend.scripting.GlobalScript.call("onScriptCreated", [this, "hscript"]);
if (code != null && code.trim() != "")
loadFromString(code);
loadFromString(code);
}
public override function loadFromString(code:String) {
try {
expr = parser.parseString(code, fileName);
if (code != null && code.trim() != "")
expr = parser.parseString(code, fileName);
} catch(e:Error) {
_errorHandler(e);
} catch(e) {
@@ -94,7 +91,9 @@ class HScript extends Script {
}
private function _errorHandler(error:Error) {
var fileName = error.origin;
if(remappedNames.exists(fileName))
fileName = remappedNames.get(fileName);
var fn = '$fileName:${error.line}: ';
var err = error.toString();
if (err.startsWith(fn)) err = err.substr(fn.length);
+19
View File
@@ -124,13 +124,22 @@ class Script extends FlxBasic implements IFlxDestroyable {
*/
public var fileName:String;
/**
* Script Extension
*/
public var extension:String;
/**
* Path to the script.
*/
public var path:String = null;
private var rawPath:String = null;
private var didLoad:Bool = false;
public var remappedNames:Map<String, String> = [];
/**
* Creates a script from the specified asset path. The language is automatically determined.
* @param path Path in assets
@@ -177,7 +186,11 @@ class Script extends FlxBasic implements IFlxDestroyable {
public function new(path:String) {
super();
rawPath = path;
path = Paths.getFilenameFromLibFile(path);
fileName = Path.withoutDirectory(path);
extension = Path.extension(path);
this.path = path;
onCreate(path);
for(k=>e in getDefaultVariables(this)) {
@@ -223,6 +236,9 @@ class Script extends FlxBasic implements IFlxDestroyable {
* Traces something as this script.
*/
public function trace(v:Dynamic) {
var fileName = this.fileName;
if(remappedNames.exists(fileName))
fileName = remappedNames.get(fileName);
Logs.traceColored([
Logs.logText('${fileName}: ', GREEN),
Logs.logText(Std.string(v))
@@ -280,6 +296,9 @@ class Script extends FlxBasic implements IFlxDestroyable {
* @param additionalInfo Additional information you could provide.
*/
public function error(text:String, ?additionalInfo:Dynamic):Void {
var fileName = this.fileName;
if(remappedNames.exists(fileName))
fileName = remappedNames.get(fileName);
Logs.traceColored([
Logs.logText(fileName, RED),
Logs.logText(text)
@@ -7,7 +7,7 @@ final class AmountEvent extends CancellableEvent {
public var amount:Int;
/**
* Shows wether or not psych users complained about this class
* Shows whether or not psych users complained about this class
*/
public var psychUsersComplained:Bool = true;
}
}
@@ -0,0 +1,12 @@
package funkin.backend.scripting.events;
/**
* Event with random dynamic values useful for making your own custom events
*/
final class DynamicEvent extends CancellableEvent {
public var value1:Dynamic = null;
public var value2:Dynamic = null;
public var value3:Dynamic = null;
public var value4:Dynamic = null;
public var value5:Dynamic = null;
}
@@ -16,7 +16,7 @@ final class GameOverCreationEvent extends CancellableEvent
public var gameOverSong:String;
public var bpm:Int;
public var bpm:Float;
public var lossSFX:String;
@@ -0,0 +1,10 @@
package funkin.backend.scripting.events.dialogue;
import funkin.game.cutscenes.dialogue.DialogueCharacter;
final class DialogueBoxCharPopupEvent extends CancellableEvent
{
public var char:DialogueCharacter;
public var force:Bool;
}
@@ -0,0 +1,25 @@
package funkin.backend.scripting.events.dialogue;
import funkin.backend.utils.XMLUtil.TextFormat;
import flixel.sound.FlxSound;
final class DialogueBoxPlayBubbleEvent extends CancellableEvent
{
public var bubble:String;
public var suffix:String;
public var text:String;
public var format:Array<TextFormat>;
public var speed:Float;
public var customSFX:FlxSound;
public var customTypeSFX:Array<FlxSound>;
public var setTextAfter:Bool;
public var allowDefault:Bool;
}
@@ -0,0 +1,15 @@
package funkin.backend.scripting.events.dialogue;
import funkin.backend.utils.XMLUtil.TextFormat;
import flixel.sound.FlxSound;
final class DialogueBoxSetTextEvent extends CancellableEvent
{
public var text:String;
public var format:Array<TextFormat>;
public var speed:Null<Float>;
public var customTypeSFX:Array<FlxSound>;
}
@@ -0,0 +1,15 @@
package funkin.backend.scripting.events.dialogue;
import haxe.xml.Access;
/**
* CANCEL this event to customize from 0 the xml structure part!
*/
final class DialogueBoxStructureEvent extends CancellableEvent
{
public var name:String;
public var textTypeSFX:String;
public var dialogueBoxData:Access;
}
@@ -0,0 +1,10 @@
package funkin.backend.scripting.events.dialogue;
import funkin.game.cutscenes.dialogue.DialogueCharacter.DialogueCharAnimContext;
final class DialogueCharHideEvent extends CancellableEvent
{
public var animation:String;
public var lastAnimContext:DialogueCharAnimContext;
}
@@ -0,0 +1,14 @@
package funkin.backend.scripting.events.dialogue;
import funkin.game.cutscenes.dialogue.DialogueCharacter.DialogueCharAnimContext;
final class DialogueCharShowEvent extends CancellableEvent
{
public var x:Float;
public var y:Float;
public var animation:String;
public var lastAnimContext:DialogueCharAnimContext;
}
@@ -0,0 +1,15 @@
package funkin.backend.scripting.events.dialogue;
import haxe.xml.Access;
/**
* CANCEL this event to customize from 0 the xml structure part!
*/
final class DialogueCharStructureEvent extends CancellableEvent
{
public var name:String;
public var position:String;
public var charData:Access;
}
@@ -0,0 +1,11 @@
package funkin.backend.scripting.events.dialogue;
import haxe.xml.Access;
/**
* CANCEL this event to customize from 0 the xml structure part!
*/
final class DialogueStructureEvent extends CancellableEvent
{
public var dialogueData:Access;
}
@@ -0,0 +1,40 @@
package funkin.backend.utils;
import flixel.sound.FlxSound;
import lime.media.AudioBuffer;
// Thank you yosh :DDD -lunar
// (https://github.com/FNF-CNE-Devs/YoshiCrafterEngine/blob/main/source/WaveformSprite.hx)
class AudioAnalyzer {
public var buffer:AudioBuffer;
var __peakByte:Float = 0;
var __timeMulti:Float = 0;
public function new(sound:FlxSound) {
@:privateAccess buffer = sound._sound.__buffer;
__peakByte = Math.pow(2, buffer.bitsPerSample-1)-1;
__timeMulti = 1 / (1 / buffer.sampleRate);
__timeMulti *= buffer.bitsPerSample;
__timeMulti -= __timeMulti % buffer.bitsPerSample;
}
public function analyze(startPos:Float, endPos:Float):Float {
var bytesStartPos:Int = Math.floor(startPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample;
var bytesEndPos:Int = Math.floor(endPos * __timeMulti / 4000 / buffer.bitsPerSample) * buffer.bitsPerSample;
bytesStartPos -= bytesStartPos % buffer.bitsPerSample;
bytesEndPos -= bytesEndPos % buffer.bitsPerSample;
var maxByte:Int = 0;
for(i in 0...Math.floor((bytesEndPos - bytesStartPos) / buffer.bitsPerSample)) {
var byte:Int = buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample))
| (buffer.data.buffer.get(bytesStartPos + (i * buffer.bitsPerSample) + 1) << 8);
if (byte > 256 * 128) byte -= 256 * 256;
if (maxByte < byte) maxByte = byte;
}
return maxByte/__peakByte;
}
}
+2 -2
View File
@@ -19,7 +19,7 @@ class BitmapUtil {
for(x in 0...bmap.width) {
color = bmap.getPixel32(x, y);
fixedColor = 0xFF000000 + (color % 0x1000000);
if (colorMap[fixedColor] == null)
if (!colorMap.exists(fixedColor))
colorMap[fixedColor] = 0;
colorMap[fixedColor] += color.alphaFloat;
}
@@ -50,7 +50,7 @@ class BitmapUtil {
for(x in 0...bmap.width) {
color = bmap.getPixel32(x, y);
fixedColor = 0xFF000000 + (color % 0x1000000);
if (colorMap[fixedColor] == null)
if (!colorMap.exists(fixedColor))
colorMap[fixedColor] = 0;
colorMap[fixedColor] += color.alphaFloat * 0.33 + (0.67 * (color.saturation * (2 * (color.lightness > 0.5 ? 0.5 - (color.lightness) : color.lightness))));
}
+45 -1
View File
@@ -1,5 +1,8 @@
package funkin.backend.utils;
import flixel.text.FlxText;
import openfl.text.TextFormatAlign;
import funkin.backend.utils.XMLUtil.TextFormat;
import flixel.tweens.FlxTween;
import flixel.system.frontEnds.SoundFrontEnd;
import flixel.sound.FlxSound;
@@ -457,6 +460,47 @@ class CoolUtil
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<TextFormat>) {
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
@@ -618,7 +662,7 @@ class CoolUtil
frontEnd.music = music;
}
public static inline function flxeaseFromString(mainEase:String, suffix:String)
@:noUsing public static inline function flxeaseFromString(mainEase:String, suffix:String)
return Reflect.field(FlxEase, mainEase + (mainEase == "linear" ? "" : suffix));
}
+94 -17
View File
@@ -1,14 +1,17 @@
package funkin.backend.utils;
import flixel.animation.FlxAnimation;
import funkin.backend.FunkinSprite;
import funkin.backend.system.ErrorCode;
import funkin.backend.FunkinSprite.XMLAnimType;
import funkin.backend.system.interfaces.IBeatReceiver;
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.
*/
@@ -61,16 +64,14 @@ class XMLUtil {
}
/**
* Creates a new sprite based on a XML node.
* Overrides a sprite based on a XML node.
*/
public static function createSpriteFromXML(node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT, ?cl:Class<FunkinSprite>):FunkinSprite {
public static function loadSpriteFromXML(spr:FunkinSprite, node:Access, parentFolder:String = "", defaultAnimType:XMLAnimType = BEAT):FunkinSprite {
if (parentFolder == null) parentFolder = "";
var spr:FunkinSprite = cl != null ? Type.createInstance(cl, []) : new FunkinSprite();
spr.name = node.getAtt("name");
spr.antialiasing = true;
spr.loadSprite(Paths.image('$parentFolder${node.getAtt("sprite")}', null, true));
spr.loadSprite(Paths.image('$parentFolder${node.getAtt("sprite").getDefault(spr.name)}', null, true));
spr.spriteAnimType = defaultAnimType;
if (node.has.type) {
@@ -88,15 +89,14 @@ class XMLUtil {
if (node.has.scroll) {
var scroll:Null<Float> = Std.parseFloat(node.att.scroll);
if (scroll.isNotNull()) spr.scrollFactor.set(scroll, scroll);
} else {
if (node.has.scrollx) {
var scroll:Null<Float> = Std.parseFloat(node.att.scrollx);
if (scroll.isNotNull()) spr.scrollFactor.x = scroll;
}
if (node.has.scrolly) {
var scroll:Null<Float> = Std.parseFloat(node.att.scrolly);
if (scroll.isNotNull()) spr.scrollFactor.y = scroll;
}
}
if (node.has.scrollx) {
var scroll:Null<Float> = Std.parseFloat(node.att.scrollx);
if (scroll.isNotNull()) spr.scrollFactor.x = scroll;
}
if (node.has.scrolly) {
var scroll:Null<Float> = Std.parseFloat(node.att.scrolly);
if (scroll.isNotNull()) spr.scrollFactor.y = scroll;
}
if (node.has.skewx) {
var skew:Null<Float> = Std.parseFloat(node.att.skewx);
@@ -107,6 +107,14 @@ class XMLUtil {
if (skew.isNotNull()) spr.skew.y = skew;
}
if (node.has.antialiasing) spr.antialiasing = node.att.antialiasing == "true";
if (node.has.width) {
var width:Null<Float> = Std.parseFloat(node.att.width);
if (width.isNotNull()) spr.width = width;
}
if (node.has.height) {
var height:Null<Float> = Std.parseFloat(node.att.height);
if (height.isNotNull()) spr.height = height;
}
if (node.has.scale) {
var scale:Null<Float> = Std.parseFloat(node.att.scale);
if (scale.isNotNull()) spr.scale.set(scale, scale);
@@ -119,14 +127,29 @@ class XMLUtil {
var scale:Null<Float> = Std.parseFloat(node.att.scaley);
if (scale.isNotNull()) spr.scale.y = scale;
}
if (node.has.graphicSize) {
var graphicSize:Null<Int> = Std.parseInt(node.att.graphicSize);
if (graphicSize.isNotNull()) spr.setGraphicSize(graphicSize, graphicSize);
}
if (node.has.graphicSizex) {
var graphicSizex:Null<Int> = Std.parseInt(node.att.graphicSizex);
if (graphicSizex.isNotNull()) spr.setGraphicSize(graphicSizex);
}
if (node.has.graphicSizey) {
var graphicSizey:Null<Int> = 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)
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)
@@ -156,6 +179,15 @@ class XMLUtil {
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<FunkinSprite>, ?args:Array<Dynamic>):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,
@@ -251,6 +283,51 @@ class XMLUtil {
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<Xml, Access>, currentFormat:Dynamic = null, parsedSegments:Array<TextFormat> = null):Array<TextFormat> {
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 = {
+99 -56
View File
@@ -1,5 +1,7 @@
package funkin.editors.charter;
import openfl.geom.Rectangle;
import flixel.util.FlxSpriteUtil;
import funkin.editors.ui.UIContextMenu.UIContextMenuOptionSpr;
import funkin.editors.ui.UITopMenu.UITopMenuButton;
import funkin.editors.charter.CharterStrumline;
@@ -21,6 +23,8 @@ import funkin.editors.ui.UIContextMenu.UIContextMenuOption;
import funkin.editors.ui.UIState;
import openfl.net.FileReference;
using flixel.util.FlxColor;
class Charter extends UIState {
public static var __song:String;
static var __diff:String;
@@ -123,6 +127,27 @@ class Charter extends UIState {
onSelect: _file_saveas,
},
null,
{
label: "Save Without Events",
keybind: [CONTROL, ALT, TAB, S],
onSelect: _file_save_no_events,
},
{
label: "Save Without Events As...",
keybind: [CONTROL, SHIFT, ALT, TAB, S],
onSelect: _file_saveas_no_events,
},
{
label: "Save Events Separately",
keybind: [CONTROL, TAB, S],
onSelect: _file_events_save,
},
{
label: "Save Events Separately As...",
keybind: [CONTROL, SHIFT, TAB, S],
onSelect: _file_events_saveas,
},
null,
{
label: "Save Meta",
keybind: [CONTROL, ALT, S],
@@ -338,6 +363,8 @@ class Charter extends UIState {
uiCamera.bgColor = 0;
FlxG.cameras.add(uiCamera);
for (camera in FlxG.cameras.list) camera.antialiasing = false;
charterBG = new FunkinSprite(0, 0, Paths.image('menus/menuDesat'));
charterBG.color = 0xFF181818;
charterBG.cameras = [charterCamera];
@@ -359,10 +386,7 @@ class Charter extends UIState {
selectionBox.scrollFactor.set(1, 1);
selectionBox.incorporeal = true;
noteHoverer = new CharterNote();
noteHoverer.snappedToStrumline = noteHoverer.selectable = noteHoverer.autoAlpha = false;
@:privateAccess noteHoverer.__animSpeed = 1.25; noteHoverer.typeText.visible = false;
noteHoverer.alpha = 0; // FUCK YOU NOTE HOVERER >:(((
noteHoverer = new CharterNoteHoverer();
selectionBox.cameras = notesGroup.cameras = gridBackdrops.cameras = noteHoverer.cameras = [charterCamera];
@@ -381,7 +405,7 @@ class Charter extends UIState {
uiGroup.add(scrollBar);
songPosInfo = new UIText(FlxG.width - 30 - 400, scrollBar.y + 10, 400, "00:00\nBeat: 0\nStep: 0\nMeasure: 0\nBPM: 0\nTime Signature: 4/4");
songPosInfo.alignment = RIGHT; songPosInfo.optimized = true;
songPosInfo.alignment = RIGHT;
uiGroup.add(songPosInfo);
playBackSlider = new UISlider(FlxG.width - 160 - 26 - 20, (23/2) - (12/2), 160, 1, [{start: 0.25, end: 1, size: 0.5}, {start: 1, end: 2, size: 0.5}], true);
@@ -529,8 +553,7 @@ class Charter extends UIState {
buildNoteTypesUI();
refreshBPMSensitive();
// Cool shit :DD
__relinkSelection();
// Undo Stuffs :D
__relinkUndos();
__applyPlaytestInfo();
}
@@ -574,6 +597,7 @@ class Charter extends UIState {
else
selection = [cast n];
}
if (FlxG.mouse.justReleasedRight) {
var mousePos = FlxG.mouse.getScreenPosition(uiCamera);
if (!selection.contains(cast n))
@@ -585,7 +609,8 @@ class Charter extends UIState {
}
});
}
for(n in selection) n.selected = true;
selection = __fixSelection(selection);
for(s in selection) s.selected = true;
/**
* NOTE DRAG HANDLING
@@ -621,6 +646,8 @@ class Charter extends UIState {
if (n.handleSelection(selectionBox))
selection.push(n);
}
selection = __fixSelection(selection);
gridActionType = NONE;
}
}
@@ -645,22 +672,16 @@ class Charter extends UIState {
});
currentCursor = HAND;
} else {
var hoverOffset:FlxPoint = FlxPoint.get();
for (s in selection)
if (s.hovered) {
hoverOffset.set(mousePos.x - s.x, mousePos.y - s.y);
break;
}
dragStartPos.set(Std.int(dragStartPos.x / 40) * 40, dragStartPos.y); //credits to burgerballs
var verticalChange:Float = ((mousePos.y - hoverOffset.y) - dragStartPos.y) / 40;
dragStartPos.set(Std.int(dragStartPos.x / 40) * 40, dragStartPos.y);
var verticalChange:Float = (mousePos.y - dragStartPos.y) / 40;
var horizontalChange:Int = CoolUtil.floorInt((mousePos.x - dragStartPos.x) / 40);
var undoDrags:Array<SelectionDragChange> = [];
for (s in selection) {
if (s.draggable) {
var changePoint:FlxPoint = FlxPoint.get(verticalChange, horizontalChange);
if (!FlxG.keys.pressed.SHIFT) changePoint.x -= ((s.step + verticalChange) - quantStepRounded(s.step+verticalChange));
if (!FlxG.keys.pressed.SHIFT)
changePoint.x -= ((s.step + verticalChange) - quantStepRounded(s.step+verticalChange, verticalChange > 0 ? 0.35 : 0.65));
var boundedChange:FlxPoint = changePoint.clone();
@@ -687,12 +708,9 @@ class Charter extends UIState {
undos.addToUndo(CSelectionDrag(undoDrags));
}
hoverOffset.put();
gridActionType = NONE;
currentCursor = ARROW;
}
case NONE:
if (FlxG.mouse.justPressed)
FlxG.mouse.getWorldPosition(charterCamera, dragStartPos);
@@ -751,23 +769,6 @@ class Charter extends UIState {
if (event != null) addEventSpr.updateEdit(event);
else addEventSpr.updatePos(FlxG.keys.pressed.SHIFT ? ((mousePos.y) / 40) : quantStepRounded(mousePos.y/40));
} else addEventSpr.sprAlpha = lerp(addEventSpr.sprAlpha, 0, 0.25);
// Note Hoverer
if (mousePos.x > 0 && mousePos.x < gridBackdrops.strumlinesAmount * 160 && gridActionType == NONE && inBoundsY) {
noteHoverer.alpha = lerp(noteHoverer.alpha, 0.35, 0.25);
if (noteHoverer.id != Math.floor(mousePos.x / 40) % 4) {
@:privateAccess noteHoverer.__doAnim = false; // Un comment to restore this
noteHoverer.updatePos(
FlxMath.bound(FlxG.keys.pressed.SHIFT ? ((mousePos.y) / 40) : quantStep((mousePos.y+20)/40), 0, __endStep-1),
Math.floor(mousePos.x / 40) % 4, 0, 0, null
);
} else {
noteHoverer.step = FlxMath.bound(FlxG.keys.pressed.SHIFT ? ((mousePos.y-20) / 40) : quantStep(mousePos.y/40), 0, __endStep-1);
noteHoverer.y = noteHoverer.step * 40;
}
noteHoverer.x = lerp(noteHoverer.x, Math.floor(mousePos.x / 40) * 40, .65);
} else
noteHoverer.alpha = lerp(noteHoverer.alpha, 0, 0.25);
}
public function quantStep(step:Float):Float {
@@ -775,11 +776,14 @@ class Charter extends UIState {
return Math.floor(step/stepMulti) * stepMulti;
}
public function quantStepRounded(step:Float):Float {
public function quantStepRounded(step:Float, ?roundRatio:Float = 0.5):Float {
var stepMulti:Float = 1/(quant/16);
return Math.round(step/stepMulti) * stepMulti;
return ratioRound(step/stepMulti, roundRatio) * stepMulti;
}
public function ratioRound(val:Float, ratio:Float):Int
return Math.floor(val) + ((Math.abs(val % 1) > ratio ? 1 : 0) * (val > 0 ? 1 : -1));
public function getHoveredEvent(y:Float) {
var eventHovered:CharterEvent = null;
eventsGroup.forEach(function(e) {
@@ -1116,6 +1120,22 @@ class Charter extends UIState {
undos.save();
}
function _file_save_no_events(_) {
#if sys
saveTo('${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}', true);
undos.save();
return;
#end
_file_saveas(_);
}
function _file_saveas_no_events(_) {
openSubState(new SaveSubstate(Json.stringify(Chart.filterChartForSaving(PlayState.SONG, false, false)), {
defaultSaveFile: '${__diff.toLowerCase()}.json'
}));
undos.save();
}
function _file_meta_save(_) {
#if sys
sys.io.File.saveContent(
@@ -1133,10 +1153,29 @@ class Charter extends UIState {
}));
}
function _file_events_save(_) {
#if sys
sys.io.File.saveContent(
'${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}/events.json',
Json.stringify({events: PlayState.SONG.events == null ? [] : PlayState.SONG.events})
);
return;
#end
_file_events_saveas(_);
}
function _file_events_saveas(_) {
#if sys
openSubState(new SaveSubstate(Json.stringify({events: PlayState.SONG.events == null ? [] : PlayState.SONG.events}), {
defaultSaveFile: 'events.json'
}));
#end
}
#if sys
function saveTo(path:String) {
function saveTo(path:String, separateEvents:Bool = false) {
buildChart();
Chart.save(path, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: false});
Chart.save(path, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: false, saveEventsInChart: !separateEvents});
}
#end
@@ -1426,11 +1465,10 @@ class Charter extends UIState {
if (note.step > Conductor.curMeasure*Conductor.getMeasureLength() && note.step < (Conductor.curMeasure+1)*Conductor.getMeasureLength()) note
];
}
#end
function changeNoteSustain(change:Float) {
if (selection.length <= 0 || change == 0) return;
if (selection.length <= 0 || change == 0 || gridActionType != NONE) return;
var undoChanges:Array<NoteSustainChange> = [];
for(s in selection)
@@ -1578,9 +1616,15 @@ class Charter extends UIState {
public inline function hitsoundsEnabled(id:Int)
return strumLines.members[id] != null && strumLines.members[id].hitsounds;
public inline function __fixSelection(selection:Selection):Selection {
var newSelection:Selection = new Selection();
for (s in selection) if (newSelection.indexOf(s) == -1) newSelection.push(s);
return newSelection.filter((s:ICharterSelectable) -> {return s != null;});
}
// UH OH!!! DANGER ZONE APPOARCHING !!!! LUNARS SHITTY CODE !!!! -lunar
@:noCompletion public function __fixSingleSelection(selectable:ICharterSelectable):ICharterSelectable {
@:noCompletion public function __relinkSingleSelection(selectable:ICharterSelectable):ICharterSelectable {
if (selectable is CharterNote)
return selectable.ID == -1 ? cast(selectable, CharterNote) : notesGroup.members[selectable.ID];
else if (selectable is CharterEvent)
@@ -1588,30 +1632,29 @@ class Charter extends UIState {
return null;
}
@:noCompletion public function __fixSelection(selection:Selection) @:privateAccess {
@:noCompletion public function __relinkSelection(selection:Selection) @:privateAccess {
var newSelection:Selection = new Selection();
for (i => selectable in selection)
newSelection[i] = __fixSingleSelection(selectable);
newSelection[i] = __relinkSingleSelection(selectable);
return newSelection;
}
@:noCompletion public inline function __relinkSelection()
selection = __fixSelection(selection);
@:noCompletion public inline function __relinkUndos() {
selection = __relinkSelection(selection);
for (list => changeList in [undos.undoList, undos.redoList]) {
var newChanges:Array<CharterChange> = [];
for (i => change in changeList) {
switch (change) {
case CCreateSelection(selection):
newChanges[i] = CCreateSelection(__fixSelection(selection));
newChanges[i] = CCreateSelection(__relinkSelection(selection));
case CDeleteSelection(selection):
newChanges[i] = CDeleteSelection(__fixSelection(selection));
newChanges[i] = CDeleteSelection(__relinkSelection(selection));
case CSelectionDrag(selectionDrags):
newChanges[i] = CSelectionDrag([
for (selectionDrag in selectionDrags)
{
selectable: __fixSingleSelection(selectionDrag.selectable),
selectable: __relinkSingleSelection(selectionDrag.selectable),
change: selectionDrag.change
}
]);
@@ -1619,17 +1662,17 @@ class Charter extends UIState {
newChanges[i] = CEditSustains([
for (noteChange in noteChanges)
{
note: cast(__fixSingleSelection(noteChange.note), CharterNote),
note: cast(__relinkSingleSelection(noteChange.note), CharterNote),
before: noteChange.before,
after: noteChange.after
}
]);
case CEditEvent(event, oldEvents, newEvents):
newChanges[i] = CEditEvent(cast(__fixSingleSelection(event), CharterEvent), oldEvents, newEvents);
newChanges[i] = CEditEvent(cast(__relinkSingleSelection(event), CharterEvent), oldEvents, newEvents);
case CEditSpecNotesType(notesChanged, oldNoteTypes, newNoteTypes):
newChanges[i] = CEditSpecNotesType([
for (noteChanged in notesChanged)
cast(__fixSingleSelection(noteChanged), CharterNote)
cast(__relinkSingleSelection(noteChanged), CharterNote)
], oldNoteTypes, newNoteTypes);
default: newChanges[i] = change;
}
@@ -10,6 +10,9 @@ class CharterEventTypeSelection extends UISubstateWindow {
var buttonsBG:UISliceSprite;
var buttonCameras:FlxCamera;
var upIndicator:UIText;
var downIndicator:UIText;
public function new(callback:String->Void) {
super();
this.callback = callback;
@@ -57,10 +60,23 @@ class CharterEventTypeSelection extends UISubstateWindow {
close();
}, w));
upIndicator = new UIText(0, 0, 0, "↑", 18);
upIndicator.cameras = [buttonCameras];
upIndicator.scrollFactor.set();
upIndicator.borderSize = 2;
add(upIndicator);
downIndicator = new UIText(0, 0, 0, "↓", 18);
downIndicator.cameras = [buttonCameras];
downIndicator.scrollFactor.set();
downIndicator.borderSize = 2;
add(downIndicator);
}
var sinner:Float = 0;
override function update(elapsed:Float) {
super.update(elapsed);
sinner += elapsed;
for (button in buttons)
button.selectable = buttonsBG.hovered;
@@ -73,6 +89,12 @@ class CharterEventTypeSelection extends UISubstateWindow {
if (buttons.length > 16)
buttonCameras.scroll.y = FlxMath.bound(buttonCameras.scroll.y - (buttonsBG.hovered ? FlxG.mouse.wheel : 0) * 12, 0,
(buttons[buttons.length-1].y + buttons[buttons.length-1].bHeight) - buttonCameras.height);
upIndicator.setPosition((buttonsBG.bWidth/2) - (upIndicator.fieldWidth/2), 22 + (FlxMath.fastSin(sinner*2) * 4));
downIndicator.setPosition((buttonsBG.bWidth/2) - (downIndicator.fieldWidth/2), (buttonsBG.bHeight-downIndicator.height-22) - (FlxMath.fastSin(sinner*2) * 4));
upIndicator.alpha = CoolUtil.fpsLerp(upIndicator.alpha, (buttons[0].y + buttonCameras.scroll.y > buttons[0].bHeight) ? 1 : 0, 1/3);
downIndicator.alpha = CoolUtil.fpsLerp(downIndicator.alpha, (buttonCameras.scroll.y+buttonCameras.height > buttons[buttons.length-1].y) ? 0 : 1, 1/3);
}
override function destroy() {
@@ -78,6 +78,7 @@ class CharterNote extends UISprite implements ICharterSelectable {
typeText.exists = type != 0;
typeText.text = Std.string(this.type);
typeText.follow(this, 20 - (typeText.frameWidth/2), 20 - (typeText.frameHeight/2));
y = step * 40;
@@ -85,6 +86,7 @@ class CharterNote extends UISprite implements ICharterSelectable {
sustainSpr.updateHitbox();
sustainSpr.exists = susLength != 0;
sustainSpr.alpha = alpha;
sustainSpr.follow(this, 15, 20);
if (angleTween != null) angleTween.cancel();
@@ -0,0 +1,86 @@
package funkin.editors.charter;
class CharterNoteHoverer extends CharterNote {
public function new() {
super();
snappedToStrumline = selectable = autoAlpha = false; visible = sustainSpr.visible = false;
@:privateAccess __animSpeed = 1.25; typeText.visible = false; alpha = 0.4;
}
@:noCompletion var __mousePos:FlxPoint = FlxPoint.get();
public override function update(elapsed:Float) @:privateAccess {
FlxG.mouse.getWorldPosition(FlxG.camera, __mousePos);
switch (Charter.instance.gridActionType) {
case NONE:
var inBoundsY:Bool = (__mousePos.y > 0 && __mousePos.y < (Charter.instance.__endStep)*40);
if (__mousePos.x > 0 && __mousePos.x < Charter.instance.gridBackdrops.strumlinesAmount * 160 && inBoundsY) {
step = FlxMath.bound(FlxG.keys.pressed.SHIFT ? ((__mousePos.y-20) / 40) : Charter.instance.quantStep(__mousePos.y/40), 0, Charter.instance.__endStep-1);
id = Math.floor(__mousePos.x / 40); y = step * 40; x = id * 40; visible = true; sustainSpr.visible = typeText.visible = false;
angle = switch(animation.curAnim.curFrame = (id % 4)) {
case 0: -90;
case 1: 180;
case 2: 0;
case 3: 90;
default: 0; // how is that even possible
};
} else
visible = false;
case DRAG:
visible = sustainSpr.visible = typeText.visible = true; __doAnim = false;
default:
visible = sustainSpr.visible = typeText.visible = false; __doAnim = false;
}
}
public override function draw() @:privateAccess {
switch (Charter.instance.gridActionType) {
case NONE:
super.draw();
case DRAG:
if (Charter.instance.gridActionType == DRAG) {
var verticalChange:Float = (__mousePos.y - Charter.instance.dragStartPos.y) / 40;
var horizontalChange:Int = CoolUtil.floorInt((__mousePos.x - (Std.int(Charter.instance.dragStartPos.x / 40) * 40)) / 40);
for (s in Charter.selection) {
if (s != null && s.draggable && s is CharterNote) {
var draggingNote:CharterNote = cast(s, CharterNote);
y = (draggingNote.step + verticalChange);
if (!FlxG.keys.pressed.SHIFT)
y -= ((draggingNote.step + verticalChange)
- Charter.instance.quantStepRounded(draggingNote.step+verticalChange, verticalChange > 0 ? 0.35 : 0.65));
y *= 40;
var newID:Int = Std.int(FlxMath.bound(draggingNote.fullID + horizontalChange, 0, (Charter.instance.strumLines.members.length*4)-1));
x = (id=newID) * 40; y = FlxMath.bound(y, 0, (Charter.instance.__endStep*40) - height);
angle = switch(animation.curAnim.curFrame = (draggingNote.id % 4)) {
case 0: -90;
case 1: 180;
case 2: 0;
case 3: 90;
default: 0; // how is that even possible
};
sustainSpr.scale.set(10, (40 * draggingNote.susLength) + (height/2));
sustainSpr.color = CharterNote.colors[animation.curAnim.curFrame];
sustainSpr.updateHitbox(); sustainSpr.alpha = alpha; sustainSpr.follow(this, 15, 20);
sustainSpr.exists = draggingNote.susLength != 0;
typeText.text = Std.string(draggingNote.type);
typeText.exists = draggingNote.type != 0;
typeText.follow(this, 20 - (typeText.frameWidth/2), 20 - (typeText.frameHeight/2));
super.draw();
}
}
}
default: // do nothing
}
}
public override function destroy() {
super.destroy();
__mousePos.put();
}
}
@@ -115,6 +115,7 @@ class CharterSelection extends EditorTreeMenu {
#end
var option = new EditorIconOption(creation.meta.name, "Press ACCEPT to choose a difficulty to edit.", creation.meta.icon, function() {
curSong = creation.meta;
var list:Array<OptionType> = [
for(d in creation.meta.difficulties)
if (d != "") new TextOption(d, "Press ACCEPT to edit the chart for the selected difficulty", function() {
@@ -153,10 +153,9 @@ class CharterStrumlineScreen extends UISubstateWindow {
}
else if (!suffixlist.contains(i)) suffixlist.push(i);
}
}
vocalsSuffixDropDown = new UIDropDown(typeDropdown.x, hudScaleStepper.y + 128, 200, 32, suffixlist, strumLine.vocalsSuffix != "" ? suffixlist.indexOf(strumLine.vocalsSuffix) : 0);
vocalsSuffixDropDown = new UIDropDown(typeDropdown.x, hudScaleStepper.y + 128, 200, 32, suffixlist, strumLine.vocalsSuffix != null && strumLine.vocalsSuffix != "" ? suffixlist.indexOf(strumLine.vocalsSuffix) : 0);
add(vocalsSuffixDropDown);
addLabelOn(vocalsSuffixDropDown, "Vocal Suffix");
}
+4
View File
@@ -16,6 +16,10 @@ class UIText extends FunkinText {
this.borderSize = 1;
}
this.active = false;
this.antialiasing = false;
// this.textField.antiAliasType = ADVANCED;
// this.textField.sharpness = 400;
}
public override function applyBorderStyle() {
+1 -1
View File
@@ -20,7 +20,7 @@ class GameOverSubstate extends MusicBeatSubstate
public var characterName:String;
public var gameOverSong:String;
public var gameOverSongBPM:Int;
public var gameOverSongBPM:Float;
public var lossSFXName:String;
public var retrySFX:String;
public var player:Bool;
+5
View File
@@ -117,6 +117,11 @@ class Note extends FlxSprite
this.isSustainNote = sustain;
this.sustainLength = sustainLength;
this.strumLine = strumLine;
for(field in Reflect.fields(noteData)) {
if(!["time", "id", "type", "sLen"].contains(field)) {
this.extra.set(field, Reflect.field(noteData, field));
}
}
x += 50;
// MAKE SURE ITS DEFINITELY OFF SCREEN?
+7
View File
@@ -71,6 +71,13 @@ class NoteGroup extends FlxTypedGroup<Note> {
@:privateAccess FlxCamera._defaultCameras = oldDefaultCameras;
}
/**
* Gets the correct order of notes
**/
public function get(id:Int) {
return members[length - 1 - id];
}
public override function forEach(noteFunc:Note->Void, recursive:Bool = false) {
i = length-1;
__loopSprite = null;
+56 -57
View File
@@ -563,19 +563,22 @@ class PlayState extends MusicBeatState
comboGroup.maxSize = 25;
#end
// SCRIPTS & STAGE INITIALISATION
// CAMERA, SCRIPTS & STAGE INITIALISATION
#if REGION
camFollow = new FlxObject(0, 0, 2, 2);
add(camFollow);
if (SONG.stage == null || SONG.stage.trim() == "") SONG.stage = "stage";
add(stage = new Stage(SONG.stage));
// var camPos:FlxPoint = new FlxPoint(dadMidpoint.x, dadMidpoint.y);
// dadMidpoint.put();
var camPos:FlxPoint = new FlxPoint(0, 0);
FlxG.camera.follow(camFollow, LOCKON, 0.04);
FlxG.camera.zoom = defaultCamZoom;
// camHUD.zoom = defaultHudZoom;
if (!chartingMode || Options.charterEnablePlaytestScripts) {
switch(SONG.meta.name) {
// case "":
// ADD YOUR HARDCODED SCRIPTSa HERE!
// ADD YOUR HARDCODED SCRIPTS HERE!
default:
var scriptsFolders:Array<String> = ['songs/${SONG.meta.name.toLowerCase()}/scripts', 'data/charts/', 'songs/'];
@@ -675,7 +678,7 @@ class PlayState extends MusicBeatState
scripts.call("create");
#end
// CAMERA & HUD INITIALISATION
// HUD INITIALISATION & CAMERA FINALISATION
#if REGION
var event = EventManager.get(AmountEvent).recycle(4);
if (!scripts.event("onPreGenerateStrums", event).cancelled) {
@@ -686,13 +689,7 @@ class PlayState extends MusicBeatState
for(str in strumLines)
str.generate(str.data, (chartingMode && Charter.startHere) ? Charter.startTime : null);
camFollow = new FlxObject(0, 0, 2, 2);
camFollow.setPosition(camPos.x, camPos.y);
add(camFollow);
FlxG.camera.follow(camFollow, LOCKON, 0.04);
FlxG.camera.zoom = defaultCamZoom;
// camHUD.zoom = defaultHudZoom;
// Its after all of that code for scripts stuff - Nex
if (smoothTransitionData != null && smoothTransitionData.stage == curStage) {
FlxG.camera.scroll.set(smoothTransitionData.camX, smoothTransitionData.camY);
FlxG.camera.zoom = smoothTransitionData.camZoom;
@@ -729,7 +726,7 @@ class PlayState extends MusicBeatState
scoreTxt = new FunkinText(healthBarBG.x + 50, healthBarBG.y + 30, Std.int(healthBarBG.width - 100), "Score:0", 16);
missesTxt = new FunkinText(healthBarBG.x + 50, healthBarBG.y + 30, Std.int(healthBarBG.width - 100), "Misses:0", 16);
accuracyTxt = new FunkinText(healthBarBG.x + 50, healthBarBG.y + 30, Std.int(healthBarBG.width - 100), "Acc:-% (N/A)", 16);
accuracyTxt = new FunkinText(healthBarBG.x + 50, healthBarBG.y + 30, Std.int(healthBarBG.width - 100), "Accuracy:-% (N/A)", 16);
accuracyTxt.addFormat(accFormat, 0, 1);
for(text in [scoreTxt, missesTxt, accuracyTxt]) {
@@ -739,6 +736,7 @@ class PlayState extends MusicBeatState
scoreTxt.alignment = RIGHT;
missesTxt.alignment = CENTER;
accuracyTxt.alignment = LEFT;
updateRatingStuff();
for(e in [healthBar, healthBarBG, iconP1, iconP2, scoreTxt, missesTxt, accuracyTxt])
e.cameras = [camHUD];
@@ -801,7 +799,7 @@ class PlayState extends MusicBeatState
*/
public function startCutscene(prefix:String = "", ?cutsceneScriptPath:String, ?callback:Void->Void, checkSeen:Bool = false) {
if (callback == null) callback = startCountdown;
if (checkSeen && seenCutscene) {
if ((checkSeen && seenCutscene) || !playCutscenes) {
callback();
return;
}
@@ -809,34 +807,31 @@ class PlayState extends MusicBeatState
if (cutsceneScriptPath == null)
cutsceneScriptPath = Paths.script('songs/${SONG.meta.name.toLowerCase()}/${prefix}cutscene');
if (playCutscenes) {
inCutscene = true;
var videoCutscene = Paths.video('${PlayState.SONG.meta.name.toLowerCase()}-${prefix}cutscene');
var videoCutsceneAlt = Paths.file('songs/${PlayState.SONG.meta.name.toLowerCase()}/${prefix}cutscene.mp4');
var dialogue = Paths.file('songs/${PlayState.SONG.meta.name.toLowerCase()}/${prefix}dialogue.xml');
persistentUpdate = true;
var toCall:Void->Void = function() {
if(checkSeen) seenCutscene = true;
callback();
}
inCutscene = true;
var videoCutscene = Paths.video('${PlayState.SONG.meta.name.toLowerCase()}-${prefix}cutscene');
var videoCutsceneAlt = Paths.file('songs/${PlayState.SONG.meta.name.toLowerCase()}/${prefix}cutscene.mp4');
var dialogue = Paths.file('songs/${PlayState.SONG.meta.name.toLowerCase()}/${prefix}dialogue.xml');
persistentUpdate = true;
var toCall:Void->Void = function() {
if(checkSeen) seenCutscene = true;
callback();
}
if (cutsceneScriptPath != null && Assets.exists(cutsceneScriptPath)) {
openSubState(new ScriptedCutscene(cutsceneScriptPath, toCall));
} else if (Assets.exists(dialogue)) {
MusicBeatState.skipTransIn = true;
openSubState(new DialogueCutscene(dialogue, toCall));
} else if (Assets.exists(videoCutsceneAlt)) {
MusicBeatState.skipTransIn = true;
persistentUpdate = false;
openSubState(new VideoCutscene(videoCutsceneAlt, toCall));
persistentDraw = false;
} else if (Assets.exists(videoCutscene)) {
MusicBeatState.skipTransIn = true;
persistentUpdate = false;
openSubState(new VideoCutscene(videoCutscene, toCall));
persistentDraw = false;
} else
callback();
if (cutsceneScriptPath != null && Assets.exists(cutsceneScriptPath)) {
openSubState(new ScriptedCutscene(cutsceneScriptPath, toCall));
} else if (Assets.exists(dialogue)) {
MusicBeatState.skipTransIn = true;
openSubState(new DialogueCutscene(dialogue, toCall));
} else if (Assets.exists(videoCutsceneAlt)) {
MusicBeatState.skipTransIn = true;
persistentUpdate = false;
openSubState(new VideoCutscene(videoCutsceneAlt, toCall));
persistentDraw = false;
} else if (Assets.exists(videoCutscene)) {
MusicBeatState.skipTransIn = true;
persistentUpdate = false;
openSubState(new VideoCutscene(videoCutscene, toCall));
persistentDraw = false;
} else
callback();
}
@@ -1072,8 +1067,6 @@ class PlayState extends MusicBeatState
public inline function getIconRPC():String
return SONG.meta.icon;
var __songPlaying:Bool = false;
var __wasAutoPause:Bool = false;
@:dox(hide)
override public function onFocus():Void
{
@@ -1188,8 +1181,10 @@ class PlayState extends MusicBeatState
function updateIconPositions() {
var iconOffset:Int = 26;
iconP1.x = healthBar.x + (healthBar.width * (FlxMath.remapToRange(healthBar.percent, 0, 100, 1, 0)) - iconOffset);
iconP2.x = healthBar.x + (healthBar.width * (FlxMath.remapToRange(healthBar.percent, 0, 100, 1, 0))) - (iconP2.width - iconOffset);
var center:Float = healthBar.x + healthBar.width * FlxMath.remapToRange(healthBar.percent, 0, 100, 1, 0);
iconP1.x = center - iconOffset;
iconP2.x = center - (iconP2.width - iconOffset);
health = FlxMath.bound(health, 0, maxHealth);
@@ -1197,17 +1192,7 @@ class PlayState extends MusicBeatState
iconP2.health = 1 - (healthBar.percent / 100);
}
@:dox(hide)
override public function update(elapsed:Float)
{
scripts.call("update", [elapsed]);
if (inCutscene) {
super.update(elapsed);
scripts.call("postUpdate", [elapsed]);
return;
}
function updateRatingStuff() {
scoreTxt.text = 'Score:$songScore';
missesTxt.text = '${comboBreaks ? "Combo Breaks" : "Misses"}:$misses';
@@ -1221,6 +1206,20 @@ class PlayState extends MusicBeatState
accuracyTxt._formatRanges[0].range.start = accuracyTxt.text.length - curRating.rating.length;
accuracyTxt._formatRanges[0].range.end = accuracyTxt.text.length;
}
}
@:dox(hide)
override public function update(elapsed:Float)
{
scripts.call("update", [elapsed]);
if (inCutscene) {
super.update(elapsed);
scripts.call("postUpdate", [elapsed]);
return;
}
updateRatingStuff();
if (controls.PAUSE && startedCountdown && canPause)
pauseGame();
+60 -58
View File
@@ -23,82 +23,84 @@ class SplashGroup extends FlxTypedGroup<FunkinSprite> {
* Creates a new Splash group
* @param path Path to the splash data (xml)
*/
public function new(path) {
public function new(path:String) {
super();
try {
xml = new Access(Xml.parse(Assets.getText(path)).firstElement());
if (!xml.has.sprite) throw "The <splash> element requires a sprite attribute.";
var splashSprite = new FunkinSprite();
splashSprite.antialiasing = true;
splashSprite.active = splashSprite.visible = false;
splashSprite.loadSprite(Paths.image(xml.att.sprite));
/**
* ANIMATIONS
*/
for(strum in xml.nodes.strum) {
var id:Null<Int> = Std.parseInt(strum.att.id);
if (id != null) {
animationNames[id] = [];
for(anim in strum.nodes.anim) {
if (!anim.has.name) continue;
XMLUtil.addXMLAnimation(splashSprite, anim, false);
animationNames[id].push(anim.att.name);
}
}
}
if (animationNames.length <= 0)
animationNames.push([]);
for(anim in xml.nodes.anim) {
if (!anim.has.name) continue;
XMLUtil.addXMLAnimation(splashSprite, anim, false);
for(a in animationNames) {
if (a == null) continue;
a.push(anim.att.name);
}
}
/**
* END OF ANIMATIONS
*/
splashSprite.animation.finishCallback = function(name) {
splashSprite.active = splashSprite.visible = false;
};
if (xml.has.scale) splashSprite.scale.scale(Std.parseFloat(xml.att.scale).getDefault(1));
if (xml.has.alpha) splashSprite.alpha = Std.parseFloat(xml.att.alpha).getDefault(1);
if (xml.has.antialiasing) splashSprite.antialiasing = xml.att.antialiasing == "true";
add(splashSprite);
for (i in 0...7) {
// make 7 additional splashes
var spr = FunkinSprite.copyFrom(splashSprite);
spr.animation.finishCallback = function(name) {
spr.active = spr.visible = false;
};
add(spr);
}
var splash = createSplash(xml.att.sprite);
setupAnims(xml, splash);
pregenerateSplashes(splash);
add(splash);
// immediatly draw once and put image in GPU to prevent freezes
// TODO: change to graphics cache
splashSprite.drawComplex(FlxG.camera);
} catch(e) {
splash.drawComplex(FlxG.camera);
} catch(e:Dynamic) {
Logs.trace('Couldn\'t parse splash data for "${path}": ${e.toString()}', ERROR);
valid = false;
}
maxSize = 8;
}
function createSplash(imagePath:String) {
var splash = new FunkinSprite();
splash.antialiasing = true;
splash.active = splash.visible = false;
splash.loadSprite(Paths.image(imagePath));
if (xml.has.scale) splash.scale.scale(Std.parseFloat(xml.att.scale).getDefault(1));
if (xml.has.alpha) splash.alpha = Std.parseFloat(xml.att.alpha).getDefault(1);
if (xml.has.antialiasing) splash.antialiasing = xml.att.antialiasing == "true";
return splash;
}
function setupAnims(xml:Access, splash:FunkinSprite) {
for(strum in xml.nodes.strum) {
var id:Null<Int> = Std.parseInt(strum.att.id);
if (id != null) {
animationNames[id] = [];
for(anim in strum.nodes.anim) {
if (!anim.has.name) continue;
XMLUtil.addXMLAnimation(splash, anim, false);
animationNames[id].push(anim.att.name);
}
}
}
// if (animationNames.length <= 0)
// animationNames.push([]);
for(anim in xml.nodes.anim) {
if (!anim.has.name) continue;
XMLUtil.addXMLAnimation(splash, anim, false);
for(a in animationNames) {
if (a == null) continue;
a.push(anim.att.name);
}
}
splash.animation.finishCallback = function(name:String) {
splash.active = splash.visible = false;
};
}
function pregenerateSplashes(splash:FunkinSprite) {
// make 7 additional splashes
for(i in 0...7) {
var spr = FunkinSprite.copyFrom(splash);
spr.animation.finishCallback = function(name:String) {
spr.active = spr.visible = false;
};
add(spr);
}
}
public function getSplashAnim(id:Int):String {
if (animationNames.length <= 0) return null;
id %= animationNames.length;
if (animationNames[id] == null) return null;
if (animationNames[id].length <= 0) return null;
return animationNames[id][FlxG.random.int(0, animationNames[id].length-1)];
if (animationNames[id] == null || animationNames[id].length <= 0) return null;
return animationNames[id][FlxG.random.int(0, animationNames[id].length - 1)];
}
var __splash:FunkinSprite;
+4 -4
View File
@@ -48,10 +48,10 @@ class Stage extends FlxBasic implements IBeatReceiver {
if (stageXML != null) {
if (PlayState.instance != null) {
if (stageXML.has.zoom) {
var parsed:Null<Float> = Std.parseFloat(stageXML.att.zoom);
if (parsed != null && PlayState.instance != null) PlayState.instance.defaultCamZoom = parsed;
}
var parsed:Null<Float>;
if(stageXML.has.startCamPosX && (parsed = Std.parseFloat(stageXML.att.startCamPosX)) != null) PlayState.instance.camFollow.x = parsed;
if(stageXML.has.startCamPosY && (parsed = Std.parseFloat(stageXML.att.startCamPosY)) != null) PlayState.instance.camFollow.y = parsed;
if(stageXML.has.zoom && (parsed = Std.parseFloat(stageXML.att.zoom)) != null) PlayState.instance.defaultCamZoom = parsed;
PlayState.instance.curStage = stageXML.has.name ? stageXML.att.name : stage;
}
if (stageXML.has.folder) {
+18 -16
View File
@@ -163,31 +163,33 @@ class StrumLine extends FlxTypedGroup<Strum> {
}
public inline function updateNotes() {
__updateNote_songPos = Conductor.songPosition;
if(__updateNote_event == null) __updateNote_event = PlayState.instance.__updateNote_event;
notes.forEach(updateNote);
}
var __updateNote_strum:Strum;
var __updateNote_songPos:Float;
var __updateNote_event:NoteUpdateEvent;
public function updateNote(daNote:Note) {
__updateNote_strum = members[daNote.noteData];
if (__updateNote_strum == null) return;
PlayState.instance.__updateNote_event.recycle(daNote, FlxG.elapsed, __updateNote_strum);
onNoteUpdate.dispatch(PlayState.instance.__updateNote_event);
if (PlayState.instance.__updateNote_event.cancelled) return;
__updateNote_event.recycle(daNote, FlxG.elapsed, __updateNote_strum);
onNoteUpdate.dispatch(__updateNote_event);
if (__updateNote_event.cancelled) return;
var songPosition = Conductor.songPosition;
if (__updateNote_event.__updateHitWindow) {
daNote.canBeHit = (daNote.strumTime > __updateNote_songPos - (PlayState.instance.hitWindow * daNote.latePressWindow)
&& daNote.strumTime < __updateNote_songPos + (PlayState.instance.hitWindow * daNote.earlyPressWindow));
if (PlayState.instance.__updateNote_event.__updateHitWindow) {
daNote.canBeHit = (daNote.strumTime > songPosition - (PlayState.instance.hitWindow * daNote.latePressWindow)
&& daNote.strumTime < songPosition + (PlayState.instance.hitWindow * daNote.earlyPressWindow));
if (daNote.strumTime < songPosition - PlayState.instance.hitWindow && !daNote.wasGoodHit)
if (daNote.strumTime < __updateNote_songPos - PlayState.instance.hitWindow && !daNote.wasGoodHit)
daNote.tooLate = true;
}
if (cpu && PlayState.instance.__updateNote_event.__autoCPUHit && !daNote.avoid&& !daNote.wasGoodHit && daNote.strumTime < songPosition) PlayState.instance.goodNoteHit(this, daNote);
if (cpu && __updateNote_event.__autoCPUHit && !daNote.avoid && !daNote.wasGoodHit && daNote.strumTime < __updateNote_songPos) PlayState.instance.goodNoteHit(this, daNote);
if (daNote.wasGoodHit && daNote.isSustainNote && daNote.strumTime + (daNote.sustainLength) < songPosition) {
if (daNote.wasGoodHit && daNote.isSustainNote && daNote.strumTime + (daNote.sustainLength) < __updateNote_songPos) {
deleteNote(daNote);
return;
}
@@ -199,11 +201,11 @@ class StrumLine extends FlxTypedGroup<Strum> {
}
if (PlayState.instance.__updateNote_event.strum == null) return;
if (__updateNote_event.strum == null) return;
if (PlayState.instance.__updateNote_event.__reposNote) PlayState.instance.__updateNote_event.strum.updateNotePosition(daNote);
if (__updateNote_event.__reposNote) __updateNote_event.strum.updateNotePosition(daNote);
if (daNote.isSustainNote)
daNote.updateSustain(PlayState.instance.__updateNote_event.strum);
daNote.updateSustain(__updateNote_event.strum);
}
var __funcsToExec:Array<Note->Void> = [];
@@ -302,10 +304,10 @@ class StrumLine extends FlxTypedGroup<Strum> {
animPrefix = strumAnimPrefix[i % strumAnimPrefix.length];
var babyArrow:Strum = new Strum(startingPos.x + ((Note.swagWidth * strumScale) * i), startingPos.y);
babyArrow.ID = i;
if(data.scrollSpeed != null)
babyArrow.scrollSpeed = data.scrollSpeed;
var event = EventManager.get(StrumCreationEvent).recycle(babyArrow, PlayState.instance.strumLines.members.indexOf(this), i, animPrefix);
event.__doAnimation = !MusicBeatState.skipTransIn;
event = PlayState.instance.scripts.event("onStrumCreation", event);
+135 -19
View File
@@ -1,8 +1,14 @@
package funkin.game.cutscenes;
import funkin.backend.utils.FunkinParentDisabler;
import funkin.backend.scripting.events.dialogue.*;
import funkin.backend.scripting.events.CancellableEvent;
import funkin.backend.scripting.events.DynamicEvent;
import funkin.backend.scripting.Script;
import flixel.sound.FlxSound;
import funkin.game.cutscenes.dialogue.*;
import haxe.xml.Access;
import haxe.io.Path;
/**
* Substate made for dialogue cutscenes. To use it in a scripted cutscene, call `startDialogue`.
@@ -14,11 +20,20 @@ class DialogueCutscene extends Cutscene {
public var charMap:Map<String, DialogueCharacter> = [];
public var dialogueLines:Array<DialogueLine> = [];
public var curLine:DialogueLine = null;
public var curLine(default, set):DialogueLine = null;
public var lastLine:DialogueLine = null;
public var dialogueBox:DialogueBox;
public var nextSFX:FlxSound;
public var dialogueCamera:FlxCamera;
public var curMusic:FlxSound = null;
public static var cutscene:DialogueCutscene;
public var dialogueScript:Script;
public function set_curLine(val:DialogueLine) {
lastLine = curLine;
return curLine = val;
}
public function new(dialoguePath:String, callback:Void->Void) {
super(callback);
@@ -26,77 +41,178 @@ class DialogueCutscene extends Cutscene {
camera = dialogueCamera = new FlxCamera();
dialogueCamera.bgColor = 0;
FlxG.cameras.add(dialogueCamera, false);
dialogueScript = Script.create(Paths.script(Path.withoutExtension(dialoguePath), null, dialoguePath.startsWith('assets')));
dialogueScript.setParent(cutscene = this);
dialogueScript.load();
}
var parentDisabler:FunkinParentDisabler;
public override function create() {
super.create();
add(parentDisabler = new FunkinParentDisabler()); // MESS WITH THIS IN SCRIPTS TO RESUME PLAYSTATE'S TWEENS!! - Nex
try {
dialogueData = new Access(Xml.parse(Assets.getText(dialoguePath)).firstElement());
var event = EventManager.get(DialogueStructureEvent).recycle(dialogueData);
event.dialogueData = new Access(Xml.parse(Assets.getText(dialoguePath)).firstElement());
dialogueScript.call('structureLoaded', [event]);
if(event.cancelled) return;
dialogueData = event.dialogueData;
// Add characters
for(char in dialogueData.nodes.char) {
if (!char.has.name) continue;
if (charMap.exists(char.att.name))
Logs.trace('2 dialogue characters share the same name (${char.att.name}, ${char.att.name}). The old character has been replaced.');
add(charMap[char.att.name] = new DialogueCharacter(char.att.name, char.getAtt('position').getDefault('default')));
var leChar:DialogueCharacter = new DialogueCharacter(char.att.name, char.getAtt('position').getDefault('default'));
if(char.has.defaultAnim) leChar.defaultAnim = char.att.defaultAnim;
add(charMap[char.att.name] = leChar);
}
var useDef:Bool = false;
if(dialogueData.has.forceBoxDefaultTxtSound && dialogueData.att.forceBoxDefaultTxtSound == "true")
useDef = true;
// Add lines
for(node in dialogueData.nodes.line) {
dialogueLines.push({
text: XMLUtil.fixXMLText(node.innerHTML),
var formats = XMLUtil.getTextFormats(XMLUtil.fixSpacingInNode(node));
var volume:Null<Float> = 0.8;
var line:DialogueLine = {
text: [for(x in formats) x.text].join(""),
format: formats,
char: node.getAtt('char').getDefault('boyfriend'),
bubble: node.getAtt('bubble').getDefault('normal'),
callback: node.getAtt('callback'),
speed: node.has.speed ? Std.parseFloat(node.att.speed).getDefault(0.05) : 0.05
});
changeDefAnim: node.getAtt('changeDefAnim'),
speed: Std.parseFloat(node.getAtt("speed")).getDefault(0.05),
musicVolume: node.has.musicVolume ? (volume = Std.parseFloat(node.att.speed).getDefault(0.8)) : null,
changeMusic: node.has.changeMusic ? FlxG.sound.load(Paths.music(node.att.changeMusic), volume, true) : null,
playSound: node.has.playSound ? FlxG.sound.load(Paths.sound(node.att.playSound)) : null,
nextSound: node.has.nextSound ? FlxG.sound.load(Paths.music(node.att.nextSound)) : null,
textSound: null
};
if(node.has.textSound) line.textSound = FlxG.sound.load(Paths.sound(node.att.textSound));
else if(!useDef) {
var char:DialogueCharacter = charMap[line.char];
if(char != null && char.charData != null && char.charData.has.textSound)
line.textSound = FlxG.sound.load(Paths.sound(char.charData.att.textSound));
}
dialogueLines.push(line);
}
// Add dialogue box
dialogueBox = new DialogueBox(dialogueData.getAtt("box").getDefault("default"));
add(dialogueBox);
add(dialogueBox.text);
next();
next(true);
} catch(e) {
Logs.trace('Error while loading dialogue at ${dialoguePath}', ERROR);
Logs.trace('Error while loading dialogue at ${dialoguePath}: ${e.toString()}', ERROR);
trace(CoolUtil.getLastExceptionStack());
close();
}
dialogueScript.call("postCreate");
}
public override function beatHit(curBeat:Int) {
super.beatHit(curBeat);
dialogueScript.call("beatHit", [curBeat]);
}
public override function update(elapsed:Float) {
super.update(elapsed);
if (controls.ACCEPT)
next();
dialogueScript.call("update", [elapsed]);
if(controls.ACCEPT) {
if(dialogueBox.dialogueEnded) next();
else dialogueBox.text.skip();
}
}
public function next() {
/**
* Use this to cancel `next`!
*/
public var canProceed:Bool = true;
public function next(playFirst:Bool = false) {
var event = EventManager.get(DynamicEvent).recycle(playFirst);
dialogueScript.call("next", [playFirst]);
if(event.cancelled || !canProceed) return;
if ((curLine = dialogueLines.shift()) == null) {
close();
if(lastLine != null && dialogueBox.hasAnimation(lastLine.bubble + "-close")) {
dialogueBox.playBubbleAnim(lastLine.bubble, "-close");
dialogueBox.animation.finishCallback = (_) -> close();
} else {
FlxG.sound.play(dialogueBox.nextSFX);
close();
}
return;
}
for(k=>c in charMap)
if (curLine.callback != null)
dialogueScript.call(curLine.callback, [playFirst]);
for (k=>c in charMap)
if (k != curLine.char)
c.hide();
if (charMap[curLine.char] != null)
dialogueBox.popupChar(charMap[curLine.char]);
dialogueBox.playBubbleAnim(curLine.bubble, curLine.text, curLine.speed);
var char = charMap[curLine.char];
if (char != null) {
var force:Bool;
if(force = (curLine.changeDefAnim != null)) char.defaultAnim = curLine.changeDefAnim;
dialogueBox.popupChar(char, force);
}
var finalSuffix:String = playFirst && dialogueBox.hasAnimation(curLine.bubble + "-firstOpen") ? "-firstOpen" : dialogueBox.hasAnimation(curLine.bubble + "-open") ? "-open" : "";
dialogueBox.playBubbleAnim(curLine.bubble, finalSuffix, curLine.text, curLine.format, curLine.speed, curLine.nextSound, curLine.textSound != null ? [curLine.textSound] : null, finalSuffix == "-firstOpen" || finalSuffix == "-open", !playFirst);
if(curLine.playSound != null) curLine.playSound.play();
if(curLine.changeMusic != null) {
if(curMusic != null) curMusic.destroy();
curMusic = curLine.changeMusic;
curMusic.play();
curMusic.fadeIn(1, 0, curMusic.volume);
} else if(curLine.musicVolume != null && curMusic != null) curMusic.volume = curLine.musicVolume;
dialogueScript.call("postNext", [playFirst]);
}
public override function close() {
var event = new CancellableEvent();
for(c in charMap) c.dialogueCharScript.call("close", [event]);
dialogueBox.dialogueBoxScript.call("close", [event]);
dialogueScript.call("close", [event]);
if(event.cancelled) return;
super.close();
}
public override function destroy() {
if(curMusic != null && !curMusic.persist) curMusic.destroy();
dialogueScript.call("destroy");
dialogueScript.destroy();
super.destroy();
cutscene = null;
FlxG.cameras.remove(dialogueCamera);
}
}
typedef DialogueLine = {
var text:String;
var format:Array<XMLUtil.TextFormat>;
var char:String;
var bubble:String;
var callback:String;
var speed:Float;
var musicVolume:Null<Float>;
var changeMusic:FlxSound;
var playSound:FlxSound;
var nextSound:FlxSound;
var textSound:FlxSound;
var changeDefAnim:String;
}
@@ -1,30 +1,59 @@
package funkin.game.cutscenes.dialogue;
import funkin.backend.scripting.events.dialogue.*;
import funkin.backend.scripting.events.PlayAnimEvent;
import funkin.backend.scripting.events.PlayAnimEvent.PlayAnimContext;
import funkin.backend.scripting.events.CancellableEvent;
import funkin.backend.scripting.Script;
import flixel.sound.FlxSound;
import flixel.addons.text.FlxTypeText;
import flixel.text.FlxText;
import flixel.math.FlxPoint;
import flixel.util.FlxColor;
import haxe.xml.Access;
class DialogueBox extends FunkinSprite {
public var dialogueBoxData:Access;
public var positions:Map<String, CharPosDef> = [];
public var dialogueEnded:Bool = false; // Using text._typing is also fair but it doesnt check for eventual opening anims! - Nex
public var textTypeSFX:String = Paths.sound('dialogue/text');
public var nextSFX:String = Paths.sound('dialogue/next');
public var defaultTextTypeSFX:Array<FlxSound>;
public var text:FlxTypeText;
public var defPath:String = 'dialogue/boxes/';
public var dialogueBoxScript:Script;
public var cutscene:DialogueCutscene = DialogueCutscene.cutscene;
public function new(name:String) {
super();
var textTypeSFX:String = Paths.sound('dialogue/text'); // Default if xml doesnt have it - Nex
dialogueBoxScript = Script.create(Paths.script('data/' + defPath + name));
dialogueBoxScript.setParent(this);
dialogueBoxScript.load();
var event = EventManager.get(DialogueBoxStructureEvent).recycle(name, textTypeSFX, null);
dialogueBoxScript.call('create', [event]);
if(event.cancelled) return; // No idea the pyscho that would do this but hey, it can happen - Nex
try {
dialogueBoxData = new Access(Xml.parse(Assets.getText(Paths.xml('dialogue/boxes/$name'))).firstElement());
event.dialogueBoxData = new Access(Xml.parse(Assets.getText(Paths.xml(defPath + event.name))).firstElement());
dialogueBoxScript.call('structureLoaded', [event]);
if(event.cancelled) return;
name = event.name;
textTypeSFX = event.textTypeSFX;
dialogueBoxData = event.dialogueBoxData;
loadSprite(Paths.image('dialogue/boxes/${dialogueBoxData.getAtt('sprite').getDefault(name)}'));
if(!dialogueBoxData.has.sprite) dialogueBoxData.x.set("sprite", name);
XMLUtil.loadSpriteFromXML(this, dialogueBoxData, defPath, NONE);
visible = false;
for(anim in dialogueBoxData.nodes.anim) {
if (!anim.has.name) continue;
XMLUtil.addXMLAnimation(this, anim);
}
var preX:Float = x;
screenCenter(X); x += preX;
y += FlxG.height - height;
if(dialogueBoxData.has.textSound) textTypeSFX = Paths.sound(dialogueBoxData.att.textSound);
if(dialogueBoxData.has.nextSound) nextSFX = Paths.sound(dialogueBoxData.att.nextSound);
for(pos in dialogueBoxData.nodes.charpos) {
if (!pos.has.name) continue;
@@ -35,16 +64,6 @@ class DialogueBox extends FunkinSprite {
};
}
if (dialogueBoxData.has.textSound) textTypeSFX = Paths.sound(dialogueBoxData.att.textSound);
if (dialogueBoxData.has.nextSound) nextSFX = Paths.sound(dialogueBoxData.att.nextSound);
antialiasing = dialogueBoxData.getAtt("antialiasing").getDefault("true") == "true";
screenCenter(X);
y = FlxG.height - height;
if (dialogueBoxData.has.x) x += Std.parseFloat(dialogueBoxData.att.x).getDefault(0);
if (dialogueBoxData.has.y) y += Std.parseFloat(dialogueBoxData.att.y).getDefault(0);
visible = false;
var textNode = dialogueBoxData.node.text;
if (textNode == null)
throw "The dialog box XML requires one text element.";
@@ -54,58 +73,117 @@ class DialogueBox extends FunkinSprite {
textNode.has.width ? Std.parseInt(textNode.att.width).getDefault(FlxG.width) : FlxG.width, "");
text.color = textNode.getAtt("color").getColorFromDynamic().getDefault(0xFF000000);
text.size = Std.parseInt(textNode.att.size).getDefault(20);
text.font = Paths.font('${textNode.getAtt("font").getDefault("vcr.ttf")}');
text.font = Paths.font(textNode.getAtt("font").getDefault("vcr.ttf"));
text.antialiasing = textNode.getAtt("antialiasing").getDefault("false") == "true";
text.sounds = [FlxG.sound.load(textTypeSFX)];
if(textNode.has.borderStyle) {
text.borderStyle = switch(textNode.att.borderStyle.trim().toLowerCase()) {
case "none": NONE;
case "shadow": SHADOW;
case "outline_fast": OUTLINE_FAST;
default: OUTLINE;
}
text.borderQuality = Std.parseFloat(textNode.getAtt("borderQuality")).getDefault(1);
text.shadowOffset.x = Std.parseFloat(textNode.getAtt("shadowOffsetX")).getDefault(1);
text.shadowOffset.y = Std.parseFloat(textNode.getAtt("shadowOffsetY")).getDefault(1);
text.borderSize = Std.parseFloat(textNode.getAtt("borderSize")).getDefault(1);
text.borderColor = textNode.getAtt("borderColor").getColorFromDynamic().getDefault(0xFFFFFFFF);
}
} catch(e) {
active = false;
Logs.trace('Couldn\'t load dialogue box "$name": ${e.toString()}', ERROR);
}
defaultTextTypeSFX = [FlxG.sound.load(textTypeSFX)];
FlxG.sound.cache(nextSFX);
FlxG.sound.cache(textTypeSFX);
dialogueBoxScript.call("postCreate");
}
public function popupChar(char:DialogueCharacter) {
if (!active) return;
var pos = positions[char.positionName];
if (pos == null) return;
public override function playAnim(AnimName:String, Force:Bool = false, Context:PlayAnimContext = NONE, Reversed:Bool = false, Frame:Int = 0) {
var event = EventManager.get(PlayAnimEvent).recycle(AnimName, Force, Reversed, Frame, Context);
dialogueBoxScript.call("onPlayAnim", [event]);
if(event.cancelled) return;
char.show((FlxG.width / 2) + pos.x, FlxG.height - pos.y);
super.playAnim(event.animName, event.force, event.context, event.reverse, event.startingFrame);
}
private var __nextText:String;
private var __speed:Float;
public function playBubbleAnim(bubble:String, text:String, speed:Float = 0.05) {
this.__nextText = text;
this.__speed = speed;
this.text.resetText(text);
FlxG.sound.play(nextSFX);
if (hasAnimation('$bubble-open'))
playAnim('$bubble-open', true);
else {
playAnim(bubble);
setText(__nextText, __speed);
}
visible = true;
}
public function setText(text:String, speed:Float = 0.02) {
this.text.delay = speed;
this.text.start(speed, true);
public override function beatHit(curBeat:Int) {
super.beatHit(curBeat);
dialogueBoxScript.call("beatHit", [curBeat]);
}
public override function update(elapsed:Float) {
super.update(elapsed);
if (isAnimFinished()) {
var animName = getAnimName();
if (animName.endsWith("-open")) {
playAnim(animName.substr(0, animName.length - 5));
setText(__nextText, __speed);
}
dialogueBoxScript.call("update", [elapsed]);
}
public function popupChar(char:DialogueCharacter, force:Bool = false) {
var event = EventManager.get(DialogueBoxCharPopupEvent).recycle(char, force);
dialogueBoxScript.call("popupChar", [event]);
if (event.cancelled || event.char == null || !active) return;
var pos = positions[event.char.positionName];
if (pos == null) return;
event.char.show((FlxG.width / 2) + pos.x, FlxG.height - pos.y, null, event.force);
flipX = pos.flipBubble;
dialogueBoxScript.call("postPopupChar", [event]);
}
public function playBubbleAnim(bubble:String, suffix:String = '', text:String = '', ?format:Array<XMLUtil.TextFormat>, speed:Float = 0.05, ?customSFX:FlxSound, ?customTypeSFX:Array<FlxSound>, setTextAfter:Bool = false, allowDefault:Bool = true) {
var event = EventManager.get(DialogueBoxPlayBubbleEvent).recycle(bubble, suffix, text, format, speed, customSFX, customTypeSFX, setTextAfter, allowDefault);
dialogueBoxScript.call("playBubbleAnim", [event]);
if(event.cancelled) return;
if(event.customSFX != null) event.customSFX.play();
else if(event.allowDefault) FlxG.sound.play(nextSFX);
var idk:Void->Void = () -> {
if(text != null && text.trim().length > 0) startText(event.text, event.format, event.speed, event.customTypeSFX);
else dialogueEnded = true;
}
dialogueEnded = false;
resetText();
var anim:String = event.bubble + event.suffix;
if(hasAnimation(anim)) playAnim(anim, true);
if(!event.setTextAfter) idk();
else animation.finishCallback = (name:String) -> {
if(name != anim) return;
playAnim(event.bubble);
idk(); animation.finishCallback = null;
}
visible = true;
dialogueBoxScript.call("postPlayBubbleAnim", [event]);
}
public function resetText(text:String = '', ?formats:Array<XMLUtil.TextFormat>, ?event:DialogueBoxSetTextEvent) {
if(event == null) event = EventManager.get(DialogueBoxSetTextEvent).recycle(text, formats, null, null);
dialogueBoxScript.call("resetText", [event]);
if(event.cancelled) return;
this.text.clearFormats();
if(formats != null)
this.text.autoSetFormat(formats);
this.text.resetText(event.text);
dialogueBoxScript.call("postResetText");
}
public function startText(text:String = '', ?format:Array<XMLUtil.TextFormat>, speed:Float = 0.05, ?customTypeSFX:Array<FlxSound>) {
var event = EventManager.get(DialogueBoxSetTextEvent).recycle(text, format, speed, customTypeSFX);
dialogueBoxScript.call("startText", [event]);
if(event.cancelled) return;
resetText(event.text, event.format, event);
this.text.sounds = event.customTypeSFX != null ? event.customTypeSFX : defaultTextTypeSFX;
this.text.delay = event.speed;
this.text.start(event.speed, true);
this.text.completeCallback = () -> dialogueEnded = true;
dialogueBoxScript.call("postStartText");
}
public override function destroy() {
dialogueBoxScript.call("destroy");
dialogueBoxScript.destroy();
super.destroy();
positions = null;
}
@@ -1,58 +1,137 @@
package funkin.game.cutscenes.dialogue;
import funkin.backend.scripting.events.PlayAnimEvent;
import funkin.backend.scripting.events.dialogue.*;
import funkin.backend.scripting.events.CancellableEvent;
import funkin.backend.scripting.Script;
import flixel.tweens.FlxTween;
import haxe.xml.Access;
class DialogueCharacter extends FunkinSprite {
public var charData:Access;
public var curTween:FlxTween;
public var curTweenContext:DialogueCharTweenContext = NONE;
public var curAnimContext:DialogueCharAnimContext = NONE;
public var positionName:String;
public var finishAnimCallback:String->Void = null;
public var defaultAnim:String = 'normal';
public var defPath:String = 'dialogue/characters/';
public var dialogueCharScript:Script;
public var cutscene:DialogueCutscene = DialogueCutscene.cutscene;
public function new(name:String, position:String) {
super();
this.positionName = position;
dialogueCharScript = Script.create(Paths.script('data/' + defPath + name));
dialogueCharScript.setParent(this);
dialogueCharScript.load();
var event = EventManager.get(DialogueCharStructureEvent).recycle(name, position, null);
dialogueCharScript.call('create', [event]); // Its not really create() since its inside new() but, nah, it makes no difference at least here - Nex
if(event.cancelled) return;
try {
charData = new Access(Xml.parse(Assets.getText(Paths.xml('dialogue/characters/$name'))).firstElement());
loadSprite(Paths.image('dialogue/characters/${charData.getAtt('sprite').getDefault(name)}'));
antialiasing = charData.getAtt('antialiasing').getDefault('true') == 'true';
scale.scale(charData.has.scale ? Std.parseFloat(charData.att.scale).getDefault(1) : 1);
updateHitbox();
offset.set(
charData.has.x ? Std.parseFloat(charData.att.x).getDefault(0) : 0,
(charData.has.y ? Std.parseFloat(charData.att.y).getDefault(0) : 0) + this.height);
for(anim in charData.nodes.anim)
XMLUtil.addXMLAnimation(this, anim, true);
event.charData = new Access(Xml.parse(Assets.getText(Paths.xml(defPath + event.name))).firstElement());
dialogueCharScript.call('structureLoaded', [event]);
if(event.cancelled) return;
name = event.name;
positionName = event.position;
charData = event.charData;
if(!charData.has.sprite) charData.x.set("sprite", name);
if(!charData.has.updateHitbox) charData.x.set("updateHitbox", "true");
XMLUtil.loadSpriteFromXML(this, charData, defPath, LOOP);
animation.finishCallback = (name:String) -> {
if(finishAnimCallback != null) finishAnimCallback(name);
if(name.endsWith("-show") || name == 'show') hasAnimation(defaultAnim) ? playAnim(defaultAnim, true) : animation.stop();
else if(name.endsWith("-hide") || name == 'hide') alpha = 0;
}
offset.set(x, y + height);
x = 0; y = 0;
} catch(e) {
Logs.trace('Failed to load dialogue character $name: ${e.toString()}', ERROR);
var message:String = e.toString();
Logs.trace('Failed to load dialogue character $name: ${message}', ERROR);
dialogueCharScript.call("loadingError", [message]);
}
visible = false;
dialogueCharScript.call("postCreate");
}
public function show(x:Float, y:Float) {
if (curTweenContext != (curTweenContext = POPIN)) {
setPosition(x, y + 100);
if (curTween != null)
curTween.cancel();
public override function playAnim(AnimName:String, Force:Bool = false, Context:PlayAnimContext = NONE, Reversed:Bool = false, Frame:Int = 0) {
var event = EventManager.get(PlayAnimEvent).recycle(AnimName, Force, Reversed, Frame, Context);
dialogueCharScript.call("playAnim", [event]);
if(event.cancelled) return;
super.playAnim(event.animName, event.force, event.context, event.reverse, event.startingFrame);
}
public override function beatHit(curBeat:Int) {
super.beatHit(curBeat);
dialogueCharScript.call("beatHit", [curBeat]);
}
public override function update(elapsed:Float) {
super.update(elapsed);
dialogueCharScript.call("update", [elapsed]);
}
public function show(x:Float, y:Float, ?animation:String, force:Bool = false) {
if(animation == null) animation = defaultAnim;
var lastAnimContext:DialogueCharAnimContext = force ? POPOUT : curAnimContext;
curAnimContext = POPIN;
var event = EventManager.get(DialogueCharShowEvent).recycle(x, y, animation, lastAnimContext);
dialogueCharScript.call("show", [event]);
if (event.cancelled || event.lastAnimContext == curAnimContext) return;
visible = true;
var anim:String;
if (hasAnimation(anim = '${event.animation}-show') || hasAnimation(anim = 'show')) {
playAnim(anim, true);
setPosition(event.x, event.y);
alpha = 1;
} else {
setPosition(event.x, event.y + 100);
if(curTween != null) curTween.cancel();
playAnim(event.animation, true);
alpha = 0;
visible = true;
curTween = FlxTween.tween(this, {alpha: 1, y: y}, 0.2, {ease: FlxEase.quintOut});
curTween = FlxTween.tween(this, {alpha: 1, y: event.y}, 0.2, {ease: FlxEase.quintOut, onComplete: function(twn:FlxTween) dialogueCharScript.call("showTweenCompleted", [twn])});
}
dialogueCharScript.call("postShow", [event]);
}
public function hide() {
if (curTweenContext != (curTweenContext = POPOUT)) {
if (curTween != null)
curTween.cancel();
curTween = FlxTween.tween(this, {alpha: 0, y: y + 100}, 0.2, {ease: FlxEase.quintIn});
public function hide(?animation:String, force:Bool = false) {
if(animation == null) animation = defaultAnim;
var lastAnimContext:DialogueCharAnimContext = force ? POPIN : curAnimContext;
curAnimContext = POPOUT;
var event = EventManager.get(DialogueCharHideEvent).recycle(animation, lastAnimContext);
dialogueCharScript.call("hide", [event]);
if (event.cancelled || event.lastAnimContext == curAnimContext) return;
var anim:String;
if(hasAnimation(anim = '${event.animation}-hide') || hasAnimation(anim = 'hide')) playAnim(anim, true);
else {
if(curTween != null) curTween.cancel();
curTween = FlxTween.tween(this, {alpha: 0, y: y + 100}, 0.2, {ease: FlxEase.quintIn, onComplete: function(twn:FlxTween) dialogueCharScript.call("hideTweenCompleted", [twn])});
}
dialogueCharScript.call("postHide", [event]);
}
override function destroy()
{
dialogueCharScript.call("destroy");
dialogueCharScript.destroy();
super.destroy();
}
}
enum abstract DialogueCharTweenContext(Int) {
enum abstract DialogueCharAnimContext(Int) {
var NONE = -1;
var POPIN = 0;
var POPOUT = 1;
+394
View File
@@ -0,0 +1,394 @@
/*
* Copyright (C)2005-2019 Haxe Foundation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package haxe.format;
/**
An implementation of JSON parser in Haxe.
This class is used by `haxe.Json` when native JSON implementation
is not available.
@see https://haxe.org/manual/std-Json-parsing.html
**/
class JsonParser {
/**
Parses given JSON-encoded `str` and returns the resulting object.
JSON objects are parsed into anonymous structures and JSON arrays
are parsed into `Array<Dynamic>`.
If given `str` is not valid JSON, an exception will be thrown.
If `str` is null, the result is unspecified.
**/
static public inline function parse(str:String):Dynamic {
return new JsonParser(str).doParse();
}
var str:String;
var pos:Int;
function new(str:String) {
this.str = str;
this.pos = 0;
}
function doParse():Dynamic {
var result = parseRec();
var c;
while (!StringTools.isEof(c = nextChar())) {
if (checkComments(c)) {
continue;
}
switch (c) {
case ' '.code, '\r'.code, '\n'.code, '\t'.code:
// allow trailing whitespace
default:
invalidChar();
}
}
return result;
}
var inComment = false;
var blockComment = false;
inline function checkComments(c:Int) {
var ret = false;
if (!inComment) { // Starting Comment
if (c == '/'.code) {
var d = nextChar();
if (d == '/'.code || (blockComment = (d == '*'.code))) {
inComment = true;
} else {
pos--;
}
}
} else { // In Comment
if (blockComment) { // Block Comment
if (c == '*'.code) {
var d = nextChar();
if (d == '/'.code) {
inComment = blockComment = false;
ret = true;
} else {
pos--;
}
}
} else { // Line Comment
if (c == '\n'.code) {
inComment = false;
}
}
}
return ret || inComment;
}
function parseRec():Dynamic {
while (true) {
var c = nextChar();
if (checkComments(c)) {
continue;
}
switch (c) {
case ' '.code, '\r'.code, '\n'.code, '\t'.code:
// loop
case '{'.code:
var obj = {}, field = null, comma:Null<Bool> = null;
while (true) {
var c = nextChar();
if (checkComments(c)) {
continue;
}
switch (c) {
case ' '.code, '\r'.code, '\n'.code, '\t'.code:
// loop
case '}'.code:
if (field != null || comma == false)
invalidChar();
return obj;
case ':'.code:
if (field == null)
invalidChar();
Reflect.setField(obj, field, parseRec());
field = null;
comma = true;
case ','.code:
if (comma) comma = false else invalidChar();
case '"'.code:
if (field != null || comma) invalidChar();
field = parseString();
default:
invalidChar();
}
}
case '['.code:
var arr = [], comma:Null<Bool> = null;
while (true) {
var c = nextChar();
if (checkComments(c)) {
continue;
}
switch (c) {
case ' '.code, '\r'.code, '\n'.code, '\t'.code:
// loop
case ']'.code:
if (comma == false) invalidChar();
return arr;
case ','.code:
if (comma) comma = false else invalidChar();
default:
if (comma) invalidChar();
pos--;
arr.push(parseRec());
comma = true;
}
}
case 't'.code:
var save = pos;
if (nextChar() != 'r'.code || nextChar() != 'u'.code || nextChar() != 'e'.code) {
pos = save;
invalidChar();
}
return true;
case 'f'.code:
var save = pos;
if (nextChar() != 'a'.code || nextChar() != 'l'.code || nextChar() != 's'.code || nextChar() != 'e'.code) {
pos = save;
invalidChar();
}
return false;
case 'n'.code:
var save = pos;
if (nextChar() != 'u'.code || nextChar() != 'l'.code || nextChar() != 'l'.code) {
pos = save;
invalidChar();
}
return null;
case '"'.code:
return parseString();
case '0'.code, '1'.code, '2'.code, '3'.code, '4'.code, '5'.code, '6'.code, '7'.code, '8'.code, '9'.code, '-'.code:
return parseNumber(c);
default:
invalidChar();
}
}
}
function parseString() {
var start = pos;
var buf:StringBuf = null;
#if target.unicode
var prev = -1;
inline function cancelSurrogate() {
// invalid high surrogate (not followed by low surrogate)
buf.addChar(0xFFFD);
prev = -1;
}
#end
while (true) {
var c = nextChar();
if (c == '"'.code)
break;
if (c == '\\'.code) {
if (buf == null) {
buf = new StringBuf();
}
buf.addSub(str, start, pos - start - 1);
c = nextChar();
#if target.unicode
if (c != "u".code && prev != -1)
cancelSurrogate();
#end
switch (c) {
case "r".code:
buf.addChar("\r".code);
case "n".code:
buf.addChar("\n".code);
case "t".code:
buf.addChar("\t".code);
case "b".code:
buf.addChar(8);
case "f".code:
buf.addChar(12);
case "/".code, '\\'.code, '"'.code:
buf.addChar(c);
case 'u'.code:
var uc:Int = Std.parseInt("0x" + str.substr(pos, 4));
pos += 4;
#if !target.unicode
if (uc <= 0x7F)
buf.addChar(uc);
else if (uc <= 0x7FF) {
buf.addChar(0xC0 | (uc >> 6));
buf.addChar(0x80 | (uc & 63));
} else if (uc <= 0xFFFF) {
buf.addChar(0xE0 | (uc >> 12));
buf.addChar(0x80 | ((uc >> 6) & 63));
buf.addChar(0x80 | (uc & 63));
} else {
buf.addChar(0xF0 | (uc >> 18));
buf.addChar(0x80 | ((uc >> 12) & 63));
buf.addChar(0x80 | ((uc >> 6) & 63));
buf.addChar(0x80 | (uc & 63));
}
#else
if (prev != -1) {
if (uc < 0xDC00 || uc > 0xDFFF)
cancelSurrogate();
else {
buf.addChar(((prev - 0xD800) << 10) + (uc - 0xDC00) + 0x10000);
prev = -1;
}
} else if (uc >= 0xD800 && uc <= 0xDBFF)
prev = uc;
else
buf.addChar(uc);
#end
default:
throw "Invalid escape sequence \\" + String.fromCharCode(c) + " at position " + (pos - 1);
}
start = pos;
}
#if !(target.unicode)
// ensure utf8 chars are not cut
else if (c >= 0x80) {
pos++;
if (c >= 0xFC)
pos += 4;
else if (c >= 0xF8)
pos += 3;
else if (c >= 0xF0)
pos += 2;
else if (c >= 0xE0)
pos++;
}
#end
else if (StringTools.isEof(c))
throw "Unclosed string";
}
#if target.unicode
if (prev != -1)
cancelSurrogate();
#end
if (buf == null) {
return str.substr(start, pos - start - 1);
} else {
buf.addSub(str, start, pos - start - 1);
return buf.toString();
}
}
inline function parseNumber(c:Int):Dynamic {
var start = pos - 1;
var minus = c == '-'.code, digit = !minus, zero = c == '0'.code;
var point = false, e = false, pm = false, end = false;
while (true) {
c = nextChar();
switch (c) {
case '0'.code:
if (zero && !point)
invalidNumber(start);
if (minus) {
minus = false;
zero = true;
}
digit = true;
case '1'.code, '2'.code, '3'.code, '4'.code, '5'.code, '6'.code, '7'.code, '8'.code, '9'.code:
if (zero && !point)
invalidNumber(start);
if (minus)
minus = false;
digit = true;
zero = false;
case '.'.code:
if (minus || point || e)
invalidNumber(start);
digit = false;
point = true;
case 'e'.code, 'E'.code:
if (minus || zero || e)
invalidNumber(start);
digit = false;
e = true;
case '+'.code, '-'.code:
if (!e || pm)
invalidNumber(start);
digit = false;
pm = true;
default:
if (!digit)
invalidNumber(start);
pos--;
end = true;
}
if (end)
break;
}
var f = Std.parseFloat(str.substr(start, pos - start));
if (point) {
return f;
} else {
var i = Std.int(f);
return if (i == f) i else f;
}
}
inline function nextChar() {
return StringTools.fastCodeAt(str, pos++);
}
function invalidChar() {
pos--; // rewind
var col = 1;
var line = 1;
for (i in 0...pos) {
var c = StringTools.fastCodeAt(str, i);
if (c == '\n'.code) {
col = 1;
line++;
}
else {
col++;
}
}
throw "Invalid char " + StringTools.fastCodeAt(str, pos) + " ('" + str.charAt(pos) + "')" + " at line " + line + " col " + col;
}
function invalidNumber(start:Int) {
var col = 1;
var line = 1;
for (i in 0...start) {
var c = StringTools.fastCodeAt(str, i);
if (c == '\n'.code) {
col = 1;
line++;
}
else {
col++;
}
}
throw "Invalid number at line " + line + " col " + col + ": " + str.substr(start, pos - start);
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ class Config {
// Runs support for abstract support in these
public static final ALLOWED_ABSTRACT_AND_ENUM = [
"flixel",
"openfl.display.BlendMode",
"openfl",
"haxe.xml",
"haxe.CallStack",