cleaner rule matching implementation based on Either

This commit is contained in:
octarine-noise
2021-07-15 00:50:46 +02:00
parent 8ef84718b5
commit 4637e282ce
7 changed files with 354 additions and 300 deletions

View File

@@ -92,14 +92,15 @@ void matchValueToList(List<Node.Value> values) : {
Node.Value matchValue() : {
Token t;
} {
t = <stringLiteral> { return new Node.Value.Literal(t.image); }
t = <stringLiteral>
{ return new Node.Value.Literal(getSource(t), t.image); }
|
"classOf" <parenStart> t = <stringLiteral> <parenEnd>
{ return new Node.Value.ClassOf(t.image); }
{ return new Node.Value.ClassOf(getSource(t), t.image); }
|
"model.texture" <parenStart> t = <stringLiteral> <parenEnd>
{ return new Node.Value.Texture(t.image); }
{ return new Node.Value.Texture(getSource(t), t.image); }
|
"model.tint" <parenStart> t = <stringLiteral> <parenEnd>
{ return new Node.Value.Tint(t.image); }
{ return new Node.Value.Tint(getSource(t), t.image); }
}

View File

@@ -1,117 +1,157 @@
package mods.betterfoliage.config.match
typealias RuleLogConsumer = (ConfigSource, String) -> Unit
sealed class MatchValue(val description: String) {
class Found<T>(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>) : 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>) : 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 <T> Node.MatchValueList.compare(sourceValue: MatchValue, targetValue: Node.Value, func: (MatchValue.Found<T>) -> Boolean): MatchResult {
if (sourceValue is MatchValue.Missing || sourceValue is MatchValue.Invalid) return invalidValue(sourceValue)
val isSuccess = func(sourceValue as MatchValue.Found<T>)
return MatchResult.UniComparison(isSuccess, true, configSource, sourceValue, targetValue.value, matchMethod)
}
fun <T> Node.MatchValueList.compare(sourceValue: MatchValue, targetValue: MatchValue, func: (MatchValue.Found<T>, MatchValue.Found<T>) -> 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<T>, targetValue as MatchValue.Found<T>)
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<out L, out R> {
class Left<L>(val left: L) : Either<L, Nothing>()
class Right<R>(val right: R) : Either<Nothing, R>()
fun leftOrNull() = if (this is Left) left else null
fun rightOrNull() = if (this is Right) right else null
fun <R2> map(func: (R) -> R2): Either<L, R2> = when (this) {
is Left<L> -> this
is Right<R> -> Right(func(right))
}
fun <L2> mapLeft(func: (L) -> L2): Either<L2, R> = when (this) {
is Left<L> -> Left(func(left))
is Right<R> -> this
}
fun ifRight(action: (R) -> Unit) {
if (this is Right) action(right)
}
companion object {
fun <L> ofLeft(left: L) = Left(left)
fun <R> ofRight(right: R) = Right(right)
}
}
// this cannot be inside the class for variance reasons
fun <L, R, R2> Either<L, R>.flatMap(func: (R) -> Either<L, R2>) = when (this) {
is Either.Left<L> -> this
is Either.Right<R> -> func(right)
}
fun <L, R, L2> Either<L, R>.flatMapLeft(func: (L) -> Either<L2, R>) = when (this) {
is Either.Left<L> -> func(left)
is Either.Right<R> -> this
}
fun <T> Either<T, T>.flatten() = when (this) {
is Either.Left -> left
is Either.Right -> right
}
interface MAnything<out T> {
val value: T
val immutable: Boolean
}
class MListAll(val list: List<MAnything<Boolean>>) : MAnything<Boolean> {
override val value get() = list.all { it.value }
override val immutable get() = list.all { it.immutable }
}
class MListAny(val list: List<MValue<Boolean>>) : MAnything<Boolean> {
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<out T>(
override val value: T,
val description: String,
val configSource: ConfigSource,
override val immutable: Boolean,
) : MAnything<T> {
companion object {
fun <T> 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<T> = Either<MValue<Boolean>, MValue<T>>
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 <T, R> MEither<T>.mapValue(func: (T) -> R) = map {
MValue(func(it.value), it.description, it.configSource, it.immutable)
}
fun <T> MEither<T>.mapDescription(func: (MValue<T>) -> String) = map {
MValue(it.value, func(it), it.configSource, it.immutable)
}
fun <T, R> MEither<T>.map(
func: (T) -> R,
description: (MValue<T>, R) -> String
) = map { t -> func(t.value).let { r -> MValue(r, description(t, r), t.configSource, t.immutable) } }
fun <T, R> MEither<T>.mapNotNull(
func: (T) -> R?,
dLeft: (MValue<T>) -> String = { it.description },
dRight: (MValue<T>, 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 <T> MEither<T>.toRight(value: T) =
flatMapLeft { MValue.right(value, it.description, it.configSource, it.immutable) }
data class MComparison<T1, T2>(
private val opTrue: String,
private val opFalse: String,
val testFunc: (T1, T2) -> Boolean
) {
fun compare(value1: MEither<T1>, value2: MEither<T2>) = 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 <T1, T2> 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 }
}
}

View File

@@ -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<MatchResult>()
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<out Block> }
?.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<ResourceLocation>(source, value) { isExactName(it.value, namespace, path) }
MatchMethod.CONTAINS -> node.compare<ResourceLocation>(source, value) { isContainsName(it.value, namespace, path) }
MatchMethod.EXTENDS -> node.error("invalid match type for block name: \"extends\"")
}
}
private fun isExactClass(source: MatchValue.Found<Class<out Block>>, target: MatchValue.Found<Class<out Block>>) =
source.value == target.value
private fun isExtendsClass(source: MatchValue.Found<Class<out Block>>, target: MatchValue.Found<Class<out Block>>) =
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<ResourceLocation>(source, value) { isExactModel(check, namespace, path) }
MatchMethod.CONTAINS -> node.compare<ResourceLocation>(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<ResourceLocation>, namespace: String?, path: String) = models.any { model ->
(namespace == null || namespace == model.namespace) && path == model.path
}
private fun isContainsModel(models: List<ResourceLocation>, 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")
}

View File

@@ -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<Value>
) : Node(), HasSource
) : Node()
class MatchParam(
val name: String,
val values: List<Value>,
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>) : Node(), HasSource
class MatchAll(override val configSource: ConfigSource, val list: List<Node>) : 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)
}
}

