From ec0a7f6785c343eb7b9b1554504c8ed847ee8461 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 20 Jun 2023 22:35:51 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99=E8=B5=84=E6=BA=90=E5=88=86?= =?UTF-8?q?=E7=A6=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- mota.config.ts | 14 +- package.json | 6 +- public/main.js | 2 +- script/build.ts | 9 +- script/resource.ts | 421 ++++++++++++++++++++---------------- src/core/loader/load.ts | 9 + src/core/loader/resource.ts | 31 ++- src/types/core.d.ts | 2 +- tsconfig.json | 3 +- tsconfig.node.json | 5 +- 11 files changed, 295 insertions(+), 210 deletions(-) diff --git a/.gitignore b/.gitignore index 833f8af..4d12667 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ index.cjs !public/swap/*.h5save _bundle out -dist-resource \ No newline at end of file +dist-resource +_temp \ No newline at end of file diff --git a/mota.config.ts b/mota.config.ts index 1f6285e..8706246 100644 --- a/mota.config.ts +++ b/mota.config.ts @@ -1,6 +1,7 @@ interface MotaConfig { name: string; resourceName?: string; + zip?: Record; } function defineConfig(config: MotaConfig): MotaConfig { @@ -10,5 +11,16 @@ function defineConfig(config: MotaConfig): MotaConfig { export default defineConfig({ // 这里修改塔的name,请保持与全塔属性的完全相同,否则发布之后可能无法进行游玩 name: 'HumanBreak', - resourceName: 'HumanBreakRes' + resourceName: 'HumanBreakRes', + zip: { + 'resource.zip': [ + 'autotiles/*', + 'tilesets/*', + 'materials/*', + 'images/*', + 'animates/*', + 'sounds/*', + 'fonts/*' + ] + } }); diff --git a/package.json b/package.json index 105ae41..cb7543c 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "type": "module", "scripts": { "dev": "ts-node-esm script/dev.ts", - "build": "vue-tsc && vite build && ts-node-esm script/build.ts 0 1 1", - "build-gh": "vue-tsc && vite build --base=/HumanBreak/ && ts-node-esm script/build.ts 1", - "build-local": "vue-tsc && vite build --base=/ && ts-node-esm script/build.ts 1", + "build": "vue-tsc && vite build && ts-node-esm script/build.ts dist", + "build-gh": "vue-tsc && vite build --base=/HumanBreak/ && ts-node-esm script/build.ts gh", + "build-local": "vue-tsc && vite build --base=/ && ts-node-esm script/build.ts local", "preview": "vite preview", "update": "ts-node-esm script/update.ts", "declare": "ts-node-esm script/declare.ts", diff --git a/public/main.js b/public/main.js index 6264119..decbd2f 100644 --- a/public/main.js +++ b/public/main.js @@ -220,7 +220,7 @@ function main() { // 远程资源地址,在线游戏中,塔本体不包含任何资源,只包含源码,从而可以降低游戏本体的体积并平均分担资源包体积 // 从而可以优化加载并避免网站发布的大小限制 - this.USE_RESOURCE = false; + this.RESOURCE_TYPE = 'dev'; this.RESOURCE_URL = ''; this.RESOURCE_SYMBOL = ''; this.RESOURCE_INDEX = {}; diff --git a/script/build.ts b/script/build.ts index f1aadbc..4dea3ce 100644 --- a/script/build.ts +++ b/script/build.ts @@ -11,9 +11,10 @@ import commonjs from '@rollup/plugin-commonjs'; import { splitResorce } from './resource.js'; import compressing from 'compressing'; -const map = !!Number(process.argv[2]); -const resorce = !!Number(process.argv[3]); -const compress = !!Number(process.argv[4]); +const type = process.argv[2]; +const map = false; +const resorce = type !== 'dev'; +const compress = type === 'dist'; (async function () { const timestamp = Date.now(); @@ -175,7 +176,7 @@ const compress = !!Number(process.argv[4]); // 6. 资源分离 if (resorce) { - await splitResorce(compress); + await splitResorce(type); } // 7. 压缩 diff --git a/script/resource.ts b/script/resource.ts index 626c998..6b7981b 100644 --- a/script/resource.ts +++ b/script/resource.ts @@ -1,174 +1,231 @@ import fs from 'fs-extra'; import { uniqueSymbol } from './utils.js'; -import { basename, extname, resolve } from 'path'; -import { dirname } from 'path'; +import { resolve } from 'path'; import motaConfig from '../mota.config.js'; - -type ResorceType = - | 'bgms' - | 'sounds' - | 'autotiles' - | 'images' - | 'materials' - | 'tilesets' - | 'animates' - | 'fonts'; +import compressing from 'compressing'; const SYMBOL = uniqueSymbol(); const MAX_SIZE = 100 * (1 << 20) - 20 * (1 << 10); -const baseDir = './dist'; - -let totalSize = 0; +const sourceIndex: Record = {}; +const toMove: Stats[] = []; +const all = [ + 'bgms', + 'sounds', + 'autotiles', + 'images', + 'materials', + 'tilesets', + 'animates', + 'fonts' +]; type Stats = fs.Stats & { name?: string }; -export async function splitResorce(compress: boolean = false) { +export async function splitResorce(type: string) { await fs.ensureDir('./dist-resource'); await fs.emptyDir('./dist-resource'); - const folder = await fs.stat('./dist'); - totalSize = folder.size; - if (totalSize < MAX_SIZE) return; + await readySplit(); - await doSplit(compress); + await zipResource(); + await split(type === 'dist' ? MAX_SIZE : void 0); + + await endSplit(type); } -async function sortDir(dir: string, ext?: string[]) { - const path = await fs.readdir(dir); - const stats: Stats[] = []; - - for await (const one of path) { - if (ext && !ext.includes(extname(one))) continue; - if (one === 'bg.jpg') continue; - const stat = await fs.stat(resolve(dir, one)); - if (!stat.isFile()) continue; - const status: Stats = { - ...stat, - name: one - }; - stats.push(status); - } - - return stats.sort((a, b) => b.size - a.size); +async function readySplit() { + await fs.ensureDir('./_temp'); + await fs.emptyDir('./_temp'); + await fs.ensureDir('./_temp/origin'); + await copyAll(); } -async function calResourceSize() { - return ( - await Promise.all( - [ - 'animates', - 'autotiles', - 'bgms', - 'fonts', - 'images', - 'materials', - 'sounds', - 'tilesets' - ].map(v => fs.stat(resolve('./dist/project/', v))) - ) - ).reduce((pre, cur) => pre + cur.size, 0); +async function endSplit(type: string) { + await rewriteMain(type); + await fs.emptyDir('./_temp'); + await fs.rmdir('./_temp'); } -async function doSplit(compress: boolean) { - let size = await calResourceSize(); - await fs.emptyDir('./dist-resource'); - const priority: ResorceType[] = [ - 'materials', - 'tilesets', - 'autotiles', - 'animates', - 'images', - 'sounds', - 'fonts', - 'bgms' - ]; - const dirInfo: Record = Object.fromEntries( - await Promise.all( - priority.map(async v => [ - v, - await sortDir(resolve(baseDir, 'project', v)) - ]) - ) - ); +async function zipResource() { + const zip = motaConfig.zip; + if (!zip) return; + for await (const [name, files] of Object.entries(zip)) { + const stream = new compressing.zip.Stream(); + const dirs: string[] = []; - let currSize = 0; - const length = Object.fromEntries( - priority.map(v => [v, dirInfo[v].length]) - ); - const soruceIndex: Record = {}; - const split = async (index: number): Promise => { - size -= currSize; - currSize = 0; - - const cut: string[] = []; - - const mapped: ResorceType[] = []; - while (1) { - const toCut = priority.find( - v => dirInfo[v].length > 0 && !mapped.includes(v) - ); - if (!toCut) break; - - mapped.push(toCut); - const l = dirInfo[toCut].length; - const data: string[] = []; - - let pass = 0; - while (1) { - const stats = dirInfo[toCut]; - const stat = stats[pass]; - if (!stat) { - break; - } - if (currSize + stat.size >= MAX_SIZE) { - pass++; - continue; - } - data.push(`${toCut}/${stat.name}`); - stats.splice(pass, 1); - currSize += stat.size; - } - - if (l === length[toCut] && dirInfo[toCut].length === 0) { - soruceIndex[`${toCut}.*`] = index; + for await (const file of files) { + if (/^.+\/\*$/.test(file)) { + const dir = file.split('/')[0]; + dirs.push(dir); + await fs.copy(`./_temp/origin/${dir}`, `./_temp/${dir}`); } else { - data.map(v => (soruceIndex[v] = index)); + const [dir, name] = file.split('/'); + if (dirs.includes(dir)) dirs.push(dir); + await fs.ensureDir(`./_temp/${dir}`); + await fs.copyFile( + `./_temp/origin/${dir}/${name}`, + `./_temp/${dir}/${name}` + ); } - cut.push(...data); } - const dir = `./dist-resource/${index}`; - await fs.ensureDir(dir); - await Promise.all(priority.map(v => fs.ensureDir(resolve(dir, v)))); - - await Promise.all( - cut.map(v => - fs.move( - resolve('./dist/project', v), - resolve( - dir, - dirname(v), - `${basename(v).split('.')[0]}-${SYMBOL}${extname(v)}` - ) - ) - ) + dirs.forEach(v => stream.addEntry(`./_temp/${v}`)); + const dest = fs.createWriteStream(`./_temp/${name}`); + await new Promise(res => + stream.pipe(dest).on('finish', () => { + res(); + }) ); - - // 生成可发布结构 - await generatePublishStructure(dir, index); - - if (Object.values(dirInfo).every(v => v.length === 0)) return; - else return split(index + 1); - }; - - await split(0); - - await rewriteMain(soruceIndex); + const stat = await fs.stat(`./_temp/${name}`); + toMove.push({ ...stat, name: `./_temp/${name}` }); + } } -async function rewriteMain(sourceIndex: Record) { +async function getRemainReource() { + const zip = motaConfig.zip; + if (!zip) return; + const values = Object.values(zip); + for await (const one of all) { + if (values.some(v => v.includes(`${one}/*`))) continue; + const list = await fs.readdir(`./_temp/origin/${one}`); + for await (const name of list) { + if (!values.some(vv => vv.includes(`${one}/${name}`))) { + const stat = await fs.stat(`./_temp/origin/${one}/${name}`); + toMove.push({ + ...stat, + name: `./_temp/origin/${one}/${name}` + }); + } + } + } + toMove.sort((a, b) => { + if (a.name?.endsWith('.zip') && b.name?.endsWith('.zip')) { + return b.size - a.size; + } + if (a.name?.endsWith('.zip')) return -1; + if (b.name?.endsWith('.zip')) return 1; + return b.size - a.size; + }); +} + +async function split(max?: number) { + await getRemainReource(); + + const doSplit = async (index: string | number) => { + const base = + typeof index === 'string' ? index : `./dist-resource/${index}`; + + await fs.ensureDir(base); + await generatePublishStructure( + base, + typeof index === 'string' ? 0 : index + ); + + let size = (await fs.stat(base)).size; + // 计算出要移动多少资源 + const res = (() => { + if (!max) return toMove.splice(0, toMove.length); + let remain = max - size; + for (let i = 0; i < toMove.length; i++) { + const ele = toMove[i]; + remain -= ele.size; + if (remain <= 0) { + return toMove.splice(0, i); + } + } + return toMove.splice(0, toMove.length); + })(); + + if (base.endsWith('dist')) { + await fs.ensureDir(resolve(base, 'resource')); + } + + // 执行移动 + await Promise.all( + res.map(async v => { + if (!v.name) return; + // 压缩包 + if (v.name.endsWith('.zip')) { + const [, , name] = v.name.split('/'); + const split = name.split('.'); + const target = `${split + .slice(0, -1) + .join('.')}-${SYMBOL}.${split.at(-1)}`; + if (base.endsWith('dist')) { + await fs.ensureDir(resolve(base, 'resource/zip')); + return fs.copyFile( + v.name, + resolve(base, 'resource', 'zip', target) + ); + } else { + await fs.ensureDir(resolve(base, 'zip')); + return fs.copyFile( + v.name, + resolve(base, 'zip', target) + ); + } + } + + // 非压缩包 + if (!v.name.endsWith('.zip')) { + const [, , , type, name] = v.name.split('/'); + const split = name.split('.'); + const target = `${split + .slice(0, -1) + .join('.')}-${SYMBOL}.${split.at(-1)}`; + if (base.endsWith('dist')) { + await fs.ensureDir(resolve(base, 'resource', type)); + } else { + await fs.ensureDir(resolve(base, type)); + } + if (base.endsWith('dist')) { + return fs.copyFile( + v.name, + resolve(base, 'resource', type, target) + ); + } else { + return fs.copyFile(v.name, resolve(base, type, target)); + } + } + }) + ); + + // 标记资源索引 + res.forEach(v => { + if (!v.name) return; + // 压缩包 + if (v.name.endsWith('.zip')) { + const [, , name] = v.name.split('/'); + sourceIndex[`zip.${name}`] = index.toString(); + } + // 非压缩包 + if (!v.name.endsWith('.zip')) { + const [, , , type, name] = v.name.split('/'); + sourceIndex[`${type}.${name}`] = index.toString(); + } + }); + + if (toMove.length > 0) { + await doSplit(typeof index === 'string' ? 0 : index + 1); + } + }; + await doSplit('dist'); +} + +async function copyAll() { + await Promise.all( + all.map(v => { + return fs.move(`./dist/project/${v}`, `./_temp/origin/${v}`); + }) + ); +} + +async function rewriteMain(type: string) { const main = await fs.readFile('./dist/main.js', 'utf-8'); const res = main - .replace(/this\.USE_RESOURCE\s*\=\s*false/, 'this.USE_RESOURCE = true') + .replace( + /this\.RESOURCE_TYPE\s*\=\s*.*;/, + `this.RESOURCE_TYPE = '${type}';` + ) .replace( /this\.RESOURCE_URL\s*\=\s*'.*'/, `this.RESOURCE_URL = '/games/${motaConfig.resourceName}'` @@ -184,55 +241,47 @@ async function rewriteMain(sourceIndex: Record) { await fs.writeFile('./dist/main.js', res, 'utf-8'); } +/** + * 生成可发布目录 + */ async function generatePublishStructure(dir: string, index: number) { - await fs.mkdir(resolve(dir, 'libs')); - await fs.mkdir(resolve(dir, 'libs/thirdparty')); - await fs.mkdir(resolve(dir, 'project')); + await fs.ensureDir(resolve(dir, 'libs')); + await fs.ensureDir(resolve(dir, 'libs/thirdparty')); + await fs.ensureDir(resolve(dir, 'project')); await Promise.all( - [ - 'autotiles', - 'images', - 'materials', - 'animates', - 'fonts', - 'floors', - 'tilesets', - 'sounds', - 'bgms' - ].map(v => fs.mkdir(resolve(dir, 'project', v))) + all.map(v => { + fs.ensureDir(resolve(dir, 'project', v)); + fs.emptyDir(resolve(dir, 'project', v)); + }) ); - await fs.writeFile( - resolve(dir, 'project/icons.js'), - `var icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1 = -{ - "autotile": { + if (!dir.endsWith('dist')) { + await fs.writeFile( + resolve(dir, 'project/icons.js'), + `var icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1 = +{"autotile": {}} +`, + 'utf-8' + ); + await fs.writeFile( + resolve(dir, 'project/floors/none.js'), + '"none"', + 'utf-8' + ); + await fs.writeFile(resolve(dir, 'libs/none.js'), '"none"', 'utf-8'); + await fs.copyFile('./script/template/main.js', resolve(dir, 'main.js')); + const data = await fs.readFile('./script/template/data.js', 'utf-8'); + await fs.writeFile( + resolve(dir, 'project/data.js'), + data.replace('@name', `${motaConfig.resourceName}${index}`) + ); + + await fs.copyFile( + './script/template/lz-string.min.js', + resolve(dir, 'libs/thirdparty/lz-string.min.js') + ); } -}`, - 'utf-8' - ); - await fs.writeFile( - resolve(dir, 'project/floors/none.js'), - '"none"', - 'utf-8' - ); - await fs.writeFile(resolve(dir, 'libs/none.js'), '"none"', 'utf-8'); - - await fs.copyFile( - './script/template/main.js', - resolve(dir, 'project/main.js') - ); - const data = await fs.readFile('./script/template/data.js', 'utf-8'); - await fs.writeFile( - resolve(dir, 'project/data.js'), - data.replace('@name', `${motaConfig.resourceName}${index}`) - ); - - await fs.copyFile( - './script/template/lz-string.min.js', - resolve(dir, 'libs/thirdparty/lz-string.min.js') - ); await Promise.all( ['animates', 'images', 'materials', 'sounds', 'tilesets'].map(v => { diff --git a/src/core/loader/load.ts b/src/core/loader/load.ts index 25eb67e..129397f 100644 --- a/src/core/loader/load.ts +++ b/src/core/loader/load.ts @@ -3,7 +3,11 @@ import { Resource, getTypeByResource } from './resource'; const info = resource; +/** + * 构建游戏包后的加载 + */ export function readyAllResource() { + if (main.RESOURCE_TYPE === 'dev') return readyDevResource(); info.resource.forEach(v => { const type = getTypeByResource(v); if (type === 'zip') { @@ -13,3 +17,8 @@ export function readyAllResource() { } }); } + +/** + * 开发时的加载 + */ +function readyDevResource() {} diff --git a/src/core/loader/resource.ts b/src/core/loader/resource.ts index 763bbb3..d9af1ca 100644 --- a/src/core/loader/resource.ts +++ b/src/core/loader/resource.ts @@ -88,21 +88,34 @@ export class Resource< const name = (this.name = resolve.slice(1, -1).join('.')); const ext = (this.ext = '.' + resolve.at(-1)); - if (!main.USE_RESOURCE) { - return `/games/${core.data.firstData.name}/project/${type}/${name}${ext}`; - } + const distBase = import.meta.env.BASE_URL; const base = main.RESOURCE_URL; const indexes = main.RESOURCE_INDEX; const symbol = main.RESOURCE_SYMBOL; + const t = main.RESOURCE_TYPE; - if (has(indexes[`${type}.*`])) { - const i = indexes[`${type}.*`]; - return `${base}${i}/${type}/${name}-${symbol}${ext}`; + if (t === 'dist') { + if (has(indexes[`${type}.*`])) { + const i = indexes[`${type}.*`]; + if (i !== 'dist') { + return `${base}${i}/${type}/${name}-${symbol}${ext}`; + } else { + return `${distBase}resource/${type}/${name}-${symbol}${ext}`; + } + } else { + const i = indexes[`${type}.${name}${ext}`]; + const index = has(i) ? i : '0'; + if (i !== 'dist') { + return `${base}${index}/${type}/${name}-${symbol}${ext}`; + } else { + return `${distBase}resource/${type}/${name}-${symbol}${ext}`; + } + } + } else if (t === 'gh' || t === 'local') { + return `${distBase}resource/${type}/${name}-${symbol}${ext}`; } else { - const i = indexes[`${type}.${name}${ext}`]; - const index = has(i) ? i : 0; - return `${base}${index}/${type}/${name}-${symbol}${ext}`; + return `${distBase}project/${type}/${name}${ext}`; } } diff --git a/src/types/core.d.ts b/src/types/core.d.ts index 76d0139..25b734d 100644 --- a/src/types/core.d.ts +++ b/src/types/core.d.ts @@ -1259,7 +1259,7 @@ interface Main extends MainData { readonly RESOURCE_INDEX: Record; readonly RESOURCE_URL: string; readonly RESOURCE_SYMBOL: string; - readonly USE_RESOURCE: boolean; + readonly RESOURCE_TYPE: 'dev' | 'dist' | 'gh' | 'local'; /** * 初始化游戏 diff --git a/tsconfig.json b/tsconfig.json index 766f7ee..0c21b42 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,8 +19,7 @@ "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", - "mota.config.ts", - "script/**/*.ts" + "mota.config.ts" ], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/tsconfig.node.json b/tsconfig.node.json index af9e6f2..6899000 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -4,7 +4,8 @@ "composite": true, "module": "ESNext", "moduleResolution": "Node", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "strict": true }, - "include": ["vite.config.ts", "mota.config.ts"] + "include": ["vite.config.ts", "mota.config.ts", "script/**/*.ts"] }