From eada3105a471034250aa4b385f3e49d8c801567d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=8D=9A=7ENex?= <87421482+NexIsDumb@users.noreply.github.com> Date: Sat, 30 Nov 2024 02:15:35 +0100 Subject: [PATCH 1/4] psych 1.0 format + some charts improvements Fixes #448 --- source/funkin/backend/FunkinSprite.hx | 4 +- source/funkin/backend/chart/Chart.hx | 105 +++++++++++++----- .../funkin/backend/chart/FNFLegacyParser.hx | 14 +-- source/funkin/backend/chart/PsychParser.hx | 32 ++++-- source/funkin/editors/charter/Charter.hx | 2 +- source/hscript/Config.hx | 1 - 6 files changed, 111 insertions(+), 47 deletions(-) diff --git a/source/funkin/backend/FunkinSprite.hx b/source/funkin/backend/FunkinSprite.hx index fa79a95b..b04cdffd 100644 --- a/source/funkin/backend/FunkinSprite.hx +++ b/source/funkin/backend/FunkinSprite.hx @@ -22,9 +22,9 @@ enum abstract XMLAnimType(Int) var BEAT = 1; var LOOP = 2; - public static function fromString(str:String, def:XMLAnimType = NONE) + public static function fromString(str:String, def:XMLAnimType = XMLAnimType.NONE) { - return switch (str.trim().toLowerCase()) + return switch (StringTools.trim(str).toLowerCase()) { case "none": NONE; case "beat" | "onbeat": BEAT; diff --git a/source/funkin/backend/chart/Chart.hx b/source/funkin/backend/chart/Chart.hx index bd13fd50..a65f45b7 100644 --- a/source/funkin/backend/chart/Chart.hx +++ b/source/funkin/backend/chart/Chart.hx @@ -1,6 +1,5 @@ package funkin.backend.chart; -import funkin.backend.system.Conductor; import funkin.backend.chart.ChartData; import flixel.util.FlxColor; import haxe.io.Path; @@ -13,12 +12,68 @@ import sys.FileSystem; using StringTools; +enum abstract ChartFormat(Int) { + var CODENAME = 0; + var LEGACY = 1; // also used by many other engines (old Psych, Kade and more) - Nex + var VSLICE = 2; + var PSYCH_NEW = 3; + + @:to public function toString():String { + return switch(cast (this, ChartFormat)) { + case CODENAME: "CODENAME"; + case LEGACY: "LEGACY"; + case VSLICE: "VSLICE"; + case PSYCH_NEW: "PSYCH_NEW"; + } + } + + public static function fromString(str:String, def:ChartFormat = ChartFormat.LEGACY) { + str = str.toLowerCase(); + str = StringTools.replace(str, " ", ""); + str = StringTools.replace(str, "_", ""); + str = StringTools.replace(str, ".", ""); + + if(StringTools.startsWith(str, "psychv1") || StringTools.startsWith(str, "psych1")) + return PSYCH_NEW; + + return switch(str) { + case "codename" | "codenameengine": CODENAME; + case "newpsych" | "psychnew": PSYCH_NEW; + default: def; + } + } +} + class Chart { /** * Default background colors for songs without bg color */ public inline static var defaultColor:FlxColor = 0xFF9271FD; + public static function cleanSongData(data:Dynamic):Dynamic { + if (Reflect.hasField(data, "song")) { + var field:Dynamic = Reflect.field(data, "song"); + if (field != null && Type.typeof(field) == TObject) // Cant use Reflect.isObject, because it detects strings for some reason + return field; + } + return data; + } + + public static function detectChartFormat(data:Dynamic):ChartFormat { + var __temp:Dynamic; // imma reuse this var so the program doesnt have to get values multiple times - Nex + + if ((__temp = data.codenameChart) == true || __temp == "true") + return CODENAME; + + if (Reflect.hasField(data, "version") && Reflect.hasField(data, "scrollSpeed")) + return VSLICE; + + if ((__temp = cleanSongData(data).format) != null && __temp is String && StringTools.startsWith(__temp, "psych_v1")) + return PSYCH_NEW; + + return LEGACY; + } + public static function loadEventsJson(songName:String) { var path = Paths.file('songs/${songName.toLowerCase()}/events.json'); var data:Array = null; @@ -33,8 +88,9 @@ class Chart { } 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'); + var songNameLower = songName.toLowerCase(); + var metaPath = Paths.file('songs/${songNameLower}/meta.json'); + var metaDiffPath = Paths.file('songs/${songNameLower}/meta-${difficulty.toLowerCase()}.json'); var data:ChartMetaData = null; var fromMods:Bool = fromMods; @@ -67,7 +123,7 @@ class Chart { data.setFieldDefault("parsedColor", data.color.getColorFromDynamic().getDefault(defaultColor)); if (data.difficulties.length <= 0) { - data.difficulties = [for(f in Paths.getFolderContent('songs/${songName.toLowerCase()}/charts/', false, !fromMods)) if (Path.extension(f = f.toUpperCase()) == "JSON") Path.withoutExtension(f)]; + data.difficulties = [for(f in Paths.getFolderContent('songs/${songNameLower}/charts/', false, !fromMods)) if (Path.extension(f = f.toUpperCase()) == "JSON") Path.withoutExtension(f)]; if (data.difficulties.length == 3) { var hasHard = false, hasNormal = false, hasEasy = false; for(d in data.difficulties) { @@ -116,12 +172,12 @@ class Chart { Logs.trace('Could not parse chart for song ${songName} ($difficulty): ${Std.string(e)}', ERROR, RED); } - if (data != null) { - /** - * CHART CONVERSION - */ - #if REGION - if (Reflect.hasField(data, "codenameChart") && Reflect.field(data, "codenameChart") == true) { + /** + * CHART CONVERSION + */ + #if REGION + if (data != null) switch (detectChartFormat(data)) { + case CODENAME: // backward compat on events since its caused problems var eventTypesToString:Map = [ -1 => "HScript Call", @@ -132,28 +188,23 @@ class Chart { ]; if (data.events == null) data.events = []; - for (event in cast(data.events, Array)) { - if (Reflect.hasField(event, "type")) { - if(event.type != null) - event.name = eventTypesToString[event.type]; - Reflect.deleteField(event, "type"); - } + for (event in cast(data.events, Array)) if (Reflect.hasField(event, "type")) { + if (event.type != null) + event.name = eventTypesToString[event.type]; + Reflect.deleteField(event, "type"); } - // codename chart base = data; - } else { - // base game chart - FNFLegacyParser.parse(data, base); - } - #end + case PSYCH_NEW: PsychParser.parse(data, base); + case VSLICE: // TODO + case LEGACY: FNFLegacyParser.parse(data, base); } + #end - if (base.meta == null) - base.meta = loadChartMeta(songName, difficulty, base.fromMods); + var loadedMeta = loadChartMeta(songName, difficulty, base.fromMods); + if (base.meta == null) base.meta = loadedMeta; else { - var loadedMeta = loadChartMeta(songName, difficulty, base.fromMods); - for(field in Reflect.fields(base.meta)) { + for (field in Reflect.fields(base.meta)) { var f = Reflect.field(base.meta, field); if (f != null) Reflect.setField(loadedMeta, field, f); @@ -249,4 +300,4 @@ typedef ChartSaveSettings = { var ?saveEventsInChart:Bool; var ?prettyPrint:Bool; var ?folder:String; -} +} \ No newline at end of file diff --git a/source/funkin/backend/chart/FNFLegacyParser.hx b/source/funkin/backend/chart/FNFLegacyParser.hx index ede54e18..61a0f837 100644 --- a/source/funkin/backend/chart/FNFLegacyParser.hx +++ b/source/funkin/backend/chart/FNFLegacyParser.hx @@ -6,12 +6,7 @@ import funkin.backend.system.Conductor; class FNFLegacyParser { public static function parse(data:Dynamic, result:ChartData) { // base fnf chart parsing - var data:SwagSong = data; - if (Reflect.hasField(data, "song")) { - var field:Dynamic = Reflect.field(data, "song"); - if (!(field is String)) - data = field; - } + var data:SwagSong = Chart.cleanSongData(data); result.scrollSpeed = data.speed; result.stage = data.stage; @@ -29,7 +24,7 @@ class FNFLegacyParser { position: "boyfriend", notes: [] }); - var gfName = data.gf != null ? data.gf : (data.gfVersion != null ? data.gfVersion : "gf"); + var gfName = data.gf != null ? data.gf : (data.gfVersion != null ? data.gfVersion : (data.player3 != null ? data.player3 : "gf")); if (!p2isGF && gfName != "none") { result.strumLines.push({ characters: [gfName], @@ -132,10 +127,10 @@ class FNFLegacyParser { if ((swagSection.mustHitSection && strumLine.type == OPPONENT) || (!swagSection.mustHitSection && strumLine.type == PLAYER)) sectionNote[1] += 4; - swagSection.sectionNotes.push(sectionNote); + swagSection.sectionNotes.push(sectionNote); } } - + return {song: base}; } @@ -214,6 +209,7 @@ typedef SwagSong = var player1:String; var player2:String; + var ?player3:String; // alt for gf but idr if it was just used in psych or what but eh whatever maybe its better here anyways - Nex var ?gf:String; var ?gfVersion:String; var validScore:Bool; diff --git a/source/funkin/backend/chart/PsychParser.hx b/source/funkin/backend/chart/PsychParser.hx index 1af9962a..a600b141 100644 --- a/source/funkin/backend/chart/PsychParser.hx +++ b/source/funkin/backend/chart/PsychParser.hx @@ -6,15 +6,33 @@ import funkin.backend.chart.FNFLegacyParser.SwagSong; import funkin.backend.system.Conductor; class PsychParser { - // Alreadly parsed in sections + // Already parsed in sections public static var ignoreEvents:Array = [ "Camera Movement", "Alt Animation Toggle", "BPM Change" ]; - public static function parse(data:Dynamic, result:ChartData) + /** + * Converts 1.0 Psych charts to older ones + */ + public static function standardize(data:Dynamic, result:ChartData) { + var sectionsData:Array = Chart.cleanSongData(data).notes; + if (sectionsData == null) return; + + for (section in sectionsData) for (note in cast(section.sectionNotes, Array)) { + var gottaHitNote:Bool = section.mustHitSection == (note[1] >= 4); + note[1] = (note[1] % 4) + (gottaHitNote ? 4 : 0); + + if (note[3] != null && Std.isOfType(note[3], String)) + note[3] = Chart.addNoteType(result, note[3]); + } + } + + public static function parse(data:Dynamic, result:ChartData) { + if (Chart.detectChartFormat(data) == PSYCH_NEW) standardize(data, result); FNFLegacyParser.parse(data, result); + } public static function encode(chart:ChartData):Dynamic { var base:SwagSong = FNFLegacyParser.__convertToSwagSong(chart); @@ -32,7 +50,7 @@ class PsychParser { for (note in strumLine.notes) { var section:Int = Math.floor(Conductor.getStepForTime(note.time) / Conductor.getMeasureLength()); var swagSection:SwagSection = base.notes[section]; - + if (section >= 0 && section < base.notes.length) { var sectionNote:Array = [ note.time, // TIME @@ -44,10 +62,10 @@ class PsychParser { if ((swagSection.mustHitSection && strumLine.type == OPPONENT) || (!swagSection.mustHitSection && strumLine.type == PLAYER)) sectionNote[1] += 4; - swagSection.sectionNotes.push(sectionNote); + swagSection.sectionNotes.push(sectionNote); } } - + var groupedEvents:Array> = []; var __last:Array = null; var __lastTime:Float = Math.NaN; @@ -91,7 +109,7 @@ class PsychParser { FlxMath.roundDecimal(event.params[1]/chart.scrollSpeed, 2), // SCROLL SPEED MULTIPLER FlxMath.roundDecimal( // TIME event.params[0] ? // IS TWEENED? - (Conductor.getTimeForStep(eventStep+event.params[2]) - Conductor.getTimeForStep(eventStep))/1000 + (Conductor.getTimeForStep(eventStep+event.params[2]) - Conductor.getTimeForStep(eventStep))/1000 : 0, 2) ]); default: @@ -106,7 +124,7 @@ class PsychParser { for (psychEvent in psychEvents) for (i in 1...3) // Turn both vals into strings - if (!(psychEvent[i] is String)) + if (!(psychEvent[i] is String)) psychEvent[i] = Std.string(psychEvent[i]); base.events.push([events[0].time, psychEvents]); diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index cf13c0c2..4ec2f299 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -1356,7 +1356,7 @@ class Charter extends UIState { defaultSaveFile: '${__song.toLowerCase().replace(" ", "-")}${__diff.toLowerCase() == "normal" ? "" : '-${__diff.toLowerCase()}'}.json', })); } - + function _file_saveas_psych(_) { openSubState(new SaveSubstate(Json.stringify(PsychParser.encode(PlayState.SONG), null, Options.editorPrettyPrint ? "\t" : null), { defaultSaveFile: '${__song.toLowerCase().replace(" ", "-")}${__diff.toLowerCase() == "normal" ? "" : '-${__diff.toLowerCase()}'}.json', diff --git a/source/hscript/Config.hx b/source/hscript/Config.hx index d0667ab9..4eec6ab1 100644 --- a/source/hscript/Config.hx +++ b/source/hscript/Config.hx @@ -29,7 +29,6 @@ class Config { ]; public static final DISALLOW_ABSTRACT_AND_ENUM = [ - "funkin.backend.FunkinSprite", // Error: String has no field trim, Due to Func "funkin.backend.scripting.events.PlayAnimEvent", // Error: expected member name or ';' after declaration specifiers, Due to Func ]; } \ No newline at end of file From 83cf69b4777d581d816cc0d1f4cf3c8f22254ff2 Mon Sep 17 00:00:00 2001 From: Furo Date: Sat, 30 Nov 2024 21:10:00 +0100 Subject: [PATCH 2/4] UIState Reloading Fix (#469) * Reloading UIStates does not softlock anymore * fix * Removed all prev changes and use a better method * Switch to tabs --- source/funkin/backend/MusicBeatState.hx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/source/funkin/backend/MusicBeatState.hx b/source/funkin/backend/MusicBeatState.hx index 35b684e5..6ef650bc 100644 --- a/source/funkin/backend/MusicBeatState.hx +++ b/source/funkin/backend/MusicBeatState.hx @@ -93,6 +93,9 @@ class MusicBeatState extends FlxState implements IBeatReceiver public var scriptsAllowed:Bool = true; + public static var lastScriptName:String = null; + public static var lastStateName:String = null; + public var scriptName:String = null; public static var skipTransOut:Bool = false; @@ -108,7 +111,12 @@ class MusicBeatState extends FlxState implements IBeatReceiver public function new(scriptsAllowed:Bool = true, ?scriptName:String) { super(); this.scriptsAllowed = #if SOFTCODED_STATES scriptsAllowed #else false #end; - this.scriptName = scriptName; + + if(lastStateName != (lastStateName = Type.getClassName(Type.getClass(this)))) { + lastScriptName = null; + } + this.scriptName = scriptName != null ? scriptName : lastScriptName; + lastScriptName = this.scriptName; } function loadScript() { From 3297befda227451a88fed36bfce5aae22950b42d Mon Sep 17 00:00:00 2001 From: TheZoroForce240 <86524550+TheZoroForce240@users.noreply.github.com> Date: Sat, 7 Dec 2024 21:34:18 +0000 Subject: [PATCH 3/4] Backwards compatibility with older vertex shaders (#477) * add backwards compatibility with older vertex shaders * add todo comment for fixing comments in shaders --- source/funkin/backend/shaders/FunkinShader.hx | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/source/funkin/backend/shaders/FunkinShader.hx b/source/funkin/backend/shaders/FunkinShader.hx index cbfe8846..152ca6c3 100644 --- a/source/funkin/backend/shaders/FunkinShader.hx +++ b/source/funkin/backend/shaders/FunkinShader.hx @@ -192,7 +192,11 @@ class FunkinShader extends FlxShader implements IHScriptCustomBehaviour { if(value == null) value = ShaderTemplates.defaultVertexSource; glRawVertexSource = value; - value = value.replace("#pragma header", ShaderTemplates.vertHeader).replace("#pragma body", ShaderTemplates.vertBody); + + var useBackCompat:Bool = true; + for (regex in ShaderTemplates.vertBackCompatVarList) if (!regex.match(value)) useBackCompat = false; + + value = value.replace("#pragma header", useBackCompat ? ShaderTemplates.vertHeaderBackCompat : ShaderTemplates.vertHeader).replace("#pragma body", useBackCompat ? ShaderTemplates.vertBodyBackCompat : ShaderTemplates.vertBody); if (value != __glVertexSource) { __glSourceDirty = true; @@ -625,6 +629,38 @@ if(hasColorTransform) { gl_Position = openfl_Matrix * openfl_Position;"; +// TODO: make this ignore comments +public static final vertBackCompatVarList:Array = [ + ~/attribute float alpha/, + ~/attribute vec4 colorMultiplier/, + ~/attribute vec4 colorOffset/, + ~/uniform bool hasColorTransform/ +]; + +public static final vertHeaderBackCompat:String = "attribute float openfl_Alpha; +attribute vec4 openfl_ColorMultiplier; +attribute vec4 openfl_ColorOffset; +attribute vec4 openfl_Position; +attribute vec2 openfl_TextureCoord; + +varying float openfl_Alphav; +varying vec4 openfl_ColorMultiplierv; +varying vec4 openfl_ColorOffsetv; +varying vec2 openfl_TextureCoordv; + +uniform mat4 openfl_Matrix; +uniform bool openfl_HasColorTransform; +uniform vec2 openfl_TextureSize;"; + +public static final vertBodyBackCompat:String = "openfl_Alphav = openfl_Alpha; +openfl_TextureCoordv = openfl_TextureCoord; + +if(openfl_HasColorTransform) { + openfl_ColorMultiplierv = openfl_ColorMultiplier; + openfl_ColorOffsetv = openfl_ColorOffset / 255.0; +} + +gl_Position = openfl_Matrix * openfl_Position;"; public static final defaultVertexSource:String = "#pragma header From f6deda2c84984202effdfc5f6b577c2d956aa7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=8D=9A=7ENex?= <87421482+NexIsDumb@users.noreply.github.com> Date: Sun, 8 Dec 2024 21:35:39 +0100 Subject: [PATCH 4/4] addAnim for FunkinSprite + better names so u dont have to manually add based if atlas or sparrow or else this behaves as both addByPrefix and addByIndices and like the xml animations --- source/funkin/backend/FunkinSprite.hx | 43 ++++++++++++++++++++------ source/funkin/backend/utils/XMLUtil.hx | 37 +++++++++++----------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/source/funkin/backend/FunkinSprite.hx b/source/funkin/backend/FunkinSprite.hx index b04cdffd..2e4df83d 100644 --- a/source/funkin/backend/FunkinSprite.hx +++ b/source/funkin/backend/FunkinSprite.hx @@ -328,7 +328,31 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I lastAnimContext = Context; } - public function getAnim(name:String):OneOfTwo { + public inline function addAnim(name:String, prefix:String, frameRate:Float = 24, ?looped:Bool, ?forced:Bool, ?indices:Array, x:Float = 0, y:Float = 0, animType:XMLAnimType = NONE) + { + return XMLUtil.addAnimToSprite(this, { + name: name, + anim: prefix, + fps: frameRate, + loop: looped == null ? animType == LOOP : looped, + animType: animType, + x: x, + y: y, + indices: indices, + forced: forced + }); + } + + public inline function removeAnim(name:String) + { + if (animateAtlas != null) + @:privateAccess animateAtlas.anim.animsMap.remove(name); + else + animation.remove(name); + } + + public function getAnim(name:String):OneOfTwo + { if(animateAtlas != null) return animateAtlas.anim.getByName(name); return animation.getByName(name); @@ -341,7 +365,7 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I return FlxPoint.weak(0, 0); } - public inline function hasAnimation(AnimName:String):Bool @:privateAccess + public inline function hasAnim(AnimName:String):Bool @:privateAccess return animateAtlas != null ? (animateAtlas.anim.animsMap.exists(AnimName) || animateAtlas.anim.symbolDictionary.exists(AnimName)) : animation.exists(AnimName); @@ -364,13 +388,6 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I return animateAtlas != null ? animateAtlas.anim.reversed : animation.curAnim != null ? animation.curAnim.reversed : false; } - public inline function removeAnimation(name:String) { - if (animateAtlas != null) - @:privateAccess animateAtlas.anim.animsMap.remove(name); - else - animation.remove(name); - } - public inline function getNameList():Array { if (animateAtlas != null) return [for (name in @:privateAccess animateAtlas.anim.animsMap.keys()) name]; @@ -378,7 +395,8 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I return animation.getNameList(); } - public inline function stopAnimation() { + public inline function stopAnim() + { if (animateAtlas != null) animateAtlas.anim.pause(); else @@ -393,6 +411,11 @@ class FunkinSprite extends FlxSkewedSprite implements IBeatReceiver implements I public inline function isAnimAtEnd() { return animateAtlas != null ? animateAtlas.anim.isAtEnd : (animation.curAnim != null ? animation.curAnim.isAtEnd : false); } + + // Backwards compat (the names used to be all different and it sucked, please lets use the same format in the future) - Nex + public inline function hasAnimation(AnimName:String) return hasAnim(AnimName); + public inline function removeAnimation(name:String) return removeAnim(name); + public inline function stopAnimation() return stopAnim(); #end // Getter / Setters diff --git a/source/funkin/backend/utils/XMLUtil.hx b/source/funkin/backend/utils/XMLUtil.hx index afb8ae04..df551985 100644 --- a/source/funkin/backend/utils/XMLUtil.hx +++ b/source/funkin/backend/utils/XMLUtil.hx @@ -164,19 +164,17 @@ class XMLUtil { if(node.hasNode.anim) { for(anim in node.nodes.anim) addXMLAnimation(spr, anim); - } else { - if (spr.frames != null && spr.frames.frames != null) { - addAnimToSprite(spr, { - name: "idle", - anim: null, - fps: 24, - loop: spr.spriteAnimType == LOOP, - animType: spr.spriteAnimType, - x: 0, - y: 0, - indices: [for(i in 0...spr.frames.frames.length) i] - }); - } + } else if (spr.frames != null && spr.frames.frames != null) { + addAnimToSprite(spr, { + name: "idle", + anim: null, + fps: 24, + loop: spr.spriteAnimType == LOOP, + animType: spr.spriteAnimType, + x: 0, + y: 0, + indices: [for(i in 0...spr.frames.frames.length) i] + }); } return spr; @@ -238,16 +236,17 @@ class XMLUtil { if(animData.anim == null) return MISSING_PROPERTY; - if (animData.indices.length > 0) + if (animData.indices != null && animData.indices.length > 0) animateAnim.addBySymbolIndices(animData.name, animData.anim, animData.indices, animData.fps, animData.loop); else animateAnim.addBySymbol(animData.name, animData.anim, animData.fps, animData.loop); } else { - if (animData.anim == null && animData.indices.length > 0) - sprite.animation.add(animData.name, animData.indices, animData.fps, animData.loop); - else if (animData.indices.length > 0) - sprite.animation.addByIndices(animData.name, animData.anim, animData.indices, "", animData.fps, animData.loop); - else + if (animData.indices != null && animData.indices.length > 0) { + if (animData.anim == null) + sprite.animation.add(animData.name, animData.indices, animData.fps, animData.loop); + else + sprite.animation.addByIndices(animData.name, animData.anim, animData.indices, "", animData.fps, animData.loop); + } else sprite.animation.addByPrefix(animData.name, animData.anim, animData.fps, animData.loop); }