object oriented file system skeleton

so there at least some preparation for java nio filesystem
This commit is contained in:
DBotThePony 2022-11-24 15:30:47 +07:00
parent 6cc7ae51a0
commit e8eed40a73
Signed by: DBot
GPG Key ID: DCC23B5715498507
9 changed files with 480 additions and 394 deletions

View File

@ -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 <reified T> GsonBuilder.registerTypeAdapter(adapter: TypeAdapter<T>): GsonBuilder {
return registerTypeAdapter(T::class.java, adapter)
}
fun <T> Array<T>.stream(): Stream<T> = Arrays.stream(this)

View File

@ -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<String>()
@ -110,20 +109,67 @@ object Starbound : IVFS {
private set
var initialized = false
private set
@Volatile
var terminateLoading = false
private val archivePaths = ArrayList<File>()
private val fileSystems = ArrayList<IVFS>()
private val fileSystems = ArrayList<IStarboundFile>()
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<String> {
val listing = mutableListOf<String>()
for (fs in fileSystems) {
listing.addAll(fs.listFiles(path))
}
return listing
}
override fun listDirectories(path: String): Collection<String> {
val listing = mutableListOf<String>()
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
}
}
}

View File

@ -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<String, IStarboundFile>?
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<String, IStarboundFile>?
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<String, IStarboundFile>?
get() = null
override val exists: Boolean
get() = false
override fun open(): InputStream {
throw FileNotFoundException("File ${fullPath ?: computeFullPath()} does not exist")
}
}
fun IStarboundFile.explore(): Stream<IStarboundFile> {
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<String, PhysicalFile>?
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]"
}
}

View File

@ -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<String>
fun listDirectories(path: String): Collection<String>
fun listFilesAndDirectories(path: String): Collection<String> {
val a = listFiles(path)
val b = listDirectories(path)
return ArrayList<String>(a.size + b.size).also { it.addAll(a); it.addAll(b) }
}
fun listAllFilesWithExtension(extension: String): Collection<String> {
val listing = ArrayList<String>()
val ext = ".$extension"
for (listedFile in listAllFiles("")) {
if (listedFile.endsWith(ext)) {
listing.add(listedFile)
}
}
return listing
}
fun listAllFiles(path: String): Collection<String> {
val lists = mutableListOf<Collection<String>>()
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<String>(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<String> {
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<String> {
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()
}
}

View File

@ -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]!!
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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<String>
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<String, IStarboundFile>()
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<String, StarboundPakFile>()
val directories = HashMap<String, StarboundPakDirectory>()
override val children: Map<String, IStarboundFile> = Collections.unmodifiableMap(innerChildren)
fun resolve(path: Array<String>, 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<StarboundPakFile> = Collections.unmodifiableCollection(files.values)
fun listDirectories(): Collection<StarboundPakDirectory> = 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<String, IStarboundFile>?
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<String, ArrayList<StarboundPakFile>>()
private val filesByExtensionPath = Object2ObjectArrayMap<String, ArrayList<String>>()
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<String, StarboundPakFile>()
val indexNodes: Map<String, StarboundPakFile> = Collections.unmodifiableMap(_indexNodes)
private val _indexNodesLowercase = HashMap<String, StarboundPakFile>()
val indexNodesLowercase: Map<String, StarboundPakFile> = 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<String> {
return root.resolve(path.split("/").toTypedArray()).listFiles().map { it.name }
}
override fun listDirectories(path: String): Collection<String> {
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<String> {
return filesByExtensionPath[extension]?.let(Collections::unmodifiableList) ?: listOf()
}
}