feat: 新的加载系统

This commit is contained in:
unanmed 2024-04-21 13:37:27 +08:00
parent 30a1fd9013
commit 80a67d5197
12 changed files with 715 additions and 485 deletions

View File

@ -297,9 +297,19 @@ core.prototype.init = async function (coreData, callback) {
}
}
if (main.replayChecking || main.mode === 'editor') {
core.loader._load(function () {
core._afterLoadResources(callback);
});
} else {
if (main.renderLoaded)
Mota.require('var', 'fixedUi').open('load', { callback });
else {
Mota.require('var', 'hook').once('renderLoaded', () => {
Mota.require('var', 'fixedUi').open('load', { callback });
});
}
}
};
core.prototype.initSync = function (coreData, callback) {

View File

@ -316,6 +316,7 @@ async function writeMultiFiles(req: Request, res: Response) {
}
async function writeDevResource(data: string) {
return;
try {
const buf = Buffer.from(data, 'base64');
data = buf.toString('utf-8');
@ -324,12 +325,14 @@ async function writeDevResource(data: string) {
const icons = await fs.readFile('./public/project/icons.js', 'utf-8');
const iconData = JSON.parse(icons.split('\n').slice(1).join(''));
res.push(
...info.main.bgms.map((v: any) => `bgms.${v}`),
...info.main.fonts.map((v: any) => `fonts.${v}.ttf`),
...info.main.images.map((v: any) => `images.${v}`),
...info.main.sounds.map((v: any) => `sounds.${v}`),
...info.main.tilesets.map((v: any) => `tilesets.${v}`),
...Object.keys(iconData.autotile).map(v => `autotiles.${v}.png`),
...info.main.bgms.map((v: any) => `audio/${v}`),
...info.main.fonts.map((v: any) => `buffer/project/fonts/${v}.ttf`),
...info.main.images.map((v: any) => `image/project/images/${v}`),
...info.main.sounds.map((v: any) => `buffer/${v}`),
...info.main.tilesets.map((v: any) => `image/project/tilesets${v}`),
...Object.keys(iconData.autotile).map(
v => `image/project/autotiles/${v}.png`
),
...[
'animates',
'cloud',
@ -343,7 +346,7 @@ async function writeDevResource(data: string) {
'npcs',
'sun',
'terrains'
].map(v => `materials.${v}.png`)
].map(v => `material/${v}.png`)
);
const text = JSON.stringify(res, void 0, 4);
await fs.writeFile('./src/data/resource-dev.json', text, 'utf-8');

566
src/core/common/resource.ts Normal file
View File

@ -0,0 +1,566 @@
import axios, { AxiosRequestConfig, ResponseType } from 'axios';
import { Disposable } from './disposable';
import { logger } from './logger';
import JSZip from 'jszip';
import { EmitableEvent, EventEmitter } from './eventEmitter';
type ProgressFn = (now: number, total: number) => void;
interface ResourceType {
text: string;
buffer: ArrayBuffer;
image: HTMLImageElement;
material: HTMLImageElement;
audio: HTMLAudioElement;
json: any;
zip: JSZip;
}
interface ResourceMap {
text: TextResource;
buffer: BufferResource;
image: ImageResource;
material: MaterialResource;
audio: AudioResource;
json: JSONResource;
zip: ZipResource;
}
export abstract class Resource<T = any> extends Disposable<string> {
type = 'none';
uri: string = '';
resource?: T;
loaded: boolean = false;
/**
*
* @param uri URI type/file
* @param type none
*/
constructor(uri: string, type: string = 'none') {
super(uri);
this.type = type;
this.uri = uri;
if (this.type === 'none') {
logger.warn(1, `Resource with type of 'none' is loaded.`);
}
}
/**
* override
*/
abstract load(onProgress?: ProgressFn): Promise<T>;
/**
* URIURL
*/
abstract resolveURI(): string;
/**
* null
*/
getData(): T | null {
if (!this.activated || !this.loaded) return null;
if (this.resource === null || this.resource === void 0) return null;
return this.resource;
}
}
export class ImageResource extends Resource<HTMLImageElement> {
/**
*
* @param uri URI image/file 'image/project/images/hero.png'
*/
constructor(uri: string) {
super(uri, 'image');
}
load(onProgress?: ProgressFn): Promise<HTMLImageElement> {
const img = new Image();
img.src = this.resolveURI();
this.resource = img;
return new Promise<HTMLImageElement>(res => {
img.addEventListener('load', () => {
this.loaded = true;
img.setAttribute('_width', img.width.toString());
img.setAttribute('_height', img.height.toString());
res(img);
});
});
}
resolveURI(): string {
return `/${findURL(this.uri)}`;
}
}
export class MaterialResource extends ImageResource {
/**
* material资源
* @param uri URI material/file 'material/enemys.png'
*/
constructor(uri: string) {
super(uri);
this.type = 'material';
}
override resolveURI(): string {
return `/project/materials/${findURL(this.uri)}`;
}
}
export class TextResource extends Resource<string> {
/**
*
* @param uri URI text/file 'text/myText.txt'
* myText.txt
*/
constructor(uri: string) {
super(uri, 'text');
}
load(onProgress?: ProgressFn): Promise<string> {
return new Promise(res => {
createAxiosLoader<string>(
this.resolveURI(),
'text',
onProgress
).then(value => {
this.resource = value.data;
this.loaded = true;
res(value.data);
});
});
}
resolveURI(): string {
return `/${findURL(this.uri)}`;
}
}
export class BufferResource extends Resource<ArrayBuffer> {
/**
*
* @param uri URI buffer/file 'buffer/myBuffer.mp3'
*/
constructor(uri: string) {
super(uri, 'buffer');
}
load(onProgress?: ProgressFn): Promise<ArrayBuffer> {
return new Promise(res => {
createAxiosLoader<ArrayBuffer>(
this.resolveURI(),
'arraybuffer',
onProgress
).then(value => {
this.resource = value.data;
this.loaded = true;
res(value.data);
});
});
}
resolveURI(): string {
return `/${findURL(this.uri)}`;
}
}
export class JSONResource<T = any> extends Resource<T> {
/**
* JSON对象资源
* @param uri URI json/file 'buffer/myJSON.json'
*/
constructor(uri: string) {
super(uri, 'json');
}
load(onProgress?: ProgressFn): Promise<any> {
return new Promise(res => {
createAxiosLoader<any>(this.resolveURI(), 'json', onProgress).then(
value => {
this.resource = value.data;
this.loaded = true;
res(value.data);
}
);
});
}
resolveURI(): string {
return `/${findURL(this.uri)}`;
}
}
export class AudioResource extends Resource<HTMLAudioElement> {
/**
*
* @param uri URI audio/file 'audio/bgm.mp3'
* bgm
* 使 BufferResource
* AudioPlayer
*/
constructor(uri: string) {
super(uri, 'audio');
}
load(onProgress?: ProgressFn): Promise<HTMLAudioElement> {
const audio = new Audio();
audio.src = this.resolveURI();
this.resource = audio;
return new Promise<HTMLAudioElement>(res => {
this.loaded = true;
res(audio);
});
}
resolveURI(): string {
return `/project/bgms/${findURL(this.uri)}`;
}
}
export class ZipResource extends Resource<JSZip> {
/**
* zip压缩资源
* @param uri URI zip/file 'zip/myZip.h5data'
* zip
*/
constructor(uri: string) {
super(uri);
this.type = 'zip';
}
async load(onProgress?: ProgressFn): Promise<JSZip> {
const data = await new Promise<ArrayBuffer>(res => {
createAxiosLoader<ArrayBuffer>(
this.resolveURI(),
'arraybuffer',
onProgress
).then(value => {
res(value.data);
});
});
const unzipped = await JSZip.loadAsync(data);
this.resource = unzipped;
this.loaded = true;
return unzipped;
}
resolveURI(): string {
return `/${findURL(this.uri)}`;
}
}
function createAxiosLoader<T = any>(
url: string,
responseType: ResponseType,
onProgress?: (now: number, total: number) => void
) {
const config: AxiosRequestConfig<T> = {};
config.responseType = responseType;
if (onProgress) {
config.onDownloadProgress = e => {
onProgress(e.loaded, e.total ?? 0);
};
}
return axios.get<T>(url, config);
}
function findURL(uri: string) {
return uri.slice(uri.indexOf('/') + 1);
}
export const resourceTypeMap = {
text: TextResource,
buffer: BufferResource,
image: ImageResource,
material: MaterialResource,
audio: AudioResource,
json: JSONResource,
zip: ZipResource
};
interface LoadEvent<T extends keyof ResourceType> extends EmitableEvent {
progress: (
type: keyof ResourceType,
uri: string,
now: number,
total: number
) => void;
load: (resource: ResourceMap[T]) => void;
loadStart: (resource: ResourceMap[T]) => void;
}
type TaskProgressFn = (
loadedByte: number,
totalByte: number,
loadedTask: number,
totalTask: number
) => void;
export class LoadTask<
T extends keyof ResourceType = keyof ResourceType
> extends EventEmitter<LoadEvent<T>> {
static totalByte: number = 0;
static loadedByte: number = 0;
static totalTask: number = 0;
static loadedTask: number = 0;
static errorTask: number = 0;
/** 所有的资源,包括没有添加到加载任务里面的 */
static store: Map<string, Resource> = new Map();
static taskList: Set<LoadTask> = new Set();
static loadedTaskList: Set<LoadTask> = new Set();
private static progress: TaskProgressFn;
private static caledTask: Set<string> = new Set();
resource: Resource;
type: T;
uri: string;
private loadingStarted: boolean = false;
loading: boolean = false;
loaded: number = 0;
/**
*
* @param type
* @param uri URL
*/
constructor(type: T, uri: string) {
super();
this.resource = new resourceTypeMap[type](uri);
this.type = type;
this.uri = uri;
LoadTask.store.set(uri, this.resource);
}
/**
* Promise将会被resolve
* @returns Promise
*/
load(): Promise<ResourceType[T]> {
if (this.loadingStarted) {
logger.warn(
2,
`Repeat load of resource '${this.resource.type}/${this.resource.uri}'`
);
return new Promise<void>(res => res());
}
this.loadingStarted = true;
let totalByte = 0;
const load = this.resource
.load((now, total) => {
this.loading = true;
this.emit('progress', this.type, this.uri, now, total);
if (!LoadTask.caledTask.has(this.uri) && total !== 0) {
LoadTask.totalByte += total;
totalByte = total;
LoadTask.caledTask.add(this.uri);
}
this.loaded = now;
})
.catch(reason => {
LoadTask.errorTask++;
logger.error(
2,
`Unexpected loading error in loading resource '${this.resource.type}/${this.resource.uri}'. Error info: ${reason}`
);
});
this.emit('loadStart', this.resource);
load.then(() => {
// @ts-ignore
LoadTask.loadedTaskList.add(this);
this.loaded = totalByte;
LoadTask.loadedTask++;
this.emit('load', this.resource);
});
return load;
}
/**
*
* @param type
* @param uri URI
*/
static add<T extends keyof ResourceType>(
type: T,
uri: string
): LoadTask<T> {
const task = new LoadTask(type, uri);
// @ts-ignore
this.taskList.add(task);
return task;
}
/**
*
* @param task
*/
static addTask(task: LoadTask) {
this.taskList.add(task);
}
/**
*
*/
static async load() {
this.totalTask = this.taskList.size;
const fn = () => {
this.loadedByte = [...this.taskList].reduce((prev, curr) => {
return prev + curr.loaded;
}, 0);
this.progress?.(
this.loadedByte,
this.totalByte,
this.loadedTask,
this.totalTask
);
};
fn();
const interval = window.setInterval(fn, 100);
const data = await Promise.all([...this.taskList].map(v => v.load()));
window.clearInterval(interval);
this.loadedByte = this.totalByte;
fn();
this.progress?.(
this.totalByte,
this.totalByte,
this.totalTask,
this.totalTask
);
return data;
}
/**
*
*/
static onProgress(progress: TaskProgressFn) {
this.progress = progress;
}
/**
*
*/
static reset() {
this.loadedByte = 0;
this.loadedTask = 0;
this.totalByte = 0;
this.totalTask = 0;
this.errorTask = 0;
this.caledTask.clear();
this.taskList.clear();
}
}
export function loadDefaultResource() {
const data = data_a1e2fb4a_e986_4524_b0da_9b7ba7c0874d;
const icon = icons_4665ee12_3a1f_44a4_bea3_0fccba634dc1;
// bgm
data.main.bgms.forEach(v => {
const res = LoadTask.add('audio', `audio/${v}`);
Mota.r(() => {
res.once('loadStart', res => {
Mota.require('var', 'bgm').add(`bgms.${v}`, res.resource!);
});
});
});
// fonts
data.main.fonts.forEach(v => {
const res = LoadTask.add('buffer', `buffer/project/fonts/${v}.ttf`);
Mota.r(() => {
res.once('load', res => {
document.fonts.add(new FontFace(v, res.resource!));
});
});
});
// image
data.main.images.forEach(v => {
const res = LoadTask.add('image', `image/project/images/${v}`);
res.once('load', res => {
core.material.images.images[v] = res.resource!;
});
});
// sound
data.main.sounds.forEach(v => {
const res = LoadTask.add('buffer', `buffer/project/sounds/${v}`);
Mota.r(() => {
res.once('load', res => {
Mota.require('var', 'sound').add(`sounds.${v}`, res.resource!);
});
});
});
// tilseset
data.main.tilesets.forEach(v => {
const res = LoadTask.add('image', `image/project/tilesets/${v}`);
res.once('load', res => {
core.material.images.tilesets[v] = res.resource!;
});
});
// autotile
const autotiles: Partial<Record<AllIdsOf<'autotile'>, HTMLImageElement>> =
{};
Object.keys(icon.autotile).forEach(v => {
const res = LoadTask.add('image', `image/project/autotiles/${v}.png`);
res.once('load', res => {
autotiles[v as AllIdsOf<'autotile'>] = res.resource;
const loading = Mota.require('var', 'loading');
loading.addAutotileLoaded();
loading.onAutotileLoaded(autotiles);
core.material.images.autotile[v as AllIdsOf<'autotile'>] =
res.resource!;
});
});
// materials
const imgs = core.materials.slice() as SelectKey<
MaterialImages,
HTMLImageElement
>[];
imgs.push('keyboard');
core.materials
.map(v => `${v}.png`)
.forEach(v => {
const res = LoadTask.add('material', `material/${v}`);
res.once('load', res => {
// @ts-ignore
core.material.images[
v.slice(0, -4) as SelectKey<
MaterialImages,
HTMLImageElement
>
] = res.resource;
});
});
const weathers: (keyof Weather)[] = ['fog', 'cloud', 'sun'];
weathers.forEach(v => {
const res = LoadTask.add('material', `material/${v}.png`);
res.once('load', res => {
// @ts-ignore
core.animateFrame.weather[v] = res.resource;
});
});
// animates
{
const res = LoadTask.add(
'text',
`text/all/__all_animates__?v=${
main.version
}&id=${data.main.animates.join(',')}`
);
res.once('load', res => {
const data = res.resource!.split('@@@~~~###~~~@@@');
data.forEach((v, i) => {
const id = main.animates[i];
if (v === '') {
throw new Error(`Cannot find animate: '${id}'`);
}
core.material.animates[id] = core.loader._loadAnimate(v);
});
});
}
}
export function loadCompressedResource() {}

View File

@ -57,7 +57,6 @@ import EnemyTarget from '@/panel/enemyTarget.vue';
import KeyboardPanel from '@/panel/keyboard.vue';
import { MCGenerator } from './main/layout';
import { ResourceController } from './loader/controller';
import { readyAllResource } from './loader/load';
import { logger } from './common/logger';
// ----- 类注册
@ -133,5 +132,3 @@ Mota.register('module', 'MCGenerator', MCGenerator);
main.renderLoaded = true;
Mota.require('var', 'hook').emit('renderLoaded');
readyAllResource();

View File

@ -1,46 +0,0 @@
import resource from '@/data/resource.json';
import { EmitableEvent, EventEmitter } from '../common/eventEmitter';
import {
Resource,
getTypeByResource,
zipResource,
resource as res
} from './resource';
const info = resource;
/**
*
*/
export function readyAllResource() {
/* @__PURE__ */ if (main.RESOURCE_TYPE === 'dev') return readyDevResource();
info.resource.forEach(v => {
const type = getTypeByResource(v);
if (type === 'zip') {
zipResource.set(v, new Resource(v, 'zip'));
} else {
res.set(v, new Resource(v, type));
}
});
}
/**
*
*/
/* @__PURE__ */ async function readyDevResource() {
const loading = Mota.require('var', 'loading');
const loadData = (await import('../../data/resource-dev.json')).default;
loadData.forEach(v => {
const type = getTypeByResource(v);
if (type !== 'zip') {
res.set(v, new Resource(v, type));
}
});
res.forEach(v => v.active());
loading.once('coreInit', () => {
const animates = new Resource('__all_animates__', 'text');
res.set('__all_animates__', animates);
animates.active();
});
}

View File

@ -1,383 +0,0 @@
import axios, { AxiosResponse } from 'axios';
import { Disposable } from '../common/disposable';
import { ensureArray } from '@/plugin/utils';
import { has } from '@/plugin/utils';
import JSZip from 'jszip';
import { EmitableEvent, EventEmitter } from '../common/eventEmitter';
import { bgm } from '../audio/bgm';
// todo: 应当用register去注册资源类型然后进行分块处理
interface ResourceData {
image: HTMLImageElement;
arraybuffer: ArrayBuffer;
text: string;
json: any;
zip: ZippedResource;
bgm: HTMLAudioElement;
}
export type ResourceType = keyof ResourceData;
export type NonZipResource = Exclude<ResourceType, 'zip'>;
const autotiles: Partial<Record<AllIdsOf<'autotile'>, HTMLImageElement>> = {};
export class Resource<
T extends ResourceType = ResourceType
> extends Disposable<string> {
format: T;
request?: Promise<
AxiosResponse<ResourceData[T]> | '@imageLoaded' | '@bgmLoaded'
>;
loaded: boolean = false;
uri: string;
type!: string;
name!: string;
ext!: string;
/** 资源数据 */
resource?: ResourceData[T];
constructor(resource: string, format: T) {
super(resource);
this.data = this.resolveUrl(resource);
this.format = format;
this.uri = resource;
this.once('active', () => this.load());
this.once('load', v => this.onLoad(v));
this.once('loadstart', v => this.onLoadStart(v));
}
protected onLoadStart(v?: ResourceData[T]) {
if (this.format === 'bgm') {
// bgm 单独处理,因为它可以边播放边加载
bgm.add(this.uri, v!);
}
}
protected onLoad(v: ResourceData[T]) {
const loading = Mota.require('var', 'loading');
// 资源类型处理
if (this.type === 'fonts') {
document.fonts.add(new FontFace(this.name, v as ArrayBuffer));
} else if (this.type === 'sounds') {
Mota.require('var', 'sound').add(this.uri, v as ArrayBuffer);
} else if (this.type === 'images') {
const name = `${this.name}${this.ext}` as ImageIds;
loading.on(
'coreLoaded',
() => {
core.material.images.images[name] = v as HTMLImageElement;
},
{ immediate: true }
);
} else if (this.type === 'materials') {
const name = this.name as SelectKey<
MaterialImages,
HTMLImageElement
>;
loading.on(
'coreLoaded',
() => {
core.material.images[name] = v;
},
{ immediate: true }
);
loading.addMaterialLoaded();
} else if (this.type === 'autotiles') {
const name = this.name as AllIdsOf<'autotile'>;
autotiles[name] = v;
loading.addAutotileLoaded();
loading.onAutotileLoaded(autotiles);
} else if (this.type === 'tilesets') {
const name = `${this.name}${this.ext}`;
loading.on(
'coreLoaded',
() => {
core.material.images.tilesets[name] = v;
},
{ immediate: true }
);
}
// 资源加载类型处理
if (this.format === 'zip') {
(this.resource as ZippedResource).once('ready', data => {
data.forEach((path, file) => {
const [base, name] = path.split(/(\/|\\)/);
const id = `${base}.${name}`;
const type = getTypeByResource(id) as NonZipResource;
const format = getZipFormatByType(type);
resource.set(
id,
new Resource(id, type).setData(file.async(format))
);
});
});
} else if (this.format === 'image') {
const img = v as HTMLImageElement;
img.setAttribute('_width', img.width.toString());
img.setAttribute('_height', img.height.toString());
}
if (this.name === '__all_animates__') {
if (this.format !== 'text') {
throw new Error(
`Unexpected mismatch of '__all_animates__' response type.` +
` Expected: text. Meet: ${this.format}`
);
}
const data = (v as string).split('@@@~~~###~~~@@@');
data.forEach((v, i) => {
const id = main.animates[i];
if (v === '') {
throw new Error(`Cannot find animate: '${id}'`);
}
core.material.animates[id] = core.loader._loadAnimate(v);
});
}
}
/**
* url
* @param resource
* @returns url
*/
protected resolveUrl(resource: string) {
if (resource === '__all_animates__') {
this.type = 'animates';
this.name = '__all_animates__';
this.ext = '.animate';
return `/all/__all_animates__?v=${
main.version
}&id=${main.animates.join(',')}`;
}
const resolve = resource.split('.');
const type = (this.type = resolve[0]);
const name = (this.name = resolve.slice(1, -1).join('.'));
const ext = (this.ext = '.' + resolve.at(-1));
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 (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 {
return `${distBase}project/${type}/${name}${ext}`;
}
}
/**
*
*/
protected load() {
if (this.loaded) {
throw new Error(`Cannot load one resource twice.`);
}
const data = this.data;
if (!data) {
throw new Error(`Unexpected null of url in loading resource.`);
}
if (this.format === 'image') {
this.request = new Promise(res => {
const img = new Image();
img.src = data;
this.emit('loadstart', img);
img.addEventListener('load', () => {
this.resource = img;
this.loaded = true;
this.emit('load', img);
res('@imageLoaded');
});
});
} else if (this.format === 'bgm') {
this.request = new Promise(res => {
const audio = new Audio();
audio.src = data;
this.emit('loadstart', audio);
audio.addEventListener('load', () => {
this.resource = audio;
this.loaded = true;
this.emit('load', audio);
res('@bgmLoaded');
});
});
} else if (
this.format === 'json' ||
this.format === 'text' ||
this.format === 'arraybuffer'
) {
this.emit('loadstart');
this.request = axios
.get(data, {
responseType: this.format,
onDownloadProgress: e => {
this.emit('progress', e);
}
})
.then(v => {
this.resource = v.data;
this.loaded = true;
this.emit('load', v.data);
return v;
});
} else if (this.format === 'zip') {
this.emit('loadstart');
this.request = axios
.get(data, {
responseType: 'arraybuffer',
onDownloadProgress: e => {
this.emit('progress', e);
}
})
.then(v => {
this.resource = new ZippedResource(v.data);
this.loaded = true;
this.emit('load', this.resource);
return v;
});
}
}
/**
*
*/
async getData(): Promise<ResourceData[T] | null> {
if (!this.activated) return null;
if (this.loaded) return this.resource ?? null;
else {
if (!this.request) this.load();
await this.request;
return this.resource ?? null;
}
}
/**
*
* @param data
*/
protected setData(data: ResourceData[T] | Promise<ResourceData[T]>) {
if (data instanceof Promise) {
data.then(v => {
this.loaded = true;
this.resource = v;
this.emit('load', v);
});
} else {
this.loaded = true;
this.resource = data;
this.emit('load', data);
}
return this;
}
}
interface ZippedEvent extends EmitableEvent {
ready: (data: JSZip) => void;
}
export class ZippedResource extends EventEmitter<ZippedEvent> {
zip: Promise<JSZip>;
data?: JSZip;
constructor(buffer: ArrayBuffer) {
super();
this.zip = JSZip.loadAsync(buffer).then(v => {
this.emit('ready', v);
this.data = v;
return v;
});
}
}
export class ResourceStore<T extends ResourceType> extends Map<
string,
Resource<T>
> {
active(key: string[] | string) {
const keys = ensureArray(key);
keys.forEach(v => this.get(v)?.active());
}
dispose(key: string[] | string) {
const keys = ensureArray(key);
keys.forEach(v => this.get(v)?.dispose());
}
destroy(key: string[] | string) {
const keys = ensureArray(key);
keys.forEach(v => this.get(v)?.destroy());
}
push(data: [string, Resource<T>][] | Record<string, Resource<T>>): void {
if (data instanceof Array) {
for (const [key, res] of data) {
if (this.has(key)) {
console.warn(`Resource already exists: '${key}'.`);
}
this.set(key, res);
}
} else {
return this.push(Object.entries(data));
}
}
async getData<T extends ResourceType = ResourceType>(
key: string
): Promise<ResourceData[T] | null> {
return this.get(key)?.getData() ?? null;
}
getDataSync<T extends ResourceType = ResourceType>(
key: string
): ResourceData[T] | null {
return this.get(key)?.resource ?? null;
}
}
export function getTypeByResource(resource: string): ResourceType {
const type = resource.split('.')[0];
if (type === 'zip') return 'zip';
else if (type === 'bgms') return 'bgm';
else if (['images', 'autotiles', 'materials', 'tilesets'].includes(type)) {
return 'image';
} else if (['sounds', 'fonts'].includes(type)) return 'arraybuffer';
else if (type === 'animates') return 'json';
return 'arraybuffer';
}
export function getZipFormatByType(type: ResourceType): 'arraybuffer' | 'text' {
if (type === 'text' || type === 'json') return 'text';
else return 'arraybuffer';
}
export const resource = new ResourceStore();
export const zipResource = new ResourceStore();

View File

@ -32,7 +32,8 @@ fixedUi.register(
new GameUi('chapter', UI.Chapter),
new GameUi('completeAchi', UI.CompleteAchi),
new GameUi('start', UI.Start),
new GameUi('toolbar', UI.Toolbar)
new GameUi('toolbar', UI.Toolbar),
new GameUi('load', UI.Load)
);
fixedUi.showAll();
@ -72,15 +73,5 @@ hook.once('mounted', () => {
fixed.style.display = 'none';
});
if (loaded && !mounted) {
fixedUi.open('start');
}
mounted = true;
});
hook.once('load', () => {
if (mounted) {
// todo: 暂时先这么搞,之后重写加载界面,需要改成先显示加载界面,加载完毕后再打开这个界面
fixedUi.open('start');
}
loaded = true;
});

View File

@ -7,12 +7,6 @@ import type {
IndexedEventEmitter
} from '@/core/common/eventEmitter';
import type { loading } from './game';
import type {
Resource,
ResourceStore,
ResourceType,
ZippedResource
} from '@/core/loader/resource';
import type { Hotkey } from '@/core/main/custom/hotkey';
import type { Keyboard } from '@/core/main/custom/keyboard';
import type { CustomToolbar } from '@/core/main/custom/toolbar';
@ -38,9 +32,6 @@ interface ClassInterface {
GameStorage: typeof GameStorage;
MotaSetting: typeof MotaSetting;
SettingDisplayer: typeof SettingDisplayer;
Resource: typeof Resource;
ZippedResource: typeof ZippedResource;
ResourceStore: typeof ResourceStore;
Focus: typeof Focus;
GameUi: typeof GameUi;
UiController: typeof UiController;
@ -82,8 +73,6 @@ interface VariableInterface {
// isMobile: boolean;
bgm: BgmController;
sound: SoundController;
resource: ResourceStore<Exclude<ResourceType, 'zip'>>;
zipResource: ResourceStore<'zip'>;
settingStorage: GameStorage;
status: Ref<boolean>;
// 定义于游戏进程,渲染进程依然可用
@ -529,7 +518,8 @@ function r<T = undefined>(
fn: (this: T, packages: PackageInterface) => void,
thisArg?: T
) {
if (!main.replayChecking) fn.call(thisArg as T, MPackage.requireAll());
if (!main.replayChecking && main.mode === 'play')
fn.call(thisArg as T, MPackage.requireAll());
}
/**
@ -548,7 +538,7 @@ function rf<F extends (...params: any) => any, T>(
thisArg?: T
): (this: T, ...params: Parameters<F>) => ReturnType<F> | undefined {
// @ts-ignore
if (main.replayChecking) return () => {};
if (main.replayChecking || main.mode === 'editor') return () => {};
else {
return (...params) => {
return fn.call(thisArg, ...params);

View File

@ -386,3 +386,13 @@ export function getVitualKeyOnce(
});
});
}
export function formatSize(size: number) {
return size < 1 << 10
? `${size.toFixed(2)}B`
: size < 1 << 20
? `${(size / (1 << 10)).toFixed(2)}KB`
: size < 1 << 30
? `${(size / (1 << 20)).toFixed(2)}MB`
: `${(size / (1 << 30)).toFixed(2)}GB`;
}

4
src/types/core.d.ts vendored
View File

@ -98,6 +98,8 @@ type MaterialImages = {
*/
tilesets: Record<string, HTMLImageElement>;
keyboard: HTMLImageElement;
hero: HTMLImageElement;
};
@ -1081,6 +1083,8 @@ interface Core extends Pick<Main, CoreDataFromMain> {
_this: any,
...params: Parameters<F>
): ReturnType<F>;
_afterLoadResources(callback?: () => void): void;
}
type CoreMixin = Core &

View File

@ -1,13 +1,105 @@
<template>
<div id="load"></div>
<div id="load">
<a-progress
class="task-progress"
type="circle"
:percent="(loading / totalTask) * 100"
:success="{ percent: (loaded / totalTask) * 100 }"
>
<template #format>
<span>{{ loaded }} / {{ totalTask }}</span>
</template>
</a-progress>
<div class="byte-div">
<span class="byte-progress-tip"
>{{ formatSize(loadedByte) }} /
{{ formatSize(totalByte) }}</span
>
<a-progress
class="byte-progress"
type="line"
:percent="loadedPercent"
></a-progress>
</div>
</div>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { loadDefaultResource, LoadTask } from '@/core/common/resource';
import { GameUi } from '@/core/main/custom/ui';
import { formatSize } from '@/plugin/utils';
import { logger } from '@/core/common/logger';
import { fixedUi } from '@/core/main/init/ui';
import { sleep } from 'mutate-animate';
const props = defineProps<{
ui: GameUi;
num: number;
callback?: () => void;
}>();
const loading = ref(0);
const loaded = ref(0);
const loadedByte = ref(0);
const loadedPercent = ref(0);
const totalByte = ref(0);
const totalTask = ref(0);
let loadDiv: HTMLDivElement;
loadDefaultResource();
LoadTask.onProgress(() => {
const loadingNum = [...LoadTask.taskList].filter(v => v.loading).length;
loadedByte.value = LoadTask.loadedByte;
loadedPercent.value = parseFloat(
((LoadTask.loadedByte / LoadTask.totalByte) * 100).toFixed(2)
);
loading.value = loadingNum;
loaded.value = LoadTask.loadedTask;
totalByte.value = LoadTask.totalByte;
totalTask.value = LoadTask.totalTask;
});
LoadTask.load().then(async () => {
core.loader._loadMaterials_afterLoad();
core._afterLoadResources(props.callback);
logger.log(`Resource load end.`);
loadDiv.style.opacity = '0';
await sleep(1000);
fixedUi.close(props.num);
fixedUi.open('start');
});
onMounted(() => {
loadDiv = document.getElementById('load') as HTMLDivElement;
});
</script>
<style lang="less" scoped>
#load {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-family: 'Arial';
transition: opacity 1s linear;
position: fixed;
left: 0;
top: 0;
background-color: black;
}
.byte-div {
width: 50%;
margin-top: 10vh;
}
.byte-progress {
width: 100%;
}
</style>

View File

@ -310,9 +310,6 @@ onMounted(async () => {
start = document.getElementById('start') as HTMLDivElement;
background = document.getElementById('background') as HTMLImageElement;
const loading = Mota.require('var', 'loading');
loading.once('coreInit', async () => {
window.addEventListener('resize', resize);
resize();
@ -328,7 +325,6 @@ onMounted(async () => {
await sleep(1000);
showCursor();
await sleep(1200);
});
CustomToolbar.closeAll();
});