Skip to content

Commit

Permalink
Add EDSM parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
Silarn committed Apr 28, 2023
1 parent 8012e62 commit 96f02d7
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 22 deletions.
37 changes: 21 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# BioScan plugin for [EDMC]

<img src="BioScan-DSS.png" align="left" style="margin-right: 10px">
BioScan is a utility for Explorers and Exobiologists that attempts to determine the possible value range of biological
signals on bodies. It uses data such as the atmosphere, gravity, volcanism, surface temperature, body type, and local
star type to make the best guess as to what types of flora will be present.
<img src="BioScan-DSS.png">

BioScan is a utility for explorers and exobiologists that attempts to determine the possible species (and therefore
value) of biological signals on bodies. It uses data such as the atmosphere, gravity, volcanism, surface temperature,
body type, nearby nebulae, and local star type to make the best guess as to what types of flora will be present.

Once done, it will summarize the possible value ranges for all qualifying genera and species.

<img src="BioScan-SAA-Prog.png" align="right" style="margin-left: 10px">
<img src="BioScan-SAA-Prog.png">

After you've mapped a planet with biological signals, it will then pare down the list to the detected genera.
And finally, once you've started to scan each species it will display the final type and value of the sample as well as
indicate the scan progress.
Expand All @@ -16,24 +18,27 @@ Once fully analysed, the total system value (and possible first footfall value)

### Navigation

<img src="BioScan-Scan-Distance.png" align="left" style="margin-right: 10px">The top of the pane will track all relevant
bodies in the system, including a shorthand for the body type and the number of signals detected there. This can help
you quickly determine a DSS target.
The top of the pane will track all relevant bodies in the system, including a shorthand for the body type and the number
of signals detected there. This can help you quickly determine a DSS target.

BioScan will track your movements and show just the relevant species data if you are currently located at a body of
interest, to help reduce clutter and scrolling. After you initiate a scan, you will get a display of the required sample
distance and your current minimum distance to a previous sample.
distance and your current minimum distance to a previous sample, which is updated in real time.

It will reset your scan progress if the previous scan wasn't completed and you start a different genus. It can also
It will reset your scan progress if the previous scan wasn't completed and you start a different species. It can also
track scans with the composition scanner and will lock in the final species of the genus without requiring
you to scan biologicals one at a time. In this way you can lock in a species and value while competing the analysis of
another lifeform.

### Please Note
### EDSM Integration

Once per system, you can attempt to fetch any data from EDSM. This is helpful if you log out in the middle of scanning a
system and lose the data from the previous session. Unfortunately, EDSM does not track biological signals, so you will
have to manually look up signals for planets that haven't been mapped yet. The journals do resend the detailed signal
info from previously mapped planets, though you may need to travel to them to trigger it.

EDMC does not parse old system logs so this tool has trouble when revisiting systems. I may use EDSM data to fill in
some holes (if enabled) but it will still be missing signal data. Fortunately, reentering a system does usually trigger
the detailed scan signal data to be logged in the journal again. But not the FSS signal data.
So if you were in the middle of scanning samples on a planet, fetching data from EDSM should get you the species list
again. However, your scan progress will be lost so completed species will display as unscanned.

## Requirements
* EDMC version 5 and above
Expand All @@ -58,12 +63,12 @@ Procedurally generated nebula reference star coordinates pulled from [EDSM]'s AP

## Roadmap

* ~~Currently implementing a system to detect the presence of a nebula~~
* Refinements to species requirements
* ~~Currently implementing a system to detect the presence of a nebula~~
* ~~Track when near a planet and focus the data view for that planet~~
* ~~Add settings for visibility and display options~~
* ~~Add info about sample distance requirements~~
* Possibly utilize EDSM data to fill in missing data (revisiting a system)
* ~~Possibly utilize EDSM data to fill in missing data (revisiting a system)~~
* ~~Investigate plausibility of getting species from codex scans (Ship / SRV)~~

## License
Expand Down
129 changes: 129 additions & 0 deletions bio_scan/body_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,132 @@ def body_check(bodies: dict[str, BodyData], extra: bool = False):
return True
return False


def map_edsm_type(edsm_class):
match edsm_class:
case 'Earth-like world':
return 'Earthlike body'
case 'Metal-rich body':
return 'Metal rich body'
case 'High metal content world':
return 'High metal content body'
case 'Rocky Ice world':
return 'Rocky ice body'
case 'Class I gas giant':
return 'Sudarsky class I gas giant'
case 'Class II gas giant':
return 'Sudarsky class II gas giant'
case 'Class III gas giant':
return 'Sudarsky class III gas giant'
case 'Class IV gas giant':
return 'Sudarsky class IV gas giant'
case 'Class V gas giant':
return 'Sudarsky class V gas giant'
case 'Gas giant with ammonia-based life':
return 'Gas giant with ammonia based life'
case 'Gas giant with water-based life':
return 'Gas giant with water based life'
case 'Helium-rich gas giant':
return 'Helium rich gas giant'
case _:
return edsm_class


