Remove explosion networking jank (#12733)

This commit is contained in:
Leon Friedrich
2022-11-27 23:24:35 +13:00
committed by GitHub
parent 85ce28c0a2
commit 2dc7663d1a
12 changed files with 237 additions and 326 deletions

View File

@@ -1,11 +1,8 @@
using Content.Shared.CCVar;
using Content.Shared.Explosion;
using Content.Shared.GameTicking;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Client.Explosion;
@@ -16,12 +13,9 @@ namespace Content.Client.Explosion;
/// </summary>
public sealed class ExplosionOverlaySystem : EntitySystem
{
private ExplosionOverlay _overlay = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
[Dependency] private readonly IResourceCache _resCache = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
/// <summary>
/// For how many seconds should an explosion stay on-screen once it has finished expanding?
@@ -32,209 +26,57 @@ public sealed class ExplosionOverlaySystem : EntitySystem
{
base.Initialize();
SubscribeNetworkEvent<ExplosionEvent>(OnExplosion);
SubscribeNetworkEvent<ExplosionOverlayUpdateEvent>(HandleExplosionUpdate);
SubscribeLocalEvent<MapChangedEvent>(OnMapChanged);
SubscribeAllEvent<RoundRestartCleanupEvent>(OnReset);
_cfg.OnValueChanged(CCVars.ExplosionPersistence, SetExplosionPersistence, true);
var overlayManager = IoCManager.Resolve<IOverlayManager>();
_overlay = new ExplosionOverlay();
if (!overlayManager.HasOverlay<ExplosionOverlay>())
overlayManager.AddOverlay(_overlay);
SubscribeLocalEvent<ExplosionVisualsComponent, ComponentInit>(OnExplosionInit);
SubscribeLocalEvent<ExplosionVisualsComponent, ComponentRemove>(OnCompRemove);
SubscribeLocalEvent<ExplosionVisualsComponent, ComponentHandleState>(OnExplosionHandleState);
_overlayMan.AddOverlay(new ExplosionOverlay());
}
private void OnReset(RoundRestartCleanupEvent ev)
private void OnExplosionHandleState(EntityUid uid, ExplosionVisualsComponent component, ref ComponentHandleState args)
{
// Not sure if round restart cleans up client-side entities, but better safe than sorry.
foreach (var exp in _overlay.CompletedExplosions)
{
QueueDel(exp.LightEntity);
}
if (_overlay.ActiveExplosion != null)
QueueDel(_overlay.ActiveExplosion.LightEntity);
_overlay.CompletedExplosions.Clear();
_overlay.ActiveExplosion = null;
_overlay.Index = 0;
if (args.Current is not ExplosionVisualsState state)
return;
component.Epicenter = state.Epicenter;
component.SpaceTiles = state.SpaceTiles;
component.Tiles = state.Tiles;
component.Intensity = state.Intensity;
component.ExplosionType = state.ExplosionType;
component.SpaceMatrix = state.SpaceMatrix;
component.SpaceTileSize = state.SpaceTileSize;
}
private void OnMapChanged(MapChangedEvent ev)
private void OnCompRemove(EntityUid uid, ExplosionVisualsComponent component, ComponentRemove args)
{
if (ev.Created)
return;
if (_overlay.ActiveExplosion?.Map == ev.Map)
_overlay.ActiveExplosion = null;
_overlay.CompletedExplosions.RemoveAll(exp => exp.Map == ev.Map);
QueueDel(component.LightEntity);
}
private void SetExplosionPersistence(float value) => ExplosionPersistence = value;
public override void FrameUpdate(float frameTime)
private void OnExplosionInit(EntityUid uid, ExplosionVisualsComponent component, ComponentInit args)
{
base.FrameUpdate(frameTime);
// increment the lifetime of completed explosions, and remove them if they have been ons screen for more
// than ExplosionPersistence seconds
for (int i = _overlay.CompletedExplosions.Count - 1; i>= 0; i--)
{
var explosion = _overlay.CompletedExplosions[i];
if (_mapMan.IsMapPaused(explosion.Map))
continue;
explosion.Lifetime += frameTime;
if (explosion.Lifetime >= ExplosionPersistence)
{
EntityManager.QueueDeleteEntity(explosion.LightEntity);
// Remove-swap
_overlay.CompletedExplosions[i] = _overlay.CompletedExplosions[^1];
_overlay.CompletedExplosions.RemoveAt(_overlay.CompletedExplosions.Count - 1);
}
}
}
/// <summary>
/// The server has processed some explosion. This updates the client-side overlay so that the area covered
/// by the fire-visual matches up with the area that the explosion has affected.
/// </summary>
private void HandleExplosionUpdate(ExplosionOverlayUpdateEvent args)
{
if (args.ExplosionId != _overlay.ActiveExplosion?.Explosionid && !IsNewer(args.ExplosionId))
{
// out of order events. Ignore.
return;
}
_overlay.Index = args.Index;
if (_overlay.ActiveExplosion == null)
{
// no active explosion... events out of order?
return;
}
if (args.Index != int.MaxValue)
if (!_protoMan.TryIndex(component.ExplosionType, out ExplosionPrototype? type))
return;
// the explosion has finished expanding
_overlay.Index = 0;
_overlay.CompletedExplosions.Add(_overlay.ActiveExplosion);
_overlay.ActiveExplosion = null;
}
/// <summary>
/// A new explosion occurred. This prepares the client-side light entity and stores the
/// explosion/fire-effect overlay data.
/// </summary>
private void OnExplosion(ExplosionEvent args)
{
if (!_protoMan.TryIndex(args.TypeID, out ExplosionPrototype? type))
return;
// spawn in a light source at the epicenter
var lightEntity = Spawn("ExplosionLight", args.Epicenter);
// spawn in a client-side light source at the epicenter
var lightEntity = Spawn("ExplosionLight", component.Epicenter);
var light = EnsureComp<PointLightComponent>(lightEntity);
light.Energy = light.Radius = args.Intensity.Count;
light.Energy = light.Radius = component.Intensity.Count;
light.Color = type.LightColor;
component.LightEntity = lightEntity;
component.FireColor = type.FireColor;
component.IntensityPerState = type.IntensityPerState;
if (_overlay.ActiveExplosion == null)
var fireRsi = _resCache.GetResource<RSIResource>(type.TexturePath).RSI;
foreach (var state in fireRsi)
{
_overlay.ActiveExplosion = new(args, type, lightEntity, _resCache);
return;
component.FireFrames.Add(state.GetFrames(RSI.State.Direction.South));
if (component.FireFrames.Count == type.FireStates)
break;
}
// we have a currently active explosion. Can happen when events are received out of order. either multiple
// explosions are happening in one tick, or a new explosion was received before the event telling us the old one
// finished got through.
if (IsNewer(args.ExplosionId))
{
// This is a newer explosion. Add the old-currently-active explosions to the completed list
_overlay.CompletedExplosions.Add(_overlay.ActiveExplosion);
_overlay.ActiveExplosion = new(args, type, lightEntity, _resCache);
}
else
{
// explosions were out of order. keep the active one, and directly add the received one to the completed
// list.
_overlay.CompletedExplosions.Add(new(args, type, lightEntity, _resCache));
return;
}
}
public bool IsNewer(int explosionId)
{
if (_overlay.ActiveExplosion == null)
return true;
// If we ever get servers stable enough to live this long, the explosion Id int might overflow.
return _overlay.ActiveExplosion.Explosionid < explosionId
|| _overlay.ActiveExplosion.Explosionid > int.MaxValue/2 && explosionId < int.MinValue/2;
}
public override void Shutdown()
{
base.Shutdown();
_cfg.UnsubValueChanged(CCVars.ExplosionPersistence, SetExplosionPersistence);
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (overlayManager.HasOverlay<ExplosionOverlay>())
overlayManager.RemoveOverlay<ExplosionOverlay>();
}
}
internal sealed class Explosion
{
public readonly Dictionary<int, List<Vector2i>>? SpaceTiles;
public readonly Dictionary<EntityUid, Dictionary<int, List<Vector2i>>> Tiles;
public readonly List<float> Intensity;
public readonly EntityUid LightEntity;
public readonly MapId Map;
public readonly int Explosionid;
public readonly ushort SpaceTileSize;
public readonly float IntensityPerState;
public readonly Matrix3 SpaceMatrix;
/// <summary>
/// How long have we been drawing this explosion, starting from the time the explosion was fully drawn.
/// </summary>
public float Lifetime;
/// <summary>
/// The textures used for the explosion fire effect. Each fire-state is associated with an explosion
/// intensity range, and each stat itself has several textures.
/// </summary>
public readonly List<Texture[]> FireFrames = new();
public readonly Color? FireColor;
internal Explosion(ExplosionEvent args, ExplosionPrototype type, EntityUid lightEntity, IResourceCache resCache)
{
Map = args.Epicenter.MapId;
SpaceTiles = args.SpaceTiles;
Tiles = args.Tiles;
Intensity = args.Intensity;
SpaceMatrix = args.SpaceMatrix;
Explosionid = args.ExplosionId;
FireColor = type.FireColor;
LightEntity = lightEntity;
SpaceTileSize = args.SpaceTileSize;
IntensityPerState = type.IntensityPerState;
var fireRsi = resCache.GetResource<RSIResource>(type.TexturePath).RSI;
foreach (var state in fireRsi)
{
FireFrames.Add(state.GetFrames(RSI.State.Direction.South));
if (FireFrames.Count == type.FireStates)
break;
}
_overlayMan.RemoveOverlay<ExplosionOverlay>();
}
}