File size: 14,954 Bytes
3597f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/* ============================================================================

   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(){});
  };
})();