Should have done this a long time ago too

This commit is contained in:
Zergling_man 2022-12-21 15:22:44 +11:00
commit 4e6936cec3
6 changed files with 358 additions and 0 deletions

21
README Normal file
View File

@ -0,0 +1,21 @@
Rank Code Personal Database.
How to use:
Currently there is no real UI, so start by running python in this dir and `import codegen`.
Config file is in ~/.config/swat.cfg. If you want it somewhere else, open codegen.py and edit line 6. Do this BEFORE `import codegen`. If you've already imported it, quit python and reopen it.
Setup:
Execute `codegen.add_user('{YOUR IGN}')`. This will create the config file if it doesn't exist. You can add as many users as you like if you want to have multiple profiles for whatever reason (I cannot think of a reason, I just added it for fun).
When you run it later, it will automatically load the conf.
General use:
To load a code, use `codegen.read_code('0000-0000-0000-0000')`. You can put it in without the hyphens, also you can use anything in there, doesn't have to be hyphens. I recommend having something in between because it's helpful for checking the code is right.
To load a bunch of codes at once, do `codegen.mass_add()`, it just calls `read_code` in a loop. It stops if you give it a blank line.
To generate codes, use `codegen.generate('class/line')`, where you give it class/gun/armour/trait/spec/talent. Look in tables.py for the names you should be using; it is not case sensitive, at least. Also gun is optional unless it's mav/wm. (Note if you try to gen the wrong gun on a class, my gen will not stop you. The code will probably be invalid 🤷.)
To check your progress, use `codegen.ranks()`. It's not very pretty yet, it's a work in progress.
All of these operate on `codegen.active_profile`, which is a number. If you look at `codegen.conf`, you can see the numbers associated with each name(s). Set `codegen.active_profile={NUM}` to switch. I'll make a thing for that later too.
Every time you feed a code into it, it will automatically save your progress. You can manually override that if you want (with `codegen.read_code({CODE},False)`), and manually call `codegen.save_conf()` after. I don't know why you would want this, maybe the file write is very slow or something. Or you're putting in cheated codes and you don't want to save them. It will still update your running config but it just won't update the file so when you restart it it'll be gone.

143
codegen.py Normal file
View File

