LifePlay-Revival/Tools/DiffRemplacementNoeuds.py
edujiro5972 8015f182b9 L7i (CX-B12) : Contains/Split/FindSubstring avec motif vide au comportement 4.27 (385 noeuds, 28 Blueprints) ; ERR-005
FString::Find("" , IgnoreCase, FromStart) renvoie 0 en UE5 et INDEX_NONE en 4.27 : Contains(x, "") devenu vrai.
Test 3120 cas (transcription 4.27) : 0 ecart ; diff independant 385/385 ; compilation et cuisson 0 avertissement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:24:20 +02:00

120 lines
6.6 KiB
Python

import re,sys,collections
def parse(p):
objs={}; pkg=None; key=None
for line in open(p,encoding='utf-8'):
line=line.rstrip('\n')
if line.startswith('##### '): pkg=line[6:]; continue
m=re.match(r'^ (\S+) \[(\w+)\]$',line)
if m: key=(pkg,m.group(1)); objs[key]={'cls':m.group(2),'lines':[]}; continue
if key: objs[key]['lines'].append(line)
return objs
# Profils de remplacement : selection des anciens et nouveaux noeuds, correspondance des broches.
# Usage : DiffRemplacementNoeuds.py avant.txt apres.txt [L7a|L7b|CX-B12] (L7a par defaut)
PROFILS={
'L7a':{'old':('K2Node_VariableGet',lambda l:'Engine.SkinnedMeshComponent\'",MemberName="SkeletalMesh"' in l),
'new':('K2Node_CallFunction',lambda l:'MemberName="GetSkeletalMesh"' in l and 'LPMeshCompat' in l),
'pins':[('self','Component'),('SkeletalMesh','ReturnValue')]},
'L7b':{'old':('K2Node_CallFunction',lambda l:'MemberName="K2_GetRandomPointInNavigableRadius"' in l),
'new':('K2Node_CallFunction',lambda l:'MemberName="GetRandomLocationInNavigableRadius"' in l and 'LPNavigationHelpers' in l),
'pins':[(x,x) for x in ('WorldContextObject','Origin','RandomLocation','Radius','NavData','FilterClass','ReturnValue')]},
'CX-B12':{'old':('K2Node_CallFunction',lambda l:'KismetStringLibrary' in l and re.search(r'MemberName="(Contains|FindSubstring|Split)"',l) is not None),
'new':('K2Node_CallFunction',lambda l:'LPStringCompat' in l and re.search(r'MemberName="(Contains|FindSubstring|Split)"',l) is not None),
'pins':[(x,x) for x in ('SearchIn','Substring','bUseCase','bSearchFromEnd','StartPosition','SourceString','InStr','LeftS','RightS','SearchCase','SearchDir','ReturnValue')]},
}
PROFIL=PROFILS[sys.argv[3] if len(sys.argv)>3 else 'L7a']
A=parse(sys.argv[1]); B=parse(sys.argv[2])
def graph(path): return path.split(':')[-1].split('.')[0] if ':' in path else ''
def iscopy(path):
g=graph(path); return "ExecuteUbergraph" in g or g.endswith("_MERGED") or re.fullmatch(r"EdGraph_[0-9]+",g) is not None
def pos(o):
x=y='0'
for l in o['lines']:
if l.startswith(' NodePosX = '): x=l.split('= ')[1]
if l.startswith(' NodePosY = '): y=l.split('= ')[1]
return x,y
def select(D,sel): return {k:v for k,v in D.items() if v['cls']==sel[0] and not iscopy(k[1]) and any(sel[1](l) for l in v['lines'])}
old=select(A,PROFIL['old']); new=select(B,PROFIL['new'])
print('anciens noeuds',len(old),'nouveaux',len(new))
idx={(k[0],k[1].rsplit('.',1)[0],pos(v)):k for k,v in new.items()}
ren={}; miss=0
for k,v in old.items():
t=idx.get((k[0],k[1].rsplit('.',1)[0],pos(v)))
if not t: miss+=1; continue
ren[(k[0],graph(k[1]),k[1].rsplit('.',1)[1])]=t[1].rsplit('.',1)[1]
print('apparies',len(ren),'non apparies',miss)
skip=re.compile(r'^ (NodeGuid|GraphGuid|ErrorType|ErrorMsg|bHasCompilerMessage|NodePosX|NodePosY) = ')
def norm(pkg,g,lines):
out=[]
for l in lines:
if skip.match(l): continue
for (p,gg,o),n in ren.items():
if p==pkg and gg==g:
for op,np_ in PROFIL['pins']: l=l.replace(o+'.'+op,n+'.'+np_)
out.append(canon(l,pkg,g))
return out
def canon(l,pkg=None,g=None):
if l.startswith(' pin ') and ' -> ' in l:
head,links=l.rsplit(' -> ',1)
return head+' -> '+', '.join(sorted(x for x in links.split(', ') if x))
if l.startswith(' Nodes = ('):
items=[x.strip('"') for x in l[len(' Nodes = ('):-1].split('","')]
if pkg is not None:
items=[x for x in items if not any(pkg==p2 and ("."+o+"'") in x and (":"+gg+".") in x for (p2,gg,o) in ren)]
items+=[ "NOUVEAU:"+pkg+":"+gg+"."+n for (p2,gg,o),n in ren.items() if p2==pkg and gg==g ]
else:
items=[("NOUVEAU:"+x.split("'/Game/")[0] if False else x) for x in items]
return ' Nodes = SET'+str(sorted(items))
return l
newnames={(k[0],graph(k[1]),k[1].rsplit('.',1)[1]) for k in new}
def canonB(k,l):
if l.startswith(' Nodes = ('):
items=[x.strip('"') for x in l[len(' Nodes = ('):-1].split('","')]
keep=[]; add=[]
for x in items:
nm=x.rsplit('.',1)[1].rstrip("'")
if (k[0],k[1].split(':')[-1],nm) in newnames: add.append("NOUVEAU:"+k[0]+":"+k[1].split(':')[-1]+"."+nm)
else: keep.append(x)
return ' Nodes = SET'+str(sorted(keep+add))
return canon(l)
diffs=collections.Counter(); ex=[]
for k in set(A)|set(B):
if iscopy(k[1]) or k in old or k in new: continue
if k[1].startswith('PLACEHOLDER'): continue
a=norm(k[0],graph(k[1]),A[k]['lines']) if k in A else None; b=[canonB(k,l) for l in B[k]['lines'] if not skip.match(l)] if k in B else None
if a!=b:
cls=(A.get(k) or B.get(k))['cls']
diffs[(k[0],cls,'absent avant' if a is None else 'absent apres' if b is None else 'modifie')]+=1
if len(ex)<40: ex.append((k,[x for x in (a or []) if x not in (b or [])][:2],[x for x in (b or []) if x not in (a or [])][:2]))
print('objets en ecart hors noeuds remplaces et copies :',sum(diffs.values()))
for k,v in sorted(diffs.items()): print(' ',v,*k)
for e in ex: print(e[0]); [print(' -',x[:160]) for x in e[1]]; [print(' +',x[:160]) for x in e[2]]
def pinlinks(o,name):
for l in o['lines']:
m=re.match(r'^ pin (\S+) (\[.*\] defaut=.*) -> (.*)$',l)
if m and m.group(1)==name: return sorted(x for x in m.group(3).split(', ') if x)
return None
def pinhead(o,name):
# Categorie et valeurs par defaut de la broche (sans les liaisons).
for l in o['lines']:
m=re.match(r'^ pin (\S+) (\[.*\] defaut=.*) -> (.*)$',l)
if m and m.group(1)==name: return m.group(2)
return None
ok=bad=0
for k,v in old.items():
t=idx[(k[0],k[1].rsplit('.',1)[0],pos(v))]; n=B[t]
g=graph(k[1])
def rn(lst):
out=[]
for x in lst or []:
for (p2,gg,o),nn in ren.items():
if p2==k[0] and gg==g: x=x.replace(o+'.',nn+'.') if x.split('.')[0]==o else x
out.append(x)
return sorted(out)
# Broche absente des deux cotes (profil multi-fonctions : Split n'a pas SearchIn...) : egalite.
same=all((pinlinks(v,op) is None and pinlinks(n,np_) is None) or rn(pinlinks(v,op))==pinlinks(n,np_) for op,np_ in PROFIL['pins'])
# Profil a broches homonymes : les valeurs par defaut doivent aussi etre reportees.
if same and all(op==np_ for op,np_ in PROFIL['pins']): same=all(pinhead(v,op)==pinhead(n,op) for op,_ in PROFIL['pins'])
if same: ok+=1
else:
bad+=1; print('ECART',k[1],[(op,pinhead(v,op),rn(pinlinks(v,op)),pinhead(n,np_),pinlinks(n,np_)) for op,np_ in PROFIL['pins']])
print('noeuds remplaces : liaisons identiques',ok,'/ ecarts',bad)