first Kotlin version

This commit is contained in:
octarine-noise
2015-12-28 11:49:46 +01:00
parent 554e06176b
commit f44043bb0b
143 changed files with 5469 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.idea/
*.iml
*.ipr
*.iws
run/
.gradle/
build/

68
build.gradle Normal file
View File

@@ -0,0 +1,68 @@
apply plugin: "forge"
apply plugin: "kotlin"
group = 'com.github.octarine-noise'
version = "2.0"
archivesBaseName = rootProject.name + '-MC1.7.10'
buildscript {
ext.kotlin_version = '1.0.0-beta-4583'
repositories {
mavenCentral()
maven {
name = "forge"
url = "http://files.minecraftforge.net/maven"
}
maven {
name = "sonatype"
url = "https://oss.sonatype.org/content/repositories/snapshots/"
}
}
dependencies {
classpath "net.minecraftforge.gradle:ForgeGradle:1.2-SNAPSHOT"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
configurations {
shade
compile.extendsFrom shade
}
dependencies {
shade "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
minecraft {
version = '1.7.10-10.13.4.1448-1.7.10'
srgExtra "PK: kotlin mods/betterfoliage/kotlin"
}
processResources {
inputs.property "version", project.version
inputs.property "mcversion", project.minecraft.version
from(sourceSets.main.resources.srcDirs) {
include 'mcmod.info'
expand 'version':project.version, 'mcversion':project.minecraft.version
}
from(sourceSets.main.resources.srcDirs) {
exclude 'mcmod.info'
}
into "${buildDir}/classes/main"
}
jar {
manifest {
attributes "FMLCorePlugin": "mods.betterfoliage.loader.BetterFoliageLoader"
attributes "FMLCorePluginContainsFMLMod": "mods.betterfoliage.BetterFoliageMod"
}
configurations.shade.each { dep ->
from(project.zipTree(dep)){
exclude 'META-INF', 'META-INF/**'
}
}
}
repositories {
mavenCentral()
}

2
settings.gradle Normal file
View File

@@ -0,0 +1,2 @@
rootProject.name = 'BetterFoliage'

View File

@@ -0,0 +1,60 @@
package mods.betterfoliage
import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.common.Mod
import cpw.mods.fml.common.event.FMLPostInitializationEvent
import cpw.mods.fml.common.event.FMLPreInitializationEvent
import cpw.mods.fml.common.network.NetworkCheckHandler
import cpw.mods.fml.relauncher.Side
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.integration.ShadersModIntegration
import mods.betterfoliage.client.integration.TFCIntegration
import mods.betterfoliage.loader.Refs
import mods.octarinecore.metaprog.ClassRef
import net.minecraftforge.common.config.Configuration
import org.apache.logging.log4j.Level.*
import org.apache.logging.log4j.Logger
@Mod(
modid = BetterFoliageMod.MOD_ID,
name = BetterFoliageMod.MOD_NAME,
acceptedMinecraftVersions = BetterFoliageMod.MC_VERSIONS,
guiFactory = BetterFoliageMod.GUI_FACTORY
)
object BetterFoliageMod {
const val MOD_ID = "BetterFoliage"
const val MOD_NAME = "Better Foliage"
const val DOMAIN = "betterfoliage"
const val LEGACY_DOMAIN = "bettergrassandleaves"
const val MC_VERSIONS = "[1.7.10]"
const val GUI_FACTORY = "mods.betterfoliage.client.gui.ConfigGuiFactory"
var log: Logger? = null
var config: Configuration? = null
@JvmStatic
@Mod.InstanceFactory
// the fun never stops with the fun factory! :)
fun factory() = this
@Mod.EventHandler
fun preInit(event: FMLPreInitializationEvent) {
log = event.modLog
config = Configuration(event.suggestedConfigurationFile, null, true)
}
@Mod.EventHandler
fun postInit(event: FMLPostInitializationEvent) {
if (FMLCommonHandler.instance().effectiveSide == Side.CLIENT) {
Config.attach(config!!)
Client.log(INFO, "BetterFoliage initialized")
}
}
/** Mod is cosmetic only, always allow connection. */
@NetworkCheckHandler
fun checkVersion(mods: Map<String, String>, side: Side) = true
}

View File

@@ -0,0 +1,75 @@
package mods.betterfoliage.client
import cpw.mods.fml.client.FMLClientHandler
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.gui.ConfigGuiFactory
import mods.betterfoliage.client.integration.CLCIntegration
import mods.betterfoliage.client.integration.ShadersModIntegration
import mods.betterfoliage.client.integration.TFCIntegration
import mods.betterfoliage.client.render.*
import mods.betterfoliage.client.texture.*
import mods.octarinecore.client.KeyHandler
import mods.octarinecore.client.resource.CenteringTextureGenerator
import mods.octarinecore.client.resource.GeneratorPack
import net.minecraft.client.Minecraft
import org.apache.logging.log4j.Level
/**
* Object responsible for initializing (and holding a reference to) all the infrastructure of the mod
* except for the call hooks.
*
* This and all other singletons are annotated [SideOnly] to avoid someone accidentally partially
* initializing the mod on a server environment.
*/
@SideOnly(Side.CLIENT)
object Client {
val configKey = KeyHandler(BetterFoliageMod.MOD_NAME, 66, "key.betterfoliage.gui") {
FMLClientHandler.instance().showGuiScreen(
ConfigGuiFactory.ConfigGuiBetterFoliage(Minecraft.getMinecraft().currentScreen)
)
}
val genGrass = GrassGenerator("bf_gen_grass")
val genLeaves = LeafGenerator("bf_gen_leaves")
val genReeds = CenteringTextureGenerator("bf_gen_reeds", 1, 2)
val generatorPack = GeneratorPack(
"Better Foliage generated",
genGrass,
genLeaves,
genReeds
)
val logRenderer = RenderLog()
val renderers = listOf(
RenderGrass(),
RenderMycelium(),
RenderLeaves(),
RenderCactus(),
RenderLilypad(),
RenderReeds(),
RenderAlgae(),
RenderCoral(),
logRenderer,
RenderNetherrack(),
RenderConnectedGrass(),
RenderConnectedGrassLog()
)
val singletons = listOf(
LeafRegistry,
GrassRegistry,
LeafWindTracker,
RisingSoulTextures,
TFCIntegration,
ShadersModIntegration,
CLCIntegration
)
fun log(level: Level, msg: String) = BetterFoliageMod.log!!.log(level, msg)
}

View File

@@ -0,0 +1,58 @@
@file:JvmName("Hooks")
@file:SideOnly(Side.CLIENT)
package mods.betterfoliage.client
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.render.EntityFallingLeavesFX
import mods.betterfoliage.client.render.EntityRisingSoulFX
import mods.octarinecore.client.render.blockContext
import net.minecraft.block.Block
import net.minecraft.client.Minecraft
import net.minecraft.init.Blocks
import net.minecraft.world.IBlockAccess
import net.minecraft.world.World
fun getRenderTypeOverride(blockAccess: IBlockAccess, x: Int, y: Int, z: Int, block: Block, original: Int): Int {
if (!Config.enabled) return original;
// universal sign for DON'T RENDER ME!
if (original == -1) return original;
return blockContext.let { ctx ->
ctx.set(blockAccess, x, y, z)
Client.renderers.find { it.isEligible(ctx) }?.renderId ?: original
}
}
fun shouldRenderBlockSideOverride(original: Boolean, blockAccess: IBlockAccess, x: Int, y: Int, z: Int, side: Int): Boolean {
return original || (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(blockAccess.getBlock(x, y, z)));
}
fun getAmbientOcclusionLightValueOverride(original: Float, block: Block): Float {
if (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(block)) return Config.roundLogs.dimming;
return original;
}
fun getUseNeighborBrightnessOverride(original: Boolean, block: Block): Boolean {
return original || (Config.enabled && Config.roundLogs.enabled && Config.blocks.logs.matchesID(block));
}
fun onRandomDisplayTick(block: Block, world: World, x: Int, y: Int, z: Int) {
if (Config.enabled &&
Config.risingSoul.enabled &&
block == Blocks.soul_sand &&
world.isAirBlock(x, y + 1, z) &&
Math.random() < Config.risingSoul.chance) {
EntityRisingSoulFX(world, x, y, z).addIfValid()
}
if (Config.enabled &&
Config.fallingLeaves.enabled &&
Config.blocks.leaves.matchesID(block) &&
world.isAirBlock(x, y - 1, z) &&
Math.random() < Config.fallingLeaves.chance) {
EntityFallingLeavesFX(world, x, y, z).addIfValid()
}
}

View File

@@ -0,0 +1,88 @@
package mods.betterfoliage.client.config
import cpw.mods.fml.common.eventhandler.SubscribeEvent
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.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
/**
* 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() {
val blackList = linkedListOf<Class<*>>()
val whiteList = linkedListOf<Class<*>>()
val blockIDs = hashSetOf<Int>()
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.blockRegistry.getIDForObject(block))
fun matchesID(blockId: Int) = blockIDs.contains(blockId)
override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) {
lang = null
val defaults = readDefaults(domain, path)
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}")
}
read()
}
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.blockRegistry.forEach {
if (matchesClass(it as Block)) blockIDs.add(Block.blockRegistry.getIDForObject(it))
}
}
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>()
val defaults = resourceManager[domain, path]?.getLines()
defaults?.map{ it.trim() }?.filter { !it.startsWith("//") && it.isNotEmpty() }?.forEach {
if (it.startsWith("-")) { blackList.add(it.substring(1)) }
else { whiteList.add(it) }
}
return (blackList.toTypedArray() to whiteList.toTypedArray())
}
@SubscribeEvent
fun onWorldLoad(event: WorldEvent.Load) { if (event.world is WorldClient) updateIDs() }
init { MinecraftForge.EVENT_BUS.register(this) }
}

View File

@@ -0,0 +1,183 @@
package mods.betterfoliage.client.config
import cpw.mods.fml.client.event.ConfigChangedEvent
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.gui.BiomeListConfigEntry
import mods.octarinecore.config.*
import mods.octarinecore.metaprog.reflectField
import net.minecraft.client.Minecraft
import net.minecraft.world.biome.BiomeGenBase
// BetterFoliage-specific property delegates
private fun featureEnable() = boolean(true).lang("enabled")
private fun distanceLimit() = int(min=1, max=1000, default=1000).lang("distance")
fun biomeList(defaults: (BiomeGenBase) -> Boolean) = intList {
BiomeGenBase.getBiomeGenArray().filter { it != null && defaults(it) }.map { it.biomeID }.toTypedArray()
}.apply { guiClass = BiomeListConfigEntry::class.java }
// Biome filter methods
private fun BiomeGenBase.filterTemp(min: Float?, max: Float?) = (min == null || min <= temperature) && (max == null || max >= temperature)
private fun BiomeGenBase.filterRain(min: Float?, max: Float?) = (min == null || min <= rainfall) && (max == null || max >= rainfall)
private fun BiomeGenBase.filterClass(vararg name: String) = name.any { it in this.javaClass.name.toLowerCase() }
// Config singleton
@SideOnly(Side.CLIENT)
object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.DOMAIN) {
var enabled by boolean(true)
object blocks {
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")
val lilypad = BlockMatcher(BetterFoliageMod.DOMAIN, "LilypadDefault.cfg")
val cactus = BlockMatcher(BetterFoliageMod.DOMAIN, "CactusDefault.cfg")
}
object leaves {
val enabled by featureEnable()
val distance by distanceLimit()
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
val vOffset by double(max=0.4, default=0.1).lang("vOffset")
val size by double(min=0.75, max=2.5, default=1.4).lang("size")
val dense by boolean(false)
}
object shortGrass {
val grassEnabled by boolean(true)
val myceliumEnabled by boolean(true)
val snowEnabled by boolean(true)
val distance by distanceLimit()
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
val heightMin by double(min=0.1, max=2.5, default=0.6).lang("heightMin")
val heightMax by double(min=0.1, max=2.5, default=0.8).lang("heightMax")
val size by double(min=0.5, max=1.5, default=1.0).lang("size")
val useGenerated by boolean(false)
val shaderWind by boolean(true).lang("shaderWind")
val saturationThreshold by double(default=0.1)
}
// object hangingGrass {
// var enabled by featureEnable()
// var distance by distanceLimit()
// var size by double(min=0.25, max=1.5, default=0.75).lang("size")
// var separation by double(max=0.5, default=0.25)
// }
object connectedGrass {
val enabled by boolean(true)
val snowEnabled by boolean(false)
}
object roundLogs {
val enabled by featureEnable()
val distance by distanceLimit()
val radiusSmall by double(max=0.5, default=0.25)
val radiusLarge by double(max=0.5, default=0.44)
val dimming by float(default = 0.7)
val connectSolids by boolean(false)
val lenientConnect by boolean(true)
val connectPerpendicular by boolean(true)
val connectGrass by boolean(true)
val zProtection by double(min = 0.9, default = 0.99)
}
object cactus {
val enabled by featureEnable()
val distance by distanceLimit()
val size by double(min=0.5, max=1.5, default=0.8).lang("size")
val sizeVariation by double(max=0.5, default=0.1)
val hOffset by double(max=0.5, default=0.1).lang("hOffset")
}
object lilypad {
val enabled by featureEnable()
val distance by distanceLimit()
val hOffset by double(max=0.25, default=0.1).lang("hOffset")
val flowerChance by int(max=64, default=16, min=0)
}
object reed {
val enabled by featureEnable()
val distance by distanceLimit()
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
val heightMin by double(min=1.5, max=3.5, default=1.7).lang("heightMin")
val heightMax by double(min=1.5, max=3.5, default=2.2).lang("heightMax")
val population by int(max=64, default=32).lang("population")
val biomes by biomeList { it.filterTemp(0.4f, null) && it.filterRain(0.4f, null) }
val shaderWind by boolean(true).lang("shaderWind")
}
object algae {
val enabled by featureEnable()
val distance by distanceLimit()
val hOffset by double(max=0.25, default=0.1).lang("hOffset")
val size by double(min=0.5, max=1.5, default=1.0).lang("size")
val heightMin by double(min=0.1, max=1.5, default=0.5).lang("heightMin")
val heightMax by double(min=0.1, max=1.5, default=1.0).lang("heightMax")
val population by int(max=64, default=48).lang("population")
val biomes by biomeList { it.filterClass("river", "ocean") }
val shaderWind by boolean(true).lang("shaderWind")
}
object coral {
val enabled by featureEnable()
val distance by distanceLimit()
val shallowWater by boolean(false)
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
val vOffset by double(max=0.4, default=0.1).lang("vOffset")
val size by double(min=0.5, max=1.5, default=0.7).lang("size")
val crustSize by double(min=0.5, max=1.5, default=1.4)
val chance by int(max=64, default=32)
val population by int(max=64, default=48).lang("population")
val biomes by biomeList { it.filterClass("river", "ocean", "beach") }
}
object netherrack {
val enabled by featureEnable()
val distance by distanceLimit()
val hOffset by double(max=0.4, default=0.2).lang("hOffset")
val heightMin by double(min=0.1, max=1.5, default=0.6).lang("heightMin")
val heightMax by double(min=0.1, max=1.5, default=0.8).lang("heightMax")
val size by double(min=0.5, max=1.5, default=1.0).lang("size")
}
object fallingLeaves {
val enabled by featureEnable()
val speed by double(min=0.01, max=0.15, default=0.05)
val windStrength by double(min=0.1, max=2.0, default=0.5)
val stormStrength by double(min=0.1, max=2.0, default=0.8)
val size by double(min=0.25, max=1.5, default=0.75).lang("size")
val chance by double(min=0.001, max=1.0, default=0.05)
val perturb by double(min=0.01, max=1.0, default=0.25)
val lifetime by double(min=1.0, max=15.0, default=5.0)
val opacityHack by boolean(true)
}
object risingSoul {
val enabled by featureEnable()
val chance by double(min=0.001, max=1.0, default=0.02)
val perturb by double(min=0.01, max=0.25, default=0.05)
val headSize by double(min=0.25, max=1.5, default=1.0)
val trailSize by double(min=0.25, max=1.5, default=0.75)
val opacity by float(min=0.05, max=1.0, default=0.5)
val sizeDecay by double(min=0.5, max=1.0, default=0.97)
val opacityDecay by float(min=0.5, max=1.0, default=0.97)
val lifetime by double(min=1.0, max=15.0, default=4.0)
val trailLength by int(min=2, max=128, default=48)
val trailDensity by int(min=1, max=16, default=3)
}
override fun onChange(event: ConfigChangedEvent.OnConfigChangedEvent) {
super.onChange(event)
if (hasChanged(blocks, shortGrass["saturationThreshold"]))
Minecraft.getMinecraft().refreshResources()
else
Minecraft.getMinecraft().renderGlobal.loadRenderers()
}
}

View File

@@ -0,0 +1,19 @@
package mods.betterfoliage.client.gui
import cpw.mods.fml.client.config.GuiConfig
import cpw.mods.fml.client.config.GuiConfigEntries
import cpw.mods.fml.client.config.IConfigElement
import mods.octarinecore.client.gui.IdListConfigEntry
import net.minecraft.world.biome.BiomeGenBase
/** Toggleable list of all defined biomes. */
class BiomeListConfigEntry(
owningScreen: GuiConfig,
owningEntryList: GuiConfigEntries,
configElement: IConfigElement<*>)
: IdListConfigEntry<BiomeGenBase>(owningScreen, owningEntryList, configElement) {
override val baseSet: List<BiomeGenBase> get() = BiomeGenBase.getBiomeGenArray().filterNotNull()
override val BiomeGenBase.itemId: Int get() = this.biomeID
override val BiomeGenBase.itemName: String get() = this.biomeName
}

View File

@@ -0,0 +1,27 @@
package mods.betterfoliage.client.gui
import cpw.mods.fml.client.IModGuiFactory
import cpw.mods.fml.client.IModGuiFactory.RuntimeOptionCategoryElement
import cpw.mods.fml.client.config.GuiConfig
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.Config
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiScreen
class ConfigGuiFactory : IModGuiFactory {
override fun mainConfigGuiClass() = ConfigGuiBetterFoliage::class.java
override fun runtimeGuiCategories() = hashSetOf<RuntimeOptionCategoryElement>()
override fun getHandlerFor(element: RuntimeOptionCategoryElement?) = null
override fun initialize(minecraftInstance: Minecraft?) { }
class ConfigGuiBetterFoliage(parentScreen: GuiScreen?) : GuiConfig(
parentScreen,
Config.rootGuiElements,
BetterFoliageMod.MOD_ID,
null,
false,
false,
BetterFoliageMod.MOD_NAME
)
}

View File

@@ -0,0 +1,19 @@
package mods.betterfoliage.client.integration
import mods.betterfoliage.client.Client
import mods.betterfoliage.loader.Refs
import mods.octarinecore.client.render.brightnessComponents
import org.apache.logging.log4j.Level.*
/**
* Integration for Colored Lights Core.
*/
object CLCIntegration {
init {
if (Refs.CLCLoadingPlugin.element != null) {
Client.log(INFO, "Colored Lights Core integration enabled")
brightnessComponents = listOf(4, 8, 12, 16, 20)
}
}
}

View File

@@ -0,0 +1,53 @@
package mods.betterfoliage.client.integration
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.betterfoliage.loader.Refs
import mods.octarinecore.metaprog.allAvailable
import net.minecraft.block.Block
import net.minecraft.init.Blocks
import org.apache.logging.log4j.Level.*
/**
* Integration for ShadersMod.
*/
@SideOnly(Side.CLIENT)
object ShadersModIntegration {
@JvmStatic var isPresent = false
@JvmStatic val tallGrassEntityData = Block.blockRegistry.getIDForObject(Blocks.tallgrass) and 65535 or (Blocks.tallgrass.renderType shl 16)
@JvmStatic val leavesEntityData = Block.blockRegistry.getIDForObject(Blocks.leaves) and 65535 or (Blocks.leaves.renderType shl 16)
/**
* Called from transformed ShadersMod code.
* @see mods.betterfoliage.loader.BetterFoliageTransformer
*/
@JvmStatic fun getBlockIdOverride(original: Int, block: Block): Int {
if (Config.blocks.leaves.matchesID(original and 65535)) return leavesEntityData
if (Config.blocks.crops.matchesID(original and 65535)) return tallGrassEntityData
return original
}
init {
if (allAvailable(Refs.pushEntity_I, Refs.popEntity)) {
Client.log(INFO, "ShadersMod integration enabled")
isPresent = true
}
}
/** Quads rendered inside this block will behave as tallgrass blocks in shader programs. */
inline fun grass(enabled: Boolean = true, func: ()->Unit) {
if (isPresent && enabled) Refs.pushEntity_I.invokeStatic(tallGrassEntityData)
func()
if (isPresent && enabled) Refs.popEntity.invokeStatic()
}
/** Quads rendered inside this block will behave as leaf blocks in shader programs. */
inline fun leaves(enabled: Boolean = true, func: ()->Unit) {
if (isPresent && enabled) Refs.pushEntity_I.invokeStatic(leavesEntityData)
func()
if (isPresent && enabled) Refs.popEntity.invokeStatic()
}
}

View File

@@ -0,0 +1,37 @@
package mods.betterfoliage.client.integration
import cpw.mods.fml.common.Loader
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import mods.betterfoliage.client.Client
import mods.octarinecore.client.render.Axis
import net.minecraft.block.Block
import org.apache.logging.log4j.Level
/**
* Integration for TerraFirmaCraft
*/
@SideOnly(Side.CLIENT)
object TFCIntegration {
@JvmStatic val vanillaLogAxis = Client.logRenderer.axisFunc
init {
if (Loader.isModLoaded("terrafirmacraft")) {
Client.log(Level.INFO, "TerraFirmaCraft found - setting up compatibility")
// patch axis detection for log blocks to support TFC logs
Client.logRenderer.axisFunc = { block: Block, meta: Int ->
block.javaClass.name.let {
if (it.startsWith("com.bioxx.tfc")) {
if (it.contains("Horiz"))
if (meta shr 3 == 0) Axis.Z else Axis.X
else
Axis.Y
} else {
vanillaLogAxis(block, meta)
}
}
}
}
}
}

View File

@@ -0,0 +1,301 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.client.render.AbstractRenderColumn.BlockType.*
import mods.betterfoliage.client.render.AbstractRenderColumn.QuadrantType.*
import mods.octarinecore.client.render.*
import net.minecraft.block.Block
import net.minecraft.client.renderer.RenderBlocks
import net.minecraftforge.common.util.ForgeDirection.*
/** Index of SOUTH-EAST quadrant. */
const val SE = 0
/** Index of NORTH-EAST quadrant. */
const val NE = 1
/** Index of NORTH-WEST quadrant. */
const val NW = 2
/** Index of SOUTH-WEST quadrant. */
const val SW = 3
@Suppress("NOTHING_TO_INLINE")
abstract class AbstractRenderColumn(modId: String) : AbstractBlockRenderingHandler(modId) {
enum class BlockType { SOLID, NONSOLID, PARALLEL, PERPENDICULAR }
enum class QuadrantType { SMALL_RADIUS, LARGE_RADIUS, SQUARE, INVISIBLE }
/** The rotations necessary to bring the models in position for the 4 quadrants */
val quadrantRotations = Array(4) { Rotation.rot90[UP.ordinal] * it }
// ============================
// Configuration
// ============================
abstract val radiusSmall: Double
abstract val radiusLarge: Double
abstract val surroundPredicate: (Block) -> Boolean
abstract val connectPerpendicular: Boolean
abstract val connectSolids: Boolean
abstract val lenientConnect: Boolean
// ============================
// Models
// ============================
val sideSquare = model { columnSideSquare(-0.5, 0.5) }
val sideRoundSmall = model { columnSide(radiusSmall, -0.5, 0.5) }
val sideRoundLarge = model { columnSide(radiusLarge, -0.5, 0.5) }
val extendTopSquare = model { columnSideSquare(0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) }
val extendTopRoundSmall = model { columnSide(radiusSmall, 0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) }
val extendTopRoundLarge = model { columnSide(radiusLarge, 0.5, 0.5 + radiusLarge, topExtension(radiusLarge)) }
inline fun extendTop(type: QuadrantType) = when(type) {
SMALL_RADIUS -> extendTopRoundSmall.model
LARGE_RADIUS -> extendTopRoundLarge.model
SQUARE -> extendTopSquare.model
INVISIBLE -> extendTopSquare.model
else -> null
}
val extendBottomSquare = model { columnSideSquare(-0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) }
val extendBottomRoundSmall = model { columnSide(radiusSmall, -0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) }
val extendBottomRoundLarge = model { columnSide(radiusLarge, -0.5 - radiusLarge, -0.5, bottomExtension(radiusLarge)) }
inline fun extendBottom(type: QuadrantType) = when (type) {
SMALL_RADIUS -> extendBottomRoundSmall.model
LARGE_RADIUS -> extendBottomRoundLarge.model
SQUARE -> extendBottomSquare.model
INVISIBLE -> extendBottomSquare.model
else -> null
}
val topSquare = model { columnLidSquare() }
val topRoundSmall = model { columnLid(radiusSmall) }
val topRoundLarge = model { columnLid(radiusLarge) }
inline fun flatTop(type: QuadrantType) = when(type) {
SMALL_RADIUS -> topRoundSmall.model
LARGE_RADIUS -> topRoundLarge.model
SQUARE -> topSquare.model
INVISIBLE -> topSquare.model
else -> null
}
val bottomSquare = model { columnLidSquare() { it.rotate(rot(EAST) * 2 + rot(UP)) } }
val bottomRoundSmall = model { columnLid(radiusSmall) { it.rotate(rot(EAST) * 2 + rot(UP)) } }
val bottomRoundLarge = model { columnLid(radiusLarge) { it.rotate(rot(EAST) * 2 + rot(UP)) } }
inline fun flatBottom(type: QuadrantType) = when(type) {
SMALL_RADIUS -> bottomRoundSmall.model
LARGE_RADIUS -> bottomRoundLarge.model
SQUARE -> bottomSquare.model
INVISIBLE -> bottomSquare.model
else -> null
}
val transitionTop = model { mix(sideRoundLarge.model, sideRoundSmall.model) { it > 1 } }
val transitionBottom = model { mix(sideRoundSmall.model, sideRoundLarge.model) { it > 1 } }
val sideTexture = { ctx: ShadingContext, qi: Int, q: Quad -> if ((qi and 1) == 0) ctx.icon(SOUTH) else ctx.icon(EAST) }
val upTexture = { ctx: ShadingContext, qi: Int, q: Quad -> ctx.icon(UP) }
val downTexture = { ctx: ShadingContext, qi: Int, q: Quad -> ctx.icon(DOWN) }
inline fun continous(q1: QuadrantType, q2: QuadrantType) =
q1 == q2 || ((q1 == SQUARE || q1 == INVISIBLE) && (q2 == SQUARE || q2 == INVISIBLE))
abstract val axisFunc: (Block, Int)->Axis
abstract val blockPredicate: (Block, Int)->Boolean
@Suppress("NON_EXHAUSTIVE_WHEN")
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
if (ctx.isSurroundedBy(surroundPredicate) ) return false
// get AO data
if (renderWorldBlockBase(parent, face = neverRender)) return true
// check log neighborhood
val logAxis = ctx.blockAxis
val baseRotation = rotationFromUp[(logAxis to Dir.P).face.ordinal]
val upType = ctx.blockType(baseRotation, logAxis, Int3(0, 1, 0))
val downType = ctx.blockType(baseRotation, logAxis, Int3(0, -1, 0))
val quadrants = Array(4) { SMALL_RADIUS }.checkNeighbors(ctx, baseRotation, logAxis, 0)
val quadrantsTop = Array(4) { SMALL_RADIUS }
if (upType == PARALLEL) quadrantsTop.checkNeighbors(ctx, baseRotation, logAxis, 1)
val quadrantsBottom = Array(4) { SMALL_RADIUS }
if (downType == PARALLEL) quadrantsBottom.checkNeighbors(ctx, baseRotation, logAxis, -1)
quadrantRotations.forEachIndexed { idx, quadrantRotation ->
// set rotation for the current quadrant
val rotation = baseRotation + quadrantRotation
// disallow sharp discontinuities in the chamfer radius
if (quadrants[idx] == LARGE_RADIUS &&
upType == PARALLEL && quadrantsTop[idx] == SMALL_RADIUS &&
downType == PARALLEL && quadrantsBottom[idx] == SMALL_RADIUS) {
quadrants[idx] = SMALL_RADIUS
}
// render side of current quadrant
val sideModel = when (quadrants[idx]) {
SMALL_RADIUS -> sideRoundSmall.model
LARGE_RADIUS -> if (upType == PARALLEL && quadrantsTop[idx] == SMALL_RADIUS) transitionTop.model
else if (downType == PARALLEL && quadrantsBottom[idx] == SMALL_RADIUS) transitionBottom.model
else sideRoundLarge.model
SQUARE -> sideSquare.model
else -> null
}
if (sideModel != null) modelRenderer.render(
sideModel,
rotation,
blockContext.blockCenter,
icon = sideTexture,
rotateUV = { 0 },
postProcess = noPost
)
// render top and bottom end of current quadrant
var upModel: Model? = null
var downModel: Model? = null
var upIcon = upTexture
var downIcon = downTexture
when (upType) {
NONSOLID -> upModel = flatTop(quadrants[idx])
PERPENDICULAR -> {
if (!connectPerpendicular) {
upModel = flatTop(quadrants[idx])
} else {
upIcon = sideTexture
upModel = extendTop(quadrants[idx])
}
}
PARALLEL -> {
if (!continous(quadrants[idx], quadrantsTop[idx])) {
if (quadrants[idx] == SQUARE || quadrants[idx] == INVISIBLE) {
upModel = topSquare.model
}
}
}
}
when (downType) {
NONSOLID -> downModel = flatBottom(quadrants[idx])
PERPENDICULAR -> {
if (!connectPerpendicular) {
downModel = flatBottom(quadrants[idx])
} else {
downIcon = sideTexture
downModel = extendBottom(quadrants[idx])
}
}
PARALLEL -> {
if (!continous(quadrants[idx], quadrantsBottom[idx]) &&
(quadrants[idx] == SQUARE || quadrants[idx] == INVISIBLE)) {
downModel = bottomSquare.model
}
}
}
if (upModel != null) modelRenderer.render(
upModel,
rotation,
blockContext.blockCenter,
icon = upIcon,
rotateUV = { 0 },
postProcess = noPost
)
if (downModel != null) modelRenderer.render(
downModel,
rotation,
blockContext.blockCenter,
icon = downIcon,
rotateUV = { 0 },
postProcess = noPost
)
}
return true
}
/** Sets the type of the given quadrant only if the new value is "stronger" (larger ordinal). */
inline fun Array<QuadrantType>.upgrade(idx: Int, value: QuadrantType) {
if (this[idx].ordinal < value.ordinal) this[idx] = value
}
/** Fill the array of [QuadrantType]s based on the blocks to the sides of this one. */
fun Array<QuadrantType>.checkNeighbors(ctx: BlockContext, rotation: Rotation, logAxis: Axis, yOff: Int): Array<QuadrantType> {
val blkS = ctx.blockType(rotation, logAxis, Int3(0, yOff, 1))
val blkE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 0))
val blkN = ctx.blockType(rotation, logAxis, Int3(0, yOff, -1))
val blkW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, 0))
// a solid block on one side will make the 2 neighboring quadrants SQUARE
// if there are solid blocks to both sides of a quadrant, it is INVISIBLE
if (connectSolids) {
if (blkS == SOLID) {
upgrade(SW, SQUARE); upgrade(SE, SQUARE)
}
if (blkE == SOLID) {
upgrade(SE, SQUARE); upgrade(NE, SQUARE)
}
if (blkN == SOLID) {
upgrade(NE, SQUARE); upgrade(NW, SQUARE)
}
if (blkW == SOLID) {
upgrade(NW, SQUARE); upgrade(SW, SQUARE)
}
if (blkS == SOLID && blkE == SOLID) upgrade(SE, INVISIBLE)
if (blkN == SOLID && blkE == SOLID) upgrade(NE, INVISIBLE)
if (blkN == SOLID && blkW == SOLID) upgrade(NW, INVISIBLE)
if (blkS == SOLID && blkW == SOLID) upgrade(SW, INVISIBLE)
}
val blkSE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 1))
val blkNE = ctx.blockType(rotation, logAxis, Int3(1, yOff, -1))
val blkNW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, -1))
val blkSW = ctx.blockType(rotation, logAxis, Int3(-1, yOff, 1))
if (lenientConnect) {
// if the block forms the tip of an L-shape, connect to its neighbor with SQUARE quadrants
if (blkE == PARALLEL && (blkSE == PARALLEL || blkNE == PARALLEL)) {
upgrade(SE, SQUARE); upgrade(NE, SQUARE)
}
if (blkN == PARALLEL && (blkNE == PARALLEL || blkNW == PARALLEL)) {
upgrade(NE, SQUARE); upgrade(NW, SQUARE)
}
if (blkW == PARALLEL && (blkNW == PARALLEL || blkSW == PARALLEL)) {
upgrade(NW, SQUARE); upgrade(SW, SQUARE)
}
if (blkS == PARALLEL && (blkSE == PARALLEL || blkSW == PARALLEL)) {
upgrade(SW, SQUARE); upgrade(SE, SQUARE)
}
}
// if the block forms the middle of an L-shape, or is part of a 2x2 configuration,
// connect to its neighbors with SQUARE quadrants, INVISIBLE on the inner corner, and LARGE_RADIUS on the outer corner
if (blkN == PARALLEL && blkW == PARALLEL && (lenientConnect || blkNW == PARALLEL)) {
upgrade(SE, LARGE_RADIUS); upgrade(NE, SQUARE); upgrade(SW, SQUARE); upgrade(NW, INVISIBLE)
}
if (blkS == PARALLEL && blkW == PARALLEL && (lenientConnect || blkSW == PARALLEL)) {
upgrade(NE, LARGE_RADIUS); upgrade(SE, SQUARE); upgrade(NW, SQUARE); upgrade(SW, INVISIBLE)
}
if (blkS == PARALLEL && blkE == PARALLEL && (lenientConnect || blkSE == PARALLEL)) {
upgrade(NW, LARGE_RADIUS); upgrade(NE, SQUARE); upgrade(SW, SQUARE); upgrade(SE, INVISIBLE)
}
if (blkN == PARALLEL && blkE == PARALLEL && (lenientConnect || blkNE == PARALLEL)) {
upgrade(SW, LARGE_RADIUS); upgrade(SE, SQUARE); upgrade(NW, SQUARE); upgrade(NE, INVISIBLE)
}
return this
}
/** Get the axis of the block */
val BlockContext.blockAxis: Axis get() = axisFunc(block(Int3.zero), meta(Int3.zero))
/**
* Get the type of the block at the given offset in a rotated reference frame.
*/
fun BlockContext.blockType(rotation: Rotation, axis: Axis, offset: Int3): BlockType {
val offsetRot = offset.rotate(rotation)
val logBlock = block(offsetRot)
val logMeta = meta(offsetRot)
return if (!blockPredicate(logBlock, logMeta)) {
if (logBlock.isOpaqueCube) SOLID else NONSOLID
} else {
if (axisFunc(logBlock, logMeta) == axis) PARALLEL else PERPENDICULAR
}
}
}

