├── .gitignore
├── About
├── About.xml
├── Manifest.xml
├── Preview.png
└── dependencies.json
├── Assemblies
└── ResearchTree.dll
├── Defs
└── MainTabDefs
│ └── MainTabWindow.xml
├── LICENSE
├── Languages
├── ChineseSimplified
│ └── Keyed
│ │ └── KeyedTranslations.xml
├── ChineseTraditional
│ ├── DefInjected
│ │ ├── KeyBindingDef
│ │ │ └── KeyBindings_Add_MainTab.xml
│ │ └── MainButtonDef
│ │ │ └── MainTabWindow.xml
│ └── Keyed
│ │ └── KeyedTranslations.xml
├── English
│ └── Keyed
│ │ └── KeyedTranslations.xml
├── German
│ ├── Keyed
│ │ └── KeyedTranslations.xml
│ └── LanguageInfo.xml
├── Korean
│ └── Keyed
│ │ └── KeyedTranslations.xml
├── Russian
│ └── Keyed
│ │ └── KeyedTranslations.xml
├── Spanish
│ └── Keyed
│ │ └── KeyedTranslations.xml
└── SpanishLatin
│ └── Keyed
│ └── KeyedTranslations.xml
├── Readme.md
├── Source
├── .gitattributes
├── .gitignore
├── .idea
│ └── .idea.ResearchTree
│ │ └── .idea
│ │ └── contentModel.xml
├── Assets.cs
├── Constants.cs
├── Extensions
│ ├── Building_ResearchBench_Extensions.cs
│ ├── Def_Extensions.cs
│ ├── ResearchProjectDef_Extensions.cs
│ └── SyncWorker_ResearchNode.cs
├── FloatMenu_Fixed.cs
├── Graph
│ ├── DummyNode.cs
│ ├── Edge.cs
│ ├── Node.cs
│ ├── ResearchNode.cs
│ └── Tree.cs
├── Log.cs
├── MainButtonWorker_ResearchTree.cs
├── MainTabWindow_ResearchTree.cs
├── Profiler.cs
├── Properties
│ └── AssemblyInfo.cs
├── Queue
│ ├── Queue.cs
│ └── Queue_HarmonyPatches.cs
├── ResearchTree.cs
├── ResearchTree.csproj
├── ResearchTree.sln
├── description.md
└── packages.config
└── Textures
├── Buttons
├── button-active.png
└── button.png
├── Icons
├── Research.png
├── circle-fill.png
├── magnifying-glass.png
├── more.png
└── padlock.png
└── Lines
├── Outline
├── circle.png
├── end.png
├── ew.png
└── ns.png
└── Solid
├── circle.png
├── end.png
├── ew.png
└── ns.png
/.gitignore:
--------------------------------------------------------------------------------
1 | ModConfig.json
2 | SteamConfig.vdf
3 | .mailmap
4 | .idea
5 |
6 | # Windows image file caches
7 | Thumbs.db
8 | ehthumbs.db
9 |
10 | # Folder config file
11 | Desktop.ini
12 |
13 | # Recycle Bin used on file shares
14 | $RECYCLE.BIN/
15 |
16 | **/bin/*
17 | **/obj/*
18 | **/.vs/*
19 | **/bin
20 | **/obj
21 | **/.vs
22 |
23 | # Windows Installer files
24 | *.cab
25 | *.msi
26 | *.msm
27 | *.msp
28 |
29 | # Windows shortcuts
30 | *.lnk
31 |
32 | # =========================
33 | # Operating System Files
34 | # =========================
35 |
36 | # OSX
37 | # =========================
38 |
39 | .DS_Store
40 | .AppleDouble
41 | .LSOverride
42 |
43 | # Thumbnails
44 | ._*
45 |
46 | # Files that might appear on external disk
47 | .Spotlight-V100
48 | .Trashes
49 |
50 | # Directories potentially created on remote AFP share
51 | .AppleDB
52 | .AppleDesktop
53 | Network Trash Folder
54 | Temporary Items
55 | .apdisk
56 | *.txt
57 | Source/BetterAnimalsTab/obj/Debug/BetterAnimalsTab.pdb
58 | *.cache
59 | *.pxd
60 | *.pdb
61 |
--------------------------------------------------------------------------------
/About/About.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Research Tree
4 | Fluffy
5 | https://ludeon.com/forums/index.php?topic=16120
6 | A better research tree.
7 |
8 | <size=24>Features</size>
9 |
10 | automatically generated to maximize readability*.
11 | shows research projects, buildings, plants and recipes unlocked by each research project.
12 | projects can be queued, and colonists will automatically start the next project when the current research project completes.
13 | search functionality to quickly find research projects.
14 |
15 | <size=24>FAQ</size>
16 | <i>Can I add/remove this from an existing save?</i>
17 | You can add it to existing saves without problems. Removing this mod will lead to some errors when loading, but these should not affect gameplay - and will go away after saving.
18 |
19 | <i>Why is research X in position Y?</i>
20 | Honestly, I have no idea. The placement of projects (nodes) is automated to minimize the number of crossings between dependancies (edges), and reduce the total length of these edges. There are many possible scenarios in which this can lead to placements that may appear non-optimal. Sometimes they really are non-optimal, sometimes they just appear to be so. See also the <i>technical</i> section below for more information.
21 |
22 | <i>Can I use this with mod X</i>
23 | Most likely, yes. Added researches and their requirements are automatically parsed and the tree layout will be updated accordingly. ResearchPal implements a lot of the same functionality as this mod, and the research queue will likely not work correctly if both mods are loaded.
24 |
25 | <i>This looks very similar to ResearchPal</i>
26 | Yep. ResearchPal is based on a legacy version of this mod that was kept up-to-date by SkyArkAngel in the HCSK modpack. I haven’t worked on this mod in a long time, but I recently had some spare time and decided to give it another go. Feel free to use whichever you like better (ResearchPal has an entirely different layout algorithm). You can run both mods side by side to check out the different tree layouts, but be aware that the research queue will not work correctly if both mods are loaded.
27 |
28 | <size=24>Known Issues</size>
29 |
30 | Layouts are not perfect, if you have experience with graph layouts - please do feel free to look at the source code, and/or implement a Sugiyama layout algorithm for me that runs in C# .NET 3.5 (Mono 2.0).
31 |
32 | <size=24>Technical</size>
33 | So how does this all work?
34 |
35 | Creating an optimal layout is a known problem in the area of <i>Graph Theory</i>. There’s serious mathematicians who’ve spent years of their live trying to figure out this problem, and numerous solutions exist. The group of solutions most relevant to our research tree (a <i>directed acyclic graph</i>, or <i>DAG</i>) is that derived from Sugiyama’s work. Generally speaking, these algorithms have four steps;
36 |
37 |
38 | layering (set the <i>x</i> coordinates of nodes, enforcing that follow-up research is always at a higher x position than any of its prerequisites, this is a fairly straightforward heuristic)
39 | crossing reduction (set the <i>y</i> coordinates of nodes such that there is a minimal amount of intersections of connections between nodes)
40 | edge length reduction (set the <i>y</i> coordinates of nodes such that the length of connections between nodes is minimal)
41 | horizontal alignment (set the <i>y</i> coordinates of nodes such that the connections between nodes are straight as much as possible)
42 |
43 | The final step is the hardest, but also the most important to create a visually pleasing tree. Sadly, I’ve been unable to implement two of the most well known algorithms for this purpose;
44 |
45 |
46 | Brandes, U., & Köpf, B. (2001, September). Fast and simple horizontal coordinate assignment.
47 | Eiglsperger M., Siebenhaller M., Kaufmann M. (2005) An Efficient Implementation of Sugiyama’s Algorithm for Layered Graph Drawing.
48 | Luckily, the crossing reduction and edge length reduction steps partially achieve the goals of the final step. The final graph is not as pretty as it could be, but it’s still pretty good - in most scenarios.
49 |
50 | <size=24>Contributors</size>
51 |
52 | Templarr: Russian translation
53 | Suh. Junmin: Korean translation
54 | rw-chaos: German translation
55 | 53N4: Spanish translation
56 | Silverside: Fix UI scaling bug for vertical text
57 | shiuanyue: Chinese (traditional) translation
58 | notfood: Implement techprint requirements
59 | HanYaodong: Add simplified Chinese translation
60 |
61 | <size=24>Version</size>
62 | This is version 3.17.537, for RimWorld 1.2.2753.
63 |
64 |
65 |
66 | 1.2
67 |
68 | fluffy.researchtree
69 |
70 |
71 | brrainz.harmony
72 | Harmony
73 | steam://url/CommunityFilePage/2009463077
74 | https://github.com/pardeike/HarmonyRimWorld/releases/latest
75 |
76 |
77 |
78 | brrainz.harmony
79 |
80 |
--------------------------------------------------------------------------------
/About/Manifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 3.17.533
4 | https://raw.githubusercontent.com/fluffy-mods/ResearchTree/1.1/About/Manifest.xml
5 | https://github.com/fluffy-mods/ResearchTree/releases/v3.17.533
6 |
--------------------------------------------------------------------------------
/About/Preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/About/Preview.png
--------------------------------------------------------------------------------
/About/dependencies.json:
--------------------------------------------------------------------------------
1 | {
2 | "depends": [
3 | {
4 | "id": "brrainz.harmony",
5 | "name": "Harmony",
6 | "steam": 2009463077,
7 | "url": "https://github.com/pardeike/HarmonyRimWorld/releases/latest"
8 | }
9 | ],
10 | "after": [
11 | "brrainz.harmony"
12 | ]
13 | }
--------------------------------------------------------------------------------
/Assemblies/ResearchTree.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Assemblies/ResearchTree.dll
--------------------------------------------------------------------------------
/Defs/MainTabDefs/MainTabWindow.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Research
6 | research
7 | Examine and decide on research projects.
8 | FluffyResearchTree.MainButtonWorker_ResearchTree
9 | FluffyResearchTree.MainTabWindow_ResearchTree
10 | 50
11 | F5
12 |
13 |
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | =======================================================================
2 | The following MIT license applies to the software and any documentation.
3 | Any original content (e.g. text, imagery or sounds) are licensed under
4 | CC-BY-SA 4.0, the text of which is given below.
5 | =======================================================================
6 |
7 | Copyright 2020 Fluffy
8 |
9 | Permission is hereby granted, free of charge, to any person obtaining
10 | a copy of this software and associated documentation files (the
11 | "Software"), to deal in the Software without restriction, including
12 | without limitation the rights to use, copy, modify, merge, publish,
13 | distribute, sublicense, and/or sell copies of the Software, and to
14 | permit persons to whom the Software is furnished to do so, subject to
15 | the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included
18 | in all copies or substantial portions of the Software.
19 |
20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABI-
22 | LITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
24 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
25 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | OTHER DEALINGS IN THE SOFTWARE.
27 |
28 |
29 |
30 | =======================================================================
31 | The following CC BY-SA 4.0 license applies to any original content. The
32 | software and documentation are licensed under the MIT license, the text
33 | of which is given above.
34 | =======================================================================
35 |
36 | Creative Commons Attribution-ShareAlike 4.0 International Public
37 | License
38 |
39 | By exercising the Licensed Rights (defined below), You accept and agree
40 | to be bound by the terms and conditions of this Creative Commons
41 | Attribution-ShareAlike 4.0 International Public License ("Public
42 | License"). To the extent this Public License may be interpreted as a
43 | contract, You are granted the Licensed Rights in consideration of Your
44 | acceptance of these terms and conditions, and the Licensor grants You
45 | such rights in consideration of benefits the Licensor receives from
46 | making the Licensed Material available under these terms and
47 | conditions.
48 |
49 |
50 | Section 1 -- Definitions.
51 |
52 | a. Adapted Material means material subject to Copyright and Similar
53 | Rights that is derived from or based upon the Licensed Material
54 | and in which the Licensed Material is translated, altered,
55 | arranged, transformed, or otherwise modified in a manner requiring
56 | permission under the Copyright and Similar Rights held by the
57 | Licensor. For purposes of this Public License, where the Licensed
58 | Material is a musical work, performance, or sound recording,
59 | Adapted Material is always produced where the Licensed Material is
60 | synched in timed relation with a moving image.
61 |
62 | b. Adapter's License means the license You apply to Your Copyright
63 | and Similar Rights in Your contributions to Adapted Material in
64 | accordance with the terms and conditions of this Public License.
65 |
66 | c. BY-SA Compatible License means a license listed at
67 | creativecommons.org/compatiblelicenses, approved by Creative
68 | Commons as essentially the equivalent of this Public License.
69 |
70 | d. Copyright and Similar Rights means copyright and/or similar rights
71 | closely related to copyright including, without limitation,
72 | performance, broadcast, sound recording, and Sui Generis Database
73 | Rights, without regard to how the rights are labeled or
74 | categorized. For purposes of this Public License, the rights
75 | specified in Section 2(b)(1)-(2) are not Copyright and Similar
76 | Rights.
77 |
78 | e. Effective Technological Measures means those measures that, in the
79 | absence of proper authority, may not be circumvented under laws
80 | fulfilling obligations under Article 11 of the WIPO Copyright
81 | Treaty adopted on December 20, 1996, and/or similar international
82 | agreements.
83 |
84 | f. Exceptions and Limitations means fair use, fair dealing, and/or
85 | any other exception or limitation to Copyright and Similar Rights
86 | that applies to Your use of the Licensed Material.
87 |
88 | g. License Elements means the license attributes listed in the name
89 | of a Creative Commons Public License. The License Elements of this
90 | Public License are Attribution and ShareAlike.
91 |
92 | h. Licensed Material means the artistic or literary work, database,
93 | or other material to which the Licensor applied this Public
94 | License.
95 |
96 | i. Licensed Rights means the rights granted to You subject to the
97 | terms and conditions of this Public License, which are limited to
98 | all Copyright and Similar Rights that apply to Your use of the
99 | Licensed Material and that the Licensor has authority to license.
100 |
101 | j. Licensor means the individual(s) or entity(ies) granting rights
102 | under this Public License.
103 |
104 | k. Share means to provide material to the public by any means or
105 | process that requires permission under the Licensed Rights, such
106 | as reproduction, public display, public performance, distribution,
107 | dissemination, communication, or importation, and to make material
108 | available to the public including in ways that members of the
109 | public may access the material from a place and at a time
110 | individually chosen by them.
111 |
112 | l. Sui Generis Database Rights means rights other than copyright
113 | resulting from Directive 96/9/EC of the European Parliament and of
114 | the Council of 11 March 1996 on the legal protection of databases,
115 | as amended and/or succeeded, as well as other essentially
116 | equivalent rights anywhere in the world.
117 |
118 | m. You means the individual or entity exercising the Licensed Rights
119 | under this Public License. Your has a corresponding meaning.
120 |
121 |
122 | Section 2 -- Scope.
123 |
124 | a. License grant.
125 |
126 | 1. Subject to the terms and conditions of this Public License,
127 | the Licensor hereby grants You a worldwide, royalty-free,
128 | non-sublicensable, non-exclusive, irrevocable license to
129 | exercise the Licensed Rights in the Licensed Material to:
130 |
131 | a. reproduce and Share the Licensed Material, in whole or
132 | in part; and
133 |
134 | b. produce, reproduce, and Share Adapted Material.
135 |
136 | 2. Exceptions and Limitations. For the avoidance of doubt, where
137 | Exceptions and Limitations apply to Your use, this Public
138 | License does not apply, and You do not need to comply with
139 | its terms and conditions.
140 |
141 | 3. Term. The term of this Public License is specified in Section
142 | 6(a).
143 |
144 | 4. Media and formats; technical modifications allowed. The
145 | Licensor authorizes You to exercise the Licensed Rights in
146 | all media and formats whether now known or hereafter created,
147 | and to make technical modifications necessary to do so. The
148 | Licensor waives and/or agrees not to assert any right or
149 | authority to forbid You from making technical modifications
150 | necessary to exercise the Licensed Rights, including
151 | technical modifications necessary to circumvent Effective
152 | Technological Measures. For purposes of this Public License,
153 | simply making modifications authorized by this Section 2(a)
154 | (4) never produces Adapted Material.
155 |
156 | 5. Downstream recipients.
157 |
158 | a. Offer from the Licensor -- Licensed Material. Every
159 | recipient of the Licensed Material automatically
160 | receives an offer from the Licensor to exercise the
161 | Licensed Rights under the terms and conditions of this
162 | Public License.
163 |
164 | b. Additional offer from the Licensor -- Adapted Material.
165 | Every recipient of Adapted Material from You
166 | automatically receives an offer from the Licensor to
167 | exercise the Licensed Rights in the Adapted Material
168 | under the conditions of the Adapter's License You apply.
169 |
170 | c. No downstream restrictions. You may not offer or impose
171 | any additional or different terms or conditions on, or
172 | apply any Effective Technological Measures to, the
173 | Licensed Material if doing so restricts exercise of the
174 | Licensed Rights by any recipient of the Licensed
175 | Material.
176 |
177 | 6. No endorsement. Nothing in this Public License constitutes or
178 | may be construed as permission to assert or imply that You
179 | are, or that Your use of the Licensed Material is, connected
180 | with, or sponsored, endorsed, or granted official status by,
181 | the Licensor or others designated to receive attribution as
182 | provided in Section 3(a)(1)(A)(i).
183 |
184 | b. Other rights.
185 |
186 | 1. Moral rights, such as the right of integrity, are not
187 | licensed under this Public License, nor are publicity,
188 | privacy, and/or other similar personality rights; however, to
189 | the extent possible, the Licensor waives and/or agrees not to
190 | assert any such rights held by the Licensor to the limited
191 | extent necessary to allow You to exercise the Licensed
192 | Rights, but not otherwise.
193 |
194 | 2. Patent and trademark rights are not licensed under this
195 | Public License.
196 |
197 | 3. To the extent possible, the Licensor waives any right to
198 | collect royalties from You for the exercise of the Licensed
199 | Rights, whether directly or through a collecting society
200 | under any voluntary or waivable statutory or compulsory
201 | licensing scheme. In all other cases the Licensor expressly
202 | reserves any right to collect such royalties.
203 |
204 |
205 | Section 3 -- License Conditions.
206 |
207 | Your exercise of the Licensed Rights is expressly made subject to the
208 | following conditions.
209 |
210 | a. Attribution.
211 |
212 | 1. If You Share the Licensed Material (including in modified
213 | form), You must:
214 |
215 | a. retain the following if it is supplied by the Licensor
216 | with the Licensed Material:
217 |
218 | i. identification of the creator(s) of the Licensed
219 | Material and any others designated to receive
220 | attribution, in any reasonable manner requested by
221 | the Licensor (including by pseudonym if
222 | designated);
223 |
224 | ii. a copyright notice;
225 |
226 | iii. a notice that refers to this Public License;
227 |
228 | iv. a notice that refers to the disclaimer of
229 | warranties;
230 |
231 | v. a URI or hyperlink to the Licensed Material to the
232 | extent reasonably practicable;
233 |
234 | b. indicate if You modified the Licensed Material and
235 | retain an indication of any previous modifications; and
236 |
237 | c. indicate the Licensed Material is licensed under this
238 | Public License, and include the text of, or the URI or
239 | hyperlink to, this Public License.
240 |
241 | 2. You may satisfy the conditions in Section 3(a)(1) in any
242 | reasonable manner based on the medium, means, and context in
243 | which You Share the Licensed Material. For example, it may be
244 | reasonable to satisfy the conditions by providing a URI or
245 | hyperlink to a resource that includes the required
246 | information.
247 |
248 | 3. If requested by the Licensor, You must remove any of the
249 | information required by Section 3(a)(1)(A) to the extent
250 | reasonably practicable.
251 |
252 | b. ShareAlike.
253 |
254 | In addition to the conditions in Section 3(a), if You Share
255 | Adapted Material You produce, the following conditions also apply.
256 |
257 | 1. The Adapter's License You apply must be a Creative Commons
258 | license with the same License Elements, this version or
259 | later, or a BY-SA Compatible License.
260 |
261 | 2. You must include the text of, or the URI or hyperlink to, the
262 | Adapter's License You apply. You may satisfy this condition
263 | in any reasonable manner based on the medium, means, and
264 | context in which You Share Adapted Material.
265 |
266 | 3. You may not offer or impose any additional or different terms
267 | or conditions on, or apply any Effective Technological
268 | Measures to, Adapted Material that restrict exercise of the
269 | rights granted under the Adapter's License You apply.
270 |
271 |
272 | Section 4 -- Sui Generis Database Rights.
273 |
274 | Where the Licensed Rights include Sui Generis Database Rights that
275 | apply to Your use of the Licensed Material:
276 |
277 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right
278 | to extract, reuse, reproduce, and Share all or a substantial
279 | portion of the contents of the database;
280 |
281 | b. if You include all or a substantial portion of the database
282 | contents in a database in which You have Sui Generis Database
283 | Rights, then the database in which You have Sui Generis Database
284 | Rights (but not its individual contents) is Adapted Material,
285 |
286 | including for purposes of Section 3(b); and
287 | c. You must comply with the conditions in Section 3(a) if You Share
288 | all or a substantial portion of the contents of the database.
289 |
290 | For the avoidance of doubt, this Section 4 supplements and does not
291 | replace Your obligations under this Public License where the Licensed
292 | Rights include other Copyright and Similar Rights.
293 |
294 |
295 | Section 5 -- Disclaimer of Warranties and Limitation of Liability.
296 |
297 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
298 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
299 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
300 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
301 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
302 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
303 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
304 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
305 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
306 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
307 |
308 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
309 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
310 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
311 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
312 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
313 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
314 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
315 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
316 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
317 |
318 | c. The disclaimer of warranties and limitation of liability provided
319 | above shall be interpreted in a manner that, to the extent
320 | possible, most closely approximates an absolute disclaimer and
321 | waiver of all liability.
322 |
323 |
324 | Section 6 -- Term and Termination.
325 |
326 | a. This Public License applies for the term of the Copyright and
327 | Similar Rights licensed here. However, if You fail to comply with
328 | this Public License, then Your rights under this Public License
329 | terminate automatically.
330 |
331 | b. Where Your right to use the Licensed Material has terminated under
332 | Section 6(a), it reinstates:
333 |
334 | 1. automatically as of the date the violation is cured, provided
335 | it is cured within 30 days of Your discovery of the
336 | violation; or
337 |
338 | 2. upon express reinstatement by the Licensor.
339 |
340 | For the avoidance of doubt, this Section 6(b) does not affect any
341 | right the Licensor may have to seek remedies for Your violations
342 | of this Public License.
343 |
344 | c. For the avoidance of doubt, the Licensor may also offer the
345 | Licensed Material under separate terms or conditions or stop
346 | distributing the Licensed Material at any time; however, doing so
347 | will not terminate this Public License.
348 |
349 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
350 | License.
351 |
352 |
353 | Section 7 -- Other Terms and Conditions.
354 |
355 | a. The Licensor shall not be bound by any additional or different
356 | terms or conditions communicated by You unless expressly agreed.
357 |
358 | b. Any arrangements, understandings, or agreements regarding the
359 | Licensed Material not stated herein are separate from and
360 | independent of the terms and conditions of this Public License.
361 |
362 |
363 | Section 8 -- Interpretation.
364 |
365 | a. For the avoidance of doubt, this Public License does not, and
366 | shall not be interpreted to, reduce, limit, restrict, or impose
367 | conditions on any use of the Licensed Material that could lawfully
368 | be made without permission under this Public License.
369 |
370 | b. To the extent possible, if any provision of this Public License is
371 | deemed unenforceable, it shall be automatically reformed to the
372 | minimum extent necessary to make it enforceable. If the provision
373 | cannot be reformed, it shall be severed from this Public License
374 | without affecting the enforceability of the remaining terms and
375 | conditions.
376 |
377 | c. No term or condition of this Public License will be waived and no
378 | failure to comply consented to unless expressly agreed to by the
379 | Licensor.
380 |
381 | d. Nothing in this Public License constitutes or may be interpreted
382 | as a limitation upon, or waiver of, any privileges and immunities
383 | that apply to the Licensor or You, including from the legal
384 | processes of any jurisdiction or authority.
385 |
386 |
387 | =======================================================================
388 |
389 | Creative Commons is not a party to its public
390 | licenses. Notwithstanding, Creative Commons may elect to apply one of
391 | its public licenses to material it publishes and in those instances
392 | will be considered the “Licensor.” The text of the Creative Commons
393 | public licenses is dedicated to the public domain under the CC0 Public
394 | Domain Dedication. Except for the limited purpose of indicating that
395 | material is shared under a Creative Commons public license or as
396 | otherwise permitted by the Creative Commons policies published at
397 | creativecommons.org/policies, Creative Commons does not authorize the
398 | use of the trademark "Creative Commons" or any other trademark or logo
399 | of Creative Commons without its prior written consent including,
400 | without limitation, in connection with any unauthorized modifications
401 | to any of its public licenses or any other arrangements,
402 | understandings, or agreements concerning use of licensed material. For
403 | the avoidance of doubt, this paragraph does not form part of the
404 | public licenses.
405 |
406 | Creative Commons may be contacted at creativecommons.org.
407 |
--------------------------------------------------------------------------------
/Languages/ChineseSimplified/Keyed/KeyedTranslations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 解锁建筑:{0}
5 | 解锁配方:{0}
6 | {1} 中可种植作物:{0}
7 | 解锁作物:{0}
8 |
9 | 左键:替换队列
10 | 左键:从队列中移除
11 | Shift+左键:添加到队列
12 |
13 | (DEBUG) 右键:立即完成
14 |
15 | 需要
16 | 导向
17 |
18 | 已在队列内的研究节点
19 | 缺少 {0}
20 | 科研蓝图不足 {0}/{1}
21 |
22 | 找不到研究项目
23 | 研究队列为空
24 | 下个项目: {0}
25 |
26 | 研究节点准备中
27 | 减少交叉
28 | 完成布局
29 | 恢复队列
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Languages/ChineseTraditional/DefInjected/KeyBindingDef/KeyBindings_Add_MainTab.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Languages/ChineseTraditional/DefInjected/MainButtonDef/MainTabWindow.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Languages/ChineseTraditional/Keyed/KeyedTranslations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 解鎖建築:{0}
5 | 解鎖配方:{0}
6 | {1}可種植新農作物:{0}
7 | 解鎖作物:{0}
8 |
9 | 左鍵:變更排程
10 | 左鍵:從排程清單中移除該項研究
11 | Shift+滑鼠左鍵將項目添加到隊列後
12 |
13 | (DEBUG) 右鍵單擊即可立即完成
14 |
15 | 需要
16 | 導致
17 |
18 | 已排隊的研究點
19 | 缺少 {0}
20 | 科技藍圖要求 {0}/{1}
21 |
22 | 找不到相關研究項目
23 | 未安排任何研究排程
24 | 下個項目: {0}
25 |
26 | 研究節點準備中
27 | 減少交叉
28 | 完成佈局
29 | 回復隊列
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Languages/English/Keyed/KeyedTranslations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Enables construction {0}
5 | Enables recipe {0}
6 | Enables planting {0} in {1}
7 | Enables growing {0}
8 |
9 | Left-Click to replace queue
10 | Left-Click to remove from queue
11 | Shift-Left-Click to add to queue
12 |
13 | (DEBUG) Right-Click to finish instantly
14 |
15 | Requires
16 | Leads to
17 | none
18 |
19 | Already queued node
20 | Missing {0}
21 | Insufficient techprints {0}/{1}
22 |
23 | no research found
24 | no research queued
25 | Next in queue: {0}
26 |
27 | Preparing research nodes
28 | Reducing crossings
29 | Finalizing layout
30 | Restoring queue
31 |
32 |
--------------------------------------------------------------------------------
/Languages/German/Keyed/KeyedTranslations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Ermöglicht die Konstruktion {0}
5 | Aktiviert das Rezept {0}
6 | Ermöglicht das Anpflanzen von {0} in {1}
7 | Ermöglicht das Wachsen von {0}
8 |
9 | Linksklick, um die Warteschlange zu ersetzen
10 | Linksklick, um es von der Warteschlange zu entfernen
11 | Shift-Linksklick, um es zur Warteschlange hinzuzufügen
12 |
13 | (DEBUG) Rechtsklick, um direkt abzuschließen
14 |
15 | Benötigt
16 | Führt zu
17 |
18 | Bereits in Warteschlange enthalten
19 | Fehlt {0}
20 | Techprints benötigt {0}/{1}
21 |
22 | kein aktives Forschungsprojekt gefunden
23 | keine Warteschlange für Forschungsprojekte vorhanden
24 | Nächstes in der Warteschlange: {0}
25 |
26 | Vorbereitung der Forschungsknoten
27 | Reduziere Überschneidungen
28 | Finalisiere Layout
29 | Stelle Warteschlange wieder her
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Languages/German/LanguageInfo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Deutsch
4 | German
5 | true
6 |
7 |
8 | Credit_Translator
9 | Chaos
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Languages/Korean/Keyed/KeyedTranslations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {0} 건설 사용 가능
5 | {0} 레시피 사용 가능
6 | {0}을/를 {1}에 심기 가능
7 | {0} 재배 가능
8 |
9 | 왼쪽 클릭으로 대기열을 변경합니다.
10 | 왼쪽 클릭으로 대기열에서 제거합니다.
11 | Shift-왼쪽 클릭으로 대기열에 추가합니다.
12 |
13 | (디버그) 오른쪽 클릭으로 즉시 완료
14 |
15 | 요구 사항
16 | 선행 기술
17 |
18 | 이미 대기열에 있는 노드입니다.
19 | {0} 필요
20 | 불충분 한 기술 인쇄 {0}/{1}
21 |
22 | 검색 결과 없음
23 | 대기열에 연구 없음
24 | 대기열에 있는 다음 연구: {0}
25 |
26 | 연구 노드 준비
27 | 겹침 줄이기
28 | 레이아웃 다듬기
29 | 대기열 복원
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Languages/Russian/Keyed/KeyedTranslations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Позволяет строить {0}
4 | Позволяет {0}
5 | Позволяет высаживать {0} in {1}
6 | Позволяет выращивать {0}
7 |
8 | ЛКМ для замены в очереди
9 | ЛКМ для удаления из очереди
10 | Shift+ЛКМ для добавления в очередь
11 |
12 | (DEBUG) ПКМ для мгновенного изучения
13 |
14 | Требует
15 | Ведет к
16 |
17 | Уже в очереди
18 | Отсутствует {0}
19 | Недостаточно технологических отпечатков {0}/{1}
20 |
21 | нечего исследовать
22 | исследований не назначено
23 | Следующее на очереди: {0}
24 |
25 | Подготовка списка исследований
26 | Уменьшение пересечений
27 | Окончательная обработка слоев
28 | Восстановление очереди
29 |
30 |
--------------------------------------------------------------------------------
/Languages/Spanish/Keyed/KeyedTranslations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Permite la construcción de {0}
5 | Permite la producción de {0}
6 | Permite la plantación de {0} en {1}
7 | Permite el cultivo {0}
8 |
9 | Clic izquierdo para reemplazar la cola
10 | Clic izquierdo para eliminar de la cola
11 | Shift+Clic izquierdo para añadir a la cola
12 |
13 | (DEPURACIÓN) Clic derecho para terminarla instantáneamente
14 |
15 | Requiere
16 | Lleva a
17 |
18 | El nodo de investigación ya está en cola
19 | Falta {0}
20 | Planos tecnológicos insuficientes {0}/{1}
21 |
22 | no se ha encontrado ninguna investigación
23 | no hay una cola para investigar
24 | Siguiente en la cola: {0}
25 |
26 | Preparando nodos de investigación
27 | Reduciendo los cruces
28 | Finalizando el diseño
29 | Restablecer la cola
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Languages/SpanishLatin/Keyed/KeyedTranslations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Permite la construcción de {0}
5 | Permite la producción de {0}
6 | Permite la plantación de {0} en {1}
7 | Permite el cultivo {0}
8 |
9 | Clic izquierdo para reemplazar la cola
10 | Clic izquierdo para eliminar de la cola
11 | Shift+Clic izquierdo para añadir a la cola
12 |
13 | (DEPURACIÓN) Clic derecho para terminarla instantáneamente
14 |
15 | Requiere
16 | Lleva a
17 |
18 | El nodo de investigación ya está en cola
19 | Falta {0}
20 | Planos tecnológicos insuficientes {0}/{1}
21 |
22 | no se ha encontrado ninguna investigación
23 | no hay una cola para investigar
24 | Siguiente en la cola: {0}
25 |
26 | Preparando nodos de investigación
27 | Reduciendo los cruces
28 | Finalizando el diseño
29 | Restablecer la cola
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | [](http://rimworldgame.com/)
2 |
3 | A better research tree.
4 |
5 | 
6 | - automatically generated to maximize readability*.
7 | - shows research projects, buildings, plants and recipes unlocked by each research project.
8 | - projects can be queued, and colonists will automatically start the next project when the current research project completes.
9 | - search functionality to quickly find research projects.
10 |
11 | 
12 | *Can I add/remove this from an existing save?*
13 | You can add it to existing saves without problems. Removing this mod will lead to some errors when loading, but these should not affect gameplay - and will go away after saving.
14 |
15 | *Why is research X in position Y?*
16 | Honestly, I have no idea. The placement of projects (nodes) is automated to minimize the number of crossings between dependancies (edges), and reduce the total length of these edges. There are many possible scenarios in which this can lead to placements that may appear non-optimal. Sometimes they really are non-optimal, sometimes they just appear to be so. See also the *technical* section below for more information.
17 |
18 | *Can I use this with mod X*
19 | Most likely, yes. Added researches and their requirements are automatically parsed and the tree layout will be updated accordingly. ResearchPal implements a lot of the same functionality as this mod, and the research queue will likely not work correctly if both mods are loaded.
20 |
21 | *This looks very similar to ResearchPal*
22 | Yep. ResearchPal is based on a legacy version of this mod that was kept up-to-date by SkyArkAngel in the HCSK modpack. I haven't worked on this mod in a long time, but I recently had some spare time and decided to give it another go. Feel free to use whichever you like better (ResearchPal has an entirely different layout algorithm). You can run both mods side by side to check out the different tree layouts, but be aware that the research queue will not work correctly if both mods are loaded.
23 |
24 | 
25 | - Layouts are not perfect, if you have experience with graph layouts - please do feel free to look at the source code, and/or implement a Sugiyama layout algorithm for me that runs in C..png)
26 | 
27 | So how does this all work?
28 |
29 | Creating an optimal layout is a known problem in the area of *Graph Theory*. There's serious mathematicians who've spent years of their live trying to figure out this problem, and numerous solutions exist. The group of solutions most relevant to our research tree (a *directed acyclic graph*, or *DAG*) is that derived from Sugiyama's work. Generally speaking, these algorithms have four steps;
30 | - layering (set the *x* coordinates of nodes, enforcing that follow-up research is always at a higher x position than any of its prerequisites, this is a fairly straightforward heuristic)
31 | - crossing reduction (set the *y* coordinates of nodes such that there is a minimal amount of intersections of connections between nodes)
32 | - edge length reduction (set the *y* coordinates of nodes such that the length of connections between nodes is minimal)
33 | - horizontal alignment (set the *y* coordinates of nodes such that the connections between nodes are straight as much as possible)
34 |
35 | The final step is the hardest, but also the most important to create a visually pleasing tree. Sadly, I've been unable to implement two of the most well known algorithms for this purpose;
36 | - Brandes, U., & Köpf, B. (2001, September). Fast and simple horizontal coordinate assignment.
37 | - Eiglsperger M., Siebenhaller M., Kaufmann M. (2005) An Efficient Implementation of Sugiyama’s Algorithm for Layered Graph Drawing.
38 | Luckily, the crossing reduction and edge length reduction steps partially achieve the goals of the final step. The final graph is not as pretty as it could be, but it's still pretty good - in most scenarios.
39 |
40 |
41 |
42 | 
43 | - Templarr: Russian translation
44 | - Suh. Junmin: Korean translation
45 | - rw-chaos: German translation
46 | - 53N4: Spanish translation
47 | - Silverside: Fix UI scaling bug for vertical text
48 | - shiuanyue: Chinese (traditional) translation
49 | - notfood: Implement techprint requirements
50 | - HanYaodong: Add simplified Chinese translation
51 |
52 | 
53 | Please read [this guide](http://steamcommunity.com/sharedfiles/filedetails/?id=725234314) before creating a bug report,
54 | and then create a bug report [here](https://github.com/fluffy-mods/ResearchTree/issues)
55 |
56 | 
57 | All current and past versions of this mod can be downloaded from [GitHub](https://github.com/fluffy-mods/ResearchTree/releases).
58 |
59 | 
60 | All original code in this mod is licensed under the [MIT license](https://opensource.org/licenses/MIT). Do what you want, but give me credit.
61 | All original content (e.g. text, imagery, sounds) in this mod is licensed under the [CC-BY-SA 4.0 license](http://creativecommons.org/licenses/by-sa/4.0/).
62 |
63 | Parts of the code in this mod, and some content may be licensed by their original authors. If this is the case, the original author & license will either be given in the source code, or be in a LICENSE file next to the content. Please do not decompile my mods, but use the original source code available on [GitHub](https://github.com/fluffy-mods/ResearchTree/), so license information in the source code is preserved.
64 |
65 | [](https://ko-fi.com/fluffymods)
66 |
67 | 
68 | Become a supporter and show your appreciation by buying me a coffee (or contribute towards a nice single malt).
69 |
70 | [](https://ko-fi.com/fluffymods)
71 |
72 | [](https://www.youtube.com/watch?v=XiCrniLQGYc)
73 |
74 |
75 | 
76 | This is version 3.17.537, for RimWorld 1.2.2753.
--------------------------------------------------------------------------------
/Source/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/Source/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | build/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 |
28 | # MSTest test Results
29 | [Tt]est[Rr]esult*/
30 | [Bb]uild[Ll]og.*
31 |
32 | # NUNIT
33 | *.VisualState.xml
34 | TestResult.xml
35 |
36 | # Build Results of an ATL Project
37 | [Dd]ebugPS/
38 | [Rr]eleasePS/
39 | dlldata.c
40 |
41 | # DNX
42 | project.lock.json
43 | artifacts/
44 |
45 | *_i.c
46 | *_p.c
47 | *_i.h
48 | *.ilk
49 | *.meta
50 | *.obj
51 | *.pch
52 | *.pdb
53 | *.pgc
54 | *.pgd
55 | *.rsp
56 | *.sbr
57 | *.tlb
58 | *.tli
59 | *.tlh
60 | *.tmp
61 | *.tmp_proj
62 | *.log
63 | *.vspscc
64 | *.vssscc
65 | .builds
66 | *.pidb
67 | *.svclog
68 | *.scc
69 |
70 | # Chutzpah Test files
71 | _Chutzpah*
72 |
73 | # Visual C++ cache files
74 | ipch/
75 | *.aps
76 | *.ncb
77 | *.opensdf
78 | *.sdf
79 | *.cachefile
80 |
81 | # Visual Studio profiler
82 | *.psess
83 | *.vsp
84 | *.vspx
85 |
86 | # TFS 2012 Local Workspace
87 | $tf/
88 |
89 | # Guidance Automation Toolkit
90 | *.gpState
91 |
92 | # ReSharper is a .NET coding add-in
93 | _ReSharper*/
94 | *.[Rr]e[Ss]harper
95 | *.DotSettings.user
96 |
97 | # JustCode is a .NET coding add-in
98 | .JustCode
99 |
100 | # TeamCity is a build add-in
101 | _TeamCity*
102 |
103 | # DotCover is a Code Coverage Tool
104 | *.dotCover
105 |
106 | # NCrunch
107 | _NCrunch_*
108 | .*crunch*.local.xml
109 |
110 | # MightyMoose
111 | *.mm.*
112 | AutoTest.Net/
113 |
114 | # Web workbench (sass)
115 | .sass-cache/
116 |
117 | # Installshield output folder
118 | [Ee]xpress/
119 |
120 | # DocProject is a documentation generator add-in
121 | DocProject/buildhelp/
122 | DocProject/Help/*.HxT
123 | DocProject/Help/*.HxC
124 | DocProject/Help/*.hhc
125 | DocProject/Help/*.hhk
126 | DocProject/Help/*.hhp
127 | DocProject/Help/Html2
128 | DocProject/Help/html
129 |
130 | # Click-Once directory
131 | publish/
132 |
133 | # Publish Web Output
134 | *.[Pp]ublish.xml
135 | *.azurePubxml
136 | ## TODO: Comment the next line if you want to checkin your
137 | ## web deploy settings but do note that will include unencrypted
138 | ## passwords
139 | #*.pubxml
140 |
141 | *.publishproj
142 |
143 | # NuGet Packages
144 | *.nupkg
145 | # The packages folder can be ignored because of Package Restore
146 | **/packages/*
147 | # except build/, which is used as an MSBuild target.
148 | !**/packages/build/
149 | # Uncomment if necessary however generally it will be regenerated when needed
150 | #!**/packages/repositories.config
151 |
152 | # Windows Azure Build Output
153 | csx/
154 | *.build.csdef
155 |
156 | # Windows Store app package directory
157 | AppPackages/
158 |
159 | # Visual Studio cache files
160 | # files ending in .cache can be ignored
161 | *.[Cc]ache
162 | # but keep track of directories ending in .cache
163 | !*.[Cc]ache/
164 |
165 | # Others
166 | ClientBin/
167 | [Ss]tyle[Cc]op.*
168 | ~$*
169 | *~
170 | *.dbmdl
171 | *.dbproj.schemaview
172 | *.pfx
173 | *.publishsettings
174 | node_modules/
175 | orleans.codegen.cs
176 |
177 | # RIA/Silverlight projects
178 | Generated_Code/
179 |
180 | # Backup & report files from converting an old project file
181 | # to a newer Visual Studio version. Backup files are not needed,
182 | # because we have git ;-)
183 | _UpgradeReport_Files/
184 | Backup*/
185 | UpgradeLog*.XML
186 | UpgradeLog*.htm
187 |
188 | # SQL Server files
189 | *.mdf
190 | *.ldf
191 |
192 | # Business Intelligence projects
193 | *.rdl.data
194 | *.bim.layout
195 | *.bim_*.settings
196 |
197 | # Microsoft Fakes
198 | FakesAssemblies/
199 |
200 | # Node.js Tools for Visual Studio
201 | .ntvs_analysis.dat
202 |
203 | # Visual Studio 6 build log
204 | *.plg
205 |
206 | # Visual Studio 6 workspace options file
207 | *.opt
208 |
209 | # LightSwitch generated files
210 | GeneratedArtifacts/
211 | _Pvt_Extensions/
212 | ModelManifest.xml
213 |
--------------------------------------------------------------------------------
/Source/.idea/.idea.ResearchTree/.idea/contentModel.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/Source/Assets.cs:
--------------------------------------------------------------------------------
1 | // Assets.cs
2 | // Copyright Karel Kroeze, 2018-2020
3 |
4 | using System.Collections.Generic;
5 | using RimWorld;
6 | using UnityEngine;
7 | using Verse;
8 |
9 | namespace FluffyResearchTree
10 | {
11 | [StaticConstructorOnStartup]
12 | public static class Assets
13 | {
14 | public static Texture2D Button = ContentFinder.Get( "Buttons/button" );
15 | public static Texture2D ButtonActive = ContentFinder.Get( "Buttons/button-active" );
16 | public static Texture2D ResearchIcon = ContentFinder.Get( "Icons/Research" );
17 | public static Texture2D MoreIcon = ContentFinder.Get( "Icons/more" );
18 | public static Texture2D Lock = ContentFinder.Get( "Icons/padlock" );
19 | internal static readonly Texture2D CircleFill = ContentFinder.Get( "Icons/circle-fill" );
20 |
21 | public static Color NegativeMouseoverColor = new Color( .4f, .1f, .1f );
22 | public static Dictionary ColorCompleted = new Dictionary();
23 | public static Dictionary ColorAvailable = new Dictionary();
24 | public static Dictionary ColorUnavailable = new Dictionary();
25 | public static Color TechLevelColor = new Color( 1f, 1f, 1f, .2f );
26 |
27 | public static Texture2D SlightlyDarkBackground =
28 | SolidColorMaterials.NewSolidColorTexture( 0f, 0f, 0f, .1f );
29 |
30 | public static Texture2D Search =
31 | ContentFinder.Get( "Icons/magnifying-glass" );
32 |
33 | static Assets()
34 | {
35 | var techlevels = Tree.RelevantTechLevels;
36 | var n = techlevels.Count;
37 | for ( var i = 0; i < n; i++ )
38 | {
39 | ColorCompleted[techlevels[i]] = Color.HSVToRGB( 1f / n * i, .75f, .75f );
40 | ColorAvailable[techlevels[i]] = Color.HSVToRGB( 1f / n * i, .33f, .33f );
41 | ColorUnavailable[techlevels[i]] = Color.HSVToRGB( 1f / n * i, .125f, .33f );
42 | }
43 | }
44 |
45 | [StaticConstructorOnStartup]
46 | public static class Lines
47 | {
48 | public static Texture2D Circle = ContentFinder.Get( "Lines/Outline/circle" );
49 | public static Texture2D End = ContentFinder.Get( "Lines/Outline/end" );
50 | public static Texture2D EW = ContentFinder.Get( "Lines/Outline/ew" );
51 | public static Texture2D NS = ContentFinder.Get( "Lines/Outline/ns" );
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/Source/Constants.cs:
--------------------------------------------------------------------------------
1 | // Constants.cs
2 | // Copyright Karel Kroeze, 2018-2020
3 |
4 | using UnityEngine;
5 |
6 | namespace FluffyResearchTree
7 | {
8 | public static class Constants
9 | {
10 | public const double Epsilon = 1e-4;
11 | public const float HubSize = 16f;
12 | public const float DetailedModeZoomLevelCutoff = 1.5f;
13 | public const float Margin = 6f;
14 | public const float QueueLabelSize = 30f;
15 | public const float SmallQueueLabelSize = 20f;
16 | public const float AbsoluteMaxZoomLevel = 3f;
17 | public const float ZoomStep = .05f;
18 | public static readonly Vector2 IconSize = new Vector2( 18f, 18f );
19 | public static readonly Vector2 NodeMargins = new Vector2( 50f, 10f );
20 | public static readonly Vector2 NodeSize = new Vector2( 200f, 50f );
21 | public static readonly float TopBarHeight = NodeSize.y + Margin * 2;
22 | public static readonly Vector2 TechLevelLabelSize = new Vector2( 200f, 30f );
23 | }
24 | }
--------------------------------------------------------------------------------
/Source/Extensions/Building_ResearchBench_Extensions.cs:
--------------------------------------------------------------------------------
1 | // Building_ResearchBench_Extensions.cs
2 | // Copyright Karel Kroeze, 2016-2020
3 |
4 | using System.Linq;
5 | using RimWorld;
6 | using Verse;
7 |
8 | namespace FluffyResearchTree
9 | {
10 | public static class Building_ResearchBench_Extensions
11 | {
12 | public static bool HasFacility( this Building_ResearchBench building, ThingDef facility )
13 | {
14 | var comp = building.GetComp();
15 | if ( comp == null )
16 | return false;
17 |
18 | if ( comp.LinkedFacilitiesListForReading.Select( f => f.def ).Contains( facility ) )
19 | return true;
20 |
21 | return false;
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/Source/Extensions/Def_Extensions.cs:
--------------------------------------------------------------------------------
1 | // Def_Extensions.cs
2 | // Copyright Karel Kroeze, 2018-2020
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using RimWorld;
7 | using UnityEngine;
8 | using Verse;
9 |
10 | namespace FluffyResearchTree
11 | {
12 | public static class Def_Extensions
13 | {
14 | ///
15 | /// hold a cached list of icons per def
16 | ///
17 | private static readonly Dictionary _cachedDefIcons = new Dictionary();
18 |
19 | private static readonly Dictionary _cachedIconColors = new Dictionary();
20 |
21 | public static void DrawColouredIcon( this Def def, Rect canvas )
22 | {
23 | GUI.color = def.IconColor();
24 | GUI.DrawTexture( canvas, def.IconTexture(), ScaleMode.ScaleToFit );
25 | GUI.color = Color.white;
26 | }
27 |
28 | ///
29 | /// Gets an appropriate drawColor for this def.
30 | /// Will use a default stuff or DrawColor, if defined.
31 | ///
32 | ///
33 | ///
34 | public static Color IconColor( this Def def )
35 | {
36 | // garbage in, garbage out
37 | if ( def == null )
38 | return Color.cyan;
39 |
40 | // check cache
41 | if ( _cachedIconColors.ContainsKey( def ) ) return _cachedIconColors[def];
42 |
43 | // otherwise try to determine icon
44 | var bdef = def as BuildableDef;
45 | var tdef = def as ThingDef;
46 | var pdef = def as PawnKindDef;
47 | var rdef = def as RecipeDef;
48 |
49 | // get product color for recipes
50 | if ( rdef != null )
51 | if ( !rdef.products.NullOrEmpty() )
52 | {
53 | _cachedIconColors.Add( def, rdef.products.First().thingDef.IconColor() );
54 | return _cachedIconColors[def];
55 | }
56 |
57 | // get color from final lifestage for pawns
58 | if ( pdef != null )
59 | {
60 | _cachedIconColors.Add( def, pdef.lifeStages.Last().bodyGraphicData.color );
61 | return _cachedIconColors[def];
62 | }
63 |
64 | if ( bdef == null )
65 | {
66 | // if we reach this point, def.IconTexture() would return null. Just store and return white to make sure we don't get weird errors down the line.
67 | _cachedIconColors.Add( def, Color.white );
68 | return _cachedIconColors[def];
69 | }
70 |
71 | // built def != listed def
72 | if (
73 | tdef != null &&
74 | tdef.entityDefToBuild != null
75 | )
76 | {
77 | _cachedIconColors.Add( def, tdef.entityDefToBuild.IconColor() );
78 | return _cachedIconColors[def];
79 | }
80 |
81 | // graphic.color set?
82 | if ( bdef.graphic != null )
83 | {
84 | _cachedIconColors.Add( def, bdef.graphic.color );
85 | return _cachedIconColors[def];
86 | }
87 |
88 | // stuff used?
89 | if (
90 | tdef != null &&
91 | tdef.MadeFromStuff
92 | )
93 | {
94 | var stuff = GenStuff.DefaultStuffFor( tdef );
95 | _cachedIconColors.Add( def, stuff.stuffProps.color );
96 | return _cachedIconColors[def];
97 | }
98 |
99 | // all else failed.
100 | _cachedIconColors.Add( def, Color.white );
101 | return _cachedIconColors[def];
102 | }
103 |
104 | ///
105 | /// Get a texture for the def, where defined.
106 | ///
107 | ///
108 | ///
109 | public static Texture2D IconTexture( this Def def )
110 | {
111 | // garbage in, garbage out
112 | if ( def == null )
113 | return null;
114 |
115 | // check cache
116 | if ( _cachedDefIcons.ContainsKey( def ) )
117 | return _cachedDefIcons[def];
118 |
119 | // otherwise try to determine icon
120 | var buildableDef = def as BuildableDef;
121 | var thingDef = def as ThingDef;
122 | var pawnKindDef = def as PawnKindDef;
123 | var recipeDef = def as RecipeDef;
124 |
125 | // recipes will be passed icon of first product, if defined.
126 | if (
127 | recipeDef != null &&
128 | !recipeDef.products.NullOrEmpty()
129 | )
130 | {
131 | _cachedDefIcons.Add( def, recipeDef.products.First().thingDef.IconTexture() );
132 | return _cachedDefIcons[def];
133 | }
134 |
135 | // animals need special treatment ( this will still only work for animals, pawns are a whole different can o' worms ).
136 | if ( pawnKindDef != null )
137 | try
138 | {
139 | _cachedDefIcons.Add(
140 | def, pawnKindDef.lifeStages.Last().bodyGraphicData.Graphic.MatSouth.mainTexture as Texture2D );
141 | return _cachedDefIcons[def];
142 | }
143 | catch
144 | {
145 | // ignored
146 | }
147 |
148 | if ( buildableDef != null )
149 | {
150 | // if def built != def listed.
151 | if ( thingDef?.entityDefToBuild != null )
152 | {
153 | _cachedDefIcons.Add( def, thingDef.entityDefToBuild.IconTexture() );
154 | return _cachedDefIcons[def];
155 | }
156 |
157 | _cachedDefIcons.Add( def, buildableDef.uiIcon );
158 | return buildableDef.uiIcon;
159 | }
160 |
161 | // nothing stuck
162 | _cachedDefIcons.Add( def, null );
163 | return null;
164 | }
165 | }
166 | }
--------------------------------------------------------------------------------
/Source/Extensions/ResearchProjectDef_Extensions.cs:
--------------------------------------------------------------------------------
1 | // ResearchProjectDef_Extensions.cs
2 | // Copyright Karel Kroeze, 2019-2020
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Runtime.Remoting.Messaging;
7 | using Verse;
8 |
9 | namespace FluffyResearchTree
10 | {
11 | public static class ResearchProjectDef_Extensions
12 | {
13 | private static readonly Dictionary>> _unlocksCache =
14 | new Dictionary>>();
15 |
16 | public static List Descendants( this ResearchProjectDef research )
17 | {
18 | var descendants = new HashSet();
19 |
20 | // recursively go through all children
21 | // populate initial queue
22 | var queue = new Queue(
23 | DefDatabase.AllDefsListForReading.Where(
24 | res => res.prerequisites?.Contains( research ) ?? false ) );
25 |
26 | // add to the list, and queue up children.
27 | while ( queue.Count > 0 )
28 | {
29 | var current = queue.Dequeue();
30 | descendants.Add( current );
31 |
32 | foreach ( var descendant in DefDatabase.AllDefsListForReading.Where(
33 | res =>
34 | res.prerequisites?.Contains(
35 | current ) ??
36 | false && !descendants.Contains(
37 | res ) ) )
38 | queue.Enqueue( descendant );
39 | }
40 |
41 | return descendants.ToList();
42 | }
43 |
44 | public static IEnumerable GetPlantsUnlocked( this ResearchProjectDef research )
45 | {
46 | return DefDatabase.AllDefsListForReading
47 | .Where(
48 | td => td.plant?.sowResearchPrerequisites?.Contains( research ) ?? false );
49 | }
50 |
51 | public static List Ancestors( this ResearchProjectDef research )
52 | {
53 | // keep a list of prerequites
54 | var prerequisites = new List();
55 | if ( research.prerequisites.NullOrEmpty() )
56 | return prerequisites;
57 |
58 | // keep a stack of prerequisites that should be checked
59 | var stack = new Stack( research.prerequisites.Where( parent => parent != research ) );
60 |
61 | // keep on checking everything on the stack until there is nothing left
62 | while ( stack.Count > 0 )
63 | {
64 | // add to list of prereqs
65 | var parent = stack.Pop();
66 | prerequisites.Add( parent );
67 |
68 | // add prerequitsite's prereqs to the stack
69 | if ( !parent.prerequisites.NullOrEmpty() )
70 | foreach ( var grandparent in parent.prerequisites )
71 | // but only if not a prerequisite of itself, and not a cyclic prerequisite
72 | if ( grandparent != parent && !prerequisites.Contains( grandparent ) )
73 | stack.Push( grandparent );
74 | }
75 |
76 | return prerequisites.Distinct().ToList();
77 | }
78 |
79 | public static IEnumerable GetRecipesUnlocked( this ResearchProjectDef research )
80 | {
81 | // recipe directly locked behind research
82 | var direct =
83 | DefDatabase.AllDefsListForReading.Where( rd => rd.researchPrerequisite == research );
84 |
85 | // recipe building locked behind research
86 | var building = DefDatabase.AllDefsListForReading
87 | .Where(
88 | td => ( td.researchPrerequisites?.Contains( research ) ?? false )
89 | && !td.AllRecipes.NullOrEmpty() )
90 | .SelectMany( td => td.AllRecipes )
91 | .Where( rd => rd.researchPrerequisite == null );
92 |
93 | // return union of these two sets
94 | return direct.Concat( building ).Distinct();
95 | }
96 |
97 | public static IEnumerable GetTerrainUnlocked( this ResearchProjectDef research )
98 | {
99 | return DefDatabase.AllDefsListForReading
100 | .Where( td => td.researchPrerequisites?.Contains( research ) ?? false );
101 | }
102 |
103 | public static IEnumerable GetThingsUnlocked( this ResearchProjectDef research )
104 | {
105 | return DefDatabase.AllDefsListForReading
106 | .Where( td => td.researchPrerequisites?.Contains( research ) ?? false );
107 | }
108 |
109 | public static List> GetUnlockDefsAndDescs( this ResearchProjectDef research, bool dedupe = true )
110 | {
111 | if ( _unlocksCache.ContainsKey( research ) )
112 | return _unlocksCache[research];
113 |
114 | var unlocks = new List>();
115 |
116 | unlocks.AddRange( research.GetThingsUnlocked()
117 | .Where( d => d.IconTexture() != null )
118 | .Select( d => new Pair(
119 | d, "Fluffy.ResearchTree.AllowsBuildingX".Translate( d.LabelCap ) ) ) );
120 | unlocks.AddRange( research.GetTerrainUnlocked()
121 | .Where( d => d.IconTexture() != null )
122 | .Select( d => new Pair(
123 | d, "Fluffy.ResearchTree.AllowsBuildingX".Translate( d.LabelCap ) ) ) );
124 | unlocks.AddRange( research.GetRecipesUnlocked()
125 | .Where( d => d.IconTexture() != null )
126 | .Select( d => new Pair(
127 | d, "Fluffy.ResearchTree.AllowsCraftingX".Translate( d.LabelCap ) ) ) );
128 | unlocks.AddRange( research.GetPlantsUnlocked()
129 | .Where( d => d.IconTexture() != null )
130 | .Select( d => new Pair(
131 | d, "Fluffy.ResearchTree.AllowsPlantingX".Translate( d.LabelCap ) ) ) );
132 |
133 | // get unlocks for all descendant research, and remove duplicates.
134 | var descendants = research.Descendants();
135 | if ( dedupe && descendants.Any() )
136 | {
137 | var descendantUnlocks = research.Descendants()
138 | .SelectMany( c => c.GetUnlockDefsAndDescs( false ).Select( u => u.First ) )
139 | .Distinct()
140 | .ToList();
141 | unlocks = unlocks.Where( u => !descendantUnlocks.Contains( u.First ) ).ToList();
142 | }
143 |
144 | _unlocksCache.Add( research, unlocks );
145 | return unlocks;
146 | }
147 |
148 | public static ResearchNode ResearchNode( this ResearchProjectDef research )
149 | {
150 | var node = Tree.Nodes.OfType().FirstOrDefault( n => n.Research == research );
151 | if ( node == null )
152 | Log.Error( "Node for {0} not found. Was it intentionally hidden or locked?", true, research.LabelCap );
153 | return node;
154 | }
155 | }
156 | }
--------------------------------------------------------------------------------
/Source/Extensions/SyncWorker_ResearchNode.cs:
--------------------------------------------------------------------------------
1 | // SyncWorker_ResearchNode.cs
2 | // Copyright Karel Kroeze, 2020-2020
3 |
4 | //using System.Collections.Generic;
5 | //using System.Linq;
6 | //using Multiplayer.API;
7 | //using Verse;
8 | //
9 | //namespace FluffyResearchTree
10 | //{
11 | // public class SyncWorker_ResearchNode
12 | // {
13 | // [SyncWorker]
14 | // static void SyncResearchNode( SyncWorker worker, ref ResearchNode node )
15 | // {
16 | // Log.Debug( $"Syncing" );
17 | // if ( worker.isWriting )
18 | // {
19 | // Log.Debug( $"writing" );
20 | // worker.Write( node.Research.defName );
21 | // }
22 | // else
23 | // {
24 | // Log.Debug( $"reading" );
25 | // string researchDef = worker.Read();
26 | // node = ( DefDatabase.GetNamed( researchDef ) ).ResearchNode();
27 | // }
28 | // }
29 | //
30 | // [SyncWorker]
31 | // static void SyncResearchNodes( SyncWorker worker, ref IEnumerable nodes )
32 | // {
33 | //
34 | // Log.Debug( $"Syncing" );
35 | // if ( worker.isWriting )
36 | // {
37 | // Log.Debug( $"writing" );
38 | // worker.Write( nodes.Select( node => node.Research.defName ) );
39 | // }
40 | // else
41 | // {
42 | // Log.Debug( $"reading" );
43 | // var defNames = worker.Read>();
44 | // nodes = defNames.Select( name => ( DefDatabase.GetNamed( name ) ).ResearchNode() );
45 | // }
46 | // }
47 | // }
48 | //
49 | //
50 | //}
51 |
52 |
--------------------------------------------------------------------------------
/Source/FloatMenu_Fixed.cs:
--------------------------------------------------------------------------------
1 | // FloatMenu_Fixed.cs
2 | // Copyright Karel Kroeze, 2018-2020
3 |
4 | using System.Collections.Generic;
5 | using UnityEngine;
6 | using Verse;
7 |
8 | namespace FluffyResearchTree
9 | {
10 | public class FloatMenu_Fixed : FloatMenu
11 | {
12 | private readonly Vector2 _position;
13 |
14 | public FloatMenu_Fixed( List options, Vector2 position, bool focus = false ) : base( options )
15 | {
16 | _position = position;
17 | vanishIfMouseDistant = false;
18 | focusWhenOpened = focus;
19 | }
20 |
21 | protected override void SetInitialSizeAndPosition()
22 | {
23 | var position = _position;
24 | if ( position.x + InitialSize.x > UI.screenWidth ) position.x = UI.screenWidth - InitialSize.x;
25 | if ( position.y + InitialSize.y > UI.screenHeight ) position.y = UI.screenHeight - InitialSize.y;
26 | windowRect = new Rect( position.x, position.y, InitialSize.x, InitialSize.y );
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/Source/Graph/DummyNode.cs:
--------------------------------------------------------------------------------
1 | // DummyNode.cs
2 | // Copyright Karel Kroeze, 2018-2020
3 |
4 | using System.Linq;
5 | using UnityEngine;
6 |
7 | namespace FluffyResearchTree
8 | {
9 | public class DummyNode : Node
10 | {
11 | #region Overrides of Node
12 |
13 | public override string Label => "DUMMY: " + ( Parent?.Label ?? "??" ) + " -> " + ( Child?.Label ?? "??" );
14 |
15 | #endregion
16 |
17 | #region Overrides of Node
18 |
19 | #if DEBUG_DUMMIES
20 | public override void Draw()
21 | {
22 | // cop out if off-screen
23 | var screen = new Rect( MainTabWindow_ResearchTree._scrollPosition.x,
24 | MainTabWindow_ResearchTree._scrollPosition.y, Screen.width, Screen.height - 35 );
25 | if ( Rect.xMin > screen.xMax ||
26 | Rect.xMax < screen.xMin ||
27 | Rect.yMin > screen.yMax ||
28 | Rect.yMax < screen.yMin )
29 | {
30 | return;
31 | }
32 |
33 | Widgets.DrawBox( Rect );
34 | Widgets.Label( Rect, Label );
35 | }
36 | #endif
37 |
38 | #endregion
39 |
40 | public ResearchNode Parent
41 | {
42 | get
43 | {
44 | var parent = InNodes.FirstOrDefault() as ResearchNode;
45 | if ( parent != null )
46 | return parent;
47 |
48 | var dummyParent = InNodes.FirstOrDefault() as DummyNode;
49 |
50 | return dummyParent?.Parent;
51 | }
52 | }
53 |
54 | public ResearchNode Child
55 | {
56 | get
57 | {
58 | var child = OutNodes.FirstOrDefault() as ResearchNode;
59 | if ( child != null )
60 | return child;
61 |
62 | var dummyChild = OutNodes.FirstOrDefault() as DummyNode;
63 |
64 | return dummyChild?.Child;
65 | }
66 | }
67 |
68 | public override bool Completed => OutNodes.FirstOrDefault()?.Completed ?? false;
69 | public override bool Available => OutNodes.FirstOrDefault()?.Available ?? false;
70 | public override bool Highlighted => OutNodes.FirstOrDefault()?.Highlighted ?? false;
71 | public override Color Color => OutNodes.FirstOrDefault()?.Color ?? Color.white;
72 | public override Color EdgeColor => OutNodes.FirstOrDefault()?.EdgeColor ?? Color.white;
73 | }
74 | }
--------------------------------------------------------------------------------
/Source/Graph/Edge.cs:
--------------------------------------------------------------------------------
1 | // Edge.cs
2 | // Copyright Karel Kroeze, 2018-2020
3 |
4 | using System;
5 | using UnityEngine;
6 | using static FluffyResearchTree.Assets;
7 | using static FluffyResearchTree.Constants;
8 |
9 | namespace FluffyResearchTree
10 | {
11 | public class Edge where T1 : Node where T2 : Node
12 | {
13 | private T1 _in;
14 | private T2 _out;
15 |
16 | public Edge( T1 @in, T2 @out )
17 | {
18 | _in = @in;
19 | _out = @out;
20 | IsDummy = _out is DummyNode;
21 | }
22 |
23 | public T1 In
24 | {
25 | get => _in;
26 | set
27 | {
28 | _in = value;
29 | IsDummy = _out is DummyNode;
30 | }
31 | }
32 |
33 | public T2 Out
34 | {
35 | get => _out;
36 | set
37 | {
38 | _out = value;
39 | IsDummy = _out is DummyNode;
40 | }
41 | }
42 |
43 | public int Span => _out.X - _in.X;
44 | public float Length => Mathf.Abs( _in.Yf - _out.Yf ) * ( IsDummy ? 10 : 1 );
45 | public bool IsDummy { get; private set; }
46 |
47 | public int DrawOrder
48 | {
49 | get
50 | {
51 | if ( Out.Highlighted )
52 | return 3;
53 | if ( Out.Completed )
54 | return 2;
55 | if ( Out.Available )
56 | return 1;
57 | return 0;
58 | }
59 | }
60 |
61 | public void Draw( Rect visibleRect )
62 | {
63 | if ( !In.IsVisible( visibleRect ) && !Out.IsVisible( visibleRect ) )
64 | return;
65 |
66 | var color = Out.EdgeColor;
67 | GUI.color = color;
68 |
69 | var left = In.Right;
70 | var right = Out.Left;
71 |
72 | // if left and right are on the same level, just draw a straight line.
73 | if ( Math.Abs( left.y - right.y ) < Epsilon )
74 | {
75 | var line = new Rect( left.x, left.y - 2f, right.x - left.x, 4f );
76 | GUI.DrawTexture( line, Lines.EW );
77 | }
78 |
79 | // draw three line pieces and two curves.
80 | else
81 | {
82 | // determine top and bottom y positions
83 | var top = Math.Min( left.y, right.y ) + NodeMargins.x / 4f;
84 | var bottom = Math.Max( left.y, right.y ) - NodeMargins.x / 4f;
85 |
86 | // straight bits
87 | // left to curve
88 | var leftToCurve = new Rect(
89 | left.x,
90 | left.y - 2f,
91 | NodeMargins.x / 4f,
92 | 4f );
93 | GUI.DrawTexture( leftToCurve, Lines.EW );
94 |
95 | // curve to curve
96 | var curveToCurve = new Rect(
97 | left.x + NodeMargins.x / 2f - 2f,
98 | top,
99 | 4f,
100 | bottom - top );
101 | GUI.DrawTexture( curveToCurve, Lines.NS );
102 |
103 | // curve to right
104 | var curveToRight = new Rect(
105 | left.x + NodeMargins.x / 4f * 3,
106 | right.y - 2f,
107 | right.x - left.x - NodeMargins.x / 4f * 3,
108 | 4f );
109 | GUI.DrawTexture( curveToRight, Lines.EW );
110 |
111 | // curve positions
112 | var curveLeft = new Rect(
113 | left.x + NodeMargins.x / 4f,
114 | left.y - NodeMargins.x / 4f,
115 | NodeMargins.x / 2f,
116 | NodeMargins.x / 2f );
117 | var curveRight = new Rect(
118 | left.x + NodeMargins.x / 4f,
119 | right.y - NodeMargins.x / 4f,
120 | NodeMargins.x / 2f,
121 | NodeMargins.x / 2f );
122 |
123 | // going down
124 | if ( left.y < right.y )
125 | {
126 | GUI.DrawTextureWithTexCoords( curveLeft, Lines.Circle, new Rect( 0.5f, 0.5f, 0.5f, 0.5f ) );
127 | // bottom right quadrant
128 | GUI.DrawTextureWithTexCoords( curveRight, Lines.Circle, new Rect( 0f, 0f, 0.5f, 0.5f ) );
129 | // top left quadrant
130 | }
131 | // going up
132 | else
133 | {
134 | GUI.DrawTextureWithTexCoords( curveLeft, Lines.Circle, new Rect( 0.5f, 0f, 0.5f, 0.5f ) );
135 | // top right quadrant
136 | GUI.DrawTextureWithTexCoords( curveRight, Lines.Circle, new Rect( 0f, 0.5f, 0.5f, 0.5f ) );
137 | // bottom left quadrant
138 | }
139 | }
140 |
141 | // draw the end arrow (if not dummy)
142 | if ( !IsDummy )
143 | {
144 | var end = new Rect(
145 | right.x - 16f,
146 | right.y - 8f,
147 | 16f,
148 | 16f );
149 | GUI.DrawTexture( end, Lines.End );
150 | }
151 |
152 | // or draw a line piece through the dummy
153 | else
154 | {
155 | var through = new Rect(
156 | right.x,
157 | right.y - 2,
158 | NodeSize.x,
159 | 4f
160 | );
161 | GUI.DrawTexture( through, Lines.EW );
162 | }
163 |
164 | // reset color
165 | GUI.color = Color.white;
166 | }
167 |
168 | public override string ToString()
169 | {
170 | return _in + " -> " + _out;
171 | }
172 | }
173 | }
--------------------------------------------------------------------------------
/Source/Graph/Node.cs:
--------------------------------------------------------------------------------
1 | // Node.cs
2 | // Copyright Karel Kroeze, 2019-2020
3 |
4 | // #define TRACE_POS
5 |
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Text;
10 | using UnityEngine;
11 | using Verse;
12 | using static FluffyResearchTree.Constants;
13 |
14 | namespace FluffyResearchTree
15 | {
16 | public class Node
17 | {
18 | protected const float Offset = 2f;
19 | protected List> _inEdges = new List>();
20 | protected bool _largeLabel;
21 | protected List> _outEdges = new List>();
22 | protected Vector2 _pos = Vector2.zero;
23 |
24 | protected Rect
25 | _queueRect,
26 | _rect,
27 | _labelRect,
28 | _costLabelRect,
29 | _costIconRect,
30 | _iconsRect,
31 | _lockRect;
32 |
33 | protected bool _rectsSet;
34 |
35 | protected Vector2 _topLeft = Vector2.zero,
36 | _right = Vector2.zero,
37 | _left = Vector2.zero;
38 |
39 | public List Descendants
40 | {
41 | get { return OutNodes.Concat( OutNodes.SelectMany( n => n.Descendants ) ).ToList(); }
42 | }
43 |
44 | public List> OutEdges => _outEdges;
45 | public List OutNodes => _outEdges.Select( e => e.Out ).ToList();
46 | public List> InEdges => _inEdges;
47 | public List InNodes => _inEdges.Select( e => e.In ).ToList();
48 | public List> Edges => _inEdges.Concat( _outEdges ).ToList();
49 | public List Nodes => InNodes.Concat( OutNodes ).ToList();
50 |
51 | public Rect CostIconRect
52 | {
53 | get
54 | {
55 | if ( !_rectsSet )
56 | SetRects();
57 |
58 | return _costIconRect;
59 | }
60 | }
61 |
62 | public Rect CostLabelRect
63 | {
64 | get
65 | {
66 | if ( !_rectsSet )
67 | SetRects();
68 |
69 | return _costLabelRect;
70 | }
71 | }
72 |
73 | public virtual Color Color => Color.white;
74 | public virtual Color EdgeColor => Color;
75 |
76 | public Rect IconsRect
77 | {
78 | get
79 | {
80 | if ( !_rectsSet )
81 | SetRects();
82 |
83 | return _iconsRect;
84 | }
85 | }
86 |
87 | public Rect LabelRect
88 | {
89 | get
90 | {
91 | if ( !_rectsSet )
92 | SetRects();
93 |
94 | return _labelRect;
95 | }
96 | }
97 |
98 | ///
99 | /// Middle of left node edge
100 | ///
101 | public Vector2 Left
102 | {
103 | get
104 | {
105 | if ( !_rectsSet )
106 | SetRects();
107 |
108 | return _left;
109 | }
110 | }
111 |
112 | ///
113 | /// Tag UI Rect
114 | ///
115 | public Rect QueueRect
116 | {
117 | get
118 | {
119 | if ( !_rectsSet )
120 | SetRects();
121 |
122 | return _queueRect;
123 | }
124 | }
125 |
126 | public Rect LockRect
127 | {
128 | get
129 | {
130 | if ( !_rectsSet )
131 | SetRects();
132 |
133 | return _lockRect;
134 | }
135 | }
136 |
137 | ///
138 | /// Static UI rect for this node
139 | ///
140 | public Rect Rect
141 | {
142 | get
143 | {
144 | if ( !_rectsSet )
145 | SetRects();
146 |
147 | return _rect;
148 | }
149 | }
150 |
151 | ///
152 | /// Middle of right node edge
153 | ///
154 | public Vector2 Right
155 | {
156 | get
157 | {
158 | if ( !_rectsSet )
159 | SetRects();
160 |
161 | return _right;
162 | }
163 | }
164 |
165 | public Vector2 Center => ( Left + Right ) / 2f;
166 |
167 | public virtual int X
168 | {
169 | get => (int) _pos.x;
170 | set
171 | {
172 | if ( value < 0 )
173 | throw new ArgumentOutOfRangeException( nameof( value ) );
174 | if ( Math.Abs( _pos.x - value ) < Epsilon )
175 | return;
176 |
177 | Log.Trace( "\t" + this + " X: " + _pos.x + " -> " + value );
178 | _pos.x = value;
179 |
180 | // update caches
181 | _rectsSet = false;
182 | Tree.Size.x = Tree.Nodes.Max( n => n.X );
183 | Tree.OrderDirty = true;
184 | }
185 | }
186 |
187 | public virtual int Y
188 | {
189 | get => (int) _pos.y;
190 | set
191 | {
192 | if ( value < 0 )
193 | throw new ArgumentOutOfRangeException( nameof( value ) );
194 | if ( Math.Abs( _pos.y - value ) < Epsilon )
195 | return;
196 |
197 | Log.Trace( "\t" + this + " Y: " + _pos.y + " -> " + value );
198 | _pos.y = value;
199 |
200 | // update caches
201 | _rectsSet = false;
202 | Tree.Size.z = Tree.Nodes.Max( n => n.Y );
203 | Tree.OrderDirty = true;
204 | }
205 | }
206 |
207 | public virtual Vector2 Pos => new Vector2( X, Y );
208 |
209 | public virtual float Yf
210 | {
211 | get => _pos.y;
212 | set
213 | {
214 | if ( Math.Abs( _pos.y - value ) < Epsilon )
215 | return;
216 |
217 | _pos.y = value;
218 |
219 | // update caches
220 | Tree.Size.z = Tree.Nodes.Max( n => n.Y ) + 1;
221 | Tree.OrderDirty = true;
222 | }
223 | }
224 |
225 | public virtual string Label { get; }
226 |
227 | public virtual bool Completed => false;
228 | public virtual bool Available => false;
229 | public virtual bool Highlighted { get; set; }
230 |
231 | protected internal virtual bool SetDepth( int min = 1 )
232 | {
233 | // calculate desired position
234 | var isRoot = InNodes.NullOrEmpty();
235 | var desired = isRoot ? 1 : InNodes.Max( n => n.X ) + 1;
236 | var depth = Mathf.Max( desired, min );
237 |
238 | // no change
239 | if ( depth == X )
240 | return false;
241 |
242 | // update
243 | X = depth;
244 | return true;
245 | }
246 |
247 | ///
248 | /// Prints debug information.
249 | ///
250 | public virtual void Debug()
251 | {
252 | var text = new StringBuilder();
253 | text.AppendLine( Label + " (" + X + ", " + Y + "):" );
254 | text.AppendLine( "- Parents" );
255 | foreach ( var parent in InNodes ) text.AppendLine( "-- " + parent.Label );
256 |
257 | text.AppendLine( "- Children" );
258 | foreach ( var child in OutNodes ) text.AppendLine( "-- " + child.Label );
259 |
260 | text.AppendLine( "" );
261 | Log.Message( text.ToString() );
262 | }
263 |
264 |
265 | public override string ToString()
266 | {
267 | return Label + _pos;
268 | }
269 |
270 | public void SetRects()
271 | {
272 | // origin
273 | _topLeft = new Vector2(
274 | ( X - 1 ) * ( NodeSize.x + NodeMargins.x ),
275 | ( Yf - 1 ) * ( NodeSize.y + NodeMargins.y ) );
276 |
277 | SetRects( _topLeft );
278 | }
279 |
280 | public void SetRects( Vector2 topLeft )
281 | {
282 | // main rect
283 | _rect = new Rect( topLeft.x,
284 | topLeft.y,
285 | NodeSize.x,
286 | NodeSize.y );
287 |
288 | // left and right edges
289 | _left = new Vector2( _rect.xMin, _rect.yMin + _rect.height / 2f );
290 | _right = new Vector2( _rect.xMax, _left.y );
291 |
292 | // queue rect
293 | _queueRect = new Rect( _rect.xMax - QueueLabelSize / 2f,
294 | _rect.yMin + ( _rect.height - QueueLabelSize ) / 2f, QueueLabelSize,
295 | QueueLabelSize );
296 |
297 | // label rect
298 | _labelRect = new Rect( _rect.xMin + 6f,
299 | _rect.yMin + 3f,
300 | _rect.width * 2f / 3f - 6f,
301 | _rect.height * .5f - 3f );
302 |
303 | // research cost rect
304 | _costLabelRect = new Rect( _rect.xMin + _rect.width * 2f / 3f,
305 | _rect.yMin + 3f,
306 | _rect.width * 1f / 3f - 16f - 3f,
307 | _rect.height * .5f - 3f );
308 |
309 | // research icon rect
310 | _costIconRect = new Rect( _costLabelRect.xMax,
311 | _rect.yMin + ( _costLabelRect.height - 16f ) / 2,
312 | 16f,
313 | 16f );
314 |
315 | // icon container rect
316 | _iconsRect = new Rect( _rect.xMin,
317 | _rect.yMin + _rect.height * .5f,
318 | _rect.width,
319 | _rect.height * .5f );
320 |
321 | // lock icon rect
322 | _lockRect = new Rect( 0f, 0f, 32f, 32f );
323 | _lockRect = _lockRect.CenteredOnXIn( _rect );
324 | _lockRect = _lockRect.CenteredOnYIn( _rect );
325 |
326 | // see if the label is too big
327 | _largeLabel = Text.CalcHeight( Label, _labelRect.width ) > _labelRect.height;
328 |
329 | // done
330 | _rectsSet = true;
331 | }
332 |
333 | public virtual bool IsVisible( Rect visibleRect )
334 | {
335 | return !(
336 | Rect.xMin > visibleRect.xMax ||
337 | Rect.xMax < visibleRect.xMin ||
338 | Rect.yMin > visibleRect.yMax ||
339 | Rect.yMax < visibleRect.yMin );
340 | }
341 |
342 | public virtual void Draw( Rect visibleRect, bool forceDetailedMode = false )
343 | {
344 | }
345 | }
346 | }
--------------------------------------------------------------------------------
/Source/Graph/ResearchNode.cs:
--------------------------------------------------------------------------------
1 | // ResearchNode.cs
2 | // Copyright Karel Kroeze, 2020-2020
3 |
4 | using System.Collections.Generic;
5 | using System.Globalization;
6 | using System.Linq;
7 | using System.Text;
8 | using RimWorld;
9 | using UnityEngine;
10 | using Verse;
11 | using static FluffyResearchTree.Constants;
12 |
13 | namespace FluffyResearchTree
14 | {
15 | public class ResearchNode : Node
16 | {
17 | private static readonly Dictionary _buildingPresentCache =
18 | new Dictionary();
19 |
20 | private static readonly Dictionary> _missingFacilitiesCache =
21 | new Dictionary>();
22 |
23 | public ResearchProjectDef Research;
24 |
25 | public ResearchNode( ResearchProjectDef research )
26 | {
27 | Research = research;
28 |
29 | // initialize position at vanilla y position, leave x at zero - we'll determine this ourselves
30 | _pos = new Vector2( 0, research.researchViewY + 1 );
31 | }
32 |
33 | public List Parents
34 | {
35 | get
36 | {
37 | var parents = InNodes.OfType();
38 | parents.Concat( InNodes.OfType().Select( dn => dn.Parent ) );
39 | return parents.ToList();
40 | }
41 | }
42 |
43 | public override Color Color
44 | {
45 | get
46 | {
47 | if ( Highlighted )
48 | return GenUI.MouseoverColor;
49 | if ( Completed )
50 | return Assets.ColorCompleted[Research.techLevel];
51 | if ( Available )
52 | return Assets.ColorCompleted[Research.techLevel];
53 | return Assets.ColorUnavailable[Research.techLevel];
54 | }
55 | }
56 |
57 | public override Color EdgeColor
58 | {
59 | get
60 | {
61 | if ( Highlighted )
62 | return GenUI.MouseoverColor;
63 | if ( Completed )
64 | return Assets.ColorCompleted[Research.techLevel];
65 | if ( Available )
66 | return Assets.ColorAvailable[Research.techLevel];
67 | return Assets.ColorUnavailable[Research.techLevel];
68 | }
69 | }
70 |
71 | public List Children
72 | {
73 | get
74 | {
75 | var children = OutNodes.OfType();
76 | children.Concat( OutNodes.OfType().Select( dn => dn.Child ) );
77 | return children.ToList();
78 | }
79 | }
80 |
81 | public override string Label => Research.LabelCap;
82 |
83 | public static bool BuildingPresent( ResearchProjectDef research )
84 | {
85 | if ( DebugSettings.godMode && Prefs.DevMode )
86 | return true;
87 |
88 | // try get from cache
89 | bool result;
90 | if ( _buildingPresentCache.TryGetValue( research, out result ) )
91 | return result;
92 |
93 | // do the work manually
94 | if ( research.requiredResearchBuilding == null )
95 | result = true;
96 | else
97 | result = Find.Maps.SelectMany( map => map.listerBuildings.allBuildingsColonist )
98 | .OfType()
99 | .Any( b => research.CanBeResearchedAt( b, true ) );
100 |
101 | if ( result )
102 | result = research.Ancestors().All( BuildingPresent );
103 |
104 | // update cache
105 | _buildingPresentCache.Add( research, result );
106 | return result;
107 | }
108 |
109 | public static bool TechprintAvailable( ResearchProjectDef research )
110 | {
111 | return research.TechprintRequirementMet;
112 | }
113 |
114 | public static void ClearCaches()
115 | {
116 | _buildingPresentCache.Clear();
117 | _missingFacilitiesCache.Clear();
118 | }
119 |
120 | public static implicit operator ResearchNode( ResearchProjectDef def )
121 | {
122 | return def.ResearchNode();
123 | }
124 |
125 | public int Matches( string query )
126 | {
127 | var culture = CultureInfo.CurrentUICulture;
128 | query = query.ToLower( culture );
129 |
130 | if ( Research.LabelCap.RawText.ToLower( culture ).Contains( query ) )
131 | return 1;
132 | if ( Research.GetUnlockDefsAndDescs()
133 | .Any( unlock => unlock.First.LabelCap.RawText.ToLower( culture ).Contains( query ) ) )
134 | return 2;
135 | if ( Research.description.ToLower( culture ).Contains( query ) )
136 | return 3;
137 | return 0;
138 | }
139 |
140 | public static List MissingFacilities( ResearchProjectDef research )
141 | {
142 | // try get from cache
143 | List missing;
144 | if ( _missingFacilitiesCache.TryGetValue( research, out missing ) )
145 | return missing;
146 |
147 | // get list of all researches required before this
148 | var thisAndPrerequisites = research.Ancestors().Where( rpd => !rpd.IsFinished ).ToList();
149 | thisAndPrerequisites.Add( research );
150 |
151 | // get list of all available research benches
152 | var availableBenches = Find.Maps.SelectMany( map => map.listerBuildings.allBuildingsColonist )
153 | .OfType();
154 | var availableBenchDefs = availableBenches.Select( b => b.def ).Distinct();
155 | missing = new List();
156 |
157 | // check each for prerequisites
158 | // TODO: We should really build this list recursively so we can re-use results for prerequisites.
159 | foreach ( var rpd in thisAndPrerequisites )
160 | {
161 | if ( rpd.requiredResearchBuilding == null )
162 | continue;
163 |
164 | if ( !availableBenchDefs.Contains( rpd.requiredResearchBuilding ) )
165 | missing.Add( rpd.requiredResearchBuilding );
166 |
167 | if ( rpd.requiredResearchFacilities.NullOrEmpty() )
168 | continue;
169 |
170 | foreach ( var facility in rpd.requiredResearchFacilities )
171 | if ( !availableBenches.Any( b => b.HasFacility( facility ) ) )
172 | missing.Add( facility );
173 | }
174 |
175 | // add to cache
176 | missing = missing.Distinct().ToList();
177 | _missingFacilitiesCache.Add( research, missing );
178 | return missing;
179 | }
180 |
181 | public bool BuildingPresent()
182 | {
183 | return BuildingPresent( Research );
184 | }
185 |
186 | public bool TechprintAvailable()
187 | {
188 | return TechprintAvailable( Research );
189 | }
190 |
191 | ///
192 | /// Draw the node, including interactions.
193 | ///
194 | public override void Draw( Rect visibleRect, bool forceDetailedMode = false )
195 | {
196 | if ( !IsVisible( visibleRect ) )
197 | {
198 | Highlighted = false;
199 | return;
200 | }
201 |
202 | var detailedMode = forceDetailedMode ||
203 | MainTabWindow_ResearchTree.Instance.ZoomLevel < DetailedModeZoomLevelCutoff;
204 | var mouseOver = Mouse.IsOver( Rect );
205 | if ( Event.current.type == EventType.Repaint )
206 | {
207 | // researches that are completed or could be started immediately, and that have the required building(s) available
208 | GUI.color = mouseOver ? GenUI.MouseoverColor : Color;
209 |
210 | if ( mouseOver || Highlighted )
211 | GUI.DrawTexture( Rect, Assets.ButtonActive );
212 | else
213 | GUI.DrawTexture( Rect, Assets.Button );
214 |
215 | // grey out center to create a progress bar effect, completely greying out research not started.
216 | if ( Available )
217 | {
218 | var progressBarRect = Rect.ContractedBy( 3f );
219 | GUI.color = Assets.ColorAvailable[Research.techLevel];
220 | progressBarRect.xMin += Research.ProgressPercent * progressBarRect.width;
221 | GUI.DrawTexture( progressBarRect, BaseContent.WhiteTex );
222 | }
223 |
224 | Highlighted = false;
225 |
226 |
227 | // draw the research label
228 | if ( !Completed && !Available )
229 | GUI.color = Color.grey;
230 | else
231 | GUI.color = Color.white;
232 |
233 | if ( detailedMode )
234 | {
235 | Text.Anchor = TextAnchor.UpperLeft;
236 | Text.WordWrap = false;
237 | Text.Font = _largeLabel ? GameFont.Tiny : GameFont.Small;
238 | Widgets.Label( LabelRect, Research.LabelCap );
239 | }
240 | else
241 | {
242 | Text.Anchor = TextAnchor.MiddleCenter;
243 | Text.WordWrap = false;
244 | Text.Font = GameFont.Medium;
245 | Widgets.Label( Rect, Research.LabelCap );
246 | }
247 |
248 | // draw research cost and icon
249 | if ( detailedMode )
250 | {
251 | Text.Anchor = TextAnchor.UpperRight;
252 | Text.Font = Research.CostApparent > 1000000 ? GameFont.Tiny : GameFont.Small;
253 | Widgets.Label( CostLabelRect, Research.CostApparent.ToStringByStyle( ToStringStyle.Integer ) );
254 | GUI.DrawTexture( CostIconRect, !Completed && !Available ? Assets.Lock : Assets.ResearchIcon,
255 | ScaleMode.ScaleToFit );
256 | }
257 |
258 | Text.WordWrap = true;
259 |
260 | // attach description and further info to a tooltip
261 | TooltipHandler.TipRegion( Rect, GetResearchTooltipString, Research.GetHashCode() );
262 | if ( !BuildingPresent() )
263 | TooltipHandler.TipRegion( Rect, "Fluffy.ResearchTree.MissingFacilities".Translate(
264 | string.Join( ", ",
265 | MissingFacilities()
266 | .Select( td => td.LabelCap ).ToArray() ) ) );
267 |
268 | else if ( !TechprintAvailable() )
269 | TooltipHandler.TipRegion( Rect, "Fluffy.ResearchTree.MissingTechprints".Translate(
270 | Research.TechprintsApplied, Research.techprintCount ) );
271 |
272 | // draw unlock icons
273 | if ( detailedMode )
274 | {
275 | var unlocks = Research.GetUnlockDefsAndDescs();
276 | for ( var i = 0; i < unlocks.Count; i++ )
277 | {
278 | var iconRect = new Rect(
279 | IconsRect.xMax - ( i + 1 ) * ( IconSize.x + 4f ),
280 | IconsRect.yMin + ( IconsRect.height - IconSize.y ) / 2f,
281 | IconSize.x,
282 | IconSize.y );
283 |
284 | if ( iconRect.xMin - IconSize.x < IconsRect.xMin &&
285 | i + 1 < unlocks.Count )
286 | {
287 | // stop the loop if we're about to overflow and have 2 or more unlocks yet to print.
288 | iconRect.x = IconsRect.x + 4f;
289 | GUI.DrawTexture( iconRect, Assets.MoreIcon, ScaleMode.ScaleToFit );
290 | var tip = string.Join( "\n",
291 | unlocks.GetRange( i, unlocks.Count - i ).Select( p => p.Second )
292 | .ToArray() );
293 | TooltipHandler.TipRegion( iconRect, tip );
294 | // new TipSignal( tip, Settings.TipID, TooltipPriority.Pawn ) );
295 | break;
296 | }
297 |
298 | // draw icon
299 | unlocks[i].First.DrawColouredIcon( iconRect );
300 |
301 | // tooltip
302 | TooltipHandler.TipRegion( iconRect, unlocks[i].Second );
303 | }
304 | }
305 |
306 | if ( mouseOver )
307 | {
308 | // highlight prerequisites if research available
309 | if ( Available )
310 | {
311 | Highlighted = true;
312 | foreach ( var prerequisite in GetMissingRequiredRecursive() )
313 | prerequisite.Highlighted = true;
314 | }
315 | // highlight children if completed
316 | else if ( Completed )
317 | {
318 | foreach ( var child in Children )
319 | child.Highlighted = true;
320 | }
321 | }
322 | }
323 |
324 | // if clicked and not yet finished, queue up this research and all prereqs.
325 | if ( Widgets.ButtonInvisible( Rect ) && Available )
326 | {
327 | // LMB is queue operations, RMB is info
328 | if ( Event.current.button == 0 && !Research.IsFinished )
329 | {
330 | if ( !Queue.IsQueued( this ) )
331 | {
332 | // if shift is held, add to queue, otherwise replace queue
333 | var queue = GetMissingRequiredRecursive()
334 | .Concat( new List( new[] {this} ) )
335 | .Distinct();
336 | Queue.EnqueueRange( queue, Event.current.shift );
337 | }
338 | else
339 | {
340 | Queue.Dequeue( this );
341 | }
342 | }
343 |
344 | if ( DebugSettings.godMode && Prefs.DevMode && Event.current.button == 1 && !Research.IsFinished )
345 | {
346 | Find.ResearchManager.FinishProject( Research );
347 | Queue.Notify_InstantFinished();
348 | }
349 | }
350 | }
351 |
352 | ///
353 | /// Get recursive list of all incomplete prerequisites
354 | ///
355 | /// List prerequisites
356 | public List GetMissingRequiredRecursive()
357 | {
358 | var parents = Research.prerequisites?.Where( rpd => !rpd.IsFinished ).Select( rpd => rpd.ResearchNode() );
359 | if ( parents == null )
360 | return new List();
361 | var allParents = new List( parents );
362 | foreach ( var parent in parents )
363 | allParents.AddRange( parent.GetMissingRequiredRecursive() );
364 |
365 | return allParents.Distinct().ToList();
366 | }
367 |
368 | public override bool Completed => Research.IsFinished;
369 | public override bool Available => !Research.IsFinished && ( DebugSettings.godMode || (BuildingPresent() && TechprintAvailable()));
370 |
371 | public List MissingFacilities()
372 | {
373 | return MissingFacilities( Research );
374 | }
375 |
376 | ///
377 | /// Creates text version of research description and additional unlocks/prereqs/etc sections.
378 | ///
379 | /// string description
380 | private string GetResearchTooltipString()
381 | {
382 | // start with the descripton
383 | var text = new StringBuilder();
384 | text.AppendLine( Research.description );
385 | text.AppendLine();
386 |
387 | if ( Queue.IsQueued( this ) )
388 | {
389 | text.AppendLine( "Fluffy.ResearchTree.LClickRemoveFromQueue".Translate() );
390 | }
391 | else
392 | {
393 | text.AppendLine( "Fluffy.ResearchTree.LClickReplaceQueue".Translate() );
394 | text.AppendLine( "Fluffy.ResearchTree.SLClickAddToQueue".Translate() );
395 | }
396 |
397 | if ( DebugSettings.godMode ) text.AppendLine( "Fluffy.ResearchTree.RClickInstaFinish".Translate() );
398 |
399 |
400 | return text.ToString();
401 | }
402 |
403 | public void DrawAt( Vector2 pos, Rect visibleRect, bool forceDetailedMode = false )
404 | {
405 | SetRects( pos );
406 | Draw( visibleRect, forceDetailedMode );
407 | SetRects();
408 | }
409 | }
410 | }
--------------------------------------------------------------------------------
/Source/Graph/Tree.cs:
--------------------------------------------------------------------------------
1 | // Tree.cs
2 | // Copyright Karel Kroeze, 2020-2020
3 |
4 | //using Multiplayer.API;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Diagnostics;
8 | using System.Linq;
9 | using System.Text;
10 | using RimWorld;
11 | using UnityEngine;
12 | using Verse;
13 | using static FluffyResearchTree.Constants;
14 |
15 | namespace FluffyResearchTree
16 | {
17 | public static class Tree
18 | {
19 | public static bool Initialized;
20 | public static IntVec2 Size = IntVec2.Zero;
21 | private static List _nodes;
22 | private static List> _edges;
23 | private static List _relevantTechLevels;
24 | private static Dictionary _techLevelBounds;
25 |
26 | private static bool _initializing;
27 |
28 | public static bool OrderDirty;
29 |
30 | public static Dictionary TechLevelBounds
31 | {
32 | get
33 | {
34 | if ( _techLevelBounds == null )
35 | throw new Exception( "TechLevelBounds called before they are set." );
36 | return _techLevelBounds;
37 | }
38 | }
39 |
40 | public static List RelevantTechLevels
41 | {
42 | get
43 | {
44 | if ( _relevantTechLevels == null )
45 | _relevantTechLevels = Enum.GetValues( typeof( TechLevel ) )
46 | .Cast()
47 | // filter down to relevant tech levels only.
48 | .Where(
49 | tl => DefDatabase.AllDefsListForReading.Any(
50 | rp => rp.techLevel ==
51 | tl ) )
52 | .ToList();
53 | return _relevantTechLevels;
54 | }
55 | }
56 |
57 | public static List Nodes
58 | {
59 | get
60 | {
61 | if ( _nodes == null )
62 | PopulateNodes();
63 |
64 | return _nodes;
65 | }
66 | }
67 |
68 | public static List> Edges
69 | {
70 | get
71 | {
72 | if ( _edges == null )
73 | throw new Exception( "Trying to access edges before they are initialized." );
74 |
75 | return _edges;
76 | }
77 | }
78 |
79 | // [SyncMethod]
80 | public static void Initialize()
81 | {
82 | if ( Initialized ) return;
83 |
84 | // make sure we only have one initializer running
85 | if ( _initializing )
86 | return;
87 | _initializing = true;
88 |
89 | // setup
90 | LongEventHandler.QueueLongEvent( CheckPrerequisites, "Fluffy.ResearchTree.PreparingTree.Setup", false,
91 | null );
92 | LongEventHandler.QueueLongEvent( CreateEdges, "Fluffy.ResearchTree.PreparingTree.Setup", false, null );
93 | LongEventHandler.QueueLongEvent( HorizontalPositions, "Fluffy.ResearchTree.PreparingTree.Setup", false,
94 | null );
95 | LongEventHandler.QueueLongEvent( NormalizeEdges, "Fluffy.ResearchTree.PreparingTree.Setup", false, null );
96 | #if DEBUG
97 | LongEventHandler.QueueLongEvent( DebugStatus, "Fluffy.ResearchTree.PreparingTree.Setup", false, null );
98 | #endif
99 |
100 | // crossing reduction
101 | LongEventHandler.QueueLongEvent( Collapse, "Fluffy.ResearchTree.PreparingTree.CrossingReduction", false,
102 | null );
103 | LongEventHandler.QueueLongEvent( MinimizeCrossings, "Fluffy.ResearchTree.PreparingTree.CrossingReduction",
104 | false, null );
105 | #if DEBUG
106 | LongEventHandler.QueueLongEvent( DebugStatus, "Fluffy.ResearchTree.PreparingTree.CrossingReduction", false,
107 | null );
108 | #endif
109 |
110 | // layout
111 | LongEventHandler.QueueLongEvent( MinimizeEdgeLength, "Fluffy.ResearchTree.PreparingTree.Layout", false,
112 | null );
113 | LongEventHandler.QueueLongEvent( RemoveEmptyRows, "Fluffy.ResearchTree.PreparingTree.Layout", false, null );
114 | #if DEBUG
115 | LongEventHandler.QueueLongEvent( DebugStatus, "Fluffy.ResearchTree.PreparingTree.Layout", false, null );
116 | #endif
117 |
118 | // done!
119 | LongEventHandler.QueueLongEvent( () => { Initialized = true; }, "Fluffy.ResearchTree.PreparingTree.Layout",
120 | false, null );
121 |
122 | // tell research tab we're ready
123 | LongEventHandler.QueueLongEvent( MainTabWindow_ResearchTree.Instance.Notify_TreeInitialized,
124 | "Fluffy.ResearchTree.RestoreQueue", false, null );
125 | }
126 |
127 | private static void RemoveEmptyRows()
128 | {
129 | Log.Debug( "Removing empty rows" );
130 | Profiler.Start();
131 | var y = 1;
132 | while ( y <= Size.z )
133 | {
134 | var row = Row( y );
135 | if ( row.NullOrEmpty() )
136 | foreach ( var node in Nodes.Where( n => n.Y > y ) )
137 | node.Y--;
138 | else
139 | y++;
140 | }
141 |
142 | Profiler.End();
143 | }
144 |
145 | private static void MinimizeEdgeLength()
146 | {
147 | Log.Debug( "Minimize edge length." );
148 | Profiler.Start();
149 |
150 | // move and/or swap nodes to reduce the total edge length
151 | // perform sweeps of adjacent node reorderings
152 | var progress = false;
153 | int iteration = 0, burnout = 2, max_iterations = 50;
154 | while ( ( !progress || burnout > 0 ) && iteration < max_iterations )
155 | {
156 | progress = EdgeLengthSweep_Local( iteration++ );
157 | if ( !progress )
158 | burnout--;
159 | }
160 |
161 | // sweep until we had no progress 2 times, then keep sweeping until we had progress
162 | iteration = 0;
163 | burnout = 2;
164 | while ( burnout > 0 && iteration < max_iterations )
165 | {
166 | progress = EdgeLengthSweep_Global( iteration++ );
167 | if ( !progress )
168 | burnout--;
169 | }
170 |
171 | Profiler.End();
172 | }
173 |
174 | private static bool EdgeLengthSweep_Global( int iteration )
175 | {
176 | Profiler.Start( "iteration" + iteration );
177 | // calculate edge length before sweep
178 | var before = EdgeLength();
179 |
180 | // do left/right sweep, align with left/right nodes for 4 different iterations.
181 | //if (iteration % 2 == 0)
182 | for ( var l = 2; l <= Size.x; l++ )
183 | EdgeLengthSweep_Global_Layer( l, true );
184 | //else
185 | // for (var l = 1; l < Size.x; l++)
186 | // EdgeLengthSweep_Global_Layer(l, false);
187 |
188 | // calculate edge length after sweep
189 | var after = EdgeLength();
190 |
191 | // return progress
192 | Log.Debug( $"EdgeLengthSweep_Global, iteration {iteration}: {before} -> {after}" );
193 | Profiler.End();
194 | return after < before;
195 | }
196 |
197 |
198 | private static bool EdgeLengthSweep_Local( int iteration )
199 | {
200 | Profiler.Start( "iteration" + iteration );
201 | // calculate edge length before sweep
202 | var before = EdgeLength();
203 |
204 | // do left/right sweep, align with left/right nodes for 4 different iterations.
205 | if ( iteration % 2 == 0 )
206 | for ( var l = 2; l <= Size.x; l++ )
207 | EdgeLengthSweep_Local_Layer( l, true );
208 | else
209 | for ( var l = Size.x - 1; l >= 0; l-- )
210 | EdgeLengthSweep_Local_Layer( l, false );
211 |
212 | // calculate edge length after sweep
213 | var after = EdgeLength();
214 |
215 | // return progress
216 | Log.Debug( $"EdgeLengthSweep_Local, iteration {iteration}: {before} -> {after}" );
217 | Profiler.End();
218 | return after < before;
219 | }
220 |
221 | private static void EdgeLengthSweep_Global_Layer( int l, bool @in )
222 | {
223 | // The objective here is to;
224 | // (1) move and/or swap nodes to reduce total edge length
225 | // (2) not increase the number of crossings
226 |
227 | var length = EdgeLength( l, @in );
228 | var crossings = Crossings( l );
229 | if ( Math.Abs( length ) < Epsilon )
230 | return;
231 |
232 | var layer = Layer( l, true );
233 | foreach ( var node in layer )
234 | {
235 | // we only need to loop over positions that might be better for this node.
236 | // min = minimum of current position, minimum of any connected nodes current position
237 | var neighbours = node.Nodes;
238 | if ( !neighbours.Any() )
239 | continue;
240 |
241 | var min = Mathf.Min( node.Y, neighbours.Min( n => n.Y ) );
242 | var max = Mathf.Max( node.Y, neighbours.Max( n => n.Y ) );
243 | if ( min == max && min == node.Y )
244 | continue;
245 |
246 | for ( var y = min; y <= max; y++ )
247 | {
248 | if ( y == node.Y )
249 | continue;
250 |
251 | // is this spot occupied?
252 | var otherNode = NodeAt( l, y );
253 |
254 | // occupied, try swapping
255 | if ( otherNode != null )
256 | {
257 | Swap( node, otherNode );
258 | var candidateCrossings = Crossings( l );
259 | if ( candidateCrossings > crossings )
260 | {
261 | // abort
262 | Swap( otherNode, node );
263 | }
264 | else
265 | {
266 | var candidateLength = EdgeLength( l, @in );
267 | if ( length - candidateLength < Epsilon )
268 | {
269 | // abort
270 | Swap( otherNode, node );
271 | }
272 | else
273 | {
274 | Log.Trace( "\tSwapping {0} and {1}: {2} -> {3}", node, otherNode, length,
275 | candidateLength );
276 | length = candidateLength;
277 | }
278 | }
279 | }
280 |
281 | // not occupied, try moving
282 | else
283 | {
284 | var oldY = node.Y;
285 | node.Y = y;
286 | var candidateCrossings = Crossings( l );
287 | if ( candidateCrossings > crossings )
288 | {
289 | // abort
290 | node.Y = oldY;
291 | }
292 | else
293 | {
294 | var candidateLength = EdgeLength( l, @in );
295 | if ( length - candidateLength < Epsilon )
296 | {
297 | // abort
298 | node.Y = oldY;
299 | }
300 | else
301 | {
302 | Log.Trace( "\tMoving {0} -> {1}: {2} -> {3}", node, new Vector2( node.X, oldY ), length,
303 | candidateLength );
304 | length = candidateLength;
305 | }
306 | }
307 | }
308 | }
309 | }
310 | }
311 |
312 |
313 | private static void EdgeLengthSweep_Local_Layer( int l, bool @in )
314 | {
315 | // The objective here is to;
316 | // (1) move and/or swap nodes to reduce local edge length
317 | // (2) not increase the number of crossings
318 | var x = @in ? l - 1 : l + 1;
319 | var crossings = Crossings( x );
320 |
321 | var layer = Layer( l, true );
322 | foreach ( var node in layer )
323 | {
324 | foreach ( var edge in @in ? node.InEdges : node.OutEdges )
325 | {
326 | // current length
327 | var length = edge.Length;
328 | var neighbour = @in ? edge.In : edge.Out;
329 | if ( neighbour.X != x )
330 | Log.Warning( "{0} is not at layer {1}", neighbour, x );
331 |
332 | // we only need to loop over positions that might be better for this node.
333 | // min = minimum of current position, node position
334 | var min = Mathf.Min( node.Y, neighbour.Y );
335 | var max = Mathf.Max( node.Y, neighbour.Y );
336 |
337 | // already at only possible position
338 | if ( min == max && min == node.Y )
339 | continue;
340 |
341 | for ( var y = min; y <= max; y++ )
342 | {
343 | if ( y == neighbour.Y )
344 | continue;
345 |
346 | // is this spot occupied?
347 | var otherNode = NodeAt( x, y );
348 |
349 | // occupied, try swapping
350 | if ( otherNode != null )
351 | {
352 | Swap( neighbour, otherNode );
353 | var candidateCrossings = Crossings( x );
354 | if ( candidateCrossings > crossings )
355 | {
356 | // abort
357 | Swap( otherNode, neighbour );
358 | }
359 | else
360 | {
361 | var candidateLength = edge.Length;
362 | if ( length - candidateLength < Epsilon )
363 | {
364 | // abort
365 | Swap( otherNode, neighbour );
366 | }
367 | else
368 | {
369 | Log.Trace( "\tSwapping {0} and {1}: {2} -> {3}", neighbour, otherNode, length,
370 | candidateLength );
371 | length = candidateLength;
372 | }
373 | }
374 | }
375 |
376 | // not occupied, try moving
377 | else
378 | {
379 | var oldY = neighbour.Y;
380 | neighbour.Y = y;
381 | var candidateCrossings = Crossings( x );
382 | if ( candidateCrossings > crossings )
383 | {
384 | // abort
385 | neighbour.Y = oldY;
386 | }
387 | else
388 | {
389 | var candidateLength = edge.Length;
390 | if ( length - candidateLength < Epsilon )
391 | {
392 | // abort
393 | neighbour.Y = oldY;
394 | }
395 | else
396 | {
397 | Log.Trace( "\tMoving {0} -> {1}: {2} -> {3}", neighbour,
398 | new Vector2( neighbour.X, oldY ), length, candidateLength );
399 | length = candidateLength;
400 | }
401 | }
402 | }
403 | }
404 | }
405 | }
406 | }
407 |
408 | public static void HorizontalPositions()
409 | {
410 | // get list of techlevels
411 | var techlevels = RelevantTechLevels;
412 | bool anyChange;
413 | var iteration = 1;
414 | var maxIterations = 50;
415 |
416 | Log.Debug( "Assigning horizontal positions." );
417 | Profiler.Start();
418 |
419 | // assign horizontal positions based on tech levels and prerequisites
420 | do
421 | {
422 | Profiler.Start( "iteration " + iteration );
423 | var min = 1;
424 | anyChange = false;
425 |
426 | foreach ( var techlevel in techlevels )
427 | {
428 | // enforce minimum x position based on techlevels
429 | var nodes = Nodes.OfType().Where( n => n.Research.techLevel == techlevel );
430 | if ( !nodes.Any() )
431 | continue;
432 |
433 | foreach ( var node in nodes )
434 | anyChange = node.SetDepth( min ) || anyChange;
435 |
436 | min = nodes.Max( n => n.X ) + 1;
437 |
438 | Log.Trace( "\t{0}, change: {1}", techlevel, anyChange );
439 | }
440 |
441 | Profiler.End();
442 | } while ( anyChange && iteration++ < maxIterations );
443 |
444 |
445 | // store tech level boundaries
446 | _techLevelBounds = new Dictionary();
447 | foreach ( var techlevel in techlevels )
448 | {
449 | var nodes = Nodes.OfType().Where( n => n.Research.techLevel == techlevel );
450 | _techLevelBounds[techlevel] = new IntRange( nodes.Min( n => n.X ) - 1, nodes.Max( n => n.X ) );
451 | }
452 |
453 | Profiler.End();
454 | }
455 |
456 | private static void NormalizeEdges()
457 | {
458 | Log.Debug( "Normalizing edges." );
459 | Profiler.Start();
460 | foreach ( var edge in new List>( Edges.Where( e => e.Span > 1 ) ) )
461 | {
462 | Log.Trace( "\tCreating dummy chain for {0}", edge );
463 |
464 | // remove and decouple long edge
465 | Edges.Remove( edge );
466 | edge.In.OutEdges.Remove( edge );
467 | edge.Out.InEdges.Remove( edge );
468 | var cur = edge.In;
469 | var yOffset = ( edge.Out.Yf - edge.In.Yf ) / edge.Span;
470 |
471 | // create and hook up dummy chain
472 | for ( var x = edge.In.X + 1; x < edge.Out.X; x++ )
473 | {
474 | var dummy = new DummyNode();
475 | dummy.X = x;
476 | dummy.Yf = edge.In.Yf + yOffset * ( x - edge.In.X );
477 | var dummyEdge = new Edge( cur, dummy );
478 | cur.OutEdges.Add( dummyEdge );
479 | dummy.InEdges.Add( dummyEdge );
480 | _nodes.Add( dummy );
481 | Edges.Add( dummyEdge );
482 | cur = dummy;
483 | Log.Trace( "\t\tCreated dummy {0}", dummy );
484 | }
485 |
486 | // hook up final dummy to out node
487 | var finalEdge = new Edge( cur, edge.Out );
488 | cur.OutEdges.Add( finalEdge );
489 | edge.Out.InEdges.Add( finalEdge );
490 | Edges.Add( finalEdge );
491 | }
492 |
493 | Profiler.End();
494 | }
495 |
496 | private static void CreateEdges()
497 | {
498 | Log.Debug( "Creating edges." );
499 | Profiler.Start();
500 | // create links between nodes
501 | if ( _edges.NullOrEmpty() ) _edges = new List>();
502 |
503 | foreach ( var node in Nodes.OfType() )
504 | {
505 | if ( node.Research.prerequisites.NullOrEmpty() )
506 | continue;
507 | foreach ( var prerequisite in node.Research.prerequisites )
508 | {
509 | ResearchNode prerequisiteNode = prerequisite;
510 | if ( prerequisiteNode == null )
511 | continue;
512 | var edge = new Edge( prerequisiteNode, node );
513 | Edges.Add( edge );
514 | node.InEdges.Add( edge );
515 | prerequisiteNode.OutEdges.Add( edge );
516 | Log.Trace( "\tCreated edge {0}", edge );
517 | }
518 | }
519 |
520 | Profiler.End();
521 | }
522 |
523 | private static void CheckPrerequisites()
524 | {
525 | // check prerequisites
526 | Log.Debug( "Checking prerequisites." );
527 | Profiler.Start();
528 |
529 | var nodes = new Queue( Nodes.OfType() );
530 | // remove redundant prerequisites
531 | while ( nodes.Count > 0 )
532 | {
533 | var node = nodes.Dequeue();
534 | if ( node.Research.prerequisites.NullOrEmpty() )
535 | continue;
536 |
537 | var ancestors = node.Research.prerequisites?.SelectMany( r => r.Ancestors() ).ToList();
538 | var redundant = ancestors.Intersect( node.Research.prerequisites );
539 | if ( redundant.Any() )
540 | {
541 | Log.Warning( "\tredundant prerequisites for {0}: {1}", node.Research.LabelCap,
542 | string.Join( ", ", redundant.Select( r => r.LabelCap ).ToArray() ) );
543 | foreach ( var redundantPrerequisite in redundant )
544 | node.Research.prerequisites.Remove( redundantPrerequisite );
545 | }
546 | }
547 |
548 | // fix bad techlevels
549 | nodes = new Queue( Nodes.OfType() );
550 | while ( nodes.Count > 0 )
551 | {
552 | var node = nodes.Dequeue();
553 | if ( !node.Research.prerequisites.NullOrEmpty() )
554 | // warn and fix badly configured techlevels
555 | if ( node.Research.prerequisites.Any( r => r.techLevel > node.Research.techLevel ) )
556 | {
557 | Log.Warning( "\t{0} has a lower techlevel than (one of) it's prerequisites",
558 | node.Research.defName );
559 | node.Research.techLevel = node.Research.prerequisites.Max( r => r.techLevel );
560 |
561 | // re-enqeue all descendants
562 | foreach ( var descendant in node.Descendants.OfType() )
563 | nodes.Enqueue( descendant );
564 | }
565 | }
566 |
567 | Profiler.End();
568 | }
569 |
570 | private static void PopulateNodes()
571 | {
572 | Log.Debug( "Populating nodes." );
573 | Profiler.Start();
574 |
575 | var projects = DefDatabase.AllDefsListForReading;
576 |
577 | // find hidden nodes (nodes that have themselves as a prerequisite)
578 | var hidden = projects.Where( p => p.prerequisites?.Contains( p ) ?? false );
579 |
580 | // find locked nodes (nodes that have a hidden node as a prerequisite)
581 | var locked = projects.Where( p => p.Ancestors().Intersect( hidden ).Any() );
582 |
583 | // populate all nodes
584 | _nodes = new List( DefDatabase.AllDefsListForReading
585 | .Except( hidden )
586 | .Except( locked )
587 | .Select( def => new ResearchNode( def ) as Node ) );
588 | Log.Debug( "\t{0} nodes", _nodes.Count );
589 | Profiler.End();
590 | }
591 |
592 | private static void Collapse()
593 | {
594 | Log.Debug( "Collapsing nodes." );
595 | Profiler.Start();
596 | var pre = Size;
597 | for ( var l = 1; l <= Size.x; l++ )
598 | {
599 | var nodes = Layer( l, true );
600 | var Y = 1;
601 | foreach ( var node in nodes )
602 | node.Y = Y++;
603 | }
604 |
605 | Log.Debug( "{0} -> {1}", pre, Size );
606 | Profiler.End();
607 | }
608 |
609 | [Conditional( "DEBUG" )]
610 | internal static void DebugDraw()
611 | {
612 | foreach ( var v in Nodes )
613 | {
614 | foreach ( var w in v.OutNodes ) Widgets.DrawLine( v.Right, w.Left, Color.white, 1 );
615 | }
616 | }
617 |
618 |
619 | public static void Draw( Rect visibleRect )
620 | {
621 | Profiler.Start( "Tree.Draw" );
622 | Profiler.Start( "techlevels" );
623 | foreach ( var techlevel in RelevantTechLevels )
624 | DrawTechLevel( techlevel, visibleRect );
625 | Profiler.End();
626 |
627 | Profiler.Start( "edges" );
628 | foreach ( var edge in Edges.OrderBy( e => e.DrawOrder ) )
629 | edge.Draw( visibleRect );
630 | Profiler.End();
631 |
632 | Profiler.Start( "nodes" );
633 | foreach ( var node in Nodes )
634 | node.Draw( visibleRect );
635 | Profiler.End();
636 | }
637 |
638 | public static void DrawTechLevel( TechLevel techlevel, Rect visibleRect )
639 | {
640 | // determine positions
641 | var xMin = ( NodeSize.x + NodeMargins.x ) * TechLevelBounds[techlevel].min - NodeMargins.x / 2f;
642 | var xMax = ( NodeSize.x + NodeMargins.x ) * TechLevelBounds[techlevel].max - NodeMargins.x / 2f;
643 |
644 | GUI.color = Assets.TechLevelColor;
645 | Text.Anchor = TextAnchor.MiddleCenter;
646 |
647 | // lower bound
648 | if ( TechLevelBounds[techlevel].min > 0 && xMin > visibleRect.xMin && xMin < visibleRect.xMax )
649 | {
650 | // line
651 | Widgets.DrawLine( new Vector2( xMin, visibleRect.yMin ), new Vector2( xMin, visibleRect.yMax ),
652 | Assets.TechLevelColor, 1f );
653 |
654 | // label
655 | var labelRect = new Rect(
656 | xMin + TechLevelLabelSize.y / 2f - TechLevelLabelSize.x / 2f,
657 | visibleRect.center.y - TechLevelLabelSize.y / 2f,
658 | TechLevelLabelSize.x,
659 | TechLevelLabelSize.y );
660 |
661 | VerticalLabel( labelRect, techlevel.ToStringHuman() );
662 | }
663 |
664 | // upper bound
665 | if ( TechLevelBounds[techlevel].max < Size.x && xMax > visibleRect.xMin && xMax < visibleRect.xMax )
666 | {
667 | // label
668 | var labelRect = new Rect(
669 | xMax - TechLevelLabelSize.y / 2f - TechLevelLabelSize.x / 2f,
670 | visibleRect.center.y - TechLevelLabelSize.y / 2f,
671 | TechLevelLabelSize.x,
672 | TechLevelLabelSize.y );
673 |
674 | VerticalLabel( labelRect, techlevel.ToStringHuman() );
675 | }
676 |
677 | GUI.color = Color.white;
678 | Text.Anchor = TextAnchor.UpperLeft;
679 | }
680 |
681 | private static void VerticalLabel( Rect rect, string text )
682 | {
683 | // store the scaling matrix
684 | var matrix = GUI.matrix;
685 |
686 | // rotate and then apply the scaling
687 | GUI.matrix = Matrix4x4.identity;
688 | GUIUtility.RotateAroundPivot( -90f, rect.center );
689 | GUI.matrix = matrix * GUI.matrix;
690 |
691 | Widgets.Label( rect, text );
692 |
693 | // restore the original scaling matrix
694 | GUI.matrix = matrix;
695 | }
696 |
697 | private static Node NodeAt( int X, int Y )
698 | {
699 | return Nodes.FirstOrDefault( n => n.X == X && n.Y == Y );
700 | }
701 |
702 | public static void MinimizeCrossings()
703 | {
704 | // initialize each layer by putting nodes with the most (recursive!) children on bottom
705 | Log.Debug( "Minimize crossings." );
706 | Profiler.Start();
707 |
708 | for ( var X = 1; X <= Size.x; X++ )
709 | {
710 | var nodes = Layer( X ).OrderBy( n => n.Descendants.Count ).ToList();
711 | for ( var i = 0; i < nodes.Count; i++ )
712 | nodes[i].Y = i + 1;
713 | }
714 |
715 | // up-down sweeps of mean reordering
716 | var progress = false;
717 | int iteration = 0, burnout = 2, max_iterations = 50;
718 | while ( ( !progress || burnout > 0 ) && iteration < max_iterations )
719 | {
720 | progress = BarymetricSweep( iteration++ );
721 | if ( !progress )
722 | burnout--;
723 | }
724 |
725 | // greedy sweep for local optima
726 | iteration = 0;
727 | burnout = 2;
728 | while ( burnout > 0 && iteration < max_iterations )
729 | {
730 | progress = GreedySweep( iteration++ );
731 | if ( !progress )
732 | burnout--;
733 | }
734 |
735 | Profiler.End();
736 | }
737 |
738 | private static bool GreedySweep( int iteration )
739 | {
740 | Profiler.Start( "iteration " + iteration );
741 |
742 | // count number of crossings before sweep
743 | var before = Crossings();
744 |
745 | // do up/down sweep on aternating iterations
746 | if ( iteration % 2 == 0 )
747 | for ( var l = 1; l <= Size.x; l++ )
748 | GreedySweep_Layer( l );
749 | else
750 | for ( var l = Size.x; l >= 1; l-- )
751 | GreedySweep_Layer( l );
752 |
753 | // count number of crossings after sweep
754 | var after = Crossings();
755 |
756 | Log.Debug( $"GreedySweep: {before} -> {after}" );
757 | Profiler.End();
758 |
759 | // return progress
760 | return after < before;
761 | }
762 |
763 | private static void GreedySweep_Layer( int l )
764 | {
765 | // The objective here is twofold;
766 | // 1: Swap nodes to reduce the number of crossings
767 | // 2: Swap nodes so that inner edges (edges between dummies)
768 | // avoid crossings at all costs.
769 | //
770 | // If I'm reasoning this out right, both objectives should be served by
771 | // minimizing the amount of crossings between each pair of nodes.
772 | var crossings = Crossings( l );
773 | if ( crossings == 0 )
774 | return;
775 |
776 | var layer = Layer( l, true );
777 | for ( var i = 0; i < layer.Count - 1; i++ )
778 | {
779 | for ( var j = i + 1; j < layer.Count; j++ )
780 | {
781 | // swap, then count crossings again. If lower, leave it. If higher, revert.
782 | Swap( layer[i], layer[j] );
783 | var candidateCrossings = Crossings( l );
784 | if ( candidateCrossings < crossings )
785 | // update current crossings
786 | crossings = candidateCrossings;
787 | else
788 | // revert change
789 | Swap( layer[j], layer[i] );
790 | }
791 | }
792 | }
793 |
794 | private static void Swap( Node A, Node B )
795 | {
796 | if ( A.X != B.X )
797 | throw new Exception( "Can't swap nodes on different layers" );
798 |
799 | // swap Y positions of adjacent nodes
800 | var tmp = A.Y;
801 | A.Y = B.Y;
802 | B.Y = tmp;
803 | }
804 |
805 | private static bool BarymetricSweep( int iteration )
806 | {
807 | Profiler.Start( "iteration " + iteration );
808 |
809 | // count number of crossings before sweep
810 | var before = Crossings();
811 |
812 | // do up/down sweep on alternating iterations
813 | if ( iteration % 2 == 0 )
814 | for ( var i = 2; i <= Size.x; i++ )
815 | BarymetricSweep_Layer( i, true );
816 | else
817 | for ( var i = Size.x - 1; i > 0; i-- )
818 | BarymetricSweep_Layer( i, false );
819 |
820 | // count number of crossings after sweep
821 | var after = Crossings();
822 |
823 | // did we make progress? please?
824 | Log.Debug(
825 | $"BarymetricSweep {iteration} ({( iteration % 2 == 0 ? "left" : "right" )}): {before} -> {after}" );
826 | Profiler.End();
827 | return after < before;
828 | }
829 |
830 | private static void BarymetricSweep_Layer( int layer, bool left )
831 | {
832 | var means = Layer( layer )
833 | .ToDictionary( n => n, n => GetBarycentre( n, left ? n.InNodes : n.OutNodes ) )
834 | .OrderBy( n => n.Value );
835 |
836 | // create groups of nodes at similar means
837 | var cur = float.MinValue;
838 | var groups = new Dictionary>();
839 | foreach ( var mean in means )
840 | {
841 | if ( Math.Abs( mean.Value - cur ) > Epsilon )
842 | {
843 | cur = mean.Value;
844 | groups[cur] = new List();
845 | }
846 |
847 | groups[cur].Add( mean.Key );
848 | }
849 |
850 | // position nodes as close to their desired mean as possible
851 | var Y = 1;
852 | foreach ( var group in groups )
853 | {
854 | var mean = group.Key;
855 | var N = group.Value.Count;
856 | Y = (int) Mathf.Max( Y, mean - ( N - 1 ) / 2 );
857 |
858 | foreach ( var node in group.Value )
859 | node.Y = Y++;
860 | }
861 | }
862 |
863 | private static float GetBarycentre( Node node, List neighbours )
864 | {
865 | if ( neighbours.NullOrEmpty() )
866 | return node.Yf;
867 |
868 | return neighbours.Sum( n => n.Yf ) / neighbours.Count;
869 | }
870 |
871 | private static int Crossings()
872 | {
873 | var crossings = 0;
874 | for ( var layer = 1; layer < Size.x; layer++ ) crossings += Crossings( layer, true );
875 | return crossings;
876 | }
877 |
878 | private static float EdgeLength()
879 | {
880 | var length = 0f;
881 | for ( var layer = 1; layer < Size.x; layer++ ) length += EdgeLength( layer, true );
882 | return length;
883 | }
884 |
885 | private static int Crossings( int layer )
886 | {
887 | if ( layer == 0 )
888 | return Crossings( layer, false );
889 | if ( layer == Size.x )
890 | return Crossings( layer, true );
891 | return Crossings( layer, true ) + Crossings( layer, false );
892 | }
893 |
894 | private static float EdgeLength( int layer )
895 | {
896 | if ( layer == 0 )
897 | return EdgeLength( layer, false );
898 | if ( layer == Size.x )
899 | return EdgeLength( layer, true );
900 | return EdgeLength( layer, true ) *
901 | EdgeLength( layer, false ); // multply to favor moving nodes closer to one endpoint
902 | }
903 |
904 | private static int Crossings( int layer, bool @in )
905 | {
906 | // get in/out edges for layer
907 | var edges = Layer( layer )
908 | .SelectMany( n => @in ? n.InEdges : n.OutEdges )
909 | .OrderBy( e => e.In.Y )
910 | .ThenBy( e => e.Out.Y )
911 | .ToList();
912 |
913 | if ( edges.Count < 2 )
914 | return 0;
915 |
916 | // count number of inversions
917 | var inversions = 0;
918 | for ( var i = 0; i < edges.Count - 1; i++ )
919 | {
920 | for ( var j = i + 1; j < edges.Count; j++ )
921 | if ( edges[j].Out.Y < edges[i].Out.Y )
922 | inversions++;
923 | }
924 |
925 | return inversions;
926 | }
927 |
928 | private static float EdgeLength( int layer, bool @in )
929 | {
930 | // get in/out edges for layer
931 | var edges = Layer( layer )
932 | .SelectMany( n => @in ? n.InEdges : n.OutEdges )
933 | .OrderBy( e => e.In.Y )
934 | .ThenBy( e => e.Out.Y )
935 | .ToList();
936 |
937 | if ( edges.NullOrEmpty() )
938 | return 0f;
939 |
940 | return edges.Sum( e => e.Length ) * ( @in ? 2 : 1 );
941 | }
942 |
943 | public static List Layer( int depth, bool ordered = false )
944 | {
945 | if ( ordered && OrderDirty )
946 | {
947 | _nodes = Nodes.OrderBy( n => n.X ).ThenBy( n => n.Y ).ToList();
948 | OrderDirty = false;
949 | }
950 |
951 | return Nodes.Where( n => n.X == depth ).ToList();
952 | }
953 |
954 | public static List Row( int Y )
955 | {
956 | return Nodes.Where( n => n.Y == Y ).ToList();
957 | }
958 |
959 | public new static string ToString()
960 | {
961 | var text = new StringBuilder();
962 |
963 | for ( var l = 1; l <= Nodes.Max( n => n.X ); l++ )
964 | {
965 | text.AppendLine( $"Layer {l}:" );
966 | var layer = Layer( l, true );
967 |
968 | foreach ( var n in layer )
969 | {
970 | text.AppendLine( $"\t{n}" );
971 | text.AppendLine( "\t\tAbove: " +
972 | string.Join( ", ", n.InNodes.Select( a => a.ToString() ).ToArray() ) );
973 | text.AppendLine( "\t\tBelow: " +
974 | string.Join( ", ", n.OutNodes.Select( b => b.ToString() ).ToArray() ) );
975 | }
976 | }
977 |
978 | return text.ToString();
979 | }
980 |
981 | public static void DebugStatus()
982 | {
983 | Log.Message( "duplicated positions:\n " +
984 | string.Join(
985 | "\n",
986 | Nodes.Where( n => Nodes.Any( n2 => n != n2 && n.X == n2.X && n.Y == n2.Y ) )
987 | .Select( n => n.X + ", " + n.Y + ": " + n.Label ).ToArray() ) );
988 | Log.Message( "out-of-bounds nodes:\n" +
989 | string.Join(
990 | "\n", Nodes.Where( n => n.X < 1 || n.Y < 1 ).Select( n => n.ToString() ).ToArray() ) );
991 | Log.Trace( ToString() );
992 | }
993 | }
994 | }
--------------------------------------------------------------------------------
/Source/Log.cs:
--------------------------------------------------------------------------------
1 | // Log.cs
2 | // Copyright Karel Kroeze, 2018-2020
3 |
4 | using System.Diagnostics;
5 |
6 | namespace FluffyResearchTree
7 | {
8 | public static class Log
9 | {
10 | public static void Message( string msg, params object[] args )
11 | {
12 | Verse.Log.Message( Format( msg, args ) );
13 | }
14 |
15 | public static void Warning( string msg, params object[] args )
16 | {
17 | Verse.Log.Warning( Format( msg, args ) );
18 | }
19 |
20 | private static string Format( string msg, params object[] args )
21 | {
22 | return "ResearchTree :: " + string.Format( msg, args );
23 | }
24 |
25 | public static void Error( string msg, bool once, params object[] args )
26 | {
27 | var _msg = Format( msg, args );
28 | if ( once )
29 | Verse.Log.ErrorOnce( _msg, _msg.GetHashCode() );
30 | else
31 | Verse.Log.Error( _msg );
32 | }
33 |
34 | [Conditional( "DEBUG" )]
35 | public static void Debug( string msg, params object[] args )
36 | {
37 | Verse.Log.Message( Format( msg, args ) );
38 | }
39 |
40 | [Conditional( "TRACE" )]
41 | public static void Trace( string msg, params object[] args )
42 | {
43 | Verse.Log.Message( Format( msg, args ) );
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/Source/MainButtonWorker_ResearchTree.cs:
--------------------------------------------------------------------------------
1 | // MainButtonWorker_ResearchTree.cs
2 | // Copyright Karel Kroeze, 2018-2020
3 |
4 | using RimWorld;
5 | using UnityEngine;
6 | using Verse;
7 |
8 | namespace FluffyResearchTree
9 | {
10 | public class MainButtonWorker_ResearchTree : MainButtonWorker_ToggleResearchTab
11 | {
12 | public override void DoButton( Rect rect )
13 | {
14 | base.DoButton( rect );
15 |
16 | if ( Queue.NumQueued > 0 )
17 | {
18 | var queueRect = new Rect(
19 | rect.xMax - Constants.SmallQueueLabelSize - Constants.Margin,
20 | 0f,
21 | Constants.SmallQueueLabelSize,
22 | Constants.SmallQueueLabelSize ).CenteredOnYIn( rect );
23 | Queue.DrawLabel( queueRect, Color.white, Color.grey, Queue.NumQueued );
24 | }
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Source/MainTabWindow_ResearchTree.cs:
--------------------------------------------------------------------------------
1 | // MainTabWindow_ResearchTree.cs
2 | // Copyright Karel Kroeze, 2020-2020
3 |
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using RimWorld;
7 | using UnityEngine;
8 | using Verse;
9 | using static FluffyResearchTree.Constants;
10 |
11 | namespace FluffyResearchTree
12 | {
13 | public class MainTabWindow_ResearchTree : MainTabWindow
14 | {
15 | internal static Vector2 _scrollPosition = Vector2.zero;
16 |
17 | private static Rect _treeRect;
18 |
19 | private Rect _baseViewRect;
20 | private Rect _baseViewRect_Inner;
21 |
22 | private bool _dragging;
23 | private Vector2 _mousePosition = Vector2.zero;
24 |
25 | private string _query = "";
26 | private Rect _viewRect;
27 |
28 | private Rect _viewRect_Inner;
29 | private bool _viewRect_InnerDirty = true;
30 | private bool _viewRectDirty = true;
31 |
32 | private float _zoomLevel = 1f;
33 |
34 | public MainTabWindow_ResearchTree()
35 | {
36 | closeOnClickedOutside = false;
37 | Instance = this;
38 | }
39 |
40 | public static MainTabWindow_ResearchTree Instance { get; private set; }
41 |
42 | public float ScaledMargin => Constants.Margin * ZoomLevel / Prefs.UIScale;
43 |
44 | public float ZoomLevel
45 | {
46 | get => _zoomLevel;
47 | set
48 | {
49 | _zoomLevel = Mathf.Clamp( value, 1f, MaxZoomLevel );
50 | _viewRectDirty = true;
51 | _viewRect_InnerDirty = true;
52 | }
53 | }
54 |
55 | public Rect ViewRect
56 | {
57 | get
58 | {
59 | if ( _viewRectDirty )
60 | {
61 | _viewRect = new Rect(
62 | _baseViewRect.xMin * ZoomLevel,
63 | _baseViewRect.yMin * ZoomLevel,
64 | _baseViewRect.width * ZoomLevel,
65 | _baseViewRect.height * ZoomLevel
66 | );
67 | _viewRectDirty = false;
68 | }
69 |
70 | return _viewRect;
71 | }
72 | }
73 |
74 | public Rect ViewRect_Inner
75 | {
76 | get
77 | {
78 | if ( _viewRect_InnerDirty )
79 | {
80 | _viewRect_Inner = _viewRect.ContractedBy( Margin * ZoomLevel );
81 | _viewRect_InnerDirty = false;
82 | }
83 |
84 | return _viewRect_Inner;
85 | }
86 | }
87 |
88 | public Rect TreeRect
89 | {
90 | get
91 | {
92 | if ( _treeRect == default )
93 | {
94 | var width = Tree.Size.x * ( NodeSize.x + NodeMargins.x );
95 | var height = Tree.Size.z * ( NodeSize.y + NodeMargins.y );
96 | _treeRect = new Rect( 0f, 0f, width, height );
97 | }
98 |
99 | return _treeRect;
100 | }
101 | }
102 |
103 | public Rect VisibleRect =>
104 | new Rect(
105 | _scrollPosition.x,
106 | _scrollPosition.y,
107 | ViewRect_Inner.width,
108 | ViewRect_Inner.height );
109 |
110 | internal float MaxZoomLevel
111 | {
112 | get
113 | {
114 | // get the minimum zoom level at which the entire tree fits onto the screen, or a static maximum zoom level.
115 | var fitZoomLevel = Mathf.Max( TreeRect.width / _baseViewRect_Inner.width,
116 | TreeRect.height / _baseViewRect_Inner.height );
117 | return Mathf.Min( fitZoomLevel, AbsoluteMaxZoomLevel );
118 | }
119 | }
120 |
121 | public override void PreClose()
122 | {
123 | base.PreClose();
124 | Log.Debug( "CloseOnClickedOutside: {0}", closeOnClickedOutside );
125 | Log.Debug( StackTraceUtility.ExtractStackTrace() );
126 | }
127 |
128 | public void Notify_TreeInitialized()
129 | {
130 | SetRects();
131 | }
132 |
133 | public override void PreOpen()
134 | {
135 | base.PreOpen();
136 | SetRects();
137 |
138 | if ( !Tree.Initialized )
139 | // initialize tree
140 | Tree.Initialize();
141 |
142 | // clear node availability caches
143 | ResearchNode.ClearCaches();
144 |
145 | _dragging = false;
146 | closeOnClickedOutside = false;
147 | }
148 |
149 | private void SetRects()
150 | {
151 | // tree view rects, have to deal with UIScale and ZoomLevel manually.
152 | _baseViewRect = new Rect(
153 | StandardMargin / Prefs.UIScale,
154 | ( TopBarHeight + Constants.Margin + StandardMargin ) / Prefs.UIScale,
155 | ( Screen.width - StandardMargin * 2f ) / Prefs.UIScale,
156 | ( Screen.height - MainButtonDef.ButtonHeight - StandardMargin * 2f - TopBarHeight - Constants.Margin ) /
157 | Prefs.UIScale );
158 | _baseViewRect_Inner = _baseViewRect.ContractedBy( Constants.Margin / Prefs.UIScale );
159 |
160 | // windowrect, set to topleft (for some reason vanilla alignment overlaps bottom buttons).
161 | windowRect.x = 0f;
162 | windowRect.y = 0f;
163 | windowRect.width = UI.screenWidth;
164 | windowRect.height = UI.screenHeight - MainButtonDef.ButtonHeight;
165 | }
166 |
167 | public override void DoWindowContents( Rect canvas )
168 | {
169 | if ( !Tree.Initialized )
170 | return;
171 |
172 |
173 | // top bar
174 | var topRect = new Rect(
175 | canvas.xMin,
176 | canvas.yMin,
177 | canvas.width,
178 | TopBarHeight );
179 | DrawTopBar( topRect );
180 |
181 | ApplyZoomLevel();
182 |
183 | // draw background
184 | GUI.DrawTexture( ViewRect, Assets.SlightlyDarkBackground );
185 |
186 | // draw the actual tree
187 | // TODO: stop scrollbars scaling with zoom
188 | _scrollPosition = GUI.BeginScrollView( ViewRect, _scrollPosition, TreeRect );
189 | GUI.BeginGroup(
190 | new Rect(
191 | ScaledMargin,
192 | ScaledMargin,
193 | TreeRect.width + ScaledMargin * 2f,
194 | TreeRect.height + ScaledMargin * 2f
195 | )
196 | );
197 |
198 | Tree.Draw( VisibleRect );
199 | Queue.DrawLabels( VisibleRect );
200 |
201 | HandleZoom();
202 |
203 | GUI.EndGroup();
204 | GUI.EndScrollView( false );
205 |
206 | HandleDragging();
207 | HandleDolly();
208 |
209 | // reset zoom level
210 | ResetZoomLevel();
211 |
212 |
213 | // cleanup;
214 | GUI.color = Color.white;
215 | Text.Anchor = TextAnchor.UpperLeft;
216 | }
217 |
218 | private void HandleDolly()
219 | {
220 | var dollySpeed = 10f;
221 | if ( KeyBindingDefOf.MapDolly_Left.IsDown )
222 | _scrollPosition.x -= dollySpeed;
223 | if ( KeyBindingDefOf.MapDolly_Right.IsDown )
224 | _scrollPosition.x += dollySpeed;
225 | if ( KeyBindingDefOf.MapDolly_Up.IsDown )
226 | _scrollPosition.y -= dollySpeed;
227 | if ( KeyBindingDefOf.MapDolly_Down.IsDown )
228 | _scrollPosition.y += dollySpeed;
229 | }
230 |
231 |
232 | private void HandleZoom()
233 | {
234 | // handle zoom
235 | if ( Event.current.isScrollWheel )
236 | {
237 | // absolute position of mouse on research tree
238 | var absPos = Event.current.mousePosition;
239 | // Log.Debug( "Absolute position: {0}", absPos );
240 |
241 | // relative normalized position of mouse on visible tree
242 | var relPos = ( Event.current.mousePosition - _scrollPosition ) / ZoomLevel;
243 | // Log.Debug( "Normalized position: {0}", relPos );
244 |
245 | // update zoom level
246 | ZoomLevel += Event.current.delta.y * ZoomStep * ZoomLevel;
247 |
248 | // we want to keep the _normalized_ relative position the same as before zooming
249 | _scrollPosition = absPos - relPos * ZoomLevel;
250 |
251 | Event.current.Use();
252 | }
253 | }
254 |
255 | private void HandleDragging()
256 | {
257 | if ( Event.current.type == EventType.MouseDown )
258 | {
259 | _dragging = true;
260 | _mousePosition = Event.current.mousePosition;
261 | Event.current.Use();
262 | }
263 |
264 | if ( Event.current.type == EventType.MouseUp )
265 | {
266 | _dragging = false;
267 | _mousePosition = Vector2.zero;
268 | }
269 |
270 | if ( Event.current.type == EventType.MouseDrag )
271 | {
272 | var _currentMousePosition = Event.current.mousePosition;
273 | _scrollPosition += _mousePosition - _currentMousePosition;
274 | _mousePosition = _currentMousePosition;
275 | }
276 | }
277 |
278 | private void ApplyZoomLevel()
279 | {
280 | GUI.EndClip(); // window contents
281 | GUI.EndClip(); // window itself?
282 | GUI.matrix = Matrix4x4.TRS( new Vector3( 0f, 0f, 0f ), Quaternion.identity,
283 | new Vector3( Prefs.UIScale / ZoomLevel, Prefs.UIScale / ZoomLevel, 1f ) );
284 | }
285 |
286 | private void ResetZoomLevel()
287 | {
288 | // dummies to maintain correct stack size
289 | // TODO; figure out how to get actual clipping rects in ApplyZoomLevel();
290 | UI.ApplyUIScale();
291 | GUI.BeginClip( windowRect );
292 | GUI.BeginClip( new Rect( 0f, 0f, UI.screenWidth, UI.screenHeight ) );
293 | }
294 |
295 | private void DrawTopBar( Rect canvas )
296 | {
297 | var searchRect = canvas;
298 | var queueRect = canvas;
299 | searchRect.width = 200f;
300 | queueRect.xMin += 200f + Constants.Margin;
301 |
302 | GUI.DrawTexture( searchRect, Assets.SlightlyDarkBackground );
303 | GUI.DrawTexture( queueRect, Assets.SlightlyDarkBackground );
304 |
305 | DrawSearchBar( searchRect.ContractedBy( Constants.Margin ) );
306 | Queue.DrawQueue( queueRect.ContractedBy( Constants.Margin ), !_dragging );
307 | }
308 |
309 | private void DrawSearchBar( Rect canvas )
310 | {
311 | Profiler.Start( "DrawSearchBar" );
312 | var iconRect = new Rect(
313 | canvas.xMax - Constants.Margin - 16f,
314 | 0f,
315 | 16f,
316 | 16f )
317 | .CenteredOnYIn( canvas );
318 | var searchRect = new Rect(
319 | canvas.xMin,
320 | 0f,
321 | canvas.width,
322 | 30f )
323 | .CenteredOnYIn( canvas );
324 |
325 | GUI.DrawTexture( iconRect, Assets.Search );
326 | var query = Widgets.TextField( searchRect, _query );
327 |
328 | if ( query != _query )
329 | {
330 | _query = query;
331 | Find.WindowStack.FloatMenu?.Close( false );
332 |
333 | if ( query.Length > 2 )
334 | {
335 | // open float menu with search results, if any.
336 | var options = new List();
337 |
338 | foreach ( var result in Tree.Nodes.OfType()
339 | .Select( n => new {node = n, match = n.Matches( query )} )
340 | .Where( result => result.match > 0 )
341 | .OrderBy( result => result.match ) )
342 | options.Add( new FloatMenuOption( result.node.Label, () => CenterOn( result.node ),
343 | MenuOptionPriority.Default, () => CenterOn( result.node ) ) );
344 |
345 | if ( !options.Any() )
346 | options.Add( new FloatMenuOption( "Fluffy.ResearchTree.NoResearchFound".Translate(), null ) );
347 |
348 | Find.WindowStack.Add( new FloatMenu_Fixed( options,
349 | UI.GUIToScreenPoint(
350 | new Vector2(
351 | searchRect.xMin, searchRect.yMax ) ) ) );
352 | }
353 | }
354 |
355 | Profiler.End();
356 | }
357 |
358 | public void CenterOn( Node node )
359 | {
360 | var position = new Vector2(
361 | ( NodeSize.x + NodeMargins.x ) * ( node.X - .5f ),
362 | ( NodeSize.y + NodeMargins.y ) * ( node.Y - .5f ) );
363 |
364 | node.Highlighted = true;
365 |
366 | position -= new Vector2( UI.screenWidth, UI.screenHeight ) / 2f;
367 |
368 | position.x = Mathf.Clamp( position.x, 0f, TreeRect.width - ViewRect.width );
369 | position.y = Mathf.Clamp( position.y, 0f, TreeRect.height - ViewRect.height );
370 | _scrollPosition = position;
371 | }
372 | }
373 | }
--------------------------------------------------------------------------------
/Source/Profiler.cs:
--------------------------------------------------------------------------------
1 | // Profiler.cs
2 | // Copyright Karel Kroeze, 2018-2020
3 |
4 | using System.Diagnostics;
5 | using Verse;
6 |
7 | namespace FluffyResearchTree
8 | {
9 | public class Profiler
10 | {
11 | [Conditional( "DEBUG" )]
12 | public static void Start( string label = null )
13 | {
14 | DeepProfiler.Start( label );
15 | }
16 |
17 | [Conditional( "DEBUG" )]
18 | public static void End()
19 | {
20 | DeepProfiler.End();
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/Source/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | // AssemblyInfo.cs
2 | // Copyright Karel Kroeze, 2020-2020
3 |
4 | using System.Reflection;
5 | using System.Runtime.InteropServices;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle( "ResearchTree" )]
11 | [assembly: AssemblyDescription( "" )]
12 | [assembly: AssemblyConfiguration( "" )]
13 | [assembly: AssemblyCompany( "" )]
14 | [assembly: AssemblyProduct( "ResearchTree" )]
15 | [assembly: AssemblyCopyright( "Copyright © 2015" )]
16 | [assembly: AssemblyTrademark( "" )]
17 | [assembly: AssemblyCulture( "" )]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible( false )]
23 |
24 | // The following GUID is for the ID of the typelib if this project is exposed to COM
25 | [assembly: Guid( "3eb50a4a-c426-40e5-b682-458b34007336" )]
26 |
27 | // Version information for an assembly consists of the following four values:
28 | //
29 | // Major Version
30 | // Minor Version
31 | // Build Number
32 | // Revision
33 | //
34 | // You can specify all the values or you can default the Build and Revision Numbers
35 | // by using the '*' as shown below:
36 | // [assembly: AssemblyVersion("1.0.*")]
37 | [assembly: AssemblyVersion("3.0.0")]
38 | [assembly: AssemblyFileVersion("3.17.537")]
--------------------------------------------------------------------------------
/Source/Queue/Queue.cs:
--------------------------------------------------------------------------------
1 | // Queue.cs
2 | // Copyright Karel Kroeze, 2020-2020
3 |
4 | //using Multiplayer.API;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using RimWorld;
8 | using RimWorld.Planet;
9 | using UnityEngine;
10 | using Verse;
11 | using static FluffyResearchTree.Assets;
12 | using static FluffyResearchTree.Constants;
13 |
14 | namespace FluffyResearchTree
15 | {
16 | public class Queue : WorldComponent
17 | {
18 | private static Queue _instance;
19 | private readonly List _queue = new List();
20 | private List _saveableQueue;
21 |
22 | public Queue( World world ) : base( world )
23 | {
24 | _instance = this;
25 | }
26 |
27 | ///
28 | /// Removes and returns the first node in the queue.
29 | ///
30 | ///
31 | public static ResearchNode Pop
32 | {
33 | get
34 | {
35 | if ( _instance._queue != null && _instance._queue.Count > 0 )
36 | {
37 | var node = _instance._queue[0];
38 | _instance._queue.RemoveAt( 0 );
39 | return node;
40 | }
41 |
42 | return null;
43 | }
44 | }
45 |
46 | public static int NumQueued => _instance._queue.Count - 1;
47 |
48 | public static void TryDequeue( ResearchNode node )
49 | {
50 | if ( _instance._queue.Contains( node ) )
51 | Dequeue( node );
52 | }
53 |
54 | // [SyncMethod]
55 | public static void Dequeue( ResearchNode node )
56 | {
57 | // remove this node
58 | _instance._queue.Remove( node );
59 |
60 | // remove all nodes that depend on it
61 | var followUps = _instance._queue.Where( n => n.GetMissingRequiredRecursive().Contains( node ) ).ToList();
62 | foreach ( var followUp in followUps )
63 | _instance._queue.Remove( followUp );
64 |
65 | // if currently researching this node, stop that
66 | if ( Find.ResearchManager.currentProj == node.Research )
67 | Find.ResearchManager.currentProj = null;
68 | }
69 |
70 | public static void DrawLabels( Rect visibleRect )
71 | {
72 | Profiler.Start( "Queue.DrawLabels" );
73 | var i = 1;
74 | foreach ( var node in _instance._queue )
75 | {
76 | if ( node.IsVisible( visibleRect ) )
77 | {
78 | var main = ColorCompleted[node.Research.techLevel];
79 | var background = i > 1 ? ColorUnavailable[node.Research.techLevel] : main;
80 | DrawLabel( node.QueueRect, main, background, i );
81 | }
82 |
83 | i++;
84 | }
85 |
86 | Profiler.End();
87 | }
88 |
89 | public static void DrawLabel( Rect canvas, Color main, Color background, int label )
90 | {
91 | // draw coloured tag
92 | GUI.color = main;
93 | GUI.DrawTexture( canvas, CircleFill );
94 |
95 | // if this is not first in line, grey out centre of tag
96 | if ( background != main )
97 | {
98 | GUI.color = background;
99 | GUI.DrawTexture( canvas.ContractedBy( 2f ), CircleFill );
100 | }
101 |
102 | // draw queue number
103 | GUI.color = Color.white;
104 | Text.Anchor = TextAnchor.MiddleCenter;
105 | Widgets.Label( canvas, label.ToString() );
106 | Text.Anchor = TextAnchor.UpperLeft;
107 | }
108 |
109 | public static void Enqueue( ResearchNode node, bool add )
110 | {
111 | Log.Debug( $"Enqueuing: {node.Research.defName}" );
112 |
113 | // if we're not adding, clear the current queue and current research project
114 | if ( !add )
115 | {
116 | _instance._queue.Clear();
117 | Find.ResearchManager.currentProj = null;
118 | }
119 |
120 | // add to the queue if not already in it
121 | if ( !_instance._queue.Contains( node ) )
122 | _instance._queue.Add( node );
123 |
124 | // try set the first research in the queue to be the current project.
125 | var next = _instance._queue.First();
126 | Find.ResearchManager.currentProj = next?.Research; // null if next is null.
127 | }
128 |
129 | // [SyncMethod]
130 | public static void EnqueueRange( IEnumerable nodes, bool add )
131 | {
132 | TutorSystem.Notify_Event( "StartResearchProject" );
133 |
134 | // clear current Queue if not adding
135 | if ( !add )
136 | {
137 | _instance._queue.Clear();
138 | Find.ResearchManager.currentProj = null;
139 | }
140 |
141 | // sorting by depth ensures prereqs are met - cost is just a bonus thingy.
142 | foreach ( var node in nodes.OrderBy( node => node.X ).ThenBy( node => node.Research.CostApparent ) )
143 | Enqueue( node, true );
144 | }
145 |
146 | public static bool IsQueued( ResearchNode node )
147 | {
148 | return _instance._queue.Contains( node );
149 | }
150 |
151 | public static void TryStartNext( ResearchProjectDef finished )
152 | {
153 | var current = _instance._queue.FirstOrDefault()?.Research;
154 | Log.Debug( "TryStartNext: current; {0}, finished; {1}", current, finished );
155 | if ( finished != _instance._queue.FirstOrDefault()?.Research )
156 | {
157 | TryDequeue( finished );
158 | return;
159 | }
160 |
161 | _instance._queue.RemoveAt( 0 );
162 | var next = _instance._queue.FirstOrDefault()?.Research;
163 | Log.Debug( "TryStartNext: next; {0}", next );
164 | Find.ResearchManager.currentProj = next;
165 | DoCompletionLetter( current, next );
166 | }
167 |
168 | private static void DoCompletionLetter( ResearchProjectDef current, ResearchProjectDef next )
169 | {
170 | // message
171 | string label = "ResearchFinished".Translate( current.LabelCap );
172 | string text = current.LabelCap + "\n\n" + current.description;
173 |
174 | if ( next != null )
175 | {
176 | text += "\n\n" + "Fluffy.ResearchTree.NextInQueue".Translate( next.LabelCap );
177 | Find.LetterStack.ReceiveLetter( label, text, LetterDefOf.PositiveEvent );
178 | }
179 | else
180 | {
181 | text += "\n\n" + "Fluffy.ResearchTree.NextInQueue".Translate( "Fluffy.ResearchTree.None".Translate() );
182 | Find.LetterStack.ReceiveLetter( label, text, LetterDefOf.NeutralEvent );
183 | }
184 | }
185 |
186 | public override void ExposeData()
187 | {
188 | base.ExposeData();
189 |
190 | // store research defs as these are the defining elements
191 | if ( Scribe.mode == LoadSaveMode.Saving )
192 | _saveableQueue = _queue.Select( node => node.Research ).ToList();
193 |
194 | Scribe_Collections.Look( ref _saveableQueue, "Queue", LookMode.Def );
195 |
196 | if ( Scribe.mode == LoadSaveMode.PostLoadInit )
197 | // initialize the queue
198 | foreach ( var research in _saveableQueue )
199 | {
200 | // find a node that matches the research - or null if none found
201 | var node = research.ResearchNode();
202 |
203 | // enqueue the node
204 | if ( node != null )
205 | {
206 | Log.Debug( "Adding {0} to queue", node.Research.LabelCap );
207 | Enqueue( node, true );
208 | }
209 | else
210 | {
211 | Log.Debug( "Could not find node for {0}", research.LabelCap );
212 | }
213 | }
214 | }
215 |
216 | public static void DrawQueue( Rect canvas, bool interactible )
217 | {
218 | Profiler.Start( "Queue.DrawQueue" );
219 | if ( !_instance._queue.Any() )
220 | {
221 | Text.Anchor = TextAnchor.MiddleCenter;
222 | GUI.color = TechLevelColor;
223 | Widgets.Label( canvas, "Fluffy.ResearchTree.NothingQueued".Translate() );
224 | Text.Anchor = TextAnchor.UpperLeft;
225 | GUI.color = Color.white;
226 | return;
227 | }
228 |
229 | var pos = canvas.min;
230 | for ( var i = 0; i < _instance._queue.Count && pos.x + NodeSize.x < canvas.xMax; i++ )
231 | {
232 | var node = _instance._queue[i];
233 | var rect = new Rect(
234 | pos.x - Margin,
235 | pos.y - Margin,
236 | NodeSize.x + 2 * Margin,
237 | NodeSize.y + 2 * Margin
238 | );
239 | node.DrawAt( pos, rect, true );
240 | if ( interactible && Mouse.IsOver( rect ) )
241 | MainTabWindow_ResearchTree.Instance.CenterOn( node );
242 | pos.x += NodeSize.x + Margin;
243 | }
244 |
245 | Profiler.End();
246 | }
247 |
248 | public static void Notify_InstantFinished()
249 | {
250 | foreach ( var node in new List( _instance._queue ) )
251 | if ( node.Research.IsFinished )
252 | TryDequeue( node );
253 |
254 | Find.ResearchManager.currentProj = _instance._queue.FirstOrDefault()?.Research;
255 | }
256 | }
257 | }
--------------------------------------------------------------------------------
/Source/Queue/Queue_HarmonyPatches.cs:
--------------------------------------------------------------------------------
1 | // Queue_HarmonyPatches.cs
2 | // Copyright Karel Kroeze, 2020-2020
3 |
4 | using HarmonyLib;
5 | using RimWorld;
6 | using Verse;
7 |
8 | namespace FluffyResearchTree
9 | {
10 | public class HarmonyPatches_Queue
11 | {
12 | [HarmonyPatch( typeof( ResearchManager ), "ResearchPerformed", typeof( float ), typeof( Pawn ) )]
13 | public class ResearchPerformed
14 | {
15 | // check if last active project was finished. If so, try start the next project.
16 | // Thanks to NotFood for this nice simplification, I've adapted his/her code;
17 | // https://github.com/notfood/RimWorld-ResearchPal/blob/master/Source/Injectors/ResearchManagerPatch.cs
18 | private static void Prefix( ResearchManager __instance, ref ResearchProjectDef __state )
19 | {
20 | __state = __instance.currentProj;
21 | Log.Debug( "{0} progress: {1}", __state.LabelCap, __state.ProgressPercent );
22 | }
23 |
24 | private static void Postfix( ResearchProjectDef __state )
25 | {
26 | Log.Debug( "{0} finished?: {1}", __state, __state?.IsFinished );
27 | if ( __state?.IsFinished ?? false )
28 | {
29 | Log.Debug( "{0} finished", __state.LabelCap );
30 | Queue.TryStartNext( __state );
31 | }
32 | }
33 | }
34 |
35 | [HarmonyPatch( typeof( ResearchManager ), "FinishProject" )]
36 | public class DoCompletionDialog
37 | {
38 | // suppress vanilla completion dialog, we never want to show it.
39 | private static void Prefix( ref bool doCompletionDialog )
40 | {
41 | doCompletionDialog = false;
42 | }
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/Source/ResearchTree.cs:
--------------------------------------------------------------------------------
1 | // ResearchTree.cs
2 | // Copyright Karel Kroeze, 2020-2020
3 |
4 | using System.Reflection;
5 | using HarmonyLib;
6 | using Verse;
7 | //using Multiplayer.API;
8 |
9 | namespace FluffyResearchTree
10 | {
11 | public class ResearchTree : Mod
12 | {
13 | public ResearchTree( ModContentPack content ) : base( content )
14 | {
15 | var harmony = new Harmony( "Fluffy.ResearchTree" );
16 | harmony.PatchAll( Assembly.GetExecutingAssembly() );
17 |
18 | // if ( MP.enabled )
19 | // MP.RegisterAll();
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/Source/ResearchTree.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {3EB50A4A-C426-40E5-B682-458B34007336}
8 | Library
9 | Properties
10 | ResearchTree
11 | ResearchTree
12 | v4.7.2
13 | 512
14 |
15 |
16 |
17 | false
18 | none
19 | false
20 | ..\Assemblies\
21 | DEBUG
22 | prompt
23 | 4
24 | true
25 | false
26 |
27 |
28 | none
29 | true
30 | ..\Assemblies\
31 |
32 |
33 | prompt
34 | 4
35 | true
36 | false
37 |
38 |
39 |
40 | packages\Lib.Harmony.2.0.0.8\lib\net472\0Harmony.dll
41 | False
42 |
43 |
44 | C:\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\Assembly-CSharp.dll
45 | False
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | C:\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll
56 | False
57 |
58 |
59 | C:\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.IMGUIModule.dll
60 | False
61 |
62 |
63 | C:\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.TextRenderingModule.dll
64 | False
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | mod update -x
95 |
96 |
103 |
--------------------------------------------------------------------------------
/Source/ResearchTree.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 14
4 | VisualStudioVersion = 14.0.24720.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResearchTree", "ResearchTree.csproj", "{3EB50A4A-C426-40E5-B682-458B34007336}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {3EB50A4A-C426-40E5-B682-458B34007336}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {3EB50A4A-C426-40E5-B682-458B34007336}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {3EB50A4A-C426-40E5-B682-458B34007336}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {3EB50A4A-C426-40E5-B682-458B34007336}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/Source/description.md:
--------------------------------------------------------------------------------
1 | A better research tree.
2 |
3 | # Features
4 | - automatically generated to maximize readability*.
5 | - shows research projects, buildings, plants and recipes unlocked by each research project.
6 | - projects can be queued, and colonists will automatically start the next project when the current research project completes.
7 | - search functionality to quickly find research projects.
8 |
9 | # FAQ
10 | *Can I add/remove this from an existing save?*
11 | You can add it to existing saves without problems. Removing this mod will lead to some errors when loading, but these should not affect gameplay - and will go away after saving.
12 |
13 | *Why is research X in position Y?*
14 | Honestly, I have no idea. The placement of projects (nodes) is automated to minimize the number of crossings between dependancies (edges), and reduce the total length of these edges. There are many possible scenarios in which this can lead to placements that may appear non-optimal. Sometimes they really are non-optimal, sometimes they just appear to be so. See also the *technical* section below for more information.
15 |
16 | *Can I use this with mod X*
17 | Most likely, yes. Added researches and their requirements are automatically parsed and the tree layout will be updated accordingly. ResearchPal implements a lot of the same functionality as this mod, and the research queue will likely not work correctly if both mods are loaded.
18 |
19 | *This looks very similar to ResearchPal*
20 | Yep. ResearchPal is based on a legacy version of this mod that was kept up-to-date by SkyArkAngel in the HCSK modpack. I haven't worked on this mod in a long time, but I recently had some spare time and decided to give it another go. Feel free to use whichever you like better (ResearchPal has an entirely different layout algorithm). You can run both mods side by side to check out the different tree layouts, but be aware that the research queue will not work correctly if both mods are loaded.
21 |
22 | # Known Issues
23 | - Layouts are not perfect, if you have experience with graph layouts - please do feel free to look at the source code, and/or implement a Sugiyama layout algorithm for me that runs in C# .NET 3.5 (Mono 2.0).
24 |
25 | # Technical
26 | So how does this all work?
27 |
28 | Creating an optimal layout is a known problem in the area of *Graph Theory*. There's serious mathematicians who've spent years of their live trying to figure out this problem, and numerous solutions exist. The group of solutions most relevant to our research tree (a *directed acyclic graph*, or *DAG*) is that derived from Sugiyama's work. Generally speaking, these algorithms have four steps;
29 | - layering (set the *x* coordinates of nodes, enforcing that follow-up research is always at a higher x position than any of its prerequisites, this is a fairly straightforward heuristic)
30 | - crossing reduction (set the *y* coordinates of nodes such that there is a minimal amount of intersections of connections between nodes)
31 | - edge length reduction (set the *y* coordinates of nodes such that the length of connections between nodes is minimal)
32 | - horizontal alignment (set the *y* coordinates of nodes such that the connections between nodes are straight as much as possible)
33 |
34 | The final step is the hardest, but also the most important to create a visually pleasing tree. Sadly, I've been unable to implement two of the most well known algorithms for this purpose;
35 | - Brandes, U., & Köpf, B. (2001, September). Fast and simple horizontal coordinate assignment.
36 | - Eiglsperger M., Siebenhaller M., Kaufmann M. (2005) An Efficient Implementation of Sugiyama’s Algorithm for Layered Graph Drawing.
37 | Luckily, the crossing reduction and edge length reduction steps partially achieve the goals of the final step. The final graph is not as pretty as it could be, but it's still pretty good - in most scenarios.
38 |
39 |
--------------------------------------------------------------------------------
/Source/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Textures/Buttons/button-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Buttons/button-active.png
--------------------------------------------------------------------------------
/Textures/Buttons/button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Buttons/button.png
--------------------------------------------------------------------------------
/Textures/Icons/Research.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Icons/Research.png
--------------------------------------------------------------------------------
/Textures/Icons/circle-fill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Icons/circle-fill.png
--------------------------------------------------------------------------------
/Textures/Icons/magnifying-glass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Icons/magnifying-glass.png
--------------------------------------------------------------------------------
/Textures/Icons/more.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Icons/more.png
--------------------------------------------------------------------------------
/Textures/Icons/padlock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Icons/padlock.png
--------------------------------------------------------------------------------
/Textures/Lines/Outline/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Lines/Outline/circle.png
--------------------------------------------------------------------------------
/Textures/Lines/Outline/end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Lines/Outline/end.png
--------------------------------------------------------------------------------
/Textures/Lines/Outline/ew.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Lines/Outline/ew.png
--------------------------------------------------------------------------------
/Textures/Lines/Outline/ns.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Lines/Outline/ns.png
--------------------------------------------------------------------------------
/Textures/Lines/Solid/circle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Lines/Solid/circle.png
--------------------------------------------------------------------------------
/Textures/Lines/Solid/end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Lines/Solid/end.png
--------------------------------------------------------------------------------
/Textures/Lines/Solid/ew.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Lines/Solid/ew.png
--------------------------------------------------------------------------------
/Textures/Lines/Solid/ns.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fluffy-mods/ResearchTree/ca5c74c23774426adb2316c6f0f10fc1e6db5483/Textures/Lines/Solid/ns.png
--------------------------------------------------------------------------------