def parse_edsm_star_class(subtype):
star_class = ""
subclass = "0"
match subtype:
case 'White Dwarf (D) Star':
star_class = 'D'
case 'White Dwarf (DA) Star':
star_class = 'DA'
case 'White Dwarf (DAB) Star':
star_class = 'DAB'
case 'White Dwarf (DAO) Star':
star_class = 'DAO'
case 'White Dwarf (DAZ) Star':
star_class = 'DAZ'
case 'White Dwarf (DB) Star':
star_class = 'DB'
case 'White Dwarf (DBZ) Star':
star_class = 'DBZ'
case 'White Dwarf (DBV) Star':
star_class = 'DBV'
case 'White Dwarf (DO) Star':
star_class = 'DO'
case 'White Dwarf (DOV) Star':
star_class = 'DOV'
case 'White Dwarf (DQ) Star':
star_class = 'DQ'
case 'White Dwarf (DC) Star':
star_class = 'DC'
case 'White Dwarf (DCV) Star':
star_class = 'DCV'
case 'White Dwarf (DX) Star':
star_class = 'DX'
case 'CS Star':
star_class = 'CS'
case 'C Star':
star_class = 'C'
case 'CN Star':
star_class = 'CN'
case 'CJ Star':
star_class = 'CJ'
case 'CH Star':
star_class = 'CH'
case 'CHd Star':
star_class = 'CHd'
case 'MS-type Star':
star_class = 'MS'
case 'S-type Star':
star_class = 'S'
case 'Neutron Star':
star_class = 'N'
case 'Black Hole':
star_class = 'H'
case 'Supermassive Black Hole':
star_class = 'SupermassiveBlackHole'

return star_class, subclass


def map_edsm_atmosphere(atmosphere: str) -> str:
if atmosphere.endswith("Ammonia"):
return "Ammonia"
if atmosphere.endswith("Water"):
return "Water"
if atmosphere.endswith("Carbon dioxide"):
return "CarbonDioxide"
if atmosphere.endswith("Sulphur dioxide"):
return "SulphurDioxide"
if atmosphere.endswith("Nitrogen"):
return "Nitrogen"
if atmosphere.endswith("Water-rich"):
return "WaterRich"
if atmosphere.endswith("Methane-rich"):
return "MethaneRich"
if atmosphere.endswith("Ammonia-rich"):
return "AmmoniaRich"
if atmosphere.endswith("Carbon dioxide-rich"):
return "CarbonDioxideRich"
if atmosphere.endswith("Methane"):
return "Methane"
if atmosphere.endswith("Helium"):
return "Helium"
if atmosphere.endswith("Silicate vapour"):
return "SilicateVapour"
if atmosphere.endswith("Metallic vapour"):
return "MetallicVapour"
if atmosphere.endswith("Neon-rich"):
return "NeonRich"
if atmosphere.endswith("Argon-rich"):
return "ArgonRich"
if atmosphere.endswith("Neon"):
return "Neon"
if atmosphere.endswith("Argon"):
return "Argon"
if atmosphere.endswith("Oxygen"):
return "Oxygen"
if atmosphere == "No atmosphere":
return "None"
return atmosphere
87 changes: 81 additions & 6 deletions load.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
# Licensed under the [GNU Public License (GPL)](http://www.gnu.org/licenses/gpl-2.0.html) version 2 or later.

import sys
import threading
import tkinter as tk
from tkinter import ttk
from urllib.parse import quote

import requests
import semantic_version
import math

Expand All @@ -20,13 +24,14 @@

from RegionMap import findRegion

from bio_scan.body_data import BodyData, get_body_shorthand, body_check
from bio_scan.body_data import BodyData, get_body_shorthand, body_check, parse_edsm_star_class, \
map_edsm_type, map_edsm_atmosphere
from bio_scan.bio_data import bio_genus, bio_types, get_species_from_codex, region_map, guardian_sectors
from bio_scan.format_util import Formatter

logger = get_main_logger()

VERSION = '1.0.0'
VERSION = '1.1.0'

this = sys.modules[__name__] # For holding module globals
this.formatter = Formatter()
Expand All @@ -41,6 +46,7 @@
this.label = None
this.values_label = None
this.total_label = None
this.edsm_button = None
this.bodies = dict[str, BodyData]()
this.odyssey = False
this.game_version = semantic_version.Version.coerce('0.0.0.0')
Expand All @@ -61,6 +67,10 @@
this.body_location = 0
this.starsystem = ''

this.edsm_session = None
this.edsm_bodies = None
this.fetched_edsm = False