View File

@@ -0,0 +1,131 @@
package mods.betterfoliage.client.render
import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import cpw.mods.fml.common.gameevent.TickEvent
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.texture.LeafRegistry
import mods.octarinecore.PI2
import mods.octarinecore.client.render.AbstractEntityFX
import mods.octarinecore.client.render.Double3
import mods.octarinecore.client.render.HSB
import mods.octarinecore.minmax
import mods.octarinecore.random
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.Tessellator
import net.minecraft.util.MathHelper
import net.minecraft.world.World
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.common.util.ForgeDirection.DOWN
import net.minecraftforge.event.world.WorldEvent
import org.lwjgl.opengl.GL11
import java.lang.Math.*
import java.util.*
class EntityFallingLeavesFX(world: World, x: Int, y: Int, z: Int) :
AbstractEntityFX(world, x.toDouble() + 0.5, y.toDouble(), z.toDouble() + 0.5) {
companion object {
@JvmStatic val biomeBrightnessMultiplier = 0.5f
}
var particleRot = rand.nextInt(64)
var rotPositive = true
val isMirrored = (rand.nextInt() and 1) == 1
var wasOnGround = false
init {
particleMaxAge = MathHelper.floor_double(random(0.6, 1.0) * Config.fallingLeaves.lifetime * 20.0)
motionY = -Config.fallingLeaves.speed
particleScale = Config.fallingLeaves.size.toFloat() * 0.1f
val block = world.getBlock(x, y, z)
LeafRegistry.leaves[block.getIcon(world, x, y, z, DOWN.ordinal)]?.let {
particleIcon = it.particleTextures[rand.nextInt(1024)]
calculateParticleColor(it.averageColor, block.colorMultiplier(world, x, y, z))
}
}
override val isValid: Boolean get() = (particleIcon != null)
override fun update() {
if (rand.nextFloat() > 0.95f) rotPositive = !rotPositive
if (particleAge > particleMaxAge - 20) particleAlpha = 0.05f * (particleMaxAge - particleAge)
if (onGround || wasOnGround) {
velocity.setTo(0.0, 0.0, 0.0)
if (!wasOnGround) {
particleAge = Math.max(particleAge, particleMaxAge - 20)
wasOnGround = true
}
} else {
velocity.setTo(cos[particleRot], 0.0, sin[particleRot]).mul(Config.fallingLeaves.perturb)
.add(LeafWindTracker.current).add(0.0, -1.0, 0.0).mul(Config.fallingLeaves.speed)
particleRot = (particleRot + (if (rotPositive) 1 else -1)) and 63
}
}
override fun render(tessellator: Tessellator, partialTickTime: Float) {
if (Config.fallingLeaves.opacityHack) GL11.glDepthMask(true)
renderParticleQuad(tessellator, partialTickTime, rotation = particleRot, isMirrored = isMirrored)
}
fun calculateParticleColor(textureAvgColor: Int, blockColor: Int) {
val texture = HSB.fromColor(textureAvgColor)
val block = HSB.fromColor(blockColor)
val weightTex = texture.saturation / (texture.saturation + block.saturation)
val weightBlock = 1.0f - weightTex
// avoid circular average for hue for performance reasons
// one of the color components should dominate anyway
val particle = HSB(
weightTex * texture.hue + weightBlock * block.hue,
weightTex * texture.saturation + weightBlock * block.saturation,
weightTex * texture.brightness + weightBlock * block.brightness * biomeBrightnessMultiplier
)
setColor(particle.asColor)
}
}
@SideOnly(Side.CLIENT)
object LeafWindTracker {
var random = Random()
val target = Double3.zero
val current = Double3.zero
var nextChange: Long = 0
init {
MinecraftForge.EVENT_BUS.register(this)
FMLCommonHandler.instance().bus().register(this)
}
fun changeWind(world: World) {
nextChange = world.worldInfo.worldTime + 120 + random.nextInt(80)
val direction = PI2 * random.nextDouble()
val speed = abs(random.nextGaussian()) * Config.fallingLeaves.windStrength +
(if (!world.isRaining) 0.0 else abs(random.nextGaussian()) * Config.fallingLeaves.stormStrength)
target.setTo(cos(direction) * speed, 0.0, sin(direction) * speed)
}
@SubscribeEvent
fun handleWorldTick(event: TickEvent.ClientTickEvent) {
if (event.phase == TickEvent.Phase.START) Minecraft.getMinecraft().theWorld?.let { world ->
// change target wind speed
if (world.worldInfo.worldTime >= nextChange) changeWind(world)
// change current wind speed
val changeRate = if (world.isRaining) 0.015 else 0.005
current.add(
(target.x - current.x).minmax(-changeRate, changeRate),
0.0,
(target.z - current.z).minmax(-changeRate, changeRate)
)
}
}
@SubscribeEvent
fun handleWorldLoad(event: WorldEvent.Load) { if (event.world.isRemote) changeWind(event.world) }
}

View File

@@ -0,0 +1,75 @@
package mods.betterfoliage.client.render
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.AbstractEntityFX
import mods.octarinecore.client.render.Double3
import mods.octarinecore.client.resource.ResourceHandler
import mods.octarinecore.forEachPairIndexed
import net.minecraft.client.renderer.Tessellator
import net.minecraft.util.MathHelper
import net.minecraft.world.World
import org.apache.logging.log4j.Level.*
import java.util.*
class EntityRisingSoulFX(world: World, x: Int, y: Int, z: Int) :
AbstractEntityFX(world, x.toDouble() + 0.5, y.toDouble() + 1.0, z.toDouble() + 0.5) {
val particleTrail: Deque<Double3> = linkedListOf()
val initialPhase = rand.nextInt(64)
init {
motionY = 0.1
particleGravity = 0.0f
particleIcon = RisingSoulTextures.headIcons[rand.nextInt(256)]
particleMaxAge = MathHelper.floor_double((0.6 + 0.4 * rand.nextDouble()) * Config.risingSoul.lifetime * 20.0)
}
override val isValid: Boolean get() = true
override fun update() {
val phase = (initialPhase + particleAge) % 64
velocity.setTo(cos[phase] * Config.risingSoul.perturb, 0.1, sin[phase] * Config.risingSoul.perturb)
particleTrail.addFirst(currentPos.copy())
while (particleTrail.size > Config.risingSoul.trailLength) particleTrail.removeLast()
if (!Config.enabled) setDead()
}
override fun render(tessellator: Tessellator, partialTickTime: Float) {
var alpha = Config.risingSoul.opacity
if (particleAge > particleMaxAge - 40) alpha *= (particleMaxAge - particleAge) / 40.0f
renderParticleQuad(tessellator, partialTickTime,
size = Config.risingSoul.headSize * 0.25,
alpha = alpha
)
var scale = Config.risingSoul.trailSize * 0.25
particleTrail.forEachPairIndexed { idx, current, previous ->
scale *= Config.risingSoul.sizeDecay
alpha *= Config.risingSoul.opacityDecay
if (idx % Config.risingSoul.trailDensity == 0) renderParticleQuad(tessellator, partialTickTime,
currentPos = current,
prevPos = previous,
size = scale,
alpha = alpha,
icon = RisingSoulTextures.trackIcon.icon!!
)
}
}
}
@SideOnly(Side.CLIENT)
object RisingSoulTextures : ResourceHandler(BetterFoliageMod.MOD_ID) {
val headIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "rising_soul_%d")
val trackIcon = iconStatic(BetterFoliageMod.LEGACY_DOMAIN, "soul_track")
override fun afterStitch() {
Client.log(INFO, "Registered ${headIcons.num} soul particle textures")
}
}

View File

@@ -0,0 +1,140 @@
@file:JvmName("ModelColumn")
package mods.betterfoliage.client.render
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.*
import mods.octarinecore.exchange
import net.minecraftforge.common.util.ForgeDirection.*
/** Weight of the same-side AO values on the outer edges of the 45deg chamfered column faces. */
const val chamferAffinity = 0.9f
/** Amount to shrink column extension bits to stop Z-fighting. */
val zProtectionScale: Double3 get() = Double3(Config.roundLogs.zProtection, 1.0, Config.roundLogs.zProtection)
fun Model.columnSide(radius: Double, yBottom: Double, yTop: Double, transform: (Quad) -> Quad = { it }) {
val halfRadius = radius * 0.5
listOf(
verticalRectangle(x1 = 0.0, z1 = 0.5, x2 = 0.5 - radius, z2 = 0.5, yBottom = yBottom, yTop = yTop)
.clampUV(minU = 0.0, maxU = 0.5 - radius)
.setAoShader(faceOrientedInterpolate(overrideFace = SOUTH)),
verticalRectangle(x1 = 0.5 - radius, z1 = 0.5, x2 = 0.5 - halfRadius, z2 = 0.5 - halfRadius, yBottom = yBottom, yTop = yTop)
.clampUV(minU = 0.5 - radius)
.setAoShader(
faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming))
)
.setAoShader(
faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming)),
predicate = { v, vi -> vi == 1 || vi == 2}
)
).forEach { transform(it.setFlatShader(FaceFlat(SOUTH))).add() }
listOf(
verticalRectangle(x1 = 0.5 - halfRadius, z1 = 0.5 - halfRadius, x2 = 0.5, z2 = 0.5 - radius, yBottom = yBottom, yTop = yTop)
.clampUV(maxU = radius - 0.5)
.setAoShader(
faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming)))
.setAoShader(
faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming)),
predicate = { v, vi -> vi == 0 || vi == 3}
),
verticalRectangle(x1 = 0.5, z1 = 0.5 - radius, x2 = 0.5, z2 = 0.0, yBottom = yBottom, yTop = yTop)
.clampUV(minU = radius - 0.5, maxU = 0.0)
.setAoShader(faceOrientedInterpolate(overrideFace = EAST))
).forEach { transform(it.setFlatShader(FaceFlat(EAST))).add() }
quads.exchange(1, 2)
}
/**
* Create a model of the side of a square column quadrant.
*
* @param[transform] transformation to apply to the model
*/
fun Model.columnSideSquare(yBottom: Double, yTop: Double, transform: (Quad) -> Quad = { it }) {
listOf(
verticalRectangle(x1 = 0.0, z1 = 0.5, x2 = 0.5, z2 = 0.5, yBottom = yBottom, yTop = yTop)
.clampUV(minU = 0.0)
.setAoShader(faceOrientedInterpolate(overrideFace = SOUTH))
.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y)), predicate = { v, vi -> vi == 1 || vi == 2}),
verticalRectangle(x1 = 0.5, z1 = 0.5, x2 = 0.5, z2 = 0.0, yBottom = yBottom, yTop = yTop)
.clampUV(maxU = 0.0)
.setAoShader(faceOrientedInterpolate(overrideFace = EAST))
.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y)), predicate = { v, vi -> vi == 1 || vi == 2})
).forEach {
transform(it.setFlatShader(faceOrientedAuto(corner = cornerFlat))).add()
}
}
/**
* Create a model of the top lid of a chamfered column quadrant.
*
* @param[radius] the chamfer radius
* @param[transform] transformation to apply to the model
*/
fun Model.columnLid(radius: Double, transform: (Quad)->Quad = { it }) {
val v1 = Vertex(Double3(0.0, 0.5, 0.0), UV(0.0, 0.0))
val v2 = Vertex(Double3(0.0, 0.5, 0.5), UV(0.0, 0.5))
val v3 = Vertex(Double3(0.5 - radius, 0.5, 0.5), UV(0.5 - radius, 0.5))
val v4 = Vertex(Double3(0.5 - radius * 0.5, 0.5, 0.5 - radius * 0.5), UV(0.5, 0.5))
val v5 = Vertex(Double3(0.5, 0.5, 0.5 - radius), UV(0.5, 0.5 - radius))
val v6 = Vertex(Double3(0.5, 0.5, 0.0), UV(0.5, 0.0))
val q1 = Quad(v1, v2, v3, v4).setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y)))
.transformVI { vertex, idx -> vertex.copy(aoShader = when(idx) {
0 -> FaceCenter(UP)
1 -> EdgeInterpolateFallback(UP, SOUTH, 0.0)
else -> vertex.aoShader
})}
val q2 = Quad(v1, v4, v5, v6).setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y)))
.transformVI { vertex, idx -> vertex.copy(aoShader = when(idx) {
0 -> FaceCenter(UP)
3 -> EdgeInterpolateFallback(UP, EAST, 0.0)
else -> vertex.aoShader
})}
listOf(q1, q2).forEach { transform(it.setFlatShader(FaceFlat(UP))).add() }
}
/**
* Create a model of the top lid of a square column quadrant.
*
* @param[transform] transformation to apply to the model
*/
fun Model.columnLidSquare(transform: (Quad)-> Quad = { it }) {
transform(
horizontalRectangle(x1 = 0.0, x2 = 0.5, z1 = 0.0, z2 = 0.5, y = 0.5)
.transformVI { vertex, idx -> vertex.copy(uv = UV(vertex.xyz.x, vertex.xyz.z), aoShader = when(idx) {
0 -> FaceCenter(UP)
1 -> EdgeInterpolateFallback(UP, SOUTH, 0.0)
2 -> CornerSingleFallback(UP, SOUTH, EAST, UP)
else -> EdgeInterpolateFallback(UP, EAST, 0.0)
}) }
.setFlatShader(FaceFlat(UP))
).add()
}
/**
* Transform a chamfered side quadrant model of a column that extends from the top of the block.
* (clamp UV coordinates, apply some scaling to avoid Z-fighting).
*
* @param[size] amount that the model extends from the top
*/
fun topExtension(size: Double) = { q: Quad ->
q.clampUV(minV = 0.5 - size).transformVI { vertex, idx ->
if (idx < 2) vertex else vertex.copy(xyz = vertex.xyz * zProtectionScale)
}
}
/**
* Transform a chamfered side quadrant model of a column that extends from the bottom of the block.
* (clamp UV coordinates, apply some scaling to avoid Z-fighting).
*
* @param[size] amount that the model extends from the bottom
*/
fun bottomExtension(size: Double) = { q: Quad ->
q.clampUV(maxV = -0.5 + size).transformVI { vertex, idx ->
if (idx > 1) vertex else vertex.copy(xyz = vertex.xyz * zProtectionScale)
}
}

View File

@@ -0,0 +1,49 @@
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.ShadersModIntegration
import mods.octarinecore.client.render.*
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.RenderBlocks
import net.minecraft.init.Blocks
import org.apache.logging.log4j.Level.INFO
class RenderAlgae : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val noise = simplexNoise()
val algaeIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_algae_%d")
val algaeModels = modelSet(64, RenderGrass.grassTopQuads)
override fun afterStitch() {
Client.log(INFO, "Registered ${algaeIcons.num} algae textures")
}
override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.algae.enabled &&
ctx.cameraDistance < Config.algae.distance &&
ctx.block(up2).material == Material.water &&
ctx.block(up1).material == Material.water &&
Config.blocks.dirt.matchesID(ctx.block) &&
ctx.biomeId in Config.algae.biomes &&
noise[ctx.x, ctx.z] < Config.algae.population
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
if (renderWorldBlockBase(parent, face = alwaysRender)) return true
val rand = ctx.semiRandomArray(3)
ShadersModIntegration.grass(Config.algae.shaderWind) {
modelRenderer.render(
algaeModels[rand[2]],
Rotation.identity,
icon = { ctx, qi, q -> algaeIcons[rand[qi and 1]]!! },
rotateUV = { 0 },
postProcess = noPost
)
}
return true
}
}

View File

@@ -0,0 +1,83 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.*
import net.minecraft.client.renderer.RenderBlocks
import net.minecraftforge.common.util.ForgeDirection.*
import org.apache.logging.log4j.Level
class RenderCactus : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val cactusStemRadius = 0.4375
val cactusArmRotation = listOf(NORTH, SOUTH, EAST, WEST).map { Rotation.rot90[it.ordinal] }
val iconCross = iconStatic(BetterFoliageMod.LEGACY_DOMAIN, "better_cactus")
val iconArm = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_cactus_arm_%d")
val modelStem = model {
horizontalRectangle(x1 = -cactusStemRadius, x2 = cactusStemRadius, z1 = -cactusStemRadius, z2 = cactusStemRadius, y = 0.5)
.scaleUV(cactusStemRadius * 2.0)
.let { listOf(it.flipped.move(1.0 to DOWN), it) }
.forEach { it.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y), edge = null)).add() }
verticalRectangle(x1 = -0.5, z1 = cactusStemRadius, x2 = 0.5, z2 = cactusStemRadius, yBottom = -0.5, yTop = 0.5)
.setAoShader(faceOrientedAuto(corner = cornerAo(Axis.Y), edge = null))
.toCross(UP).addAll()
}
val modelCross = modelSet(64) { modelIdx ->
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41)
.setAoShader(edgeOrientedAuto(corner = cornerAoMaxGreen))
.scale(1.4)
.transformV { v ->
val perturb = xzDisk(modelIdx) * Config.cactus.sizeVariation
Vertex(v.xyz + (if (v.uv.u < 0.0) perturb else -perturb), v.uv, v.aoShader)
}
.toCross(UP).addAll()
}
val modelArm = modelSet(64) { modelIdx ->
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0)
.scale(Config.cactus.size).move(0.5 to UP)
.setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y), edge = null))
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.cactus.hOffset) }.addAll()
}
override fun afterStitch() {
Client.log(Level.INFO, "Registered ${iconArm.num} cactus arm textures")
}
override fun isEligible(ctx: BlockContext): Boolean =
Config.enabled && Config.cactus.enabled &&
ctx.cameraDistance < Config.cactus.distance &&
Config.blocks.cactus.matchesID(ctx.block)
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
// get AO data
if (renderWorldBlockBase(parent, face = neverRender)) return true
modelRenderer.render(
modelStem.model,
Rotation.identity,
icon = { ctx, qi, q -> ctx.icon(forgeDirs[qi])},
rotateUV = { 0 },
postProcess = noPost
)
modelRenderer.render(
modelCross[ctx.random(0)],
Rotation.identity,
icon = { ctx, qi, q -> iconCross.icon!!},
rotateUV = { 0 },
postProcess = noPost
)
modelRenderer.render(
modelArm[ctx.random(1)],
cactusArmRotation[ctx.random(2) % 4],
icon = { ctx2, qi, q -> iconArm[ctx.random(3)]!!},
rotateUV = { 0 },
postProcess = noPost
)
return true
}
}

View File

