import { Texture } from "./rendering/texture" ; import { state } from "./state" ; import { mainRenderer } from "./ui/misc" ; const imageCacheElement = document . querySelector ( '#image-cache' ) ; /** Holds a directory structure. If the value is null, then the key is a file, otherwise the key is a directory and the value is another directory structure. */ export type DirectoryStructure = { [ name ] : null | DirectoryStructure ; } ; const MBP_REDIRECT_RULES = { 'assets/data_mbp/sound/gotgem.wav' : 'assets/data_mbp/sound/gotdiamond.wav' , 'assets/data_mbp/sound/gotallgems.wav' : 'assets/data_mbp/sound/gotalldiamonds.wav' , 'assets/data_mbp/sound/music/groovepolice' : 'assets/data_mbp/sound/music/groove police' , } ; /** Manages loading and caching of resources. */ export abstract class ResourceManager { static textureCache = new Map < string , Texture > ( ) ; static loadTexturePromises = new Map < string , Promise < Texture > > ( ) ; /** The structure in the assets/data directory. Used mainly to look up file extensions. */ static dataDirectoryStructure : DirectoryStructure = { } ; static dataMbpDirectoryStructure : DirectoryStructure = { } ; static loadResourcePromises = new Map < string , Promise < Blob > > ( ) ; static cachedResources = new Map < string , Blob > ( ) ; static loadImagePromises = new Map < string , Promise < HTMLImageElement > > ( ) ; static loadedImages = new Map < string , HTMLImageElement > ( ) ; static urlCache = new Map < Blob , string > ( ) ; static get mainDataPath ( ) { return ( state . modification === 'gold' ) ? './assets/data/' : './assets/data_mbp/' ; } static async init ( ) { let promiseMbg = this . loadResource ( './api/directory_structure' ) ; let promiseMbp = this . loadResource ( './api/directory_structure_mbp' ) ; let [ responseMbg , responseMbp ] = await Promise . all ( [ promiseMbg , promiseMbp ] ) ; this . dataDirectoryStructure = JSON . parse ( await this . readBlobAsText ( responseMbg ) ) ; this . dataMbpDirectoryStructure = JSON . parse ( await this . readBlobAsText ( responseMbp ) ) ; } /** Creates a Texture from the path, or returns the cached version. */ static getTexture ( path , prependPath = this . mainDataPath ) { let fullPath = prependPath + path ; let cached = this . textureCache . get ( fullPath ) ; if ( cached ) return Promise . resolve ( cached ) ; if ( this . loadTexturePromises . get ( fullPath ) ) return this . loadTexturePromises . get ( fullPath ) ; let promise = new Promise < Texture > ( async ( resolve ) => { let image = await this . loadImage ( fullPath ) ; let texture = new Texture ( image ) ; texture . getGLTexture ( mainRenderer ) ; this . textureCache . set ( fullPath , texture ) ; resolve ( texture ) ; } ) ; this . loadTexturePromises . set ( fullPath , promise ) ; return promise ; } static getTextureFromCache ( path , prependPath = this . mainDataPath ) { let fullPath = prependPath + path ; let cached = this . textureCache . get ( fullPath ) ; if ( cached ) return cached ; return null ; } /** Gets the full filenames (with extension) of the file located at the given path (without extension). */ static getFullNamesOf ( path , mbp = state . modification === 'platinum' ) { let parts = path . split ( '/' ) ; let current = mbp ? this . dataMbpDirectoryStructure : this . dataDirectoryStructure ; while ( parts . length ) { let part = parts . shift ( ) ; if ( parts . length === 0 ) { let results = [ ] ; for ( let name in current ) { if ( name . toLowerCase ( ) . startsWith ( part . toLowerCase ( ) ) && ( name . length === part . length || part . length === name . lastIndexOf ( '.' ) ) ) results . push ( name ) ; } return results ; } else { current = current [ part ] ; if ( ! current ) return [ ] ; } } } static redirectPath ( path ) { if ( state . modification !== 'gold' ) { for ( let key in MBP_REDIRECT_RULES ) { if ( path . includes ( key ) ) return path . replace ( key , MBP_REDIRECT_RULES [ key as keyof typeof MBP_REDIRECT_RULES ] ) ; } } return path ; } /** Loads a resource from a path. Retries until it worked. */ static loadResource ( path ) { path = this . redirectPath ( path ) ; let cached = this . cachedResources . get ( path ) ; if ( cached ) return Promise . resolve ( cached ) ; if ( this . loadResourcePromises . get ( path ) ) return this . loadResourcePromises . get ( path ) ; let promise = new Promise < Blob > ( ( resolve ) => { const attempt = async ( ) => { try { let response = await fetch ( path ) ; if ( ! response . ok ) { this . cachedResources . set ( path , null ) ; resolve ( null ) ; return ; } let blob = await response . blob ( ) ; this . cachedResources . set ( path , blob ) ; resolve ( blob ) ; } catch ( e ) { setTimeout ( attempt , 1000 ) ; } } ; attempt ( ) ; } ) ; this . loadResourcePromises . set ( path , promise ) ; return promise ; } /** Preloads an image at a given path. */ static loadImage ( path ) { if ( this . loadedImages . get ( path ) ) return Promise . resolve ( this . loadedImages . get ( path ) ) ; if ( this . loadImagePromises . get ( path ) ) return this . loadImagePromises . get ( path ) ; let promise = new Promise < HTMLImageElement > ( ( resolve ) => { let image = new Image ( ) ; image . src = path ; image . onload = ( ) => { imageCacheElement . appendChild ( image ) ; this . loadedImages . set ( path , image ) ; resolve ( image ) ; } ; } ) ; this . loadImagePromises . set ( path , promise ) ; return promise ; } static loadImages ( paths ) { return Promise . all ( paths . map ( ( path ) => this . loadImage ( path ) ) ) ; } static getImageFromCache ( path ) { return this . loadedImages . get ( path ) || null ; } static readBlobAsText ( blob , encoding ? ) { if ( blob . text ) return blob . text ( ) ; else return new Promise < string > ( ( resolve ) => { let reader = new FileReader ( ) ; reader . onload = ( e ) => resolve ( e . target . result as string ) ; reader . readAsText ( blob , encoding ) ; } ) ; } static async readBlobAsJson ( blob , encoding ? ) { let text = await this . readBlobAsText ( blob , encoding ) ; return JSON . parse ( text ) ; } static readBlobAsArrayBuffer ( blob ) { if ( blob . arrayBuffer ) return blob . arrayBuffer ( ) ; else return new Promise < ArrayBuffer > ( ( resolve ) => { let reader = new FileReader ( ) ; reader . onload = ( e ) => resolve ( e . target . result as ArrayBuffer ) ; reader . readAsArrayBuffer ( blob ) ; } ) ; } static readBlobAsDataUrl ( blob ) { return new Promise < string > ( ( resolve ) => { let reader = new FileReader ( ) ; reader . onload = ( e ) => resolve ( e . target . result as string ) ; reader . readAsDataURL ( blob ) ; } ) ; } /** Converts a blob to a temporary URL. Returns a cached URL whenever possible. */ static getUrlToBlob ( blob ) { if ( this . urlCache . get ( blob ) ) return this . urlCache . get ( blob ) ; let url = URL . createObjectURL ( blob ) ; this . urlCache . set ( blob , url ) ; return url ; } /** Fetches a URL until a response a received. */ static retryFetch ( input , init ? ) { return new Promise < Blob > ( resolve => { const attempt = async ( ) => { try { let response = await fetch ( input , init ) ; let blob = await response . blob ( ) ; resolve ( blob ) ; } catch ( e ) { setTimeout ( attempt , 1000 ) ; } } ; attempt ( ) ; } ) ; } }