@ -0,0 +1,143 @@
import encode, decode
import tables
import os
conf={}
confp=os.path.expanduser('~/.config/swat.cfg') # This won't work on Windows. You'll need to tweak this to run it on Windows. Not my problem.
defaults={'classes':[(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(0,0),(0,0),(1,3),(1,3),(1,3)],'guns':[(1,3),(1,3),(1,3),(1,3),(1,3),(0,0),(0,0),(0,0)],'armour':[(1,3),(1,3),(1,3),(1,3)],'traits':[(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3)],'specs':[(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3)],'talents':[(12,12),(1,3),(1,3),(1,3),(1,3),(1,3),(1,3)]}
def add_user(name):
namehash=encode.hash_name(name) # Store the hash so that if they request a different name that happens to have the same hash, it can just be stored together.
if namehash in conf : conf[namehash]['names'].add(name)
else: conf[namehash]=defaults.copy()|{'names':{name}}
save_conf()
global active_profile
active_profile=namehash
def parse_conf_line(line):
a=line.split(' ')
out=[]
for n in a:
b=n.split('/')
out.append((int(b[0]),int(b[1])))
return out
def load_conf():
if not os.path.exists(confp): return
a=open(confp).read()
for n in a.split('\n\n'):
lines=n.split('\n')
header=lines[0].split(' ')
nhash=int(header[0]); names=set(header[1:]) # Makes deduping way easier
stuff={k:parse_conf_line(v) for k,v in zip(tables.displays_str[:-1],lines[1:])}
conf[nhash]={'names':names}|stuff
update_all_talents()
global active_profile
active_profile=nhash
def unparse_conf_line(line):
return ' '.join([str(n[0])+'/'+str(n[1]) for n in line])
def save_conf():
write=''
for k,v in conf.items():
write+=' '.join([str(k)]+list(v['names']))+'\n'
for n in map(lambda x:unparse_conf_line(v[x]),tables.displays_str[:-1]):
write+=n+'\n'
write+='\n'
write=write[:-2]
open(confp,'w').write(write)
def update_all_talents():
for profile in conf:
cl=dict(enumerate(conf[profile]['classes']))
t=defaults['talents'].copy()
conf[profile]['talents']=t
for n in range(1,7):
t[n]=update_talent(n,cl)
def update_talents(clas,profile=None):
if profile is None: profile=active_profile
cl=dict(enumerate(conf[profile]['classes']))
for talent in tables.clas[clas][1:]: # Don't care about courage
conf[profile]['talents'][talent]=update_talent(talent,cl)
def update_talent(talent,classes):
a=enumerate(tables.clas); b=filter(lambda x:talent in x[1],a); c=list(map(lambda x:x[0],b)); d=filter(lambda x:x[0] in c,classes.items()); rel_classes=list(map(lambda x:x[1],d))
ranks,caps=[],[]
for n in rel_classes: ranks.append(n[0]); caps.append(n[1])
ranks,caps=sorted(ranks),sorted(caps)
lon=len(ranks)//2+len(ranks)%2
return (ranks[-lon],caps[-lon])
def read_code(code,save=True):
data=list(decode.decode(code,active_profile))
check,validator=data.pop(-1),data.pop(-1)
talent=data.pop(5)
hero=data[0:6]
show=list(map(lambda x:x[0][x[1]],zip(tables.displays,hero)))
rank,cap=data[6:8]
if cap<4: cap=tables.cap[cap]
else: cap=rank
data=data[8:]
if rank<9: show[-1]='Hidden' # Don't spoil the talent
print(show)
print(str(rank)+'/'+str(cap))
print(data)
if not check: print('No name given, code validation not performed')
else:
if check!=validator: print('code did not validate:',validator,check); return
else: print('code validated')
if not save: return
for thing in zip(tables.displays_str[:-1],hero):
saved=conf[active_profile][thing[0]][thing[1]]
saved=(max(saved[0],rank),max(saved[1],cap))
conf[active_profile][thing[0]][thing[1]]=saved
update_talents(hero[0])
save_conf()
def mass_add():
a=input('>')
while a:
try: read_code(a)
except: pass
a=input('>')
def generate(officer=None):
pro=conf[active_profile]
if officer is None: officer=input('> ')
pieces=officer.split('/')
c=pieces[0].lower()
if 'mav' in c:
gun=c[0]
pieces[0]='mav'
elif 'wm' in c:
gun=c[0:2]
pieces[0]='wm'
else: gun=''
out=[]
rank,cap=12,12
for n in zip(pieces,tables.displays_nogun,tables.displays_nogun_str):
ind=lookup_piece(n[0],n[1])
rank,cap=min(rank,pro[n[2]][ind][0]),min(cap,pro[n[2]][ind][1])
out.append(ind)
cap=(cap-(cap<10)+(cap>10))//3 # Derp
if gun: out.insert(1,lookup_piece(gun,displays.guns))
else: out.insert(1,tables.gunmaps_wrapped[out[0]])
out[-1]=tables.clas[out[0]].index(out[-1])
return encode.encode(active_profile,*out,rank,cap)
def lookup_piece(piece,table):
if len(piece)<3: p=piece.upper()
else: p=piece.capitalize() # Bloody Americans
return table.index(p)
def ranks():
for n,m in zip(tables.displays_str,tables.displays):
a=zip(m,conf[active_profile][n])
for o in a:
print(f'{o[0]}: {o[1][0]}/{o[1][1]}',end=', ')
print()
load_conf()

55
decode.py Normal file
View File

@ -0,0 +1,55 @@
import shared
import tables
import encode
from functools import reduce
def decode(code,name=''): # This is going to be a huge function
code=shared.delimit(code) # It didn't end up being that huge
code=shared.scramble(code,True)
code,validator=shared.combine(code)
xp=int(code[-1]); seed=int(code[0])
code=code[1:-1]
data=unzip(code)
xp+=data.pop(0)
rank,cap,cob,lsa,rem,clas,armour,trait,spec,talent=data
cob,lsa=bool(cob),bool(lsa)
rank,cap,key,moh,pcc=unadjust_rank(rank,cap)
if seed%2: seed-=1; clas+=10
if clas==6: clas=[8,9,6][armour]; armour=3
check=''
oldclas=clas # Whoops this all needs to be done before globalising talent
gun=tables.gunmaps[clas]
clas-=(clas>8)+min(4,max(0,clas-10)) # WM and mav adjustments
gtalent=tables.clas[clas][talent] # Globalise it lol
# And do it *before* validation because it needs gtalent passed in now
if name:
check=encode.validate_data(name, oldclas,armour,trait,spec,gtalent, rank,cap,xp, key,moh,pcc,cob,lsa,rem)
rank+=1 # This has to come after extracting medals
return clas,gun,armour,trait,spec,talent,gtalent,rank,cap,xp,key,moh,pcc,cob,lsa,rem,seed,validator,check
############################
# SUPPORTING FUNCTIONS BELOW
############################
def unzip(code):
code=int(code)
out=[]
weight=reduce(lambda x,y:x*y,tables.weights)
for n in list(zip(tables.weights,['xp//10','rank','cap','cob','lsa','rem','clas','armour','trait','spec','talent']))[::-1]:
shared.trace(code,weight,n,out)
out.append(code//weight)
code%=weight
weight//=n[0]
out[-1]*=10 # This is XP
return out[::-1]
def unadjust_rank(rank,cap):
key,moh,pcc=False,False,False
if rank>8:
pcc=bool((cap+1)%2)
cap=2+(cap+1)//2
if rank>11: # Actually need to check this on the way out
key=rank>12
moh=bool((rank-11)%2)
rank=11
return rank,cap,key,moh,pcc

76
encode.py Normal file
View File

@ -0,0 +1,76 @@
import shared
import random
import tables
def encode(name,clas,gun,armour,trait,spec,talent,rank=-1,cap=-1,xp=0, key=False,moh=False,pcc=False,cob=False,lsa=False,rem=False,seed=-1):
if rank==-1: # actually means gun wasn't given, so shift them
gun,armour,trait,spec,talent,rank=-1,gun,armour,trait,spec,talent
gtalent=tables.clas[clas][talent]
if clas>9: clas+=5 # Shift tech/Alice up to their positions - always
# ... And do it before modifying clas below.
if gun!=-1:
#encode gun into class
if clas==9: clas+=gun+1 # Because 9 outside is Mav, and 9 inside is GLWM
if clas==8: clas+=gun-5
rank-=1 # This actually DOES affect part 2 for some fucking reason
cap=max(cap,(rank)//3+(rank>9)) #Sanity-check
code1=encode_data(clas,armour,trait,spec,talent,rank,cap,xp, key,moh,pcc,cob,lsa,rem,seed)
code2=validate_data(name,clas,armour,trait,spec,gtalent,rank,cap,xp, key,moh,pcc,cob,lsa,rem)
shared.trace(code1,code2)
code=shared.combine(code1,code2)
shared.trace(code)
return shared.delimit(shared.scramble(code),'-')
def encode_data(clas,armour,trait,spec,talent,rank,cap=-1,xp=0, key=False,moh=False,pcc=False,cob=False,lsa=False,rem=False,seed=-1):
if clas==6: armour=2 # Borg is heavy borg
if clas==8: clas=6; armour=0 # LR WM is light borg
if clas==9: clas=6; armour=1 # GL WM is med borg
seed,clas=rand(seed,clas)
rank,cap=adjust_rank(rank,cap,key,moh,pcc)
code1=0
weight=1
for n in list(zip(tables.weights,[xp//10,rank,cap,cob,lsa,rem,clas,armour,trait,spec,talent])):
shared.trace(code1,weight,n[0],n[1])
weight*=n[0]
code1+=n[1]*weight
return str(seed)+shared.fill0s(code1,9)+str(xp%10)
def validate_data(name,clas,armour,trait,spec,talent,rank,cap=0,xp=0, key=False,moh=False,pcc=False,cob=False,lsa=False,rem=False):
# gtalent is now looked up outside, before class modification - because I've adjusted that table to be more useful in general.
if isinstance(name,str): name=hash_name(name)
fac1=[171,142,175,157,167,150,149,151,153,165]
fac2=[169,170,166,173,158,177,161,180,186,159] # I don't understand these, but I hope they work.
code2=name*(spec+1) +(trait+4)*(trait+6) +(rank+1)*(xp+1) +(talent+1)*43*fac1[name%10] -(clas+1)*(241+fac2[name%10]) -(rem+1)*50 +(key+1)*4 +(moh+1)*9 +(pcc+1)*19 +(cob+1)*39 +(lsa+1)*79 +(armour+1)*159
code2+=100*(cap+1)*(code2%1000) # To be honest I don't understand any of this. It's just random nonsense.
while code2>99999:
code2=code2%100000+code2//100000
return shared.fill0s(code2,5)
############################
# SUPPORTING FUNCTIONS BELOW
############################
def getval(char):
vals='_9483726150rstlmeaiuonycdpjkhgxwfvqzb(-[.!' # szszss'
#vals='_5698472031aeioyusptndchbrxvzjmlkwgfq-!.([' # meebs'
if char in vals: return vals.index(char)+1
return 43*len(char.encode('utf-8')) # Apparently multibyte chars do this
def hash_name(name):
parsed=name.lower().replace(')','(').replace(']','[')
checksum=0
for i in range(len(parsed)):
checksum+=getval(parsed[i])*((i+1)%3+1)
return checksum
def adjust_rank(rank,cap=-1, key=False,moh=False,pcc=False):
rank+=key*2+moh # Invalid if rank <11, but whatever
if rank>8: #Can't have PCC below r10, and there are only two possible cap states after that
cap=(cap==4)*2+pcc+1 # I don't know why pcc is encoded like this, I would have thought cap would behave normally if not pcc, but nope.
return rank,cap
def rand(seed,clas):
if seed>-1: n=seed
else: n=int(random.random()*4)*2
if clas>9: return n+1,clas-10
return n,clas

29
shared.py Normal file
View File

@ -0,0 +1,29 @@
tracing=False
def trace(*args,**kwargs):
if tracing: print(*args,**kwargs)
def delimit(code,spacer=''):
if not spacer:
if len(code)==16: return code
if len(code)!=19: raise Exception('code length should be 19 for despacing, got',len(code),'from',code)
return code[0:4]+code[5:9]+code[10:14]+code[15:19]
if spacer:
if len(code)==19: return code
if len(code)!=16: raise Exception('code length should be 16 for spacing, got',len(code),'from',code)
return code[0:4]+spacer+code[4:8]+spacer+code[8:12]+spacer+code[12:16]
def scramble(code,unscramble=False):
n=int(code[0])
if unscramble: n=15-n # Because the 1st digit is left alone this needs to be +1
return code[0]+code[16-n:16]+code[1:16-n]
def combine(code,namehash=''):
if namehash: return code[0:2]+namehash[4:5]+code[2:6]+namehash[3:4]+code[6:8]+namehash[1:3]+code[8:11]+namehash[0:1]
charcode=code[0:2]+code[3:7]+code[8:10]+code[12:15]
namehash=code[15]+code[10:12]+code[7]+code[2]
return charcode,namehash
def fill0s(num,zeroes=0):
if not zeroes: return str(int(num))
n=str(num)
return '0'*(zeroes-len(n))+n

34
tables.py Normal file
View File

@ -0,0 +1,34 @@
#Data tables: Mostly used by the codegen itself
clas=[
[0,1,2,5], # Sniper: Crg, wire, run, tinker
[0,1,4,6], # Medic: Crg, wire, tough, hack
[0,1,2,6], # Tact: Crg, wire, run, hack
[0,1,3,6], # Psy: Crg, wire, spot, hack
[0,2,4,5], # HO: Crg, run, tough, tinker
[0,2,4,3], # Demo: Crg, run, tough, spot
[0,3,2,6], # Borg: Crg, spot, run, hack
[0,3,5,4], # Pyro: Crg, spot, tinker, tough
[0,4,5,3], # WM (LR, GL): Crg, tough, tinker, spot
[0,2,6,5], # Mav (AR, SR, CG, RL, F): Crg, run, hack, tinker
[0,1,5,6], # Tech: Crg, wire, tinker, hack
[0,1,3,4] # Alice: Crg, wire, spot, tough
]
weights=[1,50,15,5,2,2,4,8,3,18,9]
#Display tables: Mostly used by the rank code manager
classes=['Sniper','Medic','Tact','Psy','HO','Demo','Borg','Pyro','WM','Mav','Tech','Alice']
guns=['AR','SR','CG','RL','F','LR','GL','PF']
gunmaps=[1,0,0,0,2,3,2,4,5,6,0,1,2,3,4,1,7] # This is unwrapped mappings so every value matters
gunmaps_wrapped=[1,0,0,0,2,3,2,4,-1,-1,1,7] # Wrapped mappings return -1 for classes that have gun choices. Hopefully you'll never use it with them.
armour='LMHA'
trait=['SK', 'Gift', 'Surv', 'Goon', 'Acro', 'SL', 'Healer', 'FC', 'CR', 'RR', 'Gadg', 'Prowl', 'Gizer', 'PR', 'Engi', 'Reck']
spec=['Weap', 'PA', 'Cells', 'Cyber', 'Tri', 'Chem', 'Leader', 'Robo', 'Esp']
talent=['Crg','Wire','Run','Spot','Tough','Tinker','Hack']
#Convenience
displays=[classes,guns,armour,trait,spec,talent]
displays_str=['classes','guns','armour','traits','specs','talents']
displays_nogun=[classes,armour,trait,spec,talent]
displays_nogun_str=['classes','armour','traits','specs','talents']
rank=range(12)
xp=range(2500)
cap=[3, 6, 9, 10, 11/12]