@@ -0,0 +1,23 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.*
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.RenderBlocks
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.connectedGrass.snowEnabled || !ctx.block(up2).isSnow)
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
return ctx.withOffset(Int3.zero, up1) {
ctx.withOffset(up1, up2) {
renderWorldBlockBase(parent, face = alwaysRender)
}
}
}
}

View File

@@ -0,0 +1,31 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.*
import net.minecraft.client.renderer.RenderBlocks
import net.minecraftforge.common.util.ForgeDirection.*
class RenderConnectedGrassLog : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val grassCheckDirs = listOf(EAST, WEST, NORTH, SOUTH)
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))
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
val grassDir = grassCheckDirs.find {
Config.blocks.grass.matchesID(ctx.block(it.offset))
}
return if (grassDir != null) {
ctx.withOffset(Int3.zero, grassDir.offset) {
renderWorldBlockBase(parent, face = alwaysRender)
}
} else {
renderWorldBlockBase(parent, face = alwaysRender)
}
}
}

View File

@@ -0,0 +1,65 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.*
import mods.octarinecore.random
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.RenderBlocks
import net.minecraftforge.common.util.ForgeDirection.UP
import org.apache.logging.log4j.Level.INFO
class RenderCoral : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val noise = simplexNoise()
val coralIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_coral_%d")
val crustIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_crust_%d")
val coralModels = modelSet(64) { modelIdx ->
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0)
.scale(Config.coral.size).move(0.5 to UP)
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.coral.hOffset) }.addAll()
val separation = random(0.01, Config.coral.vOffset)
horizontalRectangle(x1 = -0.5, x2 = 0.5, z1 = -0.5, z2 = 0.5, y = 0.0)
.scale(Config.coral.crustSize).move(0.5 + separation to UP).add()
transformQ {
it.setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y)))
.setFlatShader(faceOrientedAuto(overrideFace = UP, corner = cornerFlat))
}
}
override fun afterStitch() {
Client.log(INFO, "Registered ${coralIcons.num} algae textures")
}
override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.coral.enabled &&
ctx.cameraDistance < Config.coral.distance &&
ctx.block(up2).material == Material.water &&
ctx.block(up1).material == Material.water &&
Config.blocks.sand.matchesID(ctx.block) &&
ctx.biomeId in Config.coral.biomes &&
noise[ctx.x, ctx.z] < Config.coral.population
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
if (renderWorldBlockBase(parent, face = alwaysRender)) return true
forgeDirs.forEachIndexed { idx, face ->
if (!ctx.block(forgeDirOffsets[idx]).isOpaqueCube && blockContext.random(idx) < Config.coral.chance) {
var variation = blockContext.random(6)
modelRenderer.render(
coralModels[variation++],
rotationFromUp[idx],
icon = { ctx, qi, q -> if (qi == 4) crustIcons[variation]!! else coralIcons[variation + (qi and 1)]!!},
rotateUV = { 0 },
postProcess = noPost
)
}
}
return true
}
}

View File

@@ -0,0 +1,106 @@
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.ShadersModIntegration
import mods.betterfoliage.client.texture.GrassRegistry
import mods.octarinecore.client.render.*
import mods.octarinecore.random
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.RenderBlocks
import net.minecraftforge.common.util.ForgeDirection.UP
import org.apache.logging.log4j.Level.INFO
class RenderGrass : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
companion object {
@JvmStatic val grassTopQuads: Model.(Int)->Unit = { modelIdx ->
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.5,
yTop = 0.5 + random(Config.shortGrass.heightMin, Config.shortGrass.heightMax)
)
.setAoShader(faceOrientedAuto(overrideFace = UP, corner = cornerAo(Axis.Y)))
.setFlatShader(faceOrientedAuto(overrideFace = UP, corner = cornerFlat))
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.shortGrass.hOffset) }.addAll()
}
}
val normalIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_grass_long_%d")
val snowedIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_grass_snowed_%d")
val normalGenIcon = iconStatic(Client.genGrass.generatedResource("minecraft:tallgrass", "snowed" to false))
val snowedGenIcon = iconStatic(Client.genGrass.generatedResource("minecraft:tallgrass", "snowed" to true))
val grassModels = modelSet(64, grassTopQuads)
override fun afterStitch() {
Client.log(INFO, "Registered ${normalIcons.num} grass textures")
Client.log(INFO, "Registered ${snowedIcons.num} snowed grass textures")
}
override fun isEligible(ctx: BlockContext) =
Config.enabled &&
ctx.cameraDistance < Config.shortGrass.distance &&
(Config.shortGrass.grassEnabled || Config.connectedGrass.enabled) &&
Config.blocks.grass.matchesID(ctx.block)
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
val isConnected = ctx.block(down1).let { Config.blocks.dirt.matchesID(it) || Config.blocks.grass.matchesID(it) }
val isSnowed = ctx.block(up1).isSnow
val connectedGrass = isConnected && Config.connectedGrass.enabled && (!isSnowed || Config.connectedGrass.snowEnabled)
val grassInfo = GrassRegistry.grass[ctx.icon(UP)]
if (grassInfo == null) {
renderWorldBlockBase(parent, face = alwaysRender)
return true
}
val cubeTexture = if (isSnowed) ctx.icon(up1, UP) else null ?: grassInfo.grassTopTexture
val blockColor = ctx.blockColor(Int3.zero)
if (connectedGrass) {
// get AO data
if (renderWorldBlockBase(parent, face = neverRender)) return true
// render full grass block
modelRenderer.render(
fullCube,
Rotation.identity,
ctx.blockCenter,
icon = { ctx, qi, q -> cubeTexture },
rotateUV = { 2 },
postProcess = { ctx, qi, q, vi, v ->
if (isSnowed) { if(!ctx.aoEnabled) setGrey(1.4f) }
else if (qi != UP.ordinal && ctx.aoEnabled) multiplyColor(blockColor)
}
)
} else {
// render normally
if (renderWorldBlockBase(parent, face = alwaysRender)) return true
}
if (!Config.shortGrass.grassEnabled) return true
if (isSnowed && !Config.shortGrass.snowEnabled) return true
if (ctx.block(up1).isOpaqueCube) return true
// render grass quads
val iconset = if (isSnowed) snowedIcons else normalIcons
val iconGen = if (isSnowed) snowedGenIcon else normalGenIcon
val rand = ctx.semiRandomArray(2)
ShadersModIntegration.grass(Config.shortGrass.shaderWind) {
modelRenderer.render(
grassModels[rand[0]],
Rotation.identity,
ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero),
icon = if (Config.shortGrass.useGenerated)
{ ctx: ShadingContext, qi: Int, q: Quad -> iconGen.icon!! }
else
{ ctx: ShadingContext, qi: Int, q: Quad -> iconset[rand[qi and 1]]!! },
rotateUV = { 0 },
postProcess = if (isSnowed) whitewash else if (grassInfo.overrideColor == null) noPost else
{ ctx, qi, q, vi, v -> multiplyColor(grassInfo.overrideColor) }
)
}
return true
}
}

View File

@@ -0,0 +1,72 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.integration.ShadersModIntegration
import mods.betterfoliage.client.texture.LeafRegistry
import mods.octarinecore.PI2
import mods.octarinecore.client.render.*
import mods.octarinecore.random
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.RenderBlocks
import net.minecraftforge.common.util.ForgeDirection.DOWN
import net.minecraftforge.common.util.ForgeDirection.UP
import java.lang.Math.cos
import java.lang.Math.sin
class RenderLeaves : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val leavesModel = model {
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41)
.setAoShader(edgeOrientedAuto(corner = cornerAoMaxGreen))
.setFlatShader(FlatOffset(Int3.zero))
.scale(Config.leaves.size)
.toCross(UP).addAll()
}
val snowedIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_leaves_snowed_%d")
val perturbs = vectorSet(64) { idx ->
val angle = PI2 * idx / 64.0
Double3(cos(angle), 0.0, sin(angle)) * Config.leaves.hOffset +
UP.vec * random(-1.0, 1.0) * Config.leaves.vOffset
}
override fun isEligible(ctx: BlockContext) =
Config.enabled &&
Config.leaves.enabled &&
ctx.cameraDistance < Config.leaves.distance &&
Config.blocks.leaves.matchesID(ctx.block)
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
val isSnowed = ctx.block(up1).material.let {
it == Material.snow || it == Material.craftedSnow
}
if (renderWorldBlockBase(parent, face = alwaysRender)) return true
val leafInfo = LeafRegistry.leaves[ctx.icon(DOWN)]
if (leafInfo != null) ShadersModIntegration.leaves {
val rand = ctx.semiRandomArray(2)
(if (Config.leaves.dense) denseLeavesRot else normalLeavesRot).forEach { rotation ->
modelRenderer.render(
leavesModel.model,
rotation,
ctx.blockCenter + perturbs[rand[0]],
icon = { ctx, qi, q -> leafInfo.roundLeafTexture },
rotateUV = { q -> rand[1] },
postProcess = noPost
)
}
if (isSnowed) modelRenderer.render(
leavesModel.model,
Rotation.identity,
ctx.blockCenter + perturbs[rand[0]],
icon = { ctx, qi, q -> snowedIcon[rand[1]]!! },
rotateUV = { 0 },
postProcess = whitewash
)
}
return true
}
}

View File

@@ -0,0 +1,64 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.*
import net.minecraft.client.renderer.RenderBlocks
import net.minecraftforge.common.util.ForgeDirection.*
import org.apache.logging.log4j.Level
class RenderLilypad : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val rootModel = model {
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -1.5, yTop = -0.5)
.setFlatShader(FlatOffsetNoColor(Int3.zero))
.toCross(UP).addAll()
}
val flowerModel = model {
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0)
.scale(0.5).move(0.5 to DOWN)
.setFlatShader(FlatOffsetNoColor(Int3.zero))
.toCross(UP).addAll()
}
val rootIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_lilypad_roots_%d")
val flowerIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_lilypad_flower_%d")
val perturbs = vectorSet(64) { modelIdx -> xzDisk(modelIdx) * Config.lilypad.hOffset }
override fun afterStitch() {
Client.log(Level.INFO, "Registered ${rootIcon.num} lilypad root textures")
Client.log(Level.INFO, "Registered ${flowerIcon.num} lilypad flower textures")
}
override fun isEligible(ctx: BlockContext): Boolean =
Config.enabled && Config.lilypad.enabled &&
ctx.cameraDistance < Config.lilypad.distance &&
Config.blocks.lilypad.matchesID(ctx.block)
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
if (renderWorldBlockBase(parent, face = alwaysRender)) return true
val rand = ctx.semiRandomArray(5)
modelRenderer.render(
rootModel.model,
Rotation.identity,
ctx.blockCenter.add(perturbs[rand[2]]),
forceFlat = true,
icon = { ctx, qi, q -> rootIcon[rand[qi and 1]]!! },
rotateUV = { 0 },
postProcess = noPost
)
if (rand[3] < Config.lilypad.flowerChance) modelRenderer.render(
flowerModel.model,
Rotation.identity,
ctx.blockCenter.add(perturbs[rand[4]]),
forceFlat = true,
icon = { ctx, qi, q -> flowerIcon[rand[0]]!! },
rotateUV = { 0 },
postProcess = noPost
)
return true
}
}

View File

@@ -0,0 +1,30 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.Axis
import mods.octarinecore.client.render.BlockContext
import net.minecraft.block.Block
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)
override var axisFunc = { block: Block, meta: Int -> when ((meta shr 2) and 3) {
1 -> Axis.X
2 -> Axis.Z
else -> Axis.Y
} }
override val blockPredicate = { block: Block, meta: Int -> Config.blocks.logs.matchesID(block) }
override val surroundPredicate = { block: Block -> block.isOpaqueCube && !Config.blocks.logs.matchesID(block) }
override val connectPerpendicular: Boolean get() = Config.roundLogs.connectPerpendicular
override val connectSolids: Boolean get() = Config.roundLogs.connectSolids
override val lenientConnect: Boolean get() = Config.roundLogs.lenientConnect
override val radiusLarge: Double get() = Config.roundLogs.radiusLarge
override val radiusSmall: Double get() = Config.roundLogs.radiusSmall
}

View File

@@ -0,0 +1,48 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.*
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.RenderBlocks
import net.minecraft.init.Blocks
import org.apache.logging.log4j.Level.INFO
class RenderMycelium : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val myceliumIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_mycel_%d")
val myceliumModel = modelSet(64, RenderGrass.grassTopQuads)
override fun afterStitch() {
Client.log(INFO, "Registered ${myceliumIcon.num} mycelium textures")
}
override fun isEligible(ctx: BlockContext): Boolean {
if (!Config.enabled || !Config.shortGrass.myceliumEnabled) return false
return ctx.block == Blocks.mycelium &&
ctx.cameraDistance < Config.shortGrass.distance
}
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
val isSnowed = ctx.block(up1).material.let {
it == Material.snow || it == Material.craftedSnow
}
if (renderWorldBlockBase(parent, face = alwaysRender)) return true
if (isSnowed && !Config.shortGrass.snowEnabled) return true
if (ctx.block(up1).isOpaqueCube) return true
val rand = ctx.semiRandomArray(2)
modelRenderer.render(
myceliumModel[rand[0]],
Rotation.identity,
ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero),
icon = { ctx, qi, q -> myceliumIcon[rand[qi and 1]]!! },
rotateUV = { 0 },
postProcess = if (isSnowed) whitewash else noPost
)
return true
}
}

View File

@@ -0,0 +1,51 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.*
import mods.octarinecore.random
import net.minecraft.client.renderer.RenderBlocks
import net.minecraft.init.Blocks
import net.minecraftforge.common.util.ForgeDirection.DOWN
import net.minecraftforge.common.util.ForgeDirection.UP
import org.apache.logging.log4j.Level.INFO
class RenderNetherrack : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val netherrackIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "better_netherrack_%d")
val netherrackModel = modelSet(64) { modelIdx ->
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yTop = -0.5,
yBottom = -0.5 - random(Config.netherrack.heightMin, Config.netherrack.heightMax))
.setAoShader(faceOrientedAuto(overrideFace = DOWN, corner = cornerAo(Axis.Y)))
.setFlatShader(faceOrientedAuto(overrideFace = DOWN, corner = cornerFlat))
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.shortGrass.hOffset) }.addAll()
}
override fun afterStitch() {
Client.log(INFO, "Registered ${netherrackIcon.num} netherrack textures")
}
override fun isEligible(ctx: BlockContext): Boolean {
if (!Config.enabled || !Config.netherrack.enabled) return false
return ctx.block == Blocks.netherrack &&
ctx.cameraDistance < Config.netherrack.distance
}
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
if (renderWorldBlockBase(parent, face = alwaysRender)) return true
if (ctx.block(down1).isOpaqueCube) return true
val rand = ctx.semiRandomArray(2)
modelRenderer.render(
netherrackModel[rand[0]],
Rotation.identity,
icon = { ctx, qi, q -> netherrackIcon[rand[qi and 1]]!! },
rotateUV = { 0 },
postProcess = noPost
)
return true
}
}

View File

@@ -0,0 +1,65 @@
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.ShadersModIntegration
import mods.octarinecore.client.render.*
import mods.octarinecore.random
import net.minecraft.block.material.Material
import net.minecraft.client.renderer.RenderBlocks
import net.minecraftforge.common.util.ForgeDirection.UP
import org.apache.logging.log4j.Level
class RenderReeds : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val noise = simplexNoise()
val reedIcons = iconSet(Client.genReeds.generatedResource("${BetterFoliageMod.LEGACY_DOMAIN}:better_reed_%d"))
val reedModels = modelSet(64) { modelIdx ->
val height = random(Config.reed.heightMin, Config.reed.heightMax)
val waterline = 0.875f
val vCutLine = 0.5 - waterline / height
listOf(
// below waterline
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.5, yTop = 0.5 + waterline)
.setFlatShader(FlatOffsetNoColor(up1)).clampUV(minV = vCutLine),
// above waterline
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.5 + waterline, yTop = 0.5 + height)
.setFlatShader(FlatOffsetNoColor(up2)).clampUV(maxV = vCutLine)
).forEach {
it.clampUV(minU = -0.25, maxU = 0.25)
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.reed.hOffset) }.addAll()
}
}
override fun afterStitch() {
Client.log(Level.INFO, "Registered ${reedIcons.num} reed textures")
}
override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.reed.enabled &&
ctx.cameraDistance < Config.reed.distance &&
ctx.block(up2).material == Material.air &&
ctx.block(up1).material == Material.water &&
Config.blocks.dirt.matchesID(ctx.block) &&
ctx.biomeId in Config.reed.biomes &&
noise[ctx.x, ctx.z] < Config.reed.population
override fun render(ctx: BlockContext, parent: RenderBlocks): Boolean {
if (renderWorldBlockBase(parent, face = alwaysRender)) return true
val iconVar = ctx.random(1)
ShadersModIntegration.grass(Config.reed.shaderWind) {
modelRenderer.render(
reedModels[ctx.random(0)],
Rotation.identity,
forceFlat = true,
icon = { ctx, qi, q -> reedIcons[iconVar]!! },
rotateUV = { 0 },
postProcess = noPost
)
}
return true
}
}

View File

@@ -0,0 +1,54 @@
@file:JvmName("Utils")
package mods.betterfoliage.client.render
import mods.octarinecore.PI2
import mods.octarinecore.client.render.*
import net.minecraft.block.Block
import net.minecraft.block.material.Material
import net.minecraft.tileentity.TileEntity
import net.minecraft.world.IBlockAccess
import net.minecraft.world.biome.BiomeGenBase
import net.minecraftforge.common.util.ForgeDirection
import net.minecraftforge.common.util.ForgeDirection.*
val up1 = Int3(1 to UP)
val up2 = Int3(2 to UP)
val down1 = Int3(1 to DOWN)
val snowOffset = UP * 0.0625
val normalLeavesRot = arrayOf(Rotation.identity)
val denseLeavesRot = arrayOf(Rotation.identity, Rotation.rot90[EAST.ordinal], Rotation.rot90[SOUTH.ordinal])
val whitewash: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex)->Unit = { ctx, qi, q, vi, v -> setGrey(1.4f) }
val greywash: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex)->Unit = { ctx, qi, q, vi, v -> setGrey(1.0f) }
val Block.isSnow: Boolean get() = material.let { it == Material.snow || it == Material.craftedSnow }
fun Quad.toCross(rotAxis: ForgeDirection, trans: (Quad)->Quad) =
(0..3).map { rotIdx ->
trans(rotate(Rotation.rot90[rotAxis.ordinal] * rotIdx).mirrorUV(rotIdx > 1, false))
}
fun Quad.toCross(rotAxis: ForgeDirection) = toCross(rotAxis) { it }
fun xzDisk(modelIdx: Int) = (PI2 * modelIdx / 64.0).let { Double3(Math.cos(it), 0.0, Math.sin(it)) }
val rotationFromUp = arrayOf(
Rotation.rot90[EAST.ordinal] * 2,
Rotation.identity,
Rotation.rot90[WEST.ordinal],
Rotation.rot90[EAST.ordinal],
Rotation.rot90[SOUTH.ordinal],
Rotation.rot90[NORTH.ordinal]
)
fun Model.mix(first: Model, second: Model, predicate: (Int)->Boolean) {
first.quads.forEachIndexed { qi, quad ->
val otherQuad = second.quads[qi]
Quad(
if (predicate(0)) otherQuad.v1.copy() else quad.v1.copy(),
if (predicate(1)) otherQuad.v2.copy() else quad.v2.copy(),
if (predicate(2)) otherQuad.v3.copy() else quad.v3.copy(),
if (predicate(3)) otherQuad.v4.copy() else quad.v4.copy()
).add()
}
}

View File

@@ -0,0 +1,35 @@
package mods.betterfoliage.client.texture
import mods.octarinecore.client.resource.*
import net.minecraft.util.ResourceLocation
import java.awt.image.BufferedImage
/**
* Generate Short Grass textures from [Blocks.tallgrass] block textures.
* The bottom 3/8 of the base texture is chopped off.
*
* @param[domain] Resource domain of generator
*/
class GrassGenerator(domain: String) : TextureGenerator(domain) {
override fun generate(params: ParameterList): BufferedImage? {
val target = targetResource(params)!!
val isSnowed = params["snowed"]?.toBoolean() ?: false
val baseTexture = resourceManager[target.second]?.loadImage() ?: return null
// draw bottom half of texture
val result = BufferedImage(baseTexture.width, baseTexture.height, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = result.createGraphics()
graphics.drawImage(baseTexture, 0, 3 * baseTexture.height / 8, null)
// blend with white if snowed
if (isSnowed && target.first == ResourceType.COLOR) {
for (x in 0..result.width - 1) for (y in 0..result.height - 1) {
result[x, y] = blendRGB(result[x, y], 16777215, 2, 3)
}
}
return result
}
}

View File

@@ -0,0 +1,69 @@
package mods.betterfoliage.client.texture
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.HSB
import mods.octarinecore.client.resource.averageColor
import net.minecraft.block.Block
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.client.renderer.texture.TextureMap
import net.minecraft.util.IIcon
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.common.MinecraftForge
import org.apache.logging.log4j.Level.DEBUG
import org.apache.logging.log4j.Level.INFO
const val defaultGrassColor = 0
/** Rendering-related information for a grass block. */
class GrassInfo(
/** Top texture of the grass block. */
val grassTopTexture: TextureAtlasSprite,
/**
* 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.
*/
val overrideColor: Int?
)
/** Collects and manages rendering-related information for grass blocks. */
@SideOnly(Side.CLIENT)
object GrassRegistry {
val grass: MutableMap<IIcon, GrassInfo> = hashMapOf()
init {
MinecraftForge.EVENT_BUS.register(this)
}
@SubscribeEvent
fun handleTextureReload(event: TextureStitchEvent.Pre) {
if (event.map.textureType != 0) return
grass.clear()
Client.log(INFO, "Inspecting grass textures")
Block.blockRegistry.forEach { block ->
if (Config.blocks.grass.matchesClass(block as Block)) {
block.registerBlockIcons { location ->
val original = event.map.getTextureExtry(location)
Client.log(DEBUG, "Found grass texture: $location")
registerGrass(event.map, original)
return@registerBlockIcons original
}
}
}
}
fun registerGrass(atlas: TextureMap, icon: TextureAtlasSprite) {
val hsb = HSB.fromColor(icon.averageColor ?: defaultGrassColor)
val overrideColor = if (hsb.saturation > Config.shortGrass.saturationThreshold) hsb.copy(brightness = 0.8f).asColor else null
grass.put(icon, GrassInfo(icon, overrideColor))
}
}

View File

@@ -0,0 +1,77 @@
package mods.betterfoliage.client.texture
import mods.betterfoliage.BetterFoliageMod
import mods.octarinecore.client.resource.*
import mods.octarinecore.stripStart
import net.minecraft.util.ResourceLocation
import java.awt.image.BufferedImage
/**
* Generate round leaf textures from leaf block textures.
* The base texture is tiled 2x2, then parts of it are made transparent by applying a mask to the alpha channel.
*
* Generator parameter _type_: Leaf type (configurable by user). Different leaf types may have their own alpha mask.
*
* @param[domain] Resource domain of generator
*/
class LeafGenerator(domain: String) : TextureGenerator(domain) {
override fun generate(params: ParameterList): BufferedImage? {
val target = targetResource(params)!!
val leafType = params["type"] ?: "default"
val handDrawnLoc = target.second.stripStart("textures/").stripStart("blocks/").let {
ResourceLocation(BetterFoliageMod.DOMAIN, "textures/blocks/${it.resourceDomain}/${it.resourcePath}")
}
resourceManager[handDrawnLoc]?.loadImage()?.let { return it }
val baseTexture = resourceManager[target.second]?.loadImage() ?: return null
val size = baseTexture.width
val frames = baseTexture.height / size
val maskTexture = (getLeafMask(leafType, size * 2) ?: getLeafMask("default", size * 2))?.loadImage()
fun scale(i: Int) = i * maskTexture!!.width / (size * 2)
val leafTexture = BufferedImage(size * 2, size * 2 * frames, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = leafTexture.createGraphics()
// iterate all frames
for (frame in 0 .. frames - 1) {
val baseFrame = baseTexture.getSubimage(0, size * frame, size, size)
val leafFrame = BufferedImage(size * 2, size * 2, BufferedImage.TYPE_4BYTE_ABGR)
// tile leaf texture 2x2
leafFrame.createGraphics().apply {
drawImage(baseFrame, 0, 0, null)
drawImage(baseFrame, 0, size, null)
drawImage(baseFrame, size, 0, null)
drawImage(baseFrame, size, size, null)
}
// overlay alpha mask
if (target.first == ResourceType.COLOR && maskTexture != null) {
for (x in 0 .. size * 2 - 1) for (y in 0 .. size * 2 - 1) {
val basePixel = leafFrame[x, y].toLong() and 0xFFFFFFFFL
val maskPixel = maskTexture[scale(x), scale(y)].toLong() and 0xFF000000L or 0xFFFFFFL
leafFrame[x, y] = (basePixel and maskPixel).toInt()
}
}
// add to animated png
graphics.drawImage(leafFrame, 0, size * frame * 2, null)
}
return leafTexture
}
/**
* Get the alpha mask to use
*
* @param[type] Alpha mask type.
* @param[maxSize] Preferred mask size.
*/
fun getLeafMask(type: String, maxSize: Int) = getMultisizeTexture(maxSize) { size ->
ResourceLocation(BetterFoliageMod.DOMAIN, "textures/blocks/leafmask_${size}_${type}.png")
}
}

View File

@@ -0,0 +1,94 @@
package mods.betterfoliage.client.texture
import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.resource.IconSet
import mods.octarinecore.client.resource.averageColor
import net.minecraft.block.Block
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.client.renderer.texture.TextureMap
import net.minecraft.util.IIcon
import net.minecraft.util.ResourceLocation
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.common.MinecraftForge
import org.apache.logging.log4j.Level.*
const val defaultLeafColor = 0
/** Rendering-related information for a leaf block. */
class LeafInfo(
/** The generated round leaf texture. */
val roundLeafTexture: TextureAtlasSprite,
/** Type of the leaf block (configurable by user). */
val leafType: String,
/** Average color of the round leaf texture. */
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]!!
}
/** Collects and manages rendering-related information for leaf blocks. */
@SideOnly(Side.CLIENT)
object LeafRegistry {
val leaves: MutableMap<IIcon, LeafInfo> = hashMapOf()
val particles: MutableMap<String, IconSet> = hashMapOf()
val typeMappings = TextureMatcher()
init {
MinecraftForge.EVENT_BUS.register(this)
}
@SubscribeEvent
fun handleTextureReload(event: TextureStitchEvent.Pre) {
if (event.map.textureType != 0) return
leaves.clear()
particles.clear()
typeMappings.loadMappings(ResourceLocation("betterfoliage", "leafTypeMappings.cfg"))
Client.log(INFO, "Generating leaf textures")
IconSet("betterfoliage", "falling_leaf_default_%d").let {
it.onStitch(event.map)
particles.put("default", it)
}
Block.blockRegistry.forEach { block ->
if (Config.blocks.leaves.matchesClass(block as Block)) {
block.registerBlockIcons { location ->
val original = event.map.getTextureExtry(location)
Client.log(DEBUG, "Found leaf texture: $location")
registerLeaf(event.map, original)
return@registerBlockIcons original
}
}
}
}
fun registerLeaf(atlas: TextureMap, icon: TextureAtlasSprite) {
var leafType = typeMappings.getType(icon) ?: "default"
val generated = atlas.registerIcon(
Client.genLeaves.generatedResource(icon.iconName, "type" to leafType).toString()
)
if (leafType !in particles.keys) {
val particleSet = IconSet("betterfoliage", "falling_leaf_${leafType}_%d")
particleSet.onStitch(atlas)
if (particleSet.num == 0) {
Client.log(WARN, "Leaf particle textures not found for leaf type: $leafType")
leafType == "default"
} else {
particles.put(leafType, particleSet)
}
}
val leafInfo = LeafInfo(generated as TextureAtlasSprite, leafType)
leaves.put(icon, leafInfo)
}
}

View File

@@ -0,0 +1,35 @@
package mods.betterfoliage.client.texture
import mods.octarinecore.client.resource.resourceManager
import mods.octarinecore.client.resource.get
import mods.octarinecore.client.resource.getLines
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.ResourceLocation
class TextureMatcher() {
data class Mapping(val domain: String?, val path: String, val type: String) {
fun matches(icon: TextureAtlasSprite): Boolean {
val iconLocation = ResourceLocation(icon.iconName)
return (domain == null || domain == iconLocation.resourceDomain) && iconLocation.resourcePath.contains(path)
}
}
val mappings: MutableList<Mapping> = linkedListOf()
fun getType(icon: TextureAtlasSprite): String? = mappings.filter { it.matches(icon) }.map { it.type }.firstOrNull()
fun loadMappings(mappingLocation: ResourceLocation) {
mappings.clear()
resourceManager[mappingLocation]?.getLines()?.let { lines ->
lines.filter { !it.startsWith("//") }.filter { !it.isEmpty() }.forEach { line ->
val line2 = line.trim().split('=')
if (line2.size == 2) {
val mapping = line2[0].trim().split(':')
if (mapping.size == 1) mappings.add(Mapping(null, mapping[0].trim(), line2[1].trim()))
else if (mapping.size == 2) mappings.add(Mapping(mapping[1].trim(), mapping[0].trim(), line2[1].trim()))
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
@file:JvmName("Utils")
package mods.betterfoliage.client.texture
fun blendRGB(rgb1: Int, rgb2: Int, weight1: Int, weight2: Int): Int {
val r = (((rgb1 shr 16) and 255) * weight1 + ((rgb2 shr 16) and 255) * weight2) / (weight1 + weight2)
val g = (((rgb1 shr 8) and 255) * weight1 + ((rgb2 shr 8) and 255) * weight2) / (weight1 + weight2)
val b = ((rgb1 and 255) * weight1 + (rgb2 and 255) * weight2) / (weight1 + weight2)
val a = (rgb1 shr 24) and 255
val result = ((a shl 24) or (r shl 16) or (g shl 8) or b).toInt()
return result
}

View File

@@ -0,0 +1,106 @@
package mods.betterfoliage.loader
import cpw.mods.fml.relauncher.FMLLaunchHandler
import cpw.mods.fml.relauncher.IFMLLoadingPlugin
import mods.octarinecore.metaprog.*
import org.objectweb.asm.Opcodes.*
@IFMLLoadingPlugin.TransformerExclusions(
"mods.betterfoliage.loader",
"mods.octarinecore.metaprog",
"kotlin",
"mods.betterfoliage.kotlin"
)
class BetterFoliageLoader : ASMPlugin(BetterFoliageTransformer::class.java)
class BetterFoliageTransformer : Transformer() {
init {
if (FMLLaunchHandler.side().isClient) setupClient()
}
fun setupClient() {
// where: RenderBlocks.renderBlockByRenderType()
// what: invoke BF code to overrule the return value of Block.getRenderType()
// why: allows us to use custom block renderers for any block, without touching block code
transformMethod(Refs.renderBlockByRenderType) {
find(varinsn(ISTORE, 5))?.insertAfter {
log.info("Applying block render type override")
varinsn(ALOAD, 0)
getField(Refs.blockAccess)
varinsn(ILOAD, 2)
varinsn(ILOAD, 3)
varinsn(ILOAD, 4)
varinsn(ALOAD, 1)
varinsn(ILOAD, 5)
invokeStatic(Refs.getRenderTypeOverride)
varinsn(ISTORE, 5)
} ?: log.warn("Failed to apply block render type override!")
}
// where: WorldClient.doVoidFogParticles(), right before the end of the loop
// what: invoke BF code for every random display tick
// why: allows us to catch random display ticks, without touching block code
transformMethod(Refs.doVoidFogParticles) {
find(IINC)?.insertBefore {
log.info("Applying random display tick call hook")
varinsn(ALOAD, 10)
varinsn(ALOAD, 0)
varinsn(ILOAD, 7)
varinsn(ILOAD, 8)
varinsn(ILOAD, 9)
invokeStatic(Refs.onRandomDisplayTick)
} ?: log.warn("Failed to apply random display tick call hook!")
}
// where: shadersmodcore.client.Shaders.pushEntity()
// what: invoke BF code to overrule block data
// why: allows us to change the block ID seen by shader programs
transformMethod(Refs.pushEntity) {
find(IASTORE)?.insertBefore {
log.info("Applying Shaders.pushEntity() block id override")
varinsn(ALOAD, 1)
invokeStatic(Refs.getBlockIdOverride)
} ?: log.warn("Failed to apply Shaders.pushEntity() block id override!")
}
// where: Block.getAmbientOcclusionLightValue()
// what: invoke BF code to overrule AO transparency value
// why: allows us to have light behave properly on non-solid log blocks without
// messing with isOpaqueBlock(), which could have gameplay effects
transformMethod(Refs.getAmbientOcclusionLightValue) {
find(FRETURN)?.insertBefore {
log.info("Applying Block.getAmbientOcclusionLightValue() override")
varinsn(ALOAD, 0)
invokeStatic(Refs.getAmbientOcclusionLightValueOverride)
} ?: log.warn("Failed to apply Block.getAmbientOcclusionLightValue() override!")
}
// where: Block.getUseNeighborBrightness()
// what: invoke BF code to overrule _useNeighborBrightness_
// why: allows us to have light behave properly on non-solid log blocks
transformMethod(Refs.getUseNeighborBrightness) {
find(IRETURN)?.insertBefore {
log.info("Applying Block.getUseNeighborBrightness() override")
varinsn(ALOAD, 0)
invokeStatic(Refs.getUseNeighborBrightnessOverride)
} ?: log.warn("Failed to apply Block.getUseNeighborBrightness() override!")
}
// where: Block.shouldSideBeRendered()
// what: invoke BF code to overrule condition
// why: allows us to make log blocks non-solid without
// messing with isOpaqueBlock(), which could have gameplay effects
transformMethod(Refs.shouldSideBeRendered) {
find(IRETURN)?.insertBefore {
log.info("Applying Block.shouldSideBeRendered() override")
varinsn(ALOAD, 1)
varinsn(ILOAD, 2)
varinsn(ILOAD, 3)
varinsn(ILOAD, 4)
varinsn(ILOAD, 5)
invokeStatic(Refs.shouldRenderBlockSideOverride)
} ?: log.warn("Failed to apply Block.shouldSideBeRendered() override!")
}
}
}

View File

@@ -0,0 +1,71 @@
package mods.betterfoliage.loader
import cpw.mods.fml.relauncher.FMLInjectionData
import mods.octarinecore.metaprog.ClassRef
import mods.octarinecore.metaprog.FieldRef
import mods.octarinecore.metaprog.MethodRef
/** Singleton object holding references to foreign code elements. */
object Refs {
val mcVersion = FMLInjectionData.data()[4].toString()
// Java
val Map = ClassRef("java.util.Map")
val List = ClassRef("java.util.List")
// Minecraft
val IBlockAccess = ClassRef("net.minecraft.world.IBlockAccess", "ahl")
val Block = ClassRef("net.minecraft.block.Block", "aji")
val getAmbientOcclusionLightValue = MethodRef(Block, "getAmbientOcclusionLightValue", "func_149685_I", "I", ClassRef.float)
val getUseNeighborBrightness = MethodRef(Block, "getUseNeighborBrightness", "func_149710_n", "n", ClassRef.boolean)
val shouldSideBeRendered = MethodRef(Block, "shouldSideBeRendered", "func_149646_a", "a", ClassRef.boolean, IBlockAccess, ClassRef.int, ClassRef.int, ClassRef.int, ClassRef.int)
val RenderBlocks = ClassRef("net.minecraft.client.renderer.RenderBlocks", "blm")
val blockAccess = FieldRef(RenderBlocks, "blockAccess", null, "a", IBlockAccess)
val renderBlockByRenderType = MethodRef(RenderBlocks, "renderBlockByRenderType", null, "b", ClassRef.boolean, Block, ClassRef.int, ClassRef.int, ClassRef.int)
val WorldClient = ClassRef("net.minecraft.client.multiplayer.WorldClient", "bjf")
val doVoidFogParticles = MethodRef(WorldClient, "doVoidFogParticles", null, "C", ClassRef.void, ClassRef.int, ClassRef.int, ClassRef.int)
val World = ClassRef("net.minecraft.world.World", "ahb")
val TextureMap = ClassRef("net.minecraft.client.renderer.texture.TextureMap", "bpr")
val mapRegisteredSprites = FieldRef(TextureMap, "mapRegisteredSprites", "field_110574_e", "bpr", Map)
val IMetadataSerializer = ClassRef("net.minecraft.client.resources.data.IMetadataSerializer", "brw")
val SimpleReloadableResourceManager = ClassRef("net.minecraft.client.resources.SimpleReloadableResourceManager", "brg")
val metadataSerializer = FieldRef(SimpleReloadableResourceManager, "rmMetadataSerializer", "field_110547_c", "f", IMetadataSerializer)
val IIcon = ClassRef("net.minecraft.util.IIcon", "rf")
val TextureAtlasSprite = ClassRef("net.minecraft.client.renderer.texture.TextureAtlasSprite", "bqd")
// Better Foliage
val BetterFoliageHooks = ClassRef("mods.betterfoliage.client.Hooks")
val getAmbientOcclusionLightValueOverride = MethodRef(BetterFoliageHooks, "getAmbientOcclusionLightValueOverride", ClassRef.float, ClassRef.float, Block)
val getUseNeighborBrightnessOverride = MethodRef(BetterFoliageHooks, "getUseNeighborBrightnessOverride", ClassRef.boolean, ClassRef.boolean, Block)
val shouldRenderBlockSideOverride = MethodRef(BetterFoliageHooks, "shouldRenderBlockSideOverride", ClassRef.boolean, ClassRef.boolean, IBlockAccess, ClassRef.int, ClassRef.int, ClassRef.int, ClassRef.int)
val getRenderTypeOverride = MethodRef(BetterFoliageHooks, "getRenderTypeOverride", ClassRef.int, IBlockAccess, ClassRef.int, ClassRef.int, ClassRef.int, Block, ClassRef.int)
val onRandomDisplayTick = MethodRef(BetterFoliageHooks, "onRandomDisplayTick", ClassRef.void, Block, World, ClassRef.int, ClassRef.int, ClassRef.int)
// Shaders mod
val Shaders = ClassRef("shadersmodcore.client.Shaders")
val pushEntity = MethodRef(Shaders, "pushEntity", ClassRef.void, RenderBlocks, Block, ClassRef.int, ClassRef.int, ClassRef.int)
val pushEntity_I = MethodRef(Shaders, "pushEntity", ClassRef.void, ClassRef.int)
val popEntity = MethodRef(Shaders, "popEntity", ClassRef.void)
val ShadersModIntegration = ClassRef("mods.betterfoliage.client.integration.ShadersModIntegration")
val getBlockIdOverride = MethodRef(ShadersModIntegration, "getBlockIdOverride", ClassRef.int, ClassRef.int, Block)
// Optifine
val ConnectedTextures = ClassRef("ConnectedTextures")
val getConnectedTexture = MethodRef(ConnectedTextures, "getConnectedTexture", IIcon, IBlockAccess, Block, ClassRef.int, ClassRef.int, ClassRef.int, ClassRef.int, IIcon)
val CTBlockProperties = FieldRef(ConnectedTextures, "blockProperties", null)
val CTTileProperties = FieldRef(ConnectedTextures, "tileProperties", null)
val ConnectedProperties = ClassRef("ConnectedProperties")
val CPTileIcons = FieldRef(ConnectedProperties, "tileIcons", null)
// Colored Lights Core
val CLCLoadingPlugin = ClassRef("coloredlightscore.src.asm.ColoredLightsCoreLoadingPlugin")
}

View File

@@ -0,0 +1,70 @@
@file:JvmName("Utils")
@file:Suppress("NOTHING_TO_INLINE")
package mods.octarinecore
import net.minecraft.util.ResourceLocation
import kotlin.reflect.KProperty
import java.lang.Math.*
const val PI2 = 2.0 * PI
inline fun String.stripStart(str: String) = if (startsWith(str)) substring(str.length) else this
inline fun ResourceLocation.stripStart(str: String) = ResourceLocation(resourceDomain, resourcePath.stripStart(str))
/** Mutating version of _map_. Replace each element of the list with the result of the given transformation. */
inline fun <reified T> MutableList<T>.replace(transform: (T) -> T) = forEachIndexed { idx, value -> this[idx] = transform(value) }
/** Exchange the two elements of the list with the given indices */
inline fun <T> MutableList<T>.exchange(idx1: Int, idx2: Int) {
val e = this[idx1]
this[idx1] = this[idx2]
this[idx2] = e
}
/** Cross product of this [Iterable] with the parameter. */
fun <A, B> Iterable<A>.cross(other: Iterable<B>) = flatMap { a -> other.map { b -> a to b } }
/**
* Property-level delegate backed by a [ThreadLocal].
*
* @param[init] Lambda to get initial value
*/
class ThreadLocalDelegate<T>(init: () -> T) {
var tlVal = ThreadLocal.withInitial(init)
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = tlVal.get()
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { tlVal.set(value) }
}
/**
* Starting with the second element of this [Iterable] until the last, call the supplied lambda with
* the parameters (index, element, previous element).
*/
inline fun <reified T> Iterable<T>.forEachPairIndexed(func: (Int, T, T)->Unit) {
var previous: T? = null
forEachIndexed { idx, current ->
if (previous != null) func(idx, current, previous!!)
previous = current
}
}
/** Call the supplied lambda and return its result, or the given default value if an exception is thrown. */
fun <T> tryDefault(default: T, work: ()->T) = try { work() } catch (e: Throwable) { default }
/** Return a random [Double] value between the given two limits (inclusive min, exclusive max). */
fun random(min: Double, max: Double) = Math.random().let { min + (max - min) * it }
/**
* Return this [Double] value if it lies between the two limits. If outside, return the
* minimum/maximum value correspondingly.
*/
fun Double.minmax(minVal: Double, maxVal: Double) = min(max(this, minVal), maxVal)
/**
* Return this [Int] value if it lies between the two limits. If outside, return the
* minimum/maximum value correspondingly.
*/
fun Int.minmax(minVal: Int, maxVal: Int) = min(max(this, minVal), maxVal)
fun nextPowerOf2(x: Int): Int {
return 1 shl (if (x == 0) 0 else 32 - Integer.numberOfLeadingZeros(x - 1))
}

View File

@@ -0,0 +1,22 @@
package mods.octarinecore.client
import cpw.mods.fml.client.registry.ClientRegistry
import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import cpw.mods.fml.common.gameevent.InputEvent
import net.minecraft.client.settings.KeyBinding
class KeyHandler(val modId: String, val defaultKey: Int, val lang: String, val action: (InputEvent.KeyInputEvent)->Unit) {
val keyBinding = KeyBinding(lang, defaultKey, modId)
init {
ClientRegistry.registerKeyBinding(keyBinding)
FMLCommonHandler.instance().bus().register(this)
}
@SubscribeEvent
fun handleKeyPress(event: InputEvent.KeyInputEvent) {
if (keyBinding.isPressed) action(event)
}
}

View File

@@ -0,0 +1,58 @@
package mods.octarinecore.client.gui
import cpw.mods.fml.client.config.*
import net.minecraft.client.gui.GuiScreen
import net.minecraft.client.resources.I18n
import net.minecraft.util.EnumChatFormatting.GOLD
import net.minecraft.util.EnumChatFormatting.YELLOW
/**
* Base class for a config GUI element.
* The GUI representation is a list of toggleable objects.
* The config representation is an integer list of the selected objects' IDs.
*/
abstract class IdListConfigEntry<T>(
owningScreen: GuiConfig,
owningEntryList: GuiConfigEntries,
configElement: IConfigElement<*>
) : GuiConfigEntries.CategoryEntry(owningScreen, owningEntryList, configElement) {
/** Create the child GUI elements. */
fun createChildren() = baseSet.map {
ItemWrapperElement(it, it.itemId in configElement.list, it.itemId in configElement.defaults)
}
init { stripTooltipDefaultText(toolTip as MutableList<String>) }
override fun buildChildScreen(): GuiScreen {
return GuiConfig(this.owningScreen, createChildren(), this.owningScreen.modID,
owningScreen.allRequireWorldRestart || this.configElement.requiresWorldRestart(),
owningScreen.allRequireMcRestart || this.configElement.requiresMcRestart(), this.owningScreen.title,
((if (this.owningScreen.titleLine2 == null) "" else this.owningScreen.titleLine2) + " > " + this.name))
}
override fun saveConfigElement(): Boolean {
val requiresRestart = (childScreen as GuiConfig).entryList.saveConfigElements()
val children = (childScreen as GuiConfig).configElements as List<ItemWrapperElement>
val ids = children.filter { it.booleanValue == true }.map { it.item.itemId }
configElement.set(ids.sorted().toTypedArray())
return requiresRestart
}
abstract val baseSet: List<T>
abstract val T.itemId: Int
abstract val T.itemName: String
/** Child config GUI element of a single toggleable object. */
inner class ItemWrapperElement(val item: T, value: Boolean, val default: Boolean) :
DummyConfigElement<Boolean>(item.itemName, default, ConfigGuiType.BOOLEAN, item.itemName) {
init { set(value) }
override fun getComment() = I18n.format("${configElement.languageKey}.tooltip.element", "${GOLD}${item.itemName}${YELLOW}")
override fun set(value: Boolean) { this.value = value }
fun setDefault(value: Boolean) { this.defaultValue = value }
val booleanValue: Boolean get() = value as Boolean
}
}

View File

@@ -0,0 +1,25 @@
package mods.octarinecore.client.gui
import cpw.mods.fml.client.config.GuiConfig
import cpw.mods.fml.client.config.GuiConfigEntries
import cpw.mods.fml.client.config.IConfigElement
import net.minecraft.client.resources.I18n
import net.minecraft.util.EnumChatFormatting.*
class NonVerboseArrayEntry(
owningScreen: GuiConfig,
owningEntryList: GuiConfigEntries,
configElement: IConfigElement<*>
) : GuiConfigEntries.ArrayEntry(owningScreen, owningEntryList, configElement) {
init {
stripTooltipDefaultText(toolTip as MutableList<String>)
val shortDefaults = I18n.format("${configElement.languageKey}.arrayEntry", configElement.defaults.size)
toolTip.addAll(mc.fontRenderer.listFormattedStringToWidth("$AQUA${I18n.format("fml.configgui.tooltip.default", shortDefaults)}", 300))
}
override fun updateValueButtonText() {
btnValue.displayString = I18n.format("${configElement.languageKey}.arrayEntry", currentValues.size)
}
}

View File

@@ -0,0 +1,13 @@
@file:JvmName("Utils")
package mods.octarinecore.client.gui
import net.minecraft.util.EnumChatFormatting
fun stripTooltipDefaultText(tooltip: MutableList<String>) {
var defaultRows = false
val iter = tooltip.iterator()
while (iter.hasNext()) {
if (iter.next().startsWith(EnumChatFormatting.AQUA.toString())) defaultRows = true
if (defaultRows) iter.remove()
}
}

View File

@@ -0,0 +1,160 @@
@file:JvmName("RendererHolder")
package mods.octarinecore.client.render
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler
import cpw.mods.fml.client.registry.RenderingRegistry
import mods.octarinecore.ThreadLocalDelegate
import mods.octarinecore.client.resource.ResourceHandler
import net.minecraft.block.Block
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.RenderBlocks
import net.minecraft.util.IIcon
import net.minecraft.util.MathHelper
import net.minecraft.world.IBlockAccess
import net.minecraftforge.common.util.ForgeDirection
/**
* [ThreadLocal] instance of [ExtendedRenderBlocks] used instead of the vanilla [RenderBlocks] to get the
* AO values and textures used in rendering without duplicating vanilla code.
*/
val renderBlocks by ThreadLocalDelegate { ExtendedRenderBlocks() }
/**
* [ThreadLocal] instance of [BlockContext] representing the block being rendered.
*/
val blockContext by ThreadLocalDelegate { BlockContext() }
/**
* [ThreadLocal] instance of [ModelRenderer].
*/
val modelRenderer by ThreadLocalDelegate { ModelRenderer() }
abstract class AbstractBlockRenderingHandler(modId: String) : ResourceHandler(modId), ISimpleBlockRenderingHandler {
// ============================
// Self-registration
// ============================
val id = RenderingRegistry.getNextAvailableRenderId()
init {
RenderingRegistry.registerBlockHandler(this);
}
// ============================
// Custom rendering
// ============================
abstract fun isEligible(ctx: BlockContext): Boolean
abstract fun render(ctx: BlockContext, parent: RenderBlocks): Boolean
// ============================
// Interface implementation
// ============================
override fun renderWorldBlock(world: IBlockAccess?, x: Int, y: Int, z: Int, block: Block?, modelId: Int, parentRenderer: RenderBlocks?): Boolean {
renderBlocks.blockAccess = world
return render(blockContext, parentRenderer!!)
}
override fun renderInventoryBlock(block: Block?, metadata: Int, modelId: Int, renderer: RenderBlocks?) {}
override fun shouldRender3DInInventory(modelId: Int) = true
override fun getRenderId(): Int = id
// ============================
// Vanilla rendering wrapper
// ============================
/**
* Render the block in the current [BlockContext], and capture shading and texture data.
*
* @param[parentRenderer] parent renderer passed in by rendering pipeline, used only for block breaking overlay
* @param[targetPass] which render pass to save shading and texture data from
* @param[block] lambda to use to render the block if it does not have a custom renderer
* @param[face] lambda to determine which faces of the block to render
*/
fun renderWorldBlockBase(
parentRenderer: RenderBlocks = renderBlocks,
targetPass: Int = 1,
block: () -> Unit = { blockContext.let { ctx -> renderBlocks.renderStandardBlock(ctx.block, ctx.x, ctx.y, ctx.z) } },
face: (ShadingCapture, ForgeDirection, Int, IIcon?) -> Boolean
): Boolean {
val ctx = blockContext
val renderBlocks = renderBlocks
// use original renderer for block breaking overlay
if (parentRenderer.hasOverrideBlockTexture()) {
parentRenderer.setRenderBoundsFromBlock(ctx.block);
parentRenderer.renderStandardBlock(ctx.block, ctx.x, ctx.y, ctx.z);
return true;
}
// render block
renderBlocks.capture.reset(targetPass)
renderBlocks.capture.renderCallback = face
renderBlocks.setRenderBoundsFromBlock(ctx.block);
val handler = renderingHandlers[ctx.block.renderType];
if (handler != null && ctx.block.renderType != 0) {
handler.renderWorldBlock(ctx.world, ctx.x, ctx.y, ctx.z, ctx.block, ctx.block.renderType, renderBlocks);
} else {
block()
}
return false;
}
}
/**
* Represents the block being rendered. Has properties and methods to query the neighborhood of the block in
* block-relative coordinates.
*/
class BlockContext() {
var world: IBlockAccess? = null
var x: Int = 0
var y: Int = 0
var z: Int = 0
fun set(world: IBlockAccess, x: Int, y: Int, z: Int) { this.world = world; this.x = x; this.y = y; this.z = z; }
/** Get the [Block] at the given offset. */
val block: Block get() = world!!.getBlock(x, y, z)
fun block(offset: Int3) = world!!.getBlock(x + offset.x, y + offset.y, z + offset.z)
/** Get the metadata at the given offset. */
val meta: Int get() = world!!.getBlockMetadata(x, y, z)
fun meta(offset: Int3) = world!!.getBlockMetadata(x + offset.x, y + offset.y, z + offset.z)
/** Get the block color multiplier at the given offset. */
val blockColor: Int get() = block.colorMultiplier(world, x, y, z)
fun blockColor(offset: Int3) = block(offset).colorMultiplier(world, x + offset.x, y + offset.y, z + offset.z)
/** Get the block brightness at the given offset. */
val blockBrightness: Int get() = block.getMixedBrightnessForBlock(world, x, y, z)
fun blockBrightness(offset: Int3) = block(offset).getMixedBrightnessForBlock(world, x + offset.x, y + offset.y, z + offset.z)
/** Get the biome ID at the block position. */
val biomeId: Int get() = world!!.getBiomeGenForCoords(x, z).biomeID
/** Get the texture on a given face of the block being rendered. */
fun icon(face: ForgeDirection) = block(Int3.zero).getIcon(face.ordinal, meta(Int3.zero))
/** Get the texture on a given face of the block at the given offset. */
fun icon(offset: Int3, face: ForgeDirection) = block(offset).getIcon(face.ordinal, meta(offset))
/** Get the centerpoint of the block being rendered. */
val blockCenter: Double3 get() = Double3(x + 0.5, y + 0.5, z + 0.5)
/** Is the block surrounded by other blocks that satisfy the predicate on all sides? */
fun isSurroundedBy(predicate: (Block)->Boolean) = forgeDirOffsets.all { predicate(block(it)) }
/** Get a semi-random value based on the block coordinate and the given seed. */
fun random(seed: Int): Int {
var value = (x * x + y * y + z * z + x * y + y * z + z * x + (seed * seed)) and 63
value = (3 * x * value + 5 * y * value + 7 * z * value + (11 * seed)) and 63
return value
}
/** Get an array of semi-random values based on the block coordinate. */
fun semiRandomArray(num: Int): Array<Int> = Array(num) { random(it) }
/** Get the distance of the block from the camera (player). */
val cameraDistance: Int get() {
val camera = Minecraft.getMinecraft().renderViewEntity ?: return 0
return Math.abs(x - MathHelper.floor_double(camera.posX)) +
Math.abs(y - MathHelper.floor_double(camera.posY)) +
Math.abs(z - MathHelper.floor_double(camera.posZ))
}
}

View File

@@ -0,0 +1,98 @@
package mods.octarinecore.client.render
import mods.octarinecore.PI2
import net.minecraft.client.Minecraft
import net.minecraft.client.particle.EntityFX
import net.minecraft.client.renderer.Tessellator
import net.minecraft.util.IIcon
import net.minecraft.world.World
abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) : EntityFX(world, x, y, z) {
companion object {
@JvmStatic val sin = Array(64) { idx -> Math.sin(PI2 / 64.0 * idx) }
@JvmStatic val cos = Array(64) { idx -> Math.cos(PI2 / 64.0 * idx) }
}
val billboardRot = Pair(Double3.zero, Double3.zero)
val currentPos = Double3.zero
val prevPos = Double3.zero
val velocity = Double3.zero
override fun onUpdate() {
super.onUpdate()
currentPos.setTo(posX, posY, posZ)
prevPos.setTo(prevPosX, prevPosY, prevPosZ)
velocity.setTo(motionX, motionY, motionZ)
update()
posX = currentPos.x; posY = currentPos.y; posZ = currentPos.z;
motionX = velocity.x; motionY = velocity.y; motionZ = velocity.z;
}
/** Render the particle. */
abstract fun render(tessellator: Tessellator, partialTickTime: Float)
/** Update particle on world tick. */
abstract fun update()
/** True if the particle is renderable. */
abstract val isValid: Boolean
/** Add the particle to the effect renderer if it is valid. */
fun addIfValid() { if (isValid) Minecraft.getMinecraft().effectRenderer.addEffect(this) }
override fun renderParticle(tessellator: Tessellator, partialTickTime: Float, rotX: Float, rotZ: Float, rotYZ: Float, rotXY: Float, rotXZ: Float) {
billboardRot.first.setTo(rotX + rotXY, rotZ, rotYZ + rotXZ)
billboardRot.second.setTo(rotX - rotXY, -rotZ, rotYZ - rotXZ)
render(tessellator, partialTickTime)
}
/**
* Render a particle quad.
*
* @param[tessellator] the [Tessellator] instance to use
* @param[partialTickTime] partial tick time
* @param[currentPos] render position
* @param[prevPos] previous tick position for interpolation
* @param[size] particle size
* @param[rotation] viewpoint-dependent particle rotation (64 steps)
* @param[icon] particle texture
* @param[isMirrored] mirror particle texture along V-axis
* @param[alpha] aplha blending
*/
fun renderParticleQuad(tessellator: Tessellator,
partialTickTime: Float,
currentPos: Double3 = this.currentPos,
prevPos: Double3 = this.prevPos,
size: Double = particleScale.toDouble(),
rotation: Int = 0,
icon: IIcon = particleIcon,
isMirrored: Boolean = false,
alpha: Float = this.particleAlpha) {
val minU = (if (isMirrored) icon.minU else icon.maxU).toDouble()
val maxU = (if (isMirrored) icon.maxU else icon.minU).toDouble()
val minV = icon.minV.toDouble()
val maxV = icon.maxV.toDouble()
val center = currentPos.copy().sub(prevPos).mul(partialTickTime.toDouble()).add(prevPos).sub(interpPosX, interpPosY, interpPosZ)
val v1 = if (rotation == 0) billboardRot.first * size else
Double3.weight(billboardRot.first, cos[rotation and 63] * size, billboardRot.second, sin[rotation and 63] * size)
val v2 = if (rotation == 0) billboardRot.second * size else
Double3.weight(billboardRot.first, -sin[rotation and 63] * size, billboardRot.second, cos[rotation and 63] * size)
tessellator.setColorRGBA_F(this.particleRed, this.particleGreen, this.particleBlue, alpha)
tessellator.addVertexWithUV(center.x - v1.x, center.y - v1.y, center.z - v1.z, maxU, maxV)
tessellator.addVertexWithUV(center.x - v2.x, center.y - v2.y, center.z - v2.z, maxU, minV)
tessellator.addVertexWithUV(center.x + v1.x, center.y + v1.y, center.z + v1.z, minU, minV)
tessellator.addVertexWithUV(center.x + v2.x, center.y + v2.y, center.z + v2.z, minU, maxV)
}
override fun getFXLayer() = 1
fun setColor(color: Int) {
particleBlue = (color and 255) / 256.0f
particleGreen = ((color shr 8) and 255) / 256.0f
particleRed = ((color shr 16) and 255) / 256.0f
}
}

View File

@@ -0,0 +1,214 @@
package mods.octarinecore.client.render
import mods.octarinecore.client.render.Axis.*
import mods.octarinecore.client.render.Dir.N
import mods.octarinecore.client.render.Dir.P
import mods.octarinecore.cross
import net.minecraftforge.common.util.ForgeDirection
import net.minecraftforge.common.util.ForgeDirection.*
// ================================
// Axes and directions
// ================================
enum class Axis { X, Y, Z }
enum class Dir { P, N }
val axes = listOf(X, Y, Z)
val axisDirs = listOf(P, N)
val forgeDirs = ForgeDirection.VALID_DIRECTIONS
val forgeDirOffsets = forgeDirs.map { Int3(it) }
val ForgeDirection.axis: Axis get() = when(this) {EAST, WEST -> X; UP, DOWN -> Y; else -> Z }
val ForgeDirection.dir: Dir get() = when(this) {UP, SOUTH, EAST -> P; else -> N }
val Pair<Axis, Dir>.face: ForgeDirection get() = when(this) {
X to P -> EAST; X to N -> WEST; Y to P -> UP; Y to N -> DOWN; Z to P -> SOUTH; Z to N -> NORTH; else -> UNKNOWN
}
val ForgeDirection.perpendiculars: List<ForgeDirection> get() =
axes.filter { it != this.axis }.cross(axisDirs).map { it.face }
val ForgeDirection.offset: Int3 get() = forgeDirOffsets[ordinal]
// ================================
// Vectors
// ================================
operator fun ForgeDirection.times(scale: Double) =
Double3(offsetX.toDouble() * scale, offsetY.toDouble() * scale, offsetZ.toDouble() * scale)
val ForgeDirection.vec: Double3 get() = Double3(offsetX.toDouble(), offsetY.toDouble(), offsetZ.toDouble())
/** 3D vector of [Double]s. Offers both mutable operations, and immutable operations in operator notation. */
data class Double3(var x: Double, var y: Double, var z: Double) {
constructor(x: Float, y: Float, z: Float) : this(x.toDouble(), y.toDouble(), z.toDouble())
constructor(dir: ForgeDirection) : this(dir.offsetX.toDouble(), dir.offsetY.toDouble(), dir.offsetZ.toDouble())
companion object {
val zero: Double3 get() = Double3(0.0, 0.0, 0.0)
fun weight(v1: Double3, weight1: Double, v2: Double3, weight2: Double) =
Double3(v1.x * weight1 + v2.x * weight2, v1.y * weight1 + v2.y * weight2, v1.z * weight1 + v2.z * weight2)
}
// immutable operations
operator fun plus(other: Double3) = Double3(x + other.x, y + other.y, z + other.z)
operator fun unaryMinus() = Double3(-x, -y, -z)
operator fun minus(other: Double3) = Double3(x - other.x, y - other.y, z - other.z)
operator fun times(scale: Double) = Double3(x * scale, y * scale, z * scale)
operator fun times(other: Double3) = Double3(x * other.x, y * other.y, z * other.z)
/** Rotate this vector, and return coordinates in the unrotated frame */
fun rotate(rot: Rotation) = Double3(
rot.rotatedComponent(EAST, x, y, z),
rot.rotatedComponent(UP, x, y, z),
rot.rotatedComponent(SOUTH, x, y, z)
)
// mutable operations
fun setTo(other: Double3): Double3 { x = other.x; y = other.y; z = other.z; return this }
fun setTo(x: Double, y: Double, z: Double): Double3 { this.x = x; this.y = y; this.z = z; return this }
fun setTo(x: Float, y: Float, z: Float) = setTo(x.toDouble(), y.toDouble(), z.toDouble())
fun add(other: Double3): Double3 { x += other.x; y += other.y; z += other.z; return this }
fun add(x: Double, y: Double, z: Double): Double3 { this.x += x; this.y += y; this.z += z; return this }
fun sub(other: Double3): Double3 { x -= other.x; y -= other.y; z -= other.z; return this }
fun sub(x: Double, y: Double, z: Double): Double3 { this.x -= x; this.y -= y; this.z -= z; return this }
fun invert(): Double3 { x = -x; y = -y; z = -z; return this }
fun mul(scale: Double): Double3 { x *= scale; y *= scale; z *= scale; return this }
fun mul(other: Double3): Double3 { x *= other.x; y *= other.y; z *= other.z; return this }
fun rotateMut(rot: Rotation): Double3 {
val rotX = rot.rotatedComponent(EAST, x, y, z)
val rotY = rot.rotatedComponent(UP, x, y, z)
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
return setTo(rotX, rotY, rotZ)
}
// misc operations
infix fun dot(other: Double3) = x * other.x + y * other.y + z * other.z
infix fun cross(o: Double3) = Double3(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x)
val length: Double get() = Math.sqrt(x * x + y * y + z * z)
val normalize: Double3 get() = (1.0 / length).let { Double3(x * it, y * it, z * it) }
val nearestCardinal: ForgeDirection get() = nearestAngle(this, forgeDirs.asIterable()) { it.vec }.first
}
/** 3D vector of [Int]s. Offers both mutable operations, and immutable operations in operator notation. */
data class Int3(var x: Int, var y: Int, var z: Int) {
constructor(dir: ForgeDirection) : this(dir.offsetX, dir.offsetY, dir.offsetZ)
constructor(offset: Pair<Int, ForgeDirection>) : this(
offset.first * offset.second.offsetX,
offset.first * offset.second.offsetY,
offset.first * offset.second.offsetZ
)
companion object {
val zero = Int3(0, 0, 0)
}
// immutable operations
operator fun plus(other: Int3) = Int3(x + other.x, y + other.y, z + other.z)
operator fun plus(other: Pair<Int, ForgeDirection>) = Int3(
x + other.first * other.second.offsetX,
y + other.first * other.second.offsetY,
z + other.first * other.second.offsetZ
)
operator fun unaryMinus() = Int3(-x, -y, -z)
operator fun minus(other: Int3) = Int3(x - other.x, y - other.y, z - other.z)
operator fun times(scale: Int) = Int3(x * scale, y * scale, z * scale)
operator fun times(other: Int3) = Int3(x * other.x, y * other.y, z * other.z)
/** Rotate this vector, and return coordinates in the unrotated frame */
fun rotate(rot: Rotation) = Int3(
rot.rotatedComponent(EAST, x, y, z),
rot.rotatedComponent(UP, x, y, z),
rot.rotatedComponent(SOUTH, x, y, z)
)
// mutable operations
fun setTo(other: Int3): Int3 { x = other.x; y = other.y; z = other.z; return this }
fun setTo(x: Int, y: Int, z: Int): Int3 { this.x = x; this.y = y; this.z = z; return this }
fun add(other: Int3): Int3 { x += other.x; y += other.y; z += other.z; return this }
fun sub(other: Int3): Int3 { x -= other.x; y -= other.y; z -= other.z; return this }
fun invert(): Int3 { x = -x; y = -y; z = -z; return this }
fun mul(scale: Int): Int3 { x *= scale; y *= scale; z *= scale; return this }
fun mul(other: Int3): Int3 { x *= other.x; y *= other.y; z *= other.z; return this }
fun rotateMut(rot: Rotation): Int3 {
val rotX = rot.rotatedComponent(EAST, x, y, z)
val rotY = rot.rotatedComponent(UP, x, y, z)
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
return setTo(rotX, rotY, rotZ)
}
}
// ================================
// Rotation
// ================================
val ForgeDirection.rotations: Array<ForgeDirection> get() =
Array(6) { idx -> ForgeDirection.values()[ForgeDirection.ROTATION_MATRIX[ordinal][idx]] }
fun ForgeDirection.rotate(rot: Rotation) = rot.forward[ordinal]
fun rot(axis: ForgeDirection) = Rotation.rot90[axis.ordinal]
/**
* Class representing an arbitrary rotation (or combination of rotations) around cardinal axes by 90 degrees.
* In effect, a permutation of [ForgeDirection]s.
*/
@Suppress("NOTHING_TO_INLINE")
class Rotation(val forward: Array<ForgeDirection>, val reverse: Array<ForgeDirection>) {
operator fun plus(other: Rotation) = Rotation(
Array(6) { idx -> forward[other.forward[idx].ordinal] },
Array(6) { idx -> other.reverse[reverse[idx].ordinal] }
)
operator fun unaryMinus() = Rotation(reverse, forward)
operator fun times(num: Int) = when(num % 4) { 1 -> this; 2 -> this + this; 3 -> -this; else -> identity }
inline fun rotatedComponent(dir: ForgeDirection, x: Int, y: Int, z: Int) =
when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0 }
inline fun rotatedComponent(dir: ForgeDirection, x: Double, y: Double, z: Double) =
when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0.0 }
companion object {
// Forge rotation matrix is left-hand
val rot90 = Array(6) { idx -> Rotation(forgeDirs[idx].opposite.rotations, forgeDirs[idx].rotations) }
val identity = Rotation(forgeDirs, forgeDirs)
}
}
// ================================
// Miscellaneous
// ================================
/** List of all 12 box edges, represented as a [Pair] of [ForgeDirection]s */
val boxEdges = forgeDirs.flatMap { face1 -> forgeDirs.filter { it.axis > face1.axis }.map { face1 to it } }
/**
* Get the closest object to the specified point from a list of objects.
*
* @param[vertex] the reference point
* @param[objs] list of geomertric objects
* @param[objPos] lambda to calculate the position of an object
* @return [Pair] of (object, distance)
*/
fun <T> nearestPosition(vertex: Double3, objs: Iterable<T>, objPos: (T)->Double3): Pair<T, Double> =
objs.map { it to (objPos(it) - vertex).length }.minBy { it.second }!!
/**
* Get the object closest in orientation to the specified vector from a list of objects.
*
* @param[vector] the reference vector (direction)
* @param[objs] list of geomertric objects
* @param[objAngle] lambda to calculate the orientation of an object
* @return [Pair] of (object, normalized dot product)
*/
fun <T> nearestAngle(vector: Double3, objs: Iterable<T>, objAngle: (T)->Double3): Pair<T, Double> =
objs.map { it to objAngle(it).dot(vector) }.maxBy { it.second }!!
data class FaceCorners(val topLeft: Pair<ForgeDirection, ForgeDirection>,
val topRight: Pair<ForgeDirection, ForgeDirection>,
val bottomLeft: Pair<ForgeDirection, ForgeDirection>,
val bottomRight: Pair<ForgeDirection, ForgeDirection>) {
constructor(top: ForgeDirection, left: ForgeDirection) :
this(top to left, top to left.opposite, top.opposite to left, top.opposite to left.opposite)
val asArray = arrayOf(topLeft, topRight, bottomLeft, bottomRight)
val asList = listOf(topLeft, topRight, bottomLeft, bottomRight)
}
val faceCorners = forgeDirs.map { when(it) {
DOWN -> FaceCorners(SOUTH, WEST)
UP -> FaceCorners(SOUTH, EAST)
NORTH -> FaceCorners(WEST, UP)
SOUTH -> FaceCorners(UP, WEST)
WEST -> FaceCorners(SOUTH, UP)
EAST ->FaceCorners(SOUTH, DOWN)
else -> FaceCorners(UNKNOWN, UNKNOWN)
}}

View File

@@ -0,0 +1,138 @@
package mods.octarinecore.client.render
import mods.octarinecore.minmax
import mods.octarinecore.replace
import net.minecraftforge.common.util.ForgeDirection
import java.lang.Math.*
/**
* Vertex UV coordinates
*
* Zero-centered: coordinates fall between (-0.5, 0.5) (inclusive)
*/
data class UV(val u: Double, val v: Double) {
companion object {
val topLeft = UV(-0.5, -0.5)
val topRight = UV(0.5, -0.5)
val bottomLeft = UV(-0.5, 0.5)
val bottomRight = UV(0.5, 0.5)
}
val rotate: UV get() = UV(v, -u)
fun rotate(n: Int) = when(n % 4) {
0 -> copy()
1 -> UV(v, -u)
2 -> UV(-u, -v)
else -> UV(-v, u)
}
fun clamp(minU: Double = -0.5, maxU: Double = 0.5, minV: Double = -0.5, maxV: Double = 0.5) =
UV(u.minmax(minU, maxU), v.minmax(minV, maxV))
fun mirror(mirrorU: Boolean, mirrorV: Boolean) = UV(if (mirrorU) -u else u, if (mirrorV) -v else v)
}
/**
* Model vertex
*
* @param[xyz] x, y, z coordinates
* @param[uv] u, v coordinates
* @param[aoShader] [Shader] instance to use with AO rendering
* @param[flatShader] [Shader] instance to use with non-AO rendering
*/
data class Vertex(val xyz: Double3 = Double3(0.0, 0.0, 0.0),
val uv: UV = UV(0.0, 0.0),
val aoShader: Shader = NoShader,
val flatShader: Shader = NoShader)
/**
* Model quad
*/
data class Quad(val v1: Vertex, val v2: Vertex, val v3: Vertex, val v4: Vertex) {
val verts = arrayOf(v1, v2, v3, v4)
inline fun transformV(trans: (Vertex)->Vertex): Quad = transformVI { vertex, idx -> trans(vertex) }
inline fun transformVI(trans: (Vertex, Int)->Vertex): Quad =
Quad(trans(v1, 0), trans(v2, 1), trans(v3, 2), trans(v4, 3))
val normal: Double3 get() = (v2.xyz - v1.xyz).cross(v4.xyz - v1.xyz).normalize
fun move(trans: Double3) = transformV { it.copy(xyz = it.xyz + trans) }
fun move(trans: Pair<Double, ForgeDirection>) = move(Double3(trans.second) * trans.first)
fun scale (scale: Double) = transformV { it.copy(xyz = it.xyz * scale) }
fun scale (scale: Double3) = transformV { it.copy(xyz = Double3(it.xyz.x * scale.x, it.xyz.y * scale.y, it.xyz.z * scale.z)) }
fun scaleUV (scale: Double) = transformV { it.copy(uv = UV(it.uv.u * scale, it.uv.v * scale)) }
fun rotate(rot: Rotation) = transformV {
it.copy(xyz = it.xyz.rotate(rot), aoShader = it.aoShader.rotate(rot), flatShader = it.flatShader.rotate(rot))
}
fun rotateUV(n: Int) = transformV { it.copy(uv = it.uv.rotate(n)) }
fun clampUV(minU: Double = -0.5, maxU: Double = 0.5, minV: Double = -0.5, maxV: Double = 0.5) =
transformV { it.copy(uv = it.uv.clamp(minU, maxU, minV, maxV)) }
fun mirrorUV(mirrorU: Boolean, mirrorV: Boolean) = transformV { it.copy(uv = it.uv.mirror(mirrorU, mirrorV)) }
fun setAoShader(resolver: (Quad, Vertex)->Shader, predicate: (Vertex, Int)->Boolean = { v, vi -> true }) =
transformVI { vertex, idx ->
if (!predicate(vertex, idx)) vertex else vertex.copy(aoShader = resolver(this@Quad, vertex))
}
fun setFlatShader(resolver: (Quad, Vertex)->Shader, predicate: (Vertex, Int)->Boolean = { v, vi -> true }) =
transformVI { vertex, idx ->
if (!predicate(vertex, idx)) vertex else vertex.copy(flatShader = resolver(this@Quad, vertex))
}
fun setFlatShader(shader: Shader) = transformVI { vertex, idx -> vertex.copy(flatShader = shader) }
val flipped: Quad get() = Quad(v4, v3, v2, v1)
}
/**
* Model. The basic unit of rendering blocks with OctarineCore.
*
* The model should be positioned so that (0,0,0) is the block center.
* The block extends to (-0.5, 0.5) in all directions (inclusive).
*/
class Model() {
constructor(other: List<Quad>) : this() { quads.addAll(other) }
val quads = linkedListOf<Quad>()
fun Quad.add() = quads.add(this)
fun Iterable<Quad>.addAll() = forEach { quads.add(it) }
fun transformQ(trans: (Quad)->Quad) = quads.replace(trans)
fun transformV(trans: (Vertex)->Vertex) = quads.replace{ it.transformV(trans) }
fun verticalRectangle(x1: Double, z1: Double, x2: Double, z2: Double, yBottom: Double, yTop: Double) = Quad(
Vertex(Double3(x1, yBottom, z1), UV.bottomLeft),
Vertex(Double3(x2, yBottom, z2), UV.bottomRight),
Vertex(Double3(x2, yTop, z2), UV.topRight),
Vertex(Double3(x1, yTop, z1), UV.topLeft)
)
fun horizontalRectangle(x1: Double, z1: Double, x2: Double, z2: Double, y: Double): Quad {
val xMin = min(x1, x2); val xMax = max(x1, x2)
val zMin = min(z1, z2); val zMax = max(z1, z2)
return Quad(
Vertex(Double3(xMin, y, zMin), UV.topLeft),
Vertex(Double3(xMin, y, zMax), UV.bottomLeft),
Vertex(Double3(xMax, y, zMax), UV.bottomRight),
Vertex(Double3(xMax, y, zMin), UV.topRight)
)
}
fun faceQuad(face: ForgeDirection): Quad {
val base = face.vec * 0.5
val top = faceCorners[face.ordinal].topLeft.first.vec * 0.5
val left = faceCorners[face.ordinal].topLeft.second.vec * 0.5
return Quad(
Vertex(base + top + left, UV.topLeft),
Vertex(base - top + left, UV.bottomLeft),
Vertex(base - top - left, UV.bottomRight),
Vertex(base + top - left, UV.topRight)
)
}
}
val fullCube = Model().apply {
forgeDirs.forEach {
faceQuad(it)
.setAoShader(faceOrientedAuto(corner = cornerAo(it.axis), edge = null))
.setFlatShader(faceOrientedAuto(corner = cornerFlat, edge = null))
.add()
}
}

View File

@@ -0,0 +1,138 @@
package mods.octarinecore.client.render
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.Tessellator
import net.minecraft.util.IIcon
import net.minecraftforge.common.util.ForgeDirection
import net.minecraftforge.common.util.ForgeDirection.*
class ModelRenderer() : ShadingContext() {
/** Holds final vertex data before it goes to the [Tessellator]. */
val temp = RenderVertex()
/**
* Render a [Model].
* The [blockContext] and [renderBlocks] need to be set up correctly, including first rendering the
* corresponding block to capture shading values!
*
* @param[model] model to render
* @param[rot] rotation to apply to the model
* @param[trans] translation to apply to the model
* @param[forceFlat] force flat shading even if AO is enabled
* @param[icon] lambda to resolve the texture to use for each quad
* @param[rotateUV] lambda to get amount of UV rotation for each quad
* @param[postProcess] lambda to perform arbitrary modifications on the [RenderVertex] just before it goes to the [Tessellator]
*/
inline fun render(
model: Model,
rot: Rotation,
trans: Double3 = blockContext.blockCenter,
forceFlat: Boolean = false,
icon: (ShadingContext, Int, Quad) -> IIcon,
rotateUV: (Quad) -> Int,
postProcess: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex) -> Unit
) {
rotation = rot
aoEnabled = Minecraft.isAmbientOcclusionEnabled()
model.quads.forEachIndexed { quadIdx, quad ->
val drawIcon = icon(this, quadIdx, quad)
val uvRot = rotateUV(quad)
quad.verts.forEachIndexed { vertIdx, vert ->
temp.init(vert)
temp.rotate(rotation).translate(trans).rotateUV(uvRot).setIcon(drawIcon)
val shader = if (aoEnabled && !forceFlat) vert.aoShader else vert.flatShader
shader.shade(this, temp)
temp.postProcess(this, quadIdx, quad, vertIdx, vert)
Tessellator.instance.apply {
setBrightness(temp.brightness)
setColorOpaque_F(temp.red, temp.green, temp.blue)
addVertexWithUV(temp.x, temp.y, temp.z, temp.u, temp.v)
}
}
}
}
}
/**
* Queried by [Shader] objects to get rendering-relevant data of the current block in a rotated frame of reference.
*/
open class ShadingContext {
var rotation = Rotation.identity
var aoEnabled = Minecraft.isAmbientOcclusionEnabled()
fun aoShading(face: ForgeDirection, corner1: ForgeDirection, corner2: ForgeDirection) =
renderBlocks.capture.aoShading(face.rotate(rotation), corner1.rotate(rotation), corner2.rotate(rotation))
fun blockColor(offset: Int3) = blockContext.blockColor(offset.rotate(rotation))
fun blockBrightness(offset: Int3) = blockContext.blockBrightness(offset.rotate(rotation))
fun icon(face: ForgeDirection) = blockContext.icon(face.rotate(rotation))
}
/**
*
*/
@Suppress("NOTHING_TO_INLINE")
class RenderVertex() {
var x: Double = 0.0
var y: Double = 0.0
var z: Double = 0.0
var u: Double = 0.0
var v: Double = 0.0
var brightness: Int = 0
var red: Float = 0.0f
var green: Float = 0.0f
var blue: Float = 0.0f
fun init(vertex: Vertex, rot: Rotation, trans: Double3): RenderVertex {
val result = vertex.xyz.rotate(rot) + trans
x = result.x; y = result.y; z = result.z
return this
}
fun init(vertex: Vertex): RenderVertex {
x = vertex.xyz.x; y = vertex.xyz.y; z = vertex.xyz.z;
u = vertex.uv.u; v = vertex.uv.v
return this
}
fun translate(trans: Double3): RenderVertex { x += trans.x; y += trans.y; z += trans.z; return this }
fun rotate(rot: Rotation): RenderVertex {
if (rot === Rotation.identity) return this
val rotX = rot.rotatedComponent(EAST, x, y, z)
val rotY = rot.rotatedComponent(UP, x, y, z)
val rotZ = rot.rotatedComponent(SOUTH, x, y, z)
x = rotX; y = rotY; z = rotZ
return this
}
inline fun rotateUV(n: Int): RenderVertex {
when (n % 4) {
1 -> { val t = v; v = -u; u = t; return this }
2 -> { u = -u; v = -v; return this }
3 -> { val t = -v; v = u; u = t; return this }
else -> { return this }
}
}
inline fun setIcon(icon: IIcon): RenderVertex {
u = (icon.maxU - icon.minU) * (u + 0.5) + icon.minU
v = (icon.maxV - icon.minV) * (v + 0.5) + icon.minV
return this
}
inline fun setGrey(level: Float) {
val grey = Math.min((red + green + blue) * 0.333f * level, 1.0f)
red = grey; green = grey; blue = grey
}
inline fun multiplyColor(color: Int) {
red *= (color shr 16 and 255) / 256.0f
green *= (color shr 8 and 255) / 256.0f
blue *= (color and 255) / 256.0f
}
inline fun setColor(color: Int) {
red = (color shr 16 and 255) / 256.0f
green = (color shr 8 and 255) / 256.0f
blue = (color and 255) / 256.0f
}
}
/** Perform no post-processing */
val noPost: RenderVertex.(ShadingContext, Int, Quad, Int, Vertex) -> Unit = { ctx, qi, q, vi, v -> }

View File

@@ -0,0 +1,68 @@
package mods.octarinecore.client.render
import mods.octarinecore.minmax
import net.minecraft.world.IBlockAccess
import net.minecraftforge.common.util.ForgeDirection
/**
* Delegating [IBlockAccess] that fakes a _modified_ location to return values from a _target_ location.
* All other locations are handled normally.
*
* @param[original] the [IBlockAccess] that is delegated to
* @param[xModded] x coordinate of the _modified_ location
* @param[yModded] y coordinate of the _modified_ location
* @param[zModded] z coordinate of the _modified_ location
* @param[xTarget] x coordinate of the _target_ location
* @param[yTarget] y coordinate of the _target_ location
* @param[zTarget] z coordinate of the _target_ location
*/
class OffsetBlockAccess(val original: IBlockAccess,
@JvmField val xModded: Int, @JvmField val yModded: Int, @JvmField val zModded: Int,
@JvmField val xTarget: Int, @JvmField val yTarget: Int, @JvmField val zTarget: Int) : IBlockAccess {
inline fun <reified T> withOffset(x: Int, y: Int, z: Int, func: (Int,Int,Int)->T): T {
if (x == xModded && y == yModded && z == zModded) {
return func(xTarget, yTarget, zTarget)
} else {
return func(x, y, z)
}
}
override fun getBlock(x: Int, y: Int, z: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.getBlock(xAct, yAct, zAct) }
override fun getBlockMetadata(x: Int, y: Int, z: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.getBlockMetadata(xAct, yAct, zAct) }
override fun getTileEntity(x: Int, y: Int, z: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.getTileEntity(xAct, yAct, zAct) }
override fun isSideSolid(x: Int, y: Int, z: Int, side: ForgeDirection?, _default: Boolean) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.isSideSolid(xAct, yAct, zAct, side, _default) }
override fun isAirBlock(x: Int, y: Int, z: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.isAirBlock(xAct, yAct, zAct) }
override fun getLightBrightnessForSkyBlocks(x: Int, y: Int, z: Int, side: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.getLightBrightnessForSkyBlocks(xAct, yAct, zAct, side) }
override fun isBlockProvidingPowerTo(x: Int, y: Int, z: Int, side: Int) = withOffset(x, y, z)
{ xAct, yAct, zAct -> original.isBlockProvidingPowerTo(xAct, yAct, zAct, side) }
override fun getBiomeGenForCoords(x: Int, z: Int) = withOffset(x, 0, z)
{ xAct, yAct, zAct -> original.getBiomeGenForCoords(xAct, zAct) }
override fun getHeight() = original.height
override fun extendedLevelsInChunkCache() = original.extendedLevelsInChunkCache()
}
/**
* Temporarily replaces the [IBlockAccess] used by this [BlockContext] and the corresponding [ExtendedRenderBlocks]
* to use an [OffsetBlockAccess] while executing this lambda.
*
* @param[modded] the _modified_ location
* @param[target] the _target_ location
* @param[func] the lambda to execute
*/
inline fun <reified T> BlockContext.withOffset(modded: Int3, target: Int3, func: () -> T): T {
val original = world!!
world = OffsetBlockAccess(original, x + modded.x, y + modded.y, z + modded.z, x + target.x, y + target.y, z + target.z)
renderBlocks.blockAccess = world
val result = func()
world = original
renderBlocks.blockAccess = original
return result
}

View File

@@ -0,0 +1,56 @@
@file:JvmName("PixelFormat")
package mods.octarinecore.client.render
import java.awt.Color
/** List of bit-shift offsets in packed brightness values where meaningful (4-bit) data is contained. */
var brightnessComponents = listOf(20, 4)
/** Multiply the components of this packed brightness value with the given [Float]. */
infix fun Int.brMul(f: Float): Int {
val weight = (f * 256.0f).toInt()
var result = 0
brightnessComponents.forEach { shift ->
val raw = (this shr shift) and 15
val weighted = (raw) * weight / 256
result = result or (weighted shl shift)
}
return result
}
/** Multiply the components of this packed color value with the given [Float]. */
infix fun Int.colorMul(f: Float): Int {
val weight = (f * 256.0f).toInt()
val red = (this shr 16 and 255) * weight / 256
val green = (this shr 8 and 255) * weight / 256
val blue = (this and 255) * weight / 256
return (red shl 16) or (green shl 8) or blue
}
/** Sum the components of all packed brightness values given. */
fun brSum(multiplier: Float?, vararg brightness: Int): Int {
val sum = Array(brightnessComponents.size) { 0 }
brightnessComponents.forEachIndexed { idx, shift -> brightness.forEach { br ->
val comp = (br shr shift) and 15
sum[idx] += comp
} }
var result = 0
brightnessComponents.forEachIndexed { idx, shift ->
val comp = if (multiplier == null)
((sum[idx]) shl shift)
else
((sum[idx].toFloat() * multiplier).toInt() shl shift)
result = result or comp
}
return result
}
data class HSB(var hue: Float, var saturation: Float, var brightness: Float) {
companion object {
fun fromColor(color: Int): HSB {
val hsbVals = Color.RGBtoHSB((color shr 16) and 255, (color shr 8) and 255, color and 255, null)
return HSB(hsbVals[0], hsbVals[1], hsbVals[2])
}
}
val asColor: Int get() = Color.HSBtoRGB(hue, saturation, brightness)
}

View File

@@ -0,0 +1,147 @@
package mods.octarinecore.client.render
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler
import cpw.mods.fml.client.registry.RenderingRegistry
import mods.octarinecore.metaprog.reflectField
import mods.octarinecore.metaprog.reflectStaticField
import net.minecraft.block.Block
import net.minecraft.client.renderer.RenderBlocks
import net.minecraft.util.IIcon
import net.minecraftforge.common.util.ForgeDirection
import net.minecraftforge.common.util.ForgeDirection.*
/** Reference to the handler list in Forge [RenderingRegistry]. */
val renderingHandlers: Map<Int, ISimpleBlockRenderingHandler> = RenderingRegistry::class.java
.reflectStaticField<RenderingRegistry>("INSTANCE")!!
.reflectField<Map<Int, ISimpleBlockRenderingHandler>>("blockRenderers")!!
/**
* Used instead of the vanilla [RenderBlocks] to get to the AO values and textures used in rendering
* without duplicating vanilla code.
*/
class ExtendedRenderBlocks : RenderBlocks() {
/** Captures the AO values and textures used in a specific rendering pass when rendering a block. */
val capture = ShadingCapture()
override fun renderFaceXPos(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(EAST, block, x, y, z, icon)
override fun renderFaceXNeg(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(WEST, block, x, y, z, icon)
override fun renderFaceYPos(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(UP, block, x, y, z, icon)
override fun renderFaceYNeg(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(DOWN, block, x, y, z, icon)
override fun renderFaceZPos(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(SOUTH, block, x, y, z, icon)
override fun renderFaceZNeg(block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) = renderFace(NORTH, block, x, y, z, icon)
/**
* Render a block face, saving relevant data if appropriate.
*/
@Suppress("NON_EXHAUSTIVE_WHEN")
fun renderFace(face: ForgeDirection, block: Block?, x: Double, y: Double, z: Double, icon: IIcon?) {
if (capture.isCorrectPass(face)) {
saveAllShading(face); capture.icons[face.ordinal] = icon
}
if (capture.renderCallback(capture, face, capture.passes[face.ordinal], icon)) when (face) {
EAST -> super.renderFaceXPos(block, x, y, z, icon)
WEST -> super.renderFaceXNeg(block, x, y, z, icon)
UP -> super.renderFaceYPos(block, x, y, z, icon)
DOWN -> super.renderFaceYNeg(block, x, y, z, icon)
SOUTH -> super.renderFaceZPos(block, x, y, z, icon)
NORTH -> super.renderFaceZNeg(block, x, y, z, icon)
}
}
fun saveTopLeft(face: ForgeDirection, corner: Pair<ForgeDirection, ForgeDirection>) =
capture.aoShading(face, corner.first, corner.second)
.set(brightnessTopLeft, colorRedTopLeft, colorGreenTopLeft, colorBlueTopLeft)
fun saveTopRight(face: ForgeDirection, corner: Pair<ForgeDirection, ForgeDirection>) =
capture.aoShading(face, corner.first, corner.second)
.set(brightnessTopRight, colorRedTopRight, colorGreenTopRight, colorBlueTopRight)
fun saveBottomLeft(face: ForgeDirection, corner: Pair<ForgeDirection, ForgeDirection>) =
capture.aoShading(face, corner.first, corner.second)
.set(brightnessBottomLeft, colorRedBottomLeft, colorGreenBottomLeft, colorBlueBottomLeft)
fun saveBottomRight(face: ForgeDirection, corner: Pair<ForgeDirection, ForgeDirection>) =
capture.aoShading(face, corner.first, corner.second)
.set(brightnessBottomRight, colorRedBottomRight, colorGreenBottomRight, colorBlueBottomRight)
fun saveAllShading(face: ForgeDirection) {
saveTopLeft(face, faceCorners[face.ordinal].topLeft)
saveTopRight(face, faceCorners[face.ordinal].topRight)
saveBottomLeft(face, faceCorners[face.ordinal].bottomLeft)
saveBottomRight(face, faceCorners[face.ordinal].bottomRight)
}
}
/**
* Captures the AO values and textures used in a specific rendering pass when rendering a block.
*/
class ShadingCapture {
/** Sparse array of stored AO data. */
val aoShadings = arrayOfNulls<AoData>(6 * 6 * 6)
/** List of stored AO data (only valid instances). */
var shadingsList = listOf<AoData>()
/** List of stored texture data. */
val icons = arrayOfNulls<IIcon>(6)
/** Number of passes to go on a given face. */
val passes = Array(6) { 0 }
/** lambda to determine which faces to render. */
var renderCallback = alwaysRender
init {
(0..5).forEach { i1 ->
(0..5).forEach { i2 ->
(i2..5).forEach { i3 ->
aoShadings[cornerId(i1, i2, i3)] = AoData()
}
}
}
shadingsList = aoShadings.filterNotNull()
}
/**
* Get the AO data of a specific corner.
*
* The two corner directions are interchangeable. All 3 parameters must lie on different axes.
*
* @param[face] block face
* @param[corner1] first direction of corner on face
* @param[corner2] second direction of corner on face
*/
fun aoShading(face: ForgeDirection, corner1: ForgeDirection, corner2: ForgeDirection) =
aoShadings[cornerId(face, corner1, corner2)]!!
/** Returns true if the AO and texture data should be saved. Mutates state. */
fun isCorrectPass(face: ForgeDirection) = (passes[face.ordinal]-- > 0)
/**
* Reset all data and pass counters.
*
* @param[targetPass] which render pass to save
*/
fun reset(targetPass: Int) {
shadingsList.forEach { it.reset() }
(0..5).forEach { idx -> icons[idx] = null; passes[idx] = targetPass }
}
/** One-dimensional index of a specific corner. */
protected fun cornerId(face: Int, corner1: Int, corner2: Int) = when (corner2 > corner1) {
true -> 36 * face + 6 * corner1 + corner2
false -> 36 * face + 6 * corner2 + corner1
}
/** One-dimensional index of a specific corner. */
protected fun cornerId(face: ForgeDirection, corner1: ForgeDirection, corner2: ForgeDirection) =
cornerId(face.ordinal, corner1.ordinal, corner2.ordinal)
}
/** Lambda to render all faces of a block */
val alwaysRender: (ShadingCapture, ForgeDirection, Int, IIcon?) -> Boolean = { ctx, face, pass, icon -> true }
/** Lambda to render no faces of a block */
val neverRender: (ShadingCapture, ForgeDirection, Int, IIcon?) -> Boolean = { ctx, face, pass, icon -> false }

View File

@@ -0,0 +1,148 @@
package mods.octarinecore.client.render
import net.minecraftforge.common.util.ForgeDirection
const val defaultCornerDimming = 0.5f
const val defaultEdgeDimming = 0.8f
// ================================
// Resolvers for automatic shading
// ================================
fun cornerAo(fallbackAxis: Axis): (ForgeDirection, ForgeDirection, ForgeDirection)->Shader = { face, dir1, dir2 ->
val fallbackDir = listOf(face, dir1, dir2).find { it.axis == fallbackAxis }!!
CornerSingleFallback(face, dir1, dir2, fallbackDir)
}
val cornerFlat = { face: ForgeDirection, dir1: ForgeDirection, dir2: ForgeDirection -> FaceFlat(face) }
fun cornerAoTri(func: (AoData, AoData)-> AoData) = { face: ForgeDirection, dir1: ForgeDirection, dir2: ForgeDirection ->
CornerTri(face, dir1, dir2, func)
}
val cornerAoMaxGreen = cornerAoTri { s1, s2 -> if (s1.green > s2.green) s1 else s2 }
fun cornerInterpolate(edgeAxis: Axis, weight: Float, dimming: Float): (ForgeDirection, ForgeDirection, ForgeDirection)->Shader = { dir1, dir2, dir3 ->
val edgeDir = listOf(dir1, dir2, dir3).find { it.axis == edgeAxis }!!
val faceDirs = listOf(dir1, dir2, dir3).filter { it.axis != edgeAxis }
CornerInterpolateDimming(faceDirs[0], faceDirs[1], edgeDir, weight, dimming)
}
// ================================
// Shaders
// ================================
object NoShader : Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) = vertex.shade(AoData.black)
override fun rotate(rot: Rotation) = this
}
class CornerSingleFallback(val face: ForgeDirection, val dir1: ForgeDirection, val dir2: ForgeDirection, val fallbackDir: ForgeDirection, val fallbackDimming: Float = defaultCornerDimming) : Shader {
val offset = Int3(fallbackDir)
override fun shade(context: ShadingContext, vertex: RenderVertex) {
val shading = context.aoShading(face, dir1, dir2)
if (shading.valid)
vertex.shade(shading)
else
vertex.shade(context.blockBrightness(offset) brMul fallbackDimming, context.blockColor(offset) colorMul fallbackDimming)
}
override fun rotate(rot: Rotation) = CornerSingleFallback(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), fallbackDir.rotate(rot), fallbackDimming)
}
inline fun accumulate(v1: AoData?, v2: AoData?, func: ((AoData, AoData)-> AoData)): AoData? {
val v1ok = v1 != null && v1.valid
val v2ok = v2 != null && v2.valid
if (v1ok && v2ok) return func(v1!!, v2!!)
if (v1ok) return v1
if (v2ok) return v2
return null
}
class CornerTri(val face: ForgeDirection, val dir1: ForgeDirection, val dir2: ForgeDirection,
val func: ((AoData, AoData)-> AoData)) : Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
var acc = accumulate(
context.aoShading(face, dir1, dir2),
context.aoShading(dir1, face, dir2),
func)
acc = accumulate(
acc,
context.aoShading(dir2, face, dir1),
func)
vertex.shade(acc ?: AoData.black)
}
override fun rotate(rot: Rotation) = CornerTri(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), func)
}
class EdgeInterpolateFallback(val face: ForgeDirection, val edgeDir: ForgeDirection, val pos: Double, val fallbackDimming: Float = defaultEdgeDimming): Shader {
val offset = Int3(edgeDir)
val edgeAxis = axes.find { it != face.axis && it != edgeDir.axis }!!
val weightN = (0.5 - pos).toFloat()
val weightP = (0.5 + pos).toFloat()
override fun shade(context: ShadingContext, vertex: RenderVertex) {
val shadingP = context.aoShading(face, edgeDir, (edgeAxis to Dir.P).face)
val shadingN = context.aoShading(face, edgeDir, (edgeAxis to Dir.N).face)
if (!shadingP.valid && !shadingN.valid)
return vertex.shade(context.blockBrightness(offset) brMul fallbackDimming, context.blockColor(offset) colorMul fallbackDimming)
if (!shadingP.valid) return vertex.shade(shadingN)
if (!shadingN.valid) return vertex.shade(shadingP)
vertex.shade(shadingP, shadingN, weightP, weightN)
}
override fun rotate(rot: Rotation) = EdgeInterpolateFallback(face.rotate(rot), edgeDir.rotate(rot), pos)
}
class CornerInterpolateDimming(val face1: ForgeDirection, val face2: ForgeDirection, val edgeDir: ForgeDirection,
val weight: Float, val dimming: Float, val fallbackDimming: Float = defaultCornerDimming) : Shader {
val offset = Int3(edgeDir)
override fun shade(context: ShadingContext, vertex: RenderVertex) {
var shading1 = context.aoShading(face1, edgeDir, face2)
var shading2 = context.aoShading(face2, edgeDir, face1)
var weight1 = weight
var weight2 = 1.0f - weight
if (!shading1.valid && !shading2.valid)
return vertex.shade(context.blockBrightness(offset) brMul fallbackDimming, context.blockColor(offset) colorMul fallbackDimming)
if (!shading1.valid) { shading1 = shading2; weight1 *= dimming }
if (!shading2.valid) { shading2 = shading1; weight2 *= dimming }
vertex.shade(shading1, shading2, weight1, weight2)
}
override fun rotate(rot: Rotation) =
CornerInterpolateDimming(face1.rotate(rot), face2.rotate(rot), edgeDir.rotate(rot), weight, dimming, fallbackDimming)
}
class FaceCenter(val face: ForgeDirection): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
vertex.red = 0.0f; vertex.green = 0.0f; vertex.blue = 0.0f;
val b = IntArray(4)
faceCorners[face.ordinal].asList.forEachIndexed { idx, corner ->
val shading = context.aoShading(face, corner.first, corner.second)
vertex.red += shading.red
vertex.green += shading.green
vertex.blue += shading.blue
b[idx] = shading.brightness
}
vertex.apply { red *= 0.25f; green *= 0.25f; blue *= 0.25f }
vertex.brightness = brSum(0.25f, *b)
}
override fun rotate(rot: Rotation) = FaceCenter(face.rotate(rot))
}
class FaceFlat(val face: ForgeDirection): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
val color = context.blockColor(Int3.zero)
vertex.shade(context.blockBrightness(face.offset), color)
}
override fun rotate(rot: Rotation): Shader = FaceFlat(face.rotate(rot))
}
class FlatOffset(val offset: Int3): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
vertex.brightness = context.blockBrightness(offset)
vertex.setColor(context.blockColor(offset))
}
override fun rotate(rot: Rotation): Shader = this
}
class FlatOffsetNoColor(val offset: Int3): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
vertex.brightness = context.blockBrightness(offset)
vertex.red = 1.0f; vertex.green = 1.0f; vertex.blue = 1.0f
}
override fun rotate(rot: Rotation): Shader = this
}

