diff --git a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerMovementState.cs b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerMovementState.cs index 464f5b9a..0b55e575 100644 --- a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerMovementState.cs +++ b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerMovementState.cs @@ -449,6 +449,13 @@ public sealed class RuntimeLocalPlayerMovementState { _controller?.RetireRuntimePublication(); _controller = controller; + // The PRODUCTION install path must carry the same wiring the + // internal Controller setter applies: this line was missing, so + // every live graphical controller ran with a null OnInterfaceText + // and client-local refusals (jump-in-air) silently vanished — + // proven by the round-3 probe line + // "[jump] ReportJumpRefusal result=NotGrounded hasCallback=False". + controller.OnInterfaceText = _onInterfaceText; ControllerOwnershipEpoch++; Interlocked.Increment(ref _revision); } diff --git a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerMovementStateTests.cs b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerMovementStateTests.cs index f2723e9c..e64009b8 100644 --- a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerMovementStateTests.cs +++ b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeLocalPlayerMovementStateTests.cs @@ -172,6 +172,28 @@ public sealed class RuntimeLocalPlayerMovementStateTests Assert.Equal(AcDream.Core.Chat.RetailLogTextType.ClientLocal, received[0].Type); } + [Fact] + public void OnInterfaceText_CommitRuntimeOwnedController_CarriesTheCallback() + { + // The PRODUCTION install path (CommitRuntimeOwnedController via the + // publication lifecycle) bypasses the internal Controller setter. + // It shipped WITHOUT the OnInterfaceText application, so every live + // graphical controller ran with a null callback and client-local + // refusals silently vanished — round-3 probe evidence: + // "[jump] ReportJumpRefusal result=NotGrounded hasCallback=False". + using var movement = new RuntimeLocalPlayerMovementState(); + var received = new List(); + movement.OnInterfaceText = (text, _) => received.Add(text); + + var controller = new PlayerMovementController(new PhysicsEngine()); + movement.CommitRuntimeOwnedController(controller); + + Assert.NotNull(controller.OnInterfaceText); + controller.OnInterfaceText!( + "commit-path line", AcDream.Core.Chat.RetailLogTextType.ClientLocal); + Assert.Equal(["commit-path line"], received); + } + [Fact] public void OnInterfaceText_SetAfterControllerInstall_StillAppliesToTheCurrentController() {