using System.Diagnostics.CodeAnalysis; using Content.Shared.Xenoarchaeology.Artifact.Components; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared.Xenoarchaeology.Artifact; /// /// User-friendly API for viewing and modifying the complex graph relationship in XenoArtifacts /// public abstract partial class SharedXenoArtifactSystem { /// /// Gets the index, corresponding to a given node, throwing if the node is not present. /// public int GetIndex(Entity ent, EntityUid node) { if (TryGetIndex((ent, ent), node, out var index)) { return index.Value; } throw new ArgumentException($"node {ToPrettyString(node)} is not present in {ToPrettyString(ent)}"); } /// /// Tries to get index inside nodes collection, corresponding to a given node EntityUid. /// public bool TryGetIndex(Entity ent, EntityUid node, [NotNullWhen(true)] out int? index) { index = null; if (!Resolve(ent, ref ent.Comp)) return false; for (var i = 0; i < ent.Comp.NodeVertices.Length; i++) { if (!TryGetNode(ent, i, out var iNode)) continue; if (node != iNode.Value.Owner) continue; index = i; return true; } return false; } /// /// Gets node entity with node component from artifact by index of node inside artifact nodes collection. /// /// Throws if requested index doesn't exist on artifact. public Entity GetNode(Entity ent, int index) { if (ent.Comp.NodeVertices[index] is { } netUid && GetEntity(netUid) is var uid && _nodeQuery.TryComp(uid, out var comp)) return (uid, comp); throw new ArgumentException($"index {index} does not correspond to an existing node in {ToPrettyString(ent)}"); } /// /// Tries to get node entity with node component from artifact by index of node inside artifact nodes collection. /// public bool TryGetNode(Entity ent, int index, [NotNullWhen(true)] out Entity? node) { node = null; if (!Resolve(ent, ref ent.Comp)) return false; if (index < 0 || index >= ent.Comp.NodeVertices.Length) return false; if (ent.Comp.NodeVertices[index] is { } netUid && GetEntity(netUid) is var uid && _nodeQuery.TryComp(uid, out var comp)) node = (uid, comp); return node != null; } /// /// Gets the index of the first empty spot in the NodeVertices array. /// If there is none, resizes both arrays and returns the new index. /// public int GetFreeNodeIndex(Entity ent) { var length = ent.Comp.NodeVertices.Length; for (var i = 0; i < length; i++) { if (ent.Comp.NodeVertices[i] == null) return i; } ResizeNodeGraph(ent, length + 1); return length; } /// /// Extracts node entities from artifact container /// (uses pre-cached and mapping from NetEntity). /// public IEnumerable> GetAllNodes(Entity ent) { foreach (var netNode in ent.Comp.NodeVertices) { if (TryGetEntity(netNode, out var node) && _nodeQuery.TryComp(node, out var comp)) yield return (node.Value, comp); } } /// /// Extracts enumeration of all indices that artifact node container have. /// public IEnumerable GetAllNodeIndices(Entity ent) { for (var i = 0; i < ent.Comp.NodeVertices.Length; i++) { if (ent.Comp.NodeVertices[i] is not null) yield return i; } } /// /// Adds edge between artifact nodes - and /// /// Artifact entity that contains 'from' and 'to' node entities. /// Node from which we need to draw edge. /// Node to which we need to draw edge. /// /// Marker, if we need to recalculate caches and mark related components dirty to update on client side. /// Should be disabled for initial graph creation to not recalculate cache on each node/edge. /// /// True if adding edge was successful, false otherwise. public bool AddEdge(Entity ent, EntityUid from, EntityUid to, bool dirty = true) { if (!Resolve(ent, ref ent.Comp)) return false; if (!TryGetIndex(ent, from, out var fromIdx) || !TryGetIndex(ent, to, out var toIdx)) return false; return AddEdge(ent, fromIdx.Value, toIdx.Value, dirty: dirty); } /// /// Adds edge between artifact nodes by indices inside node container - and /// /// Artifact entity that contains 'from' and 'to' node entities. /// Node index inside artifact node container, from which we need to draw edge. /// Node index inside artifact node container, to which we need to draw edge. /// /// Marker, if we need to recalculate caches and mark related components dirty to update on client side. /// Should be disabled for initial graph creation to not recalculate cache on each node/edge. /// /// True if adding edge was successful, false otherwise. public bool AddEdge(Entity ent, int fromIdx, int toIdx, bool dirty = true) { if (!Resolve(ent, ref ent.Comp)) return false; DebugTools.Assert(fromIdx >= 0 && fromIdx < ent.Comp.NodeVertices.Length, $"fromIdx is out of bounds for fromIdx {fromIdx}"); DebugTools.Assert(toIdx >= 0 && toIdx < ent.Comp.NodeVertices.Length, $"toIdx is out of bounds for toIdx {toIdx}"); if (ent.Comp.NodeAdjacencyMatrix[fromIdx][toIdx]) return false; //Edge already exists // TODO: add a safety check to prohibit cyclic paths. ent.Comp.NodeAdjacencyMatrix[fromIdx][toIdx] = true; if (dirty) { RebuildXenoArtifactMetaData(ent); } return true; } /// /// Removes edge between artifact nodes. /// /// Artifact entity that contains 'from' and 'to' node entities. /// Entity of node from which edge to remove is connected. /// Entity of node to which edge to remove is connected. /// /// Marker, if we need to recalculate caches and mark related components dirty to update on client side. /// Should be disabled for initial graph creation to not recalculate cache on each node/edge. /// /// True if removed edge was successfully, false otherwise. public bool RemoveEdge(Entity ent, EntityUid from, EntityUid to, bool dirty = true) { if (!Resolve(ent, ref ent.Comp)) return false; if (!TryGetIndex(ent, from, out var fromIdx) || !TryGetIndex(ent, to, out var toIdx)) return false; return RemoveEdge(ent, fromIdx.Value, toIdx.Value, dirty); } /// /// Removes edge between artifact nodes. /// /// Artifact entity that contains 'from' and 'to' node entities. /// First node index inside artifact node container, from which we need to remove connecting edge. /// Other node index inside artifact node container, from which we need to remove connecting edge. /// /// Marker, if we need to recalculate caches and mark related components dirty to update on client side. /// Should be disabled for initial graph creation to not recalculate cache on each node/edge. /// /// True if removed edge was successfully, false otherwise. public bool RemoveEdge(Entity ent, int fromIdx, int toIdx, bool dirty = true) { if (!Resolve(ent, ref ent.Comp)) return false; DebugTools.Assert(fromIdx >= 0 && fromIdx < ent.Comp.NodeVertices.Length, $"fromIdx is out of bounds for fromIdx {fromIdx}"); DebugTools.Assert(toIdx >= 0 && toIdx < ent.Comp.NodeVertices.Length, $"toIdx is out of bounds for toIdx {toIdx}"); if (!ent.Comp.NodeAdjacencyMatrix[fromIdx][toIdx]) return false; //Edge doesn't exist ent.Comp.NodeAdjacencyMatrix[fromIdx][toIdx] = false; if (dirty) { RebuildXenoArtifactMetaData(ent); } return true; } /// /// Creates node entity (spawns) and adds node into artifact node container. /// /// Artifact entity, to container of which node should be added. /// EntProtoId of node to be added. /// Created node or null. /// /// Marker, if we need to recalculate caches and mark related components dirty to update on client side. /// Should be disabled for initial graph creation to not recalculate cache on each node/edge. /// /// True if node creation and adding was successful, false otherwise. public bool AddNode( Entity ent, EntProtoId entProtoId, [NotNullWhen(true)] out Entity? node, bool dirty = true ) { node = null; if (!Resolve(ent, ref ent.Comp)) return false; var uid = Spawn(entProtoId); var comp = EnsureComp(uid); node = (uid, comp); return AddNode(ent, (node.Value, node.Value.Comp), dirty: dirty); } /// /// Adds node entity to artifact node container. /// /// Artifact entity, to container of which node should be added. /// Node entity to add. /// /// Marker, if we need to recalculate caches and mark related components dirty to update on client side. /// Should be disabled for initial graph creation to not recalculate cache on each node/edge. /// /// True if node adding was successful, false otherwise. public bool AddNode(Entity ent, Entity node, bool dirty = true) { if (!Resolve(ent, ref ent.Comp) || !Resolve(node, ref node.Comp, false)) return false; node.Comp.Attached = ent.Owner; var nodeIdx = GetFreeNodeIndex((ent, ent.Comp)); _container.Insert(node.Owner, ent.Comp.NodeContainer); ent.Comp.NodeVertices[nodeIdx] = GetNetEntity(node); Dirty(node); if (dirty) { RebuildXenoArtifactMetaData(ent); } return true; } /// /// Removes artifact node from artifact node container. /// /// Artifact from container of which node should be removed /// Node entity to be removed. /// /// Marker, if we need to recalculate caches and mark related components dirty to update on client side. /// Should be disabled for initial graph creation to not recalculate cache on each node/edge. /// /// True if node was removed successfully, false otherwise. public bool RemoveNode(Entity ent, Entity node, bool dirty = true) { if (!Resolve(ent, ref ent.Comp) || !Resolve(node, ref node.Comp, false)) return false; if (!TryGetIndex(ent, node, out var idx)) return false; // node isn't attached to this entity. RemoveAllNodeEdges(ent, idx.Value, dirty: false); _container.Remove(node.Owner, ent.Comp.NodeContainer); node.Comp.Attached = null; ent.Comp.NodeVertices[idx.Value] = null; if (dirty) { RebuildXenoArtifactMetaData(ent); } Dirty(node); return true; } /// /// Remove edges, connected to passed artifact node. /// /// Entity of artifact, in node container of which node resides. /// Index of node (inside node container), for which all edges should be removed. /// /// Marker, if we need to recalculate caches and mark related components dirty to update on client side. /// Should be disabled for initial graph creation to not recalculate cache on each node/edge. /// public void RemoveAllNodeEdges(Entity ent, int nodeIdx, bool dirty = true) { if (!Resolve(ent, ref ent.Comp)) return; var predecessors = GetDirectPredecessorNodes(ent, nodeIdx); foreach (var p in predecessors) { RemoveEdge(ent, p, nodeIdx, dirty: false); } var successors = GetDirectSuccessorNodes(ent, nodeIdx); foreach (var s in successors) { RemoveEdge(ent, nodeIdx, s, dirty: false); } if (dirty) { RebuildXenoArtifactMetaData(ent); } } /// /// Gets set of node entities, that are direct predecessors to passed node entity. /// /// /// Direct predecessors are nodes, which are connected by edges directly to target node, /// and are on outgoing ('FROM') side of edge connection. /// public HashSet> GetDirectPredecessorNodes(Entity ent, EntityUid node) { if (!Resolve(ent, ref ent.Comp)) return new(); if (!TryGetIndex(ent, node, out var index)) return new(); var indices = GetDirectPredecessorNodes(ent, index.Value); var output = new HashSet>(); foreach (var i in indices) { if (TryGetNode(ent, i, out var predecessor)) output.Add(predecessor.Value); } return output; } /// /// Gets set of node indices (in artifact node container) which are direct predecessors to node with passed node index. /// /// /// Direct predecessors are nodes, which are connected by edges directly to target node, /// and are on outgoing ('FROM') side of edge connection. /// public HashSet GetDirectPredecessorNodes(Entity ent, int nodeIdx) { if (!Resolve(ent, ref ent.Comp)) return new(); DebugTools.Assert(nodeIdx >= 0 && nodeIdx < ent.Comp.NodeVertices.Length, $"node index {nodeIdx} is out of bounds!"); var indices = new HashSet(); for (var i = 0; i < ent.Comp.NodeAdjacencyMatrixRows; i++) { if (ent.Comp.NodeAdjacencyMatrix[i][nodeIdx]) indices.Add(i); } return indices; } /// /// Gets set of node entities, that are direct successors to passed node entity. /// /// /// Direct successors are nodes, which are connected by edges /// directly to target node, and are on incoming ('TO') side of edge connection. /// public HashSet> GetDirectSuccessorNodes(Entity ent, EntityUid node) { if (!Resolve(ent, ref ent.Comp)) return new(); if (!TryGetIndex(ent, node, out var index)) return new(); var indices = GetDirectSuccessorNodes(ent, index.Value); var output = new HashSet>(); foreach (var i in indices) { if (TryGetNode(ent, i, out var successor)) output.Add(successor.Value); } return output; } /// /// Gets set of node indices (in artifact node container) which are direct successors to node with passed node index. /// /// /// Direct successors are nodes, which are connected by edges /// directly to target node, and are on incoming ('TO') side of edge connection. /// public HashSet GetDirectSuccessorNodes(Entity ent, int nodeIdx) { if (!Resolve(ent, ref ent.Comp)) return new(); DebugTools.Assert(nodeIdx >= 0 && nodeIdx < ent.Comp.NodeVertices.Length, "node index is out of bounds!"); var indices = new HashSet(); for (var i = 0; i < ent.Comp.NodeAdjacencyMatrixColumns; i++) { if (ent.Comp.NodeAdjacencyMatrix[nodeIdx][i]) indices.Add(i); } return indices; } /// /// Gets set of node entities, that are predecessors to passed node entity. /// /// /// Predecessors are nodes, which are connected by edges directly to target node on 'FROM' side of edge, /// or connected to such node on 'FROM' side of edge, etc recursively. /// public HashSet> GetPredecessorNodes(Entity ent, Entity node) { if (!Resolve(ent, ref ent.Comp)) return new(); var predecessors = GetPredecessorNodes(ent, GetIndex((ent, ent.Comp), node)); var output = new HashSet>(); foreach (var p in predecessors) { output.Add(GetNode((ent, ent.Comp), p)); } return output; } /// /// Gets set of node indices inside artifact node container, that are predecessors to entity with passed node index. /// /// /// Predecessors are nodes, which are connected by edges directly to target node on 'FROM' side of edge, /// or connected to such node on 'FROM' side of edge, etc recursively. /// public HashSet GetPredecessorNodes(Entity ent, int nodeIdx) { if (!Resolve(ent, ref ent.Comp)) return new(); var predecessors = GetDirectPredecessorNodes(ent, nodeIdx); if (predecessors.Count == 0) return new(); var output = new HashSet(); foreach (var p in predecessors) { output.Add(p); var recursivePredecessors = GetPredecessorNodes(ent, p); foreach (var rp in recursivePredecessors) { output.Add(rp); } } return output; } /// /// Gets set of node entities, that are successors to passed node entity. /// /// /// Successors are nodes, which are connected by edges directly to target node on 'TO' side of edge, /// or connected to such node on 'TO' side of edge, etc recursively. /// public HashSet> GetSuccessorNodes(Entity ent, Entity node) { if (!Resolve(ent, ref ent.Comp)) return new(); var successors = GetSuccessorNodes(ent, GetIndex((ent, ent.Comp), node)); var output = new HashSet>(); foreach (var s in successors) { output.Add(GetNode((ent, ent.Comp), s)); } return output; } /// /// Gets set of node indices inside artifact node container, that are successors to entity with passed node index. /// /// /// Successors are nodes, which are connected by edges directly to target node on 'TO' side of edge, /// or connected to such node on 'TO' side of edge, etc recursively. /// public HashSet GetSuccessorNodes(Entity ent, int nodeIdx) { if (!Resolve(ent, ref ent.Comp)) return new(); var successors = GetDirectSuccessorNodes(ent, nodeIdx); if (successors.Count == 0) return new(); var output = new HashSet(); foreach (var s in successors) { output.Add(s); var recursiveSuccessors = GetSuccessorNodes(ent, s); foreach (var rs in recursiveSuccessors) { output.Add(rs); } } return output; } /// /// Determines, if there is an edge (directed link) FROM one node TO other in passed artifact. /// /// Artifact, inside which node container nodes are. /// Node FROM which existence of edge should be checked. /// Node TO which existance of edge should be checked. public bool NodeHasEdge( Entity ent, Entity from, Entity to ) { if (!Resolve(ent, ref ent.Comp)) return new(); var fromIdx = GetIndex((ent, ent.Comp), from); var toIdx = GetIndex((ent, ent.Comp), to); return ent.Comp.NodeAdjacencyMatrix[fromIdx][toIdx]; } /// /// Resizes the adjacency matrix and vertices array to , /// or at least what it WOULD do if i wasn't forced to use shitty lists. /// protected void ResizeNodeGraph(Entity ent, int newSize) { Array.Resize(ref ent.Comp.NodeVertices, newSize); while (ent.Comp.NodeAdjacencyMatrix.Count < newSize) { ent.Comp.NodeAdjacencyMatrix.Add(new()); } foreach (var row in ent.Comp.NodeAdjacencyMatrix) { while (row.Count < newSize) { row.Add(false); } } Dirty(ent); } /// Removes unlocking state from artifact. private void CancelUnlockingOnGraphStructureChange(Entity ent) { if (!TryComp(ent, out var unlockingComponent)) return; Entity artifactEnt = (ent, unlockingComponent, ent.Comp); CancelUnlockingState(artifactEnt); } }