import { generateMap, generateMapDraft, generateMapTerrainDraft } from "./mapPipeline.js"; import { collectTransferableBuffers } from "./transferUtils.js"; import { compactRawPatchCandidate, summarizeRawPatchCandidate } from "./rawPatchCandidate.js"; function prepareDraftForTransfer(candidate) { if (!candidate || typeof candidate !== "object") return candidate; // generateMapFeatures exposes cityPopulationCap as a closure over production // terrain fields. Functions are not structured-cloneable, so materialize the // exact per-city caps before transferring a resident draft to the coordinator. // finishMapOutput consumes and removes this temporary numeric field when the // winning draft is finalized; no approximation is introduced. const features = candidate.features; const capFn = features?.cityPopulationCap; if (typeof capFn === "function") { for (const city of features?.modernCities || []) { const cap = Number(capFn(city)); if (Number.isFinite(cap)) city.__productionPopulationCap = cap; } delete features.cityPopulationCap; } if (typeof candidate.cityPopulationCap === "function") delete candidate.cityPopulationCap; return candidate; } function scopeTaskProgress(event = {}, taskId) { const scoped = { ...event }; if (event?.workUnitId != null && String(event.workUnitId).length > 0) { scoped.workUnitId = `${taskId}/${String(event.workUnitId)}`; } return scoped; } if (typeof self !== "undefined") { self.onmessage = (event) => { const message = event.data || {}; const terrainScoutOnly = message.type === "generate-raw-patch-terrain-scout"; const draftOnly = message.type === "generate-raw-patch-draft"; if (!terrainScoutOnly && !draftOnly && message.type !== "generate-raw-patch-candidate") return; const taskId = String(message.taskId || `${terrainScoutOnly ? "terrain-scout" : draftOnly ? "draft" : "raw"}-${message.id || 0}`); const startedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now(); try { const generator = terrainScoutOnly ? generateMapTerrainDraft : draftOnly ? generateMapDraft : generateMap; let candidate = generator(Number(message.seed) >>> 0, { ...(message.mapOptions || {}), // A boolean sentinel is sufficient: mapPipeline only records whether a // boundary world exists. Actual seam/quality work remains in the parent // patch worker after this raw candidate/draft has been transferred back. boundaryWorld: true, onProgress: (progress) => { self.postMessage({ type: terrainScoutOnly ? "raw-patch-terrain-scout-progress" : draftOnly ? "raw-patch-draft-progress" : "raw-patch-candidate-progress", id: message.id, taskId, progress: scopeTaskProgress(progress, taskId), }); }, }); const elapsedMs = (typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - startedAt; let transferSummary; if (terrainScoutOnly) { const transferables = [...collectTransferableBuffers(candidate)]; transferSummary = { transferableBytes: transferables.reduce((sum, buffer) => sum + Number(buffer?.byteLength || 0), 0), transferableCount: transferables.length, }; self.postMessage({ type: "raw-patch-terrain-scout-result", id: message.id, taskId, ok: true, elapsedMs, transferSummary, candidate, }, transferables); return; } if (draftOnly) { candidate = prepareDraftForTransfer(candidate); // Drafts stay complete because the finalist reuses terrain, geographic // basis and features verbatim. Transferable buffers move ownership to // the coordinator without copying while the helper Worker remains alive // for the next lane task. const transferables = [...collectTransferableBuffers(candidate)]; transferSummary = { transferableBytes: transferables.reduce((sum, buffer) => sum + Number(buffer?.byteLength || 0), 0), transferableCount: transferables.length, }; self.postMessage({ type: "raw-patch-draft-result", id: message.id, taskId, ok: true, elapsedMs, transferSummary, candidate, }, transferables); return; } // Transfer only the roots consumed by mapPatch. Full-map geography/debug // graphs can exceed the actual patch payload and are never consulted by // candidate merge/quality logic. Dropping them before structured clone // creates a hard cross-worker memory bound for large tiled operations. candidate = compactRawPatchCandidate(candidate); transferSummary = summarizeRawPatchCandidate(candidate); const transferables = [...collectTransferableBuffers(candidate)]; self.postMessage({ type: "raw-patch-candidate-result", id: message.id, taskId, ok: true, elapsedMs, transferSummary, candidate, }, transferables); } catch (error) { self.postMessage({ type: terrainScoutOnly ? "raw-patch-terrain-scout-result" : draftOnly ? "raw-patch-draft-result" : "raw-patch-candidate-result", id: message.id, taskId, ok: false, code: error?.code || (terrainScoutOnly ? "raw-patch-terrain-scout-error" : draftOnly ? "raw-patch-draft-error" : "raw-patch-candidate-error"), error: error?.message || String(error), stack: error?.stack || "", }); } }; }