From e6504e3c3ee9daa9e84d1abd6f87c573651b6b0a Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Thu, 19 Feb 2026 11:00:00 -0700 Subject: [PATCH 01/24] New Stage class WIP --- source/funkin/backend/utils/CoolUtil.hx | 2 +- source/funkin/game/NewStage.hx | 509 ++++++++++++++++++++++++ 2 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 source/funkin/game/NewStage.hx diff --git a/source/funkin/backend/utils/CoolUtil.hx b/source/funkin/backend/utils/CoolUtil.hx index 60f9251111..d8be42dcb1 100644 --- a/source/funkin/backend/utils/CoolUtil.hx +++ b/source/funkin/backend/utils/CoolUtil.hx @@ -706,7 +706,7 @@ final class CoolUtil * @param xml XML to get the attribute from * @param name Name of the attribute */ - public static inline function getAtt(xml:Access, name:String) { + public static inline function getAtt(xml:Access, name:String):Null { /*if (!xml.has.resolve(name)) return null; return xml.att.resolve(name);*/ // Reason for change: diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx new file mode 100644 index 0000000000..c8a749a70f --- /dev/null +++ b/source/funkin/game/NewStage.hx @@ -0,0 +1,509 @@ +package funkin.game; + +import hscript.IHScriptCustomBehaviour; +import funkin.backend.scripting.ScriptPack; +import funkin.game.Stage.StageCharPos; +import funkin.game.Stage.StageCharPosInfo; +import funkin.backend.scripting.Script; +import funkin.backend.utils.XMLUtil; +import funkin.backend.system.interfaces.IBeatReceiver; + +import flixel.group.FlxGroup; +import flixel.math.FlxPoint; + +import haxe.xml.Access; + +using StringTools; + +class StageLayer extends FlxTypedGroup implements IBeatReceiver implements IHScriptCustomBehaviour { + private static final __instanceFields = Type.getInstanceFields(StageLayer); + + public var name:String; + + private var stageSprites:Map = []; + private var stageLayers:Map = []; + + public var onAddSprite:FlxObject -> Void = null; + public var onAddLayer:StageLayer -> Void = null; + + public function new(name:String = "stage") { + super(); + this.name = name; + } + + public function addSprite(name:String, spr:FlxObject):FlxObject { + this.add(spr); + stageSprites.set(name, this.members.indexOf(spr)); // TODO: faster way to set the index + if(onAddSprite != null) + onAddSprite(spr); + return spr; + } + + public function addLayer(name:String, layer:StageLayer):StageLayer { + this.add(layer); + stageLayers.set(name, this.members.indexOf(layer)); + if(onAddLayer != null) + onAddLayer(layer); + return layer; + } + + public function beatHit(curBeat:Int) { + for(m in members) if(m != null && m is IBeatReceiver) cast(m, IBeatReceiver).beatHit(curBeat); + } + public function stepHit(curStep:Int) { + for(m in members) if(m != null && m is IBeatReceiver) cast(m, IBeatReceiver).stepHit(curStep); + } + public function measureHit(curMeasure:Int) { + for(m in members) if(m != null && m is IBeatReceiver) cast(m, IBeatReceiver).measureHit(curMeasure); + } + + public function hget(name:String):Dynamic { + if (__instanceFields.contains(name) || __instanceFields.contains('get_${name}')) + return Reflect.getProperty(this, name); + if(stageSprites.exists(name)) return this.members[stageSprites[name]]; + if(stageLayers.exists(name)) return this.members[stageLayers[name]]; + return null; + } + + public function hset(name:String, val:Dynamic):Dynamic { + if (__instanceFields.contains(name) || __instanceFields.contains('set_${name}')) { + Reflect.setProperty(this, name, val); + return val; + } + if(stageSprites.exists(name)) return this.members[stageSprites[name]] = val; + if(stageLayers.exists(name)) return this.members[stageLayers[name]] = val; + return null; + } +} + +class NewStage extends StageLayer { + private static final __instanceFields = Type.getInstanceFields(NewStage); + + public static function getStage(name:String):NewStage { + return new NewStage(name); + } + + private static final DEFAULT_ATTRIBUTES:Array = ["name", "startCamPosX", "startCamPosY", "zoom", "folder"]; + + private static inline function getDefaultPos(name:String):StageCharPosInfo { + return switch(name) { + case "boyfriend" | "bf" | "player": + {x: 770, y: 100, scroll: 1, flip: true}; + case "girlfriend" | "gf": + {x: 400, y: 130, scroll: 0.95, flip: false}; + case "dad" | "opponent": + {x: 100, y: 100, scroll: 1, flip: false}; + default: + {x: 0, y: 0, scroll: 1, flip: false}; + } + } + + public final fileName:String; + public final xmlFilePath:String; + public final scriptFilePath:String; + + public var xmlFile:Access; + + public var script:Script; + public var allowScripts:Bool = true; + public var xmlImportedScripts:Array = []; + + public var defaultZoom:Float = 1.05; + public var spritesParentFolder = ""; + public var extra:Map = []; + public var startCam:FlxPoint = FlxPoint.get(); + + // Callbacks + public var onStageScriptLoad:Script -> Void; + public var onStartCamSet:FlxPoint -> Float -> Void; + public var onPrepareInfo:Access -> XMLImportedScriptInfo; + public var onRemoveInfo:Script -> Void; + public var onXMLLoaded:(Access, Array)->Array = null; + public var onNodeLoaded:(Access, Dynamic)->Dynamic = null; + public var onNodeFinished:(Access, Dynamic)->Void = null; + public var onXMLPostLoaded:(Access, Array)->Array = null; + public var onRatingSet:Float->Float->FlxBasic; + public var onPostStageCreation:(Access, Array)->Array; + public var onStageDestroy:NewStage -> Void; + public var onSilentDestroy:Script -> Void; + + private var characterPosLookup:Map = []; + + /** + * Sets the sprites in the script, so you can access them by the name. + **/ + public function setStagesSprites(script:Script) + for (k=>e in stageSprites) script.set(k, this.members[e]); + + public function new(stage:String, load:Bool = false) { + super(); + + fileName = stage; + xmlFilePath = Paths.xml('stages/$fileName'); + scriptFilePath = Paths.script('data/stages/$fileName'); + if(Assets.exists(xmlFilePath)) { + try xmlFile = new Access(Xml.parse(Assets.getText(xmlFilePath)).firstElement()) + catch (e) Logs.trace('Couldn\'t load stage "$xmlFilePath": ${e.message}', ERROR); + } + + if(load) + loadStage(); + } + + private function loadStage(loadAll:Bool = false) { + if(allowScripts) { + script = Script.create(scriptFilePath); + // Performed by "onStageScriptLoad" + // PlayState.instance.scripts.add(stageScript); + if (onStageScriptLoad != null) + onStageScriptLoad(script); + script.load(); + } + + if(xmlFile == null) { + postLoadStage(null); + return; + } + + loadStartCam(); + + this.name = xmlFile.getAtt("name").getDefault(fileName); + + if(onStartCamSet != null) + onStartCamSet(startCam, defaultZoom); + + if (xmlFile.has.folder) { + spritesParentFolder = xmlFile.att.folder; + if (!spritesParentFolder.endsWith("/")) + spritesParentFolder += "/"; + } + + // Load custom attributes + loadCustomAttributes(); + + var elems:Array = []; + + // some way to tag that the sprites are from the group + checkMemoryMode(xmlFile, loadAll, elems); + /* + for (node in xmlFile.elements) + { + if (node.name == "high-memory" && (!Options.lowMemoryMode || forceLoadAll)) + for (e in node.elements) + __pushNcheckNode(elems, e); + else if (node.name == "low-memory" && (Options.lowMemoryMode || forceLoadAll)) + for (e in node.elements) + __pushNcheckNode(elems, e); + else + __pushNcheckNode(elems, node); + } + */ + + // This should be performed by the "onXMLLoaded" callback + /* + if (PlayState.instance == state) { + event = EventManager.get(StageXMLEvent).recycle(this, stageXML, elems); + elems = PlayState.instance.gameAndCharsEvent("onStageXMLParsed", event).elems; + } + */ + if(onXMLLoaded != null) { + elems = onXMLLoaded(xmlFile, elems); + } + + loadLayer(this, elems); + + postLoadStage(elems); + } + + private inline function loadStartCam() { + startCam.x = Std.parseFloat(xmlFile.getAtt("startCamPosX")).getDefaultFloat(0); + startCam.y = Std.parseFloat(xmlFile.getAtt("startCamPosY")).getDefaultFloat(0); + defaultZoom = Std.parseFloat(xmlFile.getAtt("zoom")).getDefaultFloat(1.05); + /* + var parsed:Null; + if ((parsed = Std.parseFloat(xmlFile.getAtt("startCamPosX"))).isNotNull()) + startCam.x = parsed; + if ((parsed = Std.parseFloat(xmlFile.getAtt("startCamPosY"))).isNotNull()) + startCam.y = parsed; + if ((parsed = Std.parseFloat(xmlFile.getAtt("zoom"))).isNotNull()) + defaultZoom = parsed; + */ + } + + private inline function loadCustomAttributes() { + for (att in xmlFile.x.attributes()) + if (!DEFAULT_ATTRIBUTES.contains(att)) + extra.set(att, xmlFile.x.get(att)); + } + + private function loadLayer(layer:StageLayer, elems:Array) { + for(node in elems) { + var sprite = switch(node.name) { + case "layer": // TODO + if (!node.has.name) continue; + var layerName = node.att.name; + var layer = new StageLayer(layerName); + // recursive so it will allow nested layers + loadLayer(layer, [for(n in node.elements) n]); + addLayer(layerName, layer); + case "sprite" | "spr" | "sparrow": + if (!node.has.sprite || !node.has.name) continue; + + var spr = XMLUtil.createSpriteFromXML(node, spritesParentFolder, LOOP); + addSprite(spr.name, spr); + case "box" | "solid": + if (!node.has.name || !node.has.width || !node.has.height) + continue; + + var isSolid = node.name == "solid"; + + var spr = new FunkinSprite(); + (isSolid ? spr.makeSolid : spr.makeGraphic)(Std.parseInt(node.att.width), Std.parseInt(node.att.height), + (node.has.color) ? CoolUtil.getColorFromDynamic(node.att.color) : -1); + + if (isSolid) + node.x.remove("updateHitbox"); + for (a in ["width", "height", "color"]) + node.x.remove(a); + XMLUtil.loadSpriteFromXML(spr, node, "", NONE, false); + addSprite(spr.name, spr); + case "boyfriend" | "bf" | "player": + setCharPos("boyfriend", node, getDefaultPos("boyfriend")); + case "girlfriend" | "gf": + setCharPos("girlfriend", node, getDefaultPos("girlfriend")); + case "dad" | "opponent": + setCharPos("dad", node, getDefaultPos("dad")); + case "character" | "char": + if (!node.has.name) continue; + setCharPos(node.att.name, node); + case "ratings" | "combo": + if(onRatingSet == null) continue; + var ratingPos = { + x: Std.parseFloat(node.getAtt("x")), + y: Std.parseFloat(node.getAtt("y")) + } + onRatingSet(ratingPos.x, ratingPos.y); + case "use-extension" | "extension" | "ext": + if(XMLImportedScriptInfo.shouldLoadBefore(node)) continue; + if(onPrepareInfo != null && onPrepareInfo(node) == null) continue; + null; + default: null; + } + + if(onNodeLoaded != null) { + sprite = onNodeLoaded(node, sprite); + } + + if (sprite != null) { + for (e in node.nodes.property) + XMLUtil.applyXMLProperty(sprite, e); + } + + if(onNodeFinished != null) { + onNodeFinished(node, sprite); + } + } + } + + private function postLoadStage(?elems:Array) { + for(defaultChar in ["girlfriend", "dad", "boyfriend"]) { + if(!characterPosLookup.exists(defaultChar)) + setCharPos(defaultChar, null, getDefaultPos(defaultChar)); + } + + if(allowScripts) { + setStagesSprites(this.script); + + // i know this for gets run twice under, but its better like this in case a script modifies the short lived ones, i dont wanna save them in an array; more dynamic like this - Nex + for (info in xmlImportedScripts) if (info.importStageSprites) { + var script = info.getScript(); + if (script != null) + setStagesSprites(script); + } + + // idk lemme check anyways just in case scripts did smth - Nex + //if (event != null) PlayState.instance.gameAndCharsEvent("onPostStageCreation", event); + if(onPostStageCreation != null) + onPostStageCreation(xmlFile, elems); + + // shortlived scripts destroy when the stage finishes setting up - Nex + for (info in xmlImportedScripts) if (info.shortLived) { + var script = info.getScript(); + if (script == null) continue; + + //PlayState.instance.scripts.remove(script); + if(onRemoveInfo != null) + onRemoveInfo(script); + script.destroy(); + } + } + + if(xmlFile != null && onXMLPostLoaded != null) { + elems = onXMLPostLoaded(xmlFile, elems); + } + } + + private function setCharPos(name:String, ?node:Access, ?defaultCharPos:StageCharPosInfo) { + var charPos = new StageCharPos(); + charPos.visible = charPos.active = false; + charPos.name = name; + + if(defaultCharPos != null) { + charPos.setPosition(defaultCharPos.x, defaultCharPos.y); + charPos.scrollFactor.set(defaultCharPos.scroll, defaultCharPos.scroll); + charPos.flipX = defaultCharPos.flip; + } + + if(node != null) { + charPos.x = Std.parseFloat(node.getAtt("x")).getDefault(charPos.x); + charPos.y = Std.parseFloat(node.getAtt("y")).getDefault(charPos.y); + charPos.charSpacingX = Std.parseFloat(node.getAtt("spacingx")).getDefault(charPos.charSpacingX); + charPos.charSpacingY = Std.parseFloat(node.getAtt("spacingy")).getDefault(charPos.charSpacingY); + charPos.camxoffset = Std.parseFloat(node.getAtt("camxoffset")).getDefault(charPos.camxoffset); + charPos.camyoffset = Std.parseFloat(node.getAtt("camyoffset")).getDefault(charPos.camyoffset); + charPos.skewX = Std.parseFloat(node.getAtt("skewx")).getDefault(charPos.skewX); + charPos.skewY = Std.parseFloat(node.getAtt("skewy")).getDefault(charPos.skewY); + charPos.alpha = Std.parseFloat(node.getAtt("alpha")).getDefault(charPos.alpha); + charPos.angle = Std.parseFloat(node.getAtt("angle")).getDefault(charPos.angle); + charPos.flipX = (node.has.flip || node.has.flipX) ? (node.getAtt("flip") == "true" || node.getAtt("flipX") == "true") : charPos.flipX; + charPos.zoomFactor = Std.parseFloat(node.getAtt("zoomfactor")).getDefault(charPos.zoomFactor); + // Scaling + if (node.has.scale) { + /* + var scale:Null = Std.parseFloat(node.att.scale); + if (scale.isNotNull()) charPos.scale.set(scale, scale); + */ + var scale:Float = Std.parseFloat(node.att.scale).getDefaultFloat(1); + charPos.scale.set(scale, scale); + } + if (node.has.scalex) { + /* + var scale:Null = Std.parseFloat(node.att.scalex); + if (scale.isNotNull()) charPos.scale.x = scale; + */ + charPos.scale.x = Std.parseFloat(node.att.scalex).getDefaultFloat(1); + } + if (node.has.scaley) { + /* + var scale:Null = Std.parseFloat(node.att.scaley); + if (scale.isNotNull()) charPos.scale.y = scale; + */ + charPos.scale.y = Std.parseFloat(node.att.scaley).getDefaultFloat(1); + } + + // Scroll Factor + if (node.has.scroll) { + /* + var scroll:Null = Std.parseFloat(node.att.scroll); + if (scroll != null) charPos.scrollFactor.set(scroll, scroll); + */ + var scroll:Float = Std.parseFloat(node.att.scroll).getDefaultFloat(1); + charPos.scrollFactor.set(scroll, scroll); + } + if (node.has.scrollx) { + /* + var scroll:Null = Std.parseFloat(node.att.scrollx); + if (scroll != null) charPos.scrollFactor.x = scroll; + */ + charPos.scrollFactor.x = Std.parseFloat(node.att.scrollx).getDefaultFloat(1); + } + if (node.has.scrolly) { + /* + var scroll:Null = Std.parseFloat(node.att.scrolly); + if (scroll != null) charPos.scrollFactor.y = scroll; + */ + charPos.scrollFactor.y = Std.parseFloat(node.att.scrolly).getDefaultFloat(1); + } + } + return add(characterPosLookup[name] = charPos); + } + + /** + * Checks if a character is flipped or not. + * @param posName The name of the character position + * @param def The default value + **/ + public inline function isCharFlipped(posName:String, isPlayer:Bool = false) + return characterPosLookup.exists(posName) ? characterPosLookup[posName].flipX : isPlayer; + + public function applyCharPos(char:Character, posName:String, id:Float = 0) { + // Should I add the characters to the "stageSprites" list? + var charName:String = char.curCharacter; + var charPos:Null = characterPosLookup.exists(charName) ? characterPosLookup.get(charName) : characterPosLookup.get(posName); + if(charPos != null) { + charPos.prepareCharacter(char, id); + this.insert(this.members.indexOf(charPos), char); + } + else + this.add(char); + } + + /** + * Same of destroy, but doesn't call the various script events. + * @param destroySprites Whether the stage sprites should be destroyed + * @param destroyScript Whether the stage script should be destroyed + **/ + public function destroySilently(destroySprites:Bool = true, destroyScript:Bool = true) { + if(destroyScript && script != null) { + // if (PlayState.instance == state && PlayState.instance.scripts != null) PlayState.instance.scripts.remove(stageScript); + if(onSilentDestroy != null) + onSilentDestroy(this.script); + script.destroy(); + } + + // Not need to destroy the sprites manually since "destroy" does it. - Jamextreme140 + /* + if(destroySprites) + this.forEach((spr) -> { + spr.destroy(); + }, true); + */ + startCam.put(); + super.destroy(); + } + + override function destroy() { + // if (PlayState.instance == state && PlayState.instance.scripts != null) PlayState.instance.gameAndCharsCall("onStageDestroy", [this]); + if(onStageDestroy != null) + onStageDestroy(this); + script?.call("destroy"); + destroySilently(); + } + + @:dox(hide) private function checkMemoryMode(xml:Access, loadAll:Bool, elems:Array) { + for(node in xml.elements) { + if (node.name == "high-memory" && (!Options.lowMemoryMode || loadAll)) + for (e in node.elements) + pushNode(e, elems); + else if (node.name == "low-memory" && (Options.lowMemoryMode || loadAll)) + for (e in node.elements) + pushNode(e, elems); + else if (node.name == "layer") + checkMemoryMode(node, loadAll, elems); // recursive check in layers + else + pushNode(node, elems); + } + } + + @:dox(hide) private function pushNode(node:Access, elems:Array) { + elems.push(node); + if ((node.name == "use-extension" || node.name == "extension" || node.name == "ext") && XMLImportedScriptInfo.shouldLoadBefore(node)) + if(onPrepareInfo != null) // :3 + onPrepareInfo(node); + } + + // bruh... + override function hget(name:String):Dynamic { + if (__instanceFields.contains(name) || __instanceFields.contains('get_${name}')) + return Reflect.getProperty(this, name); + return super.hget(name); + } + + override function hset(name:String, val:Dynamic):Dynamic { + if (__instanceFields.contains(name) || __instanceFields.contains('set_${name}')) { + Reflect.setProperty(this, name, val); + return val; + } + return super.hset(name, val); + } +} From 201944311b6e65ebb6887cce13f7c191d4b5d38d Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Fri, 20 Feb 2026 10:58:00 -0700 Subject: [PATCH 02/24] Implementing new Stage class for Gameplay UNTESTED --- .../scripting/events/stage/StageNodeEvent.hx | 2 +- .../scripting/events/stage/StageXMLEvent.hx | 2 +- source/funkin/game/Character.hx | 1 + source/funkin/game/NewStage.hx | 74 +++++++++++++++---- source/funkin/game/PlayState.hx | 48 +++++++++++- 5 files changed, 109 insertions(+), 18 deletions(-) diff --git a/source/funkin/backend/scripting/events/stage/StageNodeEvent.hx b/source/funkin/backend/scripting/events/stage/StageNodeEvent.hx index 505e92b897..1935a28e6c 100644 --- a/source/funkin/backend/scripting/events/stage/StageNodeEvent.hx +++ b/source/funkin/backend/scripting/events/stage/StageNodeEvent.hx @@ -7,7 +7,7 @@ final class StageNodeEvent extends CancellableEvent { /** * The stage instance */ - public var stage:Stage; + public var stage:Dynamic; /** * The node which is currently being parsed diff --git a/source/funkin/backend/scripting/events/stage/StageXMLEvent.hx b/source/funkin/backend/scripting/events/stage/StageXMLEvent.hx index 9bc6f10b60..3336030c58 100644 --- a/source/funkin/backend/scripting/events/stage/StageXMLEvent.hx +++ b/source/funkin/backend/scripting/events/stage/StageXMLEvent.hx @@ -7,7 +7,7 @@ final class StageXMLEvent extends CancellableEvent { /** * The stage instance */ - public var stage:Stage; + public var stage:Dynamic; /** * The xml diff --git a/source/funkin/game/Character.hx b/source/funkin/game/Character.hx index 7ecbe0b777..ae2c085dba 100644 --- a/source/funkin/game/Character.hx +++ b/source/funkin/game/Character.hx @@ -14,6 +14,7 @@ import funkin.backend.scripting.Script; import funkin.backend.scripting.ScriptPack; import funkin.backend.scripting.events.CancellableEvent; import funkin.backend.scripting.events.character.*; +import funkin.backend.scripting.events.sprite.PlayAnimContext; import funkin.backend.scripting.events.sprite.*; import funkin.backend.scripting.events.PointEvent; import funkin.backend.scripting.events.DrawEvent; diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index c8a749a70f..f247993a0c 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -1,5 +1,6 @@ package funkin.game; +import funkin.backend.scripting.events.stage.StageXMLEvent; import hscript.IHScriptCustomBehaviour; import funkin.backend.scripting.ScriptPack; import funkin.game.Stage.StageCharPos; @@ -15,6 +16,9 @@ import haxe.xml.Access; using StringTools; +/** + * A class that handles loading a stage and putting the sprites into the state. +**/ class StageLayer extends FlxTypedGroup implements IBeatReceiver implements IHScriptCustomBehaviour { private static final __instanceFields = Type.getInstanceFields(StageLayer); @@ -26,6 +30,14 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public var onAddSprite:FlxObject -> Void = null; public var onAddLayer:StageLayer -> Void = null; + public inline function getSprite(name:String):Null { + return stageSprites.exists(name) ? this.members[stageSprites[name]] : null; + } + + public inline function getLayer(name:String):Null { + return stageLayers.exists(name) ? cast this.members[stageLayers[name]] : null; + } + public function new(name:String = "stage") { super(); this.name = name; @@ -118,12 +130,12 @@ class NewStage extends StageLayer { public var onStartCamSet:FlxPoint -> Float -> Void; public var onPrepareInfo:Access -> XMLImportedScriptInfo; public var onRemoveInfo:Script -> Void; - public var onXMLLoaded:(Access, Array)->Array = null; + public var onXMLLoaded:(StageXMLEvent)->Array = null; public var onNodeLoaded:(Access, Dynamic)->Dynamic = null; public var onNodeFinished:(Access, Dynamic)->Void = null; public var onXMLPostLoaded:(Access, Array)->Array = null; public var onRatingSet:Float->Float->FlxBasic; - public var onPostStageCreation:(Access, Array)->Array; + public var onPostStageCreation:StageXMLEvent->Void; public var onStageDestroy:NewStage -> Void; public var onSilentDestroy:Script -> Void; @@ -150,6 +162,8 @@ class NewStage extends StageLayer { loadStage(); } + private var stageEvent:StageXMLEvent; + private function loadStage(loadAll:Bool = false) { if(allowScripts) { script = Script.create(scriptFilePath); @@ -207,7 +221,8 @@ class NewStage extends StageLayer { } */ if(onXMLLoaded != null) { - elems = onXMLLoaded(xmlFile, elems); + stageEvent = EventManager.get(StageXMLEvent).recycle(this, xmlFile, elems); + elems = onXMLLoaded(stageEvent); } loadLayer(this, elems); @@ -239,7 +254,7 @@ class NewStage extends StageLayer { private function loadLayer(layer:StageLayer, elems:Array) { for(node in elems) { var sprite = switch(node.name) { - case "layer": // TODO + case "layer": if (!node.has.name) continue; var layerName = node.att.name; var layer = new StageLayer(layerName); @@ -258,13 +273,14 @@ class NewStage extends StageLayer { var isSolid = node.name == "solid"; var spr = new FunkinSprite(); - (isSolid ? spr.makeSolid : spr.makeGraphic)(Std.parseInt(node.att.width), Std.parseInt(node.att.height), - (node.has.color) ? CoolUtil.getColorFromDynamic(node.att.color) : -1); - - if (isSolid) - node.x.remove("updateHitbox"); - for (a in ["width", "height", "color"]) - node.x.remove(a); + var w:Int = Std.parseInt(node.att.width); + var h:Int = Std.parseInt(node.att.height); + var c:flixel.util.FlxColor = (node.has.color) ? CoolUtil.getColorFromDynamic(node.att.color) : -1; + if(isSolid) spr.makeSolid(w, h, c); + else spr.makeGraphic(w, h, c); + + if (isSolid) node.x.remove("updateHitbox"); + node.x.remove("width"); node.x.remove("height"); node.x.remove("color"); XMLUtil.loadSpriteFromXML(spr, node, "", NONE, false); addSprite(spr.name, spr); case "boyfriend" | "bf" | "player": @@ -323,8 +339,8 @@ class NewStage extends StageLayer { // idk lemme check anyways just in case scripts did smth - Nex //if (event != null) PlayState.instance.gameAndCharsEvent("onPostStageCreation", event); - if(onPostStageCreation != null) - onPostStageCreation(xmlFile, elems); + if(onPostStageCreation != null && stageEvent != null) + onPostStageCreation(stageEvent); // shortlived scripts destroy when the stage finishes setting up - Nex for (info in xmlImportedScripts) if (info.shortLived) { @@ -506,4 +522,36 @@ class NewStage extends StageLayer { } return super.hset(name, val); } + + // Backwards compatibility + public var stagePath(get, never):String; + public var stageFile(get, never):String; + public var stageName(get, set):String; + public var stageScript(get, never):Script; + + public var characterPoses(get, never):Map; + + function get_stageScript():Script { + return this.script; + } + + function get_stagePath():String { + return this.xmlFilePath; + } + + function get_stageFile():String { + return this.fileName; + } + + function get_stageName():String { + return this.name; + } + + function set_stageName(name:String):String { + return this.name = name; + } + + function get_characterPoses():Map { + return this.characterPosLookup; + } } diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index cb4570d259..1d983f13e4 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -22,8 +22,10 @@ import funkin.backend.scripting.ScriptPack; import funkin.backend.scripting.events.*; import funkin.backend.scripting.events.gameplay.*; import funkin.backend.scripting.events.note.*; +import funkin.backend.scripting.events.stage.*; import funkin.backend.system.Conductor; import funkin.backend.system.RotatingSpriteGroup; +import funkin.backend.utils.XMLUtil.XMLImportedScriptInfo; import funkin.editors.SaveWarning; import funkin.editors.charter.Charter; import funkin.editors.charter.CharterSelection; @@ -122,7 +124,7 @@ class PlayState extends MusicBeatState /** * Current Stage. */ - public var stage:Stage; + public var stage(default, set):NewStage; /** * Whenever the score will save when you beat the song. */ @@ -713,7 +715,7 @@ class PlayState extends MusicBeatState add(camFollow); if (SONG.stage == null || SONG.stage.trim() == "") SONG.stage = Flags.DEFAULT_STAGE; - add(stage = new Stage(SONG.stage)); + add(stage = new NewStage(SONG.stage)); if (!chartingMode || Options.charterEnablePlaytestScripts) { switch(SONG.meta.name) { @@ -786,7 +788,7 @@ class PlayState extends MusicBeatState }) : strumLine.position; if (strumLine.characters != null) for(k=>charName in strumLine.characters) { var char = new Character(0, 0, charName, stage.isCharFlipped(stage.characterPoses[charName] != null ? charName : charPosName, strumLine.type == 1)); - stage.applyCharStuff(char, charPosName, k); + stage.applyCharPos(char, charPosName, k); chars.push(char); } @@ -2191,6 +2193,46 @@ class PlayState extends MusicBeatState private inline static function get_campaignAccuracy() return campaignAccuracyCount == 0 ? 0 : campaignAccuracyTotal / campaignAccuracyCount; + + private function set_stage(stage:NewStage):NewStage { + stage.onStageScriptLoad = (script) -> {scripts.add(script);} + stage.onStartCamSet = (startCam, defaultZoom) -> { + camFollow.x = startCam.x; + camFollow.y = startCam.y; + defaultCamZoom = defaultZoom; + } + stage.onXMLLoaded = (stageEvent) -> { + return this.gameAndCharsEvent("onStageXMLParsed", stageEvent).elems; + } + stage.onRatingSet = (x, y) -> { + comboGroup.setPosition(x, y); + add(comboGroup); + return comboGroup; + } + stage.onPrepareInfo = (node) -> { + return XMLImportedScriptInfo.prepareInfos(node, this.scripts, (infos) -> { + stage.xmlImportedScripts.push(infos); + }); + } + stage.onRemoveInfo = (script) -> { + scripts.remove(script); + } + stage.onNodeLoaded = (node, sprite) -> { + return this.gameAndCharsEvent("onStageNodeParsed", EventManager.get(StageNodeEvent).recycle(stage, node, sprite, node.name)).sprite; + } + stage.onPostStageCreation = (stageEvent) -> { + this.gameAndCharsEvent("onPostStageCreation", stageEvent); + } + stage.onSilentDestroy = (script) -> { + if(this.scripts != null) + scripts.remove(script); + } + stage.onStageDestroy = (stage) -> { + this.gameAndCharsCall("onStageDestroy", [stage]); + } + + return this.stage = stage; + } #end /** From b261875c8dbd9f574609aa30163508546fc64a6b Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Sat, 21 Feb 2026 13:46:10 -0700 Subject: [PATCH 03/24] `onAddSprite` & `onAddLayer` is now `FlxTypedSignal` instead of a `Null` because why not Cleaned up a bit of formatting as well --- source/funkin/game/NewStage.hx | 157 ++++++++++++++------------------- 1 file changed, 64 insertions(+), 93 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index f247993a0c..9d8d1b0380 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -1,18 +1,24 @@ package funkin.game; -import funkin.backend.scripting.events.stage.StageXMLEvent; +import haxe.xml.Access; + import hscript.IHScriptCustomBehaviour; -import funkin.backend.scripting.ScriptPack; + import funkin.game.Stage.StageCharPos; import funkin.game.Stage.StageCharPosInfo; -import funkin.backend.scripting.Script; + import funkin.backend.utils.XMLUtil; + +import funkin.backend.scripting.Script; +import funkin.backend.scripting.ScriptPack; +import funkin.backend.scripting.events.stage.StageXMLEvent; + import funkin.backend.system.interfaces.IBeatReceiver; import flixel.group.FlxGroup; import flixel.math.FlxPoint; -import haxe.xml.Access; +import flixel.util.FlxSignal.FlxTypedSignal; using StringTools; @@ -27,8 +33,8 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private var stageSprites:Map = []; private var stageLayers:Map = []; - public var onAddSprite:FlxObject -> Void = null; - public var onAddLayer:StageLayer -> Void = null; + public var onAddSprite:FlxTypedSignal Void> = new FlxTypedSignal(); + public var onAddLayer:FlxTypedSignal Void> = new FlxTypedSignal(); public inline function getSprite(name:String):Null { return stageSprites.exists(name) ? this.members[stageSprites[name]] : null; @@ -46,16 +52,18 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public function addSprite(name:String, spr:FlxObject):FlxObject { this.add(spr); stageSprites.set(name, this.members.indexOf(spr)); // TODO: faster way to set the index - if(onAddSprite != null) - onAddSprite(spr); + + onAddSprite.dispatch(spr); + return spr; } public function addLayer(name:String, layer:StageLayer):StageLayer { this.add(layer); stageLayers.set(name, this.members.indexOf(layer)); - if(onAddLayer != null) - onAddLayer(layer); + + onAddLayer.dispatch(layer); + return layer; } @@ -70,7 +78,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem } public function hget(name:String):Dynamic { - if (__instanceFields.contains(name) || __instanceFields.contains('get_${name}')) + if (__instanceFields.contains(name) || __instanceFields.contains('get_$name')) return Reflect.getProperty(this, name); if(stageSprites.exists(name)) return this.members[stageSprites[name]]; if(stageLayers.exists(name)) return this.members[stageLayers[name]]; @@ -78,7 +86,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem } public function hset(name:String, val:Dynamic):Dynamic { - if (__instanceFields.contains(name) || __instanceFields.contains('set_${name}')) { + if (__instanceFields.contains(name) || __instanceFields.contains('set_$name')) { Reflect.setProperty(this, name, val); return val; } @@ -127,15 +135,19 @@ class NewStage extends StageLayer { // Callbacks public var onStageScriptLoad:Script -> Void; - public var onStartCamSet:FlxPoint -> Float -> Void; + public var onPostStageCreation:StageXMLEvent->Void; + public var onPrepareInfo:Access -> XMLImportedScriptInfo; public var onRemoveInfo:Script -> Void; + public var onXMLLoaded:(StageXMLEvent)->Array = null; public var onNodeLoaded:(Access, Dynamic)->Dynamic = null; public var onNodeFinished:(Access, Dynamic)->Void = null; public var onXMLPostLoaded:(Access, Array)->Array = null; + + public var onStartCamSet:FlxPoint -> Float -> Void; public var onRatingSet:Float->Float->FlxBasic; - public var onPostStageCreation:StageXMLEvent->Void; + public var onStageDestroy:NewStage -> Void; public var onSilentDestroy:Script -> Void; @@ -153,28 +165,26 @@ class NewStage extends StageLayer { fileName = stage; xmlFilePath = Paths.xml('stages/$fileName'); scriptFilePath = Paths.script('data/stages/$fileName'); - if(Assets.exists(xmlFilePath)) { + if (Assets.exists(xmlFilePath)) { try xmlFile = new Access(Xml.parse(Assets.getText(xmlFilePath)).firstElement()) catch (e) Logs.trace('Couldn\'t load stage "$xmlFilePath": ${e.message}', ERROR); } - if(load) - loadStage(); + if (load) loadStage(); } private var stageEvent:StageXMLEvent; private function loadStage(loadAll:Bool = false) { - if(allowScripts) { + if (allowScripts) { script = Script.create(scriptFilePath); // Performed by "onStageScriptLoad" // PlayState.instance.scripts.add(stageScript); - if (onStageScriptLoad != null) - onStageScriptLoad(script); + if (onStageScriptLoad != null) onStageScriptLoad(script); script.load(); } - if(xmlFile == null) { + if (xmlFile == null) { postLoadStage(null); return; } @@ -183,7 +193,7 @@ class NewStage extends StageLayer { this.name = xmlFile.getAtt("name").getDefault(fileName); - if(onStartCamSet != null) + if (onStartCamSet != null) onStartCamSet(startCam, defaultZoom); if (xmlFile.has.folder) { @@ -276,12 +286,13 @@ class NewStage extends StageLayer { var w:Int = Std.parseInt(node.att.width); var h:Int = Std.parseInt(node.att.height); var c:flixel.util.FlxColor = (node.has.color) ? CoolUtil.getColorFromDynamic(node.att.color) : -1; - if(isSolid) spr.makeSolid(w, h, c); + if (isSolid) spr.makeSolid(w, h, c); else spr.makeGraphic(w, h, c); if (isSolid) node.x.remove("updateHitbox"); node.x.remove("width"); node.x.remove("height"); node.x.remove("color"); XMLUtil.loadSpriteFromXML(spr, node, "", NONE, false); + addSprite(spr.name, spr); case "boyfriend" | "bf" | "player": setCharPos("boyfriend", node, getDefaultPos("boyfriend")); @@ -370,66 +381,39 @@ class NewStage extends StageLayer { charPos.flipX = defaultCharPos.flip; } - if(node != null) { + if (node != null) { charPos.x = Std.parseFloat(node.getAtt("x")).getDefault(charPos.x); charPos.y = Std.parseFloat(node.getAtt("y")).getDefault(charPos.y); + charPos.charSpacingX = Std.parseFloat(node.getAtt("spacingx")).getDefault(charPos.charSpacingX); charPos.charSpacingY = Std.parseFloat(node.getAtt("spacingy")).getDefault(charPos.charSpacingY); + charPos.camxoffset = Std.parseFloat(node.getAtt("camxoffset")).getDefault(charPos.camxoffset); charPos.camyoffset = Std.parseFloat(node.getAtt("camyoffset")).getDefault(charPos.camyoffset); + charPos.skewX = Std.parseFloat(node.getAtt("skewx")).getDefault(charPos.skewX); charPos.skewY = Std.parseFloat(node.getAtt("skewy")).getDefault(charPos.skewY); + charPos.alpha = Std.parseFloat(node.getAtt("alpha")).getDefault(charPos.alpha); charPos.angle = Std.parseFloat(node.getAtt("angle")).getDefault(charPos.angle); charPos.flipX = (node.has.flip || node.has.flipX) ? (node.getAtt("flip") == "true" || node.getAtt("flipX") == "true") : charPos.flipX; charPos.zoomFactor = Std.parseFloat(node.getAtt("zoomfactor")).getDefault(charPos.zoomFactor); + // Scaling if (node.has.scale) { - /* - var scale:Null = Std.parseFloat(node.att.scale); - if (scale.isNotNull()) charPos.scale.set(scale, scale); - */ var scale:Float = Std.parseFloat(node.att.scale).getDefaultFloat(1); charPos.scale.set(scale, scale); } - if (node.has.scalex) { - /* - var scale:Null = Std.parseFloat(node.att.scalex); - if (scale.isNotNull()) charPos.scale.x = scale; - */ - charPos.scale.x = Std.parseFloat(node.att.scalex).getDefaultFloat(1); - } - if (node.has.scaley) { - /* - var scale:Null = Std.parseFloat(node.att.scaley); - if (scale.isNotNull()) charPos.scale.y = scale; - */ - charPos.scale.y = Std.parseFloat(node.att.scaley).getDefaultFloat(1); - } + if (node.has.scalex) charPos.scale.x = Std.parseFloat(node.att.scalex).getDefaultFloat(1); + if (node.has.scaley) charPos.scale.y = Std.parseFloat(node.att.scaley).getDefaultFloat(1); // Scroll Factor if (node.has.scroll) { - /* - var scroll:Null = Std.parseFloat(node.att.scroll); - if (scroll != null) charPos.scrollFactor.set(scroll, scroll); - */ var scroll:Float = Std.parseFloat(node.att.scroll).getDefaultFloat(1); charPos.scrollFactor.set(scroll, scroll); } - if (node.has.scrollx) { - /* - var scroll:Null = Std.parseFloat(node.att.scrollx); - if (scroll != null) charPos.scrollFactor.x = scroll; - */ - charPos.scrollFactor.x = Std.parseFloat(node.att.scrollx).getDefaultFloat(1); - } - if (node.has.scrolly) { - /* - var scroll:Null = Std.parseFloat(node.att.scrolly); - if (scroll != null) charPos.scrollFactor.y = scroll; - */ - charPos.scrollFactor.y = Std.parseFloat(node.att.scrolly).getDefaultFloat(1); - } + if (node.has.scrollx) charPos.scrollFactor.x = Std.parseFloat(node.att.scrollx).getDefaultFloat(1); + if (node.has.scrolly) charPos.scrollFactor.y = Std.parseFloat(node.att.scrolly).getDefaultFloat(1); } return add(characterPosLookup[name] = charPos); } @@ -442,6 +426,12 @@ class NewStage extends StageLayer { public inline function isCharFlipped(posName:String, isPlayer:Bool = false) return characterPosLookup.exists(posName) ? characterPosLookup[posName].flipX : isPlayer; + /** + * Applies the character position to the character. + * @param char The character to apply the position to. + * @param posName The name of the character position. + * @param id ?????? no fucking clue why does it have an ID it's never used!!!!!!!!!!!!!!!! + **/ public function applyCharPos(char:Character, posName:String, id:Float = 0) { // Should I add the characters to the "stageSprites" list? var charName:String = char.curCharacter; @@ -460,28 +450,21 @@ class NewStage extends StageLayer { * @param destroyScript Whether the stage script should be destroyed **/ public function destroySilently(destroySprites:Bool = true, destroyScript:Bool = true) { - if(destroyScript && script != null) { + if (destroyScript && script != null) { // if (PlayState.instance == state && PlayState.instance.scripts != null) PlayState.instance.scripts.remove(stageScript); - if(onSilentDestroy != null) - onSilentDestroy(this.script); + if (onSilentDestroy != null) onSilentDestroy(this.script); script.destroy(); } - // Not need to destroy the sprites manually since "destroy" does it. - Jamextreme140 - /* - if(destroySprites) - this.forEach((spr) -> { - spr.destroy(); - }, true); - */ startCam.put(); + + // Properly destroy the sprites here. super.destroy(); } override function destroy() { // if (PlayState.instance == state && PlayState.instance.scripts != null) PlayState.instance.gameAndCharsCall("onStageDestroy", [this]); - if(onStageDestroy != null) - onStageDestroy(this); + if (onStageDestroy != null) onStageDestroy(this); script?.call("destroy"); destroySilently(); } @@ -504,19 +487,19 @@ class NewStage extends StageLayer { @:dox(hide) private function pushNode(node:Access, elems:Array) { elems.push(node); if ((node.name == "use-extension" || node.name == "extension" || node.name == "ext") && XMLImportedScriptInfo.shouldLoadBefore(node)) - if(onPrepareInfo != null) // :3 + if (onPrepareInfo != null) // :3 onPrepareInfo(node); } // bruh... override function hget(name:String):Dynamic { - if (__instanceFields.contains(name) || __instanceFields.contains('get_${name}')) + if (__instanceFields.contains(name) || __instanceFields.contains('get_$name')) return Reflect.getProperty(this, name); return super.hget(name); } override function hset(name:String, val:Dynamic):Dynamic { - if (__instanceFields.contains(name) || __instanceFields.contains('set_${name}')) { + if (__instanceFields.contains(name) || __instanceFields.contains('set_$name')) { Reflect.setProperty(this, name, val); return val; } @@ -531,27 +514,15 @@ class NewStage extends StageLayer { public var characterPoses(get, never):Map; - function get_stageScript():Script { - return this.script; - } + function get_stageScript():Script { return this.script; } - function get_stagePath():String { - return this.xmlFilePath; - } + function get_stagePath():String { return this.xmlFilePath; } - function get_stageFile():String { - return this.fileName; - } + function get_stageFile():String { return this.fileName; } - function get_stageName():String { - return this.name; - } + function get_stageName():String { return this.name; } - function set_stageName(name:String):String { - return this.name = name; - } + function set_stageName(name:String):String { return this.name = name; } - function get_characterPoses():Map { - return this.characterPosLookup; - } + function get_characterPoses():Map { return this.characterPosLookup; } } From 9bca1d3f8d8b03148c77cd526ffdb338530fb31a Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Mon, 23 Feb 2026 11:38:33 -0700 Subject: [PATCH 04/24] stage load on setup for PlayState --- source/funkin/game/NewStage.hx | 10 ++-------- source/funkin/game/PlayState.hx | 2 ++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 9d8d1b0380..e1b072087b 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -1,25 +1,19 @@ package funkin.game; import haxe.xml.Access; - import hscript.IHScriptCustomBehaviour; import funkin.game.Stage.StageCharPos; import funkin.game.Stage.StageCharPosInfo; - import funkin.backend.utils.XMLUtil; - import funkin.backend.scripting.Script; -import funkin.backend.scripting.ScriptPack; import funkin.backend.scripting.events.stage.StageXMLEvent; - import funkin.backend.system.interfaces.IBeatReceiver; +import flixel.util.FlxSignal.FlxTypedSignal; import flixel.group.FlxGroup; import flixel.math.FlxPoint; -import flixel.util.FlxSignal.FlxTypedSignal; - using StringTools; /** @@ -175,7 +169,7 @@ class NewStage extends StageLayer { private var stageEvent:StageXMLEvent; - private function loadStage(loadAll:Bool = false) { + public function loadStage(loadAll:Bool = false) { if (allowScripts) { script = Script.create(scriptFilePath); // Performed by "onStageScriptLoad" diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 1d983f13e4..f609727b81 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -2230,6 +2230,8 @@ class PlayState extends MusicBeatState stage.onStageDestroy = (stage) -> { this.gameAndCharsCall("onStageDestroy", [stage]); } + + stage.loadStage(); return this.stage = stage; } From 7a2725c796fb09bea4ddf3cbf86578206fede104 Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Mon, 23 Feb 2026 12:40:29 -0700 Subject: [PATCH 05/24] StageLayer bounds --- source/funkin/game/NewStage.hx | 102 ++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index e1b072087b..3bf7f97b3d 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -1,5 +1,6 @@ package funkin.game; +import flixel.math.FlxRect; import haxe.xml.Access; import hscript.IHScriptCustomBehaviour; @@ -23,6 +24,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private static final __instanceFields = Type.getInstanceFields(StageLayer); public var name:String; + public var bounds(get, never):FlxRect; private var stageSprites:Map = []; private var stageLayers:Map = []; @@ -46,7 +48,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public function addSprite(name:String, spr:FlxObject):FlxObject { this.add(spr); stageSprites.set(name, this.members.indexOf(spr)); // TODO: faster way to set the index - + updateBounds(); onAddSprite.dispatch(spr); return spr; @@ -55,7 +57,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public function addLayer(name:String, layer:StageLayer):StageLayer { this.add(layer); stageLayers.set(name, this.members.indexOf(layer)); - + updateBounds(); onAddLayer.dispatch(layer); return layer; @@ -88,6 +90,102 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem if(stageLayers.exists(name)) return this.members[stageLayers[name]] = val; return null; } + + override function destroy() { + super.destroy(); + _bounds.put(); + } + + private var _bounds:FlxRect = FlxRect.get(); + private function get_bounds():FlxRect { + return _bounds; + } + + private function updateBounds() { + if(this.length == 0) return; + var x = findMinX(); + var y = findMinY(); + var width = findMaxX() - x; + var height = findMaxY() - y; + + _bounds.set(x, y, width, height); + } + + // From FlxSpriteGroup + + private function findMinX():Float { + if(this.length == 0) return 0; + var value = Math.POSITIVE_INFINITY; + + for(m in this.members) { + if(m == null) continue; + + var minX:Float; + if(m is StageLayer) minX = cast(m, StageLayer).findMinX(); + else minX = cast(m, FlxObject).x; + + if (minX < value) value = minX; + } + + return value; + } + + private function findMaxX():Float { + if(this.length == 0) return 0; + var value = Math.NEGATIVE_INFINITY; + + for(m in this.members) { + if(m == null) continue; + + var maxX:Float; + if(m is StageLayer) maxX = cast(m, StageLayer).findMaxX(); + else { + var obj:FlxObject = cast m; + maxX = obj.x + obj.width; + } + + if (maxX > value) value = maxX; + } + + return value; + } + + private function findMinY():Float { + if(this.length == 0) return 0; + var value = Math.POSITIVE_INFINITY; + + for(m in this.members) { + if(m == null) continue; + + var minY:Float; + if(m is StageLayer) minY = cast(m, StageLayer).findMinY(); + else minY = cast(m, FlxObject).y; + + if (minY < value) value = minY; + } + + return value; + } + + private function findMaxY():Float { + if(this.length == 0) return 0; + var value = Math.NEGATIVE_INFINITY; + + for(m in this.members) { + if(m == null) continue; + + var maxY:Float; + if(m is StageLayer) maxY = cast(m, StageLayer).findMaxY(); + else { + var obj:FlxObject = cast m; + maxY = obj.y + obj.height; + } + + if (maxY > value) value = maxY; + } + + return value; + } } class NewStage extends StageLayer { From b310c5b7e4eb8e63cb8af008874c58ab4199695e Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Thu, 26 Feb 2026 09:38:08 -0700 Subject: [PATCH 06/24] Some cleanup :3 --- source/funkin/game/NewStage.hx | 45 ++++------------------------------ 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 3bf7f97b3d..917b31596b 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -1,9 +1,5 @@ package funkin.game; -import flixel.math.FlxRect; -import haxe.xml.Access; -import hscript.IHScriptCustomBehaviour; - import funkin.game.Stage.StageCharPos; import funkin.game.Stage.StageCharPosInfo; import funkin.backend.utils.XMLUtil; @@ -11,9 +7,13 @@ import funkin.backend.scripting.Script; import funkin.backend.scripting.events.stage.StageXMLEvent; import funkin.backend.system.interfaces.IBeatReceiver; -import flixel.util.FlxSignal.FlxTypedSignal; import flixel.group.FlxGroup; import flixel.math.FlxPoint; +import flixel.math.FlxRect; +import flixel.util.FlxSignal.FlxTypedSignal; + +import haxe.xml.Access; +import hscript.IHScriptCustomBehaviour; using StringTools; @@ -270,8 +270,6 @@ class NewStage extends StageLayer { public function loadStage(loadAll:Bool = false) { if (allowScripts) { script = Script.create(scriptFilePath); - // Performed by "onStageScriptLoad" - // PlayState.instance.scripts.add(stageScript); if (onStageScriptLoad != null) onStageScriptLoad(script); script.load(); } @@ -301,27 +299,7 @@ class NewStage extends StageLayer { // some way to tag that the sprites are from the group checkMemoryMode(xmlFile, loadAll, elems); - /* - for (node in xmlFile.elements) - { - if (node.name == "high-memory" && (!Options.lowMemoryMode || forceLoadAll)) - for (e in node.elements) - __pushNcheckNode(elems, e); - else if (node.name == "low-memory" && (Options.lowMemoryMode || forceLoadAll)) - for (e in node.elements) - __pushNcheckNode(elems, e); - else - __pushNcheckNode(elems, node); - } - */ - // This should be performed by the "onXMLLoaded" callback - /* - if (PlayState.instance == state) { - event = EventManager.get(StageXMLEvent).recycle(this, stageXML, elems); - elems = PlayState.instance.gameAndCharsEvent("onStageXMLParsed", event).elems; - } - */ if(onXMLLoaded != null) { stageEvent = EventManager.get(StageXMLEvent).recycle(this, xmlFile, elems); elems = onXMLLoaded(stageEvent); @@ -336,15 +314,6 @@ class NewStage extends StageLayer { startCam.x = Std.parseFloat(xmlFile.getAtt("startCamPosX")).getDefaultFloat(0); startCam.y = Std.parseFloat(xmlFile.getAtt("startCamPosY")).getDefaultFloat(0); defaultZoom = Std.parseFloat(xmlFile.getAtt("zoom")).getDefaultFloat(1.05); - /* - var parsed:Null; - if ((parsed = Std.parseFloat(xmlFile.getAtt("startCamPosX"))).isNotNull()) - startCam.x = parsed; - if ((parsed = Std.parseFloat(xmlFile.getAtt("startCamPosY"))).isNotNull()) - startCam.y = parsed; - if ((parsed = Std.parseFloat(xmlFile.getAtt("zoom"))).isNotNull()) - defaultZoom = parsed; - */ } private inline function loadCustomAttributes() { @@ -441,7 +410,6 @@ class NewStage extends StageLayer { } // idk lemme check anyways just in case scripts did smth - Nex - //if (event != null) PlayState.instance.gameAndCharsEvent("onPostStageCreation", event); if(onPostStageCreation != null && stageEvent != null) onPostStageCreation(stageEvent); @@ -450,7 +418,6 @@ class NewStage extends StageLayer { var script = info.getScript(); if (script == null) continue; - //PlayState.instance.scripts.remove(script); if(onRemoveInfo != null) onRemoveInfo(script); script.destroy(); @@ -543,7 +510,6 @@ class NewStage extends StageLayer { **/ public function destroySilently(destroySprites:Bool = true, destroyScript:Bool = true) { if (destroyScript && script != null) { - // if (PlayState.instance == state && PlayState.instance.scripts != null) PlayState.instance.scripts.remove(stageScript); if (onSilentDestroy != null) onSilentDestroy(this.script); script.destroy(); } @@ -555,7 +521,6 @@ class NewStage extends StageLayer { } override function destroy() { - // if (PlayState.instance == state && PlayState.instance.scripts != null) PlayState.instance.gameAndCharsCall("onStageDestroy", [this]); if (onStageDestroy != null) onStageDestroy(this); script?.call("destroy"); destroySilently(); From b93c284aad56b779d2e02f3f8f427a10f31b8c29 Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Thu, 26 Feb 2026 09:56:38 -0700 Subject: [PATCH 07/24] Useful attributes + organization --- source/funkin/game/NewStage.hx | 42 +++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 917b31596b..faae55dba4 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -24,11 +24,12 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private static final __instanceFields = Type.getInstanceFields(StageLayer); public var name:String; + public var x(get, never):Float; + public var y(get, never):Float; + public var width(get, never):Float; + public var height(get, never):Float; public var bounds(get, never):FlxRect; - private var stageSprites:Map = []; - private var stageLayers:Map = []; - public var onAddSprite:FlxTypedSignal Void> = new FlxTypedSignal(); public var onAddLayer:FlxTypedSignal Void> = new FlxTypedSignal(); @@ -45,7 +46,11 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem this.name = name; } + private var stageSprites:Map = []; + private var stageLayers:Map = []; + public function addSprite(name:String, spr:FlxObject):FlxObject { + if(stageSprites.exists(name)) return spr; this.add(spr); stageSprites.set(name, this.members.indexOf(spr)); // TODO: faster way to set the index updateBounds(); @@ -55,6 +60,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem } public function addLayer(name:String, layer:StageLayer):StageLayer { + if(stageLayers.exists(name)) return layer; this.add(layer); stageLayers.set(name, this.members.indexOf(layer)); updateBounds(); @@ -186,6 +192,15 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return value; } + + private function get_x():Float + return _bounds.x; + private function get_y():Float + return _bounds.y; + private function get_width():Float + return _bounds.width; + private function get_height():Float + return _bounds.height; } class NewStage extends StageLayer { @@ -529,23 +544,18 @@ class NewStage extends StageLayer { @:dox(hide) private function checkMemoryMode(xml:Access, loadAll:Bool, elems:Array) { for(node in xml.elements) { if (node.name == "high-memory" && (!Options.lowMemoryMode || loadAll)) - for (e in node.elements) - pushNode(e, elems); + for (e in node.elements) pushNode(e, elems); else if (node.name == "low-memory" && (Options.lowMemoryMode || loadAll)) - for (e in node.elements) - pushNode(e, elems); - else if (node.name == "layer") - checkMemoryMode(node, loadAll, elems); // recursive check in layers - else - pushNode(node, elems); + for (e in node.elements) pushNode(e, elems); + else if (node.name == "layer") checkMemoryMode(node, loadAll, elems); // recursive check in layers + else pushNode(node, elems); } } @:dox(hide) private function pushNode(node:Access, elems:Array) { elems.push(node); if ((node.name == "use-extension" || node.name == "extension" || node.name == "ext") && XMLImportedScriptInfo.shouldLoadBefore(node)) - if (onPrepareInfo != null) // :3 - onPrepareInfo(node); + if (onPrepareInfo != null) onPrepareInfo(node); } // bruh... @@ -568,18 +578,12 @@ class NewStage extends StageLayer { public var stageFile(get, never):String; public var stageName(get, set):String; public var stageScript(get, never):Script; - public var characterPoses(get, never):Map; function get_stageScript():Script { return this.script; } - function get_stagePath():String { return this.xmlFilePath; } - function get_stageFile():String { return this.fileName; } - function get_stageName():String { return this.name; } - function set_stageName(name:String):String { return this.name = name; } - function get_characterPoses():Map { return this.characterPosLookup; } } From 34e7ffef3f7bd7342035c34d254fdd4fe4d8d0a7 Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Mon, 9 Mar 2026 11:03:19 -0700 Subject: [PATCH 08/24] handling stage switching (WIP) --- source/funkin/game/NewStage.hx | 1 + source/funkin/game/PlayState.hx | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index faae55dba4..941c3ff7e1 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -203,6 +203,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return _bounds.height; } +// TODO: make it preload class NewStage extends StageLayer { private static final __instanceFields = Type.getInstanceFields(NewStage); diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index d3a214d2ce..b58781a3a1 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -715,7 +715,7 @@ class PlayState extends MusicBeatState add(camFollow); if (SONG.stage == null || SONG.stage.trim() == "") SONG.stage = Flags.DEFAULT_STAGE; - add(stage = new NewStage(SONG.stage)); + add(stage = NewStage.getStage(SONG.stage)); if (!chartingMode || Options.charterEnablePlaytestScripts) { switch(SONG.meta.name) { @@ -780,7 +780,7 @@ class PlayState extends MusicBeatState for(i=>strumLine in SONG.strumLines) { if (strumLine == null) continue; - var chars = []; + var chars:Array = []; var charPosName:String = strumLine.position == null ? (switch(strumLine.type) { case 0: "dad"; case 1: "boyfriend"; @@ -2197,6 +2197,8 @@ class PlayState extends MusicBeatState return campaignAccuracyCount == 0 ? 0 : campaignAccuracyTotal / campaignAccuracyCount; private function set_stage(stage:NewStage):NewStage { + var replacingStage:Bool = this.stage != null; // check if there is already a stage + stage.onStageScriptLoad = (script) -> {scripts.add(script);} stage.onStartCamSet = (startCam, defaultZoom) -> { camFollow.x = startCam.x; @@ -2234,6 +2236,26 @@ class PlayState extends MusicBeatState } stage.loadStage(); + + if(replacingStage) { + // reset the characters positions with the new stage + var stageIndex:Int = members.indexOf(this.stage); + for(strumline in this.strumLines.members) { + if(strumline.characters.length == 0) continue; + for(i => char in strumline.characters) { + var posName:String = strumline.data.position ?? switch(strumline.data.type) { + case 0: "dad"; + case 1: "boyfriend"; + case 2: "girlfriend"; + }; + // var char = new Character(0, 0, charName, stage.isCharFlipped(stage.characterPoses[charName] != null ? charName : charPosName, strumLine.type == 1)); + char.isPlayer = stage.isCharFlipped(stage.characterPoses[char.curCharacter] != null ? char.curCharacter : posName, !strumline.opponentSide); + stage.applyCharPos(char, posName, i); + } + } + + replace(this.stage, stage); + } return this.stage = stage; } From 040916432f72cb781b7c4c442f777a8e704f0806 Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Sat, 9 May 2026 18:59:50 -0700 Subject: [PATCH 09/24] ocd prevents me from not not pushing this --- source/funkin/game/NewStage.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 9d8d1b0380..7a68c60acf 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -375,7 +375,7 @@ class NewStage extends StageLayer { charPos.visible = charPos.active = false; charPos.name = name; - if(defaultCharPos != null) { + if (defaultCharPos != null) { charPos.setPosition(defaultCharPos.x, defaultCharPos.y); charPos.scrollFactor.set(defaultCharPos.scroll, defaultCharPos.scroll); charPos.flipX = defaultCharPos.flip; From dbe8a9d241b71735dfbe4e4fb6f17921fb4c6ef3 Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Sat, 9 May 2026 20:56:50 -0700 Subject: [PATCH 10/24] changes & fixes to backcompatability? --- source/funkin/game/NewStage.hx | 84 +++++++++++++++++++++++++++++---- source/funkin/game/PlayState.hx | 4 +- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index e54d11c0f6..2e39ea8fca 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -41,7 +41,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return stageLayers.exists(name) ? cast this.members[stageLayers[name]] : null; } - public function new(name:String = "stage") { + public function new(name:String = "stage_layer") { super(); this.name = name; } @@ -49,16 +49,45 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private var stageSprites:Map = []; private var stageLayers:Map = []; + // Stage Sprite Management public function addSprite(name:String, spr:FlxObject):FlxObject { if(stageSprites.exists(name)) return spr; this.add(spr); + stageSprites.set(name, this.members.indexOf(spr)); // TODO: faster way to set the index updateBounds(); + onAddSprite.dispatch(spr); return spr; } + public function insertSprite(name:String, spr:FlxObject, index:Int):FlxObject { + if (stageSprites.exists(name)) return spr; + this.insert(index, spr); + + stageSprites.set(name, this.members.indexOf(spr)); // TODO: faster way to set the index + updateBounds(); + + onAddSprite.dispatch(spr); + + return spr; + } + + public function removeSprite(name:String, splice:Bool = false):Bool { + if (!stageSprites.exists(name)) return false; + + var index:Int = stageSprites.get(name); + if (this.members[index] == null) return false; + + this.remove(this.members[index], splice); + stageSprites.remove(name); + updateBounds(); + + return true; + } + + // Stage Layer Management public function addLayer(name:String, layer:StageLayer):StageLayer { if(stageLayers.exists(name)) return layer; this.add(layer); @@ -69,6 +98,29 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return layer; } + public function insertLayer(name:String, layer:StageLayer, index:Int):StageLayer { + if (stageLayers.exists(name)) return layer; + this.insert(index, layer); + stageLayers.set(name, this.members.indexOf(layer)); + updateBounds(); + onAddLayer.dispatch(layer); + + return layer; + } + + public function removeLayer(name:String, splice:Bool = false):Bool { + if (!stageLayers.exists(name)) return false; + + var index:Int = stageLayers.get(name); + if (this.members[index] == null) return false; + + this.remove(this.members[index], splice); + stageLayers.remove(name); + updateBounds(); + + return true; + } + public function beatHit(curBeat:Int) { for(m in members) if(m != null && m is IBeatReceiver) cast(m, IBeatReceiver).beatHit(curBeat); } @@ -103,12 +155,13 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem } private var _bounds:FlxRect = FlxRect.get(); - private function get_bounds():FlxRect { - return _bounds; - } + private function get_bounds():FlxRect { return _bounds; } private function updateBounds() { - if(this.length == 0) return; + if (this.length == 0) return; + + // TODO: Optimize the code so that we don't run 4 for loops every time we update the bounds. + // We should just do one giant loop and check the bounds all here without wasting time, keep the other functions for specific checks. var x = findMinX(); var y = findMinY(); var width = findMaxX() - x; @@ -249,6 +302,7 @@ class NewStage extends StageLayer { public var onRemoveInfo:Script -> Void; public var onXMLLoaded:(StageXMLEvent)->Array = null; + public var onNodeInitalize:(Access)->Dynamic = null; public var onNodeLoaded:(Access, Dynamic)->Dynamic = null; public var onNodeFinished:(Access, Dynamic)->Void = null; public var onXMLPostLoaded:(Access, Array)->Array = null; @@ -287,6 +341,13 @@ class NewStage extends StageLayer { if (allowScripts) { script = Script.create(scriptFilePath); if (onStageScriptLoad != null) onStageScriptLoad(script); + script.setParent(PlayState.instance); + script.set("stage", this); + script.set("add", this.add); + script.set("remove", this.remove); + script.set("insert", this.insert); + script.set("replace", this.replace); + script.set("members", this.members); script.load(); } @@ -316,7 +377,7 @@ class NewStage extends StageLayer { // some way to tag that the sprites are from the group checkMemoryMode(xmlFile, loadAll, elems); - if(onXMLLoaded != null) { + if (onXMLLoaded != null) { stageEvent = EventManager.get(StageXMLEvent).recycle(this, xmlFile, elems); elems = onXMLLoaded(stageEvent); } @@ -340,16 +401,20 @@ class NewStage extends StageLayer { private function loadLayer(layer:StageLayer, elems:Array) { for(node in elems) { - var sprite = switch(node.name) { + // If `onNodeInitalize` returns a valid value, then why waste time on checking other values, since we should only care about what the user + // sets it too. Optimizations be like: + var sprite:Dynamic = (onNodeInitalize != null) ? onNodeInitalize(node) : null; + if (sprite == null) sprite = switch(node.name) { case "layer": if (!node.has.name) continue; + var layerName = node.att.name; var layer = new StageLayer(layerName); // recursive so it will allow nested layers loadLayer(layer, [for(n in node.elements) n]); addLayer(layerName, layer); case "sprite" | "spr" | "sparrow": - if (!node.has.sprite || !node.has.name) continue; + if (!node.has.name) continue; var spr = XMLUtil.createSpriteFromXML(node, spritesParentFolder, LOOP); addSprite(spr.name, spr); @@ -381,7 +446,7 @@ class NewStage extends StageLayer { if (!node.has.name) continue; setCharPos(node.att.name, node); case "ratings" | "combo": - if(onRatingSet == null) continue; + if (onRatingSet == null) continue; var ratingPos = { x: Std.parseFloat(node.getAtt("x")), y: Std.parseFloat(node.getAtt("y")) @@ -508,7 +573,6 @@ class NewStage extends StageLayer { * @param id ?????? no fucking clue why does it have an ID it's never used!!!!!!!!!!!!!!!! **/ public function applyCharPos(char:Character, posName:String, id:Float = 0) { - // Should I add the characters to the "stageSprites" list? var charName:String = char.curCharacter; var charPos:Null = characterPosLookup.exists(charName) ? characterPosLookup.get(charName) : characterPosLookup.get(posName); if(charPos != null) { diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 046bf4cff0..07d19f0035 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -635,10 +635,10 @@ class PlayState extends MusicBeatState } private inline function get_curStage() - return stage == null ? "" : stage.stageName; + return stage == null ? "" : stage.name; private inline function set_curStage(name:String) { - if (stage != null) stage.stageName = name; + if (stage != null) stage.name = name; return name; } From 17ff25ab54690834e35a1fe3731185d6efa14853 Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Sun, 10 May 2026 19:22:40 -0700 Subject: [PATCH 11/24] Fixes and using References in the StageLayer instead of member index. --- source/funkin/game/NewStage.hx | 95 +++++++++++++++++++++------------ source/funkin/game/PlayState.hx | 80 ++++++++++++--------------- 2 files changed, 96 insertions(+), 79 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 2e39ea8fca..cb6e48a3c3 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -8,10 +8,13 @@ import funkin.backend.scripting.events.stage.StageXMLEvent; import funkin.backend.system.interfaces.IBeatReceiver; import flixel.group.FlxGroup; +import flixel.group.FlxSpriteGroup; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.util.FlxSignal.FlxTypedSignal; +import flixel.system.FlxAssets.FlxShader; + import haxe.xml.Access; import hscript.IHScriptCustomBehaviour; @@ -33,12 +36,19 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public var onAddSprite:FlxTypedSignal Void> = new FlxTypedSignal(); public var onAddLayer:FlxTypedSignal Void> = new FlxTypedSignal(); + public var shader(default, set):FlxShader; + private function set_shader(s:FlxShader):FlxShader { + for (key=>ref in this.members) cast(ref, FlxSprite).shader = s; + this.shader = s; + return s; + } + public inline function getSprite(name:String):Null { - return stageSprites.exists(name) ? this.members[stageSprites[name]] : null; + return stageSprites.exists(name) ? stageSprites[name] : null; } public inline function getLayer(name:String):Null { - return stageLayers.exists(name) ? cast this.members[stageLayers[name]] : null; + return stageLayers.exists(name) ? cast stageLayers[name] : null; } public function new(name:String = "stage_layer") { @@ -46,15 +56,15 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem this.name = name; } - private var stageSprites:Map = []; - private var stageLayers:Map = []; + private var stageSprites:Map = []; + private var stageLayers:Map = []; // Stage Sprite Management public function addSprite(name:String, spr:FlxObject):FlxObject { - if(stageSprites.exists(name)) return spr; + if (stageSprites.exists(name)) return spr; this.add(spr); - stageSprites.set(name, this.members.indexOf(spr)); // TODO: faster way to set the index + stageSprites.set(name, spr); updateBounds(); onAddSprite.dispatch(spr); @@ -66,7 +76,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem if (stageSprites.exists(name)) return spr; this.insert(index, spr); - stageSprites.set(name, this.members.indexOf(spr)); // TODO: faster way to set the index + stageSprites.set(name, spr); updateBounds(); onAddSprite.dispatch(spr); @@ -77,11 +87,17 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public function removeSprite(name:String, splice:Bool = false):Bool { if (!stageSprites.exists(name)) return false; - var index:Int = stageSprites.get(name); - if (this.members[index] == null) return false; - - this.remove(this.members[index], splice); + var spr:FlxBasic = stageSprites.get(name); stageSprites.remove(name); + + // If it's null we want to still attempt to remove the reference from the Array methinks. + if (spr == null) { + var idx:Int = this.members.indexOf(spr); + if (idx < 0) return false; + + this.remove(this.members[idx], splice); + } else + this.remove(spr, splice); updateBounds(); return true; @@ -89,9 +105,11 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem // Stage Layer Management public function addLayer(name:String, layer:StageLayer):StageLayer { - if(stageLayers.exists(name)) return layer; + if (stageLayers.exists(name)) return layer; + this.add(layer); - stageLayers.set(name, this.members.indexOf(layer)); + stageLayers.set(name, layer); + updateBounds(); onAddLayer.dispatch(layer); @@ -100,8 +118,10 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public function insertLayer(name:String, layer:StageLayer, index:Int):StageLayer { if (stageLayers.exists(name)) return layer; + this.insert(index, layer); - stageLayers.set(name, this.members.indexOf(layer)); + stageLayers.set(name, layer); + updateBounds(); onAddLayer.dispatch(layer); @@ -111,16 +131,23 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public function removeLayer(name:String, splice:Bool = false):Bool { if (!stageLayers.exists(name)) return false; - var index:Int = stageLayers.get(name); - if (this.members[index] == null) return false; - - this.remove(this.members[index], splice); + var layer:StageLayer = stageLayers.get(name); stageLayers.remove(name); + + // If it's null we want to still attempt to remove the reference from the Array methinks. + if (layer == null) { + var idx:Int = this.members.indexOf(layer); + if (idx < 0) return false; + + this.remove(this.members[idx], splice); + } else + this.remove(layer, splice); updateBounds(); - + return true; } + // IBeatReceiver implementation public function beatHit(curBeat:Int) { for(m in members) if(m != null && m is IBeatReceiver) cast(m, IBeatReceiver).beatHit(curBeat); } @@ -131,11 +158,12 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem for(m in members) if(m != null && m is IBeatReceiver) cast(m, IBeatReceiver).measureHit(curMeasure); } + // IHScriptCustomBehaviour implementation public function hget(name:String):Dynamic { if (__instanceFields.contains(name) || __instanceFields.contains('get_$name')) return Reflect.getProperty(this, name); - if(stageSprites.exists(name)) return this.members[stageSprites[name]]; - if(stageLayers.exists(name)) return this.members[stageLayers[name]]; + if (stageSprites.exists(name)) return stageSprites[name]; + if (stageLayers.exists(name)) return stageLayers[name]; return null; } @@ -144,11 +172,12 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem Reflect.setProperty(this, name, val); return val; } - if(stageSprites.exists(name)) return this.members[stageSprites[name]] = val; - if(stageLayers.exists(name)) return this.members[stageLayers[name]] = val; + if (stageSprites.exists(name)) return stageSprites[name] = val; + if (stageLayers.exists(name)) return stageLayers[name] = val; return null; } + // then whatever below override function destroy() { super.destroy(); _bounds.put(); @@ -256,10 +285,10 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return _bounds.height; } -// TODO: make it preload class NewStage extends StageLayer { private static final __instanceFields = Type.getInstanceFields(NewStage); + // TODO: Preload the Assets and scripts of the stage before returning 👀 public static function getStage(name:String):NewStage { return new NewStage(name); } @@ -319,7 +348,7 @@ class NewStage extends StageLayer { * Sets the sprites in the script, so you can access them by the name. **/ public function setStagesSprites(script:Script) - for (k=>e in stageSprites) script.set(k, this.members[e]); + for (key=>ref in stageSprites) script.set(key, ref); public function new(stage:String, load:Bool = false) { super(); @@ -459,7 +488,7 @@ class NewStage extends StageLayer { default: null; } - if(onNodeLoaded != null) { + if (onNodeLoaded != null) { sprite = onNodeLoaded(node, sprite); } @@ -468,7 +497,7 @@ class NewStage extends StageLayer { XMLUtil.applyXMLProperty(sprite, e); } - if(onNodeFinished != null) { + if (onNodeFinished != null) { onNodeFinished(node, sprite); } } @@ -476,18 +505,17 @@ class NewStage extends StageLayer { private function postLoadStage(?elems:Array) { for(defaultChar in ["girlfriend", "dad", "boyfriend"]) { - if(!characterPosLookup.exists(defaultChar)) + if (!characterPosLookup.exists(defaultChar)) setCharPos(defaultChar, null, getDefaultPos(defaultChar)); } - if(allowScripts) { + if (allowScripts) { setStagesSprites(this.script); // i know this for gets run twice under, but its better like this in case a script modifies the short lived ones, i dont wanna save them in an array; more dynamic like this - Nex for (info in xmlImportedScripts) if (info.importStageSprites) { var script = info.getScript(); - if (script != null) - setStagesSprites(script); + if (script != null) setStagesSprites(script); } // idk lemme check anyways just in case scripts did smth - Nex @@ -499,13 +527,12 @@ class NewStage extends StageLayer { var script = info.getScript(); if (script == null) continue; - if(onRemoveInfo != null) - onRemoveInfo(script); + if (onRemoveInfo != null) onRemoveInfo(script); script.destroy(); } } - if(xmlFile != null && onXMLPostLoaded != null) { + if (xmlFile != null && onXMLPostLoaded != null) { elems = onXMLPostLoaded(xmlFile, elems); } } diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 07d19f0035..cd85ec6b63 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -2186,7 +2186,7 @@ class PlayState extends MusicBeatState private inline function set_playerStrums(v:StrumLine):StrumLine return strumLines.members[1] = v; private inline function get_gfSpeed():Int - return (strumLines.members[2] != null && strumLines.members[2].characters[0] != null) ? strumLines.members[2].characters[0].beatInterval : 1; + return strumLines.members[2] != null ? strumLines?.members[2]?.characters[0]?.beatInterval : 1; private inline function set_gfSpeed(v:Int):Int { if (strumLines.members[2] != null && strumLines.members[2].characters[0] != null) strumLines.members[2].characters[0].beatInterval = v; @@ -2196,68 +2196,58 @@ class PlayState extends MusicBeatState private inline static function get_campaignAccuracy() return campaignAccuracyCount == 0 ? 0 : campaignAccuracyTotal / campaignAccuracyCount; - private function set_stage(stage:NewStage):NewStage { - var replacingStage:Bool = this.stage != null; // check if there is already a stage + // For now, I'd like the stage to manage all of this itself, but also it's completely fine if we have code here like `gameAndCharsEvent` for PlayState. + private function set_stage(new_stage:NewStage):NewStage { + var isReplacing:Bool = (this.stage != null); // check if there is already a stage - stage.onStageScriptLoad = (script) -> {scripts.add(script);} - stage.onStartCamSet = (startCam, defaultZoom) -> { + new_stage.onStageScriptLoad = (script) -> scripts.add(script); + new_stage.onStartCamSet = (startCam, defaultZoom) -> { camFollow.x = startCam.x; camFollow.y = startCam.y; defaultCamZoom = defaultZoom; } - stage.onXMLLoaded = (stageEvent) -> { + new_stage.onXMLLoaded = (stageEvent) -> { return this.gameAndCharsEvent("onStageXMLParsed", stageEvent).elems; } - stage.onRatingSet = (x, y) -> { + new_stage.onRatingSet = (x, y) -> { comboGroup.setPosition(x, y); add(comboGroup); return comboGroup; } - stage.onPrepareInfo = (node) -> { + new_stage.onPrepareInfo = (node) -> { return XMLImportedScriptInfo.prepareInfos(node, this.scripts, (infos) -> { - stage.xmlImportedScripts.push(infos); + new_stage.xmlImportedScripts.push(infos); }); } - stage.onRemoveInfo = (script) -> { - scripts.remove(script); - } - stage.onNodeLoaded = (node, sprite) -> { - return this.gameAndCharsEvent("onStageNodeParsed", EventManager.get(StageNodeEvent).recycle(stage, node, sprite, node.name)).sprite; - } - stage.onPostStageCreation = (stageEvent) -> { - this.gameAndCharsEvent("onPostStageCreation", stageEvent); - } - stage.onSilentDestroy = (script) -> { - if(this.scripts != null) - scripts.remove(script); - } - stage.onStageDestroy = (stage) -> { - this.gameAndCharsCall("onStageDestroy", [stage]); + new_stage.onRemoveInfo = (script) -> scripts.remove(script); + new_stage.onNodeLoaded = (node, sprite) -> { + return this.gameAndCharsEvent("onStageNodeParsed", EventManager.get(StageNodeEvent).recycle(new_stage, node, sprite, node.name)).sprite; } + new_stage.onPostStageCreation = (stageEvent) -> this.gameAndCharsEvent("onPostStageCreation", stageEvent); + new_stage.onSilentDestroy = (script) -> this.scripts?.remove(script); + new_stage.onStageDestroy = (_) -> this.gameAndCharsCall("onStageDestroy", [_]); - stage.loadStage(); - - if(replacingStage) { - // reset the characters positions with the new stage - var stageIndex:Int = members.indexOf(this.stage); - for(strumline in this.strumLines.members) { - if(strumline.characters.length == 0) continue; - for(i => char in strumline.characters) { - var posName:String = strumline.data.position ?? switch(strumline.data.type) { - case 0: "dad"; - case 1: "boyfriend"; - case 2: "girlfriend"; - }; - // var char = new Character(0, 0, charName, stage.isCharFlipped(stage.characterPoses[charName] != null ? charName : charPosName, strumLine.type == 1)); - char.isPlayer = stage.isCharFlipped(stage.characterPoses[char.curCharacter] != null ? char.curCharacter : posName, !strumline.opponentSide); - stage.applyCharPos(char, posName, i); - } - } + new_stage.loadStage(); - replace(this.stage, stage); - } + if (!isReplacing) return this.stage = new_stage; - return this.stage = stage; + // reset the characters positions with the new stage + for (strumline in this.strumLines.members) { + if (strumline.characters.length == 0) continue; + for (i => char in strumline.characters) { + var posName:String = strumline.data.position ?? switch(strumline.data.type) { + case 0: "dad"; + case 1: "boyfriend"; + case 2: "girlfriend"; + }; + // var char = new Character(0, 0, charName, new_stage.isCharFlipped(new_stage.characterPoses[charName] != null ? charName : charPosName, strumLine.type == 1)); + char.isPlayer = new_stage.isCharFlipped(new_stage.characterPoses[char.curCharacter] != null ? char.curCharacter : posName, !strumline.opponentSide); + new_stage.applyCharPos(char, posName, i); + } + } + + replace(this.stage, new_stage); + return this.stage = new_stage; } #end From 9b3412e2f77daad399d5518969ba9c5ed685ca70 Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Mon, 11 May 2026 17:55:20 -0700 Subject: [PATCH 12/24] quick fixes and now you can access PlayState values in Stage Scripts, but you will access Stage class variables over PlayState variables. --- source/funkin/game/NewStage.hx | 40 +++++++++++++++++---------------- source/funkin/game/PlayState.hx | 1 + 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index cb6e48a3c3..65488fc881 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -36,6 +36,8 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public var onAddSprite:FlxTypedSignal Void> = new FlxTypedSignal(); public var onAddLayer:FlxTypedSignal Void> = new FlxTypedSignal(); + // TODO: better way to set all members that have the `shader` value ig?? + // Also use FlxAnimate's implementation on useRenderTexture so we can apply the shader onto all sprites as 1 sprite !!! public var shader(default, set):FlxShader; private function set_shader(s:FlxShader):FlxShader { for (key=>ref in this.members) cast(ref, FlxSprite).shader = s; @@ -72,7 +74,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return spr; } - public function insertSprite(name:String, spr:FlxObject, index:Int):FlxObject { + public function insertSprite(index:Int, name:String, spr:FlxObject):FlxObject { if (stageSprites.exists(name)) return spr; this.insert(index, spr); @@ -104,11 +106,11 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem } // Stage Layer Management - public function addLayer(name:String, layer:StageLayer):StageLayer { - if (stageLayers.exists(name)) return layer; + public function addLayer(layer:StageLayer):StageLayer { + if (stageLayers.exists(layer.name)) return layer; this.add(layer); - stageLayers.set(name, layer); + stageLayers.set(layer.name, layer); updateBounds(); onAddLayer.dispatch(layer); @@ -116,11 +118,11 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return layer; } - public function insertLayer(name:String, layer:StageLayer, index:Int):StageLayer { - if (stageLayers.exists(name)) return layer; + public function insertLayer(index:Int, layer:StageLayer):StageLayer { + if (stageLayers.exists(layer.name)) return layer; this.insert(index, layer); - stageLayers.set(name, layer); + stageLayers.set(layer.name, layer); updateBounds(); onAddLayer.dispatch(layer); @@ -128,11 +130,11 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return layer; } - public function removeLayer(name:String, splice:Bool = false):Bool { - if (!stageLayers.exists(name)) return false; + public function removeLayer(layer:StageLayer, splice:Bool = false):Bool { + if (!stageLayers.exists(layer.name)) return false; - var layer:StageLayer = stageLayers.get(name); - stageLayers.remove(name); + var layer:StageLayer = stageLayers.get(layer.name); + stageLayers.remove(layer.name); // If it's null we want to still attempt to remove the reference from the Array methinks. if (layer == null) { @@ -370,13 +372,7 @@ class NewStage extends StageLayer { if (allowScripts) { script = Script.create(scriptFilePath); if (onStageScriptLoad != null) onStageScriptLoad(script); - script.setParent(PlayState.instance); - script.set("stage", this); - script.set("add", this.add); - script.set("remove", this.remove); - script.set("insert", this.insert); - script.set("replace", this.replace); - script.set("members", this.members); + script.setParent(this); script.load(); } @@ -441,7 +437,7 @@ class NewStage extends StageLayer { var layer = new StageLayer(layerName); // recursive so it will allow nested layers loadLayer(layer, [for(n in node.elements) n]); - addLayer(layerName, layer); + addLayer(layer); case "sprite" | "spr" | "sparrow": if (!node.has.name) continue; @@ -654,6 +650,8 @@ class NewStage extends StageLayer { override function hget(name:String):Dynamic { if (__instanceFields.contains(name) || __instanceFields.contains('get_$name')) return Reflect.getProperty(this, name); + if (PlayState.__instanceFields.contains(name) || PlayState.__instanceFields.contains('get_$name')) + return Reflect.getProperty(PlayState.instance, name); return super.hget(name); } @@ -662,6 +660,10 @@ class NewStage extends StageLayer { Reflect.setProperty(this, name, val); return val; } + if (PlayState.__instanceFields.contains(name) || PlayState.__instanceFields.contains('set_$name')) { + Reflect.setProperty(PlayState.instance, name, val); + return val; + } return super.hset(name, val); } diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index cd85ec6b63..f4a2271d43 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -44,6 +44,7 @@ using StringTools; @:access(funkin.game.StrumLine) class PlayState extends MusicBeatState { + public static final __instanceFields = Type.getInstanceFields(PlayState); /** * Current PlayState instance. */ From 4b98297e6d94a38d5e0400cd00afe0e9b98505b4 Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Mon, 22 Jun 2026 16:54:28 -0700 Subject: [PATCH 13/24] Simplified Signal dispatch and bounds update The signal dispatch and bounds update has been moved to "memberAdded" and "memberRemoved" signals to allow those tasks to be ran using `add` and `remove` functions. Also, started to prepare all the rendering stuff. --- source/funkin/game/NewStage.hx | 103 ++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 34 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 65488fc881..71b25a9a33 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -1,5 +1,7 @@ package funkin.game; +import animate.internal.RenderTexture; + import funkin.game.Stage.StageCharPos; import funkin.game.Stage.StageCharPosInfo; import funkin.backend.utils.XMLUtil; @@ -8,7 +10,6 @@ import funkin.backend.scripting.events.stage.StageXMLEvent; import funkin.backend.system.interfaces.IBeatReceiver; import flixel.group.FlxGroup; -import flixel.group.FlxSpriteGroup; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.util.FlxSignal.FlxTypedSignal; @@ -36,15 +37,40 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public var onAddSprite:FlxTypedSignal Void> = new FlxTypedSignal(); public var onAddLayer:FlxTypedSignal Void> = new FlxTypedSignal(); + /** + * Whether to internally use a render texture when drawing the stage layer. + * This flattens all of the sprites and subsequent layers into a single graphic, making effects such as alpha or shaders apply to + * the entire sprite instead of individual members of the group. + */ + public var useRenderTexture:Bool = false; + private var _renderTexture:RenderTexture; + // TODO: better way to set all members that have the `shader` value ig?? // Also use FlxAnimate's implementation on useRenderTexture so we can apply the shader onto all sprites as 1 sprite !!! public var shader(default, set):FlxShader; private function set_shader(s:FlxShader):FlxShader { - for (key=>ref in this.members) cast(ref, FlxSprite).shader = s; + for (member in this.members) { + if(member is FlxSprite) + cast(member, FlxSprite).shader = s; + else if(member is StageLayer) + cast(member, StageLayer).shader = s; + } this.shader = s; return s; } + public var alpha(default, set):Float = 1.0; + private function set_alpha(a:Float):Float { + for (member in this.members) { + if(member is FlxSprite) + cast(member, FlxSprite).alpha = a; + else if(member is StageLayer) + cast(member, StageLayer).alpha = a; + } + this.alpha = a; + return a; + } + public inline function getSprite(name:String):Null { return stageSprites.exists(name) ? stageSprites[name] : null; } @@ -53,9 +79,28 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return stageLayers.exists(name) ? cast stageLayers[name] : null; } - public function new(name:String = "stage_layer") { + public function new(name:String = "stage_layer", useRenderTexture:Bool = false) { super(); this.name = name; + this.useRenderTexture = useRenderTexture; + this.memberAdded.add((obj) -> { + if(obj is FlxObject) { + if(obj is StageCharPos) return; + if(obj is StageLayer) + onAddLayer.dispatch(cast obj); + else + onAddSprite.dispatch(cast obj); + updateBounds(); + } + }); + this.memberRemoved.add((obj) -> { + if(obj == null || obj is FlxObject) + updateBounds(); + }); + } + + function checkRenderTexture():Bool { + return useRenderTexture && (/*alpha != 1 || */shader != null /*|| (blend != null && blend != NORMAL)*/); } private var stageSprites:Map = []; @@ -64,25 +109,19 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem // Stage Sprite Management public function addSprite(name:String, spr:FlxObject):FlxObject { if (stageSprites.exists(name)) return spr; - this.add(spr); - stageSprites.set(name, spr); - updateBounds(); - - onAddSprite.dispatch(spr); - + this.add(spr); + //updateBounds(); + //onAddSprite.dispatch(spr); return spr; } public function insertSprite(index:Int, name:String, spr:FlxObject):FlxObject { if (stageSprites.exists(name)) return spr; - this.insert(index, spr); - stageSprites.set(name, spr); - updateBounds(); - - onAddSprite.dispatch(spr); - + this.insert(index, spr); + //updateBounds(); + //onAddSprite.dispatch(spr); return spr; } @@ -100,7 +139,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem this.remove(this.members[idx], splice); } else this.remove(spr, splice); - updateBounds(); + //updateBounds(); return true; } @@ -108,25 +147,19 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem // Stage Layer Management public function addLayer(layer:StageLayer):StageLayer { if (stageLayers.exists(layer.name)) return layer; - - this.add(layer); stageLayers.set(layer.name, layer); - - updateBounds(); - onAddLayer.dispatch(layer); - + this.add(layer); + //updateBounds(); + //onAddLayer.dispatch(layer); return layer; } public function insertLayer(index:Int, layer:StageLayer):StageLayer { if (stageLayers.exists(layer.name)) return layer; - - this.insert(index, layer); stageLayers.set(layer.name, layer); - - updateBounds(); - onAddLayer.dispatch(layer); - + this.insert(index, layer); + //updateBounds(); + //onAddLayer.dispatch(layer); return layer; } @@ -144,7 +177,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem this.remove(this.members[idx], splice); } else this.remove(layer, splice); - updateBounds(); + //updateBounds(); return true; } @@ -295,7 +328,7 @@ class NewStage extends StageLayer { return new NewStage(name); } - private static final DEFAULT_ATTRIBUTES:Array = ["name", "startCamPosX", "startCamPosY", "zoom", "folder"]; + private static final DEFAULT_ATTRIBUTES:Array = ["name", "startCamPosX", "startCamPosY", "zoom", "folder", "useRenderTexture"]; private static inline function getDefaultPos(name:String):StageCharPosInfo { return switch(name) { @@ -384,6 +417,7 @@ class NewStage extends StageLayer { loadStartCam(); this.name = xmlFile.getAtt("name").getDefault(fileName); + this.useRenderTexture = xmlFile.has.useRenderTexture ? xmlFile.att.useRenderTexture == "true" : false; if (onStartCamSet != null) onStartCamSet(startCam, defaultZoom); @@ -433,8 +467,9 @@ class NewStage extends StageLayer { case "layer": if (!node.has.name) continue; - var layerName = node.att.name; - var layer = new StageLayer(layerName); + var layerName:String = node.att.name; + var renderLayer:Bool = node.has.useRenderTexture ? node.att.useRenderTexture == "true" : false; + var layer:StageLayer = new StageLayer(layerName, renderLayer); // recursive so it will allow nested layers loadLayer(layer, [for(n in node.elements) n]); addLayer(layer); @@ -650,7 +685,7 @@ class NewStage extends StageLayer { override function hget(name:String):Dynamic { if (__instanceFields.contains(name) || __instanceFields.contains('get_$name')) return Reflect.getProperty(this, name); - if (PlayState.__instanceFields.contains(name) || PlayState.__instanceFields.contains('get_$name')) + if (PlayState.instance != null && (PlayState.__instanceFields.contains(name) || PlayState.__instanceFields.contains('get_$name'))) return Reflect.getProperty(PlayState.instance, name); return super.hget(name); } @@ -660,7 +695,7 @@ class NewStage extends StageLayer { Reflect.setProperty(this, name, val); return val; } - if (PlayState.__instanceFields.contains(name) || PlayState.__instanceFields.contains('set_$name')) { + if (PlayState.instance != null && (PlayState.__instanceFields.contains(name) || PlayState.__instanceFields.contains('set_$name'))) { Reflect.setProperty(PlayState.instance, name, val); return val; } From c7bdb6125344e8c9e96bc5ec90b65fe305b961d0 Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Tue, 23 Jun 2026 03:53:37 -0700 Subject: [PATCH 14/24] remove dumb code and re-ordered some code to ensure things exist in the right order of operations --- source/funkin/game/NewStage.hx | 38 +++++++++++++++------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 71b25a9a33..fd77018a45 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -109,19 +109,23 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem // Stage Sprite Management public function addSprite(name:String, spr:FlxObject):FlxObject { if (stageSprites.exists(name)) return spr; - stageSprites.set(name, spr); this.add(spr); + stageSprites.set(name, spr); + //updateBounds(); //onAddSprite.dispatch(spr); + return spr; } public function insertSprite(index:Int, name:String, spr:FlxObject):FlxObject { if (stageSprites.exists(name)) return spr; - stageSprites.set(name, spr); this.insert(index, spr); + stageSprites.set(name, spr); + //updateBounds(); //onAddSprite.dispatch(spr); + return spr; } @@ -129,16 +133,10 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem if (!stageSprites.exists(name)) return false; var spr:FlxBasic = stageSprites.get(name); - stageSprites.remove(name); - // If it's null we want to still attempt to remove the reference from the Array methinks. - if (spr == null) { - var idx:Int = this.members.indexOf(spr); - if (idx < 0) return false; - - this.remove(this.members[idx], splice); - } else - this.remove(spr, splice); + this.remove(spr, splice); + stageSprites.remove(name); + //updateBounds(); return true; @@ -147,19 +145,23 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem // Stage Layer Management public function addLayer(layer:StageLayer):StageLayer { if (stageLayers.exists(layer.name)) return layer; - stageLayers.set(layer.name, layer); this.add(layer); + stageLayers.set(layer.name, layer); + //updateBounds(); //onAddLayer.dispatch(layer); + return layer; } public function insertLayer(index:Int, layer:StageLayer):StageLayer { if (stageLayers.exists(layer.name)) return layer; - stageLayers.set(layer.name, layer); this.insert(index, layer); + stageLayers.set(layer.name, layer); + //updateBounds(); //onAddLayer.dispatch(layer); + return layer; } @@ -167,16 +169,10 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem if (!stageLayers.exists(layer.name)) return false; var layer:StageLayer = stageLayers.get(layer.name); + + this.remove(layer, splice); stageLayers.remove(layer.name); - // If it's null we want to still attempt to remove the reference from the Array methinks. - if (layer == null) { - var idx:Int = this.members.indexOf(layer); - if (idx < 0) return false; - - this.remove(this.members[idx], splice); - } else - this.remove(layer, splice); //updateBounds(); return true; From c8969385a948d678b1a74eba1b815ccd6aa607aa Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Wed, 24 Jun 2026 02:52:13 -0700 Subject: [PATCH 15/24] fixed updateBounds to no longer fail casting if any FlxBasic objects are in the members of the StageLayer --- source/funkin/game/NewStage.hx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index fd77018a45..f626a44f13 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -237,7 +237,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.POSITIVE_INFINITY; for(m in this.members) { - if(m == null) continue; + if(m == null || m is FlxBasic) continue; var minX:Float; if(m is StageLayer) minX = cast(m, StageLayer).findMinX(); @@ -254,7 +254,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.NEGATIVE_INFINITY; for(m in this.members) { - if(m == null) continue; + if(m == null || m is FlxBasic) continue; var maxX:Float; if(m is StageLayer) maxX = cast(m, StageLayer).findMaxX(); @@ -274,7 +274,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.POSITIVE_INFINITY; for(m in this.members) { - if(m == null) continue; + if(m == null || m is FlxBasic) continue; var minY:Float; if(m is StageLayer) minY = cast(m, StageLayer).findMinY(); @@ -291,7 +291,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.NEGATIVE_INFINITY; for(m in this.members) { - if(m == null) continue; + if(m == null || m is FlxBasic) continue; var maxY:Float; if(m is StageLayer) maxY = cast(m, StageLayer).findMaxY(); From 402d283765a24e7ec88370f54e26c429f2b04db6 Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Thu, 25 Jun 2026 16:16:40 -0700 Subject: [PATCH 16/24] ok re-fixed `updateBounds` to no longer die if encountering a non-`FlxObject` class. Also optimized so it only needs to do one passthrough instead of going down to sub-levels and doing more for-loops that may or may not be needed. This might not be 100% accurate in some points but you can force a `hard_check` if needed --- source/funkin/game/NewStage.hx | 185 +++++++++++++++++++++++---------- 1 file changed, 131 insertions(+), 54 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index f626a44f13..40b22de584 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -45,10 +45,14 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public var useRenderTexture:Bool = false; private var _renderTexture:RenderTexture; - // TODO: better way to set all members that have the `shader` value ig?? - // Also use FlxAnimate's implementation on useRenderTexture so we can apply the shader onto all sprites as 1 sprite !!! + // TODO: use FlxAnimate's implementation on useRenderTexture so we can apply the shader onto all sprites as 1 sprite !!! public var shader(default, set):FlxShader; + // this setter is a temporary plug until we get `useRenderTexture` working private function set_shader(s:FlxShader):FlxShader { + if (useRenderTexture) { + this.shader = s; + return s; + } for (member in this.members) { if(member is FlxSprite) cast(member, FlxSprite).shader = s; @@ -61,16 +65,42 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public var alpha(default, set):Float = 1.0; private function set_alpha(a:Float):Float { + if (useRenderTexture) { + this.alpha = a; + return a; + } for (member in this.members) { - if(member is FlxSprite) + if (member is FlxSprite) cast(member, FlxSprite).alpha = a; - else if(member is StageLayer) + else if (member is StageLayer) cast(member, StageLayer).alpha = a; } this.alpha = a; return a; } + public var scrollFactor(default, set):FlxPoint = FlxPoint.get(1, 1); + private function set_scrollFactor(s:FlxPoint):FlxPoint { + if (useRenderTexture) { + this.scrollFactor = s; + return s; + } + for (member in this.members) { + if (member is FlxSprite) { + var spr:FlxSprite = cast member; + spr.scrollFactor.x += s.x; + spr.scrollFactor.y += s.y; + } + else if (member is StageLayer) { + var layer:StageLayer = cast member; + layer.scrollFactor.x += s.x; + layer.scrollFactor.y += s.y; + } + } + this.scrollFactor = s; + return s; + } + public inline function getSprite(name:String):Null { return stageSprites.exists(name) ? stageSprites[name] : null; } @@ -106,26 +136,22 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private var stageSprites:Map = []; private var stageLayers:Map = []; - // Stage Sprite Management + //region Stage Sprite Management public function addSprite(name:String, spr:FlxObject):FlxObject { if (stageSprites.exists(name)) return spr; + this.add(spr); stageSprites.set(name, spr); - //updateBounds(); - //onAddSprite.dispatch(spr); - return spr; } public function insertSprite(index:Int, name:String, spr:FlxObject):FlxObject { if (stageSprites.exists(name)) return spr; + this.insert(index, spr); stageSprites.set(name, spr); - //updateBounds(); - //onAddSprite.dispatch(spr); - return spr; } @@ -136,32 +162,27 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem this.remove(spr, splice); stageSprites.remove(name); - - //updateBounds(); return true; - } + } + //endregion - // Stage Layer Management + //region Stage Layer Management public function addLayer(layer:StageLayer):StageLayer { if (stageLayers.exists(layer.name)) return layer; + this.add(layer); stageLayers.set(layer.name, layer); - //updateBounds(); - //onAddLayer.dispatch(layer); - return layer; } public function insertLayer(index:Int, layer:StageLayer):StageLayer { if (stageLayers.exists(layer.name)) return layer; + this.insert(index, layer); stageLayers.set(layer.name, layer); - //updateBounds(); - //onAddLayer.dispatch(layer); - return layer; } @@ -173,12 +194,11 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem this.remove(layer, splice); stageLayers.remove(layer.name); - //updateBounds(); - return true; } + //endregion - // IBeatReceiver implementation + //region IBeatReceiver implementation public function beatHit(curBeat:Int) { for(m in members) if(m != null && m is IBeatReceiver) cast(m, IBeatReceiver).beatHit(curBeat); } @@ -188,8 +208,9 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public function measureHit(curMeasure:Int) { for(m in members) if(m != null && m is IBeatReceiver) cast(m, IBeatReceiver).measureHit(curMeasure); } + //endregion - // IHScriptCustomBehaviour implementation + //region IHScriptCustomBehaviour implementation public function hget(name:String):Dynamic { if (__instanceFields.contains(name) || __instanceFields.contains('get_$name')) return Reflect.getProperty(this, name); @@ -207,6 +228,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem if (stageLayers.exists(name)) return stageLayers[name] = val; return null; } + //endregion // then whatever below override function destroy() { @@ -217,17 +239,49 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private var _bounds:FlxRect = FlxRect.get(); private function get_bounds():FlxRect { return _bounds; } - private function updateBounds() { + /** + * Updates the bounds of the layer. + * Without `hard_check` enabled, it will check values of `x`, `y`, `width` and `height` of the members directly. + * This won't propagate `updateBounds` or `findMinX`, `findMinY`, `findMaxX` and `findMaxY` to the members, since it access the values directly. + * This is faster but under some circumstances, it might not be 100% accurate. + * @param hard_check If true, it will use `findMinX`, `findMinY`, `findMaxX` and `findMaxY` to find the bounds, which is slower but is guaranteed to be accurate when running + */ + private function updateBounds(hard_check:Bool = false) { if (this.length == 0) return; - // TODO: Optimize the code so that we don't run 4 for loops every time we update the bounds. - // We should just do one giant loop and check the bounds all here without wasting time, keep the other functions for specific checks. - var x = findMinX(); - var y = findMinY(); - var width = findMaxX() - x; - var height = findMaxY() - y; + if (hard_check) { + var x = findMinX(); + var y = findMinY(); + var width = findMaxX() - x; + var height = findMaxY() - y; + + _bounds.set(x, y, width, height); + } else { + var minX:Float = Math.POSITIVE_INFINITY; + var minY:Float = Math.POSITIVE_INFINITY; + + var maxX:Float = Math.NEGATIVE_INFINITY; + var maxY:Float = Math.NEGATIVE_INFINITY; + for (m in this.members) { + if (m == null || !(m is FlxObject)) continue; + + if (m is StageLayer) { + var layer:StageLayer = cast m; + if (layer.x < minX) minX = layer.x; + if (layer.y < minY) minY = layer.y; + if (layer.x + layer.width > maxX) maxX = layer.x + layer.width; + if (layer.y + layer.height > maxY) maxY = layer.y + layer.height; + } else { + var obj:FlxObject = cast m; + if (obj.x < minX) minX = obj.x; + if (obj.y < minY) minY = obj.y; + if (obj.x + obj.width > maxX) maxX = obj.x + obj.width; + if (obj.y + obj.height > maxY) maxY = obj.y + obj.height; + } + } - _bounds.set(x, y, width, height); + _bounds.set(minX, minY, maxX - minX, maxY - minY); + } } // From FlxSpriteGroup @@ -237,7 +291,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.POSITIVE_INFINITY; for(m in this.members) { - if(m == null || m is FlxBasic) continue; + if(m == null || !(m is FlxObject)) continue; var minX:Float; if(m is StageLayer) minX = cast(m, StageLayer).findMinX(); @@ -254,7 +308,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.NEGATIVE_INFINITY; for(m in this.members) { - if(m == null || m is FlxBasic) continue; + if(m == null || !(m is FlxObject)) continue; var maxX:Float; if(m is StageLayer) maxX = cast(m, StageLayer).findMaxX(); @@ -274,7 +328,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.POSITIVE_INFINITY; for(m in this.members) { - if(m == null || m is FlxBasic) continue; + if(m == null || !(m is FlxObject)) continue; var minY:Float; if(m is StageLayer) minY = cast(m, StageLayer).findMinY(); @@ -291,7 +345,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.NEGATIVE_INFINITY; for(m in this.members) { - if(m == null || m is FlxBasic) continue; + if(m == null || !(m is FlxObject)) continue; var maxY:Float; if(m is StageLayer) maxY = cast(m, StageLayer).findMaxY(); @@ -403,6 +457,11 @@ class NewStage extends StageLayer { if (onStageScriptLoad != null) onStageScriptLoad(script); script.setParent(this); script.load(); + script.call("create"); + script.call("onStageLoad"); + + onAddSprite.add((obj:Dynamic) -> script.call("onAddSprite", [obj])); + onAddLayer.add((layer:StageLayer) -> script.call("onAddLayer", [layer])); } if (xmlFile == null) { @@ -440,6 +499,8 @@ class NewStage extends StageLayer { loadLayer(this, elems); postLoadStage(elems); + script?.call("postCreate"); + script?.call("onPostStageLoad"); } private inline function loadStartCam() { @@ -467,7 +528,9 @@ class NewStage extends StageLayer { var renderLayer:Bool = node.has.useRenderTexture ? node.att.useRenderTexture == "true" : false; var layer:StageLayer = new StageLayer(layerName, renderLayer); // recursive so it will allow nested layers + script?.call("onLoadLayer", [layer]); loadLayer(layer, [for(n in node.elements) n]); + script?.call("onPostLoadLayer", [layer]); addLayer(layer); case "sprite" | "spr" | "sparrow": if (!node.has.name) continue; @@ -484,10 +547,12 @@ class NewStage extends StageLayer { var w:Int = Std.parseInt(node.att.width); var h:Int = Std.parseInt(node.att.height); var c:flixel.util.FlxColor = (node.has.color) ? CoolUtil.getColorFromDynamic(node.att.color) : -1; - if (isSolid) spr.makeSolid(w, h, c); - else spr.makeGraphic(w, h, c); + if (isSolid) { + spr.makeSolid(w, h, c); + node.x.remove("updateHitbox"); + } else + spr.makeGraphic(w, h, c); - if (isSolid) node.x.remove("updateHitbox"); node.x.remove("width"); node.x.remove("height"); node.x.remove("color"); XMLUtil.loadSpriteFromXML(spr, node, "", NONE, false); @@ -503,20 +568,19 @@ class NewStage extends StageLayer { setCharPos(node.att.name, node); case "ratings" | "combo": if (onRatingSet == null) continue; - var ratingPos = { - x: Std.parseFloat(node.getAtt("x")), - y: Std.parseFloat(node.getAtt("y")) - } - onRatingSet(ratingPos.x, ratingPos.y); + onRatingSet(Std.parseFloat(node.getAtt("x")), Std.parseFloat(node.getAtt("y"))); case "use-extension" | "extension" | "ext": - if(XMLImportedScriptInfo.shouldLoadBefore(node)) continue; - if(onPrepareInfo != null && onPrepareInfo(node) == null) continue; + if (XMLImportedScriptInfo.shouldLoadBefore(node)) continue; + if (onPrepareInfo != null && onPrepareInfo(node) == null) continue; null; default: null; } if (onNodeLoaded != null) { + var _prevSprite = sprite; sprite = onNodeLoaded(node, sprite); + // cleanup since there will be a random sprite floating around in memory + if (_prevSprite != sprite && _prevSprite != null) _prevSprite.destroy(); } if (sprite != null) { @@ -541,8 +605,8 @@ class NewStage extends StageLayer { // i know this for gets run twice under, but its better like this in case a script modifies the short lived ones, i dont wanna save them in an array; more dynamic like this - Nex for (info in xmlImportedScripts) if (info.importStageSprites) { - var script = info.getScript(); - if (script != null) setStagesSprites(script); + var scriptInfo = info.getScript(); + if (scriptInfo != null) setStagesSprites(scriptInfo); } // idk lemme check anyways just in case scripts did smth - Nex @@ -551,11 +615,11 @@ class NewStage extends StageLayer { // shortlived scripts destroy when the stage finishes setting up - Nex for (info in xmlImportedScripts) if (info.shortLived) { - var script = info.getScript(); - if (script == null) continue; + var scriptInfo = info.getScript(); + if (scriptInfo == null) continue; - if (onRemoveInfo != null) onRemoveInfo(script); - script.destroy(); + if (onRemoveInfo != null) onRemoveInfo(scriptInfo); + scriptInfo.destroy(); } } @@ -660,6 +724,18 @@ class NewStage extends StageLayer { destroySilently(); } + override function update(elapsed:Float) { + script?.call("update", [elapsed]); + super.update(elapsed); + script?.call("postUpdate", [elapsed]); + } + + override function draw() { + script?.call("draw"); + super.draw(); + script?.call("postDraw"); + } + @:dox(hide) private function checkMemoryMode(xml:Access, loadAll:Bool, elems:Array) { for(node in xml.elements) { if (node.name == "high-memory" && (!Options.lowMemoryMode || loadAll)) @@ -677,7 +753,7 @@ class NewStage extends StageLayer { if (onPrepareInfo != null) onPrepareInfo(node); } - // bruh... + //region IHScriptCustomBehaviour implementation override function hget(name:String):Dynamic { if (__instanceFields.contains(name) || __instanceFields.contains('get_$name')) return Reflect.getProperty(this, name); @@ -697,6 +773,7 @@ class NewStage extends StageLayer { } return super.hset(name, val); } + //endregion // Backwards compatibility public var stagePath(get, never):String; From ebfed78d787a4f885398e308aed1dc07fe5b41ed Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Thu, 25 Jun 2026 16:27:38 -0700 Subject: [PATCH 17/24] OK FOR REAL IVE FIXED `updateBounds` PLEASE --- source/funkin/game/NewStage.hx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 40b22de584..7c554ba89f 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -239,6 +239,10 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private var _bounds:FlxRect = FlxRect.get(); private function get_bounds():FlxRect { return _bounds; } + private inline function __allowedToUpdateBounds(m:FlxBasic):Bool { + return (m != null && !(m is StageCharPos) && m is FlxObject); + } + /** * Updates the bounds of the layer. * Without `hard_check` enabled, it will check values of `x`, `y`, `width` and `height` of the members directly. @@ -263,7 +267,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var maxX:Float = Math.NEGATIVE_INFINITY; var maxY:Float = Math.NEGATIVE_INFINITY; for (m in this.members) { - if (m == null || !(m is FlxObject)) continue; + if (!__allowedToUpdateBounds(m)) continue; if (m is StageLayer) { var layer:StageLayer = cast m; @@ -291,7 +295,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.POSITIVE_INFINITY; for(m in this.members) { - if(m == null || !(m is FlxObject)) continue; + if (!__allowedToUpdateBounds(m)) continue; var minX:Float; if(m is StageLayer) minX = cast(m, StageLayer).findMinX(); @@ -308,7 +312,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.NEGATIVE_INFINITY; for(m in this.members) { - if(m == null || !(m is FlxObject)) continue; + if (!__allowedToUpdateBounds(m)) continue; var maxX:Float; if(m is StageLayer) maxX = cast(m, StageLayer).findMaxX(); @@ -328,7 +332,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.POSITIVE_INFINITY; for(m in this.members) { - if(m == null || !(m is FlxObject)) continue; + if (!__allowedToUpdateBounds(m)) continue; var minY:Float; if(m is StageLayer) minY = cast(m, StageLayer).findMinY(); @@ -345,7 +349,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.NEGATIVE_INFINITY; for(m in this.members) { - if(m == null || !(m is FlxObject)) continue; + if (!__allowedToUpdateBounds(m)) continue; var maxY:Float; if(m is StageLayer) maxY = cast(m, StageLayer).findMaxY(); From 1f387a6a258f14b87e3d7d5ecb90bb6108adb3a6 Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Thu, 25 Jun 2026 16:59:20 -0700 Subject: [PATCH 18/24] adding `StageLayers` to a `StageLayer` with `add` / `insert` / `remove` functions will now register them automatically. This is so stage scripts, or other external accessing of `StageLayer` doesn't need to remember a specific function to register the newly added `StageLayer`. `stageSprites` still need to be set directly as they require a custom name to map them, otherwise they are considered stray objects. --- source/funkin/game/NewStage.hx | 81 ++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 7c554ba89f..2e005c40c0 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -135,6 +135,33 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private var stageSprites:Map = []; private var stageLayers:Map = []; + + override public function add(basic:T):T { + if (!(basic is StageLayer)) return super.add(basic, splice); + + var layer:StageLayer = cast basic; + if (stageLayers.exists(layer.name)) return stageLayers.get(layer.name); + + stageLayers.set(layer.name, layer); + return super.add(layer); + } + + override public function insert(position:Int, basic:T):T { + if (!(basic is StageLayer)) return super.insert(basic, splice); + var layer:StageLayer = cast basic; + if (stageLayers.exists(layer.name)) return stageLayers.get(layer.name); + + stageLayers.set(layer.name, layer); + return super.insert(position, layer); + } + + override public function remove(basic:T, splice:Bool = false):T { + if (!(basic is StageLayer)) return super.remove(basic, splice); + + var layer:StageLayer = cast basic; + stageLayers.remove(layer.name); + return super.remove(layer, splice); + } //region Stage Sprite Management public function addSprite(name:String, spr:FlxObject):FlxObject { @@ -167,37 +194,6 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem } //endregion - //region Stage Layer Management - public function addLayer(layer:StageLayer):StageLayer { - if (stageLayers.exists(layer.name)) return layer; - - this.add(layer); - stageLayers.set(layer.name, layer); - - return layer; - } - - public function insertLayer(index:Int, layer:StageLayer):StageLayer { - if (stageLayers.exists(layer.name)) return layer; - - this.insert(index, layer); - stageLayers.set(layer.name, layer); - - return layer; - } - - public function removeLayer(layer:StageLayer, splice:Bool = false):Bool { - if (!stageLayers.exists(layer.name)) return false; - - var layer:StageLayer = stageLayers.get(layer.name); - - this.remove(layer, splice); - stageLayers.remove(layer.name); - - return true; - } - //endregion - //region IBeatReceiver implementation public function beatHit(curBeat:Int) { for(m in members) if(m != null && m is IBeatReceiver) cast(m, IBeatReceiver).beatHit(curBeat); @@ -521,8 +517,8 @@ class NewStage extends StageLayer { private function loadLayer(layer:StageLayer, elems:Array) { for(node in elems) { - // If `onNodeInitalize` returns a valid value, then why waste time on checking other values, since we should only care about what the user - // sets it too. Optimizations be like: + // If `onNodeInitalize` returns a valid value, then why waste time on checking other values, + // since we should only care about what the user sets it too. Optimizations be like: var sprite:Dynamic = (onNodeInitalize != null) ? onNodeInitalize(node) : null; if (sprite == null) sprite = switch(node.name) { case "layer": @@ -535,7 +531,7 @@ class NewStage extends StageLayer { script?.call("onLoadLayer", [layer]); loadLayer(layer, [for(n in node.elements) n]); script?.call("onPostLoadLayer", [layer]); - addLayer(layer); + add(layer); case "sprite" | "spr" | "sparrow": if (!node.has.name) continue; @@ -573,11 +569,14 @@ class NewStage extends StageLayer { case "ratings" | "combo": if (onRatingSet == null) continue; onRatingSet(Std.parseFloat(node.getAtt("x")), Std.parseFloat(node.getAtt("y"))); - case "use-extension" | "extension" | "ext": - if (XMLImportedScriptInfo.shouldLoadBefore(node)) continue; - if (onPrepareInfo != null && onPrepareInfo(node) == null) continue; + default: + // moved it to be like this, so we can just update the inline function - LJ + if (__isExtensionNode(node)) { + if (XMLImportedScriptInfo.shouldLoadBefore(node)) continue; + if (onPrepareInfo != null && onPrepareInfo(node) == null) continue; + null; + } null; - default: null; } if (onNodeLoaded != null) { @@ -740,6 +739,10 @@ class NewStage extends StageLayer { script?.call("postDraw"); } + @:dox(hide) private inline function __isExtensionNode(node:Access):Bool { + return node.name == "use-extension" || node.name == "extension" || node.name == "ext"; + } + @:dox(hide) private function checkMemoryMode(xml:Access, loadAll:Bool, elems:Array) { for(node in xml.elements) { if (node.name == "high-memory" && (!Options.lowMemoryMode || loadAll)) @@ -753,7 +756,7 @@ class NewStage extends StageLayer { @:dox(hide) private function pushNode(node:Access, elems:Array) { elems.push(node); - if ((node.name == "use-extension" || node.name == "extension" || node.name == "ext") && XMLImportedScriptInfo.shouldLoadBefore(node)) + if (__isExtensionNode(node) && XMLImportedScriptInfo.shouldLoadBefore(node)) if (onPrepareInfo != null) onPrepareInfo(node); } From 01e5642c8df300c415a8c4e17248f122502c78d1 Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Thu, 25 Jun 2026 17:05:07 -0700 Subject: [PATCH 19/24] im just a fat idot --- source/funkin/game/NewStage.hx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 2e005c40c0..c8b632c4f2 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -136,8 +136,8 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private var stageSprites:Map = []; private var stageLayers:Map = []; - override public function add(basic:T):T { - if (!(basic is StageLayer)) return super.add(basic, splice); + override public function add(basic:FlxBasic):FlxBasic { + if (!(basic is StageLayer)) return super.add(basic); var layer:StageLayer = cast basic; if (stageLayers.exists(layer.name)) return stageLayers.get(layer.name); @@ -146,8 +146,8 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return super.add(layer); } - override public function insert(position:Int, basic:T):T { - if (!(basic is StageLayer)) return super.insert(basic, splice); + override public function insert(position:Int, basic:FlxBasic):FlxBasic { + if (!(basic is StageLayer)) return super.insert(position, basic); var layer:StageLayer = cast basic; if (stageLayers.exists(layer.name)) return stageLayers.get(layer.name); @@ -155,7 +155,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return super.insert(position, layer); } - override public function remove(basic:T, splice:Bool = false):T { + override public function remove(basic:FlxBasic, splice:Bool = false):FlxBasic { if (!(basic is StageLayer)) return super.remove(basic, splice); var layer:StageLayer = cast basic; From 5c98f56859103e76d4f4b5c1078197d7ac4f963d Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Thu, 25 Jun 2026 18:28:06 -0700 Subject: [PATCH 20/24] better `toString` for debugging --- source/funkin/game/NewStage.hx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index c8b632c4f2..ea7a7e0ccf 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -13,6 +13,8 @@ import flixel.group.FlxGroup; import flixel.math.FlxPoint; import flixel.math.FlxRect; import flixel.util.FlxSignal.FlxTypedSignal; +import flixel.util.FlxStringUtil; +import flixel.util.FlxStringUtil.LabelValuePair; import flixel.system.FlxAssets.FlxShader; @@ -136,6 +138,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private var stageSprites:Map = []; private var stageLayers:Map = []; + //region Stage Layer Management override public function add(basic:FlxBasic):FlxBasic { if (!(basic is StageLayer)) return super.add(basic); @@ -162,6 +165,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem stageLayers.remove(layer.name); return super.remove(layer, splice); } + //endregion //region Stage Sprite Management public function addSprite(name:String, spr:FlxObject):FlxObject { @@ -368,6 +372,15 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return _bounds.width; private function get_height():Float return _bounds.height; + + override public function toString():String { + return '(Stage Layer) $name: ${FlxStringUtil.getDebugString([ + LabelValuePair.weak("x", x), + LabelValuePair.weak("y", y), + LabelValuePair.weak("width", width), + LabelValuePair.weak("height", height), + ])}'; + } } class NewStage extends StageLayer { From f85f4a131c10714353fdf88da80ae7873aac0af8 Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Thu, 2 Jul 2026 01:23:08 -0700 Subject: [PATCH 21/24] Backwords compatability for now, can be removed later. --- source/funkin/game/NewStage.hx | 1 + source/funkin/game/PlayState.hx | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index ea7a7e0ccf..770f71789b 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -808,4 +808,5 @@ class NewStage extends StageLayer { function get_stageName():String { return this.name; } function set_stageName(name:String):String { return this.name = name; } function get_characterPoses():Map { return this.characterPosLookup; } + inline function applyCharStuff(char:Character, posName:String, id:Float = 0) { return applyCharPos(char, posName, id); } } diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 8a7ed8fc0b..4a8c2d902e 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -568,6 +568,16 @@ class PlayState extends MusicBeatState */ public var noteTypesArray:Array = [null]; + // Backwards compatability for removing sprites from the Stage... + @:dox(hide) override public function remove(basic:FlxBasic, splice:Bool = false):FlxBasic { + if (members == null) return null; + + final index = members.indexOf(basic); + + if (index < 0) return stage.remove(basic, splice); + return super.remove(basic, splice); + } + /** * Hit window, in milliseconds. A Legacy CNE Hit window configuration, * Don't use this, it's for mods that still uses the old judgement timing, instead use ratingManager. From 1da336c6920c0382d345acd26e00a4350a3b1b0e Mon Sep 17 00:00:00 2001 From: ItsLJcool Date: Thu, 2 Jul 2026 06:41:17 -0700 Subject: [PATCH 22/24] Stage Caching implemented, also FINALLY added Change Stage and Change Character events --- source/funkin/backend/chart/EventsData.hx | 19 ++- source/funkin/game/NewStage.hx | 43 +++++- source/funkin/game/PlayState.hx | 152 +++++++++++++++++++--- 3 files changed, 190 insertions(+), 24 deletions(-) diff --git a/source/funkin/backend/chart/EventsData.hx b/source/funkin/backend/chart/EventsData.hx index 4920b2c6ef..fbdf1c35ec 100644 --- a/source/funkin/backend/chart/EventsData.hx +++ b/source/funkin/backend/chart/EventsData.hx @@ -10,7 +10,7 @@ import openfl.Assets; using StringTools; class EventsData { - public static var defaultEventsList:Array = ["HScript Call", "Camera Movement", "Camera Position", "Add Camera Zoom", "Camera Bop", "Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Continuous BPM Change", "Time Signature Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation"]; + public static var defaultEventsList:Array = ["HScript Call", "Camera Movement", "Camera Position", "Add Camera Zoom", "Camera Bop", "Camera Zoom", "Camera Modulo Change", "Camera Flash", "BPM Change", "Continuous BPM Change", "Time Signature Change", "Scroll Speed Change", "Alt Animation Toggle", "Play Animation", "Change Stage", "Change Character"]; public static var defaultEventsParams:Map> = [ "HScript Call" => [ {name: "Function Name", type: TString, defValue: "myFunc"}, @@ -119,6 +119,23 @@ class EventsData { defValue: "NONE" } ], + "Change Stage" => [ + {name: "Stage Name", type: TString, defValue: ""}, + {name: "Cached?", type: TBool, defValue: true}, + {name: "Destroy Previous Stage", type: TBool, defValue: false}, + {name: "Force Reload?", type: TBool, defValue: false} + ], + "Change Character" => [ + {name: "Character from StrumLine", type: TStrumLine, defValue: 0}, + {name: "Character StrumLine Index", type: TInt(0, 9999999, 1), defValue: 0}, + {name: "Character Name", type: TString, defValue: "bf"}, + {name: "Override isPlayer?", type: TBool, defValue: false}, + {name: "isPlayer?", type: TBool, defValue: false}, + + {name: "Cache?", type: TBool, defValue: true}, + {name: "Destroy Previous Character", type: TBool, defValue: false}, + {name: "Auto Stage Position", type: TBool, defValue: true}, + ] ]; public static var eventsList:Array = defaultEventsList.copy(); diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 770f71789b..dced592980 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -386,9 +386,26 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem class NewStage extends StageLayer { private static final __instanceFields = Type.getInstanceFields(NewStage); - // TODO: Preload the Assets and scripts of the stage before returning 👀 - public static function getStage(name:String):NewStage { - return new NewStage(name); + public static var CACHE:Map = []; + public inline static function clear_cache() { + for (name=>stage in CACHE) stage?.destroy(); + CACHE.clear(); + } + + public static function cache(name:String, reload:Bool = false):NewStage { + if (CACHE.exists(name) && !reload) return CACHE[name]; + else if (CACHE.exists(name) && reload) { + var stage:NewStage = CACHE[name]; + stage?.destroy(); + FlxG.state?.remove(stage, true); + } + + var stage:NewStage = new NewStage(name, true); + stage.active = stage.exists = stage.visible = false; + + CACHE[name] = stage; + + return stage; } private static final DEFAULT_ATTRIBUTES:Array = ["name", "startCamPosX", "startCamPosY", "zoom", "folder", "useRenderTexture"]; @@ -442,6 +459,8 @@ class NewStage extends StageLayer { private var characterPosLookup:Map = []; + public var hasLoaded:Bool = false; + /** * Sets the sprites in the script, so you can access them by the name. **/ @@ -464,7 +483,8 @@ class NewStage extends StageLayer { private var stageEvent:StageXMLEvent; - public function loadStage(loadAll:Bool = false) { + public function loadStage(loadAll:Bool = false):Void { + if (hasLoaded) return; if (allowScripts) { script = Script.create(scriptFilePath); if (onStageScriptLoad != null) onStageScriptLoad(script); @@ -514,6 +534,7 @@ class NewStage extends StageLayer { postLoadStage(elems); script?.call("postCreate"); script?.call("onPostStageLoad"); + hasLoaded = true; } private inline function loadStartCam() { @@ -777,9 +798,15 @@ class NewStage extends StageLayer { override function hget(name:String):Dynamic { if (__instanceFields.contains(name) || __instanceFields.contains('get_$name')) return Reflect.getProperty(this, name); + + // We should check PlayState last, and check sub-layers before. + var og_val:Dynamic = super.hget(name); + if (og_val != null) return og_val; + if (PlayState.instance != null && (PlayState.__instanceFields.contains(name) || PlayState.__instanceFields.contains('get_$name'))) return Reflect.getProperty(PlayState.instance, name); - return super.hget(name); + + return null; } override function hset(name:String, val:Dynamic):Dynamic { @@ -787,11 +814,15 @@ class NewStage extends StageLayer { Reflect.setProperty(this, name, val); return val; } + + var og_val:Dynamic = super.hget(name); + if (og_val != null) return og_val; + if (PlayState.instance != null && (PlayState.__instanceFields.contains(name) || PlayState.__instanceFields.contains('set_$name'))) { Reflect.setProperty(PlayState.instance, name, val); return val; } - return super.hset(name, val); + return null; } //endregion diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index c986c6e975..71e80de27a 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -37,6 +37,7 @@ import funkin.menus.*; import funkin.backend.week.WeekData; import funkin.savedata.FunkinSave; import haxe.io.Path; +import haxe.xml.Access; using StringTools; @@ -578,6 +579,34 @@ class PlayState extends MusicBeatState return super.remove(basic, splice); } + private var CHARACTER_CACHE:Map> = []; + private function cache_character(character:Character, index:Int = 0):Character { + if (CHARACTER_CACHE.exists(character.curCharacter)) { + var stack:Array = CHARACTER_CACHE.get(character.curCharacter); + trace('stack[index]: ${stack[index]}'); + if (stack[index] != null) return stack[index]; + + stack.insert(index, character); + trace('Stacking Character ${character.curCharacter} at index $index'); + return character; + } + + CHARACTER_CACHE.set(character.curCharacter, [character]); + trace('Caching ${character.curCharacter} at index $index'); + + return character; + } + + private function get_character_cache(index:Int, name:String):Character { + if (!CHARACTER_CACHE.exists(name)) return cache_character(new Character(0, 0, name, false), index); + + var stack:Array = CHARACTER_CACHE[name]; + if (stack.length <= 0) return cache_character(new Character(0, 0, name, false), index); + + index = CoolUtil.boundInt(index, 0, stack.length - 1); + return stack[index]; + } + /** * Hit window, in milliseconds. A Legacy CNE Hit window configuration, * Don't use this, it's for mods that still uses the old judgement timing, instead use ratingManager. @@ -746,7 +775,7 @@ class PlayState extends MusicBeatState add(camFollow); if (SONG.stage == null || SONG.stage.trim() == "") SONG.stage = Flags.DEFAULT_STAGE; - add(stage = NewStage.getStage(SONG.stage)); + add(stage = NewStage.cache(SONG.stage)); if (!chartingMode || Options.charterEnablePlaytestScripts) { switch(SONG.meta.name) { @@ -961,6 +990,36 @@ class PlayState extends MusicBeatState __updateNote_event = EventManager.get(NoteUpdateEvent); + for (event in events) { + switch(event.name) { + case "Change Character": + if (!event.params[5]) continue; + var strumline_index:Int = CoolUtil.boundInt(event.params[0], 0, strumLines.members.length - 1); + var char_idx:Int = event.params[1]; + var name:String = event.params[2]; + var override_isPlayer:Bool = event.params[3]; + + var strumline:StrumLine = (strumLines.members[strumline_index] ?? cpuStrums); + var character:Character = strumline.characters[char_idx]; + var isPlayer:Bool = (override_isPlayer) ? event.params[4] : (character?.isPlayer ?? false); + trace('character: ${character.curCharacter} ($name) | isPlayer: $isPlayer | override_isPlayer: $override_isPlayer | event.params[4]: ${event.params[4]}'); + + var path:String = Paths.xml('characters/$name'); + if (Assets.exists(path)) { + try { + var node:Access = new Access(Xml.parse(Assets.getText(path)).firstElement()); + + var folder:String = (node.has.sprite) ? node.att.sprite : name; + var iconPath:String = (node.has.icon) ? node.att.icon : name; + + graphicCache.cache(Paths.image('characters/$folder')); + graphicCache.cache(Paths.image('icons/$iconPath')); + } + catch (e) Logs.trace('Couldn\'t load character XML "$path": ${e.message}', ERROR); + } + cache_character(new Character(0, 0, name, isPlayer), char_idx); + } + } gameAndCharsCall("postCreate", null, "gamePostCreate"); } @@ -1127,6 +1186,8 @@ class PlayState extends MusicBeatState remove(stage, true); } + NewStage.clear_cache(); + scripts = FlxDestroyUtil.destroy(scripts); super.destroy(); @@ -1156,13 +1217,15 @@ class PlayState extends MusicBeatState else events = [ for (e in songData.events) { switch (e.name) { - case "Camera Movement": if (!foundCam && e.time < 10) { - foundCam = true; - executeEvent(e); - } - case "Time Signature Change": if (!foundSigs && (e.params[0] != 4 || e.params[1] != 4)) { - foundSigs = true; - } + case "Camera Movement": + if (!foundCam && e.time < 10) { + foundCam = true; + executeEvent(e); + } + case "Time Signature Change": + if (!foundSigs && (e.params[0] != 4 || e.params[1] != 4)) foundSigs = true; + case "Change Stage": + NewStage.cache(e.params[0]); } e; } @@ -1707,6 +1770,56 @@ class PlayState extends MusicBeatState if (strumLines.members[event.params[0]] != null && strumLines.members[event.params[0]].characters != null) for (char in strumLines.members[event.params[0]].characters) if (char != null && char.hasAnim(event.params[1])) char.playAnim(event.params[1], event.params[2], event.params[3] == "NONE" ? null : event.params[3]); + + case "Change Stage": + var stage_name:String = event.params[0]; + var use_cache:Bool = event.params[1]; + var destroy_previous:Bool = event.params[2]; + var force_reload:Bool = event.params[3]; + + var new_stage:NewStage = use_cache ? NewStage.cache(stage_name, force_reload) : new NewStage(stage_name); + + if (destroy_previous) stage?.destroy(); + stage = new_stage; + + case "Change Character": + var strumline_index:Int = CoolUtil.boundInt(event.params[0], 0, strumLines.members.length - 1); + var char_index:Int = event.params[1]; + var char_name:String = event.params[2]; + + var override_isPlayer:Bool = event.params[3]; + var isPlayer:Bool = event.params[4]; + + var cached:Bool = event.params[5]; + + var destroy_previous:Bool = event.params[6]; + var auto_stage_position:Bool = event.params[7]; + + var strumline:StrumLine = (strumLines.members[strumline_index] ?? cpuStrums); + var character:Character = strumline.characters[char_index]; + if (character != null) { + var new_character:Character = (cached) ? get_character_cache(char_index, char_name) : new Character(character.x, character.y, char_name, false); + + new_character.isPlayer = (override_isPlayer) ? isPlayer : character.isPlayer; + new_character.__switchAnims = (strumline.data.type == 1); + new_character.fixChar(new_character.__switchAnims, new_character.__autoInterval); + if (new_character.isPlayer) new_character.flipX = !new_character.flipX; // to re-flip 😭 + new_character.__baseFlipped = new_character.flipX; + new_character.dance(); + + var icon:HealthIcon = (new_character.isPlayer) ? iconP1 : iconP2; + icon.setIcon(new_character.icon); + + strumline.characters[char_index] = new_character; + + stage.remove(character, true); + if (destroy_previous) character?.destroy(); + + if (auto_stage_position) reposition_characters(); + + } + + case "Unknown": // nothing } @@ -2314,13 +2427,23 @@ class PlayState extends MusicBeatState return this.gameAndCharsEvent("onStageNodeParsed", EventManager.get(StageNodeEvent).recycle(new_stage, node, sprite, node.name)).sprite; } new_stage.onPostStageCreation = (stageEvent) -> this.gameAndCharsEvent("onPostStageCreation", stageEvent); - new_stage.onSilentDestroy = (script) -> this.scripts?.remove(script); new_stage.onStageDestroy = (_) -> this.gameAndCharsCall("onStageDestroy", [_]); + new_stage.active = new_stage.exists = new_stage.visible = true; new_stage.loadStage(); if (!isReplacing) return this.stage = new_stage; - + + replace(this.stage, new_stage); + this.stage = new_stage; + + reposition_characters(); + + return this.stage; + } + #end + + private function reposition_characters() { // reset the characters positions with the new stage for (strumline in this.strumLines.members) { if (strumline.characters.length == 0) continue; @@ -2330,16 +2453,11 @@ class PlayState extends MusicBeatState case 1: "boyfriend"; case 2: "girlfriend"; }; - // var char = new Character(0, 0, charName, new_stage.isCharFlipped(new_stage.characterPoses[charName] != null ? charName : charPosName, strumLine.type == 1)); - char.isPlayer = new_stage.isCharFlipped(new_stage.characterPoses[char.curCharacter] != null ? char.curCharacter : posName, !strumline.opponentSide); - new_stage.applyCharPos(char, posName, i); + char.isPlayer = this.stage.isCharFlipped(this.stage.characterPoses[char.curCharacter] != null ? char.curCharacter : posName, !strumline.opponentSide); + this.stage.applyCharPos(char, posName, i); } } - - replace(this.stage, new_stage); - return this.stage = new_stage; } - #end /** * Load a week into PlayState. From 468a5394dd865d9efd3ff46ad7d93e501e5ee3f8 Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Sat, 25 Jul 2026 23:41:19 -0700 Subject: [PATCH 23/24] more clever name :3 --- source/funkin/game/NewStage.hx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index dced592980..09f49bac73 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -239,7 +239,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem private var _bounds:FlxRect = FlxRect.get(); private function get_bounds():FlxRect { return _bounds; } - private inline function __allowedToUpdateBounds(m:FlxBasic):Bool { + private inline function __shouldUpdateBounds(m:FlxBasic):Bool { return (m != null && !(m is StageCharPos) && m is FlxObject); } @@ -267,7 +267,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var maxX:Float = Math.NEGATIVE_INFINITY; var maxY:Float = Math.NEGATIVE_INFINITY; for (m in this.members) { - if (!__allowedToUpdateBounds(m)) continue; + if (!__shouldUpdateBounds(m)) continue; if (m is StageLayer) { var layer:StageLayer = cast m; @@ -295,7 +295,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.POSITIVE_INFINITY; for(m in this.members) { - if (!__allowedToUpdateBounds(m)) continue; + if (!__shouldUpdateBounds(m)) continue; var minX:Float; if(m is StageLayer) minX = cast(m, StageLayer).findMinX(); @@ -312,7 +312,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.NEGATIVE_INFINITY; for(m in this.members) { - if (!__allowedToUpdateBounds(m)) continue; + if (!__shouldUpdateBounds(m)) continue; var maxX:Float; if(m is StageLayer) maxX = cast(m, StageLayer).findMaxX(); @@ -332,7 +332,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.POSITIVE_INFINITY; for(m in this.members) { - if (!__allowedToUpdateBounds(m)) continue; + if (!__shouldUpdateBounds(m)) continue; var minY:Float; if(m is StageLayer) minY = cast(m, StageLayer).findMinY(); @@ -349,7 +349,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem var value = Math.NEGATIVE_INFINITY; for(m in this.members) { - if (!__allowedToUpdateBounds(m)) continue; + if (!__shouldUpdateBounds(m)) continue; var maxY:Float; if(m is StageLayer) maxY = cast(m, StageLayer).findMaxY(); From 67118700316cc351de7933ae18d7b193c74a0262 Mon Sep 17 00:00:00 2001 From: Jaime Humberto Macias Bustamante Date: Fri, 31 Jul 2026 22:23:48 -0700 Subject: [PATCH 24/24] "flixel-animate" layer rendering method (WIP) --- source/funkin/game/NewStage.hx | 117 ++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/source/funkin/game/NewStage.hx b/source/funkin/game/NewStage.hx index 09f49bac73..d7ab1587d6 100644 --- a/source/funkin/game/NewStage.hx +++ b/source/funkin/game/NewStage.hx @@ -12,15 +12,19 @@ import funkin.backend.system.interfaces.IBeatReceiver; import flixel.group.FlxGroup; import flixel.math.FlxPoint; import flixel.math.FlxRect; +import flixel.math.FlxMatrix; import flixel.util.FlxSignal.FlxTypedSignal; import flixel.util.FlxStringUtil; import flixel.util.FlxStringUtil.LabelValuePair; +import flixel.util.FlxDestroyUtil; import flixel.system.FlxAssets.FlxShader; import haxe.xml.Access; import hscript.IHScriptCustomBehaviour; +import openfl.display.BlendMode; + using StringTools; /** @@ -36,6 +40,12 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem public var height(get, never):Float; public var bounds(get, never):FlxRect; + /** + * WARNING: The `origin` of the layer will default to its center. If you change this, + * the visuals will likely be pretty out-of-sync if you do any rotation. + */ + public var origin(default, null):FlxPoint = FlxPoint.get(); // This will be used for rendering btw + public var onAddSprite:FlxTypedSignal Void> = new FlxTypedSignal(); public var onAddLayer:FlxTypedSignal Void> = new FlxTypedSignal(); @@ -46,6 +56,7 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem */ public var useRenderTexture:Bool = false; private var _renderTexture:RenderTexture; + private var _renderTextureDirty:Bool = true; // TODO: use FlxAnimate's implementation on useRenderTexture so we can apply the shader onto all sprites as 1 sprite !!! public var shader(default, set):FlxShader; @@ -103,6 +114,20 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem return s; } + public var blend(default, set):BlendMode = null; + private function set_blend(b:BlendMode):BlendMode { + if(useRenderTexture) + return this.blend = b; + + for(member in this.members) { + if(member is FlxSprite) + cast(member, FlxSprite).blend = b; + else if(members is StageLayer) + cast(member, StageLayer).blend = b; + } + return this.blend = b; + } + public inline function getSprite(name:String):Null { return stageSprites.exists(name) ? stageSprites[name] : null; } @@ -132,7 +157,92 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem } function checkRenderTexture():Bool { - return useRenderTexture && (/*alpha != 1 || */shader != null /*|| (blend != null && blend != NORMAL)*/); + return useRenderTexture && (alpha != 1 || shader != null || (blend != null && blend != NORMAL)); + } + + override function draw() { + if(!checkRenderTexture()) { + super.draw(); + return; + } + + for(cam in this.cameras) { + if (!cam.visible || !cam.exists) + continue; + + drawLayer(cam); + + #if FLX_DEBUG + flixel.FlxBasic.visibleCount++; + #end + } + } + + @:noCompletion + private var layerMatrix:FlxMatrix = new FlxMatrix(); + @:noCompletion + private var _point:FlxPoint = FlxPoint.get(); + // TODO: + private function prepareLayerMatrix(matrix:FlxMatrix, camera:FlxCamera) { + matrix.translate(-origin.x, -origin.y); + + //getScreenPosition(_point, camera).subtractPoint(offset).add(origin.x, origin.y); + CoolUtil.pointToScreenPosition(FlxPoint.weak(x, y), camera, _point).add(origin.x, origin.y); + matrix.translate(_point.x, _point.y); + + if(camera.pixelPerfectRender) { + matrix.tx = Math.floor(matrix.tx); + matrix.ty = Math.floor(matrix.ty); + } + } + + private function drawLayer(cam:FlxCamera) { + layerMatrix.identity(); + prepareLayerMatrix(layerMatrix, cam); + + if(_renderTextureDirty) { + renderLayer(); + //_renderTextureDirty = false; + cam.drawPixels(_renderTexture.graphic.imageFrame.frame, null, layerMatrix, null, null, flixel.FlxSprite.defaultAntialiasing, shader != null ? shader : null); + } + } + + private function renderLayer() { + if (_renderTexture == null) + _renderTexture = new RenderTexture(Math.ceil(_bounds.width), Math.ceil(_bounds.height)); + _renderTexture.init(Math.ceil(_bounds.width), Math.ceil(_bounds.height)); + _renderTexture.drawToCamera((camera, matrix) -> { + matrix.translate(-_bounds.x, -_bounds.y); + drawMembers(camera, matrix); + }); + _renderTexture.render(); + } + + @:access(animate.internal.RenderTexture) + @:access(flixel.FlxSprite) + private function drawMembers(cam:FlxCamera, matrix:FlxMatrix) { + for(m in members) { + if(m is StageLayer) { + var sl:StageLayer = cast m; + if(sl.useRenderTexture) { + sl.renderLayer(); // cache the rendered layer + cam.drawPixels(sl._renderTexture.graphic.imageFrame.frame, null, matrix, null, null, flixel.FlxSprite.defaultAntialiasing, shader != null ? shader : null); + } + else + sl.draw(); + } + else if(m is StageCharPos) + continue; + else if(m is FlxSprite){ // TODO: maybe take into account the `onDraw` callback + var spr:FlxSprite = cast m; + spr.drawComplex(cam); + } + #if FLX_DEBUG + else if(m is FlxObject) { + cast(m, FlxObject).drawDebugOnCamera(cam); + } + #end + } } private var stageSprites:Map = []; @@ -233,7 +343,8 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem // then whatever below override function destroy() { super.destroy(); - _bounds.put(); + _bounds = FlxDestroyUtil.put(_bounds); + origin = FlxDestroyUtil.put(origin); } private var _bounds:FlxRect = FlxRect.get(); @@ -286,6 +397,8 @@ class StageLayer extends FlxTypedGroup implements IBeatReceiver implem _bounds.set(minX, minY, maxX - minX, maxY - minY); } + + origin = _bounds.getMidpoint(origin); } // From FlxSpriteGroup