{"record":{"id":"d85b7d415d533f3e","repo":"moeru-ai/airi","slug":"spine-zip-must-contain-a-atlas-or-atlas-txt-fi","errorCode":null,"errorMessage":"Spine ZIP must contain a .atlas (or .atlas.txt) file","messagePattern":"Spine ZIP must contain a \\.atlas \\(or \\.atlas\\.txt\\) file","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui-spine/src/utils/spine-zip-loader.ts","lineNumber":119,"sourceCode":" * 3. Return the first matched pair as the primary layout.\n */\nexport function detectSpineLayout(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelLayout {\n  const variants = detectAllSpineLayouts(entries, atlasText)\n  if (variants.length === 0)\n    throw new Error('Spine ZIP must contain a .skel or .json skeleton file paired with a .atlas')\n  return variants[0].layout\n}\n\n/**\n * Detects all skeleton+atlas pairs in a ZIP, returning them as named\n * variants. Useful for ZIPs containing multiple outfits/characters in\n * separate folders.\n */\nexport function detectAllSpineLayouts(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelVariant[] {\n  const allFiles = Object.keys(entries)\n  const atlasCandidates = allFiles.filter(isAtlasPath)\n  if (atlasCandidates.length === 0)\n    throw new Error('Spine ZIP must contain a .atlas (or .atlas.txt) file')\n\n  const variants: SpineModelVariant[] = []\n  const usedAtlases = new Set<string>()\n\n  // First pass: match each atlas with a same-basename skeleton.\n  for (const candidate of atlasCandidates) {\n    const baseName = stripExt(stripExt(basename(candidate)))\n    const dir = dirname(candidate)\n\n    const binaryPath = `${dir}${baseName}${SKELETON_BINARY_EXT}`\n    const jsonPath = `${dir}${baseName}${SKELETON_JSON_EXT}`\n\n    let skeletonPath: string | undefined\n    let skeletonFormat: SpineModelLayout['skeletonFormat'] = 'binary'\n\n    if (entries[binaryPath] !== undefined) {\n      skeletonPath = binaryPath\n      skeletonFormat = 'binary'","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-ui-spine/src/utils/spine-zip-loader.ts#L101-L137","documentation":"`detectAllSpineLayouts(entries, atlasText)` first filters `entries` for atlas candidates via `isAtlasPath`. If there are zero `.atlas` / `.atlas.txt` files it throws `Spine ZIP must contain a .atlas (or .atlas.txt) file` before attempting skeleton matching. An atlas file is a hard prerequisite for Spine loading.","triggerScenarios":"An archive with a `.skel`/`.json` skeleton and textures but no `.atlas`; the atlas file renamed to `.txt` without the `.atlas.txt` dual extension; only a `.json` atlas reference that is not a real atlas; an archive produced without the texture-pack atlas export.","commonSituations":"Spine export configured to skip the atlas; the atlas file lost in transfer; a non-Spine archive (Live2D/MMD) loaded through the Spine path; case mismatch where `isAtlasPath` did not recognize the extension.","solutions":["Ensure the archive contains a `.atlas` (or `.atlas.txt`) file alongside the skeleton.","Re-export from Spine enabling 'Create atlas' in the texture packer settings.","If the atlas was renamed, restore the `.atlas` / `.atlas.txt` extension.","Confirm the archive is actually a Spine export before routing here."],"exampleFix":"// before\nconst variants = detectAllSpineLayouts(entries, atlasText)  // throws, no .atlas\n\n// after\nconst atlasFiles = Object.keys(entries).filter(isAtlasPath)\nif (!atlasFiles.length) throw new Error('Archive is missing a .atlas file; re-export with texture packer')\nconst variants = detectAllSpineLayouts(entries, atlasText)","handlingStrategy":"validation","validationCode":"const atlasFiles = Object.keys(entries).filter(isAtlasPath)\nif (!atlasFiles.length)\n  throw new Error('Archive is missing a .atlas file')\ndetectAllSpineLayouts(entries, atlasText)","typeGuard":"function zipHasAtlas(entries: Record<string, string>): boolean {\n  return Object.keys(entries).some(isAtlasPath)\n}","tryCatchPattern":"try {\n  detectAllSpineLayouts(entries, atlasText)\n} catch (e) {\n  if (e instanceof Error && e.message.includes('.atlas (or .atlas.txt)')) {\n    // tell user the atlas is missing; re-export\n  } else throw e\n}","preventionTips":["Ensure Spine export produces a .atlas / .atlas.txt alongside the skeleton.","Confirm the archive is a Spine export before routing here.","Watch for renamed atlas files."],"tags":["spine","model-loading","archive","input-validation"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}