View File

@@ -0,0 +1,131 @@
package mods.octarinecore.client.render
import net.minecraftforge.common.util.ForgeDirection
import java.lang.Math.*
/** Holds shading values for block corners as calculated by vanilla Minecraft rendering. */
class AoData() {
var valid = false
var brightness = 0
var red: Float = 0.0f
var green: Float = 0.0f
var blue: Float = 0.0f
fun reset() { valid = false }
fun set(brightness: Int, red: Float, green: Float, blue: Float) {
if (valid) return
this.valid = true
this.brightness = brightness
this.red = red
this.green = green
this.blue = blue
}
companion object {
val black = AoData();
}
}
/**
* Instances of this interface are associated with [Model] vertices, and used to apply brightness and color
* values to a [RenderVertex].
*/
interface Shader {
/**
* Set shading values of a [RenderVertex]
*
* @param[context] context that can be queried for shading data in a [Model]-relative frame of reference
* @param[vertex] the [RenderVertex] to manipulate
*/
fun shade(context: ShadingContext, vertex: RenderVertex)
/**
* Return a new rotated version of this [Shader]. Used during [Model] setup when rotating the model itself.
*/
fun rotate(rot: Rotation): Shader
/** Set all shading values on the [RenderVertex] to match the given [AoData]. */
fun RenderVertex.shade(shading: AoData) {
brightness = shading.brightness; red = shading.red; green = shading.green; blue = shading.blue
}
/** Set the shading values on the [RenderVertex] to a weighted average of the two [AoData] instances. */
fun RenderVertex.shade(shading1: AoData, shading2: AoData, weight1: Float = 0.5f, weight2: Float = 0.5f) {
red = min(shading1.red * weight1 + shading2.red * weight2, 1.0f)
green = min(shading1.green * weight1 + shading2.green * weight2, 1.0f)
blue = min(shading1.blue * weight1 + shading2.blue * weight2, 1.0f)
brightness = brSum(null, shading1.brightness brMul weight1, shading2.brightness brMul weight2)
}
/**
* Set the shading values on the [RenderVertex] directly.
*
* @param[brightness] packed brightness value
* @param[color] packed color value
*/
fun RenderVertex.shade(brightness: Int, color: Int) {
this.brightness = brightness; setColor(color)
}
}
/**
* Returns a shader resolver for quads that point towards one of the 6 block faces.
* The resolver works the following way:
* - determines which face the _quad_ normal points towards (if not overridden)
* - determines the distance of the _vertex_ to the corners and edge midpoints on that block face
* - if _corner_ is given, and the _vertex_ is closest to a block corner, returns the [Shader] created by _corner_
* - if _edge_ is given, and the _vertex_ is closest to an edge midpoint, returns the [Shader] created by _edge_
*
* @param[overrideFace] assume the given face instead of going by the _quad_ normal
* @param[corner] shader instantiation lambda for corner vertices
* @param[edge] shader instantiation lambda for edge midpoint vertices
*/
fun faceOrientedAuto(overrideFace: ForgeDirection? = null,
corner: ((ForgeDirection, ForgeDirection, ForgeDirection)->Shader)? = null,
edge: ((ForgeDirection, ForgeDirection)->Shader)? = null) =
fun(quad: Quad, vertex: Vertex): Shader {
val quadFace = overrideFace ?: quad.normal.nearestCardinal
val nearestCorner = nearestPosition(vertex.xyz, faceCorners[quadFace.ordinal].asList) {
(quadFace.vec + it.first.vec + it.second.vec) * 0.5
}
val nearestEdge = nearestPosition(vertex.xyz, quadFace.perpendiculars) {
(quadFace.vec + it.vec) * 0.5
}
if (edge != null && (nearestEdge.second < nearestCorner.second || corner == null))
return edge(quadFace, nearestEdge.first)
else return corner!!(quadFace, nearestCorner.first.first, nearestCorner.first.second)
}
/**
* Returns a shader resolver for quads that point towards one of the 12 block edges.
* The resolver works the following way:
* - determines which edge the _quad_ normal points towards (if not overridden)
* - determines which face midpoint the _vertex_ is closest to, of the 2 block faces that share this edge
* - determines which block corner _of this face_ the _vertex_ is closest to
* - returns the [Shader] created by _corner_
*
* @param[overrideEdge] assume the given edge instead of going by the _quad_ normal
* @param[corner] shader instantiation lambda
*/
fun edgeOrientedAuto(overrideEdge: Pair<ForgeDirection, ForgeDirection>? = null,
corner: (ForgeDirection, ForgeDirection, ForgeDirection)->Shader) =
fun(quad: Quad, vertex: Vertex): Shader {
val edgeDir = overrideEdge ?: nearestAngle(quad.normal, boxEdges) { it.first.vec + it.second.vec }.first
val nearestFace = nearestPosition(vertex.xyz, edgeDir.toList()) { it.vec }.first
val nearestCorner = nearestPosition(vertex.xyz, faceCorners[nearestFace.ordinal].asList) {
(nearestFace.vec + it.first.vec + it.second.vec) * 0.5
}.first
return corner(nearestFace, nearestCorner.first, nearestCorner.second)
}
fun faceOrientedInterpolate(overrideFace: ForgeDirection? = null) =
fun(quad: Quad, vertex: Vertex): Shader {
val resolver = faceOrientedAuto(overrideFace, edge = { face, edgeDir ->
val axis = axes.find { it != face.axis && it != edgeDir.axis }!!
val vec = Double3((axis to Dir.P).face)
val pos = vertex.xyz.dot(vec)
EdgeInterpolateFallback(face, edgeDir, pos)
})
return resolver(quad, vertex)
}

