diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc6f264 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +*.iml +*.ipr +*.iws +run/ +.gradle/ +build/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..35a385c --- /dev/null +++ b/build.gradle @@ -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() +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..75d2e73 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'BetterFoliage' + diff --git a/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt b/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt new file mode 100644 index 0000000..bf40168 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/BetterFoliageMod.kt @@ -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, side: Side) = true +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/Client.kt b/src/main/kotlin/mods/betterfoliage/client/Client.kt new file mode 100644 index 0000000..ad79fa4 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/Client.kt @@ -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) +} + diff --git a/src/main/kotlin/mods/betterfoliage/client/Hooks.kt b/src/main/kotlin/mods/betterfoliage/client/Hooks.kt new file mode 100644 index 0000000..a5ee1e7 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/Hooks.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/config/BlockMatcher.kt b/src/main/kotlin/mods/betterfoliage/client/config/BlockMatcher.kt new file mode 100644 index 0000000..73bd8bf --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/config/BlockMatcher.kt @@ -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>() + val whiteList = linkedListOf>() + val blockIDs = hashSetOf() + 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 get() = listOf(whitelistProperty!!, blacklistProperty!!) + + fun readDefaults(domain: String, path: String): Pair, Array> { + val blackList = arrayListOf() + val whiteList = arrayListOf() + 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) } + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/config/Config.kt b/src/main/kotlin/mods/betterfoliage/client/config/Config.kt new file mode 100644 index 0000000..79b15fe --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/config/Config.kt @@ -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() + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/gui/BiomeListConfigEntry.kt b/src/main/kotlin/mods/betterfoliage/client/gui/BiomeListConfigEntry.kt new file mode 100644 index 0000000..ef0a93e --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/gui/BiomeListConfigEntry.kt @@ -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(owningScreen, owningEntryList, configElement) { + + override val baseSet: List get() = BiomeGenBase.getBiomeGenArray().filterNotNull() + override val BiomeGenBase.itemId: Int get() = this.biomeID + override val BiomeGenBase.itemName: String get() = this.biomeName +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/gui/ConfigGuiFactory.kt b/src/main/kotlin/mods/betterfoliage/client/gui/ConfigGuiFactory.kt new file mode 100644 index 0000000..655b75f --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/gui/ConfigGuiFactory.kt @@ -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() + 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 + ) +} diff --git a/src/main/kotlin/mods/betterfoliage/client/integration/CLCIntegration.kt b/src/main/kotlin/mods/betterfoliage/client/integration/CLCIntegration.kt new file mode 100644 index 0000000..90febdb --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/integration/CLCIntegration.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/integration/ShadersModIntegration.kt b/src/main/kotlin/mods/betterfoliage/client/integration/ShadersModIntegration.kt new file mode 100644 index 0000000..86c3395 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/integration/ShadersModIntegration.kt @@ -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() + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/integration/TFCIntegration.kt b/src/main/kotlin/mods/betterfoliage/client/integration/TFCIntegration.kt new file mode 100644 index 0000000..eb6f189 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/integration/TFCIntegration.kt @@ -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) + } + } + } + } + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/render/AbstractRenderColumn.kt b/src/main/kotlin/mods/betterfoliage/client/render/AbstractRenderColumn.kt new file mode 100644 index 0000000..fba339d --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/AbstractRenderColumn.kt @@ -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.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.checkNeighbors(ctx: BlockContext, rotation: Rotation, logAxis: Axis, yOff: Int): Array { + 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 + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/EntityFallingLeavesFX.kt b/src/main/kotlin/mods/betterfoliage/client/render/EntityFallingLeavesFX.kt new file mode 100644 index 0000000..da37ac3 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/EntityFallingLeavesFX.kt @@ -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) } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/EntityRisingSoulFX.kt b/src/main/kotlin/mods/betterfoliage/client/render/EntityRisingSoulFX.kt new file mode 100644 index 0000000..624c924 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/EntityRisingSoulFX.kt @@ -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 = 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") + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt b/src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt new file mode 100644 index 0000000..3a32d0f --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/ModelColumn.kt @@ -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) + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderAlgae.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderAlgae.kt new file mode 100644 index 0000000..03f3392 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderAlgae.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt new file mode 100644 index 0000000..9fbddeb --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderCactus.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrass.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrass.kt new file mode 100644 index 0000000..f80eed6 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrass.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrassLog.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrassLog.kt new file mode 100644 index 0000000..208ae65 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderConnectedGrassLog.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderCoral.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderCoral.kt new file mode 100644 index 0000000..68383b1 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderCoral.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt new file mode 100644 index 0000000..f4282c0 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderGrass.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderLeaves.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderLeaves.kt new file mode 100644 index 0000000..9cfad37 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderLeaves.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderLilypad.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderLilypad.kt new file mode 100644 index 0000000..19cc6ac --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderLilypad.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderLog.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderLog.kt new file mode 100644 index 0000000..0a74550 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderLog.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderMycelium.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderMycelium.kt new file mode 100644 index 0000000..84cc053 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderMycelium.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderNetherrack.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderNetherrack.kt new file mode 100644 index 0000000..67eaa68 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderNetherrack.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/RenderReeds.kt b/src/main/kotlin/mods/betterfoliage/client/render/RenderReeds.kt new file mode 100644 index 0000000..13cf9a8 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/RenderReeds.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/render/Utils.kt b/src/main/kotlin/mods/betterfoliage/client/render/Utils.kt new file mode 100644 index 0000000..0982195 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/render/Utils.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/GrassGenerator.kt b/src/main/kotlin/mods/betterfoliage/client/texture/GrassGenerator.kt new file mode 100644 index 0000000..b0d6c6d --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/GrassGenerator.kt @@ -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 + } +} diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/GrassRegistry.kt b/src/main/kotlin/mods/betterfoliage/client/texture/GrassRegistry.kt new file mode 100644 index 0000000..3d72fc8 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/GrassRegistry.kt @@ -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 = 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)) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/LeafGenerator.kt b/src/main/kotlin/mods/betterfoliage/client/texture/LeafGenerator.kt new file mode 100644 index 0000000..1ef79dd --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/LeafGenerator.kt @@ -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") + } + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/LeafRegistry.kt b/src/main/kotlin/mods/betterfoliage/client/texture/LeafRegistry.kt new file mode 100644 index 0000000..3da9dab --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/LeafRegistry.kt @@ -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 = hashMapOf() + val particles: MutableMap = 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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/TextureMatcher.kt b/src/main/kotlin/mods/betterfoliage/client/texture/TextureMatcher.kt new file mode 100644 index 0000000..1791f99 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/TextureMatcher.kt @@ -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 = 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())) + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt b/src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt new file mode 100644 index 0000000..e714990 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/client/texture/Utils.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/loader/BetterFoliageCore.kt b/src/main/kotlin/mods/betterfoliage/loader/BetterFoliageCore.kt new file mode 100644 index 0000000..0e897d9 --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/loader/BetterFoliageCore.kt @@ -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!") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/betterfoliage/loader/Refs.kt b/src/main/kotlin/mods/betterfoliage/loader/Refs.kt new file mode 100644 index 0000000..6b91abd --- /dev/null +++ b/src/main/kotlin/mods/betterfoliage/loader/Refs.kt @@ -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") +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/Utils.kt b/src/main/kotlin/mods/octarinecore/Utils.kt new file mode 100644 index 0000000..c4fda09 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/Utils.kt @@ -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 MutableList.replace(transform: (T) -> T) = forEachIndexed { idx, value -> this[idx] = transform(value) } + +/** Exchange the two elements of the list with the given indices */ +inline fun MutableList.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 Iterable.cross(other: Iterable) = flatMap { a -> other.map { b -> a to b } } + +/** + * Property-level delegate backed by a [ThreadLocal]. + * + * @param[init] Lambda to get initial value + */ +class ThreadLocalDelegate(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 Iterable.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 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)) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/KeyHandler.kt b/src/main/kotlin/mods/octarinecore/client/KeyHandler.kt new file mode 100644 index 0000000..0c6748d --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/KeyHandler.kt @@ -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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/gui/IdListConfigEntry.kt b/src/main/kotlin/mods/octarinecore/client/gui/IdListConfigEntry.kt new file mode 100644 index 0000000..9f722ee --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/gui/IdListConfigEntry.kt @@ -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( + 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) } + + 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 + val ids = children.filter { it.booleanValue == true }.map { it.item.itemId } + configElement.set(ids.sorted().toTypedArray()) + return requiresRestart + } + + abstract val baseSet: List + 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(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 + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/gui/NonVerboseArrayEntry.kt b/src/main/kotlin/mods/octarinecore/client/gui/NonVerboseArrayEntry.kt new file mode 100644 index 0000000..89f97f3 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/gui/NonVerboseArrayEntry.kt @@ -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) + 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) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/gui/Utils.kt b/src/main/kotlin/mods/octarinecore/client/gui/Utils.kt new file mode 100644 index 0000000..e5c4ea5 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/gui/Utils.kt @@ -0,0 +1,13 @@ +@file:JvmName("Utils") +package mods.octarinecore.client.gui + +import net.minecraft.util.EnumChatFormatting + +fun stripTooltipDefaultText(tooltip: MutableList) { + var defaultRows = false + val iter = tooltip.iterator() + while (iter.hasNext()) { + if (iter.next().startsWith(EnumChatFormatting.AQUA.toString())) defaultRows = true + if (defaultRows) iter.remove() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/AbstractBlockRenderingHandler.kt b/src/main/kotlin/mods/octarinecore/client/render/AbstractBlockRenderingHandler.kt new file mode 100644 index 0000000..d92703d --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/AbstractBlockRenderingHandler.kt @@ -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 = 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)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/AbstractEntityFX.kt b/src/main/kotlin/mods/octarinecore/client/render/AbstractEntityFX.kt new file mode 100644 index 0000000..348159f --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/AbstractEntityFX.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/Geometry.kt b/src/main/kotlin/mods/octarinecore/client/render/Geometry.kt new file mode 100644 index 0000000..ccddb10 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/Geometry.kt @@ -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.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 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) : 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) = 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 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, val reverse: Array) { + 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 nearestPosition(vertex: Double3, objs: Iterable, objPos: (T)->Double3): Pair = + 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 nearestAngle(vector: Double3, objs: Iterable, objAngle: (T)->Double3): Pair = + objs.map { it to objAngle(it).dot(vector) }.maxBy { it.second }!! + +data class FaceCorners(val topLeft: Pair, + val topRight: Pair, + val bottomLeft: Pair, + val bottomRight: Pair) { + 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) +}} + diff --git a/src/main/kotlin/mods/octarinecore/client/render/Model.kt b/src/main/kotlin/mods/octarinecore/client/render/Model.kt new file mode 100644 index 0000000..7a07377 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/Model.kt @@ -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) = 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) : this() { quads.addAll(other) } + val quads = linkedListOf() + + fun Quad.add() = quads.add(this) + fun Iterable.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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/ModelRenderer.kt b/src/main/kotlin/mods/octarinecore/client/render/ModelRenderer.kt new file mode 100644 index 0000000..509030f --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/ModelRenderer.kt @@ -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 -> } \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/OffsetBlockAccess.kt b/src/main/kotlin/mods/octarinecore/client/render/OffsetBlockAccess.kt new file mode 100644 index 0000000..c0581aa --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/OffsetBlockAccess.kt @@ -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 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 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 +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/PixelFormat.kt b/src/main/kotlin/mods/octarinecore/client/render/PixelFormat.kt new file mode 100644 index 0000000..bc4e58a --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/PixelFormat.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/RenderBlocks.kt b/src/main/kotlin/mods/octarinecore/client/render/RenderBlocks.kt new file mode 100644 index 0000000..22cfb43 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/RenderBlocks.kt @@ -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 = RenderingRegistry::class.java + .reflectStaticField("INSTANCE")!! + .reflectField>("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) = + capture.aoShading(face, corner.first, corner.second) + .set(brightnessTopLeft, colorRedTopLeft, colorGreenTopLeft, colorBlueTopLeft) + + fun saveTopRight(face: ForgeDirection, corner: Pair) = + capture.aoShading(face, corner.first, corner.second) + .set(brightnessTopRight, colorRedTopRight, colorGreenTopRight, colorBlueTopRight) + + fun saveBottomLeft(face: ForgeDirection, corner: Pair) = + capture.aoShading(face, corner.first, corner.second) + .set(brightnessBottomLeft, colorRedBottomLeft, colorGreenBottomLeft, colorBlueBottomLeft) + + fun saveBottomRight(face: ForgeDirection, corner: Pair) = + 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(6 * 6 * 6) + + /** List of stored AO data (only valid instances). */ + var shadingsList = listOf() + + /** List of stored texture data. */ + val icons = arrayOfNulls(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 } + diff --git a/src/main/kotlin/mods/octarinecore/client/render/Shaders.kt b/src/main/kotlin/mods/octarinecore/client/render/Shaders.kt new file mode 100644 index 0000000..d08eea9 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/Shaders.kt @@ -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 +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/render/Shading.kt b/src/main/kotlin/mods/octarinecore/client/render/Shading.kt new file mode 100644 index 0000000..568bdf1 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/render/Shading.kt @@ -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? = 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) + } \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/CenteringTextureGenerator.kt b/src/main/kotlin/mods/octarinecore/client/resource/CenteringTextureGenerator.kt new file mode 100644 index 0000000..623e0b9 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/CenteringTextureGenerator.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/ResourceGeneration.kt b/src/main/kotlin/mods/octarinecore/client/resource/ResourceGeneration.kt new file mode 100644 index 0000000..8941669 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/ResourceGeneration.kt @@ -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>("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, 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) = 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() + 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 ?: "")) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/ResourceHandler.kt b/src/main/kotlin/mods/octarinecore/client/resource/ResourceHandler.kt new file mode 100644 index 0000000..199ca5d --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/ResourceHandler.kt @@ -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() + 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(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) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/TextureGenerator.kt b/src/main/kotlin/mods/octarinecore/client/resource/TextureGenerator.kt new file mode 100644 index 0000000..b4c9ef3 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/TextureGenerator.kt @@ -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) = 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? { + 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() + while(size > 2) { sizes.add(size); size /= 2 } + return sizes.map { resourceManager[maskPath(it)] }.filterNotNull().firstOrNull() + } +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/client/resource/Utils.kt b/src/main/kotlin/mods/octarinecore/client/resource/Utils.kt new file mode 100644 index 0000000..97ca2d5 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/client/resource/Utils.kt @@ -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 { + val result = arrayListOf() + 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}") +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/config/DelegatingConfig.kt b/src/main/kotlin/mods/octarinecore/config/DelegatingConfig.kt new file mode 100644 index 0000000..ab6f89a --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/config/DelegatingConfig.kt @@ -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>() + + /** Attach this config object to the given [Configuration] and update all properties. */ + fun attach(config: Configuration) { + this.config = config + val subProperties = LinkedListMultimap.create() + 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(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("$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>? = 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 + + /** Re-read the property value from the [Configuration]. */ + open fun read() {} +} + +/** Delegate for a property backed by a single [Property] instance. */ +abstract class ConfigPropertyDelegate() : ConfigPropertyBase() { + /** Cached value of the property. */ + var cached: T? = null + /** The [Property] backing this delegate. */ + var property: Property? = null + + override val guiProperties: List 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() { + 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() { + 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() { + 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() { + 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) : + ConfigPropertyDelegate>() { + 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) = 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) = ConfigPropertyIntList(defaults) +fun boolean(default: Boolean) = ConfigPropertyBoolean(default) \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt b/src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt new file mode 100644 index 0000000..3cf4e85 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/metaprog/Reflection.kt @@ -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 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 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> 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 { + 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>() { + 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() { + 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() { + 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) +} \ No newline at end of file diff --git a/src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt b/src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt new file mode 100644 index 0000000..1ff2d32 --- /dev/null +++ b/src/main/kotlin/mods/octarinecore/metaprog/Transformation.kt @@ -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) {} +} + +/** + * 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: MutableListUnit>> = 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: ListUnit, 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.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.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 = 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) + )) +} \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/CactusDefault.cfg b/src/main/resources/assets/betterfoliage/CactusDefault.cfg new file mode 100644 index 0000000..b5720e8 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/CactusDefault.cfg @@ -0,0 +1,5 @@ +// Vanilla +net.minecraft.block.BlockCactus + +// TerraFirmaCraft +com.bioxx.tfc.Blocks.Vanilla.BlockCustomCactus \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/CropDefault.cfg b/src/main/resources/assets/betterfoliage/CropDefault.cfg new file mode 100644 index 0000000..bd95c37 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/CropDefault.cfg @@ -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 \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/DirtDefault.cfg b/src/main/resources/assets/betterfoliage/DirtDefault.cfg new file mode 100644 index 0000000..7322ae7 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/DirtDefault.cfg @@ -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 \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/GrassDefault.cfg b/src/main/resources/assets/betterfoliage/GrassDefault.cfg new file mode 100644 index 0000000..2f44802 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/GrassDefault.cfg @@ -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 \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/LeavesDefault.cfg b/src/main/resources/assets/betterfoliage/LeavesDefault.cfg new file mode 100644 index 0000000..3dbacee --- /dev/null +++ b/src/main/resources/assets/betterfoliage/LeavesDefault.cfg @@ -0,0 +1,8 @@ +// Vanilla +net.minecraft.block.BlockLeavesBase + +// Forestry +forestry.arboriculture.gadgets.BlockLeaves + +// Thaumcraft +thaumcraft.common.blocks.BlockMagicalLeaves \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/LilypadDefault.cfg b/src/main/resources/assets/betterfoliage/LilypadDefault.cfg new file mode 100644 index 0000000..4af6afd --- /dev/null +++ b/src/main/resources/assets/betterfoliage/LilypadDefault.cfg @@ -0,0 +1,5 @@ +// Vanilla +net.minecraft.block.BlockLilyPad + +// TerraFirmaCraft +com.bioxx.tfc.Blocks.Vanilla.BlockCustomLilyPad \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/LogDefault.cfg b/src/main/resources/assets/betterfoliage/LogDefault.cfg new file mode 100644 index 0000000..14c2403 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/LogDefault.cfg @@ -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 \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/SandDefault.cfg b/src/main/resources/assets/betterfoliage/SandDefault.cfg new file mode 100644 index 0000000..a1330e8 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/SandDefault.cfg @@ -0,0 +1,5 @@ +// Vanilla +net.minecraft.block.BlockSand + +// TerraFirmaCraft +com.bioxx.tfc.Blocks.Terrain.BlockSand \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/lang/en_US.lang b/src/main/resources/assets/betterfoliage/lang/en_US.lang new file mode 100644 index 0000000..ce8dbf1 --- /dev/null +++ b/src/main/resources/assets/betterfoliage/lang/en_US.lang @@ -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. \ No newline at end of file diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_0.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_0.png new file mode 100644 index 0000000..5d1143e Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_0.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_1.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_1.png new file mode 100644 index 0000000..118562d Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_1.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_2.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_2.png new file mode 100644 index 0000000..6c3fcee Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_2.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_3.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_3.png new file mode 100644 index 0000000..59f003f Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_default_3.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_0.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_0.png new file mode 100644 index 0000000..3b3ea39 Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_0.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_1.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_1.png new file mode 100644 index 0000000..b44a85f Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_1.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_2.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_2.png new file mode 100644 index 0000000..1020259 Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_2.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_3.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_3.png new file mode 100644 index 0000000..8665fcc Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_jungle_3.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_0.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_0.png new file mode 100644 index 0000000..714b14b Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_0.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_1.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_1.png new file mode 100644 index 0000000..7103dcc Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_1.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_2.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_2.png new file mode 100644 index 0000000..480f409 Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_2.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_3.png b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_3.png new file mode 100644 index 0000000..6fd5ea4 Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/falling_leaf_spruce_3.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_default.png b/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_default.png new file mode 100644 index 0000000..e8117fb Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_default.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_fine.png b/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_fine.png new file mode 100644 index 0000000..f546a8a Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_fine.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_full.png b/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_full.png new file mode 100644 index 0000000..f173eb7 Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_full.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_jumbled.png b/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_jumbled.png new file mode 100644 index 0000000..26b8459 Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/leafmask_32_jumbled.png differ diff --git a/src/main/resources/assets/betterfoliage/textures/blocks/missing_leaf.png b/src/main/resources/assets/betterfoliage/textures/blocks/missing_leaf.png new file mode 100644 index 0000000..ef4b740 Binary files /dev/null and b/src/main/resources/assets/betterfoliage/textures/blocks/missing_leaf.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_0.png new file mode 100644 index 0000000..3703b07 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_1.png new file mode 100644 index 0000000..2601579 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_2.png new file mode 100644 index 0000000..5377e8b Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_3.png new file mode 100644 index 0000000..af1145c Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_algae_3.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus.png new file mode 100644 index 0000000..cfbf609 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_0.png new file mode 100644 index 0000000..dd7de05 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_1.png new file mode 100644 index 0000000..b758ab4 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_cactus_arm_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_0.png new file mode 100644 index 0000000..b735f79 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_1.png new file mode 100644 index 0000000..3361e22 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_2.png new file mode 100644 index 0000000..2db5674 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_3.png new file mode 100644 index 0000000..f850531 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_3.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_4.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_4.png new file mode 100644 index 0000000..f24ef6c Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_4.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_5.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_5.png new file mode 100644 index 0000000..9ea9fe6 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_coral_5.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_0.png new file mode 100644 index 0000000..f1c6ab9 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_1.png new file mode 100644 index 0000000..27f09fc Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_2.png new file mode 100644 index 0000000..a54af00 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_3.png new file mode 100644 index 0000000..5f383bf Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_crust_3.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_0.png new file mode 100644 index 0000000..314e900 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_1.png new file mode 100644 index 0000000..76b92ea Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_2.png new file mode 100644 index 0000000..ac9aaf1 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_3.png new file mode 100644 index 0000000..e8f46df Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_3.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_4.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_4.png new file mode 100644 index 0000000..386e5db Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_long_4.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_0.png new file mode 100644 index 0000000..5372504 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_1.png new file mode 100644 index 0000000..cda1e73 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_2.png new file mode 100644 index 0000000..a82a97e Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_short_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_0.png new file mode 100644 index 0000000..978d6c3 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_1.png new file mode 100644 index 0000000..0af29c9 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_side_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_0.png new file mode 100644 index 0000000..00ffe89 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_1.png new file mode 100644 index 0000000..68d2f8f Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_grass_snowed_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_0.png new file mode 100644 index 0000000..33c9897 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_1.png new file mode 100644 index 0000000..fc2946f Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_2.png new file mode 100644 index 0000000..38ad5ac Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_leaves_snowed_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_0.png new file mode 100644 index 0000000..655b016 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_1.png new file mode 100644 index 0000000..96db7d2 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_flower_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_0.png new file mode 100644 index 0000000..ee4acf2 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_1.png new file mode 100644 index 0000000..b2411f4 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_2.png new file mode 100644 index 0000000..53eff58 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_lilypad_roots_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_0.png new file mode 100644 index 0000000..936902e Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_1.png new file mode 100644 index 0000000..a4291e3 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_2.png new file mode 100644 index 0000000..82172c2 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_3.png new file mode 100644 index 0000000..a658ec1 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_3.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_0.png new file mode 100644 index 0000000..12e9c43 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_1.png new file mode 100644 index 0000000..6aeab09 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_mycel_side_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_0.png new file mode 100644 index 0000000..8c19a4d Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_1.png new file mode 100644 index 0000000..03f03c1 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_2.png new file mode 100644 index 0000000..72a676f Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_netherrack_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_0.png new file mode 100644 index 0000000..f7f107c Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_1.png new file mode 100644 index 0000000..e75e52c Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_2.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_2.png new file mode 100644 index 0000000..ff22ae9 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_2.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_3.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_3.png new file mode 100644 index 0000000..96dcb2e Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/better_reed_3.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/rising_soul_0.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/rising_soul_0.png new file mode 100644 index 0000000..5d86d5e Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/rising_soul_0.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/rising_soul_1.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/rising_soul_1.png new file mode 100644 index 0000000..ee6e749 Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/rising_soul_1.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/soul_track.png b/src/main/resources/assets/bettergrassandleaves/textures/blocks/soul_track.png new file mode 100644 index 0000000..c5bb28f Binary files /dev/null and b/src/main/resources/assets/bettergrassandleaves/textures/blocks/soul_track.png differ diff --git a/src/main/resources/assets/bettergrassandleaves/textures/blocks/soul_track.png.mcmeta b/src/main/resources/assets/bettergrassandleaves/textures/blocks/soul_track.png.mcmeta new file mode 100644 index 0000000..55c2941 --- /dev/null +++ b/src/main/resources/assets/bettergrassandleaves/textures/blocks/soul_track.png.mcmeta @@ -0,0 +1 @@ +{ "animation": { "frametime": 2 } } \ No newline at end of file diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info new file mode 100644 index 0000000..60cd829 --- /dev/null +++ b/src/main/resources/mcmod.info @@ -0,0 +1,8 @@ +[{ + "modid": "BetterFoliage", + "name": "Better Foliage", + "version": "$version", + "mcversion": "$mcversion", + "description": "Leafier leaves and grassier grass", + "authorList" : ["octarine-noise (code)", "Meringue (textures)"] +}] \ No newline at end of file diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..4a45a2a --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 1, + "description": "Better Foliage mod resources made by Meringue" + } +}