Files
Terrarum/src/net/torvald/terrarum/ModMgr.kt
2024-01-29 22:28:33 +09:00

863 lines
34 KiB
Kotlin

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.audio.AudioCodex
import net.torvald.terrarum.blockproperties.BlockCodex
import net.torvald.terrarum.blockproperties.BlockProp
import net.torvald.terrarum.blockproperties.OreCodex
import net.torvald.terrarum.blockproperties.WireCodex
import net.torvald.terrarum.gameactors.ActorWithBody
import net.torvald.terrarum.gamecontroller.IME
import net.torvald.terrarum.gameitems.GameItem
import net.torvald.terrarum.gameitems.ItemID
import net.torvald.terrarum.itemproperties.CraftingCodex
import net.torvald.terrarum.itemproperties.ItemCodex
import net.torvald.terrarum.itemproperties.MaterialCodex
import net.torvald.terrarum.langpack.Lang
import net.torvald.terrarum.modulebasegame.TerrarumIngame
import net.torvald.terrarum.modulebasegame.gameitems.BlockBase
import net.torvald.terrarum.modulebasegame.worldgenerator.OregenParams
import net.torvald.terrarum.modulebasegame.worldgenerator.Worldgen
import net.torvald.terrarum.serialise.Common
import net.torvald.terrarum.ui.UICanvas
import net.torvald.terrarum.utils.CSVFetcher
import net.torvald.terrarum.utils.JsonFetcher
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<String, String>,
val author: String,
val packageName: String,
val entryPoint: String,
val releaseDate: String,
val version: String,
val jar: String,
val dependencies: Array<String>,
val isInternal: Boolean,
val configPlan: List<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")}"
}
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<String, ModuleMetadata>()
val moduleInfoErrored = HashMap<String, ModuleMetadata>()
val entryPointClasses = ArrayList<ModuleEntryPoint>()
val moduleClassloader = HashMap<String, URLClassLoader>()
val loadOrder = ArrayList<String>()
val errorLogs = ArrayList<ModuleErrorInfo>()
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<String>.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 loadScriptMod = if (App.getConfigBoolean("enablescriptmods")) true else (index == 0)
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<String, String>()
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<String>()
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()) {
if (!loadScriptMod) {
throw ScriptModDisallowedException()
}
var newClass: Class<*>? = null
try {
// for modules that has JAR defined
if (jar.isNotBlank()) {
val urls = arrayOf<URL>()
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 (!App.IS_DEVELOPMENT_BUILD && 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 (noScriptModule: ScriptModDisallowedException) {
printmsgerr(this, noScriptModule.message)
logError(LoadErrorType.MY_FAULT, moduleName, noScriptModule)
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")
private class ScriptModDisallowedException : RuntimeException("Script Mods disabled")
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<File> {
checkExistence(module)
val dir = getFile(module, path)
if (!dir.isDirectory) {
throw FileNotFoundException("The path is not a directory")
}
else {
return dir.listFiles()
}
}
fun getGdxFiles(module: String, path: String): Array<FileHandle> {
checkExistence(module)
val dir = getGdxFile(module, path)
if (!dir.isDirectory) {
throw FileNotFoundException("The path is not a directory")
}
else {
return dir.list()
}
}
/** 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 = 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<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 = 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") { tile ->
// register blocks as items
ItemCodex[tile.id] = makeNewItemObj(tile, false)
if (IS_DEVELOPMENT_BUILD) print(tile.id+" ")
if (BlockCodex[tile.id].isWallable) {
ItemCodex["wall@" + tile.id] = makeNewItemObj(tile, true).also {
it.tags.add("WALL")
}
if (IS_DEVELOPMENT_BUILD) print("wall@" + tile.id + " ")
}
// crafting recipes: tile -> 2x wall
if (tile.isWallable && tile.isSolid && !tile.isActorBlock) {
CraftingRecipeCodex.addRecipe(
CraftingCodex.CraftingRecipe(
"",
arrayOf(
CraftingCodex.CraftingIngredients(
tile.id, CraftingCodex.CraftingItemKeyMode.VERBATIM, 1
)),
2,
"wall@"+tile.id,
module
))
}
}
Terrarum.wireCodex.fromModule(module, "wires/")
}
private fun makeNewItemObj(tile: BlockProp, isWall: Boolean) = object : GameItem(
if (isWall) "wall@"+tile.id else tile.id
) {
override var baseMass: Double = tile.density / 100.0
override var baseToolSize: Double? = null
override var inventoryCategory = if (isWall) Category.WALL else Category.BLOCK
override var isDynamic = false
override val materialId = tile.material
override var equipPosition = EquipPosition.HAND_GRIP
// override val itemImage: TextureRegion
// get() {
// val itemSheetNumber = App.tileMaker.tileIDtoItemSheetNumber(originalID)
// val bucket = if (isWall) BlocksDrawer.tileItemWall else BlocksDrawer.tileItemTerrain
// return bucket.get(
// itemSheetNumber % App.tileMaker.ITEM_ATLAS_TILES_X,
// itemSheetNumber / App.tileMaker.ITEM_ATLAS_TILES_X
// )
// }
init {
tags.addAll(tile.tags)
originalName =
if (isWall && tags.contains("UNLIT")) "${tile.nameKey}>>=BLOCK_UNLIT_TEMPLATE>>=BLOCK_WALL_NAME_TEMPLATE"
else if (isWall) "${tile.nameKey}>>=BLOCK_WALL_NAME_TEMPLATE"
else if (tags.contains("UNLIT")) "${tile.nameKey}>>=BLOCK_UNLIT_TEMPLATE"
else tile.nameKey
}
override fun startPrimaryUse(actor: ActorWithBody, delta: Float): Long {
return BlockBase.blockStartPrimaryUse(actor, this, dynamicID, delta)
}
override fun effectWhileEquipped(actor: ActorWithBody, delta: Float) {
BlockBase.blockEffectWhenEquipped(actor, delta)
}
}
}
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 ratio = rec.get("ratio").toDouble()
val tiling = rec.get("tiling")
Worldgen.registerOre(OregenParams(tile, freq, power, scale, ratio, 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<CSVRecord>) {
csv.forEach {
val className: String = it["classname"].toString()
val internalID: Int = it["id"].toInt()
val itemName: String = "item@$module:$internalID"
val tags = it["tags"].split(',').map { it.trim().toUpperCase() }.toHashSet()
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
ItemCodex.tags[itemName] = tags
}
else {
val loadedClass = it.loadClass(className)
val loadedClassConstructor = loadedClass.getConstructor(ItemID::class.java)
val loadedClassInstance = loadedClassConstructor.newInstance(itemName)
ItemCodex[itemName] = loadedClassInstance as GameItem
ItemCodex.tags[itemName] = tags
}
}
}
}
}
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")
}
}
object GameAudioLoader {
val audioPath = listOf(
"audio/music",
"audio/effects",
"audio/ambient",
)
init {
Terrarum.audioCodex = AudioCodex()
}
private fun loadAudio(basename: String, file: FileHandle) {
if (file.isDirectory)
file.list().forEach { loadAudio("$basename.${it.name()}", it) }
else {
val id = basename.substringBeforeLast('.').substringBeforeLast('.')
Terrarum.audioCodex.addToAudioPool(id, file)
printdbg(this, "Registering audio $id ($file)")
}
}
@JvmStatic operator fun invoke(module: String) {
audioPath.forEach {
if (getGdxFile(module, it).let { it.exists() && it.isDirectory }) {
getGdxFiles(module, it).forEach { file -> loadAudio("${it.substringAfter("audio/")}.${file.name()}", file) }
}
}
}
}
/**
* A sugar-library for easy texture pack creation
*/
object GameRetextureLoader {
const val retexturesPath = "retextures/"
val retexables = listOf("blocks","wires")
val altFilePaths = HashMap<String, FileHandle>()
val retexableCallbacks = HashMap<String, () -> 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/<module>/retextures/basegame
// printdbg(this, "baseTargetModDir = $baseTargetModDir")
retexables.forEach { category ->
val dir = File(baseTargetModDir, category)
// modules/<module>/retextures/basegame/blocks
// printdbg(this, "cats: ${dir.path}")
if (dir.isDirectory && dir.exists()) {
dir.listFiles { it: File ->
it.name.contains('-')
}?.forEach {
// <other modname>-<hopefully a number>.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)
}
}
}
object GameExtraGuiLoader {
internal val guis = ArrayList<(TerrarumIngame) -> UICanvas>()
@JvmStatic fun register(uiCreationFun: (TerrarumIngame) -> UICanvas) {
guis.add(uiCreationFun)
}
}
}
private class JarFileLoader(urls: Array<URL>) : URLClassLoader(urls) {
@Throws(MalformedURLException::class)
fun addFile(path: String) {
val urlPath = "jar:file://$path!/"
addURL(URL(urlPath))
}
}