Skip to content

Space may wrongly get unlocked during step() #246

@aatle

Description

@aatle

Space has a _locked attribute so that during a step, the add() and remove() methods may automatically delay until the step finishes.
This _locked attribute is being corrupted during the step which can lead to a Chipmunk error when using add or remove.

The _locked attribute is temporarily set to True for separate collision handlers as it may occur outside of a step if the colliding shapes are removed, but afterwards it is set back to False, even in the middle of a step where it should be True. So if a collision handler (that is not separate) adds or removes later in the same step, a Chipmunk error is raised that the operation is illegal. (This isn't much of a common scenario.)

pymunk/pymunk/_callbacks.py

Lines 220 to 227 in ca53085

try:
# this try is needed since a separate callback will be called
# if a colliding object is removed, regardless if its in a
# step or not.
handler._space._locked = True
handler._separate(Arbiter(_arb, handler._space), handler._space, handler.data)
finally:
handler._space._locked = False

The easy fix is to set the _locked attribute back to the original _locked value instead of False literal.

Minimal reproducible example:
import pymunk as pm


def get_locked(self):
    return self._locked_debug
def set_locked(self, value: bool):
    print("SET _locked", value)
    self._locked_debug = value

pm.Space._locked = property(get_locked, set_locked)  # type: ignore[assignment]


space = pm.Space()

space.gravity = 0, -100

body1 = pm.Body()
shape1 = pm.Circle(body1, 20)
shape1.density = 5
shape1.collision_type = 222

floor1 = pm.Segment(space.static_body, (-100,-30), (100,-30), 1)

body2 = pm.Body()
body2.position = 500,0
shape2 = pm.Circle(body2, 20)
shape2.density = 5
shape2.collision_type = 333

floor2 = pm.Segment(space.static_body, (400,-30), (600,-30), 1)

space.add(body1, shape1, body2, shape2, floor1, floor2)


separate_occurred = False

def separate(arbiter: pm.Arbiter, space: pm.Space, data):
    global separate_occurred
    separate_occurred = True
    print('SEPARATE')


def post_solve(arbiter: pm.Arbiter, space: pm.Space, data):
    print('POST SOLVE')
    if separate_occurred:
        print('ADDING/REMOVING')
        space.add(pm.Circle(space.static_body, 5))


space.add_wildcard_collision_handler(222).separate = separate
space.add_wildcard_collision_handler(333).post_solve = post_solve


for i in range(60):
    print('step START', i)
    space.step(1/60)
    print('step END', i)
    if i == 30:
        body1.apply_impulse_at_local_point((0,1000000))

(I was implementing a bullet that destroyed itself on impact, but it gave an error during removal, specifically when the bullet was shot point blank into an enemy. I also encountered a separate minor bug while debugging this that deserves a separate issue.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions