Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
632
extensions/plugin-import-2x/creator/index.ts
Normal file
632
extensions/plugin-import-2x/creator/index.ts
Normal file
@@ -0,0 +1,632 @@
|
||||
'use strict';
|
||||
// @ts-ignore
|
||||
import { shell } from 'electron';
|
||||
// @ts-ignore
|
||||
import { readFileSync, statSync, readdirSync, existsSync, writeFileSync, readJSONSync, writeJSONSync } from 'fs-extra';
|
||||
import { join, basename, extname, relative, dirname } from 'path';
|
||||
|
||||
import { registerConverter, getConverter, bubbleSort } from './convertor';
|
||||
import {
|
||||
clear,
|
||||
initGroupList,
|
||||
replaceScriptList,
|
||||
importProjectAssets,
|
||||
uuidList,
|
||||
import2DChunks,
|
||||
replaceFbxUuidMap,
|
||||
scanningDefaultAssets2D,
|
||||
importSubAssets,
|
||||
addImportProjectAssets,
|
||||
scriptList,
|
||||
SKIPS_SCRIPT,
|
||||
collator, compareVersion,
|
||||
} from './common/utlis';
|
||||
|
||||
import { MigrateManager } from "./components/MigrateManager";
|
||||
import { ImporterBase } from "./common/base";
|
||||
|
||||
exports.style = readFileSync(join(__dirname, '../creator/index.css'), 'utf8');
|
||||
|
||||
const { I18n, Dialog, Message } = Editor;
|
||||
|
||||
const MANUAL: string = 'https://github.com/cocos-creator/migrate-cocos-creator2.x-plugin';
|
||||
|
||||
const PackageJSON = readJSONSync(join(__dirname, '../package.json'))
|
||||
|
||||
function t(key: string, args?: any) {
|
||||
return args ? I18n.t(`${PackageJSON.name}.${key}`, args) : I18n.t(`${PackageJSON.name}.${key}`);
|
||||
}
|
||||
|
||||
exports.linteners = {
|
||||
resize() {
|
||||
this.$.tree.render(true);
|
||||
},
|
||||
};
|
||||
|
||||
exports.template = `
|
||||
<header>
|
||||
<h1>
|
||||
<ui-label value="i18n:${PackageJSON.name}.title"></ui-label>
|
||||
</h1>
|
||||
|
||||
<ui-progress></ui-progress>
|
||||
|
||||
<ui-file
|
||||
class="project"
|
||||
type="directory"
|
||||
placeholder="i18n:${PackageJSON.name}.select_dialog.title"
|
||||
></ui-file>
|
||||
</header>
|
||||
<section>
|
||||
<ui-tree></ui-tree>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<ui-label class="version"></ui-label>
|
||||
<ui-button class="import">
|
||||
<ui-label value="i18n:${PackageJSON.name}.btnImport"></ui-label>
|
||||
</ui-button>
|
||||
</footer>
|
||||
<iframe class="engine2D" style="display: none"></iframe>
|
||||
`;
|
||||
|
||||
exports.$ = {
|
||||
tree: 'ui-tree',
|
||||
project: '.project',
|
||||
import: '.import',
|
||||
progress: 'ui-progress',
|
||||
// serialize: '.serialize',
|
||||
search: '.search',
|
||||
engine2D: '.engine2D',
|
||||
version: '.version',
|
||||
};
|
||||
|
||||
exports.methods = {
|
||||
|
||||
updateTreeState() {
|
||||
this.$.tree.list.forEach((data: any) => {
|
||||
const detail = data.detail;
|
||||
detail.isNew = ImporterBase.isNew(this.projectRoot, detail.file)
|
||||
});
|
||||
this.$.tree.render(true);
|
||||
},
|
||||
|
||||
async updateTree(root: string) {
|
||||
const tree: any[] = [];
|
||||
function step(file: string, children: any) {
|
||||
try {
|
||||
if (file.endsWith('.DS_Store')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SKIPS_SCRIPT.includes(basename(file))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stat = statSync(file);
|
||||
const ext = extname(file);
|
||||
|
||||
const elements = file.split('assets');
|
||||
let url = '';
|
||||
if (elements) {
|
||||
url = 'db://assets' + elements[1].replace(ext, '');
|
||||
}
|
||||
const item: any = {
|
||||
detail: {
|
||||
value: basename(file),
|
||||
file: file,
|
||||
checked: true,
|
||||
extname: ext,
|
||||
isDirectory: stat.isDirectory(),
|
||||
legal: !!getConverter(ext) && !stat.isDirectory(),
|
||||
isNew: ImporterBase.isNew(root, file),
|
||||
path: url,
|
||||
},
|
||||
showArrow: false,
|
||||
children: [],
|
||||
};
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
item.showArrow = true;
|
||||
const names = readdirSync(file);
|
||||
|
||||
names.forEach((name: string) => {
|
||||
const tempPath = join(file, name);
|
||||
if (name.endsWith('.meta')) {
|
||||
addImportProjectAssets(root, tempPath);
|
||||
return;
|
||||
}
|
||||
step(tempPath, item.children);
|
||||
});
|
||||
|
||||
if (item.children.length > 0) {
|
||||
children.push(item);
|
||||
}
|
||||
} else {
|
||||
if (item.detail.legal) {
|
||||
children.push(item);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
const path = join(root, 'assets');
|
||||
step(path, tree);
|
||||
|
||||
const state = await Editor.Message.request('assets', 'unstaging');
|
||||
let sortType = 'type';
|
||||
if (state) {
|
||||
sortType = state.sortType;
|
||||
}
|
||||
this.sortTree(tree, sortType);
|
||||
|
||||
this.$.tree.tree = this.baseTree = tree;
|
||||
},
|
||||
|
||||
sortTree(tree: any[], sortType: string) {
|
||||
tree.sort((a: any, b: any) => {
|
||||
const detailA = a.detail, detailB = b.detail;
|
||||
if (detailA.isDirectory && !detailB.isDirectory) {
|
||||
return -1;
|
||||
} else if (!detailA.isDirectory && detailB.isDirectory) {
|
||||
return 1;
|
||||
} else {
|
||||
if (sortType === 'type' && detailA.extname !== detailB.extname) {
|
||||
return collator.compare(detailA.extname, detailB.extname);
|
||||
} else {
|
||||
return collator.compare(detailA.path, detailB.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
for (const item of tree) {
|
||||
this.sortTree(item.children, sortType);
|
||||
}
|
||||
},
|
||||
|
||||
async scanProject(root: string) {
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
const path = join(root, 'assets');
|
||||
const projectPath = join(root, 'project.json');
|
||||
if (!existsSync(path) || !existsSync(projectPath)) {
|
||||
Dialog.warn(t('warn_dialog.message'), {
|
||||
detail: t('warn_dialog.detail', {
|
||||
path: root,
|
||||
}),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const project = readJSONSync(projectPath);
|
||||
if (!project.version || compareVersion(project.version, '2.4.3') === -1) {
|
||||
Dialog.warn(t('warn_dialog.title'), {
|
||||
detail: t('warn_dialog.version', { version: project.version }),
|
||||
});
|
||||
}
|
||||
|
||||
this.projectRoot = root;
|
||||
this.$.import.removeAttribute('disabled');
|
||||
|
||||
// 导入时的初始化
|
||||
uuidList.clear();
|
||||
importProjectAssets.clear();
|
||||
importSubAssets.clear();
|
||||
initGroupList(path);
|
||||
scanningDefaultAssets2D();
|
||||
|
||||
await this.updateTree(this.projectRoot);
|
||||
this.updateList();
|
||||
return true;
|
||||
},
|
||||
|
||||
updateList() {
|
||||
// 更新 list
|
||||
this.totalTree = [];
|
||||
this.list = [];
|
||||
this.$.tree.list.forEach((data: any) => {
|
||||
if (data.detail.checked) {
|
||||
if (!data.detail.legal) {
|
||||
// 统计统一弹窗提示,打印太多 log 了
|
||||
// console.warn(`无法导入文件: ${data.detail.file}`);
|
||||
return;
|
||||
}
|
||||
this.list.push(data);
|
||||
}
|
||||
this.totalTree.push(data);
|
||||
});
|
||||
this.progressCurrentIdx = 0;
|
||||
this.$.progress.message = t('import_message_init', {
|
||||
current: this.progressCurrentIdx,
|
||||
total: this.list.length,
|
||||
});
|
||||
if (this.list.length === 0) {
|
||||
this.$.import.setAttribute('disabled', '');
|
||||
}
|
||||
else {
|
||||
this.$.import.removeAttribute('disabled');
|
||||
}
|
||||
},
|
||||
|
||||
getScripts() {
|
||||
const scripts: Map<string, any> = new Map<string, any>();
|
||||
const root = join(Editor.Project.path, 'assets');
|
||||
function step(file: string) {
|
||||
const files = readdirSync(file);
|
||||
files.forEach((item: any) => {
|
||||
const fPath = join(file, item);
|
||||
const stat = statSync(fPath);
|
||||
if(stat.isDirectory()) {
|
||||
step(fPath);
|
||||
}
|
||||
if (!stat.isDirectory() && fPath.endsWith('.ts')) {
|
||||
try {
|
||||
const content = readFileSync(fPath, 'utf8');
|
||||
const matchArray = content.match(/@ccclass\('(.*)'\)/);
|
||||
const className = matchArray && matchArray[1];
|
||||
if (className) {
|
||||
scripts.set(basename(fPath, extname(fPath)), {
|
||||
className: className,
|
||||
path: fPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e + ',\n this file path: ' + fPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
step(root);
|
||||
return scripts.size > 0 ? scripts : scriptList;
|
||||
},
|
||||
|
||||
replaceScript() {
|
||||
const scripts = this.getScripts();
|
||||
for (const obj of replaceScriptList) {
|
||||
// @ts-ignore
|
||||
const hasReplaceExtendClass = obj.extendClassName !== undefined;
|
||||
// @ts-ignore
|
||||
const key = hasReplaceExtendClass ? obj.extendClassName : obj.importPath;
|
||||
const name = key.replace(/[.|\\/]/g, '');
|
||||
const script = scripts.get(name);
|
||||
if (!script) {
|
||||
continue;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (script.className) {
|
||||
// @ts-ignore
|
||||
let code = readFileSync(obj.path, 'utf8');
|
||||
// @ts-ignore
|
||||
const dirOne = dirname(obj.path);
|
||||
const dirTwo = dirname(script.path);
|
||||
// @ts-ignore
|
||||
let relativePath = relative(dirname(obj.path), script.path);
|
||||
if (dirOne === dirTwo) {
|
||||
relativePath = './' + relativePath;
|
||||
}
|
||||
if (hasReplaceExtendClass) {
|
||||
// @ts-ignore
|
||||
code = code.replace(obj.extendClassName, relativePath.replace('.ts', ''));
|
||||
code = code.replace(/@@@@@@/g, script.className);
|
||||
}
|
||||
else {
|
||||
const newPath = `from '${relativePath.replace('.ts', '')}'`;
|
||||
// @ts-ignore
|
||||
code = code.replace(`from '${obj.importPath}'`, newPath);
|
||||
// @ts-ignore
|
||||
code = code.replace(`from "${obj.importPath}"`, newPath);
|
||||
}
|
||||
// @ts-ignore
|
||||
writeFileSync(obj.path, code, { encoding: 'utf8' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async replaceFbx() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let idx = 0;
|
||||
if (replaceFbxUuidMap.size === 0) {
|
||||
resolve(true);
|
||||
}
|
||||
replaceFbxUuidMap.forEach((data: any, path: string) => {
|
||||
let done = false;
|
||||
const meta = readJSONSync(path);
|
||||
if (meta) {
|
||||
for (const value of data) {
|
||||
const subMeta = meta.subMetas[value.name];
|
||||
if (subMeta) {
|
||||
subMeta.uuid = value.uuid;
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
writeJSONSync(path, meta, { spaces: 2 });
|
||||
}
|
||||
idx++;
|
||||
if (idx === replaceFbxUuidMap.size) {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
reject(e);
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async importProject() {
|
||||
let list = this.list;
|
||||
|
||||
// 根据勾选的情况进行剔除
|
||||
list = list.filter((item: any) => {
|
||||
return item.detail.checked;
|
||||
});
|
||||
|
||||
// 排序基础资源先导入
|
||||
bubbleSort(list);
|
||||
clear();
|
||||
|
||||
this.progressCurrentIdx = 0;
|
||||
this.progressCurrentName = '';
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const detail = list[i].detail;
|
||||
|
||||
this.progressCurrentIdx = i + 1;
|
||||
this.progressCurrentName = detail.value;
|
||||
this.$.progress.value = this.progressCurrentIdx / list.length * 100;
|
||||
this.$.progress.message = t('.import_message', {
|
||||
// @ts-ignore
|
||||
current: this.progressCurrentIdx,
|
||||
total: list.length,
|
||||
name: detail.value,
|
||||
});
|
||||
const converter = getConverter(detail.extname);
|
||||
if (converter) {
|
||||
if ((converter.type === 'mtl' || converter.type === 'effect')) {
|
||||
await import2DChunks();
|
||||
}
|
||||
let isDone = await converter.beforeImport(this.projectRoot, detail.file);
|
||||
// if (!converter.needImport()) {
|
||||
// continue;
|
||||
// }
|
||||
if (isDone) {
|
||||
isDone = await converter.import(this);
|
||||
}
|
||||
if (isDone) {
|
||||
await converter.afterImport();
|
||||
}
|
||||
// 无需每次都进行刷新,导入完后统一一次刷新
|
||||
// const next = list[i + 1];
|
||||
// if (next) {
|
||||
// const extname = next.detail.extname;
|
||||
// const nextConverter = getConverter(extname);
|
||||
// if (nextConverter) {
|
||||
// if (nextConverter.type !== converter.type) {
|
||||
// if (converter.type === 'script' || nextConverter.type === 'script') {
|
||||
// continue;
|
||||
// }
|
||||
// await Message.request('asset-db', 'refresh-asset', 'db://assets');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
this.replaceScript();
|
||||
console.log(t('import_refresh'));
|
||||
// await Message.request('asset-db', 'refresh-asset', 'db://assets');
|
||||
console.log(t('import_refreshend'));
|
||||
try {
|
||||
await this.replaceFbx();
|
||||
}
|
||||
catch (e) {}
|
||||
|
||||
MigrateManager.logs.length > 0 && console.log(t('no_support_type', {
|
||||
type: MigrateManager.logs.toString()
|
||||
}));
|
||||
|
||||
this.$.progress.value = 100;
|
||||
this.$.progress.message = t('complete_message');
|
||||
// 统一刷新
|
||||
const { response } = await Dialog.info(t('imported_dialog.message'), {
|
||||
default: 0,
|
||||
buttons: [
|
||||
t('imported_dialog.btn_refresh'),
|
||||
t('imported_dialog.btn_continue'),
|
||||
]
|
||||
});
|
||||
if (0 === response) {
|
||||
await Editor.Panel.close(`${PackageJSON.name}.creator`);
|
||||
Editor.Message.send('asset-db', 'refresh');
|
||||
}
|
||||
},
|
||||
|
||||
async onSerializeComponent() {
|
||||
await Editor.Message.request('scene', 'execute-scene-script', {
|
||||
name: 'importer',
|
||||
method: 'onSerializeComponent',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
exports.ready = async function() {
|
||||
this.$.engine2D.src = join(__dirname, '../creator/engine/engine2D.html');
|
||||
// 通过后缀名来注册转换器
|
||||
registerConverter();
|
||||
|
||||
Message.addBroadcastListener('i18n:change', () => {
|
||||
if (this.$.progress.message) {
|
||||
const total = this.list.length;
|
||||
if (this.progressCurrentIdx === total) {
|
||||
this.$.progress.message = t('complete_message');
|
||||
}
|
||||
else if (this.progressCurrentIdx === 0) {
|
||||
this.$.progress.message = t('import_message_init', {
|
||||
// @ts-ignore
|
||||
current: 0,
|
||||
total: total,
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.$.progress.value = this.progressCurrentIdx / total * 100;
|
||||
this.$.progress.message = t('import_message', {
|
||||
// @ts-ignore
|
||||
current: this.progressCurrentIdx,
|
||||
total: total,
|
||||
name: this.progressCurrentName,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.$.version.value = t('import_version', { version: PackageJSON.version });
|
||||
|
||||
this.$.tree.setTemplateInit('item', ($item: any) => {
|
||||
$item.addEventListener('contextmenu', () => {
|
||||
Editor.Menu.popup({
|
||||
menu: [{
|
||||
label: t('menu.popup_open'),
|
||||
click() {
|
||||
shell.showItemInFolder($item.data.detail.file);
|
||||
},
|
||||
}],
|
||||
});
|
||||
});
|
||||
});
|
||||
const newIcon = join(__dirname, `../creator/icon/new.png`);
|
||||
this.$.tree.setTemplate('text', `<ui-checkbox></ui-checkbox><ui-icon></ui-icon><span class="name"></span><image class="new" ></image>`);
|
||||
this.$.tree.setTemplateInit('text', ($text: any) => {
|
||||
$text.$checkbox = $text.querySelector('ui-checkbox');
|
||||
$text.$icon = $text.querySelector('ui-icon');
|
||||
$text.$name = $text.querySelector('.name');
|
||||
$text.$new = $text.querySelector('.new');
|
||||
$text.$new.style.display = 'none';
|
||||
$text.$new.src = newIcon;
|
||||
|
||||
$text.$checkbox.addEventListener('confirm', () => {
|
||||
const data = $text.data;
|
||||
data.detail.checked = !data.detail.checked;
|
||||
|
||||
// 将配置同步给子元素
|
||||
function stepChildren(data: any, boolean: boolean) {
|
||||
data.detail.checked = boolean;
|
||||
data.children && data.children.forEach((child: any) => {
|
||||
stepChildren(child, boolean);
|
||||
});
|
||||
}
|
||||
stepChildren(data, data.detail.checked);
|
||||
|
||||
// 更新父级勾选状态
|
||||
function stepParent(info: any) {
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
|
||||
const checked = info.children.some((data: any) => {
|
||||
return data.detail.checked;
|
||||
});
|
||||
|
||||
if (checked !== info.detail.checked) {
|
||||
info.detail.checked = checked;
|
||||
stepParent(info.parent);
|
||||
}
|
||||
|
||||
}
|
||||
stepParent(data.parent);
|
||||
|
||||
this.updateList();
|
||||
this.$.tree.render(true);
|
||||
});
|
||||
});
|
||||
this.$.tree.setRender('text', ($text: any, data: any) => {
|
||||
$text.$checkbox.value = data.detail.checked;
|
||||
$text.$name.innerHTML = data.detail.value;
|
||||
if (data.detail.isDirectory) {
|
||||
$text.$icon.setAttribute('value', 'folder');
|
||||
} else {
|
||||
$text.$icon.setAttribute('value', 'file');
|
||||
}
|
||||
$text.$new.style.display = data.detail.isNew ? '' : 'none';
|
||||
});
|
||||
|
||||
this.$.tree.setRender('item', ($item: any) => {
|
||||
const data = $item.data;
|
||||
if (data.detail.legal) {
|
||||
$item.removeAttribute('disabled');
|
||||
} else {
|
||||
$item.setAttribute('disabled', '');
|
||||
}
|
||||
});
|
||||
|
||||
this.$.tree.css = readFileSync(join(__dirname, '../creator/tree.css'), 'utf8');
|
||||
|
||||
this.$.serialize && this.$.serialize.addEventListener('confirm', () => {
|
||||
this.onSerializeComponent();
|
||||
});
|
||||
|
||||
this.$.search && this.$.search.addEventListener('change', (event: Event) => {
|
||||
// @ts-ignore
|
||||
const value = event.target.value;
|
||||
if (!value) {
|
||||
this.$.tree.tree = this.baseTree;
|
||||
}
|
||||
else {
|
||||
this.$.tree.tree = this.totalTree.map((item: any) => {
|
||||
if ((item.detail.value.toLocaleLowerCase().indexOf(value.toLocaleLowerCase()) > -1)) {
|
||||
return item;
|
||||
}
|
||||
return false;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
this.$.tree.render(true);
|
||||
});
|
||||
|
||||
// 选择项目按钮
|
||||
this.$.import.setAttribute('disabled', '');
|
||||
this.$.project.addEventListener('change', async (event: Event) => {
|
||||
// @ts-ignore
|
||||
const path = event.target.value;
|
||||
const done = await this.scanProject(path);
|
||||
if (done) {
|
||||
Editor.Profile.setConfig(PackageJSON.name, 'import-path', path);
|
||||
}
|
||||
else {
|
||||
this.$.project.value = this.projectRoot;
|
||||
}
|
||||
});
|
||||
|
||||
// 开始导入按钮
|
||||
this.$.import.addEventListener('confirm', async () => {
|
||||
this.$.import.setAttribute('disabled', '');
|
||||
await this.importProject();
|
||||
console.log(t('complete_message'));
|
||||
setTimeout(async () => {
|
||||
this.updateTreeState();
|
||||
this.$.import.removeAttribute('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
// this.$.notes && this.$.notes.addEventListener('confirm', () => {
|
||||
// const electron = require('electron');
|
||||
// electron.shell.openExternal(MANUAL);
|
||||
// });
|
||||
|
||||
const path = await Editor.Profile.getConfig(PackageJSON.name, 'import-path');
|
||||
this.$.project.value = path;
|
||||
await this.scanProject(path);
|
||||
// await this.scanProject('/Users/huangyanbin/helloworld-typescript');
|
||||
// await this.scanProject('/Users/huangyanbin/test-cases/v2_4_0');
|
||||
};
|
||||
|
||||
exports.beforeClose = function() {
|
||||
|
||||
};
|
||||
|
||||
exports.close = function() {
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user