package net.torvald.terrarum import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.graphics.Pixmap import com.badlogic.gdx.utils.JsonValue import net.torvald.terrarum.App.* import net.torvald.terrarum.App.setToGameConfig import net.torvald.terrarum.blockproperties.BlockCodex import net.torvald.terrarum.blockproperties.OreCodex import net.torvald.terrarum.blockproperties.WireCodex import net.torvald.terrarum.gamecontroller.IME 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.modulebasegame.worldgenerator.OregenParams import net.torvald.terrarum.modulebasegame.worldgenerator.Worldgen import net.torvald.terrarum.serialise.Common import net.torvald.terrarum.utils.CSVFetcher import net.torvald.terrarum.utils.JsonFetcher import net.torvald.terrarum.utils.forEachSiblings import net.torvald.terrarumsansbitmap.gdx.TextureRegionPack import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVParser import org.apache.commons.csv.CSVRecord import java.io.File import java.io.FileInputStream import java.io.FileNotFoundException import java.io.IOException import java.net.MalformedURLException import java.net.URL import java.net.URLClassLoader import java.nio.file.FileSystems import java.util.* /** * Modules (or Mods) Resource Manager * * The very first mod on the load set must have a title screen * * 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 iconFile: FileHandle, val properName: String, val description: String, val descTranslations: Map, val author: String, val packageName: String, val entryPoint: String, val releaseDate: String, val version: String, val jar: String, val dependencies: Array, val isInternal: Boolean, val configPlan: List ) { 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")}" } data class ModuleErrorInfo( val type: LoadErrorType, val moduleName: String, val cause: Throwable? = null, ) enum class LoadErrorType { YOUR_FAULT, MY_FAULT, NOT_EVEN_THERE } const val modDirInternal = "./assets/mods" val modDirExternal = "${App.defaultDir}/Modules" /** Module name (directory name), ModuleMetadata */ val moduleInfo = HashMap() val moduleInfoErrored = HashMap() val entryPointClasses = ArrayList() val moduleClassloader = HashMap() val loadOrder = ArrayList() val errorLogs = ArrayList() fun logError(type: LoadErrorType, moduleName: String, cause: Throwable? = null) { errorLogs.add(ModuleErrorInfo(type, moduleName, cause)) } private val digester = DigestUtils.getSha256Digest() /** * Try to create an instance of a "titlescreen" from the current load order set. */ fun getTitleScreen(batch: FlippingSpriteBatch): IngameInstance? = entryPointClasses.getOrNull(0)?.getTitleScreen(batch) private fun List.toVersionNumber() = 0L or (this[0].replaceFirst('*','0').removeSuffix("+").toLong().shl(24)) or (this.getOrElse(1) {"0"}.replaceFirst('*','0').removeSuffix("+").toLong().shl(16)) or (this.getOrElse(2) {"0"}.replaceFirst('*','0').removeSuffix("+").toLong().coerceAtMost(65535)) init { val loadOrderFile = File(App.loadOrderDir) if (loadOrderFile.exists()) { // load modules val loadOrderCSVparser = CSVParser.parse( loadOrderFile, 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") var module: ModuleMetadata? = null try { val modMetadata = Properties() val _internalFile = File("$modDirInternal/$moduleName/$metaFilename") val _externalFile = File("$modDirExternal/$moduleName/$metaFilename") // external mod has precedence over the internal val isInternal = if (_externalFile.exists()) false else if (_internalFile.exists()) true else throw FileNotFoundException() val file = if (isInternal) _internalFile else _externalFile val modDir = if (isInternal) modDirInternal else modDirExternal fun getGdxFile(path: String) = if (isInternal) Gdx.files.internal(path) else Gdx.files.absolute(path) modMetadata.load(FileInputStream(file)) if (File("$modDir/$moduleName/$defaultConfigFilename").exists()) { try { val defaultConfig = JsonFetcher("$modDir/$moduleName/$defaultConfigFilename") // read config and store it to the game var entry: JsonValue? = defaultConfig.child while (entry != null) { setToGameConfig(entry, moduleName) entry = entry.next } // copied from App.java // write to user's config file } catch (e: IOException) { e.printStackTrace() } } val descTranslations = HashMap() modMetadata.stringPropertyNames().filter { it.startsWith("description_") }.forEach { key -> val langCode = key.substringAfter('_') descTranslations[langCode] = modMetadata.getProperty(key) } 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 jarHash = modMetadata.getProperty("jarhash").uppercase() val dependency = modMetadata.getProperty("dependency").split(Regex(""";[ ]*""")).filter { it.isNotEmpty() }.toTypedArray() val isDir = FileSystems.getDefault().getPath("$modDir/$moduleName").toFile().isDirectory val configPlan = ArrayList() File("$modDir/$moduleName/configplan.csv").let { if (it.exists() && it.isFile) { configPlan.addAll(it.readLines(Common.CHARSET).filter { it.isNotBlank() }) } } module = ModuleMetadata(index, isDir, getGdxFile("$modDir/$moduleName/icon.png"), properName, description, descTranslations, author, packageName, entryPoint, releaseDate, version, jar, dependency, isInternal, configPlan) val versionNumeral = version.split('.') val versionNumber = versionNumeral.toVersionNumber() dependency.forEach { nameAndVersionStr -> val (moduleName, moduleVersionStr) = nameAndVersionStr.split(' ') val numbers = moduleVersionStr.split('.') val checkVersionNumber = numbers.toVersionNumber() // version number required var operator = numbers.last().last() // can be '+', '*', or a number val checkAgainstStr = moduleInfo[moduleName]?.version ?: throw ModuleDependencyNotSatisfied(nameAndVersionStr, "(module not installed)") val checkAgainst =checkAgainstStr.split('.').toVersionNumber() // version number of what's installed when (operator) { '+', '*' -> if (checkVersionNumber > checkAgainst) throw ModuleDependencyNotSatisfied(nameAndVersionStr, "$moduleName $checkAgainstStr") else -> if (checkVersionNumber != checkAgainst) throw ModuleDependencyNotSatisfied(nameAndVersionStr, "$moduleName $checkAgainstStr") } } moduleInfo[moduleName] = module printdbg(this, module) // do retexturing if retextures directory exists if (hasFile(moduleName, "retextures")) { printdbg(this, "Trying to load Retextures on ${moduleName}") GameRetextureLoader(moduleName) } // add locales if exists if (hasFile(moduleName, "locales")) { printdbg(this, "Trying to load Locales on ${moduleName}") GameLanguageLoader(moduleName) } // add keylayouts if exists if (hasFile(moduleName, "keylayout")) { printdbg(this, "Trying to load Keyboard Layouts on ${moduleName}") GameIMELoader(moduleName) } // run entry script in entry point if (entryPoint.isNotBlank()) { var newClass: Class<*>? = null try { // for modules that has JAR defined if (jar.isNotBlank()) { val urls = arrayOf() val jarFilePath = "${File(modDir).absolutePath}/$moduleName/$jar" val cl = JarFileLoader(urls) cl.addFile(jarFilePath) moduleClassloader[moduleName] = cl // check for hash digester.reset() val hash = digester.digest(File(jarFilePath).readBytes()).joinToString("","","") { it.toInt().and(255).toString(16).uppercase().padStart(2,'0') } if (jarHash != hash) { printdbg(this, "Hash expected: $jarHash, got: $hash") throw IllegalStateException("Module Jarfile hash mismatch") } // check for module-info.java val moduleInfoPath = cl.getResources("module-info.class").toList().filter { it.toString().contains("$moduleName/$jar!/module-info.class") && it.toString().endsWith("module-info.class")} if (moduleInfoPath.isEmpty()) { throw IllegalStateException("module-info not found on $moduleName") } newClass = cl.loadClass(entryPoint) } // 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") print(App.csiR); e.printStackTrace(System.out); print(App.csi0) logError(LoadErrorType.YOUR_FAULT, moduleName, e) moduleInfo.remove(moduleName) moduleInfoErrored[moduleName] = module } if (newClass != null) { 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") } else { moduleInfo.remove(moduleName) moduleInfoErrored[moduleName] = module printdbg(this, "$moduleName did not load...") } } printmsg(this, "Module $moduleName processed") } catch (noSuchModule: FileNotFoundException) { printmsgerr(this, "No such module: $moduleName, skipping...") logError(LoadErrorType.NOT_EVEN_THERE, moduleName, noSuchModule) moduleInfo.remove(moduleName) if (module != null) moduleInfoErrored[moduleName] = module } catch (noSuchModule2: ModuleDependencyNotSatisfied) { printmsgerr(this, noSuchModule2.message) logError(LoadErrorType.NOT_EVEN_THERE, moduleName, noSuchModule2) moduleInfo.remove(moduleName) if (module != null) moduleInfoErrored[moduleName] = module } catch (e: Throwable) { // TODO: Instead of skipping module with error, just display the error message onto the face? printmsgerr(this, "There was an error while loading module $moduleName") printmsgerr(this, "\t$e") print(App.csiR); e.printStackTrace(System.out); print(App.csi0) logError(LoadErrorType.YOUR_FAULT, moduleName, e) moduleInfo.remove(moduleName) if (module != null) moduleInfoErrored[moduleName] = module } finally { } } } } private class ModuleDependencyNotSatisfied(want: String, have: String) : RuntimeException("Required: $want, Installed: $have") 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 "$modDirInternal/$module/${path.sanitisePath()}" }*/ /** Returning files are read-only */ fun getGdxFile(module: String, path: String): FileHandle { checkExistence(module) return if (moduleInfo[module]!!.isInternal) Gdx.files.internal("$modDirInternal/$module/$path") else Gdx.files.absolute("$modDirExternal/$module/$path") } fun getFile(module: String, path: String): File { checkExistence(module) return if (moduleInfo[module]!!.isInternal) FileSystems.getDefault().getPath("$modDirInternal/$module/$path").toFile() else FileSystems.getDefault().getPath("$modDirExternal/$module/$path").toFile() } fun hasFile(module: String, path: String): Boolean { if (!moduleInfo.containsKey(module)) return false return getFile(module, path).exists() } fun getFiles(module: String, path: String): Array { checkExistence(module) val dir = getFile(module, path) 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 */ fun getFilesFromEveryMod(path: String): List> { val path = path.sanitisePath() val moduleNames = moduleInfo.keys.toList() val filesList = ArrayList>() moduleNames.forEach { val file = getFile(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 */ fun getGdxFilesFromEveryMod(path: String): List> { val path = path.sanitisePath() val moduleNames = moduleInfo.keys.toList() val filesList = ArrayList>() moduleNames.forEach { val file = getGdxFile(it, path) if (file.exists()) filesList.add(it to file) } return filesList.toList() } fun disposeMods() { entryPointClasses.forEach { it.dispose() } } fun getLoadOrderTextForSavegame(): String { return loadOrder.filter { moduleInfo[it] != null }.map { "$it ${moduleInfo[it]!!.version}" }.joinToString("\n") } object GameBlockLoader { init { Terrarum.blockCodex = BlockCodex() Terrarum.wireCodex = WireCodex() } @JvmStatic operator fun invoke(module: String) { Terrarum.blockCodex.fromModule(module, "blocks/blocks.csv") Terrarum.wireCodex.fromModule(module, "wires/") } } object GameOreLoader { init { Terrarum.oreCodex = OreCodex() } @JvmStatic operator fun invoke(module: String) { // register ore codex Terrarum.oreCodex.fromModule(module, "ores/ores.csv") // register to worldgen try { CSVFetcher.readFromModule(module, "ores/worldgen.csv").forEach { rec -> val tile = "ores@$module:${rec.get("id")}" val freq = rec.get("freq").toDouble() val power = rec.get("power").toDouble() val scale = rec.get("scale").toDouble() val tiling = rec.get("tiling") Worldgen.registerOre(OregenParams(tile, freq, power, scale, tiling)) } } catch (e: IOException) { e.printStackTrace() } } } object GameItemLoader { const val itemPath = "items/" init { Terrarum.itemCodex = ItemCodex() } @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) { 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") moduleClassloader[module].let { if (it == null) { val loadedClass = Class.forName(className) val loadedClassConstructor = loadedClass.getConstructor(ItemID::class.java) val loadedClassInstance = loadedClassConstructor.newInstance(itemName) ItemCodex[itemName] = loadedClassInstance as GameItem } else { val loadedClass = it.loadClass(className) val loadedClassConstructor = loadedClass.getConstructor(ItemID::class.java) val loadedClassInstance = loadedClassConstructor.newInstance(itemName) ItemCodex[itemName] = loadedClassInstance as GameItem } } } } } object GameLanguageLoader { const val langPath = "locales/" @JvmStatic operator fun invoke(module: String) { Lang.load(getFile(module, langPath)) } } object GameIMELoader { const val keebPath = "keylayout/" @JvmStatic operator fun invoke(module: String) { val FILE = getFile(module, keebPath) FILE.listFiles { file, s -> s.endsWith(".${IME.KEYLAYOUT_EXTENSION}") }.sortedBy { it.name }.forEach { printdbg(this, "Registering Low layer ${it.nameWithoutExtension.lowercase()}") IME.registerLowLayer(it.nameWithoutExtension.lowercase(), IME.parseKeylayoutFile(it)) } FILE.listFiles { file, s -> s.endsWith(".${IME.IME_EXTENSION}") }.sortedBy { it.name }.forEach { printdbg(this, "Registering High layer ${it.nameWithoutExtension.lowercase()}") IME.registerHighLayer(it.nameWithoutExtension.lowercase(), IME.parseImeFile(it)) } val iconFile = getFile(module, keebPath + "icons.tga").let { if (it.exists()) it else getFile(module, keebPath + "icons.png") } if (iconFile.exists()) { val iconSheet = TextureRegionPack(iconFile.path, 20, 20) val iconPixmap = Pixmap(Gdx.files.absolute(iconFile.path)) for (k in 0 until iconPixmap.height step 20) { val langCode = StringBuilder() for (c in 0 until 20) { val x = c var charnum = 0 for (b in 0 until 7) { val y = k + b if (iconPixmap.getPixel(x, y) and 255 != 0) { charnum = charnum or (1 shl b) } } if (charnum != 0) langCode.append(charnum.toChar()) } if (langCode.isNotEmpty()) { printdbg(this, "Icon order #${(k+1) / 20} - icons[\"$langCode\"] = iconSheet.get(1, ${k/20})") IME.icons["$langCode"] = iconSheet.get(1, k / 20).also { it.flip(false, false) } } } App.disposables.add(iconSheet) iconPixmap.dispose() } } } object GameMaterialLoader { const val matePath = "materials/" init { Terrarum.materialCodex = MaterialCodex() } @JvmStatic operator fun invoke(module: String) { Terrarum.materialCodex.fromModule(module, matePath + "materials.csv") } } /** * A sugar-library for easy texture pack creation */ object GameRetextureLoader { const val retexturesPath = "retextures/" val retexables = listOf("blocks","wires") val altFilePaths = HashMap() val retexableCallbacks = HashMap Unit>() init { retexableCallbacks["blocks"] = { App.tileMaker(true) } } @JvmStatic operator fun invoke(module: String) { val targetModNames = getFiles(module, retexturesPath).filter { it.isDirectory } targetModNames.forEach { baseTargetModDir -> // modules//retextures/basegame // printdbg(this, "baseTargetModDir = $baseTargetModDir") retexables.forEach { category -> val dir = File(baseTargetModDir, category) // modules//retextures/basegame/blocks // printdbg(this, "cats: ${dir.path}") if (dir.isDirectory && dir.exists()) { dir.listFiles { it: File -> it.name.contains('-') }?.forEach { // -.tga or .png val tokens = it.name.split('-') if (tokens.size > 1) { val modname = tokens[0] val filename = tokens.tail().joinToString("-") altFilePaths["$modDirInternal/$modname/$category/$filename"] = getGdxFile(module, "$retexturesPath${baseTargetModDir.name}/$category/${it.name}") } } } // retexableCallbacks[category]?.invoke() } } printdbg(this, "ALT FILE PATHS") altFilePaths.forEach { (k, v) -> printdbg(this, "$k -> $v") } } } object GameCraftingRecipeLoader { const val recipePath = "crafting/" @JvmStatic operator fun invoke(module: String) { getFile(module, recipePath).listFiles { it: File -> it.name.lowercase().endsWith(".json") }?.forEach { jsonFile -> Terrarum.craftingCodex.addFromJson(JsonFetcher(jsonFile), module, jsonFile.name) } } } } private class JarFileLoader(urls: Array) : URLClassLoader(urls) { @Throws(MalformedURLException::class) fun addFile(path: String) { val urlPath = "jar:file://$path!/" addURL(URL(urlPath)) } }