"""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)))
"""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)))