mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-02 18:17:09 +02:00
* [Dependency] source generator No more reflection, no more codegen at runtime Also various changes to Roslyn helpers to make this easier to write. Requires all types with dependencies to be partial and not have readonly dependency fields. An analyzer enforces this at warning level, the previous injection strategies have remained in the code *for now* as a fallback. No fallback is available for [field: Dependency] properties, due to a Roslyn bug. Code Fixes exist. We love Roslyn * Apply dependencies generator changes to all code * Release notes * Preprocessor got hands * Handle nullable dependencies These are bad but gotta deal with it. * Apply suggestions from code review Co-authored-by: Moony <moony@hellomouse.net> * Fine, let's not use collection expressions --------- Co-authored-by: Moony <moony@hellomouse.net>
631 lines
20 KiB
C#
631 lines
20 KiB
C#
using JetBrains.Annotations;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Physics;
|
|
using Robust.Shared.Physics.Systems;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Robust.Shared.Collections;
|
|
using System.Numerics;
|
|
using Robust.Shared.Map.Components;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Shared.ComponentTrees;
|
|
|
|
/// <summary>
|
|
/// Keeps track of <see cref="DynamicTree{T}"/>s for various rendering-related components.
|
|
/// </summary>
|
|
[UsedImplicitly]
|
|
public abstract partial class ComponentTreeSystem<TTreeComp, TComp> : EntitySystem
|
|
where TTreeComp : Component, IComponentTreeComponent<TComp>, new()
|
|
where TComp : Component, IComponentTreeEntry<TComp>
|
|
{
|
|
[Dependency] private RecursiveMoveSystem _recursiveMoveSys = default!;
|
|
[Dependency] protected SharedTransformSystem XformSystem = default!;
|
|
[Dependency] private IMapManager _mapManager = default!;
|
|
[Dependency] private SharedMapSystem _mapSystem = default!;
|
|
|
|
private readonly Queue<ComponentTreeEntry<TComp>> _updateQueue = new();
|
|
protected EntityQuery<TComp> Query;
|
|
|
|
/// <summary>
|
|
/// Whether this lookup tree should even be enabled.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This can be used to disable some trees if they are not required, which helps improve performance a bit.
|
|
/// </remarks>
|
|
protected virtual bool Enabled => true;
|
|
private bool _initialized;
|
|
|
|
/// <summary>
|
|
/// If true, this system will update the tree positions every frame update. See also <see cref="DoTickUpdate"/>. Some systems may need to do both.
|
|
/// </summary>
|
|
protected abstract bool DoFrameUpdate { get; }
|
|
|
|
/// <summary>
|
|
/// If true, this system will update the tree positions every tick update. See also <see cref="DoFrameUpdate"/>. Some systems may need to do both.
|
|
/// </summary>
|
|
protected abstract bool DoTickUpdate { get; }
|
|
|
|
/// <summary>
|
|
/// Initial tree capacity. Note that client-side trees will remove entities as they leave PVS range.
|
|
/// </summary>
|
|
protected virtual int InitialCapacity { get; } = 256;
|
|
|
|
/// <summary>
|
|
/// If true, this tree requires all children to be recursively updated whenever ANY entity moves. If false, this
|
|
/// will only update when an entity with the given component moves.
|
|
/// </summary>
|
|
protected abstract bool Recursive { get; }
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
if (!Enabled)
|
|
return;
|
|
|
|
_initialized = true;
|
|
UpdatesOutsidePrediction = DoTickUpdate;
|
|
UpdatesAfter.Add(typeof(SharedTransformSystem));
|
|
UpdatesAfter.Add(typeof(SharedPhysicsSystem));
|
|
|
|
SubscribeLocalEvent<MapCreatedEvent>(MapManagerOnMapCreated);
|
|
SubscribeLocalEvent<GridInitializeEvent>(MapManagerOnGridCreated);
|
|
|
|
SubscribeLocalEvent<TComp, ComponentStartup>(OnCompStartup);
|
|
SubscribeLocalEvent<TComp, ComponentRemove>(OnCompRemoved);
|
|
|
|
if (Recursive)
|
|
{
|
|
_recursiveMoveSys.OnTreeRecursiveMove += HandleRecursiveMove;
|
|
_recursiveMoveSys.AddSubscription();
|
|
}
|
|
else
|
|
{
|
|
// TODO EXCEPTION TOLERANCE
|
|
// Ensure lookup trees update before content code handles move events.
|
|
SubscribeLocalEvent<TComp, MoveEvent>(HandleMove);
|
|
}
|
|
|
|
SubscribeLocalEvent<TTreeComp, EntityTerminatingEvent>(OnTerminating);
|
|
SubscribeLocalEvent<TTreeComp, ComponentAdd>(OnTreeAdd);
|
|
SubscribeLocalEvent<TTreeComp, ComponentRemove>(OnTreeRemove);
|
|
|
|
Query = GetEntityQuery<TComp>();
|
|
}
|
|
|
|
public override void Shutdown()
|
|
{
|
|
if (!_initialized)
|
|
return;
|
|
|
|
_initialized = false;
|
|
if (Recursive)
|
|
_recursiveMoveSys.OnTreeRecursiveMove -= HandleRecursiveMove;
|
|
}
|
|
|
|
private bool CheckEnabled()
|
|
{
|
|
if (_initialized)
|
|
return true;
|
|
|
|
Log.Error($"Attempted to use disabled lookup tree");
|
|
return false;
|
|
}
|
|
|
|
#region Queue Update
|
|
|
|
private void HandleRecursiveMove(EntityUid uid, TransformComponent xform)
|
|
{
|
|
if (Query.TryGetComponent(uid, out var component))
|
|
QueueTreeUpdate(uid, component, xform);
|
|
}
|
|
|
|
private void HandleMove(EntityUid uid, TComp component, ref MoveEvent args)
|
|
=> QueueTreeUpdate(uid, component, args.Component);
|
|
|
|
public void QueueTreeUpdate(EntityUid uid, TComp component, TransformComponent? xform = null)
|
|
{
|
|
if (!_initialized)
|
|
return;
|
|
|
|
if (component.TreeUpdateQueued || !Resolve(uid, ref xform))
|
|
return;
|
|
|
|
component.TreeUpdateQueued = true;
|
|
_updateQueue.Enqueue((component, xform));
|
|
}
|
|
|
|
public void QueueTreeUpdate(Entity<TComp> entity, TransformComponent? xform = null)
|
|
{
|
|
QueueTreeUpdate(entity.Owner, entity.Comp, xform);
|
|
}
|
|
#endregion
|
|
|
|
#region Component Management
|
|
protected virtual void OnCompStartup(EntityUid uid, TComp component, ComponentStartup args)
|
|
=> QueueTreeUpdate(uid, component);
|
|
|
|
protected virtual void OnCompRemoved(EntityUid uid, TComp component, ComponentRemove args)
|
|
=> RemoveFromTree(component);
|
|
|
|
protected virtual void OnTreeAdd(EntityUid uid, TTreeComp component, ComponentAdd args)
|
|
{
|
|
component.Tree = new(ExtractAabb, capacity: InitialCapacity);
|
|
}
|
|
|
|
protected virtual void OnTreeRemove(EntityUid uid, TTreeComp component, ComponentRemove args)
|
|
{
|
|
foreach (var entry in component.Tree)
|
|
{
|
|
entry.Component.TreeUid = null;
|
|
entry.Component.Tree = null;
|
|
}
|
|
|
|
component.Tree.Clear();
|
|
}
|
|
|
|
protected virtual void OnTerminating(EntityUid uid, TTreeComp component, ref EntityTerminatingEvent args)
|
|
{
|
|
// IIRC, this is to prevent a tree-update spam as each of the entity's children get detached to nullspace.
|
|
RemComp(uid, component);
|
|
}
|
|
|
|
private void MapManagerOnMapCreated(MapCreatedEvent e)
|
|
{
|
|
EnsureComp<TTreeComp>(e.Uid);
|
|
}
|
|
|
|
private void MapManagerOnGridCreated(GridInitializeEvent ev)
|
|
{
|
|
EnsureComp<TTreeComp>(ev.EntityUid);
|
|
}
|
|
#endregion
|
|
|
|
#region Update Trees
|
|
public override void Update(float frameTime)
|
|
{
|
|
if (DoTickUpdate && _initialized)
|
|
UpdateTreePositions();
|
|
}
|
|
|
|
public override void FrameUpdate(float frameTime)
|
|
{
|
|
if (DoFrameUpdate && _initialized)
|
|
UpdateTreePositions();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes any pending position updates. Note that this should generally always get run before directly
|
|
/// querying a tree.
|
|
/// </summary>
|
|
public void UpdateTreePositions()
|
|
{
|
|
try
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
if (_updateQueue.Count == 0)
|
|
return;
|
|
|
|
var trees = GetEntityQuery<TTreeComp>();
|
|
|
|
while (_updateQueue.TryDequeue(out var entry))
|
|
{
|
|
var (comp, xform) = entry;
|
|
|
|
// Was this entity queued multiple times?
|
|
DebugTools.Assert(comp.TreeUpdateQueued, "Entity was queued multiple times?");
|
|
|
|
comp.TreeUpdateQueued = false;
|
|
if (!comp.Running)
|
|
continue;
|
|
|
|
if (!comp.AddToTree || comp.Deleted || xform.MapUid == null)
|
|
{
|
|
RemoveFromTree(comp);
|
|
continue;
|
|
}
|
|
|
|
var newTree = xform.GridUid ?? xform.MapUid;
|
|
if (!trees.TryGetComponent(newTree, out var newTreeComp) && comp.TreeUid == null)
|
|
continue;
|
|
|
|
Vector2 pos;
|
|
Angle rot;
|
|
if (comp.TreeUid == newTree)
|
|
{
|
|
(pos, rot) = XformSystem.GetRelativePositionRotation(
|
|
entry.Transform,
|
|
newTree!.Value);
|
|
|
|
newTreeComp!.Tree.Update(entry, ExtractAabb(entry, pos, rot));
|
|
continue;
|
|
}
|
|
|
|
RemoveFromTree(comp);
|
|
|
|
if (newTreeComp == null)
|
|
return;
|
|
|
|
comp.TreeUid = newTree;
|
|
comp.Tree = newTreeComp.Tree;
|
|
|
|
(pos, rot) = XformSystem.GetRelativePositionRotation(
|
|
entry.Transform,
|
|
newTree!.Value);
|
|
|
|
newTreeComp.Tree.Add(entry, ExtractAabb(entry, pos, rot));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_updateQueue.Clear();
|
|
}
|
|
}
|
|
|
|
private void RemoveFromTree(TComp component)
|
|
{
|
|
component.Tree?.Remove(new() { Component = component });
|
|
component.Tree = null;
|
|
component.TreeUid = null;
|
|
}
|
|
#endregion
|
|
|
|
#region AABBs
|
|
protected virtual Box2 ExtractAabb(in ComponentTreeEntry<TComp> entry)
|
|
{
|
|
if (entry.Component.TreeUid == null)
|
|
return default;
|
|
|
|
var (pos, rot) = XformSystem.GetRelativePositionRotation(
|
|
entry.Transform,
|
|
entry.Component.TreeUid.Value);
|
|
|
|
return ExtractAabb(in entry, pos, rot);
|
|
}
|
|
|
|
protected abstract Box2 ExtractAabb(in ComponentTreeEntry<TComp> entry, Vector2 pos, Angle rot);
|
|
#endregion
|
|
|
|
#region Queries
|
|
public IEnumerable<(EntityUid, TTreeComp)> GetIntersectingTrees(MapId mapId, Box2Rotated worldBounds)
|
|
=> GetIntersectingTrees(mapId, worldBounds.CalcBoundingBox());
|
|
|
|
public IEnumerable<(EntityUid Uid, TTreeComp Comp)> GetIntersectingTrees(MapId mapId, Box2 worldAABB)
|
|
{
|
|
if (!CheckEnabled())
|
|
return [];
|
|
// Anything that queries these trees should only do so if there are no queued updates, otherwise it can lead to
|
|
// errors. Currently there is no easy way to enforce this, but this should work as long as nothing queries the
|
|
// trees directly:
|
|
UpdateTreePositions();
|
|
var trees = new ValueList<(EntityUid Uid, TTreeComp Comp)>();
|
|
|
|
if (mapId == MapId.Nullspace)
|
|
return trees;
|
|
|
|
var state = (EntityManager, trees);
|
|
|
|
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref state,
|
|
(EntityUid uid, MapGridComponent grid,
|
|
ref (EntityManager EntityManager, ValueList<(EntityUid, TTreeComp)> trees) tuple) =>
|
|
{
|
|
if (tuple.EntityManager.TryGetComponent<TTreeComp>(uid, out var treeComp))
|
|
{
|
|
tuple.trees.Add((uid, treeComp));
|
|
}
|
|
|
|
return true;
|
|
}, includeMap: false);
|
|
|
|
if (_mapSystem.TryGetMap(mapId, out var mapUid) && TryComp(mapUid, out TTreeComp? mapTreeComp))
|
|
{
|
|
state.trees.Add((mapUid.Value, mapTreeComp));
|
|
}
|
|
|
|
return state.trees;
|
|
}
|
|
|
|
#region HashSet
|
|
|
|
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2 worldBounds, bool approx = true)
|
|
=> QueryAabb(mapId, new Box2Rotated(worldBounds, default, default), approx);
|
|
|
|
public void QueryAabb(HashSet<Entity<TComp, TransformComponent>> results, MapId mapId, Box2 worldBounds, bool approx = true)
|
|
=> QueryAabb(results, mapId, new Box2Rotated(worldBounds, default, default), approx);
|
|
|
|
public HashSet<ComponentTreeEntry<TComp>> QueryAabb(MapId mapId, Box2Rotated worldBounds, bool approx = true)
|
|
{
|
|
var state = new HashSet<ComponentTreeEntry<TComp>>();
|
|
QueryAabb(state, mapId, worldBounds, approx);
|
|
|
|
return state;
|
|
}
|
|
|
|
[Obsolete("Use Entity<T> variant")]
|
|
internal void QueryAabb(
|
|
HashSet<ComponentTreeEntry<TComp>> results,
|
|
MapId mapId,
|
|
Box2Rotated worldBounds,
|
|
bool approx = true)
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
|
|
{
|
|
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
|
|
|
|
treeComp.Tree.QueryAabb(ref results,
|
|
static (ref HashSet<ComponentTreeEntry<TComp>> state, in ComponentTreeEntry<TComp> value) =>
|
|
{
|
|
state.Add(value);
|
|
return true;
|
|
},
|
|
bounds,
|
|
approx);
|
|
}
|
|
}
|
|
|
|
public void QueryAabb(
|
|
HashSet<Entity<TComp, TransformComponent>> results,
|
|
MapId mapId,
|
|
Box2Rotated worldBounds,
|
|
bool approx = true)
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
|
|
{
|
|
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
|
|
|
|
treeComp.Tree.QueryAabb(ref results,
|
|
static (ref HashSet<Entity<TComp, TransformComponent>> state, in ComponentTreeEntry<TComp> value) =>
|
|
{
|
|
state.Add(value);
|
|
return true;
|
|
},
|
|
bounds,
|
|
approx);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region List
|
|
|
|
public void QueryAabb(List<Entity<TComp, TransformComponent>> results, MapId mapId, Box2 worldBounds, bool approx = true)
|
|
=> QueryAabb(results, mapId, new Box2Rotated(worldBounds, default, default), approx);
|
|
|
|
public void QueryAabb(
|
|
List<Entity<TComp, TransformComponent>> results,
|
|
MapId mapId,
|
|
Box2Rotated worldBounds,
|
|
bool approx = true)
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
|
|
{
|
|
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
|
|
|
|
treeComp.Tree.QueryAabb(ref results,
|
|
static (ref List<Entity<TComp, TransformComponent>> state, in ComponentTreeEntry<TComp> value) =>
|
|
{
|
|
state.Add(value);
|
|
return true;
|
|
},
|
|
bounds,
|
|
approx);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
public void QueryAabb<TState>(
|
|
ref TState state,
|
|
DynamicTree<ComponentTreeEntry<TComp>>.QueryCallbackDelegate<TState> callback,
|
|
MapId mapId,
|
|
Box2 worldBounds,
|
|
bool approx = true)
|
|
{
|
|
QueryAabb(ref state, callback, mapId, new Box2Rotated(worldBounds, default, default), approx);
|
|
}
|
|
|
|
public void QueryAabb<TState>(
|
|
ref TState state,
|
|
DynamicTree<ComponentTreeEntry<TComp>>.QueryCallbackDelegate<TState> callback,
|
|
MapId mapId,
|
|
Box2Rotated worldBounds,
|
|
bool approx = true)
|
|
{
|
|
if (!CheckEnabled())
|
|
return;
|
|
|
|
foreach (var (tree, treeComp) in GetIntersectingTrees(mapId, worldBounds))
|
|
{
|
|
var bounds = XformSystem.GetInvWorldMatrix(tree).TransformBox(worldBounds);
|
|
treeComp.Tree.QueryAabb(ref state, callback, bounds, approx);
|
|
}
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region Rays
|
|
|
|
[Obsolete("use IntersectRay")]
|
|
public List<RayCastResults> IntersectRayWithPredicate<TState>(MapId mapId, in Ray ray, float maxLength,
|
|
TState state, Func<EntityUid, TState, bool> predicate, bool returnOnFirstHit = true)
|
|
{
|
|
var list = new List<RayCastResults>();
|
|
|
|
if (!returnOnFirstHit)
|
|
{
|
|
IntersectRay(list, mapId, ray, maxLength, state, (e, s) => predicate(e.Owner, s));
|
|
return list;
|
|
}
|
|
|
|
var result = IntersectRay(mapId, ray, maxLength, state, (e, s) => predicate(e.Owner, s));
|
|
if (result != null)
|
|
list.Add(result.Value);
|
|
return list;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a ray intersection and return on the first hit.
|
|
/// </summary>
|
|
public RayCastResults? IntersectRay(MapId mapId, in Ray ray, float length)
|
|
{
|
|
var state = new QueryState(length);
|
|
IntersectRayInternal(mapId, in ray, length, ref state, QueryCallback);
|
|
return state.Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a ray intersection and populate a provided list of results.
|
|
/// </summary>
|
|
public void IntersectRay(List<RayCastResults> results, MapId mapId, in Ray ray, float maxLength)
|
|
{
|
|
results.Clear();
|
|
var state = new QueryState(maxLength, results);
|
|
IntersectRayInternal(mapId, in ray, maxLength, ref state, QueryCallback);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a ray intersection with a predicate and return on the first hit.
|
|
/// </summary>
|
|
public RayCastResults? IntersectRay<TState>(
|
|
MapId mapId,
|
|
in Ray ray,
|
|
float length,
|
|
TState predicateState,
|
|
Func<Entity<TComp, TransformComponent>, TState, bool> ignore)
|
|
{
|
|
var state = new QueryState<TState>(new(length), predicateState, ignore);
|
|
IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback);
|
|
return state.Inner.Result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a ray intersection with a predicate and populate a provided list of results.
|
|
/// </summary>
|
|
public void IntersectRay<TState>(
|
|
List<RayCastResults> results,
|
|
MapId mapId,
|
|
in Ray ray,
|
|
float length,
|
|
TState predicateState,
|
|
Func<Entity<TComp, TransformComponent>, TState, bool> ignore)
|
|
{
|
|
var state = new QueryState<TState>(new(length, results), predicateState, ignore);
|
|
IntersectRayInternal(mapId, in ray, length, ref state, PredicateQueryCallback);
|
|
}
|
|
|
|
private void IntersectRayInternal<TState>(
|
|
MapId mapId,
|
|
in Ray ray,
|
|
float maxLength,
|
|
ref TState state,
|
|
DynamicTree<ComponentTreeEntry<TComp>>.RayQueryCallbackDelegate<TState> callback)
|
|
where TState : IDone
|
|
{
|
|
if (mapId == MapId.Nullspace)
|
|
return;
|
|
|
|
if (!CheckEnabled())
|
|
return;
|
|
var endPoint = ray.Position + ray.Direction * maxLength;
|
|
var worldBox = new Box2(Vector2.Min(ray.Position, endPoint), Vector2.Max(ray.Position, endPoint));
|
|
|
|
foreach (var (treeUid, comp) in GetIntersectingTrees(mapId, worldBox))
|
|
{
|
|
var (_, treeRot, matrix) = XformSystem.GetWorldPositionRotationInvMatrix(treeUid);
|
|
var relativeAngle = new Angle(-treeRot.Theta).RotateVec(ray.Direction);
|
|
var treeRay = new Ray(Vector2.Transform(ray.Position, matrix), relativeAngle);
|
|
comp.Tree.QueryRay(ref state, callback, treeRay);
|
|
if (state.Done)
|
|
return;
|
|
}
|
|
}
|
|
|
|
static bool QueryCallback(
|
|
ref QueryState state,
|
|
in ComponentTreeEntry<TComp> value,
|
|
in Vector2 point,
|
|
float dist)
|
|
{
|
|
if (dist > state.MaxLength)
|
|
return true;
|
|
|
|
if (state.ReturnOnFirstHit)
|
|
{
|
|
state.Result = new RayCastResults(dist, point, value.Uid);
|
|
return false;
|
|
}
|
|
|
|
state.List.Add(new RayCastResults(dist, point, value.Uid));
|
|
return true;
|
|
}
|
|
|
|
private static bool PredicateQueryCallback<TState>(
|
|
ref QueryState<TState> state,
|
|
in ComponentTreeEntry<TComp> value,
|
|
in Vector2 point,
|
|
float dist)
|
|
{
|
|
if (dist > state.Inner.MaxLength)
|
|
return true;
|
|
|
|
if (state.Ignore.Invoke(value, state.PredicateState))
|
|
return true;
|
|
|
|
if (state.Inner.ReturnOnFirstHit)
|
|
{
|
|
state.Inner.Result = new RayCastResults(dist, point, value.Uid);
|
|
return false;
|
|
}
|
|
|
|
state.Inner.List.Add(new RayCastResults(dist, point, value.Uid));
|
|
return true;
|
|
}
|
|
|
|
private struct QueryState<TPredicateState>(
|
|
QueryState inner,
|
|
TPredicateState predicateState,
|
|
Func<Entity<TComp, TransformComponent>, TPredicateState, bool> ignore) : IDone
|
|
{
|
|
public readonly TPredicateState PredicateState = predicateState;
|
|
public readonly Func<Entity<TComp, TransformComponent>, TPredicateState, bool> Ignore = ignore;
|
|
public QueryState Inner = inner;
|
|
public bool Done => Inner.Done;
|
|
}
|
|
|
|
private struct QueryState(float maxLength, List<RayCastResults>? list = null) : IDone
|
|
{
|
|
public readonly float MaxLength = maxLength;
|
|
|
|
[MemberNotNullWhen(false, nameof(List))]
|
|
public readonly bool ReturnOnFirstHit => List == null;
|
|
public readonly List<RayCastResults>? List = list;
|
|
public RayCastResults? Result;
|
|
public bool Done => Result != null;
|
|
}
|
|
|
|
private interface IDone
|
|
{
|
|
bool Done { get; }
|
|
}
|
|
|
|
#endregion
|
|
}
|