From 1233013a88d3b9cf06a37819307924bc3eae984d Mon Sep 17 00:00:00 2001 From: HEIHUAa <112499486+HEIHUAa@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:30:59 +0800 Subject: [PATCH 1/4] script optimization --- source/funkin/backend/scripting/HScript.hx | 17 +++++++++-------- source/funkin/backend/scripting/Script.hx | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/source/funkin/backend/scripting/HScript.hx b/source/funkin/backend/scripting/HScript.hx index c3656478c6..c5bcc3fec8 100644 --- a/source/funkin/backend/scripting/HScript.hx +++ b/source/funkin/backend/scripting/HScript.hx @@ -11,7 +11,7 @@ class HScript extends Script { public var expr:Expr; public var code:String = null; //public var folderlessPath:String; - var __importedPaths:Array; + var __importedPaths:Map; public static function initParser() { var parser = new Parser(); @@ -31,7 +31,7 @@ class HScript extends Script { parser = initParser(); //folderlessPath = Path.directory(path); - __importedPaths = [path]; + __importedPaths = [path => true]; interp.errorHandler = _errorHandler; interp.warnHandler = _warnHandler; @@ -53,7 +53,7 @@ class HScript extends Script { public override function loadFromString(code:String) { try { - if (code != null && code.trim() != "") + if (code != null && code.length > 0) expr = parser.parseString(code, fileName); } catch(e:Error) { _errorHandler(e); @@ -74,13 +74,13 @@ class HScript extends Script { var assetsPath = 'assets/$prefix${cl.join("/")}'; for(hxExt in ["hx", "hscript", "hsc", "hxs"]) { var p = '$assetsPath.$hxExt'; - if (__importedPaths.contains(p)) + if (__importedPaths.exists(p)) return true; // no need to reimport again if (Assets.exists(p)) { var code = Assets.getText(p); var expr:Expr = null; try { - if (code != null && code.trim() != "") { + if (code != null && code.length > 0) { parser.line = 1; // fun fact: this is all you need to reuse a parser without issues. all the other vars get reset on parse. expr = parser.parseString(code, cl.join("/") + "." + hxExt); } @@ -92,7 +92,7 @@ class HScript extends Script { if (expr != null) { @:privateAccess interp.exprReturn(expr); - __importedPaths.push(p); + __importedPaths.set(p, true); } return true; } @@ -158,15 +158,16 @@ class HScript extends Script { interp.allowStaticVariables = interp.allowPublicVariables = false; var savedVariables:Map = []; + var defaultVars = Script.getDefaultVariables(this); for(k=>e in interp.variables) { - if (!Reflect.isFunction(e)) { + if (!Reflect.isFunction(e) && !defaultVars.exists(k)) { savedVariables[k] = e; } } var oldParent = interp.scriptObject; onCreate(path); - for(k=>e in Script.getDefaultVariables(this)) + for(k=>e in defaultVars) set(k, e); load(); diff --git a/source/funkin/backend/scripting/Script.hx b/source/funkin/backend/scripting/Script.hx index 4a03275f2a..c7d23b2a73 100644 --- a/source/funkin/backend/scripting/Script.hx +++ b/source/funkin/backend/scripting/Script.hx @@ -326,7 +326,7 @@ class Script extends FlxBasic implements IFlxDestroyable { var oldScript = curScript; curScript = this; - var result = onCall(func, parameters == null ? [] : parameters); + var result = onCall(func, parameters); curScript = oldScript; return result; From f41b1d42f734a47bc40fba05d581fb89dd8c421b Mon Sep 17 00:00:00 2001 From: HEIHUAa <112499486+HEIHUAa@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:48:45 +0800 Subject: [PATCH 2/4] optimization --- .../funkin/backend/scripting/GlobalScript.hx | 2 +- source/funkin/backend/scripting/HScript.hx | 31 +++++++++++++++++-- .../backend/scripting/MultiThreadedScript.hx | 10 +++--- source/funkin/backend/scripting/Script.hx | 21 +++++++++++-- source/funkin/backend/scripting/ScriptPack.hx | 6 +++- 5 files changed, 59 insertions(+), 11 deletions(-) diff --git a/source/funkin/backend/scripting/GlobalScript.hx b/source/funkin/backend/scripting/GlobalScript.hx index 9c0656192a..5c2365c737 100644 --- a/source/funkin/backend/scripting/GlobalScript.hx +++ b/source/funkin/backend/scripting/GlobalScript.hx @@ -69,7 +69,7 @@ class GlobalScript { call("preStateCreate", [state]); }); FlxG.signals.preStateSwitch.add(function() { - call("preStateSwitch", []); + call("preStateSwitch"); var stateName = Type.getClassName(Type.getClass(@:privateAccess FlxG.game._requestedState)); stateName = stateName.substring(stateName.lastIndexOf(".") + 1); diff --git a/source/funkin/backend/scripting/HScript.hx b/source/funkin/backend/scripting/HScript.hx index c5bcc3fec8..6bb7300b0f 100644 --- a/source/funkin/backend/scripting/HScript.hx +++ b/source/funkin/backend/scripting/HScript.hx @@ -20,6 +20,22 @@ class HScript extends Script { return parser; } + /** Pool of idle parsers, reused across script instances to avoid repeated Parser construction. **/ + private static var __parserPool:Array = []; + + private static function getParser():Parser { + var parser = __parserPool.pop(); + if (parser == null) + return initParser(); + parser.line = 1; // reusing a parser only requires resetting `line`; all other vars get reset on parse + return parser; + } + + private static function returnParser(parser:Parser) { + if (parser != null) + __parserPool.push(parser); + } + public override function onCreate(path:String) { super.onCreate(path); @@ -29,7 +45,7 @@ class HScript extends Script { if(Assets.exists(rawPath)) code = Assets.getText(rawPath); } catch(e) Logs.error('Error while reading $path: ${Std.string(e)}'); - parser = initParser(); + parser = getParser(); //folderlessPath = Path.directory(path); __importedPaths = [path => true]; @@ -165,6 +181,7 @@ class HScript extends Script { } } var oldParent = interp.scriptObject; + returnParser(parser); onCreate(path); for(k=>e in defaultVars) @@ -181,11 +198,10 @@ class HScript extends Script { private override function onCall(funcName:String, parameters:Array):Dynamic { if (interp == null) return null; - if (!interp.variables.exists(funcName)) return null; var func = interp.variables.get(funcName); if (func != null && Reflect.isFunction(func)) - return Reflect.callMethod(null, func, parameters); + return Reflect.callMethod(null, func, parameters == null ? Script._EMPTY_ARGS : parameters); return null; } @@ -196,6 +212,8 @@ class HScript extends Script { public override function set(val:String, value:Dynamic) { interp.variables.set(val, value); + // A runtime-injected variable may shadow a previously-cached VNotFound/type-resolve result, so drop stale cache entries. + interp.invalidateCache(); } public override function trace(v:Dynamic) { @@ -209,4 +227,11 @@ class HScript extends Script { public override function setPublicMap(map:Map) { this.interp.publicVariables = map; } + + override public function destroy() { + returnParser(parser); + parser = null; + interp = null; + super.destroy(); + } } diff --git a/source/funkin/backend/scripting/MultiThreadedScript.hx b/source/funkin/backend/scripting/MultiThreadedScript.hx index 517dc96e49..bc2bb5e2c0 100644 --- a/source/funkin/backend/scripting/MultiThreadedScript.hx +++ b/source/funkin/backend/scripting/MultiThreadedScript.hx @@ -8,7 +8,7 @@ class MultiThreadedScript implements IFlxDestroyable implements IHScriptCustomBe */ public var script:Script; - private var __variables:Array; + private var __variables:Map; /** * Return value of the last call. @@ -40,14 +40,16 @@ class MultiThreadedScript implements IFlxDestroyable implements IHScriptCustomBe script.load(); - __variables = Type.getInstanceFields(Type.getClass(this)); + __variables = new Map(); + for (f in Type.getInstanceFields(Type.getClass(this))) + __variables.set(f, true); } public function hget(name:String):Dynamic - return __variables.contains(name) ? Reflect.getProperty(this, name) : script.get(name); + return __variables.exists(name) ? Reflect.getProperty(this, name) : script.get(name); public function hset(name:String, val:Dynamic):Dynamic { - if (__variables.contains(name)) + if (__variables.exists(name)) Reflect.setProperty(this, name, val); else script.set(name, val); diff --git a/source/funkin/backend/scripting/Script.hx b/source/funkin/backend/scripting/Script.hx index c7d23b2a73..037609d7d5 100644 --- a/source/funkin/backend/scripting/Script.hx +++ b/source/funkin/backend/scripting/Script.hx @@ -21,6 +21,20 @@ class Script extends FlxBasic implements IFlxDestroyable { * Gets the default variables for a script. */ public static function getDefaultVariables(?script:Script):Map { + var vars = _defaultVariablesTemplate != null ? _defaultVariablesTemplate : (_defaultVariablesTemplate = buildDefaultVariables()); + var copy = vars.copy(); + copy.set("state", flixel.FlxG.state); // `state` changes on state switch, so it can't be cached + copy.set("window", lime.app.Application.current.window); // same for `window`: evaluated at script creation time like before + return copy; + } + + /** + * Cached template of the default variables. + * Built once (including the `Type.resolveClass` lookups) and shallow-copied per script. + */ + private static var _defaultVariablesTemplate:Map = null; + + private static function buildDefaultVariables():Map { return [ // Haxe related stuff "Std" => Std, @@ -38,10 +52,8 @@ class Script extends FlxBasic implements IFlxDestroyable { "Assets" => openfl.utils.Assets, "Application" => lime.app.Application, "Main" => funkin.backend.system.Main, - "window" => lime.app.Application.current.window, // Flixel related stuff - "state" => flixel.FlxG.state, "FlxG" => flixel.FlxG, "FlxSprite" => flixel.FlxSprite, "FlxBasic" => flixel.FlxBasic, @@ -185,6 +197,11 @@ class Script extends FlxBasic implements IFlxDestroyable { */ public static var curScript:Script = null; + /** + * Shared empty argument array, used when calling scripts without parameters (avoids allocations). + */ + private static var _EMPTY_ARGS:Array = []; + /** * Script name (with extension) */ diff --git a/source/funkin/backend/scripting/ScriptPack.hx b/source/funkin/backend/scripting/ScriptPack.hx index dfd8e49195..9558691208 100644 --- a/source/funkin/backend/scripting/ScriptPack.hx +++ b/source/funkin/backend/scripting/ScriptPack.hx @@ -13,6 +13,9 @@ class ScriptPack extends Script { public var publicVariables:Map = []; public var parent:Dynamic = null; + /** Reused single-slot argument array for `event` calls, avoiding a per-script allocation on every event. Safe: `call` consumes args synchronously. **/ + private var __eventArgs:Array = [null]; + /** * Loads all scripts in the pack. **/ @@ -98,7 +101,8 @@ class ScriptPack extends Script { for(e in scripts) { if(!e.active) continue; - e.call(func, [event]); + __eventArgs[0] = event; // set per-script so nested event dispatch on the same pack can't clobber it + e.call(func, __eventArgs); if (event.cancelled && !event.__continueCalls) break; } return event; From c05746f5419920cbd5609037aa7bbba984236641 Mon Sep 17 00:00:00 2001 From: HEIHUAa <112499486+HEIHUAa@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:01:52 +0800 Subject: [PATCH 3/4] PlayState Array --- source/funkin/game/PlayState.hx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 07d7433893..a449c8eb65 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -581,6 +581,8 @@ class PlayState extends MusicBeatState @:noCompletion @:dox(hide) private var _startCountdownCalled:Bool = false; @:noCompletion @:dox(hide) private var _endSongCalled:Bool = false; + @:noCompletion @:dox(hide) private static var _EMPTY_ARGS:Array = []; + @:dox(hide) var __vocalSyncTimer:Float = 1; @@ -1405,11 +1407,12 @@ class PlayState extends MusicBeatState @:dox(hide) override public function update(elapsed:Float) { - scripts.call("update", [elapsed]); + _EMPTY_ARGS[0] = elapsed; + scripts.call("update", _EMPTY_ARGS); if (inCutscene) { super.update(elapsed); - scripts.call("postUpdate", [elapsed]); + scripts.call("postUpdate", _EMPTY_ARGS); return; } @@ -1499,7 +1502,7 @@ class PlayState extends MusicBeatState super.update(elapsed); - scripts.call("postUpdate", [elapsed]); + scripts.call("postUpdate", _EMPTY_ARGS); } override function draw() { From 0e165f399f76862d75e33acabe00ba71b3d9df1a Mon Sep 17 00:00:00 2001 From: HEIHUAa <112499486+HEIHUAa@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:15:09 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20PlayState.hx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/funkin/game/PlayState.hx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index a449c8eb65..8d28f34532 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -581,7 +581,7 @@ class PlayState extends MusicBeatState @:noCompletion @:dox(hide) private var _startCountdownCalled:Bool = false; @:noCompletion @:dox(hide) private var _endSongCalled:Bool = false; - @:noCompletion @:dox(hide) private static var _EMPTY_ARGS:Array = []; + @:noCompletion @:dox(hide) private static var _ONE_ARG:Array = [null]; @:dox(hide) var __vocalSyncTimer:Float = 1; @@ -1407,12 +1407,12 @@ class PlayState extends MusicBeatState @:dox(hide) override public function update(elapsed:Float) { - _EMPTY_ARGS[0] = elapsed; - scripts.call("update", _EMPTY_ARGS); + _ONE_ARG[0] = elapsed; + scripts.call("update", _ONE_ARG); if (inCutscene) { super.update(elapsed); - scripts.call("postUpdate", _EMPTY_ARGS); + scripts.call("postUpdate", _ONE_ARG); return; } @@ -1502,7 +1502,7 @@ class PlayState extends MusicBeatState super.update(elapsed); - scripts.call("postUpdate", _EMPTY_ARGS); + scripts.call("postUpdate", _ONE_ARG); } override function draw() {