Repro
In an MCP eval (or any VM script):
Player.first.position = vec3(0, 1, 15)
The console echoes the new position immediately, but within one screenshot
cycle (~200ms) the player is back near its previous spot. Subsequent
screenshot_from_player shows the original view, not the view from
(0, 1, 15).
Working around the issue: the MCP set_position tool with the player's id
does stick. That goes through unit.transform = Transform.init(pos, yaw)
inside a smooth-move loop that re-asserts the transform every ~33ms.
Why I think it's a bug
vmlib/enu/base_api.nim:
proc `position=`*(self: Unit, position: Vector3) =
self.position_set(position)
src/controllers/script_controllers/host_bridge.nim:position_set writes
transform_value.origin. The change propagates via Ed → PlayerNode's
transform_value.watch (src/nodes/player_node.nim:179-181) which writes
self.transform = change.item on the KinematicBody. So far so good.
But PlayerNode.process (line 252-254) then runs move_and_slide —
applying velocity, gravity, and collision — and writes the post-physics
transform back into the model:
self.velocity = self.move_and_slide(velocity, UP)
self.model.transform = self.transform
So a scripted teleport lives for at most one physics tick before being
overwritten by whatever physics resolves the body to (gravity pulls it
toward the ground; collisions push it sideways). For a player standing
on the ground with no input, the next-frame transform is essentially
the previous position again.
This is surprising because position= is a public, documented setter
that names what it does. Users (and AI clients) reasonably expect a
teleport to stick until they move again.
Suggested fix
A teleport via position= should reset velocity and not be clobbered by
the same-frame physics step. A few options, roughly in order of
disruption:
- Inside
position_set, also zero velocity_value when the unit is a
Player. Smallest change; covers gravity-driven drift but not collision
resolution if the new position is inside geometry.
- Skip the
model.transform = self.transform write-back in
PlayerNode.process for one frame after a transform_value change
came in from the model side (i.e. a teleport).
- Or document the limitation and add a
Player.first.teleport_to(pos)
that handles both above.
Reproduced on main (commit 01c4041) with the MCP server.
Repro
In an MCP
eval(or any VM script):The console echoes the new position immediately, but within one screenshot
cycle (~200ms) the player is back near its previous spot. Subsequent
screenshot_from_playershows the original view, not the view from(0, 1, 15).
Working around the issue: the MCP
set_positiontool with the player's iddoes stick. That goes through
unit.transform = Transform.init(pos, yaw)inside a smooth-move loop that re-asserts the transform every ~33ms.
Why I think it's a bug
vmlib/enu/base_api.nim:src/controllers/script_controllers/host_bridge.nim:position_setwritestransform_value.origin. The change propagates via Ed →PlayerNode'stransform_value.watch(src/nodes/player_node.nim:179-181) which writesself.transform = change.itemon theKinematicBody. So far so good.But
PlayerNode.process(line 252-254) then runsmove_and_slide—applying velocity, gravity, and collision — and writes the post-physics
transform back into the model:
So a scripted teleport lives for at most one physics tick before being
overwritten by whatever physics resolves the body to (gravity pulls it
toward the ground; collisions push it sideways). For a player standing
on the ground with no input, the next-frame transform is essentially
the previous position again.
This is surprising because
position=is a public, documented setterthat names what it does. Users (and AI clients) reasonably expect a
teleport to stick until they move again.
Suggested fix
A teleport via
position=should reset velocity and not be clobbered bythe same-frame physics step. A few options, roughly in order of
disruption:
position_set, also zerovelocity_valuewhen the unit is aPlayer. Smallest change; covers gravity-driven drift but not collision
resolution if the new position is inside geometry.
model.transform = self.transformwrite-back inPlayerNode.processfor one frame after atransform_valuechangecame in from the model side (i.e. a teleport).
Player.first.teleport_to(pos)that handles both above.
Reproduced on
main(commit 01c4041) with the MCP server.