improvements
This commit is contained in:
+54
-25
@@ -4978,9 +4978,10 @@
|
||||
[id, `${id}/${id.split('/').pop()}`], '.animation.json') };
|
||||
}
|
||||
|
||||
function importDialog(title, form, confirmText = 'Continue') {
|
||||
function importDialog(title, form, confirmText = 'Continue', onFormChange = null) {
|
||||
return new Promise((resolve) => new Dialog({ id: 'animationdirector_automatic_import_step', title, width: 680, form,
|
||||
buttons: ['Cancel', confirmText], cancelIndex: 0, confirmIndex: 1,
|
||||
...(onFormChange ? { onFormChange } : {}),
|
||||
onConfirm(result) { resolve(result); }, onCancel() { resolve(null); } }).show());
|
||||
}
|
||||
|
||||
@@ -5036,15 +5037,14 @@
|
||||
|
||||
function loadImportTextures(row, packs, io) {
|
||||
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();
|
||||
row.defaultTexture = undefined;
|
||||
let cachedSources;
|
||||
let modelPack;
|
||||
try { modelPack = findImportPack(row.resolved.file, io); } catch (_) { /* Standalone model. */ }
|
||||
const searchPacks = modelPack && !packs.some((pack) => pack.root === modelPack.root)
|
||||
? [modelPack, ...packs] : packs;
|
||||
for (const resource of ids) {
|
||||
const resolveTexture = (resource) => {
|
||||
const id = resource.includes(':') ? resource : `minecraft:${resource}`;
|
||||
const texture = findImportResource(searchPacks, id, '', '', io);
|
||||
if (texture) {
|
||||
@@ -5057,8 +5057,7 @@
|
||||
dimensions = importTextureDimensions(bytes, metadata);
|
||||
}
|
||||
} catch (_) { /* Let Blockbench report unreadable texture files. */ }
|
||||
row.textures.set(resource, { ...texture, dimensions });
|
||||
continue;
|
||||
return { ...texture, dimensions };
|
||||
}
|
||||
if (!cachedSources) {
|
||||
cachedSources = [];
|
||||
@@ -5067,7 +5066,7 @@
|
||||
}
|
||||
}
|
||||
const parsed = importResourceId(id);
|
||||
if (!parsed) continue;
|
||||
if (!parsed) return null;
|
||||
const relative = `assets/${parsed.namespace}/${parsed.resource}`;
|
||||
for (const source of cachedSources) {
|
||||
let bytes;
|
||||
@@ -5077,11 +5076,22 @@
|
||||
if (!dimensions) continue;
|
||||
try { dimensions = importTextureDimensions(bytes, propJson(source, `${relative}.mcmeta`)); }
|
||||
catch (_) { /* Keep the PNG dimensions when metadata is invalid. */ }
|
||||
row.textures.set(resource, { bytes, relative, dimensions });
|
||||
return { bytes, relative, dimensions };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
for (const resource of candidates) {
|
||||
const texture = resolveTexture(resource);
|
||||
if (!texture) continue;
|
||||
row.textures.set(resource, texture);
|
||||
row.defaultTexture = resource;
|
||||
break;
|
||||
}
|
||||
for (const resource of Object.values(row.json.afw_bone_textures || {})) {
|
||||
if (typeof resource !== 'string' || row.textures.has(resource)) continue;
|
||||
const texture = resolveTexture(resource);
|
||||
if (texture) row.textures.set(resource, texture);
|
||||
}
|
||||
row.defaultTexture = candidates.find((id) => row.textures.has(id));
|
||||
}
|
||||
|
||||
function commitAutomaticImport(plan) {
|
||||
@@ -5187,7 +5197,10 @@
|
||||
const available = parsed.actorLabels.find((candidate) => !usedClips.has(candidate));
|
||||
if (available) { clipRoles[index] = available; usedClips.add(available); }
|
||||
});
|
||||
const form = { info: { type: 'info', text: `### ${companions.id}\nImport into a NEW GeckoLib project. Choose concrete actors and confirm their clip roles. Use Manual Import > Pack Folders to register shared/default packs before importing.\nFolder priority: ${registry.packs.map((pack) => pack.name).join(' > ')}` } };
|
||||
const form = {
|
||||
info: { type: 'info', text: `### ${companions.id}\nImport into a NEW GeckoLib project. Use Manual Import > Pack Folders to register shared/default packs before importing.\nFolder priority: ${registry.packs.map((pack) => pack.name).join(' > ')}` },
|
||||
actors_section: { type: 'info', text: '### Actors\nChoose concrete actors and confirm their animation roles.' },
|
||||
};
|
||||
definition.actors.forEach((actor, index) => {
|
||||
const entities = (actor.entity_types || []).filter((id) => importResourceId(id));
|
||||
form[`heading_${index}`] = { type: 'info', text: `### Actor ${index + 1}: ${keys[index]}` };
|
||||
@@ -5205,7 +5218,28 @@
|
||||
if (!entities.length || entities.includes('minecraft:player')) form[`slim_${index}`] = { type: 'checkbox', label: 'Slim player model', value: false,
|
||||
condition: (result) => result[`entity_${index}`] === 'minecraft:player' };
|
||||
});
|
||||
const choices = await importDialog('Automatic Import: Actors', form);
|
||||
form.models_section = { type: 'info', text: '### Actor Models\nModels are selected automatically from the actor choices above, following local, imported, old default revision, and shared priority. Select a file only to override that lookup or supply a missing model.' };
|
||||
const modelStatus = (index, values) => {
|
||||
const entity = values[`entity_${index}`], gender = values[`gender_${index}`];
|
||||
if (!importResourceId(entity) || !['m', 'f', 'mf'].includes(gender)) return 'Choose an entity and model gender above.';
|
||||
try {
|
||||
const resolved = resolveImportModel(source, registry.packs, entity, gender,
|
||||
definition.actors[index].entity_variant, io, !!values[`slim_${index}`]);
|
||||
return resolved ? `${resolved.kind}: ${resolved.file}` : 'No automatic model found. Select a model file below.';
|
||||
} catch (error) { return `Model lookup unavailable: ${error.message}`; }
|
||||
};
|
||||
const defaults = Object.fromEntries(Object.entries(form).filter(([, field]) => 'value' in field)
|
||||
.map(([key, field]) => [key, field.value]));
|
||||
definition.actors.forEach((_, index) => {
|
||||
form[`source_${index}`] = { type: 'text', label: `Actor ${index + 1} automatic model`,
|
||||
value: modelStatus(index, defaults), readonly: true, full_width: true };
|
||||
form[`model_${index}`] = { type: 'file', label: `Actor ${index + 1} model override`,
|
||||
return_as: 'file', extensions: ['json'], readtype: 'text', filetype: 'GeckoLib geometry' };
|
||||
});
|
||||
const choices = await importDialog('Automatic Import: Actors & Models', form, 'Create Project and Import', function(result) {
|
||||
this.form.setValues(Object.fromEntries(definition.actors.map((_, index) =>
|
||||
[`source_${index}`, modelStatus(index, result)])), false);
|
||||
});
|
||||
if (!choices) return;
|
||||
const rows = definition.actors.map((actor, index) => ({ actor, label: choices[`clip_${index}`], entity: choices[`entity_${index}`], gender: choices[`gender_${index}`], slim: !!choices[`slim_${index}`] }));
|
||||
if (new Set(rows.map((row) => row.label)).size !== rows.length || rows.some((row) => !parsed.actorLabels.includes(row.label))) {
|
||||
@@ -5244,18 +5278,11 @@
|
||||
const prepared = prepareAnimationDefinitionImport(relabeled, analysis);
|
||||
const warnings = registry.warnings.slice();
|
||||
if (ignoredLabels.length) warnings.push(`Ignored animation actors not mapped to the definition: ${ignoredLabels.join(', ')}.`);
|
||||
const models = { info: { type: 'info', text: '### Actor models\nModels follow local, declared imports, old default revisions, and shared fallbacks. Choose a replacement only when necessary; missing models must be selected manually.' } };
|
||||
rows.forEach((row, index) => {
|
||||
row.resolved = resolveImportModel(source, registry.packs, row.entity, row.gender, row.actor.entity_variant, io, row.slim);
|
||||
models[`source_${index}`] = { type: 'info', text: `### ${row.label}\n${row.resolved ? `${row.resolved.kind}: ${row.resolved.file}` : 'Model not found. Select a geometry file below.'}` };
|
||||
models[`model_${index}`] = { type: 'file', label: row.resolved ? 'Override model (optional)' : 'Select model (required)',
|
||||
return_as: 'file', extensions: ['json'], readtype: 'text', filetype: 'GeckoLib geometry' };
|
||||
});
|
||||
const modelChoices = await importDialog('Automatic Import: Models', models);
|
||||
if (!modelChoices) return;
|
||||
for (const [index, row] of rows.entries()) {
|
||||
if (modelChoices[`model_${index}`]?.path) row.resolved = { file: modelChoices[`model_${index}`].path, kind: 'Manually selected' };
|
||||
if (!row.resolved) throw new Error(`Select a model for ${row.label}.`);
|
||||
const override = choices[`model_${index}`];
|
||||
row.resolved = override?.path ? { file: override.path, kind: 'Manually selected' }
|
||||
: resolveImportModel(source, registry.packs, row.entity, row.gender, row.actor.entity_variant, io, row.slim);
|
||||
if (!row.resolved) throw new Error(`No model found for ${row.label}. Select a model override for actor ${index + 1} and try again.`);
|
||||
row.json = readImportJson(row.resolved.file, io);
|
||||
row.geo = validateImportGeometry(row.json);
|
||||
loadImportTextures(row, registry.packs, io);
|
||||
@@ -5274,11 +5301,13 @@
|
||||
}
|
||||
const props = importPropPreviews(parsed.animationId, prepared, registry.packs.map((pack) => pack.root));
|
||||
warnings.push(...props.warnings);
|
||||
const review = { info: { type: 'info', text: `### Import ${parsed.animationId}\n${rows.map((row) => `${row.label}: ${row.resolved.kind}\n${row.resolved.file}`).join('\n\n')}\n\n${warnings.join('\n')}` } };
|
||||
const accepted = await importDialog('Automatic Import: Review', review, 'Create Project and Import');
|
||||
if (!accepted) return;
|
||||
commitAutomaticImport({ parsed, rows, definition: prepared, imported, blockEntry, propEntries: props.entries });
|
||||
localStorage.setItem(PACK_FOLDERS_KEY, JSON.stringify(registry.packs.map((pack) => pack.root)));
|
||||
if (warnings.length) {
|
||||
console.warn(`[AnimationDirector] Import notes for ${parsed.animationId}:\n${warnings.join('\n')}`);
|
||||
const more = warnings.length > 1 ? ` (+${warnings.length - 1} more in Blockbench console)` : '';
|
||||
Blockbench.showQuickMessage(`Imported with notes: ${warnings[0]}${more}`, 6000);
|
||||
}
|
||||
} catch (error) {
|
||||
Blockbench.showMessageBox({ title: 'Automatically Import Animation', message: error.message, buttons: ['OK'] });
|
||||
}
|
||||
|
||||
@@ -258,11 +258,22 @@ test('complete import prepares all stages, actor models, local textures and defi
|
||||
const plan = h.context.importedPlan;
|
||||
assert.equal(plan.rows[0].resolved.kind, 'Local');
|
||||
assert.equal(plan.rows[0].entity, 'minecraft:wolf');
|
||||
const actorForm = h.dialogs.find((dialog) => dialog.title.endsWith('Actors')).form;
|
||||
const importDialog = h.dialogs.find((dialog) => dialog.title.endsWith('Actors & Models'));
|
||||
const actorForm = importDialog.form;
|
||||
assert.equal(actorForm.entity_0.type, 'select');
|
||||
assert.equal(actorForm.entity_0.value, 'minecraft:wolf');
|
||||
assert.equal(actorForm.entity_0.options['minecraft:fox'], 'minecraft:fox');
|
||||
assert.equal(actorForm.gender_0.value, 'm');
|
||||
assert.equal(actorForm.models_section.type, 'info');
|
||||
assert.match(actorForm.source_0.value, /Local:.*wolf\.m\.geo\.json/);
|
||||
assert.equal(actorForm.model_0.type, 'file');
|
||||
assert.equal(h.dialogs.length, 1, 'Import needs only one configuration dialog');
|
||||
let updated;
|
||||
importDialog.onFormChange.call({ form: { setValues(values, rerender) {
|
||||
assert.equal(rerender, false);
|
||||
updated = values;
|
||||
} } }, { entity_0: 'minecraft:fox', gender_0: 'f', slim_0: false });
|
||||
assert.match(updated.source_0, /No automatic model found/);
|
||||
assert.equal(plan.rows[0].textures.size, 1);
|
||||
assert.deepEqual(Array.from(plan.imported.stageNames), ['example.p1', 'example.p2']);
|
||||
assert.ok(plan.imported.fileObj.animations['example.p2'].bones.actor1_body);
|
||||
@@ -306,6 +317,36 @@ 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('alternative vanilla texture paths do not add unused textures unless a bone references them', (t) => {
|
||||
const f = fixtures(t), h = harness(), source = f.makePack('source');
|
||||
const model = f.model(source, 'test:source/entity/silverfish.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(32, 20);
|
||||
for (const relative of ['silverfish.png', 'silverfish/silverfish.png']) {
|
||||
const file = path.join(cache, 'assets/minecraft/textures/entity', relative);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, png);
|
||||
}
|
||||
h.storage.set('animationdirector_prop_source', cache);
|
||||
const json = JSON.parse(fs.readFileSync(model, 'utf8'));
|
||||
const row = { json, geo: json['minecraft:geometry'][0], entity: 'minecraft:silverfish', slim: false,
|
||||
label: 'silverfish', resolved: { file: model } };
|
||||
h.loadImportTextures(row, [source], io);
|
||||
assert.equal(row.defaultTexture, 'minecraft:textures/entity/silverfish/silverfish.png');
|
||||
assert.deepEqual(Array.from(row.textures.keys()), [row.defaultTexture]);
|
||||
|
||||
const plan = { parsed: { animationId: 'example' }, rows: [row], definition: { actors: [{}] },
|
||||
imported: { stageNames: [], fileObj: { animations: {} } }, blockEntry: null };
|
||||
h.commitAutomaticImport(plan);
|
||||
assert.equal(h.context.Texture.all.length, 1);
|
||||
|
||||
row.json.afw_bone_textures = { body: 'minecraft:textures/entity/silverfish.png' };
|
||||
h.loadImportTextures(row, [source], io);
|
||||
assert.deepEqual(Array.from(row.textures.keys()), [row.defaultTexture, 'minecraft:textures/entity/silverfish.png']);
|
||||
});
|
||||
|
||||
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'), {
|
||||
@@ -329,7 +370,7 @@ test('complete import uses the matching Alex skin as a player-only texture fallb
|
||||
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')
|
||||
h.context.answer = (dialog, result) => dialog.title.endsWith('Actors & Models')
|
||||
? { ...result, slim_0: slim } : result;
|
||||
await h.automaticallyImportAnimation();
|
||||
assert.deepEqual(h.errors, []);
|
||||
@@ -352,7 +393,7 @@ test('definition actors exclude extra clips, their effects and stages containing
|
||||
} });
|
||||
f.model(source, 'test:source/entity/wolf.m');
|
||||
h.context.selectedFile = { path: file };
|
||||
h.context.answer = (dialog, result) => dialog.title.endsWith('Actors') ? { ...result, gender_0: 'm' } : result;
|
||||
h.context.answer = (dialog, result) => dialog.title.endsWith('Actors & Models') ? { ...result, gender_0: 'm' } : result;
|
||||
await h.automaticallyImportAnimation();
|
||||
assert.deepEqual(h.errors, []);
|
||||
const plan = h.context.importedPlan;
|
||||
@@ -364,7 +405,7 @@ test('definition actors exclude extra clips, their effects and stages containing
|
||||
assert.deepEqual(Object.keys(clip.bones), ['actor1_body']);
|
||||
assert.equal(clip.sound_effects, undefined);
|
||||
assert.equal(clip.particle_effects, undefined);
|
||||
assert.match(h.dialogs.at(-1).form.info.text, /Ignored animation actors.*unused/);
|
||||
assert.ok(h.messages.some((message) => /Ignored animation actors.*unused/.test(message)));
|
||||
});
|
||||
|
||||
test('a definition actor without a distinct clip still requires a valid mapping', async (t) => {
|
||||
@@ -442,13 +483,13 @@ test('missing models can be supplied manually, and their locally supplied textur
|
||||
});
|
||||
const model = f.model(source, 'test:unrelated/wolf.m', { afw_texture: 'test:textures/unavailable.png' });
|
||||
h.context.selectedFile = { path: file };
|
||||
h.context.answer = (dialog, result) => dialog.title.endsWith('Actors') ? { ...result, gender_0: 'm' }
|
||||
: dialog.title.endsWith('Models') ? { ...result, model_0: { path: model } } : result;
|
||||
h.context.answer = (dialog, result) => dialog.title.endsWith('Actors & Models')
|
||||
? { ...result, gender_0: 'm', model_0: { path: model } } : result;
|
||||
await h.automaticallyImportAnimation();
|
||||
assert.deepEqual(h.errors, []);
|
||||
assert.equal(h.context.importedPlan.rows[0].resolved.kind, 'Manually selected');
|
||||
assert.equal(h.context.importedPlan.rows[0].textures.size, 0);
|
||||
assert.match(h.dialogs.at(-1).form.info.text, /no base texture found locally/);
|
||||
assert.ok(h.messages.some((message) => /Imported with notes:.*no base texture/.test(message)));
|
||||
});
|
||||
|
||||
test('batch import uses a new project, one undo entry, actor prefixes and texture override bindings', () => {
|
||||
|
||||
Reference in New Issue
Block a user