import { IAssetManager, IAvatarEffectListener } from '@nitrots/api'; import { GetConfiguration } from '@nitrots/configuration'; import { AvatarRenderEffectLibraryEvent, GetEventDispatcher, NitroEvent, NitroEventType } from '@nitrots/events'; import { loadGamedata } from '@nitrots/utils'; import { AvatarStructure } from './AvatarStructure'; import { EffectAssetDownloadLibrary } from './EffectAssetDownloadLibrary'; export class EffectAssetDownloadManager { private _assets: IAssetManager; private _structure: AvatarStructure; private _missingMandatoryLibs: string[] = []; private _effectMap: Map = new Map(); private _effectListeners: Map = new Map(); private _incompleteEffects: Map = new Map(); private _currentDownloads: EffectAssetDownloadLibrary[] = []; private _libraryNames: string[] = []; private _libraryLoadedCallback: (event: AvatarRenderEffectLibraryEvent) => void = null; constructor(assets: IAssetManager, structure: AvatarStructure) { this._assets = assets; this._structure = structure; } public async init(): Promise { this._missingMandatoryLibs = GetConfiguration().getValue('avatar.mandatory.effect.libraries'); const url = GetConfiguration().getValue('avatar.effectmap.url'); if(!url || !url.length) throw new Error('Missing "avatar.effectmap.url" in config — add the effect map URL to your renderer-config.json'); let responseData: any; try { responseData = await loadGamedata(url); } catch(err) { throw new Error(`Could not load effect map from "${ url }" — check "avatar.effectmap.url" in renderer-config.json (${ err?.message || err })`); } this.processEffectMap(responseData.effects); // Store callback for cleanup this._libraryLoadedCallback = (event: AvatarRenderEffectLibraryEvent) => this.onLibraryLoaded(event); GetEventDispatcher().addEventListener(NitroEventType.AVATAR_EFFECT_DOWNLOADED, this._libraryLoadedCallback); await this.processMissingLibraries(); } public dispose(): void { if(this._libraryLoadedCallback) { GetEventDispatcher().removeEventListener(NitroEventType.AVATAR_EFFECT_DOWNLOADED, this._libraryLoadedCallback); this._libraryLoadedCallback = null; } this._effectMap.clear(); this._effectListeners.clear(); this._incompleteEffects.clear(); this._currentDownloads = []; } private processEffectMap(data: any): void { if(!data) return; const downloadUrl = GetConfiguration().getValue('avatar.asset.effect.url'); for(const effect of data) { if(!effect) continue; const id = (effect.id as string); const libraryName = (effect.lib as string); const revision = (effect.revision || ''); if(this._libraryNames.indexOf(libraryName) >= 0) continue; this._libraryNames.push(libraryName); const downloadLibrary = new EffectAssetDownloadLibrary(libraryName, revision, downloadUrl, this._assets); let existing = this._effectMap.get(id); if(!existing) existing = []; existing.push(downloadLibrary); this._effectMap.set(id, existing); } } private async processMissingLibraries(): Promise { const promises: Promise[] = []; this._missingMandatoryLibs.forEach(value => { const libraries = this._effectMap.get(value); if(libraries) for(const library of libraries) promises.push(library.downloadAsset()); }); this._missingMandatoryLibs = []; await Promise.all(promises); } private onLibraryLoaded(event: AvatarRenderEffectLibraryEvent): void { if(!event || !event.library) return; const loadedEffects: string[] = []; this._structure.registerAnimation(event.library.animation); for(const [id, libraries] of this._incompleteEffects.entries()) { let isReady = true; for(const library of libraries) { if(!library || library.isLoaded) continue; isReady = false; break; } if(isReady) { loadedEffects.push(id); const listeners = this._effectListeners.get(id); for(const listener of listeners) { if(!listener || listener.disposed) continue; listener.resetEffect(parseInt(id)); } this._effectListeners.delete(id); GetEventDispatcher().dispatchEvent(new NitroEvent(NitroEventType.AVATAR_EFFECT_LOADED)); } } for(const id of loadedEffects) this._incompleteEffects.delete(id); let index = 0; while(index < this._currentDownloads.length) { const download = this._currentDownloads[index]; if(download) { if(download.libraryName === event.library.libraryName) this._currentDownloads.splice(index, 1); } index++; } } public isAvatarEffectReady(effect: number): boolean { return !this.getAvatarEffectPendingLibraries(effect).length; } private getAvatarEffectPendingLibraries(id: number): EffectAssetDownloadLibrary[] { const pendingLibraries: EffectAssetDownloadLibrary[] = []; if(!this._structure) return pendingLibraries; const libraries = this._effectMap.get(id.toString()); if(libraries) { for(const library of libraries) { if(!library || library.isLoaded) continue; if(pendingLibraries.indexOf(library) === -1) pendingLibraries.push(library); } } return pendingLibraries; } public downloadAvatarEffect(id: number, listener: IAvatarEffectListener): void { const pendingLibraries = this.getAvatarEffectPendingLibraries(id); if(pendingLibraries && pendingLibraries.length) { if(listener && !listener.disposed) { let listeners = this._effectListeners.get(id.toString()); if(!listeners) listeners = []; listeners.push(listener); this._effectListeners.set(id.toString(), listeners); } this._incompleteEffects.set(id.toString(), pendingLibraries); for(const library of pendingLibraries) { if(!library) continue; library.downloadAsset(); } } else { if(listener && !listener.disposed) listener.resetEffect(id); } } }