godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.6k stars 21.28k forks source link

Anonymous lambdas can be randomly assigned a new context object if the original context object is missing at the time of invocation #98673

Open MajorMcDoom opened 1 month ago

MajorMcDoom commented 1 month ago

Tested versions

Reproducible in 4.3.stable

System information

Godot v4.3.stable - Windows 10.0.22631 - Vulkan (Forward+) - dedicated NVIDIA GeForce GTX 1660 SUPER (NVIDIA; 32.0.15.6590) - Intel(R) Core(TM) i7-10700F CPU @ 2.90GHz (16 Threads)

Issue description

When you make an anonymous lambda that contains a reference to a local variable or function in the current script where the lambda is defined, and then this lambda is later called when the current node is missing, Godot can (unpredictably) bind this lambda to a completely different node.

In the case that the new node does not have the same members, it will throw an error like in the screenshot below, which is super confusing. But an even more dangerous case I have encountered is if the new node does share the same members being referenced in the lambda (i.e. position or name). In this case, the lambda will silently proceed with the wrong target node as though nothing is wrong, leading to a silent bug that can be almost impossible to diagnose.

In the screenshot below, the story.gd script attached to the story node is where the error occurs. The lambda being constructed contains a call to a function _perform_place_transition which is local to story.gd. However, at runtime, the lambda construction fails. The error message and the self field in the debugger window both say that the error is occuring on a completely different node in the scene - the tf_mirror node under the right eye of a penguin.

Godot will randomly re-bind the lambda to one of a few other nodes, or it will not re-bind at all. There appears to be no pattern as to how it chooses how/whether to re-bind.

38cc0f5f-5187-4ee0-a291-1177b9d19055

The problem line of code: Game.loader.queue_load_op(func(): await _perform_place_transition(place_name, spawn_point_name)) The rebinding bug still occurs if the lambda does not contain an await: Game.loader.queue_load_op(func(): _perform_place_transition(place_name, spawn_point_name)) The rebinding bug does NOT occur if an anonymous lambda is not used: Game.loader.queue_load_op(_perform_place_transition.bind(place_name, spawn_point_name))

Just to be clear: It is a user bug to allow an anonymous lambda to be invoked on a no-longer-valid object. The bug being reported here is the random re-binding being done by Godot.

Steps to reproduce

I can't share my entire commercial project, and I have attempted but not succeeded in making a reduced or fresh repro project.

Minimal reproduction project (MRP)

N/A

MajorMcDoom commented 1 month ago

I may be misusing the term "binding" here, but what I mean is the reassignment of the lambda's contextual object.