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

commonMain.de.fabmax.kool.editor.api.ProjectDataIo.kt Maven / Gradle / Ivy

The newest version!
package de.fabmax.kool.editor.api

import de.fabmax.kool.editor.data.*
import de.fabmax.kool.modules.filesystem.*
import de.fabmax.kool.util.decodeToString
import de.fabmax.kool.util.logE
import de.fabmax.kool.util.toBuffer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class ProjectReader(private val srcDir: FileSystemDirectory) {

    var parserErrors = 0
        private set

    @OptIn(ExperimentalSerializationApi::class)
    private val codec: Json = Json {
        ignoreUnknownKeys = true
        prettyPrint = true
        prettyPrintIndent = "  "
    }

    suspend fun loadTree(): ProjectData? {
        val projMeta: ProjectMeta = srcDir.getFileOrNull("project.json")?.parseFile() ?: return null
        val materials = srcDir.getDirectoryOrNull("materials") ?.let { loadSceneEntities(it) } ?: emptyList()
        val scenes = buildList {
            val scenesDir = srcDir.getDirectoryOrNull("scenes")
            if (scenesDir != null) {
                val metas = scenesDir.listFiles()
                    .filter { it.name.endsWith("meta.json") }
                    .mapNotNull { it.parseFile() }
                    .map { meta -> meta to scenesDir.listDirectories().find { it.name.endsWith("${meta.rootId.value}") } }

                metas.forEach { (meta, dir) ->
                    val sceneEntities = dir?.let { loadSceneEntities(it) } ?: emptyList()
                    add(SceneData(meta, sceneEntities.toMutableList()))
                }
            }
        }
        return ProjectData(projMeta, scenes.toMutableList(), materials.toMutableList())
    }

    private suspend fun loadSceneEntities(dir: FileSystemDirectory): List {
        return dir.listRecursively()
            .filter { it.name.endsWith(".json", ignoreCase = true) }
            .filterIsInstance()
            .mapNotNull { it.parseFile() }
    }

    private suspend inline fun  FileSystemFile?.parseFile(): T? {
        if (this == null) return null
        return try {
            codec.decodeFromString(readText())
        } catch (e: Exception) {
            logE { "Failed parsing ${path}: $e" }
            parserErrors++
            null
        }
    }

    companion object {
        suspend fun loadProjectData(srcDir: FileSystemDirectory): ProjectData? {
            return ProjectReader(srcDir).loadTree()
        }
    }
}

class ProjectWriter private constructor(
    private val projData: ProjectData,
    private val targetDir: WritableFileSystemDirectory
) {
    @OptIn(ExperimentalSerializationApi::class)
    private val codec: Json = Json {
        prettyPrint = true
        prettyPrintIndent = "  "
    }

    private val projFiles = mutableSetOf()

    private suspend fun saveTree() {
        targetDir.createProjFile("project.json", codec.encodeToString(projData.meta))

        val materialDir = targetDir.createProjDir("materials")
        projData.materials.toHierarchy().forEach { it.saveEntities(materialDir) }

        val scenesDir = targetDir.createProjDir("scenes")
        projData.scenes.forEach { scene ->
            val sceneName = "${scene.meta.name.fileNameSafe()}_${scene.meta.rootId.value}"
            scenesDir.createProjFile("$sceneName-meta.json", codec.encodeToString(scene.meta))
            val sceneDir = scenesDir.createProjDir(sceneName)
            scene.entities.toHierarchy().forEach { it.saveEntities(sceneDir) }
        }
        deleteOldFiles()
    }

    private fun deleteOldFiles() {
        val oldFiles = targetDir.listRecursively().filterIsInstance().toSet() - projFiles
        oldFiles.forEach { it.delete() }
    }

    private suspend fun GameEntityDataHierarchy.saveEntities(targetDir: WritableFileSystemDirectory) {
        val fileName = entityData.fileName
        val json = codec.encodeToString(entityData)
        val file = targetDir.getOrCreateFile(fileName)
        val existing = targetDir.getFile(fileName).read().decodeToString()
        if (existing != json) {
            file.write(json.encodeToByteArray().toBuffer())
        }
        projFiles += file

        if (children.isNotEmpty()) {
            val subDir = targetDir.getOrCreateDirectory(entityData.dirName)
            projFiles += subDir
            children.forEach { it.saveEntities(subDir) }
        }
    }

    private fun WritableFileSystemDirectory.createProjDir(name: String): WritableFileSystemDirectory {
        return getOrCreateDirectory(name).also { projFiles += it }
    }

    private suspend fun WritableFileSystemDirectory.createProjFile(name: String, json: String) {
        val file = getOrCreateFile(name)
        projFiles += file
        val existing = file.read().decodeToString()
        if (existing != json) {
            file.writeText(json)
        }
    }

    private val GameEntityData.dirName: String get() = "${settings.name.fileNameSafe()}_${id.value}"
    private val GameEntityData.fileName: String get() = "$dirName.json"

    companion object {
        private val nameRegex = Regex("[^\\w\\-_]+")

        private fun String.fileNameSafe(): String = replace(nameRegex, "-")

        suspend fun saveProjectData(data: ProjectData, targetDir: WritableFileSystemDirectory) {
            ProjectWriter(data, targetDir).saveTree()
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy