Files
MatchMaster_level/scripts/package-editor-windows.py
2026-10-21 13:43:19 +08:00

76 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""Build a self-contained Windows editor bundle from explicit runtime/source inputs."""
import argparse,hashlib,json,re,shutil,struct,zipfile
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
NODE_VERSION='22.23.3'
ZIP_SHA='2b0ff57b049cda1bbcea2240eec20467018713c1efe1f7360c2681859b90ed71'
EXE_SHA='9c9245166b4a8e182e0b797da9c20136117ff24368eaff1fec8343a123c8db0e'
NAME='ColorBlockEditor-Windows-x64-0.20.2-20261020-r2'
def sha(data):return hashlib.sha256(data).hexdigest()
def put(target,relative,data):
p=target/relative;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(data)
def copy_tree(source,dest):
for p in sorted(source.rglob('*')):
if not p.is_file() or p.is_symlink() or any(x.startswith('.') or x=='node_modules' for x in p.relative_to(source).parts):continue
out=dest/p.relative_to(source);out.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(p,out)
def copy_levels(source,target):
files=sorted(p for p in source.iterdir() if re.fullmatch(r'level\d+\.json',p.name) and p.is_file() and not p.is_symlink())
if not files:raise ValueError('No levels found: '+str(source))
for p in files:put(target,p.name,p.read_bytes())
return len(files)
def verify(archive):
with zipfile.ZipFile(archive) as z:
assert z.testzip() is None
package_name=next(n.rsplit('/',1)[0] for n in z.namelist() if n.endswith('/PACKAGE_MANIFEST.json'))
manifest=json.loads(z.read(package_name+'/PACKAGE_MANIFEST.json'))
expected={package_name+'/'+r['path'] for r in manifest['files']}|{package_name+'/PACKAGE_MANIFEST.json'}
assert set(z.namelist())==expected
for r in manifest['files']:
data=z.read(package_name+'/'+r['path']);assert len(data)==r['bytes'] and sha(data)==r['sha256'],r['path']
return {'files':len(manifest['files']),'sha256':sha(archive.read_bytes()),'bytes':archive.stat().st_size}
def build(node_zip,game,out):
raw=node_zip.read_bytes();assert sha(raw)==ZIP_SHA,'Node ZIP differs from official release hash'
target=out/NAME;archive=out/(NAME+'.zip')
if target.exists() or archive.exists():raise FileExistsError('Refusing to replace an existing package')
target.mkdir(parents=True)
with zipfile.ZipFile(node_zip) as z:
exe=z.read('node-v'+NODE_VERSION+'-win-x64/node.exe')
assert sha(exe)==EXE_SHA and exe[:2]==b'MZ'
pe=struct.unpack_from('<I',exe,0x3c)[0]
assert exe[pe:pe+4]==b'PE\0\0' and struct.unpack_from('<H',exe,pe+4)[0]==0x8664
put(target,'runtime/node.exe',exe)
put(target,'runtime/LICENSE',z.read('node-v'+NODE_VERSION+'-win-x64/LICENSE'))
runtime={'version':NODE_VERSION,'platform':'win-x64','officialRelease':'https://nodejs.org/en/blog/release/v22.23.3','download':'https://nodejs.org/dist/v22.23.3/node-v22.23.3-win-x64.zip','archiveSHA256':ZIP_SHA,'nodeExeSHA256':EXE_SHA,'verifiedAgainstOfficialReleasePage':True}
put(target,'runtime/SOURCE.json',(json.dumps(runtime,indent=2)+'\n').encode())
for app in ['colorBlock-level-editor','colorBlock-level-generator','colorBlock-similarity-checker']:
for folder in ['src','web','toolkit-web','examples']:
if (ROOT/app/folder).exists():copy_tree(ROOT/app/folder,target/app/folder)
put(target,app+'/package.json',(ROOT/app/'package.json').read_bytes())
for guide in ['SCREENSHOT_RECOGNITION_EXPLAINED.md','SCREENSHOT_RECOGNITION_EXPLAINED.html','PETAL_EDITOR_0132.md','WOOD_DRAFTS_EXPORT_0140.md','BOUNDARIES_0150.md','BOUNDARIES_0150.html']:
put(target,'colorBlock-level-editor/docs/'+guide,(ROOT/'colorBlock-level-editor/docs'/guide).read_bytes())
counts={}
for source,dest in [(game/'assets/custom/Json','game-data/assets/custom/Json'),(ROOT/'levels','levels'),(ROOT/'colorBlock-similarity-checker/levels','colorBlock-similarity-checker/levels'),(ROOT/'colorBlock/assets/custom/Json','colorBlock/assets/custom/Json')]:counts[dest]=copy_levels(source,target/dest)
put(target,'game-data/assets/Script/module/Config/LevelTimeBonusConfig.ts',(game/'assets/Script/module/Config/LevelTimeBonusConfig.ts').read_bytes())
put(target,'StartEditor.mjs',(ROOT/'scripts/windows/StartEditor.mjs').read_bytes())
bat=(ROOT/'scripts/windows/StartEditor.bat').read_text(encoding='utf-8').replace('\r\n','\n').replace('\n','\r\n').encode('ascii')
for name in ['StartEditor.bat','启动关卡编辑器.bat']:put(target,name,bat)
manual=(ROOT/'scripts/windows/使用说明.txt').read_text(encoding='utf-8');put(target,'使用说明.txt',manual.replace('\n','\r\n').encode('utf-8-sig'))
put(target,'colorBlock-level-editor/drafts/README.txt',b'Your saved drafts will appear here. Keep this folder when updating.\r\n')
files=[]
for p in sorted(target.rglob('*')):
if p.is_file():
data=p.read_bytes();files.append({'path':p.relative_to(target).as_posix(),'bytes':len(data),'sha256':sha(data)})
manifest={'editor':'0.20.2','generator':'0.21.0','checker':'1.1.3','date':'2026-10-20','runtime':runtime,'levelCounts':counts,'excluded':['personal settings','draft samples','Git','caches','test outputs','project conversation history','full Cocos project'],'files':files}
put(target,'PACKAGE_MANIFEST.json',(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n').encode())
with zipfile.ZipFile(archive,'x',zipfile.ZIP_DEFLATED,compresslevel=6) as z:
for p in sorted(target.rglob('*')):
if p.is_file():z.write(p,NAME+'/'+p.relative_to(target).as_posix())
result=verify(archive);result.update({'archive':str(archive),'directory':str(target),'levelCounts':counts})
(out/(NAME+'.sha256')).write_text(result['sha256']+' '+archive.name+'\n')
print(json.dumps(result,ensure_ascii=False,indent=2))
if __name__=='__main__':
parser=argparse.ArgumentParser();parser.add_argument('--node-zip',type=Path);parser.add_argument('--game',type=Path);parser.add_argument('--out',type=Path);parser.add_argument('--verify',type=Path);args=parser.parse_args()
if args.verify:print(json.dumps(verify(args.verify),indent=2))
else:build(args.node_zip,args.game,args.out)