| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- using System.Diagnostics;
- using System.Linq;
- using Content.Server.Administration.Managers;
- using Content.Server.NodeContainer.NodeGroups;
- using Content.Server.NodeContainer.Nodes;
- using Content.Shared.Administration;
- using Content.Shared.NodeContainer;
- using JetBrains.Annotations;
- using Robust.Server.Player;
- using Robust.Shared.Enums;
- using Robust.Shared.Map.Components;
- using Robust.Shared.Player;
- using Robust.Shared.Utility;
- namespace Content.Server.NodeContainer.EntitySystems
- {
- /// <summary>
- /// Entity system that manages <see cref="NodeGroupSystem"/> and <see cref="Node"/> updating.
- /// </summary>
- /// <seealso cref="NodeContainerSystem"/>
- [UsedImplicitly]
- public sealed class NodeGroupSystem : EntitySystem
- {
- [Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly IAdminManager _adminManager = default!;
- [Dependency] private readonly INodeGroupFactory _nodeGroupFactory = default!;
- [Dependency] private readonly ILogManager _logManager = default!;
- private readonly List<int> _visDeletes = new();
- private readonly List<BaseNodeGroup> _visSends = new();
- private readonly HashSet<ICommonSession> _visPlayers = new();
- private readonly HashSet<BaseNodeGroup> _toRemake = new();
- private readonly HashSet<BaseNodeGroup> _nodeGroups = new();
- private readonly HashSet<Node> _toRemove = new();
- private readonly List<Node> _toReflood = new();
- private ISawmill _sawmill = default!;
- private const float VisDataUpdateInterval = 1;
- private float _accumulatedFrameTime;
- public bool VisEnabled => _visPlayers.Count != 0;
- private int _gen = 1;
- private int _groupNetIdCounter = 1;
- /// <summary>
- /// If true, UpdateGrid() will not process grids.
- /// </summary>
- /// <remarks>
- /// Useful if something like a large explosion is in the process of shredding the grid, as it avoids uneccesary
- /// updating.
- /// </remarks>
- public bool PauseUpdating = false;
- public override void Initialize()
- {
- base.Initialize();
- _sawmill = _logManager.GetSawmill("nodegroup");
- _playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
- SubscribeNetworkEvent<NodeVis.MsgEnable>(HandleEnableMsg);
- }
- public override void Shutdown()
- {
- base.Shutdown();
- _playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
- }
- private void HandleEnableMsg(NodeVis.MsgEnable msg, EntitySessionEventArgs args)
- {
- var session = args.SenderSession;
- if (!_adminManager.HasAdminFlag(session, AdminFlags.Debug))
- return;
- if (msg.Enabled)
- {
- _visPlayers.Add(session);
- VisSendFullStateImmediate(session);
- }
- else
- {
- _visPlayers.Remove(session);
- }
- }
- private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
- {
- if (e.NewStatus == SessionStatus.Disconnected)
- _visPlayers.Remove(e.Session);
- }
- public void QueueRemakeGroup(BaseNodeGroup group)
- {
- if (group.Remaking)
- return;
- _toRemake.Add(group);
- group.Remaking = true;
- foreach (var node in group.Nodes)
- {
- QueueReflood(node);
- }
- if (group.NodeCount == 0)
- {
- _nodeGroups.Remove(group);
- }
- }
- public void QueueReflood(Node node)
- {
- if (node.FlaggedForFlood)
- return;
- _toReflood.Add(node);
- node.FlaggedForFlood = true;
- }
- public void QueueNodeRemove(Node node)
- {
- _toRemove.Add(node);
- }
- public void CreateSingleNetImmediate(Node node)
- {
- if (node.NodeGroup != null)
- return;
- QueueReflood(node);
- InitGroup(node, new List<Node> {node});
- }
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
- if (!PauseUpdating)
- {
- DoGroupUpdates();
- VisDoUpdate(frameTime);
- }
- }
- // used to manually force an update for the groups
- // the VisDoUpdate will be done with the next scheduled update
- public void ForceUpdate()
- {
- DoGroupUpdates();
- }
- private void DoGroupUpdates()
- {
- // "Why is there a separate queue for group remakes and node refloods when they both cause eachother"
- // Future planning for the potential ability to do more intelligent group updating.
- if (_toRemake.Count == 0 && _toReflood.Count == 0 && _toRemove.Count == 0)
- return;
- var sw = Stopwatch.StartNew();
- var xformQuery = GetEntityQuery<TransformComponent>();
- var nodeQuery = GetEntityQuery<NodeContainerComponent>();
- foreach (var toRemove in _toRemove)
- {
- if (toRemove.NodeGroup == null)
- continue;
- var group = (BaseNodeGroup) toRemove.NodeGroup;
- group.RemoveNode(toRemove);
- toRemove.NodeGroup = null;
- QueueRemakeGroup(group);
- }
- // Break up all remaking groups.
- // Don't clear the list yet, we'll come back to these later.
- foreach (var toRemake in _toRemake)
- {
- QueueRemakeGroup(toRemake);
- }
- _gen += 1;
- // Go over all nodes to calculate reachable nodes and make an undirected graph out of them.
- // Node.GetReachableNodes() may return results asymmetrically,
- // i.e. node A may return B, but B may not return A.
- //
- // Must be for loop to allow concurrent modification from RemakeGroupImmediate.
- for (var i = 0; i < _toReflood.Count; i++)
- {
- var node = _toReflood[i];
- if (node.Deleting)
- continue;
- ClearReachableIfNecessary(node);
- if (node.NodeGroup?.Remaking == false)
- {
- QueueRemakeGroup((BaseNodeGroup) node.NodeGroup);
- }
- // GetCompatibleNodes will involve getting the transform & grid as most connection requirements are
- // based on position & anchored neighbours However, here more than one node could be attached to the
- // same parent. So there is probably a better way of doing this.
- foreach (var compatible in GetCompatibleNodes(node, xformQuery, nodeQuery))
- {
- ClearReachableIfNecessary(compatible);
- if (compatible.NodeGroup?.Remaking == false)
- {
- // We are expanding into an existing group,
- // remake it so that we can treat it uniformly.
- var group = (BaseNodeGroup) compatible.NodeGroup;
- QueueRemakeGroup(group);
- }
- node.ReachableNodes.Add(compatible);
- compatible.ReachableNodes.Add(node);
- }
- }
- var newGroups = new List<BaseNodeGroup>();
- // Flood fill over nodes. Every node will only be flood filled once.
- foreach (var node in _toReflood)
- {
- node.FlaggedForFlood = false;
- // Check if already flood filled.
- if (node.FloodGen == _gen || node.Deleting)
- continue;
- // Flood fill
- var groupNodes = FloodFillNode(node);
- var newGroup = InitGroup(node, groupNodes);
- newGroups.Add(newGroup);
- }
- // Go over dead groups that need to be cleaned up.
- // Tell them to push their data to new groups too.
- foreach (var oldGroup in _toRemake)
- {
- // Group by the NEW group.
- var newGrouped = oldGroup.Nodes.GroupBy(n => n.NodeGroup);
- oldGroup.Removed = true;
- oldGroup.AfterRemake(newGrouped);
- _nodeGroups.Remove(oldGroup);
- if (VisEnabled)
- _visDeletes.Add(oldGroup.NetId);
- }
- var refloodCount = _toReflood.Count;
- _toReflood.Clear();
- _toRemake.Clear();
- _toRemove.Clear();
- // notify entities that node groups have been updated, so they can do things like update their visuals.
- HashSet<EntityUid> entities = new();
- foreach (var group in newGroups)
- {
- foreach (var node in group.Nodes)
- {
- entities.Add(node.Owner);
- }
- }
- foreach (var uid in entities)
- {
- var ev = new NodeGroupsRebuilt(uid);
- RaiseLocalEvent(uid, ref ev, true);
- }
- _sawmill.Debug($"Updated node groups in {sw.Elapsed.TotalMilliseconds}ms. {newGroups.Count} new groups, {refloodCount} nodes processed.");
- }
- private void ClearReachableIfNecessary(Node node)
- {
- if (node.UndirectGen != _gen)
- {
- node.ReachableNodes.Clear();
- node.UndirectGen = _gen;
- }
- }
- private BaseNodeGroup InitGroup(Node node, List<Node> groupNodes)
- {
- var newGroup = (BaseNodeGroup) _nodeGroupFactory.MakeNodeGroup(node.NodeGroupID);
- newGroup.Initialize(node, EntityManager);
- newGroup.NetId = _groupNetIdCounter++;
- var netIdCounter = 0;
- foreach (var groupNode in groupNodes)
- {
- groupNode.NodeGroup = newGroup;
- groupNode.NetId = ++netIdCounter;
- }
- newGroup.LoadNodes(groupNodes);
- _nodeGroups.Add(newGroup);
- if (VisEnabled)
- _visSends.Add(newGroup);
- return newGroup;
- }
- private List<Node> FloodFillNode(Node rootNode)
- {
- // All nodes we're filling into that currently have NO network.
- var allNodes = new List<Node>();
- var stack = new Stack<Node>();
- stack.Push(rootNode);
- rootNode.FloodGen = _gen;
- while (stack.TryPop(out var node))
- {
- allNodes.Add(node);
- foreach (var reachable in node.ReachableNodes)
- {
- if (reachable.FloodGen == _gen)
- continue;
- reachable.FloodGen = _gen;
- stack.Push(reachable);
- }
- }
- return allNodes;
- }
- private IEnumerable<Node> GetCompatibleNodes(Node node, EntityQuery<TransformComponent> xformQuery, EntityQuery<NodeContainerComponent> nodeQuery)
- {
- var xform = xformQuery.GetComponent(node.Owner);
- TryComp<MapGridComponent>(xform.GridUid, out var grid);
- if (!node.Connectable(EntityManager, xform))
- yield break;
- foreach (var reachable in node.GetReachableNodes(xform, nodeQuery, xformQuery, grid, EntityManager))
- {
- DebugTools.Assert(reachable != node, "GetReachableNodes() should not include self.");
- if (reachable.NodeGroupID == node.NodeGroupID
- && reachable.Connectable(EntityManager, xformQuery.GetComponent(reachable.Owner)))
- {
- yield return reachable;
- }
- }
- }
- private void VisDoUpdate(float frametime)
- {
- if (_visPlayers.Count == 0)
- return;
- _accumulatedFrameTime += frametime;
- if (_accumulatedFrameTime < VisDataUpdateInterval
- && _visSends.Count == 0
- && _visDeletes.Count == 0)
- return;
- var msg = new NodeVis.MsgData();
- msg.GroupDeletions.AddRange(_visDeletes);
- msg.Groups.AddRange(_visSends.Select(VisMakeGroupState));
- if (_accumulatedFrameTime > VisDataUpdateInterval)
- {
- _accumulatedFrameTime -= VisDataUpdateInterval;
- foreach (var group in _nodeGroups)
- {
- if (_visSends.Contains(group))
- continue;
- msg.GroupDataUpdates.Add(group.NetId, group.GetDebugData());
- }
- }
- _visSends.Clear();
- _visDeletes.Clear();
- foreach (var player in _visPlayers)
- {
- RaiseNetworkEvent(msg, player.Channel);
- }
- }
- private void VisSendFullStateImmediate(ICommonSession player)
- {
- var msg = new NodeVis.MsgData();
- foreach (var network in _nodeGroups)
- {
- msg.Groups.Add(VisMakeGroupState(network));
- }
- RaiseNetworkEvent(msg, player.Channel);
- }
- private NodeVis.GroupData VisMakeGroupState(BaseNodeGroup group)
- {
- return new()
- {
- NetId = group.NetId,
- GroupId = group.GroupId.ToString(),
- Color = CalcNodeGroupColor(group),
- Nodes = group.Nodes.Select(n => new NodeVis.NodeDatum
- {
- Name = n.Name,
- NetId = n.NetId,
- Reachable = n.ReachableNodes.Select(r => r.NetId).ToArray(),
- Entity = GetNetEntity(n.Owner),
- Type = n.GetType().Name
- }).ToArray(),
- DebugData = group.GetDebugData()
- };
- }
- private static Color CalcNodeGroupColor(BaseNodeGroup group)
- {
- return group.GroupId switch
- {
- NodeGroupID.HVPower => Color.Orange,
- NodeGroupID.MVPower => Color.Yellow,
- NodeGroupID.Apc => Color.LimeGreen,
- NodeGroupID.AMEngine => Color.Purple,
- NodeGroupID.Pipe => Color.Blue,
- NodeGroupID.WireNet => Color.DarkMagenta,
- NodeGroupID.Teg => Color.Red,
- _ => Color.White
- };
- }
- }
- /// <summary>
- /// Event raised after node groups have been updated. Directed at any entity with a <see
- /// cref="NodeContainerComponent"/> that had a relevant node.
- /// </summary>
- [ByRefEvent]
- public readonly struct NodeGroupsRebuilt
- {
- public readonly EntityUid NodeOwner;
- public NodeGroupsRebuilt(EntityUid nodeOwner)
- {
- NodeOwner = nodeOwner;
- }
- }
- }
|