Move do_afters to an overlay (#10463)

This commit is contained in:
metalgearsloth
2022-08-13 14:32:23 +10:00
committed by GitHub
parent 36ba197a25
commit 71ffca2257
8 changed files with 203 additions and 503 deletions

View File

@@ -1,8 +1,6 @@
using System;
using Content.Client.DoAfter.UI;
using Content.Client.DoAfter;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Maths;
namespace Content.Client.CharacterInfo
{
@@ -14,7 +12,7 @@ namespace Content.Client.CharacterInfo
{
var dims = Texture != null ? GetDrawDimensions(Texture) : UIBox2.FromDimensions(Vector2.Zero, PixelSize);
dims.Top = Math.Max(dims.Bottom - dims.Bottom * Progress,0);
handle.DrawRect(dims, DoAfterHelpers.GetProgressColor(Progress));
handle.DrawRect(dims, DoAfterOverlay.GetProgressColor(Progress));
base.Draw(handle);
}

View File

@@ -1,4 +1,3 @@
using Content.Client.DoAfter.UI;
using Content.Shared.DoAfter;
namespace Content.Client.DoAfter
@@ -8,8 +7,6 @@ namespace Content.Client.DoAfter
{
public readonly Dictionary<byte, ClientDoAfter> DoAfters = new();
public readonly List<(TimeSpan CancelTime, ClientDoAfter Message)> CancelledDoAfters = new();
public DoAfterGui? Gui { get; set; }
public readonly Dictionary<byte, ClientDoAfter> CancelledDoAfters = new();
}
}

View File

@@ -0,0 +1,141 @@
using Content.Client.Resources;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.DoAfter;
public sealed class DoAfterOverlay : Overlay
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly SharedTransformSystem _transform;
private Texture _barTexture;
private ShaderInstance _shader;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
public DoAfterOverlay()
{
IoCManager.InjectDependencies(this);
_transform = _entManager.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
_barTexture = IoCManager.Resolve<IResourceCache>()
.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png");
_shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
}
protected override void Draw(in OverlayDrawArgs args)
{
var handle = args.WorldHandle;
var rotation = args.Viewport.Eye?.Rotation ?? Angle.Zero;
var spriteQuery = _entManager.GetEntityQuery<SpriteComponent>();
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var scale = _configManager.GetCVar(CVars.DisplayUIScale);
var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale));
var rotationMatrix = Matrix3.CreateRotation(-rotation);
handle.UseShader(_shader);
// TODO: Need active DoAfter component (or alternatively just make DoAfter itself active)
foreach (var comp in _entManager.EntityQuery<DoAfterComponent>(true))
{
if (comp.DoAfters.Count == 0 ||
!xformQuery.TryGetComponent(comp.Owner, out var xform) ||
xform.MapID != args.MapId)
{
continue;
}
var worldPosition = _transform.GetWorldPosition(xform);
if (!args.WorldAABB.Contains(worldPosition))
continue;
var index = 0;
var worldMatrix = Matrix3.CreateTranslation(worldPosition);
foreach (var (_, doAfter) in comp.DoAfters)
{
var elapsed = doAfter.Accumulator;
var displayRatio = MathF.Min(1.0f,
elapsed / doAfter.Delay);
Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld);
Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty);
handle.SetTransform(matty);
var offset = _barTexture.Height / scale * index;
// Use the sprite itself if we know its bounds. This means short or tall sprites don't get overlapped
// by the bar.
float yOffset;
if (spriteQuery.TryGetComponent(comp.Owner, out var sprite))
{
yOffset = sprite.Bounds.Height / 2f + 0.05f;
}
else
{
yOffset = 0.5f;
}
// Position above the entity (we've already applied the matrix transform to the entity itself)
// Offset by the texture size for every do_after we have.
var position = new Vector2(-_barTexture.Width / 2f / EyeManager.PixelsPerMeter,
yOffset / scale + offset / EyeManager.PixelsPerMeter * scale);
// Draw the underlying bar texture
handle.DrawTexture(_barTexture, position);
// Draw the bar itself
var cancelled = doAfter.Cancelled;
Color color;
const float flashTime = 0.125f;
// if we're cancelled then flick red / off.
if (cancelled)
{
var flash = Math.Floor(doAfter.CancelledAccumulator / flashTime) % 2 == 0;
color = new Color(1f, 0f, 0f, flash ? 1f : 0f);
}
else
{
color = GetProgressColor(displayRatio);
}
// Hardcoded width of the progress bar because it doesn't match the texture.
const float startX = 2f;
const float endX = 22f;
var xProgress = (endX - startX) * displayRatio + startX;
var box = new Box2(new Vector2(startX, 3f) / EyeManager.PixelsPerMeter, new Vector2(xProgress, 4f) / EyeManager.PixelsPerMeter);
box = box.Translated(position);
handle.DrawRect(box, color);
index++;
}
}
handle.UseShader(null);
handle.SetTransform(Matrix3.Identity);
}
public static Color GetProgressColor(float progress)
{
if (progress >= 1.0f)
{
return new Color(0f, 1f, 0f);
}
// lerp
var hue = (5f / 18f) * progress;
return Color.FromHsv((hue, 1f, 0.75f, 1f));
}
}

