HumanBreak/script/build-resource.ts
2025-09-01 23:21:01 +08:00

189 lines
5.0 KiB
TypeScript

import JSZip from 'jszip';
import {
RequiredData,
RequiredIconsData,
ResourceType,
ResourceUsage
} from './types';
import { Stats } from 'fs';
import { readdir, readFile, stat } from 'fs/promises';
import { resolve } from 'path';
import { fileHash } from './utils';
export interface ResourceInfo {
name: string;
type: ResourceType;
usage: ResourceUsage;
stats: Stats;
}
export interface SplittedResource {
readonly byteLength: number;
readonly resource: JSZip;
readonly buffer: Uint8Array;
readonly fileName: string;
readonly content: Readonly<ResourceInfo>[];
}
interface ResourceContent extends ResourceInfo {
content: string | Buffer | Uint8Array;
exceed: boolean;
}
interface ResourcePath {
name: string;
path: string;
usage: ResourceUsage;
}
function getTypeByUsage(usage: ResourceUsage): ResourceType {
switch (usage) {
case 'animate':
return 'text';
case 'autotile':
case 'image':
case 'tileset':
return 'image';
case 'sound':
return 'byte';
case 'font':
return 'buffer';
case 'material':
return 'material';
}
}
function readFileOfType(path: string, type: ResourceType) {
if (type === 'text') {
return readFile(path, 'utf-8');
} else {
return readFile(path);
}
}
async function compressFiles(files: ResourceContent[]) {
const zip = new JSZip();
files.forEach(v => {
const dir = `${v.type}/${v.name}`;
zip.file(dir, v.content);
});
const buffer = await zip.generateAsync({ type: 'uint8array' });
const hash = fileHash(buffer);
const name = `resource.${hash}.h5data`;
const resource: SplittedResource = {
byteLength: buffer.byteLength,
buffer: buffer,
resource: zip,
fileName: name,
content: files
};
return resource;
}
export async function splitResource(
data: RequiredData,
icons: RequiredIconsData,
base: string,
fontsDir: string,
limit: number
) {
const result: SplittedResource[] = [];
// 获取所有需要分块的资源
const { animates, fonts, images, sounds, tilesets } = data.main;
const autotiles = Object.keys(icons.autotile);
const materials = await readdir(resolve(base, 'project/materials'));
const paths: ResourcePath[] = [
...animates.map<ResourcePath>(v => ({
name: `${v}.animate`,
path: resolve(base, 'project/animates', `${v}.animate`),
usage: 'animate'
})),
...fonts.map<ResourcePath>(v => ({
name: `${v}.ttf`,
path: resolve(fontsDir, `${v}.ttf`),
usage: 'font'
})),
...images.map<ResourcePath>(v => ({
name: v,
path: resolve(base, 'project/images', v),
usage: 'image'
})),
...sounds.map<ResourcePath>(v => ({
name: v,
path: resolve(base, 'project/sounds', v),
usage: 'sound'
})),
...tilesets.map<ResourcePath>(v => ({
name: v,
path: resolve(base, 'project/tilesets', v),
usage: 'tileset'
})),
...autotiles.map<ResourcePath>(v => ({
name: `${v}.png`,
path: resolve(base, 'project/autotiles', `${v}.png`),
usage: 'autotile'
})),
...materials.map<ResourcePath>(v => ({
name: v,
path: resolve(base, 'project/materials', v),
usage: 'material'
}))
];
const files = await Promise.all(
paths.map(async ({ path, usage, name }) => {
const stats = await stat(path);
if (!stats.isFile()) {
return Promise.reject(
new ReferenceError(
`Expected resource is a file, but get directory.`
)
);
}
const type = getTypeByUsage(usage);
const content = await readFileOfType(path, type);
const info: ResourceContent = {
type,
name,
usage,
stats,
content,
exceed: stats.size > limit
};
return info;
})
);
// 从小到大排序,这样的话可以尽量减小资源分块文件数量
files.sort((a, b) => a.stats.size - b.stats.size);
let index = 0;
while (index < files.length) {
let total = 0;
const start = index;
for (let i = index; i < files.length; i++) {
const file = files[i];
if (file.exceed) {
if (i === index) i = index + 1;
index = i;
break;
} else {
total += file.stats.size;
}
if (total > limit) {
index = i;
break;
}
}
const toZip = files.slice(start, index);
result.push(await compressFiles(toZip));
}
return result;
}