moeru-ai/airi · error · Error

Spine ZIP must contain a .atlas (or .atlas.txt) file

Error message

Spine ZIP must contain a .atlas (or .atlas.txt) file

What it means

`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.

Source

Thrown at packages/stage-ui-spine/src/utils/spine-zip-loader.ts:119

 * 3. Return the first matched pair as the primary layout.
 */
export function detectSpineLayout(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelLayout {
  const variants = detectAllSpineLayouts(entries, atlasText)
  if (variants.length === 0)
    throw new Error('Spine ZIP must contain a .skel or .json skeleton file paired with a .atlas')
  return variants[0].layout
}

/**
 * Detects all skeleton+atlas pairs in a ZIP, returning them as named
 * variants. Useful for ZIPs containing multiple outfits/characters in
 * separate folders.
 */
export function detectAllSpineLayouts(entries: Record<string, string>, atlasText: Record<string, string>): SpineModelVariant[] {
  const allFiles = Object.keys(entries)
  const atlasCandidates = allFiles.filter(isAtlasPath)
  if (atlasCandidates.length === 0)
    throw new Error('Spine ZIP must contain a .atlas (or .atlas.txt) file')

  const variants: SpineModelVariant[] = []
  const usedAtlases = new Set<string>()

  // First pass: match each atlas with a same-basename skeleton.
  for (const candidate of atlasCandidates) {
    const baseName = stripExt(stripExt(basename(candidate)))
    const dir = dirname(candidate)

    const binaryPath = `${dir}${baseName}${SKELETON_BINARY_EXT}`
    const jsonPath = `${dir}${baseName}${SKELETON_JSON_EXT}`

    let skeletonPath: string | undefined
    let skeletonFormat: SpineModelLayout['skeletonFormat'] = 'binary'

    if (entries[binaryPath] !== undefined) {
      skeletonPath = binaryPath
      skeletonFormat = 'binary'

View on GitHub (pinned to 27111382b4)

Solutions

  1. Ensure the archive contains a `.atlas` (or `.atlas.txt`) file alongside the skeleton.
  2. Re-export from Spine enabling 'Create atlas' in the texture packer settings.
  3. If the atlas was renamed, restore the `.atlas` / `.atlas.txt` extension.
  4. Confirm the archive is actually a Spine export before routing here.

Example fix

// before
const variants = detectAllSpineLayouts(entries, atlasText)  // throws, no .atlas

// after
const atlasFiles = Object.keys(entries).filter(isAtlasPath)
if (!atlasFiles.length) throw new Error('Archive is missing a .atlas file; re-export with texture packer')
const variants = detectAllSpineLayouts(entries, atlasText)
Defensive patterns

Strategy: validation

Validate before calling

const atlasFiles = Object.keys(entries).filter(isAtlasPath)
if (!atlasFiles.length)
  throw new Error('Archive is missing a .atlas file')
detectAllSpineLayouts(entries, atlasText)

Type guard

function zipHasAtlas(entries: Record<string, string>): boolean {
  return Object.keys(entries).some(isAtlasPath)
}

Try / catch

try {
  detectAllSpineLayouts(entries, atlasText)
} catch (e) {
  if (e instanceof Error && e.message.includes('.atlas (or .atlas.txt)')) {
    // tell user the atlas is missing; re-export
  } else throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/d85b7d415d533f3e. Report an issue: GitHub.