XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
"""Import untouched SVG frames exported from the installed game's UI movies.

The input directory contains the local FRES/SWF evidence and JPEXS exports.
This step never draws or traces icons, connects to the game, or changes game files.
"""
import argparse, hashlib, io, json, re, struct, xml.etree.ElementTree as ET
from pathlib import Path

parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--exports',type=Path,required=True)
parser.add_argument('--output',type=Path,default=Path('XFEEDToolBox/Assets/GameIcons'))
args=parser.parse_args()
root=ET.parse(args.exports/'external-panel.xml').getroot()
sprite=next(x for x in root.iter() if x.attrib.get('type')=='DefineSpriteTag' and x.attrib.get('spriteId')=='84')
labels={};frame=1
for tag in sprite.find('subTags'):
    if tag.attrib.get('type')=='FrameLabelTag':labels[tag.attrib['name']]=frame
    if tag.attrib.get('type')=='ShowFrameTag':frame+=1
if len(labels)!=52:raise ValueError('Unexpected original location icon timeline')
args.output.mkdir(parents=True,exist_ok=True)
icons=[]
def copy_icon(key,path,resource,sprite,frame,label):
    if not re.fullmatch('[A-Za-z][A-Za-z0-9]+',key):raise ValueError('Invalid icon key')
    data=path.read_bytes();svg=ET.fromstring(data)
    if any(x.tag.endswith('}image') or any('bitmap' in key.lower() for key in x.attrib) for x in svg.iter()):
        raise ValueError('Icon has an unresolved bitmap dependency')
    if not any(x.tag.endswith('}path') for x in svg.iter()):raise ValueError('Missing original icon geometry')
    # Preserve every original path, transform, colour and opacity byte for byte.
    (args.output/(key+'.svg')).write_bytes(data)
    icons.append(dict(key=key,file=key+'.svg',resource=resource,spriteId=sprite,frame=frame,label=label,
                      sha256=hashlib.sha256(data).hexdigest()))
for label,frame in labels.items():
    # The game's TitanCloud timeline deliberately contains only a transparent
    # bounds rectangle. It is not a usable icon and must not become a blank UI.
    if label=='TitanCloud':continue
    copy_icon(label,args.exports/'svg'/'DefineSprite_84_icons.LocationTypeIcons'/f'{frame}.svg','external',84,frame,label)
features={'FeatureSummary':(1241,'Summary'),
          'FeatureEngineer':(2112,'EngineerIcon_Size1'),'FeatureOutfitting':(2114,'OutfittingIcon'),
          'FeatureRoute':(2132,'RoutePlotIcon'),'FeatureGalaxyMap':(2136,'GalaxyMapIcon'),
          'FeatureSystemMap':(2140,'SystemIcon'),'FeaturePlanetMap':(2142,'PlanetMapIcon'),'FeatureFilter':(2148,'FilterIcon')}
for key,(sprite,name) in features.items():
    copy_icon(key,args.exports/'features'/f'DefineSprite_{sprite}_icons.{name}'/'1.svg','external',sprite,1,'icons.'+name)
copy_icon('FeatureCommodity',args.exports/'internal-features'/'DefineSprite_2143_icons.GalaxyMapCommodityIcon'/'1.svg','internal',2143,1,'icons.GalaxyMapCommodityIcon')
copy_icon('FeatureMaterials',args.exports/'internal-features'/'DefineSprite_1322_MaterialsIcon'/'1.svg','internal',1322,1,'MaterialsIcon')
copy_icon('SelectedDestination',args.exports/'features'/'DefineSprite_96_CockpitExternalPanel_fla.LocationListRowTypeField_226'/'1.svg','external',96,1,'LocationListRow.type.current')
for key,sprite,name in (('OutfittingUtility',460,'UtilityMountsIcon'),('OutfittingOptional',462,'OptionalModulesIcon'),
                        ('OutfittingHardpoint',464,'HardpointsIcon'),('OutfittingCore',466,'CoreModulesIcon')):
    copy_icon(key,args.exports/'outfitting-exports'/f'DefineSprite_{sprite}_assets.{name}'/'1.svg','outfitting',sprite,1,'assets.'+name)

