File size: 14,041 Bytes
feb9f49
 
 
 
 
 
 
 
 
 
 
8d3bef6
feb9f49
 
9d42da5
 
 
 
 
8d3bef6
feb9f49
 
 
 
 
 
 
 
 
8d3bef6
feb9f49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d42da5
 
8d3bef6
 
 
 
 
 
 
 
 
 
9d42da5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d3bef6
9d42da5
8d3bef6
9d42da5
 
 
 
 
 
 
 
 
8d3bef6
9d42da5
 
 
 
 
 
 
 
 
 
 
 
 
 
8d3bef6
 
 
 
 
 
 
9d42da5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
feb9f49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d42da5
feb9f49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d3bef6
feb9f49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d3bef6
 
 
 
feb9f49
 
 
 
 
 
 
 
 
 
 
 
 
 
8d3bef6
feb9f49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d42da5
feb9f49
 
 
 
 
 
 
 
 
 
9d42da5
feb9f49
 
 
 
 
 
9d42da5
 
8d3bef6
feb9f49
 
 
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
/* --------------------------------------------------------------------------
   MASSFRONT — EXPLORATION DOCUMENT BOOTSTRAP

   The reusable lifecycle lives in space_experience.js. Direct document loads
   retain the standalone simulator; a live same-tab base-game ticket selects
   the profile-isolated solo adapter. Only the canonical content downloader is
   shared with the base document; its renderer and updater are not imported,
   so MASSFRONT and Three.js cannot compete for one WebGL context.
   -------------------------------------------------------------------------- */

import './startup_content.js?v=20260906-release5';
import { createSpaceExperience } from './space_experience.js?v=20260908-uga81r1';
import { MASSFRONT_WORLD_MODEL_LIBRARY } from './core/world_model_catalog.js';
import { LocalSandboxHost } from './host/local_sandbox_host.js?v=20260825-host1';
import {
  readNativeExplorationMount,
  acknowledgeMountedRuntimeReady,
  installNativeExplorationLoaderUrls,
  resolveBaseRuntimeNavigation
} from './host/base_runtime_url.js?v=20260908-uga81r1';
import {
  MASSFRONT_GALACTIC_ENTRY_TICKET_KEY,
  MassfrontSoloHost,
  readMassfrontGalacticEntryTicket
} from './host/massfront_solo_host.js?v=20260906-release5';

export {
  SPACE_FIRST_ENTRY_CONTINUATION,
  createSpaceExperience
} from './space_experience.js?v=20260908-uga81r1';
export {
  ExplorationHostError,
  LocalSandboxHost,
  createExplorationHostV1
} from './host/local_sandbox_host.js?v=20260825-host1';
export {
  MassfrontSoloHost,
  createMassfrontGalacticEntryTicket,
  createMassfrontGalacticTacticalReportV1,
  createMassfrontSoloHost,
  readMassfrontGalacticEntryTicket,
  validateMassfrontGalacticEntryTicket
} from './host/massfront_solo_host.js?v=20260906-release5';

let gpuRebuilds = 0;
let gpuRebuildPending = false;
let selectedHost = null;
let hostSelectionError = null;
const consumedReturnNonces = new Set();
const FATAL_RETURN_SYSTEM_IDS = new Set(['aelos', 'veyra', 'karak']);
const FATAL_RETURN_TARGET_ID = /^[a-z0-9_]{1,96}$/;
const CLASSIC_FALLBACK_SESSION_KEY = 'massfront.galactic.classic-fallback.v1';

function armClassicFallbackSession() {
  try {
    window.sessionStorage.setItem(CLASSIC_FALLBACK_SESSION_KEY, '1');
    return window.sessionStorage.getItem(CLASSIC_FALLBACK_SESSION_KEY) === '1';
  } catch (_) {
    return false;
  }
}