View File

@@ -0,0 +1,33 @@
package mods.octarinecore.client.resource
import java.awt.image.BufferedImage
import java.lang.Math.*
class CenteringTextureGenerator(domain: String, val aspectWidth: Int, val aspectHeight: Int) : TextureGenerator(domain) {
override fun generate(params: ParameterList): BufferedImage? {
val target = targetResource(params)!!
val baseTexture = resourceManager[target.second]?.loadImage() ?: return null
val frameWidth = baseTexture.width
val frameHeight = baseTexture.width * aspectHeight / aspectWidth
val frames = baseTexture.height / frameHeight
val size = max(frameWidth, frameHeight)
val resultTexture = BufferedImage(size, size * frames, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = resultTexture.createGraphics()
// iterate all frames
for (frame in 0 .. frames - 1) {
val baseFrame = baseTexture.getSubimage(0, size * frame, frameWidth, frameHeight)
val resultFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR)
resultFrame.createGraphics().apply {
drawImage(baseFrame, (size - frameWidth) / 2, (size - frameHeight) / 2, null)
}
graphics.drawImage(resultFrame, 0, size * frame, null)
}
return resultTexture
}
}

View File

@@ -0,0 +1,126 @@
package mods.octarinecore.client.resource
import cpw.mods.fml.client.FMLClientHandler
import mods.betterfoliage.loader.Refs
import mods.octarinecore.metaprog.reflectField
import net.minecraft.client.resources.IResourcePack
import net.minecraft.client.resources.data.IMetadataSerializer
import net.minecraft.client.resources.data.PackMetadataSection
import net.minecraft.util.ChatComponentText
import net.minecraft.util.ResourceLocation
import java.io.InputStream
import java.util.*
/**
* [IResourcePack] containing generated resources. Adds itself to the default resource pack list
* of Minecraft, so it is invisible and always active.
*
* @param[name] Name of the resource pack
* @param[generators] List of resource generators
*/
class GeneratorPack(val name: String, vararg val generators: GeneratorBase) : IResourcePack {
init {
// add to the default resource packs
FMLClientHandler.instance().reflectField<MutableList<IResourcePack>>("resourcePackList")!!.add(this)
}
override fun getPackName() = name
override fun getPackImage() = null
override fun getResourceDomains() = HashSet(generators.map { it.domain })
override fun getPackMetadata(serializer: IMetadataSerializer?, type: String?) =
if (type == "pack") PackMetadataSection(ChatComponentText("Generated resources"), 1) else null
override fun resourceExists(location: ResourceLocation?): Boolean =
if (location == null) false
else generators.find {
it.domain == location.resourceDomain && it.resourceExists(location)
} != null
override fun getInputStream(location: ResourceLocation?): InputStream? =
if (location == null) null
else generators.filter {
it.domain == location.resourceDomain && it.resourceExists(location)
}.map { it.getInputStream(location) }
.filterNotNull().first()
operator fun get(location: ResourceLocation?) = getInputStream(location)
}
/**
* Abstract base class for resource generators
*
* @param[domain] Resource domain of generator
*/
abstract class GeneratorBase(val domain: String) {
/** @see [IResourcePack.resourceExists] */
abstract fun resourceExists(location: ResourceLocation?): Boolean
/** @see [IResourcePack.getInputStream] */
abstract fun getInputStream(location: ResourceLocation?): InputStream?
}
/**
* Collection of named [String]-valued key-value pairs, with an extra unnamed (keyless) value.
* Meant to be encoded as a pipe-delimited list, and used as a [ResourceLocation] path
* to parametrized generated resources.
*
* @param[params] key-value pairs
* @param[value] keyless extra value
*/
class ParameterList(val params: Map<String, String>, val value: String?) {
override fun toString() =
params.entries
.sortedBy { it.key }
.fold("") { result, entry -> result + "|${entry.key}=${entry.value}"} +
(value?.let { "|$it" } ?: "")
/** Return the value of the given parameter. */
operator fun get(key: String) = params[key]
/** Check if the given parameter exists in this list. */
operator fun contains(key: String) = key in params
/** Return a new [ParameterList] with the given key-value pair appended to it. */
operator fun plus(pair: Pair<String, String>) = ParameterList(params + pair, this.value)
companion object {
/**
* Recreate the parameter list from the encoded string, i.e. the opposite of [toString].
*
* Everything before the first pipe character is dropped, so the decoding works even if
* something is prepended to the list (like _textures/blocks/_)
*/
fun fromString(input: String): ParameterList {
val params = hashMapOf<String, String>()
var value: String? = null
val slices = input.dropWhile { it != '|'}.split('|')
slices.forEach {
if (it.contains('=')) {
val keyValue = it.split('=')
if (keyValue.size == 2) params.put(keyValue[0], keyValue[1])
} else value = it
}
return ParameterList(params, value)
}
}
}
/**
* [GeneratorBase] returning parametrized generated resources.
*
* @param[domain] Resource domain of generator.
*/
abstract class ParameterBasedGenerator(domain: String) : GeneratorBase(domain) {
/** @see [IResourcePack.resourceExists] */
abstract fun resourceExists(params: ParameterList): Boolean
/** @see [IResourcePack.getInputStream] */
abstract fun getInputStream(params: ParameterList): InputStream?
override fun resourceExists(location: ResourceLocation?) =
resourceExists(ParameterList.fromString(location?.resourcePath ?: ""))
override fun getInputStream(location: ResourceLocation?) =
getInputStream(ParameterList.fromString(location?.resourcePath ?: ""))
}

