Skip to content
Browse files

Added No Mans Sky mod and the ability to OCR text from the galactic

  • Loading branch information...
BradfordBach committed May 25, 2019
1 parent a626212 commit 8102b0c5b5289e831a22cbeaff03e230ed84d1b1
Showing with 312 additions and 15 deletions.
  1. +120 −12
  2. +45 −2
  3. BIN _BHS_Helper.Rehab_Submarine.UI.pak
  4. +5 −1 config.ini
  5. +79 −0
  6. +8 −0 resolutions.txt
  7. +55 −0
@@ -5,10 +5,16 @@
import pyperclip
import winsound
import configparser
import ocr
import csv
from shutil import copyfile
from copy import deepcopy
from screenshot_crop import crop_screenshot

def get_current_location():
with open(save, "r", encoding='utf-8') as save_file:

def get_current_location(last_save):
with open(last_save, "r", encoding='utf-8') as save_file:
save_file_string =[:-1]
parsed_save = json.loads(save_file_string)

@@ -27,8 +33,14 @@ def get_current_location():
if config.getboolean('SETTINGS', 'PLAY_NOTIFICATION'):
winsound.PlaySound('notification.wav', winsound.SND_FILENAME)

def get_file_mod_time():
return os.path.getmtime(save)
return None

def get_file_mod_time(file):
return os.path.getmtime(file)

def get_latest_save_file():
all_save_files = []
@@ -39,16 +51,36 @@ def get_latest_save_file():
latest_save = max(all_save_files, key=os.path.getmtime)
return latest_save

def get_latest_screenshot():
all_screenshot_files = []
for (dirpath, dirnames, filenames) in os.walk(config.get('SETTINGS', 'SCREENSHOT_DIRECTORY')):
for dir in dirnames:
if dir != 'thumbnails':
for file in filenames:
if file[-4:] == ".jpg":
all_screenshot_files.append(os.path.join(dirpath, file))
latest_save = max(all_screenshot_files, key=os.path.getmtime)

# if latest file is already cropped it means we've already processed it, so skip it
if os.path.isfile("cropped" + os.sep + os.path.splitext(os.path.basename(file))[0] + "_cropped.png"):
latest_save = None

return latest_save

def check_if_address_exists(galactic_address):
if any(galactic_address in entry for entry in location_log):
return True

def is_date_in_log(date):
for entry in location_log:
if entry[0].date() ==
return True
return False

def enter_address_into_log(galactic_address, dt):
with open(log_loc, "a") as log:
log.write(dt.strftime("%B %d %Y %I:%M:%S %p") + ',' + galactic_address + '\n')
@@ -61,11 +93,13 @@ def enter_address_into_log(galactic_address, dt):

location_log.append([dt, galactic_address])

def format_galaxtic_coord(x, y, z, ssi):
fmt = "{0:0{1}X}"
parts = [str(fmt.format(x + 2047, 4)), str(fmt.format(y + 127, 4)), str(fmt.format(z + 2047, 4)), str(fmt.format(ssi,4))]

def load_log():
@@ -86,6 +120,7 @@ def load_log():

return location_log, log_dir + os.sep + "location_log.log"

def create_bulk_log():
if not os.path.isfile(log_dir + os.sep + "bulk.log"):
@@ -111,6 +146,29 @@ def create_bulk_log():
for address in addresses:
bulk.write(address + '\n')

def update_csv(completed_bh_pairing):
csv_dir = log_dir

if config.get('SETTINGS', 'CSV_DIRECTORY') != 'DEFAULT':
csv_dir = config.get('SETTINGS', 'CSV_DIRECTORY')

open(csv_dir + os.sep + "black_holes.csv", 'r')
except IOError:
open(csv_dir + os.sep + "black_holes.csv", 'w')

fieldnames = ['bh-address', 'bh-system', 'bh-region', 'bh-econ', 'bh-life',
'exit-address', 'exit-system', 'exit-region', 'exit-econ', 'exit-life']

with open(log_dir + os.sep + "black_holes.csv", 'a') as bhfile:
writer = csv.DictWriter(bhfile, lineterminator='\n', fieldnames=fieldnames)
if os.path.getsize(log_dir + os.sep + "black_holes.csv") == 0:


def add_years_to_location_log():
# Add years to location_log file, because I forgot to do this on the first release
if os.path.isfile(log_dir + os.sep + "location_log.log"):
@@ -133,30 +191,80 @@ def add_years_to_location_log():
except ValueError:

def load_config():
config = configparser.ConfigParser()'config.ini')
if config.get('SETTINGS', 'SCREENSHOT_DIRECTORY') == 'None' and config.getboolean('SETTINGS', 'OCR') is True:
raise SystemExit("SCREENSHOT_DIRECTORY has not been set in your config.ini, this must be set to run")
return config

