package funkin; import flixel.graphics.FlxGraphic; import flixel.FlxG; import funkin.play.notes.notestyle.NoteStyle; import openfl.utils.AssetType; import openfl.Assets; import openfl.system.System; import openfl.media.Sound; import lime.app.Future; import lime.app.Promise; /** * Handles caching of textures and sounds for the game. * TODO: Remove this once Eric finishes the memory system. */ @:nullSafety class FunkinMemory { static var permanentCachedTextures:Map = []; static var currentCachedTextures:Map = []; static var previousCachedTextures:Map = []; // waow static var permanentCachedSounds:Map = []; static var currentCachedSounds:Map = []; static var previousCachedSounds:Map = []; static var purgeFilter:Array = ["/week", "/characters", "/charSelect", "/results"]; /** * Caches textures that are always required. */ public static inline function initialCache():Void { var allImages:Array = Assets.list(); for (file in allImages) { if (!(file.endsWith(".png") #if FEATURE_COMPRESSED_TEXTURES || file.endsWith(".astc") #end) || file.contains("chart-editor") || !file.contains("ui/")) { continue; } file = file.replace(" ", ""); // Handle stray spaces. if (file.contains("shared") || Assets.exists('shared:$file', AssetType.IMAGE)) { file = 'shared:$file'; } permanentCacheTexture(file); } permanentCacheTexture(Paths.image("healthBar")); permanentCacheTexture(Paths.image("menuDesat")); permanentCacheTexture(Paths.image("notes", "shared")); permanentCacheTexture(Paths.image("noteSplashes", "shared")); permanentCacheTexture(Paths.image("noteStrumline", "shared")); permanentCacheTexture(Paths.image("NOTE_hold_assets")); // dude permanentCacheTexture(Paths.image("fonts/bold", null)); permanentCacheTexture(Paths.image("fonts/default", null)); permanentCacheTexture(Paths.image("fonts/freeplay-clear", null)); var allSounds:Array = Assets.list(AssetType.SOUND); for (file in allSounds) { if (!file.endsWith(".ogg") || !file.contains("countdown/")) continue; file = file.replace(" ", ""); if (file.contains("shared") || Assets.exists('shared:$file', AssetType.SOUND)) { file = 'shared:$file'; } permanentCacheSound(file); } permanentCacheSound(Paths.sound("cancelMenu")); permanentCacheSound(Paths.sound("confirmMenu")); permanentCacheSound(Paths.sound("screenshot")); permanentCacheSound(Paths.sound("scrollMenu")); permanentCacheSound(Paths.sound("soundtray/Voldown")); permanentCacheSound(Paths.sound("soundtray/VolMAX")); permanentCacheSound(Paths.sound("soundtray/Volup")); permanentCacheSound(Paths.music("freakyMenu/freakyMenu")); permanentCacheSound(Paths.music("offsetsLoop/offsetsLoop")); permanentCacheSound(Paths.music("offsetsLoop/drumsLoop")); permanentCacheSound(Paths.sound("missnote1", "shared")); permanentCacheSound(Paths.sound("missnote2", "shared")); permanentCacheSound(Paths.sound("missnote3", "shared")); } /** * Clears the current texture and sound caches. */ public static inline function purgeCache(callGarbageCollector:Bool = false):Void { preparePurgeTextureCache(); purgeTextureCache(); preparePurgeSoundCache(); purgeSoundCache(); #if (cpp || neko || hl) if (callGarbageCollector) funkin.util.MemoryUtil.collect(true); #end } ///// TEXTURES ///// /** * Determine whether the texture with the given key is cached. * @param key The key of the texture to check. * @return Whether the texture is cached. */ public static function isTextureCached(key:String):Bool { return FlxG.bitmap.get(key) != null; } /** * Ensures a texture with the given key is cached. * @param key The key of the texture to cache. */ public static function cacheTexture(key:String):Void { if (currentCachedTextures.exists(key)) return; if (previousCachedTextures.exists(key)) { // Move the texture from the previous cache to the current cache. var graphic:Null = previousCachedTextures.get(key); previousCachedTextures.remove(key); if (graphic != null) currentCachedTextures.set(key, graphic); return; } var graphic:Null = FlxGraphic.fromAssetKey(key, false, null, true); if (graphic == null) { FlxG.log.warn('Failed to cache graphic: $key'); return; } trace('Successfully cached graphic: $key'); graphic.persist = true; currentCachedTextures.set(key, graphic); forceRender(graphic); } /** * Permanently caches a texture with the given key. * @param key The key of the texture to cache. */ static function permanentCacheTexture(key:String):Void { if (permanentCachedTextures.exists(key)) return; var graphic:Null = FlxGraphic.fromAssetKey(key, false, null, true); if (graphic == null) { FlxG.log.warn('Failed to cache graphic: $key'); return; } trace('Successfully cached graphic: $key'); graphic.persist = true; permanentCachedTextures.set(key, graphic); forceRender(graphic); currentCachedTextures = permanentCachedTextures; } /** * Forces the GPU to load and upload a FlxGraphic. */ private static function forceRender(graphic:FlxGraphic):Void { if (graphic == null) return; var bmp:Null = FlxG.bitmap.get(graphic.key); if (bmp != null && bmp.bitmap != null) var _:Int = bmp.bitmap.width; // Trigger // Draws sprite and actually caches it. var sprite = new flixel.FlxSprite(); sprite.loadGraphic(graphic); sprite.draw(); // Draw sprite and load it into game's memory. sprite.destroy(); } /** * Checks, if graphic with given path cached in memory. */ public static inline function isGraphicCached(path:String):Bool return permanentCachedTextures.exists(path) || currentCachedTextures.exists(path) || previousCachedTextures.exists(path); public static function getCachedGraphic(path:String):Null { if (permanentCachedTextures.exists(path)) return permanentCachedTextures.get(path); if (currentCachedTextures.exists(path)) return currentCachedTextures.get(path); if (previousCachedTextures.exists(path)) return previousCachedTextures.get(path); // just in case return null; } /** * Prepares the cache for purging unused textures. */ public inline static function preparePurgeTextureCache():Void { previousCachedTextures = currentCachedTextures; for (graphicKey in previousCachedTextures.keys()) { if (permanentCachedTextures.exists(graphicKey)) { previousCachedTextures.remove(graphicKey); } } currentCachedTextures = permanentCachedTextures; } /** * Purges unused textures from the cache. */ public static function purgeTextureCache():Void { for (graphicKey in previousCachedTextures.keys()) { if (permanentCachedTextures.exists(graphicKey)) { previousCachedTextures.remove(graphicKey); continue; } if (graphicKey.contains("fonts")) continue; var graphic:Null = previousCachedTextures.get(graphicKey); if (graphic != null) { FlxG.bitmap.remove(graphic); graphic.destroy(); previousCachedTextures.remove(graphicKey); Assets.cache.clear(graphicKey); } } @:privateAccess if (FlxG.bitmap._cache == null) { @:privateAccess FlxG.bitmap._cache = new Map(); } @:privateAccess for (key in FlxG.bitmap._cache.keys()) { var obj:Null = FlxG.bitmap.get(key); if (obj == null || obj.persist || permanentCachedTextures.exists(key) || key.contains("fonts")) { continue; } if (obj.useCount > 0) { for (purgeEntry in purgeFilter) { if (key.contains(purgeEntry)) { FlxG.bitmap.removeKey(key); obj.destroy(); } } } } } ///// NOTE STYLE ////// public static function cacheNoteStyle(style:NoteStyle):Void { // TODO: Texture paths should fall back to the default values. cacheTexture(Paths.image(style.getNoteAssetPath() ?? "note")); cacheTexture(style.getHoldNoteAssetPath() ?? "noteHold"); cacheTexture(Paths.image(style.getStrumlineAssetPath() ?? "strumline")); cacheTexture(Paths.image(style.getSplashAssetPath() ?? "noteSplash")); cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(LEFT) ?? "LEFT")); cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(RIGHT) ?? "RIGHT")); cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(UP) ?? "UP")); cacheTexture(Paths.image(style.getHoldCoverDirectionAssetPath(DOWN) ?? "DOWN")); // cacheTexture(Paths.image(style.buildCountdownSpritePath(THREE) ?? "THREE")); cacheTexture(Paths.image(style.buildCountdownSpritePath(TWO) ?? "TWO")); cacheTexture(Paths.image(style.buildCountdownSpritePath(ONE) ?? "ONE")); cacheTexture(Paths.image(style.buildCountdownSpritePath(GO) ?? "GO")); cacheSound(style.getCountdownSoundPath(THREE) ?? "THREE"); cacheSound(style.getCountdownSoundPath(TWO) ?? "TWO"); cacheSound(style.getCountdownSoundPath(ONE) ?? "ONE"); cacheSound(style.getCountdownSoundPath(GO) ?? "GO"); cacheTexture(Paths.image(style.buildJudgementSpritePath("sick") ?? 'sick')); cacheTexture(Paths.image(style.buildJudgementSpritePath("good") ?? 'good')); cacheTexture(Paths.image(style.buildJudgementSpritePath("bad") ?? 'bad')); cacheTexture(Paths.image(style.buildJudgementSpritePath("shit") ?? 'shit')); cacheTexture(Paths.image(style.buildComboNumSpritePath(0) ?? '0')); cacheTexture(Paths.image(style.buildComboNumSpritePath(1) ?? '1')); cacheTexture(Paths.image(style.buildComboNumSpritePath(2) ?? '2')); cacheTexture(Paths.image(style.buildComboNumSpritePath(3) ?? '3')); cacheTexture(Paths.image(style.buildComboNumSpritePath(4) ?? '4')); cacheTexture(Paths.image(style.buildComboNumSpritePath(5) ?? '5')); cacheTexture(Paths.image(style.buildComboNumSpritePath(6) ?? '6')); cacheTexture(Paths.image(style.buildComboNumSpritePath(7) ?? '7')); cacheTexture(Paths.image(style.buildComboNumSpritePath(8) ?? '8')); cacheTexture(Paths.image(style.buildComboNumSpritePath(9) ?? '9')); } ///// SOUND ////// public static function cacheSound(key:String):Void { if (currentCachedSounds.exists(key)) return; if (previousCachedSounds.exists(key)) { // Move the texture from the previous cache to the current cache. var sound:Null = previousCachedSounds.get(key); previousCachedSounds.remove(key); if (sound != null) currentCachedSounds.set(key, sound); return; } var sound:Null = Assets.getSound(key, true); if (sound == null) return; else currentCachedSounds.set(key, sound); } public static function permanentCacheSound(key:String):Void { if (permanentCachedSounds.exists(key)) return; var sound:Null = Assets.getSound(key, true); if (sound == null) return; else permanentCachedSounds.set(key, sound); if (sound != null) currentCachedSounds.set(key, sound); } public static function preparePurgeSoundCache():Void { previousCachedSounds = currentCachedSounds; for (key in previousCachedSounds.keys()) { if (permanentCachedSounds.exists(key)) { previousCachedSounds.remove(key); } } currentCachedSounds = permanentCachedSounds; } /** * Purges unused sounds from the cache. */ public static inline function purgeSoundCache():Void { for (key in previousCachedSounds.keys()) { if (permanentCachedSounds.exists(key)) { previousCachedSounds.remove(key); continue; } var sound:Null = previousCachedSounds.get(key); if (sound != null) { Assets.cache.removeSound(key); previousCachedSounds.remove(key); } } Assets.cache.clear("songs"); Assets.cache.clear("music"); // Felt lazy. var key = Paths.music("freakyMenu/freakyMenu"); var sound:Null = Assets.getSound(key, true); if (sound != null) { permanentCachedSounds.set(key, sound); currentCachedSounds.set(key, sound); } } ///// MISC ///// public static inline function clearFreeplay():Void { var keysToRemove:Array = []; @:privateAccess for (key in FlxG.bitmap._cache.keys()) { if (!key.contains("freeplay")) continue; if (permanentCachedTextures.exists(key) || key.contains("fonts")) continue; keysToRemove.push(key); } @:privateAccess for (key in keysToRemove) { trace('Cleaning up $key'); var obj:Null = FlxG.bitmap.get(key); if (obj != null) { obj.destroy(); } FlxG.bitmap.removeKey(key); if (currentCachedTextures.exists(key)) currentCachedTextures.remove(key); Assets.cache.clear(key); } preparePurgeSoundCache(); purgeSoundCache(); } public static inline function clearStickers():Void { var keysToRemove:Array = []; @:privateAccess for (key in FlxG.bitmap._cache.keys()) { if (!key.contains("stickers")) continue; if (permanentCachedTextures.exists(key) || key.contains("fonts")) continue; keysToRemove.push(key); } @:privateAccess for (key in keysToRemove) { trace('Cleaning up $key'); var obj:Null = FlxG.bitmap.get(key); if (obj != null) { obj.destroy(); } FlxG.bitmap.removeKey(key); if (currentCachedTextures.exists(key)) currentCachedTextures.remove(key); Assets.cache.clear(key); } } }