[WIP] Config parser

This commit is contained in:
octarine-noise
2021-07-12 19:12:42 +02:00
parent 29ab544269
commit c8e79c22ff
12 changed files with 591 additions and 2 deletions

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ build/
classes/
temp/
logs
src/main/javacc/mods

View File

@@ -2,6 +2,7 @@ plugins {
kotlin("jvm").version("1.4.20")
id("net.minecraftforge.gradle").version("4.1.12")
id("org.spongepowered.mixin").version("0.7-SNAPSHOT")
id("com.intershop.gradle.javacc").version("4.0.0")
}
repositories {
@@ -20,6 +21,7 @@ dependencies {
configurations["annotationProcessor"].extendsFrom(configurations["implementation"])
sourceSets {
get("main").ext["refMap"] = "betterfoliage.refmap.json"
get("main").java.srcDir("src/main/javacc/")
}
minecraft {
@@ -36,6 +38,17 @@ minecraft {
}
}
javacc {
configs {
create("blockconfig") {
staticParam = "false"
inputFile = file("src/main/javacc/BlockConfig.jj")
outputDir = file("src/main/javacc/")
packageName = "mods.betterfoliage.config.match.parser"
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

View File

@@ -0,0 +1,102 @@
PARSER_BEGIN(BlockConfigParser)
package mods.betterfoliage.config.match.parser;
import java.util.List;
import java.util.LinkedList;
import mods.betterfoliage.config.match.*;
public class BlockConfigParser {
public String configFile;
ConfigSource getSource(Token t) {
return new ConfigSource(configFile, t.beginLine, t.beginColumn);
}
}
PARSER_END(BlockConfigParser)
// Whitespace definition
SKIP : { " " | "\n" | "\t" | "\r" }
// Single-line comment
SPECIAL_TOKEN : { <lineComment: "//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")> }
// Lexical state for string literal in quotes
SPECIAL_TOKEN : { < quoteStart : "\"" > : withinQuotes }
<withinQuotes> SPECIAL_TOKEN : { < quoteEnd : "\"" > : DEFAULT }
<withinQuotes> TOKEN : { < stringLiteral : (["a"-"z"] | ["0"-"9"] | "/" | "." | "_" | "-" | ":" )* > }
// Symbol tokens
TOKEN : {
< parenStart : "(" > |
< parenEnd : ")" > |
< dot : "." > |
< comma : "," >
}
void matchFile(List<Node.MatchAll> parent) : {
Token t;
} {
(
t = "match"
{ List<Node> nodes = new LinkedList<Node>(); }
(match(nodes))*
"end"
{ parent.add(new Node.MatchAll(getSource(t), nodes)); }
)*
}
void match(List<Node> parent) : {
Token t; Token t2; MatchMethod mm; List<Node.Value> values; Node.Value v;
} {
"block." matchBlock(parent)
|
t = "model." mm = matchMethod() <parenStart> values = matchValueList() <parenEnd>
{ parent.add(new Node.MatchValueList(Node.MatchSource.MODEL_LOCATION, mm, getSource(t), values)); }
|
t = "isParam" <parenStart> t2 = <stringLiteral> <comma> values = matchValueList() <parenEnd>
{ parent.add(new Node.MatchParam(t2.image, values, getSource(t))); }
|
t = "setParam" <parenStart> t2 = <stringLiteral> <comma> v = matchValue() <parenEnd>
{ parent.add(new Node.SetParam(t2.image, v, getSource(t))); }
}
MatchMethod matchMethod() : {} {
"matches" { return MatchMethod.EXACT_MATCH; } |
"extends" { return MatchMethod.EXTENDS; } |
"contains" { return MatchMethod.CONTAINS; }
}
void matchBlock(List<Node> parent) : {
Token t; MatchMethod mm; List<Node.Value> values;
} {
t = "class." mm = matchMethod() <parenStart> values = matchValueList() <parenEnd>
{ parent.add(new Node.MatchValueList(Node.MatchSource.BLOCK_CLASS, mm, getSource(t), values)); }
|
t = "name." mm = matchMethod() <parenStart> values = matchValueList() <parenEnd>
{ parent.add(new Node.MatchValueList(Node.MatchSource.BLOCK_NAME, mm, getSource(t), values)); }
}
List<Node.Value> matchValueList() : {
List<Node.Value> values = new LinkedList<Node.Value>();
} {
matchValueToList(values) (<comma> matchValueToList(values))* { return values; }
}
void matchValueToList(List<Node.Value> values) : {
Node.Value v;
} {
v = matchValue() { values.add(v); }
}
Node.Value matchValue() : {
Token t;
} {
t = <stringLiteral> { return new Node.Value.Literal(t.image); }
|
"classOf" <parenStart> t = <stringLiteral> <parenEnd>
{ return new Node.Value.ClassOf(t.image); }
|
"model.texture" <parenStart> t = <stringLiteral> <parenEnd>
{ return new Node.Value.Texture(t.image); }
}

View File

@@ -1,6 +1,7 @@
package mods.betterfoliage
import mods.betterfoliage.config.BlockConfig
import mods.betterfoliage.config.BlockConfigOld
import mods.betterfoliage.config.MainConfig
import mods.betterfoliage.util.tryDefault
import mods.betterfoliage.config.clothGuiRoot
@@ -72,7 +73,7 @@ object BetterFoliageMod {
}
Minecraft.getInstance().resourcePackRepository.addPackFinder(BetterFoliage.generatedPack.finder)
bus.register(BlockConfig)
bus.register(BlockConfigOld)
BetterFoliage.init()
}
}

View File

@@ -0,0 +1,40 @@
package mods.betterfoliage.config
import mods.betterfoliage.config.match.Node
import mods.betterfoliage.config.match.parser.BlockConfigParser
import mods.betterfoliage.config.match.parser.ParseException
import mods.betterfoliage.config.match.parser.TokenMgrError
import mods.betterfoliage.util.HasLogger
import mods.betterfoliage.util.stripStart
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.Level.ERROR
class BlockConfig : HasLogger() {
lateinit var rules: List<Node.MatchAll>
fun readConfig(manager: IResourceManager) {
val configs = manager.listResources("config/betterfoliage") { it.endsWith(".rules") }
rules = configs.flatMap { configLocation ->
val resource = manager.getResource(configLocation)
val parser = BlockConfigParser(resource.inputStream)
.apply { configFile = configLocation.stripStart("config/betterfoliage/").toString() }
try {
mutableListOf<Node.MatchAll>().apply { parser.matchFile(this) }
} catch (e: ParseException) {
parseError(e, configLocation)
} catch (e: TokenMgrError) {
parseError(e, configLocation)
}
}
}
fun parseError(e: Throwable, location: ResourceLocation): List<Node.MatchAll> {
"Error parsing block config $location: ${e.message}".let {
logger.log(ERROR, it)
detailLogger.log(ERROR, it)
}
return emptyList()
}
}

View File

@@ -7,7 +7,7 @@ import net.minecraft.util.ResourceLocation
import net.minecraftforge.eventbus.api.SubscribeEvent
import net.minecraftforge.fml.config.ModConfig
object BlockConfig {
object BlockConfigOld {
private val list = mutableListOf<Any>()
val leafBlocks = blocks("leaves_blocks_default.cfg")

View File

@@ -0,0 +1,117 @@
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) {
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"
}
}

View File

@@ -0,0 +1,150 @@
package mods.betterfoliage.config.match
import mods.betterfoliage.resource.discovery.RuleProcessingContext
import mods.betterfoliage.resource.discovery.getAncestry
import mods.betterfoliage.util.tryDefault
import net.minecraft.block.Block
import net.minecraft.client.renderer.model.BlockModel
import net.minecraft.util.ResourceLocation
import net.minecraftforge.registries.ForgeRegistries
object MatchRuleList {
fun visitRoot(ctx: RuleProcessingContext, node: Node.MatchAll): MatchResult {
val results = mutableListOf<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.visit(ctx, node, value)
}
} catch (e: Exception) {
MatchResult.Error(node.configSource, e.message ?: "")
}
}.let { MatchResult.Any(node.configSource, it) }
}
object BlockMatchRules {
fun visitClass(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MatchResult {
val source = ctx.discovery.blockState.block::class.java.let {
MatchValue.Found("block class \"${it.name}\"", it)
}
val target = when(value) {
is Node.Value.Literal -> tryDefault(null) { Class.forName(value.value) as Class<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 visit(ctx: RuleProcessingContext, node: Node.MatchValueList, value: Node.Value): MatchResult {
val source = ctx.discovery.modelLocation.let { MatchValue.Found("model \"$it\"", it) }
if (value !is Node.Value.Literal) return node.invalidValueType("model", value)
val (namespace, path) = value.value.splitLocation()
val check = when (node.matchMethod) {
MatchMethod.EXACT_MATCH, MatchMethod.CONTAINS -> listOf(ctx.discovery.modelLocation)
MatchMethod.EXTENDS -> ctx.discovery.bakery.getAncestry(ctx.discovery.modelLocation)
}
return when (node.matchMethod) {
MatchMethod.EXACT_MATCH, MatchMethod.EXTENDS -> node.compare<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 -> model.getMaterial(node.value.value).texture().toString().let {
MatchValue.Found("texture \"${node.value.value}\" = \"$it\"", it)
}
else -> return node.error("cannot get texture from ${model::class.java.name}")
}
else -> return node.invalidValueType("parameter", node.value)
}
ctx.params[node.name] = target.value
return node.action("parameter \"${node.name}\" set to ${target.description}")
}
}

View File

@@ -0,0 +1,37 @@
package mods.betterfoliage.config.match
data class ConfigSource(
val configFile: String,
val line: Int,
val column: Int
) {
override fun toString() = "$configFile @ R$line,C$column"
}
sealed class Node {
enum class MatchSource { BLOCK_CLASS, BLOCK_NAME, MODEL_LOCATION }
interface HasSource { val configSource: ConfigSource }
class MatchValueList(
val matchSource: MatchSource,
val matchMethod: MatchMethod,
override val configSource: ConfigSource,
val values: List<Value>
) : Node(), HasSource
class MatchParam(
val name: String,
val values: List<Value>,
override val configSource: ConfigSource,
) : Node(), HasSource
class SetParam(val name: String, val value: Value, override val configSource: ConfigSource) : Node(), HasSource
class MatchAll(override val configSource: ConfigSource, val list: List<Node>) : Node(), HasSource
abstract class Value(val value: String) : Node() {
class Literal(value: String) : Value(value)
class ClassOf(value: String) : Value(value)
class Texture(value: String) : Value(value)
}
}

View File

@@ -31,6 +31,10 @@ abstract class AbstractModelDiscovery : HasLogger(), ModelDiscovery {
}
open fun processModel(ctx: ModelDiscoveryContext) {
processContainerModel(ctx)
}
fun processContainerModel(ctx: ModelDiscoveryContext) {
val model = ctx.getUnbaked()
// built-in support for container models

View File

@@ -0,0 +1,79 @@
package mods.betterfoliage.resource.discovery
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.config.match.MatchResult
import mods.betterfoliage.config.match.MatchRuleList
import mods.betterfoliage.config.match.Node
import mods.betterfoliage.util.HasLogger
import net.minecraft.client.renderer.model.BlockModel
import net.minecraft.client.renderer.model.VariantList
import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Level
abstract class ParametrizedModelDiscovery : HasLogger() {
abstract fun processModel(ctx: ModelDiscoveryContext, params: Map<String, String>)
fun Map<String, String>.texture(key: String): ResourceLocation? {
val result = get(key)?.let { ResourceLocation(it) }
if (result == null) detailLogger.log(Level.WARN, "Cannot find texture parameter \"$key\"")
return result
}
}
class RuleProcessingContext(
val discovery: ModelDiscoveryContext
) {
val params = mutableMapOf("type" to "none")
}
class RuleBasedDiscovery : AbstractModelDiscovery() {
val discoverers = mutableMapOf<String, ParametrizedModelDiscovery>()
override fun processModel(ctx: ModelDiscoveryContext) = when(ctx.getUnbaked()) {
is VariantList -> processContainerModel(ctx)
is BlockModel -> processBlockModel(ctx)
else -> Unit
}
fun processBlockModel(ctx: ModelDiscoveryContext) {
val ruleCtx = RuleProcessingContext(ctx)
val rulesToCheck = BetterFoliage.blockConfig.rules.toMutableList()
val ruleResults = mutableListOf<MatchResult>()
var previousSize = 0
// stop processing if nothing changes anymore
while (rulesToCheck.size != previousSize) {
previousSize = rulesToCheck.size
val iterator = rulesToCheck.listIterator()
while (iterator.hasNext()) iterator.next().let { rule ->
// process single rule
MatchRuleList.visitRoot(ruleCtx, rule).let { result ->
ruleResults.add(result)
// remove rule from active list if:
// - rule succeeded (all directives returned success)
// - rule is invariant (result will always be the same)
if (result.isSuccess || result.isInvariant) iterator.remove()
}
}
}
// log result of rule processing
if (ruleResults.any { it.isSuccess }) {
detailLogger.log(Level.INFO, "================================")
detailLogger.log(Level.INFO, "block state: ${ctx.blockState}")
detailLogger.log(Level.INFO, "block class: ${ctx.blockState.block::class.java.name}")
detailLogger.log(Level.INFO, "model : ${ctx.modelLocation}")
detailLogger.log(Level.INFO, "--------------------------------")
ruleResults.forEach { result ->
if (result !is MatchResult.RootList || result.results.shouldLog())
result.log { source, message -> detailLogger.log(Level.INFO, "[$source] $message") }
}
}
discoverers[ruleCtx.params["type"]]?.processModel(ctx, ruleCtx.params)
}
fun List<MatchResult>.shouldLog() = all { it.isSuccess } || fold(false) { seenInvariantSuccess, result ->
seenInvariantSuccess || (result.isSuccess && result.isInvariant)
}
}

View File

@@ -0,0 +1,45 @@
// Leaves
match block.class.extends(classOf("minecraft:oak_leaves")) setParam("type", "leaf") end
match isParam("type", "leaf")
model.extends("minecraft:block/leaves", "minecraft:block/cube_all")
setParam("texture-leaf", model.texture("all"))
end
// Podzol
match block.name.matches("minecraft:podzol") setParam("type", "grass") end
// Grass
match block.class.extends(classOf("minecraft:grass_block")) setParam("type", "grass") end
match isParam("type", "grass")
model.extends("minecraft:block/grass_block", "minecraft:block/cube_bottom_top")
setParam("texture-grass", model.texture("top"))
end
// Dirt
match block.name.matches("minecraft:dirt") setParam("type", "dirt") end
// Wood Log
match block.class.extends(classOf("minecraft:oak_log")) setParam("type", "round-log") end
match isParam("type", "round-log")
model.extends("minecraft:block/cube_column", "minecraft:block/cube_column_horizontal")
setParam("texture-side", model.texture("side"))
setParam("texture-end", model.texture("end"))
end
match isParam("type", "round-log")
model.extends("minecraft:block/cube_all")
setParam("texture-side", model.texture("all"))
setParam("texture-end", model.texture("all"))
end
// Sand & Dirt
match block.name.matches("minecraft:sand", "minecraft:red_sand") setParam("type", "sand") end
// Cactus, Lilypad, Mycelium, Netherrack
match block.name.matches("minecraft:cactus") setParam("type", "cactus") end
match block.name.matches("minecraft:lilypad") setParam("type", "lilypad") end
match block.name.matches("minecraft:mycelium") setParam("type", "mycelium") end
match block.name.matches("minecraft:netherrack") setParam("type", "netherrack") end