From 4e6936cec3f8b96a0c886267dfa1726d86e798cf Mon Sep 17 00:00:00 2001 From: Zergling_man Date: Wed, 21 Dec 2022 15:22:44 +1100 Subject: [PATCH] Should have done this a long time ago too --- README | 21 ++++++++ codegen.py | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++ decode.py | 55 +++++++++++++++++++++ encode.py | 76 ++++++++++++++++++++++++++++ shared.py | 29 +++++++++++ tables.py | 34 +++++++++++++ 6 files changed, 358 insertions(+) create mode 100644 README create mode 100644 codegen.py create mode 100644 decode.py create mode 100644 encode.py create mode 100644 shared.py create mode 100644 tables.py diff --git a/README b/README new file mode 100644 index 0000000..e58b759 --- /dev/null +++ b/README @@ -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. \ No newline at end of file diff --git a/codegen.py b/codegen.py new file mode 100644 index 0000000..9946afb --- /dev/null +++ b/codegen.py @@ -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() \ No newline at end of file diff --git a/decode.py b/decode.py new file mode 100644 index 0000000..6b1d765 --- /dev/null +++ b/decode.py @@ -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 \ No newline at end of file diff --git a/encode.py b/encode.py new file mode 100644 index 0000000..0931e53 --- /dev/null +++ b/encode.py @@ -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 \ No newline at end of file diff --git a/shared.py b/shared.py new file mode 100644 index 0000000..b52e9c4 --- /dev/null +++ b/shared.py @@ -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 \ No newline at end of file diff --git a/tables.py b/tables.py new file mode 100644 index 0000000..c62c8ab --- /dev/null +++ b/tables.py @@ -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] \ No newline at end of file