Skip to content

Commit

Permalink
Added No Mans Sky mod and the ability to OCR text from the galactic
Browse files Browse the repository at this point in the history
screen.
  • Loading branch information
BradfordBach committed May 25, 2019
1 parent a626212 commit 8102b0c
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 15 deletions.
132 changes: 120 additions & 12 deletions NMS_Locator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = save_file.read()[:-1]
parsed_save = json.loads(save_file_string)

Expand All @@ -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(galactic_address)
else:
return None


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


def get_latest_save_file():
all_save_files = []
Expand All @@ -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() == date.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')
Expand All @@ -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))]
return(':'.join(parts))


def load_log():
try:
os.makedirs(log_dir)
Expand All @@ -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"):
try:
Expand All @@ -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')

try:
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:
writer.writeheader()

writer.writerow(completed_bh_pairing)


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"):
Expand All @@ -133,30 +191,80 @@ def add_years_to_location_log():
except ValueError:
pass


def load_config():
config = configparser.ConfigParser()
config.read('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 = {}
try:
while True:
if get_file_mod_time() not in mod_times:
mod_times.append(get_file_mod_time())
get_current_location()
if len(mod_times) >= 200:
mod_times.clear()
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)
ocr_info.clear()

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

# 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)

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

time.sleep(15)
except KeyboardInterrupt:
pass

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()
add_years_to_location_log()
location_log, log_loc = load_log()
create_bulk_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...")
run_location_gatherer()
gather_system_info()
47 changes: 45 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 www.blackholesuns.com or the subreddit [/r/NMSBlackHoleSuns](www.reddit.com/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 www.blackholesuns.com or the subreddit [/r/NMSBlackHoleSuns](www.reddit.com/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:
https://github.com/UB-Mannheim/tesseract/wiki

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 added _BHS_Helper.Rehab_Submarine.UI.pak
Binary file not shown.
6 changes: 5 additions & 1 deletion config.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
[SETTINGS]
PLAY_NOTIFICATION = False
PLAY_NOTIFICATION = False
OCR = True
CSV_DIRECTORY = DEFAULT
SCREENSHOT_DIRECTORY = None
TESSERACT_LOC = C:\Program Files\Tesseract-OCR\tesseract.exe
79 changes: 79 additions & 0 deletions ocr.py
Original file line number Diff line number Diff line change
@@ -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(Image.open(os.path.join(dirpath, 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')
print(line_split[0].strip())
if "REGION" in line:
print(line.split(':')[1].strip())
if "Sell:" in line:
print(line.split('//')[-1].strip())


def ocr_screenshot(file, tesseract):
pytesseract.pytesseract.tesseract_cmd = tesseract
screenshot_text = pytesseract.image_to_string(Image.open(file))
filename = os.path.splitext(os.path.basename(file))[0]
with open("cropped" + os.sep + filename + '.log', "w") as ocr_log:
ocr_log.write(screenshot_text)
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"]
#print(line.split('//')[-1].strip())
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
else:
return system_info
else:
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
8 changes: 8 additions & 0 deletions resolutions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
1366,768:41,417,466,645
1280,1024:38,556,436,859
1600,900:48,489,546,755
1680,1050:50,570,573,881
1920,1080:57,586,656,907
2560,1440:76,782,875,1209
2560,1421:77,772,876,1193
3840,2160:115,1173,1311,1814
Loading

0 comments on commit 8102b0c

Please sign in to comment.