def run_location_gatherer():
def gather_system_info():
galactic_address = None
system_info = {}
completed_system_info = {}
completed_bh_pairing = {}
while True:
if get_file_mod_time() not in mod_times:
if len(mod_times) >= 200:
last_modded_save = get_latest_save_file()
last_modded_save_time = os.path.getmtime(last_modded_save)
if last_modded_save_time >= int(round(time.time())) - 30:
galactic_address = get_current_location(last_modded_save)
if config.getboolean('SETTINGS', 'OCR'):
last_screenshot = get_latest_screenshot()
if last_screenshot:
cropped_screen = crop_screenshot(last_screenshot)
ocr_info = ocr.ocr_screenshot(cropped_screen, config.get('SETTINGS', 'TESSERACT_LOC'))
if ocr_info:
system_info = deepcopy(ocr_info)
for k, v in system_info.items():
print(k + ':',v)

if galactic_address and system_info:
completed_system_info = deepcopy(system_info)
completed_system_info.update({'address': galactic_address})
galactic_address = None

# log completed system info for safe keeping
for k, v in completed_system_info.items():
print(k + ':',v)

if completed_system_info['address'][-2:] == '79' and not completed_bh_pairing:
#This is a black hole system!
for key in list(completed_system_info):
completed_bh_pairing['bh-' + key] = completed_system_info.pop(key)
elif completed_system_info['address'][-2:] != '79' and not completed_bh_pairing:
print("Starting from a system that is not a black hole is not supported by this tool at this time.")
elif completed_system_info['address'][-2:] == '79' and completed_bh_pairing:
# This is if both the start and exit are black holes, this shouldn't really happen unless near the center
# and if they are near the center it will be the same black hole
raise SystemExit("Got two consecutive black hole addresses", completed_bh_pairing,
completed_system_info, "Cannot reconcile this. DOES NOT COMPUTE")
elif completed_system_info['address'][-2:] != '79' and completed_bh_pairing:
# This is the second half of a bh pairing
for key in list(completed_system_info):
completed_bh_pairing['exit-' + key] = completed_system_info.pop(key)

if 'bh-address' in completed_bh_pairing.keys() and 'exit-address' in completed_bh_pairing.keys():
print('Completed BH pairing!')

except KeyboardInterrupt:

log_dir = os.getenv('LOCALAPPDATA') + os.sep + 'Programs' + os.sep + "NMS Locator"

log_dir = os.getenv('LOCALAPPDATA') + os.sep + 'Programs' + os.sep + "NMS Locator"
config = load_config()
location_log, log_loc = load_log()
save = get_latest_save_file()
mod_times = []
print("Waiting for new locations every 15 seconds.\nNew locations will be copied to clipboard and displayed...")
@@ -1,11 +1,54 @@
# NMS Locator

