* do_after

Ports (most of) do_after from SS13.
Callers are expected to await the DoAfter task from the DoAfterSystem.
I had a dummy component for in-game testing which I removed for the PR so nothing in game uses do_after at the moment.
Currently only the movement cancellation is predicted client-side.

* Minor do_after doc cleanup

* do_the_shuffle

Fix nullable build errors.

* The last nullable

* Implement NeedHand

Thanks zum.

* nullable dereference

* Adjust the system query

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2020-08-09 02:16:13 +10:00
committed by GitHub
parent ee14d67756
commit 5b3b2e3207
14 changed files with 1186 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
#nullable enable
using System;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Graphics.Shaders;
using Robust.Client.UserInterface;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.GameObjects.EntitySystems.DoAfter
{
public sealed class DoAfterBar : Control
{
private IGameTiming _gameTiming = default!;
private 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;
private static readonly Color StartColor = new Color(0.8f, 0.0f, 0.2f);
private static readonly Color EndColor = new Color(0.2f, 0.4f, 1.0f);
public DoAfterBar()
{
IoCManager.InjectDependencies(this);
_shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
}
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 if (Ratio >= 1.0f)
{
color = new Color(0.92f, 0.77f, 0.34f);
}
else
{
// lerp
color = new Color(
StartColor.R + (EndColor.R - StartColor.R) * Ratio,
StartColor.G + (EndColor.G - StartColor.G) * Ratio,
StartColor.B + (EndColor.B - StartColor.B) * Ratio,
StartColor.A);
}
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),
-2);
handle.DrawRect(box, color);
}
}
}

View File

@@ -0,0 +1,184 @@
#nullable enable
using System;
using System.Collections.Generic;
using Content.Client.GameObjects.Components;
using Content.Client.Utility;
using Content.Shared.GameObjects.Components;
using Robust.Client.Interfaces.Graphics.ClientEye;
using Robust.Client.Interfaces.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Client.GameObjects.EntitySystems.DoAfter
{
public sealed class DoAfterGui : VBoxContainer
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private Dictionary<byte, PanelContainer> _doAfterControls = new Dictionary<byte, PanelContainer>();
private Dictionary<byte, DoAfterBar> _doAfterBars = new Dictionary<byte, DoAfterBar>();
// We'll store cancellations for a little bit just so we can flash the graphic to indicate it's cancelled
private Dictionary<byte, TimeSpan> _cancelledDoAfters = new Dictionary<byte, TimeSpan>();
public IEntity? AttachedEntity { get; set; }
private ScreenCoordinates _playerPosition;
// This behavior probably shouldn't be happening; so for whatever reason the control position is set the frame after
// I got NFI why because I don't know the UI internals
private bool _firstDraw = true;
public DoAfterGui()
{
IoCManager.InjectDependencies(this);
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(this);
SeparationOverride = 0;
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Begin);
}
/// <summary>
/// Add the necessary control for a DoAfter progress bar.
/// </summary>
/// <param name="message"></param>
public void AddDoAfter(DoAfterMessage message)
{
if (_doAfterControls.ContainsKey(message.ID))
{
return;
}
var doAfterBar = new DoAfterBar
{
SizeFlagsVertical = SizeFlags.ShrinkCenter
};
_doAfterBars[message.ID] = doAfterBar;
var control = new PanelContainer
{
Children =
{
new TextureRect
{
Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png"),
TextureScale = Vector2.One * DoAfterBar.DoAfterBarScale,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
},
doAfterBar
}
};
AddChild(control);
_doAfterControls.Add(message.ID, control);
}
// 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);
_doAfterControls.Remove(id);
_doAfterBars.Remove(id);
if (_cancelledDoAfters.ContainsKey(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;
}
var control = _doAfterControls[id];
_doAfterBars[id].Cancelled = true;
_cancelledDoAfters.Add(id, _gameTiming.CurTime);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (AttachedEntity == null || !AttachedEntity.TryGetComponent(out DoAfterComponent doAfterComponent))
{
return;
}
var doAfters = doAfterComponent.DoAfters;
// Nothing to render so we'll hide.
if (doAfters.Count == 0 && _cancelledDoAfters.Count == 0)
{
_firstDraw = true;
Visible = false;
return;
}
// Set position ready for 2nd+ frames.
_playerPosition = _eyeManager.WorldToScreen(AttachedEntity.Transform.GridPosition);
LayoutContainer.SetPosition(this, new Vector2(_playerPosition.X - Width / 2, _playerPosition.Y - Height - 30.0f));
if (_firstDraw)
{
_firstDraw = false;
return;
}
Visible = true;
var currentTime = _gameTiming.CurTime;
var toCancel = new List<byte>();
// Cleanup cancelled DoAfters
foreach (var (id, cancelTime) in _cancelledDoAfters)
{
if ((currentTime - cancelTime).TotalSeconds > DoAfterSystem.ExcessTime)
{
toCancel.Add(id);
}
}
foreach (var id in toCancel)
{
RemoveDoAfter(id);
}
// Update 0 -> 1.0f of the things
foreach (var (id, message) in doAfters)
{
if (_cancelledDoAfters.ContainsKey(id) || !_doAfterControls.ContainsKey(id))
{
continue;
}
var doAfterBar = _doAfterBars[id];
doAfterBar.Ratio = MathF.Min(1.0f,
(float) (currentTime - message.StartTime).TotalSeconds / message.Delay);
}
}
}
}

