diff --git a/source/funkin/backend/scripting/GlobalScript.hx b/source/funkin/backend/scripting/GlobalScript.hx index 9c0656192..5c2365c73 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 c3656478c..6bb7300b0 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(); @@ -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,9 +45,9 @@ 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]; + __importedPaths = [path => true]; interp.errorHandler = _errorHandler; interp.warnHandler = _warnHandler; @@ -53,7 +69,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 +90,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 +108,7 @@ class HScript extends Script { if (expr != null) { @:privateAccess interp.exprReturn(expr); - __importedPaths.push(p); + __importedPaths.set(p, true); } return true; } @@ -158,15 +174,17 @@ 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; + returnParser(parser); onCreate(path); - for(k=>e in Script.getDefaultVariables(this)) + for(k=>e in defaultVars) set(k, e); load(); @@ -180,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; } @@ -195,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) { @@ -208,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 517dc96e4..bc2bb5e2c 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 4a03275f2..037609d7d 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) */ @@ -326,7 +343,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; diff --git a/source/funkin/backend/scripting/ScriptPack.hx b/source/funkin/backend/scripting/ScriptPack.hx index dfd8e4919..955869120 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; diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 07d743389..8d28f3453 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 _ONE_ARG:Array = [null]; + @: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]); + _ONE_ARG[0] = elapsed; + scripts.call("update", _ONE_ARG); if (inCutscene) { super.update(elapsed); - scripts.call("postUpdate", [elapsed]); + scripts.call("postUpdate", _ONE_ARG); return; } @@ -1499,7 +1502,7 @@ class PlayState extends MusicBeatState super.update(elapsed); - scripts.call("postUpdate", [elapsed]); + scripts.call("postUpdate", _ONE_ARG); } override function draw() {