View File

@@ -1,13 +1,11 @@
using System.Linq;
using Content.Client.DoAfter.UI;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -20,18 +18,8 @@ namespace Content.Client.DoAfter
[UsedImplicitly]
public sealed class DoAfterSystem : EntitySystem
{
/*
* How this is currently setup (client-side):
* DoAfterGui handles the actual bars displayed above heads. It also uses FrameUpdate to flash cancellations
* DoAfterEntitySystem handles checking predictions every tick as well as removing / cancelling DoAfters due to time elapsed.
* DoAfterComponent handles network messages inbound as well as storing the DoAfter data.
* It'll also handle overall cleanup when one is removed (i.e. removing it from DoAfterGui).
*/
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
/// <summary>
/// We'll use an excess time so stuff like finishing effects can show.
@@ -43,9 +31,14 @@ namespace Content.Client.DoAfter
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeNetworkEvent<CancelledDoAfterMessage>(OnCancelledDoAfter);
SubscribeLocalEvent<DoAfterComponent, ComponentStartup>(OnDoAfterStartup);
SubscribeLocalEvent<DoAfterComponent, ComponentShutdown>(OnDoAfterShutdown);
SubscribeLocalEvent<DoAfterComponent, ComponentHandleState>(OnDoAfterHandleState);
IoCManager.Resolve<IOverlayManager>().AddOverlay(new DoAfterOverlay());
}
public override void Shutdown()
{
base.Shutdown();
IoCManager.Resolve<IOverlayManager>().RemoveOverlay<DoAfterOverlay>();
}
private void OnDoAfterHandleState(EntityUid uid, DoAfterComponent component, ref ComponentHandleState args)
@@ -86,24 +79,6 @@ namespace Content.Client.DoAfter
component.DoAfters.Add(doAfter.ID, doAfter);
}
if (component.Gui == null || component.Gui.Disposed)
return;
foreach (var (_, doAfter) in component.DoAfters)
{
component.Gui.AddDoAfter(doAfter);
}
}
private void OnDoAfterStartup(EntityUid uid, DoAfterComponent component, ComponentStartup args)
{
Enable(component);
}
private void OnDoAfterShutdown(EntityUid uid, DoAfterComponent component, ComponentShutdown args)
{
Disable(component);
}
private void OnCancelledDoAfter(CancelledDoAfterMessage ev)
@@ -113,33 +88,6 @@ namespace Content.Client.DoAfter
Cancel(doAfter, ev.ID);
}
/// <summary>
/// For handling PVS so we dispose of controls if they go out of range
/// </summary>
public void Enable(DoAfterComponent component)
{
if (component.Gui?.Disposed == false)
return;
component.Gui = new DoAfterGui {AttachedEntity = component.Owner};
foreach (var (_, doAfter) in component.DoAfters)
{
component.Gui.AddDoAfter(doAfter);
}
foreach (var (_, cancelled) in component.CancelledDoAfters)
{
component.Gui.CancelDoAfter(cancelled.ID);
}
}
public void Disable(DoAfterComponent component)
{
component.Gui?.Dispose();
component.Gui = null;
}
/// <summary>
/// Remove a DoAfter without showing a cancellation graphic.
/// </summary>
@@ -150,22 +98,10 @@ namespace Content.Client.DoAfter
var found = false;
for (var i = component.CancelledDoAfters.Count - 1; i >= 0; i--)
{
var cancelled = component.CancelledDoAfters[i];
if (cancelled.Message == clientDoAfter)
{
component.CancelledDoAfters.RemoveAt(i);
found = true;
break;
}
}
component.CancelledDoAfters.Remove(clientDoAfter.ID);
if (!found)
component.DoAfters.Remove(clientDoAfter.ID);
component.Gui?.RemoveDoAfter(clientDoAfter.ID);
}
/// <summary>
@@ -174,98 +110,70 @@ namespace Content.Client.DoAfter
/// Actual removal is handled by DoAfterEntitySystem.
public void Cancel(DoAfterComponent component, byte id)
{
foreach (var (_, cancelled) in component.CancelledDoAfters)
{
if (cancelled.ID == id)
return;
}
if (component.CancelledDoAfters.ContainsKey(id))
return;
if (!component.DoAfters.ContainsKey(id))
return;
var doAfterMessage = component.DoAfters[id];
component.CancelledDoAfters.Add((_gameTiming.CurTime, doAfterMessage));
component.Gui?.CancelDoAfter(id);
doAfterMessage.Cancelled = true;
component.CancelledDoAfters.Add(id, doAfterMessage);
}
// TODO move this to an overlay
// TODO separate DoAfter & ActiveDoAfter components for the entity query.
public override void Update(float frameTime)
{
base.Update(frameTime);
var currentTime = _gameTiming.CurTime;
var attached = _player.LocalPlayer?.ControlledEntity;
// Can't see any I guess?
if (attached == null || Deleted(attached))
if (!_gameTiming.IsFirstTimePredicted)
return;
// ReSharper disable once ConvertToLocalFunction
var predicate = static (EntityUid uid, (EntityUid compOwner, EntityUid? attachedEntity) data)
=> uid == data.compOwner || uid == data.attachedEntity;
var playerEntity = _player.LocalPlayer?.ControlledEntity;
var occluded = _examineSystem.IsOccluded(attached.Value);
var viewbox = _eyeManager.GetWorldViewport().Enlarged(2.0f);
var xforms = GetEntityQuery<TransformComponent>();
var entXform = xforms.GetComponent(attached.Value);
var playerPos = _xformSystem.GetWorldPosition(entXform, xforms);
foreach (var (comp, xform) in EntityManager.EntityQuery<DoAfterComponent, TransformComponent>(true))
foreach (var (comp, xform) in EntityQuery<DoAfterComponent, TransformComponent>())
{
var doAfters = comp.DoAfters;
if (doAfters.Count == 0 || xform.MapID != entXform.MapID)
if (doAfters.Count == 0)
{
Disable(comp);
continue;
}
var compPos = _xformSystem.GetWorldPosition(xform, xforms);
if (!viewbox.Contains(compPos))
{
Disable(comp);
continue;
}
var range = (compPos - playerPos).Length + 0.01f;
if (occluded &&
comp.Owner != attached &&
// Static ExamineSystemShared.InRangeUnOccluded has to die.
!ExamineSystemShared.InRangeUnOccluded(
new(playerPos, entXform.MapID),
new(compPos, entXform.MapID), range,
(comp.Owner, attached), predicate,
entMan: EntityManager))
{
Disable(comp);
continue;
}
Enable(comp);
var userGrid = xform.Coordinates;
var toRemove = new RemQueue<ClientDoAfter>();
// Check cancellations / finishes
foreach (var (id, doAfter) in doAfters)
{
var elapsedTime = (currentTime - doAfter.StartTime).TotalSeconds;
// If we've passed the final time (after the excess to show completion graphic) then remove.
if (elapsedTime > doAfter.Delay + ExcessTime)
if ((doAfter.Accumulator + doAfter.CancelledAccumulator) > doAfter.Delay + ExcessTime)
{
toRemove.Add(doAfter);
continue;
}
// Don't predict cancellation if it's already finished.
if (elapsedTime > doAfter.Delay)
if (doAfter.Cancelled)
{
doAfter.CancelledAccumulator += frameTime;
continue;
}
doAfter.Accumulator += frameTime;
// Well we finished so don't try to predict cancels.
if (doAfter.Accumulator > doAfter.Delay)
{
continue;
}
// Predictions
if (comp.Owner != playerEntity)
continue;
// TODO: Add these back in when I work out some system for changing the accumulation rate
// based on ping. Right now these would show as cancelled near completion if we moved at the end
// despite succeeding.
continue;
if (doAfter.BreakOnUserMove)
{
if (!userGrid.InRange(EntityManager, doAfter.UserGrid, doAfter.MovementThreshold))
@@ -292,16 +200,21 @@ namespace Content.Client.DoAfter
Remove(comp, doAfter);
}
var count = comp.CancelledDoAfters.Count;
// Remove cancelled DoAfters after ExcessTime has elapsed
for (var i = count - 1; i >= 0; i--)
var toRemoveCancelled = new List<ClientDoAfter>();
foreach (var (_, doAfter) in comp.CancelledDoAfters)
{
var cancelled = comp.CancelledDoAfters[i];
if ((currentTime - cancelled.CancelTime).TotalSeconds > ExcessTime)
if (doAfter.CancelledAccumulator > ExcessTime)
{
Remove(comp, cancelled.Message);
toRemoveCancelled.Add(doAfter);
}
}
foreach (var doAfter in toRemoveCancelled)
{
Remove(comp, doAfter);
}
}
}
}