View File

@@ -0,0 +1,150 @@
#nullable enable
using System.Linq;
using Content.Client.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects.EntitySystems;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
namespace Content.Client.GameObjects.EntitySystems.DoAfter
{
/// <summary>
/// Handles events that need to happen after a certain amount of time where the event could be cancelled by factors
/// such as moving.
/// </summary>
[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 IGameTiming _gameTiming = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
/// <summary>
/// Rather than checking attached player every tick we'll just store it from the message.
/// </summary>
private IEntity? _player;
/// <summary>
/// We'll use an excess time so stuff like finishing effects can show.
/// </summary>
public const float ExcessTime = 0.5f;
public DoAfterGui? Gui { get; private set; }
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerAttachSysMessage>(message => HandlePlayerAttached(message.AttachedEntity));
}
public override void Shutdown()
{
base.Shutdown();
Gui?.Dispose();
Gui = null;
}
private void HandlePlayerAttached(IEntity? entity)
{
_player = entity;
// Setup the GUI and pass the new data to it if applicable.
Gui?.Dispose();
if (entity == null)
{
return;
}
Gui ??= new DoAfterGui();
Gui.AttachedEntity = entity;
if (entity.TryGetComponent(out DoAfterComponent doAfterComponent))
{
foreach (var (_, doAfter) in doAfterComponent.DoAfters)
{
Gui.AddDoAfter(doAfter);
}
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var currentTime = _gameTiming.CurTime;
if (_player == null || !_player.TryGetComponent(out DoAfterComponent doAfterComponent))
{
return;
}
var doAfters = doAfterComponent.DoAfters.ToList();
if (doAfters.Count == 0)
{
return;
}
var userGrid = _player.Transform.GridPosition;
// 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)
{
Gui?.RemoveDoAfter(id);
doAfterComponent.Remove(doAfter);
continue;
}
// Don't predict cancellation if it's already finished.
if (elapsedTime > doAfter.Delay)
{
continue;
}
// Predictions
if (doAfter.BreakOnUserMove)
{
if (userGrid != doAfter.UserGrid)
{
doAfterComponent.Cancel(id, currentTime);
continue;
}
}
if (doAfter.BreakOnTargetMove)
{
var targetEntity = _entityManager.GetEntity(doAfter.TargetUid);
if (targetEntity.Transform.GridPosition != doAfter.TargetGrid)
{
doAfterComponent.Cancel(id, currentTime);
continue;
}
}
}
var count = doAfterComponent.CancelledDoAfters.Count;
// Remove cancelled DoAfters after ExcessTime has elapsed
for (var i = count - 1; i >= 0; i--)
{
var cancelled = doAfterComponent.CancelledDoAfters[i];
if ((currentTime - cancelled.CancelTime).TotalSeconds > ExcessTime)
{
doAfterComponent.Remove(cancelled.Message);
}
}
}
}
}