Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion source/funkin/backend/scripting/GlobalScript.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
48 changes: 37 additions & 11 deletions source/funkin/backend/scripting/HScript.hx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class HScript extends Script {
public var expr:Expr;
public var code:String = null;
//public var folderlessPath:String;
var __importedPaths:Array<String>;
var __importedPaths:Map<String, Bool>;

public static function initParser() {
var parser = new Parser();
Expand All @@ -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<Parser> = [];

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);

Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -92,7 +108,7 @@ class HScript extends Script {
if (expr != null) {
@:privateAccess
interp.exprReturn(expr);
__importedPaths.push(p);
__importedPaths.set(p, true);
}
return true;
}
Expand Down Expand Up @@ -158,15 +174,17 @@ class HScript extends Script {

interp.allowStaticVariables = interp.allowPublicVariables = false;
var savedVariables:Map<String, Dynamic> = [];
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();
Expand All @@ -180,11 +198,10 @@ class HScript extends Script {

private override function onCall(funcName:String, parameters:Array<Dynamic>):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;
}
Expand All @@ -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) {
Expand All @@ -208,4 +227,11 @@ class HScript extends Script {
public override function setPublicMap(map:Map<String, Dynamic>) {
this.interp.publicVariables = map;
}

override public function destroy() {
returnParser(parser);
parser = null;
interp = null;
super.destroy();
}
}
10 changes: 6 additions & 4 deletions source/funkin/backend/scripting/MultiThreadedScript.hx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class MultiThreadedScript implements IFlxDestroyable implements IHScriptCustomBe
*/
public var script:Script;

private var __variables:Array<String>;
private var __variables:Map<String, Bool>;

/**
* Return value of the last call.
Expand Down Expand Up @@ -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);
Expand Down
23 changes: 20 additions & 3 deletions source/funkin/backend/scripting/Script.hx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ class Script extends FlxBasic implements IFlxDestroyable {
* Gets the default variables for a script.
*/
public static function getDefaultVariables(?script:Script):Map<String, Dynamic> {
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<String, Dynamic> = null;

private static function buildDefaultVariables():Map<String, Dynamic> {
return [
// Haxe related stuff
"Std" => Std,
Expand All @@ -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,
Expand Down Expand Up @@ -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<Dynamic> = [];

/**
* Script name (with extension)
*/
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion source/funkin/backend/scripting/ScriptPack.hx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class ScriptPack extends Script {
public var publicVariables:Map<String, Dynamic> = [];
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<Dynamic> = [null];

/**
* Loads all scripts in the pack.
**/
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 6 additions & 3 deletions source/funkin/game/PlayState.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Dynamic> = [null];

@:dox(hide)
var __vocalSyncTimer:Float = 1;

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -1499,7 +1502,7 @@ class PlayState extends MusicBeatState

super.update(elapsed);

scripts.call("postUpdate", [elapsed]);
scripts.call("postUpdate", _ONE_ARG);
}

override function draw() {
Expand Down