import glob import os import json import time import sys, getopt class UniversalCartographicsHistory: def __init__(self): self.log_path = os.path.expanduser('~') + \ '\Saved Games\Frontier Developments\Elite Dangerous' self.output = 'Universal Cartographics Histroy Scan.txt' self.verbose = 2 self.history = {'cmdr': 0, 'count_fdfm': 0, 'count_fd': 0, 'count_fm': 0, 'cmdrs': {}} self.cmdr_id = '' self.cmdr = '' self.current_scan = {'count': 0, 'count_fdfm': 0, 'count_fd': 0, 'count_fm': 0} self.keys = ['fdfm', 'fd', 'fm'] self.labels = ['First Discovery + Mapped', 'First Discovery', 'First Mapped'] self.system_name = '' self.body_name = '' def check_ed_log_path(self, arg=''): """check_ed_log_path Check if the log path is a valid path Args: arg (str, optional): User-defined path. Defaults to ''. Raises: ValueError: if the path doesn't exist or is a file """ if arg: if os.path.isdir(arg): uc_history.log_path = os.path.realpath(arg) return else: print(f'{arg} is not a valid path! Use default {self.log_path} instead') if not os.path.isdir(self.log_path): raise ValueError(f'{self.log_path} is not a valid path!') def read_journals(self): """read_journals Loop through all journal files in the log_path folder Raises: ValueError: if there is no journal file in the folder """ # find all ED logs journals = glob.glob(self.log_path + "/Journal.*.log") # sort by the last modified time in ascending order journals.sort(key=os.path.getmtime) # if there is no journal file found if len(journals) == 0: raise ValueError(f'No ED log file found in {self.log_path}') # loop through all files for journal in journals: print(f'Reading {os.path.basename(journal)}') linenumber = 0 with open(journal, 'r', encoding='UTF-8') as j: line = j.readline() while line: line = j.readline() linenumber += 1 if linenumber % 100 == 0: print(f' Reading line no. {linenumber}') self.read_event(line) self.write_output() def read_event(self, line): """read_event Read events related to UC discoveries in the journals Args: line (string): the line contain a single ED event """ # find the current commander name and ID if line[38:57] == '"event":"Commander"': cmdr = json.loads(line) self.check_cmdr(cmdr) # find current system name elif line[38:55] == '"event":"FSDJump"': jump = json.loads(line) self.add_to_history(jump) # check FSS relate event # there are two types scans within this type # auto scan: sensor picks up body information automatically # detailed: maunally done in FSS UI # they do exactly the same thing elif line[38:52] == '"event":"Scan"': fss = json.loads(line) self.check_body(fss) # check DSS elif line[38:63] == '"event":"SAAScanComplete"': dss = json.loads(line) self.check_dss(dss) def check_cmdr(self, cmdr): """check_cmdr check if the current CMDR name is already recorded [extended_summary] Args: cmdr (dict): json contains CMDR info """ self.cmdr = cmdr['Name'] self.cmdr_id = cmdr['FID'] # add cmdr to the history if self.cmdr_id not in self.history['cmdrs']: self.history['cmdr'] += 1 # you can delete the CMDR and re-create one with the same name # so the ID is used to distinguish between accounts self.history['cmdrs'][self.cmdr_id] = {'name': self.cmdr, 'count_fdfm': 0, 'count_fd': 0, 'count_fm': 0, 'discoveries': {}} def add_to_history(self, jump): """add_to_history Add the scan results of the previous system to the history if there is any new discovery `FSDJump` event marks the arrival of the next system, so it's a good time to check the results in the previous one Args: jump (dict): json contains new system info """ # if there are new findings if self.current_scan['count']: # update total counts fdfm = self.current_scan['count_fdfm'] fd = self.current_scan['count_fd'] fm = self.current_scan['count_fm'] self.update_counter(fdfm, fd, fm) # add discovered bodies names to the history history = self.history['cmdrs'][self.cmdr_id]['discoveries'] if self.system_name not in history: history[self.system_name] = {} for i in self.keys: if i in self.current_scan: history[self.system_name][i] = self.current_scan[i] # in case the system has been visited before and scanned again else: for i in self.keys: if i in self.current_scan: history[self.system_name][i].update(self.current_scan[i]) # reset the dict for scans in the current system self.current_scan = {'count': 0, 'count_fdfm': 0, 'count_fd': 0, 'count_fm': 0} # update system name self.system_name = jump['StarSystem'] def check_body(self, fss): """check_body Check body info for a FSS scan Args: fss (dict): json of a FSS scan entry """ # add undiscovered bodies to fd bodyname = fss['BodyName'] if not self.system_name: self.system_name = fss['StarSystem'] # check unmapped bodies # well-known bodies are shown as undiscovered in game but already mapped # so this way will rule out the false positives for planets and moons # unless you are indeed the first one to map the bodies if not fss['WasMapped']: if not fss['WasDiscovered']: if 'fd' not in self.current_scan: self.current_scan['fd'] = {bodyname: None} else: self.current_scan['fd'][bodyname] = None self.current_scan['count'] += 1 self.current_scan['count_fd'] += 1 else: # belts and stars are not mapable # only planets and moons should be added to the unmapped set if 'Belt' not in bodyname and 'StarType' not in fss and not \ ('d' in self.current_scan and bodyname in self.current_scan['d']): if 'unmapped' not in self.current_scan: self.current_scan['unmapped'] = {bodyname: None} else: self.current_scan['unmapped'][bodyname] = None if 'mapped' in self.current_scan and bodyname in self.current_scan['mapped']: self.check_dss(fss) # sometimes for an already mapped planet # the FSS results will show up again after DSS and the status becomes unmapped # this part is used to fix this edge case else: # d for already discovered and mapped if 'd' not in self.current_scan: self.current_scan['d'] = {bodyname: None} else: self.current_scan['d'][bodyname] = None def check_dss(self, dss): """check_dss Check if a DSS'd planets is unmapped or not Args: dss (dict): json of a DSS scan entry """ bodyname = dss['BodyName'] if not self.system_name: self.system_name = dss['StarSystem'] # if this planet is undiscovered if 'fd' in self.current_scan and bodyname in self.current_scan['fd']: # remove from first discoery del self.current_scan['fd'][bodyname] self.current_scan['count_fd'] -= 1 self.current_scan['count_fdfm'] += 1 if 'fdfm' not in self.current_scan: self.current_scan['fdfm'] = {bodyname: None} else: self.current_scan['fdfm'][bodyname] = None # if this planet is discovered but not mapped elif 'unmapped' in self.current_scan and bodyname in self.current_scan['unmapped']: self.current_scan['count'] += 1 self.current_scan['count_fm'] += 1 if 'fm' not in self.current_scan: self.current_scan['fm'] = {bodyname: None} else: self.current_scan['fm'][bodyname] = None # sometimes the DSS results show in the log first followed by the FSS results # temporarily save the candidite to a dict called mapped else: if 'mapped' not in self.current_scan: self.current_scan['mapped'] = {bodyname: None} else: self.current_scan['mapped'][bodyname] = None def update_counter(self, fdfm, fd, fm): """update_counter Update the total counts of discoveries for first discovery and first mapped, first discovery only and first mapped only in the previous system Args: fdfm (int): first discovery and first mapped fd (int): first discovery fm (int): first mapped """ self.history['count_fdfm'] += fdfm self.history['count_fd'] += fd self.history['count_fm'] += fm self.history['cmdrs'][self.cmdr_id]['count_fdfm'] += fdfm self.history['cmdrs'][self.cmdr_id]['count_fd'] += fd self.history['cmdrs'][self.cmdr_id]['count_fm'] += fm def write_output(self): """write_output Write summary to self.output file """ with open(self.output, 'w') as summary: # print header text = (f'# Scan finished on {time.ctime()}\n' '# Note: Well-known bodies are shown as undiscovered in game. While I can rule out\n' '# these false positives for planets, as they are already mapped but undiscovered.\n' "# I'm not able to do anything to the belts and stars as they are not mapable. As\n" "# a result, you'll see their names in the scan.\n") print(text, file=summary) # if there are more than 1 CNDR found, print a summary of all CMDR's stats if len(self.history['cmdrs']) > 1: output = (f"Number of CMDR's Found {self.history['cmdr']}\n" f"Total Number of First Discovery + First Mapped: {self.history['count_fdfm']}\n" f"Total Number of First Discovery: {self.history['count_fd']}\n" f"Total Number of First Mapped: {self.history['count_fm']}\n") print(output, file=summary) # for each CMDR for val in self.history['cmdrs'].values(): # if no discoery is made if len(val) == 0: continue # summary for this CMDR output = (f"Commander Name: {val['name']}\n" f"Total Number of First Discovery + First Mapped: {val['count_fdfm']}\n" f"Total Number of First Discovery: {val['count_fd']}\n" f"Total Number of First Mapped: {val['count_fm']}\n") print(output, file=summary) # details on each system for system, discoveries in val['discoveries'].items(): # system summary sys_summary = f'System Name: {system}\n' for idx, key in enumerate(self.keys): if key in discoveries: label = self.labels[idx] n = len(discoveries[key]) sys_summary += f' {label}: {n}' if n > 1: sys_summary += ' Bodies\n' else: sys_summary += ' Body\n' if self.verbose > 1: # sort all body names in alphabet bodies = sorted(list(discoveries[key])) for body in bodies: sys_summary += f' {body}\n' print(sys_summary, file=summary) def get_opt(args, history): """get_opt Get CLI options Args: args (list): list of CLI arguments history (UniversalCartographicsHistory): args should be passed here """ def print_help(msg=''): """print_help print CLI help messages Args: msg (str, optional): Used when wrong options are passed. Defaults to ''. """ text = ('Usage:\n' ' python.exe uchistory.py [OPTION...]\n\n' 'Help Option:\n' ' -h, --help Show this message\n\n' 'E:D Log Path:\n' ' -l, --log-path= Path to the E:D log files\n' ' Make sure the path is quoted if there are spaces within the path. See the default below\n' " Defaults to 'C:\\Users\\username\\Saved Games\\Frontier Developments\\Elite Dangerous'\n\n" 'Output File:\n' ' -o, --output= Output filename and path to save the scan results\n' " Defaults to 'Universal Cartographics Histroy Scan.txt' in the current folder\n\n" 'Output Level:\n' ' -v, --verbose= Output verbose level\n' ' Level 1 lists only the number of bodies discovered/mapped in the system\n' ' Level 2 lists the body names as well\n' ' Defaults to 2\n') print(text) sys.exit(msg) long_opts = ['help', 'log-path=', 'output=', 'vervbose='] try: opts, args = getopt.getopt(args, 'hl:o:v:', long_opts) except getopt.GetoptError: print_help('Wrong CLI options given. Check usage above.') for opt, arg in opts: if opt in ('-h', '--help'): print_help() elif opt in ("-l", "--log-path"): uc_history.check_ed_log_path(arg) elif opt in ("-o", "--output"): try: f = open(arg, 'w') f.close() history.output = arg except FileNotFoundError: print((f'Output {arg} provided contains wrong path. ' 'Write to Universal Cartographics Histroy Scan.txt in the current folder.')) history.output = os.path.realpath(history.output) elif opt in ("-v", "--verbose"): try: arg = int(arg) if arg < 1 or arg > 2: print(f"verbose={arg} isn't supported. Use default verbose=2") else: history.verbose = arg except ValueError: print(f"{arg} isn't a valid integer number. Use default verbose=2") print('Log path is', history.log_path) print('Output file is', history.output) print('Verbose level is', history.verbose) if __name__ == "__main__": uc_history = UniversalCartographicsHistory() get_opt(sys.argv[1:], uc_history) uc_history.read_journals()