├── .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 | 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 | [![RimWorld 1.2](https://img.shields.io/badge/RimWorld-1.2-brightgreen.svg)](http://rimworldgame.com/) 2 | 3 | A better research tree. 4 | 5 | ![Features](https://banners.karel-kroeze.nl/title/Features.png) 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 | ![FAQ](https://banners.karel-kroeze.nl/title/FAQ.png) 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 | ![Known Issues](https://banners.karel-kroeze.nl/title/Known%20Issues.png) 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![.NET 3.5 (Mono 2.0).](https://banners.karel-kroeze.nl/title/.NET%203.5%20(Mono%202.0)..png) 26 | ![Technical](https://banners.karel-kroeze.nl/title/Technical.png) 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 | ![Contributors](https://banners.karel-kroeze.nl/title/Contributors.png) 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 | ![Think you found a bug?](https://banners.karel-kroeze.nl/title/Think%20you%20found%20a%20bug%3F.png) 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 | ![Older versions](https://banners.karel-kroeze.nl/title/Older%20versions.png) 57 | All current and past versions of this mod can be downloaded from [GitHub](https://github.com/fluffy-mods/ResearchTree/releases). 58 | 59 | ![License](https://banners.karel-kroeze.nl/title/License.png) 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 | [![Supporters](https://banners.karel-kroeze.nl/donations.png)](https://ko-fi.com/fluffymods) 66 | 67 | ![Are you enjoying my mods?](https://banners.karel-kroeze.nl/title/Are%20you%20enjoying%20my%20mods%3F.png) 68 | Become a supporter and show your appreciation by buying me a coffee (or contribute towards a nice single malt). 69 | 70 | [![Buy Me a Coffee](http://i.imgur.com/EjWiUwx.gif)](https://ko-fi.com/fluffymods) 71 | 72 | [![I Have a Black Dog](https://i.ibb.co/ss59Rwy/New-Project-2.png)](https://www.youtube.com/watch?v=XiCrniLQGYc) 73 | 74 | 75 | ![Version](https://banners.karel-kroeze.nl/title/Version.png) 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 --------------------------------------------------------------------------------