Skip to content

Commit

Permalink
refactor: Swap to kotlinx.io for multiplatform paths
Browse files Browse the repository at this point in the history
feat: Allow loading paths from jar resources on JVM
  • Loading branch information
0ffz committed Oct 26, 2024
1 parent 9c99a31 commit c5ee414
Show file tree
Hide file tree
Showing 16 changed files with 276 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,58 @@ package com.mineinabyss.geary.prefabs
import co.touchlab.kermit.Logger
import com.mineinabyss.geary.components.relations.NoInherit
import com.mineinabyss.geary.datatypes.Entity
import com.mineinabyss.geary.datatypes.GearyEntity
import com.mineinabyss.geary.helpers.entity
import com.mineinabyss.geary.helpers.fastForEach
import com.mineinabyss.geary.modules.Geary
import com.mineinabyss.geary.prefabs.configuration.components.CopyToInstances
import com.mineinabyss.geary.prefabs.configuration.components.InheritPrefabs
import com.mineinabyss.geary.prefabs.configuration.components.Prefab
import com.mineinabyss.geary.prefabs.helpers.inheritPrefabsIfNeeded
import com.mineinabyss.geary.serialization.formats.Format.ConfigType.NON_STRICT
import com.mineinabyss.geary.serialization.formats.Formats
import com.mineinabyss.geary.serialization.serializers.PolymorphicListAsMapSerializer
import com.mineinabyss.geary.serialization.serializers.PolymorphicListAsMapSerializer.Companion.provideConfig
import com.mineinabyss.geary.systems.query.Query
import kotlinx.serialization.Serializable
import kotlinx.io.Source
import kotlinx.io.buffered
import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem
import kotlinx.serialization.modules.SerializersModule
import okio.Path
import kotlin.uuid.Uuid