function fatalReturnLocation() {
  let snapshot = null;
  try { snapshot = window.__MASSFRONT_SPACE__?.getState?.() || null; } catch (_) {}
  if (!snapshot) {
    try {
      const candidate = selectedHost?.loadCampaignSnapshot?.();
      if (candidate && typeof candidate.then !== 'function') snapshot = candidate;
    } catch (_) {}
  }
  const systemId = FATAL_RETURN_SYSTEM_IDS.has(snapshot?.route?.systemId) ? snapshot.route.systemId : 'aelos';
  const candidateTarget = snapshot?.route?.targetId;
  const targetId = typeof candidateTarget === 'string' && FATAL_RETURN_TARGET_ID.test(candidateTarget)
    ? candidateTarget
    : null;
  return { systemId, targetId };
}

async function returnToMassfrontFromFatalState() {
  const button = document.querySelector('[data-render-return]');
  if (button?.disabled) return;
  if (button) {
    button.disabled = true;
    button.setAttribute('aria-busy', 'true');
    button.textContent = 'RETURNING TO MASSFRONT';
  }
  const classicFallbackLatched = armClassicFallbackSession();
  try {
    if (classicFallbackLatched && selectedHost?.productionIntegrated === true && typeof selectedHost.openBaseRoute === 'function') {
      await selectedHost.openBaseRoute('war-room', fatalReturnLocation());
      return;
    }
  } catch (error) {
    // A corrupt or expired Galactic bridge must not remove the unprivileged
    // route back to the base game; the War Room request simply cannot be kept.
    console.warn('[MASSFRONT GALACTIC FAIL-SAFE]', error);
  }
  try {
    const target = resolveBaseRuntimeNavigation('../../../index.html?galacticFallback=classic');
    window.location.assign(target);
  } catch (error) {
    window.__MASSFRONT_SPACE_ERROR__ = error;
    console.error('[MASSFRONT GALACTIC FAIL-SAFE]', error);
    if (button) {
      button.disabled = false;
      button.removeAttribute('aria-busy');
      button.textContent = 'RETURN TO MASSFRONT';
    }
  }
}

function installFatalReturn() {
  const button = document.querySelector('[data-render-return]');
  window.__MASSFRONT_FATAL_RETURN_HANDLER__ = returnToMassfrontFromFatalState;
  /* index.html owns a dependency-free dispatcher so an import failure still
     has a working escape. Direct embedders without that dispatcher retain the
     normal module binding here. */
  if (button && window.__MASSFRONT_INLINE_FATAL_RETURN_BOUND__ !== true) {
    button.addEventListener('click', returnToMassfrontFromFatalState);
  }
}

function exposeFatalActions(veil) {
  const retry = veil?.querySelector('[data-render-retry]');
  if (retry) {
    retry.hidden = false;
    retry.removeAttribute('aria-busy');
  }
}

function retrySpaceAfterFatalState() {
  if (gpuRebuildPending) return;
  const button = document.querySelector('[data-render-retry]');
  if (button) button.setAttribute('aria-busy', 'true');
  // A deliberate player retry starts a fresh bounded recovery attempt. The
  // context-loss listener still caps unattended rebuilds, so this cannot loop.
  gpuRebuilds = 0;
  rebuildAfterGpuInterruption();
}

function installFatalRetry() {
  const button = document.querySelector('[data-render-retry]');
  if (button) button.addEventListener('click', retrySpaceAfterFatalState);
}

function hasIntegratedReturnQuery() {
  const parameters = new URLSearchParams(window.location.search);
  return parameters.has('groundResult') || parameters.has('groundRejected');
}

function hasGalacticEntryRecord() {
  try {
    return window.sessionStorage?.getItem(MASSFRONT_GALACTIC_ENTRY_TICKET_KEY) !== null;
  } catch (_) {
    return false;
  }
}

function chooseHost() {
  if (selectedHost) return selectedHost;
  const ticket = readMassfrontGalacticEntryTicket();
  if (ticket) {
    try {
      selectedHost = new MassfrontSoloHost({ expectedProfileId: ticket.profileId });
      return selectedHost;
    } catch (error) {
      hostSelectionError = error;
      throw error;
    }
  }
  if (hasIntegratedReturnQuery() || hasGalacticEntryRecord()) {
    hostSelectionError = new Error('The MASSFRONT Galactic entry ticket is missing, unreadable, or expired. The standalone career was not opened.');
    throw hostSelectionError;
  }
  selectedHost = new LocalSandboxHost();
  return selectedHost;
}

