All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.littlekt.AssetProvider.kt Maven / Gradle / Ivy

There is a newer version: 0.11.0
Show newest version
package com.littlekt

import com.littlekt.async.KtScope
import com.littlekt.audio.AudioClip
import com.littlekt.audio.AudioStream
import com.littlekt.file.UnsupportedFileTypeException
import com.littlekt.file.ldtk.LDtkMapLoader
import com.littlekt.file.vfs.*
import com.littlekt.graphics.Pixmap
import com.littlekt.graphics.Texture
import com.littlekt.graphics.g2d.TextureAtlas
import com.littlekt.graphics.g2d.TextureSlice
import com.littlekt.graphics.g2d.font.BitmapFont
import com.littlekt.graphics.g2d.font.CharacterSets
import com.littlekt.graphics.g2d.font.TtfFont
import com.littlekt.graphics.g2d.tilemap.tiled.TiledMap
import com.littlekt.util.datastructure.fastForEach
import com.littlekt.util.internal.lock
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

/**
 * Provides helper functions to load and prepare assets without having to use `null` or `lateinit`.
 */
open class AssetProvider(val context: Context) {
    private val assetsToPrepare = arrayListOf>()
    private var totalAssetsLoading = atomic(0)
    private var totalAssets = atomic(0)
    private var totalAssetsFinished = atomic(0)
    private val filesBeingChecked = mutableListOf()
    private val _assets = mutableMapOf, MutableMap>>()
    private val lock = Any()

    /** The percentage of the total assets finished loading, using a value between `0f` and `1f`. */
    val percentage: Float
        get() =
            if (totalAssets.value == 0) 1f
            else totalAssetsFinished.value / totalAssets.value.toFloat()

    /** A map of loaders. Custom loaders may be added directly to this. */
    val loaders: MutableMap, suspend (VfsFile, GameAssetParameters) -> Any> =
        createDefaultLoaders().toMutableMap()

    /** A map of loaded assets. */
    val assets: Map, Map>>
        get() = _assets

    /** A lambda that is invoked once all assets have been loaded and prepared. */
    var onFullyLoaded: (() -> Unit)? = null

    /** Determines if it has been fully loaded. */
    val fullyLoaded: Boolean
        get() = totalAssetsLoading.value == 0 && assetsToPrepare.isEmpty()

    private var job: Job? = null

    /** Updates to check if all assets have been loaded, and if so, prepare them. */
    fun update() {
        if (totalAssetsLoading.value > 0) return
        if (job?.isActive != true && assetsToPrepare.isNotEmpty()) {
            val prepare = assetsToPrepare.toList()
            assetsToPrepare.clear()
            job =
                KtScope.launch {
                    prepare.fastForEach { it.prepare() }
                    onFullyLoaded?.invoke()
                }
        }
    }

    /**
     * Loads an asset asynchronously.
     *
     * @param T concrete class of [Any] instance that should be loaded.
     * @param file the file to load
     * @param parameters any parameters that need setting when loading the asset
     * @see BitmapFontAssetParameter
     * @see LDtkGameAssetParameter
     * @see TextureGameAssetParameter
     * @see TtfFileAssetParameter
     */
    fun  load(
        file: VfsFile,
        clazz: KClass,
        parameters: GameAssetParameters = EmptyGameAssetParameter(),
    ): GameAsset {
        val sceneAsset = checkOrCreateNewSceneAsset(file, clazz)
        context.vfs.launch { loadVfsFile(sceneAsset, file, clazz, parameters) }
        return sceneAsset
    }

    /**
     * Loads an asset in a suspending function.
     *
     * @param T concrete class of [Any] instance that should be loaded.
     * @param file the file to load
     * @param parameters any parameters that need setting when loading the asset
     * @see BitmapFontAssetParameter
     * @see LDtkGameAssetParameter
     * @see TextureGameAssetParameter
     * @see TtfFileAssetParameter
     */
    suspend fun  loadSuspending(
        file: VfsFile,
        clazz: KClass,
        parameters: GameAssetParameters = EmptyGameAssetParameter(),
    ): GameAsset {
        val sceneAsset = checkOrCreateNewSceneAsset(file, clazz)
        loadVfsFile(sceneAsset, file, clazz, parameters)
        return sceneAsset
    }

    @Suppress("UNCHECKED_CAST")
    private fun  checkOrCreateNewSceneAsset(
        file: VfsFile,
        clazz: KClass,
    ): GameAsset {
        val sceneAsset =
            _assets[clazz]?.get(file)?.let {
                return it as GameAsset
            } ?: GameAsset(file)

        if (filesBeingChecked.contains(file)) {
            throw IllegalStateException(
                "'${file.path}' has already been triggered to load but hasn't finished yet! Ensure `load()` hasn't been called twice for the same VfsFile"
            )
        }
        filesBeingChecked += file
        totalAssetsLoading.addAndGet(1)
        totalAssets.addAndGet(1)
        return sceneAsset
    }

    @Suppress("UNCHECKED_CAST")
    private suspend fun  loadVfsFile(
        sceneAsset: GameAsset,
        file: VfsFile,
        clazz: KClass,
        parameters: GameAssetParameters = EmptyGameAssetParameter(),
    ) {
        val loader = loaders[clazz] ?: throw UnsupportedFileTypeException(file.path)
        val result = loader.invoke(file, parameters) as T
        sceneAsset.load(result)
        lock(lock) {
            _assets.getOrPut(clazz) { mutableMapOf() }.let { it[file] = sceneAsset }
            filesBeingChecked -= file
        }
        totalAssetsFinished.addAndGet(1)
        totalAssetsLoading.addAndGet(-1)
    }