View File

@@ -0,0 +1,126 @@
package mods.octarinecore.client.resource
import cpw.mods.fml.client.event.ConfigChangedEvent
import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import mods.octarinecore.client.render.Double3
import mods.octarinecore.client.render.Int3
import mods.octarinecore.client.render.Model
import net.minecraft.client.renderer.texture.IIconRegister
import net.minecraft.util.IIcon
import net.minecraft.util.MathHelper
import net.minecraft.util.ResourceLocation
import net.minecraft.world.World
import net.minecraft.world.gen.NoiseGeneratorSimplex
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.event.world.WorldEvent
import java.util.*
// ============================
// Resource types
// ============================
interface IStitchListener { fun onStitch(atlas: IIconRegister) }
interface IConfigChangeListener { fun onConfigChange() }
interface IWorldLoadListener { fun onWorldLoad(world: World) }
/**
* Base class for declarative resource handling.
*
* Resources are automatically reloaded/recalculated when the appropriate events are fired.
*
* @param[modId] mod ID associated with this handler (used to filter config change events)
*/
open class ResourceHandler(val modId: String) {
val resources = linkedListOf<Any>()
open fun afterStitch() {}
// ============================
// Self-registration
// ============================
init {
MinecraftForge.EVENT_BUS.register(this)
FMLCommonHandler.instance().bus().register(this)
}
// ============================
// Resource declarations
// ============================
fun iconStatic(domain: String, path: String) = IconHolder(domain, path).apply { resources.add(this) }
fun iconStatic(location: ResourceLocation) = iconStatic(location.resourceDomain, location.resourcePath)
fun iconSet(domain: String, pathPattern: String) = IconSet(domain, pathPattern).apply { resources.add(this) }
fun iconSet(location: ResourceLocation) = iconSet(location.resourceDomain, location.resourcePath)
fun model(init: Model.()->Unit) = ModelHolder(init).apply { resources.add(this) }
fun modelSet(num: Int, init: Model.(Int)->Unit) = ModelSet(num, init).apply { resources.add(this) }
fun vectorSet(num: Int, init: (Int)-> Double3) = VectorSet(num, init).apply { resources.add(this) }
fun simplexNoise() = SimplexNoise().apply { resources.add(this) }
// ============================
// Event registration
// ============================
@SubscribeEvent
fun onStitch(event: TextureStitchEvent.Pre) {
if (event.map.textureType == 0) {
resources.forEach { (it as? IStitchListener)?.onStitch(event.map) }
afterStitch()
}
}
@SubscribeEvent
fun handleConfigChange(event: ConfigChangedEvent.OnConfigChangedEvent) {
if (event.modID == modId) resources.forEach { (it as? IConfigChangeListener)?.onConfigChange() }
}
@SubscribeEvent
fun handleWorldLoad(event: WorldEvent.Load) =
resources.forEach { (it as? IWorldLoadListener)?.onWorldLoad(event.world) }
}
// ============================
// Resource container classes
// ============================
class IconHolder(val domain: String, val name: String) : IStitchListener {
var icon: IIcon? = null
override fun onStitch(atlas: IIconRegister) { icon = atlas.registerIcon("$domain:$name") }
}
class ModelHolder(val init: Model.()->Unit): IConfigChangeListener {
var model: Model = Model().apply(init)
override fun onConfigChange() { model = Model().apply(init) }
}
class IconSet(val domain: String, val namePattern: String) : IStitchListener {
val icons = arrayOfNulls<IIcon>(16)
var num = 0
override fun onStitch(atlas: IIconRegister) {
num = 0;
(0..15).forEach { idx ->
val locReal = ResourceLocation(domain, "textures/blocks/${namePattern.format(idx)}.png")
if (resourceManager[locReal] != null) icons[num++] = atlas.registerIcon("$domain:${namePattern.format(idx)}")
}
}
operator fun get(idx: Int) = if (num == 0) null else icons[idx % num]
}
class ModelSet(val num: Int, val init: Model.(Int)->Unit): IConfigChangeListener {
val models = Array(num) { Model().apply{ init(it) } }
override fun onConfigChange() { (0..num-1).forEach { models[it] = Model().apply{ init(it) } } }
operator fun get(idx: Int) = models[idx % num]
}
class VectorSet(val num: Int, val init: (Int)->Double3): IConfigChangeListener {
val models = Array(num) { init(it) }
override fun onConfigChange() { (0..num-1).forEach { models[it] = init(it) } }
operator fun get(idx: Int) = models[idx % num]
}
class SimplexNoise() : IWorldLoadListener {
var noise = NoiseGeneratorSimplex()
override fun onWorldLoad(world: World) { noise = NoiseGeneratorSimplex(Random(world.worldInfo.seed))
}
operator fun get(x: Int, z: Int) = MathHelper.floor_double((noise.func_151605_a(x.toDouble(), z.toDouble()) + 1.0) * 32.0)
operator fun get(pos: Int3) = get(pos.x, pos.z)
}

View File

@@ -0,0 +1,88 @@
package mods.octarinecore.client.resource
import mods.octarinecore.client.resource.ResourceType.*
import net.minecraft.client.resources.IResource
import net.minecraft.util.ResourceLocation
import java.awt.image.BufferedImage
import java.io.InputStream
/** Type of generated texture resource */
enum class ResourceType {
COLOR, // regular diffuse map
METADATA, // texture metadata
NORMAL, // ShadersMod normal map
SPECULAR // ShadersMod specular map
}
/**
* Generator returning textures based on a single other texture. This texture is located with the
* _dom_ and _path_ parameters of a [ParameterList].
*
* @param[domain] Resource domain of generator
*/
abstract class TextureGenerator(domain: String) : ParameterBasedGenerator(domain) {
/**
* Obtain a [ResourceLocation] to a generated texture
*
* @param[iconName] the name of the [TextureAtlasSprite] (not the full location) backing the generated texture
* @param[extraParams] additional parameters of the generated texture
*/
fun generatedResource(iconName: String, vararg extraParams: Pair<String, Any>) = ResourceLocation(
domain,
textureLocation(iconName).let {
ParameterList(
mapOf("dom" to it.resourceDomain, "path" to it.resourcePath) +
extraParams.map { Pair(it.first, it.second.toString()) },
"generate"
).toString()
}
)
/**
* Get the type and location of the texture resource encoded by the given [ParameterList].
*/
fun targetResource(params: ParameterList): Pair<ResourceType, ResourceLocation>? {
val baseTexture =
if (listOf("dom", "path").all { it in params }) ResourceLocation(params["dom"]!!, params["path"]!!)
else return null
return when(params.value?.toLowerCase()) {
"generate.png" -> COLOR to baseTexture + ".png"
"generate.png.mcmeta" -> METADATA to baseTexture + ".png.mcmeta"
"generate_n.png" -> NORMAL to baseTexture + "_n.png"
"generate_s.png" -> SPECULAR to baseTexture + "_s.png"
else -> null
}
}
override fun resourceExists(params: ParameterList) =
targetResource(params)?.second?.let { resourceManager[it] != null } ?: false
override fun getInputStream(params: ParameterList): InputStream? {
val target = targetResource(params)
return when(target?.first) {
null -> null
METADATA -> resourceManager[target!!.second]?.inputStream
else -> generate(params)?.asStream
}
}
/**
* Generate image data from the parameter list.
*/
abstract fun generate(params: ParameterList): BufferedImage?
/**
* Get a texture resource when multiple sizes may exist.
*
* @param[maxSize] Maximum size to consider. This value is progressively halved when searching for smaller versions.
* @param[maskPath] Location of the texture of the given size
*
*/
fun getMultisizeTexture(maxSize: Int, maskPath: (Int)->ResourceLocation): IResource? {
var size = maxSize
val sizes = linkedListOf<Int>()
while(size > 2) { sizes.add(size); size /= 2 }
return sizes.map { resourceManager[maskPath(it)] }.filterNotNull().firstOrNull()
}
}

View File

@@ -0,0 +1,90 @@
@file:JvmName("Utils")
package mods.octarinecore.client.resource
import mods.octarinecore.PI2
import mods.octarinecore.client.render.HSB
import mods.octarinecore.tryDefault
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.client.resources.IResource
import net.minecraft.client.resources.IResourceManager
import net.minecraft.client.resources.SimpleReloadableResourceManager
import net.minecraft.util.ResourceLocation
import java.awt.image.BufferedImage
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.lang.Math.*
import javax.imageio.ImageIO
/** Concise getter for the Minecraft resource manager. */
val resourceManager: SimpleReloadableResourceManager get() = Minecraft.getMinecraft().resourceManager as SimpleReloadableResourceManager
/** Append a string to the [ResourceLocation]'s path. */
operator fun ResourceLocation.plus(str: String) = ResourceLocation(resourceDomain, resourcePath + str)
/** Index operator to get a resource. */
operator fun IResourceManager.get(domain: String, path: String): IResource? = get(ResourceLocation(domain, path))
/** Index operator to get a resource. */
operator fun IResourceManager.get(location: ResourceLocation): IResource? = tryDefault(null) { getResource(location) }
/** Load an image resource. */
fun IResource.loadImage() = ImageIO.read(this.inputStream)
/** Get the lines of a text resource. */
fun IResource.getLines(): List<String> {
val result = arrayListOf<String>()
inputStream.bufferedReader().useLines { it.forEach { result.add(it) } }
return result
}
/** Index operator to get the RGB value of a pixel. */
operator fun BufferedImage.get(x: Int, y: Int) = this.getRGB(x, y)
/** Index operator to set the RGB value of a pixel. */
operator fun BufferedImage.set(x: Int, y: Int, value: Int) = this.setRGB(x, y, value)
/** Get an [InputStream] to an image object in PNG format. */
val BufferedImage.asStream: InputStream get() =
ByteArrayInputStream(ByteArrayOutputStream().let { ImageIO.write(this, "PNG", it); it.toByteArray() })
/**
* Calculate the average color of a texture.
*
* Only non-transparent pixels are considered. Averages are taken in the HSB color space (note: Hue is a circular average),
* and the result transformed back to the RGB color space.
*/
val TextureAtlasSprite.averageColor: Int? get() {
val locationNoDirs = ResourceLocation(iconName)
val locationWithDirs = ResourceLocation(locationNoDirs.resourceDomain, "textures/blocks/%s.png".format(locationNoDirs.resourcePath))
val image = resourceManager[locationWithDirs]?.loadImage() ?: return null
var numOpaque = 0
var sumHueX = 0.0
var sumHueY = 0.0
var sumSaturation = 0.0f
var sumBrightness = 0.0f
for (x in 0..image.width - 1)
for (y in 0..image.height - 1) {
val pixel = image[x, y]
val alpha = (pixel shr 24) and 255
val hsb = HSB.fromColor(pixel)
if (alpha == 255) {
numOpaque++
sumHueX += cos((hsb.hue.toDouble() - 0.5) * PI2)
sumHueY += sin((hsb.hue.toDouble() - 0.5) * PI2)
sumSaturation += hsb.saturation
sumBrightness += hsb.brightness
}
}
// circular average - transform sum vector to polar angle
val avgHue = (atan2(sumHueY.toDouble(), sumHueX.toDouble()) / PI2 + 0.5).toFloat()
return HSB(avgHue, sumSaturation / numOpaque.toFloat(), sumBrightness / numOpaque.toFloat()).asColor
}
/**
* Get the actual location of a texture from the name of its [TextureAtlasSprite].
*/
fun textureLocation(iconName: String) = ResourceLocation(iconName).let {
ResourceLocation(it.resourceDomain, "textures/blocks/${it.resourcePath}")
}

View File

@@ -0,0 +1,232 @@
package mods.octarinecore.config
import com.google.common.collect.LinkedListMultimap
import cpw.mods.fml.client.config.GuiConfigEntries
import cpw.mods.fml.client.config.IConfigElement
import cpw.mods.fml.client.event.ConfigChangedEvent
import cpw.mods.fml.common.FMLCommonHandler
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import mods.octarinecore.metaprog.reflectField
import mods.octarinecore.metaprog.reflectFieldsOfType
import mods.octarinecore.metaprog.reflectNestedObjects
import net.minecraftforge.common.config.ConfigElement
import net.minecraftforge.common.config.Configuration
import net.minecraftforge.common.config.Property
import kotlin.reflect.KProperty
// ============================
// Configuration object base
// ============================
/**
* Base class for declarative configuration handling.
*
* Subclasses should be singleton objects, containing one layer of further singleton objects representing
* config categories (nesting is not supported).
*
* Both the root object (maps to the category _global_) and category objects can contain [ConfigPropertyBase]
* instances (either directly or as a delegate), which handle the Forge [Configuration] itself.
*
* Config properties map to language keys by their field names.
*
* @param[modId] mod ID this configuration is linked to
* @param[langPrefix] prefix to use for language keys
*/
abstract class DelegatingConfig(val modId: String, val langPrefix: String) {
init { FMLCommonHandler.instance().bus().register(this) }
/** The [Configuration] backing this config object. */
var config: Configuration? = null
val rootGuiElements = linkedListOf<IConfigElement<*>>()
/** Attach this config object to the given [Configuration] and update all properties. */
fun attach(config: Configuration) {
this.config = config
val subProperties = LinkedListMultimap.create<String, String>()
rootGuiElements.clear()
forEachProperty { category, name, property ->
property.lang = property.lang ?: "$category.$name"
property.attach(config, langPrefix, category, name)
property.guiProperties.forEach { guiProperty ->
property.guiClass?.let { guiProperty.setConfigEntryClass(it) }
if (category == "global") rootGuiElements.add(ConfigElement.getTypedElement(guiProperty))
else subProperties.put(category, guiProperty.name)
}
}
for (category in subProperties.keySet()) {
val configCategory = config.getCategory(category)
configCategory.setLanguageKey("$langPrefix.$category")
configCategory.setPropertyOrder(subProperties[category])
rootGuiElements.add(ConfigElement<String>(configCategory))
}
save()
}
/**
* Execute the given lambda for all config properties.
* Lambda params: (category name, property name, property instance)
*/
inline fun forEachProperty(init: (String, String, ConfigPropertyBase)->Unit) {
reflectFieldsOfType(ConfigPropertyBase::class.java).forEach { property ->
init("global", property.first.split("$")[0], property.second as ConfigPropertyBase)
}
for (category in reflectNestedObjects) {
category.second.reflectFieldsOfType(ConfigPropertyBase::class.java).forEach { property ->
init(category.first, property.first.split("$")[0], property.second as ConfigPropertyBase)
}
}
}
/** Save changes to the [Configuration]. */
fun save() { if (config?.hasChanged() ?: false) config!!.save() }
/**
* Returns true if any of the given configuration elements have changed.
* Supports both categories and
*/
fun hasChanged(vararg elements: Any?): Boolean {
reflectNestedObjects.forEach { category ->
if (category.second in elements && config?.getCategory(category.first)?.hasChanged() ?: false) return true
}
forEachProperty { category, name, property ->
if (property in elements && property.hasChanged) return true
}
return false
}
/** Called when the configuration for the mod changes. */
open fun onChange(event: ConfigChangedEvent.OnConfigChangedEvent) {
save()
forEachProperty { c, n, prop -> prop.read() }
}
@SubscribeEvent
fun handleConfigChange(event: ConfigChangedEvent.OnConfigChangedEvent) { if (event.modID == modId) onChange(event) }
/** Extension to get the underlying delegate of a field */
operator fun Any.get(name: String) = this.reflectField<ConfigPropertyBase>("$name\$delegate")
}
// ============================
// Property delegates
// ============================
/** Base class for config property delegates. */
abstract class ConfigPropertyBase {
/** Language key of the property. */
var lang: String? = null
/** GUI class to use. */
var guiClass: Class<out GuiConfigEntries.IConfigEntry<*>>? = null
/** @return true if the property has changed. */
abstract val hasChanged: Boolean
/** Attach this delegate to a Forge [Configuration]. */
abstract fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String)
/** List of [Property] instances backing this delegate. */
abstract val guiProperties: List<Property>
/** Re-read the property value from the [Configuration]. */
open fun read() {}
}
/** Delegate for a property backed by a single [Property] instance. */
abstract class ConfigPropertyDelegate<T>() : ConfigPropertyBase() {
/** Cached value of the property. */
var cached: T? = null
/** The [Property] backing this delegate. */
var property: Property? = null
override val guiProperties: List<Property> get() = listOf(property!!)
override val hasChanged: Boolean get() = property?.hasChanged() ?: false
/** Chained setter for the language key. */
fun lang(lang: String) = apply { this.lang = lang }
/** Read the backing [Property] instance. */
abstract fun Property.read(): T
/** Write the backing [Property] instance. */
abstract fun Property.write(value: T)
/** Get the backing [Property] instance. */
abstract fun resolve(target: Configuration, category: String, name: String): Property
/** Kotlin deleagation implementation. */
operator fun getValue(thisRef: Any, delegator: KProperty<*>): T {
if (cached != null) return cached!!
cached = property!!.read()
return cached!!
}
/** Kotlin deleagation implementation. */
operator fun setValue(thisRef: Any, delegator: KProperty<*>, value: T) {
cached = value
property!!.write(value)
}
override fun read() { cached = null }
override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) {
cached = null
property = resolve(target, categoryName, propertyName)
property!!.setLanguageKey("$langPrefix.$lang")
}
}
/** [Double]-typed property delegate. */
class ConfigPropertyDouble(val min: Double, val max: Double, val default: Double) :
ConfigPropertyDelegate<Double>() {
override fun resolve(target: Configuration, category: String, name: String) =
target.get(category, name, default, null).apply { setMinValue(min); setMaxValue(max) }
override fun Property.read() = property!!.double
override fun Property.write(value: Double) = property!!.set(value)
}
/** [Float]-typed property delegate. */
class ConfigPropertyFloat(val min: Double, val max: Double, val default: Double) :
ConfigPropertyDelegate<Float>() {
override fun resolve(target: Configuration, category: String, name: String) =
target.get(category, name, default, null).apply { setMinValue(min); setMaxValue(max) }
override fun Property.read() = property!!.double.toFloat()
override fun Property.write(value: Float) = property!!.set(value.toDouble())
}
/** [Int]-typed property delegate. */
class ConfigPropertyInt(val min: Int, val max: Int, val default: Int) :
ConfigPropertyDelegate<Int>() {
override fun resolve(target: Configuration, category: String, name: String) =
target.get(category, name, default, null).apply { setMinValue(min); setMaxValue(max) }
override fun Property.read() = property!!.int
override fun Property.write(value: Int) = property!!.set(value)
}
/** [Boolean]-typed property delegate. */
class ConfigPropertyBoolean(val default: Boolean) :
ConfigPropertyDelegate<Boolean>() {
override fun resolve(target: Configuration, category: String, name: String) =
target.get(category, name, default, null)
override fun Property.read() = property!!.boolean
override fun Property.write(value: Boolean) = property!!.set(value)
}
/** [Int] array typed property delegate. */
class ConfigPropertyIntList(val defaults: ()->Array<Int>) :
ConfigPropertyDelegate<Array<Int>>() {
override fun resolve(target: Configuration, category: String, name: String) =
target.get(category, name, defaults().toIntArray(), null)
override fun Property.read() = property!!.intList.toTypedArray()
override fun Property.write(value: Array<Int>) = property!!.set(value.toIntArray())
}
// ============================
// Delegate factory methods
// ============================
fun double(min: Double = 0.0, max: Double = 1.0, default: Double) = ConfigPropertyDouble(min, max, default)
fun float(min: Double = 0.0, max: Double = 1.0, default: Double) = ConfigPropertyFloat(min, max, default)
fun int(min: Int = 0, max: Int, default: Int) = ConfigPropertyInt(min, max, default)
fun intList(defaults: ()->Array<Int>) = ConfigPropertyIntList(defaults)
fun boolean(default: Boolean) = ConfigPropertyBoolean(default)

View File

@@ -0,0 +1,166 @@
@file:JvmName("Reflection")
package mods.octarinecore.metaprog
import java.lang.reflect.Field
import java.lang.reflect.Method
import mods.octarinecore.metaprog.Namespace.*
import mods.octarinecore.tryDefault
/** Get a Java class with the given name. */
fun getJavaClass(name: String) = tryDefault(null) { Class.forName(name) }
/** Get the field with the given name and type using reflection. */
inline fun <reified T> Any.reflectField(field: String): T? =
tryDefault(null) { this.javaClass.getDeclaredField(field) }?.let {
it.isAccessible = true
it.get(this) as T
}
/** Get the static field with the given name and type using reflection. */
inline fun <reified T> Class<*>.reflectStaticField(field: String): T? =
tryDefault(null) { this.getDeclaredField(field) }?.let {
it.isAccessible = true
it.get(null) as T
}
/**
* Get all nested _object_s of this _object_ with reflection.
*
* @return [Pair]s of (name, instance)
*/
val Any.reflectNestedObjects: List<Pair<String, Any>> get() = this.javaClass.declaredClasses.map {
tryDefault(null) { it.name.split("$")[1] to it.getField("INSTANCE").get(null) }
}.filterNotNull()
/**
* Get all fields of this instance that match (or subclass) any of the given classes.
*
* @param[types] classes to look for
* @return [Pair]s of (field name, instance)
*/
fun Any.reflectFieldsOfType(vararg types: Class<*>) = this.javaClass.declaredFields
.filter { field -> types.any { it.isAssignableFrom(field.type) } }
.map { field -> field.name to field.let { it.isAccessible = true; it.get(this) } }
.filterNotNull()
enum class Namespace { OBF, SRG, MCP }
abstract class Resolvable<T> {
abstract fun resolve(): T?
val element: T? by lazy { resolve() }
}
/** Return true if all given elements are found. */
fun allAvailable(vararg codeElement: Resolvable<*>) = codeElement.all { it.element != null }
/**
* Reference to a class.
*
* @param[mcpName] MCP name of the class
* @param[obfName] obfuscated name of the class
*/
open class ClassRef(val mcpName: String, val obfName: String) : Resolvable<Class<*>>() {
constructor(mcpName: String) : this(mcpName, mcpName)
companion object {
val int = ClassRefPrimitive("I", Int::class.java)
val float = ClassRefPrimitive("F", Float::class.java)
val boolean = ClassRefPrimitive("Z", Boolean::class.java)
val void = ClassRefPrimitive("V", null)
}
fun name(namespace: Namespace) = if (namespace == Namespace.OBF) obfName else mcpName
open fun asmDescriptor(namespace: Namespace) = "L${name(namespace).replace(".", "/")};"
override fun resolve() = listOf(mcpName, obfName).map { getJavaClass(it) }.filterNotNull().firstOrNull()
}
/**
* Reference to a primitive type.
*
* @param[name] ASM descriptor of this primitive type
* @param[clazz] class of this primitive type
*/
class ClassRefPrimitive(name: String, val clazz: Class<*>?) : ClassRef(name) {
override fun asmDescriptor(namespace: Namespace) = mcpName
override fun resolve() = clazz
}
/**
* Reference to a method.
*
* @param[parentClass] reference to the class containing the method
* @param[mcpName] MCP name of the method
* @param[srgName] SRG name of the method
* @param[obfName] obfuscated name of the method
* @param[returnType] reference to the return type
* @param[returnType] references to the argument types
*/
class MethodRef(val parentClass: ClassRef,
val mcpName: String,
val srgName: String?,
val obfName: String?,
val returnType: ClassRef,
vararg argTypes: ClassRef
) : Resolvable<Method>() {
constructor(parentClass: ClassRef, mcpName: String, returnType: ClassRef, vararg argTypes: ClassRef) :
this(parentClass, mcpName, mcpName, mcpName, returnType, *argTypes)
val argTypes = argTypes
fun name(namespace: Namespace) = when(namespace) { OBF -> obfName!!; SRG -> srgName!!; MCP -> mcpName }
fun asmDescriptor(namespace: Namespace) = "(${argTypes.map { it.asmDescriptor(namespace) }.fold(""){ s1, s2 -> s1 + s2 } })${returnType.asmDescriptor(namespace)}"
override fun resolve(): Method? =
if (parentClass.element == null || argTypes.any { it.element == null }) null
else {
val args = argTypes.map { it.element!! }.toTypedArray()
listOf(srgName!!, mcpName).map { tryDefault(null) {
parentClass.element!!.getDeclaredMethod(it, *args)
}}.filterNotNull().firstOrNull()
?.apply { isAccessible = true }
}
/** Invoke this method using reflection. */
fun invoke(receiver: Any, vararg args: Any) = element?.invoke(receiver, *args)
/** Invoke this static method using reflection. */
fun invokeStatic(vararg args: Any) = element?.invoke(null, *args)
}
/**
* Reference to a field.
*
* @param[parentClass] reference to the class containing the field
* @param[mcpName] MCP name of the field
* @param[srgName] SRG name of the field
* @param[obfName] obfuscated name of the field
* @param[type] reference to the field type
*/
class FieldRef(val parentClass: ClassRef,
val mcpName: String,
val srgName: String?,
val obfName: String?,
val type: ClassRef?
) : Resolvable<Field>() {
constructor(parentClass: ClassRef, mcpName: String, type: ClassRef?) : this(parentClass, mcpName, mcpName, mcpName, type)
fun name(namespace: Namespace) = when(namespace) { OBF -> obfName!!; SRG -> srgName!!; MCP -> mcpName }
fun asmDescriptor(namespace: Namespace) = type!!.asmDescriptor(namespace)
override fun resolve(): Field? =
if (parentClass.element == null) null
else {
listOf(srgName!!, mcpName).map { tryDefault(null) {
parentClass.element!!.getDeclaredField(it)
}}.filterNotNull().firstOrNull()
?.apply{ isAccessible = true }
}
/** Get this field using reflection. */
fun get(receiver: Any?) = element?.get(receiver)
/** Get this static field using reflection. */
fun getStatic() = get(null)
}

