new: #if(n)def preprocessor for Terrarum-formatted CSVs

This commit is contained in:
minjaesong
2026-01-19 17:50:44 +09:00
parent 63566a507b
commit e838991826
2 changed files with 164 additions and 8 deletions

View File

@@ -202,7 +202,9 @@ id;classname;tags
#1048835;net.torvald.terrarum.modulebasegame.gameitems.ItemBucketIron03;FLUIDSTORAGE,OPENSTORAGE #1048835;net.torvald.terrarum.modulebasegame.gameitems.ItemBucketIron03;FLUIDSTORAGE,OPENSTORAGE
# #
## reserved for debug items ## reserved for debug items
##16777216;net.torvald.terrarum.modulebasegame.gameitems.ItemBottomlessWaterBucket;DEBUG,TOOL #ifdef App.IS_DEVELOPMENT_BUILD
##16777217;net.torvald.terrarum.modulebasegame.gameitems.ItemBottomlessLavaBucket;DEBUG,TOOL 16777216;net.torvald.terrarum.modulebasegame.gameitems.ItemBottomlessWaterBucket;DEBUG,TOOL
#16777472;net.torvald.terrarum.modulebasegame.gameitems.ItemMysteriousATM;DEBUG 16777217;net.torvald.terrarum.modulebasegame.gameitems.ItemBottomlessLavaBucket;DEBUG,TOOL
#16777473;net.torvald.terrarum.modulebasegame.gameitems.ItemDebugInventron;DEBUG 16777472;net.torvald.terrarum.modulebasegame.gameitems.ItemMysteriousATM;DEBUG
16777473;net.torvald.terrarum.modulebasegame.gameitems.ItemDebugInventron;DEBUG
#endif
1 id classname tags
202
203
204
205
206
207
208
209
210

View File

@@ -3,12 +3,16 @@ package net.torvald.terrarum.utils
import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.App.printdbg
import net.torvald.terrarum.ModMgr import net.torvald.terrarum.ModMgr
import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVFormat
import java.util.ArrayDeque
/** /**
* Created by minjaesong on 2016-02-16. * Created by minjaesong on 2016-02-16.
*/ */
object CSVFetcher { object CSVFetcher {
private const val DEFAULT_PACKAGE = "net.torvald.terrarum"
private val resolvedVariables = mutableMapOf<String, Boolean>()
val terrarumCSVFormat: CSVFormat = org.apache.commons.csv.CSVFormat.DEFAULT.withIgnoreSurroundingSpaces() val terrarumCSVFormat: CSVFormat = org.apache.commons.csv.CSVFormat.DEFAULT.withIgnoreSurroundingSpaces()
.withHeader() .withHeader()
.withIgnoreEmptyLines() .withIgnoreEmptyLines()
@@ -21,12 +25,12 @@ object CSVFetcher {
fun readFromFile(csvFilePath: String): List<org.apache.commons.csv.CSVRecord> { fun readFromFile(csvFilePath: String): List<org.apache.commons.csv.CSVRecord> {
net.torvald.terrarum.utils.CSVFetcher.csvString = StringBuffer() // reset buffer every time it called 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") printdbg(this, "Reading CSV $csvFilePath")
val csvParser = org.apache.commons.csv.CSVParser.parse( val csvParser = org.apache.commons.csv.CSVParser.parse(
net.torvald.terrarum.utils.CSVFetcher.csvString!!.toString(), preprocessed,
terrarumCSVFormat 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 readFromModule(module: String, path: String) = net.torvald.terrarum.utils.CSVFetcher.readFromFile(ModMgr.getGdxFile(module, path).path())
fun readFromString(csv: String): List<org.apache.commons.csv.CSVRecord> { fun readFromString(csv: String): List<org.apache.commons.csv.CSVRecord> {
val preprocessed = preprocessCSV(csv)
val csvParser = org.apache.commons.csv.CSVParser.parse( val csvParser = org.apache.commons.csv.CSVParser.parse(
csv, preprocessed,
terrarumCSVFormat terrarumCSVFormat
) )
@@ -57,6 +62,155 @@ object CSVFetcher {
s -> net.torvald.terrarum.utils.CSVFetcher.csvString!!.append("$s\n") 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 = "<unknown>"): String {
val result = StringBuilder()
// Stack of pairs: (shouldIncludeInThisBlock, hasSeenElse)
val stateStack = ArrayDeque<Pair<Boolean, Boolean>>()
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<String, String> {
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
} }
} }