diff --git a/AnimationDirector.js b/AnimationDirector.js index 2cfe94c..38145e8 100644 --- a/AnimationDirector.js +++ b/AnimationDirector.js @@ -1304,7 +1304,7 @@ return width > 0 && height > 0 && width <= 8192 && height <= 8192 ? { width, height } : null; } - function actorTextureCandidates(json, geo, modelPath) { + function actorTextureCandidates(json, geo, modelPath, entityId = '', slimPlayer = false) { const candidateIds = []; const addId = (id) => { if (importResourceId(id) && !candidateIds.includes(id)) candidateIds.push(id); }; if (typeof json.afw_texture === 'string') addId(json.afw_texture); @@ -1321,6 +1321,9 @@ addId(`minecraft:textures/entity/${entity}.png`); } } + if (entityId === 'minecraft:player' || filename === 'player' || filename === 'player_slim') { + addId(`minecraft:textures/entity/player/${slimPlayer || filename === 'player_slim' ? 'slim' : 'wide'}/alex.png`); + } return candidateIds; } @@ -5032,7 +5035,7 @@ } function loadImportTextures(row, packs, io) { - const candidates = actorTextureCandidates(row.json, row.geo, row.resolved.file); + const candidates = actorTextureCandidates(row.json, row.geo, row.resolved.file, row.entity, row.slim); const ids = new Set([...candidates, ...Object.values(row.json.afw_bone_textures || {}) .filter((value) => typeof value === 'string')]); row.textures = new Map(); diff --git a/test/automatic-import.test.js b/test/automatic-import.test.js index 67cae9f..376063b 100644 --- a/test/automatic-import.test.js +++ b/test/automatic-import.test.js @@ -306,6 +306,39 @@ test('complete import finds model texture in cached assets and uses its PNG dime assert.equal(JSON.parse(importing.context.Project.multiactor_actor_textures)['1'], row.defaultTexture); }); +test('complete import uses the matching Alex skin as a player-only texture fallback', async (t) => { + const f = fixtures(t), source = f.makePack('source'); + const defFile = f.write(path.join(source.root, 'data/test/afw_animdefs/example.json'), { + actors: [{ label: 'player', entity_types: ['minecraft:player'] }], stages: [{ stage: 1 }], + }); + f.write(path.join(source.root, 'assets/test/animations/afw/example.animation.json'), { + animations: { p1_player: { animation_length: 1, bones: { body: { rotation: [0, 0, 0] } } } }, + }); + f.model(source, 'test:source/entity/player.m'); + f.model(source, 'test:source/entity/player_slim.m'); + const cache = path.join(f.root, 'cached-assets'); + const png = Buffer.alloc(24); + Buffer.from('89504e470d0a1a0a', 'hex').copy(png); + png.writeUInt32BE(64, 16); png.writeUInt32BE(64, 20); + for (const shape of ['wide', 'slim']) { + const image = path.join(cache, `assets/minecraft/textures/entity/player/${shape}/alex.png`); + fs.mkdirSync(path.dirname(image), { recursive: true }); + fs.writeFileSync(image, png); + } + for (const slim of [false, true]) { + const h = harness(true); + h.storage.set('animationdirector_prop_source', cache); + h.context.selectedFile = { path: defFile }; + h.context.answer = (dialog, result) => dialog.title.endsWith('Actors') + ? { ...result, slim_0: slim } : result; + await h.automaticallyImportAnimation(); + assert.deepEqual(h.errors, []); + const row = h.context.importedPlan.rows[0]; + assert.equal(row.defaultTexture, `minecraft:textures/entity/player/${slim ? 'slim' : 'wide'}/alex.png`); + assert.equal(row.textures.get(row.defaultTexture).dimensions.width, 64); + } +}); + test('definition actors exclude extra clips, their effects and stages containing only unused actors', async (t) => { const f = fixtures(t), h = harness(true), source = f.makePack('source'); const file = f.write(path.join(source.root, 'data/test/afw_animdefs/example.json'), {