extends RefCounted # Local-ship reconciliation policy (MULTIPLAYER_SPEC.md §4.4). Kept out of # NetworkedMatch so the decision table is pure-testable; the imperative half # only writes Ship's existing Jolt-safe queued correction hooks. const DEFAULT_HARD_POSITION_ERROR := 2.0 const DEFAULT_HARD_ROTATION_ERROR_DEGREES := 60.0 const DEFAULT_MAX_VISUAL_OFFSET := 0.4 const METRIC_SAMPLE_CAPACITY := 3600 # one minute at the 60Hz snapshot rate const NetBodyState = preload("res://scripts/net_body_state.gd") const LocalPredictionHistory = preload("res://scripts/local_prediction_history.gd") var _last_reset_gen := -1 # first snapshot establishes baseline, never resets var _position_errors: Array[float] = [] var _rotation_errors: Array[float] = [] var _free_flight_position_errors: Array[float] = [] var _free_flight_rotation_errors: Array[float] = [] var _visual_correction_errors: Array[float] = [] var _free_flight_visual_correction_errors: Array[float] = [] var _hard_snap_count := 0 var _decision_count := 0 var _resync_until_seq := -1 var _metrics_started_ms := -1 var hard_position_error := DEFAULT_HARD_POSITION_ERROR var hard_rotation_error_degrees := DEFAULT_HARD_ROTATION_ERROR_DEGREES var max_visual_offset := DEFAULT_MAX_VISUAL_OFFSET var _hard_snap_reasons := {} var _hard_snap_cohorts := {"free_flight": 0, "contact": 0, "reset": 0, "resync": 0, "unsimulated": 0} var _cohort_counts := {"free_flight": 0, "contact": 0, "reset": 0, "resync": 0, "unsimulated": 0} static func decide(comparison: Dictionary, local_frozen: bool, reset_changed: bool, position_threshold: float = DEFAULT_HARD_POSITION_ERROR, rotation_threshold_degrees: float = DEFAULT_HARD_ROTATION_ERROR_DEGREES) -> Dictionary: var authoritative: NetBodyState = comparison.get("authoritative_state", null) if reset_changed: return {"mode": "hard", "reason": "reset_gen"} # An attack's skipped sequence is issued, sent, and acknowledged, but never # locally simulated — there is no predicted state to compare and nothing is # wrong. It is not history loss and must not teleport the ship or arm resync # suppression: the lead controller produces these during ordinary play, and # treating them as missing history cost several unnecessary hard snaps a # minute. Skip the acknowledgement; the next simulated sequence (at most a # tick or two later, since the server consumes one per tick) reconciles # normally against real data. if comparison.get("status", "") == "unsimulated_gap": return {"mode": "skip", "reason": "unsimulated_gap"} if comparison.get("status", "") == "warmup_not_recorded": # Sequence acknowledgements that predate the first local post-step state # are expected during startup. The initial snapshot already placed the # body, so there is no correction to apply and no resync to arm. return {"mode": "skip", "reason": "warmup_not_recorded"} if comparison.get("status", "missing_not_recorded") != "matched": return {"mode": "hard", "reason": comparison.get("status", "missing")} if authoritative == null or authoritative.frozen != local_frozen: return {"mode": "hard", "reason": "frozen_mismatch"} if float(comparison["position_error_magnitude"]) > position_threshold: return {"mode": "hard", "reason": "position_error"} if float(comparison["rotation_error_degrees"]) > rotation_threshold_degrees: return {"mode": "hard", "reason": "rotation_error"} return {"mode": "soft", "reason": "within_thresholds"} static func soft_corrected_transform(current_transform: Transform3D, comparison: Dictionary) -> Transform3D: var authoritative: NetBodyState = comparison["authoritative_state"] var predicted: NetBodyState = comparison["predicted_state"] var position_delta: Vector3 = authoritative.position - predicted.position var rotation_delta := Basis(authoritative.rotation.normalized()) * Basis(predicted.rotation.normalized()).inverse() return Transform3D( (rotation_delta * current_transform.basis).orthonormalized(), current_transform.origin + position_delta ) func reconcile(comparison: Dictionary, ship: Ship, reset_gen: int, current_seq: int, history: LocalPredictionHistory) -> Dictionary: var reset_changed := _last_reset_gen != -1 and reset_gen != _last_reset_gen _last_reset_gen = reset_gen var comparison_seq := int(comparison.get("seq", -1)) # A reset is an epoch boundary, never ordinary stale traffic. It must # preempt an outstanding missing-history suppression or the first reset # snapshot could be discarded and every later snapshot share its generation. if reset_changed: _resync_until_seq = -1 var reset_decision := decide(comparison, ship.freeze, true, hard_position_error, hard_rotation_error_degrees) _record_metrics(comparison, reset_decision) var reset_authority: NetBodyState = comparison.get("authoritative_state", null) if reset_authority != null: ship.queue_teleport_with_velocity(Transform3D(Basis(reset_authority.rotation), reset_authority.position), reset_authority.linear_velocity, reset_authority.angular_velocity) ship.net_visual_offset = Vector3.ZERO ship.net_visual_rotation_offset = Quaternion.IDENTITY if is_instance_valid(ship.visual): ship.visual.position = Vector3.ZERO ship.visual.basis = Basis.IDENTITY _resync_until_seq = current_seq + 1 return reset_decision if _resync_until_seq >= 0: if comparison.get("status", "") == "matched" and comparison_seq >= _resync_until_seq: _resync_until_seq = -1 else: return {"mode": "suppressed", "reason": "awaiting_resync"} var decision := decide(comparison, ship.freeze, false, hard_position_error, hard_rotation_error_degrees) _record_metrics(comparison, decision) if decision["mode"] == "skip": # Deliberately before the authority write below: a skipped acknowledgement # leaves the body, the visual offset and _resync_until_seq exactly as they # were. Nothing about this sequence is unhealthy, so nothing is corrected # and nothing is suppressed. return decision var authoritative: NetBodyState = comparison.get("authoritative_state", null) if authoritative == null: return decision if comparison.get("status", "") == "matched" and decision["reason"] != "reset_gen": # Transport the same-sequence authority error through current Jolt state # and retained predictions. This deliberately avoids fake single-body # replay, which cannot reproduce contact impulses/friction. var predicted: NetBodyState = comparison["predicted_state"] var position_delta: Vector3 = comparison["position_error"] var velocity_error: Vector3 = comparison["linear_velocity_error"] var angular_velocity_error: Vector3 = comparison["angular_velocity_error"] var rotation_delta := (authoritative.rotation.normalized() * predicted.rotation.normalized().inverse()).normalized() history.overwrite_state(int(comparison["seq"]), authoritative) history.rebase_state_range(int(comparison["seq"]) + 1, current_seq, position_delta, rotation_delta, velocity_error, angular_velocity_error) var old_basis := ship.global_transform.basis var corrected_transform := soft_corrected_transform(ship.global_transform, comparison) # Apply both velocity deltas to the live body atomically with pose. The # same deltas are transported through retained history above. ship.queue_teleport_with_velocity(corrected_transform, ship.linear_velocity + velocity_error, ship.angular_velocity + angular_velocity_error) var position_error: Vector3 = comparison["position_error"] if decision["mode"] == "soft": ship.net_visual_offset = (ship.global_transform.basis.inverse() * -position_error).limit_length(max_visual_offset) # The body rotates in world space. Convert the inverse correction to # the child visual's local basis so its global orientation is preserved # through the physical correction (B_old^-1 Δ^-1 B_old). var local_visual_delta: Basis = old_basis.inverse() * Basis(rotation_delta.inverse()) * old_basis ship.net_visual_rotation_offset = local_visual_delta.get_rotation_quaternion() * ship.net_visual_rotation_offset else: ship.net_visual_offset = Vector3.ZERO ship.net_visual_rotation_offset = Quaternion.IDENTITY if is_instance_valid(ship.visual): ship.visual.position = Vector3.ZERO ship.visual.basis = Basis.IDENTITY else: # Reset/missing state has no trustworthy delta. Place authority once; # callers must wait for a new matched history entry before correction. ship.queue_teleport_with_velocity(Transform3D(Basis(authoritative.rotation), authoritative.position), authoritative.linear_velocity, authoritative.angular_velocity) ship.net_visual_offset = Vector3.ZERO ship.net_visual_rotation_offset = Quaternion.IDENTITY if is_instance_valid(ship.visual): ship.visual.position = Vector3.ZERO ship.visual.basis = Basis.IDENTITY # Retain no fabricated future. Once local input history contains a # newly acknowledged sequence, normal delta reconciliation resumes. _resync_until_seq = current_seq + 1 return decision func get_metrics() -> Dictionary: return { "sample_count": _position_errors.size(), "position_error_p50": _percentile(0.50), "position_error_p95": _percentile(0.95), "position_error_p99": _percentile(0.99), "rotation_error_p50": _rotation_percentile(0.50), "rotation_error_p95": _rotation_percentile(0.95), "rotation_error_p99": _rotation_percentile(0.99), "free_flight_sample_count": _free_flight_position_errors.size(), "free_flight_position_error_p95": _percentile_from(_free_flight_position_errors, 0.95), "free_flight_position_error_p99": _percentile_from(_free_flight_position_errors, 0.99), "free_flight_rotation_error_p95": _percentile_from(_free_flight_rotation_errors, 0.95), "free_flight_rotation_error_p99": _percentile_from(_free_flight_rotation_errors, 0.99), "visual_correction_p95": _percentile_from(_visual_correction_errors, 0.95), "visual_correction_p99": _percentile_from(_visual_correction_errors, 0.99), "free_flight_visual_correction_p95": _percentile_from(_free_flight_visual_correction_errors, 0.95), "free_flight_visual_correction_p99": _percentile_from(_free_flight_visual_correction_errors, 0.99), "hard_snap_count": _hard_snap_count, "hard_snap_rate_per_min": _hard_snap_rate_per_min(), "hard_snap_reasons": _hard_snap_reasons.duplicate(), "hard_snap_cohorts": _hard_snap_cohorts.duplicate(), "cohorts": _cohort_counts.duplicate(), "position_threshold": hard_position_error, "rotation_threshold_degrees": hard_rotation_error_degrees, "max_visual_offset": max_visual_offset, } func clear_metrics() -> void: _position_errors.clear() _rotation_errors.clear() _free_flight_position_errors.clear() _free_flight_rotation_errors.clear() _visual_correction_errors.clear() _free_flight_visual_correction_errors.clear() _hard_snap_count = 0 _decision_count = 0 _resync_until_seq = -1 _metrics_started_ms = -1 _hard_snap_reasons.clear() _hard_snap_cohorts = {"free_flight": 0, "contact": 0, "reset": 0, "resync": 0, "unsimulated": 0} _cohort_counts = {"free_flight": 0, "contact": 0, "reset": 0, "resync": 0, "unsimulated": 0} func _record_metrics(comparison: Dictionary, decision: Dictionary) -> void: if _metrics_started_ms < 0: _metrics_started_ms = Time.get_ticks_msec() _decision_count += 1 var cohort := _cohort_for(comparison, decision) if decision["mode"] == "hard": _hard_snap_count += 1 var reason := str(decision.get("reason", "unknown")) _hard_snap_reasons[reason] = int(_hard_snap_reasons.get(reason, 0)) + 1 _hard_snap_cohorts[cohort] = int(_hard_snap_cohorts.get(cohort, 0)) + 1 _cohort_counts[cohort] = int(_cohort_counts.get(cohort, 0)) + 1 if comparison.get("status", "") == "matched": # Only same-sequence predictions are quality samples. Recovery events # still count in their own cohorts/reason ledger, but must not distort # p95/p99 with an error that cannot honestly be measured. _position_errors.append(float(comparison["position_error_magnitude"])) _rotation_errors.append(float(comparison.get("rotation_error_degrees", 0.0))) if cohort == "free_flight": _free_flight_position_errors.append(float(comparison["position_error_magnitude"])) _free_flight_rotation_errors.append(float(comparison.get("rotation_error_degrees", 0.0))) # The visual offset hides at most max_visual_offset of a soft correction. # Record the exposed remainder, never the capped hidden component; hard # corrections are independently gated by their cohort count above. var visual_error := maxf(0.0, float(comparison.get("position_error_magnitude", 0.0)) - max_visual_offset) if decision["mode"] == "soft" else 0.0 _visual_correction_errors.append(visual_error) if cohort == "free_flight": _free_flight_visual_correction_errors.append(visual_error) if _position_errors.size() > METRIC_SAMPLE_CAPACITY: _position_errors.pop_front() if _rotation_errors.size() > METRIC_SAMPLE_CAPACITY: _rotation_errors.pop_front() if _free_flight_position_errors.size() > METRIC_SAMPLE_CAPACITY: _free_flight_position_errors.pop_front() if _free_flight_rotation_errors.size() > METRIC_SAMPLE_CAPACITY: _free_flight_rotation_errors.pop_front() if _visual_correction_errors.size() > METRIC_SAMPLE_CAPACITY: _visual_correction_errors.pop_front() if _free_flight_visual_correction_errors.size() > METRIC_SAMPLE_CAPACITY: _free_flight_visual_correction_errors.pop_front() func _cohort_for(comparison: Dictionary, decision: Dictionary) -> String: if decision.get("reason", "") == "reset_gen": return "reset" if decision.get("reason", "") == "unsimulated_gap": # Its own cohort, not free_flight: these carry no error sample, and # folding them into a quality cohort would silently inflate its count # with rows that contributed no measurement. return "unsimulated" if decision.get("reason", "").begins_with("missing") or _resync_until_seq >= 0: return "resync" if comparison.get("contact_window", false): return "contact" return "free_flight" func _hard_snap_rate_per_min() -> float: if _metrics_started_ms < 0: return 0.0 var elapsed_seconds := maxf(float(Time.get_ticks_msec() - _metrics_started_ms) / 1000.0, 0.001) return float(_hard_snap_count) * 60.0 / elapsed_seconds func _percentile(fraction: float) -> float: return _percentile_from(_position_errors, fraction) func _percentile_from(samples: Array[float], fraction: float) -> float: if samples.is_empty(): return 0.0 var sorted := samples.duplicate() sorted.sort() var index := clampi(roundi((sorted.size() - 1) * fraction), 0, sorted.size() - 1) return sorted[index] func _rotation_percentile(fraction: float) -> float: return _percentile_from(_rotation_errors, fraction)