first Kotlin version
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
run/
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
68
build.gradle
Normal 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
@@ -0,0 +1,2 @@
|
|||||||
|
rootProject.name = 'BetterFoliage'
|
||||||
|
|
||||||
60
src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt
Normal 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
|
||||||
|
}
|
||||||
75
src/main/kotlin/mods/betterfoliage/client/Client.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
58
src/main/kotlin/mods/betterfoliage/client/Hooks.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
|
|
||||||
|
}
|
||||||
183
src/main/kotlin/mods/betterfoliage/client/config/Config.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
140
src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/main/kotlin/mods/betterfoliage/client/render/Utils.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt
Normal 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
|
||||||
|
}
|
||||||
106
src/main/kotlin/mods/betterfoliage/loader/BetterFoliageCore.kt
Normal 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!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/main/kotlin/mods/betterfoliage/loader/Refs.kt
Normal 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")
|
||||||
|
}
|
||||||
70
src/main/kotlin/mods/octarinecore/Utils.kt
Normal 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))
|
||||||
|
}
|
||||||
22
src/main/kotlin/mods/octarinecore/client/KeyHandler.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
src/main/kotlin/mods/octarinecore/client/gui/Utils.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
214
src/main/kotlin/mods/octarinecore/client/render/Geometry.kt
Normal 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)
|
||||||
|
}}
|
||||||
|
|
||||||
138
src/main/kotlin/mods/octarinecore/client/render/Model.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
138
src/main/kotlin/mods/octarinecore/client/render/ModelRenderer.kt
Normal 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 -> }
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
147
src/main/kotlin/mods/octarinecore/client/render/RenderBlocks.kt
Normal 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 }
|
||||||
|
|
||||||
148
src/main/kotlin/mods/octarinecore/client/render/Shaders.kt
Normal 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
|
||||||
|
}
|
||||||
131
src/main/kotlin/mods/octarinecore/client/render/Shading.kt
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 ?: ""))
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/main/kotlin/mods/octarinecore/client/resource/Utils.kt
Normal 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}")
|
||||||
|
}
|
||||||
232
src/main/kotlin/mods/octarinecore/config/DelegatingConfig.kt
Normal 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)
|
||||||
166
src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt
Normal 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)
|
||||||
|
}
|
||||||
199
src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt
Normal 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)
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Vanilla
|
||||||
|
net.minecraft.block.BlockCactus
|
||||||
|
|
||||||
|
// TerraFirmaCraft
|
||||||
|
com.bioxx.tfc.Blocks.Vanilla.BlockCustomCactus
|
||||||
35
src/main/resources/assets/betterfoliage/CropDefault.cfg
Normal 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
|
||||||
14
src/main/resources/assets/betterfoliage/DirtDefault.cfg
Normal 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
|
||||||
19
src/main/resources/assets/betterfoliage/GrassDefault.cfg
Normal 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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Vanilla
|
||||||
|
net.minecraft.block.BlockLeavesBase
|
||||||
|
|
||||||
|
// Forestry
|
||||||
|
forestry.arboriculture.gadgets.BlockLeaves
|
||||||
|
|
||||||
|
// Thaumcraft
|
||||||
|
thaumcraft.common.blocks.BlockMagicalLeaves
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Vanilla
|
||||||
|
net.minecraft.block.BlockLilyPad
|
||||||
|
|
||||||
|
// TerraFirmaCraft
|
||||||
|
com.bioxx.tfc.Blocks.Vanilla.BlockCustomLilyPad
|
||||||
23
src/main/resources/assets/betterfoliage/LogDefault.cfg
Normal 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
|
||||||
5
src/main/resources/assets/betterfoliage/SandDefault.cfg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Vanilla
|
||||||
|
net.minecraft.block.BlockSand
|
||||||
|
|
||||||
|
// TerraFirmaCraft
|
||||||
|
com.bioxx.tfc.Blocks.Terrain.BlockSand
|
||||||
212
src/main/resources/assets/betterfoliage/lang/en_US.lang
Normal 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.
|
||||||
|
After Width: | Height: | Size: 281 B |
|
After Width: | Height: | Size: 275 B |
|
After Width: | Height: | Size: 273 B |
|
After Width: | Height: | Size: 254 B |
|
After Width: | Height: | Size: 286 B |
|
After Width: | Height: | Size: 265 B |
|
After Width: | Height: | Size: 274 B |
|
After Width: | Height: | Size: 278 B |
|
After Width: | Height: | Size: 255 B |
|
After Width: | Height: | Size: 244 B |
|
After Width: | Height: | Size: 231 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 219 B |
|
After Width: | Height: | Size: 305 B |
|
After Width: | Height: | Size: 176 B |
|
After Width: | Height: | Size: 345 B |
|
After Width: | Height: | Size: 102 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |