import { IAssetData, IAssetManager, IGraphicAsset, IGraphicAssetCollection } from '@nitrots/api'; import { NitroBundle, NitroLogger } from '@nitrots/utils'; import { AnimatedGIF } from '@pixi/gif'; import { Assets, Spritesheet, SpritesheetData, Texture } from 'pixi.js'; import { GraphicAssetCollection } from './GraphicAssetCollection'; export class AssetManager implements IAssetManager { private _textures: Map = new Map(); private _collections: Map = new Map(); private _missingAssetNames: Set = new Set(); public getTexture(name: string): Texture { if(!name) return null; return this._textures.get(name); } public setTexture(name: string, texture: Texture): void { if(!name || !texture) return; texture.label = name; this._textures.set(name, texture); } public getAsset(name: string): IGraphicAsset { if(!name) return null; if(this._missingAssetNames.has(name)) return null; for(const collection of this._collections.values()) { if(!collection) continue; const existing = collection.getAsset(name); if(!existing) continue; return existing; } this._missingAssetNames.add(name); return null; } public addAssetToCollection(collectionName: string, assetName: string, texture: Texture, override: boolean = true): boolean { const collection = this.getCollection(collectionName); if(!collection) return false; return collection.addAsset(assetName, texture, override, 0, 0, false, false); } public getCollection(name: string): IGraphicAssetCollection { if(!name) return null; return this._collections.get(name) ?? null; } public createCollection(data: IAssetData, spritesheet: Spritesheet): IGraphicAssetCollection { if(!data) return null; const collection = new GraphicAssetCollection(data, spritesheet); for(const [name, texture] of collection.textures.entries()) this.setTexture(name, texture); this._collections.set(collection.name, collection); if(this._missingAssetNames.size > 0) this._missingAssetNames.clear(); return collection; } public async downloadAssets(urls: string[]): Promise { if(!urls || !urls.length) return Promise.resolve(true); await Promise.all(urls.map(url => this.downloadAsset(url))); return true; } public async downloadAsset(url: string): Promise { try { if(!url || !url.length) return false; if(url.startsWith('local://')) { const key = url.substring('local://'.length); switch(key) { case 'room': await this.loadLocalRoom(); return true; case 'place_holder': case 'place_holder_wall': case 'place_holder_pet': case 'tile_cursor': case 'selection_arrow': case 'avatar_additions': case 'floor_editor': case 'group_badge': await this.loadLocalAsset(key); return true; } return false; } if(url.endsWith('.nitro') || url.endsWith('.gif')) { let response: Response; try { response = await fetch(url); } catch(fetchErr) { throw new Error(`Could not fetch "${ url }" — is the URL correct and the server reachable? (${ fetchErr.message })`); } if(!response || response.status !== 200) throw new Error(`Failed to load "${ url }" — server returned HTTP ${ response?.status ?? 'no response' }`); const arrayBuffer = await response.arrayBuffer(); if(url.endsWith('.nitro')) { const nitroBundle = await NitroBundle.from(arrayBuffer); await this.processAsset(nitroBundle.texture, nitroBundle.jsonFile as IAssetData); } else { try { const animatedGif = AnimatedGIF.fromBuffer(arrayBuffer); const texture = animatedGif.texture; if(texture) this.setTexture(url, texture); } catch(gifErr) { const texture = await Assets.load(url); if(texture) this.setTexture(url, texture); } } } else if(url.endsWith('.json')) { let response: Response; try { response = await fetch(url); } catch(fetchErr) { throw new Error(`Could not fetch "${ url }" — is the URL correct and the server reachable? (${ fetchErr.message })`); } if(!response || response.status !== 200) throw new Error(`Failed to load "${ url }" — server returned HTTP ${ response?.status ?? 'no response' }`); let data: IAssetData; try { data = await response.json() as IAssetData; } catch(parseErr) { throw new Error(`Invalid JSON in "${ url }" — the URL may be wrong and returning an HTML page instead of JSON (${ parseErr.message })`); } let texture: Texture = null; const imagePath = data?.spritesheet?.meta?.image; const fallbackImagePath = ((data?.name && data.name.length > 0) ? `${data.name}.png` : url.replace(/\.json$/i, '.png')); const resolvedImageUrl = (imagePath ? new URL(imagePath, url).toString() : new URL(fallbackImagePath, url).toString()); texture = await Assets.load(resolvedImageUrl); await this.processAsset(texture, data); } else { const texture = await Assets.load(url); if(texture) this.setTexture(url, texture); } return true; } catch (err) { throw new Error(`Asset loading failed for "${ url }": ${ err.message || err }`); } } private async loadLocalRoom(): Promise { const roomDataModule = await import('./assets/room/room.asset.json'); const roomData = (roomDataModule.default ?? roomDataModule) as IAssetData; const collection = this.createCollection(roomData, null) as GraphicAssetCollection; if(!collection) return; const roomImages = import.meta.glob('./assets/room/*.png', { eager: true }); const roomImagesSub = import.meta.glob('./assets/room/images/*.png', { eager: true }); const merged = { ...roomImages, ...roomImagesSub }; for(const path in merged) { const mod = merged[path]; const imageUrl = (mod.default ?? mod) as string; const file = path.split('/').pop()!; const rawName = file.replace(/\.png$/i, ''); const texture = await Assets.load(imageUrl); if(!texture) continue; this.setTexture(rawName, texture); collection.textures.set(rawName, texture); if(rawName.startsWith('room_')) { const normalizedName = rawName.substring('room_'.length); this.setTexture(normalizedName, texture); collection.textures.set(normalizedName, texture); } } collection.define(roomData); } private async loadLocalAsset(name: string): Promise { let dataModule: any; switch(name) { case 'place_holder': dataModule = await import('./assets/place_holder/place_holder.asset.json'); break; case 'place_holder_wall': dataModule = await import('./assets/place_holder_wall/place_holder_wall.asset.json'); break; case 'place_holder_pet': dataModule = await import('./assets/place_holder_pet/place_holder_pet.asset.json'); break; case 'tile_cursor': dataModule = await import('./assets/tile_cursor/tile_cursor.asset.json'); break; case 'selection_arrow': dataModule = await import('./assets/selection_arrow/selection_arrow.asset.json'); break; case 'avatar_additions': dataModule = await import('./assets/avatar_additions/avatar_additions.asset.json'); break; case 'floor_editor': dataModule = await import('./assets/floor_editor/floor_editor.asset.json'); break; case 'group_badge': dataModule = await import('./assets/group_badge/group_badge.asset.json'); break; default: return; } const data = (dataModule.default ?? dataModule) as IAssetData; const collection = this.createCollection(data, null) as GraphicAssetCollection; if(!collection) return; const allImages = import.meta.glob('./assets/*/images/*.png', { eager: true }); const prefix = `./assets/${name}/images/`; for(const path in allImages) { if(!path.startsWith(prefix)) continue; const mod = allImages[path]; const imageUrl = (mod.default ?? mod) as string; const file = path.split('/').pop()!; const rawName = file.replace(/\.png$/i, ''); const texture = await Assets.load(imageUrl); if(!texture) continue; this.setTexture(rawName, texture); collection.textures.set(name + '_' + rawName, texture); collection.textures.set(rawName, texture); } collection.define(data); } private async processAsset(texture: Texture, data: IAssetData): Promise { let spritesheet: Spritesheet = null; if(texture && data?.spritesheet && Object.keys(data.spritesheet).length) { spritesheet = new Spritesheet(texture, data.spritesheet); await spritesheet.parse(); spritesheet.textureSource.label = data.name ?? null; } return this.createCollection(data, spritesheet); } public get collections(): Map { return this._collections; } }