You've already forked Nitro_Render_V3
mirror of
https://github.com/duckietm/Nitro_Render_V3.git
synced 2026-06-19 23:16:20 +00:00
ae9bc8bfce
Introduces loadGamedata(url, options?) and mergeGamedata(a, b) in @nitrots/utils. The loader transparently accepts: - a single-file URL (legacy) -> parsed as before - a directory URL ending with '/' -> tier-merged from core/custom/seasonal, each tier driven by its own manifest.json5 Merge rules: - arrays of objects sharing an id key (id, classname, name): merged by id, later layers overriding earlier ones - arrays without an id key: concatenated - plain objects: recursive merge per key - anything else: later value wins All gamedata consumers (FurnitureDataLoader, ProductDataLoader, EffectAssetDownloadManager, AvatarRenderManager actions+figuredata, LocalizationManager) are migrated to loadGamedata. Behaviour is unchanged for single-file URLs, so existing deployments need no config changes; opt-in to split mode by appending '/' to the URL once the layout is in place. README updated with the directory layout, merge table and programmatic usage example. The companion CLI splitter that produces the core/ tier from legacy files lives in the Nitro V3 client repo.
230 lines
6.9 KiB
TypeScript
230 lines
6.9 KiB
TypeScript
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<string, EffectAssetDownloadLibrary[]> = new Map();
|
|
private _effectListeners: Map<string, IAvatarEffectListener[]> = new Map();
|
|
private _incompleteEffects: Map<string, EffectAssetDownloadLibrary[]> = 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<void>
|
|
{
|
|
this._missingMandatoryLibs = GetConfiguration().getValue<string[]>('avatar.mandatory.effect.libraries');
|
|
|
|
const url = GetConfiguration().getValue<string>('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<string>('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<void>
|
|
{
|
|
const promises: Promise<void>[] = [];
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|