I might be retarded

This commit is contained in:
Zergling_man 2024-06-17 19:12:08 +10:00
parent 78e9f4de0e
commit a7e9d92d82
5 changed files with 389 additions and 0 deletions

185
offline/codegen.py Normal file
View File

@ -0,0 +1,185 @@
import encode, decode, shared
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),(1,3)],'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)],'medals':{},'saves':{}}
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')
lines=list(filter(lambda x:x,lines))
if not lines: continue
header=lines[0].split(' ')
nhash=int(header[0]); names=set(header[1:]) # Makes deduping way easier
medals={}
if ':' in lines[-1]: # Since this isn't used in any other line, it's safe
medals={int(n.split(':')[0]):int(n.split(':')[1]) for n in lines.pop(-1).split(' ')}
stuff={k:parse_conf_line(v) for k,v in zip(tables.displays_str[:-1],lines[1:])}
conf[nhash]={'names':names}|stuff|{'medals':medals}
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+=' '.join(f'{h}:{m}' for h,m in v['medals'].items())+'\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,name=None,save=None):
if name is None: name=active_profile; save2=True
else: save2=False # Don't save spoofed codes unless explicitly told
if save is None: save=save2
data=list(decode.decode(code,name))
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]
cap=tables.cap[cap]
if rank==12: cap=12
data=data[8:]
if rank<9: show[-1]='Hidden' # Don't spoil the talent
print(show)
print(str(rank)+'/'+str(cap))
print(data) # Extract medals and save them
medals=data[1:-1]
if not check: print('No name given, code validation not performed')
else:
if check!=validator: print('code did not validate:',validator,check); return
else: # Do not save medals unless validation passed
if any(medals): conf[active_profile]['medals'][shared.muxhero(hero[0],hero[3],hero[4])]=shared.muxmedals(*medals)
print('code validated')
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])
if not save: return
save_conf()
def mass_add():
a=input('>')
while a:
try: read_code(a,false)
except: pass
a=input('>')
save_conf()
def generate(officer=None,name=None):
pro=conf[active_profile]
if name is None: name=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=[]
if len(pieces)==6: rank=cap=int(pieces.pop(-1))
else: rank,cap=12,12
try:
for n in zip(pieces,tables.displays_nogun,tables.displays_nogun_str):
ind=lookup_piece(n[0],n[1],n[2])
rank,cap=min(rank,pro[n[2]][ind][0]),min(cap,pro[n[2]][ind][1])
if 0 in [rank,cap]: print(f"{n[0]} is locked, go unlock it first.")
out.append(ind)
if gun: out.insert(1,lookup_piece(gun,tables.guns,'guns',True))
else: out.insert(1,tables.gunmaps_wrapped[out[0]])
except LookupException as ex: print(ex); return
rank,cap=min(rank,pro['guns'][out[1]][0]),min(cap,pro['guns'][out[1]][1]) # Derp2, forgot to actually cap on gun
if 0 in [rank,cap]: return # Already warned about locks earlier.
cap=(cap-(cap<10)+(cap>10))//3 # Derp
try: out[-1]=tables.clas[out[0]].index(out[-1])
except ValueError: print(f"{tables.classes[out[0]]} can't get talent {tables.talent[out[-1]]}, options are {list(map(lambda x:tables.talent[x],tables.clas[out[0]]))}"); return
muxed=shared.muxhero(out[0],out[3],out[4])
meds=[]
if muxed in pro['medals']: meds=shared.muxmedals(pro['medals'][muxed])
return encode.encode(name,*out,rank,cap,0,*meds)
def lookup_piece(piece,table,name,partial=False):
if len(piece)<3: p=piece.upper()
else: p=piece.capitalize() # Bloody Americans
if not partial:
try: return table.index(p)
except ValueError: raise LookupException(f"Couldn't resolve {piece} from {name}")
res=list(filter(lambda x:x.startswith(p),table))
if len(res)!=1: raise LookupException(f"Couldn't resolve {piece} with partial match from {name} (found {len(res)} possibilities)") # Ambiguous lookups not allowed
return table.index(res[0])
class LookupException(Exception): pass
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()
def savebuild(code,name):
pass
def builds():
pass
def medals():
pass
load_conf()
if __name__=="__main__": print("Please open an interpreter and use 'import codegen', this script doesn't serve as an entrypoint on its own yet.")

55
offline/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
offline/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()*5)*2
if clas>9: return n+1,clas-10
return n,clas

39
offline/shared.py Normal file
View File

@ -0,0 +1,39 @@
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)
if len(n)>zeroes: raise Exception(f"can't pad {n} ({len(n)}) with {zeroes} 0s")
return '0'*(zeroes-len(n))+n
def muxhero(clas,trait,spec):
return clas+trait*12+spec*12*16 # It's actually stored backwards like this, spec is in the most significant bits. Whatever, it doesn't need to be reversable.
def muxmedals(key,moh=-1,pcc=-1,cob=-1,lsa=-1,rem=-1):
print(key,moh,pcc,cob,lsa,rem)
if moh!=-1: return key+moh*2+pcc*4+cob*8+lsa*16+rem*32
print(bool(key&1),bool(key&2),bool(key&4),bool(key&8),bool(key&16),key//32)
return bool(key&1),bool(key&2),bool(key&4),bool(key&8),bool(key&16),key//32

34
offline/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]