From c8e79c22ff42122d8833a912ea582a23c790fddf Mon Sep 17 00:00:00 2001 From: octarine-noise Date: Mon, 12 Jul 2021 19:12:42 +0200 Subject: [PATCH] [WIP] Config parser --- .gitignore | 1 + build.gradle.kts | 13 ++ src/main/javacc/BlockConfig.jj | 102 ++++++++++++ .../mods/betterfoliage/BetterFoliageMod.kt | 3 +- .../mods/betterfoliage/config/BlockConfig.kt | 40 +++++ .../mods/betterfoliage/config/Config.kt | 2 +- .../mods/betterfoliage/config/match/Match.kt | 117 ++++++++++++++ .../betterfoliage/config/match/Matchers.kt | 150 ++++++++++++++++++ .../betterfoliage/config/match/ParseTree.kt | 37 +++++ .../resource/discovery/ModelDiscovery.kt | 4 + .../resource/discovery/RuleBasedDiscovery.kt | 79 +++++++++ .../config/betterfoliage/vanilla.rules | 45 ++++++ 12 files changed, 591 insertions(+), 2 deletions(-) create mode 100644 src/main/javacc/BlockConfig.jj create mode 100644 src/main/kotlin/mods/betterfoliage/config/BlockConfig.kt create mode 100644 src/main/kotlin/mods/betterfoliage/config/match/Match.kt create mode 100644 src/main/kotlin/mods/betterfoliage/config/match/Matchers.kt create mode 100644 src/main/kotlin/mods/betterfoliage/config/match/ParseTree.kt create mode 100644 src/main/kotlin/mods/betterfoliage/resource/discovery/RuleBasedDiscovery.kt create mode 100644 src/main/resources/assets/betterfoliage/config/betterfoliage/vanilla.rules diff --git a/.gitignore b/.gitignore index 42a6791..33cdfcc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build/ classes/ temp/ logs +src/main/javacc/mods diff --git a/build.gradle.kts b/build.gradle.kts index 79ddcb0..9a540d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ plugins { kotlin("jvm").version("1.4.20") id("net.minecraftforge.gradle").version("4.1.12") id("org.spongepowered.mixin").version("0.7-SNAPSHOT") + id("com.intershop.gradle.javacc").version("4.0.0") } repositories { @@ -20,6 +21,7 @@ dependencies { configurations["annotationProcessor"].extendsFrom(configurations["implementation"]) sourceSets { get("main").ext["refMap"] = "betterfoliage.refmap.json" + get("main").java.srcDir("src/main/javacc/") } minecraft { @@ -36,6 +38,17 @@ minecraft { } } +javacc { + configs { + create("blockconfig") { + staticParam = "false" + inputFile = file("src/main/javacc/BlockConfig.jj") + outputDir = file("src/main/javacc/") + packageName = "mods.betterfoliage.config.match.parser" + } + } +} + java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 diff --git a/src/main/javacc/BlockConfig.jj b/src/main/javacc/BlockConfig.jj new file mode 100644 index 0000000..e11bb20 --- /dev/null +++ b/src/main/javacc/BlockConfig.jj @@ -0,0 +1,102 @@ +PARSER_BEGIN(BlockConfigParser) +package mods.betterfoliage.config.match.parser; + +import java.util.List; +import java.util.LinkedList; +import mods.betterfoliage.config.match.*; + +public class BlockConfigParser { + public String configFile; + + ConfigSource getSource(Token t) { + return new ConfigSource(configFile, t.beginLine, t.beginColumn); + } +} + +PARSER_END(BlockConfigParser) + +// Whitespace definition +SKIP : { " " | "\n" | "\t" | "\r" } + +// Single-line comment +SPECIAL_TOKEN : { } + +// Lexical state for string literal in quotes +SPECIAL_TOKEN : { < quoteStart : "\"" > : withinQuotes } + SPECIAL_TOKEN : { < quoteEnd : "\"" > : DEFAULT } + TOKEN : { < stringLiteral : (["a"-"z"] | ["0"-"9"] | "/" | "." | "_" | "-" | ":" )* > } + +// Symbol tokens +TOKEN : { + < parenStart : "(" > | + < parenEnd : ")" > | + < dot : "." > | + < comma : "," > +} + +void matchFile(List parent) : { + Token t; +} { + ( + t = "match" + { List nodes = new LinkedList(); } + (match(nodes))* + "end" + { parent.add(new Node.MatchAll(getSource(t), nodes)); } + )* +} + +void match(List parent) : { + Token t; Token t2; MatchMethod mm; List values; Node.Value v; +} { + "block." matchBlock(parent) + | + t = "model." mm = matchMethod() values = matchValueList() + { parent.add(new Node.MatchValueList(Node.MatchSource.MODEL_LOCATION, mm, getSource(t), values)); } + | + t = "isParam" t2 = values = matchValueList() + { parent.add(new Node.MatchParam(t2.image, values, getSource(t))); } + | + t = "setParam" t2 = v = matchValue() + { parent.add(new Node.SetParam(t2.image, v, getSource(t))); } +} + +MatchMethod matchMethod() : {} { + "matches" { return MatchMethod.EXACT_MATCH; } | + "extends" { return MatchMethod.EXTENDS; } | + "contains" { return MatchMethod.CONTAINS; } +} + +void matchBlock(List parent) : { + Token t; MatchMethod mm; List values; +} { + t = "class." mm = matchMethod() values = matchValueList() + { parent.add(new Node.MatchValueList(Node.MatchSource.BLOCK_CLASS, mm, getSource(t), values)); } + | + t = "name." mm = matchMethod() values = matchValueList() + { parent.add(new Node.MatchValueList(Node.MatchSource.BLOCK_NAME, mm, getSource(t), values)); } +} + +List matchValueList() : { + List values = new LinkedList(); +} { + matchValueToList(values) ( matchValueToList(values))* { return values; } +} + +void matchValueToList(List values) : { + Node.Value v; +} { + v = matchValue() { values.add(v); } +} + +Node.Value matchValue() : { + Token t; +} { + t = { return new Node.Value.Literal(t.image); } + | + "classOf" t = + { return new Node.Value.ClassOf(t.image); } + | + "model.texture" t = + { return new Node.Value.Texture(t.image); } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt b/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt index dafbce2..7656f55 100644 --- a/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt +++ b/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt @@ -1,6 +1,7 @@ package mods.betterfoliage import mods.betterfoliage.config.BlockConfig +import mods.betterfoliage.config.BlockConfigOld import mods.betterfoliage.config.MainConfig import mods.betterfoliage.util.tryDefault import mods.betterfoliage.config.clothGuiRoot @@ -72,7 +73,7 @@ object BetterFoliageMod { } Minecraft.getInstance().resourcePackRepository.addPackFinder(BetterFoliage.generatedPack.finder) - bus.register(BlockConfig) + bus.register(BlockConfigOld) BetterFoliage.init() } } \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/config/BlockConfig.kt b/src/main/kotlin/mods/betterfoliage/config/BlockConfig.kt new file mode 100644 index 0000000..083e2bb --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/config/BlockConfig.kt @@ -0,0 +1,40 @@ +package mods.betterfoliage.config + +import mods.betterfoliage.config.match.Node +import mods.betterfoliage.config.match.parser.BlockConfigParser +import mods.betterfoliage.config.match.parser.ParseException +import mods.betterfoliage.config.match.parser.TokenMgrError +import mods.betterfoliage.util.HasLogger +import mods.betterfoliage.util.stripStart +import net.minecraft.resources.IResourceManager +import net.minecraft.util.ResourceLocation +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.Level.ERROR + +class BlockConfig : HasLogger() { + lateinit var rules: List + + fun readConfig(manager: IResourceManager) { + val configs = manager.listResources("config/betterfoliage") { it.endsWith(".rules") } + rules = configs.flatMap { configLocation -> + val resource = manager.getResource(configLocation) + val parser = BlockConfigParser(resource.inputStream) + .apply { configFile = configLocation.stripStart("config/betterfoliage/").toString() } + try { + mutableListOf().apply { parser.matchFile(this) } + } catch (e: ParseException) { + parseError(e, configLocation) + } catch (e: TokenMgrError) { + parseError(e, configLocation) + } + } + } + + fun parseError(e: Throwable, location: ResourceLocation): List { + "Error parsing block config $location: ${e.message}".let { + logger.log(ERROR, it) + detailLogger.log(ERROR, it) + } + return emptyList() + } +} diff --git a/src/main/kotlin/mods/betterfoliage/config/Config.kt b/src/main/kotlin/mods/betterfoliage/config/Config.kt index 9d22303..4513392 100644 --- a/src/main/kotlin/mods/betterfoliage/config/Config.kt +++ b/src/main/kotlin/mods/betterfoliage/config/Config.kt @@ -7,7 +7,7 @@ import net.minecraft.util.ResourceLocation import net.minecraftforge.eventbus.api.SubscribeEvent import net.minecraftforge.fml.config.ModConfig -object BlockConfig { +object BlockConfigOld { private val list = mutableListOf() val leafBlocks = blocks("leaves_blocks_default.cfg") diff --git a/src/main/kotlin/mods/betterfoliage/config/match/Match.kt b/src/main/kotlin/mods/betterfoliage/config/match/Match.kt new file mode 100644 index 0000000..0852d28 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/config/match/Match.kt @@ -0,0 +1,117 @@ +package mods.betterfoliage.config.match + +typealias RuleLogConsumer = (ConfigSource, String) -> Unit + +sealed class MatchValue(val description: String) { + class Found(description: String, val value: T) : MatchValue(description) + class Missing(description: String) : MatchValue(description) + class Invalid(description: String) : MatchValue(description) +} + +sealed class MatchResult { + abstract val isSuccess: Boolean + abstract val isInvariant: Boolean + abstract val configSource: ConfigSource + abstract fun log(logger: RuleLogConsumer) + + class UniComparison( + override val isSuccess: Boolean, + override val isInvariant: Boolean, + override val configSource: ConfigSource, + val sourceValue: MatchValue, + val targetValue: String, + val matchMethod: MatchMethod + ) : MatchResult() { + override fun log(logger: RuleLogConsumer) = logger( + configSource, + "${sourceValue.description} ${matchMethod.description(isSuccess)} \"$targetValue\"" + ) + } + + class BiComparison( + override val isSuccess: Boolean, + override val isInvariant: Boolean, + override val configSource: ConfigSource, + val source: MatchValue, + val target: MatchValue, + val matchMethod: MatchMethod + ) : MatchResult() { + override fun log(logger: RuleLogConsumer) = logger( + configSource, + "${source.description} ${matchMethod.description(isSuccess)} ${target.description}" + ) + } + + class InvalidValue(override val configSource: ConfigSource, val value: MatchValue, val description: String) : MatchResult() { + override val isSuccess = false + override val isInvariant = true + + override fun log(logger: RuleLogConsumer) = logger(configSource, description) + } + + class Error(override val configSource: ConfigSource, val description: String) : MatchResult() { + override val isSuccess = false + override val isInvariant = true + + override fun log(logger: RuleLogConsumer) = logger(configSource, description) + } + + class Action(override val configSource: ConfigSource, val description: String) : MatchResult() { + override val isSuccess = true + override val isInvariant = false + + override fun log(logger: RuleLogConsumer) = logger(configSource, description) + } + + class Any(override val configSource: ConfigSource, val results: List) : MatchResult() { + override val isSuccess = results.any(MatchResult::isSuccess) + override val isInvariant = results.all(MatchResult::isInvariant) + + override fun log(logger: RuleLogConsumer) { + val toLog = if (results.any { it.isSuccess }) results.filter { it.isSuccess } else results + toLog.forEach { it.log(logger) } + } + } + + class RootList(override val configSource: ConfigSource, val results: List) : MatchResult() { + override val isSuccess = results.all(MatchResult::isSuccess) + override val isInvariant = results.all(MatchResult::isInvariant) + + override fun log(logger: RuleLogConsumer) { + results.forEach { it.log(logger) } + } + } +} + +fun Node.HasSource.error(description: String) = MatchResult.Error(configSource, description) +fun Node.HasSource.notImplemented() = MatchResult.Error(configSource, "match type not implemented: ${this::class.java.name}") +fun Node.HasSource.action(description: String) = MatchResult.Action(configSource, description) +fun Node.HasSource.invalidValue(value: MatchValue) = MatchResult.InvalidValue( + configSource, value, "invalid value: ${value.description}" +) +fun Node.HasSource.invalidValueType(comparisonLeft: String, value: Node.Value) = MatchResult.Error( + configSource, "invalid type for $comparisonLeft: [${value::class.java.name}] \"${value.value}\"" +) +fun Node.MatchValueList.compare(sourceValue: MatchValue, targetValue: Node.Value, func: (MatchValue.Found) -> Boolean): MatchResult { + if (sourceValue is MatchValue.Missing || sourceValue is MatchValue.Invalid) return invalidValue(sourceValue) + val isSuccess = func(sourceValue as MatchValue.Found) + return MatchResult.UniComparison(isSuccess, true, configSource, sourceValue, targetValue.value, matchMethod) +} +fun Node.MatchValueList.compare(sourceValue: MatchValue, targetValue: MatchValue, func: (MatchValue.Found, MatchValue.Found) -> Boolean): MatchResult { + if (sourceValue is MatchValue.Missing || sourceValue is MatchValue.Invalid) return invalidValue(sourceValue) + if (targetValue is MatchValue.Missing || targetValue is MatchValue.Invalid) return invalidValue(targetValue) + val isSuccess = func(sourceValue as MatchValue.Found, targetValue as MatchValue.Found) + return MatchResult.BiComparison(isSuccess, true, configSource, sourceValue, targetValue, matchMethod) +} + +enum class MatchMethod { + EXACT_MATCH, EXTENDS, CONTAINS; + + fun description(isSuccess: Boolean) = when(this) { + EXACT_MATCH -> if (isSuccess) "matches" else "does not match" + EXTENDS -> if (isSuccess) "extends" else "does not extend" + CONTAINS -> if (isSuccess) "contains" else "does not contain" + + } +} + diff --git a/src/main/kotlin/mods/betterfoliage/config/match/Matchers.kt b/src/main/kotlin/mods/betterfoliage/config/match/Matchers.kt new file mode 100644 index 0000000..1c152ab --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/config/match/Matchers.kt @@ -0,0 +1,150 @@ +package mods.betterfoliage.config.match + +import mods.betterfoliage.resource.discovery.RuleProcessingContext +import mods.betterfoliage.resource.discovery.getAncestry +import mods.betterfoliage.util.tryDefault +import net.minecraft.block.Block +import net.minecraft.client.renderer.model.BlockModel +import net.minecraft.util.ResourceLocation +import net.minecraftforge.registries.ForgeRegistries + +object MatchRuleList { + fun visitRoot(ctx: RuleProcessingContext, node: Node.MatchAll): MatchResult { + val results = mutableListOf() + for (rule in node.list) { + val result = when(rule) { + is Node.MatchValueList -> visitMatchList(ctx, rule) + is Node.MatchParam -> ParamMatchRules.visitMatch(ctx, rule) + is Node.SetParam -> ParamMatchRules.visitSet(ctx, rule) + else -> node.notImplemented() + } + results.add(result) + if (!result.isSuccess) break + } + return MatchResult.RootList(node.configSource, results) + } + + fun visitMatchList(ctx: RuleProcessingContext, node: Node.MatchValueList) = node.values.map { value -> + try { + when (node.matchSource) { + Node.MatchSource.BLOCK_CLASS -> BlockMatchRules.visitClass(ctx, node, value) + Node.MatchSource.BLOCK_NAME -> BlockMatchRules.visitName(ctx, node, value) + Node.MatchSource.MODEL_LOCATION -> ModelMatchRules.visit(ctx, node, value) + } + } catch (e: Exception) { + MatchResult.Error(node.configSource, e.message ?: "") + } + }.let { MatchResult.Any(node.configSource, it) } +} + +object BlockMatchRules { + fun visitClass(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MatchResult { + val source = ctx.discovery.blockState.block::class.java.let { + MatchValue.Found("block class \"${it.name}\"", it) + } + val target = when(value) { + is Node.Value.Literal -> tryDefault(null) { Class.forName(value.value) as Class } + ?.let { MatchValue.Found("class \"${value.value}\"", it) } + ?: MatchValue.Missing("missing class \"${value.value}\"") + + is Node.Value.ClassOf -> ForgeRegistries.BLOCKS.getValue(ResourceLocation(value.value)) + ?.let { MatchValue.Found("class \"${it::class.java}\" of block \"${value.value}\"", it::class.java) } + ?: MatchValue.Missing("class of missing block \"${value.value}\"") + + else -> MatchValue.Invalid("${value::class.java.name}(${value.value})") + } + return when(node.matchMethod) { + MatchMethod.EXACT_MATCH -> node.compare(source, target, this::isExactClass) + MatchMethod.EXTENDS -> node.compare(source, target, this::isExtendsClass) + MatchMethod.CONTAINS -> node.error("invalid match type for block class: \"contains\"") + } + } + + fun visitName(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MatchResult { + val source = ctx.discovery.blockState.block.registryName?.let { + MatchValue.Found("block name \"$it\"", it) + } ?: MatchValue.Missing("missing block name") + if (value !is Node.Value.Literal) return node.invalidValueType("block name", value) + val (namespace, path) = if (value.value.contains(":")) ResourceLocation(value.value).let { it.namespace to it.path } else null to value.value + return when(node.matchMethod) { + MatchMethod.EXACT_MATCH -> node.compare(source, value) { isExactName(it.value, namespace, path) } + MatchMethod.CONTAINS -> node.compare(source, value) { isContainsName(it.value, namespace, path) } + MatchMethod.EXTENDS -> node.error("invalid match type for block name: \"extends\"") + } + } + + private fun isExactClass(source: MatchValue.Found>, target: MatchValue.Found>) = + source.value == target.value + private fun isExtendsClass(source: MatchValue.Found>, target: MatchValue.Found>) = + target.value.isAssignableFrom(source.value) + + fun isExactName(source: ResourceLocation, namespace: String?, path: String) = + (namespace == null || namespace == source.namespace) && path == source.path + fun isContainsName(source: ResourceLocation, namespace: String?, path: String) = + (namespace == null || source.namespace.contains(namespace)) && source.path.contains(path) +} + +object ModelMatchRules { + fun visit(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MatchResult { + val source = ctx.discovery.modelLocation.let { MatchValue.Found("model \"$it\"", it) } + if (value !is Node.Value.Literal) return node.invalidValueType("model", value) + val (namespace, path) = value.value.splitLocation() + + val check = when (node.matchMethod) { + MatchMethod.EXACT_MATCH, MatchMethod.CONTAINS -> listOf(ctx.discovery.modelLocation) + MatchMethod.EXTENDS -> ctx.discovery.bakery.getAncestry(ctx.discovery.modelLocation) + } + + return when (node.matchMethod) { + MatchMethod.EXACT_MATCH, MatchMethod.EXTENDS -> node.compare(source, value) { isExactModel(check, namespace, path) } + MatchMethod.CONTAINS -> node.compare(source, value) { isContainsModel(check, namespace, path) } + } + } + + private fun String.splitLocation() = when(contains(":")) { + true -> ResourceLocation(this).let { it.namespace to it.path } + false -> null to this + } + + private fun isExactModel(models: List, namespace: String?, path: String) = models.any { model -> + (namespace == null || namespace == model.namespace) && path == model.path + } + + private fun isContainsModel(models: List, namespace: String?, path: String) = models.any { model -> + (namespace == null || model.namespace.contains(namespace)) && model.path.contains(path) + } +} + +object ParamMatchRules { + fun visitMatch(ctx: RuleProcessingContext, node: Node.MatchParam) = node.values.map { value -> + if (value !is Node.Value.Literal) return@map node.invalidValueType("parameter", value) + val currentParamValue = ctx.params[node.name] ?: return@map MatchResult.UniComparison( + isSuccess = false, isInvariant = false, + node.configSource, + MatchValue.Missing("missing parameter \"${node.name}\""), value.value, + MatchMethod.EXACT_MATCH + ) + val isSuccess = currentParamValue == value.value + MatchResult.UniComparison( + isSuccess, false, + node.configSource, + MatchValue.Found("parameter \"${node.name}\"", currentParamValue), value.value, + MatchMethod.EXACT_MATCH + ) + }.let { MatchResult.Any(node.configSource, it) } + + fun visitSet(ctx: RuleProcessingContext, node: Node.SetParam): MatchResult { + val target = when(node.value) { + is Node.Value.Literal -> node.value.value.let { MatchValue.Found("\"$it\"", it) } + is Node.Value.Texture -> when(val model = ctx.discovery.getUnbaked()) { + is BlockModel -> model.getMaterial(node.value.value).texture().toString().let { + MatchValue.Found("texture \"${node.value.value}\" = \"$it\"", it) + } + else -> return node.error("cannot get texture from ${model::class.java.name}") + } + else -> return node.invalidValueType("parameter", node.value) + } + ctx.params[node.name] = target.value + return node.action("parameter \"${node.name}\" set to ${target.description}") + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/config/match/ParseTree.kt b/src/main/kotlin/mods/betterfoliage/config/match/ParseTree.kt new file mode 100644 index 0000000..a27379f --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/config/match/ParseTree.kt @@ -0,0 +1,37 @@ +package mods.betterfoliage.config.match + +data class ConfigSource( + val configFile: String, + val line: Int, + val column: Int +) { + override fun toString() = "$configFile @ R$line,C$column" +} + +sealed class Node { + enum class MatchSource { BLOCK_CLASS, BLOCK_NAME, MODEL_LOCATION } + interface HasSource { val configSource: ConfigSource } + + class MatchValueList( + val matchSource: MatchSource, + val matchMethod: MatchMethod, + override val configSource: ConfigSource, + val values: List + ) : Node(), HasSource + + class MatchParam( + val name: String, + val values: List, + override val configSource: ConfigSource, + ) : Node(), HasSource + + class SetParam(val name: String, val value: Value, override val configSource: ConfigSource) : Node(), HasSource + + class MatchAll(override val configSource: ConfigSource, val list: List) : Node(), HasSource + + abstract class Value(val value: String) : Node() { + class Literal(value: String) : Value(value) + class ClassOf(value: String) : Value(value) + class Texture(value: String) : Value(value) + } +} diff --git a/src/main/kotlin/mods/betterfoliage/resource/discovery/ModelDiscovery.kt b/src/main/kotlin/mods/betterfoliage/resource/discovery/ModelDiscovery.kt index 35b1af1..475511f 100644 --- a/src/main/kotlin/mods/betterfoliage/resource/discovery/ModelDiscovery.kt +++ b/src/main/kotlin/mods/betterfoliage/resource/discovery/ModelDiscovery.kt @@ -31,6 +31,10 @@ abstract class AbstractModelDiscovery : HasLogger(), ModelDiscovery { } open fun processModel(ctx: ModelDiscoveryContext) { + processContainerModel(ctx) + } + + fun processContainerModel(ctx: ModelDiscoveryContext) { val model = ctx.getUnbaked() // built-in support for container models diff --git a/src/main/kotlin/mods/betterfoliage/resource/discovery/RuleBasedDiscovery.kt b/src/main/kotlin/mods/betterfoliage/resource/discovery/RuleBasedDiscovery.kt new file mode 100644 index 0000000..06428a2 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/resource/discovery/RuleBasedDiscovery.kt @@ -0,0 +1,79 @@ +package mods.betterfoliage.resource.discovery + +import mods.betterfoliage.BetterFoliage +import mods.betterfoliage.config.match.MatchResult +import mods.betterfoliage.config.match.MatchRuleList +import mods.betterfoliage.config.match.Node +import mods.betterfoliage.util.HasLogger +import net.minecraft.client.renderer.model.BlockModel +import net.minecraft.client.renderer.model.VariantList +import net.minecraft.util.ResourceLocation +import org.apache.logging.log4j.Level + +abstract class ParametrizedModelDiscovery : HasLogger() { + abstract fun processModel(ctx: ModelDiscoveryContext, params: Map) + + fun Map.texture(key: String): ResourceLocation? { + val result = get(key)?.let { ResourceLocation(it) } + if (result == null) detailLogger.log(Level.WARN, "Cannot find texture parameter \"$key\"") + return result + } +} + +class RuleProcessingContext( + val discovery: ModelDiscoveryContext +) { + val params = mutableMapOf("type" to "none") +} + +class RuleBasedDiscovery : AbstractModelDiscovery() { + val discoverers = mutableMapOf() + + override fun processModel(ctx: ModelDiscoveryContext) = when(ctx.getUnbaked()) { + is VariantList -> processContainerModel(ctx) + is BlockModel -> processBlockModel(ctx) + else -> Unit + } + + fun processBlockModel(ctx: ModelDiscoveryContext) { + val ruleCtx = RuleProcessingContext(ctx) + val rulesToCheck = BetterFoliage.blockConfig.rules.toMutableList() + val ruleResults = mutableListOf() + var previousSize = 0 + + // stop processing if nothing changes anymore + while (rulesToCheck.size != previousSize) { + previousSize = rulesToCheck.size + val iterator = rulesToCheck.listIterator() + while (iterator.hasNext()) iterator.next().let { rule -> + // process single rule + MatchRuleList.visitRoot(ruleCtx, rule).let { result -> + ruleResults.add(result) + // remove rule from active list if: + // - rule succeeded (all directives returned success) + // - rule is invariant (result will always be the same) + if (result.isSuccess || result.isInvariant) iterator.remove() + } + } + } + + // log result of rule processing + if (ruleResults.any { it.isSuccess }) { + detailLogger.log(Level.INFO, "================================") + detailLogger.log(Level.INFO, "block state: ${ctx.blockState}") + detailLogger.log(Level.INFO, "block class: ${ctx.blockState.block::class.java.name}") + detailLogger.log(Level.INFO, "model : ${ctx.modelLocation}") + detailLogger.log(Level.INFO, "--------------------------------") + ruleResults.forEach { result -> + if (result !is MatchResult.RootList || result.results.shouldLog()) + result.log { source, message -> detailLogger.log(Level.INFO, "[$source] $message") } + } + } + + discoverers[ruleCtx.params["type"]]?.processModel(ctx, ruleCtx.params) + } + + fun List.shouldLog() = all { it.isSuccess } || fold(false) { seenInvariantSuccess, result -> + seenInvariantSuccess || (result.isSuccess && result.isInvariant) + } +} \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/config/betterfoliage/vanilla.rules b/src/main/resources/assets/betterfoliage/config/betterfoliage/vanilla.rules new file mode 100644 index 0000000..f95a8a9 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/config/betterfoliage/vanilla.rules @@ -0,0 +1,45 @@ +// Leaves +match block.class.extends(classOf("minecraft:oak_leaves")) setParam("type", "leaf") end + +match isParam("type", "leaf") +model.extends("minecraft:block/leaves", "minecraft:block/cube_all") +setParam("texture-leaf", model.texture("all")) +end + +// Podzol +match block.name.matches("minecraft:podzol") setParam("type", "grass") end + +// Grass +match block.class.extends(classOf("minecraft:grass_block")) setParam("type", "grass") end + +match isParam("type", "grass") +model.extends("minecraft:block/grass_block", "minecraft:block/cube_bottom_top") +setParam("texture-grass", model.texture("top")) +end + +// Dirt +match block.name.matches("minecraft:dirt") setParam("type", "dirt") end + +// Wood Log +match block.class.extends(classOf("minecraft:oak_log")) setParam("type", "round-log") end + +match isParam("type", "round-log") +model.extends("minecraft:block/cube_column", "minecraft:block/cube_column_horizontal") +setParam("texture-side", model.texture("side")) +setParam("texture-end", model.texture("end")) +end + +match isParam("type", "round-log") +model.extends("minecraft:block/cube_all") +setParam("texture-side", model.texture("all")) +setParam("texture-end", model.texture("all")) +end + +// Sand & Dirt +match block.name.matches("minecraft:sand", "minecraft:red_sand") setParam("type", "sand") end + +// Cactus, Lilypad, Mycelium, Netherrack +match block.name.matches("minecraft:cactus") setParam("type", "cactus") end +match block.name.matches("minecraft:lilypad") setParam("type", "lilypad") end +match block.name.matches("minecraft:mycelium") setParam("type", "mycelium") end +match block.name.matches("minecraft:netherrack") setParam("type", "netherrack") end