class PrefabLoader(
val sources: PrefabSources,
val world: Geary,
val formats: Formats,
val logger: Logger,
) {
private val readFiles = mutableListOf<PrefabPath>()

private val needsInherit = world.cache(::NeedsInherit)

fun addSource(path: PrefabPath) {
readFiles.add(path)
}

class NeedsInherit(world: Geary) : Query(world) {
val inheritPrefabs by get<InheritPrefabs>()
fun markAsPrefab(entity: GearyEntity, key: PrefabKey) {
entity.set(Prefab())
entity.set<PrefabKey>(key)
entity.addRelation<NoInherit, Prefab>()
entity.addRelation<NoInherit, Uuid>()
entity.addRelation<NoInherit, CopyToInstances>()
}

fun loadOrUpdatePrefabs() {
val results = mutableListOf<String>()
readFiles.forEach { prefabsPath ->
sources.paths.forEach { prefabsPath ->
logger.i("Loading prefabs for namespace '${prefabsPath.namespace}'")
val loaded = prefabsPath.get()
.map { path -> loadFromPathOrReloadExisting(prefabsPath.namespace, path) }
.toList()

val loaded = buildList {
addAll(prefabsPath.paths().map { path -> loadFromPathOrReloadExisting(prefabsPath.namespace, path) })
addAll(
prefabsPath.sources().map {
load(
key = it.key,
source = it.source,
writeTo = world.getAddon(Prefabs).manager[it.key],
formatExt = it.formatExt
)
}
)
}
val success = loaded.count { it is PrefabLoadResult.Success }
val warn = loaded.count { it is PrefabLoadResult.Warn }
val fail = loaded.count { it is PrefabLoadResult.Failure }
Expand All @@ -64,29 +74,13 @@ class PrefabLoader(
}
}

/** If this entity has a [Prefab] component, clears it and loads components from its file. */
fun reload(entity: Entity) {
val prefab = entity.get<Prefab>() ?: error("Entity was not an already loaded prefab")
val key = entity.get<PrefabKey>() ?: error("Entity did not have a prefab key")
val file = prefab.file ?: error("Prefab did not have a file")
entity.clear()
loadFromPath(key.namespace, file, entity)
entity.inheritPrefabsIfNeeded()
}

@Serializable
class PrefabFileProperties(val namespaces: List<String> = listOf())

sealed class PrefabLoadResult {
data class Success(val entity: Entity) : PrefabLoadResult()
data class Warn(val entity: Entity) : PrefabLoadResult()
data class Failure(val error: Throwable) : PrefabLoadResult()
}

/** Registers an entity with components defined in a [path], adding a [Prefab] component. */
fun loadFromPath(namespace: String, path: Path, writeTo: Entity? = null): PrefabLoadResult {
fun load(
key: PrefabKey,
source: Source,
writeTo: Entity? = null,
formatExt: String,
): PrefabLoadResult {
var hadMalformed = false
val key = PrefabKey.of(namespace, path.name.substringBeforeLast('.'))
val decoded = runCatching {
val config = PolymorphicListAsMapSerializer.Config<Any>(
whenComponentMalformed = {
Expand All @@ -95,14 +89,15 @@ class PrefabLoader(
}
)
val serializer = PolymorphicListAsMapSerializer.ofComponents(config)
val ext = path.name.substringAfterLast('.')
val format = formats[formatExt] ?: throw IllegalArgumentException("Unknown file format $formatExt")

logger.d("Loading prefab at $path")
val format = formats[ext] ?: throw IllegalArgumentException("Unknown file format $ext")
val fileProperties = format.decodeFromFile(PrefabFileProperties.serializer(), path, configType = NON_STRICT)
format.decodeFromFile(serializer, path, overrideSerializersModule = SerializersModule {
provideConfig(config.copy(namespaces = fileProperties.namespaces))
})
format.decode(
serializer,
source,
overrideSerializersModule = SerializersModule {
provideConfig(config)
}
)
}

// Stop here if we need to make a new entity
Expand All @@ -114,22 +109,49 @@ class PrefabLoader(
}

val entity = writeTo ?: world.entity()
entity.addRelation<NoInherit, Prefab>()
entity.addRelation<NoInherit, Uuid>()
entity.addRelation<NoInherit, CopyToInstances>()
entity.set(Prefab(path))
markAsPrefab(entity, key)
decoded.getOrNull()?.let { entity.setAll(it) }
entity.set(key)
return when {
hadMalformed -> PrefabLoadResult.Warn(entity)
else -> PrefabLoadResult.Success(entity)
}
}

/** Registers an entity with components defined in a [path], adding a [Prefab] component. */
fun loadFromPath(namespace: String, path: Path, writeTo: Entity? = null): PrefabLoadResult {
val key = PrefabKey.of(namespace, path.name.substringBeforeLast('.'))
val ext = path.name.substringAfterLast('.')
logger.d("Loading prefab at path $path")
return load(key, SystemFileSystem.source(path).buffered(), writeTo, ext).also { result ->
// Mark path we loaded from to allow for reloading
if (result is PrefabLoadResult.Success) result.entity.set(Prefab(path))
}
}

fun loadFromPathOrReloadExisting(namespace: String, path: Path): PrefabLoadResult {
val key = PrefabKey.of(namespace, path.name.substringBeforeLast('.'))
val existing = world.getAddon(Prefabs).manager[key]
existing?.clear()
return loadFromPath(namespace, path, existing)
}

/** If this entity has a [Prefab] component, clears it and loads components from its file. */
fun reload(entity: Entity) {
val prefab = entity.get<Prefab>() ?: error("Entity was not an already loaded prefab")
val key = entity.get<PrefabKey>() ?: error("Entity did not have a prefab key")
val file = prefab.file ?: error("Prefab did not have a file")
entity.clear()
loadFromPath(key.namespace, file, entity)
entity.inheritPrefabsIfNeeded()
}

sealed class PrefabLoadResult {
data class Success(val entity: Entity) : PrefabLoadResult()
data class Warn(val entity: Entity) : PrefabLoadResult()
data class Failure(val error: Throwable) : PrefabLoadResult()
}

class NeedsInherit(world: Geary) : Query(world) {
val inheritPrefabs by get<InheritPrefabs>()
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package com.mineinabyss.geary.prefabs

import okio.Path
import kotlinx.io.Source
import kotlinx.io.files.Path

class PrefabPath(
data class PrefabPath(
val namespace: String,
val get: () -> Sequence<Path>,
val paths: () -> Sequence<Path> = { emptySequence() },
val sources: () -> Sequence<PrefabSource> = { emptySequence() },
)

data class PrefabSource(
val source: Source,
val key: PrefabKey,
val formatExt: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,48 @@ package com.mineinabyss.geary.prefabs

import com.mineinabyss.geary.addons.Namespaced
import com.mineinabyss.geary.addons.dsl.GearyDSL
import okio.FileSystem
import okio.Path
import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem

@GearyDSL
class PrefabsDSL(
private val prefabsBuilder: PrefabsBuilder,
private val fileSystem: FileSystem,
private val namespaced: Namespaced,
internal val prefabsBuilder: PrefabSources,
internal val namespaced: Namespaced,
) {

/** Loads prefab entities from all files inside a [directory][from], into a given [namespace] */
fun from(
fun fromFiles(
vararg from: Path,
) {
prefabsBuilder.paths.add(PrefabPath(namespaced.namespace) { from.asSequence() })
prefabsBuilder.paths.add(
PrefabPath(namespaced.namespace, paths = { from.asSequence() })
)
}

fun fromRecursive(folder: Path) {
prefabsBuilder.paths.add(PrefabPath(namespaced.namespace) {
fileSystem
.listRecursively(folder, true)
.filter { it.name.contains('.') }
})
fun fromDirectory(folder: Path) {
prefabsBuilder.paths.add(
PrefabPath(namespaced.namespace, paths = { walkFolder(folder) })
)
}

fun fromSources(vararg sources: PrefabSource) {
prefabsBuilder.paths.add(
PrefabPath(namespaced.namespace, sources = { sources.asSequence() })
)
}

private fun walkFolder(folder: Path): Sequence<Path> = sequence {
val fileSystem = SystemFileSystem
val stack = ArrayDeque<Path>()
stack.add(folder)

while (stack.isNotEmpty()) {
val current = stack.removeLast()
if (fileSystem.metadataOrNull(current)?.isDirectory == true) {
fileSystem.list(current).forEach { stack.add(it) }
} else {
yield(current)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,47 @@
package com.mineinabyss.geary.prefabs

import com.mineinabyss.geary.addons.Namespaced
import com.mineinabyss.geary.addons.dsl.Addon
import com.mineinabyss.geary.addons.dsl.GearyDSL
import com.mineinabyss.geary.addons.dsl.createAddon
import com.mineinabyss.geary.prefabs.configuration.systems.*
import com.mineinabyss.geary.prefabs.systems.createInheritPrefabsOnLoadListener
import com.mineinabyss.geary.prefabs.systems.createTrackPrefabsByKeyListener
import com.mineinabyss.geary.serialization.FileSystem
import com.mineinabyss.geary.serialization.SerializableComponents

interface PrefabsModule {
val manager: PrefabManager
val loader: PrefabLoader
}
data class PrefabsModule(
val manager: PrefabManager,
val loader: PrefabLoader,
)

class PrefabsBuilder {
val paths: MutableList<PrefabPath> = mutableListOf()
class PrefabSources {
val paths = mutableListOf<PrefabPath>()
}

val Prefabs
get() = createAddon<PrefabsBuilder, PrefabsModule>("Prefabs", { PrefabsBuilder() }) {
val formats = geary.getAddon(SerializableComponents).formats
val module = object : PrefabsModule {
override val manager = PrefabManager()
override val loader: PrefabLoader = PrefabLoader(geary, formats, logger)
}

configuration.paths.forEach { module.loader.addSource(it) }

systems {
createInheritPrefabsOnLoadListener()
createParseChildOnPrefabListener()
createParseChildrenOnPrefabListener()
createParseInstancesOnPrefabListener()
createParseRelationOnPrefabListener()
createParseRelationWithDataListener()
createTrackPrefabsByKeyListener()
createCopyToInstancesSystem()
reEmitEvent()
}
val Prefabs = createAddon<PrefabSources, PrefabsModule>("Prefabs", {
install(SerializableComponents)
PrefabSources()
}) {
val formats = geary.getAddon(SerializableComponents).formats
val module = PrefabsModule(PrefabManager(), PrefabLoader(configuration, geary, formats, logger))

systems {
createInheritPrefabsOnLoadListener()
createParseChildOnPrefabListener()
createParseChildrenOnPrefabListener()
createParseInstancesOnPrefabListener()
createParseRelationOnPrefabListener()
createParseRelationWithDataListener()
createTrackPrefabsByKeyListener()
createCopyToInstancesSystem()
reEmitEvent()
}

entities {
module.loader.loadOrUpdatePrefabs()
}
module
entities {
module.loader.loadOrUpdatePrefabs()
}
module
}

@GearyDSL
fun Namespaced.prefabs(configure: PrefabsDSL.() -> Unit): Addon<PrefabsBuilder, PrefabsModule> =
setup.install(Prefabs) { PrefabsDSL(this, setup.geary.getConfiguration(FileSystem), this@prefabs).configure() }
fun Namespaced.prefabs(configure: PrefabsDSL.() -> Unit) =
setup.install(Prefabs) { PrefabsDSL(this, this@prefabs).configure() }
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.mineinabyss.geary.prefabs.configuration.components

import okio.Path
import kotlinx.io.files.Path

/**
* A component applied to prefabs loaded from a file that allows them to be reread.
Expand Down
Loading

0 comments on commit c5ee414

Please sign in to comment.