Created
March 5, 2024 08:02
-
-
Save andrewschultz/2f8a700f2edbfc5348f8f70eb90c84cc to your computer and use it in GitHub Desktop.
Python file to decode training progress for Nox Archaist binary. Requires basic_na_func.py as well: tra.py q = process nox-(today)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#sys.path.append('c:/Users/Andrew/Documents/github/NA/source_code') | |
#import basic_na_func | |
from collections import defaultdict | |
bin_to_name = defaultdict(str) | |
areas = defaultdict(str) | |
mer_txt = "c:/Users/Andrew/Documents/github/NA/source_code/mer.txt" | |
area_txt = "c:/Users/Andrew/Documents/github/NA/source_code/areas.txt" | |
food_start = 0x70bf | |
gold_start = food_start + 2 | |
char_start = 0x11670 | |
current_disk = "los137.hdv" | |
disk_item_start = [0x3dc00, 0x3ec00] | |
def read_area_text(my_file): | |
with open(my_file) as file: | |
for line in file: | |
if '=' not in line: continue | |
if ';' in line: break | |
ary = line.strip().split("=") | |
areas[int(ary[0], 16)] = ary[1] | |
def read_item_names(my_file): | |
with open(my_file) as file: | |
for line in file: | |
if '=' not in line: continue | |
if ';' in line: break | |
ary = line.strip().split("=") | |
bin_to_name[ary[0].lower()] = ary[1] | |
# bin_to_name['00.99'] = '<one-time shipwreck gold>' | |
try: | |
read_area_text(area_txt) | |
except: | |
print("failed to read in area.txt") | |
exit() | |
try: | |
read_item_names(mer_txt) | |
except: | |
print("failed to read in mer.txt") | |
exit() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#tra.py | |
# track training | |
import os | |
import re | |
from collections import defaultdict | |
import struct | |
import sys | |
import pendulum | |
import colorama | |
sys.path.append('c:/Users/Andrew/Documents/github/NA/source_code') | |
import basic_na_func | |
save_state_name = "fight" | |
at_max_overall = False | |
disk_item_display = False | |
ignore_skill = defaultdict(int) | |
player_start = 0x11670 | |
loc_offset = 0x6d5c | |
food_gold_offset = basic_na_func.food_start | |
torch_lockpicks = 0x490 | |
level_cost = [0, 0, 6, 10, 21, 57, 72, 108, 162, 242, 363] | |
level_next = [0, 100, 300, 800, 2000, 7000, 12000, 17000, 23000, 32000, 65535] | |
need_threshold = 1 | |
global_cost = 0 | |
columns = ['LV/NEXT ', '$COST', 'DELTA', 'MEL', 'RAN', 'DOD', 'CRI', 'PIK'] | |
expanded = [ 'MELEE', 'RANGE', 'DODGE' 'CRITS', 'LOKPK' ] | |
header_offset = 3 | |
party_members = 6 | |
num_skills = 5 | |
my_slot = 0 | |
min_level = 11 | |
count_might = False | |
class_map = { | |
"Nox Xujamar": "m", | |
"Nox Aetros": "m", | |
"Bridgit": "m", | |
"X": "m", | |
"A": "m", | |
"AI": "m", | |
"Nox Thun": "m", | |
"Blade": "t", | |
"Cutter": "l", | |
"Phyrra": "x", | |
"Alluvion": "x", | |
"Kat": "x", | |
"Young Rev": "x", | |
"NPC $68": "x", | |
} | |
class_skill = { | |
"m": [1, 2, 2, 2, 0], | |
"f": [2, 0, 0, 2, 0], | |
"t": [1, 2, 2, 2, 0], | |
"l": [1, 2, 2, 2, 2], | |
"x": [0, 0, 0, 0, 0] # person to be replaced later | |
} #could also have level_until but this is tricky code, maybe not worth it | |
need_skill = [] | |
this_cost = [0, 0, 0, 0, 0] | |
def usage(): | |
print("{:x}/{:x} = food/gold".format(food_gold_offset, food_gold_offset+1)) | |
print("x/y = {:x}/{:x}".format(loc_offset, loc_offset+1)) | |
print("torches/lockpicks = {:x}/{:x}".format(torch_lockpicks, torch_lockpicks+1)) | |
print("Player 1 start={:x} + 80 for each next player".format(player_start)) | |
exit() | |
def get_classes_from_file(): | |
party_class_string = "" | |
party_names = [] | |
global party_members | |
temp_thief = -1 | |
got_lockpicker = False | |
for i in range(0, party_members): | |
x = f.read(0x80) | |
my_start = 0x4b | |
char_name = "" | |
while x[my_start] and my_start < 0x80: | |
char_name += chr(x[my_start] & 0x7f) | |
my_start += 1 | |
if char_name not in class_map: | |
print("Hardcoded check for class_map failed by name. Latest party member {} does not have a class.".format(char_name)) | |
party_members = i | |
break | |
if class_map[char_name] == 'l': | |
got_lockpicker = True | |
if class_map[char_name] == 't': | |
if x[0x25] > 0x14 or x[0x26] > 0: | |
temp_thief = i | |
party_class_string += class_map[char_name] | |
party_names.append(char_name) | |
if temp_thief > -1 and not got_lockpicker: | |
print("Changing thief", temp_thief, "to lockpicker since they have experience/training.", x[0x25], x[0x26]) | |
class_map[temp_thief] = 'l' | |
if party_class_string.count('t') > 0 and party_class_string.count('l') == 0: | |
print("Defaulting {} thief, {}, to lockpicking.".format("only" if party_class_string.count('t') == 0 else "first", party_names[party_class_string.index('t')])) | |
party_class_string = party_class_string.replace('t', 'l', 1) | |
if party_class_string.count('l') > 1: | |
temp_string = party_class_string[::-1] | |
temp_string = temp_string.replace('l', 't', temp_string.count('l') - 1)[::-1] | |
party_class_string = temp_string | |
for i in range(0, party_members): | |
this_class = party_class_string[i] | |
if this_class in class_skill: | |
need_skill.append(class_skill[this_class]) | |
else: | |
print("Oops, no class map for", this_class, "or no class skill") | |
def show_training_needs(level_offset = 0): | |
global at_max_overall | |
at_max_overall = True | |
x = f.read(0x80) | |
if x[0] == 0xaa: return 0 | |
y = [] | |
cur_level = x[1] + level_offset | |
if cur_level == 11: return 0 | |
my_start = 0x4b | |
my_string = "" | |
next_exp = level_next[cur_level] - (x[5] + 256 * x[6]) | |
delta = 0 | |
level_of = level_cost[cur_level] | |
total_cost = 0 | |
column_values = [] | |
if x[1] != 0xa: | |
at_max_overall = False | |
for w in range(0, num_skills): | |
improvable = int(x[0x2b+w]) + 6 * level_offset | |
y.append("{}{:2}{}".format(colorama.Fore.CYAN if improvable else '', improvable, colorama.Style.RESET_ALL if improvable else '') if need_skill[i][w] >= need_threshold and columns[w+header_offset] not in ignore_skill else ('xx' if need_skill[i][w] else '--')) | |
column_values.append("{:3d}+{:2d}/{:2d}".format(x[0x19+3*w], x[0x1a+3*w], x[0x1b+3*w])) | |
if need_skill[i][w] >= need_threshold and columns[w+header_offset] not in ignore_skill: | |
total_cost += level_of * improvable | |
this_cost[w] += level_of * improvable | |
try: | |
delta += (level_cost[cur_level+1] - level_of) * improvable | |
except: | |
pass | |
stat_full = ["{:2}".format(int(w)) for w in x[0x19:0x19+18]] | |
z = [] | |
start_name = 0x4b | |
my_name = '' | |
while start_name < 0x80: | |
if not x[start_name]: break | |
my_name += chr(x[start_name] & 0x7f) | |
start_name += 1 | |
#x[1] below because we really need to see current level | |
global out_string | |
out_string += "{:15s}".format(my_name) + ' |{:2d}/{:5s}|{:5d}|{:5d}|'.format(x[1], "DONE" if cur_level == len(level_next)-1 else ("NEXT" if next_exp < 0 else str(next_exp)), total_cost, delta) + ' ' + '| '.join(y) + '|' + ' ' + ' |'.join(column_values) + "\n" | |
global quick_string | |
if not level_offset: | |
quick_string += "{:s} {:d}/{:s} ".format(my_name.strip(), x[1], str(next_exp)) | |
global global_cost | |
global_cost += total_cost | |
return total_cost | |
cmd_count = 1 | |
my_wildcard = "" | |
while cmd_count < len(sys.argv): | |
got_one = False | |
arg = sys.argv[cmd_count].lower() | |
if arg == '?': usage() | |
if arg[0] == '-' and arg.count('-') == 1: | |
arg = arg[1:] | |
if arg[:2] == 'i:': | |
disk_item_display = True | |
my_wildcard = arg[2:] | |
elif arg == 'bl': | |
class_map["Blade"] = "l" | |
class_map["Cutter"] = "t" | |
elif arg == 'cu': | |
class_map["Blade"] = "t" | |
class_map["Cutter"] = "l" | |
elif arg[:2] == 'ia' or arg[:2] == 'ai': | |
disk_item_display = True | |
my_wildcard = "(mage|thief|protect|(t. | throwing) star| staff|staff of|wand)" | |
elif arg == 'q': | |
save_state_name = "nox-" + pendulum.now().format("MMDD") | |
ignore_skill['MEL'] = True | |
print("Looking for", save_state_name) | |
elif arg[0] == '=': | |
save_state_name = arg[1:] | |
elif arg[0] == 'h': | |
try: | |
need_threshold = int(arg[1:]) | |
print("New threshold", need_threshold) | |
except: | |
print("H requires number after.") | |
else: | |
for x in columns[3:]: | |
if x.lower().startswith(arg[0]): | |
ignore_skill[x] = True | |
print("Ignoring", x) | |
got_one = True | |
if not got_one: | |
print("Invalid parameter", got_one) | |
print("? for usage") | |
cmd_count += 1 | |
if not os.path.exists(save_state_name): | |
sb = os.path.basename(save_state_name) | |
save_state_name = os.path.join("c:/emu/apple/{}".format(sb)) | |
try: | |
f = open(save_state_name, "rb") | |
except: | |
sys.exit("Can't open", save_state_name) | |
def who_equipped(some_num): | |
first_binary = "{:06b}".format(some_num>>2) | |
return first_binary[::-1] | |
def print_items_from_disk(slot = 0, wildcard = ""): | |
disk_item_offset = basic_na_func.disk_item_start[slot] | |
f = open(basic_na_func.current_disk, "rb") | |
f.seek(disk_item_offset) | |
itypes = defaultdict(str) | |
needed = defaultdict(str) | |
itypes[0] = "useless" | |
my_array = [] | |
while f.tell() < disk_item_offset + 0x360: | |
my_ary = f.read(6) | |
if my_ary[0] == 0xfe and my_ary[1] == 0xfe: break | |
itm_type = "{:02x}.{:02x}".format(my_ary[0] & 0x3, my_ary[1]) | |
if basic_na_func.bin_to_name[itm_type].startswith("#"): continue | |
webin = who_equipped(my_ary[3]) | |
webin2 = who_equipped(my_ary[4]) | |
whats_equipped = "{} unequipped, {} equipped: {}".format(my_ary[2] - webin.count('1') - webin2.count('1'), webin.count('1'), webin) if my_ary[3]>>2 else "{:d} (no equip)".format(my_ary[2]) | |
whats_equipped += " second hand equipped: {}".format(webin2) if my_ary[4]>>2 else "" | |
type_string = "" | |
item_type = int(my_ary[5]) | |
if item_type & 0x20: | |
type_string += "(unsellable)" | |
item_type = item_type & 0xdf | |
if item_type in itypes: | |
item_type = itypes[item_type] | |
else: | |
needed[item_type] = True | |
if re.search(wildcard, basic_na_func.bin_to_name[itm_type].lower()): | |
my_array.append(basic_na_func.bin_to_name[itm_type] + " " + whats_equipped) | |
for x in sorted(my_array, reverse = True): | |
print(x) | |
# for x in sorted(needed): print(x) | |
if disk_item_display: | |
print_items_from_disk(wildcard = my_wildcard) | |
exit() | |
still_to_go = 0 | |
get_class_from = True | |
f.seek(player_start) | |
if get_class_from: | |
get_classes_from_file() | |
f.seek(player_start) | |
level_after = False | |
out_string = "" | |
quick_string = "" | |
for i in range(0, party_members): | |
still_to_go += show_training_needs() | |
additional_color = '' | |
if not still_to_go: | |
if at_max_overall: | |
print(colorama.Fore.RED + " **** MAXED OUT AT LEVEL 10 ****" + colorama.Style.RESET_ALL) | |
sys.exit() | |
print(colorama.Fore.RED + " **** NOTE: the table is for the level after..." + colorama.Style.RESET_ALL) | |
additional_color = colorama.Fore.YELLOW | |
else: | |
print(colorama.Fore.GREEN + "There is still stuff to do." + colorama.Style.RESET_ALL) | |
print(' ' * 16 + '|' + '|'.join(columns) + '| ' + '|'.join([" CUR " + x + " " for x in columns[header_offset:]])) | |
if not still_to_go: | |
out_string = "" | |
f.seek(player_start) | |
for i in range(0, party_members): | |
still_to_go += show_training_needs(1) | |
if additional_color: | |
out_string = out_string.replace(colorama.Fore.CYAN, '').replace(colorama.Style.RESET_ALL, '') | |
print(additional_color + out_string.strip() + colorama.Style.RESET_ALL) | |
print(quick_string) | |
else: | |
print(out_string.strip()) | |
for i in range(0, num_skills): | |
if columns[i+header_offset] not in ignore_skill and this_cost[i]: | |
print(columns[i+header_offset], "costs", this_cost[i]) | |
f.seek(food_gold_offset) | |
my_food = struct.unpack('H', f.read(2))[0] | |
my_gold = struct.unpack('H', f.read(2))[0] | |
if global_cost: | |
print(global_cost, "global cost", my_gold, "gold to pay for it. You are", "ahead" if my_gold > global_cost else "short", abs(my_gold - global_cost), "gold") | |
elif my_gold: | |
print(my_gold, "gold to burn. Yay.") | |
else: | |
print("You are broke but happy.") | |
if not my_gold: | |
print("NOTE: the gold offset may be wrong. It is currently {:2x}.".format(food_gold_offset)) | |
f.seek(loc_offset) | |
x_coord = ord(f.read(1)) | |
y_coord = ord(f.read(1)) | |
f.seek(torch_lockpicks) | |
torches = ord(f.read(1)) | |
lockpicks = ord(f.read(1)) | |
print("Your location =", x_coord, ",", y_coord, "torches =", torches, "lockpicks =", lockpicks, "food=", my_food) | |
f.close() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment