using System.Linq; using Content.Shared.EntityTable; using Content.Shared.NameIdentifier; using Content.Shared.Xenoarchaeology.Artifact.Components; using Content.Shared.Xenoarchaeology.Artifact.Prototypes; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared.Xenoarchaeology.Artifact; public abstract partial class SharedXenoArtifactSystem { [Dependency] private readonly EntityTableSystem _entityTable = default!; private EntityQuery _xenoArtifactQuery; private EntityQuery _nodeQuery; private void InitializeNode() { SubscribeLocalEvent(OnNodeMapInit); _xenoArtifactQuery = GetEntityQuery(); _nodeQuery = GetEntityQuery(); } /// /// Initializes artifact node on its creation (by setting durability). /// private void OnNodeMapInit(Entity ent, ref MapInitEvent args) { XenoArtifactNodeComponent nodeComponent = ent; nodeComponent.MaxDurability -= nodeComponent.MaxDurabilityCanDecreaseBy.Next(RobustRandom); SetNodeDurability((ent, ent), nodeComponent.MaxDurability); } public void SetNodeUnlocked(Entity ent) { if (!Resolve(ent, ref ent.Comp)) return; if (ent.Comp.Attached is not { } artifact) return; if (!TryComp(artifact, out var artifactComponent)) return; SetNodeUnlocked((artifact, artifactComponent), (ent, ent.Comp)); } public void SetNodeUnlocked(Entity artifact, Entity node) { if (!node.Comp.Locked) return; node.Comp.Locked = false; RebuildCachedActiveNodes((artifact, artifact)); Dirty(node); } /// /// Adds to the node's durability by the specified value. To reduce, provide negative value. /// public void AdjustNodeDurability(Entity ent, int durabilityDelta) { if (!Resolve(ent, ref ent.Comp)) return; SetNodeDurability(ent, ent.Comp.Durability + durabilityDelta); } /// /// Sets a node's durability to the specified value. HIGHLY recommended to not be less than 0. /// public void SetNodeDurability(Entity ent, int durability) { if (!Resolve(ent, ref ent.Comp)) return; ent.Comp.Durability = Math.Clamp(durability, 0, ent.Comp.MaxDurability); UpdateNodeResearchValue((ent, ent.Comp)); Dirty(ent); } /// /// Creates artifact node entity, attaching trigger and marking depth level for future use. /// public Entity CreateNode(Entity ent, ProtoId trigger, int depth = 0) { var triggerProto = PrototypeManager.Index(trigger); return CreateNode(ent, triggerProto, depth); } /// /// Creates artifact node entity, attaching trigger and marking depth level for future use. /// public Entity CreateNode(Entity ent, XenoArchTriggerPrototype trigger, int depth = 0) { var entProtoId = _entityTable.GetSpawns(ent.Comp.EffectsTable) .First(); AddNode((ent, ent), entProtoId, out var nodeEnt, dirty: false); DebugTools.Assert(nodeEnt.HasValue, "Failed to create node on artifact."); var nodeComponent = nodeEnt.Value.Comp; nodeComponent.Depth = depth; nodeComponent.TriggerTip = trigger.Tip; EntityManager.AddComponents(nodeEnt.Value, trigger.Components); Dirty(nodeEnt.Value); return nodeEnt.Value; } /// Checks if all predecessor nodes are marked as 'unlocked'. public bool HasUnlockedPredecessor(Entity ent, EntityUid node) { var predecessors = GetDirectPredecessorNodes((ent, ent), node); if (predecessors.Count == 0) { return true; } foreach (var predecessor in predecessors) { if (predecessor.Comp.Locked) { return false; } } return true; } /// Checks if node was marked as 'active'. Active nodes are invoked on artifact use (if durability is greater than zero). public bool IsNodeActive(Entity ent, EntityUid node) { return ent.Comp.CachedActiveNodes.Contains(GetNetEntity(node)); } /// /// Gets list of 'active' nodes. Active nodes are invoked on artifact use (if durability is greater than zero). /// public List> GetActiveNodes(Entity ent) { return ent.Comp.CachedActiveNodes .Select(activeNode => _nodeQuery.Get(GetEntity(activeNode))) .ToList(); } /// /// Gets amount of research points that can be extracted from node. /// We can only extract "what's left" - its base value, reduced by already consumed value. /// Every drained durability brings more points to be extracted. /// public int GetResearchValue(Entity ent) { if (ent.Comp.Locked) return 0; return Math.Max(0, ent.Comp.ResearchValue - ent.Comp.ConsumedResearchValue); } /// /// Sets amount of points already extracted from node. /// public void SetConsumedResearchValue(Entity ent, int value) { ent.Comp.ConsumedResearchValue = value; Dirty(ent); } /// /// Converts node entity uid to its display name (which is Identifier from . /// public string GetNodeId(EntityUid uid) { return (CompOrNull(uid)?.Identifier ?? 0).ToString("D3"); } /// /// Gets two-dimensional array in a form of nested lists, which holds artifact nodes, grouped by segments. /// Segments are groups of interconnected nodes, there might be one or more segments in non-empty artifact. /// public List>> GetSegments(Entity ent) { var output = new List>>(); foreach (var segment in ent.Comp.CachedSegments) { var outSegment = new List>(); foreach (var netNode in segment) { var node = GetEntity(netNode); if (!_nodeQuery.TryComp(node, out var comp)) continue; outSegment.Add((node, comp)); } output.Add(outSegment); } return output; } /// /// Gets list of nodes, grouped by depth level. Depth level count starts from 0. /// Only 0 depth nodes have no incoming edges - as only they are starting nodes. /// public Dictionary>> GetDepthOrderedNodes(IEnumerable> nodes) { var nodesByDepth = new Dictionary>>(); foreach (var node in nodes) { if (!nodesByDepth.TryGetValue(node.Comp.Depth, out var depthList)) { depthList = new List>(); nodesByDepth.Add(node.Comp.Depth, depthList); } depthList.Add(node); } return nodesByDepth; } /// /// Rebuilds all the data, associated with nodes in an artifact, updating caches. /// public void RebuildXenoArtifactMetaData(Entity artifact) { if (!Resolve(artifact, ref artifact.Comp)) return; RebuildCachedActiveNodes(artifact); RebuildCachedSegments(artifact); foreach (var node in GetAllNodes((artifact, artifact.Comp))) { RebuildNodeMetaData(node); } CancelUnlockingOnGraphStructureChange((artifact, artifact.Comp)); } public void RebuildNodeMetaData(Entity node) { UpdateNodeResearchValue(node); } /// /// Clears all cached active nodes and rebuilds the list using the current node state. /// Active nodes have the following property: /// - Are unlocked themselves /// - All successors are also unlocked /// /// /// You could technically modify this to have a per-node method that only checks direct predecessors /// and then does recursive updates for all successors, but I don't think the optimization is necessary right now. /// public void RebuildCachedActiveNodes(Entity ent) { if (!Resolve(ent, ref ent.Comp)) return; ent.Comp.CachedActiveNodes.Clear(); var allNodes = GetAllNodes((ent, ent.Comp)); foreach (var node in allNodes) { // Locked nodes cannot be active. if (node.Comp.Locked) continue; var successors = GetDirectSuccessorNodes(ent, node); // If this node has no successors, then we don't need to bother with this extra logic. if (successors.Count != 0) { // Checks for any of the direct successors being unlocked. var successorIsUnlocked = false; foreach (var sNode in successors) { if (sNode.Comp.Locked) continue; successorIsUnlocked = true; break; } // Active nodes must be at the end of the path. if (successorIsUnlocked) continue; } var netEntity = GetNetEntity(node); ent.Comp.CachedActiveNodes.Add(netEntity); } Dirty(ent); } public void RebuildCachedSegments(Entity ent) { if (!Resolve(ent, ref ent.Comp)) return; ent.Comp.CachedSegments.Clear(); var entities = GetAllNodes((ent, ent.Comp)) .ToList(); var segments = GetSegmentsFromNodes((ent, ent.Comp), entities); var netEntities = segments.Select( s => s.Select(n => GetNetEntity(n)) .ToList() ); ent.Comp.CachedSegments.AddRange(netEntities); Dirty(ent); } /// /// Gets two-dimensional array (as lists inside enumeration) that contains artifact nodes, grouped by segment. /// public IEnumerable>> GetSegmentsFromNodes(Entity ent, List> nodes) { var outSegments = new List>>(); foreach (var node in nodes) { var segment = new List>(); GetSegmentNodesRecursive(ent, node, segment, outSegments); if (segment.Count == 0) continue; outSegments.Add(segment); } return outSegments; } /// /// Fills nodes into segments by recursively walking through collections of predecessors and successors. /// private void GetSegmentNodesRecursive( Entity ent, Entity node, List> segment, List>> otherSegments ) { if (otherSegments.Any(s => s.Contains(node))) return; if (segment.Contains(node)) return; segment.Add(node); var predecessors = GetDirectPredecessorNodes((ent, ent), node); foreach (var p in predecessors) { GetSegmentNodesRecursive(ent, p, segment, otherSegments); } var successors = GetDirectSuccessorNodes((ent, ent), node); foreach (var s in successors) { GetSegmentNodesRecursive(ent, s, segment, otherSegments); } } /// /// Sets node research point amount that can be extracted. /// Used up durability increases amount to be extracted. /// public void UpdateNodeResearchValue(Entity node) { XenoArtifactNodeComponent nodeComponent = node; if (nodeComponent.Attached == null) { nodeComponent.ResearchValue = 0; return; } var artifact = _xenoArtifactQuery.Get(nodeComponent.Attached.Value); var nonactiveNodes = GetActiveNodes(artifact); var durabilityEffect = MathF.Pow((float)nodeComponent.Durability / nodeComponent.MaxDurability, 2); var durabilityMultiplier = nonactiveNodes.Contains(node) ? 1f - durabilityEffect : 1f + durabilityEffect; var predecessorNodes = GetPredecessorNodes((artifact, artifact), node); nodeComponent.ResearchValue = (int)(Math.Pow(1.25, Math.Pow(predecessorNodes.Count, 1.5f)) * nodeComponent.BasePointValue * durabilityMultiplier); } }