cleaner rule matching implementation based on Either
This commit is contained in:
@@ -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); }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
167
src/main/kotlin/mods/betterfoliage/config/match/Rules.kt
Normal file
167
src/main/kotlin/mods/betterfoliage/config/match/Rules.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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].
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user