public static void Initialize() { // populate all nodes Forest = new List <Node>(DefDatabase <ResearchProjectDef> .AllDefsListForReading // exclude hidden projects (prereq of itself is a common trick to hide research). .Where(def => def.prerequisites.NullOrEmpty() || !def.prerequisites.Contains(def)) .Select(def => new Node(def))); // mark, but do not remove redundant prerequisites. foreach (Node node in Forest) { if (!node.Research.prerequisites.NullOrEmpty()) { var ancestors = node.Research.prerequisites.SelectMany(r => r.GetPrerequisitesRecursive()).ToList(); if (!ancestors.NullOrEmpty() && (!node.Research.prerequisites?.Intersect(ancestors).ToList().NullOrEmpty() ?? false)) { Log.Warning("ResearchPal :: redundant prerequisites for " + node.Research.LabelCap + " the following research: " + string.Join(", ", node.Research.prerequisites.Intersect(ancestors).Select(r => r.LabelCap).ToArray())); } } } // create links between nodes foreach (Node node in Forest) { node.CreateLinks(); } // calculate Depth of each node foreach (Node node in Forest) { node.SetDepth(); } // get the main 'Trees', looping over all defs, find strings of Research named similarly. // We're aiming for finding things like Construction I/II/III/IV/V here. Dictionary <string, List <Node> > trunks = new Dictionary <string, List <Node> > (); List <Node> orphans = new List <Node> (); // temp foreach (Node node in Forest) { // try to remove the amount of random hits by requiring Trees to be directly linked. if (node.Parents.Any(parent => parent.Genus == node.Genus) || node.Children.Any(child => child.Genus == node.Genus)) { if (!trunks.ContainsKey(node.Genus)) { trunks.Add(node.Genus, new List <Node> ()); } trunks [node.Genus].Add(node); } else { orphans.Add(node); } } // Assign the working dictionary to Tree objects, culling stumps. Trees = trunks.Where(trunk => trunk.Value.Count >= Settings.MinTrunkSize) .Select(trunk => new Tree(trunk.Key, trunk.Value)) .ToList(); // add too small Trees back into orphan list orphans.AddRange(trunks.Where(trunk => trunk.Value.Count < Settings.MinTrunkSize).SelectMany(trunk => trunk.Value)); // The order in which Trees should appear; ideally we want Trees with lots of cross-references to appear together. OrderTrunks(); // Attach orphan nodes to the nearest Trunk, or the orphanage trunk Orphans = new Tree("orphans", new List <Node>()) { Color = Color.grey }; foreach (Node orphan in orphans) { Tree closest = orphan.ClosestTree() ?? Orphans; closest.AddLeaf(orphan); } // Assign colors to trunks int n = Trees.Count; for (int i = 1; i <= Trees.Count; i++) { Trees[i - 1].Color = ColorHelper.HSVtoRGB((float)i / n, 1, 1); } // update nodes with position info FixPositions(); // Done! Initialized = true; }
public void DrawTree(Rect canvas) { // get total size of Research Tree int maxDepth = 0, totalWidth = 0; if (ResearchTree.Trees.Any()) { maxDepth = ResearchTree.Trees.Max(tree => tree.MaxDepth); totalWidth = ResearchTree.Trees.Sum(tree => tree.Width); } maxDepth = Math.Max(maxDepth, ResearchTree.Orphans.MaxDepth); totalWidth += ResearchTree.Orphans.Width; float width = (maxDepth + 1) * (Settings.NodeSize.x + Settings.NodeMargins.x); // zero based float height = (totalWidth - 1) * (Settings.NodeSize.y + Settings.NodeMargins.y); // main view rect Rect view = new Rect(0f, 0f, width, height); // create the scroll area below the search box (plus a small margin) so it stays on top Widgets.BeginScrollView(new Rect(canvas.x, canvas.y + filterManager.Height + Settings.NodeMargins.y, canvas.width, canvas.height - filterManager.Height - Settings.NodeMargins.y), ref _scrollPosition, view); GUI.BeginGroup(view); Text.Anchor = TextAnchor.MiddleCenter; // draw regular connections, not done first to better highlight done. foreach (Pair <Node, Node> connection in connections.Where(pair => !pair.Second.Research.IsFinished)) { ResearchTree.DrawLine(connection, filterManager.FilterPhrase.NullOrEmpty() ? connection.First.Tree.GreyedColor : ColorHelper.AdjustAlpha(connection.First.Tree.GreyedColor, Settings.FilterNonMatchAlpha)); } // draw connections from completed nodes foreach (Pair <Node, Node> connection in connections.Where(pair => pair.Second.Research.IsFinished)) { if (!filterManager.FilterPhrase.NullOrEmpty()) { ResearchTree.DrawLine(connection, ColorHelper.AdjustAlpha(connection.First.Tree.GreyedColor, Settings.FilterNonMatchAlpha)); } else { ResearchTree.DrawLine(connection, connection.First.Tree.MediumColor); } } connections.Clear(); // draw highlight connections on top foreach (Pair <Node, Node> connection in highlightedConnections) { ResearchTree.DrawLine(connection, GenUI.MouseoverColor, true); } highlightedConnections.Clear(); // draw nodes on top of lines bool reqScroll = true; Node scrollToNode = null; foreach (Node node in nodes) { // draw the node bool visible = node.Draw(); // ensure that at least one matching node is visible, prioritize highest on the screen if (filterManager.FilterDirty) { if (node.FilterMatch.IsValidMatch()) { if (!reqScroll) { continue; } // this node is a match and is currently visible, we don't need to scroll if (visible) { reqScroll = false; scrollToNode = null; } else { // this node is a match, but isn't visible. if it's the highest node then we'll scroll to it if (scrollToNode == null || node.Pos.z < scrollToNode.Pos.z) { scrollToNode = node; } } } } } if (filterManager.FilterDirty) { // scroll to a matching node if necessary if (scrollToNode != null) { // scale the focus area to ensure it all fits on the screen Rect r = scrollToNode.Rect.ScaledBy(2.0f); _scrollPosition = new Vector2(r.xMin, r.yMin); } else if (filterManager.FilterPhrase == "") { _scrollPosition = Vector2.zero; } } nodes.Clear(); // register hub tooltips foreach (KeyValuePair <Rect, List <string> > pair in hubTips) { string text = string.Join("\n", pair.Value.ToArray()); TooltipHandler.TipRegion(pair.Key, text); } hubTips.Clear(); // draw Queue labels Queue.DrawLabels(); // reset anchor Text.Anchor = TextAnchor.UpperLeft; GUI.EndGroup(); Widgets.EndScrollView(); }