mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-03-07 20:31:51 +09:00
369 lines
14 KiB
Kotlin
369 lines
14 KiB
Kotlin
package net.torvald.terrarum
|
|
|
|
import com.badlogic.gdx.Gdx
|
|
import com.badlogic.gdx.files.FileHandle
|
|
import net.torvald.terrarum.App.*
|
|
import net.torvald.terrarum.blockproperties.BlockCodex
|
|
import net.torvald.terrarum.blockproperties.WireCodex
|
|
import net.torvald.terrarum.gameitems.GameItem
|
|
import net.torvald.terrarum.gameitems.ItemID
|
|
import net.torvald.terrarum.itemproperties.ItemCodex
|
|
import net.torvald.terrarum.itemproperties.MaterialCodex
|
|
import net.torvald.terrarum.langpack.Lang
|
|
import net.torvald.terrarum.savegame.ByteArray64GrowableOutputStream
|
|
import net.torvald.terrarum.savegame.ByteArray64OutputStream
|
|
import net.torvald.terrarum.savegame.ByteArray64Reader
|
|
import net.torvald.terrarum.utils.CSVFetcher
|
|
import net.torvald.terrarum.utils.JsonFetcher
|
|
import org.apache.commons.csv.CSVFormat
|
|
import org.apache.commons.csv.CSVParser
|
|
import org.apache.commons.csv.CSVRecord
|
|
import java.io.*
|
|
import java.net.URL
|
|
import java.net.URLClassLoader
|
|
import java.nio.file.FileSystems
|
|
import java.util.*
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Modules (or Mods) Resource Manager
|
|
*
|
|
*
|
|
* NOTE!!: Usage of Groovy is only temporary; if Kotlin's "JSR 223" is no longer experimental and
|
|
* is readily available, ditch that Groovy.
|
|
*
|
|
*
|
|
* Created by minjaesong on 2017-04-17.
|
|
*/
|
|
object ModMgr {
|
|
|
|
val metaFilename = "metadata.properties"
|
|
val defaultConfigFilename = "default.json"
|
|
|
|
data class ModuleMetadata(
|
|
val order: Int,
|
|
val isDir: Boolean,
|
|
val properName: String,
|
|
val description: String,
|
|
val author: String,
|
|
val packageName: String,
|
|
val entryPoint: String,
|
|
val releaseDate: String,
|
|
val version: String,
|
|
val jar: String,
|
|
val dependencies: Array<String>
|
|
) {
|
|
override fun toString() =
|
|
"\tModule #$order -- $properName | $version | $author\n" +
|
|
"\t$description | $releaseDate\n" +
|
|
"\tEntry point: $entryPoint\n" +
|
|
"\tJarfile: $jar\n" +
|
|
"\tDependencies: ${dependencies.joinToString("\n\t")}"
|
|
}
|
|
const val modDir = "./assets/mods"
|
|
|
|
/** Module name (directory name), ModuleMetadata */
|
|
val moduleInfo = HashMap<String, ModuleMetadata>()
|
|
val entryPointClasses = ArrayList<ModuleEntryPoint>()
|
|
|
|
val moduleClassloader = HashMap<String, URLClassLoader>()
|
|
|
|
val loadOrder = ArrayList<String>()
|
|
|
|
init {
|
|
// load modules
|
|
val loadOrderCSVparser = CSVParser.parse(
|
|
FileSystems.getDefault().getPath("$modDir/LoadOrder.csv").toFile(),
|
|
Charsets.UTF_8,
|
|
CSVFormat.DEFAULT.withCommentMarker('#')
|
|
)
|
|
val loadOrder = loadOrderCSVparser.records
|
|
loadOrderCSVparser.close()
|
|
|
|
|
|
loadOrder.forEachIndexed { index, it ->
|
|
val moduleName = it[0]
|
|
this.loadOrder.add(moduleName)
|
|
printmsg(this, "Loading module $moduleName")
|
|
|
|
try {
|
|
val modMetadata = Properties()
|
|
modMetadata.load(FileInputStream("$modDir/$moduleName/$metaFilename"))
|
|
|
|
if (File("$modDir/$moduleName/$defaultConfigFilename").exists()) {
|
|
val defaultConfig = JsonFetcher("$modDir/$moduleName/$defaultConfigFilename")
|
|
// read config and store it to the game
|
|
|
|
// write to user's config file
|
|
}
|
|
|
|
|
|
|
|
val properName = modMetadata.getProperty("propername")
|
|
val description = modMetadata.getProperty("description")
|
|
val author = modMetadata.getProperty("author")
|
|
val packageName = modMetadata.getProperty("package")
|
|
val entryPoint = modMetadata.getProperty("entrypoint")
|
|
val releaseDate = modMetadata.getProperty("releasedate")
|
|
val version = modMetadata.getProperty("version")
|
|
val jar = modMetadata.getProperty("jar")
|
|
val dependency = modMetadata.getProperty("dependency").split(Regex(""";[ ]*""")).toTypedArray()
|
|
val isDir = FileSystems.getDefault().getPath("$modDir/$moduleName").toFile().isDirectory
|
|
moduleInfo[moduleName] = ModuleMetadata(index, isDir, properName, description, author, packageName, entryPoint, releaseDate, version, jar, dependency)
|
|
|
|
printdbg(this, moduleInfo[moduleName])
|
|
|
|
|
|
// run entry script in entry point
|
|
if (entryPoint.isNotBlank()) {
|
|
var newClass: Class<*>? = null
|
|
try {
|
|
|
|
// FIXME creating new classloader somehow messes up with the default classloader??!!?
|
|
// if CommandDict fails with NullPointerException, it means it's trying to use a wrong classloader
|
|
// bypassing only reveals further issues related to the classloader, such as loading wrong copy if ItemCodex
|
|
// that does not contain entry "basegame:0"
|
|
|
|
// for modules that has JAR defined
|
|
// if (jar.isNotBlank()) {
|
|
// val child = URLClassLoader(arrayOf<URL>(File("$modDir/$moduleName/$jar").toURI().toURL()),
|
|
// this.javaClass.classLoader
|
|
// )
|
|
// moduleClassloader[moduleName] = child
|
|
// newClass = Class.forName(entryPoint, true, child)
|
|
// }
|
|
// for modules that are not (meant to be used by the "basegame" kind of modules)
|
|
// else {
|
|
newClass = Class.forName(entryPoint)
|
|
// }
|
|
}
|
|
catch (e: Throwable) {
|
|
printdbgerr(this, "$moduleName failed to load, skipping...")
|
|
printdbgerr(this, "\t$e")
|
|
val ba = ByteArray64GrowableOutputStream()
|
|
val sw = PrintStream(ba, true)
|
|
e.printStackTrace(sw)
|
|
val bw = ByteArray64Reader(ba.toByteArray64(), Charsets.UTF_8)
|
|
printdbgerr(this, bw.readText())
|
|
moduleInfo.remove(moduleName)
|
|
}
|
|
|
|
newClass?.let {
|
|
val newClassConstructor = newClass.getConstructor(/* no args defined */)
|
|
val newClassInstance = newClassConstructor.newInstance(/* no args defined */)
|
|
|
|
entryPointClasses.add(newClassInstance as ModuleEntryPoint)
|
|
(newClassInstance as ModuleEntryPoint).invoke()
|
|
}
|
|
|
|
}
|
|
|
|
|
|
printdbg(this, "$moduleName loaded successfully")
|
|
}
|
|
catch (noSuchModule: FileNotFoundException) {
|
|
printdbgerr(this, "No such module: $moduleName, skipping...")
|
|
moduleInfo.remove(moduleName)
|
|
}
|
|
catch (e: Throwable) {
|
|
printdbgerr(this, "There was an error while loading module $moduleName")
|
|
printdbgerr(this, "\t$e")
|
|
print(App.csiR); e.printStackTrace(System.out); print(App.csi0)
|
|
moduleInfo.remove(moduleName)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
operator fun invoke() { }
|
|
|
|
/*fun reloadModules() {
|
|
loadOrder.forEach {
|
|
val moduleName = it
|
|
|
|
printmsg(this, "Reloading module $moduleName")
|
|
|
|
try {
|
|
checkExistence(moduleName)
|
|
val modMetadata = moduleInfo[it]!!
|
|
val entryPoint = modMetadata.entryPoint
|
|
|
|
|
|
// run entry script in entry point
|
|
if (entryPoint.isNotBlank()) {
|
|
var newClass: Class<*>? = null
|
|
try {
|
|
newClass = Class.forName(entryPoint)
|
|
}
|
|
catch (e: ClassNotFoundException) {
|
|
printdbgerr(this, "$moduleName has nonexisting entry point, skipping...")
|
|
printdbgerr(this, "\t$e")
|
|
moduleInfo.remove(moduleName)
|
|
}
|
|
|
|
newClass?.let {
|
|
val newClassConstructor = newClass!!.getConstructor(/* no args defined */)
|
|
val newClassInstance = newClassConstructor.newInstance(/* no args defined */)
|
|
|
|
entryPointClasses.add(newClassInstance as ModuleEntryPoint)
|
|
(newClassInstance as ModuleEntryPoint).invoke()
|
|
}
|
|
}
|
|
|
|
printdbg(this, "$moduleName reloaded successfully")
|
|
}
|
|
catch (noSuchModule: FileNotFoundException) {
|
|
printdbgerr(this, "No such module: $moduleName, skipping...")
|
|
moduleInfo.remove(moduleName)
|
|
}
|
|
catch (e: Throwable) {
|
|
printdbgerr(this, "There was an error while loading module $moduleName")
|
|
printdbgerr(this, "\t$e")
|
|
print(App.csiR); e.printStackTrace(System.out); print(App.csi0)
|
|
moduleInfo.remove(moduleName)
|
|
}
|
|
}
|
|
}*/
|
|
|
|
private fun checkExistence(module: String) {
|
|
if (!moduleInfo.containsKey(module))
|
|
throw FileNotFoundException("No such module: $module")
|
|
}
|
|
private fun String.sanitisePath() = if (this[0] == '/' || this[0] == '\\')
|
|
this.substring(1..this.lastIndex)
|
|
else this
|
|
|
|
|
|
|
|
fun getPath(module: String, path: String): String {
|
|
checkExistence(module)
|
|
return "$modDir/$module/${path.sanitisePath()}"
|
|
}
|
|
/** Returning files are read-only */
|
|
fun getGdxFile(module: String, path: String): FileHandle {
|
|
return Gdx.files.internal(getPath(module, path))
|
|
}
|
|
fun getFile(module: String, path: String): File {
|
|
checkExistence(module)
|
|
return FileSystems.getDefault().getPath(getPath(module, path)).toFile()
|
|
}
|
|
fun getFiles(module: String, path: String): Array<File> {
|
|
checkExistence(module)
|
|
val dir = FileSystems.getDefault().getPath(getPath(module, path)).toFile()
|
|
if (!dir.isDirectory) {
|
|
throw FileNotFoundException("The path is not a directory")
|
|
}
|
|
else {
|
|
return dir.listFiles()
|
|
}
|
|
}
|
|
|
|
/** Get a common file (literal file or directory) from all the installed mods. Files are guaranteed to exist. If a mod does not
|
|
* contain the file, the mod will be skipped.
|
|
*
|
|
* @return List of pairs<modname, file>
|
|
*/
|
|
fun getFilesFromEveryMod(path: String): List<Pair<String, File>> {
|
|
val path = path.sanitisePath()
|
|
val moduleNames = moduleInfo.keys.toList()
|
|
|
|
val filesList = ArrayList<Pair<String, File>>()
|
|
moduleNames.forEach {
|
|
val file = File(getPath(it, path))
|
|
|
|
if (file.exists()) filesList.add(it to file)
|
|
}
|
|
|
|
return filesList.toList()
|
|
}
|
|
|
|
/** Get a common file (literal file or directory) from all the installed mods. Files are guaranteed to exist. If a mod does not
|
|
* contain the file, the mod will be skipped.
|
|
*
|
|
* Returning files are read-only.
|
|
* @return List of pairs<modname, filehandle>
|
|
*/
|
|
fun getGdxFilesFromEveryMod(path: String): List<Pair<String, FileHandle>> {
|
|
val path = path.sanitisePath()
|
|
val moduleNames = moduleInfo.keys.toList()
|
|
|
|
val filesList = ArrayList<Pair<String, FileHandle>>()
|
|
moduleNames.forEach {
|
|
val file = Gdx.files.internal(getPath(it, path))
|
|
|
|
if (file.exists()) filesList.add(it to file)
|
|
}
|
|
|
|
return filesList.toList()
|
|
}
|
|
|
|
fun disposeMods() {
|
|
entryPointClasses.forEach { it.dispose() }
|
|
}
|
|
|
|
|
|
object GameBlockLoader {
|
|
@JvmStatic operator fun invoke(module: String) {
|
|
Terrarum.blockCodex = BlockCodex(module, "blocks/blocks.csv")
|
|
Terrarum.wireCodex = WireCodex(module, "wires/")
|
|
}
|
|
}
|
|
|
|
object GameItemLoader {
|
|
val itemPath = "items/"
|
|
|
|
@JvmStatic operator fun invoke(module: String) {
|
|
register(module, CSVFetcher.readFromModule(module, itemPath + "itemid.csv"))
|
|
}
|
|
|
|
fun fromCSV(module: String, csvString: String) {
|
|
val csvParser = org.apache.commons.csv.CSVParser.parse(
|
|
csvString,
|
|
CSVFetcher.terrarumCSVFormat
|
|
)
|
|
val csvRecordList = csvParser.records
|
|
csvParser.close()
|
|
register(module, csvRecordList)
|
|
}
|
|
|
|
private fun register(module: String, csv: List<CSVRecord>) {
|
|
Terrarum.itemCodex = ItemCodex()
|
|
|
|
csv.forEach {
|
|
val className: String = it["classname"].toString()
|
|
val internalID: Int = it["id"].toInt()
|
|
val itemName: String = "item@$module:$internalID"
|
|
|
|
printdbg(this, "Reading item ${itemName} <<- internal #$internalID with className $className")
|
|
|
|
val loadedClass = if (moduleClassloader[module] != null)
|
|
Class.forName(className, true, moduleClassloader[module])
|
|
else
|
|
Class.forName(className)
|
|
val loadedClassConstructor = loadedClass.getConstructor(ItemID::class.java)
|
|
val loadedClassInstance = loadedClassConstructor.newInstance(itemName)
|
|
|
|
ItemCodex[itemName] = loadedClassInstance as GameItem
|
|
}
|
|
}
|
|
}
|
|
|
|
object GameLanguageLoader {
|
|
val langPath = "locales/"
|
|
|
|
@JvmStatic operator fun invoke(module: String) {
|
|
Lang.load(getPath(module, langPath))
|
|
}
|
|
}
|
|
|
|
object GameMaterialLoader {
|
|
val matePath = "materials/"
|
|
|
|
@JvmStatic operator fun invoke(module: String) {
|
|
Terrarum.materialCodex = MaterialCodex(module, matePath + "materials.csv")
|
|
}
|
|
}
|
|
} |