diff --git a/MinecraftClient/Pathing/Execution/PathSegmentManager.cs b/MinecraftClient/Pathing/Execution/PathSegmentManager.cs index f36a0247..d64052d4 100644 --- a/MinecraftClient/Pathing/Execution/PathSegmentManager.cs +++ b/MinecraftClient/Pathing/Execution/PathSegmentManager.cs @@ -58,6 +58,22 @@ namespace MinecraftClient.Pathing.Execution private int _lastObservedSegmentIndex = -1; private int _ticksSinceSegmentStart; + // Diagnostic emission runs on the thread pool to keep the 20 TPS tick + // unblocked. Each batch is appended to a single chained Task so that + // line ordering is preserved across batches, even when the same tick + // produces both a slow-segment dump and the next seg-> header. Without + // the chain, multiple Task.Run calls could interleave and mangle the + // log output. + // + // Without this offload, a 200-line failure dump issued synchronously + // through ConsoleIO.WriteLogLine -> file logger took 200-500 ms on + // the main tick. The stalled tick stops position packets so the + // server view freezes, then snaps forward when the tick resumes - + // exactly the "freeze, jump, freeze" the user reported when /pathdiag + // was on. + private Task _diagFlushTail = Task.CompletedTask; + private readonly object _diagFlushLock = new(); + public bool IsNavigating => (_executor is not null && !_executor.IsComplete) || _nextExecutor is not null @@ -203,24 +219,29 @@ namespace MinecraftClient.Pathing.Execution if (_lastObservedSegmentIndex >= 0 && _ticksSinceSegmentStart >= SlowSegmentDumpTickThreshold) { - _infoLog?.Invoke( - $"[PathDiag] Slow segment {_lastObservedSegmentIndex}/{_executor.TotalSegments} took {_ticksSinceSegmentStart} ticks, dumping last {Math.Min(_diagnosticsTail.Count, _ticksSinceSegmentStart)} ticks:"); int toDump = Math.Min(_diagnosticsTail.Count, _ticksSinceSegmentStart); int skipCount = _diagnosticsTail.Count - toDump; + var batch = new List(toDump + 1) + { + $"[PathDiag] Slow segment {_lastObservedSegmentIndex}/{_executor.TotalSegments} took {_ticksSinceSegmentStart} ticks, dumping last {toDump} ticks:" + }; int i = 0; foreach (string line in _diagnosticsTail) { if (i++ < skipCount) continue; - _infoLog?.Invoke($"[PathDiag] t-{toDump - (i - skipCount)}: {line}"); + batch.Add($"[PathDiag] t-{toDump - (i - skipCount)}: {line}"); } + DispatchDiagnosticsBatch(batch); } _lastObservedSegmentIndex = segIdx; _ticksSinceSegmentStart = 0; - _infoLog?.Invoke( - $"[PathDiag] seg->{segIdx}/{_executor.TotalSegments} pos=({pos.X:F2},{pos.Y:F2},{pos.Z:F2}) yaw={physics.Yaw:F1} vy={physics.DeltaMovement.Y:F3} og={physics.OnGround} " + - (seg is null ? "none" : $"{seg.MoveType} ({seg.Start.X:F1},{seg.Start.Y:F1},{seg.Start.Z:F1})->({seg.End.X:F1},{seg.End.Y:F1},{seg.End.Z:F1}) exit={seg.ExitTransition}")); + DispatchDiagnosticsBatch(new[] + { + $"[PathDiag] seg->{segIdx}/{_executor.TotalSegments} pos=({pos.X:F2},{pos.Y:F2},{pos.Z:F2}) yaw={physics.Yaw:F1} vy={physics.DeltaMovement.Y:F3} og={physics.OnGround} " + + (seg is null ? "none" : $"{seg.MoveType} ({seg.Start.X:F1},{seg.Start.Y:F1},{seg.Start.Z:F1})->({seg.End.X:F1},{seg.End.Y:F1},{seg.End.Z:F1}) exit={seg.ExitTransition}") + }); } else { @@ -239,28 +260,70 @@ namespace MinecraftClient.Pathing.Execution return; PathSegment? seg = _executor.CurrentSegment; int segIdx = _executor.CurrentIndex; - _infoLog?.Invoke($"[PathDiag] Failure context: pos=({pos.X:F2},{pos.Y:F2},{pos.Z:F2}) failingSeg={segIdx}/{_executor.TotalSegments} " + - (seg is null ? "seg=" : $"seg={seg.MoveType} ({seg.Start.X:F1},{seg.Start.Y:F1},{seg.Start.Z:F1})->({seg.End.X:F1},{seg.End.Y:F1},{seg.End.Z:F1}) exit={seg.ExitTransition}")); + var batch = new List(_diagnosticsTail.Count + 2) + { + $"[PathDiag] Failure context: pos=({pos.X:F2},{pos.Y:F2},{pos.Z:F2}) failingSeg={segIdx}/{_executor.TotalSegments} " + + (seg is null ? "seg=" : $"seg={seg.MoveType} ({seg.Start.X:F1},{seg.Start.Y:F1},{seg.Start.Z:F1})->({seg.End.X:F1},{seg.End.Y:F1},{seg.End.Z:F1}) exit={seg.ExitTransition}") + }; if (_diagnosticsTail.Count > 0) { - _infoLog?.Invoke($"[PathDiag] Recent tick trace (last {_diagnosticsTail.Count}):"); + batch.Add($"[PathDiag] Recent tick trace (last {_diagnosticsTail.Count}):"); int i = 0; + int total = _diagnosticsTail.Count; foreach (string line in _diagnosticsTail) - _infoLog?.Invoke($"[PathDiag] t-{_diagnosticsTail.Count - i++ - 1}: {line}"); + batch.Add($"[PathDiag] t-{total - i++ - 1}: {line}"); } + DispatchDiagnosticsBatch(batch); } private void EmitPathDumpDiagnostics(string label, PathResult result, int startIdx = 0) { if (!DiagnosticsEnabled) return; - _infoLog?.Invoke($"[PathDiag] {label}: {result.Path.Count} waypoints, status={result.Status}, nodes={result.NodesExplored}, time={result.ElapsedMs}ms"); int count = result.Path.Count; + var batch = new List(count + 1) + { + $"[PathDiag] {label}: {count} waypoints, status={result.Status}, nodes={result.NodesExplored}, time={result.ElapsedMs}ms" + }; for (int i = 0; i < count; i++) { var node = result.Path[i]; string move = i == 0 ? "Start" : node.MoveUsed.ToString(); - _infoLog?.Invoke($"[PathDiag] [{startIdx + i:D2}] {move,-22} ({node.X},{node.Y},{node.Z})"); + batch.Add($"[PathDiag] [{startIdx + i:D2}] {move,-22} ({node.X},{node.Y},{node.Z})"); + } + DispatchDiagnosticsBatch(batch); + } + + /// + /// Schedule a diagnostics line batch for emission on a background task, + /// chained behind any prior batch so output order is preserved. The + /// caller's snapshot is captured by reference; the input list MUST not + /// be mutated after dispatch. + /// + private void DispatchDiagnosticsBatch(IReadOnlyList lines) + { + Action? infoLog = _infoLog; + if (infoLog is null || lines.Count == 0) + return; + + lock (_diagFlushLock) + { + _diagFlushTail = _diagFlushTail.ContinueWith(_ => + { + for (int i = 0; i < lines.Count; i++) + { + try + { + infoLog(lines[i]); + } + catch + { + // Swallow logger faults so a downstream sink failure + // never tears down the chain (which would silently + // drop every subsequent diagnostics batch). + } + } + }, TaskScheduler.Default); } } diff --git a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs index e481e361..bf44286c 100644 --- a/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs +++ b/MinecraftClient/Pathing/Execution/Templates/DescendTemplate.cs @@ -145,7 +145,24 @@ namespace MinecraftClient.Pathing.Execution.Templates double segmentYDrop = _segment.Start.Y - _segment.End.Y; bool isSingleStepDescend = segmentYDrop <= 1.0; bool footInsideTarget = TemplateFootingHelper.IsFootprintInsideTargetBlock(pos, ExpectedEnd); - bool biasTowardExitInAir = footInsideTarget + + // Long-descend lateral drift guard. When the bot's footprint + // enters the landing block at the very start of a multi-block + // fall (e.g. a 22-block water drop where target X/Z column + // matches the launch column), `biasTowardExitInAir` would + // immediately rotate yaw to the next segment's heading. With + // Forward held during the entire fall, the perpendicular air + // drift accumulates ~0.05 m/tick and over 20+ airborne ticks + // walks the bot a full block out of the landing column, so it + // misses the water/landing target and dies on the rim. Only + // permit exit-heading bias for non-single-step descends once + // the bot is within ~1.5 m of the landing Y (~3 ticks of + // free-fall), so any exit-heading drift cannot displace the + // landing footprint by more than a fraction of a block. + double remainingFallY = pos.Y - _segment.End.Y; + bool nearLanding = remainingFallY <= 1.5; + + bool biasTowardExitInAir = (footInsideTarget && (isSingleStepDescend || nearLanding)) || (isSingleStepDescend && (onOrPastTarget || (_hasFallen @@ -204,11 +221,25 @@ namespace MinecraftClient.Pathing.Execution.Templates // release forward input so sprint momentum decays // via air drag over the final 1-2 ticks of fall, // pulling the bot back into the landing column. + // + // The same guard applies to long water/landing + // drops with any non-PrepareJump exit. A 22-block + // fall lasts 20+ airborne ticks; at ~0.2 m/tick + // peak air-control velocity, holding Forward for + // the entire fall accumulates 4+ m of horizontal + // drift past the start ledge and the bot lands + // outside the 1x1 water column. Once the + // footprint is inside the target column, brake + // horizontal velocity so the bot falls straight + // down into the water/landing block. bool riskyOvershoot = _hasFallen && segmentYDrop >= 2.0 && onOrPastTarget && _segment.ExitTransition == PathTransitionType.PrepareJump; - if (riskyOvershoot) + bool longFallFootprintLanding = _hasFallen + && segmentYDrop >= 2.0 + && footInsideTarget; + if (riskyOvershoot || longFallFootprintLanding) { input.Forward = false; input.Sprint = false;