diff --git a/Source/Addons/RaycastManager.cs b/Source/Addons/RaycastManager.cs new file mode 100644 index 0000000..a6f14ff --- /dev/null +++ b/Source/Addons/RaycastManager.cs @@ -0,0 +1,241 @@ + + +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Jobs; +using UnityEngine; +using UnityEngine.Jobs; + +namespace SystemHeat.Addons; + +[DefaultExecutionOrder(-1)] +[KSPAddon(KSPAddon.Startup.AllGameScenes, once: false)] +internal class RaycastManager : MonoBehaviour +{ + public static RaycastManager Instance { get; private set; } + + struct RaycastParams + { + public float range; + public int layerMask; + } + + const int DefaultCap = 128; + + private TransformAccessArray transforms; + private NativeArray args; + private NativeArray hits; + private readonly Dictionary mapping = []; + private readonly Dictionary reverse = []; + private JobHandle handle = default; + + public void Register(PartModule module, Transform transform, float range, int layerMask = -5) + { + if (module == null || transform == null || this == null) + return; + + handle.Complete(); + + if (!enabled) + enabled = true; + + var moduleID = module.GetInstanceID(); + var arg = new RaycastParams + { + range = range, + layerMask = layerMask + }; + + // If the module is already registered then this overrides its existing + // raycast request. + if (mapping.TryGetValue(moduleID, out int index)) + { + transforms[index] = transform; + args[index] = arg; + return; + } + + EnsureCapacity(); + + index = transforms.length; + transforms.Add(transform); + args[index] = arg; + + mapping.Add(moduleID, index); + reverse.Add(index, moduleID); + } + + public void Unregister(PartModule module) + { + if (module == null) + return; + + handle.Complete(); + + var moduleID = module.GetInstanceID(); + if (!mapping.TryGetValue(moduleID, out var index)) + return; + + mapping.Remove(moduleID); + reverse.Remove(index); + transforms.RemoveAtSwapBack(index); + + int last = transforms.length; + if (reverse.TryGetValue(last, out var swappedID)) + { + args[index] = args[last]; + if (hits.IsCreated && last < hits.Length) + hits[index] = hits[last]; + + mapping[swappedID] = index; + reverse.Remove(last); + reverse[index] = swappedID; + } + } + + /// + /// Get the precomputed raycast hit for . + /// + /// + /// null if there is no hit, and the hit otherwise. + /// + /// will be null if the raycast did + /// not hit anything. + /// + public RaycastHit? GetRaycastHit(PartModule module) + { + var moduleID = module.GetInstanceID(); + if (!mapping.TryGetValue(moduleID, out var index)) + return null; + + if ((uint)index >= (uint)hits.Length) + return null; + + if (!handle.IsCompleted) + handle.Complete(); + + return hits[index]; + } + + void EnsureCapacity() + { + int length = transforms.length; + if (length < transforms.capacity) + return; + + var newcap = Math.Max(length * 2, 128); + var nt = new TransformAccessArray(newcap, desiredJobCount: 1); + var na = new NativeArray(newcap, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); + + for (int i = 0; i < length; ++i) + nt.Add(transforms[i]); + NativeArray.Copy(args, na, length); + + transforms.Dispose(); + args.Dispose(); + + transforms = nt; + args = na; + } + + void Awake() + { + Instance = this; + } + + void OnDestroy() + { + if (Instance == this) + Instance = null; + } + + void OnEnable() + { + transforms = new TransformAccessArray(DefaultCap, desiredJobCount: 1); + args = new NativeArray(DefaultCap, Allocator.Persistent); + } + + void OnDisable() + { + try + { + if (!handle.IsCompleted) + handle.Complete(); + } + catch (Exception e) + { + Debug.LogException(e); + } + + transforms.Dispose(); + args.Dispose(); + if (hits.IsCreated) + hits.Dispose(); + + transforms = default; + args = default; + hits = default; + + mapping.Clear(); + reverse.Clear(); + } + + void FixedUpdate() + { + if (!handle.IsCompleted) + handle.Complete(); + + var count = mapping.Count; + if (count == 0) + { + handle = default; + return; + } + + var commands = new NativeArray(count, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + hits = new NativeArray(count, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + + // RaycastCommand.ScheduleBatch reads maxHits immediately but the remaining + // fields are not read until the job runs. So we need to initialize maxHits + // immediately but the rest can happen in a job. + for (int i = 0; i < count; ++i) + commands[i] = new() { maxHits = 1 }; + + handle = new BuildCommandJob { args = args, commands = commands } + .Schedule(transforms, handle); + handle = RaycastCommand.ScheduleBatch(commands, hits, 64, handle); + JobHandle.ScheduleBatchedJobs(); + } + + void Update() + { + handle.Complete(); + + if (mapping.Count == 0) + enabled = false; + } + + struct BuildCommandJob : IJobParallelForTransform + { + [ReadOnly] + public NativeArray args; + [WriteOnly] + public NativeArray commands; + + public void Execute(int index, TransformAccess transform) + { + var arg = args[index]; + var command = new RaycastCommand + { + from = transform.position, + direction = transform.rotation * Vector3.forward, + distance = arg.range, + layerMask = arg.layerMask, + maxHits = 1 + }; + + commands[index] = command; + } + } +} \ No newline at end of file diff --git a/Source/Modules/ModuleSystemHeatAsteroidHarvester.cs b/Source/Modules/ModuleSystemHeatAsteroidHarvester.cs index 69bf931..ef9b38c 100644 --- a/Source/Modules/ModuleSystemHeatAsteroidHarvester.cs +++ b/Source/Modules/ModuleSystemHeatAsteroidHarvester.cs @@ -1,5 +1,6 @@ using UnityEngine; using KSP.Localization; +using SystemHeat.Addons; using Unity.Profiling; namespace SystemHeat @@ -39,6 +40,9 @@ public class ModuleSystemHeatAsteroidHarvester : ModuleAsteroidDrill protected ModuleSystemHeat heatModule; + // Stock ModuleAsteroidDrill uses Physics.DefaultRaycastLayers (no mask). + private const int ImpactLayerMask = -5; + private static readonly ProfilerMarker BaseFixedUpdateMarker = new("ModuleAsteroidDrill.FixedUpdate"); public override string GetInfo() @@ -63,6 +67,13 @@ public void Start() Utils.Log("[ModuleSystemHeatAsteroidHarvester] Setup completed", LogType.Modules); Fields["HarvesterEfficiency"].guiName = Localizer.Format("#LOC_SystemHeat_ModuleSystemHeatHarvester_Field_Efficiency", ConverterName); + + RegisterImpactRaycast(); + } + + void OnEnable() + { + RegisterImpactRaycast(); } public override void FixedUpdate() @@ -92,6 +103,35 @@ void OnDisable() { heatModule?.AddFlux(moduleID, 0f, 0f, false); HarvesterEfficiency = "-"; + RaycastManager.Instance?.Unregister(this); + } + + void RegisterImpactRaycast() + { + if (!HighLogic.LoadedSceneIsFlight || impactTransformCache == null) + return; + RaycastManager.Instance?.Register(this, impactTransformCache, ImpactRange, ImpactLayerMask); + } + + protected override bool CheckForImpact() + { + if (string.IsNullOrEmpty(ImpactTransform) || impactTransformCache == null) + return true; + + var hit = RaycastManager.Instance?.GetRaycastHit(this); + if (hit is not RaycastHit raycastHit) + { + // If we're not registered for whatever reason then do the raycast ourselves. + var origin = impactTransformCache.position; + if (!Physics.Raycast(new Ray(origin, impactTransformCache.forward), out raycastHit, ImpactRange, ImpactLayerMask)) + return false; + } + + var collider = raycastHit.collider; + if (collider == null) + return false; + + return collider.gameObject.GetComponentUpwards() != null; } void FixedUpdateFlight() diff --git a/Source/Modules/ModuleSystemHeatCometHarvester.cs b/Source/Modules/ModuleSystemHeatCometHarvester.cs index 454a18e..cc8ed75 100644 --- a/Source/Modules/ModuleSystemHeatCometHarvester.cs +++ b/Source/Modules/ModuleSystemHeatCometHarvester.cs @@ -1,5 +1,7 @@ using KSP.Localization; +using SystemHeat.Addons; using Unity.Profiling; +using UnityEngine; namespace SystemHeat { @@ -38,6 +40,9 @@ public class ModuleSystemHeatCometHarvester : ModuleCometDrill protected ModuleSystemHeat heatModule; + // Stock ModuleCometDrill uses Physics.DefaultRaycastLayers (no mask). + private const int ImpactLayerMask = -5; + private static readonly ProfilerMarker BaseFixedUpdateMarker = new("ModuleCometDrill.FixedUpdate"); public override string GetInfo() @@ -62,6 +67,13 @@ public void Start() Utils.Log("[ModuleSystemHeatCometHarvester] Setup completed", LogType.Modules); Fields["HarvesterEfficiency"].guiName = Localizer.Format("#LOC_SystemHeat_ModuleSystemHeatHarvester_Field_Efficiency", ConverterName); + + RegisterImpactRaycast(); + } + + void OnEnable() + { + RegisterImpactRaycast(); } public override void FixedUpdate() @@ -91,6 +103,40 @@ void OnDisable() { heatModule?.AddFlux(moduleID, 0f, 0f, false); HarvesterEfficiency = "-"; + RaycastManager.Instance?.Unregister(this); + } + + void RegisterImpactRaycast() + { + if (!HighLogic.LoadedSceneIsFlight || impactTransformCache == null) + return; + RaycastManager.Instance?.Register(this, impactTransformCache, ImpactRange, ImpactLayerMask); + } + + protected override bool CheckForImpact() + { + if (string.IsNullOrEmpty(ImpactTransform) || impactTransformCache == null) + return true; + + Collider collider; + var hit = RaycastManager.Instance?.GetRaycastHit(this); + if (hit != null) + { + collider = hit.Value.collider; + } + else + { + // If we're not registered for whatever reason then do the raycast ourselves. + var origin = impactTransformCache.position; + if (!Physics.Raycast(new Ray(origin, impactTransformCache.forward), out var fallback, ImpactRange, ImpactLayerMask)) + return false; + collider = fallback.collider; + } + + if (collider == null) + return false; + + return collider.gameObject.GetComponentUpwards() != null; } void FixedUpdateFlight() diff --git a/Source/Modules/ModuleSystemHeatHarvester.cs b/Source/Modules/ModuleSystemHeatHarvester.cs index 42d40ee..e986827 100644 --- a/Source/Modules/ModuleSystemHeatHarvester.cs +++ b/Source/Modules/ModuleSystemHeatHarvester.cs @@ -4,6 +4,7 @@ using System.Text; using UnityEngine; using KSP.Localization; +using SystemHeat.Addons; using Unity.Profiling; namespace SystemHeat @@ -52,6 +53,9 @@ public void ToggleEditorThermalSim() protected ModuleSystemHeat heatModule; + // Stock ModuleResourceHarvester raycasts against layer 15 only. + private const int ImpactLayerMask = 32768; + private static readonly ProfilerMarker BaseFixedUpdateMarker = new("ModuleResourceHarvester.FixedUpdate"); public override string GetInfo() @@ -77,6 +81,13 @@ public void Start() Utils.Log("[ModuleSystemHeatHarvester] Setup completed", LogType.Modules); Events["ToggleEditorThermalSim"].guiName = Localizer.Format("#LOC_SystemHeat_ModuleSystemHeatHarvester_Field_SimulateEditor", ConverterName); Fields["HarvesterEfficiency"].guiName = Localizer.Format("#LOC_SystemHeat_ModuleSystemHeatHarvester_Field_Efficiency", ConverterName); + + RegisterImpactRaycast(); + } + + void OnEnable() + { + RegisterImpactRaycast(); } public override void FixedUpdate() @@ -107,6 +118,31 @@ void OnDisable() { heatModule?.AddFlux(moduleID, 0f, 0f, false); HarvesterEfficiency = "-"; + RaycastManager.Instance?.Unregister(this); + } + + void RegisterImpactRaycast() + { + if (!HighLogic.LoadedSceneIsFlight || impactTransformCache == null) + return; + RaycastManager.Instance?.Register(this, impactTransformCache, ImpactRange, ImpactLayerMask); + } + + protected override bool CheckForImpact() + { + if (string.IsNullOrEmpty(ImpactTransform) || impactTransformCache == null) + return true; + + var hit = RaycastManager.Instance?.GetRaycastHit(this); + if (hit is not RaycastHit raycastHit) + { + // If we're not registered for whatever reason then do the raycast ourselves. + var origin = impactTransformCache.position; + if (!Physics.Raycast(new Ray(origin, impactTransformCache.forward), out raycastHit, ImpactRange, ImpactLayerMask)) + return false; + } + + return raycastHit.collider != null; } void FixedUpdateFlight()