View File

@@ -1,138 +0,0 @@
using System;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.DoAfter.UI
{
public sealed class DoAfterBar : Control
{
private IGameTiming _gameTiming = default!;
private readonly ShaderInstance _shader;
/// <summary>
/// Set from 0.0f to 1.0f to reflect bar progress
/// </summary>
public float Ratio
{
get => _ratio;
set => _ratio = value;
}
private float _ratio = 1.0f;
/// <summary>
/// Flash red until removed
/// </summary>
public bool Cancelled
{
get => _cancelled;
set
{
if (_cancelled == value)
{
return;
}
_cancelled = value;
if (_cancelled)
{
_gameTiming = IoCManager.Resolve<IGameTiming>();
_lastFlash = _gameTiming.CurTime;
}
}
}
private bool _cancelled;
/// <summary>
/// Is the cancellation bar red?
/// </summary>
private bool _flash = true;
/// <summary>
/// Last time we swapped the flash.
/// </summary>
private TimeSpan _lastFlash;
/// <summary>
/// How long each cancellation bar flash lasts in seconds.
/// </summary>
private const float FlashTime = 0.125f;
private const int XPixelDiff = 20 * DoAfterBarScale;
public const byte DoAfterBarScale = 2;
public DoAfterBar()
{
IoCManager.InjectDependencies(this);
_shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
VerticalAlignment = VAlignment.Center;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (Cancelled)
{
if ((_gameTiming.CurTime - _lastFlash).TotalSeconds > FlashTime)
{
_lastFlash = _gameTiming.CurTime;
_flash = !_flash;
}
}
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
Color color;
if (Cancelled)
{
if ((_gameTiming.CurTime - _lastFlash).TotalSeconds > FlashTime)
{
_lastFlash = _gameTiming.CurTime;
_flash = !_flash;
}
color = new Color(1.0f, 0.0f, 0.0f, _flash ? 1.0f : 0.0f);
}
else
{
color = DoAfterHelpers.GetProgressColor(Ratio);
}
handle.UseShader(_shader);
// If you want to make this less hard-coded be my guest
var leftOffset = 2 * DoAfterBarScale;
var box = new UIBox2i(
leftOffset,
-2 + 2 * DoAfterBarScale,
leftOffset + (int) (XPixelDiff * Ratio * UIScale),
-2);
handle.DrawRect(box, color);
handle.UseShader(null);
}
}
public static class DoAfterHelpers
{
public static Color GetProgressColor(float progress)
{
if (progress >= 1.0f)
{
return new Color(0f, 1f, 0f);
}
// lerp
var hue = (5f / 18f) * progress;
return Color.FromHsv((hue, 1f, 0.75f, 1f));
}
}
}

