diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ec3990e..914e2bc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,23 +5,7 @@ Briefly describe what this PR changes and why. - **Related Issues** -- - -**Type of Change** -- [ ] Bug fix -- [ ] Feature -- [ ] Refactor -- [ ] Documentation -- [ ] Tests -- [ ] Chore -- [ ] Breaking change +- Closes # **How to Test**(if applicable) Steps to verify the change: - - -**Checklist** -- [ ] Code follows project style -- [ ] Self-reviewed -- [ ] Tests added/updated (if applicable) -- [ ] Docs updated (if applicable) diff --git a/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Context.kt b/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Context.kt index c221c9a..bc031df 100644 --- a/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Context.kt +++ b/engine/core/src/main/kotlin/io/canopy/engine/core/flow/Context.kt @@ -1,7 +1,26 @@ package io.canopy.engine.core.flow +import kotlin.jvm.Throws import io.canopy.engine.core.nodes.Node +/** + * Represents a typed key used to store and resolve values from a [Context]. + * + * Users are encouraged to implement this interface with their own enums instead of + * using raw strings, as it improves readability, refactor safety, and discoverability. + * + * Example: + * ``` + * enum class GameContextKey(override val key: String) : ContextKey { + * THEME("theme"), + * DIFFICULTY("difficulty") + * } + * ``` + */ +interface ContextKey { + val key: String +} + /** * A transparent "scope" node used to attach contextual values to a subtree. * @@ -15,13 +34,20 @@ import io.canopy.engine.core.nodes.Node * * Example: * ``` + * enum class GameContextKey(override val key: String) : ContextKey { + * THEME("theme"), + * DIFFICULTY("difficulty") + * } + * * root.context { - * provide("theme" to "dark", "difficulty" to 3) + * provide(GameContextKey.THEME) { "dark" } + * provide("debug") { true } * - * +PlayerNode { ... } + * +PlayerNode { ... } * } * - * val theme: String = player.context("theme") + * val theme: String = player.fromContext(GameContextKey.THEME) + * val debug: Boolean = player.fromContext("debug") * ``` */ class Context( @@ -30,9 +56,19 @@ class Context( block: Context.() -> Unit = {}, ) : Node(name, block) { + /** + * Provides a value under a raw string key. + */ fun provide(key: String, value: () -> T?) { provided[key] = value } + + /** + * Provides a value under a typed [ContextKey]. + */ + fun provide(key: ContextKey, value: () -> T?) { + provide(key.key, value) + } } /** @@ -44,26 +80,70 @@ class Context( * - For each ancestor that is a [Context], checks if it provides [key]. * - The closest scope wins (nearest ancestor). * - * @throws IllegalStateException if the key does not exist in any scope. + * Returns null if no matching key is found. */ @Suppress("UNCHECKED_CAST") -fun Node<*>.resolve(key: String): T { - var cur: Node<*>? = this - while (cur != null) { - if (cur is Context && cur.provided.containsKey(key)) { - val callback = cur.provided[key] ?: error("$key not found in context") - return callback() as T // may be null -> will throw if T is non-null; that’s fine +fun Node<*>.fromContextOrNull(key: String): T? { + var current: Node<*>? = this + + while (current != null) { + if (current is Context) { + val provider = current.provided[key] + if (provider != null) { + val value = provider.invoke() as? T + if (value != null) return value + } } - cur = cur.parent + current = current.parent } - error("Missing context key '$key' from node $path") + + return null } -fun Node<*>.lazyResolve(key: String) = lazy { resolve(key) } +/** + * Typed overload of [fromContextOrNull] using a [ContextKey]. + */ +fun Node<*>.fromContextOrNull(key: ContextKey): T? = fromContextOrNull(key.key) + +/** + * Lazily resolves a context value by raw string key, returning null if not found. + */ +fun Node<*>.lazyFromContextOrNull(key: String): Lazy = lazy { fromContextOrNull(key) } + +/** + * Lazily resolves a context value by [ContextKey], returning null if not found. + */ +fun Node<*>.lazyFromContextOrNull(key: ContextKey): Lazy = lazy { fromContextOrNull(key) } + +/** + * Resolves a context value by raw string key. + * + * @throws NoSuchElementException if the key does not exist in any visible context scope. + */ +@Throws(NoSuchElementException::class) +fun Node<*>.fromContext(key: String): T = fromContextOrNull(key) + ?: throw NoSuchElementException("Context key '$key' not found") + +/** + * Resolves a context value by [ContextKey]. + * + * @throws NoSuchElementException if the key does not exist in any visible context scope. + */ +@Throws(NoSuchElementException::class) +fun Node<*>.fromContext(key: ContextKey): T = fromContext(key.key) /** - * Fetches value from [Context]s, or null if no value is found + * Lazily resolves a context value by raw string key. + * + * @throws NoSuchElementException if the key does not exist in any visible context scope. */ -fun Node<*>.resolveOrNull(key: String): T? = runCatching { resolve(key) }.getOrNull() +@Throws(NoSuchElementException::class) +fun Node<*>.lazyFromContext(key: String): Lazy = lazy { fromContext(key) } -fun Node<*>.lazyResolveOrNull(key: String) = lazy { resolveOrNull(key) } +/** + * Lazily resolves a context value by [ContextKey]. + * + * @throws NoSuchElementException if the key does not exist in any visible context scope. + */ +@Throws(NoSuchElementException::class) +fun Node<*>.lazyFromContext(key: ContextKey): Lazy = lazy { fromContext(key) } diff --git a/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node.kt b/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node.kt index cab34e3..b0969cf 100644 --- a/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node.kt +++ b/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node.kt @@ -352,26 +352,6 @@ abstract class Node> protected constructor( fun hasChildType(type: KClass>) = children.values.any { it::class == type } - /* ============================================================ - * Group management (mirrors into SceneManager) - * ============================================================ */ - - fun inGroup(group: String) = group in groups - - fun addGroup(group: String) = groups.add(group).also { - LogContext.with("nodePath" to path, "group" to group) { - log.trace("event" to "node.group.add") { "Add group" } - } - sceneManager.addToGroup(group, this) - } - - fun removeGroup(group: String) = groups.remove(group).also { - LogContext.with("nodePath" to path, "group" to group) { - log.trace("event" to "node.group.remove") { "Remove group" } - } - sceneManager.removeFromGroup(group, this) - } - /* ============================================================ * Tree building * ============================================================ */ @@ -551,7 +531,7 @@ abstract class Node> protected constructor( infix fun child(node: Node<*>) = addChild(node) - fun groups(vararg groups: String) = apply { groups.forEach { addGroup(it) } } + // fun groups(vararg groups: String) = apply { groups.forEach { addGroup(it) } } fun > patch(path: String, handler: T.() -> Unit) = getNode(path).apply(handler) diff --git a/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node2D.kt b/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node2D.kt index 5916686..bc11047 100644 --- a/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node2D.kt +++ b/engine/core/src/main/kotlin/io/canopy/engine/core/nodes/Node2D.kt @@ -40,21 +40,15 @@ abstract class Node2D> protected constructor(name: String, block: * ============================================================ */ /** Local position in 2D space. */ - open var position: Vector2 = Vector2.Zero + var position: Vector2 = Vector2.Zero /** Local scale in 2D space. */ - open var scale: Vector2 = Vector2(1f, 1f) + var scale: Vector2 = Vector2(1f, 1f) /** Local rotation in radians. */ - open var rotation: Float = 0f + var rotation: Float = 0f /* ============================================================ * DSL helpers * ============================================================ */ - - fun at(x: Float, y: Float) = apply { position.set(x, y) } - fun at(pos: Vector2) = apply { position.set(pos) } - - fun scaled(x: Float, y: Float) = apply { this.scale.set(x, y) } - fun scaled(scale: Vector2) = apply { this.scale.set(scale) } } diff --git a/engine/core/src/test/kotlin/io/canopy/engine/core/flow/ContextTests.kt b/engine/core/src/test/kotlin/io/canopy/engine/core/flow/ContextTests.kt index 5c41aef..d5e6630 100644 --- a/engine/core/src/test/kotlin/io/canopy/engine/core/flow/ContextTests.kt +++ b/engine/core/src/test/kotlin/io/canopy/engine/core/flow/ContextTests.kt @@ -30,12 +30,6 @@ class ContextTests { private fun n(name: String, block: EmptyNode.() -> Unit = {}) = EmptyNode(name, block) - private fun Node<*>.child(name: String, block: EmptyNode.() -> Unit = {}): Node<*> { - val c = n(name, block) - addChild(c) - return c - } - private object DebugModeKey // any object key (no ::class) private object SeasonKey @@ -57,7 +51,7 @@ class ContextTests { // Find the "child" node. Adjust if you have find-by-path utilities. val child = root.getNode("./child") - val debug: Boolean = child.resolve("debug") + val debug: Boolean = child.fromContext("debug") assertTrue(debug) } @@ -76,7 +70,7 @@ class ContextTests { val b = root.getNode("./a/b") - val debug: Boolean = b.resolve("debug") + val debug: Boolean = b.fromContext("debug") assertTrue(debug) } @@ -100,8 +94,8 @@ class ContextTests { val a = root.getNode("./a") val b = root.getNode("./b") - val aDebug: Boolean = a.resolve("debug") - val bDebug: Boolean = b.resolve("debug") + val aDebug: Boolean = a.fromContext("debug") + val bDebug: Boolean = b.fromContext("debug") assertTrue(aDebug) assertFalse(bDebug) @@ -115,7 +109,7 @@ class ContextTests { val child = root.getNode("./child") - val missing: String? = child.resolveOrNull("nope") + val missing: String? = child.fromContextOrNull("nope") assertNull(missing) } @@ -127,8 +121,8 @@ class ContextTests { val child = root.getNode("./child") - val ex = assertThrows { - child.resolve("missing") + val ex = assertThrows { + child.fromContext("missing") } // Optional: if your error message includes path/name @@ -149,8 +143,8 @@ class ContextTests { val child = root.getNode("./child") - val debug: Boolean = child.resolve("debugMode") - val season: String = child.resolve("season") + val debug: Boolean = child.fromContext("debugMode") + val season: String = child.fromContext("season") assertTrue(debug) assertEquals("winter", season) @@ -170,8 +164,8 @@ class ContextTests { val child = root.getNode("./child") - assertEquals(1, child.resolve("keyA")) - assertEquals(2, child.resolve("keyB")) + assertEquals(1, child.fromContext("keyA")) + assertEquals(2, child.fromContext("keyB")) } @Test @@ -191,7 +185,7 @@ class ContextTests { root.buildTree() val c = root.getNode("./a/b/c") - assertEquals(42, c.resolve("x")) + assertEquals(42, c.fromContext("x")) } @Test @@ -211,7 +205,7 @@ class ContextTests { val c = root.getNode("./a") - assertEquals(1, c.resolve("keyA")) + assertEquals(1, c.fromContext("keyA")) } // --- Tiny adapter ------------------------------------------------------- diff --git a/engine/core/src/test/kotlin/io/canopy/engine/core/nodes/NodeTests.kt b/engine/core/src/test/kotlin/io/canopy/engine/core/nodes/NodeTests.kt index 3df718b..8ed1eb4 100644 --- a/engine/core/src/test/kotlin/io/canopy/engine/core/nodes/NodeTests.kt +++ b/engine/core/src/test/kotlin/io/canopy/engine/core/nodes/NodeTests.kt @@ -254,7 +254,7 @@ class NodeTests { val node = CustomScene { patch("./empty") { name = "patched" - at(100f, 100f) + position = Vector2(100f, 100f) } } node.buildTree() diff --git a/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/JsonParser.kt b/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/Json.kt similarity index 96% rename from engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/JsonParser.kt rename to engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/Json.kt index 046abf3..3ba06fe 100644 --- a/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/JsonParser.kt +++ b/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/Json.kt @@ -22,7 +22,7 @@ import kotlinx.serialization.serializer * Note: * The [config] lambda is applied last, so callers can override any default. */ -object JsonParser { +object Json { /* ============================================================ * Decoding @@ -111,13 +111,13 @@ object JsonParser { * Useful when you already parsed JSON and want to decode only a subtree. */ inline fun decodeJsonElement(serializer: KSerializer = serializer(), element: JsonElement): T = - Json.decodeFromJsonElement(serializer, element) + kotlinx.serialization.json.Json.decodeFromJsonElement(serializer, element) /** * Encodes [data] into a [JsonElement] using the provided [serializer]. */ inline fun encodeJsonElement(serializer: KSerializer = serializer(), data: T): JsonElement = - Json.encodeToJsonElement(serializer, data) + kotlinx.serialization.json.Json.encodeToJsonElement(serializer, data) fun buildJson(module: SerializersModule? = null, config: JsonBuilder.() -> Unit = {}) = Json { if (module != null) serializersModule = module diff --git a/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/TomlParser.kt b/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/Toml.kt similarity index 99% rename from engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/TomlParser.kt rename to engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/Toml.kt index fbbec42..60dffc1 100644 --- a/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/TomlParser.kt +++ b/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/parsers/Toml.kt @@ -23,7 +23,7 @@ import kotlinx.serialization.modules.SerializersModule * tomlkt parses TOML according to the TOML specification. This parser does not attempt * to accept non-standard TOML extensions by default. */ -object TomlParser { +object Toml { /* ============================================================ * Decoding diff --git a/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/registry/IdRegistry.kt b/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/registry/IdRegistry.kt index 4ff9b46..b3a1b32 100644 --- a/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/registry/IdRegistry.kt +++ b/engine/data/data-core/src/main/kotlin/io/canopy/engine/data/core/registry/IdRegistry.kt @@ -1,7 +1,7 @@ package io.canopy.engine.data.core.registry import com.badlogic.gdx.files.FileHandle -import io.canopy.engine.data.core.parsers.JsonParser +import io.canopy.engine.data.core.parsers.Json /** * Simple ID-based registry that loads [IdEntry] items from JSON files. @@ -71,7 +71,7 @@ class IdRegistry( .filter { it.extension() == "json" } .forEach { file -> // R is reified so JsonParser can decode List. - val items: List = JsonParser.fromFile(file) + val items: List = Json.fromFile(file) addItemsToRegistry(items) } } diff --git a/engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/JsonParserTests.kt b/engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/JsonTests.kt similarity index 90% rename from engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/JsonParserTests.kt rename to engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/JsonTests.kt index 2d9f3c3..2e0a133 100644 --- a/engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/JsonParserTests.kt +++ b/engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/JsonTests.kt @@ -12,13 +12,13 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test /** - * Tests for [JsonParser]. + * Tests for [Json]. * * JsonParser defaults (important for these tests): * - ignoreUnknownKeys = true * - classDiscriminator = "type" for polymorphic decoding */ -class JsonParserTests { +class JsonTests { // --- Test fixtures ------------------------------------------------------- @@ -47,7 +47,7 @@ class JsonParserTests { // Verifies basic decode from a JSON object into a Kotlin @Serializable type. val jsonString = """{"id":1,"name":"Test"}""" - val parsed = JsonParser.fromString(jsonString) + val parsed = Json.fromString(jsonString) assertEquals(1, parsed.id) assertEquals("Test", parsed.name) @@ -58,7 +58,7 @@ class JsonParserTests { // JsonParser is configured with ignoreUnknownKeys = true. val jsonString = """{"id":2,"name":"Unknown","extraField":"ignored"}""" - val parsed = JsonParser.fromString(jsonString) + val parsed = Json.fromString(jsonString) assertEquals(2, parsed.id) assertEquals("Unknown", parsed.name) @@ -69,7 +69,7 @@ class JsonParserTests { // Verifies decoding generic collections works (reified type). val jsonString = """[{"id":3,"name":"Item1"},{"id":4,"name":"Item2"}]""" - val parsed = JsonParser.fromString>(jsonString) + val parsed = Json.fromString>(jsonString) assertEquals(2, parsed.size) @@ -109,7 +109,7 @@ class JsonParserTests { } """.trimIndent() - val parsed = JsonParser.fromString(jsonString, module) + val parsed = Json.fromString(jsonString, module) val a = assertIs(parsed) assertEquals("Hello", a.valueA) @@ -124,7 +124,7 @@ class JsonParserTests { ] """.trimIndent() - val parsed = JsonParser.fromString>(jsonString, module) + val parsed = Json.fromString>(jsonString, module) assertEquals(2, parsed.size) diff --git a/engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/TomlParserTests.kt b/engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/TomlTests.kt similarity index 87% rename from engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/TomlParserTests.kt rename to engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/TomlTests.kt index cf6302c..a9c7cca 100644 --- a/engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/TomlParserTests.kt +++ b/engine/data/data-core/src/test/kotlin/io/canopy/engine/data/core/parsers/TomlTests.kt @@ -5,12 +5,11 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows /** - * Tests for [TomlParser]. + * Tests for [Toml]. * * Important constraints: * - TOML has no `null` literal. Strings like `name = null` are invalid TOML. @@ -18,7 +17,7 @@ import org.junit.jupiter.api.assertThrows * - decoding: missing keys map to defaults / nullables * - encoding: nullable fields that are null are typically *omitted*, not written as `null` */ -class TomlParserTests { +class TomlTests { // --- Fixtures ----------------------------------------------------------- @@ -52,7 +51,7 @@ class TomlParserTests { retries = 3 """.trimIndent() - val cfg = TomlParser.fromString(toml) + val cfg = Toml.fromString(toml) assertEquals("canopy", cfg.name) assertTrue(cfg.enabled) @@ -64,7 +63,7 @@ class TomlParserTests { // Missing keys should map to default constructor values. val toml = """name = "canopy"""" - val cfg = TomlParser.fromString(toml) + val cfg = Toml.fromString(toml) assertEquals("canopy", cfg.name) assertTrue(cfg.enabled) // default @@ -81,7 +80,7 @@ class TomlParserTests { """.trimIndent() assertThrows { - TomlParser.fromString(toml) + Toml.fromString(toml) } } @@ -91,7 +90,7 @@ class TomlParserTests { val toml = """values = [1, "two", 3]""" assertThrows { - TomlParser.fromString(toml) + Toml.fromString(toml) } } @@ -106,7 +105,7 @@ class TomlParserTests { """.trimIndent() assertThrows { - TomlParser.fromString(toml) + Toml.fromString(toml) } } @@ -119,7 +118,7 @@ class TomlParserTests { port = 8080 """.trimIndent() - val cfg = TomlParser.fromString(toml) + val cfg = Toml.fromString(toml) assertEquals("localhost", cfg.server.host) assertEquals(8080, cfg.server.port) @@ -130,8 +129,8 @@ class TomlParserTests { // Verifies encode -> decode stability. val original = SimpleConfig(name = "canopy", enabled = false, retries = 7) - val encoded = TomlParser.toString(original) - val decoded = TomlParser.fromString(encoded) + val encoded = Toml.toString(original) + val decoded = Toml.fromString(encoded) assertEquals(original, decoded) @@ -149,13 +148,13 @@ class TomlParserTests { val original = NullableField(null) - val encoded = TomlParser.toString(original) + val encoded = Toml.toString(original) // We should NOT emit `maybe = null` (invalid TOML). assertFalse(encoded.contains("maybe")) // Roundtrip should keep null. - val decoded = TomlParser.fromString(encoded) + val decoded = Toml.fromString(encoded) assertEquals(original, decoded) } } @@ -176,7 +175,7 @@ class TomlParserTests { retries = 1 """.trimIndent() - val cfg = TomlParser.fromString(toml) + val cfg = Toml.fromString(toml) assertEquals(null, cfg.name) assertTrue(cfg.enabled) diff --git a/engine/data/data-saving/src/main/kotlin/io/canopy/engine/data/saving/SaveManager.kt b/engine/data/data-saving/src/main/kotlin/io/canopy/engine/data/saving/SaveManager.kt index e4e9091..a20d9a5 100644 --- a/engine/data/data-saving/src/main/kotlin/io/canopy/engine/data/saving/SaveManager.kt +++ b/engine/data/data-saving/src/main/kotlin/io/canopy/engine/data/saving/SaveManager.kt @@ -3,7 +3,7 @@ package io.canopy.engine.data.saving import kotlin.reflect.KClass import com.badlogic.gdx.files.FileHandle import io.canopy.engine.core.managers.Manager -import io.canopy.engine.data.core.parsers.JsonParser +import io.canopy.engine.data.core.parsers.Json import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject @@ -92,14 +92,14 @@ class SaveManager(vararg destinations: Pair FileHandle>) val fileLocation = destinationsMap[destination]?.invoke(slot) ?: return if (!fileLocation.exists()) return - val jsonData = JsonParser.rawParseFile(fileLocation) + val jsonData = Json.rawParseFile(fileLocation) val registry = dataRegistry[destination] ?: return registry.keys.forEach { module -> val jsonElement = jsonData[module.id] ?: return@forEach val typedModule = module as SaveModule - val decodedData = JsonParser.decodeJsonElement(typedModule.serializer, jsonElement) + val decodedData = Json.decodeJsonElement(typedModule.serializer, jsonElement) registry[typedModule] = decodedData typedModule.onLoad(decodedData) @@ -148,12 +148,12 @@ class SaveManager(vararg destinations: Pair FileHandle>) val data = typedModule.onSave() put( typedModule.id, - JsonParser.encodeJsonElement(typedModule.serializer, data) + Json.encodeJsonElement(typedModule.serializer, data) ) } } - JsonParser.toFile(JsonObject(jsonMap), fileLocation) + Json.toFile(JsonObject(jsonMap), fileLocation) } /** Saves all destinations for the given slot (e.g. profile + world + settings). */