def plugin_start3(plugin_dir):
return plugin_start()
Expand All @@ -74,6 +84,7 @@ def plugin_start():
def plugin_app(parent: tk.Frame) -> tk.Frame:
parse_config()
this.frame = tk.Frame(parent)
this.frame.bind('<<BioScanEDSMData>>', edsm_data)
this.label = tk.Label(this.frame)
this.label.grid(row=0, column=0, columnspan=2, sticky=tk.N)
this.scroll_canvas = tk.Canvas(this.frame, height=80, highlightthickness=0)
Expand All @@ -97,6 +108,9 @@ def plugin_app(parent: tk.Frame) -> tk.Frame:
this.scrollbar.grid(row=1, column=1, sticky=tk.NSEW)
this.total_label = tk.Label(this.frame)
this.total_label.grid(row=2, column=0, columnspan=2, sticky=tk.N)
this.edsm_button = tk.Label(this.frame, text='Fetch EDSM Data', fg="white", cursor="hand2")
this.edsm_button.grid(row=3, columnspan=2, sticky=tk.EW)
this.edsm_button.bind("<Button-1>", lambda e: edsm_fetch())
update_display()
theme.register(this.values_label)
return this.frame
Expand Down Expand Up @@ -186,6 +200,64 @@ def log(*args):
logger.debug(args)


def edsm_fetch():
thread = threading.Thread(target=edsm_worker, name='EDSM worker', args=(this.starsystem,))
thread.daemon = True
thread.start()


def edsm_worker(system_name):
if not this.edsm_session:
this.edsm_session = requests.Session()

try:
r = this.edsm_session.get('https://www.edsm.net/api-system-v1/bodies?systemName=%s' % quote(system_name),
timeout=10)
r.raise_for_status()
this.edsm_bodies = r.json() or {}
except:
this.edsm_bodies = None

this.frame.event_generate('<<BioScanEDSMData>>', when='tail')


def edsm_data(event):
if this.edsm_bodies is None:
return

for body in this.edsm_bodies.get('bodies', []):
bodyname_insystem = get_bodyname(body['name'])
if body['type'] == 'Star':
if body['isMainStar']:
this.main_star_name = "{}{}".format(
parse_edsm_star_class(body['subType']),
body['luminosity']
)
this.main_star_id = body['bodyId']

elif body['type'] == 'Planet':
try:
if bodyname_insystem not in this.bodies:
this.bodies[bodyname_insystem] = BodyData(bodyname_insystem)
planet_type = map_edsm_type(body['subType'])
this.bodies[bodyname_insystem].set_type(planet_type)
this.bodies[bodyname_insystem].set_distance(body['distanceToArrival'])
this.bodies[bodyname_insystem].set_id(body['bodyId'])
this.bodies[bodyname_insystem].set_atmosphere(map_edsm_atmosphere(body['atmosphereType']))
if body['volcanismType'] == "No volcanism":
volcanism = ""
else:
volcanism = body['volcanismType'].lower().capitalize() + " volcanism"
this.bodies[bodyname_insystem].set_volcanism(volcanism)
this.bodies[bodyname_insystem].set_gravity(body['gravity'])
this.bodies[bodyname_insystem].set_temp(body['surfaceTemperature'])

except Exception as e:
logger.error(e)
this.fetched_edsm = True
update_display()


def scan_label(scans: int):
match scans:
case 0:
Expand Down Expand Up @@ -401,12 +473,12 @@ def get_bodyname(fullname: str = "") -> str:
def journal_entry(
cmdr: str, is_beta: bool, system: str, station: str, entry: dict[str, any], state: dict[str, any]
) -> str:
this.starsystem = system
if entry['event'] == 'Fileheader' or entry['event'] == 'LoadGame':
this.odyssey = entry.get('Odyssey', False)
this.game_version = semantic_version.Version.coerce(entry.get('gameversion'))

elif entry['event'] == 'Location':
this.starsystem = entry['StarSystem']
this.coordinates = entry['StarPos']

elif entry['event'] == 'FSDJump':
Expand All @@ -417,6 +489,7 @@ def journal_entry(
this.location_name = ""
this.location_id = -1
this.location_state = ""
this.fetched_edsm = False
this.bodies = {}
this.coordinates = entry['StarPos']
update_display()
Expand All @@ -428,9 +501,6 @@ def journal_entry(
if entry['BodyID'] == this.main_star_id or entry['BodyID'] == 0:
this.main_star_type = "{}{}".format(entry['StarType'], entry['Luminosity'])
if 'PlanetClass' in entry:
if 'StarSystem' in entry:
this.starsystem = entry['StarSystem']

if bodyname_insystem not in this.bodies:
body_data = BodyData(bodyname_insystem)
else:
Expand Down Expand Up @@ -602,6 +672,11 @@ def get_distance() -> float:


def update_display() -> None:
if this.fetched_edsm or this.starsystem == "":
this.edsm_button.grid_remove()
else:
this.edsm_button.grid()

detail_text = ""
bio_bodies = dict(sorted(dict(filter(lambda fitem: fitem[1].get_bio_signals() > 0 or len(fitem[1].get_flora()) > 0, this.bodies.items())).items(),
key=lambda item: item[1].get_id()))
Expand Down

0 comments on commit 96f02d7

Please sign in to comment.