View File

@@ -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<String?, String>
object MatchRules {
fun visitRoot(ctx: RuleProcessingContext, node: Node.MatchAll): MListAll {
val results = mutableListOf<MAnything<Boolean>>()
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<Boolean> {
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<Boolean> {
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<Boolean> {
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<Boolean> {
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<*>, Class<*>>(EXTENDS) { c1, c2 -> c2.isAssignableFrom(c1) }
private val blockNameExact = MComparison.of<ResourceLocation, PartialLocation>(EXACT_MATCH) { block, partial ->
locationMatches(block, partial)
}
private val blockNameContains = MComparison.of<ResourceLocation, Pair<String?, String>>(CONTAINS) { block, partial ->
locationContains(block, partial)
}
private fun anyModel(matchMethod: MatchMethod, func: (ResourceLocation, PartialLocation)->Boolean) =
MComparison.of<List<BlockModel>, 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 <T, R> quoteString(mValue: MValue<T>, newValue: R) = mValue.description.quoted
fun BlockModel.ancestors(): List<BlockModel> = 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
}
}
}

View File

@@ -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<MatchResult>()
val ruleResults = mutableListOf<MListAll>()
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<MatchResult>.shouldLog() = all { it.isSuccess } || fold(false) { seenInvariantSuccess, result ->
seenInvariantSuccess || (result.isSuccess && result.isInvariant)
fun logResult(match: MAnything<Boolean>) {
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<Boolean> -> detailLogger.log(Level.INFO, "[${match.configSource}] ${match.description}")
}
}
}

View File

@@ -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].
*