    /**
     * Loads an asset asynchronously.
     *
     * @param T concrete class of [Any] instance that should be loaded.
     * @param file the file to load
     * @param parameters any parameters that need setting when loading the asset
     * @see BitmapFontAssetParameter
     * @see LDtkGameAssetParameter
     * @see TextureGameAssetParameter
     * @see TtfFileAssetParameter
     */
    inline fun  load(
        file: VfsFile,
        parameters: GameAssetParameters = EmptyGameAssetParameter(),
    ) = load(file, T::class, parameters)

    /**
     * Loads an asset in a suspending function.
     *
     * @param T concrete class of [Any] instance that should be loaded.
     * @param file the file to load
     * @param parameters any parameters that need setting when loading the asset
     * @see BitmapFontAssetParameter
     * @see LDtkGameAssetParameter
     * @see TextureGameAssetParameter
     * @see TtfFileAssetParameter
     */
    suspend inline fun  loadSuspending(
        file: VfsFile,
        parameters: GameAssetParameters = EmptyGameAssetParameter(),
    ) = loadSuspending(file, T::class, parameters)

    /**
     * Prepares a value once assets have finished loading. This acts the same as [lazy] except this
     * will invoke the [action] once loading is finished to ensure everything is initialized before
     * the first frame.
     *
     * @param action the action to initialize this value
     * @see load
     */
    @OptIn(ExperimentalContracts::class)
    fun  prepare(action: suspend () -> T): PreparableGameAsset {
        contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) }
        return PreparableGameAsset(action).also { assetsToPrepare += it }
    }

    /** @return the cached content of the specified [vfsFile]. */
    @Suppress("UNCHECKED_CAST")
    fun  get(clazz: KClass, vfsFile: VfsFile) =
        assets[clazz]?.get(vfsFile)?.content as T

    /** @return the cached content of the specified [file]. */
    inline fun  get(
        file: VfsFile,
    ) = get(T::class, file)

    companion object {
        /** Creates a map of the default asset loaders. */
        fun createDefaultLoaders() =
            mapOf, suspend (VfsFile, GameAssetParameters) -> Any>(
                Texture::class to
                    { file, param ->
                        if (param is TextureGameAssetParameter) {
                            file.readTexture()
                        } else {
                            file.readTexture()
                        }
                    },
                Pixmap::class to { file, _ -> file.readPixmap() },
                AudioClip::class to { file, _ -> file.readAudioClip() },
                AudioStream::class to { file, _ -> file.readAudioStream() },
                TextureAtlas::class to { file, _ -> file.readAtlas() },
                TtfFont::class to
                    { file, params ->
                        if (params is TtfFileAssetParameter) {
                            file.readTtfFont(params.chars)
                        } else {
                            file.readTtfFont()
                        }
                    },
                BitmapFont::class to
                    { file, params ->
                        if (params is BitmapFontAssetParameter) {
                            file.readBitmapFont(params.preloadedTextures)
                        } else {
                            file.readBitmapFont()
                        }
                    },
                LDtkMapLoader::class to
                    { file, params ->
                        if (params is LDtkGameAssetParameter) {
                            file.readLDtkMapLoader(params.atlas, params.tilesetBorderThickness)
                        } else {
                            file.readLDtkMapLoader()
                        }
                    },
                TiledMap::class to { file, _ -> file.readTiledMap() }
            )
    }
}

/** An asset that wraps a [VfsFile] that caches the loaded content. */
class GameAsset(val vfsFile: VfsFile) {
    private var result: T? = null
    private var isLoaded = false

    /**
     * The cached content.
     *
     * @throws IllegalStateException if asset hasn't been loaded yet.
     */
    val content: T & Any
        get() =
            if (isLoaded) result!!
            else throw IllegalStateException("Asset not loaded yet! ${vfsFile.path}")

    /** @return [content] */
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T = content

    /**
     * Load this asset manually by setting the [content] directly. This marks the asset as loaded.
     */
    fun load(content: T) {
        result = content
        isLoaded = true
    }
}

/**
 * An asset that may be "prepared" by invoking the specified [action] which in turn returns the
 * content required. This is mainly used when calling [AssetProvider.load] on a [VfsFile] and
 * needing a specific piece of content from the result.
 */
class PreparableGameAsset(val action: suspend () -> T) {
    private var isPrepared = false
    private var result: T? = null

    /**
     * @return the [action] result, if [prepare] has been called, otherwise throws an error.
     * @throws IllegalStateException if asset hasn't been prepared.
     */
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        if (isPrepared) {
            return result!!
        } else {
            throw IllegalStateException("Asset not prepared yet!")
        }
    }

    /** Invokes [action] and is marked as prepared. */
    suspend fun prepare() {
        result = action.invoke()
        isPrepared = true
    }
}

/** Specific parameters for loading game assets. */
interface GameAssetParameters

/** An empty game asset parameter. */
class EmptyGameAssetParameter : GameAssetParameters

/** Parameters related to loading a [Texture]. */
class TextureGameAssetParameter : GameAssetParameters

/** Parameters related to loading a [LDtkMapLoader]. */
class LDtkGameAssetParameter(
    val atlas: TextureAtlas? = null,
    val tilesetBorderThickness: Int = 2,
) : GameAssetParameters

/**
 * Parameters related to loading a [TtfFont].
 *
 * @param chars chars to load a glyph for.
 * @see CharacterSets
 */
class TtfFileAssetParameter(
    val chars: String = CharacterSets.LATIN_ALL,
) : GameAssetParameters

/**
 * Parameters related to loading a [BitmapFont].
 *
 * @param preloadedTextures
 */
class BitmapFontAssetParameter(
    val preloadedTextures: List = listOf(),
) : GameAssetParameters




© 2015 - 2025 Weber Informatics LLC | Privacy Policy