CREATORJD's picture
Publish MASSFRONT web 1.33.33 (part 3)
3597f5a verified
Raw
History Blame Contribute Delete
15 kB
/* ============================================================================
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/unitrows.js','./assets/data/unitsheet.js','./assets/data/itemart.js','./src/engine/gl.js','./src/engine/terragen.js','./src/terralab.js','./src/engine/mesh.js','./src/engine/billboard.js','./src/engine/tacticons.js','./src/engine/gpufx.js','./src/engine/organicfx.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/sitetemplates.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/modkit.js','./src/game/sim.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/hazards.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/daily.js','./src/account.js','./src/economy-net.js','./src/updater.js','./src/intro.js','./src/session.js','./src/faction-id.js','./src/glrecover.js','./src/main.js','./src/intel.js','./src/repairbay.js','./src/galaxyui.js','./src/warprimer.js','./src/uistack.js','./src/ui/hudflow.js','./src/ui/hotslots.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.33';
var DB='massfront-updates', STORE='bundles';
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 <html> survives that
replacement and continues intercepting the release of the install tap. */
document.documentElement.appendChild(bootShield);
for(var i=0;i<bootShieldEvents.length;i++)
document.addEventListener(bootShieldEvents[i],blockBootInput,true);
/* Watchdog backstop: drop the shield even if a frame is never confirmed, so
a failed boot degrades to a usable menu rather than a permanently dead
screen. releaseBootShield()/clearBootShield() cancel it on the happy path. */
bootShieldWatchdog=setTimeout(clearBootShield,BOOT_SHIELD_MAX_MS);
}
function clearBootShield(){
if(bootShieldWatchdog){ clearTimeout(bootShieldWatchdog); bootShieldWatchdog=0; }
if(bootShieldTimer){ clearTimeout(bootShieldTimer); bootShieldTimer=0; }
for(var i=0;i<bootShieldEvents.length;i++)
document.removeEventListener(bootShieldEvents[i],blockBootInput,true);
if(bootShield&&bootShield.parentNode) bootShield.parentNode.removeChild(bootShield);
bootShield=null;
window.__MASSFRONT_INPUT_GUARD_UNTIL=0;
}
function releaseBootShield(){
if(bootShieldTimer) return;
window.__MASSFRONT_INPUT_GUARD_UNTIL=Date.now()+450;
bootShieldTimer=setTimeout(clearBootShield,450);
}
installBootShield();
function idb(){
return new Promise(function(res,rej){
var r=indexedDB.open(DB,1);
r.onupgradeneeded=function(){ var d=r.result;
if(!d.objectStoreNames.contains(STORE)) d.createObjectStore(STORE); };
r.onsuccess=function(){ res(r.result); };
r.onerror=function(){ rej(r.error); };
});
}
function get(db,key){
return new Promise(function(res){
try{
var tx=db.transaction(STORE,'readonly'), q=tx.objectStore(STORE).get(key);
q.onsuccess=function(){ res(q.result); }; q.onerror=function(){ res(null); };
}catch(e){ res(null); }
});
}
function del(db,key){
return new Promise(function(res){
try{
var tx=db.transaction(STORE,'readwrite');
tx.objectStore(STORE).delete(key);
tx.oncomplete=function(){ res(); }; tx.onerror=function(){ res(); };
}catch(e){ res(); }
});
}
function put(db,key,val){
return new Promise(function(res){
try{
var tx=db.transaction(STORE,'readwrite');
tx.objectStore(STORE).put(val,key);
tx.oncomplete=function(){ res(); }; tx.onerror=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;i<Math.max(pa.length,pb.length);i++){
var x=pa[i]||0, y=pb[i]||0;
if(x!==y) return x>y;
}
return false;
}
function failed(db,version,reason){
version=version||'?';
return get(db,'applyFailure').then(function(prev){
var count=prev&&prev.version===version?(prev.count|0)+1:1;
var rec={version:version,at:Date.now(),reason:reason,count:count,
quarantined:count>=2};
return put(db,'applyFailure',rec).then(function(){ return rec; });
});
}
function dropPendingVersion(db,version){
return get(db,'pending').then(function(p){
return p&&p.version===version?del(db,'pending'):null;
});
}
function validBundle(b){
if(!b||!b.files||!verNewer(b.version,PACKAGED_REV)) return false;
var order=b.order&&b.order.length?b.order:MANIFEST;
for(var i=0;i<order.length;i++) if(typeof b.files[order[i]]!=='string') return false;
return true;
}
function restorePreviousOrPackaged(db,failedVersion){
return get(db,'previous').then(function(prev){
if(!validBundle(prev)||String(prev.version)===String(failedVersion)){
return del(db,'previous').then(function(){ runPackaged(); });
}
/* This record was captured only while its exact version was running after
probation. Consume it once: a broken update must never bounce between
two patches. Keep the failed pending payload so the Update screen can
offer one controlled retry and explain what happened. */
return put(db,'active',prev).then(function(){ return del(db,'previous'); })
.then(function(){
window.__MASSFRONT_PATCHED=prev.version||'?';
window.__MASSFRONT_RECOVERED_PATCH=failedVersion||'?';
console.warn('boot: restored validated patch '+prev.version+
' after '+failedVersion+' failed');
runBundle(prev);
});
});
}
function rejectPatch(db,version,reason){
return failed(db,version,reason).then(function(rec){
if(rec.quarantined)
console.warn('boot: patch '+version+' failed twice and was quarantined');
return del(db,'active')
.then(function(){ return del(db,'probation'); })
.then(function(){ return rec.quarantined?dropPendingVersion(db,version):null; })
.then(function(){ return restorePreviousOrPackaged(db,version); });
});
}
function evictSuperseded(db){
var keys=['active','pending','probation','previous'];
return Promise.all(keys.map(function(key){ return get(db,key); }))
.then(function(records){
var work=[];
for(var i=0;i<keys.length;i++){
var rec=records[i];
if(rec&&!verNewer(rec.version,PACKAGED_REV)){
console.info('boot: discarding '+keys[i]+' patch '+(rec.version||'?')+
'; packaged build is '+PACKAGED_REV);
work.push(del(db,keys[i]));
}
}
return Promise.all(work);
});
}
function runPackaged(){
var i=0;
(function next(){
if(i>=MANIFEST.length) return;
var s=document.createElement('script');
s.src=MANIFEST[i++]+'?v='+PACKAGED_REV; s.async=false;
s.onload=next;
s.onerror=function(){ console.error('boot: failed',s.src); next(); };
document.body.appendChild(s);
})();
}
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;
var i=0;
(function next(){
if(i>=order.length) return;
var path=order[i++], src=b.files[path];
var s=document.createElement('script');
s.async=false;
if(src==null){ s.src=path; }
else s.src=URL.createObjectURL(new Blob([src+'\n//# sourceURL='+path],{type:'text/javascript'}));
s.onload=next;
s.onerror=function(){ console.error('boot: failed',path); next(); };
document.body.appendChild(s);
})();
}
idb().then(function(db){
/* Native app upgrades preserve WebView IndexedDB. Never let an old OTA
remain above a newer packaged APK merely because it was active before
the installer ran. Pending and probation records follow the same rule. */
return evictSuperseded(db).then(function(){
return get(db,'probation').then(function(prob){
/* Probation counts ATTEMPTS, not intent. The updater writes it at zero
before reloading; this loader claims it by incrementing. Seeing a
record that has already been claimed means the previous launch ran the
patch and never reached a frame — so it is bad, and out it goes.
Counting rather than merely existing is the difference between "we are
about to try" and "we tried and it died". */
if(prob && (prob.tries|0)>=1){
console.warn('boot: rolling back a patch that failed to start');
return rejectPatch(db,prob.version,'The downloaded update did not finish starting.');
}
return get(db,'active').then(function(b){
if(!b||!b.files){
if(!prob) return runPackaged();
return rejectPatch(db,prob.version,'The downloaded update was not available at restart.');
}
if(!validBundle(b)){
console.warn('boot: patched bundle incomplete or superseded, using a validated fallback');
return rejectPatch(db,b.version,'The downloaded update was incomplete at restart.');
}
window.__MASSFRONT_PATCHED=b.version||'?';
if(prob){
var tx=db.transaction(STORE,'readwrite');
tx.objectStore(STORE).put({version:prob.version,at:prob.at,tries:(prob.tries|0)+1},'probation');
}
runBundle(b);
});
});
});
}).catch(runPackaged);
/* Called by the game once it is genuinely running. Clearing probation is what
marks a patch as good. Only now may the retryable download be deleted. */
window.__bootOk=function(){
releaseBootShield();
idb().then(function(db){
return del(db,'probation')
/* A packaged fallback also reaches a frame. It must not erase the
failure that explains why the patch did not start; only a confirmed
patched frame has earned the right to clear recovery state. */
.then(function(){ return window.__MASSFRONT_PATCHED&&!window.__MASSFRONT_RECOVERED_PATCH?del(db,'applyFailure'):null; })
.then(function(){ return window.__MASSFRONT_PATCHED&&!window.__MASSFRONT_RECOVERED_PATCH?del(db,'pending'):null; });
}).catch(function(){});
};
})();