NMS locator is a python application for use with the No Man's Sky video game that will log the galactic coordinates of the player to the screen and add it to the clipboard. This was created to aid in the logging and cataloging of black holes as part of the Black Hole Suns project. More info about the Black Hole Suns can be found at or the subreddit [/r/NMSBlackHoleSuns](
NMS locator is a python application for use with the No Man's Sky video game that will log the galactic coordinates of the player to the screen and add it to the clipboard. With a combination of mods, and ocr, this program is also able to read in data needed for mapping black holes and save them into a csv file for easy upload.
This was created to aid in the logging and cataloging of black holes as part of the Black Hole Suns project. More info about the Black Hole Suns can be found at or the subreddit [/r/NMSBlackHoleSuns](

# How to use
# Setup

NMS Locator needs a few things in order to properly OCR screenshots.

1. Download and install Tesseract available here:

I used the `4.10.2019.0314` 64 bit setup but any of them should work

2. Put the `_BHS_Helper.Rehab_Submarine.UI.pak` mod file into your mods folder for NMS and enable mods. This mod is used to put a solid background behind the system information on the galaxy screen.
3. Open the `config.ini` file and update the following information:
- Set `SCREENSHOT_DIRECTORY` to wherever your screenshots for NMS are automatically saved
- Set `TESSERACT_LOC` to the exe file you installed in step 1. This is typically in `C:\Program Files\Tesseract-OCR\tesseract.exe`
- Set `CSV_DIRECTORY` to wherever you want the output of the CSV file to be stored. By default this is stored in the `%APPDATA%\Local\Programs\NMS Locator` folder
4. In the `resolutions.txt` file this details the area the program crops out of your screenshots, the first two numbers are your screen resolution. Make sure that whatever resolution you play NMS at is listed in the list. If it is not, you will need to add a line to the file in the following format:
- `screen-width`,`screen-height`, `top x pos`, `top y pos`, `bottom x pos`, `bottom y pos`
- The `top x pos` and the `top y pos` is the top left corner of black box surrounding the system info in the galaxy screen.
- The `bottom x pos` and the `bottom y pos` is the bottom right corner of the black box
- These two values will allow the crop function of the program to properly cut out only the info it needs to complete OCR making it more accurate
- NOTE: Smaller resolutions will have more errors in the OCR process, the larger you can run the game at, the more accurate the results will be.

# How to use
#### Non-OCR
You can either execute the raw python code or download the executable found on the releases tab of this page.
When running the program it will:
- Automatically display the latest location of the player from the latest modified No Man Sky's save file.
- Put the galactic coordinates into your clipboard, so you can copy and paste it into the spreadsheet or website for tracking black holes.
- Keep a running log of all solar systems you have visited which can be found in your AppData\Local\NMS Locator folder.

#### OCR
The OCR will read screenshots from your screenshot directory and pair them with your last save location and display the resulting output and when a black hole entry and exit points are both available, it will pair them up and store them in the black_holes.csv file located in the `CSV_DIRECTORY` folder explained in setup

To do this, requires a bit of a standard workflow. Here is the optimum steps in order to get the proper entry and exit points paired correctly.
1. Start the Locator.
2. Make sure the first time you save automatically it is in a black hole system, or a system that is already logged.
3. Take a picture of your galaxy screen, either before or after you save, it will associate the first pairing of a valid screenshot and a valid galactic address via the save file as a single "system"
4. Travel through the blackhole, you should be able to wander throughout the current system or older systems without any effect, but if you go to a new non-blackh hole system it will think you went through the blackhole.
5. Once you're at the exit system, take a screenshot of galaxy page, and save your game by landing your ship somewhere.

That's pretty much it, if you follow those steps it will then log those entry-exit system info correctly, pair them, and put them into the CSV you can upload to the black hole tracking website.
You should be able to stop the system anytime after you log a complete black hole entry and exit system and have it pick up wherever you want if you want to deviate from this, like to just do general exploring, traveling to unlogged systems via portal and saving, ect.

## Important Limitations:
- This locator will likely incorrectly label items if you are running closer to the core, since it expects the exit point of the black hole to be a non black hole system
- Likewise the locator expects each black hole entry **and exit** to be a unique universe address. If it is already logged, it will simply ignore the address.
- The locator only stores OCR details after a full black hole exit and entry system are logged. If you quit after running the OCR on the black hole system, that OCR data will be lost.
- OCR tools are notoriously incorrect, and while I have made some effort to make sure the readings are more accurate, in some cases it may make mistakes. The universal address is calculated and not a part of the OCR, so it will always be correc.
- The locator is a work in progress, and there may be issues

Binary file not shown.
@@ -1,2 +1,6 @@
OCR = True
TESSERACT_LOC = C:\Program Files\Tesseract-OCR\tesseract.exe
@@ -0,0 +1,79 @@
import pytesseract
from PIL import Image
import PIL.ImageOps
import os

def ocr_many():
for(dirpath, dirnames, filenames) in os.walk("cropped"):
for file in filenames:
print(os.path.join(dirpath, file))

screenshot_text = pytesseract.image_to_string(, file)))
screenshot_lines = screenshot_text.split('\n')
for line in screenshot_lines:
if "System" in line:
line_split = line.split('System')
if '-l' in line_split:
line_split = line_split.replace('-l', '-I')
if '|' in line_split:
line_split = line_split.replace('|', 'I')
if "REGION" in line:
if "Sell:" in line:

def ocr_screenshot(file, tesseract):
pytesseract.pytesseract.tesseract_cmd = tesseract
screenshot_text = pytesseract.image_to_string(
filename = os.path.splitext(os.path.basename(file))[0]
with open("cropped" + os.sep + filename + '.log', "w") as ocr_log:
if screenshot_text:
screenshot_lines = screenshot_text.split('\n')
system_info = {'system': None, 'region': None, 'econ': None, 'life': None}
for line in screenshot_lines:
if "System" in line:
line_split = line.split('System')
system_info['system'] = fix_common_ocr_issues(line_split[0].strip())
if "REGION" in line:
system_info['region'] = fix_common_ocr_issues(line.split(':')[1].strip())
if "Sell:" in line:
econ_values = ["Declining", "Destitute", "Failing", "Fledgling", "Low Supply", "Struggling", "Unpromising", "Unsuccessful",
"Adequate", "Balanced", "Comfortable", "Developing", "Medium Supply", "Promising", "Satisfactory", "Sustainable",
"Advanced", "Affluent", "Booming", "Flourishing", "High Supply", "Opulent", "Prosperous", "Wealthy"]
if line.split('//')[-1].strip() in econ_values:
system_info['econ'] = line.split('//')[-1].strip()
elif "Med" in line.split('//')[-1].strip():
system_info['econ'] = 'Medium Supply'
if "Gek" in line:
system_info['life'] = "Gek"
if "Korvax" in line:
system_info['life'] = "Korvax"
if "Vy'keen" in line:
system_info['life'] = "Vy'keen"

if not system_info['system'] or not system_info['region']:
print('Skipping latest screenshot, no system or region info found.')
return None
return system_info
print('Skipping latest screenshot, no system or region info found.')
return None

def fix_common_ocr_issues(text):
if '-l' in text:
text = text.replace('-l', '-I')
if '|' in text:
text = text.replace('|', 'I')
if text[:1] == 'l':
text[:1] = 'I'
if ' l ' in text:
text = text.replace(' l ', ' I ')

return text
@@ -0,0 +1,8 @@

0 comments on commit 8102b0c

Please sign in to comment.
You can’t perform that action at this time.