Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ END TEMPLATE-->

### New features

*None yet*
* Added `IStaggeredUpdate` and `EntitySystem.GetStaggeredUpdateTracker<TComp>()` for spreading component updates over time.

### Bugfixes

Expand Down
35 changes: 35 additions & 0 deletions Robust.Shared.Tests/Collections/RingBufferListTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,41 @@ public void TestBasicAddAfterWrap()
});
}

[Test]
public void TestTryPeekFrontEmpty()
{
var list = new RingBufferList<int>();

using (Assert.EnterMultipleScope())
{
Assert.That(list.TryPeekFront(out var value), Is.False);
Assert.That(value, Is.Default);
}
}

[Test]
public void TestTryPeekFrontAfterWrap()
{
var list = new RingBufferList<int>(6)
{
1,
2,
3
};
list.RemoveAt(0);
list.RemoveAt(0);
list.Add(4);
list.Add(5);
list.Add(6);

using (Assert.EnterMultipleScope())
{
Assert.That(list.TryPeekFront(out var value), Is.True);
Assert.That(value, Is.EqualTo(3));
Assert.That(list, Is.EquivalentTo([3, 4, 5, 6]));
}
}

[Test]
public void TestMiddleRemoveAtScenario1()
{
Expand Down
12 changes: 12 additions & 0 deletions Robust.Shared/Collections/RingBufferList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ public bool Remove(T item)
return true;
}

public bool TryPeekFront(out T value)
{
if (Count == 0)
{
value = default!;
return false;
}

value = _items[_read];
return true;
}

