diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt index 86d2162d..f160e5e2 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Ext.kt @@ -2,7 +2,11 @@ package ru.dbotthepony.kstarbound import com.google.gson.GsonBuilder import com.google.gson.TypeAdapter +import java.util.Arrays +import java.util.stream.Stream inline fun GsonBuilder.registerTypeAdapter(adapter: TypeAdapter): GsonBuilder { return registerTypeAdapter(T::class.java, adapter) } + +fun Array.stream(): Stream = Arrays.stream(this) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt index 9dc3fe94..d669bed1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/Starbound.kt @@ -3,10 +3,10 @@ package ru.dbotthepony.kstarbound import com.google.gson.* import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap import org.apache.logging.log4j.LogManager -import ru.dbotthepony.kstarbound.api.IVFS -import ru.dbotthepony.kstarbound.api.PhysicalFS -import ru.dbotthepony.kstarbound.api.getPathFilename -import ru.dbotthepony.kstarbound.api.getPathFolder +import ru.dbotthepony.kstarbound.api.IStarboundFile +import ru.dbotthepony.kstarbound.api.NonExistingFile +import ru.dbotthepony.kstarbound.api.PhysicalFile +import ru.dbotthepony.kstarbound.api.explore import ru.dbotthepony.kstarbound.defs.* import ru.dbotthepony.kstarbound.defs.liquid.LiquidDefinition import ru.dbotthepony.kstarbound.defs.projectile.* @@ -25,7 +25,6 @@ import ru.dbotthepony.kvector.vector.ndouble.Vector2d import ru.dbotthepony.kvector.vector.nfloat.Vector2f import ru.dbotthepony.kvector.vector.nint.Vector2i import java.io.* -import java.nio.ByteBuffer import java.text.DateFormat import java.util.* import kotlin.collections.ArrayList @@ -40,7 +39,7 @@ const val PIXELS_IN_STARBOUND_UNITf = 8.0f // class TileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause) // class ProjectileDefLoadingException(message: String, cause: Throwable? = null) : IllegalStateException(message, cause) -object Starbound : IVFS { +object Starbound { private val LOGGER = LogManager.getLogger() private val _readingFolder = ThreadLocal() @@ -110,20 +109,67 @@ object Starbound : IVFS { private set var initialized = false private set + + @Volatile var terminateLoading = false private val archivePaths = ArrayList() - private val fileSystems = ArrayList() + private val fileSystems = ArrayList() fun addFilePath(path: File) { - fileSystems.add(PhysicalFS(path)) + fileSystems.add(PhysicalFile(path)) } - /** - * Добавляет уже прочитанный pak - */ fun addPak(pak: StarboundPak) { - fileSystems.add(pak) + fileSystems.add(pak.root) + } + + fun exists(path: String): Boolean { + @Suppress("name_shadowing") + var path = path + + if (path[0] == '/') { + path = path.substring(1) + } + + for (fs in fileSystems) { + if (fs.locate(path).exists) { + return true + } + } + + return false + } + + fun locate(path: String): IStarboundFile { + @Suppress("name_shadowing") + var path = path + + if (path[0] == '/') { + path = path.substring(1) + } + + for (fs in fileSystems) { + val file = fs.locate(path) + + if (file.exists) { + return file + } + } + + return NonExistingFile(path.split("/").last(), fullPath = path) + } + + fun locate(vararg path: String): IStarboundFile { + for (p in path) { + val get = locate(p) + + if (get.exists) { + return get + } + } + + return NonExistingFile(path[0].split("/").last(), fullPath = path[0]) } /** @@ -134,9 +180,11 @@ object Starbound : IVFS { } fun loadJson(path: String): JsonElement { - return JsonParser.parseReader(getReader(path)) + return JsonParser.parseReader(locate(path).reader()) } + fun readDirect(path: String) = locate(path).readDirect() + fun getTileDefinition(name: String) = tiles[name] private val initCallbacks = ArrayList<() -> Unit>() @@ -198,46 +246,6 @@ object Starbound : IVFS { }, "Asset Loader").start() } - override fun pathExists(path: String): Boolean { - for (fs in fileSystems) { - if (fs.pathExists(path)) { - return true - } - } - - return false - } - - override fun readOrNull(path: String): ByteBuffer? { - for (fs in fileSystems) { - if (fs.pathExists(path)) { - return fs.read(path) - } - } - - return null - } - - override fun listFiles(path: String): List { - val listing = mutableListOf() - - for (fs in fileSystems) { - listing.addAll(fs.listFiles(path)) - } - - return listing - } - - override fun listDirectories(path: String): Collection { - val listing = mutableListOf() - - for (fs in fileSystems) { - listing.addAll(fs.listDirectories(path)) - } - - return listing - } - fun onInitialize(callback: () -> Unit) { if (initialized) { callback() @@ -260,12 +268,12 @@ object Starbound : IVFS { readingFolder = "/tiles/materials" for (fs in fileSystems) { - for (listedFile in fs.listAllFilesWithExtension("material")) { + for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".material") }) { try { callback("Loading $listedFile") - readingFolder = getPathFolder(listedFile) - val tileDef = gson.fromJson(getReader(listedFile), TileDefinition::class.java) + readingFolder = listedFile.computeDirectory() + val tileDef = gson.fromJson(listedFile.reader(), TileDefinition::class.java) check(tiles[tileDef.materialName] == null) { "Already has material with name ${tileDef.materialName} loaded!" } check(tilesByMaterialID[tileDef.materialId] == null) { "Already has material with ID ${tileDef.materialId} loaded!" } @@ -275,6 +283,10 @@ object Starbound : IVFS { //throw TileDefLoadingException("Loading tile file $listedFile", err) LOGGER.error("Loading tile file $listedFile", err) } + + if (terminateLoading) { + return + } } } @@ -283,28 +295,32 @@ object Starbound : IVFS { private fun loadProjectiles(callback: (String) -> Unit) { for (fs in fileSystems) { - for (listedFile in fs.listAllFilesWithExtension("projectile")) { + for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".projectile") }) { try { callback("Loading $listedFile") - val def = gson.fromJson(getReader(listedFile), ConfigurableProjectile::class.java).assemble(getPathFolder(listedFile)) + val def = gson.fromJson(listedFile.reader(), ConfigurableProjectile::class.java).assemble(listedFile.computeDirectory()) check(projectiles[def.projectileName] == null) { "Already has projectile with ID ${def.projectileName} loaded!" } projectiles[def.projectileName] = def } catch(err: Throwable) { //throw ProjectileDefLoadingException("Loading projectile file $listedFile", err) LOGGER.error("Loading projectile file $listedFile", err) } + + if (terminateLoading) { + return + } } } } private fun loadFunctions(callback: (String) -> Unit) { for (fs in fileSystems) { - for (listedFile in fs.listAllFilesWithExtension("functions")) { + for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".functions") }) { try { callback("Loading $listedFile") - val readObject = loadJson(listedFile) as JsonObject + val readObject = JsonParser.parseReader(listedFile.reader()) as JsonObject for (key in readObject.keySet()) { val def = gson.fromJson(readObject[key], JsonFunction::class.java) @@ -313,22 +329,28 @@ object Starbound : IVFS { } catch(err: Throwable) { LOGGER.error("Loading function file $listedFile", err) } + + if (terminateLoading) { + return + } } } } private fun loadParallax(callback: (String) -> Unit) { for (fs in fileSystems) { - for (listedFile in fs.listAllFiles("parallax")) { - if (listedFile.endsWith(".parallax")) { - try { - callback("Loading $listedFile") + for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".parallax") }) { + try { + callback("Loading $listedFile") - val def = gson.fromJson(getReader(listedFile), ParallaxPrototype::class.java) - parallax[getPathFilename(listedFile).substringBefore('.')] = def - } catch(err: Throwable) { - LOGGER.error("Loading parallax file $listedFile", err) - } + val def = gson.fromJson(listedFile.reader(), ParallaxPrototype::class.java) + parallax[listedFile.name.substringBefore('.')] = def + } catch(err: Throwable) { + LOGGER.error("Loading parallax file $listedFile", err) + } + + if (terminateLoading) { + return } } } @@ -338,12 +360,12 @@ object Starbound : IVFS { readingFolder = "/tiles/materials" for (fs in fileSystems) { - for (listedFile in fs.listAllFilesWithExtension("matmod")) { + for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".matmod") }) { try { callback("Loading $listedFile") - readingFolder = getPathFolder(listedFile) - val tileDef = gson.fromJson(getReader(listedFile), MaterialModifier::class.java) + readingFolder = listedFile.computeDirectory() + val tileDef = gson.fromJson(listedFile.reader(), MaterialModifier::class.java) check(tileModifiers[tileDef.modName] == null) { "Already has material with name ${tileDef.modName} loaded!" } check(tileModifiersByID[tileDef.modId] == null) { "Already has material with ID ${tileDef.modId} loaded!" } @@ -353,6 +375,10 @@ object Starbound : IVFS { //throw TileDefLoadingException("Loading tile file $listedFile", err) LOGGER.error("Loading tile modifier file $listedFile", err) } + + if (terminateLoading) { + return + } } } @@ -361,12 +387,12 @@ object Starbound : IVFS { private fun loadLiquidDefinitions(callback: (String) -> Unit) { for (fs in fileSystems) { - for (listedFile in fs.listAllFilesWithExtension("liquid")) { + for (listedFile in fs.explore().filter { it.isFile }.filter { it.name.endsWith(".liquid") }) { try { callback("Loading $listedFile") - readingFolder = getPathFolder(listedFile) - val liquidDef = gson.fromJson(getReader(listedFile), LiquidDefinition::class.java) + readingFolder = listedFile.computeDirectory() + val liquidDef = gson.fromJson(listedFile.reader(), LiquidDefinition::class.java) check(liquid.put(liquidDef.name, liquidDef) == null) { "Already has liquid with name ${liquidDef.name} loaded!" } check(liquidByID.put(liquidDef.liquidId, liquidDef) == null) { "Already has liquid with ID ${liquidDef.liquidId} loaded!" } @@ -374,6 +400,10 @@ object Starbound : IVFS { //throw TileDefLoadingException("Loading tile file $listedFile", err) LOGGER.error("Loading liquid definition file $listedFile", err) } + + if (terminateLoading) { + return + } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/api/IStarboundFileSystem.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/api/IStarboundFileSystem.kt new file mode 100644 index 00000000..acf2db15 --- /dev/null +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/api/IStarboundFileSystem.kt @@ -0,0 +1,228 @@ +package ru.dbotthepony.kstarbound.api + +import com.google.common.collect.ImmutableMap +import com.google.gson.JsonElement +import com.google.gson.JsonParser +import ru.dbotthepony.kstarbound.Starbound +import ru.dbotthepony.kstarbound.io.StarboundPak +import ru.dbotthepony.kstarbound.stream +import java.io.* +import java.nio.ByteBuffer +import java.util.stream.Stream + +interface IStarboundFile { + val exists: Boolean + val isDirectory: Boolean + + /** + * null if root + */ + val parent: IStarboundFile? + val isFile: Boolean + + /** + * null if not a directory + */ + val children: Map? + val name: String + + fun orNull(): IStarboundFile? = if (exists) this else null + + fun computeFullPath(): String { + var path = name + var parent = parent + + while (parent != null) { + path = parent.name + "/" + path + parent = parent.parent + } + + return path + } + + fun computeDirectory(): String { + var path = "" + var parent = parent + + while (parent != null) { + if (path == "") + path = parent.name + else + path = parent.name + "/" + path + + parent = parent.parent + } + + return path + } + + fun locate(path: String): IStarboundFile { + @Suppress("name_shadowing") + val path = path.trim() + + if (path == "" || path == ".") { + return this + } + + if (path == "..") { + return parent ?: NonExistingFile(computeFullPath() + "/..") + } + + val split = path.lowercase().split("/") + var file = this + + for (splitIndex in split.indices) { + if (split[splitIndex].isEmpty()) { + continue + } + + val children = file.children ?: return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path) + val find = children[split[splitIndex]] + + if (find is StarboundPak.SBDirectory) { + file = find + } else if (find is StarboundPak.SBFile) { + if (splitIndex + 1 != split.size) { + return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path) + } + + return find + } else { + break + } + } + + return NonExistingFile(name = split.last(), fullPath = computeFullPath() + "/" + path) + } + + /** + * @throws IllegalStateException if file is a directory + * @throws FileNotFoundException if file does not exist + */ + fun open(): InputStream + + /** + * @throws IllegalStateException if file is a directory + * @throws FileNotFoundException if file does not exist + */ + fun reader(): Reader = InputStreamReader(open()) + + /** + * @throws IllegalStateException if file is a directory + * @throws FileNotFoundException if file does not exist + */ + fun readJson(): JsonElement = JsonParser.parseReader(reader()) + + /** + * @throws IllegalStateException if file is a directory + * @throws FileNotFoundException if file does not exist + */ + fun read(): ByteBuffer { + val stream = open() + val read = stream.readAllBytes() + stream.close() + return ByteBuffer.wrap(read) + } + + /** + * @throws IllegalStateException if file is a directory + * @throws FileNotFoundException if file does not exist + */ + fun readDirect(): ByteBuffer { + val read = read() + val buf = ByteBuffer.allocateDirect(read.capacity()) + + read.position(0) + + for (i in 0 until read.capacity()) { + buf.put(read[i]) + } + + buf.position(0) + + return buf + } + + /** + * non existent file, without any context + */ + companion object : IStarboundFile { + override val exists: Boolean + get() = false + override val isDirectory: Boolean + get() = false + override val parent: IStarboundFile? + get() = null + override val isFile: Boolean + get() = false + override val children: Map? + get() = null + override val name: String + get() = "" + + override fun open(): InputStream { + throw FileNotFoundException() + } + } +} + +class NonExistingFile( + override val name: String, + override val parent: IStarboundFile? = null, + val fullPath: String? = null +) : IStarboundFile { + override val isDirectory: Boolean + get() = false + override val isFile: Boolean + get() = false + override val children: Map? + get() = null + + override val exists: Boolean + get() = false + + override fun open(): InputStream { + throw FileNotFoundException("File ${fullPath ?: computeFullPath()} does not exist") + } +} + +fun IStarboundFile.explore(): Stream { + val children = children ?: return Stream.of(this) + return Stream.concat(Stream.of(this), children.values.stream().flatMap { it.explore() }) +} + +fun getPathFolder(path: String): String { + return path.substringBeforeLast('/') +} + +fun getPathFilename(path: String): String { + return path.substringAfterLast('/') +} + +class PhysicalFile(val real: File) : IStarboundFile { + override val exists: Boolean + get() = real.exists() + override val isDirectory: Boolean + get() = real.isDirectory + override val parent: PhysicalFile? + get() { + return PhysicalFile(real.parentFile ?: return null) + } + + override val isFile: Boolean + get() = real.isFile + override val children: Map? + get() { + return real.list()?.stream()?.map { it to PhysicalFile(File(it)) }?.collect(ImmutableMap.toImmutableMap({ it.first }, { it.second })) + } + override val name: String + get() = real.name + + override fun open(): InputStream { + return BufferedInputStream(real.inputStream()) + } + + override fun toString(): String { + return "PhysicalFile[$real]" + } +} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt deleted file mode 100644 index 4d26e9a7..00000000 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/api/IVFS.kt +++ /dev/null @@ -1,164 +0,0 @@ -package ru.dbotthepony.kstarbound.api - -import java.io.* -import java.nio.ByteBuffer - -interface IVFS { - fun pathExists(path: String): Boolean - - fun pathExistsOrElse(path: String, orElse: String): String { - if (pathExists(path)) - return path - - return orElse - } - - fun firstExisting(vararg pathList: String): String { - for (path in pathList) - if (pathExists(path)) - return path - - throw FileNotFoundException("Unable to find any of files specified") - } - - fun firstExistingOrNull(vararg pathList: String): String? { - for (path in pathList) - if (pathExists(path)) - return path - - return null - } - - fun read(path: String): ByteBuffer { - return readOrNull(path) ?: throw FileNotFoundException("No such file $path") - } - - fun readOrNull(path: String): ByteBuffer? - - fun readString(path: String): String { - return read(path).array().toString(Charsets.UTF_8) - } - - fun getReader(path: String): Reader { - return InputStreamReader(ByteArrayInputStream(read(path).array())) - } - - fun listFiles(path: String): Collection - fun listDirectories(path: String): Collection - - fun listFilesAndDirectories(path: String): Collection { - val a = listFiles(path) - val b = listDirectories(path) - - return ArrayList(a.size + b.size).also { it.addAll(a); it.addAll(b) } - } - - fun listAllFilesWithExtension(extension: String): Collection { - val listing = ArrayList() - val ext = ".$extension" - - for (listedFile in listAllFiles("")) { - if (listedFile.endsWith(ext)) { - listing.add(listedFile) - } - } - - return listing - } - - fun listAllFiles(path: String): Collection { - val lists = mutableListOf>() - - lists.add(listFiles(path)) - - for (dir in listDirectories(path)) { - lists.add(listAllFiles(dir)) - } - - // flatten медленный - // return lists.flatten() - - var size = 0 - - for (list in lists) { - size += list.size - } - - return ArrayList(size).also { lists.forEach(it::addAll) } - } - - fun readDirect(path: String): ByteBuffer { - val read = read(path) - - val buf = ByteBuffer.allocateDirect(read.capacity()) - - read.position(0) - - for (i in 0 until read.capacity()) { - buf.put(read[i]) - } - - buf.position(0) - - return buf - } -} - -fun getPathFolder(path: String): String { - return path.substringBeforeLast('/') -} - -fun getPathFilename(path: String): String { - return path.substringAfterLast('/') -} - -class PhysicalFS(root: File) : IVFS { - val root: File = root.absoluteFile - - override fun pathExists(path: String): Boolean { - if (path.contains("..")) { - return false - } - - return File(root.absolutePath, path).exists() - } - - override fun readOrNull(path: String): ByteBuffer? { - if (path.contains("..")) { - return null - } - - val fpath = File(root.path, path) - - if (!fpath.exists()) { - return null - } - - val reader = fpath.inputStream() - val buf = ByteBuffer.allocate(reader.channel.size().toInt()) - reader.read(buf.array()) - return buf - } - - override fun listFiles(path: String): List { - if (path.contains("..")) { - return listOf() - } - - val fpath = File(root.absolutePath, path) - return fpath.listFiles()?.filter { it.isFile }?.map { - it.path.replace('\\', '/').substring(root.path.length) - } ?: return listOf() - } - - override fun listDirectories(path: String): Collection { - if (path.contains("..")) { - return listOf() - } - - val fpath = File(root.absolutePath, path) - return fpath.listFiles()?.filter { it.isDirectory }?.map { - it.path.replace('\\', '/').substring(root.path.length) - } ?: return listOf() - } -} diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt index 77f6b19a..bb396e73 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/client/gl/GLStateTracker.kt @@ -445,7 +445,7 @@ class GLStateTracker { fun loadNamedTexture(path: String, memoryFormat: Int, fileFormat: Int): GLTexture2D { return named2DTextures.computeIfAbsent(path) { - if (!Starbound.pathExists(path)) { + if (!Starbound.exists(path)) { throw FileNotFoundException("Unable to locate $path") } @@ -455,7 +455,7 @@ class GLStateTracker { fun loadNamedTexture(path: String): GLTexture2D { return named2DTextures.computeIfAbsent(path) { - if (!Starbound.pathExists(path)) { + if (!Starbound.exists(path)) { throw FileNotFoundException("Unable to locate $path") } @@ -473,7 +473,7 @@ class GLStateTracker { } return named2DTextures.computeIfAbsent(path) { - if (!Starbound.pathExists(path)) { + if (!Starbound.exists(path)) { LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath) return@computeIfAbsent named2DTextures[missingTexturePath]!! } @@ -489,7 +489,7 @@ class GLStateTracker { } return named2DTextures.computeIfAbsent(path) { - if (!Starbound.pathExists(path)) { + if (!Starbound.exists(path)) { LOGGER.error("Texture {} is missing! Falling back to {}", path, missingTexturePath) return@computeIfAbsent named2DTextures[missingTexturePath]!! } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt index 9d98dfda..c5fc2316 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/FrameGrid.kt @@ -266,7 +266,7 @@ interface IFrameGrid { if (splitLast.size == 1) { // имя уже абсолютное return cache.computeIfAbsent(path) { - val frames = Starbound.firstExistingOrNull("$path.frames", "${splitPath.joinToString("/")}/default.frames") + val frames = Starbound.locate("$path.frames", "${splitPath.joinToString("/")}/default.frames").orNull() if (weak && frames == null) { LOGGER.warn("Expected animated texture at {}, but .frames metafile is missing.", path) @@ -274,14 +274,14 @@ interface IFrameGrid { return@computeIfAbsent singleFrame("$path.png", weakSize) } - return@computeIfAbsent fromJson(Starbound.loadJson(frames ?: throw FileNotFoundException("Unable to find .frames meta for $path")) as JsonObject, path) + return@computeIfAbsent fromJson((frames?.readJson() ?: throw FileNotFoundException("Unable to find .frames meta for $path")) as JsonObject, path) } } val newPath = "${splitPath.joinToString("/")}/${splitLast[0]}" return cache.computeIfAbsent(newPath) { - val frames = Starbound.firstExistingOrNull("$newPath.frames", "${splitPath.joinToString("/")}/default.frames") + val frames = Starbound.locate("$newPath.frames", "${splitPath.joinToString("/")}/default.frames").orNull() if (weak && frames == null) { LOGGER.warn("Expected animated texture at {}, but .frames metafile is missing.", newPath) @@ -289,7 +289,7 @@ interface IFrameGrid { return@computeIfAbsent singleFrame("$newPath.png", weakSize) } - return@computeIfAbsent fromJson(Starbound.loadJson(frames ?: throw FileNotFoundException("Unable to find .frames meta for $path")) as JsonObject, newPath) + return@computeIfAbsent fromJson((frames?.readJson() ?: throw FileNotFoundException("Unable to find .frames meta for $path")) as JsonObject, newPath) } } catch (err: Throwable) { throw MalformedFrameGridException("Reading animated texture definition $path", err) diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt index 1ae90871..a6208e32 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/projectile/Configurable.kt @@ -172,7 +172,7 @@ class ActionConfig : IConfigurableAction { override fun configure(directory: String): IActionOnReap { return cache.computeIfAbsent(ensureAbsolutePath(file, directory)) { - if (!Starbound.pathExists(it)) { + if (!Starbound.exists(it)) { LOGGER.error("Config $it does not exist") return@computeIfAbsent CActionConfig(file, null) } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt index b39c351b..3c8e9ff1 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/defs/tile/RenderTemplate.kt @@ -300,7 +300,7 @@ data class RenderTemplate( } return cache.computeIfAbsent(path) { - return@computeIfAbsent Starbound.gson.fromJson(Starbound.getReader(it), RenderTemplate::class.java) + return@computeIfAbsent Starbound.gson.fromJson(Starbound.locate(it).reader(), RenderTemplate::class.java) } } } diff --git a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt index 6a12c565..f5a2ffdd 100644 --- a/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt +++ b/src/main/kotlin/ru/dbotthepony/kstarbound/io/StarboundPak.kt @@ -1,134 +1,137 @@ package ru.dbotthepony.kstarbound.io -import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap import it.unimi.dsi.fastutil.objects.Object2ObjectFunction -import ru.dbotthepony.kstarbound.api.IVFS -import java.io.* +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import ru.dbotthepony.kstarbound.api.IStarboundFile +import java.io.BufferedInputStream +import java.io.Closeable +import java.io.DataInputStream +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.RandomAccessFile import java.nio.ByteBuffer import java.nio.channels.Channels import java.util.* -import kotlin.collections.ArrayList -import kotlin.collections.HashMap private fun readHeader(reader: RandomAccessFile, required: Int) { val read = reader.read() require(read == required) { "Bad Starbound Pak header, expected $required, got $read" } } -class StarboundPakFile( - val storage: StarboundPak, - val name: String, - val offset: Long, - val length: Long -) { - val directoryName: String - val directoryHiearchy: Array - val fileName: String +class StarboundPak(val path: File, callback: (finished: Boolean, status: String) -> Unit = { _, _ -> }) : Closeable { + internal inner class SBDirectory( + override val name: String, + override val parent: IStarboundFile?, + ) : IStarboundFile { + override val exists: Boolean + get() = true - init { - val split = name.substring(1).split('/').toMutableList() - fileName = split.last() - split.removeAt(split.size - 1) - directoryHiearchy = split.toTypedArray() - directoryName = split.joinToString("/") - } + override val isDirectory: Boolean + get() = true - fun read(): ByteBuffer { - val buf = ByteBuffer.allocate(length.toInt()) - storage.reader.seek(offset) - storage.reader.readFully(buf.array()) - return buf - } + override val isFile: Boolean + get() = false - override fun toString(): String { - return name - } + private var frozen = false + private val innerChildren = Object2ObjectOpenHashMap() - fun readString(): String { - return read().array().toString(Charsets.UTF_8) - } - - companion object { - fun read(storage: StarboundPak, reader: RandomAccessFile): StarboundPakFile { - val readLength = reader.read() - //val name = reader.readASCIIString(readLength).lowercase() - val name = reader.readASCIIString(readLength) - require(name[0] == '/') { "$name appears to be not an absolute filename" } - val offset = reader.readLong() - val length = reader.readLong() - - return StarboundPakFile(storage, name.intern(), offset, length) + fun put(value: IStarboundFile) { + check(!frozen) { "Can't put, already frozen!" } + innerChildren[value.name] = value } - fun read(storage: StarboundPak, reader: DataInputStream): StarboundPakFile { - val readLength = reader.readVarInt() - val name = reader.readASCIIString(readLength) - require(name[0] == '/') { "$name appears to be not an absolute filename" } - val offset = reader.readLong() - val length = reader.readLong() - - return StarboundPakFile(storage, name.intern(), offset, length) + fun subdir(name: String): SBDirectory { + check(!frozen) { "Can't subdir, already frozen!" } + require(name != "") { "Empty directory name provided" } + return innerChildren.computeIfAbsent(name, Object2ObjectFunction { SBDirectory(it as String, this) }) as? SBDirectory ?: throw IllegalStateException("$name already exists (in ${computeFullPath()})") } - } -} -class StarboundPakDirectory(val name: String, val parent: StarboundPakDirectory? = null) { - val files = HashMap() - val directories = HashMap() + override val children: Map = Collections.unmodifiableMap(innerChildren) - fun resolve(path: Array, level: Int = 0): StarboundPakDirectory { - if (path.size == level) - return this + fun freeze() { + check(!frozen) { "Already frozen" } + frozen = true - if (level == 0 && path[0] == "" && name == "/") - return resolve(path, 1) - - if (directories.containsKey(path[level])) { - return directories[path[level]]!!.resolve(path, level + 1) - } else { - val dir = StarboundPakDirectory(path[level], this) - directories[path[level]] = dir - return dir.resolve(path, level + 1) - } - } - - fun getFile(name: String) = files[name] - fun getDirectory(name: String) = directories[name] - - fun listFiles(): Collection = Collections.unmodifiableCollection(files.values) - fun listDirectories(): Collection = Collections.unmodifiableCollection(directories.values) - - fun writeFile(file: StarboundPakFile) { - files[file.name.split('/').last()] = file - } - - fun fullName(): String { - var build = name - var getParent = parent - - while (getParent != null) { - if (getParent.parent != null) { - build = "${getParent.name}/$build" - } else { - build = "/$build" - break + for (children in innerChildren.values) { + if (children is SBDirectory) { + children.freeze() + } } - - getParent = getParent.parent } - return build + override fun open(): InputStream { + throw IllegalStateException("${computeFullPath()} is a directory") + } + + override fun toString(): String { + return "SBDirectory[${computeFullPath()} @ $path]" + } } - override fun toString(): String { - return fullName() - } -} + internal inner class SBFile( + override val name: String, + override val parent: IStarboundFile?, + val offset: Long, + val length: Long + ) : IStarboundFile { + override val exists: Boolean + get() = true + + override val isDirectory: Boolean + get() = false + + override val isFile: Boolean + get() = true + + override val children: Map? + get() = null + + override fun open(): InputStream { + return object : InputStream() { + private var innerOffset = 0L + + override fun read(): Int { + if (innerOffset >= length) { + return -1 + } + + reader.seek(innerOffset + offset) + innerOffset++ + return reader.read() + } + + override fun read(b: ByteArray, off: Int, len: Int): Int { + Objects.checkFromIndexSize(off, len, b.size) + + // ok + if (len == 0) + return 0 + + val readMax = len.coerceAtMost((length - innerOffset).toInt()) + + if (readMax <= 0) + return -1 + + reader.seek(innerOffset + offset) + val readBytes = reader.read(b, off, readMax) + + if (readBytes == -1) { + throw RuntimeException("Unexpected EOF, want to read $readMax bytes from starting $offset in $path") + } + + innerOffset += readBytes + return readBytes + } + } + } + + override fun toString(): String { + return "SBFile[${computeFullPath()} @ $path]" + } + } -class StarboundPak(val path: File, callback: (finished: Boolean, status: String) -> Unit = { _, _ -> }) : Closeable, IVFS { val reader = RandomAccessFile(path, "r") - private val filesByExtension = Object2ObjectArrayMap>() - private val filesByExtensionPath = Object2ObjectArrayMap>() init { readHeader(reader, 0x53) // S @@ -163,75 +166,60 @@ class StarboundPak(val path: File, callback: (finished: Boolean, status: String) // сразу за метаданными идёт количество файлов внутри данного pak в формате Big Endian variable int val indexNodeCount = reader.readVarLong() - private val _indexNodes = HashMap() - val indexNodes: Map = Collections.unmodifiableMap(_indexNodes) - private val _indexNodesLowercase = HashMap() - val indexNodesLowercase: Map = Collections.unmodifiableMap(_indexNodesLowercase) - val root = StarboundPakDirectory("/") + val root: IStarboundFile = SBDirectory("", null) init { // Сразу же за количеством файлов идут сами файлы в формате - // byte (длинна имени файла) + // VarInt (длинна имени файла) // byte[] (utf-8 имя файла) // long (offset от начала файла) - // long (длинна файла) + // long (длина файла) val stream = DataInputStream(BufferedInputStream(Channels.newInputStream(reader.channel))) for (i in 0 until indexNodeCount) { + var name: String? = null + try { callback(false, "Reading index node $i") - val read = StarboundPakFile.read(this, stream) - if (read.offset > reader.length()) { - throw IndexOutOfBoundsException("Garbage offset at index $i: ${read.offset}") + val readLength = stream.readVarInt() + name = stream.readASCIIString(readLength) + require(name[0] == '/') { "index node at $i with '$name' appears to be not an absolute filename" } + val offset = stream.readLong() + val length = stream.readLong() + + if (offset > reader.length()) { + throw IndexOutOfBoundsException("Garbage offset at index $i: ${offset}") } - if (read.length > reader.length()) { - throw IndexOutOfBoundsException("Garbage length at index $i: ${read.length}") + if (length + offset > reader.length()) { + throw IndexOutOfBoundsException("Garbage offset + length at index $i: ${length + offset}") } - _indexNodes[read.name] = read // Starbound игнорирует регистр букв когда ищет пути, даже внутри pak архивов - _indexNodesLowercase[read.name.lowercase()] = read + val split = name.lowercase().split("/") + var parent = root as SBDirectory - root.resolve(read.directoryHiearchy).writeFile(read) - - val last = read.name.substringAfterLast('/').substringAfterLast('.', "") - - if (last != "") { - filesByExtension.computeIfAbsent(last, Object2ObjectFunction { ArrayList() }).add(read) - filesByExtensionPath.computeIfAbsent(last, Object2ObjectFunction { ArrayList() }).add(read.name) + for (splitIndex in 1 until split.size - 1) { + parent = parent.subdir(split[splitIndex]) } + + parent.put(SBFile(split.last(), parent, offset, length)) } catch (err: Throwable) { - throw IOException("Reading index node at $i", err) + if (name == null) { + throw IOException("Reading index node at $i", err) + } else { + throw IOException("Reading index node at $i ($name)", err) + } } } + callback(false, "Freezing virtual file system") + (root as SBDirectory).freeze() callback(true, "Reading indexes finished") } - override fun listFiles(path: String): Collection { - return root.resolve(path.split("/").toTypedArray()).listFiles().map { it.name } - } - - override fun listDirectories(path: String): Collection { - return root.resolve(path.split("/").toTypedArray()).listDirectories().map { it.fullName() } - } - - override fun pathExists(path: String): Boolean { - return _indexNodesLowercase.containsKey(path) - } - - override fun readOrNull(path: String): ByteBuffer? { - val node = _indexNodesLowercase[path] ?: return null - return node.read() - } - override fun close() { reader.close() } - - override fun listAllFilesWithExtension(extension: String): Collection { - return filesByExtensionPath[extension]?.let(Collections::unmodifiableList) ?: listOf() - } }