diff --git a/.github/CONTRIBUTORS.md b/.github/CONTRIBUTORS.md index 433316bd..d5c4161a 100644 --- a/.github/CONTRIBUTORS.md +++ b/.github/CONTRIBUTORS.md @@ -43,7 +43,7 @@ Project contributors listed chronologically. * Improved [app](../app) utilities. * [@Quillraven](https://github.com/Quillraven) * Author of the [Tiled](../tiled) module. - * Contributed [actors](../actors) and [ashley](../ashley) utilities. + * Contributed [actors](../actors), [ashley](../ashley) and [collections](../collections) utilities. * Wrote a complete [KTX tutorial](https://github.com/Quillraven/SimpleKtxGame/wiki) based on the original LibGDX introduction. * Author of the [preferences](../preferences) module. * Tested and reviewed the [assets async](../assets-async) module. diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ad10f8..f2f8de52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ _See also: [the official LibGDX changelog](https://github.com/libgdx/libgdx/blob/master/CHANGES)._ +#### 1.9.14-b1 + +- **[UPDATE]** Updated to LibGDX 1.9.14. +- **[UPDATE]** Updated to Kotlin 1.4.30. +- **[UPDATE]** Updated to VisUI 1.4.11. +- **[FEATURE]** (`ktx-app`) `clearScreen` now accepts additional `clearDepth` boolean parameter that controls whether +the `GL_DEPTH_BUFFER_BIT` is added to the mask. +- **[FEATURE]** (`ktx-assets-async`) Added `AssetStorageSnapshot` class that stores a copy of `AssetStorage` state +for debugging purposes. Supports formatted string output with `prettyFormat`. +- **[FEATURE]** (`ktx-assets-async`) `AssetStorage` now includes `takeSnapshot` and `takeSnapshotAsync` methods that +allow to copy and inspect the internal state of the storage for debugging purposes. +- **[FEATURE]** (`ktx-collections`) Added `getOrPut` extension function for LibGDX map collections including +`ObjectMap`, `IdentityMap`, `ArrayMap` and `IntMap`. + #### 1.9.13-b1 - **[UPDATE]** Updated to LibGDX 1.9.13. diff --git a/README.md b/README.md index e0a29242..cfb68bdc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![GitHub Build](https://github.com/libktx/ktx/workflows/build/badge.svg)](https://github.com/libktx/ktx/actions?query=workflow%3Abuild) -[![Kotlin](https://img.shields.io/badge/kotlin-1.4.21--2-orange.svg)](http://kotlinlang.org/) -[![LibGDX](https://img.shields.io/badge/libgdx-1.9.13-red.svg)](https://libgdx.badlogicgames.com/) +[![Kotlin](https://img.shields.io/badge/kotlin-1.4.30-orange.svg)](http://kotlinlang.org/) +[![LibGDX](https://img.shields.io/badge/libgdx-1.9.14-red.svg)](https://libgdx.com/) [![Maven Central](https://img.shields.io/maven-central/v/io.github.libktx/ktx-async.svg)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22io.github.libktx%22) [![KTX](.github/ktx-logo.png "KTX")](http://libktx.github.io) @@ -75,7 +75,7 @@ in your `build.gradle` file: ```Groovy ext { // Update this version to match the latest KTX release: - ktxVersion = '1.9.13-b1' + ktxVersion = '1.9.14-b1' } dependencies { @@ -129,7 +129,7 @@ repositories { ext { // Update this version to match the latest LibGDX release: - ktxVersion = '1.9.13-SNAPSHOT' + ktxVersion = '1.9.14-SNAPSHOT' } ``` @@ -150,7 +150,7 @@ Browse through the directories in the root folder to find out more about each li All public classes and functions are also documented with standard Kotlin _KDocs_. You can access the documentation by: -- Viewing the generated Dokka files hosted on the the [project website](https://libktx.github.io/ktx/). +- Viewing the generated Dokka files hosted on the the [project website](https://libktx.github.io/docs/). - Reading the sources directly. - Using the `doc` archive in [GitHub releases](https://github.com/libktx/ktx/releases) with generated Dokka files. diff --git a/app/src/main/kotlin/ktx/app/graphics.kt b/app/src/main/kotlin/ktx/app/graphics.kt index 34bd7294..184da92f 100644 --- a/app/src/main/kotlin/ktx/app/graphics.kt +++ b/app/src/main/kotlin/ktx/app/graphics.kt @@ -1,17 +1,18 @@ package ktx.app -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.graphics.GL20 +import com.badlogic.gdx.utils.ScreenUtils /** * Clears current screen with the selected color. Inlined to lower the total method count. Assumes alpha is 1f. + * Clears depth by default. * @param red red color value. * @param green green color value. * @param blue blue color value. * @param alpha color alpha value. Optional, defaults to 1f (non-transparent). + * @param clearDepth adds the GL_DEPTH_BUFFER_BIT mask if true. + * @see ScreenUtils.clear */ @Suppress("NOTHING_TO_INLINE") -inline fun clearScreen(red: Float, green: Float, blue: Float, alpha: Float = 1f) { - Gdx.gl.glClearColor(red, green, blue, alpha) - Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT or GL20.GL_DEPTH_BUFFER_BIT) +inline fun clearScreen(red: Float, green: Float, blue: Float, alpha: Float = 1f, clearDepth: Boolean = true) { + ScreenUtils.clear(red, green, blue, alpha, clearDepth) } diff --git a/app/src/test/kotlin/ktx/app/GraphicsTest.kt b/app/src/test/kotlin/ktx/app/GraphicsTest.kt index a39d3e9d..b4d85d78 100644 --- a/app/src/test/kotlin/ktx/app/GraphicsTest.kt +++ b/app/src/test/kotlin/ktx/app/GraphicsTest.kt @@ -21,7 +21,7 @@ class GraphicsTest { } @Test - fun `should clear with optional alpha`() { + fun `should clear screen with optional alpha`() { Gdx.gl = mock() clearScreen(0.25f, 0.5f, 0.75f) @@ -29,4 +29,14 @@ class GraphicsTest { verify(Gdx.gl).glClearColor(0.25f, 0.5f, 0.75f, 1f) verify(Gdx.gl).glClear(GL20.GL_COLOR_BUFFER_BIT or GL20.GL_DEPTH_BUFFER_BIT) } + + @Test + fun `should clear screen without the depth buffer`() { + Gdx.gl = mock() + + clearScreen(0.25f, 0.5f, 0.75f, alpha = 0.5f, clearDepth = false) + + verify(Gdx.gl).glClearColor(0.25f, 0.5f, 0.75f, 0.5f) + verify(Gdx.gl).glClear(GL20.GL_COLOR_BUFFER_BIT) + } } diff --git a/assets-async/README.md b/assets-async/README.md index be805414..f641b749 100644 --- a/assets-async/README.md +++ b/assets-async/README.md @@ -338,7 +338,7 @@ fun loadAssets() { val assetStorage = AssetStorage(asyncContext = newAsyncContext(threads = 2)) // Instead of using Kotlin's built-in `async`, you can also use - // the `loadAsync` method of AssetStorage with is a shortcut for + // the `loadAsync` method of AssetStorage which is a shortcut for // `async(assetStorage.asyncContext) { assetStorage.load }`: val texture = assetStorage.loadAsync("images/logo.png") val font = assetStorage.loadAsync("fonts/font.fnt") @@ -589,7 +589,7 @@ fun createCustomAssetStorage(): AssetStorage { It is completely safe to call `load` and `loadAsync` multiple times with the same asset data, even just to obtain asset instances. In that sense, they can be used as an alternative to `getAsync` inside coroutines. -Instead of loading the same asset multiple times, `AssetStorage` will just increase the reference count +Instead of loading the same asset multiple times, `AssetStorage` will just increase the count of references to the asset and return the same instance on each request. This also works concurrently - the storage will always load just _one_ asset instance, regardless of how many different threads and coroutines called `load` in parallel. @@ -883,8 +883,8 @@ Closest equivalents in `AssetManager` and `AssetStorage` APIs: `AssetManager` | `AssetStorage` | Note :---: | :---: | --- -`get(String)` | `get(String)` | -`get(String, Class)` | `get(Identifier)` | +`get(String)` | `get(String)` | `AssetStorage` uses reified types to prevent from runtime class cast exceptions. +`get(String, Class)` | `get(Identifier)` | `Identifier` pairs file path and asset type to identify an asset. `get(AssetDescriptor)` | `get(AssetDescriptor)` | `load(String, Class)` | `loadAsync(String)` | `load(String)` can also be used as an alternative within coroutines. `load(String, Class, AssetLoaderParameters)` | `loadAsync(String, AssetLoaderParameters)` | `load(String, AssetLoaderParameters)` can also be used as an alternative within coroutines. @@ -901,6 +901,7 @@ Closest equivalents in `AssetManager` and `AssetStorage` APIs: `finishLoading()` | N/A | `AssetStorage` does not provide methods that block the thread until all assets are loaded. Rely on `progress.isFinished` instead. `addAsset(String, Class, T)` | `add(String, T)` | `contains(String)` | `contains(String)`, `contains(Identifier)` | `AssetStorage` requires asset type, so the methods are generic. +`getDiagnostics` | `takeSnapshot`, `takeSnapshotAsync` | Returns a copy of the internal state. Returned `AssetStorageSnapshot` instance provides a `prettyPrint` method with formatted output. `setErrorHandler` | N/A, `try-catch` | With `AssetStorage` you can handle loading errors immediately with regular built-in `try-catch` syntax. Error listener is not required. `clear()` | `dispose()` | `AssetStorage.dispose` will not kill `AssetStorage` threads and can be safely used multiple times like `AssetManager.clear`. `dispose()` | `dispose()` | `AssetStorage` also provides a suspending variant with custom error handling. @@ -908,8 +909,8 @@ Closest equivalents in `AssetManager` and `AssetStorage` APIs: ##### Integration with LibGDX and known unsupported features `AssetStorage` does its best to integrate with LibGDX APIs - including the `AssetLoader` implementations, which were -designed for the `AssetManager`. [A dedicated wrapper](src/main/kotlin/ktx/assets/async/wrapper.kt) extends and -overrides `AssetManager`, delegating a subset of supported methods to `AssetStorage`. The official `AssetLoader` +designed for the `AssetManager`. [A dedicated wrapper](src/main/kotlin/ktx/assets/async/AssetManagerWrapper.kt) extends +and overrides `AssetManager`, delegating a subset of supported methods to `AssetStorage`. The official `AssetLoader` implementations use supported methods such as `get`, but please note that some third-party loaders might not work out of the box with `AssetStorage`. Exceptions related to broken loaders include `UnsupportedMethodException` and `MissingDependencyException`. diff --git a/assets-async/src/main/kotlin/ktx/assets/async/storage.kt b/assets-async/src/main/kotlin/ktx/assets/async/storage.kt index 4f2dafa2..2ec52b21 100644 --- a/assets-async/src/main/kotlin/ktx/assets/async/storage.kt +++ b/assets-async/src/main/kotlin/ktx/assets/async/storage.kt @@ -1194,6 +1194,58 @@ class AssetStorage( return dependencies?.map { it.identifier } ?: emptyList() } + /** + * Creates a deep copy of the internal assets storage. Returns an [AssetStorageSnapshot] + * with the current storage state. For debugging purposes. + * + * Note that the [CompletableDeferred] that store references to assets are preserved only + * when completed, otherwise new instances of [CompletableDeferred] are returned. Even if + * the [CompletableDeferred] instances are completed manually, they will not affect the + * internal state of the storage. + */ + suspend fun takeSnapshotAsync(): AssetStorageSnapshot { + lock.withLock { + // Creating a safe copy of all assets without unfinished internal completable deferred instances: + val assetsCopy = assets.mapValues { + @Suppress("UNCHECKED_CAST") val asset: Asset = it.value as Asset + val reference: CompletableDeferred = if (asset.reference.isCompleted || asset.reference.isCancelled) { + asset.reference + } else { + CompletableDeferred() + } + asset.copy(reference = reference) + } + // Replacing dependencies with safe copies: + return AssetStorageSnapshot( + assets = assetsCopy.mapValues { + val asset = it.value + if (asset.dependencies.isEmpty()) { + asset + } else { + asset.copy(dependencies = asset.dependencies.mapNotNull { dependency -> + assetsCopy[dependency.identifier] + }) + } + } + ) + } + } + + /** + * Creates a deep copy of the internal assets storage. Returns an [AssetStorageSnapshot] + * with the current storage state. Blocks the current thread until the snapshot is complete. + * If assets are currently being loaded, avoid calling this method from within the rendering + * thread. For debugging purposes. + * + * Note that the [CompletableDeferred] that store references to assets are preserved only + * when completed, otherwise new instances of [CompletableDeferred] are returned. Even if + * the [CompletableDeferred] instances are completed manually, they will not affect the + * internal state of the storage. + */ + fun takeSnapshot(): AssetStorageSnapshot { + return runBlocking { takeSnapshotAsync() } + } + /** * Unloads all assets. Blocks current thread until are assets are unloaded. * Logs all disposing exceptions. @@ -1246,14 +1298,14 @@ class AssetStorage( } override fun toString(): String = "AssetStorage(assets=${ - assets.keys.sortedBy { it.path }.joinToString(separator = ", ", prefix = "[", postfix = "]") + assets.keys.sortedBy { it.path }.joinToString(separator = ", ", prefix = "[", postfix = "]") })" } /** * Container for a single asset of type [T] managed by [AssetStorage]. */ -internal data class Asset( +data class Asset( /** Stores asset loading data. */ val descriptor: AssetDescriptor, /** Unique identifier of the asset. */ @@ -1322,3 +1374,36 @@ data class Identifier( * is required, use [AssetDescriptor] for loading instead. */ fun AssetDescriptor.toIdentifier(): Identifier = Identifier(fileName, type) + +/** + * Stores a copy of state of an [AssetStorage]. For debugging purposes. + */ +data class AssetStorageSnapshot( + val assets: Map, Asset<*>> +) { + /** + * Prints [AssetStorage] state for debugging. Lists registered assets with their dependencies + * and reference counts. + */ + fun prettyPrint(): String { + return """[ +${ + assets.values + .sortedBy { it.identifier.type.name } + .sortedBy { it.identifier.path } + .joinToString(separator = "\n") { + """ "${it.identifier.path}" (${it.identifier.type.name}) { + references=${it.referenceCount}, + dependencies=${ + it.dependencies.joinToString(separator = ", ", prefix = "[", postfix = "]") { dependency -> + "\"${dependency.identifier.path}\" (${dependency.identifier.type.name})" + } + }, + loaded=${it.reference.isCompleted || it.reference.isCancelled}, + loader=${it.loader.javaClass.name}, + },""" + } + } +]""" + } +} diff --git a/assets-async/src/test/kotlin/ktx/assets/async/AssetStorageTest.kt b/assets-async/src/test/kotlin/ktx/assets/async/AssetStorageTest.kt index 86f10757..daec372f 100644 --- a/assets-async/src/test/kotlin/ktx/assets/async/AssetStorageTest.kt +++ b/assets-async/src/test/kotlin/ktx/assets/async/AssetStorageTest.kt @@ -1513,7 +1513,7 @@ class AssetStorageTest : AsyncTest() { /** For loaders testing. */ open class FakeSyncLoader( - private val onLoad: (asset: FakeAsset) -> Unit, + private val onLoad: (asset: FakeAsset) -> Unit = {}, private val dependencies: GdxArray> = GdxArray.with() ) : SynchronousAssetLoader(ClasspathFileHandleResolver()) { @Suppress("UNCHECKED_CAST") @@ -2165,4 +2165,130 @@ class AssetStorageTest : AsyncTest() { // Should still count the reference, since load was called: assertEquals(2, storage.getReferenceCount(dependency)) } + + @Test + fun `should return empty snapshot of the storage state`() { + // Given: + val storage = AssetStorage(useDefaultLoaders = false) + + // When: + val snapshot = storage.takeSnapshot() + + // Then: + assertEquals(AssetStorageSnapshot(mapOf()), snapshot) + } + + @Test + fun `should return empty snapshot of the storage state asynchronously`() { + // Given: + val storage = AssetStorage(useDefaultLoaders = false) + + // When: + val snapshot = runBlocking { storage.takeSnapshotAsync() } + + // Then: + assertEquals(AssetStorageSnapshot(mapOf()), snapshot) + } + + @Test + fun `should take snapshot of the storage state`() { + // Given: + val storage = AssetStorage(useDefaultLoaders = false) + val path = "fake path" + val id = storage.getIdentifier(path) + val loadingStarted = CompletableFuture() + val loadingFinished = CompletableFuture() + storage.setLoader { + FakeSyncLoader( + onLoad = { + loadingStarted.complete(true) + loadingFinished.join() + } + ) + } + val reference = storage.loadAsync(path) + loadingStarted.join() + + // When: asset is not loaded yet: + var snapshot = storage.takeSnapshot() + + // Then: + assertFalse(storage.isLoaded(path)) + @Suppress("UNCHECKED_CAST") val asset: Asset = snapshot.assets[id] as Asset + assertEquals(id, asset.identifier) + assertFalse(asset.reference.isCompleted) + assertFalse(asset.reference.isCancelled) + assertEquals(1, asset.referenceCount) + assertEquals(listOf>(), asset.dependencies) + + // When: modifying asset loading manually: + asset.reference.complete(FakeAsset()) + asset.referenceCount = 10 + + // Then: internal state should be unmodified: + assertFalse(storage.isLoaded(path)) + assertEquals(1, storage.getReferenceCount(id)) + + // When: asset is loaded: + loadingFinished.complete(true) + val instance = runBlocking { reference.await() } + snapshot = storage.takeSnapshot() + + // Then: + assertTrue(storage.isLoaded(path)) + val loadedAsset = snapshot.assets[id]!! + assertEquals(id, loadedAsset.identifier) + assertTrue(loadedAsset.reference.isCompleted) + assertSame(instance, runBlocking { loadedAsset.reference.await() }) + assertEquals(1, loadedAsset.referenceCount) + assertEquals(listOf>(), loadedAsset.dependencies) + } + + @Test + fun `should pretty print snapshot`() { + // Given: + val firstId = Identifier("first.file", String::class.java) + @Suppress("UNCHECKED_CAST") val first = Asset( + descriptor = firstId.toAssetDescriptor(), + identifier = firstId, + reference = CompletableDeferred("test"), + dependencies = listOf(), + loader = FakeSyncLoader() as Loader, + referenceCount = 2 + ) + val secondId = Identifier("second.file", Int::class.java) + @Suppress("UNCHECKED_CAST") val second = Asset( + descriptor = secondId.toAssetDescriptor(), + identifier = secondId, + reference = CompletableDeferred(), + dependencies = listOf(first), + loader = FakeSyncLoader() as Loader, + referenceCount = 1 + ) + val snapshot = AssetStorageSnapshot( + assets = mapOf( + firstId to first, + secondId to second + ) + ) + + // When: + val output = snapshot.prettyPrint() + + // Then: + assertEquals("""[ + "first.file" (java.lang.String) { + references=2, + dependencies=[], + loaded=true, + loader=ktx.assets.async.AssetStorageTest${"$"}FakeSyncLoader, + }, + "second.file" (int) { + references=1, + dependencies=["first.file" (java.lang.String)], + loaded=false, + loader=ktx.assets.async.AssetStorageTest${"$"}FakeSyncLoader, + }, +]""", output) + } } diff --git a/buildSrc/src/main/kotlin/ktx/Versions.kt b/buildSrc/src/main/kotlin/ktx/Versions.kt index 5b35f89f..3cfbeb1a 100644 --- a/buildSrc/src/main/kotlin/ktx/Versions.kt +++ b/buildSrc/src/main/kotlin/ktx/Versions.kt @@ -1,10 +1,10 @@ package ktx -const val gdxVersion = "1.9.13" +const val gdxVersion = "1.9.14" const val kotlinCoroutinesVersion = "1.4.2" const val ashleyVersion = "1.7.3" -const val visUiVersion = "1.4.8" +const val visUiVersion = "1.4.11" const val spekVersion = "1.2.1" const val kotlinTestVersion = "2.0.7" diff --git a/collections/README.md b/collections/README.md index 2244b2e5..09e83f02 100644 --- a/collections/README.md +++ b/collections/README.md @@ -96,6 +96,8 @@ has to be provided - since the method is inlined, no new lambda object will be c - `GdxArrayMap`: `com.badlogic.gdx.utils.ArrayMap` - All LibGDX map entries now feature `component1()` and `component2()` operator extension methods, so they can be destructed into a key and a value. +- `getOrPut` for `ObjectMap`, `IdentityMap`, `ArrayMap` and `IntMap` method to get an existing value to a given key +or if it does not exist, create a default value, add it to the map and return it. #### Note diff --git a/collections/src/main/kotlin/ktx/collections/maps.kt b/collections/src/main/kotlin/ktx/collections/maps.kt index 916eba56..596ec879 100644 --- a/collections/src/main/kotlin/ktx/collections/maps.kt +++ b/collections/src/main/kotlin/ktx/collections/maps.kt @@ -422,3 +422,67 @@ inline fun > GdxMap.flatten(): inline fun GdxMap.flatMap(transform: (Entry) -> Iterable): GdxArray { return this.map(transform).flatten() } + +/** + * Returns the value for the given [key]. If the [key] is not found in the map, + * calls the [defaultValue] function, puts its result into the map under the given [key] and returns it. + * + * Throws an [IllegalArgumentException][java.lang.IllegalArgumentException] when key is null. + */ +inline fun GdxMap.getOrPut(key: Key, defaultValue: () -> Value): Value { + var value = this[key] + + if (value == null && key !in this) { + value = defaultValue() + this[key] = value + } + + return value +} + +/** + * Returns the value for the given [key]. If the [key] is not found in the map, + * calls the [defaultValue] function, puts its result into the map under the given [key] and returns it. + * + * Throws an [IllegalArgumentException][java.lang.IllegalArgumentException] when key is null. + */ +inline fun GdxIdentityMap.getOrPut(key: Key, defaultValue: () -> Value): Value { + var value = this[key] + + if (value == null && key !in this) { + value = defaultValue() + this[key] = value + } + + return value +} + +/** + * Returns the value for the given [key]. If the [key] is not found in the map, + * calls the [defaultValue] function, puts its result into the map under the given [key] and returns it. + */ +inline fun GdxArrayMap.getOrPut(key: Key, defaultValue: () -> Value): Value { + var value = this[key] + + if (value == null && !this.containsKey(key)) { + value = defaultValue() + this[key] = value + } + + return value +} + +/** + * Returns the value for the given [key]. If the [key] is not found in the map, + * calls the [defaultValue] function, puts its result into the map under the given [key] and returns it. + */ +inline fun IntMap.getOrPut(key: Int, defaultValue: () -> Value): Value { + var value = this[key] + + if (value == null && key !in this) { + value = defaultValue() + this[key] = value + } + + return value +} diff --git a/collections/src/test/kotlin/ktx/collections/MapsTest.kt b/collections/src/test/kotlin/ktx/collections/MapsTest.kt index 716111d3..8f8d5ff8 100644 --- a/collections/src/test/kotlin/ktx/collections/MapsTest.kt +++ b/collections/src/test/kotlin/ktx/collections/MapsTest.kt @@ -9,10 +9,13 @@ import com.badlogic.gdx.utils.LongMap import com.badlogic.gdx.utils.ObjectIntMap import com.badlogic.gdx.utils.ObjectMap import com.badlogic.gdx.utils.ObjectSet +import io.kotlintest.matchers.shouldThrow +import java.lang.IllegalArgumentException import java.util.LinkedList import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test @@ -393,4 +396,160 @@ class MapsTest { assertEquals("Three", gdxArrayMap[3]) assertEquals("Four", gdxArrayMap[4]) } + + @Test + fun `should return existing value for GdxMap when key exists`() { + val map = gdxMapOf("42" to 42) + + val actual = map.getOrPut("42") { 43 } + + assertEquals(42, actual) + assertEquals(42, map["42"]) + } + + @Test + fun `should return and put default value to GdxMap when key does not exist`() { + val map = gdxMapOf() + + val actual = map.getOrPut("42") { 43 } + + assertEquals(43, actual) + assertTrue("42" in map) + assertEquals(43, map["42"]) + } + + @Test + fun `should return null for GdxMap when null is stored for given key`() { + val map = gdxMapOf("42" to null) + + val actual = map.getOrPut("42") { 43 } + + assertNull(actual) + assertEquals(null, map["42"]) + } + + @Test + fun `should throw an IllegalArgumentException when getOrPut is called with null key for GdxMap`() { + val map = gdxMapOf() + + shouldThrow { + map.getOrPut(null) { "42" } + } + } + + @Test + fun `should return existing value for GdxIdentityMap when key exists`() { + val map = gdxIdentityMapOf("42" to 42) + + val actual = map.getOrPut("42") { 43 } + + assertEquals(42, actual) + assertEquals(42, map["42"]) + } + + @Test + fun `should return and put default value to GdxIdentityMap when key does not exist`() { + val map = gdxIdentityMapOf() + + val actual = map.getOrPut("42") { 43 } + + assertEquals(43, actual) + assertTrue("42" in map) + assertEquals(43, map["42"]) + } + + @Test + fun `should return null for GdxIdentityMap when null is stored for given key`() { + val map = gdxIdentityMapOf("42" to null) + + val actual = map.getOrPut("42") { 43 } + + assertNull(actual) + assertEquals(null, map["42"]) + } + + @Test + fun `should throw an IllegalArgumentException when getOrPut is called with null key for GdxIdentityMap`() { + val map = gdxIdentityMapOf() + + shouldThrow { + map.getOrPut(null) { "42" } + } + } + + @Test + fun `should return existing value for GdxArrayMap when key exists`() { + val map = GdxArrayMap() + map["42"] = 42 + + val actual = map.getOrPut("42") { 43 } + + assertEquals(42, actual) + assertEquals(42, map["42"]) + } + + @Test + fun `should return and put default value to GdxArrayMap when key does not exist`() { + val map = GdxArrayMap() + + val actual = map.getOrPut("42") { 43 } + + assertEquals(43, actual) + assertTrue(map.containsKey("42")) + assertEquals(43, map["42"]) + } + + @Test + fun `should return null for GdxArrayMap when null is stored for given key`() { + val map = GdxArrayMap() + map["42"] = null + + val actual = map.getOrPut("42") { 43 } + + assertNull(actual) + assertEquals(null, map["42"]) + } + + @Test + fun `should return and put default value to GdxArrayMap when key is null`() { + val map = GdxArrayMap() + + val actual = map.getOrPut(null) { "42" } + + assertEquals("42", actual) + assertEquals("42", map[null]) + } + + @Test + fun `should return existing value for IntMap when key exists`() { + val map = IntMap() + map[42] = "42" + + val actual = map.getOrPut(42) { "43" } + + assertEquals("42", actual) + assertEquals("42", map[42]) + } + + @Test + fun `should return and put default value to IntMap when key does not exist`() { + val map = IntMap() + + val actual = map.getOrPut(42) { "43" } + + assertEquals("43", actual) + assertTrue(42 in map) + assertEquals("43", map[42]) + } + + @Test + fun `should return null for IntMap when null is stored for given key`() { + val map = IntMap() + map.put(42, null) + + val actual = map.getOrPut(42) { "43" } + + assertNull(actual) + assertEquals(null, map[42]) + } } diff --git a/gradle.properties b/gradle.properties index 4a849460..d7bceecd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ libGroup=io.github.libktx -kotlinVersion=1.4.21-2 +kotlinVersion=1.4.30 junitPlatformVersion=1.2.0 dokkaVersion=1.4.10.2 diff --git a/inject/README.md b/inject/README.md index 7c87b2a4..6725db12 100644 --- a/inject/README.md +++ b/inject/README.md @@ -122,7 +122,7 @@ class ClassWithLazyInjectedValue(context: Context) { Removing a registered provider: ```Kotlin context.remove() -// Note that this method work for both singletons and providers. +// Note that this method works for both singletons and providers. ``` Removing all components from the `Context`: @@ -175,6 +175,9 @@ class Container: Disposable { This will ensure that the `Context` itself will not attempt to dispose of the `Container`. +Note that this also applies to extensions of `KtxApplicationAdapter` and `KtxGame`, both of which +are `Disposable`. + ### Notes on implementation and design choices > How does it work? @@ -185,40 +188,38 @@ framework to extract the actual `Class` object from generic argument - and used Singletons are implemented as providers that always return the same instance of the selected type on each call. It _is_ dead simple and aims to introduce as little runtime overhead as possible. -> No scopes? Huh? +> Are scopes supported? -How often do you need these in simple games, anyway? More complex projects might benefit from features of mature -projects like [Koin](https://insert-koin.io/), but in most simple games you just end up needing some glue between -the components. Sometimes simplicity is something you aim for. +No. More complex projects might benefit from features of mature projects like [Koin](https://insert-koin.io/), +but in most simple games you just end up needing some glue between the components. Sometimes simplicity is something +you aim for. -As for testing scope, it should be obvious that you can just register different components during testing. Don't worry, -classes using `ktx-inject` are usually trivial to test. +As for testing scope, you can just register different components during testing. Classes using `ktx-inject` are usually +easy to test. -> Not even any named providers? +> Are named providers supported? Nope. Providers are mapped to the class of instances that they return - and that's it. Criteria systems - which are a sensible alternative to simple string names - are somewhat easy to use when your system is based on annotations, but we don't have much to work with when the goal is simplicity. -> Kodein-style single-parameter factories, anyone? +> What about Kodein-style single-parameter factories? It seems that Kodein keeps all its "providers" as single-parameter functions. To avoid wrapping all no-arg providers (which seem to be the most common by far) in `null`-consuming functions, factories are not implemented in `ktx-inject` -at all. Honestly, it's hard to get it right - single-parameter factories might not be enough in many situations and -type-safe multi-argument factories might look _really_ awkward_ in code thanks to a ton of generics. If you need -specialized providers, just create a simple class with `invoke` operator. +at all. If you need specialized providers, create a simple class with `invoke` operator. > Is this framework for me? This dependency injection system is as trivial as it gets. It will help you with platform-specific classes and gluing -your application together, but don't expect wonders. This library might be great if you're just starting with dependency -injection - all you need to learn is using a few simple functions. It's also hard to imagine a more lightweight -solution: getting a provider is a single call to a map. +your application together, but don't expect much more. This library might be great if you're just starting with +dependency injection - all you need to learn is using a few simple functions. It is also very lightweight: getting +a provider is a single call to a map. If you never end up needing more features, you might consider sticking with `ktx-inject` altogether, but just so you -know - there _are_ other Kotlin dependency injection and they work _great_ with LibGDX. There was no point in creating -another _complex_ dependency injection framework, and we were fully aware of that. Simplicity and little-to-none runtime -overhead - this pretty much sums up the strong sides of `ktx-inject`. +know - there _are_ other Kotlin dependency injection frameworks and they work well with LibGDX. There was no point in +creating another _complex_ dependency injection framework, and we were fully aware of that. Simplicity and +little-to-none runtime overhead is what sums up the strong sides of `ktx-inject`. ### Alternatives diff --git a/scene2d/src/test/kotlin/ktx/scene2d/widgetTest.kt b/scene2d/src/test/kotlin/ktx/scene2d/widgetTest.kt index b63f53e5..d4f4e15b 100644 --- a/scene2d/src/test/kotlin/ktx/scene2d/widgetTest.kt +++ b/scene2d/src/test/kotlin/ktx/scene2d/widgetTest.kt @@ -493,7 +493,7 @@ class KTreeWidgetTest : NeedsLibGDX() { assertSame(actor, node.actor) assertSame(tree, node.tree) assertSame(tree, node.actor.parent) - assertSame(node, tree.nodes.first()) + assertSame(node, tree.rootNodes.first()) } } diff --git a/version.txt b/version.txt index 8c6c6615..6f113fb9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.9.13-b1 +1.9.14-b1 diff --git a/vis/README.md b/vis/README.md index d7aba52a..6cbc9104 100644 --- a/vis/README.md +++ b/vis/README.md @@ -1,4 +1,4 @@ -[![VisUI](https://img.shields.io/badge/vis--ui-1.4.8-blue.svg)](https://github.com/kotcrab/vis-ui) +[![VisUI](https://img.shields.io/badge/vis--ui-1.4.11-blue.svg)](https://github.com/kotcrab/vis-ui) [![Maven Central](https://img.shields.io/maven-central/v/io.github.libktx/ktx-vis.svg)](https://search.maven.org/artifact/io.github.libktx/ktx-vis) # KTX: VisUI type-safe builders diff --git a/vis/src/main/kotlin/ktx/scene2d/vis/factory.kt b/vis/src/main/kotlin/ktx/scene2d/vis/factory.kt index 07ade737..d8802d93 100644 --- a/vis/src/main/kotlin/ktx/scene2d/vis/factory.kt +++ b/vis/src/main/kotlin/ktx/scene2d/vis/factory.kt @@ -943,8 +943,6 @@ inline fun KWidget.tabbedPane( contract { callsInPlace(init, InvocationKind.EXACTLY_ONCE) } val pane = KTabbedPane(style) val table = pane.table - var storage: S? = null - actor(table, { storage = it }) - pane.init(storage!!) + actor(table, { pane.init(it) }) return pane } diff --git a/vis/src/test/kotlin/ktx/scene2d/vis/widgetTest.kt b/vis/src/test/kotlin/ktx/scene2d/vis/widgetTest.kt index 657dd824..53eb4282 100644 --- a/vis/src/test/kotlin/ktx/scene2d/vis/widgetTest.kt +++ b/vis/src/test/kotlin/ktx/scene2d/vis/widgetTest.kt @@ -86,7 +86,7 @@ class KVisTreeTest : NeedsLibGDX() { assertSame(actor, node.actor) assertSame(tree, node.tree) assertSame(tree, node.actor.parent) - assertSame(node, tree.nodes.first()) + assertSame(node, tree.rootNodes.first()) } }