moeru-ai/airi · error · Error

Expected exactly one moc file, got

Error message

Expected exactly one moc file, got ${mocFiles.length} ${fileList}

What it means

Thrown by createFakeSettings in the Live2D ZIP loader when the uploaded file list does not contain exactly one .moc (Cubism 2) or .moc3 (Cubism 4) file. This fallback path runs when no .model3.json/.model.json settings file is present, so the loader must synthesize settings from the moc; zero mocs or multiple mocs make the target model ambiguous.

Solutions

  1. Best fix: include the proper .model3.json settings file in the archive — then createModelSettings is used and multiple mocs are resolved by FileReferences instead of this heuristic
  2. If the pack contains several models, split it into one archive per model (one moc each)
  3. Delete unused variant .moc3/.moc files from the archive so exactly one remains
  4. When accepting user uploads, pre-scan entries with isMocFile and tell the user the count before attempting the load

Example fix

// before
const settings = FileLoader.createSettings(files) // throws 'Expected exactly one moc file, got 2 (...)'

// after
// pre-check before loading
const mocFiles = files.filter(f => /\.moc3?$/.test(f.name))
if (mocFiles.length !== 1) {
  throw new Error(`Upload must contain exactly one .moc/.moc3 (found ${mocFiles.length}). Include the model's .model3.json or split multi-model packs.`)
}
Defensive patterns

Strategy: validation

Validate before calling

const mocCount = files.filter(f => /\.moc3?$/i.test(f.name)).length
if (mocCount !== 1) {
  throw new Error(`Expected exactly one .moc/.moc3, found ${mocCount}. Include a .model3.json or split the pack.`)
}

Try / catch

catch (e) { if (e.message.includes('exactly one moc file')) { guideUserToIncludeSettingsFile(); return } throw e }

Prevention

When it happens

Trigger: Uploading a ZIP with two .moc3 files (base model plus a clothing variant or a second character); uploading a folder where the moc was left out; nested archives (a .zip inside the .zip) so the outer pass sees no moc entries; case-sensitive mismatch aside, isMocFile only matches .moc/.moc3 endings, so renamed files count as missing.

Common situations: Multi-model packs, 'all outfits' bundles, or scenes with background characters; contributors zipping the parent folder that contains several models; models distributed with extra moc3 files for physics toggle variants.

Related errors


AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-08-18). Data as JSON: /api/errors/b7539c3470cfe575. Report an issue: GitHub.

Appendix: source

Thrown at packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts:167

}

export function isMocFile(file: string) {
  return file.endsWith('.moc3')
}

export function basename(path: string): string {
  // https://stackoverflow.com/a/15270931
  return path.split(/[\\/]/).pop()!
}

// copy and modified from https://github.com/guansss/live2d-viewer-web/blob/f6060b2ce52c2e26b6b61fa903c837fe343f72d1/src/app/upload.ts#L81-L142
function createFakeSettings(files: string[]): ModelSettings {
  const mocFiles = files.filter(file => isMocFile(file))

  if (mocFiles.length !== 1) {
    const fileList = mocFiles.length ? `(${mocFiles.map(f => `"${f}"`).join(',')})` : ''

    throw new Error(`Expected exactly one moc file, got ${mocFiles.length} ${fileList}`)
  }

  const mocFile = mocFiles[0]
  const modelName = basename(mocFile).replace(/\.moc3?/, '')

  const textures = files.filter(f => f.endsWith('.png'))

  if (!textures.length) {
    throw new Error('Textures not found')
  }

  const motions = files.filter(f => f.endsWith('.mtn') || f.endsWith('.motion3.json'))
  const physics = files.find(f => f.includes('physics'))
  const pose = files.find(f => f.includes('pose'))

  const settings = new Cubism4ModelSettings({
    url: `${modelName}.model3.json`,
    Version: 3,

View on GitHub (pinned to 9c213115f8)