Skip to content

Instantly share code, notes, and snippets.

@andrewschultz
Created March 5, 2024 08:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andrewschultz/2f8a700f2edbfc5348f8f70eb90c84cc to your computer and use it in GitHub Desktop.
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)
#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()
#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