Files
factorio-ios/tests/check_save_layout.py
T
SashaGoncharov19 51d085e865 Native iPhone port of Factorio 2.0.77
Run the original macOS ARM64 game code on iOS:
- tools/retarget.py: header-only Mach-O retargeting to an iOS dylib
- platform/native: UIKit host and macOS API shims (AppKit for SDL's
  Cocoa backend, CoreGraphics displays, CoreAudio device, Metal device
  properties, Wuffs PNG decoding, Steam and peripheral stubs)
- touch, overlay, iOS keyboard and hardware keyboard input
- tools/build_native.py: build, sign, install, data copy, launch
- docs: build guide, controls, architecture

Also includes the earlier static-translation research (ARM64 -> WASM ->
native) under docs/research, runtime/, tests/ and tools/.

No game files are included; the build reads them from a local install.
2026-10-23 15:27:58 +04:00

95 lines
4.1 KiB
Python

"""Verify the bounded 2.0.77 scenario envelope, offsets and unsupported inputs."""
import io
import json
import struct
import subprocess
import tempfile
import zipfile
import zlib
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
PROBE=ROOT/'build/save-layout/probe'
TICKS=(720001,42,0xffffffffffffffff)
def compact(value,size=4):
return bytes([value]) if value<255 else b'\xff'+value.to_bytes(size,'little')
def string(value): return compact(len(value))+value
def version(values=(2,0,77)): return b''.join(compact(v,2) for v in values)
def localised(text=None,children=(),kind=1):
return b'\0' if text is None else bytes([kind])+string(text)+bytes([len(children)])+b''.join(children)
def ending(title=None,children=()):
return localised(title,children)+localised()+compact(0)+localised()+string(b'')
def fixture(long_string=b'freeplay',mod_count=1,extended_version=False,nested=False):
result=bytearray(struct.pack('<4HB',2,0,77,0,0))
positions={}
result+=string(b'')+string(long_string)+string(b'base')
positions['difficulty']=len(result);result+=b'\1'
positions['boolean']=len(result);result+=b'\0\0'+string(b'')+b'\0\0\0\1'
result+=version((65535,255,254) if extended_version else (2,0,77))
result+=struct.pack('<I',1234)+b'\1'+struct.pack('<I',0xa00000)
positions['mod_count']=len(result);result+=compact(mod_count)
for i in range(mod_count):
result+=string(b'base')+version((255,256,65535) if extended_version else (2,0,77))+struct.pack('<I',i)
result+=struct.pack('<I',0)
positions['property_type']=len(result);result+=b'\5\0'
positions['property_count']=len(result);result+=struct.pack('<I',0)
positions['ending']=len(result)
children=(localised(b'child',(localised(),)),) if nested else ()
result+=ending(b'victory',children)+ending(b'defeat')+b'\0'
offset=len(result)
result+=struct.pack('<3Q',*TICKS)
return bytes(result),offset,positions
checks=0
def probe(data,status=0,mode='bytes',offset=None,mods=1):
global checks
with tempfile.TemporaryDirectory() as directory:
p=Path(directory)/'fixture';p.write_bytes(data)
result=subprocess.run([str(PROBE),mode,str(p)],capture_output=True,text=True)
assert result.returncode==(1 if status else 0),(result.returncode,result.stderr)
report=json.loads(result.stdout)
assert report['status']==status,report
if not status:
assert report['header_offset']==offset,report
assert report['next_offset']==offset+24,report
assert report['mod_count']==mods,report
assert report['tick_fields_hex']==[f'{v:016x}' for v in TICKS],report
elif status==6 and mode=='bytes':
assert report['needed']>len(data),report
checks+=1
for length in (0,1,254,255,256,4097):
data,offset,_=fixture(long_string=b'x'*length,extended_version=True,nested=True)
probe(data,offset=offset)
for count in (0,1,254,255):
data,offset,_=fixture(mod_count=count)
probe(data,offset=offset,mods=count)
data,offset,positions=fixture()
for length in range(len(data)):
probe(data[:length],6)
for index,value in [(4,78),(8,1),(positions['difficulty'],3),(positions['boolean'],2),
(positions['property_type'],4),(positions['property_count'],1)]:
changed=bytearray(data);changed[index]=value;probe(changed,4)
probe(data[:9]+b'\xff'+struct.pack('<I',1024*1024+1),7)
probe(data[:positions['mod_count']]+b'\xff'+struct.pack('<I',4097),7)
deep=localised()
for i in range(65): deep=localised(b'',(deep,))
probe(data[:positions['ending']]+deep+data[positions['ending']:],7)
for method in (zipfile.ZIP_STORED,zipfile.ZIP_DEFLATED):
output=io.BytesIO()
with zipfile.ZipFile(output,'w',compression=method) as z:
z.writestr('fixture/level.dat0',zlib.compress(data))
probe(output.getvalue(),mode='zip',offset=offset)
print(f'PASS: {checks} scenario-envelope cases, variable offsets, sentinel lengths, truncation, limits and failure atomicity')
(ROOT/'build/save-layout/verification.json').write_text(json.dumps({'status':'pass','cases':checks,
'scope':'2.0.77 envelope with empty startup settings; not world loading'},indent=2)+'\n')