{"record":{"id":"b7539c3470cfe575","repo":"moeru-ai/airi","slug":"expected-exactly-one-moc-file-got-mocfiles-leng","errorCode":null,"errorMessage":"Expected exactly one moc file, got ${mocFiles.length} ${fileList}","messagePattern":"Expected exactly one moc file, got (.+?) (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts","lineNumber":167,"sourceCode":"}\n\nexport function isMocFile(file: string) {\n  return file.endsWith('.moc3')\n}\n\nexport function basename(path: string): string {\n  // https://stackoverflow.com/a/15270931\n  return path.split(/[\\\\/]/).pop()!\n}\n\n// copy and modified from https://github.com/guansss/live2d-viewer-web/blob/f6060b2ce52c2e26b6b61fa903c837fe343f72d1/src/app/upload.ts#L81-L142\nfunction createFakeSettings(files: string[]): ModelSettings {\n  const mocFiles = files.filter(file => isMocFile(file))\n\n  if (mocFiles.length !== 1) {\n    const fileList = mocFiles.length ? `(${mocFiles.map(f => `\"${f}\"`).join(',')})` : ''\n\n    throw new Error(`Expected exactly one moc file, got ${mocFiles.length} ${fileList}`)\n  }\n\n  const mocFile = mocFiles[0]\n  const modelName = basename(mocFile).replace(/\\.moc3?/, '')\n\n  const textures = files.filter(f => f.endsWith('.png'))\n\n  if (!textures.length) {\n    throw new Error('Textures not found')\n  }\n\n  const motions = files.filter(f => f.endsWith('.mtn') || f.endsWith('.motion3.json'))\n  const physics = files.find(f => f.includes('physics'))\n  const pose = files.find(f => f.includes('pose'))\n\n  const settings = new Cubism4ModelSettings({\n    url: `${modelName}.model3.json`,\n    Version: 3,","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/moeru-ai/airi/blob/9c213115f8bd0fff9e6eabab02b077ac32da21be/packages/stage-ui-live2d/src/utils/live2d-zip-loader.ts#L149-L185","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","If the pack contains several models, split it into one archive per model (one moc each)","Delete unused variant .moc3/.moc files from the archive so exactly one remains","When accepting user uploads, pre-scan entries with isMocFile and tell the user the count before attempting the load"],"exampleFix":"// before\nconst settings = FileLoader.createSettings(files) // throws 'Expected exactly one moc file, got 2 (...)'\n\n// after\n// pre-check before loading\nconst mocFiles = files.filter(f => /\\.moc3?$/.test(f.name))\nif (mocFiles.length !== 1) {\n  throw new Error(`Upload must contain exactly one .moc/.moc3 (found ${mocFiles.length}). Include the model's .model3.json or split multi-model packs.`)\n}","handlingStrategy":"validation","validationCode":"const mocCount = files.filter(f => /\\.moc3?$/i.test(f.name)).length\nif (mocCount !== 1) {\n  throw new Error(`Expected exactly one .moc/.moc3, found ${mocCount}. Include a .model3.json or split the pack.`)\n}","typeGuard":null,"tryCatchPattern":"catch (e) { if (e.message.includes('exactly one moc file')) { guideUserToIncludeSettingsFile(); return } throw e }","preventionTips":["Always ship the .model3.json with the archive so fake settings are never needed","Split multi-model packs into one archive per model","Pre-scan uploads and show the moc count before loading"],"tags":["live2d","zip","moc-file","multi-model-archive"],"backgroundTag":"live2d-model-load-failed","analyzedSha":"9c213115f8bd0fff9e6eabab02b077ac32da21be","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}