diff --git a/assets/mods/basegame/items/itemid.csv b/assets/mods/basegame/items/itemid.csv index 2d281985a..489d74b68 100644 --- a/assets/mods/basegame/items/itemid.csv +++ b/assets/mods/basegame/items/itemid.csv @@ -202,7 +202,9 @@ id;classname;tags #1048835;net.torvald.terrarum.modulebasegame.gameitems.ItemBucketIron03;FLUIDSTORAGE,OPENSTORAGE # ## reserved for debug items -##16777216;net.torvald.terrarum.modulebasegame.gameitems.ItemBottomlessWaterBucket;DEBUG,TOOL -##16777217;net.torvald.terrarum.modulebasegame.gameitems.ItemBottomlessLavaBucket;DEBUG,TOOL -#16777472;net.torvald.terrarum.modulebasegame.gameitems.ItemMysteriousATM;DEBUG -#16777473;net.torvald.terrarum.modulebasegame.gameitems.ItemDebugInventron;DEBUG +#ifdef App.IS_DEVELOPMENT_BUILD +16777216;net.torvald.terrarum.modulebasegame.gameitems.ItemBottomlessWaterBucket;DEBUG,TOOL +16777217;net.torvald.terrarum.modulebasegame.gameitems.ItemBottomlessLavaBucket;DEBUG,TOOL +16777472;net.torvald.terrarum.modulebasegame.gameitems.ItemMysteriousATM;DEBUG +16777473;net.torvald.terrarum.modulebasegame.gameitems.ItemDebugInventron;DEBUG +#endif diff --git a/src/net/torvald/terrarum/utils/CSVFetcher.kt b/src/net/torvald/terrarum/utils/CSVFetcher.kt index 26f454f6b..6889e588e 100644 --- a/src/net/torvald/terrarum/utils/CSVFetcher.kt +++ b/src/net/torvald/terrarum/utils/CSVFetcher.kt @@ -3,12 +3,16 @@ package net.torvald.terrarum.utils import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.ModMgr import org.apache.commons.csv.CSVFormat +import java.util.ArrayDeque /** * Created by minjaesong on 2016-02-16. */ object CSVFetcher { + private const val DEFAULT_PACKAGE = "net.torvald.terrarum" + private val resolvedVariables = mutableMapOf() + val terrarumCSVFormat: CSVFormat = org.apache.commons.csv.CSVFormat.DEFAULT.withIgnoreSurroundingSpaces() .withHeader() .withIgnoreEmptyLines() @@ -21,12 +25,12 @@ object CSVFetcher { fun readFromFile(csvFilePath: String): List { net.torvald.terrarum.utils.CSVFetcher.csvString = StringBuffer() // reset buffer every time it called - net.torvald.terrarum.utils.CSVFetcher.readCSVasString(csvFilePath) + val preprocessed = net.torvald.terrarum.utils.CSVFetcher.readCSVasString(csvFilePath) printdbg(this, "Reading CSV $csvFilePath") val csvParser = org.apache.commons.csv.CSVParser.parse( - net.torvald.terrarum.utils.CSVFetcher.csvString!!.toString(), + preprocessed, terrarumCSVFormat ) @@ -39,8 +43,9 @@ object CSVFetcher { fun readFromModule(module: String, path: String) = net.torvald.terrarum.utils.CSVFetcher.readFromFile(ModMgr.getGdxFile(module, path).path()) fun readFromString(csv: String): List { + val preprocessed = preprocessCSV(csv) val csvParser = org.apache.commons.csv.CSVParser.parse( - csv, + preprocessed, terrarumCSVFormat ) @@ -57,6 +62,155 @@ object CSVFetcher { s -> net.torvald.terrarum.utils.CSVFetcher.csvString!!.append("$s\n") } - return net.torvald.terrarum.utils.CSVFetcher.csvString!!.toString() + return preprocessCSV(net.torvald.terrarum.utils.CSVFetcher.csvString!!.toString(), path) + } + + /** + * Preprocesses CSV content, handling #ifdef, #ifndef, #else, and #endif directives. + * + * Supported syntax: + * - #ifdef VARIABLE_NAME ... #endif + * - #ifdef VARIABLE_NAME ... #else ... #endif + * - #ifndef VARIABLE_NAME ... #endif + * + * Variables can be specified as: + * - Short form: App.IS_DEVELOPMENT_BUILD (resolved with net.torvald.terrarum prefix) + * - Fully qualified: net.torvald.terrarum.App.IS_DEVELOPMENT_BUILD + * + * @param content The raw CSV content to preprocess + * @param sourcePath Optional source path for error messages + * @return The preprocessed CSV content + */ + fun preprocessCSV(content: String, sourcePath: String = ""): String { + val result = StringBuilder() + // Stack of pairs: (shouldIncludeInThisBlock, hasSeenElse) + val stateStack = ArrayDeque>() + var lineNumber = 0 + + fun shouldInclude(): Boolean = stateStack.isEmpty() || stateStack.all { it.first } + + for (line in content.lineSequence()) { + lineNumber++ + val trimmed = line.trim() + + when { + trimmed.startsWith("#ifdef ") -> { + val varName = trimmed.substring(7).trim() + if (varName.isEmpty()) { + printdbg(this, "Warning: Empty #ifdef at $sourcePath:$lineNumber") + stateStack.addLast(Pair(false, false)) + } else { + val parentIncluding = shouldInclude() + val value = if (parentIncluding) resolveVariable(varName, sourcePath, lineNumber) else false + stateStack.addLast(Pair(parentIncluding && value, false)) + } + } + + trimmed.startsWith("#ifndef ") -> { + val varName = trimmed.substring(8).trim() + if (varName.isEmpty()) { + printdbg(this, "Warning: Empty #ifndef at $sourcePath:$lineNumber") + stateStack.addLast(Pair(true, false)) + } else { + val parentIncluding = shouldInclude() + val value = if (parentIncluding) resolveVariable(varName, sourcePath, lineNumber) else true + stateStack.addLast(Pair(parentIncluding && !value, false)) + } + } + + trimmed == "#else" -> { + if (stateStack.isEmpty()) { + printdbg(this, "Warning: Unmatched #else at $sourcePath:$lineNumber") + } else { + val (wasIncluding, hasSeenElse) = stateStack.removeLast() + if (hasSeenElse) { + printdbg(this, "Warning: Duplicate #else at $sourcePath:$lineNumber") + stateStack.addLast(Pair(false, true)) + } else { + // Check if parent blocks are including + val parentIncluding = stateStack.isEmpty() || stateStack.all { it.first } + // Invert the condition, but only if parent is including and we weren't including before + stateStack.addLast(Pair(parentIncluding && !wasIncluding, true)) + } + } + } + + trimmed == "#endif" -> { + if (stateStack.isEmpty()) { + printdbg(this, "Warning: Unmatched #endif at $sourcePath:$lineNumber") + } else { + stateStack.removeLast() + } + } + + // Regular line (including plain # comments) + else -> { + if (shouldInclude()) { + result.append(line).append('\n') + } + } + } + } + + // Check for unclosed directives + if (stateStack.isNotEmpty()) { + printdbg(this, "Warning: ${stateStack.size} unclosed #ifdef/#ifndef directive(s) in $sourcePath") + } + + return result.toString() + } + + private fun resolveVariable(varSpec: String, sourcePath: String, lineNumber: Int): Boolean { + // Check cache first + resolvedVariables[varSpec]?.let { return it } + + val result = try { + val (className, fieldName) = parseVariableSpec(varSpec) + val clazz = Class.forName(className) + val field = clazz.getDeclaredField(fieldName) + field.isAccessible = true + + if (field.type != Boolean::class.javaPrimitiveType && field.type != Boolean::class.java) { + printdbg(this, "Warning: Variable '$varSpec' is not a boolean at $sourcePath:$lineNumber") + false + } else { + field.getBoolean(null) // null for static fields + } + } catch (e: ClassNotFoundException) { + printdbg(this, "Warning: Class not found for '$varSpec' at $sourcePath:$lineNumber") + false + } catch (e: NoSuchFieldException) { + printdbg(this, "Warning: Field not found for '$varSpec' at $sourcePath:$lineNumber") + false + } catch (e: IllegalAccessException) { + printdbg(this, "Warning: Cannot access '$varSpec' at $sourcePath:$lineNumber") + false + } catch (e: Exception) { + printdbg(this, "Warning: Error resolving '$varSpec' at $sourcePath:$lineNumber: ${e.message}") + false + } + + // Cache the result + resolvedVariables[varSpec] = result + return result + } + + private fun parseVariableSpec(varSpec: String): Pair { + val lastDot = varSpec.lastIndexOf('.') + if (lastDot == -1) { + throw IllegalArgumentException("Invalid variable spec: $varSpec (expected ClassName.fieldName)") + } + + val classPath = varSpec.substring(0, lastDot) + val fieldName = varSpec.substring(lastDot + 1) + + // Apply default package if class path doesn't contain a dot (simple class name) + val fullClassName = if (classPath.contains('.')) { + classPath + } else { + "$DEFAULT_PACKAGE.$classPath" + } + + return fullClassName to fieldName } }