public int Count
{
get
Expand Down
8 changes: 4 additions & 4 deletions Robust.Shared/GameObjects/EntityManager.Components.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1799,10 +1799,10 @@ public NetComponentEnumerator(Dictionary<ushort, IComponent> dictionary) =>
/// <seealso cref="M:Robust.Shared.GameObjects.EntityManager.GetEntityQuery``1">EntityManager.GetEntityQuery()</seealso>
public readonly struct EntityQuery<TComp1> where TComp1 : IComponent
{
private readonly EntityManager _entMan;
private readonly EntityManager? _entMan;
private readonly Dictionary<EntityUid, IComponent> _traitDict;

internal EntityQuery(EntityManager entMan, Dictionary<EntityUid, IComponent> traitDict)
internal EntityQuery(EntityManager? entMan, Dictionary<EntityUid, IComponent> traitDict)
{
_entMan = entMan;
_traitDict = traitDict;
Expand Down Expand Up @@ -1957,7 +1957,7 @@ public bool Resolve(EntityUid uid, [NotNullWhen(true)] ref TComp1? component, bo
}

if (logMissing)
_entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{Environment.StackTrace}");
_entMan?.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{Environment.StackTrace}");

return false;
}
Expand Down Expand Up @@ -2076,7 +2076,7 @@ internal bool ResolveInternal(EntityUid uid, [NotNullWhen(true)] ref TComp1? com
}

if (logMissing)
_entMan.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{new StackTrace(1, true)}");
_entMan?.ResolveSawmill.Error($"Can't resolve \"{typeof(TComp1)}\" on entity {_entMan.ToPrettyString(uid)}!\n{new StackTrace(1, true)}");

return false;
}
Expand Down
199 changes: 199 additions & 0 deletions Robust.Shared/GameObjects/EntitySystem.StaggeredUpdate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.Collections;
using Robust.Shared.Random;
using Robust.Shared.Timing;

namespace Robust.Shared.GameObjects;

public partial class EntitySystem
{
[IoC.Dependency] private IRobustRandom _rng = default!;
[IoC.Dependency] private IGameTiming _gameTiming = default!;

/// <summary>
/// Creates a tracker for updating entities with <typeparamref name="TComp"/> staggered over time.
/// </summary>
/// <remarks>
/// Entities are added to the tracker when their component receives <see cref="MapInitEvent"/>.
/// Each tracked entity is returned by the tracker at <see cref="IStaggeredUpdate.UpdateInterval"/>
/// intervals, with the first update randomly offset to spread work across ticks.
/// </remarks>
/// <param name="mapInit">Optional MapInit handler to invoke before the component is added to the tracker.</param>
/// <typeparam name="TComp">The component type to track.</typeparam>
/// <returns>A tracker that enumerates entities due for their staggered update.</returns>
protected StaggeredUpdateTracker<TComp> GetStaggeredUpdateTracker<TComp>(
EntityEventRefHandler<TComp, MapInitEvent>? mapInit) where TComp : IComponent, IStaggeredUpdate
{
var tracker = new StaggeredUpdateTracker<TComp>(
mapInit, GetEntityQuery<TComp>(), GetEntityQuery<MetaDataComponent>(), _rng, _gameTiming);
SubscribeLocalEvent<TComp, MapInitEvent>(tracker.OnMapInit);
return tracker;
}
}

public interface IStaggeredUpdate
{
static abstract TimeSpan UpdateInterval { get; }
}

public sealed class StaggeredUpdateTracker<TComp>
where TComp : IComponent, IStaggeredUpdate
{
private readonly PriorityQueue<EntityUid, TimeSpan> _insertQueue = new();
private readonly RingBufferList<(EntityUid entity, TimeSpan when)> _schedule = [];
private readonly HashSet<EntityUid> _tracked = [];

private readonly EntityQuery<TComp> _compQuery;
private readonly EntityQuery<MetaDataComponent> _metaQuery;
private readonly IRobustRandom _rng;
private readonly IGameTiming _timing;
private readonly TimeSpan _updateInterval;
private readonly EntityEventRefHandler<TComp, MapInitEvent>? _mapInit;

internal StaggeredUpdateTracker(
EntityEventRefHandler<TComp, MapInitEvent>? mapInit,
EntityQuery<TComp> compQuery,
EntityQuery<MetaDataComponent> metaQuery,
IRobustRandom rng,
IGameTiming timing)
{
var interval = TComp.UpdateInterval;
if (interval <= TimeSpan.Zero)
{
throw new InvalidOperationException(
$"{typeof(TComp)} has an invalid staggered update interval: {interval}. " +
"Staggered update interval must be positive and non-zero.");
}

_mapInit = mapInit;
_updateInterval = interval;
_compQuery = compQuery;
_metaQuery = metaQuery;
_rng = rng;
_timing = timing;
}

internal void OnMapInit(Entity<TComp> ent, ref MapInitEvent args)
{
_mapInit?.Invoke(ent, ref args); // call a chained event handler if we have one

if (!_tracked.Add(ent.Owner)) return;

// randomize an offset from the current tick, up to interval
// we start from current tick + 1 because updates for the current tick may already have been processed
var when = _timing.CurTime + _timing.TickPeriod + _rng.Next(TimeSpan.Zero, _updateInterval);
_insertQueue.Enqueue(ent.Owner, when);
}

public Enumerator GetEnumerator()
{
return new Enumerator(this);
}

public readonly struct Enumerator(StaggeredUpdateTracker<TComp> tracker)
{
private readonly PriorityQueue<EntityUid, TimeSpan> _insertQueue = tracker._insertQueue;
private readonly RingBufferList<(EntityUid entity, TimeSpan when)> _schedule = tracker._schedule;
private readonly HashSet<EntityUid> _tracked = tracker._tracked;
private readonly EntityQuery<TComp> _compQuery = tracker._compQuery;
private readonly EntityQuery<MetaDataComponent> _metaQuery = tracker._metaQuery;
private readonly TimeSpan _updateInterval = tracker._updateInterval;
private readonly TimeSpan _until = tracker._timing.CurTime;

public bool MoveNext(out EntityUid uid, [NotNullWhen(true)] out TComp? comp)
{
if (_insertQueue.Count != 0) return MoveNextMixed(out uid, out comp);

while (_schedule.TryPeekFront(out var sched))
{
if (sched.when > _until)
{
uid = default;
comp = default;
return false;
}

_schedule.RemoveAt(0);
uid = sched.entity;

// since we only schedule when the component can be resolved, entities where the component has been
// deleted are dropped from the tracker
if (_compQuery.TryComp(uid, out comp))
{
_schedule.Add((uid, sched.when + _updateInterval));

if (!_metaQuery.TryGetComponentInternal(uid, out var metaComp)
|| metaComp.EntityPaused)
{
continue;
}

return true;
}

// if our component is missing, stop tracking this entity
_tracked.Remove(uid);
}

uid = default;
comp = default;
return false;
}

private bool MoveNextMixed(out EntityUid uid, [NotNullWhen(true)] out TComp? comp)
{
while (true)
{
TimeSpan when;

// the next entity may come either from the insertion list or the schedule, whichever is sooner
var queueWhen = _insertQueue.TryPeek(out var queueEnt, out var w)
? w
: TimeSpan.MaxValue;
var (schedEnt, schedWhen) = _schedule.TryPeekFront(out var sched)
? sched
: (default, TimeSpan.MaxValue);

if (schedWhen > _until && queueWhen > _until)
{
uid = default;
comp = default;
return false;
}

if (queueWhen < schedWhen)
{
_insertQueue.Dequeue();
uid = queueEnt;
when = queueWhen;
}
else
{
_schedule.RemoveAt(0);
uid = schedEnt;
when = schedWhen;
}

// since we only schedule when the component can be resolved, entities where the component has been
// deleted are dropped from the tracker
if (_compQuery.TryComp(uid, out comp))
{
_schedule.Add((uid, when + _updateInterval));

if (!_metaQuery.TryGetComponentInternal(uid, out var metaComp)
|| metaComp.EntityPaused)
{
continue;
}

return true;
}

// if our component is missing, stop tracking this entity
_tracked.Remove(uid);
}
}
}
}
Loading
Loading