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() : { Node.Value matchValue() : {
Token t; 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> "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> "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> "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 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 { enum class MatchMethod {
EXACT_MATCH, EXTENDS, CONTAINS; 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" EXACT_MATCH -> if (isSuccess) "matches" else "does not match"
EXTENDS -> if (isSuccess) "extends" else "does not extend" EXTENDS -> if (isSuccess) "extends" else "does not extend"
CONTAINS -> if (isSuccess) "contains" else "does not contain" 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 { sealed class Node {
enum class MatchSource { BLOCK_CLASS, BLOCK_NAME, MODEL_LOCATION } enum class MatchSource { BLOCK_CLASS, BLOCK_NAME, MODEL_LOCATION }
interface HasSource { val configSource: ConfigSource } abstract val configSource: ConfigSource
class MatchValueList( class MatchValueList(
val matchSource: MatchSource, val matchSource: MatchSource,
val matchMethod: MatchMethod, val matchMethod: MatchMethod,
override val configSource: ConfigSource, override val configSource: ConfigSource,
val values: List<Value> val values: List<Value>
) : Node(), HasSource ) : Node()
class MatchParam( class MatchParam(
val name: String, val name: String,
val values: List<Value>, val values: List<Value>,
override val configSource: ConfigSource, 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() { abstract class Value(override val configSource: ConfigSource, val value: String) : Node() {
class Literal(value: String) : Value(value) class Literal(configSource: ConfigSource, value: String) : Value(configSource, value)
class ClassOf(value: String) : Value(value) class ClassOf(configSource: ConfigSource, value: String) : Value(configSource, value)
class Texture(value: String) : Value(value) class Texture(configSource: ConfigSource, value: String) : Value(configSource, value)
class Tint(value: String) : Value(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 package mods.betterfoliage.resource.discovery
import mods.betterfoliage.BetterFoliage import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.config.match.MatchResult import mods.betterfoliage.config.match.MAnything
import mods.betterfoliage.config.match.MatchRuleList import mods.betterfoliage.config.match.MListAll
import mods.betterfoliage.config.match.Node import mods.betterfoliage.config.match.MListAny
import mods.betterfoliage.config.match.MValue
import mods.betterfoliage.config.match.MatchRules
import mods.betterfoliage.util.HasLogger import mods.betterfoliage.util.HasLogger
import net.minecraft.client.renderer.model.BlockModel import net.minecraft.client.renderer.model.BlockModel
import net.minecraft.client.renderer.model.VariantList import net.minecraft.client.renderer.model.VariantList
@@ -44,7 +46,7 @@ class RuleBasedDiscovery : AbstractModelDiscovery() {
fun processBlockModel(ctx: ModelDiscoveryContext) { fun processBlockModel(ctx: ModelDiscoveryContext) {
val ruleCtx = RuleProcessingContext(ctx) val ruleCtx = RuleProcessingContext(ctx)
val rulesToCheck = BetterFoliage.blockConfig.rules.toMutableList() val rulesToCheck = BetterFoliage.blockConfig.rules.toMutableList()
val ruleResults = mutableListOf<MatchResult>() val ruleResults = mutableListOf<MListAll>()
var previousSize = 0 var previousSize = 0
// stop processing if nothing changes anymore // stop processing if nothing changes anymore
@@ -53,33 +55,41 @@ class RuleBasedDiscovery : AbstractModelDiscovery() {
val iterator = rulesToCheck.listIterator() val iterator = rulesToCheck.listIterator()
while (iterator.hasNext()) iterator.next().let { rule -> while (iterator.hasNext()) iterator.next().let { rule ->
// process single rule // process single rule
MatchRuleList.visitRoot(ruleCtx, rule).let { result -> MatchRules.visitRoot(ruleCtx, rule).let { result ->
ruleResults.add(result) ruleResults.add(result)
// remove rule from active list if: // remove rule from active list if:
// - rule succeeded (all directives returned success) // - rule succeeded (all directives returned success)
// - rule is invariant (result will always be the same) // - rule is immutable (result will always be the same)
if (result.isSuccess || result.isInvariant) iterator.remove() if (result.value || result.immutable) iterator.remove()
} }
} }
} }
// log result of rule processing // log result of rule processing
if (ruleResults.any { it.isSuccess }) { if (ruleResults.any { it.value }) {
detailLogger.log(Level.INFO, "================================") detailLogger.log(Level.INFO, "================================")
detailLogger.log(Level.INFO, "block state: ${ctx.blockState}") detailLogger.log(Level.INFO, "block state: ${ctx.blockState}")
detailLogger.log(Level.INFO, "block class: ${ctx.blockState.block::class.java.name}") detailLogger.log(Level.INFO, "block class: ${ctx.blockState.block::class.java.name}")
detailLogger.log(Level.INFO, "model : ${ctx.modelLocation}") detailLogger.log(Level.INFO, "model : ${ctx.modelLocation}")
detailLogger.log(Level.INFO, "--------------------------------") detailLogger.log(Level.INFO, "--------------------------------")
ruleResults.forEach { result -> ruleResults.forEach { logResult(it) }
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) discoverers[ruleCtx.params["type"]]?.processModel(ctx, ruleCtx.params)
} }
fun List<MatchResult>.shouldLog() = all { it.isSuccess } || fold(false) { seenInvariantSuccess, result -> fun logResult(match: MAnything<Boolean>) {
seenInvariantSuccess || (result.isSuccess && result.isInvariant) 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.stripStart(str: String) = ResourceLocation(namespace, path.stripStart(str))
inline fun ResourceLocation.stripEnd(str: String) = ResourceLocation(namespace, path.stripEnd(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]. * Property-level delegate backed by a [ThreadLocal].
* *