View File

@@ -0,0 +1,199 @@
package mods.octarinecore.metaprog
import cpw.mods.fml.relauncher.IFMLLoadingPlugin
import net.minecraft.launchwrapper.IClassTransformer
import mods.octarinecore.metaprog.Namespace.*
import org.apache.logging.log4j.LogManager
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.*
@IFMLLoadingPlugin.TransformerExclusions(
"mods.octarinecore.metaprog",
"kotlin"
)
open class ASMPlugin(vararg val classes: Class<*>) : IFMLLoadingPlugin {
override fun getASMTransformerClass() = classes.map { it.canonicalName }.toTypedArray()
override fun getAccessTransformerClass() = null
override fun getModContainerClass() = null
override fun getSetupClass() = null
override fun injectData(data: Map<String, Any>) {}
}
/**
* Base class for convenient bytecode transformers.
*/
open class Transformer : IClassTransformer {
val log = LogManager.getLogger(this)
/** The type of environment we are in. Assume MCP until proven otherwise. */
var environment: Namespace = MCP
/** The list of transformers and targets. */
var transformers: MutableList<Pair<MethodRef, MethodTransformContext.()->Unit>> = arrayListOf()
/** Add a transformation to perform. Call this during instance initialization.
*
* @param[method] the target method of the transformation
* @param[trans] method transformation lambda
*/
fun transformMethod(method: MethodRef, trans: MethodTransformContext.()->Unit) = transformers.add(method to trans)
override fun transform(name: String?, transformedName: String?, classData: ByteArray?): ByteArray? {
if (classData == null) return null
if (name != transformedName) environment = OBF
val classNode = ClassNode().apply { val reader = ClassReader(classData); reader.accept(this, 0) }
var workDone = false
val transformations: List<Pair<MethodTransformContext.()->Unit, MethodNode?>> = transformers.map { transformer ->
if (transformedName != transformer.first.parentClass.mcpName) return@map transformer.second to null
log.debug("Found class: $name -> $transformedName")
log.debug(" searching: ${transformer.first.name(OBF)} ${transformer.first.asmDescriptor(OBF)} -> ${transformer.first.name(MCP)} ${transformer.first.asmDescriptor(MCP)}")
transformer.second to classNode.methods.find {
log.debug(" ${it.name} ${it.desc}")
it.name == transformer.first.name(MCP) && it.desc == transformer.first.asmDescriptor(MCP) ||
it.name == transformer.first.name(OBF) && it.desc == transformer.first.asmDescriptor(OBF)
}
}
transformations.filter { it.second != null }.forEach {
synchronized(it.second!!) {
try {
val trans = it.first
MethodTransformContext(it.second!!, environment).trans()
workDone = true
} catch (e: Throwable) {
log.warn("Error transforming method ${it.second!!.name} ${it.second!!.desc}")
}
}
}
return if (!workDone) classData else ClassWriter(0).apply { classNode.accept(this) }.toByteArray()
}
}
/**
* Allows builder-style declarative definition of transformations. Transformation lambdas are extension
* methods on this class.
*
* @param[method] the [MethodNode] currently being transformed
* @param[environment] the type of environment we are in
*/
class MethodTransformContext(val method: MethodNode, val environment: Namespace) {
/**
* Find the first instruction that matches a predicate.
*
* @param[start] the instruction node to start iterating from
* @param[predicate] the predicate to check
*/
fun find(start: AbstractInsnNode, predicate: (AbstractInsnNode) -> Boolean): AbstractInsnNode? {
var current: AbstractInsnNode? = start
while (current != null && !predicate(current)) current = current.next
return current
}
/** Find the first instruction in the current [MethodNode] that matches a predicate. */
fun find(predicate: (AbstractInsnNode)->Boolean): AbstractInsnNode? = find(method.instructions.first, predicate)
/** Find the first instruction in the current [MethodNode] with the given opcode. */
fun find(opcode: Int) = find { it.opcode == opcode }
/**
* Insert new instructions after this one.
*
* @param[init] builder-style lambda to assemble instruction list
*/
fun AbstractInsnNode.insertAfter(init: InstructionList.()->Unit) = InstructionList(environment).apply{
this.init(); list.reversed().forEach { method.instructions.insert(this@insertAfter, it) }
}
/**
* Insert new instructions before this one.
*
* @param[init] builder-style lambda to assemble instruction list
*/
fun AbstractInsnNode.insertBefore(init: InstructionList.()->Unit) = InstructionList(environment).apply{
this.init(); list.forEach { method.instructions.insertBefore(this@insertBefore, it) }
}
/** Remove all isntructiuons between the given two (inclusive). */
fun Pair<AbstractInsnNode, AbstractInsnNode>.remove() {
var current: AbstractInsnNode? = first
while (current != null && current != second) {
val next = current.next
method.instructions.remove(current)
current = next
}
if (current != null) method.instructions.remove(current)
}
/**
* Replace all isntructiuons between the given two (inclusive) with the specified instruction list.
*
* @param[init] builder-style lambda to assemble instruction list
*/
fun Pair<AbstractInsnNode, AbstractInsnNode>.replace(init: InstructionList.()->Unit) {
val beforeInsn = first.previous
remove()
beforeInsn.insertAfter(init)
}
/**
* Matches variable instructions.
*
* @param[opcode] instruction opcode
* @param[idx] variable the opcode references
*/
fun varinsn(opcode: Int, idx: Int): (AbstractInsnNode)->Boolean = { insn ->
insn.opcode == opcode && insn is VarInsnNode && insn.`var` == idx
}
}
/**
* Allows builder-style declarative definition of instruction lists.
*
* @param[environment] the type of environment we are in
*/
class InstructionList(val environment: Namespace) {
/** The instruction list being assembled. */
val list: MutableList<AbstractInsnNode> = arrayListOf()
/**
* Adds a variable instruction.
*
* @param[opcode] instruction opcode
* @param[idx] variable the opcode references
*/
fun varinsn(opcode: Int, idx: Int) = list.add(VarInsnNode(opcode, idx))
/**
* Adds an INVOKESTATIC instruction.
*
* @param[target] the target method of the instruction
* @param[isInterface] true if the target method is defined by an interface
*/
fun invokeStatic(target: MethodRef, isInterface: Boolean = false) = list.add(MethodInsnNode(
Opcodes.INVOKESTATIC,
target.parentClass.name(environment).replace(".", "/"),
target.name(environment),
target.asmDescriptor(environment),
isInterface
))
/**
* Adds a GETFIELD instruction.
*
* @param[target] the target field of the instruction
*/
fun getField(target: FieldRef) = list.add(FieldInsnNode(
Opcodes.GETFIELD,
target.parentClass.name(environment).replace(".", "/"),
target.name(environment),
target.asmDescriptor(environment)
))
}

View File

@@ -0,0 +1,5 @@
// Vanilla
net.minecraft.block.BlockCactus
// TerraFirmaCraft
com.bioxx.tfc.Blocks.Vanilla.BlockCustomCactus

View File

@@ -0,0 +1,35 @@
// Vanilla
net.minecraft.block.BlockTallGrass
net.minecraft.block.BlockCrops
net.minecraft.block.BlockReed
net.minecraft.block.BlockDoublePlant
-net.minecraft.block.BlockCarrot
-net.minecraft.block.BlockPotato
// Biomes O'Plenty
biomesoplenty.common.blocks.BlockBOPFlower
biomesoplenty.common.blocks.BlockBOPFlower2
// Tinkers' Construct
tconstruct.blocks.slime.SlimeTallGrass
// Plant Mega Pack
plantmegapack.block.PMPBlockBerrybush
plantmegapack.block.PMPBlockCrops
plantmegapack.block.PMPBlockDesert
plantmegapack.block.PMPBlockFern
plantmegapack.block.PMPBlockFlowerMulti
plantmegapack.block.PMPBlockFlowerSingle
plantmegapack.block.PMPBlockForest
plantmegapack.block.PMPBlockGrass
plantmegapack.block.PMPBlockJungle
plantmegapack.block.PMPBlockMountain
plantmegapack.block.PMPBlockSavanna
plantmegapack.block.PMPBlockShrub
plantmegapack.block.PMPBlockWetlands
// Pam's HarvestCraft
com.pam.harvestcraft.BlockPamCrop
com.pam.harvestcraft.BlockPamDesertGarden
com.pam.harvestcraft.BlockPamNormalGarden
com.pam.harvestcraft.BlockPamWaterGarden

View File

@@ -0,0 +1,14 @@
// Vanilla
net.minecraft.block.BlockDirt
// Biomes O'Plenty
biomesoplenty.common.blocks.BlockNewDirt
// Enhanced Biomes
enhancedbiomes.blocks.BlockSoilEB
// TerraFirmaCraft
com.bioxx.tfc.Blocks.Terrain.BlockDirt
// Aether
net.aetherteam.aether.blocks.natural.BlockAetherDirt

View File

@@ -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

View File

@@ -0,0 +1,8 @@
// Vanilla
net.minecraft.block.BlockLeavesBase
// Forestry
forestry.arboriculture.gadgets.BlockLeaves
// Thaumcraft
thaumcraft.common.blocks.BlockMagicalLeaves

View File

@@ -0,0 +1,5 @@
// Vanilla
net.minecraft.block.BlockLilyPad
// TerraFirmaCraft
com.bioxx.tfc.Blocks.Vanilla.BlockCustomLilyPad

View File

@@ -0,0 +1,23 @@
// Vanilla
net.minecraft.block.BlockLog
// Biomes O'Plenty
biomesoplenty.common.blocks.BlockBOPLog
// Natura
mods.natura.blocks.trees.DarkTreeBlock
mods.natura.blocks.trees.LogTwoxTwo
mods.natura.blocks.trees.SimpleLog
// Thaumcraft
thaumcraft.common.blocks.BlockMagicalLog
// Forestry
forestry.arboriculture.gadgets.BlockLog
// Extra Biomes XL
-extrabiomes.blocks.BlockMiniLog
// TerraFirmaCraft
com.bioxx.tfc.Blocks.Flora.BlockLogVert
com.bioxx.tfc.Blocks.Flora.BlockLogNatural

View File

@@ -0,0 +1,5 @@
// Vanilla
net.minecraft.block.BlockSand
// TerraFirmaCraft
com.bioxx.tfc.Blocks.Terrain.BlockSand

View File

@@ -0,0 +1,212 @@
key.betterfoliage.gui=Open Settings
betterfoliage.global.enabled=Enable Mod
betterfoliage.global.enabled.tooltip=If set to false, BetterFoliage will not render anything
betterfoliage.enabled=Enable
betterfoliage.enabled.tooltip=Is this feature enabled?
betterfoliage.hOffset=Horizontal offset
betterfoliage.hOffset.tooltip=The distance this element is shifted horizontally, in blocks
betterfoliage.vOffset=Vertical offset
betterfoliage.vOffset.tooltip=The distance this element is shifted vertically, in blocks
betterfoliage.size=Size
betterfoliage.size.tooltip=Size of this element
betterfoliage.heightMin=Minimum height
betterfoliage.heightMin.tooltip=Minimum height of element
betterfoliage.heightMax=Maximum height
betterfoliage.heightMax.tooltip=Maximum height of element
betterfoliage.population=Population
betterfoliage.population.tooltip=Chance (N in 64) that an eligible block will have this feature
betterfoliage.shaderWind=Shader wind effects
betterfoliage.shaderWind.tooltip=Apply wind effects from ShaderMod shaders to this element?
betterfoliage.distance=Distance limit
betterfoliage.distance.tooltip=Maximum distance from player at which to render this feature
betterfoliage.blocks=Block Types
betterfoliage.blocks.tooltip=Configure lists of block classes that will have specific features applied to them
betterfoliage.blocks.dirtWhitelist=Dirt Whitelist
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.leavesWhitelist=Leaves Whitelist
betterfoliage.blocks.leavesBlacklist=Leaves Blacklist
betterfoliage.blocks.leavesWhitelist.arrayEntry=%d entries
betterfoliage.blocks.leavesBlacklist.arrayEntry=%d entries
betterfoliage.blocks.cropsWhitelist=Crop Whitelist
betterfoliage.blocks.cropsBlacklist=Crop Blacklist
betterfoliage.blocks.cropsWhitelist.arrayEntry=%d entries
betterfoliage.blocks.cropsBlacklist.arrayEntry=%d entries
betterfoliage.blocks.logsWhitelist=Wood Log Whitelist
betterfoliage.blocks.logsBlacklist=Wood Log Blacklist
betterfoliage.blocks.logsWhitelist.arrayEntry=%d entries
betterfoliage.blocks.logsBlacklist.arrayEntry=%d entries
betterfoliage.blocks.sandWhitelist=Sand Whitelist
betterfoliage.blocks.sandBlacklist=Sand Blacklist
betterfoliage.blocks.sandWhitelist.arrayEntry=%d entries
betterfoliage.blocks.sandBlacklist.arrayEntry=%d entries
betterfoliage.blocks.lilypadWhitelist=Lilypad Whitelist
betterfoliage.blocks.lilypadBlacklist=Lilypad Blacklist
betterfoliage.blocks.lilypadWhitelist.arrayEntry=%d entries
betterfoliage.blocks.lilypadBlacklist.arrayEntry=%d entries
betterfoliage.blocks.cactusWhitelist=Cactus Whitelist
betterfoliage.blocks.cactusBlacklist=Cactus Blacklist
betterfoliage.blocks.cactusWhitelist.arrayEntry=%d entries
betterfoliage.blocks.cactusBlacklist.arrayEntry=%d entries
betterfoliage.blocks.dirtWhitelist.tooltip=Blocks recognized as Dirt. Has an impact on Reeds, Algae, Connected Grass
betterfoliage.blocks.dirtBlacklist.tooltip=Blocks never accepted as Dirt. Has an impact on Reeds, Algae, Connected Grass
betterfoliage.blocks.grassWhitelist.tooltip=Blocks recognized as Grass. Has an impact on Short Grass, Connected Grass
betterfoliage.blocks.grassBlacklist.tooltip=Blocks never accepted as Grass. Has an impact on Short Grass, Connected Grass
betterfoliage.blocks.leavesWhitelist.tooltip=Blocks recognized as Leaves. Has an impact on Extra Leaves, Falling Leaves. Leaves will render with leaves block ID in shader programs
betterfoliage.blocks.leavesBlacklist.tooltip=Blocks never accepted as Leaves. Has an impact on Extra Leaves, Falling Leaves. Leaves will render with leaves block ID in shader programs
betterfoliage.blocks.cropsWhitelist.tooltip=Blocks recognized as crops. Crops will render with tallgrass block ID in shader programs
betterfoliage.blocks.cropsBlacklist.tooltip=Blocks never accepted as crops. Crops will render with tallgrass block ID in shader programs
betterfoliage.blocks.logsWhitelist.tooltip=Blocks recognized as wooden logs. Has an impact on Rounded Logs
betterfoliage.blocks.logsBlacklist.tooltip=Blocks never accepted as wooden logs. Has an impact on Rounded Logs
betterfoliage.blocks.sandWhitelist.tooltip=Blocks recognized as Sand. Has an impact on Coral
betterfoliage.blocks.sandBlacklist.tooltip=Blocks never accepted Sand. Has an impact on Coral
betterfoliage.blocks.lilypadWhitelist.tooltip=Blocks recognized as Lilypad. Has an impact on Better Lilypad
betterfoliage.blocks.lilypadBlacklist.tooltip=Blocks never accepted Lilypad. Has an impact on Better Lilypad
betterfoliage.blocks.cactusWhitelist.tooltip=Blocks recognized as Cactus. Has an impact on Better Cactus
betterfoliage.blocks.cactusBlacklist.tooltip=Blocks never accepted Cactus. Has an impact on Better Cactus
betterfoliage.leaves=Extra Leaves
betterfoliage.leaves.tooltip=Extra round leaves on leaf blocks
betterfoliage.leaves.dense=Dense mode
betterfoliage.leaves.dense.tooltip=Dense mode has more round leaves
betterfoliage.shortGrass=Short Grass & Mycelium
betterfoliage.shortGrass.tooltip=Tufts of grass/mycelium on top of appropriate blocks
betterfoliage.shortGrass.useGenerated=Use generated texture for grass
betterfoliage.shortGrass.useGenerated.tooltip=Generated texture is made by slicing the tallgrass texture from the active resource pack in half
betterfoliage.shortGrass.myceliumEnabled=Enable Mycelium
betterfoliage.shortGrass.myceliumEnabled.tooltip=Is this feature enabled for mycelium blocks?
betterfoliage.shortGrass.grassEnabled=Enable Grass
betterfoliage.shortGrass.grassEnabled.tooltip=Is this feature enabled for grass blocks?
betterfoliage.shortGrass.snowEnabled=Enable under snow
betterfoliage.shortGrass.snowEnabled.tooltip=Enable on snowed grass blocks?
betterfoliage.shortGrass.saturationThreshold=Saturation threshold
betterfoliage.shortGrass.saturationThreshold.tooltip=Color saturation cutoff between "colorless" blocks (using biome color) and "colorful" blocks (using their own specific color)
betterfoliage.hangingGrass=Hanging Grass
betterfoliage.hangingGrass.tooltip=Grass tufts hanging down from the top edges of grass blocks
betterfoliage.hangingGrass.separation=Separation
betterfoliage.hangingGrass.separation.tooltip=How much the hanging grass stands out from the block
betterfoliage.cactus=Better Cactus
betterfoliage.cactus.tooltip=Enhance cactus with extra bits and smooth shading
betterfoliage.cactus.sizeVariation=Size variation
betterfoliage.cactus.sizeVariation.tooltip=Amount of random variation on cactus size
betterfoliage.lilypad=Better Lilypad
betterfoliage.lilypad.tooltip=Enhance lilypad with roots and occasional flowers
betterfoliage.lilypad.flowerChance=Flower chance
betterfoliage.lilypad.flowerChance.tooltip=Chance (N in 64) of a lilypad having a flower on it
betterfoliage.reed=Reeds
betterfoliage.reed.tooltip=Reeds on dirt blocks in shallow water
betterfoliage.reed.biomes=Biome List
betterfoliage.reed.biomes.tooltip=Configure which biomes reeds are allowed to appear in
betterfoliage.reed.biomes.tooltip.element=Should reeds appear in the %s biome?
betterfoliage.algae=Algae
betterfoliage.algae.tooltip=Algae on dirt blocks in deep water
betterfoliage.algae.biomes=Biome List
betterfoliage.algae.biomes.tooltip=Configure which biomes algae is allowed to appear in
betterfoliage.algae.biomes.tooltip.element=Should algae appear in the %s biome?
betterfoliage.coral=Coral
betterfoliage.coral.tooltip=Coral on sand blocks in deep water
betterfoliage.coral.size=Coral size
betterfoliage.coral.size.tooltip=Size of coral bits sticking out
betterfoliage.coral.crustSize=Crust size
betterfoliage.coral.crustSize.tooltip=Size of the flat coral part
betterfoliage.coral.chance=Coral chance
betterfoliage.coral.chance.tooltip=Chance (N in 64) of a specific face of the block to show coral
betterfoliage.coral.biomes=Biome List
betterfoliage.coral.biomes.tooltip=Configure which biomes coral is allowed to appear in
betterfoliage.coral.biomes.tooltip.element=Should coral appear in the %s biome?
betterfoliage.coral.shallowWater=Shallow water coral
betterfoliage.coral.shallowWater.tooltip=Should coral appear in 1 block deep water?
betterfoliage.netherrack=Netherrack Vines
betterfoliage.netherrack.tooltip=Hanging Vines under netherrack
betterfoliage.fallingLeaves=Falling leaves
betterfoliage.fallingLeaves.tooltip=Falling leaf particle FX emitted from the bottom of leaf blocks
betterfoliage.fallingLeaves.speed=Particle speed
betterfoliage.fallingLeaves.speed.tooltip=Overall particle speed
betterfoliage.fallingLeaves.windStrength=Wind strength
betterfoliage.fallingLeaves.windStrength.tooltip=Magnitude of wind effects in good weather (spread of normal distribution centered on 0)
betterfoliage.fallingLeaves.stormStrength=Storm strength
betterfoliage.fallingLeaves.stormStrength.tooltip=Additional magnitude of wind effects in rainy weather (spread of normal distribution centered on 0)
betterfoliage.fallingLeaves.size=Particle size
betterfoliage.fallingLeaves.chance=Particle chance
betterfoliage.fallingLeaves.chance.tooltip=Chance of each random render tick hitting a leaf block to spawn a particle
betterfoliage.fallingLeaves.perturb=Perturbation
betterfoliage.fallingLeaves.perturb.tooltip=Magnitude of perturbation effect. Adds a corkscrew-like motion to the particle synchronized to its rotation
betterfoliage.fallingLeaves.lifetime=Maximum lifetime
betterfoliage.fallingLeaves.lifetime.tooltip=Maximum lifetime of particle in seconds. Minimum lifetime is 60%% of this value
betterfoliage.fallingLeaves.opacityHack=Opaque particles
betterfoliage.fallingLeaves.opacityHack.tooltip=Stop transparent blocks obscuring particles even when particle is in front. WARNING: may cause glitches.
betterfoliage.risingSoul=Rising souls
betterfoliage.risingSoul.tooltip=Rising soul particle FX emitted from the top of soulsand blocks
betterfoliage.risingSoul.chance=Particle chance
betterfoliage.risingSoul.chance.tooltip=Chance of each random render tick hitting a soulsand block to spawn a particle
betterfoliage.risingSoul.speed=Particle speed
betterfoliage.risingSoul.speed.tooltip=Vertical speed of soul particles
betterfoliage.risingSoul.perturb=Perturbation
betterfoliage.risingSoul.perturb.tooltip=Magnitude of perturbation effect. Adds a corkscrew-like motion to the particle
betterfoliage.risingSoul.headSize=Soul size
betterfoliage.risingSoul.headSize.tooltip=Size of the soul particle
betterfoliage.risingSoul.trailSize=Trail size
betterfoliage.risingSoul.trailSize.tooltip=Initial size of the particle trail
betterfoliage.risingSoul.opacity=Opacity
betterfoliage.risingSoul.opacity.tooltip=Opacity of the particle effect
betterfoliage.risingSoul.sizeDecay=Size decay
betterfoliage.risingSoul.sizeDecay.tooltip=Trail particle size relative to its size in the previous tick
betterfoliage.risingSoul.opacityDecay=Opacity decay
betterfoliage.risingSoul.opacityDecay.tooltip=Trail particle opacity relative to its opacity in the previous tick
betterfoliage.risingSoul.lifetime=Maximum lifetime
betterfoliage.risingSoul.lifetime.tooltip=Maximum lifetime of particle effect in seconds. Minimum lifetime is 60%% of this value
betterfoliage.risingSoul.trailLength=Trail length
betterfoliage.risingSoul.trailLength.tooltip=Number of previous positions the particle remembers in ticks
betterfoliage.risingSoul.trailDensity=Trail density
betterfoliage.risingSoul.trailDensity.tooltip=Render every Nth previous position in the particle trail
betterfoliage.connectedGrass=Connected grass textures
betterfoliage.connectedGrass.enabled=Enable
betterfoliage.connectedGrass.enabled.tooltip=If there is a grass block on top of a dirt block: draw grass top texture on all grass block sides,
betterfoliage.roundLogs=Round Logs
betterfoliage.roundLogs.tooltip=Connect round blocks to solid full blocks?
betterfoliage.roundLogs.connectSolids=Connect to solid
betterfoliage.roundLogs.connectSolids.tooltip=Connect round blocks to solid full blocks?
betterfoliage.roundLogs.connectPerpendicular=Connect to perpendicular logs
betterfoliage.roundLogs.connectPerpendicular.tooltip=Connect round logs to perpendicular logs alons its axis?
betterfoliage.roundLogs.lenientConnect=Lenient rounding
betterfoliage.roundLogs.lenientConnect.tooltip=Connect parallel round logs in an L-shape too, not just 2x2
betterfoliage.roundLogs.connectGrass=Connect Grass
betterfoliage.roundLogs.connectGrass.tooltip=Render grass block under trees instead of dirt if there is grass nearby
betterfoliage.roundLogs.radiusSmall=Chamfer radius
betterfoliage.roundLogs.radiusSmall.tooltip=How much to chop off from the log corner
betterfoliage.roundLogs.radiusLarge=Connected chamfer radius
betterfoliage.roundLogs.radiusLarge.tooltip=How much to chop off from the outer corner of connected logs
betterfoliage.roundLogs.dimming=Dimming
betterfoliage.roundLogs.dimming.tooltip=Amount to darken onscured log faces
betterfoliage.roundLogs.zProtection=Z-Protection
betterfoliage.roundLogs.zProtection.tooltip=Amount to scale parallel log connection bits to stop Z-fighting (flickering). Try to set it as high as possible without having glitches.

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Some files were not shown because too many files have changed in this diff Show More