function showIntegratedFailure(error) {
  window.__MASSFRONT_SPACE_ERROR__ = error;
  window.__MASSFRONT_GALACTIC_RESULT__ = null;
  console.error('[MASSFRONT GALACTIC BRIDGE]', error);
  const frame = document.getElementById('moduleFrame');
  const veil = frame?.querySelector('#renderVeil');
  if (!veil) return;
  veil.className = 'render-veil failed';
  const title = veil.querySelector('b');
  const status = veil.querySelector('#loadStatus');
  if (title) title.textContent = 'GALACTIC RESULT REJECTED';
  if (status) status.textContent = String(error?.message || 'THE TACTICAL RESULT COULD NOT BE VERIFIED').toUpperCase();
  exposeFatalActions(veil);
}

function quarantineReturnedTacticalResult(experience, error) {
  window.__MASSFRONT_SPACE_ERROR__ = error;
  window.__MASSFRONT_GALACTIC_RESULT__ = null;
  console.warn('[MASSFRONT GALACTIC RESULT RECOVERY]', error);
  if (window.history && typeof window.history.replaceState === 'function') {
    window.history.replaceState(window.history.state, '', `${window.location.pathname}${window.location.hash || ''}`);
  }
  experience.recoverGroundOperation?.(error);
}

async function consumeReturnedTacticalResult(experience, host) {
  const parameters = new URLSearchParams(window.location.search);
  if (!parameters.has('groundResult')) return;
  await experience.ready;
  const nonce = parameters.get('groundResult');
  if (consumedReturnNonces.has(nonce)) return;
  if (hostSelectionError) throw hostSelectionError;
  if (host.productionIntegrated !== true || typeof host.consumeTacticalResult !== 'function') {
    throw new Error('A live MASSFRONT Galactic entry ticket is required to consume this tactical result.');
  }
  const outcome = await host.consumeTacticalResult(nonce);
  if (!outcome?.accepted && !outcome?.duplicate) throw new Error('The MASSFRONT tactical result was not accepted.');
  consumedReturnNonces.add(nonce);
  let queryStripped = false;
  if (window.history && typeof window.history.replaceState === 'function') {
    window.history.replaceState(window.history.state, '', `${window.location.pathname}${window.location.hash || ''}`);
    queryStripped = !new URLSearchParams(window.location.search).has('groundResult');
  }
  let finalization = { finalized: false, deferred: true };
  if (queryStripped && typeof host.finalizeTacticalResult === 'function') {
    try {
      finalization = await host.finalizeTacticalResult(nonce, outcome.resultId);
    } catch (error) {
      finalization = { finalized: false, deferred: true, warning: String(error?.message || error) };
      console.warn('[MASSFRONT GALACTIC RESULT FINALIZATION]', error);
    }
  }
  window.__MASSFRONT_GALACTIC_RESULT__ = { ...outcome, finalization };
}

