Files
wwdpublic/Content.Server/Emp/EmpSystem.cs
WarMechanic 6a89b524d5 Event Scheduler System (#2355)
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Excerpt

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

thou shall be thy axe that continues the work - to behead all
`EntityQueryEnumerator`s, and thus i require it. 'tis imperative i have
at thee and perform the duties. if the plague bearing shitcode is not
cut at thy roots, it shall only nest a deeper grave and spread locusts.

---

# Description

It has been a reoccuring theme that someone irresponsibly uses
`EntityQueryEnumerator` and then suddenly server performance is worse. A
lot of these cases involve using EQE to iterate a timer on a component,
to start or stop an effect after a delay. Rather than iterating `frames
* n` times per second FOR EVERY UNIQUE SYSTEM THAT DOES THIS (QUITE A
FEW), we instead iterate `frames` per second regardless of systems using
the Event Scheduler.

The Event Scheduler itself is a list of events that wait to be triggered
after a delay. Rather than iterating through all of them, they are
sorted in order of occurance using a PriorityQueue. I love priority
queues because they sort as you enqueue to them, and apparently the sort
complexity is logarithmic? But mostly because of the former.

I chose to write the scheduler the way I did because the choice to use
async seems too big for me alone. So this system is synchronous and
updates on game time.

This is mostly a practical optimisation. The code which I have written
is almost certainly not optimal, but the simple act of replacing EQE
delays will significantly improve server performance anyway. Rust
monsters feel free to rewrite the event scheduler to be more performant.

NOTE: For some reason PriorityQueue is banned on the client, and
configuring it requires editing a RobustToolbox file. So for now this
system is restricted to Content.Server until we start using our engine
fork.
---

# TODO

<!--
A list of everything you have to do before this PR is "complete"
You probably won't have to complete everything before merging but it's
good to leave future references
-->

- [x] Working queue of events ordered by execution times
- [x] A function to enqueue any-event-defined-ever into the scheduler
with a delay
- [x] Delay the event, and then fire it
- [x] Implement retroactive schedule cancelling

In a future PR:
- Add ```System.Collection.Generic.PriorityQueue`2``` to whitelist in
`RobustToolbox/Robust.Server/ContentPack/Sandbox.yml`
- - Shared files had to be relocated to server because they were banned
on the client and would cause an exception.
- Investigate insert performance as more systems are added to use the
EventScheduler
- - MLGTASTICa rose the idea of 'buckets' as an optimisation.
- - I theorise multiple priority queues for different types of events
might also work.

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>

Demonstration:
https://youtu.be/CGB6SDWGc-Q

Response to MLGTASTICa, stress testing:
https://youtu.be/30OA06Pzhtk

</p>
</details>

---

# Changelog

<!--
You can add an author after the `🆑` to change the name that appears
in the changelog (ex: `🆑 Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->
- add: Added the EventScheduler system that lets you raise an event at a
certain time or after a delay without killing performance.
- fix: Optimised EMP by migrating from EQE to the new EventScheduler
system.
2025-07-12 03:02:21 +10:00

148 lines
5.9 KiB
C#

using Content.Server.Explosion.EntitySystems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Radio;
using Content.Server.EventScheduler;
using Content.Shared.Emp;
using Content.Shared.Examine;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.Emp;
public sealed class EmpSystem : SharedEmpSystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly EventSchedulerSystem _eventScheduler = default!;
public const string EmpPulseEffectPrototype = "EffectEmpPulse";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmpDisabledComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<EmpOnTriggerComponent, TriggerEvent>(HandleEmpTrigger);
SubscribeLocalEvent<EmpDisabledComponent, EmpDisabledRemoved>(OnEmpDisabledRemoved);
SubscribeLocalEvent<EmpDisabledComponent, RadioSendAttemptEvent>(OnRadioSendAttempt);
SubscribeLocalEvent<EmpDisabledComponent, RadioReceiveAttemptEvent>(OnRadioReceiveAttempt);
}
/// <summary>
/// Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then a raising <see cref="EmpPulseEvent"/> on all entities in range.
/// </summary>
/// <param name="coordinates">The location to trigger the EMP pulse at.</param>
/// <param name="range">The range of the EMP pulse.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP pulse.</param>
/// <param name="duration">The duration of the EMP effects.</param>
public void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration)
{
foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range))
{
TryEmpEffects(uid, energyConsumption, duration);
}
Spawn(EmpPulseEffectPrototype, coordinates);
}
/// <summary>
/// Attempts to apply the effects of an EMP pulse onto an entity by first raising an <see cref="EmpAttemptEvent"/>, followed by raising a <see cref="EmpPulseEvent"/> on it.
/// </summary>
/// <param name="uid">The entity to apply the EMP effects on.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
/// <param name="duration">The duration of the EMP effects.</param>
public void TryEmpEffects(EntityUid uid, float energyConsumption, float duration)
{
var attemptEv = new EmpAttemptEvent();
RaiseLocalEvent(uid, attemptEv);
if (attemptEv.Cancelled)
return;
DoEmpEffects(uid, energyConsumption, duration);
}
/// <summary>
/// Applies the effects of an EMP pulse onto an entity by raising a <see cref="EmpPulseEvent"/> on it.
/// </summary>
/// <param name="uid">The entity to apply the EMP effects on.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP.</param>
/// <param name="duration">The duration of the EMP effects.</param>
public void DoEmpEffects(EntityUid uid, float energyConsumption, float duration)
{
TimeSpan delay = TimeSpan.FromSeconds(duration);
var ev = new EmpPulseEvent(energyConsumption, false, false, delay);
RaiseLocalEvent(uid, ref ev);
if (ev.Affected)
{
Spawn(EmpDisabledEffectPrototype, Transform(uid).Coordinates);
}
if (ev.Disabled)
{
var disabled = EnsureComp<EmpDisabledComponent>(uid);
// couldnt use null-coalescing operator here sadge
if (disabled.LastDelayedEvent != null)
_eventScheduler.TryPostponeDelayedEvent(disabled.LastDelayedEvent, delay);
else
{
var dEv = new EmpDisabledRemoved();
disabled.LastDelayedEvent = _eventScheduler.DelayEvent(uid, ref dEv, delay);
}
/// i tried my best to go through the Pow3r server code but i literally couldn't find in relation to PowerNetworkBatteryComponent that uses the event system
/// the code is otherwise too esoteric for my innocent eyes
if (TryComp<PowerNetworkBatteryComponent>(uid, out var powerNetBattery))
{
powerNetBattery.CanCharge = false;
}
}
}
private void OnEmpDisabledRemoved(EntityUid uid, EmpDisabledComponent component, EmpDisabledRemoved args)
{
RemComp<EmpDisabledComponent>(uid);
var ev = new EmpDisabledRemoved();
RaiseLocalEvent(uid, ref ev);
if (TryComp<PowerNetworkBatteryComponent>(uid, out var powerNetBattery))
{
powerNetBattery.CanCharge = true;
}
}
private void OnExamine(EntityUid uid, EmpDisabledComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("emp-disabled-comp-on-examine"));
}
private void HandleEmpTrigger(EntityUid uid, EmpOnTriggerComponent comp, TriggerEvent args)
{
EmpPulse(_transform.GetMapCoordinates(uid), comp.Range, comp.EnergyConsumption, comp.DisableDuration);
args.Handled = true;
}
private void OnRadioSendAttempt(EntityUid uid, EmpDisabledComponent component, ref RadioSendAttemptEvent args)
{
args.Cancelled = true;
}
private void OnRadioReceiveAttempt(EntityUid uid, EmpDisabledComponent component, ref RadioReceiveAttemptEvent args)
{
args.Cancelled = true;
}
}
/// <summary>
/// Raised on an entity before <see cref="EmpPulseEvent"/>. Cancel this to prevent the emp event being raised.
/// </summary>
public sealed partial class EmpAttemptEvent : CancellableEntityEventArgs
{
}
[ByRefEvent]
public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled, TimeSpan Duration);
[ByRefEvent]
public record struct EmpDisabledRemoved();