# The rating images are bitmap symbols, not vector frames. Decode the original
# BC7 atlas and crop the exact DefineSubImage rectangles; do not trace or tint it.
from PIL import Image
body=(args.exports/'outfitting.bin').read_bytes()
if hashlib.sha256(body).hexdigest()!='fd5b6fb3c63c49b9d4b9ab01f6700306acb9fb566e8f0d4b9de1e45ec7a2fae6':
    raise ValueError('Unrecognised outfitting resource version')
buffers=[struct.unpack_from('<II',body,524+i*8) for i in range(25)]
groups=[];cursor=0
for offset in range(108,524,32):
    count=struct.unpack_from('<H',body,offset+10)[0]
    groups.append(buffers[cursor:cursor+count]);cursor+=count
if cursor!=len(buffers):raise ValueError('Incomplete original buffer table')
payload=len(body)-sum(size for _,size in buffers)
gpu_offset=payload
for _,size in (group[i] for i in range(max(map(len,groups))) for group in groups if len(group)>i):
    if size==2048*884:break
    gpu_offset+=size
else:raise ValueError('Original outfitting atlas missing')
gpu=body[gpu_offset:gpu_offset+2048*884]
if hashlib.sha256(gpu).hexdigest()!='0a803de1d5f1cb72b80e93e9755b8e20d217ffa9708d414e99ef58e87ce1739d':
    raise ValueError('Outfitting atlas buffer order mismatch')
dds=(b'DDS '+struct.pack('<7I',124,0x81007,884,2048,len(gpu),0,1)+bytes(44)
     +struct.pack('<8I',32,4,int.from_bytes(b'DX10','little'),0,0,0,0,0)
     +struct.pack('<5I',0x1000,0,0,0,0)+struct.pack('<5I',98,3,0,1,0))
atlas=Image.open(io.BytesIO(dds+gpu)).convert('RGBA')
gfx=ET.parse(args.exports/'outfitting-gfx.xml').getroot()
raster_icons=[]
for key,character,symbol in [('EngineeringGradeEmpty',24,'EngineerLevel0'),('EngineeringGradeSelected',23,'EngineerLevel1')]:
    sub=next(x for x in gfx.iter() if x.attrib.get('type')=='DefineSubImage' and x.attrib.get('characterID')==str(character))
    box=tuple(int(sub.attrib[x]) for x in ('x1','y1','x2','y2'))
    if sub.attrib['imageId']!='1':raise ValueError('Grade icon belongs to an unexpected atlas')
    crop=atlas.crop(box);destination=args.output/(key+'.png');crop.save(destination)
    raster_icons.append(dict(key=key,file=destination.name,resource='outfitting',symbol='assets.'+symbol,
                             characterId=character,atlas='Outfitting3_I1.dds',atlasBounds=box,
                             width=crop.width,height=crop.height,originalPixelsUnchanged=True,
                             rgbaSha256=hashlib.sha256(crop.tobytes()).hexdigest(),
                             sha256=hashlib.sha256(destination.read_bytes()).hexdigest()))
manifest=dict(generator='research/Build-GameIconAssets.py',exporter='JPEXS Free Flash Decompiler 26.4.0',
              originalsUnchanged=True,gameProcessAccess=False,
              omittedFrames=[dict(key='TitanCloud',spriteId=84,frame=labels['TitanCloud'],reason='Original frame is completely transparent')],
              sources={'external':dict(resourceName='cockpitexternalpanel',resourceHash='d5cd6f1c6d4c730748078d9675e19c5d151b4887',sourceSha256='681e539c3386f31ab3a44da33b8e0a6dd25e73bded7a58dca67742993d549c48'),
                       'internal':dict(resourceName='cockpitinternalpanel2',resourceHash='469d5b272ca4c4e23e9649303cba6ebb894f487d',sourceSha256='c1f00073826b929418d2feecbc7da3ce1b8f750434dba98f083d91da01d2e58d'),
                       'outfitting':dict(resourceName='outfitting3',resourceHash='d644aab217bdd3cf8404c3463b7041b0ca50eb42',sourceSha256='d9d394604b18a9ce386f342fbd67042e97d6da2d4c64246d2cd43008e9c2bfb3')},icons=icons,rasterIcons=raster_icons)
(args.output/'source.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(dict(imported=len(icons)+len(raster_icons),locationIcons=len(labels)-1,output=str(args.output),originalsUnchanged=True)))