diff --git a/src/main/javacc/BlockConfig.jj b/src/main/javacc/BlockConfig.jj index 29cea2d..7e2524e 100644 --- a/src/main/javacc/BlockConfig.jj +++ b/src/main/javacc/BlockConfig.jj @@ -92,14 +92,15 @@ void matchValueToList(List values) : { Node.Value matchValue() : { Token t; } { - t = { return new Node.Value.Literal(t.image); } + t = + { return new Node.Value.Literal(getSource(t), t.image); } | "classOf" t = - { return new Node.Value.ClassOf(t.image); } + { return new Node.Value.ClassOf(getSource(t), t.image); } | "model.texture" t = - { return new Node.Value.Texture(t.image); } + { return new Node.Value.Texture(getSource(t), t.image); } | "model.tint" t = - { return new Node.Value.Tint(t.image); } + { return new Node.Value.Tint(getSource(t), t.image); } } \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/config/match/Match.kt b/src/main/kotlin/mods/betterfoliage/config/match/Match.kt index 0852d28..e1aae77 100644 --- a/src/main/kotlin/mods/betterfoliage/config/match/Match.kt +++ b/src/main/kotlin/mods/betterfoliage/config/match/Match.kt @@ -1,117 +1,157 @@ 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) { + 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" - } } +/** + * Basic Either monad implementation + */ +sealed class Either { + class Left(val left: L) : Either() + class Right(val right: R) : Either() + + fun leftOrNull() = if (this is Left) left else null + fun rightOrNull() = if (this is Right) right else null + + fun map(func: (R) -> R2): Either = when (this) { + is Left -> this + is Right -> Right(func(right)) + } + + fun mapLeft(func: (L) -> L2): Either = when (this) { + is Left -> Left(func(left)) + is Right -> this + } + + fun ifRight(action: (R) -> Unit) { + if (this is Right) action(right) + } + + companion object { + fun ofLeft(left: L) = Left(left) + fun ofRight(right: R) = Right(right) + } +} + +// this cannot be inside the class for variance reasons +fun Either.flatMap(func: (R) -> Either) = when (this) { + is Either.Left -> this + is Either.Right -> func(right) +} + +fun Either.flatMapLeft(func: (L) -> Either) = when (this) { + is Either.Left -> func(left) + is Either.Right -> this +} + +fun Either.flatten() = when (this) { + is Either.Left -> left + is Either.Right -> right +} + +interface MAnything { + val value: T + val immutable: Boolean +} + +class MListAll(val list: List>) : MAnything { + override val value get() = list.all { it.value } + override val immutable get() = list.all { it.immutable } +} + +class MListAny(val list: List>) : MAnything { + override val value get() = list.any { it.value } + override val immutable get() = list.all { it.immutable } +} + +/** + * Value with metadata related to rule matching applied. + * + * @param value the wrapped value + * @param description human-readable description of what the value represents + * @param configSource identifies where the value is described in the config + * @param immutable true if the value never changes + * (another [MValue] constructed in the same way will have the same value) + * + */ +class MValue( + override val value: T, + val description: String, + val configSource: ConfigSource, + override val immutable: Boolean, +) : MAnything { + companion object { + fun right(value: T, description: String, configSource: ConfigSource, immutable: Boolean = true) = + Either.ofRight(MValue(value, description, configSource, immutable)) + + fun left(description: String, configSource: ConfigSource, immutable: Boolean = true) = + Either.ofLeft(MValue(false, description, configSource, immutable)) + } +} + +typealias MEither = Either, MValue> + +val Node.Value.asEither get() = MValue.right(value, value, configSource, true) +fun Node.Value.left(description: String) = MValue.left(description, configSource) +fun Node.invalidTypeFor(type: String) = MValue.left("invalid type for $type: [${this::class.java.name}]", configSource) +fun Node.error(description: String) = MValue.left(description, configSource) + +fun MEither.mapValue(func: (T) -> R) = map { + MValue(func(it.value), it.description, it.configSource, it.immutable) +} + +fun MEither.mapDescription(func: (MValue) -> String) = map { + MValue(it.value, func(it), it.configSource, it.immutable) +} + +fun MEither.map( + func: (T) -> R, + description: (MValue, R) -> String +) = map { t -> func(t.value).let { r -> MValue(r, description(t, r), t.configSource, t.immutable) } } + +fun MEither.mapNotNull( + func: (T) -> R?, + dLeft: (MValue) -> String = { it.description }, + dRight: (MValue, R) -> String = { m, _ -> m.description } +) = flatMap { t -> + func(t.value)?.let { r -> + MValue.right(r, dRight(t, r), t.configSource, t.immutable) + } ?: MValue.left(dLeft(t), t.configSource, t.immutable) +} + +fun MEither.toRight(value: T) = + flatMapLeft { MValue.right(value, it.description, it.configSource, it.immutable) } + +data class MComparison( + private val opTrue: String, + private val opFalse: String, + val testFunc: (T1, T2) -> Boolean +) { + fun compare(value1: MEither, value2: MEither) = when { + value1 is Either.Left -> value1 + value2 is Either.Left -> value2 + else -> { + val isSuccess = testFunc((value1 as Either.Right).right.value, (value2 as Either.Right).right.value) + MValue.right( + isSuccess, + "${value1.right.description} ${if (isSuccess) opTrue else opFalse} ${value2.right.description}", + value2.right.configSource, + value1.right.immutable && value2.right.immutable + ) + } + }.flatten() + + companion object { + fun of(matchMethod: MatchMethod, testFunc: (T1, T2) -> Boolean) = + MComparison(matchMethod.description(true), matchMethod.description(false), testFunc) + + val equals = of(MatchMethod.EXACT_MATCH) { t1: Any, t2: Any -> t1 == t2 } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/config/match/Matchers.kt b/src/main/kotlin/mods/betterfoliage/config/match/Matchers.kt deleted file mode 100644 index 516fdb9..0000000 --- a/src/main/kotlin/mods/betterfoliage/config/match/Matchers.kt +++ /dev/null @@ -1,166 +0,0 @@ -package mods.betterfoliage.config.match - -import mods.betterfoliage.resource.discovery.RuleProcessingContext -import mods.betterfoliage.resource.discovery.getAncestry -import mods.betterfoliage.util.findFirst -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.visitModel(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 visitModel(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 -> getModelTexture(model, node.value.value) - else -> return node.error("cannot get texture from ${model::class.java.name}") - } - is Node.Value.Tint -> when(val model = ctx.discovery.getUnbaked()) { - is BlockModel -> getModelTint(ctx.discovery.loadHierarchy(model), node.value.value) - else -> return node.error("cannot get tint index 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}") - } - - fun getModelTexture(model: BlockModel, spriteName: String) = - model.getMaterial(spriteName).texture().toString().let { - MatchValue.Found("texture \"${spriteName}\" = \"$it\"", it) - } - - fun getModelTint(model: BlockModel, spriteName: String) = - model.elements.findFirst { element -> - element.faces.entries.firstOrNull { (_, face) -> - face.texture == "#$spriteName" - }?.value?.tintIndex ?: -1 - }?.let { MatchValue.Found("tint index \"$it\" for sprite \"${spriteName}\"", it.toString()) } - ?: MatchValue.Found("tint index \"-1\" for unused sprite \"${spriteName}\"", "-1") -} \ 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 index a4d8219..7f2dba6 100644 --- a/src/main/kotlin/mods/betterfoliage/config/match/ParseTree.kt +++ b/src/main/kotlin/mods/betterfoliage/config/match/ParseTree.kt @@ -10,29 +10,29 @@ data class ConfigSource( sealed class Node { enum class MatchSource { BLOCK_CLASS, BLOCK_NAME, MODEL_LOCATION } - interface HasSource { val configSource: ConfigSource } + abstract val configSource: ConfigSource class MatchValueList( val matchSource: MatchSource, val matchMethod: MatchMethod, override val configSource: ConfigSource, val values: List - ) : Node(), HasSource + ) : Node() class MatchParam( val name: String, val values: List, override val configSource: ConfigSource, - ) : Node(), HasSource + ) : Node() - class SetParam(val name: String, val value: Value, override val configSource: ConfigSource) : Node(), HasSource + class SetParam(val name: String, val value: Value, override val configSource: ConfigSource) : Node() - class MatchAll(override val configSource: ConfigSource, val list: List) : Node(), HasSource + class MatchAll(override val configSource: ConfigSource, val list: List) : Node() - 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) - class Tint(value: String) : Value(value) + abstract class Value(override val configSource: ConfigSource, val value: String) : Node() { + class Literal(configSource: ConfigSource, value: String) : Value(configSource, value) + class ClassOf(configSource: ConfigSource, value: String) : Value(configSource, value) + class Texture(configSource: ConfigSource, value: String) : Value(configSource, value) + class Tint(configSource: ConfigSource, value: String) : Value(configSource, value) } } diff --git a/src/main/kotlin/mods/betterfoliage/config/match/Rules.kt b/src/main/kotlin/mods/betterfoliage/config/match/Rules.kt new file mode 100644 index 0000000..1ecfdff --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/config/match/Rules.kt @@ -0,0 +1,167 @@ +package mods.betterfoliage.config.match + +import mods.betterfoliage.config.match.MatchMethod.CONTAINS +import mods.betterfoliage.config.match.MatchMethod.EXACT_MATCH +import mods.betterfoliage.config.match.MatchMethod.EXTENDS +import mods.betterfoliage.resource.discovery.RuleProcessingContext +import mods.betterfoliage.util.findFirst +import mods.betterfoliage.util.quoted +import mods.betterfoliage.util.tryDefault +import net.minecraft.client.renderer.model.BlockModel +import net.minecraft.util.ResourceLocation +import net.minecraftforge.registries.ForgeRegistries + +typealias PartialLocation = Pair + +object MatchRules { + fun visitRoot(ctx: RuleProcessingContext, node: Node.MatchAll): MListAll { + val results = mutableListOf>() + for (rule in node.list) { + val result = when(rule) { + is Node.MatchValueList -> mMatchList(ctx, rule) + is Node.MatchParam -> mParam(ctx, rule) + is Node.SetParam -> mParamSet(ctx, rule) + else -> rule.error("match type not implemented: ${rule::class.java.name.quoted}").left + } + results.add(result) + if (!result.value) break + } + return MListAll(results) + } + + fun mMatchList(ctx: RuleProcessingContext, node: Node.MatchValueList) = node.values.map { value -> + when (node.matchSource) { + Node.MatchSource.BLOCK_CLASS -> mBlockClass(ctx, node, value) + Node.MatchSource.BLOCK_NAME -> mBlockName(ctx, node, value) + Node.MatchSource.MODEL_LOCATION -> mModel(ctx, node, value) + } + }.let { MListAny(it) } + + fun mBlockClass(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MValue { + val blockClass = ctx.discovery.blockState.block::class.java.let { + MValue.right(it, "block class ${it.name.quoted}", node.configSource) + } + val target = when(value) { + is Node.Value.Literal -> value.asEither.mapNotNull( + func = { tryDefault(null) { Class.forName(it) }}, + dLeft = { "missing class ${it.value}" }, + dRight = { m, _ -> " class ${m.value}" } + ) + is Node.Value.ClassOf -> value.asEither.mapValue(::ResourceLocation).mapNotNull( + func = { loc -> ForgeRegistries.BLOCKS.getValue(loc)?.let { it::class.java } }, + dLeft = { "missing block ${it.value.toString().quoted}" }, + dRight = { m, r -> "class ${r.name.quoted} of block ${m.value}" } + ) + else -> value.invalidTypeFor("block class") + } + return when(node.matchMethod) { + EXACT_MATCH -> MComparison.equals.compare(blockClass, target) + EXTENDS -> classExtends.compare(blockClass, target) + CONTAINS -> node.error("invalid match type for block class: contains").left + } + } + + fun mBlockName(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MValue { + val blockName = MValue.right(ctx.discovery.blockState.block, "", node.configSource).mapNotNull( + func = { it.registryName }, dLeft = { "missing block name" }, dRight = { _, r -> "block name ${r.toString().quoted}" } + ) + val target = when(value) { + is Node.Value.Literal -> value.asEither.map(::splitLocation, ::quoteString) + else -> value.invalidTypeFor("block name") + } + return when(node.matchMethod) { + EXACT_MATCH -> blockNameExact.compare(blockName, target) + CONTAINS -> blockNameContains.compare(blockName, target) + EXTENDS -> node.error("invalid match type for block name: extends").left + } + } + + fun mModel(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MValue { + val model = (ctx.discovery.getUnbaked() as? BlockModel)?.let { + MValue.right(it, "model ${it.name.quoted}", node.configSource) + } ?: node.error("unsupported model type: ${ctx.discovery.getUnbaked()::class.java.name.quoted}") + + val target = when(value) { + is Node.Value.Literal -> value.asEither.map(::splitLocation, ::quoteString) + else -> value.invalidTypeFor("model") + } + val models = when(node.matchMethod) { + EXTENDS -> model.mapValue { ctx.discovery.loadHierarchy(it).ancestors() } + else -> model.mapValue { listOf(it) } + } + return when(node.matchMethod) { + EXACT_MATCH, EXTENDS -> anyModel(node.matchMethod, ::locationMatches) + CONTAINS -> anyModel(CONTAINS, ::locationContains) + }.compare(models, target) + } + + fun mParam(ctx: RuleProcessingContext, node: Node.MatchParam) = node.values.map { value -> + val paramValue = ctx.params[node.name] ?.let { + MValue.right(it, "parameter ${node.name.quoted}", node.configSource, immutable = false) + } ?: node.error("missing parameter ${node.name.quoted}") + + val target = when(value) { + is Node.Value.Literal -> value.asEither.mapDescription { it.description.quoted } + else -> value.invalidTypeFor("parameter") + } + MComparison.equals.compare(paramValue, target) + }.let { MListAny(it) } + + fun mParamSet(ctx: RuleProcessingContext, node: Node.SetParam): MValue { + val target = when(node.value) { + is Node.Value.Literal -> node.value.asEither + is Node.Value.Texture -> when(val model = ctx.discovery.getUnbaked()) { + is BlockModel -> node.value.asEither.map( + func = { model.getMaterial(it).texture().toString() }, + description = { m, r -> "texture \"${m.value}\" = \"$r\" of model ${model.name}"} + ) + else -> node.error("unsupported model type: ${model::class.java.name.quoted}") + } + is Node.Value.Tint -> when(val model = ctx.discovery.getUnbaked()) { + is BlockModel -> node.value.asEither.mapNotNull( + func = { model.tintOf(it)?.toString() }, + dRight = { m, r -> "tint index $r for sprite ${m.value}" }, + dLeft = { m -> "tint index -1 for unused sprite ${m.value}"} + ).toRight("-1") + + else -> node.error("unsupported model type: ${model::class.java.name.quoted}") + } + else -> node.value.invalidTypeFor("prameter") + } + target.ifRight { ctx.params[node.name] = it.value } + return target.mapDescription { m -> "parameter ${node.name} set to ${m.value}" }.mapValue { true }.flatten() + } + + private val classExtends = MComparison.of, Class<*>>(EXTENDS) { c1, c2 -> c2.isAssignableFrom(c1) } + + private val blockNameExact = MComparison.of(EXACT_MATCH) { block, partial -> + locationMatches(block, partial) + } + private val blockNameContains = MComparison.of>(CONTAINS) { block, partial -> + locationContains(block, partial) + } + private fun anyModel(matchMethod: MatchMethod, func: (ResourceLocation, PartialLocation)->Boolean) = + MComparison.of, PartialLocation>(matchMethod) { models, partial -> + models.any { func(ResourceLocation(it.name), partial) } + } + + + fun locationMatches(loc: ResourceLocation, partial: PartialLocation) = + (partial.first == null || loc.namespace == partial.first) && loc.path == partial.second + fun locationContains(loc: ResourceLocation, partial: PartialLocation) = + (partial.first == null || loc.namespace.contains(partial.first!!)) && loc.path.contains(partial.second) + + fun splitLocation(str: String): PartialLocation = + if (str.contains(":")) ResourceLocation(str).let { it.namespace to it.path } else null to str + + fun quoteString(mValue: MValue, newValue: R) = mValue.description.quoted + + fun BlockModel.ancestors(): List = if (parent == null) listOf(this) else parent!!.ancestors() + this + + fun BlockModel.tintOf(spriteName: String) = + elements.findFirst { element -> + element.faces.entries.findFirst { (_, face) -> + if (face.texture == "#$spriteName") face.tintIndex else null + } + } +} diff --git a/src/main/kotlin/mods/betterfoliage/resource/discovery/RuleBasedDiscovery.kt b/src/main/kotlin/mods/betterfoliage/resource/discovery/RuleBasedDiscovery.kt index 9693402..14182a2 100644 --- a/src/main/kotlin/mods/betterfoliage/resource/discovery/RuleBasedDiscovery.kt +++ b/src/main/kotlin/mods/betterfoliage/resource/discovery/RuleBasedDiscovery.kt @@ -1,9 +1,11 @@ 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.config.match.MAnything +import mods.betterfoliage.config.match.MListAll +import mods.betterfoliage.config.match.MListAny +import mods.betterfoliage.config.match.MValue +import mods.betterfoliage.config.match.MatchRules import mods.betterfoliage.util.HasLogger import net.minecraft.client.renderer.model.BlockModel import net.minecraft.client.renderer.model.VariantList @@ -44,7 +46,7 @@ class RuleBasedDiscovery : AbstractModelDiscovery() { fun processBlockModel(ctx: ModelDiscoveryContext) { val ruleCtx = RuleProcessingContext(ctx) val rulesToCheck = BetterFoliage.blockConfig.rules.toMutableList() - val ruleResults = mutableListOf() + val ruleResults = mutableListOf() var previousSize = 0 // stop processing if nothing changes anymore @@ -53,33 +55,41 @@ class RuleBasedDiscovery : AbstractModelDiscovery() { val iterator = rulesToCheck.listIterator() while (iterator.hasNext()) iterator.next().let { rule -> // process single rule - MatchRuleList.visitRoot(ruleCtx, rule).let { result -> + MatchRules.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() + // - rule is immutable (result will always be the same) + if (result.value || result.immutable) iterator.remove() } } } // log result of rule processing - if (ruleResults.any { it.isSuccess }) { + if (ruleResults.any { it.value }) { 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") } - } + ruleResults.forEach { logResult(it) } } discoverers[ruleCtx.params["type"]]?.processModel(ctx, ruleCtx.params) } - fun List.shouldLog() = all { it.isSuccess } || fold(false) { seenInvariantSuccess, result -> - seenInvariantSuccess || (result.isSuccess && result.isInvariant) + fun logResult(match: MAnything) { + when(match) { + is MListAll -> if (match.list.any { it.value }) { + var seenInvariantSuccess = false + match.list.forEach { item -> + if (item.immutable && item.value) seenInvariantSuccess = true + if (seenInvariantSuccess) logResult(item) + } + } + is MListAny -> if (match.value) match.list.first { it.value }.let { logResult(it) } + else match.list.forEach { logResult(it) } + is MValue -> detailLogger.log(Level.INFO, "[${match.configSource}] ${match.description}") + } } } \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/util/Misc.kt b/src/main/kotlin/mods/betterfoliage/util/Misc.kt index d170c42..a0a8af7 100644 --- a/src/main/kotlin/mods/betterfoliage/util/Misc.kt +++ b/src/main/kotlin/mods/betterfoliage/util/Misc.kt @@ -22,6 +22,8 @@ inline fun String.stripEnd(str: String, ignoreCase: Boolean = true) = if (endsWi inline fun ResourceLocation.stripStart(str: String) = ResourceLocation(namespace, path.stripStart(str)) inline fun ResourceLocation.stripEnd(str: String) = ResourceLocation(namespace, path.stripEnd(str)) +val String.quoted: String get() = "\"$this\"" + /** * Property-level delegate backed by a [ThreadLocal]. *