function boot() {
  const container = document.getElementById('moduleFrame');
  if (!container || window.__MASSFRONT_SPACE__) return;
  try {
    const mountedRuntime = readNativeExplorationMount();
    installNativeExplorationLoaderUrls();
    const host = chooseHost();
    const integrated = host.productionIntegrated === true;
    container.dataset.runtime = integrated ? 'massfront' : 'sandbox';
    container.dataset.entryView = host.ticket?.entryView || 'system';
    const reset = container.querySelector('#btnResetRoom');
    if (reset) reset.hidden = integrated;
    const experience = createSpaceExperience(container, {
      host,
      entryView: host.ticket?.entryView || 'system',
      seed: 'massfront-cinematic-test-room-v1'
    });
    window.__MASSFRONT_SPACE__ = experience;
    window.__MASSFRONT_SPACE_HOST__ = host;
    window.__MASSFRONT_SPACE_ERROR__ = null;
    window.__MASSFRONT_STORY_RAIL__ = experience.transmissions;
    window.__MASSFRONT_WORLD_MODELS__ = MASSFRONT_WORLD_MODEL_LIBRARY;
    window.__MASSFRONT_SPACE_ENTRY__ = {
      getState: () => experience.firstEntryIntro,
      start: () => experience.startFirstEntryIntro()
    };
    const entryView = host.ticket?.entryView || 'system';
    const introRequired = host.ticket?.introRequired !== false;
    if (host.productionIntegrated === true && entryView === 'system' && introRequired && !hasIntegratedReturnQuery()) {
      /* A new experimental career first sees the real exterior scene. The
         story rail then offers the protected tutorial or the required faction
         gate without ever routing through the ship cutaway. */
      experience.ready.then(() => experience.startFirstEntryIntro()).catch(error => {
        window.__MASSFRONT_SPACE_ERROR__ = error;
        console.error('[MASSFRONT GALACTIC FIRST ENTRY]', error);
      });
    } else if (host.productionIntegrated === true && entryView === 'campaign_hub') {
      /* Strategic controls are DOM + campaign state, so expose them on the
         first module frame. Exterior PBR and the optional ship cutaway no
         longer hold Campaign Hub navigation behind a multi-minute asset load. */
      Promise.resolve(experience.openCampaignHub({ persist: false })).catch(error => {
        window.__MASSFRONT_SPACE_ERROR__ = error;
        console.error('[MASSFRONT GALACTIC ENTRY]', error);
      });
    }
    experience.ready.catch(error => {
      window.__MASSFRONT_SPACE_ERROR__ = error;
      console.error('[MASSFRONT SPACE]', error);
    });
    experience.ready.then(() => {
      if (window.__MASSFRONT_SPACE__ === experience && !experience.disposed
        && !window.__MASSFRONT_SPACE_ERROR__) acknowledgeMountedRuntimeReady(mountedRuntime);
    }, () => {});
    consumeReturnedTacticalResult(experience, host).catch(error => quarantineReturnedTacticalResult(experience, error));
  } catch (error) {
    if (hostSelectionError || hasIntegratedReturnQuery() || selectedHost?.productionIntegrated === true) showIntegratedFailure(error);
    else {
      // createSpaceExperience paints the player-facing failure state before it
      // throws. Catch here so the standalone bootstrap never strands the shell
      // in an indefinite loading state or produces an unhandled module error.
      window.__MASSFRONT_SPACE_ERROR__ = error;
      console.error('[MASSFRONT SPACE]', error);
    }
  }
}

function rebuildAfterGpuInterruption() {
  if (gpuRebuildPending) return;
  gpuRebuildPending = true;
  const current = window.__MASSFRONT_SPACE__;
  window.__MASSFRONT_SPACE__ = null;
  try { current?.dispose?.(); } catch (error) { console.warn('[MASSFRONT GPU] renderer disposal during recovery failed', error); }
  gpuRebuilds++;
  window.setTimeout(() => {
    gpuRebuildPending = false;
    document.querySelector('[data-render-retry]')?.removeAttribute('aria-busy');
    if (gpuRebuilds <= 2) boot();
    else {
      const frame = document.getElementById('moduleFrame');
      const veil = frame?.querySelector('#renderVeil');
      if (veil) {
        veil.className = 'render-veil failed';
        const title = veil.querySelector('b');
        const status = veil.querySelector('#loadStatus');
        if (title) title.textContent = 'GPU PROCESS REMAINS UNSTABLE';
        if (status) status.textContent = 'TWO CLEAN WEBGL2 RENDERERS WERE INTERRUPTED · CLOSE OTHER GPU-HEAVY WINDOWS, THEN RETRY';
        exposeFatalActions(veil);
      }
    }
  }, 500);
}

window.addEventListener('massfront:space-gpu-stalled', rebuildAfterGpuInterruption);
installFatalReturn();
installFatalRetry();
window.__MASSFRONT_SPACE_MODULE_BOUND__ = true;

if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true });
else boot();