├── .gitignore ├── BioScan-Active-Waypoint.png ├── BioScan-Codex.png ├── BioScan-FSS.png ├── BioScan-SAA-Prog-2.png ├── BioScan-SAA-Prog.png ├── BioScan-Scan-Distance.png ├── BioScan-Waypoints.png ├── LICENSE.txt ├── README.md └── src ├── L10n ├── de.strings ├── en.template ├── es.strings ├── fr.strings └── ru.strings ├── bio_scan ├── bio_data │ ├── codex.py │ ├── regions.py │ ├── rulesets │ │ ├── aleoida.py │ │ ├── anemone.py │ │ ├── bacterium.py │ │ ├── brain_tree.py │ │ ├── cactoida.py │ │ ├── clypeus.py │ │ ├── concha.py │ │ ├── electricae.py │ │ ├── fonticulua.py │ │ ├── frutexa.py │ │ ├── fumerola.py │ │ ├── fungoida.py │ │ ├── osseus.py │ │ ├── recepta.py │ │ ├── shard.py │ │ ├── stratum.py │ │ ├── tubers.py │ │ ├── tubus.py │ │ └── tussock.py │ └── species.py ├── body_data │ └── util.py ├── const.py ├── format_util.py ├── globals.py ├── nebula_data │ ├── reference_stars.py │ └── sectors.py ├── overlay.py ├── settings.py ├── status_flags.py ├── tooltip.py └── util.py └── load.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.bak 3 | *.pyc 4 | *.pyo 5 | *.zip 6 | .idea 7 | venv 8 | 9 | # Release prep 10 | EDMC-BioScan/ 11 | /*.zip -------------------------------------------------------------------------------- /BioScan-Active-Waypoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silarn/EDMC-BioScan/ad49dda3b1afb7c7c4c934d6b1f8c06be67fb73d/BioScan-Active-Waypoint.png -------------------------------------------------------------------------------- /BioScan-Codex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silarn/EDMC-BioScan/ad49dda3b1afb7c7c4c934d6b1f8c06be67fb73d/BioScan-Codex.png -------------------------------------------------------------------------------- /BioScan-FSS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silarn/EDMC-BioScan/ad49dda3b1afb7c7c4c934d6b1f8c06be67fb73d/BioScan-FSS.png -------------------------------------------------------------------------------- /BioScan-SAA-Prog-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silarn/EDMC-BioScan/ad49dda3b1afb7c7c4c934d6b1f8c06be67fb73d/BioScan-SAA-Prog-2.png -------------------------------------------------------------------------------- /BioScan-SAA-Prog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silarn/EDMC-BioScan/ad49dda3b1afb7c7c4c934d6b1f8c06be67fb73d/BioScan-SAA-Prog.png -------------------------------------------------------------------------------- /BioScan-Scan-Distance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silarn/EDMC-BioScan/ad49dda3b1afb7c7c4c934d6b1f8c06be67fb73d/BioScan-Scan-Distance.png -------------------------------------------------------------------------------- /BioScan-Waypoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Silarn/EDMC-BioScan/ad49dda3b1afb7c7c4c934d6b1f8c06be67fb73d/BioScan-Waypoints.png -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BioScan 2 | 3 | ## Summary 4 | 5 | BioScan is a multipurpose exobiology tool for Elite Dangerous. It utilizes the plugin system of the [Elite Dangerous 6 | Market Connector][EDMC] for backend journal processing and display. 7 | 8 | ## Core Functionality and Comparison 9 | 10 | The core functionality of BioScan is to provide the best possible estimates for which species will be present on a 11 | planet, much like the [Observatory] [BioInsights] plugin. With the latest updates, BioScan can be slightly more accurate 12 | at ruling out certain bios with overlapping criteria. In a few edge cases, BioInsights may be slightly more accurate. 13 | Determining region boundaries and proximity to nebulae can be hard. 14 | 15 | There are a few key differences between each. BioScan is intended to be used as an active tool with a compact 16 | interface which could be potentially overlaid onto the game. It also presents key information to help you navigate and 17 | track flora while landing, moving, and scanning on a planet. 18 | 19 | It supports both Horizons and Odyssey biologicals. 20 | 21 | ## Predictions 22 | 23 | 24 | 25 | While collecting scan data of a system, planets with biological signals will be added to the main tracker. Based on the 26 | properties of that planet (as well as your location in the galaxy, and the various stars and body types present), the 27 | scroll pane will display all possible genera and species that could be on that body as well as the potential value range 28 | of those possibilities. You can optionally display a complete breakdown of the possible species and variants. 29 | 30 | ## Scanning 31 | 32 | 33 | 34 | After you've mapped a planet with biological signals, it will then pare down the list to the detected genera or species. 35 | Once you've started to scan each species, it will display the final type and value of the sample as well as indicate the 36 | scan progress. Additional info will be discussed in the [navigation](#navigation) section below. 37 | 38 | Once fully analysed, the total system value (and possible first find value) will be shown at the bottom of the pane. 39 | 40 | ## Codex Entries 41 | 42 | 43 | 44 | As of version 2.5, any potential species that has not yet been logged to your local region's codex will be marked. If 45 | there are multiple possible species or variants (colors), those will be individually marked in the detailed breakdown. 46 | 47 | This is marked by the 📝 memo emoji in front of the genus or variant. For best results, 48 | see [journal importing](#journal-importing). 49 | 50 | ## Navigation 51 | 52 | The top of the pane will track all relevant bodies in the system, including a shorthand for the body type and the number 53 | of signals detected there. This can help you quickly determine a DSS target. There are additional indicators for high 54 | gravity `^G^` and extreme gravity `!G!` planets. High gravity is currently considered 1G or greater. Extreme gravity 55 | is 2.7G or greater which makes it impossible to go on foot. 56 | 57 | BioScan will track your movements and show just the relevant species data if you are currently located at a body of 58 | interest, to help reduce clutter and scrolling. It will also give you the gravity of the planet to help you gauge your 59 | landing. After you initiate a scan, you will get a display of the required sample distance and your current minimum 60 | distance to a previous sample, which is updated in real time. 61 | 62 | It will reset your scan progress if the previous scan wasn't completed and you start a different species. It can also 63 | track scans with the composition scanner and will lock in the final species of the genus without requiring you to scan 64 | biologicals one at a time. In this way you can lock in a species and value while completing the analysis of another 65 | lifeform. 66 | 67 | #### Waypoints 68 | 69 |
70 | 71 | Scans with the comp. scanner will optionally log waypoints for any incomplete species. If you have an active scan, the 72 | nearest waypoint for that species will display below the progress indicator. Waypoints within the minimum distance of 73 | previous samples are excluded from the list. When you have no active scan, any remaining unanalyzed species will display 74 | the nearest waypoint in the detailed species scroll list. 75 | 76 | The waypoint indicator will display the distance to the waypoint, the compass heading toward that waypoint, and an 77 | indicator for the direction and degrees to turn to face that heading. Note that waypoints will log your current 78 | location, so for best results make the scan as close to the target species as possible. 79 | 80 | ### Persistent Data 81 | 82 | As of version 2.0, BioScan now maintains a database of all relevant system data and scan progress. It segments scans, 83 | waypoints, and codex data by commander. You can safely stop and restart EDMC without losing your data. 84 | 85 | From version 2.6 forward, the database management is handled by the [ExploData] plugin/module. See that repository for 86 | more details. 87 | 88 | ### Journal Importing 89 | 90 | To facilitate accurate codex entry data, you can import your past journals into the persistent database. Clicking the 91 | button in the bottom left of the settings panel will start the process. This can run in the background while you 92 | continue to use EDMC / BioScan, and the progress will display at the bottom of the BioScan pane. 93 | 94 | Despite using a few threads to help speed up the process, parsing several years of journal data can take a good amount 95 | of time. As each journal is completed, it will get logged in the database. This process can be stopped and restarted. It 96 | will pick up at the oldest unprocessed journal, though you will need to click the button in the settings to resume. 97 | 98 | ### EDSM Integration 99 | 100 | Once per system, you can attempt to fetch any data from EDSM. This is helpful in systems that can't be scanned for one 101 | reason or another. Unfortunately, EDSM's API does not currently provide access to biological signal info, so you will 102 | have to manually look up signals if that data was not previously logged. 103 | 104 | ### Overlay (Version 2.7+) 105 | 106 | BioScan can now make use of EDMCOverlay to draw predictions, signal summaries, and status info directly onto the game 107 | window. The text color and anchor points are configurable. Suggestions welcome. 108 | 109 | ## Requirements 110 | * [EDMC] version 5.7 and above (python 3.11) 111 | * SQLAlchemy python module (embedded in the exe distribution) 112 | * (Optional) [EDMCOverlay] / [edmcoverlay2] 113 | 114 | ## Installation 115 | 116 | * Download the [latest release] of both BioScan and ExploData 117 | * Appropriate versions of both are available on every release page 118 | * ExploData has multiple versions depending on how you run EDMC: 119 | * Users of the prebuilt Windows EXE must use the Windows EXE version of ExploData 120 | * Flatpak users must use the flatpak version, which is bundled with SQLAlchemy 121 | * Native python users should use the native python version (see additional instructions below) 122 | * If you use [Pioneer], make sure the version you're using is up-to-date with ExploData 123 | * Extract the `.zip` archives that you downloaded into the EDMC `plugins` folder 124 | * This is accessible via the plugins tab in the EDMC settings window 125 | * The ExploData plugin directory must be named as it is packaged, or you will run into trouble loading dependencies 126 | * For native python users: 127 | * If you use `venv`, install the SQLAlchemy requirement from the `requirements.txt` to the EDMC venv 128 | * For system python, run `pip install -r requirements.txt` within the ExploData plugin directory to install SQLAlchemy 129 | * Ensure the correct `pip` is used for your version of EDMC 130 | * (Optional) Install the [EDMCOverlay] plugin for overlay support (or [edmcoverlay2] for Linux) 131 | * Either 'EDMCOverlay' or 'edmcoverlay' should work as the plugin directory name 132 | * Start or restart EDMC to register the plugin and run any necessary database migrations 133 | 134 | ## Acknowledgements 135 | 136 | Conversion of system coordinates to regions thanks to klightspeed's [EliteDangerousRegionMap]. 137 | 138 | Species calculations are based on various sources, primarily the [Deep Space Network], the 139 | [Codex NSP and Bio requirements spreadsheet][Bio req spreadsheet], and the [Canonn Biosheet]. 140 | 141 | Nebula locations pulled from the [Catalog of Galactic Nebulae] (thanks marx and contributors) 142 | 143 | Procedurally generated nebula reference star coordinates pulled from [EDSM]'s API 144 | 145 | ## Roadmap 146 | 147 | * Voice support 148 | * Translations support 149 | 150 | ## License 151 | 152 | [BioScan plugin][BioScan] Copyright © 2024 Jeremy Rimpo 153 | 154 | Licensed under the [GNU Public License (GPL)][GPLv2] version 2 or later. 155 | 156 | [EDMC]: https://github.com/EDCD/EDMarketConnector/wiki 157 | [EDSM]: https://www.edsm.net/ 158 | [Deep Space Network]: https://ed-dsn.net/ 159 | [Bio req spreadsheet]: https://docs.google.com/spreadsheets/d/1nV_UD_0kIxkWAHhAqvf62ILHpbYzdZpJ53CqPHn3qlA/ 160 | [Canonn Biosheet]: https://canonn.fyi/biosheet 161 | [EliteDangerousRegionMap]: https://github.com/klightspeed/EliteDangerousRegionMap/ 162 | [Catalog of Galactic Nebulae]: https://forums.frontier.co.uk/threads/catalogue-of-galactic-nebulae-submit-your-planetary-nebulae.511743/ 163 | [BioScan]: https://github.com/Silarn/EDMC-BioScan 164 | [Pioneer]: https://github.com/Silarn/EDMC-Pioneer 165 | [ExploData]: https://github.com/Silarn/EDMC-ExploData 166 | [Observatory]: https://github.com/Xjph/ObservatoryCore 167 | [BioInsights]: https://edjp.colacube.net/observatory 168 | [EDMCOverlay]: https://github.com/inorton/EDMCOverlay 169 | [edmcoverlay2]: https://github.com/pan-mroku/edmcoverlay2 170 | [latest release]: https://github.com/Silarn/EDMC-BioScan/releases/latest 171 | [GPLv2]: http://www.gnu.org/licenses/gpl-2.0.html 172 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/codex.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from sqlalchemy import select 4 | 5 | from ExploData.explo_data.bio_data.genus import data as bio_genus 6 | from ExploData.explo_data.db import CodexScans 7 | 8 | from ExploData.explo_data import db 9 | 10 | from bio_scan.bio_data.species import rules as bio_types 11 | from bio_scan.util import translate_species 12 | 13 | 14 | def check_codex(commander: int, region: int | None, genus: str, species: str, variant: str = '') -> bool: 15 | if region is None: 16 | return False 17 | biological = species 18 | if variant: 19 | if species in bio_genus: 20 | code = '' 21 | color_data = bio_genus[species]['colors']['star'] 22 | for key, color in color_data.items(): # type: str, str 23 | if color == variant: 24 | code = key.capitalize() 25 | break 26 | if code: 27 | match = re.match('^(.*)_Name;$', species) 28 | if match: 29 | biological = f'{match.group(1)}_{code}_Name;' 30 | 31 | elif 'colors' in bio_genus[genus]: 32 | variant_data = bio_genus[genus]['colors'] 33 | code = '' 34 | color_data = [] 35 | if 'species' in variant_data: 36 | if 'star' in variant_data['species'][species]: 37 | color_data = variant_data['species'][species]['star'] 38 | elif 'element' in variant_data['species'][species]: 39 | color_data = variant_data['species'][species]['element'] 40 | else: 41 | color_data = variant_data['star'] 42 | for key, color in color_data.items(): # type: str, str 43 | if color == variant: 44 | code = key.capitalize() 45 | break 46 | 47 | if code: 48 | match = re.match('^(.*)_Name;$', species) 49 | if match: 50 | biological = f'{match.group(1)}_{code}_Name;' 51 | 52 | session = db.get_session() 53 | entry: CodexScans = session.scalar(select(CodexScans).where(CodexScans.commander_id == commander) 54 | .where(CodexScans.region == region).where(CodexScans.biological == biological)) 55 | session.close() 56 | 57 | if entry: 58 | return True 59 | return False 60 | 61 | 62 | def check_codex_from_name(commander: int, region: int, name: str, variant: str = '') -> bool: 63 | for genus, species_data in bio_types.items(): 64 | for species_code, data in species_data.items(): 65 | if translate_species(data['name']) == name: 66 | return check_codex(commander, region, genus, species_code, variant) 67 | return False 68 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/regions.py: -------------------------------------------------------------------------------- 1 | region_map: dict[str, list[int]] = { 2 | 'orion-cygnus': [1, 4, 7, 8, 16, 17, 18, 35], 3 | 'orion-cygnus-1': [4, 7, 8, 16, 17, 18, 35], 4 | 'orion-cygnus-core': [7, 8, 16, 17, 18, 35], 5 | 'sagittarius-carina': [1, 4, 9, 18, 19, 20, 21, 22, 23, 40], 6 | 'sagittarius-carina-core': [9, 18, 19, 20, 21, 22, 23, 40], 7 | 'sagittarius-carina-core-9': [18, 19, 20, 21, 22, 23, 40], 8 | 'scutum-centaurus': [1, 4, 9, 10, 11, 12, 24, 25, 26, 42, 28], 9 | 'scutum-centaurus-core': [9, 10, 11, 12, 24, 25, 26, 42, 28], 10 | 'outer': [1, 2, 5, 6, 13, 14, 27, 29, 31, 41, 37], 11 | 'perseus': [1, 3, 7, 15, 30, 32, 33, 34, 36, 38, 39], 12 | 'perseus-core': [3, 7, 15, 30, 32, 33, 34, 36, 38, 39], 13 | 'exterior': [14, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 34, 36, 14 | 37, 38, 39, 40, 41, 42], 15 | 'anemone-a': [7, 8, 13, 14, 15, 16, 17, 18, 27, 32], 16 | 'amphora': [10, 19, 20, 21, 22], 17 | 'brain-tree': [2, 9, 10, 17, 18, 35], 18 | 'empyrean-straits': [2], 19 | 'center': [1, 2, 3] 20 | } 21 | 22 | guardian_nebulae: dict[str, tuple[int, tuple[float, float, float]]] = { 23 | 'Hen 2-333': (750, (-840.65625, -561.15625, 13361.8125)), 24 | 'Gamma Velorum': (750, (1099.21875, -146.6875, -133.59375)), 25 | 'Skaudai AA-A h71': (100, (-5493.09375, -589.28125, 10424.4375)), 26 | 'Blaa Hypai AA-A h68': (100, (1220.40625, -694.625, 12312.8125)), 27 | 'Eorl Auwsy AA-A h72': (100, (4949.9375, 164, 20640.125)), 28 | 'Prai Hypoo AA-A h60': (100, (-9294.875, -458.40625, 7905.71875)), 29 | 'Eta Carina Nebula': (100, (8579.96875, -138.96875, 2701.375)), 30 | 'NGC 3199': (100, (14574.15625, -259.625, 3511.90625)) 31 | } 32 | 33 | tuber_zones: dict[str, tuple[tuple[int, int], tuple[float, float, float]]] = { 34 | 'Arcadian Stream': ((200, 600), (8885, -20, 20535)), 35 | 'Empyrean Straits': ((200, 400), (4325, 400, 21185)), 36 | 'Galactic Center': ((500, 1000), (44.5, 492.7, 25916)), 37 | 'Hawking A': ((150, 600), (5788, 150, 6335)), 38 | 'Hawking B': ((200, 600), (9990, -40, 8335)), 39 | 'Inner Orion Spur': ((200, 600), (-3485, 39, 7320)), 40 | 'Inner O-P Conflux': ((350, 750), (-13245, -80, 30285)), 41 | 'Inner S-C Arm A': ((200, 600), (-1600, -37, 10720)), 42 | 'Inner S-C Arm B 1': ((150, 300), (-6645, 0, 12590)), 43 | 'Inner S-C Arm B 2': ((200, 600), (-6645, 0, 12590)), 44 | 'Inner S-C Arm C': ((200, 600), (-9355, -50, 17175)), 45 | 'Inner S-C Arm D': ((300, 400), (-12000, 232, 22670)), 46 | 'Izanami': ((200, 750), (-4610, 370, 37225)), 47 | 'Norma Arm A': ((500, 1000), (3722.6, 200, 16441)), 48 | 'Norma Arm B': ((200, 500), (3740, 175, 16460)), 49 | 'Norma Expanse A': ((200, 600), (4245, -42, 12071)), 50 | 'Norma Expanse B': ((150, 250), (5580, 40, 11727)), 51 | 'Odin A': ((750, 1000), (-7945, 230, 28025)), 52 | 'Odin B': ((200, 600), (-5329, -68, 18647)), 53 | 'Ryker A': ((250, 750), (1715, 766, 34070)), 54 | 'Ryker B': ((750, 1500), (-1445, 345, 30345)), 55 | 'Trojan Belt': ((250, 500), (18600, 65, 31750)), 56 | } 57 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/aleoida.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Aleoids_Genus_Name;': { 5 | '$Codex_Ent_Aleoids_01_Name;': { 6 | 'name': 'Aleoida Arcus', 7 | 'value': 7252500, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['CarbonDioxide'], 11 | 'min_gravity': 0.04, 12 | 'max_gravity': 0.276, 13 | 'min_temperature': 175.0, 14 | 'max_temperature': 180.0, 15 | 'min_pressure': 0.0161, 16 | 'body_type': ['Rocky body', 'High metal content body'], 17 | 'volcanism': 'None' 18 | } 19 | ], 20 | }, 21 | '$Codex_Ent_Aleoids_02_Name;': { 22 | 'name': 'Aleoida Coronamus', 23 | 'value': 6284600, 24 | 'rulesets': [ 25 | { 26 | 'atmosphere': ['CarbonDioxide'], 27 | 'min_gravity': 0.04, 28 | 'max_gravity': 0.276, 29 | 'min_temperature': 180.0, 30 | 'max_temperature': 190.0, 31 | 'min_pressure': 0.025, 32 | 'body_type': ['Rocky body', 'High metal content body'], 33 | 'volcanism': 'None' 34 | } 35 | ], 36 | }, 37 | '$Codex_Ent_Aleoids_03_Name;': { 38 | 'name': 'Aleoida Spica', 39 | 'value': 3385200, 40 | 'rulesets': [ 41 | { 42 | 'atmosphere': ['Ammonia'], 43 | 'min_gravity': 0.04, 44 | 'max_gravity': 0.276, 45 | 'min_temperature': 170.0, 46 | 'max_temperature': 177.0, 47 | 'max_pressure': 0.0135, 48 | 'body_type': ['Rocky body', 'High metal content body'], 49 | 'regions': ['outer', 'perseus', 'scutum-centaurus'] 50 | } 51 | ], 52 | }, 53 | '$Codex_Ent_Aleoids_04_Name;': { 54 | 'name': 'Aleoida Laminiae', 55 | 'value': 3385200, 56 | 'rulesets': [ 57 | { 58 | 'atmosphere': ['Ammonia'], 59 | 'min_gravity': 0.04, 60 | 'max_gravity': 0.276, 61 | 'min_temperature': 152.0, 62 | 'max_temperature': 177.0, 63 | 'max_pressure': 0.0135, 64 | 'body_type': ['Rocky body', 'High metal content body'], 65 | 'regions': ['orion-cygnus', 'sagittarius-carina'] 66 | } 67 | ], 68 | }, 69 | '$Codex_Ent_Aleoids_05_Name;': { 70 | 'name': 'Aleoida Gravis', 71 | 'value': 12934900, 72 | 'rulesets': [ 73 | { 74 | 'atmosphere': ['CarbonDioxide'], 75 | 'min_gravity': 0.04, 76 | 'max_gravity': 0.276, 77 | 'min_temperature': 190.0, 78 | 'max_temperature': 197.0, 79 | 'min_pressure': 0.054, 80 | 'body_type': ['Rocky body', 'High metal content body'], 81 | 'volcanism': 'None' 82 | } 83 | ], 84 | } 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/anemone.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Sphere_Name;': { 5 | '$Codex_Ent_Sphere_Name;': { 6 | 'name': 'Luteolum Anemone', 7 | 'value': 1499900, 8 | 'rulesets': [ 9 | { 10 | 'min_gravity': 0.044, 11 | 'max_gravity': 1.28, 12 | 'max_temperature': 440.0, 13 | 'min_temperature': 200.0, 14 | 'volcanism': ['metallic', 'silicate', 'rocky', 'water'], 15 | 'body_type': ['Rocky body'], 16 | 'star': [('B', 'IV'), ('B', 'V')], 17 | 'regions': ['anemone-a'] 18 | } 19 | ], 20 | }, 21 | '$Codex_Ent_SphereABCD_01_Name;': { 22 | 'name': 'Croceum Anemone', 23 | 'value': 1499900, 24 | 'rulesets': [ 25 | { 26 | 'min_gravity': 0.047, 27 | 'max_gravity': 0.37, 28 | 'max_temperature': 440.0, 29 | 'min_temperature': 200.0, 30 | 'volcanism': ['silicate', 'rocky', 'metallic'], 31 | 'body_type': ['Rocky body'], 32 | 'star': [('B', 'V'), ('B', 'VI'), ('A', 'III')], 33 | 'regions': ['anemone-a'] 34 | } 35 | ], 36 | }, 37 | '$Codex_Ent_SphereABCD_02_Name;': { 38 | 'name': 'Puniceum Anemone', 39 | 'value': 1499900, 40 | 'rulesets': [ 41 | { 42 | 'min_gravity': 0.17, 43 | 'max_gravity': 2.52, 44 | 'max_temperature': 800.0, 45 | 'min_temperature': 65.0, 46 | 'volcanism': 'None', 47 | 'body_type': ['Icy body', 'Rocky ice body'], 48 | 'star': ['O'], 49 | 'regions': ['anemone-a'] 50 | }, 51 | { 52 | 'min_gravity': 0.17, 53 | 'max_gravity': 2.52, 54 | 'max_temperature': 800.0, 55 | 'min_temperature': 65.0, 56 | 'volcanism': ['carbon dioxide geysers'], 57 | 'body_type': ['Icy body', 'Rocky ice body'], 58 | 'star': ['O'], 59 | 'regions': ['anemone-a'] 60 | } 61 | ], 62 | }, 63 | '$Codex_Ent_SphereABCD_03_Name;': { 64 | 'name': 'Roseum Anemone', 65 | 'value': 1499900, 66 | 'rulesets': [ 67 | { 68 | 'min_gravity': 0.045, 69 | 'max_gravity': 0.37, 70 | 'max_temperature': 440.0, 71 | 'min_temperature': 200.0, 72 | 'volcanism': ['silicate', 'rocky', 'metallic'], 73 | 'body_type': ['Rocky body'], 74 | 'star': [('B', 'I'), ('B', 'II'), ('B', 'III'), ('B', 'IV')], 75 | 'regions': ['anemone-a'] 76 | } 77 | ], 78 | }, 79 | '$Codex_Ent_SphereEFGH_01_Name;': { 80 | 'name': 'Rubeum Bioluminescent Anemone', 81 | 'value': 1499900, 82 | 'rulesets': [ 83 | { 84 | 'min_gravity': 0.036, 85 | 'max_gravity': 4.61, 86 | 'min_temperature': 160.0, 87 | 'max_temperature': 1800.0, 88 | 'volcanism': 'Any', 89 | 'body_type': ['Metal rich body', 'High metal content body'], 90 | 'star': [('B', 'VI'), ['A', 'I'], ['A', 'II'], ['A', 'III'], 'N'] 91 | } 92 | ], 93 | }, 94 | '$Codex_Ent_SphereEFGH_02_Name;': { 95 | 'name': 'Prasinum Bioluminescent Anemone', 96 | 'value': 1499900, 97 | 'rulesets': [ 98 | { 99 | 'min_gravity': 0.036, 100 | 'min_temperature': 110.0, 101 | 'max_temperature': 3050.0, 102 | 'body_type': ['Metal rich body', 'Rocky body', 'High metal content body'], 103 | 'star': ['O'] 104 | } 105 | ], 106 | }, 107 | '$Codex_Ent_SphereEFGH_03_Name;': { 108 | 'name': 'Roseum Bioluminescent Anemone', 109 | 'value': 1499900, 110 | 'rulesets': [ 111 | { 112 | 'min_gravity': 0.036, 113 | 'max_gravity': 4.61, 114 | 'min_temperature': 400.0, 115 | 'volcanism': 'Any', 116 | 'body_type': ['Metal rich body', 'High metal content body'], 117 | 'star': [('B', 'I'), ('B', 'II'), ('B', 'III')] 118 | } 119 | ], 120 | }, 121 | '$Codex_Ent_SphereEFGH_Name;': { 122 | 'name': 'Blatteum Bioluminescent Anemone', 123 | 'value': 1499900, 124 | 'rulesets': [ 125 | { 126 | 'min_temperature': 220.0, 127 | 'volcanism': 'Any', 128 | 'body_type': ['Metal rich body', 'High metal content body'], 129 | 'star': [('B', 'IV'), ('B', 'V')], 130 | 'regions': ['anemone-a'] 131 | } 132 | ], 133 | }, 134 | }, 135 | } 136 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/brain_tree.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Brancae_Name;': { 5 | '$Codex_Ent_Seed_Name;': { 6 | 'name': 'Roseum Brain Tree', 7 | 'value': 1593700, 8 | 'rulesets': [ 9 | { 10 | 'min_temperature': 200.0, 11 | 'max_temperature': 500.0, 12 | 'volcanism': 'Any', 13 | 'guardian': True, 14 | 'region': ['brain-tree'] 15 | } 16 | ], 17 | }, 18 | '$Codex_Ent_SeedABCD_01_Name;': { 19 | 'name': 'Gypseeum Brain Tree', 20 | 'value': 1593700, 21 | 'rulesets': [ 22 | { 23 | 'body_type': ['Rocky body'], 24 | 'min_temperature': 200.0, 25 | 'max_temperature': 400.0, 26 | 'max_gravity': 0.42, 27 | 'volcanism': ['metallic', 'rocky', 'silicate', 'water'], 28 | 'guardian': True, 29 | 'region': ['brain-tree'], 30 | 'bodies': ['Earthlike body', 'Gas giant with water based life', 'Water giant'] 31 | } 32 | ], 33 | }, 34 | '$Codex_Ent_SeedABCD_02_Name;': { 35 | 'name': 'Ostrinum Brain Tree', 36 | 'value': 1593700, 37 | 'rulesets': [ 38 | { 39 | 'body_type': ['Metal rich body', 'Rocky body', 'High metal content body'], 40 | 'volcanism': ['metallic', 'rocky', 'silicate'], 41 | 'guardian': True, 42 | 'region': ['brain-tree'] 43 | } 44 | ], 45 | }, 46 | '$Codex_Ent_SeedABCD_03_Name;': { 47 | 'name': 'Viride Brain Tree', 48 | 'value': 1593700, 49 | 'rulesets': [ 50 | { 51 | 'body_type': ['Rocky ice body'], 52 | 'min_temperature': 100.0, 53 | 'max_temperature': 270.0, 54 | 'max_gravity': 0.4, 55 | 'volcanism': 'Any', 56 | 'guardian': True, 57 | 'region': ['brain-tree'], 58 | 'bodies': ['Earthlike body', 'Gas giant with water based life', 'Water giant'] 59 | } 60 | ], 61 | }, 62 | '$Codex_Ent_SeedEFGH_01_Name;': { 63 | 'name': 'Aureum Brain Tree', 64 | 'value': 1593700, 65 | 'rulesets': [ 66 | { 67 | 'body_type': ['Metal rich body', 'High metal content body'], 68 | 'min_temperature': 300.0, 69 | 'max_temperature': 500.0, 70 | 'max_gravity': 2.9, 71 | 'volcanism': ['metallic', 'rocky', 'silicate'], 72 | 'guardian': True, 73 | 'region': ['brain-tree'], 74 | #'bodies': ['Earthlike body', 'Gas giant with water based life', 'Water giant'] 75 | } 76 | ], 77 | }, 78 | '$Codex_Ent_SeedEFGH_02_Name;': { 79 | 'name': 'Puniceum Brain Tree', 80 | 'value': 1593700, 81 | 'rulesets': [ 82 | { 83 | 'body_type': ['Metal rich body', 'High metal content body'], 84 | 'volcanism': 'Any', 85 | 'guardian': True, 86 | 'region': ['brain-tree'], 87 | 'bodies': ['Earthlike body', 'Gas giant with water based life', 'Water giant'] 88 | } 89 | ], 90 | }, 91 | '$Codex_Ent_SeedEFGH_03_Name;': { 92 | 'name': 'Lindigoticum Brain Tree', 93 | 'value': 1593700, 94 | 'rulesets': [ 95 | { 96 | 'body_type': ['Rocky body', 'High metal content body'], 97 | 'min_temperature': 300.0, 98 | 'max_temperature': 500.0, 99 | 'max_gravity': 2.7, 100 | 'volcanism': ['rocky', 'silicate', 'metallic'], 101 | 'guardian': True, 102 | 'region': ['brain-tree'], 103 | 'bodies': ['Earthlike body', 'Gas giant with water based life', 'Water giant'] 104 | } 105 | ], 106 | }, 107 | '$Codex_Ent_SeedEFGH_Name;': { 108 | 'name': 'Lividum Brain Tree', 109 | 'value': 1593700, 110 | 'rulesets': [ 111 | { 112 | 'body_type': ['Rocky body'], 113 | 'min_temperature': 300.0, 114 | 'max_temperature': 500.0, 115 | 'max_gravity': 0.5, 116 | 'volcanism': ['metallic', 'rocky', 'silicate', 'water'], 117 | 'guardian': True, 118 | 'region': ['brain-tree'], 119 | #'bodies': ['Earthlike body', 'Gas giant with water based life', 'Water giant'] 120 | } 121 | ], 122 | }, 123 | }, 124 | } 125 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/cactoida.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Cactoid_Genus_Name;': { 5 | '$Codex_Ent_Cactoid_01_Name;': { 6 | 'name': 'Cactoida Cortexum', 7 | 'value': 3667600, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['CarbonDioxide'], 11 | 'body_type': ['Rocky body', 'High metal content body'], 12 | 'min_gravity': 0.04, 13 | 'max_gravity': 0.276, 14 | 'min_temperature': 180.0, 15 | 'max_temperature': 197.0, 16 | 'min_pressure': 0.025, 17 | 'volcanism': 'None', 18 | 'regions': ['orion-cygnus'] 19 | } 20 | ], 21 | }, 22 | '$Codex_Ent_Cactoid_02_Name;': { 23 | 'name': 'Cactoida Lapis', 24 | 'value': 2483600, 25 | 'rulesets': [ 26 | { 27 | 'atmosphere': ['Ammonia'], 28 | 'body_type': ['Rocky body', 'High metal content body'], 29 | 'min_gravity': 0.04, 30 | 'max_gravity': 0.276, 31 | 'min_temperature': 160.0, 32 | 'max_temperature': 177.0, 33 | 'max_pressure': 0.0135, 34 | 'regions': ['sagittarius-carina'] 35 | } 36 | ], 37 | }, 38 | '$Codex_Ent_Cactoid_03_Name;': { 39 | 'name': 'Cactoida Vermis', 40 | 'value': 16202800, 41 | 'rulesets': [ 42 | { 43 | 'atmosphere': ['SulphurDioxide'], 44 | 'body_type': ['Rocky body'], 45 | 'min_gravity': 0.265, 46 | 'max_gravity': 0.276, 47 | 'min_temperature': 160.0, 48 | 'max_temperature': 210.0, 49 | 'max_pressure': 0.005, 50 | 'volcanism': 'None' 51 | }, 52 | { 53 | 'atmosphere': ['Water'], 54 | 'body_type': ['Rocky body', 'High metal content body'], 55 | 'min_gravity': 0.04, 56 | 'max_gravity': 0.276, 57 | 'volcanism': 'None' 58 | }, 59 | { 60 | 'atmosphere': ['Water'], 61 | 'body_type': ['Rocky body', 'High metal content body'], 62 | 'min_gravity': 0.04, 63 | 'max_gravity': 0.276, 64 | 'volcanism': ['water'] 65 | } 66 | ], 67 | }, 68 | '$Codex_Ent_Cactoid_04_Name;': { 69 | 'name': 'Cactoida Pullulanta', 70 | 'value': 3667600, 71 | 'rulesets': [ 72 | { 73 | 'atmosphere': ['CarbonDioxide'], 74 | 'body_type': ['Rocky body', 'High metal content body'], 75 | 'min_gravity': 0.04, 76 | 'max_gravity': 0.276, 77 | 'min_temperature': 180.0, 78 | 'max_temperature': 197.0, 79 | 'min_pressure': 0.025, 80 | 'volcanism': 'None', 81 | 'regions': ['perseus'] 82 | } 83 | ], 84 | }, 85 | '$Codex_Ent_Cactoid_05_Name;': { 86 | 'name': 'Cactoida Peperatis', 87 | 'value': 2483600, 88 | 'rulesets': [ 89 | { 90 | 'atmosphere': ['Ammonia'], 91 | 'body_type': ['Rocky body', 'High metal content body'], 92 | 'min_gravity': 0.04, 93 | 'max_gravity': 0.276, 94 | 'min_temperature': 160.0, 95 | 'max_temperature': 177.0, 96 | 'max_pressure': 0.0135, 97 | 'regions': ['scutum-centaurus'] 98 | } 99 | ], 100 | }, 101 | }, 102 | } 103 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/clypeus.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Clypeus_Genus_Name;': { 5 | '$Codex_Ent_Clypeus_01_Name;': { 6 | 'name': 'Clypeus Lacrimam', 7 | 'value': 8418000, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['CarbonDioxide'], 11 | 'body_type': ['Rocky body'], 12 | 'min_gravity': 0.04, 13 | 'max_gravity': 0.276, 14 | 'min_temperature': 190.0, 15 | 'max_temperature': 197.0, 16 | 'min_pressure': 0.054, 17 | 'volcanism': 'None' 18 | }, 19 | { 20 | 'atmosphere': ['Water'], 21 | 'body_type': ['Rocky body'], 22 | 'min_gravity': 0.04, 23 | 'max_gravity': 0.276, 24 | 'volcanism': 'None' 25 | }, 26 | { 27 | 'atmosphere': ['Water'], 28 | 'body_type': ['Rocky body'], 29 | 'min_gravity': 0.04, 30 | 'max_gravity': 0.276, 31 | 'volcanism': ['water'] 32 | } 33 | ], 34 | }, 35 | '$Codex_Ent_Clypeus_02_Name;': { 36 | 'name': 'Clypeus Margaritus', 37 | 'value': 11873200, 38 | 'rulesets': [ 39 | { 40 | 'atmosphere': ['CarbonDioxide'], 41 | 'body_type': ['High metal content body'], 42 | 'min_gravity': 0.04, 43 | 'max_gravity': 0.276, 44 | 'min_temperature': 190.0, 45 | 'max_temperature': 197.0, 46 | 'min_pressure': 0.054, 47 | 'volcanism': 'None' 48 | }, 49 | { 50 | 'atmosphere': ['Water'], 51 | 'body_type': ['High metal content body'], 52 | 'min_gravity': 0.04, 53 | 'max_gravity': 0.276, 54 | 'volcanism': 'None' 55 | } 56 | ], 57 | }, 58 | '$Codex_Ent_Clypeus_03_Name;': { 59 | 'name': 'Clypeus Speculumi', 60 | 'value': 16202800, 61 | 'rulesets': [ 62 | { 63 | 'atmosphere': ['CarbonDioxide'], 64 | 'min_gravity': 0.04, 65 | 'max_gravity': 0.276, 66 | 'min_temperature': 190.0, 67 | 'max_temperature': 197.0, 68 | 'min_pressure': 0.055, 69 | 'body_type': ['Rocky body'], 70 | 'volcanism': 'None', 71 | 'distance': 2000.0 72 | }, 73 | { 74 | 'atmosphere': ['Water'], 75 | 'min_gravity': 0.04, 76 | 'max_gravity': 0.276, 77 | 'body_type': ['Rocky body'], 78 | 'volcanism': 'None', 79 | 'distance': 2000.0 80 | }, 81 | { 82 | 'atmosphere': ['Water'], 83 | 'min_gravity': 0.04, 84 | 'max_gravity': 0.276, 85 | 'body_type': ['Rocky body'], 86 | 'volcanism': ['water'], 87 | 'distance': 2000.0 88 | } 89 | ], 90 | }, 91 | }, 92 | } 93 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/concha.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Conchas_Genus_Name;': { 5 | '$Codex_Ent_Conchas_01_Name;': { 6 | 'name': 'Concha Renibus', 7 | 'value': 4572400, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['Ammonia'], 11 | 'body_type': ['Rocky body', 'High metal content body'], 12 | 'min_gravity': 0.04, 13 | 'max_gravity': 0.045, 14 | 'min_temperature': 176.0, 15 | 'max_temperature': 177.0, 16 | 'volcanism': ['silicate', 'metallic'] 17 | }, 18 | { 19 | 'atmosphere': ['CarbonDioxide'], 20 | 'body_type': ['Rocky body', 'High metal content body'], 21 | 'min_gravity': 0.04, 22 | 'max_gravity': 0.276, 23 | 'min_temperature': 180.0, 24 | 'max_temperature': 197.0, 25 | 'min_pressure': 0.025, 26 | 'volcanism': 'None' 27 | }, 28 | { 29 | 'atmosphere': ['Methane'], 30 | 'body_type': ['Rocky body', 'High metal content body'], 31 | 'min_gravity': 0.04, 32 | 'max_gravity': 0.15, 33 | 'min_temperature': 78.0, 34 | 'max_temperature': 100.0, 35 | 'min_pressure': 0.01, 36 | 'volcanism': ['silicate', 'metallic'] 37 | }, 38 | { 39 | 'atmosphere': ['Water'], 40 | 'body_type': ['Rocky body', 'High metal content body'], 41 | 'min_gravity': 0.04, 42 | 'max_gravity': 0.65, 43 | 'volcanism': 'None' 44 | }, 45 | { 46 | 'atmosphere': ['Water'], 47 | 'body_type': ['Rocky body', 'High metal content body'], 48 | 'min_gravity': 0.04, 49 | 'max_gravity': 0.65, 50 | 'volcanism': ['water'] 51 | } 52 | ], 53 | }, 54 | '$Codex_Ent_Conchas_02_Name;': { 55 | 'name': 'Concha Aureolas', 56 | 'value': 7774700, 57 | 'rulesets': [ 58 | { 59 | 'atmosphere': ['Ammonia'], 60 | 'body_type': ['Rocky body', 'High metal content body'], 61 | 'min_gravity': 0.04, 62 | 'max_gravity': 0.276, 63 | 'min_temperature': 152.0, 64 | 'max_temperature': 177.0, 65 | 'max_pressure': 0.0135 66 | } 67 | ], 68 | }, 69 | '$Codex_Ent_Conchas_03_Name;': { 70 | 'name': 'Concha Labiata', 71 | 'value': 2352400, 72 | 'rulesets': [ 73 | { 74 | 'atmosphere': ['CarbonDioxide'], 75 | 'body_type': ['Rocky body', 'High metal content body'], 76 | 'min_gravity': 0.04, 77 | 'max_gravity': 0.276, 78 | 'min_temperature': 150.0, 79 | 'max_temperature': 200.0, 80 | 'min_pressure': 0.002, 81 | 'volcanism': 'None' 82 | } 83 | ], 84 | }, 85 | '$Codex_Ent_Conchas_04_Name;': { 86 | 'name': 'Concha Biconcavis', 87 | 'value': 16777215, 88 | 'rulesets': [ 89 | { 90 | 'atmosphere': ['Nitrogen'], 91 | 'body_type': ['Rocky body', 'High metal content body'], 92 | 'min_gravity': 0.053, 93 | 'max_gravity': 0.275, 94 | 'min_temperature': 42.0, 95 | 'max_temperature': 52.0, 96 | 'max_pressure': 0.0047, 97 | 'volcanism': 'None' 98 | } 99 | ], 100 | }, 101 | }, 102 | } 103 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/electricae.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Electricae_Genus_Name;': { 5 | '$Codex_Ent_Electricae_01_Name;': { 6 | 'name': 'Electricae Pluma', 7 | 'value': 6284600, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['Argon', 'ArgonRich'], 11 | 'body_type': ['Icy body'], 12 | 'min_gravity': 0.025, 13 | 'max_gravity': 0.276, 14 | 'min_temperature': 50.0, 15 | 'max_temperature': 150.0, 16 | 'parent_star': ['A', 'N', 'D', 'H', 'AeBe'] 17 | }, 18 | { 19 | 'atmosphere': ['Neon', 'NeonRich'], 20 | 'body_type': ['Icy body'], 21 | 'min_gravity': 0.26, 22 | 'max_gravity': 0.276, 23 | 'min_temperature': 20.0, 24 | 'max_temperature': 70.0, 25 | 'max_pressure': 0.005, 26 | 'parent_star': ['A', 'N', 'D', 'H', 'AeBe'] 27 | } 28 | ], 29 | }, 30 | '$Codex_Ent_Electricae_02_Name;': { 31 | 'name': 'Electricae Radialem', 32 | 'value': 6284600, 33 | 'rulesets': [ 34 | { 35 | 'atmosphere': ['Argon', 'ArgonRich'], 36 | 'min_gravity': 0.025, 37 | 'max_gravity': 0.276, 38 | 'min_temperature': 50.0, 39 | 'max_temperature': 150.0, 40 | 'body_type': ['Icy body'], 41 | 'nebula': 'all' 42 | }, 43 | { 44 | 'atmosphere': ['Neon', 'NeonRich'], 45 | 'min_gravity': 0.026, 46 | 'max_gravity': 0.276, 47 | 'min_temperature': 20.0, 48 | 'max_temperature': 70.0, 49 | 'max_pressure': 0.005, 50 | 'body_type': ['Icy body'], 51 | 'nebula': 'all' 52 | } 53 | ], 54 | }, 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/fonticulua.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Fonticulus_Genus_Name;': { 5 | '$Codex_Ent_Fonticulus_01_Name;': { 6 | 'name': 'Fonticulua Segmentatus', 7 | 'value': 19010800, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['Neon', 'NeonRich'], 11 | 'body_type': ['Icy body'], 12 | 'min_gravity': 0.25, 13 | 'max_gravity': 0.276, 14 | 'min_temperature': 50.0, 15 | 'max_temperature': 75.0, 16 | 'max_pressure': 0.006, 17 | 'volcanism': 'None' 18 | } 19 | ], 20 | }, 21 | '$Codex_Ent_Fonticulus_02_Name;': { 22 | 'name': 'Fonticulua Campestris', 23 | 'value': 1000000, 24 | 'rulesets': [ 25 | { 26 | 'atmosphere': ['Argon'], 27 | 'body_type': ['Icy body', 'Rocky ice body'], 28 | 'min_gravity': 0.027, 29 | 'max_gravity': 0.276, 30 | 'min_temperature': 50.0, 31 | 'max_temperature': 150.0 32 | } 33 | ], 34 | }, 35 | '$Codex_Ent_Fonticulus_03_Name;': { 36 | 'name': 'Fonticulua Upupam', 37 | 'value': 5727600, 38 | 'rulesets': [ 39 | { 40 | 'atmosphere': ['ArgonRich'], 41 | 'body_type': ['Icy body', 'Rocky ice body'], 42 | 'min_gravity': 0.209, 43 | 'max_gravity': 0.276, 44 | 'min_temperature': 61.0, 45 | 'max_temperature': 125.0, 46 | 'min_pressure': 0.0175 47 | } 48 | ], 49 | }, 50 | '$Codex_Ent_Fonticulus_04_Name;': { 51 | 'name': 'Fonticulua Lapida', 52 | 'value': 3111000, 53 | 'rulesets': [ 54 | { 55 | 'atmosphere': ['Nitrogen'], 56 | 'min_gravity': 0.19, 57 | 'max_gravity': 0.276, 58 | 'min_temperature': 50.0, 59 | 'max_temperature': 81.0, 60 | 'body_type': ['Icy body', 'Rocky ice body'] 61 | } 62 | ], 63 | }, 64 | '$Codex_Ent_Fonticulus_05_Name;': { 65 | 'name': 'Fonticulua Fluctus', 66 | 'value': 20000000, 67 | 'rulesets': [ 68 | { 69 | 'atmosphere': ['Oxygen'], 70 | 'body_type': ['Icy body'], 71 | 'min_gravity': 0.235, 72 | 'max_gravity': 0.276, 73 | 'min_temperature': 143.0, 74 | 'max_temperature': 200.0, 75 | 'min_pressure': 0.012 76 | } 77 | ], 78 | }, 79 | '$Codex_Ent_Fonticulus_06_Name;': { 80 | 'name': 'Fonticulua Digitos', 81 | 'value': 1804100, 82 | 'rulesets': [ 83 | { 84 | 'atmosphere': ['Methane'], 85 | 'body_type': ['Icy body', 'Rocky ice body'], 86 | 'min_gravity': 0.025, 87 | 'max_gravity': 0.07, 88 | 'min_temperature': 83.0, 89 | 'max_temperature': 109.0, 90 | 'min_pressure': 0.03 91 | } 92 | ], 93 | }, 94 | }, 95 | } 96 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/frutexa.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Shrubs_Genus_Name;': { 5 | '$Codex_Ent_Shrubs_01_Name;': { 6 | 'name': 'Frutexa Flabellum', 7 | 'value': 1808900, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['Ammonia'], 11 | 'body_type': ['Rocky body'], 12 | 'min_gravity': 0.04, 13 | 'max_gravity': 0.276, 14 | 'min_temperature': 152.0, 15 | 'max_temperature': 177.0, 16 | 'max_pressure': 0.0135, 17 | 'regions': ['!scutum-centaurus'] 18 | } 19 | ], 20 | }, 21 | '$Codex_Ent_Shrubs_02_Name;': { 22 | 'name': 'Frutexa Acus', 23 | 'value': 7774700, 24 | 'rulesets': [ 25 | { 26 | 'atmosphere': ['CarbonDioxide'], 27 | 'body_type': ['Rocky body'], 28 | 'min_gravity': 0.04, 29 | 'max_gravity': 0.237, 30 | 'min_temperature': 146.0, 31 | 'max_temperature': 197.0, 32 | 'min_pressure': 0.0029, 33 | 'volcanism': 'None', 34 | 'regions': ['orion-cygnus'] 35 | } 36 | ], 37 | }, 38 | '$Codex_Ent_Shrubs_03_Name;': { 39 | 'name': 'Frutexa Metallicum', 40 | 'value': 1632500, 41 | 'rulesets': [ 42 | { 43 | 'atmosphere': ['Ammonia'], 44 | 'body_type': ['High metal content body'], 45 | 'min_gravity': 0.04, 46 | 'max_gravity': 0.276, 47 | 'min_temperature': 152.0, 48 | 'max_temperature': 176.0, 49 | 'max_pressure': 0.01, 50 | 'volcanism': 'None', 51 | }, 52 | { 53 | 'atmosphere': ['CarbonDioxide'], 54 | 'body_type': ['High metal content body'], 55 | 'min_gravity': 0.04, 56 | 'max_gravity': 0.276, 57 | 'min_temperature': 146.0, 58 | 'max_temperature': 197.0, 59 | 'min_pressure': 0.002, 60 | 'volcanism': 'None', 61 | }, 62 | { # Only two samples 63 | 'atmosphere': ['Methane'], 64 | 'body_type': ['High metal content body'], 65 | 'min_gravity': 0.05, 66 | 'max_gravity': 0.1, 67 | 'min_temperature': 100.0, 68 | 'max_temperature': 300.0, 69 | }, 70 | { 71 | 'atmosphere': ['Water'], 72 | 'body_type': ['High metal content body'], 73 | 'min_gravity': 0.04, 74 | 'max_gravity': 0.07, 75 | 'max_temperature': 400.0, 76 | 'max_pressure': 0.07, 77 | 'volcanism': 'None', 78 | } 79 | ], 80 | }, 81 | '$Codex_Ent_Shrubs_04_Name;': { 82 | 'name': 'Frutexa Flammasis', 83 | 'value': 10326000, 84 | 'rulesets': [ 85 | { 86 | 'atmosphere': ['Ammonia'], 87 | 'body_type': ['Rocky body'], 88 | 'min_gravity': 0.04, 89 | 'max_gravity': 0.276, 90 | 'min_temperature': 152.0, 91 | 'max_temperature': 177.0, 92 | 'max_pressure': 0.0135, 93 | 'regions': ['scutum-centaurus'] 94 | } 95 | ], 96 | }, 97 | '$Codex_Ent_Shrubs_05_Name;': { 98 | 'name': 'Frutexa Fera', 99 | 'value': 1632500, 100 | 'rulesets': [ 101 | { 102 | 'atmosphere': ['CarbonDioxide'], 103 | 'body_type': ['Rocky body'], 104 | 'min_gravity': 0.04, 105 | 'max_gravity': 0.276, 106 | 'min_temperature': 146.0, 107 | 'max_temperature': 197.0, 108 | 'min_pressure': 0.003, 109 | 'volcanism': 'None', 110 | 'regions': ['outer'] 111 | } 112 | ], 113 | }, 114 | '$Codex_Ent_Shrubs_06_Name;': { 115 | 'name': 'Frutexa Sponsae', 116 | 'value': 5988000, 117 | 'rulesets': [ 118 | { 119 | 'atmosphere': ['Water'], 120 | 'body_type': ['Rocky body'], 121 | 'min_gravity': 0.04, 122 | 'max_gravity': 0.056, 123 | 'volcanism': 'None' 124 | }, 125 | { 126 | 'atmosphere': ['Water'], 127 | 'body_type': ['Rocky body'], 128 | 'min_gravity': 0.04, 129 | 'max_gravity': 0.056, 130 | 'volcanism': ['water'] 131 | } 132 | ], 133 | }, 134 | '$Codex_Ent_Shrubs_07_Name;': { 135 | 'name': 'Frutexa Collum', 136 | 'value': 1639800, 137 | 'rulesets': [ 138 | { 139 | 'atmosphere': ['SulphurDioxide'], 140 | 'body_type': ['Rocky body'], 141 | 'min_gravity': 0.04, 142 | 'max_gravity': 0.276, 143 | 'min_temperature': 132.0, 144 | 'max_temperature': 215.0, 145 | 'max_pressure': 0.004 146 | }, 147 | { 148 | 'atmosphere': ['SulphurDioxide'], 149 | 'body_type': ['High metal content body'], 150 | 'min_gravity': 0.265, 151 | 'max_gravity': 0.276, 152 | 'min_temperature': 132.0, 153 | 'max_temperature': 135.0, 154 | 'max_pressure': 0.004, 155 | 'volcanism': 'None' 156 | } 157 | ], 158 | }, 159 | }, 160 | } 161 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/fumerola.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Fumerolas_Genus_Name;': { 5 | '$Codex_Ent_Fumerolas_01_Name;': { 6 | 'name': 'Fumerola Carbosis', 7 | 'value': 6284600, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['Argon'], 11 | 'body_type': ['Icy body', 'Rocky ice body'], 12 | 'min_gravity': 0.168, 13 | 'max_gravity': 0.276, 14 | 'min_temperature': 57.0, 15 | 'max_temperature': 150.0, 16 | 'volcanism': ['carbon', 'methane'] 17 | }, 18 | { 19 | 'atmosphere': ['Methane'], 20 | 'body_type': ['Icy body'], 21 | 'min_gravity': 0.025, 22 | 'max_gravity': 0.047, 23 | 'min_temperature': 84.0, 24 | 'max_temperature': 110.0, 25 | 'min_pressure': 0.03, 26 | 'volcanism': ['methane magma'] 27 | }, 28 | { 29 | 'atmosphere': ['Neon'], 30 | 'body_type': ['Icy body'], 31 | 'min_gravity': 0.26, 32 | 'max_gravity': 0.276, 33 | 'min_temperature': 40.0, 34 | 'max_temperature': 60.0, 35 | 'volcanism': ['carbon', 'methane'] 36 | }, 37 | { 38 | 'atmosphere': ['Nitrogen'], 39 | 'body_type': ['Icy body'], 40 | 'min_gravity': 0.2, 41 | 'max_gravity': 0.276, 42 | 'min_temperature': 57.0, 43 | 'max_temperature': 70.0, 44 | 'volcanism': ['carbon', 'methane'] 45 | }, 46 | { 47 | 'atmosphere': ['Oxygen'], 48 | 'body_type': ['Icy body'], 49 | 'min_gravity': 0.26, 50 | 'max_gravity': 0.276, 51 | 'min_temperature': 160.0, 52 | 'max_temperature': 180.0, 53 | 'volcanism': ['carbon'] 54 | }, 55 | { 56 | 'atmosphere': ['SulphurDioxide'], 57 | 'body_type': ['Icy body', 'Rocky ice body'], 58 | 'min_gravity': 0.185, 59 | 'max_gravity': 0.276, 60 | 'min_temperature': 149.0, 61 | 'max_temperature': 272.0, 62 | 'volcanism': ['carbon', 'methane'] 63 | }, 64 | { # Probably incomplete 65 | 'atmosphere': ['Ammonia', 'ArgonRich', 'CarbonDioxideRich'], 66 | 'body_type': ['Icy body'], 67 | 'max_gravity': 0.276, 68 | 'volcanism': ['carbon'] 69 | } 70 | ], 71 | }, 72 | '$Codex_Ent_Fumerolas_02_Name;': { 73 | 'name': 'Fumerola Extremus', 74 | 'value': 16202800, 75 | 'rulesets': [ 76 | { 77 | 'atmosphere': ['Ammonia'], 78 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 79 | 'min_gravity': 0.04, 80 | 'max_gravity': 0.09, 81 | 'min_temperature': 161.0, 82 | 'max_temperature': 177.0, 83 | 'max_pressure': 0.0135, 84 | 'volcanism': ['silicate', 'metallic', 'rocky'] 85 | }, 86 | { 87 | 'atmosphere': ['Argon'], 88 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 89 | 'min_gravity': 0.07, 90 | 'max_gravity': 0.276, 91 | 'min_temperature': 50.0, 92 | 'max_temperature': 121.0, 93 | 'volcanism': ['silicate', 'metallic', 'rocky'] 94 | }, 95 | { 96 | 'atmosphere': ['Methane'], 97 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 98 | 'min_gravity': 0.025, 99 | 'max_gravity': 0.127, 100 | 'min_temperature': 77.0, 101 | 'max_temperature': 109.0, 102 | 'min_pressure': 0.01, 103 | 'volcanism': ['silicate', 'metallic', 'rocky'] 104 | }, 105 | { 106 | 'atmosphere': ['SulphurDioxide'], 107 | 'body_type': ['Rocky body', 'Rocky ice body'], 108 | 'min_gravity': 0.07, 109 | 'max_gravity': 0.276, 110 | 'min_temperature': 54.0, 111 | 'max_temperature': 210.0, 112 | 'volcanism': ['silicate', 'metallic', 'rocky'] 113 | } 114 | ], 115 | }, 116 | '$Codex_Ent_Fumerolas_03_Name;': { 117 | 'name': 'Fumerola Nitris', 118 | 'value': 7500900, 119 | 'rulesets': [ 120 | { # Only one example 121 | 'atmosphere': ['Neon'], 122 | 'body_type': ['Icy body'], 123 | 'min_gravity': 0.04, 124 | 'max_gravity': 0.276, 125 | 'min_temperature': 30.0, 126 | 'max_temperature': 129.0, 127 | 'volcanism': ['nitrogen', 'ammonia'] 128 | }, 129 | { 130 | 'atmosphere': ['Argon', 'ArgonRich', 'NeonRich'], 131 | 'body_type': ['Icy body'], 132 | 'min_gravity': 0.044, 133 | 'max_gravity': 0.276, 134 | 'min_temperature': 50.0, 135 | 'max_temperature': 141.0, 136 | 'volcanism': ['nitrogen', 'ammonia'] 137 | }, 138 | { 139 | 'atmosphere': ['Methane'], 140 | 'body_type': ['Icy body'], 141 | 'min_gravity': 0.025, 142 | 'max_gravity': 0.1, 143 | 'min_temperature': 83.0, 144 | 'max_temperature': 109.0, 145 | 'volcanism': ['nitrogen'] 146 | }, 147 | { 148 | 'atmosphere': ['Nitrogen'], 149 | 'body_type': ['Icy body'], 150 | 'min_gravity': 0.21, 151 | 'max_gravity': 0.276, 152 | 'min_temperature': 60.0, 153 | 'max_temperature': 81.0, 154 | 'volcanism': ['nitrogen', 'ammonia'] 155 | }, 156 | { 157 | 'atmosphere': ['Oxygen'], 158 | 'body_type': ['Icy body'], 159 | 'max_gravity': 0.276, 160 | 'min_temperature': 150.0, 161 | 'volcanism': ['nitrogen', 'ammonia'] 162 | }, 163 | { 164 | 'atmosphere': ['SulphurDioxide'], 165 | 'body_type': ['Icy body'], 166 | 'min_gravity': 0.21, 167 | 'max_gravity': 0.276, 168 | 'min_temperature': 160.0, 169 | 'max_temperature': 250.0, 170 | 'volcanism': ['nitrogen', 'ammonia'] 171 | }, 172 | ], 173 | }, 174 | '$Codex_Ent_Fumerolas_04_Name;': { 175 | 'name': 'Fumerola Aquatis', 176 | 'value': 6284600, 177 | 'rulesets': [ 178 | { 179 | 'atmosphere': ['Ammonia'], 180 | 'body_type': ['Icy body', 'Rocky ice body', 'Rocky body'], 181 | 'min_gravity': 0.028, 182 | 'max_gravity': 0.276, 183 | 'min_temperature': 161.0, 184 | 'max_temperature': 177.0, 185 | 'min_pressure': 0.002, 186 | 'max_pressure': 0.02, 187 | 'volcanism': ['water'] 188 | }, 189 | { 190 | 'atmosphere': ['Argon', 'ArgonRich'], 191 | 'body_type': ['Icy body', 'Rocky ice body'], 192 | 'min_gravity': 0.166, 193 | 'max_gravity': 0.276, 194 | 'min_temperature': 57.0, 195 | 'max_temperature': 150.0, 196 | 'volcanism': ['water'] 197 | }, 198 | { 199 | 'atmosphere': ['CarbonDioxide'], 200 | 'body_type': ['Icy body'], 201 | 'min_gravity': 0.25, 202 | 'max_gravity': 0.276, 203 | 'min_temperature': 160.0, 204 | 'max_temperature': 180.0, 205 | 'min_pressure': 0.01, 206 | 'max_pressure': 0.03, 207 | 'volcanism': ['water'] 208 | }, 209 | { 210 | 'atmosphere': ['Methane'], 211 | 'body_type': ['Rocky body'], 212 | 'min_gravity': 0.04, 213 | 'max_gravity': 0.276, 214 | 'min_temperature': 80.0, 215 | 'max_temperature': 100.0, 216 | 'min_pressure': 0.01, 217 | 'volcanism': ['water'] 218 | }, 219 | { 220 | 'atmosphere': ['Neon'], 221 | 'body_type': ['Icy body'], 222 | 'min_gravity': 0.26, 223 | 'max_gravity': 0.276, 224 | 'min_temperature': 20.0, 225 | 'max_temperature': 60.0, 226 | 'volcanism': ['water'] 227 | }, 228 | { 229 | 'atmosphere': ['Nitrogen'], 230 | 'body_type': ['Icy body'], 231 | 'min_gravity': 0.195, 232 | 'max_gravity': 0.245, 233 | 'min_temperature': 56.0, 234 | 'max_temperature': 80.0, 235 | 'volcanism': ['water'] 236 | }, 237 | { 238 | 'atmosphere': ['Oxygen'], 239 | 'body_type': ['Icy body'], 240 | 'min_gravity': 0.23, 241 | 'max_gravity': 0.276, 242 | 'min_temperature': 153.0, 243 | 'max_temperature': 190.0, 244 | 'min_pressure': 0.01, 245 | 'volcanism': ['water'] 246 | }, 247 | { 248 | 'atmosphere': ['SulphurDioxide'], 249 | 'body_type': ['Icy body', 'Rocky ice body', 'Rocky body'], 250 | 'min_gravity': 0.18, 251 | 'max_gravity': 0.276, 252 | 'min_temperature': 150.0, 253 | 'max_temperature': 270.0, 254 | 'volcanism': ['water'] 255 | }, 256 | { 257 | 'atmosphere': ['Water'], 258 | 'body_type': ['Rocky body'], 259 | 'min_gravity': 0.04, 260 | 'max_gravity': 0.06, 261 | 'volcanism': ['water'] 262 | } 263 | ], 264 | }, 265 | }, 266 | } 267 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/fungoida.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Fungoids_Genus_Name;': { 5 | '$Codex_Ent_Fungoids_01_Name;': { 6 | 'name': 'Fungoida Setisis', 7 | 'value': 1670100, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['Ammonia'], 11 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 12 | 'min_gravity': 0.04, 13 | 'max_gravity': 0.276, 14 | 'min_temperature': 152.0, 15 | 'max_temperature': 177.0, 16 | 'max_pressure': 0.0135 17 | }, 18 | { 19 | 'atmosphere': ['Methane'], 20 | 'body_type': ['Rocky ice body'], 21 | 'min_gravity': 0.033, 22 | 'max_gravity': 0.276, 23 | 'min_temperature': 68.0, 24 | 'max_temperature': 109.0, 25 | 'volcanism': 'None' 26 | }, 27 | { 28 | 'atmosphere': ['Methane'], 29 | 'body_type': ['Rocky body', 'High metal content body'], 30 | 'min_gravity': 0.033, 31 | 'max_gravity': 0.276, 32 | 'min_temperature': 67.0, 33 | 'max_temperature': 109.0 34 | } 35 | ], 36 | }, 37 | '$Codex_Ent_Fungoids_02_Name;': { 38 | 'name': 'Fungoida Stabitis', 39 | 'value': 2680300, 40 | 'rulesets': [ 41 | { 42 | 'atmosphere': ['Ammonia'], 43 | 'body_type': ['Rocky body', 'Rocky ice body'], 44 | 'min_gravity': 0.04, 45 | 'max_gravity': 0.045, 46 | 'min_temperature': 172.0, 47 | 'max_temperature': 177.0, 48 | 'volcanism': ['silicate'], 49 | 'regions': ['orion-cygnus'] 50 | }, 51 | { 52 | 'atmosphere': ['Argon'], 53 | 'body_type': ['Rocky ice body'], 54 | 'min_gravity': 0.20, 55 | 'max_gravity': 0.23, 56 | 'min_temperature': 60.0, 57 | 'max_temperature': 90.0, 58 | 'volcanism': ['silicate', 'rocky'], 59 | 'regions': ['orion-cygnus'] 60 | }, 61 | { # Only one sample 62 | 'atmosphere': ['ArgonRich'], 63 | 'body_type': ['Icy body'], 64 | 'min_gravity': 0.3, 65 | 'max_gravity': 0.5, 66 | 'min_temperature': 60.0, 67 | 'max_temperature': 90.0, 68 | 'regions': ['orion-cygnus'] 69 | }, 70 | { 71 | 'atmosphere': ['CarbonDioxide'], 72 | 'body_type': ['Rocky body', 'High metal content body'], 73 | 'min_gravity': 0.0405, 74 | 'max_gravity': 0.27, 75 | 'min_temperature': 180.0, 76 | 'max_temperature': 197.0, 77 | 'min_pressure': 0.025, 78 | 'volcanism': 'None', 79 | 'regions': ['orion-cygnus'] 80 | }, 81 | { 82 | 'atmosphere': ['Methane'], 83 | 'body_type': ['Rocky body'], 84 | 'min_gravity': 0.043, 85 | 'max_gravity': 0.126, 86 | 'min_temperature': 78.5, 87 | 'max_temperature': 109.0, 88 | 'min_pressure': 0.012, 89 | 'volcanism': ['major silicate'], 90 | 'regions': ['orion-cygnus'] 91 | }, 92 | { 93 | 'atmosphere': ['Water'], 94 | 'body_type': ['Rocky body', 'High metal content body'], 95 | 'min_gravity': 0.039, 96 | 'max_gravity': 0.064, 97 | 'volcanism': 'None', 98 | 'regions': ['orion-cygnus'] 99 | } 100 | ], 101 | }, 102 | '$Codex_Ent_Fungoids_03_Name;': { 103 | 'name': 'Fungoida Bullarum', 104 | 'value': 3703200, 105 | 'rulesets': [ 106 | { 107 | 'atmosphere': ['Argon'], 108 | 'min_gravity': 0.058, 109 | 'max_gravity': 0.276, 110 | 'min_temperature': 50.0, 111 | 'max_temperature': 129.0, 112 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 113 | 'volcanism': 'None', 114 | }, 115 | { 116 | 'atmosphere': ['Nitrogen'], 117 | 'min_gravity': 0.155, 118 | 'max_gravity': 0.276, 119 | 'min_temperature': 50.0, 120 | 'max_temperature': 70.0, 121 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 122 | 'volcanism': 'None', 123 | } 124 | ], 125 | }, 126 | '$Codex_Ent_Fungoids_04_Name;': { 127 | 'name': 'Fungoida Gelata', 128 | 'value': 3330300, 129 | 'rulesets': [ 130 | { # Only one sample - review 131 | 'atmosphere': ['Argon'], 132 | 'body_type': ['Rocky body', 'Rocky ice body'], 133 | 'min_gravity': 0.041, 134 | 'max_gravity': 0.276, 135 | 'min_temperature': 160.0, 136 | 'max_temperature': 180.0, 137 | 'max_pressure': 0.0135, 138 | 'volcanism': ['major silicate'], 139 | 'regions': ['!orion-cygnus-core'] 140 | }, 141 | { 142 | 'atmosphere': ['Ammonia'], 143 | 'body_type': ['Rocky body', 'Rocky ice body'], 144 | 'min_gravity': 0.042, 145 | 'max_gravity': 0.071, 146 | 'min_temperature': 160.0, 147 | 'max_temperature': 180.0, 148 | 'max_pressure': 0.0135, 149 | 'volcanism': ['major silicate'], 150 | 'regions': ['!orion-cygnus-core'] 151 | }, 152 | { 153 | 'atmosphere': ['Ammonia'], 154 | 'body_type': ['High metal content body'], 155 | 'min_gravity': 0.042, 156 | 'max_gravity': 0.071, 157 | 'min_temperature': 160.0, 158 | 'max_temperature': 180.0, 159 | 'max_pressure': 0.0135, 160 | 'volcanism': ['major rocky'], 161 | 'regions': ['!orion-cygnus-core'] 162 | }, 163 | { 164 | 'atmosphere': ['CarbonDioxide'], 165 | 'min_gravity': 0.041, 166 | 'max_gravity': 0.276, 167 | 'min_temperature': 180.0, 168 | 'max_temperature': 200.0, 169 | 'min_pressure': 0.025, 170 | 'body_type': ['Rocky body', 'High metal content body'], 171 | 'volcanism': 'None', 172 | 'regions': ['!orion-cygnus-core'] 173 | }, 174 | { 175 | 'atmosphere': ['Methane'], 176 | 'body_type': ['Rocky body', 'High metal content body'], 177 | 'min_gravity': 0.044, 178 | 'max_gravity': 0.125, 179 | 'min_temperature': 80.0, 180 | 'max_temperature': 110.0, 181 | 'min_pressure': 0.01, 182 | 'volcanism': ['major silicate', 'major metallic'], 183 | 'regions': ['!orion-cygnus-core'] 184 | }, 185 | { 186 | 'atmosphere': ['Water'], 187 | 'body_type': ['Rocky body', 'High metal content body'], 188 | 'min_gravity': 0.039, 189 | 'max_gravity': 0.063, 190 | 'volcanism': 'None', 191 | 'regions': ['!orion-cygnus-core'] 192 | } 193 | ], 194 | }, 195 | }, 196 | } 197 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/osseus.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Osseus_Genus_Name;': { 5 | '$Codex_Ent_Osseus_01_Name;': { 6 | 'name': 'Osseus Fractus', 7 | 'value': 4027800, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['CarbonDioxide'], 11 | 'body_type': ['Rocky body', 'High metal content body'], 12 | 'min_gravity': 0.04, 13 | 'max_gravity': 0.276, 14 | 'min_temperature': 180.0, 15 | 'max_temperature': 190.0, 16 | 'min_pressure': 0.025, 17 | 'volcanism': 'None', 18 | 'regions': ['!perseus'] 19 | } 20 | ], 21 | }, 22 | '$Codex_Ent_Osseus_02_Name;': { 23 | 'name': 'Osseus Discus', 24 | 'value': 12934900, 25 | 'rulesets': [ 26 | { 27 | 'atmosphere': ['Ammonia'], 28 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 29 | 'min_gravity': 0.04, 30 | 'max_gravity': 0.088, 31 | 'min_temperature': 161.0, 32 | 'max_temperature': 177.0, 33 | 'max_pressure': 0.0135, 34 | 'volcanism': 'Any' 35 | }, 36 | { 37 | 'atmosphere': ['Argon'], 38 | 'body_type': ['Rocky ice body'], 39 | 'min_gravity': 0.2, 40 | 'max_gravity': 0.276, 41 | 'min_temperature': 65.0, 42 | 'max_temperature': 120.0, 43 | 'volcanism': 'Any' 44 | }, 45 | { 46 | 'atmosphere': ['Methane'], 47 | 'body_type': ['Rocky body'], 48 | 'min_gravity': 0.04, 49 | 'max_gravity': 0.127, 50 | 'min_temperature': 80.0, 51 | 'max_temperature': 110.0, 52 | 'min_pressure': 0.012, 53 | 'volcanism': 'Any' 54 | }, 55 | { 56 | 'atmosphere': ['Water'], 57 | 'body_type': ['Rocky body', 'High metal content body'], 58 | 'min_gravity': 0.04, 59 | 'max_gravity': 0.055, 60 | } 61 | ], 62 | }, 63 | '$Codex_Ent_Osseus_03_Name;': { 64 | 'name': 'Osseus Spiralis', 65 | 'value': 2404700, 66 | 'rulesets': [ 67 | { 68 | 'atmosphere': ['Ammonia'], 69 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 70 | 'min_gravity': 0.04, 71 | 'max_gravity': 0.276, 72 | 'min_temperature': 160.0, 73 | 'max_temperature': 177.0, 74 | 'max_pressure': 0.0135 75 | } 76 | ], 77 | }, 78 | '$Codex_Ent_Osseus_04_Name;': { 79 | 'name': 'Osseus Pumice', 80 | 'value': 3156300, 81 | 'rulesets': [ 82 | { 83 | 'atmosphere': ['Argon'], 84 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 85 | 'min_gravity': 0.059, 86 | 'max_gravity': 0.276, 87 | 'min_temperature': 50.0, 88 | 'max_temperature': 135.0, 89 | 'volcanism': 'None' 90 | }, 91 | { 92 | 'atmosphere': ['Argon'], 93 | 'body_type': ['Rocky ice body'], 94 | 'min_gravity': 0.059, 95 | 'max_gravity': 0.276, 96 | 'min_temperature': 50.0, 97 | 'max_temperature': 135.0, 98 | 'volcanism': ['water', 'geysers'] 99 | }, 100 | { 101 | 'atmosphere': ['ArgonRich'], 102 | 'body_type': ['Rocky ice body'], 103 | 'min_gravity': 0.035, 104 | 'max_gravity': 0.276, 105 | 'min_temperature': 60.0, 106 | 'max_temperature': 80.5, 107 | 'min_pressure': 0.03, 108 | 'volcanism': 'None' 109 | }, 110 | { 111 | 'atmosphere': ['Methane'], 112 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 113 | 'min_gravity': 0.033, 114 | 'max_gravity': 0.276, 115 | 'min_temperature': 67.0, 116 | 'max_temperature': 109.0 117 | }, 118 | { 119 | 'atmosphere': ['Nitrogen'], 120 | 'body_type': ['Rocky body', 'Rocky ice body', 'High metal content body'], 121 | 'min_gravity': 0.05, 122 | 'max_gravity': 0.276, 123 | 'min_temperature': 42.0, 124 | 'max_temperature': 70.1, 125 | 'volcanism': 'None' 126 | } 127 | ], 128 | }, 129 | '$Codex_Ent_Osseus_05_Name;': { 130 | 'name': 'Osseus Cornibus', 131 | 'value': 1483000, 132 | 'rulesets': [ 133 | { 134 | 'atmosphere': ['CarbonDioxide'], 135 | 'body_type': ['Rocky body', 'High metal content body'], 136 | 'min_gravity': 0.0405, 137 | 'max_gravity': 0.276, 138 | 'min_temperature': 180.0, 139 | 'max_temperature': 197.0, 140 | 'min_pressure': 0.025, 141 | 'volcanism': 'None', 142 | 'regions': ['perseus'] 143 | } 144 | ], 145 | }, 146 | '$Codex_Ent_Osseus_06_Name;': { 147 | 'name': 'Osseus Pellebantus', 148 | 'value': 9739000, 149 | 'rulesets': [ 150 | { 151 | 'atmosphere': ['CarbonDioxide'], 152 | 'body_type': ['Rocky body', 'High metal content body'], 153 | 'min_gravity': 0.0405, 154 | 'max_gravity': 0.276, 155 | 'min_temperature': 191.0, 156 | 'max_temperature': 197.0, 157 | 'min_pressure': 0.057, 158 | 'volcanism': 'None', 159 | 'regions': ['!perseus'] 160 | } 161 | ], 162 | }, 163 | }, 164 | } 165 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/recepta.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Recepta_Genus_Name;': { 5 | '$Codex_Ent_Recepta_01_Name;': { 6 | 'name': 'Recepta Umbrux', 7 | 'value': 12934900, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['CarbonDioxide'], 11 | 'min_gravity': 0.04, 12 | 'max_gravity': 0.276, 13 | 'min_temperature': 151.0, 14 | 'max_temperature': 200.0, 15 | 'atmosphere_component': {'SulphurDioxide': 1.05} 16 | }, 17 | { 18 | 'atmosphere': ['Oxygen'], 19 | 'body_type': ['Icy body'], 20 | 'min_gravity': 0.23, 21 | 'max_gravity': 0.276, 22 | 'min_temperature': 154.0, 23 | 'max_temperature': 175.0, 24 | 'min_pressure': 0.01, 25 | 'volcanism': 'None', 26 | 'atmosphere_component': {'SulphurDioxide': 1.05} 27 | }, 28 | { 29 | 'atmosphere': ['Oxygen'], 30 | 'body_type': ['Icy body'], 31 | 'min_gravity': 0.23, 32 | 'max_gravity': 0.276, 33 | 'min_temperature': 154.0, 34 | 'max_temperature': 175.0, 35 | 'min_pressure': 0.01, 36 | 'volcanism': ['water'], 37 | 'atmosphere_component': {'SulphurDioxide': 1.05} 38 | }, 39 | { 40 | 'atmosphere': ['SulphurDioxide'], 41 | 'min_gravity': 0.04, 42 | 'max_gravity': 0.276, 43 | 'min_temperature': 132.0, 44 | 'max_temperature': 273.0, 45 | 'atmosphere_component': {'SulphurDioxide': 1.05} 46 | } 47 | ], 48 | }, 49 | '$Codex_Ent_Recepta_02_Name;': { 50 | 'name': 'Recepta Deltahedronix', 51 | 'value': 16202800, 52 | 'rulesets': [ 53 | { 54 | 'atmosphere': ['CarbonDioxide'], 55 | 'min_gravity': 0.04, 56 | 'max_gravity': 0.276, 57 | 'min_temperature': 150.0, 58 | 'max_temperature': 195.0, 59 | 'volcanism': 'None', 60 | 'atmosphere_component': {'SulphurDioxide': 1.05} 61 | }, 62 | { 63 | 'atmosphere': ['CarbonDioxide'], 64 | 'body_type': ['Icy body', 'Rocky ice body'], 65 | 'min_gravity': 0.04, 66 | 'max_gravity': 0.276, 67 | 'min_temperature': 150.0, 68 | 'max_temperature': 195.0, 69 | 'volcanism': ['water'], 70 | 'atmosphere_component': {'SulphurDioxide': 1.05} 71 | }, 72 | { 73 | 'atmosphere': ['SulphurDioxide'], 74 | 'min_gravity': 0.04, 75 | 'max_gravity': 0.276, 76 | 'min_temperature': 132.0, 77 | 'max_temperature': 272.0, 78 | 'atmosphere_component': {'SulphurDioxide': 1.05} 79 | } 80 | ], 81 | }, 82 | '$Codex_Ent_Recepta_03_Name;': { 83 | 'name': 'Recepta Conditivus', 84 | 'value': 14313700, 85 | 'rulesets': [ 86 | { 87 | 'atmosphere': ['CarbonDioxide', 'CarbonDioxideRich'], 88 | 'body_type': ['Icy body', 'Rocky body', 'High metal content body'], 89 | 'min_gravity': 0.04, 90 | 'max_gravity': 0.276, 91 | 'min_temperature': 150.0, 92 | 'max_temperature': 195.0, 93 | 'volcanism': 'None', 94 | 'atmosphere_component': {'SulphurDioxide': 1.05} 95 | }, 96 | { 97 | 'atmosphere': ['Oxygen'], 98 | 'body_type': ['Icy body'], 99 | 'min_gravity': 0.23, 100 | 'max_gravity': 0.276, 101 | 'min_temperature': 154.0, 102 | 'max_temperature': 175.0, 103 | 'min_pressure': 0.01, 104 | 'volcanism': 'None', 105 | 'atmosphere_component': {'SulphurDioxide': 1.05} 106 | }, 107 | { 108 | 'atmosphere': ['Oxygen'], 109 | 'body_type': ['Icy body'], 110 | 'min_gravity': 0.23, 111 | 'max_gravity': 0.276, 112 | 'min_temperature': 154.0, 113 | 'max_temperature': 175.0, 114 | 'min_pressure': 0.01, 115 | 'volcanism': ['water'], 116 | 'atmosphere_component': {'SulphurDioxide': 1.05} 117 | }, 118 | { 119 | 'atmosphere': ['SulphurDioxide'], 120 | 'min_gravity': 0.04, 121 | 'max_gravity': 0.276, 122 | 'min_temperature': 132.0, 123 | 'max_temperature': 275.0, 124 | 'atmosphere_component': {'SulphurDioxide': 1.05} 125 | } 126 | ], 127 | }, 128 | }, 129 | } 130 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/shard.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Ground_Struct_Ice_Name;': { 5 | '$Codex_Ent_Ground_Struct_Ice_Name;': { 6 | 'name': 'Crystalline Shards', 7 | 'value': 1628800, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['None', 'Argon', 'ArgonRich', 'CarbonDioxide', 'CarbonDioxideRich', 11 | 'Helium', 'Methane', 'Neon', 'NeonRich'], 12 | 'max_gravity': 2.0, 13 | 'max_temperature': 273.0, 14 | 'star': ['A', 'F', 'G', 'K', 'MS', 'S'], 15 | 'distance': 12000.0, 16 | 'bodies': ['Earthlike body', 'Ammonia world', 'Water world', 'Gas giant with water based life', 17 | 'Gas giant with ammonia based life', 'Water giant'], 18 | 'regions': ['exterior'] 19 | } 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/stratum.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Stratum_04_Name;': { 5 | '$Codex_Ent_Stratum_04_Name;': { 6 | 'name': 'Stratum Aranaemus', 7 | 'value': 2448900, 8 | 'rulesets': [] 9 | } 10 | }, 11 | '$Codex_Ent_Stratum_Genus_Name;': { 12 | '$Codex_Ent_Stratum_01_Name;': { 13 | 'name': 'Stratum Excutitus', 14 | 'value': 2448900, 15 | 'rulesets': [ 16 | { 17 | 'atmosphere': ['CarbonDioxide'], 18 | 'min_gravity': 0.04, 19 | 'max_gravity': 0.48, 20 | 'min_temperature': 165.0, 21 | 'max_temperature': 190.0, 22 | 'min_pressure': 0.0035, 23 | 'body_type': ['Rocky body'], 24 | 'volcanism': 'None', 25 | 'regions': ['orion-cygnus'] 26 | }, 27 | { 28 | 'atmosphere': ['SulphurDioxide'], 29 | 'min_gravity': 0.27, 30 | 'max_gravity': 0.4, 31 | 'min_temperature': 165.0, 32 | 'max_temperature': 190.0, 33 | 'body_type': ['Rocky body'], 34 | 'regions': ['orion-cygnus'] 35 | } 36 | ], 37 | }, 38 | '$Codex_Ent_Stratum_02_Name;': { 39 | 'name': 'Stratum Paleas', 40 | 'value': 1362000, 41 | 'rulesets': [ 42 | { 43 | 'atmosphere': ['Ammonia'], 44 | 'min_gravity': 0.04, 45 | 'max_gravity': 0.35, 46 | 'min_temperature': 165.0, 47 | 'max_temperature': 177.0, 48 | 'max_pressure': 0.0135, 49 | 'body_type': ['Rocky body'] 50 | }, 51 | { 52 | 'atmosphere': ['CarbonDioxide'], 53 | 'min_gravity': 0.04, 54 | 'max_gravity': 0.585, 55 | 'min_temperature': 165.0, 56 | 'max_temperature': 395.0, 57 | 'body_type': ['Rocky body'], 58 | 'volcanism': 'None' 59 | }, 60 | { 61 | 'atmosphere': ['CarbonDioxideRich'], 62 | 'min_gravity': 0.43, 63 | 'max_gravity': 0.585, 64 | 'min_temperature': 185.0, 65 | 'max_temperature': 260.0, 66 | 'min_pressure': 0.015, 67 | 'body_type': ['Rocky body'], 68 | 'volcanism': 'None' 69 | }, 70 | { 71 | 'atmosphere': ['Water'], 72 | 'min_gravity': 0.04, 73 | 'max_gravity': 0.056, 74 | 'body_type': ['Rocky body'], 75 | 'volcanism': 'None' 76 | }, 77 | { 78 | 'atmosphere': ['Water'], 79 | 'min_gravity': 0.04, 80 | 'max_gravity': 0.056, 81 | 'min_pressure': 0.065, 82 | 'body_type': ['Rocky body'], 83 | 'volcanism': ['water'] 84 | }, 85 | { 86 | 'atmosphere': ['Oxygen'], 87 | 'min_gravity': 0.39, 88 | 'max_gravity': 0.59, 89 | 'min_temperature': 165.0, 90 | 'max_temperature': 250.0, 91 | 'min_pressure': 0.022, 92 | 'body_type': ['Rocky body'] 93 | } 94 | ], 95 | }, 96 | '$Codex_Ent_Stratum_03_Name;': { 97 | 'name': 'Stratum Laminamus', 98 | 'value': 2788300, 99 | 'rulesets': [ 100 | { 101 | 'atmosphere': ['Ammonia'], 102 | 'min_gravity': 0.04, 103 | 'max_gravity': 0.34, 104 | 'min_temperature': 165.0, 105 | 'max_temperature': 177.0, 106 | 'max_pressure': 0.0135, 107 | 'body_type': ['Rocky body'], 108 | 'regions': ['orion-cygnus'] 109 | } 110 | ], 111 | }, 112 | '$Codex_Ent_Stratum_04_Name;': { 113 | 'name': 'Stratum Araneamus', 114 | 'value': 2448900, 115 | 'rulesets': [ 116 | { 117 | 'atmosphere': ['SulphurDioxide'], 118 | 'min_gravity': 0.26, 119 | 'max_gravity': 0.57, 120 | 'min_temperature': 165.0, 121 | 'max_temperature': 373.0, 122 | 'body_type': ['Rocky body'] 123 | } 124 | ], 125 | }, 126 | '$Codex_Ent_Stratum_05_Name;': { 127 | 'name': 'Stratum Limaxus', 128 | 'value': 1362000, 129 | 'rulesets': [ 130 | { 131 | 'atmosphere': ['CarbonDioxide'], 132 | 'min_gravity': 0.03, 133 | 'max_gravity': 0.4, 134 | 'min_temperature': 165.0, 135 | 'max_temperature': 190.0, 136 | 'min_pressure': 0.05, 137 | 'body_type': ['Rocky body'], 138 | 'volcanism': 'None', 139 | 'regions': ['scutum-centaurus-core'] 140 | }, 141 | { 142 | 'atmosphere': ['SulphurDioxide'], 143 | 'min_gravity': 0.27, 144 | 'max_gravity': 0.4, 145 | 'min_temperature': 165.0, 146 | 'max_temperature': 190.0, 147 | 'body_type': ['Rocky body'], 148 | 'regions': ['scutum-centaurus-core'] 149 | } 150 | ], 151 | }, 152 | '$Codex_Ent_Stratum_06_Name;': { 153 | 'name': 'Stratum Cucumisis', 154 | 'value': 16202800, 155 | 'rulesets': [ 156 | { 157 | 'atmosphere': ['CarbonDioxide'], 158 | 'min_gravity': 0.04, 159 | 'max_gravity': 0.6, 160 | 'min_temperature': 191.0, 161 | 'max_temperature': 371.0, 162 | 'body_type': ['Rocky body'], 163 | 'volcanism': 'None', 164 | 'regions': ['sagittarius-carina'] 165 | }, 166 | { 167 | 'atmosphere': ['CarbonDioxideRich'], 168 | 'min_gravity': 0.44, 169 | 'max_gravity': 0.56, 170 | 'min_temperature': 210.0, 171 | 'max_temperature': 246.0, 172 | 'min_pressure': 0.01, 173 | 'body_type': ['Rocky body'], 174 | 'volcanism': 'None', 175 | 'regions': ['sagittarius-carina'] 176 | }, 177 | { 178 | 'atmosphere': ['Oxygen'], 179 | 'min_gravity': 0.4, 180 | 'max_gravity': 0.6, 181 | 'min_temperature': 200.0, 182 | 'max_temperature': 250.0, 183 | 'min_pressure': 0.01, 184 | 'body_type': ['Rocky body'], 185 | 'regions': ['sagittarius-carina'] 186 | }, 187 | { 188 | 'atmosphere': ['SulphurDioxide'], 189 | 'min_gravity': 0.26, 190 | 'max_gravity': 0.55, 191 | 'min_temperature': 191.0, 192 | 'max_temperature': 373.0, 193 | 'body_type': ['Rocky body'], 194 | 'regions': ['sagittarius-carina'] 195 | } 196 | ], 197 | }, 198 | '$Codex_Ent_Stratum_07_Name;': { 199 | 'name': 'Stratum Tectonicas', 200 | 'value': 19010800, 201 | 'rulesets': [ 202 | { 203 | 'atmosphere': ['Ammonia'], 204 | 'min_gravity': 0.045, 205 | 'max_gravity': 0.38, 206 | 'min_temperature': 165.0, 207 | 'max_temperature': 177.0, 208 | 'body_type': ['High metal content body'] 209 | }, 210 | { 211 | 'atmosphere': ['Argon', 'ArgonRich'], 212 | 'min_gravity': 0.485, 213 | 'max_gravity': 0.54, 214 | 'min_temperature': 167.0, 215 | 'max_temperature': 199.0, 216 | 'body_type': ['High metal content body'], 217 | 'volcanism': 'None' 218 | }, 219 | { 220 | 'atmosphere': ['CarbonDioxide'], 221 | 'min_gravity': 0.045, 222 | 'max_gravity': 0.61, 223 | 'min_temperature': 165.0, 224 | 'max_temperature': 430.0, 225 | 'body_type': ['High metal content body'] 226 | }, 227 | { 228 | 'atmosphere': ['CarbonDioxideRich'], 229 | 'min_gravity': 0.035, 230 | 'max_gravity': 0.61, 231 | 'min_temperature': 165.0, 232 | 'max_temperature': 260.0, 233 | 'body_type': ['High metal content body'] 234 | }, 235 | { 236 | 'atmosphere': ['Oxygen'], 237 | 'min_gravity': 0.4, 238 | 'max_gravity': 0.52, 239 | 'min_temperature': 165.0, 240 | 'max_temperature': 246.0, 241 | 'body_type': ['High metal content body'] 242 | }, 243 | { 244 | 'atmosphere': ['SulphurDioxide'], 245 | 'min_gravity': 0.29, 246 | 'max_gravity': 0.62, 247 | 'min_temperature': 165.0, 248 | 'max_temperature': 450.0, 249 | 'body_type': ['High metal content body'] 250 | }, 251 | { 252 | 'atmosphere': ['Water'], 253 | 'min_gravity': 0.045, 254 | 'max_gravity': 0.063, 255 | 'body_type': ['High metal content body'], 256 | 'volcanism': 'None' 257 | }, 258 | ], 259 | }, 260 | '$Codex_Ent_Stratum_08_Name;': { 261 | 'name': 'Stratum Frigus', 262 | 'value': 2637500, 263 | 'rulesets': [ 264 | { 265 | 'atmosphere': ['CarbonDioxide'], 266 | 'min_gravity': 0.043, 267 | 'max_gravity': 0.54, 268 | 'min_temperature': 191.0, 269 | 'max_temperature': 365.0, 270 | 'min_pressure': 0.001, 271 | 'body_type': ['Rocky body'], 272 | 'volcanism': 'None', 273 | 'regions': ['perseus-core'] 274 | }, 275 | { 276 | 'atmosphere': ['CarbonDioxideRich'], 277 | 'min_gravity': 0.45, 278 | 'max_gravity': 0.56, 279 | 'min_temperature': 200.0, 280 | 'max_temperature': 250.0, 281 | 'min_pressure': 0.01, 282 | 'body_type': ['Rocky body'], 283 | 'volcanism': 'None', 284 | 'regions': ['perseus-core'] 285 | }, 286 | { 287 | 'atmosphere': ['SulphurDioxide'], 288 | 'min_gravity': 0.29, 289 | 'max_gravity': 0.52, 290 | 'min_temperature': 191.0, 291 | 'max_temperature': 369.0, 292 | 'body_type': ['Rocky body'], 293 | 'regions': ['perseus-core'] 294 | } 295 | ], 296 | }, 297 | }, 298 | } 299 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/tubers.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Tube_Name;': { 5 | '$Codex_Ent_Tube_Name;': { 6 | 'name': 'Roseum Sinuous Tubers', 7 | 'value': 1514500, 8 | 'rulesets': [ 9 | { 10 | 'body_type': ['High metal content body'], 11 | 'min_temperature': 200.0, 12 | 'max_temperature': 500.0, 13 | 'volcanism': ['rocky magma'], 14 | 'tuber': ['Galactic Center', 'Odin A', 'Ryker B'] 15 | } 16 | ], 17 | }, 18 | '$Codex_Ent_TubeABCD_01_Name;': { 19 | 'name': 'Prasinum Sinuous Tubers', 20 | 'value': 1514500, 21 | 'rulesets': [ 22 | { 23 | 'body_type': ['Metal rich body', 'High metal content body', 'Rocky body'], 24 | 'min_temperature': 200.0, 25 | 'max_temperature': 500.0, 26 | 'volcanism': 'Any', 27 | 'tuber': ['Inner S-C Arm B 1'] 28 | }, 29 | { 30 | 'body_type': ['Metal rich body', 'High metal content body'], 31 | 'min_temperature': 200.0, 32 | 'max_temperature': 500.0, 33 | 'volcanism': ['major rocky magma', 'major silicate vapour'], 34 | 'tuber': ['Inner S-C Arm D', 'Norma Expanse B', 'Odin B'] 35 | }, 36 | { 37 | 'body_type': ['Metal rich body', 'High metal content body'], 38 | 'min_temperature': 200.0, 39 | 'max_temperature': 500.0, 40 | 'volcanism': ['major rocky magma', 'major silicate vapour'], 41 | 'regions': ['empyrean-straits'] 42 | } 43 | ], 44 | }, 45 | '$Codex_Ent_TubeABCD_02_Name;': { # High % sulphur requirement? 46 | 'name': 'Albidum Sinuous Tubers', 47 | 'value': 1514500, 48 | 'rulesets': [ 49 | { 50 | 'body_type': ['Rocky body'], 51 | 'min_temperature': 200.0, 52 | 'max_temperature': 500.0, 53 | 'max_orbital_period': 86400, 54 | 'volcanism': ['major silicate vapour', 'major metallic magma'], 55 | 'tuber': ['Inner S-C Arm B 2', 'Inner S-C Arm D', 'Trojan Belt'] 56 | } 57 | ], 58 | }, 59 | '$Codex_Ent_TubeABCD_03_Name;': { 60 | 'name': 'Caeruleum Sinuous Tubers', 61 | 'value': 1514500, 62 | 'rulesets': [ 63 | { 64 | 'body_type': ['Rocky body'], 65 | 'min_temperature': 200.0, 66 | 'max_temperature': 500.0, 67 | 'max_orbital_period': 86400, 68 | 'volcanism': ['major silicate vapour'], 69 | 'tuber': ['Galactic Center', 'Inner S-C Arm D', 'Norma Arm A'] 70 | }, 71 | { 72 | 'body_type': ['Rocky body'], 73 | 'min_temperature': 200.0, 74 | 'max_temperature': 500.0, 75 | 'volcanism': ['major silicate vapour'], 76 | 'regions': ['empyrean-straits'] 77 | } 78 | ], 79 | }, 80 | '$Codex_Ent_TubeEFGH_01_Name;': { 81 | 'name': 'Lindigoticum Sinuous Tubers', 82 | 'value': 1514500, 83 | 'rulesets': [ 84 | { 85 | 'body_type': ['Rocky body'], 86 | 'min_temperature': 200.0, 87 | 'max_temperature': 500.0, 88 | 'max_orbital_period': 86400, 89 | 'volcanism': ['major silicate vapour'], 90 | 'tuber': ['Inner S-C Arm A', 'Inner S-C Arm C', 'Hawking B', 'Norma Expanse A', 'Odin B'] 91 | } 92 | ], 93 | }, 94 | '$Codex_Ent_TubeEFGH_02_Name;': { 95 | 'name': 'Violaceum Sinuous Tubers', 96 | 'value': 1514500, 97 | 'rulesets': [ 98 | { 99 | 'body_type': ['Metal rich body', 'High metal content body'], 100 | 'min_temperature': 200.0, 101 | 'max_temperature': 500.0, 102 | 'volcanism': ['major rocky magma', 'major silicate vapour'], 103 | 'tuber': ['Arcadian Stream', 'Empyrean Straits', 'Norma Arm B'] 104 | } 105 | ], 106 | }, 107 | '$Codex_Ent_TubeEFGH_03_Name;': { 108 | 'name': 'Viride Sinuous Tubers', 109 | 'value': 1514500, 110 | 'rulesets': [ 111 | { 112 | 'body_type': ['High metal content body'], 113 | 'min_temperature': 200.0, 114 | 'max_temperature': 500.0, 115 | 'volcanism': ['major rocky magma', 'major silicate vapour'], 116 | 'tuber': ['Inner O-P Conflux', 'Izanami', 'Ryker A'] 117 | }, 118 | { 119 | 'body_type': ['Rocky body'], 120 | 'min_temperature': 200.0, 121 | 'max_temperature': 500.0, 122 | 'max_orbital_period': 86400, 123 | 'volcanism': ['major rocky magma', 'major silicate vapour'], 124 | 'tuber': ['Inner O-P Conflux', 'Izanami', 'Ryker A'] 125 | } 126 | ], 127 | }, 128 | '$Codex_Ent_TubeEFGH_Name;': { 129 | 'name': 'Blatteum Sinuous Tubers', 130 | 'value': 1514500, 131 | 'rulesets': [ 132 | { 133 | 'body_type': ['Metal rich body', 'High metal content body'], 134 | 'min_temperature': 200.0, 135 | 'max_temperature': 500.0, 136 | 'volcanism': ['=metallic magma volcanism', '=rocky magma volcanism', 'major silicate vapour'], 137 | 'tuber': ['Arcadian Stream', 'Inner Orion Spur', 'Inner S-C Arm B 2', 'Hawking A'] 138 | } 139 | ], 140 | }, 141 | }, 142 | } 143 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/tubus.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Tubus_Genus_Name;': { 5 | '$Codex_Ent_Tubus_01_Name;': { 6 | 'name': 'Tubus Conifer', 7 | 'value': 2415500, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['CarbonDioxide'], 11 | 'body_type': ['Rocky body'], 12 | 'min_gravity': 0.041, 13 | 'max_gravity': 0.153, 14 | 'min_temperature': 160.0, 15 | 'max_temperature': 197.0, 16 | 'min_pressure': 0.003, 17 | 'volcanism': 'None', 18 | 'regions': ['perseus'] 19 | }, 20 | ], 21 | }, 22 | '$Codex_Ent_Tubus_02_Name;': { 23 | 'name': 'Tubus Sororibus', 24 | 'value': 5727600, 25 | 'rulesets': [ 26 | { 27 | 'atmosphere': ['Ammonia'], 28 | 'body_type': ['High metal content body'], 29 | 'min_gravity': 0.045, 30 | 'max_gravity': 0.152, 31 | 'min_temperature': 160.0, 32 | 'max_temperature': 177.0, 33 | 'max_pressure': 0.0135, 34 | }, 35 | { 36 | 'atmosphere': ['CarbonDioxide'], 37 | 'body_type': ['High metal content body'], 38 | 'min_gravity': 0.045, 39 | 'max_gravity': 0.152, 40 | 'min_temperature': 160.0, 41 | 'max_temperature': 195.0, 42 | 'volcanism': 'None' 43 | } 44 | ], 45 | }, 46 | '$Codex_Ent_Tubus_03_Name;': { 47 | 'name': 'Tubus Cavas', 48 | 'value': 11873200, 49 | 'rulesets': [ 50 | { 51 | 'body_type': ['Rocky body'], 52 | 'atmosphere': ['CarbonDioxide'], 53 | 'min_gravity': 0.04, 54 | 'max_gravity': 0.152, 55 | 'min_temperature': 160.0, 56 | 'max_temperature': 197.0, 57 | 'min_pressure': 0.003, 58 | 'volcanism': 'None', 59 | 'regions': ['scutum-centaurus'] 60 | }, 61 | ], 62 | }, 63 | '$Codex_Ent_Tubus_04_Name;': { 64 | 'name': 'Tubus Rosarium', 65 | 'value': 2637500, 66 | 'rulesets': [ 67 | { 68 | 'atmosphere': ['Ammonia'], 69 | 'body_type': ['Rocky body'], 70 | 'min_gravity': 0.04, 71 | 'max_gravity': 0.153, 72 | 'min_temperature': 160.0, 73 | 'max_temperature': 177.0, 74 | 'max_pressure': 0.0135 75 | }, 76 | ], 77 | }, 78 | '$Codex_Ent_Tubus_05_Name;': { 79 | 'name': 'Tubus Compagibus', 80 | 'value': 7774700, 81 | 'rulesets': [ 82 | { 83 | 'atmosphere': ['CarbonDioxide'], 84 | 'body_type': ['Rocky body'], 85 | 'min_gravity': 0.04, 86 | 'max_gravity': 0.153, 87 | 'min_temperature': 160.0, 88 | 'max_temperature': 197.0, 89 | 'min_pressure': 0.003, 90 | 'volcanism': 'None', 91 | 'regions': ['sagittarius-carina'] 92 | }, 93 | ], 94 | }, 95 | }, 96 | } 97 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/rulesets/tussock.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | 3 | catalog: dict[str, dict[str, Mapping]] = { 4 | '$Codex_Ent_Tussocks_Genus_Name;': { 5 | '$Codex_Ent_Tussocks_01_Name;': { 6 | 'name': 'Tussock Pennata', 7 | 'value': 5853800, 8 | 'rulesets': [ 9 | { 10 | 'atmosphere': ['CarbonDioxide'], 11 | 'body_type': ['Rocky body', 'High metal content body'], 12 | 'min_gravity': 0.04, 13 | 'max_gravity': 0.09, 14 | 'min_temperature': 146.0, 15 | 'max_temperature': 154.0, 16 | 'min_pressure': 0.00289, 17 | 'volcanism': 'None', 18 | 'regions': ['sagittarius-carina-core-9', 'perseus-core', 'orion-cygnus-core'] 19 | } 20 | ], 21 | }, 22 | '$Codex_Ent_Tussocks_02_Name;': { 23 | 'name': 'Tussock Ventusa', 24 | 'value': 3227700, 25 | 'rulesets': [ 26 | { 27 | 'atmosphere': ['CarbonDioxide'], 28 | 'body_type': ['Rocky body', 'High metal content body'], 29 | 'min_gravity': 0.04, 30 | 'max_gravity': 0.13, 31 | 'min_temperature': 155.0, 32 | 'max_temperature': 160.0, 33 | 'min_pressure': 0.00289, 34 | 'volcanism': 'None', 35 | 'regions': ['sagittarius-carina-core-9', 'perseus-core', 'orion-cygnus-core'] 36 | } 37 | ], 38 | }, 39 | '$Codex_Ent_Tussocks_03_Name;': { 40 | 'name': 'Tussock Ignis', 41 | 'value': 1849000, 42 | 'rulesets': [ 43 | { 44 | 'atmosphere': ['CarbonDioxide'], 45 | 'body_type': ['Rocky body', 'High metal content body'], 46 | 'min_gravity': 0.04, 47 | 'max_gravity': 0.2, 48 | 'min_temperature': 161.0, 49 | 'max_temperature': 170.0, 50 | 'min_pressure': 0.00289, 51 | 'volcanism': 'None', 52 | 'regions': ['sagittarius-carina-core-9', 'perseus-core', 'orion-cygnus-core'] 53 | } 54 | ], 55 | }, 56 | '$Codex_Ent_Tussocks_04_Name;': { 57 | 'name': 'Tussock Cultro', 58 | 'value': 1766600, 59 | 'rulesets': [ 60 | { 61 | 'atmosphere': ['Ammonia'], 62 | 'body_type': ['Rocky body', 'High metal content body'], 63 | 'min_gravity': 0.04, 64 | 'max_gravity': 0.276, 65 | 'min_temperature': 152.0, 66 | 'max_temperature': 177.0, 67 | 'max_pressure': 0.0135, 68 | 'regions': ['orion-cygnus'] 69 | } 70 | ], 71 | }, 72 | '$Codex_Ent_Tussocks_05_Name;': { 73 | 'name': 'Tussock Catena', 74 | 'value': 1766600, 75 | 'rulesets': [ 76 | { 77 | 'atmosphere': ['Ammonia'], 78 | 'body_type': ['Rocky body', 'High metal content body'], 79 | 'min_gravity': 0.04, 80 | 'max_gravity': 0.276, 81 | 'min_temperature': 152.0, 82 | 'max_temperature': 177.0, 83 | 'max_pressure': 0.0135, 84 | 'regions': ['scutum-centaurus-core'] 85 | } 86 | ], 87 | }, 88 | '$Codex_Ent_Tussocks_06_Name;': { 89 | 'name': 'Tussock Pennatis', 90 | 'value': 1000000, 91 | 'rulesets': [ 92 | { 93 | 'atmosphere': ['CarbonDioxide'], 94 | 'body_type': ['Rocky body', 'High metal content body'], 95 | 'min_gravity': 0.04, 96 | 'max_gravity': 0.276, 97 | 'min_temperature': 147.0, 98 | 'max_temperature': 197.0, 99 | 'min_pressure': 0.00289, 100 | 'volcanism': 'None', 101 | 'regions': ['outer'] 102 | } 103 | ], 104 | }, 105 | '$Codex_Ent_Tussocks_07_Name;': { 106 | 'name': 'Tussock Serrati', 107 | 'value': 4447100, 108 | 'rulesets': [ 109 | { 110 | 'atmosphere': ['CarbonDioxide'], 111 | 'body_type': ['Rocky body', 'High metal content body'], 112 | 'min_gravity': 0.042, 113 | 'max_gravity': 0.23, 114 | 'min_temperature': 171.0, 115 | 'max_temperature': 174.0, 116 | 'min_pressure': 0.01, 117 | 'max_pressure': 0.071, 118 | 'volcanism': 'None', 119 | 'regions': ['sagittarius-carina-core-9', 'perseus-core', 'orion-cygnus-core'] 120 | } 121 | ], 122 | }, 123 | '$Codex_Ent_Tussocks_08_Name;': { 124 | 'name': 'Tussock Albata', 125 | 'value': 3252500, 126 | 'rulesets': [ 127 | { 128 | 'atmosphere': ['CarbonDioxide'], 129 | 'body_type': ['Rocky body', 'High metal content body'], 130 | 'min_gravity': 0.042, 131 | 'max_gravity': 0.276, 132 | 'min_temperature': 175.0, 133 | 'max_temperature': 180.0, 134 | 'min_pressure': 0.016, 135 | 'volcanism': 'None', 136 | 'regions': ['sagittarius-carina-core-9', 'perseus-core', 'orion-cygnus-core'] 137 | } 138 | ], 139 | }, 140 | '$Codex_Ent_Tussocks_09_Name;': { 141 | 'name': 'Tussock Propagito', 142 | 'value': 1000000, 143 | 'rulesets': [ 144 | { 145 | 'atmosphere': ['CarbonDioxide'], 146 | 'body_type': ['Rocky body', 'High metal content body'], 147 | 'min_gravity': 0.04, 148 | 'max_gravity': 0.276, 149 | 'min_temperature': 145.0, 150 | 'max_temperature': 197.0, 151 | 'min_pressure': 0.00289, 152 | 'volcanism': 'None', 153 | 'regions': ['scutum-centaurus'] 154 | } 155 | ], 156 | }, 157 | '$Codex_Ent_Tussocks_10_Name;': { 158 | 'name': 'Tussock Divisa', 159 | 'value': 1766600, 160 | 'rulesets': [ 161 | { 162 | 'atmosphere': ['Ammonia'], 163 | 'body_type': ['Rocky body', 'High metal content body'], 164 | 'min_gravity': 0.042, 165 | 'max_gravity': 0.276, 166 | 'min_temperature': 152.0, 167 | 'max_temperature': 177.0, 168 | 'max_pressure': 0.0135, 169 | 'regions': ['perseus-core'] 170 | } 171 | ], 172 | }, 173 | '$Codex_Ent_Tussocks_11_Name;': { 174 | 'name': 'Tussock Caputus', 175 | 'value': 3472400, 176 | 'rulesets': [ 177 | { 178 | 'atmosphere': ['CarbonDioxide'], 179 | 'body_type': ['Rocky body', 'High metal content body'], 180 | 'min_gravity': 0.041, 181 | 'max_gravity': 0.27, 182 | 'min_temperature': 181.0, 183 | 'max_temperature': 190.0, 184 | 'min_pressure': 0.0275, 185 | 'volcanism': 'None', 186 | 'regions': ['sagittarius-carina-core-9', 'perseus-core', 'orion-cygnus-core'] 187 | } 188 | ], 189 | }, 190 | '$Codex_Ent_Tussocks_12_Name;': { 191 | 'name': 'Tussock Triticum', 192 | 'value': 7774700, 193 | 'rulesets': [ 194 | { 195 | 'atmosphere': ['CarbonDioxide'], 196 | 'body_type': ['Rocky body', 'High metal content body'], 197 | 'min_gravity': 0.04, 198 | 'max_gravity': 0.276, 199 | 'min_temperature': 191.0, 200 | 'max_temperature': 197.0, 201 | 'min_pressure': 0.058, 202 | 'volcanism': 'None', 203 | 'regions': ['sagittarius-carina-core-9', 'perseus-core', 'orion-cygnus-core'] 204 | } 205 | ], 206 | }, 207 | '$Codex_Ent_Tussocks_13_Name;': { 208 | 'name': 'Tussock Stigmasis', 209 | 'value': 19010800, 210 | 'rulesets': [ 211 | { 212 | 'atmosphere': ['SulphurDioxide'], 213 | 'body_type': ['Rocky body', 'High metal content body'], 214 | 'min_gravity': 0.04, 215 | 'max_gravity': 0.276, 216 | 'min_temperature': 132.0, 217 | 'max_temperature': 180.0, 218 | 'max_pressure': 0.01 219 | } 220 | ], 221 | }, 222 | '$Codex_Ent_Tussocks_14_Name;': { 223 | 'name': 'Tussock Virgam', 224 | 'value': 14313700, 225 | 'rulesets': [ 226 | { 227 | 'atmosphere': ['Water'], 228 | 'body_type': ['Rocky body', 'High metal content body'], 229 | 'min_gravity': 0.04, 230 | 'max_gravity': 0.065, 231 | 'volcanism': 'None', 232 | }, 233 | { 234 | 'atmosphere': ['Water'], 235 | 'body_type': ['Rocky body', 'High metal content body'], 236 | 'min_gravity': 0.04, 237 | 'max_gravity': 0.065, 238 | 'volcanism': ['water'], 239 | } 240 | ], 241 | }, 242 | '$Codex_Ent_Tussocks_15_Name;': { 243 | 'name': 'Tussock Capillum', 244 | 'value': 7025800, 245 | 'rulesets': [ 246 | { 247 | 'atmosphere': ['Argon'], 248 | 'min_gravity': 0.22, 249 | 'max_gravity': 0.276, 250 | 'min_temperature': 80.0, 251 | 'max_temperature': 129.0, 252 | 'body_type': ['Rocky ice body'] 253 | }, 254 | { 255 | 'atmosphere': ['Methane'], 256 | 'min_gravity': 0.033, 257 | 'max_gravity': 0.276, 258 | 'min_temperature': 80.0, 259 | 'max_temperature': 110.0, 260 | 'body_type': ['Rocky body', 'Rocky ice body'] 261 | } 262 | ], 263 | }, 264 | }, 265 | } 266 | -------------------------------------------------------------------------------- /src/bio_scan/bio_data/species.py: -------------------------------------------------------------------------------- 1 | from typing import Mapping 2 | from bio_scan.bio_data.rulesets.aleoida import catalog as aleoida 3 | from bio_scan.bio_data.rulesets.anemone import catalog as anemone 4 | from bio_scan.bio_data.rulesets.bacterium import catalog as bacterium 5 | from bio_scan.bio_data.rulesets.brain_tree import catalog as brain_tree 6 | from bio_scan.bio_data.rulesets.cactoida import catalog as cactoida 7 | from bio_scan.bio_data.rulesets.clypeus import catalog as clypeus 8 | from bio_scan.bio_data.rulesets.concha import catalog as concha 9 | from bio_scan.bio_data.rulesets.electricae import catalog as electricae 10 | from bio_scan.bio_data.rulesets.fonticulua import catalog as fonticulua 11 | from bio_scan.bio_data.rulesets.frutexa import catalog as frutexa 12 | from bio_scan.bio_data.rulesets.fumerola import catalog as fumerola 13 | from bio_scan.bio_data.rulesets.fungoida import catalog as fungoida 14 | from bio_scan.bio_data.rulesets.osseus import catalog as osseus 15 | from bio_scan.bio_data.rulesets.recepta import catalog as recepta 16 | from bio_scan.bio_data.rulesets.shard import catalog as shard 17 | from bio_scan.bio_data.rulesets.stratum import catalog as stratum 18 | from bio_scan.bio_data.rulesets.tubers import catalog as tubers 19 | from bio_scan.bio_data.rulesets.tubus import catalog as tubus 20 | from bio_scan.bio_data.rulesets.tussock import catalog as tussock 21 | 22 | _mound_amphora: dict[str, dict[str, Mapping]] = { 23 | '$Codex_Ent_Cone_Name;': { # No atmos, near center of nebula 24 | '$Codex_Ent_Cone_Name;': { 25 | 'name': 'Bark Mound', 26 | 'value': 1471900, 27 | 'rulesets': [ 28 | { 29 | 'volcanism': 'Any', 30 | 'nebula': 'large', 31 | 'regions': ['!center'] 32 | } 33 | ] 34 | } 35 | }, 36 | '$Codex_Ent_Vents_Name;': { 37 | '$Codex_Ent_Vents_Name;': { 38 | 'name': 'Amphora Plant', 39 | 'value': 1628800, 40 | 'rulesets': [ 41 | { 42 | 'body_type': ['Metal rich body'], 43 | 'atmosphere': ['None'], 44 | 'star': 'A', 45 | 'min_temperature': 1000.0, 46 | 'max_temperature': 1750.0, 47 | 'volcanism': ['metallic', 'rocky', 'silicate'], 48 | 'bodies': ['Earthlike body', 'Gas giant with water based life', 'Water giant'], 49 | 'regions': ['amphora'] 50 | } 51 | ] 52 | } 53 | }, 54 | } 55 | 56 | rules: dict[str, dict[str, Mapping]] = (_mound_amphora | aleoida | anemone | bacterium | brain_tree | cactoida | 57 | clypeus | concha | electricae | fonticulua | frutexa | fumerola | fungoida | 58 | osseus | recepta | shard | stratum | tubers | tubus | tussock) 59 | -------------------------------------------------------------------------------- /src/bio_scan/body_data/util.py: -------------------------------------------------------------------------------- 1 | import locale 2 | import math 3 | from typing import Optional 4 | 5 | from ExploData.explo_data.body_data.struct import PlanetData 6 | 7 | from l10n import translations as tr 8 | 9 | from bio_scan.globals import bioscan_globals 10 | 11 | 12 | def get_body_shorthand(body_type: str) -> str: 13 | """ 14 | Return a shorthand for a given body type 15 | :param body_type: The ED journal string for a body type 16 | :return: A shorthand string representing the given body type 17 | """ 18 | 19 | shorthand = '' 20 | match body_type: 21 | case 'Icy body': 22 | shorthand = tr.tl('I', bioscan_globals.translation_context) 23 | case 'Rocky body': 24 | shorthand = tr.tl('R', bioscan_globals.translation_context) 25 | case 'Rocky ice body': 26 | shorthand = tr.tl('RI', bioscan_globals.translation_context) 27 | case 'Metal rich body': 28 | shorthand = tr.tl('MR', bioscan_globals.translation_context) 29 | case 'High metal content body': 30 | shorthand = tr.tl('HMC', bioscan_globals.translation_context) 31 | return f' ({shorthand})' 32 | 33 | 34 | def body_check(required_types: list[str], bodies: dict[str, PlanetData]) -> bool: 35 | """ 36 | Basic search function to check if one of the given body types is present in a dictionary of planet data. 37 | 38 | :param required_types: A list of ED Journal body type strings to search for 39 | :param bodies: A dictionary of planets names and ExploData PlanetData objects to search 40 | :return: Whether or not one of the required body types was found 41 | """ 42 | 43 | for _, body_data in bodies.items(): 44 | if body_data.get_type() in required_types: 45 | return True 46 | return False 47 | 48 | 49 | def get_gravity_warning(gravity: Optional[float], with_gravity: bool = False) -> str: 50 | """ 51 | Get gravity warning string based on provided gravity value. 2.7G is extreme gravity. 1G is high gravity. 52 | 53 | :param gravity: Surface gravity of a planet (in m/s^2) 54 | :param with_gravity: Whether or not to include the gravity value (in Gs) 55 | :return: Gravity warning string. ![#]G! for extreme gravity, ^[#]G^ for high gravity. 56 | """ 57 | 58 | if gravity: 59 | g_gravity = gravity / 9.80665 60 | g_formatted = locale.format_string('%.2f', g_gravity, grouping=True, monetary=False) 61 | if g_gravity > 2.69: 62 | return f' !{g_formatted}G!' if with_gravity else ' !G!' 63 | if g_gravity >= 1.0: 64 | return f' ^{g_formatted}G^' if with_gravity else ' ^G^' 65 | return f' {g_formatted}G' if with_gravity else '' 66 | return '' 67 | 68 | 69 | def star_check(star_query: str, star_type: str) -> bool: 70 | """ 71 | Check if the given star type string (by ED journal value) matches a base type identifier. 72 | 73 | This is necessary because super giants have different type IDs from standard stars and certain star types have 74 | variations with qualifiers where a basic equality comparison is insufficient. 75 | 76 | :param star_query: Simple star type string (A, F, K, O, D, H, etc.) 77 | :param star_type: ED journal type string for comparison star 78 | :return: Whether the basic query type matches the ED journal type string 79 | """ 80 | 81 | match star_query: 82 | case 'A': 83 | return star_type in ['A', 'A_BlueWhiteSuperGiant'] 84 | case 'B': 85 | return star_type in ['B', 'B_BlueWhiteSuperGiant'] 86 | case 'F': 87 | return star_type in ['F', 'F_WhiteSuperGiant'] 88 | case 'G': 89 | return star_type in ['G', 'G_WhiteSuperGiant'] 90 | case 'K': 91 | return star_type in ['K', 'K_OrangeGiant'] 92 | case 'M': 93 | return star_type in ['M', 'M_RedGiant', 'M_RedSuperGiant'] 94 | case 'D' | 'C' | 'W': 95 | return star_type.startswith(star_query) 96 | case _: 97 | return star_type == star_query 98 | 99 | 100 | def calc_bearing(lat_long: tuple[float, float]) -> float: 101 | """ 102 | Get the bearing angle from your current position to the target position using lat/long coordinates. 103 | 104 | :param lat_long: The target lat/long coordinates. 105 | :return: The bearing angle (from 0-359) 106 | """ 107 | 108 | lat_long2 = (bioscan_globals.planet_latitude, bioscan_globals.planet_longitude) 109 | phi_1 = math.radians(lat_long2[0]) 110 | phi_2 = math.radians(lat_long[0]) 111 | delta_lambda = math.radians(lat_long[1] - lat_long2[1]) 112 | y = math.sin(delta_lambda) * math.cos(phi_2) 113 | x = math.cos(phi_1) * math.sin(phi_2) \ 114 | - math.sin(phi_1) * math.cos(phi_2) * math.cos(delta_lambda) 115 | theta = math.atan2(y, x) 116 | return (math.degrees(theta) + 360) % 360 117 | -------------------------------------------------------------------------------- /src/bio_scan/const.py: -------------------------------------------------------------------------------- 1 | name = 'BioScan' 2 | version = '2.10.1' 3 | db_version = 9 4 | -------------------------------------------------------------------------------- /src/bio_scan/format_util.py: -------------------------------------------------------------------------------- 1 | import locale 2 | 3 | from l10n import translations as tr 4 | 5 | from EDMCLogging import get_plugin_logger 6 | 7 | from bio_scan.globals import bioscan_globals 8 | 9 | logger = get_plugin_logger('BioScan') 10 | 11 | credits_string = tr.tl('Cr', bioscan_globals.translation_context) # LANG: Credits unit 12 | 13 | def convert_locale(locale_code: str) -> str: 14 | match locale_code: 15 | case 'cs': 16 | return 'cs_CZ' 17 | case 'de': 18 | return 'de_DE' 19 | case 'en': 20 | return 'en_US' 21 | case 'es': 22 | return 'es_ES' 23 | case 'fi': 24 | return 'fi_FI' 25 | case 'fr': 26 | return 'fr_FR' 27 | case 'hu': 28 | return 'hu_HU' 29 | case 'it': 30 | return 'it_IT' 31 | case 'ja': 32 | return 'ja_JP' 33 | case 'ko': 34 | return 'ko_KR' 35 | case 'lv': 36 | return 'lv_LV' 37 | case 'nl': 38 | return 'nl_NL' 39 | case 'pl': 40 | return 'pl_PL' 41 | case 'pt' | 'pt-PT': 42 | return 'pt_PT' 43 | case 'pt-BR': 44 | return 'pt_BR' 45 | case 'ru': 46 | return 'ru_RU' 47 | case 'sl': 48 | return 'sl_SI' 49 | case 'sr' | 'sr-Latn' | 'sr-Latn-BA': 50 | return 'sr_RS' 51 | case 'tr': 52 | return 'tr_TR' 53 | case 'uk': 54 | return 'uk_UA' 55 | case 'zh-Hans': 56 | return 'zh_ZA' 57 | case _: 58 | return 'en_US' 59 | 60 | class Formatter: 61 | 62 | def __init__(self, shorten=False): 63 | locale.setlocale(locale.LC_ALL, '') 64 | self.shorten: bool = shorten 65 | 66 | def set_locale(self, locale_code: str): 67 | converted_locale_code = convert_locale(locale_code) 68 | try: 69 | locale.setlocale(locale.LC_ALL, converted_locale_code) 70 | except locale.Error as ex: 71 | logger.error(f'Failed to set locale: {locale_code} -> {converted_locale_code}') 72 | locale.setlocale(locale.LC_ALL, '') 73 | logger.error(f'Falling back to system default: {locale.getlocale()}') 74 | 75 | def set_shorten(self, value: bool) -> None: 76 | """ 77 | Set the shorten setting which determines how numbers are displayed. 78 | 79 | :param value: Whether or not to shorten number displays 80 | """ 81 | 82 | self.shorten = value 83 | 84 | def format_unit(self, num: float, unit: str, space: bool = True, monetary: bool = True) -> str: 85 | """ 86 | Number formatting. Automatically convert base unit to kilo- or mega-. 87 | 88 | :param num: Base numeral in standard unit. (e.g. meter, lightsecond, etc.) 89 | :param unit: Base unit abbreviation 90 | :param space: Whether to include a space before the unit 91 | :param monetary: Whether number is a monetary value 92 | :return: Formatted number string with metric unit conversion 93 | """ 94 | 95 | if num > 999999: 96 | # 1.3 Mu 97 | # LANG: Millions unit 98 | s = locale.format_string('%.2f %s%s', 99 | (num / 1000000.0, tr.tl('M', bioscan_globals.translation_context), unit), 100 | grouping=True, monetary=monetary) 101 | elif num > 999: 102 | # 456 ku 103 | # LANG: Thousands unit 104 | s = locale.format_string('%.2f %s%s', 105 | (num / 1000.0, tr.tl('k', bioscan_globals.translation_context), unit), 106 | grouping=True, monetary=monetary) 107 | else: 108 | # 789 u 109 | s = locale.format_string('%.0f %s', (num, unit), grouping=True, monetary=monetary) 110 | 111 | if not space: 112 | s = s.replace(' ', '') 113 | 114 | return s 115 | 116 | def format_credits(self, credit_amount: float, space: bool = True) -> str: 117 | """ 118 | Currency formatting. 119 | 120 | :param credit_amount: Base credit amount. 121 | :param space: Whether to add a space before the credits unit 122 | :return: Formatted credits string 123 | """ 124 | if self.shorten: 125 | return self.format_unit(credit_amount, credits_string, space) 126 | credits_formatted = credits_string 127 | if space: 128 | credits_formatted = ' ' + credits_formatted 129 | return locale.format_string(f'%d%s', (credit_amount, credits_formatted), grouping=True, monetary=True) 130 | 131 | def format_credit_range(self, min_value: float, max_value: float, space: bool = True) -> str: 132 | """ 133 | Currency range formatting. 134 | 135 | :param min_value: Minimum credit amount 136 | :param max_value: Maximum credit amount 137 | :param space: Whether or not to add a space before the credits unit 138 | :return: Formatted credit range string 139 | """ 140 | 141 | if min_value != max_value: 142 | if self.shorten: 143 | return "{} - {}".format(self.format_unit(min_value, '', space), 144 | self.format_unit(max_value, f' {credits_string}', space)) 145 | return locale.format_string('%d - %d Cr', (min_value, max_value), grouping=True, monetary=True) 146 | else: 147 | return self.format_credits(min_value, space) 148 | 149 | def format_distance(self, distance: int, unit: str, space: bool = True) -> str: 150 | """ 151 | Distance formatter. 152 | 153 | :param distance: Base distance. 154 | :param unit: The unit for the distance. 155 | :param space: Whether to add a space before the distance unit 156 | :return: Formatted distance string with specified unit 157 | """ 158 | 159 | return self.format_unit(distance, unit, space, False) 160 | -------------------------------------------------------------------------------- /src/bio_scan/globals.py: -------------------------------------------------------------------------------- 1 | import semantic_version 2 | 3 | # TKinter imports 4 | import tkinter as tk 5 | from tkinter import ttk 6 | 7 | # Database objects 8 | from sqlalchemy.orm import Session 9 | 10 | from ExploData.explo_data.db import Commander, System 11 | from ExploData.explo_data.body_data.struct import PlanetData, StarData 12 | 13 | # Local imports 14 | import bio_scan.const 15 | import bio_scan.overlay as overlay 16 | 17 | # EDMC imports 18 | from ttkHyperlinkLabel import HyperlinkLabel 19 | 20 | class Globals: 21 | """Holds module globals.""" 22 | 23 | def __init__(self): 24 | self.formatter = None 25 | self.translation_context = '' 26 | 27 | self.VERSION = semantic_version.Version(bio_scan.const.version) 28 | self.NAME = bio_scan.const.name 29 | 30 | # Settings vars 31 | # General 32 | self.credits_setting: tk.BooleanVar | None = None 33 | self.focus_setting: tk.StringVar | None = None 34 | self.signal_setting: tk.StringVar | None = None 35 | self.focus_breakdown: tk.BooleanVar | None = None 36 | self.scan_display_mode: tk.StringVar | None = None 37 | self.waypoints_enabled: tk.BooleanVar | None = None 38 | self.hide_waypoint_bearings: tk.BooleanVar | None = None 39 | self.exclude_signals: tk.BooleanVar | None = None 40 | self.minimum_signals: tk.IntVar | None = None 41 | self.debug_logging_enabled: tk.BooleanVar | None = None 42 | self.focus_distance: tk.IntVar | None = None 43 | self.box_height: tk.IntVar | None = None 44 | # Overlay 45 | self.use_overlay: tk.BooleanVar | None = None 46 | self.overlay_color: tk.StringVar | None = None 47 | self.overlay_anchor_x: tk.IntVar | None = None 48 | self.overlay_anchor_y: tk.IntVar | None = None 49 | self.overlay_summary_x: tk.IntVar | None = None 50 | self.overlay_summary_y: tk.IntVar | None = None 51 | self.overlay_detail_scroll: tk.BooleanVar | None = None 52 | self.overlay_detail_length: tk.IntVar | None = None 53 | self.overlay_detail_delay: tk.DoubleVar | None = None 54 | self.overlay_line_spacing_normal: tk.IntVar | None = None 55 | self.overlay_line_spacing_large: tk.IntVar | None = None 56 | ## Radar 57 | self.radar_enabled: tk.BooleanVar | None = None 58 | self.radar_ship_loc_enabled: tk.BooleanVar | None = None 59 | self.radar_anchor_x: tk.IntVar | None = None 60 | self.radar_anchor_y: tk.IntVar | None = None 61 | self.radar_radius: tk.IntVar | None = None 62 | self.radar_max_distance: tk.IntVar | None = None 63 | self.radar_use_log: tk.BooleanVar | None = None 64 | ## Whitelist 65 | self.ship_whitelist: list[str] = [] 66 | 67 | # GUI Objects 68 | self.parent: tk.Frame | None = None 69 | self.frame: tk.Frame | None = None 70 | self.scroll_canvas: tk.Canvas | None = None 71 | self.scrollbar: ttk.Scrollbar | None = None 72 | self.scrollable_frame: ttk.Frame | None = None 73 | self.label: tk.Label | None = None 74 | self.values_label: tk.Label | None = None 75 | self.total_label: tk.Label | None = None 76 | self.edsm_button: tk.Label | None = None 77 | self.update_button: HyperlinkLabel | None = None 78 | self.journal_label: tk.Label | None = None 79 | self.overlay: overlay.Overlay = overlay.Overlay() 80 | 81 | # Plugin state data 82 | # DB elements 83 | self.commander: Commander | None = None 84 | self.planets: dict[str, PlanetData] = {} 85 | self.stars: dict[str, StarData] = {} 86 | self.planet_cache: dict[ 87 | str, dict[str, tuple[bool, tuple[str, int, int, list[tuple[str, list[str], int]]]]]] = {} 88 | self.migration_failed: bool = False 89 | self.db_mismatch: bool = False 90 | self.sql_session: Session | None = None 91 | # System info 92 | self.system: System | None = None 93 | self.main_star_type: str = '' 94 | self.main_star_luminosity: str = '' 95 | self.location_name: str = '' 96 | self.location_id: str = '' 97 | self.location_state: str = '' 98 | # Body info 99 | self.ship_location: tuple[float, float] | None = None 100 | self.planet_radius: float = 0.0 101 | self.planet_latitude: float | None = None 102 | self.planet_longitude: float | None = None 103 | self.planet_altitude: float = 10000.0 104 | self.planet_heading: int | None = None 105 | # Ship / player status 106 | self.docked: bool = False 107 | self.on_foot: bool = False 108 | self.suit_name: str = '' 109 | self.analysis_mode: bool = True 110 | self.in_supercruise: bool = False 111 | self.mode_changed: bool = False 112 | self.current_scan: tuple[str, str] = ('', '') 113 | self.gui_focus: int = 0 114 | 115 | # EDSM vars 116 | self.fetched_edsm = False 117 | 118 | # EDDMC compat 119 | self.edd_replay: bool = False 120 | 121 | bioscan_globals = Globals() -------------------------------------------------------------------------------- /src/bio_scan/nebula_data/sectors.py: -------------------------------------------------------------------------------- 1 | data = [ 2 | 'Aquila Dark Region Sector', 3 | 'B133 Sector', 4 | 'B352 Sector', 5 | 'B92 Sector', 6 | 'Barnard\'s Loop Sector', 7 | 'Bubble Sector', 8 | 'California Sector', 9 | 'Cat\'s Paw Sector', 10 | 'Cave Sector', 11 | 'Cepheus Dark Region Sector', 12 | 'Chamaeleon Sector', 13 | 'Coalsack Dark Region Sector', 14 | 'Cocoon Sector', 15 | 'Cone Sector', 16 | 'Corona Australis Dark Region Sector', 17 | 'Crab Sector', 18 | 'Crescent Sector', 19 | 'Eagle Sector', 20 | 'Elephant\'s Trunk Sector', 21 | 'Eta Carina Sector', 22 | 'Fine Ring Sector', 23 | 'Flame Sector', 24 | 'Flaming Star Sector', 25 | 'G2 Dust Cloud Sector', 26 | 'Heart Sector', 27 | 'Helix Sector', 28 | 'Hind Sector', 29 | 'Horsehead Dark Region Sector', 30 | 'Horsehead Sector', 31 | 'IC 4604 Sector', 32 | 'Iris Sector', 33 | 'Jellyfish Sector', 34 | 'Lagoon Sector', 35 | 'LBN 623 Sector', 36 | 'Lupus Dark Region Sector', 37 | 'Lupus Dark Region B Sector', 38 | 'Messier 78 Sector', 39 | 'Monkey Head Sector', 40 | 'Musca Dark Region Sector', 41 | 'NGC 1333 Sector', 42 | 'NGC 1491 Sector', 43 | 'NGC 1931 Sector', 44 | 'NGC 281 Sector', 45 | 'NGC 3199 Sector', 46 | 'NGC 5367 Sector', 47 | 'NGC 6188 Sector', 48 | 'NGC 6357 Sector', 49 | 'NGC 6820 Sector', 50 | 'NGC 7538 Sector', 51 | 'NGC 7822 Sector', 52 | 'North America Sector', 53 | 'Omega Sector', 54 | 'Ophiuchus Dark Region Sector', 55 | 'Ophiuchus Dark Region B Sector', 56 | 'Ophiuchus Dark Region C Sector', 57 | 'Orion Dark Region Sector', 58 | 'Orion Sector', 59 | 'Parrot\'s Head Sector', 60 | 'Pelican Sector', 61 | 'Pencil Sector', 62 | 'Perseus Dark Region Sector', 63 | 'Pipe (Bowl) Sector', 64 | 'Pipe (Stem) Sector', 65 | 'Pleiades Sector', 66 | 'Puppis Dark Region Sector', 67 | 'Puppis Dark Region B Sector', 68 | 'Rho Ophiuchi Sector', 69 | 'Ring Sector', 70 | 'Rosette Sector', 71 | 'Running Man Sector', 72 | 'Scorpius Dark Region Sector', 73 | 'Scutum Dark Region Sector', 74 | 'Seagull Sector', 75 | 'Skull and Crossbones Sector', 76 | 'Skull Sector', 77 | 'Snake Sector', 78 | 'Soul Sector', 79 | 'Statue of Liberty Sector', 80 | 'Struve\'s Lost Sector', 81 | 'Taurus Dark Region Sector', 82 | 'Trifid Sector', 83 | 'Trifid of the North Sector', 84 | 'Veil East Sector', 85 | 'Veil West Sector', 86 | 'Vela Dark Region Sector', 87 | 'Vulpecula Dark Region Sector', 88 | 'Witch Head Sector' 89 | ] 90 | -------------------------------------------------------------------------------- /src/bio_scan/overlay.py: -------------------------------------------------------------------------------- 1 | import math 2 | import sys 3 | import threading 4 | from os import environ 5 | from typing import Callable, Self 6 | 7 | from EDMCLogging import get_plugin_logger 8 | from bio_scan import const 9 | 10 | try: 11 | from EDMCOverlay import edmcoverlay 12 | except ImportError: 13 | try: 14 | from edmcoverlay import edmcoverlay 15 | except ImportError: 16 | edmcoverlay = None 17 | 18 | logger = get_plugin_logger(const.name) 19 | 20 | VIRTUAL_WIDTH = 1280.0 21 | VIRTUAL_HEIGHT = 1024.0 22 | VIRTUAL_ORIGIN_X = 20.0 23 | VIRTUAL_ORIGIN_Y = 40.0 24 | 25 | def setInterval(interval: float) -> Callable: 26 | """ 27 | Decorator which automatically repeats the function at a given interval. 28 | 29 | :param interval: Interval in seconds 30 | """ 31 | 32 | def decorator(function: Callable) -> Callable: 33 | def wrapper(*args, **kwargs) -> threading.Event: 34 | stopped = threading.Event() 35 | 36 | def loop() -> None: # executed in another thread 37 | while not stopped.wait(interval): # until stopped 38 | function(*args, **kwargs) 39 | 40 | t = threading.Thread(target=loop) 41 | t.daemon = True # stop if the program exits 42 | t.start() 43 | return stopped 44 | 45 | return wrapper 46 | 47 | return decorator 48 | 49 | 50 | def round_away(val): 51 | val += -0.5 if val < 0 else 0.5 52 | return int(val) 53 | 54 | 55 | class TextBlock: 56 | """ 57 | Cache class for a multi-line text block on the overlay. 58 | 59 | These can be configured to auto-scroll beyond a certain length, with a given delay before scrolling 60 | the opposite direction. 61 | """ 62 | def __init__(self, text: list[str], x: int, y: int, size: str, color: str, 63 | scrolled: bool = False, limit: int = 0, delay: float = 10): 64 | """ 65 | Constructor. 66 | 67 | :param text: List of strings to display 68 | :param x: X coordinate for upper left corner 69 | :param y: Y coordinate for upper left corner 70 | :param size: Text size (normal or large) 71 | :param color: Text color (#aarrggbb, #rrggbb, red, green, blue, yellow, white, black) 72 | :param scrolled: Whether to scroll the text display (default: False) 73 | :param limit: Maximum line length of display, for use with 'scrolled' (default: 0) 74 | :param delay: Time to wait before scrolling the opposite direction when reaching the 75 | end of the text (default 10) 76 | """ 77 | 78 | self.text = text 79 | self.x = x 80 | self.y = y 81 | self.size = size 82 | self.color = color 83 | self.scrolled = scrolled 84 | self.limit = limit 85 | self.delay = delay 86 | self.direction = 'down' 87 | self.offset = 0 88 | self.timer = threading.Timer(self.delay, lambda *args: None) 89 | if scrolled: 90 | self.timer.start() 91 | 92 | 93 | class RadarSet: 94 | """ 95 | Cache class for a radar display. 96 | 97 | Contains circles and marker positions with a given position and radius. Can be set to display 98 | linearly or logarithmically. 99 | """ 100 | def __init__(self, markers: list[dict], circles: list[dict], x: int, y: int, r: int, d: int, log: bool): 101 | """ 102 | Constructor. 103 | 104 | :param markers: List of marker data. Should contain a dict of 'distance', 'bearing', 'color', and 'genus'. 105 | :param circles: List of circle data. Should contain a dict of 'radius', 'color' and optionally 'text'. 106 | :param x: X coordinate for upper left corner 107 | :param y: Y coordinate for upper left corner 108 | :param r: Radius of circle 109 | :param d: Maximum distance tracked by radar (at r) 110 | :param log: Whether to display a logarithmic distance scale 111 | """ 112 | 113 | self.markers: list[dict] = markers 114 | self.circles: list[dict] = circles 115 | self.x = x 116 | self.y = y 117 | self.r = r 118 | self.d = d 119 | self.log = log 120 | 121 | 122 | class Overlay: 123 | """ 124 | An interface for displaying multiple text blocks with EDMCOverlay. Breaks multi-line text into 125 | multiple individual lines to work around EDMCOverlay limitations. Redraws the text every 5 minutes in order to 126 | display text indefinitely. 127 | """ 128 | 129 | def __init__(self): 130 | if edmcoverlay: 131 | self._overlay: edmcoverlay.Overlay | None = edmcoverlay.Overlay() 132 | if hasattr(self._overlay, 'connection'): 133 | self._overlay_type = 'EDMCOverlay' 134 | elif hasattr(self._overlay, 'connect') and not hasattr(self._overlay, 'server'): 135 | self._overlay_type = 'edmcoverlay_for_linux' 136 | else: 137 | if environ.get('XDG_SESSION_TYPE', 'X11') == 'wayland' and hasattr(self._overlay, 'server'): 138 | self._overlay_type = 'edmcoverlay2_wayland' 139 | else: 140 | self._overlay_type = 'edmcoverlay2' 141 | else: 142 | self._overlay_type = "none" 143 | self._overlay: edmcoverlay.Overlay | None = None 144 | self._normal_spacer: int = 16 145 | self._large_spacer: int = 26 146 | self._text_blocks: dict[str, TextBlock] = {} 147 | self._markers: dict[str, RadarSet] = {} 148 | self._redraw_timer = self.redraw() 149 | self._redraw_radar_timer = self.redraw_radar() 150 | self._scroll_timer = self.scroll() 151 | self._screen_width = 1920 152 | self._screen_height = 1080 153 | self._over_aspect_x = self._calc_aspect_x() 154 | 155 | def set_screen_dimensions(self, w: int, h: int) -> Self: 156 | """ 157 | Sets screen dimensions for use in pixel ratio scaling. 158 | 159 | :param w: Width. 160 | :param h: Height. 161 | :return: Returns self. 162 | """ 163 | 164 | self._screen_width = w 165 | self._screen_height = h 166 | self._over_aspect_x = self._calc_aspect_x() 167 | return self 168 | 169 | def set_line_spacing(self, normal: int, large: int) -> Self: 170 | """ 171 | Sets line spacing for text displays. 172 | 173 | :param normal: Normal text size spacing 174 | :param large: Large text size spacing 175 | :return: Returns self. 176 | """ 177 | 178 | self._normal_spacer = normal 179 | self._large_spacer = large 180 | return self 181 | 182 | def disconnect(self) -> None: 183 | """ 184 | Shut down overlay draw timers 185 | """ 186 | 187 | self._redraw_timer.set() 188 | self._redraw_radar_timer.set() 189 | self._scroll_timer.set() 190 | 191 | def _calc_aspect_x(self) -> float: 192 | if self._overlay_type == 'EDMCOverlay': 193 | return (VIRTUAL_WIDTH+32) / (VIRTUAL_HEIGHT+18) * (self._screen_height-2*VIRTUAL_ORIGIN_Y) / (self._screen_width-2*VIRTUAL_ORIGIN_X) 194 | else: 195 | return (VIRTUAL_WIDTH / VIRTUAL_HEIGHT) * (self._screen_height / self._screen_width) 196 | 197 | def _aspect_x(self, x: float) -> int: 198 | if self._overlay_type in ['EDMCOverlay', 'edmcoverlay2']: 199 | return round_away(self._over_aspect_x * x) 200 | return int(x) 201 | 202 | def _aspect_y(self, y: float) -> int: 203 | if self._overlay_type == 'edmcoverlay2_wayland': 204 | return int(y + 20) 205 | return int(y) 206 | 207 | def display(self, message_id: str, text: str, x: int = 0, y: int = 0, color: str = "#ffffff", size: str = "normal", 208 | scrolled: bool = False, limit: int = 0, delay: float = 10) -> None: 209 | """ 210 | Displays text with given attributes. Saves text in cache to allow for redraw and clearing. 211 | Expires after 60 seconds. 212 | 213 | :param message_id: Unique identifier for a given text block. 214 | :param text: Message to display. 215 | :param x: X coordinate 216 | :param y: Y coordinate 217 | :param color: Accepts "red", "green", "blue", and hex "#ffffff" 218 | :param size: Accepts "normal" and "large" 219 | :param scrolled: Toggle for scrolled text panel 220 | :param limit: Line limit for display; 0 is no limit 221 | :param delay: Time between up/down scroll of scrolled text 222 | """ 223 | 224 | if sys.platform == 'linux' and self._overlay_type in ['edmcoverlay2', 'edmcoverlay_for_linux']: 225 | formatted_text = (text.replace('🗸', '*').replace('\N{memo}', '»') 226 | .replace(' ', ' ').split('\n')) 227 | elif self._overlay_type == 'EDMCOverlay': 228 | formatted_text = text.replace('🗸', '√').replace('\N{memo}', '♦').split('\n') 229 | else: 230 | formatted_text = text.split('\n') 231 | if (not scrolled or 232 | (message_id in self._text_blocks and len(formatted_text) < len(self._text_blocks[message_id].text))): 233 | self.clear(message_id, len(formatted_text)-1, False) 234 | self._text_blocks[message_id] = TextBlock( 235 | text=formatted_text, x=x, y=y, size=size, color=color, scrolled=scrolled, limit=limit, delay=delay 236 | ) 237 | self.draw(message_id) 238 | 239 | def clear(self, message_id: str, new_length: int = 0, remove: bool = True) -> None: 240 | """ 241 | Clears a given text block identified by a unique message ID. 242 | 243 | :param message_id: Unique ID of text to clear. 244 | :param new_length: Start point of clear 245 | :param remove: Remove the message from the cache. 246 | """ 247 | 248 | try: 249 | if message_id in self._text_blocks: 250 | last_len = self._text_blocks[message_id].limit if self._text_blocks[message_id].limit else len(self._text_blocks[message_id].text) 251 | last_len = min(len(self._text_blocks[message_id].text), last_len) 252 | if new_length: 253 | if new_length < last_len: 254 | for item in range(new_length, last_len): 255 | self._overlay.send_raw({'id': f'{message_id}_{item}', 'text': '', 'ttl': 0}) 256 | else: 257 | for item in range(last_len): 258 | self._overlay.send_raw({'id': f'{message_id}_{item}', 'text': '', 'ttl': 0}) 259 | if remove: 260 | self._text_blocks.pop(message_id, None) 261 | except Exception as ex: 262 | logger.debug("Exception during overlay clear", exc_info=ex) 263 | 264 | def render_radar(self, message_id: str, x: int, y: int, r: int, d: int, 265 | markers: list | None = None, circles: list | None = None, logarithmic: bool = False) -> None: 266 | """ 267 | Render radar display, cached to refresh on a timer. 268 | 269 | :param message_id: ID of radar group, used to update or clear existing radar display 270 | :param x: Center x coordinate of radar 271 | :param y: Center y coordinate of radar 272 | :param r: Radius of radar 273 | :param d: Max distance of radar display 274 | :param markers: List of markers as a dict of 'distance', 'bearing', and 'genus' 275 | :param circles: List of circles to draw as a dict of 'radius' and 'color' 276 | :param logarithmic: Make radar scale logarithmic (default: False) 277 | """ 278 | 279 | if self._overlay_type == 'EDMCOverlay': 280 | self.clear_radar(message_id) 281 | else: 282 | self.trim_radar(message_id, circles, markers) 283 | self._markers[message_id] = RadarSet(markers, circles, x, y, r, d, logarithmic) 284 | self.draw_circles(message_id) 285 | self.draw_markers(message_id) 286 | 287 | def clear_radar(self, message_id: str, remove: bool = True) -> None: 288 | """ 289 | Clears radar display 290 | """ 291 | 292 | if message_id in self._markers: 293 | for item in range(len(self._markers[message_id].circles)): 294 | self._overlay.send_raw({'id': f'{message_id}_circle_{item}', 'ttl': 0}) 295 | if 'text' in self._markers[message_id].circles[item]: 296 | self._overlay.send_raw({'id': f'{message_id}_circle_{item}_text', 'ttl': 0}) 297 | for item in range(len(self._markers[message_id].markers) + 1): 298 | self._overlay.send_raw({'id': f'{message_id}_{item}'}) 299 | if message_id in self._markers and remove: 300 | self._markers.pop(message_id) 301 | 302 | def trim_radar(self, message_id: str, circles: list | None = None, markers: list | None = None) -> None: 303 | """ 304 | Removes expired radar components 305 | """ 306 | if message_id in self._markers: 307 | if len(self._markers[message_id].circles) > len(circles): 308 | for item in range(len(circles) - 1, len(self._markers[message_id].circles)): 309 | self._overlay.send_raw({'id': f'{message_id}_circle_{item}'}) 310 | if 'text' in self._markers[message_id].circles[item]: 311 | self._overlay.send_raw({'id': f'{message_id}_circle_{item}_text'}) 312 | if len(self._markers[message_id].markers) > len(markers): 313 | for item in range(len(markers), len(self._markers[message_id].markers) + 1): 314 | self._overlay.send_raw({'id': f'{message_id}_{item}'}) 315 | 316 | @setInterval(10) 317 | def redraw(self): 318 | """ 319 | Redraws all cached text blocks on a 30-second interval. 320 | :rtype: threading.Event 321 | """ 322 | 323 | if self.available(): 324 | for message_id, message_info in self._text_blocks.copy().items(): 325 | if message_info.scrolled: 326 | continue 327 | self.draw(message_id) 328 | 329 | @setInterval(10) 330 | def redraw_radar(self): 331 | """ 332 | Redraws all cached radars on a 5-second interval. 333 | :rtype: threading.Event 334 | """ 335 | 336 | if self.available(): 337 | for message_id, markers in self._markers.copy().items(): 338 | if self._overlay_type == 'EDMCOverlay': 339 | self.clear_radar(message_id, False) 340 | self.draw_circles(message_id) 341 | self.draw_markers(message_id) 342 | 343 | @setInterval(.75) 344 | def scroll(self): 345 | """ 346 | Redraw scrolled displays based on given lines. 347 | :rtype: threading.Event 348 | """ 349 | 350 | if self.available(): 351 | try: 352 | for message_id, message_info in self._text_blocks.copy().items(): 353 | if not message_info.scrolled: 354 | continue 355 | 356 | if not message_info.timer.is_alive(): 357 | self.draw(message_id) 358 | if message_info.limit != 0 and message_info.limit < len(message_info.text): 359 | offset = message_info.offset + 1 if message_info.direction == "down" else \ 360 | len(message_info.text) - message_info.offset 361 | display = offset + message_info.limit if message_info.direction == "down" else offset 362 | if display >= len(message_info.text): 363 | self._text_blocks[message_id].direction = "up" if message_info.direction == "down" \ 364 | else "down" 365 | self._text_blocks[message_id].timer = threading.Timer(message_info.delay, 366 | lambda *args: None) 367 | self._text_blocks[message_id].timer.start() 368 | if self._text_blocks[message_id].direction == "down": 369 | self._text_blocks[message_id].offset += 1 370 | else: 371 | self._text_blocks[message_id].offset -= 1 372 | elif message_info.limit != 0 and message_info.limit >= len(message_info.text): 373 | self.draw(message_id) 374 | except Exception as ex: 375 | logger.debug("Exception during scroll repaint", exc_info=ex) 376 | 377 | def draw(self, message_id: str): 378 | """ 379 | Render one overlay display 380 | """ 381 | 382 | if message_id in self._text_blocks: 383 | block = self._text_blocks[message_id] 384 | count = block.offset 385 | line_count = 0 386 | spacer = self._normal_spacer if block.size == "normal" else self._large_spacer 387 | while (block.limit == 0 or count - block.offset <= block.limit) and count < len(block.text): 388 | try: 389 | self._overlay.send_message("{}_{}".format(message_id, line_count), block.text[count], block.color, 390 | block.x, self._aspect_y(block.y) + (spacer * (count - block.offset)), 391 | ttl=20, size=block.size) 392 | except AttributeError: 393 | count -= 1 394 | self._overlay.connect() 395 | except Exception as ex: 396 | logger.debug("Exception during draw", exc_info=ex) 397 | count += 1 398 | line_count += 1 399 | 400 | def draw_circles(self, message_id: str): 401 | if message_id in self._markers: 402 | for index in range(len(self._markers[message_id].circles)): 403 | try: 404 | points = [] 405 | x = self._markers[message_id].x 406 | y = self._markers[message_id].y 407 | r = self._markers[message_id].circles[index]['radius'] 408 | for pie_slice in range(49): 409 | x_point = x + (r * math.cos(math.radians(7.5 * pie_slice))) 410 | y_point = y + (r * math.sin(math.radians(7.5 * pie_slice))) 411 | point = { 412 | 'x': self._aspect_x(x_point), 413 | 'y': self._aspect_y(y_point) 414 | } 415 | points.append(point) 416 | 417 | message = {'id': f'{message_id}_circle_{index}', 418 | 'shape': 'vect', 419 | 'vector': points, 420 | 'color': self._markers[message_id].circles[index]['color'], 421 | 'ttl': 20} 422 | self._overlay.send_raw(message) 423 | if 'text' in self._markers[message_id].circles[index]: 424 | point = { 425 | 'x': self._aspect_x(x + (r * math.cos(math.radians(0)))), 426 | 'y': self._aspect_y(y + (r * math.sin(math.radians(0)))), 427 | 'color': self._markers[message_id].circles[index]['color'], 428 | 'text': self._markers[message_id].circles[index]['text'], 429 | 'marker': 'circle' 430 | } 431 | message = {'id': f'{message_id}_circle_{index}_text', 'shape': 'vect', 'vector': [point], 'ttl': 20} 432 | self._overlay.send_raw(message) 433 | except AttributeError: 434 | self._overlay.connect() 435 | except Exception as ex: 436 | logger.debug('Exception during radar circle draw', exc_info=ex) 437 | 438 | def draw_markers(self, message_id: str) -> None: 439 | if message_id in self._markers: 440 | try: 441 | if self._markers[message_id].markers is None: 442 | return 443 | x = self._markers[message_id].x 444 | y = self._markers[message_id].y 445 | r = self._markers[message_id].r 446 | d = self._markers[message_id].d 447 | markers = self._markers[message_id].markers 448 | message = { 449 | 'id': f'{message_id}_0', 450 | 'shape': 'vect', 451 | 'vector': [{ 452 | 'x': self._aspect_x(x), 'y': self._aspect_y(y), 453 | 'color': self._markers[message_id].circles[0]['color'], 454 | 'marker': 'circle' 455 | }], 456 | 'ttl': 20 457 | } 458 | self._overlay.send_raw(message) 459 | for item in range(len(markers)): 460 | if self._markers[message_id].log: 461 | log_d = math.log(markers[item]['distance']+1, d) 462 | log_d = log_d if markers[item]['distance'] > 0 else 0 463 | marker_radius = r if log_d > 1.0 else log_d * r 464 | else: 465 | marker_radius = r if markers[item]['distance'] > self._markers[message_id].d \ 466 | else r * markers[item]['distance'] / self._markers[message_id].d 467 | marker_bearing = markers[item]['bearing'] 468 | x_point = x + (marker_radius * math.cos(math.radians(marker_bearing))) 469 | y_point = y + (marker_radius * math.sin(math.radians(marker_bearing))) 470 | message = { 471 | 'id': f'{message_id}_{item+1}', 472 | 'shape': 'vect', 473 | 'vector': [{ 474 | 'x': self._aspect_x(x_point), 'y': self._aspect_y(y_point), 475 | 'color': markers[item]['color'], 'marker': 'cross', 476 | 'text': markers[item]['text'] 477 | }], 478 | 'ttl': 20 479 | } 480 | self._overlay.send_raw(message) 481 | except AttributeError: 482 | self._overlay.connect() 483 | except Exception as ex: 484 | logger.debug('Exception during radar marker draw', exc_info=ex) 485 | 486 | def available(self) -> bool: 487 | """ 488 | Get availability of EDMCOverlay interface 489 | 490 | :return: Availability of EDMCOverlay 491 | """ 492 | 493 | if self._overlay is not None: 494 | if hasattr(self._overlay, 'connection'): 495 | if self._overlay.connection is None: 496 | self._overlay = edmcoverlay.Overlay() 497 | return True 498 | else: 499 | if edmcoverlay: 500 | self._overlay = edmcoverlay.Overlay() 501 | return True 502 | return False 503 | -------------------------------------------------------------------------------- /src/bio_scan/status_flags.py: -------------------------------------------------------------------------------- 1 | from enum import Flag, auto 2 | 3 | 4 | class StatusFlags(Flag): 5 | """ 6 | Enum definition for Elite Dangerous Status.json flags (group 1) 7 | """ 8 | 9 | DOCKED = auto() 10 | LANDED = auto() 11 | LANDING_GEAR = auto() 12 | SHIELDS_UP = auto() 13 | SUPERCRUISE = auto() 14 | FA_OFF = auto() 15 | HARDPOINTS = auto() 16 | IN_WING = auto() 17 | LIGHTS = auto() 18 | CARGO_SCOOP = auto() 19 | SILENT = auto() 20 | FUEL_SCOOP = auto() 21 | HANDBREAK = auto() 22 | SRV_USING_TURRET = auto() 23 | SRV_TURRET_BLOCKED = auto() 24 | SRV_DRIVE_ASSIST_ON = auto() 25 | MASS_LOCKED = auto() 26 | FSD_CHARGING = auto() 27 | FSD_COOLDOWN = auto() 28 | LOW_FUEL = auto() 29 | OVERHEAT = auto() 30 | HAVE_LATLONG = auto() 31 | DANGER = auto() 32 | INTERDICTED = auto() 33 | IN_SHIP = auto() 34 | IN_FIGHTER = auto() 35 | IN_SRV = auto() 36 | IS_ANALYSIS_MODE = auto() 37 | NIGHT_VISION = auto() 38 | HAVE_ALTITUDE = auto() 39 | FSD_JUMP_IN_PROGRESS = auto() 40 | SRV_HIGHBEAM = auto() 41 | 42 | 43 | class StatusFlags2(Flag): 44 | """ 45 | Enum definition for Elite Dangerous Status.json flags (group 2) 46 | """ 47 | 48 | ON_FOOT = auto() 49 | IN_TAXI = auto() 50 | MULTICREW = auto() 51 | STATION_ON_FOOT = auto() 52 | PLANET_ON_FOOT = auto() 53 | AIM_DOWN_SIGHT = auto() 54 | LOW_OXYGEN = auto() 55 | LOW_HEALTH = auto() 56 | COLD = auto() 57 | HOT = auto() 58 | VERY_COLD = auto() 59 | VERY_HOT = auto() 60 | GLIDING = auto() 61 | HANGAR_ON_FOOT = auto() 62 | SOCIAL_ON_FOOT = auto() 63 | EXTERIOR_ON_FOOT = auto() 64 | BREATHABLE = auto() 65 | MULTICREW_TELEPRESENCE = auto() 66 | MULTICREW_PHYSICAL = auto() 67 | SUPERCHARGING_FSD = auto() 68 | SCO_ACTIVE = auto() 69 | SUPERCRUISE_ASSIST = auto() 70 | NPC_CREW = auto() -------------------------------------------------------------------------------- /src/bio_scan/tooltip.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.ttk as ttk 3 | 4 | 5 | class Tooltip: 6 | """ 7 | It creates a tooltip for a given widget as the mouse goes on it. 8 | 9 | see: 10 | 11 | https://stackoverflow.com/questions/3221956/ 12 | what-is-the-simplest-way-to-make-tooltips- 13 | in-tkinter/36221216#36221216 14 | 15 | http://www.daniweb.com/programming/software-development/ 16 | code/484591/a-tooltip-class-for-tkinter 17 | 18 | - Originally written by vegaseat on 2014.09.09. 19 | 20 | - Modified to include a delay time by Victor Zaccardo on 2016.03.25. 21 | 22 | - Modified 23 | - to correct extreme right and extreme bottom behavior, 24 | - to stay inside the screen whenever the tooltip might go out on 25 | the top but still the screen is higher than the tooltip, 26 | - to use the more flexible mouse positioning, 27 | - to add customizable background color, padding, waittime and 28 | wraplength on creation 29 | by Alberto Vassena on 2016.11.05. 30 | 31 | Tested on Ubuntu 16.04/16.10, running Python 3.5.2 32 | 33 | TODO: themes styles support 34 | """ 35 | 36 | def __init__(self, widget, 37 | *, 38 | bg='#FFFFEA', 39 | pad=(5, 3, 5, 3), 40 | text='widget info', 41 | waittime=400, 42 | wraplength=250): 43 | 44 | self.waittime = waittime # in miliseconds, originally 500 45 | self.wraplength = wraplength # in pixels, originally 180 46 | self.widget = widget 47 | self.text = text 48 | self.widget.bind("", self.onEnter) 49 | self.widget.bind("", self.onLeave) 50 | self.widget.bind("", self.onLeave) 51 | self.bg = bg 52 | self.pad = pad 53 | self.id = None 54 | self.tw = None 55 | 56 | def onEnter(self, event=None): 57 | self.schedule() 58 | 59 | def onLeave(self, event=None): 60 | self.unschedule() 61 | self.hide() 62 | 63 | def schedule(self): 64 | self.unschedule() 65 | self.id = self.widget.after(self.waittime, self.show) 66 | 67 | def unschedule(self): 68 | id_ = self.id 69 | self.id = None 70 | if id_: 71 | self.widget.after_cancel(id_) 72 | 73 | def show(self): 74 | def tip_pos_calculator(widget, label, 75 | *, 76 | tip_delta=(10, 5), pad=(5, 3, 5, 3)): 77 | 78 | w = widget 79 | 80 | s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight() 81 | 82 | width, height = (pad[0] + label.winfo_reqwidth() + pad[2], 83 | pad[1] + label.winfo_reqheight() + pad[3]) 84 | 85 | mouse_x, mouse_y = w.winfo_pointerxy() 86 | 87 | x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1] 88 | x2, y2 = x1 + width, y1 + height 89 | 90 | x_delta = x2 - s_width 91 | if x_delta < 0: 92 | x_delta = 0 93 | y_delta = y2 - s_height 94 | if y_delta < 0: 95 | y_delta = 0 96 | 97 | offscreen = (x_delta, y_delta) != (0, 0) 98 | 99 | if offscreen: 100 | 101 | if x_delta: 102 | x1 = mouse_x - tip_delta[0] - width 103 | 104 | if y_delta: 105 | y1 = mouse_y - tip_delta[1] - height 106 | 107 | offscreen_again = y1 < 0 # out on the top 108 | 109 | if offscreen_again: 110 | # No further checks will be done. 111 | 112 | # TIP: 113 | # A further mod might automagically augment the 114 | # wraplength when the tooltip is too high to be 115 | # kept inside the screen. 116 | y1 = 0 117 | 118 | return x1, y1 119 | 120 | bg = self.bg 121 | pad = self.pad 122 | widget = self.widget 123 | 124 | # creates a toplevel window 125 | self.tw = tk.Toplevel(widget) 126 | 127 | # Leaves only the label and removes the app window 128 | self.tw.wm_overrideredirect(True) 129 | 130 | win = tk.Frame(self.tw, 131 | background=bg, 132 | borderwidth=0) 133 | label = ttk.Label(win, 134 | text=self.text, 135 | justify=tk.LEFT, 136 | background=bg, 137 | relief=tk.SOLID, 138 | borderwidth=0, 139 | wraplength=self.wraplength) 140 | 141 | label.grid(padx=(pad[0], pad[2]), 142 | pady=(pad[1], pad[3]), 143 | sticky=tk.NSEW) 144 | win.grid() 145 | 146 | x, y = tip_pos_calculator(widget, label) 147 | 148 | self.tw.wm_geometry("+%d+%d" % (x, y)) 149 | 150 | def hide(self): 151 | tw = self.tw 152 | if tw: 153 | tw.destroy() 154 | self.tw = None 155 | -------------------------------------------------------------------------------- /src/bio_scan/util.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from l10n import translations as tr 4 | 5 | from bio_scan.globals import bioscan_globals 6 | 7 | def system_distance(coordinate_a: tuple[float, float, float], coordinate_b: tuple[float, float, float]) -> float: 8 | """ 9 | Calculate distance between 3D coordinates 10 | 11 | :param coordinate_a: x, y, z tuple 12 | :param coordinate_b: x, y, z tuple 13 | :return: Calculated distance between a and b 14 | """ 15 | 16 | return math.sqrt((coordinate_a[0] - coordinate_b[0]) ** 2 17 | + (coordinate_a[1] - coordinate_b[1]) ** 2 18 | + (coordinate_a[2] - coordinate_b[2]) ** 2) 19 | 20 | 21 | def translate_colors(color: str) -> str: 22 | """ 23 | Translates color strings 24 | 25 | :param color: Original (UK English) color 26 | :return: Translated color 27 | """ 28 | 29 | match color: 30 | case 'Amethyst': 31 | return tr.tl('Amethyst', bioscan_globals.translation_context) # LANG: Color amethyst 32 | case 'Aquamarine': 33 | return tr.tl('Aquamarine', bioscan_globals.translation_context) # LANG: Color aquamarine 34 | case 'Blue': 35 | return tr.tl('Blue', bioscan_globals.translation_context) # LANG: Color blue 36 | case 'Cobalt': 37 | return tr.tl('Cobalt', bioscan_globals.translation_context) # LANG: Color cobalt 38 | case 'Cyan': 39 | return tr.tl('Cyan', bioscan_globals.translation_context) # LANG: Color cyan 40 | case 'Emerald': 41 | return tr.tl('Emerald', bioscan_globals.translation_context) # LANG: Color emerald 42 | case 'Gold': 43 | return tr.tl('Gold', bioscan_globals.translation_context) # LANG: Color gold 44 | case 'Green': 45 | return tr.tl('Green', bioscan_globals.translation_context) # LANG: Color green 46 | case 'Grey': 47 | return tr.tl('Grey', bioscan_globals.translation_context) # LANG: Color grey 48 | case 'Indigo': 49 | return tr.tl('Indigo', bioscan_globals.translation_context) # LANG: Color indigo 50 | case 'Lime': 51 | return tr.tl('Lime', bioscan_globals.translation_context) # LANG: Color lime 52 | case 'Magenta': 53 | return tr.tl('Magenta', bioscan_globals.translation_context) # LANG: Color magenta 54 | case 'Maroon': 55 | return tr.tl('Maroon', bioscan_globals.translation_context) # LANG: Color maroon 56 | case 'Mauve': 57 | return tr.tl('Mauve', bioscan_globals.translation_context) # LANG: Color mauve 58 | case 'Mulberry': 59 | return tr.tl('Mulberry', bioscan_globals.translation_context) # LANG: Color mulberry 60 | case 'Ocher': 61 | return tr.tl('Ocher', bioscan_globals.translation_context) # LANG: Color ocher 62 | case 'Orange': 63 | return tr.tl('Orange', bioscan_globals.translation_context) # LANG: Color orange 64 | case 'Peach': 65 | return tr.tl('Peach', bioscan_globals.translation_context) # LANG: Color peach 66 | case 'Red': 67 | return tr.tl('Red', bioscan_globals.translation_context) # LANG: Color red 68 | case 'Sage': 69 | return tr.tl('Sage', bioscan_globals.translation_context) # LANG: Color sage 70 | case 'Teal': 71 | return tr.tl('Teal', bioscan_globals.translation_context) # LANG: Color teal 72 | case 'Turquoise': 73 | return tr.tl('Turquoise', bioscan_globals.translation_context) # LANG: Color turquoise 74 | case 'White': 75 | return tr.tl('White', bioscan_globals.translation_context) # LANG: Color white 76 | case 'Yellow': 77 | return tr.tl('Yellow', bioscan_globals.translation_context) # LANG: Color yellow 78 | case _: 79 | return color 80 | 81 | 82 | def translate_body(body_type: str) -> str: 83 | """ 84 | Return translation for a given body type 85 | :param body_type: The ED journal string for a body type 86 | :return: The translated body type 87 | """ 88 | 89 | match body_type: 90 | case 'Icy body': 91 | return tr.tl('Icy body', bioscan_globals.translation_context) # LANG: Body type icy body 92 | case 'Rocky body': 93 | return tr.tl('Rocky body', bioscan_globals.translation_context) # LANG: Body type rocky body 94 | case 'Rocky ice body': 95 | return tr.tl('Rocky ice body', bioscan_globals.translation_context) # LANG: Body type rocky ice body 96 | case 'Metal rich body': 97 | return tr.tl('Metal rich body', bioscan_globals.translation_context) # LANG: Body type metal rich body 98 | case 'High metal content body': 99 | return tr.tl('High metal content body', bioscan_globals.translation_context) # LANG: Body type high metal content body 100 | case _: 101 | return body_type 102 | 103 | 104 | def translate_genus(genus: str) -> str: 105 | """ 106 | Return translation for a given genus 107 | :param genus: The ED journal string for a genus 108 | :return: The translated genus 109 | """ 110 | 111 | match genus: 112 | case 'Aleoida': 113 | return tr.tl('Aleoida', bioscan_globals.translation_context) # LANG: Genus aleoida 114 | case 'Bacterium': 115 | return tr.tl('Bacterium', bioscan_globals.translation_context) # LANG: Genus bacterium 116 | case 'Cactoida': 117 | return tr.tl('Cactoida', bioscan_globals.translation_context) # LANG: Genus cactoida 118 | case 'Clypeus': 119 | return tr.tl('Clypeus', bioscan_globals.translation_context) # LANG: Genus clypeus 120 | case 'Concha': 121 | return tr.tl('Concha', bioscan_globals.translation_context) # LANG: Genus concha 122 | case 'Electricae': 123 | return tr.tl('Electricae', bioscan_globals.translation_context) # LANG: Genus electricae 124 | case 'Fonticulua': 125 | return tr.tl('Fonticulua', bioscan_globals.translation_context) # LANG: Genus fonticula 126 | case 'Fumerola': 127 | return tr.tl('Fumerola', bioscan_globals.translation_context) # LANG: Genus fumerola 128 | case 'Fungoida': 129 | return tr.tl('Fungoida', bioscan_globals.translation_context) # LANG: Genus fungoida 130 | case 'Osseus': 131 | return tr.tl('Osseus', bioscan_globals.translation_context) # LANG: Genus osseus 132 | case 'Recepta': 133 | return tr.tl('Recepta', bioscan_globals.translation_context) # LANG: Genus recepta 134 | case 'Frutexa': 135 | return tr.tl('Frutexa', bioscan_globals.translation_context) # LANG: Genus frutexa 136 | case 'Stratum': 137 | return tr.tl('Stratum', bioscan_globals.translation_context) # LANG: Genus stratum 138 | case 'Tubus': 139 | return tr.tl('Tubus', bioscan_globals.translation_context) # LANG: Genus tubus 140 | case 'Tussock': 141 | return tr.tl('Tussock', bioscan_globals.translation_context) # LANG: Genus tussock 142 | case 'Anemone': 143 | return tr.tl('Anemone', bioscan_globals.translation_context) # LANG: Genus anemone 144 | case 'Amphora Plant': 145 | return tr.tl('Amphora Plant', bioscan_globals.translation_context) # LANG: Genus amphora plant 146 | case 'Bark Mound': 147 | return tr.tl('Bark Mound', bioscan_globals.translation_context) # LANG: Genus bark mound 148 | case 'Brain Tree': 149 | return tr.tl('Brain Tree', bioscan_globals.translation_context) # LANG: Genus brain tree 150 | case 'Crystalline Shards': 151 | return tr.tl('Crystalline Shards', bioscan_globals.translation_context) # LANG: Genus crystalline shards 152 | case 'Sinuous Tubers': 153 | return tr.tl('Sinuous Tubers', bioscan_globals.translation_context) # LANG: Genus sinuous tubers 154 | case _: 155 | return genus 156 | 157 | 158 | def translate_species(species: str) -> str: 159 | """ 160 | Return translation for a given species 161 | :param species: The ED journal string for a species 162 | :return: The translated species 163 | """ 164 | 165 | match species: 166 | case 'Aleoida Arcus': 167 | return tr.tl('Aleoida Arcus', bioscan_globals.translation_context) 168 | case 'Aleoida Coronamus': 169 | return tr.tl('Aleoida Coronamus', bioscan_globals.translation_context) 170 | case 'Aleoida Spica': 171 | return tr.tl('Aleoida Spica', bioscan_globals.translation_context) 172 | case 'Aleoida Laminiae': 173 | return tr.tl('Aleoida Laminiae', bioscan_globals.translation_context) 174 | case 'Aleoida Gravis': 175 | return tr.tl('Aleoida Gravis', bioscan_globals.translation_context) 176 | case 'Luteolum Anemone': 177 | return tr.tl('Luteolum Anemone', bioscan_globals.translation_context) 178 | case 'Croceum Anemone': 179 | return tr.tl('Croceum Anemone', bioscan_globals.translation_context) 180 | case 'Puniceum Anemone': 181 | return tr.tl('Puniceum Anemone', bioscan_globals.translation_context) 182 | case 'Roseum Anemone': 183 | return tr.tl('Roseum Anemone', bioscan_globals.translation_context) 184 | case 'Rubeum Bioluminescent Anemone': 185 | return tr.tl('Rubeum Bioluminescent Anemone', bioscan_globals.translation_context) 186 | case 'Prasinum Bioluminescent Anemone': 187 | return tr.tl('Prasinum Bioluminescent Anemone', bioscan_globals.translation_context) 188 | case 'Roseum Bioluminescent Anemone': 189 | return tr.tl('Roseum Bioluminescent Anemone', bioscan_globals.translation_context) 190 | case 'Blatteum Bioluminescent Anemone': 191 | return tr.tl('Blatteum Bioluminescent Anemone', bioscan_globals.translation_context) 192 | case 'Bacterium Aurasus': 193 | return tr.tl('Bacterium Aurasus', bioscan_globals.translation_context) 194 | case 'Bacterium Nebulus': 195 | return tr.tl('Bacterium Nebulus', bioscan_globals.translation_context) 196 | case 'Bacterium Scopulum': 197 | return tr.tl('Bacterium Scopulum', bioscan_globals.translation_context) 198 | case 'Bacterium Acies': 199 | return tr.tl('Bacterium Acies', bioscan_globals.translation_context) 200 | case 'Bacterium Vesicula': 201 | return tr.tl('Bacterium Vesicula', bioscan_globals.translation_context) 202 | case 'Bacterium Alcyoneum': 203 | return tr.tl('Bacterium Alcyoneum', bioscan_globals.translation_context) 204 | case 'Bacterium Tela': 205 | return tr.tl('Bacterium Tela', bioscan_globals.translation_context) 206 | case 'Bacterium Informem': 207 | return tr.tl('Bacterium Informem', bioscan_globals.translation_context) 208 | case 'Bacterium Volu': 209 | return tr.tl('Bacterium Volu', bioscan_globals.translation_context) 210 | case 'Bacterium Bullaris': 211 | return tr.tl('Bacterium Bullaris', bioscan_globals.translation_context) 212 | case 'Bacterium Omentum': 213 | return tr.tl('Bacterium Omentum', bioscan_globals.translation_context) 214 | case 'Bacterium Cerbrus': 215 | return tr.tl('Bacterium Cerbrus', bioscan_globals.translation_context) 216 | case 'Bacterium Verrata': 217 | return tr.tl('Bacterium Verrata', bioscan_globals.translation_context) 218 | case 'Roseum Brain Tree': 219 | return tr.tl('Roseum Brain Tree', bioscan_globals.translation_context) 220 | case 'Gypseeum Brain Tree': 221 | return tr.tl('Gypseeum Brain Tree', bioscan_globals.translation_context) 222 | case 'Ostrinum Brain Tree': 223 | return tr.tl('Ostrinum Brain Tree', bioscan_globals.translation_context) 224 | case 'Viride Brain Tree': 225 | return tr.tl('Viride Brain Tree', bioscan_globals.translation_context) 226 | case 'Aureum Brain Tree': 227 | return tr.tl('Aureum Brain Tree', bioscan_globals.translation_context) 228 | case 'Puniceum Brain Tree': 229 | return tr.tl('Puniceum Brain Tree', bioscan_globals.translation_context) 230 | case 'Lindigoticum Brain Tree': 231 | return tr.tl('Lindigoticum Brain Tree', bioscan_globals.translation_context) 232 | case 'Lividum Brain Tree': 233 | return tr.tl('Lividum Brain Tree', bioscan_globals.translation_context) 234 | case 'Cactoida Cortexum': 235 | return tr.tl('Cactoida Cortexum', bioscan_globals.translation_context) 236 | case 'Cactoida Lapis': 237 | return tr.tl('Cactoida Lapis', bioscan_globals.translation_context) 238 | case 'Cactoida Vermis': 239 | return tr.tl('Cactoida Vermis', bioscan_globals.translation_context) 240 | case 'Cactoida Pullulanta': 241 | return tr.tl('Cactoida Pullulanta', bioscan_globals.translation_context) 242 | case 'Cactoida Peperatis': 243 | return tr.tl('Cactoida Peperatis', bioscan_globals.translation_context) 244 | case 'Clypeus Lacrimam': 245 | return tr.tl('Clypeus Lacrimam', bioscan_globals.translation_context) 246 | case 'Clypeus Margaritus': 247 | return tr.tl('Clypeus Margaritus', bioscan_globals.translation_context) 248 | case 'Clypeus Speculumi': 249 | return tr.tl('Clypeus Speculumi', bioscan_globals.translation_context) 250 | case 'Concha Renibus': 251 | return tr.tl('Concha Renibus', bioscan_globals.translation_context) 252 | case 'Concha Aureolas': 253 | return tr.tl('Concha Aureolas', bioscan_globals.translation_context) 254 | case 'Concha Labiata': 255 | return tr.tl('Concha Labiata', bioscan_globals.translation_context) 256 | case 'Concha Biconcavis': 257 | return tr.tl('Concha Biconcavis', bioscan_globals.translation_context) 258 | case 'Electricae Pluma': 259 | return tr.tl('Electricae Pluma', bioscan_globals.translation_context) 260 | case 'Electricae Radialem': 261 | return tr.tl('Electricae Radialem', bioscan_globals.translation_context) 262 | case 'Fonticulua Segmentatus': 263 | return tr.tl('Fonticulua Segmentatus', bioscan_globals.translation_context) 264 | case 'Fonticulua Campestris': 265 | return tr.tl('Fonticulua Campestris', bioscan_globals.translation_context) 266 | case 'Fonticulua Upupam': 267 | return tr.tl('Fonticulua Upupam', bioscan_globals.translation_context) 268 | case 'Fonticulua Lapida': 269 | return tr.tl('Fonticulua Lapida', bioscan_globals.translation_context) 270 | case 'Fonticulua Fluctus': 271 | return tr.tl('Fonticulua Fluctus', bioscan_globals.translation_context) 272 | case 'Fonticulua Digitos': 273 | return tr.tl('Fonticulua Digitos', bioscan_globals.translation_context) 274 | case 'Frutexa Flabellum': 275 | return tr.tl('Frutexa Flabellum', bioscan_globals.translation_context) 276 | case 'Frutexa Acus': 277 | return tr.tl('Frutexa Acus', bioscan_globals.translation_context) 278 | case 'Frutexa Metallicum': 279 | return tr.tl('Frutexa Metallicum', bioscan_globals.translation_context) 280 | case 'Frutexa Flammasis': 281 | return tr.tl('Frutexa Flammasis', bioscan_globals.translation_context) 282 | case 'Frutexa Fera': 283 | return tr.tl('Frutexa Fera', bioscan_globals.translation_context) 284 | case 'Frutexa Sponsae': 285 | return tr.tl('Frutexa Sponsae', bioscan_globals.translation_context) 286 | case 'Frutexa Collum': 287 | return tr.tl('Frutexa Collum', bioscan_globals.translation_context) 288 | case 'Fumerola Carbosis': 289 | return tr.tl('Fumerola Carbosis', bioscan_globals.translation_context) 290 | case 'Fumerola Extremus': 291 | return tr.tl('Fumerola Extremus', bioscan_globals.translation_context) 292 | case 'Fumerola Nitris': 293 | return tr.tl('Fumerola Nitris', bioscan_globals.translation_context) 294 | case 'Fumerola Aquatis': 295 | return tr.tl('Fumerola Aquatis', bioscan_globals.translation_context) 296 | case 'Fungoida Setisis': 297 | return tr.tl('Fungoida Setisis', bioscan_globals.translation_context) 298 | case 'Fungoida Stabitis': 299 | return tr.tl('Fungoida Stabitis', bioscan_globals.translation_context) 300 | case 'Fungoida Bullarum': 301 | return tr.tl('Fungoida Bullarum', bioscan_globals.translation_context) 302 | case 'Fungoida Gelata': 303 | return tr.tl('Fungoida Gelata', bioscan_globals.translation_context) 304 | case 'Osseus Fractus': 305 | return tr.tl('Osseus Fractus', bioscan_globals.translation_context) 306 | case 'Osseus Discus': 307 | return tr.tl('Osseus Discus', bioscan_globals.translation_context) 308 | case 'Osseus Spiralis': 309 | return tr.tl('Osseus Spiralis', bioscan_globals.translation_context) 310 | case 'Osseus Pumice': 311 | return tr.tl('Osseus Pumice', bioscan_globals.translation_context) 312 | case 'Osseus Cornibus': 313 | return tr.tl('Osseus Cornibus', bioscan_globals.translation_context) 314 | case 'Osseus Pellebantus': 315 | return tr.tl('Osseus Pellebantus', bioscan_globals.translation_context) 316 | case 'Recepta Umbrux': 317 | return tr.tl('Recepta Umbrux', bioscan_globals.translation_context) 318 | case 'Recepta Deltahedronix': 319 | return tr.tl('Recepta Deltahedronix', bioscan_globals.translation_context) 320 | case 'Recepta Conditivus': 321 | return tr.tl('Recepta Conditivus', bioscan_globals.translation_context) 322 | case 'Stratum Aranaemus': 323 | return tr.tl('Stratum Aranaemus', bioscan_globals.translation_context) 324 | case 'Stratum Excutitus': 325 | return tr.tl('Stratum Excutitus', bioscan_globals.translation_context) 326 | case 'Stratum Paleas': 327 | return tr.tl('Stratum Paleas', bioscan_globals.translation_context) 328 | case 'Stratum Laminamus': 329 | return tr.tl('Stratum Laminamus', bioscan_globals.translation_context) 330 | case 'Stratum Limaxus': 331 | return tr.tl('Stratum Limaxus', bioscan_globals.translation_context) 332 | case 'Stratum Cucumisis': 333 | return tr.tl('Stratum Cucumisis', bioscan_globals.translation_context) 334 | case 'Stratum Tectonicas': 335 | return tr.tl('Stratum Tectonicas', bioscan_globals.translation_context) 336 | case 'Stratum Frigus': 337 | return tr.tl('Stratum Frigus', bioscan_globals.translation_context) 338 | case 'Roseum Sinuous Tubers': 339 | return tr.tl('Roseum Sinuous Tubers', bioscan_globals.translation_context) 340 | case 'Prasinum Sinuous Tubers': 341 | return tr.tl('Prasinum Sinuous Tubers', bioscan_globals.translation_context) 342 | case 'Albidum Sinuous Tubers': 343 | return tr.tl('Albidum Sinuous Tubers', bioscan_globals.translation_context) 344 | case 'Caeruleum Sinuous Tubers': 345 | return tr.tl('Caeruleum Sinuous Tubers', bioscan_globals.translation_context) 346 | case 'Lindigoticum Sinuous Tubers': 347 | return tr.tl('Lindigoticum Sinuous Tubers', bioscan_globals.translation_context) 348 | case 'Violaceum Sinuous Tubers': 349 | return tr.tl('Violaceum Sinuous Tubers', bioscan_globals.translation_context) 350 | case 'Viride Sinuous Tubers': 351 | return tr.tl('Viride Sinuous Tubers', bioscan_globals.translation_context) 352 | case 'Blatteum Sinuous Tubers': 353 | return tr.tl('Blatteum Sinuous Tubers', bioscan_globals.translation_context) 354 | case 'Tubus Conifer': 355 | return tr.tl('Tubus Conifer', bioscan_globals.translation_context) 356 | case 'Tubus Sororibus': 357 | return tr.tl('Tubus Sororibus', bioscan_globals.translation_context) 358 | case 'Tubus Cavas': 359 | return tr.tl('Tubus Cavas', bioscan_globals.translation_context) 360 | case 'Tubus Rosarium': 361 | return tr.tl('Tubus Rosarium', bioscan_globals.translation_context) 362 | case 'Tubus Compagibus': 363 | return tr.tl('Tubus Compagibus', bioscan_globals.translation_context) 364 | case 'Tussock Pennata': 365 | return tr.tl('Tussock Pennata', bioscan_globals.translation_context) 366 | case 'Tussock Ventusa': 367 | return tr.tl('Tussock Ventusa', bioscan_globals.translation_context) 368 | case 'Tussock Ignis': 369 | return tr.tl('Tussock Ignis', bioscan_globals.translation_context) 370 | case 'Tussock Cultro': 371 | return tr.tl('Tussock Cultro', bioscan_globals.translation_context) 372 | case 'Tussock Catena': 373 | return tr.tl('Tussock Catena', bioscan_globals.translation_context) 374 | case 'Tussock Pennatis': 375 | return tr.tl('Tussock Pennatis', bioscan_globals.translation_context) 376 | case 'Tussock Serrati': 377 | return tr.tl('Tussock Serrati', bioscan_globals.translation_context) 378 | case 'Tussock Albata': 379 | return tr.tl('Tussock Albata', bioscan_globals.translation_context) 380 | case 'Tussock Propagito': 381 | return tr.tl('Tussock Propagito', bioscan_globals.translation_context) 382 | case 'Tussock Divisa': 383 | return tr.tl('Tussock Divisa', bioscan_globals.translation_context) 384 | case 'Tussock Caputus': 385 | return tr.tl('Tussock Caputus', bioscan_globals.translation_context) 386 | case 'Tussock Triticum': 387 | return tr.tl('Tussock Triticum', bioscan_globals.translation_context) 388 | case 'Tussock Stigmasis': 389 | return tr.tl('Tussock Stigmasis', bioscan_globals.translation_context) 390 | case 'Tussock Virgam': 391 | return tr.tl('Tussock Virgam', bioscan_globals.translation_context) 392 | case 'Tussock Capillum': 393 | return tr.tl('Tussock Capillum', bioscan_globals.translation_context) 394 | case 'Crystalline Shards': 395 | return tr.tl('Crystalline Shards', bioscan_globals.translation_context) 396 | case 'Bark Mound': 397 | return tr.tl('Bark Mound', bioscan_globals.translation_context) 398 | case 'Amphora Plant': 399 | return tr.tl('Amphora Plant', bioscan_globals.translation_context) 400 | case _: 401 | return species 402 | --------------------------------------------------------------------------------