/* ============================================================================ BOOT LOADER ---------------------------------------------------------------------------- Decides which copy of the game to run: the one packaged in this build, or a patch the updater has downloaded since. It has to be tiny and it has to be safe, because it is the one piece of code that cannot itself be patched. Two rules keep it safe: 1. A patched bundle is only used if it is COMPLETE — every file named in its own load order is present. A partial bundle is discarded on sight. 2. Before running a patch it writes a probation record, and the game clears that record once it has actually reached its first frame. If the record is still there on the next launch, the patch failed to boot and is thrown away automatically. The packaged build never leaves the device, so falling back costs nothing. Scripts are appended in manifest order and run as classic scripts sharing one scope, exactly as the static tags did — the load order is the contract, and it does not change just because the source came from storage. ============================================================================ */ (function(){ var MANIFEST=[ './assets/data/theatreprofiles-stage10.js', './assets/data/interiortopology-stage10.js','./assets/data/orbitaltopology-stage10.js', './src/game/determinism.js', './assets/data/unitrows.js','./assets/data/unitsheet.js','./assets/data/itemart.js','./assets/data/planetart.js','./src/engine/gl.js','./src/engine/planetpreview.js','./src/engine/perf.js','./src/engine/noisegen.js','./src/engine/factionenergy.js','./src/engine/ordnancetrails.js','./src/engine/terragen.js','./src/terralab.js','./src/engine/mesh.js','./src/engine/billboard.js','./src/engine/macrofx.js','./src/engine/shieldfx.js','./src/engine/tacticons.js','./src/engine/gpufx.js','./src/engine/volfx.js','./src/engine/shockwave.js','./src/engine/vfxlayers.js','./src/engine/organicfx.js','./assets/basis/basis_transcoder.js','./src/engine/materials.js','./src/engine/materials-v2.js','./src/engine/terrain.js','./src/engine/models-world-data.js','./src/engine/models-world-loader.js','./src/engine/models.js','./assets/data/worldkit.js','./assets/data/locationgrammar.js','./assets/data/sitetemplates.js','./assets/data/sitetemplates-stage9.js','./assets/data/locationplans.js','./assets/data/battlefieldtopology-stage10.js','./src/engine/worldsites.js','./assets/data/meshes.js','./src/engine/models-legion.js','./src/engine/models-machine.js','./src/engine/models-infestation.js','./src/engine/models-civic.js','./src/engine/models-skyline.js','./src/engine/materials-world-v2.js','./src/engine/models-units-nova.js','./src/engine/models-units-legion.js','./src/engine/models-units-syndicate.js','./src/engine/models-units-brood.js','./src/engine/brood-crowd.js','./src/engine/modkit.js','./src/engine/physics.js','./src/engine/cloudfx.js','./src/engine/cloudpostfx.js','./src/game/utilityjobs.js','./src/game/sim.js','./src/game/airwarfare.js','./src/game/economy.js','./src/game/commander.js','./src/game/meta.js','./src/game/ai.js','./src/ui/input.js','./src/ui/facticons.js','./src/ui/hud.js','./src/ui/render3d.js','./src/ui/orderfx.js','./src/airlift.js','./src/airlift-factions.js','./src/rumble.js','./src/factions.js','./src/factext.js','./src/offline.js','./src/audio.js','./src/assetpack.js','./src/content-mount.js','./src/hazards.js','./src/game/statehash.js','./src/authportal.js','./src/tutorial.js','./src/adboards.js','./src/storeui.js','./src/restree3d.js','./src/develop.js','./src/factiondoctrine.js','./src/endgame.js','./src/story.js','./src/socialui.js','./src/game/matchconsumer.js','./src/daily.js','./src/account.js','./src/economy-net.js','./src/updater.js','./src/intro.js','./src/launcher.js','./src/session.js','./src/faction-id.js','./src/glrecover.js','./src/main.js','./src/intel.js','./src/repairbay.js','./src/galaxyui.js','./src/departure.js','./src/warprimer.js','./src/uistack.js','./src/ui/hudflow.js','./src/ui/hotslots.js','./src/ui/unit-stack-hotbar.js','./src/galactic-operations.js','./src/onboarding.js','./src/career-faction-gate.js','./src/ui/cinematic-hud.js','./src/stormpeak-tester.js' ]; /* Packaged scripts need a release key. WebViews and development browsers can otherwise reuse a stale source even after the installer or local preview has changed, which made new settings appear to be missing until cache was cleared manually. Patch bundles keep their content-addressed Blob URLs. */ var PACKAGED_REV='1.33.99'; /* Source-cache revision is deliberately independent from the user-facing release number. Local/hotfix rebuilds of the same release must not reuse an older gl.js merely because its ?v= version string is unchanged. */ /* Factory command and native content fixes change the same candidate's source bytes. A new cache key prevents an earlier local preview lingering. */ var PACKAGED_SRC_REV=PACKAGED_REV+'-boot1'; var DB='massfront-updates', STORE='bundles'; /* Packaged boot capability flag. OTA source checks this before staging a descriptor that references per-artifact records. Old APK-bound boot loaders never set it and therefore continue receiving the legacy complete in-record bundle they understand. */ window.__MF_ARTIFACT_BOOT_V1=true; /* Source preview and the default player pack include the signed Galactic runtime allowlist. pack-www rewrites only its copied boot.js to false for an explicitly slim package, giving later OTA launcher code local build authority without ever requesting an intentionally absent module path. */ window.__MF_BUILD_HAS_GALACTIC_EXPLORATION=true; window.__MF_BUILD_HAS_STORMPEAK_TESTER=true; var bootShield=null, bootShieldTimer=0, bootShieldWatchdog=0; var bootShieldEvents=['pointerdown','pointerup','touchstart','touchend','click']; /* Absolute ceiling on how long the shield/guard may live. The guard is only ever lowered by __bootOk()->releaseBootShield() on the first frame; if that never runs (a WebGL failure, a throw before the render loop, or an OTA shell whose release hook is absent) an unbounded sentinel would strand every guard-reading control forever — a real Android device saw exactly that on the Account button. Cap it and back it with a watchdog so it can never persist; the install gesture cannot still be in flight seconds on. */ var BOOT_SHIELD_MAX_MS=6000; /* The install gesture began in the previous document. Block every pointer until this document has rendered a real frame, plus the short interval in which Android can synthesize a click from that old gesture. This lives in the immutable boot loader rather than an OTA bundle, so an old or broken patch cannot omit the protection it needs in order to restart safely. */ function blockBootInput(e){ e.preventDefault(); e.stopImmediatePropagation(); } function installBootShield(){ /* Bounded deadline, never Number.MAX_SAFE_INTEGER: apBindTap-style controls suppress taps while Date.now() is below this, so an infinite sentinel the release path failed to lower would kill them permanently. */ window.__MASSFRONT_INPUT_GUARD_UNTIL=Date.now()+BOOT_SHIELD_MAX_MS; bootShield=document.createElement('div'); bootShield.setAttribute('aria-hidden','true'); bootShield.setAttribute('data-mf-boot-input-shield',''); bootShield.style.cssText='position:fixed;inset:0;z-index:2147483647;background:transparent;pointer-events:auto;touch-action:none'; /* The OTA shell replaces body.innerHTML. A child of survives that replacement and continues intercepting the release of the install tap. */ document.documentElement.appendChild(bootShield); for(var i=0;i=0&&age<5*60*1000); } function probationMatchesBundle(prob,bundle){ if(!prob||!bundle||String(prob.version)!==String(bundle.version)|| patchChannel(prob)!==patchChannel(bundle)||!sameRoots(prob,bundle)) return false; return prob.pendingAt==null||String(prob.pendingAt)===String(bundle.at||''); } function probationOwnsMetaExact(prob,meta){ return !!(prob&&meta&&prob.pendingAt!=null&&meta.at!=null&& String(prob.version)===String(meta.version)&& patchChannel(prob)===patchChannel(meta)&& sameRoots(prob,meta)&& String(prob.pendingAt)===String(meta.at)); } function pendingMatchesFailure(pending,prob,bundle,version){ if(!pending||String(pending.version)!==String(version)) return false; var owner=prob||bundle; if(owner&&patchChannel(pending)!==patchChannel(owner)) return false; if(owner&&!sameRoots(pending,owner)) return false; var token=prob&&prob.pendingAt!=null?prob.pendingAt: (bundle&&bundle.at!=null?bundle.at:null); /* Old records without an attempt token cannot be distinguished from a freshly downloaded rebuild carrying the same version. Retaining one ambiguous payload is safer than deleting new verified bytes. */ return token!=null&&String(pending.at||'')===String(token); } /* Confirm exactly the patch represented by probation in one transaction. The old three independent deletes left a cross-window gap: a newly staged pending payload could land after probation was cleared and then be erased by the final unconditional delete. Serialized conditional cleanup means a concurrent stage either sees probation and stops, or commits after this transaction and survives for its own install. */ function confirmPatch(db,identity){ return new Promise(function(res){ try{ var tx=db.transaction(STORE,'readwrite'),store=tx.objectStore(STORE); var probation=store.get('probation'),pending=store.get('pendingMeta'); var failure=store.get('applyFailure'),active=store.get('activeMeta'); var operation=store.get('operation'),ready=0; function inspect(){ if(++ready<5) return; var prob=probation.result,pend=pending.result,fail=failure.result; var running=active.result,op=operation.result; if(!sameBundle(running,identity)||!probationMatchesBundle(prob,running)) return; store.delete('probation'); if(fail&&String(fail.version)===String(running.version)) store.delete('applyFailure'); if(pendingMatchesFailure(pend,prob,running,running.version)){ store.delete('pending'); store.delete('pendingMeta'); } /* A killed Apply document can leave its lease after the atomic active/probation commit. Only the first proven frame of that exact target owns the orphan; an unrelated or still-uncommitted Apply remains visible to updater recovery. */ if(op&&op.kind==='apply'&&sameBundle(op.target,running)) store.delete('operation'); } probation.onsuccess=inspect; pending.onsuccess=inspect; failure.onsuccess=inspect; active.onsuccess=inspect; operation.onsuccess=inspect; tx.oncomplete=function(){ res(); }; tx.onerror=function(){ res(); }; tx.onabort=function(){ res(); }; }catch(e){ res(); } }); } function verNewer(a,b){ var pa=String(a||'').split('.').map(Number); var pb=String(b||'').split('.').map(Number); for(var i=0;iy; } return false; } function validBundle(b){ if(!b||!b.files||!verNewer(b.version,PACKAGED_REV)) return false; var order=b.order&&b.order.length?b.order:MANIFEST; var schema=Number(b.schema||1); if(schema>=3){ var hex=/^[0-9a-f]{64}$/; if(!Number.isSafeInteger(schema)|| !hex.test(String(b.manifestRoot||''))|| !hex.test(String(b.payloadRoot||''))|| !hex.test(String(b.sourcePayloadRoot||''))|| !hex.test(String(b.fullRoot||''))|| !hex.test(String(b.targetRoot||''))|| !hex.test(String(b.runtimeRoot||''))|| String(b.targetRoot)!==String(b.fullRoot)|| (String(b.payloadRoot)!==String(b.sourcePayloadRoot)&& String(b.payloadRoot)!==String(b.fullRoot))|| (b.manifestKind!=='full'&&b.manifestKind!=='patch')|| ['system','hotfix','content','overhaul'].indexOf(b.manifestCategory)<0|| (b.manifestKind==='patch'&&!/^\d+\.\d+\.\d+$/.test(String(b.manifestPatchFrom||'')))|| (b.manifestKind==='full'&&String(b.manifestPatchFrom||'')!=='')|| (b.channel!=='stable'&&b.channel!=='preview')|| (b.storage!=='artifact-v1'&&b.storage!=='bundle-v1')|| !Array.isArray(b.order)||!b.order.length|| !b.files||typeof b.files!=='object'||Array.isArray(b.files)) return false; var own=Object.keys(b.files),seen={}; if(own.length!==b.order.length) return false; for(var p=0;p512|| !/^[A-Za-z0-9._/-]+$/.test(path)||path.charAt(0)==='/'|| path.slice(-1)==='/'||path.indexOf('//')>=0|| path.split('/').some(function(part){return !part||part==='.'||part==='..';})|| seen[path]||!Object.prototype.hasOwnProperty.call(b.files,path)) return false; seen[path]=true; } for(var o=0;o=3&&b.storage!=='bundle-v1') return false; continue; } if(!file||typeof file!=='object'||typeof file.key!=='string'||!file.key|| typeof file.sha256!=='string'||!/^[0-9a-f]{64}$/i.test(file.sha256)|| !Number.isSafeInteger(file.size)||file.size<=0) return false; if(schema>=3){ if(b.storage!=='artifact-v1') return false; var suffix=':file:'+order[i]; if(file.key.slice(-suffix.length)!==suffix|| !/^transfer-v1:(stable|preview)-\d+\.\d+\.\d+-[0-9a-f]{64}-(payload|full)$/.test( file.key.slice(0,-suffix.length))) return false; } } return true; } function evictSuperseded(db){ var keys=['activeMeta','pendingMeta','probation','previousRef', 'previousAMeta','previousBMeta','previousMeta','operation']; return new Promise(function(res,rej){ try{ var tx=db.transaction(STORE,'readwrite'),store=tx.objectStore(STORE); var requests=[],records={},ready=0,evicted=[]; function discard(metaKey,payload,label,rec){ store.delete(payload); if(metaKey!==payload) store.delete(metaKey); evicted.push({key:label,version:rec.version||'?'}); } function inspect(){ if(++ready=2}; store.put(failure,'applyFailure'); store.delete('probation'); /* Recovered builds deliberately skip __bootOk confirmation. Retire the Apply lease here only when probation, failed active identity, and lease target all name the same exact committed payload. */ var op=records.operation; if(op&&op.kind==='apply'&&probationOwnsMetaExact(prob,bundle)&& sameBundleExact(op.target,bundle)) store.delete('operation'); if(failure.quarantined&& pendingMatchesFailure(records.pendingMeta,prob,bundle,version)){ store.delete('pending'); store.delete('pendingMeta'); } /* Rollback payloads are about 85 MB. Resolve the small atomic pointer first and queue exactly one payload read only after failure is certain. A torn/mismatched slot pointer falls back to the legacy key without ever deserialising the unowned slot. */ var selected=previousSelection(records); var previous=store.get(selected.key); previous.onsuccess=function(){ var value=previous.result,meta=selected.meta; var good=validBundle(value)&&String(value.version)!==String(version)&& (selected.slotted ?sameBundleExact(meta,value)&&sameBundleExact(selected.ref,value) :(!meta||sameBundle(meta,value))); if(good){ store.put(value,'active'); store.put(bundleMeta(value),'activeMeta'); /* Re-validate the rollback payload on this launch. Its metadata was previously trusted, but descriptor records may since have been evicted or corrupted. tries:1 makes the next launch fail closed to packaged code unless this exact recovered runtime reaches __bootOk now. */ store.put({version:value.version,channel:value.channel||'stable', manifestRoot:value.manifestRoot||'',targetRoot:value.targetRoot||'', runtimeRoot:value.runtimeRoot||'', pendingAt:value.at,at:Date.now(),tries:1,recoveryOf:version},'probation'); clearPreviousRecords(store); clearFinishedRollback(value); decision={kind:'recovered',bundle:value,failedVersion:version, failure:failure}; }else{ store.delete('active'); store.delete('activeMeta'); clearPreviousRecords(store); clearFinishedRollback(null); decision={kind:'packaged',failedVersion:version,failure:failure}; } }; } function inspectActive(bundle){ var prob=records.probation,meta=records.activeMeta; /* Metadata-backed supersession was already handled without touching payloads. This is the one compatibility read required for a pre-metadata active record. */ if(bundle&&!verNewer(bundle.version,PACKAGED_REV)){ store.delete('active'); store.delete('activeMeta'); if(prob&&probationMatchesBundle(prob,bundle)) store.delete('probation'); clearFinishedRollback(null); decision={kind:'packaged'}; return; } if(bundle&&!validBundle(bundle)){ fail(bundle.version,'The downloaded update was incomplete at restart.',prob,bundle); return; } if(bundle&&meta&&!sameBundle(meta,bundle)){ fail(bundle.version,'The installed update metadata did not match its payload.',prob,bundle); return; } if(!bundle&&meta){ fail(meta.version,'The downloaded update was not available at restart.',prob,meta); return; } if(prob&&!bundle){ fail(prob.version,'The downloaded update was not available at restart.',prob,null); return; } if(bundle&&!meta){ /* One-time migration for installs written before lightweight metadata existed. Confirmation can now stay off the payload. */ store.put(bundleMeta(bundle),'activeMeta'); } if(bundle&&prob&&probationMatchesBundle(prob,bundle)){ if((prob.tries|0)>=1){ fail(prob.version,'The downloaded update did not finish starting.',prob,bundle); return; } var claimed={version:bundle.version,channel:bundle.channel||prob.channel, manifestRoot:bundle.manifestRoot||'',targetRoot:bundle.targetRoot||'', runtimeRoot:bundle.runtimeRoot||'', pendingAt:bundle.at!=null?bundle.at:prob.pendingAt, at:prob.at||Date.now(),tries:(prob.tries|0)+1}; store.put(claimed,'probation'); clearFinishedRollback(bundle); decision={kind:'run',bundle:bundle,claimed:true}; return; } if(bundle&&prob){ /* A mismatched legacy/torn guard must never be used to validate a different active record. Replace it with an exact first claim. */ store.put({version:bundle.version,channel:bundle.channel||'stable', manifestRoot:bundle.manifestRoot||'',targetRoot:bundle.targetRoot||'', runtimeRoot:bundle.runtimeRoot||'', pendingAt:bundle.at,at:Date.now(),tries:1},'probation'); clearFinishedRollback(bundle); decision={kind:'run',bundle:bundle,claimed:true,repaired:true}; return; } if(bundle){ clearFinishedRollback(bundle); decision={kind:'run',bundle:bundle,claimed:false}; }else clearFinishedRollback(null); } function inspect(){ if(++ready=1 already proves those bytes started and never confirmed. Reject them without deserialising the failed 85 MiB active payload; recovery needs only the one previous payload. */ if(prob&&(prob.tries|0)>=1&&probationOwnsMetaExact(prob,meta)){ fail(prob.version,'The downloaded update did not finish starting.',prob,meta); return; } var active=store.get('active'); active.onsuccess=function(){ records.active=active.result; inspectActive(active.result); }; } for(var i=0;i0&&done>=total&&!verifying; var clean=String(path||'').replace(/^\.\//,'').split('?')[0]; var phase=finished?'STARTING MASSFRONT':verifying?'VERIFYING INSTALLED UPDATE': /engine\/gl\.js/.test(clean)?'INITIALIZING GRAPHICS': /engine\/|assets\/data\//.test(clean)?'PREPARING WORLD SYSTEMS': /audio|factions|story|tutorial/.test(clean)?'PREPARING GAME CONTENT': /ui\/|intro|launcher|main\.js/.test(clean)?'PREPARING COMMAND INTERFACE':'PREPARING COMMAND SYSTEMS'; bootText('mfBootPct',total?Math.max(0,Math.min(total,done))+' / '+total:'STARTING'); bootText('mfBootPhase',phase); var detail=/glrecover/.test(clean)?'Preparing graphics recovery and reconnect controls.': /engine\//.test(clean)?'Loading terrain, models and visual systems.': /audio|assetpack/.test(clean)?'Preparing audio and additional-content delivery.': /auth|account|session/.test(clean)?'Preparing account access and local career support.': /ui\/|intro|launcher|main\.js/.test(clean)?'Preparing menus and command controls.': /assets\/data\//.test(clean)?'Loading world and unit definitions.':'Preparing game rules and career systems.'; bootText('mfBootDetail',finished?'Preparing the first frame and launch interface.':verifying?'Checking the integrity of stored runtime files.':detail); var bar=document.getElementById('mfBootBar'),fill=document.getElementById('mfBootFill'); if(bar&&bar.setAttribute){bar.setAttribute('aria-valuemin','0');bar.setAttribute('aria-valuemax',String(total||1));bar.setAttribute('aria-valuenow',String(done));bar.setAttribute('aria-valuetext',finished?'Game systems loaded; starting MASSFRONT':done+' of '+total+' game systems '+(verifying?'verified':'loaded'));} if(fill&&fill.style)fill.style.width=(total?Math.max(0,Math.min(100,done/total*100)):0)+'%'; var cover=document.getElementById('mfBootCover'); if(cover){cover.setAttribute('data-progress','determinate');cover.setAttribute('aria-busy','true');} } function rendererGateIndex(order){ for(var i=0;i=total){ textDigest(runtimeRows.join('\n')).then(function(actual){ if(Number(b.schema||1)<3) return actual; if(actual.sha256!==String(b.runtimeRoot||'').toLowerCase()) throw new Error('ordered runtime root mismatch'); var contract=[ 'schema='+b.schema,'channel='+b.channel,'version='+b.version, 'kind='+b.manifestKind,'category='+b.manifestCategory, 'patchFrom='+b.manifestPatchFrom, 'payload='+b.sourcePayloadRoot,'full='+b.fullRoot, 'runtime='+actual.sha256 ].join('\n'); return textDigest(contract).then(function(root){ if(root.sha256!==String(b.manifestRoot||'').toLowerCase()) throw new Error('manifest root contract mismatch'); return actual; }); }).then(function(){ index=0;bootProgress(0,total);next(); }).catch(function(error){ rangeFailure('__runtime__',error&&error.message); }); return; } var path=order[i],ref=b.files[path]; if(typeof ref==='string'){ textDigest(ref).then(function(actual){ if(!Number.isSafeInteger(actual.size)||actual.size<=0) throw new Error('stored size mismatch'); runtimeRows.push(path+'|'+actual.size+'|'+actual.sha256); bootProgress(i+1,total,path,'verify');preflight(i+1); }).catch(function(error){ rangeFailure(path,error&&error.message); }); return; } readArtifact(path,ref).then(function(){ runtimeRows.push(path+'|'+ref.size+'|'+String(ref.sha256).toLowerCase()); bootProgress(i+1,total,path,'verify');preflight(i+1); }).catch(function(error){ rangeFailure(path,error&&error.message); }); } function next(){ if(stopped||index>=total) return; var path=order[index],ref=b.files[path]; if(typeof ref==='string'){ append(ref,path); return; } readArtifact(path,ref).then(function(text){ append(text,path); }) .catch(function(error){ rangeFailure(path,error&&error.message); }); } preflight(0); } function runBundle(b){ /* Blob URLs rather than inline text: the browser keeps a real filename for each source, so a stack trace from a patched build is still readable. */ var order=b.order&&b.order.length? b.order : MANIFEST; if(Number(b.schema||1)>=3&&b.runtimeRoot){ runArtifactBundle(b);return; } for(var i=0;i