106 lines
4.8 KiB
Python
106 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Import supported legacy settings without modifying either source or an existing destination."""
|
|
import argparse,json,os,tempfile,sys
|
|
from pathlib import Path
|
|
SCHEMA={
|
|
'telemetryEnabled':(1,0,1),
|
|
'aiDoctrine':(0,0,1),
|
|
'aiShadow':(0,0,1),
|
|
'droneReconCount':(0,0,3),
|
|
'droneCooldownSeconds':(300,60,1800),
|
|
'planeAltitudeMetres':(250,150,500),
|
|
'planeReplacementSeconds':(60,30,1800),
|
|
'planeSectorSeconds':(120,30,600),
|
|
'planeReconEnabled':(1,0,1),
|
|
'roadblockCount':(4,0,30),
|
|
'largeRoadblockCount':(4,0,30),
|
|
'mainRoadWidthMetres':(6,5,12),
|
|
'civilianTrafficCount':(3,0,6),
|
|
'garageVehicleCount':(16,0,24),
|
|
'garageChancePercent':(70,0,100),
|
|
'garageRadiusMetres':(1000,250,1500),
|
|
'garageSpawnIntervalSeconds':(5,5,60),
|
|
'startMode':(0,0,1),
|
|
'keepWeaponOnUnconscious':(1,0,1),
|
|
'selfEpinephrine':(0,0,1),
|
|
'restartOnEmpty':(1,0,1),
|
|
'emptyRestartHours':(5,0,168),
|
|
'reportIntervalSeconds':(10,5,60),
|
|
'supportSquadLimit':(4,0,8),
|
|
'responseRadiusMetres':(2000,500,5000),
|
|
'aiIntelChancePercent':(60,0,100),
|
|
'intelRevealChancePercent':(15,0,100),
|
|
'radioIntelSiteCount':(5,0,20),
|
|
'showAllSites':(0,0,1),
|
|
'radioDefenceSeconds':(180,30,1800),
|
|
'startTimeMinutes':(840,0,1439),
|
|
'timeMultiplier':(1,1,24),
|
|
'startDateCode':(0,0,1501231),
|
|
'weatherProfile':(0,0,3),
|
|
'viewDistanceMetres':(0,0,10000),
|
|
'automaticEquipmentChancePercent':(25,0,100),
|
|
'antiAirChancePercent':(7,0,100),
|
|
'antiPersonnelMineChancePercent':(60,0,100),
|
|
'automaticConvoyEnabled':(1,0,1),
|
|
'convoyCount':(3,0,20),
|
|
'heliTimerMinutes':(15,1,1440),
|
|
}
|
|
AI_KEYS=('droneReconCount','droneCooldownSeconds','aiDoctrine','aiShadow','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','planeReconEnabled','reportIntervalSeconds','supportSquadLimit','responseRadiusMetres','automaticEquipmentChancePercent','antiAirChancePercent','antiPersonnelMineChancePercent','automaticConvoyEnabled','heliTimerMinutes')
|
|
INTEL_KEYS=('aiIntelChancePercent','intelRevealChancePercent','radioIntelSiteCount','showAllSites')
|
|
def merge(ai=None,intel=None):
|
|
result={key:spec[0] for key,spec in SCHEMA.items()};deferred=[]
|
|
for label,data,keys in [('ai',ai,AI_KEYS),('intel',intel,INTEL_KEYS)]:
|
|
if data is None:continue
|
|
if not isinstance(data,dict):raise ValueError(label+' settings must be a JSON object')
|
|
if 'configVersion' in data and (type(data['configVersion']) not in (int,float) or data['configVersion']!=1):
|
|
raise ValueError(label+' configVersion must be 1')
|
|
for key in keys:
|
|
if key not in data:continue
|
|
value=data[key];_,lo,hi=SCHEMA[key]
|
|
if type(value) not in (int,float) or not lo<=value<=hi or int(value)!=value:
|
|
raise ValueError(f'{key} must be an integer in [{lo},{hi}]')
|
|
result[key]=int(value)
|
|
recognized=set(keys)|{'configVersion'}
|
|
if label=='ai' and 'stingerChanceOneIn' in data:
|
|
old=data['stingerChanceOneIn']
|
|
if type(old) not in (int,float) or not 0<=old<=10000 or int(old)!=old:
|
|
raise ValueError('stingerChanceOneIn must be an integer in [0,10000]')
|
|
if 'antiAirChancePercent' not in data:result['antiAirChancePercent']=int(100/old+0.5) if old else 0
|
|
recognized.add('stingerChanceOneIn')
|
|
if label=='ai' and 'convoyCount' in data:
|
|
if 'automaticConvoyEnabled' in data:raise ValueError('Supply either convoyCount or automaticConvoyEnabled, not both')
|
|
count=data['convoyCount']
|
|
if type(count) not in (int,float) or not 0<=count<=20 or int(count)!=count:
|
|
raise ValueError('convoyCount must be an integer in [0,20]')
|
|
result['automaticConvoyEnabled']=int(count>0)
|
|
result['convoyCount']=int(count)
|
|
recognized.add('convoyCount')
|
|
deferred.extend(label+':'+key for key in data if key not in recognized)
|
|
return result,deferred
|
|
|
|
def write_new(path,text):
|
|
path=Path(path)
|
|
with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,prefix='.rereintroduced-',delete=False) as f:
|
|
tmp=Path(f.name)
|
|
try:
|
|
f.write(text);f.flush();os.fsync(f.fileno())
|
|
# Atomic installation that cannot overwrite an existing file, even in a race.
|
|
os.link(tmp,path)
|
|
finally:tmp.unlink(missing_ok=True)
|
|
|
|
def main():
|
|
p=argparse.ArgumentParser(description=__doc__)
|
|
p.add_argument('--ai',type=Path,help='Read-only escapists-ai.json source')
|
|
p.add_argument('--intel',type=Path,help='Read-only escapists-intel.json source')
|
|
p.add_argument('--out',type=Path,help='New rereintroduced.json; omit to preview on stdout')
|
|
a=p.parse_args()
|
|
if not a.ai and not a.intel:p.error('Supply --ai and/or --intel')
|
|
try:
|
|
result,deferred=merge(json.loads(a.ai.read_text()) if a.ai else None,json.loads(a.intel.read_text()) if a.intel else None)
|
|
text=json.dumps(result,indent=2,allow_nan=False)+'\n'
|
|
if a.out:write_new(a.out,text)
|
|
else:print(text,end='')
|
|
if deferred:print('Migration notes; unrecognized settings remain deferred (source files retained): '+', '.join(deferred),file=sys.stderr)
|
|
except (ValueError,OSError) as e:p.error(str(e))
|
|
if __name__=='__main__':main()
|