View File

@@ -1,45 +0,0 @@
using Content.Client.Resources;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Client.DoAfter.UI;
public sealed class DoAfterControl : PanelContainer
{
public float Ratio
{
get => _bar.Ratio;
set => _bar.Ratio = value;
}
public bool Cancelled
{
get => _bar.Cancelled;
set => _bar.Cancelled = value;
}
private DoAfterBar _bar;
public DoAfterControl()
{
IoCManager.InjectDependencies(this);
var cache = IoCManager.Resolve<IResourceCache>();
AddChild(new TextureRect
{
Texture = cache.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png"),
TextureScale = Vector2.One * DoAfterBar.DoAfterBarScale,
VerticalAlignment = VAlignment.Center,
});
_bar = new DoAfterBar();
AddChild(_bar);
VerticalAlignment = VAlignment.Bottom;
_bar.Measure(Vector2.Infinity);
Measure(Vector2.Infinity);
}
}

View File

@@ -1,176 +0,0 @@
using Content.Shared.DoAfter;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.DoAfter.UI
{
public sealed class DoAfterGui : BoxContainer
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private readonly Dictionary<byte, DoAfterControl> _doAfterControls = new();
// We'll store cancellations for a little bit just so we can flash the graphic to indicate it's cancelled
private readonly Dictionary<byte, TimeSpan> _cancelledDoAfters = new();
public EntityUid? AttachedEntity { get; set; }
public DoAfterGui()
{
Orientation = LayoutOrientation.Vertical;
IoCManager.InjectDependencies(this);
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(this);
SeparationOverride = 0;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (Disposed)
return;
foreach (var (_, control) in _doAfterControls)
{
control.Dispose();
}
_doAfterControls.Clear();
_cancelledDoAfters.Clear();
}
/// <summary>
/// Add the necessary control for a DoAfter progress bar.
/// </summary>
public void AddDoAfter(ClientDoAfter message)
{
if (_doAfterControls.ContainsKey(message.ID))
return;
var doAfterBar = new DoAfterControl();
AddChild(doAfterBar);
_doAfterControls.Add(message.ID, doAfterBar);
Measure(Vector2.Infinity);
}
// NOTE THAT THE BELOW ONLY HANDLES THE UI SIDE
/// <summary>
/// Removes a DoAfter without showing a cancel graphic.
/// </summary>
/// <param name="id"></param>
public void RemoveDoAfter(byte id)
{
if (!_doAfterControls.ContainsKey(id))
return;
var control = _doAfterControls[id];
RemoveChild(control);
control.DisposeAllChildren();
_doAfterControls.Remove(id);
_cancelledDoAfters.Remove(id);
}
/// <summary>
/// Cancels a DoAfter and shows a graphic indicating it has been cancelled to the player.
/// </summary>
/// Can be called multiple times on the 1 DoAfter because of the client predicting the cancellation.
/// <param name="id"></param>
public void CancelDoAfter(byte id)
{
if (_cancelledDoAfters.ContainsKey(id))
return;
if (!_doAfterControls.TryGetValue(id, out var doAfterControl))
{
doAfterControl = new DoAfterControl();
AddChild(doAfterControl);
}
doAfterControl.Cancelled = true;
_cancelledDoAfters.Add(id, _gameTiming.CurTime);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_entityManager.TryGetComponent(AttachedEntity, out DoAfterComponent? doAfterComponent))
{
Visible = false;
return;
}
var doAfters = doAfterComponent.DoAfters;
if (doAfters.Count == 0)
{
Visible = false;
return;
}
var transform = _entityManager.GetComponent<TransformComponent>(AttachedEntity.Value);
if (_eyeManager.CurrentMap != transform.MapID || !transform.Coordinates.IsValid(_entityManager))
{
Visible = false;
return;
}
Visible = true;
var currentTime = _gameTiming.CurTime;
var toRemove = new List<byte>();
// Cleanup cancelled DoAfters
foreach (var (id, cancelTime) in _cancelledDoAfters)
{
if ((currentTime - cancelTime).TotalSeconds > DoAfterSystem.ExcessTime)
toRemove.Add(id);
}
foreach (var id in toRemove)
{
RemoveDoAfter(id);
}
toRemove.Clear();
// Update 0 -> 1.0f of the things
foreach (var (id, message) in doAfters)
{
if (_cancelledDoAfters.ContainsKey(id) || !_doAfterControls.ContainsKey(id))
continue;
var control = _doAfterControls[id];
var ratio = (currentTime - message.StartTime).TotalSeconds;
control.Ratio = MathF.Min(1.0f,
(float) ratio / message.Delay);
// Just in case it doesn't get cleaned up by the system for whatever reason.
if (ratio > message.Delay + DoAfterSystem.ExcessTime)
{
toRemove.Add(id);
continue;
}
}
foreach (var id in toRemove)
{
RemoveDoAfter(id);
}
UpdatePosition(transform);
}
public void UpdatePosition(TransformComponent xform)
{
var screenCoordinates = _eyeManager.CoordinatesToScreen(xform.Coordinates);
var position = screenCoordinates.Position / UIScale - DesiredSize / 2f;
LayoutContainer.SetPosition(this, position - new Vector2(0, 40f));
}
}
}

View File

@@ -34,12 +34,20 @@ namespace Content.Shared.DoAfter
}
}
// TODO: Merge this with the actual DoAfter
/// <summary>
/// We send a trimmed-down version of the DoAfter for the client for it to use.
/// </summary>
[Serializable, NetSerializable]
public sealed class ClientDoAfter
{
public bool Cancelled = false;
/// <summary>
/// Accrued time when cancelled.
/// </summary>
public float CancelledAccumulator;
// To see what these do look at DoAfter and DoAfterEventArgs
public byte ID { get; }
@@ -51,6 +59,8 @@ namespace Content.Shared.DoAfter
public EntityUid? Target { get; }
public float Accumulator;
public float Delay { get; }
// TODO: The other ones need predicting