rewrite model and texture detection
expose in mod configuration
This commit is contained in:
@@ -9,8 +9,15 @@ import net.minecraftforge.fml.common.event.FMLPostInitializationEvent
|
||||
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent
|
||||
import net.minecraftforge.fml.common.network.NetworkCheckHandler
|
||||
import net.minecraftforge.fml.relauncher.Side
|
||||
import org.apache.logging.log4j.Level.DEBUG
|
||||
import org.apache.logging.log4j.Level.INFO
|
||||
import org.apache.logging.log4j.Logger
|
||||
import org.apache.logging.log4j.simple.SimpleLogger
|
||||
import org.apache.logging.log4j.simple.SimpleLoggerContext
|
||||
import org.apache.logging.log4j.util.PropertiesUtil
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
import java.util.*
|
||||
|
||||
@Mod(
|
||||
modid = BetterFoliageMod.MOD_ID,
|
||||
@@ -28,6 +35,8 @@ object BetterFoliageMod {
|
||||
const val GUI_FACTORY = "mods.betterfoliage.client.gui.ConfigGuiFactory"
|
||||
|
||||
lateinit var log: Logger
|
||||
lateinit var logDetail: Logger
|
||||
|
||||
var config: Configuration? = null
|
||||
var isAfterPostInit = false
|
||||
|
||||
@@ -39,8 +48,16 @@ object BetterFoliageMod {
|
||||
@Mod.EventHandler
|
||||
fun preInit(event: FMLPreInitializationEvent) {
|
||||
log = event.modLog
|
||||
logDetail = SimpleLogger(
|
||||
"BetterFoliage",
|
||||
DEBUG,
|
||||
false, false, true, false,
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
null,
|
||||
PropertiesUtil(Properties()),
|
||||
PrintStream(File(event.modConfigurationDirectory.parentFile, "logs/betterfoliage.log"))
|
||||
)
|
||||
config = Configuration(event.suggestedConfigurationFile, null, false)
|
||||
|
||||
}
|
||||
|
||||
@Mod.EventHandler
|
||||
|
||||
@@ -5,10 +5,7 @@ import mods.betterfoliage.client.gui.ConfigGuiFactory
|
||||
import mods.betterfoliage.client.integration.OptifineCTM
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration
|
||||
import mods.betterfoliage.client.render.*
|
||||
import mods.betterfoliage.client.texture.GrassGenerator
|
||||
import mods.betterfoliage.client.texture.GrassRegistry
|
||||
import mods.betterfoliage.client.texture.LeafGenerator
|
||||
import mods.betterfoliage.client.texture.LeafRegistry
|
||||
import mods.betterfoliage.client.texture.*
|
||||
import mods.octarinecore.client.KeyHandler
|
||||
import mods.octarinecore.client.resource.CenteringTextureGenerator
|
||||
import mods.octarinecore.client.resource.GeneratorPack
|
||||
@@ -63,14 +60,21 @@ object Client {
|
||||
)
|
||||
|
||||
val singletons = listOf(
|
||||
LeafRegistry,
|
||||
GrassRegistry,
|
||||
StandardLeafSupport,
|
||||
StandardGrassSupport,
|
||||
LeafWindTracker,
|
||||
RisingSoulTextures,
|
||||
ShadersModIntegration,
|
||||
OptifineCTM
|
||||
)
|
||||
|
||||
fun log(level: Level, msg: String) = BetterFoliageMod.log.log(level, msg)
|
||||
fun log(level: Level, msg: String) {
|
||||
BetterFoliageMod.log.log(level, msg)
|
||||
BetterFoliageMod.logDetail.log(level, msg)
|
||||
}
|
||||
|
||||
fun logDetail(msg: String) {
|
||||
BetterFoliageMod.logDetail.log(Level.DEBUG, msg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,35 +15,38 @@ import net.minecraft.block.Block
|
||||
import net.minecraft.block.state.IBlockState
|
||||
import net.minecraft.client.renderer.BlockRendererDispatcher
|
||||
import net.minecraft.client.renderer.VertexBuffer
|
||||
import net.minecraft.client.renderer.block.model.IBakedModel
|
||||
import net.minecraft.init.Blocks
|
||||
import net.minecraft.util.BlockRenderLayer
|
||||
import net.minecraft.util.BlockRenderLayer.*
|
||||
import net.minecraft.util.BlockRenderLayer.CUTOUT
|
||||
import net.minecraft.util.BlockRenderLayer.CUTOUT_MIPPED
|
||||
import net.minecraft.util.EnumFacing
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IBlockAccess
|
||||
import net.minecraft.world.World
|
||||
import net.minecraftforge.client.model.IModel
|
||||
import net.minecraftforge.client.model.ModelLoader
|
||||
import net.minecraftforge.common.MinecraftForge
|
||||
import net.minecraftforge.fml.relauncher.Side
|
||||
import net.minecraftforge.fml.relauncher.SideOnly
|
||||
|
||||
fun doesSideBlockRenderingOverride(original: Boolean, blockAccess: IBlockAccess, pos: BlockPos, side: EnumFacing): Boolean {
|
||||
return original && !(Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(blockAccess.getBlockState(pos).block));
|
||||
return original && !(Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesClass(blockAccess.getBlockState(pos).block));
|
||||
}
|
||||
|
||||
fun isOpaqueCubeOverride(original: Boolean, state: IBlockState): Boolean {
|
||||
// caution: blocks are initialized and the method called during startup
|
||||
if (!BetterFoliageMod.isAfterPostInit) return original
|
||||
return original && !(Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(state.block))
|
||||
return original && !(Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesClass(state.block))
|
||||
}
|
||||
|
||||
fun getAmbientOcclusionLightValueOverride(original: Float, state: IBlockState): Float {
|
||||
if (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(state.block)) return Config.roundLogs.dimming;
|
||||
if (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesClass(state.block)) return Config.roundLogs.dimming;
|
||||
return original;
|
||||
}
|
||||
|
||||
fun getUseNeighborBrightnessOverride(original: Boolean, state: IBlockState): Boolean {
|
||||
return original || (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(state.block));
|
||||
return original || (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesClass(state.block));
|
||||
}
|
||||
|
||||
fun onRandomDisplayTick(world: World, state: IBlockState, pos: BlockPos) {
|
||||
@@ -57,7 +60,7 @@ fun onRandomDisplayTick(world: World, state: IBlockState, pos: BlockPos) {
|
||||
|
||||
if (Config.enabled &&
|
||||
Config.fallingLeaves.enabled &&
|
||||
Config.blocks.leaves.matchesID(state.block) &&
|
||||
Config.blocks.leavesClasses.matchesClass(state.block) &&
|
||||
world.isAirBlock(pos + down1) &&
|
||||
Math.random() < Config.fallingLeaves.chance) {
|
||||
EntityFallingLeavesFX(world, pos).addIfValid()
|
||||
|
||||
@@ -31,9 +31,11 @@ object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.DOMAI
|
||||
var enabled by boolean(true)
|
||||
|
||||
object blocks {
|
||||
val leavesClasses = BlockMatcher(BetterFoliageMod.DOMAIN, "LeavesBlocksDefault.cfg")
|
||||
val leavesModels = ModelTextureListConfigOption(BetterFoliageMod.DOMAIN, "LeavesModelsDefault.cfg", 1)
|
||||
val grassClasses = BlockMatcher(BetterFoliageMod.DOMAIN, "GrassBlocksDefault.cfg")
|
||||
val grassModels = ModelTextureListConfigOption(BetterFoliageMod.DOMAIN, "GrassModelsDefault.cfg", 1)
|
||||
val dirt = BlockMatcher(BetterFoliageMod.DOMAIN, "DirtDefault.cfg")
|
||||
val grass = BlockMatcher(BetterFoliageMod.DOMAIN, "GrassDefault.cfg")
|
||||
val leaves = BlockMatcher(BetterFoliageMod.DOMAIN, "LeavesDefault.cfg")
|
||||
val crops = BlockMatcher(BetterFoliageMod.DOMAIN, "CropDefault.cfg")
|
||||
val logs = BlockMatcher(BetterFoliageMod.DOMAIN, "LogDefault.cfg")
|
||||
val sand = BlockMatcher(BetterFoliageMod.DOMAIN, "SandDefault.cfg")
|
||||
@@ -177,9 +179,17 @@ object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.DOMAI
|
||||
val trailDensity by int(min=1, max=16, default=3)
|
||||
}
|
||||
|
||||
val forceReloadOptions = listOf(
|
||||
blocks.leavesClasses,
|
||||
blocks.leavesModels,
|
||||
blocks.grassClasses,
|
||||
blocks.grassModels,
|
||||
shortGrass["saturationThreshold"]
|
||||
)
|
||||
|
||||
override fun onChange(event: ConfigChangedEvent.OnConfigChangedEvent) {
|
||||
super.onChange(event)
|
||||
if (hasChanged(blocks, shortGrass["saturationThreshold"]))
|
||||
if (hasChanged(forceReloadOptions))
|
||||
Minecraft.getMinecraft().refreshResources()
|
||||
else
|
||||
Minecraft.getMinecraft().renderGlobal.loadRenderers()
|
||||
|
||||
@@ -61,6 +61,9 @@ object OptifineCTM {
|
||||
return result
|
||||
}
|
||||
|
||||
fun getAllCTM(states: List<IBlockState>, icon: TextureAtlasSprite): Collection<TextureAtlasSprite> =
|
||||
states.flatMap { getAllCTM(it, icon) }.toSet()
|
||||
|
||||
fun override(texture: TextureAtlasSprite, ctx: BlockContext, face: EnumFacing) =
|
||||
override(texture, ctx.world!!, ctx.pos, face)
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ object ShadersModIntegration {
|
||||
* Called from transformed ShadersMod code.
|
||||
* @see mods.betterfoliage.loader.BetterFoliageTransformer
|
||||
*/
|
||||
@JvmStatic fun getBlockIdOverride(original: Long, blockState: IBlockState): Long {
|
||||
if (Config.blocks.leaves.matchesID(blockState.block)) return leavesEntityData
|
||||
if (Config.blocks.crops.matchesID(blockState.block)) return tallGrassEntityData
|
||||
@JvmStatic fun getBlockIdOverride(original: Long, blockState: IBlockState): Long {
|
||||
if (Config.blocks.leavesClasses.matchesClass(blockState.block)) return leavesEntityData
|
||||
if (Config.blocks.crops.matchesClass(blockState.block)) return tallGrassEntityData
|
||||
return original
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.client.config.BlockMatcher
|
||||
import mods.betterfoliage.client.integration.OptifineCTM
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration
|
||||
import mods.betterfoliage.client.render.AbstractRenderColumn.BlockType.*
|
||||
@@ -8,6 +7,7 @@ import mods.betterfoliage.client.render.AbstractRenderColumn.QuadrantType.*
|
||||
import mods.octarinecore.client.render.*
|
||||
import mods.octarinecore.client.resource.BlockTextureInspector
|
||||
import mods.octarinecore.common.*
|
||||
import mods.octarinecore.common.config.BlockMatcher
|
||||
import net.minecraft.block.state.IBlockState
|
||||
import net.minecraft.client.renderer.BlockRendererDispatcher
|
||||
import net.minecraft.client.renderer.VertexBuffer
|
||||
|
||||
@@ -2,6 +2,7 @@ package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.texture.LeafRegistry
|
||||
import mods.betterfoliage.client.texture.defaultLeafColor
|
||||
import mods.octarinecore.PI2
|
||||
import mods.octarinecore.client.render.AbstractEntityFX
|
||||
import mods.octarinecore.client.render.HSB
|
||||
@@ -42,9 +43,14 @@ AbstractEntityFX(world, pos.x.toDouble() + 0.5, pos.y.toDouble(), pos.z.toDouble
|
||||
particleScale = Config.fallingLeaves.size.toFloat() * 0.1f
|
||||
|
||||
val state = world.getBlockState(pos)
|
||||
LeafRegistry[state, world, pos, DOWN]?.let {
|
||||
particleTexture = it.particleTextures[rand.nextInt(1024)]
|
||||
calculateParticleColor(it.averageColor, Minecraft.getMinecraft().blockColors.colorMultiplier(state, world, pos, 0))
|
||||
val blockColor = Minecraft.getMinecraft().blockColors.colorMultiplier(state, world, pos, 0)
|
||||
val leafInfo = LeafRegistry.get(state, world, pos, DOWN)
|
||||
if (leafInfo != null) {
|
||||
particleTexture = leafInfo.particleTextures?.get(rand.nextInt(1024))
|
||||
calculateParticleColor(leafInfo.averageColor, blockColor)
|
||||
} else {
|
||||
particleTexture = LeafRegistry.particles["default"]?.get(rand.nextInt(1024))
|
||||
setColor(blockColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class RenderAlgae : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
|
||||
ctx.cameraDistance < Config.algae.distance &&
|
||||
ctx.blockState(up2).material == Material.WATER &&
|
||||
ctx.blockState(up1).material == Material.WATER &&
|
||||
Config.blocks.dirt.matchesID(ctx.block) &&
|
||||
Config.blocks.dirt.matchesClass(ctx.block) &&
|
||||
ctx.biomeId in Config.algae.biomes &&
|
||||
noise[ctx.pos] < Config.algae.population
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class RenderCactus : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
|
||||
override fun isEligible(ctx: BlockContext): Boolean =
|
||||
Config.enabled && Config.cactus.enabled &&
|
||||
ctx.cameraDistance < Config.cactus.distance &&
|
||||
Config.blocks.cactus.matchesID(ctx.block)
|
||||
Config.blocks.cactus.matchesClass(ctx.block)
|
||||
|
||||
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: VertexBuffer, layer: BlockRenderLayer): Boolean {
|
||||
// get AO data
|
||||
|
||||
@@ -13,8 +13,8 @@ import net.minecraft.util.BlockRenderLayer
|
||||
class RenderConnectedGrass : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
|
||||
override fun isEligible(ctx: BlockContext) =
|
||||
Config.enabled && Config.connectedGrass.enabled &&
|
||||
Config.blocks.dirt.matchesID(ctx.block) &&
|
||||
Config.blocks.grass.matchesID(ctx.block(up1)) &&
|
||||
Config.blocks.dirt.matchesClass(ctx.block) &&
|
||||
Config.blocks.grassClasses.matchesClass(ctx.block(up1)) &&
|
||||
(Config.connectedGrass.snowEnabled || !ctx.blockState(up2).isSnow)
|
||||
|
||||
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: VertexBuffer, layer: BlockRenderLayer): Boolean {
|
||||
|
||||
@@ -18,12 +18,12 @@ class RenderConnectedGrassLog : AbstractBlockRenderingHandler(BetterFoliageMod.M
|
||||
|
||||
override fun isEligible(ctx: BlockContext) =
|
||||
Config.enabled && Config.roundLogs.enabled && Config.roundLogs.connectGrass &&
|
||||
Config.blocks.dirt.matchesID(ctx.block) &&
|
||||
Config.blocks.logs.matchesID(ctx.block(up1))
|
||||
Config.blocks.dirt.matchesClass(ctx.block) &&
|
||||
Config.blocks.logs.matchesClass(ctx.block(up1))
|
||||
|
||||
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: VertexBuffer, layer: BlockRenderLayer): Boolean {
|
||||
val grassDir = grassCheckDirs.find {
|
||||
Config.blocks.grass.matchesID(ctx.block(it.offset))
|
||||
Config.blocks.grassClasses.matchesClass(ctx.block(it.offset))
|
||||
}
|
||||
|
||||
return if (grassDir != null) {
|
||||
|
||||
@@ -47,7 +47,7 @@ class RenderCoral : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
|
||||
ctx.cameraDistance < Config.coral.distance &&
|
||||
(ctx.blockState(up2).material == Material.WATER || Config.coral.shallowWater) &&
|
||||
ctx.blockState(up1).material == Material.WATER &&
|
||||
Config.blocks.sand.matchesID(ctx.block) &&
|
||||
Config.blocks.sand.matchesClass(ctx.block) &&
|
||||
ctx.biomeId in Config.coral.biomes &&
|
||||
noise[ctx.pos] < Config.coral.population
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package mods.betterfoliage.client.render
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.integration.OptifineCTM
|
||||
import mods.betterfoliage.client.integration.ShadersModIntegration
|
||||
import mods.betterfoliage.client.texture.GrassRegistry
|
||||
import mods.octarinecore.client.render.*
|
||||
@@ -47,16 +46,17 @@ class RenderGrass : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
|
||||
Config.enabled &&
|
||||
ctx.cameraDistance < Config.shortGrass.distance &&
|
||||
(Config.shortGrass.grassEnabled || Config.connectedGrass.enabled) &&
|
||||
Config.blocks.grass.matchesID(ctx.block)
|
||||
GrassRegistry[ctx, UP] != null
|
||||
|
||||
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: VertexBuffer, layer: BlockRenderLayer): Boolean {
|
||||
val isConnected = ctx.block(down1).let { Config.blocks.dirt.matchesID(it) || Config.blocks.grass.matchesID(it) }
|
||||
val isConnected = ctx.block(down1).let {
|
||||
Config.blocks.dirt.matchesClass(it) ||
|
||||
Config.blocks.grassClasses.matchesClass(it)
|
||||
}
|
||||
val isSnowed = ctx.blockState(up1).isSnow
|
||||
val connectedGrass = isConnected && Config.connectedGrass.enabled && (!isSnowed || Config.connectedGrass.snowEnabled)
|
||||
|
||||
val grassInfo = GrassRegistry[ctx.blockState(Int3.zero)] ?: return renderWorldBlockBase(ctx, dispatcher, renderer, layer)
|
||||
val grassTopTexture = OptifineCTM.override(grassInfo.grassTopTexture, ctx, UP)
|
||||
|
||||
val grassInfo = GrassRegistry[ctx, UP]!!
|
||||
val blockColor = ctx.blockData(Int3.zero).color
|
||||
|
||||
if (connectedGrass) {
|
||||
@@ -70,7 +70,7 @@ class RenderGrass : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
|
||||
fullCube,
|
||||
Rotation.identity,
|
||||
ctx.blockCenter,
|
||||
icon = { ctx, qi, q -> grassTopTexture },
|
||||
icon = { ctx, qi, q -> grassInfo.grassTopTexture },
|
||||
postProcess = { ctx, qi, q, vi, v ->
|
||||
rotateUV(2)
|
||||
if (isSnowed) {
|
||||
|
||||
@@ -41,14 +41,14 @@ class RenderLeaves : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
|
||||
Config.enabled &&
|
||||
Config.leaves.enabled &&
|
||||
ctx.cameraDistance < Config.leaves.distance &&
|
||||
Config.blocks.leaves.matchesID(ctx.block)
|
||||
LeafRegistry[ctx, DOWN] != null
|
||||
|
||||
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: VertexBuffer, layer: BlockRenderLayer): Boolean {
|
||||
val isSnowed = ctx.blockState(up1).material.let {
|
||||
it == Material.SNOW || it == Material.CRAFTED_SNOW
|
||||
}
|
||||
renderWorldBlockBase(ctx, dispatcher, renderer, null)
|
||||
val leafInfo = LeafRegistry[ctx, DOWN] ?: return false
|
||||
val leafInfo = LeafRegistry[ctx, DOWN]!!
|
||||
val blockColor = ctx.blockData(Int3.zero).color
|
||||
|
||||
modelRenderer.updateShading(Int3.zero, allFaces)
|
||||
|
||||
@@ -39,7 +39,7 @@ class RenderLilypad : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
|
||||
override fun isEligible(ctx: BlockContext): Boolean =
|
||||
Config.enabled && Config.lilypad.enabled &&
|
||||
ctx.cameraDistance < Config.lilypad.distance &&
|
||||
Config.blocks.lilypad.matchesID(ctx.block)
|
||||
Config.blocks.lilypad.matchesClass(ctx.block)
|
||||
|
||||
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: VertexBuffer, layer: BlockRenderLayer): Boolean {
|
||||
renderWorldBlockBase(ctx, dispatcher, renderer, null)
|
||||
|
||||
@@ -2,17 +2,12 @@ package mods.betterfoliage.client.render
|
||||
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.integration.OptifineCTM
|
||||
import mods.octarinecore.client.render.BlockContext
|
||||
import mods.octarinecore.client.render.Quad
|
||||
import mods.octarinecore.client.render.ShadingContext
|
||||
import mods.octarinecore.client.render.blockContext
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.rotate
|
||||
import mods.octarinecore.tryDefault
|
||||
import net.minecraft.block.BlockLog
|
||||
import net.minecraft.block.state.IBlockState
|
||||
import net.minecraft.util.EnumFacing.*
|
||||
import net.minecraft.util.EnumFacing.Axis
|
||||
|
||||
class RenderLog : AbstractRenderColumn(BetterFoliageMod.MOD_ID) {
|
||||
|
||||
@@ -21,7 +16,7 @@ class RenderLog : AbstractRenderColumn(BetterFoliageMod.MOD_ID) {
|
||||
override fun isEligible(ctx: BlockContext) =
|
||||
Config.enabled && Config.roundLogs.enabled &&
|
||||
ctx.cameraDistance < Config.roundLogs.distance &&
|
||||
Config.blocks.logs.matchesID(ctx.block)
|
||||
Config.blocks.logs.matchesClass(ctx.block)
|
||||
|
||||
override var axisFunc = { state: IBlockState ->
|
||||
val axis = tryDefault(null) { state.getValue(BlockLog.LOG_AXIS).toString() } ?:
|
||||
@@ -44,8 +39,8 @@ class RenderLog : AbstractRenderColumn(BetterFoliageMod.MOD_ID) {
|
||||
|
||||
override fun resolver(ctx: BlockContext): ColumnTextureResolver? = columnTextures[ctx.blockState(Int3.zero)]
|
||||
|
||||
override val blockPredicate = { state: IBlockState -> Config.blocks.logs.matchesID(state.block) }
|
||||
override val surroundPredicate = { state: IBlockState -> state.isOpaqueCube && !Config.blocks.logs.matchesID(state.block) }
|
||||
override val blockPredicate = { state: IBlockState -> Config.blocks.logs.matchesClass(state.block) }
|
||||
override val surroundPredicate = { state: IBlockState -> state.isOpaqueCube && !Config.blocks.logs.matchesClass(state.block) }
|
||||
|
||||
override val connectPerpendicular: Boolean get() = Config.roundLogs.connectPerpendicular
|
||||
override val connectSolids: Boolean get() = Config.roundLogs.connectSolids
|
||||
|
||||
@@ -46,7 +46,7 @@ class RenderReeds : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
|
||||
ctx.cameraDistance < Config.reed.distance &&
|
||||
ctx.blockState(up2).material == Material.AIR &&
|
||||
ctx.blockState(up1).material == Material.WATER &&
|
||||
Config.blocks.dirt.matchesID(ctx.block) &&
|
||||
Config.blocks.dirt.matchesClass(ctx.block) &&
|
||||
ctx.biomeId in Config.reed.biomes &&
|
||||
noise[ctx.pos] < Config.reed.population
|
||||
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
package mods.betterfoliage.client.texture
|
||||
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.integration.OptifineCTM
|
||||
import mods.octarinecore.client.render.BlockContext
|
||||
import mods.octarinecore.client.render.HSB
|
||||
import mods.octarinecore.client.resource.BlockTextureInspector
|
||||
import mods.octarinecore.client.resource.TextureListModelProcessor
|
||||
import mods.octarinecore.client.resource.TextureMediatedRegistry
|
||||
import mods.octarinecore.client.resource.averageColor
|
||||
import mods.octarinecore.client.resource.get
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.config.BlockMatcher
|
||||
import mods.octarinecore.common.config.ModelTextureList
|
||||
import mods.octarinecore.findFirst
|
||||
import net.minecraft.block.state.IBlockState
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.client.renderer.texture.TextureMap
|
||||
import net.minecraft.util.EnumFacing
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IBlockAccess
|
||||
import net.minecraftforge.common.MinecraftForge
|
||||
import net.minecraftforge.fml.relauncher.Side
|
||||
import net.minecraftforge.fml.relauncher.SideOnly
|
||||
import org.apache.logging.log4j.Level.INFO
|
||||
|
||||
const val defaultGrassColor = 0
|
||||
|
||||
@@ -23,28 +34,66 @@ class GrassInfo(
|
||||
* Color to use for Short Grass rendering instead of the biome color.
|
||||
*
|
||||
* Value is null if the texture is mostly grey (the saturation of its average color is under a configurable limit),
|
||||
* the average color of the texture (significantly brightened) otherwise.
|
||||
* the average color of the texture (significantly ) otherwise.
|
||||
*/
|
||||
val overrideColor: Int?
|
||||
)
|
||||
|
||||
interface IGrassRegistry {
|
||||
fun get(state: IBlockState): GrassInfo?
|
||||
fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos, face: EnumFacing): GrassInfo?
|
||||
}
|
||||
|
||||
/** Collects and manages rendering-related information for grass blocks. */
|
||||
@SideOnly(Side.CLIENT)
|
||||
object GrassRegistry : BlockTextureInspector<GrassInfo>() {
|
||||
object GrassRegistry : IGrassRegistry {
|
||||
val subRegistries = mutableListOf(StandardGrassSupport)
|
||||
|
||||
init {
|
||||
matchClassAndModel(Config.blocks.grass, "block/grass", listOf("top"))
|
||||
matchClassAndModel(Config.blocks.grass, "block/cube_bottom_top", listOf("top"))
|
||||
override fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos, face: EnumFacing) =
|
||||
subRegistries.findFirst { it.get(state, world, pos, face) }
|
||||
|
||||
operator fun get(ctx: BlockContext, face: EnumFacing) = get(ctx.blockState(Int3.zero), ctx.world!!, ctx.pos, face)
|
||||
|
||||
override fun get(state: IBlockState) = subRegistries.findFirst { it.get(state) }
|
||||
}
|
||||
|
||||
object StandardGrassSupport :
|
||||
TextureListModelProcessor<TextureAtlasSprite>,
|
||||
TextureMediatedRegistry<List<String>, GrassInfo>,
|
||||
IGrassRegistry
|
||||
{
|
||||
init { MinecraftForge.EVENT_BUS.register(this) }
|
||||
|
||||
override var stateToKey = mutableMapOf<IBlockState, List<String>>()
|
||||
override var stateToValue = mapOf<IBlockState, TextureAtlasSprite>()
|
||||
override var textureToValue = mutableMapOf<TextureAtlasSprite, GrassInfo>()
|
||||
|
||||
override val logger = BetterFoliageMod.logDetail
|
||||
override val logName = "StandardGrassSupport"
|
||||
override val matchClasses: BlockMatcher get() = Config.blocks.grassClasses
|
||||
override val modelTextures: List<ModelTextureList> get() = Config.blocks.grassModels.list
|
||||
|
||||
override fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos, face: EnumFacing): GrassInfo? {
|
||||
val baseTexture = stateToValue[state] ?: return null
|
||||
return textureToValue[OptifineCTM.override(baseTexture, world, pos, face)] ?: textureToValue[baseTexture]
|
||||
}
|
||||
|
||||
override fun onAfterModelLoad() {
|
||||
super.onAfterModelLoad()
|
||||
Client.log(INFO, "Inspecting grass textures")
|
||||
override fun get(state: IBlockState) = StandardLeafSupport.stateToValue[state].let {
|
||||
if (it == null) null else textureToValue[it]
|
||||
}
|
||||
|
||||
override fun processTextures(state: IBlockState, textures: List<TextureAtlasSprite>, atlas: TextureMap): GrassInfo {
|
||||
val hsb = HSB.fromColor(textures[0].averageColor ?: defaultGrassColor)
|
||||
override fun processStitch(state: IBlockState, key: List<String>, atlas: TextureMap) = atlas[key[0]]
|
||||
|
||||
override fun processTexture(states: List<IBlockState>, texture: TextureAtlasSprite, atlas: TextureMap) {
|
||||
registerGrass(texture, atlas)
|
||||
OptifineCTM.getAllCTM(states, texture).forEach {
|
||||
registerGrass(it, atlas)
|
||||
}
|
||||
}
|
||||
|
||||
fun registerGrass(texture: TextureAtlasSprite, atlas: TextureMap) {
|
||||
val hsb = HSB.fromColor(texture.averageColor ?: defaultGrassColor)
|
||||
val overrideColor = if (hsb.saturation > Config.shortGrass.saturationThreshold) hsb.copy(brightness = 0.8f).asColor else null
|
||||
return GrassInfo(textures[0], overrideColor)
|
||||
textureToValue[texture] = GrassInfo(texture, overrideColor)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
package mods.betterfoliage.client.texture
|
||||
|
||||
import mods.betterfoliage.BetterFoliageMod
|
||||
import mods.betterfoliage.client.Client
|
||||
import mods.betterfoliage.client.config.Config
|
||||
import mods.betterfoliage.client.integration.OptifineCTM
|
||||
import mods.octarinecore.client.render.BlockContext
|
||||
import mods.octarinecore.client.resource.BlockTextureInspector
|
||||
import mods.octarinecore.client.resource.IconSet
|
||||
import mods.octarinecore.client.resource.averageColor
|
||||
import mods.octarinecore.client.resource.*
|
||||
import mods.octarinecore.common.Int3
|
||||
import mods.octarinecore.common.config.BlockMatcher
|
||||
import mods.octarinecore.common.config.ModelTextureList
|
||||
import mods.octarinecore.findFirst
|
||||
import net.minecraft.block.state.IBlockState
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.client.renderer.texture.TextureMap
|
||||
import net.minecraft.util.EnumFacing
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraft.util.math.BlockPos
|
||||
import net.minecraft.world.IBlockAccess
|
||||
import net.minecraftforge.client.event.TextureStitchEvent
|
||||
import net.minecraftforge.common.MinecraftForge
|
||||
import net.minecraftforge.fml.common.eventhandler.EventPriority
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
|
||||
import net.minecraftforge.fml.relauncher.Side
|
||||
import net.minecraftforge.fml.relauncher.SideOnly
|
||||
import org.apache.logging.log4j.Level.INFO
|
||||
import org.apache.logging.log4j.Level.WARN
|
||||
import org.apache.logging.log4j.Level
|
||||
import org.apache.logging.log4j.Logger
|
||||
|
||||
const val defaultLeafColor = 0
|
||||
|
||||
@@ -34,61 +39,94 @@ class LeafInfo(
|
||||
val averageColor: Int = roundLeafTexture.averageColor ?: defaultLeafColor
|
||||
) {
|
||||
/** [IconSet] of the textures to use for leaf particles emitted from this block. */
|
||||
val particleTextures: IconSet get() = LeafRegistry.particles[leafType]!!
|
||||
val particleTextures: IconSet? get() = LeafRegistry.particles[leafType]
|
||||
}
|
||||
|
||||
/** Collects and manages rendering-related information for leaf blocks. */
|
||||
@SideOnly(Side.CLIENT)
|
||||
object LeafRegistry : BlockTextureInspector<TextureAtlasSprite>() {
|
||||
interface ILeafRegistry {
|
||||
operator fun get(state: IBlockState): LeafInfo?
|
||||
operator fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos, face: EnumFacing): LeafInfo?
|
||||
}
|
||||
|
||||
val leaves: MutableMap<TextureAtlasSprite, LeafInfo> = hashMapOf()
|
||||
val particles: MutableMap<String, IconSet> = hashMapOf()
|
||||
/** Collects and manages rendering-related information for grass blocks. */
|
||||
object LeafRegistry : ILeafRegistry {
|
||||
val subRegistries: MutableList<ILeafRegistry> = mutableListOf(StandardLeafSupport)
|
||||
val typeMappings = TextureMatcher()
|
||||
val particles = hashMapOf<String, IconSet>()
|
||||
|
||||
init {
|
||||
matchClassAndModel(Config.blocks.leaves, "minecraft:block/leaves", listOf("all"))
|
||||
matchClassAndModel(Config.blocks.leaves, "minecraft:block/cube_all", listOf("all"))
|
||||
matchClassAndModel(Config.blocks.leaves, "biomesoplenty:block/leaves_overlay", listOf("under"))
|
||||
init { MinecraftForge.EVENT_BUS.register(this) }
|
||||
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
fun handlePreStitch(event: TextureStitchEvent.Pre) {
|
||||
particles.clear()
|
||||
}
|
||||
|
||||
operator fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos, face: EnumFacing): LeafInfo? {
|
||||
val baseTexture = get(state) ?: return null
|
||||
return leaves[OptifineCTM.override(baseTexture, world, pos, face)] ?: leaves[baseTexture]
|
||||
}
|
||||
override fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos, face: EnumFacing) =
|
||||
subRegistries.findFirst { it.get(state, world, pos, face) }
|
||||
|
||||
operator fun get(ctx: BlockContext, face: EnumFacing) = get(ctx.blockState(Int3.zero), ctx.world!!, ctx.pos, face)
|
||||
|
||||
override fun onAfterModelLoad() {
|
||||
super.onAfterModelLoad()
|
||||
Client.log(INFO, "Inspecting leaf textures")
|
||||
particles.clear()
|
||||
typeMappings.loadMappings(ResourceLocation("betterfoliage", "leafTextureMappings.cfg"))
|
||||
}
|
||||
override fun get(state: IBlockState) = subRegistries.findFirst { it.get(state) }
|
||||
|
||||
override fun processTextures(state: IBlockState, textures: List<TextureAtlasSprite>, atlas: TextureMap): TextureAtlasSprite {
|
||||
val texture = textures[0]
|
||||
registerLeaf(texture, atlas)
|
||||
OptifineCTM.getAllCTM(state, texture).forEach { registerLeaf(it, atlas) }
|
||||
return texture
|
||||
}
|
||||
|
||||
fun registerLeaf(texture: TextureAtlasSprite, atlas: TextureMap) {
|
||||
fun getParticleType(texture: TextureAtlasSprite, atlas: TextureMap): String {
|
||||
var leafType = typeMappings.getType(texture) ?: "default"
|
||||
val generated = atlas.registerSprite(
|
||||
Client.genLeaves.generatedResource(texture.iconName, "type" to leafType)
|
||||
)
|
||||
|
||||
if (leafType !in particles.keys) {
|
||||
val particleSet = IconSet("betterfoliage", "blocks/falling_leaf_${leafType}_%d")
|
||||
particleSet.onStitch(atlas)
|
||||
if (particleSet.num == 0) {
|
||||
Client.log(WARN, "Leaf particle textures not found for leaf type: $leafType")
|
||||
Client.log(Level.WARN, "Leaf particle textures not found for leaf type: $leafType")
|
||||
leafType == "default"
|
||||
} else {
|
||||
particles.put(leafType, particleSet)
|
||||
}
|
||||
}
|
||||
|
||||
leaves[texture] = LeafInfo(generated, leafType)
|
||||
return leafType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
object StandardLeafSupport :
|
||||
TextureListModelProcessor<TextureAtlasSprite>,
|
||||
TextureMediatedRegistry<List<String>, LeafInfo>,
|
||||
ILeafRegistry
|
||||
{
|
||||
|
||||
init { MinecraftForge.EVENT_BUS.register(this) }
|
||||
|
||||
override val logName = "StandardLeafSupport"
|
||||
override val matchClasses: BlockMatcher get() = Config.blocks.leavesClasses
|
||||
override val modelTextures: List<ModelTextureList> get() = Config.blocks.leavesModels.list
|
||||
override val logger: Logger? get() = BetterFoliageMod.logDetail
|
||||
|
||||
override var stateToKey = mutableMapOf<IBlockState, List<String>>()
|
||||
override var stateToValue = mapOf<IBlockState, TextureAtlasSprite>()
|
||||
override var textureToValue = mutableMapOf<TextureAtlasSprite, LeafInfo>()
|
||||
|
||||
override fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos, face: EnumFacing): LeafInfo? {
|
||||
val baseTexture = stateToValue[state] ?: return null
|
||||
return textureToValue[OptifineCTM.override(baseTexture, world, pos, face)] ?: textureToValue[baseTexture]
|
||||
}
|
||||
|
||||
override fun get(state: IBlockState) = stateToValue[state].let {
|
||||
if (it == null) null else textureToValue[it]
|
||||
}
|
||||
|
||||
override fun processStitch(state: IBlockState, key: List<String>, atlas: TextureMap) = atlas[key[0]]
|
||||
|
||||
override fun processTexture(states: List<IBlockState>, texture: TextureAtlasSprite, atlas: TextureMap) {
|
||||
registerLeaf(texture, atlas)
|
||||
OptifineCTM.getAllCTM(states, texture).forEach {
|
||||
registerLeaf(it, atlas)
|
||||
}
|
||||
}
|
||||
|
||||
fun registerLeaf(texture: TextureAtlasSprite, atlas: TextureMap) {
|
||||
var leafType = LeafRegistry.typeMappings.getType(texture) ?: "default"
|
||||
val generated = atlas.registerSprite(
|
||||
Client.genLeaves.generatedResource(texture.iconName, "type" to leafType)
|
||||
)
|
||||
textureToValue[texture] = LeafInfo(generated, LeafRegistry.getParticleType(texture, atlas))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import mods.octarinecore.metaprog.Transformer
|
||||
import mods.octarinecore.metaprog.allAvailable
|
||||
import net.minecraftforge.fml.relauncher.FMLLaunchHandler
|
||||
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin
|
||||
import org.objectweb.asm.Opcodes
|
||||
import org.objectweb.asm.Opcodes.*
|
||||
|
||||
@IFMLLoadingPlugin.TransformerExclusions(
|
||||
|
||||
@@ -61,12 +61,15 @@ object Refs {
|
||||
val VanillaModelWrapper = ClassRef("net.minecraftforge.client.model.ModelLoader\$VanillaModelWrapper")
|
||||
val model_VMW = FieldRef(VanillaModelWrapper, "model", ModelBlock)
|
||||
val location_VMW = FieldRef(VanillaModelWrapper, "location", ModelBlock)
|
||||
val WeightedPartWrapper = ClassRef("net.minecraftforge.client.model.ModelLoader\$WeightedPartWrapper")
|
||||
val model_WPW = FieldRef(WeightedPartWrapper, "model", IModel)
|
||||
// val WeightedPartWrapper = ClassRef("net.minecraftforge.client.model.ModelLoader\$WeightedPartWrapper")
|
||||
// val model_WPW = FieldRef(WeightedPartWrapper, "model", IModel)
|
||||
val WeightedRandomModel = ClassRef("net.minecraftforge.client.model.ModelLoader\$WeightedRandomModel")
|
||||
val models_WRM = FieldRef(WeightedRandomModel, "models", List)
|
||||
val MultiModel = ClassRef("net.minecraftforge.client.model.MultiModel")
|
||||
val base_MM = FieldRef(MultiModel, "base", IModel)
|
||||
val WeightedBakedModel = ClassRef("net.minecraft.client.renderer.block.model.WeightedBakedModel")
|
||||
val models_WBM = FieldRef(WeightedBakedModel, "models", List)
|
||||
|
||||
|
||||
// Better Foliage
|
||||
val BetterFoliageHooks = ClassRef("mods.betterfoliage.client.Hooks")
|
||||
@@ -76,6 +79,7 @@ object Refs {
|
||||
val isOpaqueCubeOverride = MethodRef(BetterFoliageHooks, "isOpaqueCubeOverride", ClassRef.boolean, ClassRef.boolean, IBlockState)
|
||||
val onRandomDisplayTick = MethodRef(BetterFoliageHooks, "onRandomDisplayTick", ClassRef.void, World, IBlockState, BlockPos)
|
||||
val onAfterLoadModelDefinitions = MethodRef(BetterFoliageHooks, "onAfterLoadModelDefinitions", ClassRef.void, ModelLoader)
|
||||
val onAfterBakeModels = MethodRef(BetterFoliageHooks, "onAfterBakeModels", ClassRef.void, Map)
|
||||
val renderWorldBlock = MethodRef(BetterFoliageHooks, "renderWorldBlock", ClassRef.boolean, BlockRendererDispatcher, IBlockState, BlockPos, IBlockAccess, VertexBuffer, BlockRenderLayer)
|
||||
val canRenderBlockInLayer = MethodRef(BetterFoliageHooks, "canRenderBlockInLayer", ClassRef.boolean, Block, IBlockState, BlockRenderLayer)
|
||||
|
||||
|
||||
@@ -33,6 +33,14 @@ inline fun <T1, T2> forEachNested(list1: Iterable<T1>, list2: Iterable<T2>, func
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <K, V> Map<K, V?>.filterValuesNotNull() = filterValues { it != null } as Map<K, V>
|
||||
|
||||
inline fun <reified T, R> Iterable<T>.findFirst(func: (T)->R?): R? {
|
||||
forEach { func(it)?.let { return it } }
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Property-level delegate backed by a [ThreadLocal].
|
||||
*
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package mods.octarinecore.client.resource
|
||||
|
||||
import mods.betterfoliage.client.config.BlockMatcher
|
||||
import mods.betterfoliage.loader.Refs
|
||||
import mods.octarinecore.stripStart
|
||||
import mods.octarinecore.common.config.BlockMatcher
|
||||
import net.minecraft.block.Block
|
||||
import net.minecraft.block.state.IBlockState
|
||||
import net.minecraft.client.renderer.block.model.ModelBlock
|
||||
import net.minecraft.client.renderer.block.model.ModelResourceLocation
|
||||
import net.minecraft.client.renderer.block.statemap.DefaultStateMapper
|
||||
import net.minecraft.client.renderer.block.statemap.IStateMapper
|
||||
@@ -34,8 +32,8 @@ abstract class ModelDataInspector {
|
||||
@SubscribeEvent
|
||||
fun handleLoadModelData(event: LoadModelDataEvent) {
|
||||
val stateMappings = Block.REGISTRY.flatMap { block ->
|
||||
((event.loader.blockModelShapes.blockStateMapper.blockStateMap[block] as? IStateMapper ?: DefaultStateMapper())
|
||||
.putStateModelLocations(block as Block) as Map<IBlockState, ModelResourceLocation>).entries
|
||||
val mapper = event.loader.blockModelShapes.blockStateMapper.blockStateMap[block] as? IStateMapper ?: DefaultStateMapper()
|
||||
(mapper.putStateModelLocations(block as Block) as Map<IBlockState, ModelResourceLocation>).entries
|
||||
}
|
||||
val stateModels = Refs.stateModels.get(event.loader) as Map<ModelResourceLocation, IModel>
|
||||
|
||||
@@ -96,31 +94,4 @@ abstract class BlockTextureInspector<T> : ModelDataInspector() {
|
||||
}
|
||||
|
||||
abstract fun processTextures(state: IBlockState, textures: List<TextureAtlasSprite>, atlas: TextureMap): T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val IModel.modelBlockAndLoc: Pair<ModelBlock, ResourceLocation>? get() {
|
||||
if (Refs.VanillaModelWrapper.isInstance(this))
|
||||
return Pair(Refs.model_VMW.get(this) as ModelBlock, Refs.location_VMW.get(this) as ResourceLocation)
|
||||
else if (Refs.WeightedPartWrapper.isInstance(this)) Refs.model_WPW.get(this)?.let {
|
||||
return (it as IModel).modelBlockAndLoc
|
||||
}
|
||||
else if (Refs.WeightedRandomModel.isInstance(this)) Refs.models_WRM.get(this)?.let {
|
||||
(it as List<IModel>).forEach {
|
||||
it.modelBlockAndLoc.let { if (it != null) return it }
|
||||
}
|
||||
}
|
||||
else if (Refs.MultiModel.isInstance(this)) Refs.base_MM.get(this)?.let {
|
||||
return (it as IModel).modelBlockAndLoc
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun Pair<ModelBlock, ResourceLocation>.derivesFrom(targetLocation: String): Boolean {
|
||||
if (second.stripStart("models/") == ResourceLocation(targetLocation)) return true
|
||||
if (first.parent != null && first.parentLocation != null)
|
||||
return Pair(first.parent, first.parentLocation!!).derivesFrom(targetLocation)
|
||||
return false
|
||||
}
|
||||
|
||||
fun IModel.derivesFromModel(modelLocation: String) = modelBlockAndLoc?.derivesFrom(modelLocation) ?: false
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package mods.octarinecore.client.resource
|
||||
|
||||
import com.google.common.base.Joiner
|
||||
import mods.betterfoliage.loader.Refs
|
||||
import mods.octarinecore.common.config.BlockMatcher
|
||||
import mods.octarinecore.common.config.ModelTextureList
|
||||
import mods.octarinecore.filterValuesNotNull
|
||||
import net.minecraft.block.Block
|
||||
import net.minecraft.block.state.IBlockState
|
||||
import net.minecraft.client.renderer.block.model.ModelResourceLocation
|
||||
import net.minecraft.client.renderer.block.statemap.DefaultStateMapper
|
||||
import net.minecraft.client.renderer.block.statemap.IStateMapper
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.client.renderer.texture.TextureMap
|
||||
import net.minecraftforge.client.event.TextureStitchEvent
|
||||
import net.minecraftforge.client.model.IModel
|
||||
import net.minecraftforge.fml.common.eventhandler.EventPriority
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
|
||||
import org.apache.logging.log4j.Level
|
||||
import org.apache.logging.log4j.Logger
|
||||
|
||||
interface ModelProcessor<T1, T2> {
|
||||
val logger: Logger?
|
||||
var stateToKey: MutableMap<IBlockState, T1>
|
||||
var stateToValue: Map<IBlockState, T2>
|
||||
|
||||
fun onPostLoad() { }
|
||||
fun onPreStitch() { }
|
||||
|
||||
fun processModelLoad(state: IBlockState, modelLoc: ModelResourceLocation, model: IModel): T1?
|
||||
fun processStitch(state: IBlockState, key: T1, atlas: TextureMap): T2?
|
||||
|
||||
@SubscribeEvent
|
||||
fun handleLoadModelData(event: LoadModelDataEvent) {
|
||||
stateToKey.clear()
|
||||
onPostLoad()
|
||||
|
||||
val stateMappings = Block.REGISTRY.flatMap { block ->
|
||||
val mapper = event.loader.blockModelShapes.blockStateMapper.blockStateMap[block] as? IStateMapper ?: DefaultStateMapper()
|
||||
(mapper.putStateModelLocations(block as Block) as Map<IBlockState, ModelResourceLocation>).entries
|
||||
}
|
||||
val stateModels = Refs.stateModels.get(event.loader) as Map<ModelResourceLocation, IModel>
|
||||
|
||||
stateMappings.forEach { mapping ->
|
||||
if (mapping.key.block != null) stateModels[mapping.value]?.let { model ->
|
||||
processModelLoad(mapping.key, mapping.value, model)?.let { key -> stateToKey.put(mapping.key, key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@SubscribeEvent(priority = EventPriority.LOW)
|
||||
fun handlePreStitch(event: TextureStitchEvent.Pre) {
|
||||
onPreStitch()
|
||||
stateToValue = stateToKey.mapValues { processStitch(it.key, it.value, event.map) }.filterValuesNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
interface TextureListModelProcessor<T2> : ModelProcessor<List<String>, T2> {
|
||||
val logName: String
|
||||
val matchClasses: BlockMatcher
|
||||
val modelTextures: List<ModelTextureList>
|
||||
|
||||
override fun processModelLoad(state: IBlockState, modelLoc: ModelResourceLocation, model: IModel): List<String>? {
|
||||
val matchClass = matchClasses.matchingClass(state.block) ?: return null
|
||||
logger?.log(Level.DEBUG, "$logName: block state ${state.toString()}")
|
||||
logger?.log(Level.DEBUG, "$logName: class ${state.block.javaClass.name} matches ${matchClass.name}")
|
||||
|
||||
val blockLoc = model.modelBlockAndLoc
|
||||
if (blockLoc == null) {
|
||||
logger?.log(Level.DEBUG, "$logName: no models found")
|
||||
return null
|
||||
}
|
||||
val modelMatch = modelTextures.firstOrNull { blockLoc.derivesFrom(it.modelLocation) }
|
||||
if (modelMatch == null) {
|
||||
logger?.log(Level.DEBUG, "$logName: no matching models found")
|
||||
return null
|
||||
}
|
||||
logger?.log(Level.DEBUG, "$logName: model ${blockLoc.second} matches ${modelMatch.modelLocation.toString()}")
|
||||
|
||||
val textures = modelMatch.textureNames.map { it to blockLoc.first.resolveTextureName(it) }
|
||||
val texMapString = Joiner.on(", ").join(textures.map { "${it.first}=${it.second}" })
|
||||
logger?.log(Level.DEBUG, "$logName: textures [$texMapString]")
|
||||
|
||||
return if (textures.all { it.second != "missingno" }) textures.map { it.second } else null
|
||||
}
|
||||
}
|
||||
|
||||
interface TextureMediatedRegistry<T1, T3> : ModelProcessor<T1, TextureAtlasSprite> {
|
||||
|
||||
var textureToValue: MutableMap<TextureAtlasSprite, T3>
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
// @SubscribeEvent(priority = EventPriority.LOW)
|
||||
override fun handlePreStitch(event: TextureStitchEvent.Pre) {
|
||||
textureToValue.clear()
|
||||
super.handlePreStitch(event)
|
||||
|
||||
val textureToStates = stateToValue.entries.groupBy(keySelector = { it.value }, valueTransform = { it.key })
|
||||
stateToValue.values.toSet().forEach { processTexture(textureToStates[it]!!, it, event.map) }
|
||||
}
|
||||
|
||||
fun processTexture(states: List<IBlockState>, texture: TextureAtlasSprite, atlas: TextureMap)
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
@file:JvmName("Utils")
|
||||
package mods.octarinecore.client.resource
|
||||
|
||||
import mods.betterfoliage.loader.Refs
|
||||
import mods.octarinecore.PI2
|
||||
import mods.octarinecore.client.render.HSB
|
||||
import mods.octarinecore.stripStart
|
||||
import mods.octarinecore.tryDefault
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.renderer.block.model.ModelBlock
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite
|
||||
import net.minecraft.client.renderer.texture.TextureMap
|
||||
import net.minecraft.client.resources.IResource
|
||||
import net.minecraft.client.resources.IResourceManager
|
||||
import net.minecraft.client.resources.SimpleReloadableResourceManager
|
||||
import net.minecraft.util.ResourceLocation
|
||||
import net.minecraftforge.client.model.IModel
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
@@ -29,6 +34,9 @@ operator fun IResourceManager.get(domain: String, path: String): IResource? = ge
|
||||
/** Index operator to get a resource. */
|
||||
operator fun IResourceManager.get(location: ResourceLocation): IResource? = tryDefault(null) { getResource(location) }
|
||||
|
||||
/** Index operator to get a texture sprite. */
|
||||
operator fun TextureMap.get(name: String): TextureAtlasSprite? = getTextureExtry(name)
|
||||
|
||||
/** Load an image resource. */
|
||||
fun IResource.loadImage() = ImageIO.read(this.inputStream)
|
||||
|
||||
@@ -89,4 +97,29 @@ val TextureAtlasSprite.averageColor: Int? get() {
|
||||
fun textureLocation(iconName: String) = ResourceLocation(iconName).let {
|
||||
if (it.resourcePath.startsWith("mcpatcher")) it
|
||||
else ResourceLocation(it.resourceDomain, "textures/${it.resourcePath}")
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val IModel.modelBlockAndLoc: Pair<ModelBlock, ResourceLocation>? get() {
|
||||
if (Refs.VanillaModelWrapper.isInstance(this))
|
||||
return Pair(Refs.model_VMW.get(this) as ModelBlock, Refs.location_VMW.get(this) as ResourceLocation)
|
||||
else if (Refs.WeightedRandomModel.isInstance(this)) Refs.models_WRM.get(this)?.let {
|
||||
(it as List<IModel>).forEach {
|
||||
it.modelBlockAndLoc.let { if (it != null) return it }
|
||||
}
|
||||
}
|
||||
else if (Refs.MultiModel.isInstance(this)) Refs.base_MM.get(this)?.let {
|
||||
return (it as IModel).modelBlockAndLoc
|
||||
}
|
||||
// TODO support net.minecraftforge.client.model.ModelLoader.MultipartModel
|
||||
return null
|
||||
}
|
||||
|
||||
fun Pair<ModelBlock, ResourceLocation>.derivesFrom(targetLocation: ResourceLocation): Boolean {
|
||||
if (second.stripStart("models/") == targetLocation) return true
|
||||
if (first.parent != null && first.parentLocation != null)
|
||||
return Pair(first.parent, first.parentLocation!!).derivesFrom(targetLocation)
|
||||
return false
|
||||
}
|
||||
|
||||
fun IModel.derivesFromModel(modelLoc: String) = modelBlockAndLoc?.derivesFrom(ResourceLocation(modelLoc)) ?: false
|
||||
|
||||
@@ -1,41 +1,23 @@
|
||||
package mods.betterfoliage.client.config
|
||||
package mods.octarinecore.common.config
|
||||
|
||||
import mods.octarinecore.client.gui.NonVerboseArrayEntry
|
||||
import mods.octarinecore.client.resource.get
|
||||
import mods.octarinecore.client.resource.getLines
|
||||
import mods.octarinecore.client.resource.resourceManager
|
||||
import mods.octarinecore.common.config.ConfigPropertyBase
|
||||
import mods.octarinecore.metaprog.getJavaClass
|
||||
import net.minecraft.block.Block
|
||||
import net.minecraft.client.multiplayer.WorldClient
|
||||
import net.minecraftforge.common.MinecraftForge
|
||||
import net.minecraftforge.common.config.Configuration
|
||||
import net.minecraftforge.common.config.Property
|
||||
import net.minecraftforge.event.world.WorldEvent
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
|
||||
|
||||
/**
|
||||
* Match blocks based on their class names. Caches block IDs for faster lookup.
|
||||
*
|
||||
* @param[domain] resource domain for defaults file
|
||||
* @param[path] resource path for defaults file
|
||||
*/
|
||||
class BlockMatcher(val domain: String, val path: String) : ConfigPropertyBase() {
|
||||
abstract class BlackWhiteListConfigOption<VALUE>(val domain: String, val path: String) : ConfigPropertyBase() {
|
||||
|
||||
val blackList = mutableListOf<Class<*>>()
|
||||
val whiteList = mutableListOf<Class<*>>()
|
||||
val blockIDs = hashSetOf<Int>()
|
||||
val blackList = mutableListOf<VALUE>()
|
||||
val whiteList = mutableListOf<VALUE>()
|
||||
var blacklistProperty: Property? = null
|
||||
var whitelistProperty: Property? = null
|
||||
|
||||
fun matchesClass(block: Block): Boolean {
|
||||
val blockClass = block.javaClass
|
||||
blackList.forEach { if (it.isAssignableFrom(blockClass)) return false }
|
||||
whiteList.forEach { if (it.isAssignableFrom(blockClass)) return true }
|
||||
return false
|
||||
}
|
||||
fun matchesID(block: Block) = blockIDs.contains(Block.REGISTRY.getIDForObject(block))
|
||||
fun matchesID(blockId: Int) = blockIDs.contains(blockId)
|
||||
override val hasChanged: Boolean
|
||||
get() = blacklistProperty?.hasChanged() ?: false || whitelistProperty?.hasChanged() ?: false
|
||||
|
||||
override val guiProperties: List<Property> get() = listOf(whitelistProperty!!, blacklistProperty!!)
|
||||
|
||||
override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) {
|
||||
lang = null
|
||||
@@ -43,32 +25,24 @@ class BlockMatcher(val domain: String, val path: String) : ConfigPropertyBase()
|
||||
blacklistProperty = target.get(categoryName, "${propertyName}Blacklist", defaults.first)
|
||||
whitelistProperty = target.get(categoryName, "${propertyName}Whitelist", defaults.second)
|
||||
listOf(blacklistProperty!!, whitelistProperty!!).forEach {
|
||||
it.setConfigEntryClass(NonVerboseArrayEntry::class.java)
|
||||
it.setLanguageKey("$langPrefix.$categoryName.${it.name}")
|
||||
it.configEntryClass = NonVerboseArrayEntry::class.java
|
||||
it.languageKey = "$langPrefix.$categoryName.${it.name}"
|
||||
}
|
||||
read()
|
||||
}
|
||||
|
||||
abstract fun convertValue(line: String): VALUE?
|
||||
|
||||
override fun read() {
|
||||
listOf(Pair(blackList, blacklistProperty!!), Pair(whiteList, whitelistProperty!!)).forEach {
|
||||
it.first.clear()
|
||||
it.first.addAll(it.second.stringList.map { getJavaClass(it) }.filterNotNull())
|
||||
}
|
||||
updateIDs()
|
||||
}
|
||||
|
||||
fun updateIDs() {
|
||||
blockIDs.clear()
|
||||
Block.REGISTRY.forEach {
|
||||
if (matchesClass(it as Block)) blockIDs.add(Block.REGISTRY.getIDForObject(it))
|
||||
it.second.stringList.forEach { line ->
|
||||
val value = convertValue(line)
|
||||
if (value != null) it.first.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val hasChanged: Boolean
|
||||
get() = blacklistProperty?.hasChanged() ?: false || whitelistProperty?.hasChanged() ?: false
|
||||
|
||||
override val guiProperties: List<Property> get() = listOf(whitelistProperty!!, blacklistProperty!!)
|
||||
|
||||
fun readDefaults(domain: String, path: String): Pair<Array<String>, Array<String>> {
|
||||
val blackList = arrayListOf<String>()
|
||||
val whiteList = arrayListOf<String>()
|
||||
@@ -79,10 +53,4 @@ class BlockMatcher(val domain: String, val path: String) : ConfigPropertyBase()
|
||||
}
|
||||
return (blackList.toTypedArray() to whiteList.toTypedArray())
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
fun onWorldLoad(event: WorldEvent.Load) { if (event.world is WorldClient) updateIDs() }
|
||||
|
||||
init { MinecraftForge.EVENT_BUS.register(this) }
|
||||
|
||||
}
|
||||
33
src/main/kotlin/mods/octarinecore/common/config/Matchers.kt
Normal file
33
src/main/kotlin/mods/octarinecore/common/config/Matchers.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
package mods.octarinecore.common.config
|
||||
|
||||
import mods.octarinecore.metaprog.getJavaClass
|
||||
import net.minecraft.block.Block
|
||||
import net.minecraft.util.ResourceLocation
|
||||
|
||||
class BlockMatcher(domain: String, path: String) : BlackWhiteListConfigOption<Class<*>>(domain, path) {
|
||||
override fun convertValue(line: String) = getJavaClass(line)
|
||||
|
||||
fun matchesClass(block: Block): Boolean {
|
||||
val blockClass = block.javaClass
|
||||
blackList.forEach { if (it.isAssignableFrom(blockClass)) return false }
|
||||
whiteList.forEach { if (it.isAssignableFrom(blockClass)) return true }
|
||||
return false
|
||||
}
|
||||
|
||||
fun matchingClass(block: Block): Class<*>? {
|
||||
val blockClass = block.javaClass
|
||||
blackList.forEach { if (it.isAssignableFrom(blockClass)) return null }
|
||||
whiteList.forEach { if (it.isAssignableFrom(blockClass)) return it }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
data class ModelTextureList(val modelLocation: ResourceLocation, val textureNames: List<String>)
|
||||
|
||||
class ModelTextureListConfigOption(domain: String, path: String, val minTextures: Int) : StringListConfigOption<ModelTextureList>(domain, path) {
|
||||
override fun convertValue(line: String): ModelTextureList? {
|
||||
val elements = line.split(",")
|
||||
if (elements.size < minTextures + 1) return null
|
||||
return ModelTextureList(ResourceLocation(elements.first()), elements.drop(1))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package mods.octarinecore.common.config
|
||||
|
||||
import mods.octarinecore.client.gui.NonVerboseArrayEntry
|
||||
import mods.octarinecore.client.resource.get
|
||||
import mods.octarinecore.client.resource.getLines
|
||||
import mods.octarinecore.client.resource.resourceManager
|
||||
import net.minecraftforge.common.config.Configuration
|
||||
import net.minecraftforge.common.config.Property
|
||||
|
||||
abstract class StringListConfigOption<VALUE>(val domain: String, val path: String) : ConfigPropertyBase() {
|
||||
|
||||
val list = mutableListOf<VALUE>()
|
||||
lateinit var listProperty: Property
|
||||
|
||||
override val hasChanged: Boolean get() = listProperty.hasChanged() ?: false
|
||||
override val guiProperties: List<Property> get() = listOf(listProperty)
|
||||
|
||||
override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) {
|
||||
lang = null
|
||||
val defaults = readDefaults(domain, path)
|
||||
listProperty = target.get(categoryName, "${propertyName}", defaults)
|
||||
listProperty.configEntryClass = NonVerboseArrayEntry::class.java
|
||||
listProperty.languageKey = "$langPrefix.$categoryName.${listProperty.name}"
|
||||
read()
|
||||
}
|
||||
|
||||
abstract fun convertValue(line: String): VALUE?
|
||||
|
||||
override fun read() {
|
||||
list.clear()
|
||||
listProperty.stringList.forEach { line ->
|
||||
val value = convertValue(line)
|
||||
if (value != null) list.add(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun readDefaults(domain: String, path: String): Array<String> {
|
||||
val list = arrayListOf<String>()
|
||||
val defaults = resourceManager[domain, path]?.getLines()
|
||||
defaults?.map { it.trim() }?.filter { !it.startsWith("//") && it.isNotEmpty() }?.forEach { list.add(it) }
|
||||
return list.toTypedArray()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.BlockGrass
|
||||
|
||||
// Biomes O'Plenty
|
||||
biomesoplenty.common.blocks.BlockOriginGrass
|
||||
biomesoplenty.common.blocks.BlockLongGrass
|
||||
biomesoplenty.common.blocks.BlockNewGrass
|
||||
|
||||
// Tinker's Construct
|
||||
tconstruct.blocks.slime.SlimeGrass
|
||||
|
||||
// Enhanced Biomes
|
||||
enhancedbiomes.blocks.BlockGrassEB
|
||||
|
||||
// TerraFirmaCraft
|
||||
com.bioxx.tfc.Blocks.Terrain.BlockGrass
|
||||
|
||||
// Aether
|
||||
net.aetherteam.aether.blocks.natural.BlockAetherGrass
|
||||
@@ -1,14 +0,0 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.BlockGrass
|
||||
|
||||
// Biomes O'Plenty
|
||||
biomesoplenty.common.block.BlockBOPGrass
|
||||
|
||||
// Enhanced Biomes
|
||||
enhancedbiomes.blocks.BlockGrassEB
|
||||
|
||||
// TerraFirmaCraft
|
||||
com.bioxx.tfc.Blocks.Terrain.BlockGrass
|
||||
|
||||
// Random Things
|
||||
lumien.randomthings.block.BlockColoredGrass
|
||||
@@ -0,0 +1,2 @@
|
||||
block/grass,top
|
||||
block/cube_bottom_top,top
|
||||
@@ -0,0 +1,8 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.BlockLeaves
|
||||
|
||||
// Forestry
|
||||
forestry.arboriculture.gadgets.BlockLeaves
|
||||
|
||||
// Thaumcraft
|
||||
thaumcraft.common.blocks.BlockMagicalLeaves
|
||||
@@ -1,20 +0,0 @@
|
||||
// Vanilla
|
||||
net.minecraft.block.BlockLeaves
|
||||
|
||||
// Biomes O'Plenty
|
||||
biomesoplenty.common.block.BlockBOPLeaves
|
||||
|
||||
// Forestry
|
||||
forestry.arboriculture.blocks.BlockForestryLeaves
|
||||
|
||||
// Tinker's Construct
|
||||
slimeknights.tconstruct.world.block.BlockSlimeLeaves
|
||||
|
||||
// Thaumcraft
|
||||
thaumcraft.common.blocks.world.plants.BlockLeavesTC
|
||||
|
||||
// TechReborn
|
||||
//techreborn.blocks.BlockRubberLeaves
|
||||
|
||||
// Random Things
|
||||
lumien.randomthings.block.spectretree.BlockSpectreLeaf
|
||||
@@ -0,0 +1,2 @@
|
||||
minecraft:block/leaves,all
|
||||
minecraft:block/cube_all,all
|
||||
@@ -30,15 +30,19 @@ betterfoliage.blocks.dirtBlacklist=Dirt Blacklist
|
||||
betterfoliage.blocks.dirtWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.dirtBlacklist.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.grassWhitelist=Grass Whitelist
|
||||
betterfoliage.blocks.grassBlacklist=Grass Blacklist
|
||||
betterfoliage.blocks.grassWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.grassBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.grassClassesWhitelist=Grass Whitelist
|
||||
betterfoliage.blocks.grassClassesBlacklist=Grass Blacklist
|
||||
betterfoliage.blocks.grassClassesWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.grassClassesBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.grassModels=Grass Models
|
||||
betterfoliage.blocks.grassModels.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.leavesWhitelist=Leaves Whitelist
|
||||
betterfoliage.blocks.leavesBlacklist=Leaves Blacklist
|
||||
betterfoliage.blocks.leavesWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.leavesBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.leavesClassesWhitelist=Leaves Whitelist
|
||||
betterfoliage.blocks.leavesClassesBlacklist=Leaves Blacklist
|
||||
betterfoliage.blocks.leavesClassesWhitelist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.leavesClassesBlacklist.arrayEntry=%d entries
|
||||
betterfoliage.blocks.leavesModels=Leaves Models
|
||||
betterfoliage.blocks.leavesModels.arrayEntry=%d entries
|
||||
|
||||
betterfoliage.blocks.cropsWhitelist=Crop Whitelist
|
||||
betterfoliage.blocks.cropsBlacklist=Crop Blacklist
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
"version": "$version",
|
||||
"mcversion": "$mcversion",
|
||||
"description": "Leafier leaves and grassier grass",
|
||||
"authorList" : ["octarine-noise (code)", "Meringue (textures)"]
|
||||
"authorList" : ["octarine-noise (code)", "Meringue (textures)"],
|
||||
"modLanguageAdapter": ""
|
||||
}]
|
||||
Reference in New Issue
Block a user