Compare commits

7 Commits

Author SHA1 Message Date
64146a0f98 Ensure grass rendering respects air block checks and update project Java home in gradle properties. 2026-04-09 03:09:28 +02:00
octarine-noise
47c134049c Merge branch 'Snownee-1.12' into kotlin-1.12
# Conflicts:
#	src/main/resources/assets/betterfoliage/crop_default.cfg
#	src/main/resources/assets/betterfoliage/leaves_blocks_default.cfg
2021-04-26 11:53:34 +02:00
thedarkcolour
b1ad58c089 Fix an NPE with music discs
Fixes an NPE I found while playing around with music discs. Changes the "soundIn" parameter in the playRecord function in IBlockUpdateListener nullable which is safe because the parameter never gets used. This fixes the NPE because somehow taking a music disc out of the jukebox passes null for the playRecord function, which causes a crash when it shouldn't.
2021-04-26 11:34:02 +02:00
Jordan Rey
85e63b9161 "Plants" mod Crop compatibility 2021-04-26 11:33:21 +02:00
Jordan Rey
59ddaa0335 "Plants" mod Log compatibility 2021-04-26 11:33:21 +02:00
Jordan Rey
ae84741622 "Plants" mod Leaves compatibility 2021-04-26 11:33:21 +02:00
Snownee
369348f6aa Add Cuisine classes to defaults 2019-01-20 20:30:03 +08:00
188 changed files with 3980 additions and 3186 deletions

62
build.gradle Normal file
View File

@@ -0,0 +1,62 @@
apply plugin: "net.minecraftforge.gradle.forge"
apply plugin: 'kotlin'
archivesBaseName = jarName
buildscript {
repositories {
mavenCentral()
maven {
name = "forge"
url = "http://files.minecraftforge.net/maven"
}
}
dependencies {
classpath "net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
repositories {
mavenCentral()
jcenter()
maven {
name = "shadowfacts"
url = "https://maven.shadowfacts.net/"
}
}
dependencies {
compile "net.shadowfacts:Forgelin:$forgelin_version"
}
minecraft {
version = mc_version + "-" + forge_version
mappings = mcp_mappings
runDir = 'run'
}
processResources {
from(sourceSets.main.resources) { exclude 'mcmod.info' }
from(sourceSets.main.resources) { include 'mcmod.info' expand 'version':version, 'mcversion':minecraft.version }
into "${buildDir}/classes/main"
}
def manifestCfg = {
attributes "FMLCorePlugin": "mods.betterfoliage.loader.BetterFoliageLoader"
attributes "FMLCorePluginContainsFMLMod": "mods.betterfoliage.BetterFoliageMod"
attributes "FMLAT": "BetterFoliage_at.cfg"
}
jar {
manifest manifestCfg
exclude "optifine"
}
task sourcesJar(type: Jar, dependsOn: classes) {
classifier = 'sources'
manifest manifestCfg
from(sourceSets.main.kotlin)
from(sourceSets.main.resources) { exclude 'mcmod.info' }
from(sourceSets.main.resources) { include 'mcmod.info' expand 'version':version, 'mcversion':minecraft.version }
}

View File

@@ -1,59 +0,0 @@
plugins {
kotlin("jvm").version("1.3.61")
id("net.minecraftforge.gradle").version("3.0.157")
id("org.spongepowered.mixin").version("0.7-SNAPSHOT")
}
apply(plugin = "org.spongepowered.mixin")
repositories {
maven("http://files.minecraftforge.net/maven")
maven("https://repo.spongepowered.org/maven")
maven("https://minecraft.curseforge.com/api/maven")
}
dependencies {
"minecraft"("net.minecraftforge:forge:${properties["mcVersion"]}-${properties["forgeVersion"]}")
"implementation"("kottle:Kottle:${properties["kottleVersion"]}")
"implementation"("org.spongepowered:mixin:0.8-SNAPSHOT")
}
configurations["annotationProcessor"].extendsFrom(configurations["implementation"])
sourceSets {
get("main").ext["refMap"] = "betterfoliage.refmap.json"
}
minecraft {
mappings(properties["mappingsChannel"] as String, properties["mappingsVersion"] as String)
accessTransformer(file("src/main/resources/META-INF/accesstransformer.cfg"))
runs.create("client") {
workingDirectory(file("run"))
properties["forge.logging.markers"] = "CORE"
properties["forge.logging.console.level"] = "debug"
mods.create("betterfoliage") {
source(sourceSets["main"])
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlin {
target.compilations.configureEach {
kotlinOptions.freeCompilerArgs += listOf("-Xno-param-assertions", "-Xno-call-assertions")
}
}
tasks.getByName<Jar>("jar") {
archiveName = "BetterFoliage-${project.version}-Forge-${properties["mcVersion"]}.jar"
manifest {
from(file("src/main/resources/META-INF/MANIFEST.MF"))
attributes["Implementation-Version"] = project.version
}
exclude("net")
filesMatching("META-INF/mods.toml") { expand(project.properties) }
}

View File

@@ -1,14 +1,13 @@
org.gradle.jvmargs=-Xmx2G
org.gradle.daemon=false
group = com.github.octarine-noise group = com.github.octarine-noise
jarName = BetterFoliage-Forge jarName = BetterFoliage-MC1.12
version = 2.5.0 version = 2.3.1
mcVersion = 1.14.4 mc_version = 1.12.2
forgeVersion = 28.1.109 forge_version = 14.23.5.2847
mappingsChannel = snapshot mcp_mappings = stable_39
mappingsVersion = 20190719-1.14.3
kottleVersion = 1.4.0 kotlin_version = 1.3.40
forgelin_version = 1.8.4
org.gradle.java.home=C:\\Users\\catma\\.jdks\\corretto-1.8.0_482

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

2
settings.gradle Normal file
View File

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

View File

@@ -1,13 +0,0 @@
pluginManagement {
repositories {
maven("http://files.minecraftforge.net/maven")
maven("https://repo.spongepowered.org/maven")
gradlePluginPortal()
}
resolutionStrategy {
eachPlugin {
if (requested.id.let { it.namespace == "net.minecraftforge" && it.name == "gradle"} ) useModule("net.minecraftforge.gradle:ForgeGradle:${requested.version}")
if (requested.id.let { it.namespace == "org.spongepowered" && it.name == "mixin"} ) useModule("org.spongepowered:mixingradle:${requested.version}")
}
}
}

View File

@@ -1,19 +0,0 @@
package mods.betterfoliage;
import org.spongepowered.asm.mixin.Mixins;
import org.spongepowered.asm.mixin.connect.IMixinConnector;
public class MixinConnector implements IMixinConnector {
@Override
public void connect() {
Mixins.addConfiguration("betterfoliage.common.mixins.json");
try {
Class.forName("optifine.OptiFineTransformationService");
Mixins.addConfiguration("betterfoliage.optifine.mixins.json");
} catch (ClassNotFoundException e) {
Mixins.addConfiguration("betterfoliage.vanilla.mixins.json");
}
}
}

View File

@@ -0,0 +1,38 @@
package mods.betterfoliage.loader;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin;
import java.util.Map;
@IFMLLoadingPlugin.TransformerExclusions({
"mods.betterfoliage.loader",
"mods.octarinecore.metaprog",
"kotlin"
})
@IFMLLoadingPlugin.MCVersion("1.12.2")
@IFMLLoadingPlugin.SortingIndex(1400)
public class BetterFoliageLoader implements IFMLLoadingPlugin {
@Override
public String[] getASMTransformerClass() {
return new String[] { "mods.betterfoliage.loader.BetterFoliageTransformer" };
}
@Override
public String getModContainerClass() {
return null;
}
@Override
public String getSetupClass() {
return null;
}
@Override
public void injectData(Map<String, Object> data) {
}
@Override
public String getAccessTransformerClass() {
return null;
}
}

View File

@@ -1,29 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.client.Hooks;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.shapes.VoxelShape;
import net.minecraft.world.IBlockReader;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Mixin overriding the {@link VoxelShape} used for the neighbor block in {@link Block}.shouldSideBeRendered().
*
* This way log blocks can be made to not block rendering, without altering any {@link Block} or
* {@link BlockState} properties with potential gameplay ramifications.
*/
@Mixin(Block.class)
public class MixinBlock {
private static final String shouldSideBeRendered = "Lnet/minecraft/block/Block;shouldSideBeRendered(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Z";
private static final String getVoxelShape = "Lnet/minecraft/block/BlockState;func_215702_a(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Lnet/minecraft/util/math/shapes/VoxelShape;";
@Redirect(method = shouldSideBeRendered, at = @At(value = "INVOKE", target = getVoxelShape, ordinal = 1))
private static VoxelShape getVoxelShapeOverride(BlockState state, IBlockReader reader, BlockPos pos, Direction dir) {
return Hooks.getVoxelShapeOverride(state, reader, pos, dir);
}
}

View File

@@ -1,27 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.client.Hooks;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockReader;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Mixin to override the result of {@link BlockState}.getAmbientOcclusionLightValue().
*
* Needed to avoid excessive darkening of Round Logs at the corners, now that they are not full blocks.
*/
@Mixin(BlockState.class)
@SuppressWarnings({"UnnecessaryQualifiedMemberReference", "deprecation"})
public class MixinBlockState {
private static final String callFrom = "Lnet/minecraft/block/BlockState;func_215703_d(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;)F";
private static final String callTo = "Lnet/minecraft/block/Block;func_220080_a(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;)F";
@Redirect(method = callFrom, at = @At(value = "INVOKE", target = callTo))
float getAmbientOcclusionValue(Block block, BlockState state, IBlockReader reader, BlockPos pos) {
return Hooks.getAmbientOcclusionLightValueOverride(block.func_220080_a(state, reader, pos), state);
}
}

View File

@@ -1,28 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.client.Hooks;
import net.minecraft.block.BlockState;
import net.minecraft.client.renderer.BlockRendererDispatcher;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.chunk.ChunkRender;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IEnviromentBlockReader;
import net.minecraftforge.client.MinecraftForgeClient;
import net.minecraftforge.client.model.data.IModelData;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.Random;
@Mixin(ChunkRender.class)
public class MixinChunkRender {
private static final String rebuildChunk = "rebuildChunk(FFFLnet/minecraft/client/renderer/chunk/ChunkRenderTask;)V";
private static final String renderBlock = "Lnet/minecraft/client/renderer/BlockRendererDispatcher;renderBlock(Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/world/IEnviromentBlockReader;Lnet/minecraft/client/renderer/BufferBuilder;Ljava/util/Random;Lnet/minecraftforge/client/model/data/IModelData;)Z";
@Redirect(method = rebuildChunk, at = @At(value = "INVOKE", target = renderBlock))
public boolean renderBlock(BlockRendererDispatcher dispatcher, BlockState state, BlockPos pos, IEnviromentBlockReader reader, BufferBuilder buffer, Random random, IModelData modelData) {
return Hooks.renderWorldBlock(dispatcher, state, pos, reader, buffer, random, modelData, MinecraftForgeClient.getRenderLayer());
}
}

View File

@@ -1,21 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.client.Hooks;
import net.minecraft.block.BlockState;
import net.minecraft.client.renderer.chunk.ChunkRender;
import net.minecraft.util.BlockRenderLayer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ChunkRender.class)
public class MixinChunkRenderVanilla {
private static final String rebuildChunk = "rebuildChunk(FFFLnet/minecraft/client/renderer/chunk/ChunkRenderTask;)V";
private static final String canRenderInLayer = "Lnet/minecraft/block/BlockState;canRenderInLayer(Lnet/minecraft/util/BlockRenderLayer;)Z";
@Redirect(method = rebuildChunk, at = @At(value = "INVOKE", target = canRenderInLayer))
boolean canRenderInLayer(BlockState state, BlockRenderLayer layer) {
return Hooks.canRenderInLayerOverride(state, layer);
}
}

View File

@@ -1,44 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.client.Hooks;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.client.renderer.WorldRenderer;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockReader;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.Random;
@Mixin(ClientWorld.class)
public class MixinClientWorld {
private static final String worldAnimateTick = "Lnet/minecraft/client/world/ClientWorld;animateTick(IIIILjava/util/Random;ZLnet/minecraft/util/math/BlockPos$MutableBlockPos;)V";
private static final String blockAnimateTick = "Lnet/minecraft/block/Block;animateTick(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/World;Lnet/minecraft/util/math/BlockPos;Ljava/util/Random;)V";
private static final String worldNotify = "Lnet/minecraft/client/world/ClientWorld;notifyBlockUpdate(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/block/BlockState;I)V";
private static final String rendererNotify = "Lnet/minecraft/client/renderer/WorldRenderer;notifyBlockUpdate(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;Lnet/minecraft/block/BlockState;I)V";
/**
* Inject a callback to call for every random display tick. Used for adding custom particle effects to blocks.
*/
@Redirect(method = worldAnimateTick, at = @At(value = "INVOKE", target = blockAnimateTick))
void onAnimateTick(Block block, BlockState state, World world, BlockPos pos, Random random) {
Hooks.onRandomDisplayTick(block, state, world, pos, random);
block.animateTick(state, world, pos, random);
}
/**
* Inject callback to get notified of client-side blockstate changes.
* Used to invalidate caches in the {@link mods.betterfoliage.client.chunk.ChunkOverlayManager}
*/
@Redirect(method = worldNotify, at = @At(value = "INVOKE", target = rendererNotify))
void onClientBlockChanged(WorldRenderer renderer, IBlockReader world, BlockPos pos, BlockState oldState, BlockState newState, int flags) {
Hooks.onClientBlockChanged((ClientWorld) world, pos, oldState, newState, flags);
renderer.notifyBlockUpdate(world, pos, oldState, newState, flags);
}
}

View File

@@ -1,29 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.BetterFoliage;
import net.minecraft.client.renderer.model.ModelBakery;
import net.minecraft.client.renderer.texture.AtlasTexture;
import net.minecraft.profiler.IProfiler;
import net.minecraft.resources.IResourceManager;
import net.minecraft.util.ResourceLocation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ModelBakery.class)
abstract public class MixinModelBakery {
private static final String processLoading = "processLoading(Lnet/minecraft/profiler/IProfiler;)V";
private static final String stitch = "Lnet/minecraft/client/renderer/texture/AtlasTexture;stitch(Lnet/minecraft/resources/IResourceManager;Ljava/lang/Iterable;Lnet/minecraft/profiler/IProfiler;)Lnet/minecraft/client/renderer/texture/AtlasTexture$SheetData;";
@Redirect(method = processLoading, at = @At(value = "INVOKE", target = stitch))
AtlasTexture.SheetData onStitchModelTextures(AtlasTexture atlas, IResourceManager manager, Iterable<ResourceLocation> idList, IProfiler profiler) {
return BetterFoliage.INSTANCE.getBlockSprites().finish(
atlas.stitch(
manager,
BetterFoliage.INSTANCE.getBlockSprites().prepare(this, manager, idList, profiler),
profiler
), profiler
);
}
}

View File

@@ -1,24 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.client.Hooks;
import net.minecraft.block.BlockState;
import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.shapes.VoxelShape;
import net.minecraft.world.IBlockReader;
import net.optifine.util.BlockUtils;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(BlockUtils.class)
public class MixinOptifineBlockUtils {
private static final String shouldSideBeRenderedCached = "shouldSideBeRenderedCached(Lnet/minecraft/block/BlockState;Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;Lnet/optifine/render/RenderEnv;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;)Z";
private static final String getVoxelShape = "Lnet/minecraft/block/BlockState;func_215702_a(Lnet/minecraft/world/IBlockReader;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/util/Direction;)Lnet/minecraft/util/math/shapes/VoxelShape;";
@SuppressWarnings("UnresolvedMixinReference")
@Redirect(method = shouldSideBeRenderedCached, at = @At(value = "INVOKE", target = getVoxelShape, ordinal = 1))
private static VoxelShape getVoxelShapeOverride(BlockState state, IBlockReader reader, BlockPos pos, Direction dir) {
return Hooks.getVoxelShapeOverride(state, reader, pos, dir);
}
}

View File

@@ -1,31 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.client.Hooks;
import net.minecraft.block.BlockState;
import net.minecraft.client.renderer.chunk.ChunkRender;
import net.minecraft.util.BlockRenderLayer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Coerce;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.Slice;
@Mixin(ChunkRender.class)
public class MixinOptifineChunkRender {
private static final String rebuildChunk = "rebuildChunk(FFFLnet/minecraft/client/renderer/chunk/ChunkRenderTask;)V";
private static final String invokeReflector = "Lnet/optifine/reflect/Reflector;callBoolean(Ljava/lang/Object;Lnet/optifine/reflect/ReflectorMethod;[Ljava/lang/Object;)Z";
private static final String forgeBlockCanRender = "Lnet/minecraft/client/renderer/chunk/ChunkRender;FORGE_BLOCK_CAN_RENDER_IN_LAYER:Z";
@Redirect(
method = rebuildChunk,
at = @At(value = "INVOKE", target = invokeReflector),
slice = @Slice(
from = @At(value = "FIELD", target = forgeBlockCanRender)
)
)
@SuppressWarnings("UnresolvedMixinReference")
boolean canRenderInLayer(Object state, @Coerce Object reflector, Object[] layer) {
return Hooks.canRenderInLayerOverride((BlockState) state, (BlockRenderLayer) layer[0]);
}
}

View File

@@ -1,33 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.BetterFoliage;
import mods.octarinecore.client.resource.AsnycSpriteProviderManager;
import net.minecraft.client.particle.ParticleManager;
import net.minecraft.client.renderer.texture.AtlasTexture;
import net.minecraft.profiler.IProfiler;
import net.minecraft.resources.IResourceManager;
import net.minecraft.util.ResourceLocation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ParticleManager.class)
public class MixinParticleManager {
private static final String reload = "reload(Lnet/minecraft/resources/IFutureReloadListener$IStage;Lnet/minecraft/resources/IResourceManager;Lnet/minecraft/profiler/IProfiler;Lnet/minecraft/profiler/IProfiler;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;";
private static final String stitch = "Lnet/minecraft/client/renderer/texture/AtlasTexture;stitch(Lnet/minecraft/resources/IResourceManager;Ljava/lang/Iterable;Lnet/minecraft/profiler/IProfiler;)Lnet/minecraft/client/renderer/texture/AtlasTexture$SheetData;";
// ewww :S
@SuppressWarnings("UnresolvedMixinReference")
@Redirect(method = "*", at = @At(value = "INVOKE", target = stitch))
AtlasTexture.SheetData onStitchModelTextures(AtlasTexture atlas, IResourceManager manager, Iterable<ResourceLocation> idList, IProfiler profiler) {
return BetterFoliage.INSTANCE.getParticleSprites().finish(
atlas.stitch(
manager,
BetterFoliage.INSTANCE.getParticleSprites().prepare(this, manager, idList, profiler),
profiler
), profiler
);
}
}

View File

@@ -1,24 +0,0 @@
package mods.betterfoliage.mixin;
import mods.betterfoliage.client.integration.ShadersModIntegration;
import net.minecraft.block.BlockState;
import net.minecraft.client.renderer.BlockModelRenderer;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IEnviromentBlockReader;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(BlockModelRenderer.class)
public class MixinShadersBlockModelRenderer {
private static final String renderModel = "renderModel(Lnet/minecraft/world/IEnviromentBlockReader;Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/client/renderer/BufferBuilder;ZLjava/util/Random;JLnet/minecraftforge/client/model/data/IModelData;)Z";
private static final String pushEntity = "Lnet/optifine/shaders/SVertexBuilder;pushEntity(Lnet/minecraft/block/BlockState;Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/world/IEnviromentBlockReader;Lnet/minecraft/client/renderer/BufferBuilder;)V";
@SuppressWarnings("UnresolvedMixinReference")
@ModifyArg(method = renderModel, at = @At(value = "INVOKE", target = pushEntity), remap = false)
BlockState overrideBlockState(BlockState state, BlockPos pos, IEnviromentBlockReader world, BufferBuilder buffer) {
return ShadersModIntegration.getBlockStateOverride(state, world, pos);
}
}

View File

@@ -1,5 +0,0 @@
package net.optifine.util;
public class BlockUtils {
// whyyyy?
}

View File

@@ -0,0 +1,65 @@
package optifine;
import net.minecraft.launchwrapper.IClassTransformer;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class OptifineTransformerDevWrapper implements IClassTransformer {
public static String OPTIFINE_CLASSNAME = "optifine/OptiFineClassTransformer.class";
private ZipFile ofZip = null;
public OptifineTransformerDevWrapper() {
Stream<URL> loaderSources = Arrays.stream(((URLClassLoader) getClass().getClassLoader()).getURLs());
Optional<URL> optifineURL = loaderSources.filter(this::isOptifineJar).findFirst();
optifineURL.ifPresent(url -> ofZip = getZip(url));
}
private ZipFile getZip(URL url) {
try {
return new ZipFile(new File(url.toURI()));
} catch (Exception e) {
return null;
}
}
private boolean isOptifineJar(URL url) {
ZipFile zip = getZip(url);
return zip != null && zip.getEntry(OPTIFINE_CLASSNAME) != null;
}
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
if (ofZip == null) return basicClass;
ZipEntry replacement = ofZip.getEntry(name.replace(".", "/") + ".class");
if (replacement == null) return basicClass;
try {
return readAll(ofZip.getInputStream(replacement));
} catch (IOException e) {
return basicClass;
}
}
private byte[] readAll(InputStream is) throws IOException {
byte[] buf = new byte[4096];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len;
do {
len = is.read(buf, 0, 4096);
if (len > 0) bos.write(buf, 0, len);
} while (len > -1);
is.close();
return bos.toByteArray();
}
}

View File

@@ -0,0 +1,28 @@
package optifine;
import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.LaunchClassLoader;
import java.io.File;
import java.util.List;
public class OptifineTweakerDevWrapper implements ITweaker {
@Override
public void acceptOptions(List<String> args, File gameDir, File assetsDir, String profile) {
}
@Override
public void injectIntoClassLoader(LaunchClassLoader classLoader) {
classLoader.registerTransformer("optifine.OptifineTransformerDevWrapper");
}
@Override
public String getLaunchTarget() {
return "net.minecraft.client.main.Main";
}
@Override
public String[] getLaunchArguments() {
return new String[0];
}
}

View File

@@ -1,76 +0,0 @@
package mods.betterfoliage
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.render.RisingSoulTextures
import mods.octarinecore.client.gui.textComponent
import mods.octarinecore.client.resource.AsnycSpriteProviderManager
import mods.octarinecore.client.resource.AsyncSpriteProvider
import mods.octarinecore.client.resource.Atlas
import mods.octarinecore.client.resource.GeneratedBlockTexturePack
import net.minecraft.block.BlockState
import net.minecraft.client.Minecraft
import net.minecraft.client.particle.ParticleManager
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.util.math.BlockPos
import net.minecraft.util.text.TextFormatting
import net.minecraft.util.text.TranslationTextComponent
import net.minecraftforge.registries.ForgeRegistries
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.simple.SimpleLogger
import org.apache.logging.log4j.util.PropertiesUtil
import java.io.File
import java.io.PrintStream
import java.util.*
object BetterFoliage {
var log = LogManager.getLogger("BetterFoliage")
var logDetail = SimpleLogger(
"BetterFoliage",
Level.DEBUG,
false, false, true, false,
"yyyy-MM-dd HH:mm:ss",
null,
PropertiesUtil(Properties()),
PrintStream(File("logs/betterfoliage.log").apply {
parentFile.mkdirs()
if (!exists()) createNewFile()
})
)
val blockSprites = AsnycSpriteProviderManager<ModelBakery>("bf-blocks-extra")
val particleSprites = AsnycSpriteProviderManager<ParticleManager>("bf-particles-extra")
val asyncPack = GeneratedBlockTexturePack("bf_gen", "Better Foliage generated assets", logDetail)
fun getSpriteManager(atlas: Atlas) = when(atlas) {
Atlas.BLOCKS -> blockSprites
Atlas.PARTICLES -> particleSprites
} as AsnycSpriteProviderManager<Any>
init {
blockSprites.providers.add(asyncPack)
}
fun log(level: Level, msg: String) {
log.log(level, "[BetterFoliage] $msg")
logDetail.log(level, msg)
}
fun logDetail(msg: String) {
logDetail.log(Level.DEBUG, msg)
}
fun logRenderError(state: BlockState, location: BlockPos) {
if (state in Client.suppressRenderErrors) return
Client.suppressRenderErrors.add(state)
val blockName = ForgeRegistries.BLOCKS.getKey(state.block).toString()
val blockLoc = "${location.x},${location.y},${location.z}"
Minecraft.getInstance().ingameGUI.chatGUI.printChatMessage(TranslationTextComponent(
"betterfoliage.rendererror",
textComponent(blockName, TextFormatting.GOLD),
textComponent(blockLoc, TextFormatting.GOLD)
))
logDetail("Error rendering block $state at $blockLoc")
}
}

View File

@@ -1,35 +1,78 @@
package mods.betterfoliage package mods.betterfoliage
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.BlockConfig
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.resource.AsnycSpriteProviderManager import mods.betterfoliage.client.isAfterPostInit
import mods.octarinecore.client.resource.GeneratedBlockTexturePack import net.minecraftforge.common.config.Configuration
import net.alexwells.kottle.FMLKotlinModLoadingContext import net.minecraftforge.fml.common.FMLCommonHandler
import net.minecraft.client.Minecraft
import net.minecraft.client.particle.ParticleManager
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraftforge.fml.ModLoadingContext
import net.minecraftforge.fml.common.Mod import net.minecraftforge.fml.common.Mod
import net.minecraftforge.fml.config.ModConfig import net.minecraftforge.fml.common.event.FMLPostInitializationEvent
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent
import net.minecraftforge.fml.common.network.NetworkCheckHandler
import net.minecraftforge.fml.relauncher.Side
import org.apache.logging.log4j.Level.DEBUG import org.apache.logging.log4j.Level.DEBUG
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Level.INFO
import org.apache.logging.log4j.Logger
import org.apache.logging.log4j.simple.SimpleLogger import org.apache.logging.log4j.simple.SimpleLogger
import org.apache.logging.log4j.util.PropertiesUtil import org.apache.logging.log4j.util.PropertiesUtil
import java.io.File import java.io.File
import java.io.PrintStream import java.io.PrintStream
import java.util.* import java.util.*
@Mod(BetterFoliageMod.MOD_ID) @Mod(
modid = BetterFoliageMod.MOD_ID,
name = BetterFoliageMod.MOD_NAME,
acceptedMinecraftVersions = BetterFoliageMod.MC_VERSIONS,
guiFactory = BetterFoliageMod.GUI_FACTORY,
dependencies = "after:forgelin",
modLanguageAdapter = "net.shadowfacts.forgelin.KotlinAdapter",
clientSideOnly = true
)
object BetterFoliageMod { object BetterFoliageMod {
const val MOD_ID = "betterfoliage"
val bus = FMLKotlinModLoadingContext.get().modEventBus 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.12]"
const val GUI_FACTORY = "mods.betterfoliage.client.gui.ConfigGuiFactory"
lateinit var log: Logger
lateinit var logDetail: Logger
var config: Configuration? = null
init { init {
ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, Config.build()) // inject pack into default list at construction time to get domains enumerated
Minecraft.getInstance().resourcePackList.addPackFinder(BetterFoliage.asyncPack.finder) // there's no 2nd resource reload pass anymore
bus.register(BlockConfig) Client.generatorPack.inject()
Client.init()
} }
@Mod.EventHandler
fun preInit(event: FMLPreInitializationEvent) {
log = event.modLog
val logDetailFile = File(event.modConfigurationDirectory.parentFile, "logs/betterfoliage.log").apply {
parentFile.mkdirs()
if (!exists()) createNewFile()
}
logDetail = SimpleLogger(
"BetterFoliage",
DEBUG,
false, false, true, false,
"yyyy-MM-dd HH:mm:ss",
null,
PropertiesUtil(Properties()),
PrintStream(logDetailFile)
)
config = Configuration(event.suggestedConfigurationFile, null, true)
Config.attach(config!!)
Client.init()
Client.log(INFO, "BetterFoliage initialized")
isAfterPostInit = true
}
/** Mod is cosmetic only, always allow connection. */
@NetworkCheckHandler
fun checkVersion(mods: Map<String, String>, side: Side) = true
} }

View File

@@ -2,32 +2,50 @@ package mods.betterfoliage.client
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.chunk.ChunkOverlayManager import mods.betterfoliage.client.chunk.ChunkOverlayManager
import mods.betterfoliage.client.config.BlockConfig import mods.betterfoliage.client.gui.ConfigGuiFactory
import mods.betterfoliage.client.integration.* import mods.betterfoliage.client.integration.*
import mods.betterfoliage.client.render.* import mods.betterfoliage.client.render.*
import mods.betterfoliage.client.texture.AsyncGrassDiscovery import mods.betterfoliage.client.texture.*
import mods.betterfoliage.client.texture.AsyncLeafDiscovery import mods.octarinecore.client.KeyHandler
import mods.betterfoliage.client.texture.LeafParticleRegistry
import mods.octarinecore.client.gui.textComponent import mods.octarinecore.client.gui.textComponent
import mods.octarinecore.client.render.RenderDecorator import mods.octarinecore.client.render.AbstractBlockRenderingHandler
import mods.octarinecore.client.resource.IConfigChangeListener import mods.octarinecore.client.resource.CenteringTextureGenerator
import net.minecraft.block.BlockState import mods.octarinecore.client.resource.GeneratorPack
import net.minecraft.block.Block
import net.minecraft.block.state.IBlockState
import net.minecraft.client.Minecraft import net.minecraft.client.Minecraft
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.text.TextComponentTranslation
import net.minecraft.util.text.TextFormatting import net.minecraft.util.text.TextFormatting
import net.minecraft.util.text.TranslationTextComponent import net.minecraftforge.fml.client.FMLClientHandler
import net.minecraftforge.registries.ForgeRegistries import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level import org.apache.logging.log4j.Level
/** /**
* Object responsible for initializing (and holding a reference to) all the infrastructure of the mod * Object responsible for initializing (and holding a reference to) all the infrastructure of the mod
* except for the call hooks. * 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 { object Client {
var renderers= emptyList<RenderDecorator>()
var configListeners = emptyList<IConfigChangeListener>()
val suppressRenderErrors = mutableSetOf<BlockState>() lateinit var renderers: List<AbstractBlockRenderingHandler>
val suppressRenderErrors = mutableSetOf<IBlockState>()
// texture generation stuff
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
)
fun init() { fun init() {
// init renderers // init renderers
@@ -48,7 +66,8 @@ object Client {
// init other singletons // init other singletons
val singletons = listOf( val singletons = listOf(
BlockConfig, StandardCactusRegistry,
LeafParticleRegistry,
ChunkOverlayManager, ChunkOverlayManager,
LeafWindTracker, LeafWindTracker,
RisingSoulTextures RisingSoulTextures
@@ -57,22 +76,46 @@ object Client {
// init mod integrations // init mod integrations
val integrations = listOf( val integrations = listOf(
ShadersModIntegration, ShadersModIntegration,
OptifineCustomColors OptifineCustomColors,
// ForestryIntegration, ForestryIntegration,
// IC2RubberIntegration, IC2RubberIntegration,
// TechRebornRubberIntegration TechRebornRubberIntegration
) )
LeafParticleRegistry.init()
// add basic block support instances as last // add basic block support instances as last
AsyncLeafDiscovery.init() GrassRegistry.addRegistry(StandardGrassRegistry)
AsyncGrassDiscovery.init() LeafRegistry.addRegistry(StandardLeafRegistry)
AsyncLogDiscovery.init() LogRegistry.addRegistry(StandardLogRegistry)
AsyncCactusDiscovery.init()
configListeners = listOf(renderers, singletons, integrations).flatten().filterIsInstance<IConfigChangeListener>() // init config hotkey
configListeners.forEach { it.onConfigChange() } val configKey = KeyHandler(BetterFoliageMod.MOD_NAME, 66, "key.betterfoliage.gui") {
FMLClientHandler.instance().showGuiScreen(
ConfigGuiFactory.createBFConfigGui(Minecraft.getMinecraft().currentScreen)
)
}
}
fun log(level: Level, msg: String) {
BetterFoliageMod.log.log(level, "[BetterFoliage] $msg")
BetterFoliageMod.logDetail.log(level, msg)
}
fun logDetail(msg: String) {
BetterFoliageMod.logDetail.log(Level.DEBUG, msg)
}
fun logRenderError(state: IBlockState, location: BlockPos) {
if (state in suppressRenderErrors) return
suppressRenderErrors.add(state)
val blockName = Block.REGISTRY.getNameForObject(state.block).toString()
val blockLoc = "${location.x},${location.y},${location.z}"
Minecraft.getMinecraft().ingameGUI.chatGUI.printChatMessage(TextComponentTranslation(
"betterfoliage.rendererror",
textComponent(blockName, TextFormatting.GOLD),
textComponent(blockLoc, TextFormatting.GOLD)
))
logDetail("Error rendering block $state at $blockLoc")
} }
} }

View File

@@ -1,48 +1,55 @@
@file:JvmName("Hooks") @file:JvmName("Hooks")
@file:SideOnly(Side.CLIENT)
package mods.betterfoliage.client package mods.betterfoliage.client
import mods.betterfoliage.client.chunk.ChunkOverlayManager
import mods.betterfoliage.client.config.BlockConfig
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.render.* import mods.betterfoliage.client.render.*
import mods.octarinecore.ThreadLocalDelegate import mods.betterfoliage.loader.Refs
import mods.octarinecore.client.render.* import mods.octarinecore.client.render.blockContext
import mods.octarinecore.client.render.lighting.DefaultLightingCtx import mods.octarinecore.client.resource.LoadModelDataEvent
import mods.octarinecore.common.plus import mods.octarinecore.common.plus
import mods.octarinecore.metaprog.allAvailable
import net.minecraft.block.Block import net.minecraft.block.Block
import net.minecraft.block.BlockState import net.minecraft.block.state.IBlockState
import net.minecraft.block.Blocks
import net.minecraft.client.Minecraft import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.BlockRendererDispatcher import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.client.world.ClientWorld import net.minecraft.init.Blocks
import net.minecraft.util.BlockRenderLayer import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.BlockRenderLayer.CUTOUT import net.minecraft.util.BlockRenderLayer.CUTOUT
import net.minecraft.util.BlockRenderLayer.CUTOUT_MIPPED import net.minecraft.util.BlockRenderLayer.CUTOUT_MIPPED
import net.minecraft.util.Direction import net.minecraft.util.EnumFacing
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.shapes.VoxelShape import net.minecraft.world.IBlockAccess
import net.minecraft.util.math.shapes.VoxelShapes
import net.minecraft.world.IBlockReader
import net.minecraft.world.IEnviromentBlockReader
import net.minecraft.world.World import net.minecraft.world.World
import net.minecraftforge.client.model.data.IModelData import net.minecraftforge.client.model.ModelLoader
import java.util.* import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
fun getAmbientOcclusionLightValueOverride(original: Float, state: BlockState): Float { var isAfterPostInit = false
if (Config.enabled && Config.roundLogs.enabled && BlockConfig.logBlocks.matchesClass(state.block)) return Config.roundLogs.dimming.toFloat(); val isOptifinePresent = allAvailable(Refs.OptifineClassTransformer)
return original
fun doesSideBlockRenderingOverride(original: Boolean, blockAccess: IBlockAccess, pos: BlockPos, side: EnumFacing): Boolean {
return original && !(Config.enabled && Config.roundLogs.enabled && Config.blocks.logClasses.matchesClass(blockAccess.getBlockState(pos).block));
} }
fun getUseNeighborBrightnessOverride(original: Boolean, state: BlockState): Boolean { fun isOpaqueCubeOverride(original: Boolean, state: IBlockState): Boolean {
return original || (Config.enabled && Config.roundLogs.enabled && BlockConfig.logBlocks.matchesClass(state.block)); // caution: blocks are initialized and the method called during startup
if (!isAfterPostInit) return original
return original && !(Config.enabled && Config.roundLogs.enabled && Config.blocks.logClasses.matchesClass(state.block))
} }
fun onClientBlockChanged(worldClient: ClientWorld, pos: BlockPos, oldState: BlockState, newState: BlockState, flags: Int) { fun getAmbientOcclusionLightValueOverride(original: Float, state: IBlockState): Float {
ChunkOverlayManager.onBlockChange(worldClient, pos) if (Config.enabled && Config.roundLogs.enabled && Config.blocks.logClasses.matchesClass(state.block)) return Config.roundLogs.dimming;
return original;
} }
fun onRandomDisplayTick(block: Block, state: BlockState, world: World, pos: BlockPos, random: Random) { fun getUseNeighborBrightnessOverride(original: Boolean, state: IBlockState): Boolean {
return original || (Config.enabled && Config.roundLogs.enabled && Config.blocks.logClasses.matchesClass(state.block));
}
fun onRandomDisplayTick(world: World, state: IBlockState, pos: BlockPos) {
if (Config.enabled && if (Config.enabled &&
Config.risingSoul.enabled && Config.risingSoul.enabled &&
state.block == Blocks.SOUL_SAND && state.block == Blocks.SOUL_SAND &&
@@ -53,60 +60,42 @@ fun onRandomDisplayTick(block: Block, state: BlockState, world: World, pos: Bloc
if (Config.enabled && if (Config.enabled &&
Config.fallingLeaves.enabled && Config.fallingLeaves.enabled &&
BlockConfig.leafBlocks.matchesClass(state.block) && Config.blocks.leavesClasses.matchesClass(state.block) &&
world.isAirBlock(pos + down1) && world.isAirBlock(pos + down1) &&
Math.random() < Config.fallingLeaves.chance) { Math.random() < Config.fallingLeaves.chance) {
EntityFallingLeavesFX(world, pos).addIfValid() EntityFallingLeavesFX(world, pos).addIfValid()
} }
} }
fun getVoxelShapeOverride(state: BlockState, reader: IBlockReader, pos: BlockPos, dir: Direction): VoxelShape { fun onAfterLoadModelDefinitions(loader: ModelLoader) {
if (LogRegistry[state, reader, pos] != null) return VoxelShapes.empty() MinecraftForge.EVENT_BUS.post(LoadModelDataEvent(loader))
return state.func_215702_a(reader, pos, dir)
} }
val lightingCtx by ThreadLocalDelegate { DefaultLightingCtx(BasicBlockCtx(NonNullWorld, BlockPos.ZERO)) }
fun renderWorldBlock(dispatcher: BlockRendererDispatcher, fun renderWorldBlock(dispatcher: BlockRendererDispatcher,
state: BlockState, state: IBlockState,
pos: BlockPos, pos: BlockPos,
reader: IEnviromentBlockReader, blockAccess: IBlockAccess,
buffer: BufferBuilder, worldRenderer: BufferBuilder,
random: Random,
modelData: IModelData,
layer: BlockRenderLayer layer: BlockRenderLayer
): Boolean { ): Boolean {
// build context
val blockCtx = CachedBlockCtx(reader, pos)
val renderCtx = RenderCtx(dispatcher, buffer, layer, random)
lightingCtx.reset(blockCtx)
val combinedCtx = CombinedContext(blockCtx, renderCtx, lightingCtx)
// loop render decorators
val doBaseRender = state.canRenderInLayer(layer) || (layer == targetCutoutLayer && state.canRenderInLayer(otherCutoutLayer)) val doBaseRender = state.canRenderInLayer(layer) || (layer == targetCutoutLayer && state.canRenderInLayer(otherCutoutLayer))
blockContext.let { ctx ->
ctx.set(blockAccess, pos)
Client.renderers.forEach { renderer -> Client.renderers.forEach { renderer ->
if (renderer.isEligible(combinedCtx)) { if (renderer.isEligible(ctx)) {
// render on the block's default layer // render on the block's default layer
// also render on the cutout layer if the renderer requires it // also render on the cutout layer if the renderer requires it
if (doBaseRender || (renderer.addToCutout && layer == targetCutoutLayer)) {
val doCutoutRender = renderer.renderOnCutout && layer == targetCutoutLayer return renderer.render(ctx, dispatcher, worldRenderer, layer)
val stopRender = renderer.onlyOnCutout && !layer.isCutout }
if ((doBaseRender || doCutoutRender) && !stopRender) {
renderer.render(combinedCtx)
return combinedCtx.hasRendered
} }
} }
} }
// no render decorators have taken on this block, proceed to normal rendering return if (doBaseRender) dispatcher.renderBlock(state, pos, blockAccess, worldRenderer) else false
combinedCtx.render()
return combinedCtx.hasRendered
} }
fun canRenderInLayerOverride(state: BlockState, layer: BlockRenderLayer) = state.canRenderInLayer(layer) || layer == targetCutoutLayer fun canRenderBlockInLayer(block: Block, state: IBlockState, layer: BlockRenderLayer) = block.canRenderInLayer(state, layer) || layer == targetCutoutLayer
fun canRenderInLayerOverrideOptifine(state: BlockState, optifineReflector: Any?, layerArray: Array<Any>) = val targetCutoutLayer: BlockRenderLayer get() = if (Minecraft.getMinecraft().gameSettings.mipmapLevels > 0) CUTOUT_MIPPED else CUTOUT
canRenderInLayerOverride(state, layerArray[0] as BlockRenderLayer) val otherCutoutLayer: BlockRenderLayer get() = if (Minecraft.getMinecraft().gameSettings.mipmapLevels > 0) CUTOUT else CUTOUT_MIPPED
val targetCutoutLayer: BlockRenderLayer get() = if (Minecraft.getInstance().gameSettings.mipmapLevels > 0) CUTOUT_MIPPED else CUTOUT
val otherCutoutLayer: BlockRenderLayer get() = if (Minecraft.getInstance().gameSettings.mipmapLevels > 0) CUTOUT else CUTOUT_MIPPED

View File

@@ -1,71 +1,57 @@
package mods.betterfoliage.client.chunk package mods.betterfoliage.client.chunk
import mods.octarinecore.ChunkCacheOF import mods.betterfoliage.client.Client
import mods.octarinecore.client.render.BlockCtx import net.minecraft.block.state.IBlockState
import mods.octarinecore.metaprog.get import net.minecraft.client.multiplayer.WorldClient
import mods.octarinecore.metaprog.isInstance import net.minecraft.entity.Entity
import net.minecraft.client.renderer.chunk.ChunkRenderCache import net.minecraft.entity.player.EntityPlayer
import net.minecraft.client.world.ClientWorld import net.minecraft.util.SoundCategory
import net.minecraft.util.SoundEvent
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.ChunkPos import net.minecraft.util.math.ChunkPos
import net.minecraft.world.IEnviromentBlockReader import net.minecraft.world.IBlockAccess
import net.minecraft.world.IWorldReader import net.minecraft.world.IWorldEventListener
import net.minecraft.world.dimension.DimensionType import net.minecraft.world.World
import net.minecraft.world.chunk.EmptyChunk
import net.minecraftforge.common.MinecraftForge import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.event.world.ChunkEvent import net.minecraftforge.event.world.ChunkEvent
import net.minecraftforge.event.world.WorldEvent import net.minecraftforge.event.world.WorldEvent
import net.minecraftforge.eventbus.api.SubscribeEvent import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import java.util.* import org.apache.logging.log4j.Level
import kotlin.collections.List
import kotlin.collections.MutableMap
import kotlin.collections.associateWith
import kotlin.collections.forEach
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.set
val IEnviromentBlockReader.dimType: DimensionType get() = when {
this is IWorldReader -> dimension.type
this is ChunkRenderCache -> world.dimension.type
this.isInstance(ChunkCacheOF) -> this[ChunkCacheOF.chunkCache].world.dimension.type
else -> throw IllegalArgumentException("DimensionType of world with class ${this::class.qualifiedName} cannot be determined!")
}
/** /**
* Represents some form of arbitrary non-persistent data that can be calculated and cached for each block position * Represents some form of arbitrary non-persistent data that can be calculated and cached for each block position
*/ */
interface ChunkOverlayLayer<T> { interface ChunkOverlayLayer<T> {
fun calculate(ctx: BlockCtx): T abstract fun calculate(world: IBlockAccess, pos: BlockPos): T
fun onBlockUpdate(world: IEnviromentBlockReader, pos: BlockPos) abstract fun onBlockUpdate(world: IBlockAccess, pos: BlockPos)
} }
/** /**
* Query, lazy calculation and lifecycle management of multiple layers of chunk overlay data. * Query, lazy calculation and lifecycle management of multiple layers of chunk overlay data.
*/ */
object ChunkOverlayManager { object ChunkOverlayManager : IBlockUpdateListener {
var tempCounter = 0
init { init {
Client.log(Level.INFO, "Initializing client overlay manager")
MinecraftForge.EVENT_BUS.register(this) MinecraftForge.EVENT_BUS.register(this)
} }
val chunkData = IdentityHashMap<DimensionType, MutableMap<ChunkPos, ChunkOverlayData>>() val chunkData = mutableMapOf<ChunkPos, ChunkOverlayData>()
val layers = mutableListOf<ChunkOverlayLayer<*>>() val layers = mutableListOf<ChunkOverlayLayer<*>>()
/** /**
* Get the overlay data for a given layer and position * Get the overlay data for a given layer and position
* *
* @param layer Overlay layer to query * @param layer Overlay layer to query
* @param reader World to use if calculation of overlay value is necessary * @param world World to use if calculation of overlay value is necessary
* @param pos Block position * @param pos Block position
*/ */
fun <T> get(layer: ChunkOverlayLayer<T>, ctx: BlockCtx): T? { fun <T> get(layer: ChunkOverlayLayer<T>, world: IBlockAccess, pos: BlockPos): T? {
val data = chunkData[ctx.world.dimType]?.get(ChunkPos(ctx.pos)) ?: return null val data = chunkData[ChunkPos(pos)] ?: return null
data.get(layer, ctx.pos).let { value -> data.get(layer, pos).let { value ->
if (value !== ChunkOverlayData.UNCALCULATED) return value if (value !== ChunkOverlayData.UNCALCULATED) return value
val newValue = layer.calculate(ctx) val newValue = layer.calculate(world, pos)
data.set(layer, ctx.pos, newValue) data.set(layer, pos, newValue)
return newValue return newValue
} }
} }
@@ -76,37 +62,32 @@ object ChunkOverlayManager {
* @param layer Overlay layer to clear * @param layer Overlay layer to clear
* @param pos Block position * @param pos Block position
*/ */
fun <T> clear(dimension: DimensionType, layer: ChunkOverlayLayer<T>, pos: BlockPos) { fun <T> clear(layer: ChunkOverlayLayer<T>, pos: BlockPos) {
chunkData[dimension]?.get(ChunkPos(pos))?.clear(layer, pos) chunkData[ChunkPos(pos)]?.clear(layer, pos)
} }
fun onBlockChange(world: ClientWorld, pos: BlockPos) { override fun notifyBlockUpdate(world: World, pos: BlockPos, oldState: IBlockState, newState: IBlockState, flags: Int) {
if (chunkData[world.dimType]?.containsKey(ChunkPos(pos)) == true) { if (chunkData.containsKey(ChunkPos(pos))) layers.forEach { layer -> layer.onBlockUpdate(world, pos) }
layers.forEach { layer -> layer.onBlockUpdate(world, pos) } }
@SubscribeEvent
fun handleLoadWorld(event: WorldEvent.Load) {
if (event.world is WorldClient) {
event.world.addEventListener(this)
} }
} }
@SubscribeEvent @SubscribeEvent
fun handleLoadWorld(event: WorldEvent.Load) = (event.world as? ClientWorld)?.let { world -> fun handleLoadChunk(event: ChunkEvent.Load) {
chunkData[world.dimType] = mutableMapOf() if (event.world is WorldClient && event.chunk !is EmptyChunk) {
chunkData[event.chunk.pos] = ChunkOverlayData(layers)
}
} }
@SubscribeEvent @SubscribeEvent
fun handleUnloadWorld(event: WorldEvent.Unload) = (event.world as? ClientWorld)?.let { world -> fun handleUnloadChunk(event: ChunkEvent.Unload) {
chunkData.remove(world.dimType) if (event.world is WorldClient) {
chunkData.remove(event.chunk.pos)
} }
@SubscribeEvent
fun handleLoadChunk(event: ChunkEvent.Load) = (event.world as? ClientWorld)?.let { world ->
chunkData[world.dimType]?.let { chunks ->
// check for existence first because Optifine fires a TON of these
if (event.chunk.pos !in chunks.keys) chunks[event.chunk.pos] = ChunkOverlayData(layers)
}
}
@SubscribeEvent
fun handleUnloadChunk(event: ChunkEvent.Unload) = (event.world as? ClientWorld)?.let { world ->
chunkData[world.dimType]?.remove(event.chunk.pos)
} }
} }
@@ -123,3 +104,21 @@ class ChunkOverlayData(layers: List<ChunkOverlayLayer<*>>) {
val validYRange = 0 until 256 val validYRange = 0 until 256
} }
} }
/**
* IWorldEventListener helper subclass
* No-op for everything except notifyBlockUpdate()
*/
interface IBlockUpdateListener : IWorldEventListener {
override fun playSoundToAllNearExcept(player: EntityPlayer?, soundIn: SoundEvent, category: SoundCategory, x: Double, y: Double, z: Double, volume: Float, pitch: Float) {}
override fun onEntityAdded(entityIn: Entity) {}
override fun broadcastSound(soundID: Int, pos: BlockPos, data: Int) {}
override fun playEvent(player: EntityPlayer?, type: Int, blockPosIn: BlockPos, data: Int) {}
override fun onEntityRemoved(entityIn: Entity) {}
override fun notifyLightSet(pos: BlockPos) {}
override fun spawnParticle(particleID: Int, ignoreRange: Boolean, xCoord: Double, yCoord: Double, zCoord: Double, xSpeed: Double, ySpeed: Double, zSpeed: Double, vararg parameters: Int) {}
override fun spawnParticle(id: Int, ignoreRange: Boolean, minimiseParticleLevel: Boolean, x: Double, y: Double, z: Double, xSpeed: Double, ySpeed: Double, zSpeed: Double, vararg parameters: Int) {}
override fun playRecord(soundIn: SoundEvent?, pos: BlockPos) {}
override fun sendBlockBreakProgress(breakerId: Int, pos: BlockPos, progress: Int) {}
override fun markBlockRangeForRenderUpdate(x1: Int, y1: Int, z1: Int, x2: Int, y2: Int, z2: Int) {}
}

View File

@@ -1,22 +1,67 @@
package mods.betterfoliage.client.config package mods.betterfoliage.client.config
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.gui.BiomeListConfigEntry
import mods.betterfoliage.client.integration.ShadersModIntegration import mods.betterfoliage.client.integration.ShadersModIntegration
import mods.octarinecore.common.config.* import mods.octarinecore.common.config.*
import net.minecraft.util.ResourceLocation import net.minecraft.client.Minecraft
import net.minecraftforge.eventbus.api.SubscribeEvent import net.minecraft.world.biome.Biome
import net.minecraftforge.fml.config.ModConfig import net.minecraftforge.fml.client.event.ConfigChangedEvent
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.lwjgl.opengl.GL11
// BetterFoliage-specific property delegates
private val OBSOLETE = ObsoleteConfigProperty()
private fun featureEnable() = boolean(true).lang("enabled") private fun featureEnable() = boolean(true).lang("enabled")
fun biomeList(defaults: (Biome) -> Boolean) = intList {
Biome.REGISTRY
.filter { it != null && defaults(it) }
.map { Biome.REGISTRY.getIDForObject(it) }
.toTypedArray()
}.apply { guiClass = BiomeListConfigEntry::class.java }
// Biome filter methods
private fun Biome.filterTemp(min: Float?, max: Float?) = (min == null || min <= defaultTemperature) && (max == null || max >= defaultTemperature)
private fun Biome.filterRain(min: Float?, max: Float?) = (min == null || min <= rainfall) && (max == null || max >= rainfall)
private fun Biome.filterClass(vararg name: String) = name.any { it in this.javaClass.name.toLowerCase() }
// Config singleton // Config singleton
object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.MOD_ID) { @SideOnly(Side.CLIENT)
object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.DOMAIN) {
val enabled by boolean(true) var enabled by boolean(true)
val nVidia by boolean(false) var nVidia by boolean(GL11.glGetString(GL11.GL_VENDOR).toLowerCase().contains("nvidia"))
object leaves : ConfigCategory() { object shaders {
val leavesId by long(min = 1, max = 65535, default = ShadersModIntegration.leavesDefaultBlockId.toInt())
val grassId by long(min = 1, max = 65535, default = ShadersModIntegration.grassDefaultBlockId.toInt())
}
object blocks {
val leavesClasses = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "leaves_blocks_default.cfg")
val leavesModels = ModelTextureListConfigOption(BetterFoliageMod.DOMAIN, "leaves_models_default.cfg", 1)
val grassClasses = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "grass_blocks_default.cfg")
val grassModels = ModelTextureListConfigOption(BetterFoliageMod.DOMAIN, "grass_models_default.cfg", 1)
val mycelium = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "mycelium_blocks_default.cfg")
val dirt = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "dirt_default.cfg")
val crops = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "crop_default.cfg")
val logClasses = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "log_blocks_default.cfg")
val logModels = ModelTextureListConfigOption(BetterFoliageMod.DOMAIN, "log_models_default.cfg", 3)
val sand = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "sand_default.cfg")
val lilypad = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "lilypad_default.cfg")
val cactus = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "cactus_default.cfg")
val netherrack = ConfigurableBlockMatcher(BetterFoliageMod.DOMAIN, "netherrack_blocks_default.cfg")
val leavesWhitelist = OBSOLETE
val leavesBlacklist = OBSOLETE
val grassWhitelist = OBSOLETE
val grassBlacklist = OBSOLETE
val logsWhitelist = OBSOLETE
val logsBlacklist = OBSOLETE
}
object leaves {
val enabled by featureEnable() val enabled by featureEnable()
val snowEnabled by boolean(true) val snowEnabled by boolean(true)
val hOffset by double(max=0.4, default=0.2).lang("hOffset") val hOffset by double(max=0.4, default=0.2).lang("hOffset")
@@ -26,7 +71,7 @@ object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.MOD_I
val hideInternal by boolean(true) val hideInternal by boolean(true)
} }
object shortGrass : ConfigCategory(){ object shortGrass {
val grassEnabled by boolean(true) val grassEnabled by boolean(true)
val myceliumEnabled by boolean(true) val myceliumEnabled by boolean(true)
val snowEnabled by boolean(true) val snowEnabled by boolean(true)
@@ -40,16 +85,23 @@ object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.MOD_I
val saturationThreshold by double(default=0.1) val saturationThreshold by double(default=0.1)
} }
object connectedGrass : ConfigCategory(){ // 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 enabled by boolean(true)
val snowEnabled by boolean(false) val snowEnabled by boolean(false)
} }
object roundLogs : ConfigCategory(){ object roundLogs {
val enabled by featureEnable() val enabled by featureEnable()
val radiusSmall by double(max=0.5, default=0.25) val radiusSmall by double(max=0.5, default=0.25)
val radiusLarge by double(max=0.5, default=0.44) val radiusLarge by double(max=0.5, default=0.44)
val dimming by double(default = 0.7) val dimming by float(default = 0.7)
val connectSolids by boolean(false) val connectSolids by boolean(false)
val lenientConnect by boolean(true) val lenientConnect by boolean(true)
val connectPerpendicular by boolean(true) val connectPerpendicular by boolean(true)
@@ -58,43 +110,41 @@ object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.MOD_I
val zProtection by double(min = 0.9, default = 0.99) val zProtection by double(min = 0.9, default = 0.99)
} }
object cactus : ConfigCategory(){ object cactus {
val enabled by featureEnable() val enabled by featureEnable()
val size by double(min=0.5, max=1.5, default=0.8).lang("size") 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 sizeVariation by double(max=0.5, default=0.1)
val hOffset by double(max=0.5, default=0.1).lang("hOffset") val hOffset by double(max=0.5, default=0.1).lang("hOffset")
} }
object lilypad : ConfigCategory(){ object lilypad {
val enabled by featureEnable() val enabled by featureEnable()
val hOffset by double(max=0.25, default=0.1).lang("hOffset") val hOffset by double(max=0.25, default=0.1).lang("hOffset")
val flowerChance by int(max=64, default=16, min=0) val flowerChance by int(max=64, default=16, min=0)
} }
object reed : ConfigCategory(){ object reed {
val enabled by featureEnable() val enabled by featureEnable()
val hOffset by double(max=0.4, default=0.2).lang("hOffset") 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 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 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 population by int(max=64, default=32).lang("population")
val minBiomeTemp by double(default=0.4) val biomes by biomeList { it.filterTemp(0.4f, null) && it.filterRain(0.4f, null) }
val minBiomeRainfall by double(default=0.4)
// val biomes by biomeList { it.filterTemp(0.4f, null) && it.filterRain(0.4f, null) }
val shaderWind by boolean(true).lang("shaderWind") val shaderWind by boolean(true).lang("shaderWind")
} }
object algae : ConfigCategory(){ object algae {
val enabled by featureEnable() val enabled by featureEnable()
val hOffset by double(max=0.25, default=0.1).lang("hOffset") 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 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 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 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 population by int(max=64, default=48).lang("population")
// val biomes by biomeList { it.filterClass("river", "ocean") } val biomes by biomeList { it.filterClass("river", "ocean") }
val shaderWind by boolean(true).lang("shaderWind") val shaderWind by boolean(true).lang("shaderWind")
} }
object coral : ConfigCategory(){ object coral {
val enabled by featureEnable() val enabled by featureEnable()
val shallowWater by boolean(false) val shallowWater by boolean(false)
val hOffset by double(max=0.4, default=0.2).lang("hOffset") val hOffset by double(max=0.4, default=0.2).lang("hOffset")
@@ -103,10 +153,10 @@ object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.MOD_I
val crustSize by double(min=0.5, max=1.5, default=1.4) val crustSize by double(min=0.5, max=1.5, default=1.4)
val chance by int(max=64, default=32) val chance by int(max=64, default=32)
val population by int(max=64, default=48).lang("population") val population by int(max=64, default=48).lang("population")
// val biomes by biomeList { it.filterClass("river", "ocean", "beach") } val biomes by biomeList { it.filterClass("river", "ocean", "beach") }
} }
object netherrack : ConfigCategory(){ object netherrack {
val enabled by featureEnable() val enabled by featureEnable()
val hOffset by double(max=0.4, default=0.2).lang("hOffset") 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 heightMin by double(min=0.1, max=1.5, default=0.6).lang("heightMin")
@@ -114,56 +164,44 @@ object Config : DelegatingConfig(BetterFoliageMod.MOD_ID, BetterFoliageMod.MOD_I
val size by double(min=0.5, max=1.5, default=1.0).lang("size") val size by double(min=0.5, max=1.5, default=1.0).lang("size")
} }
object fallingLeaves : ConfigCategory(){ object fallingLeaves {
val enabled by featureEnable() val enabled by featureEnable()
val speed by double(min=0.01, max=0.15, default=0.05) 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 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 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 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.02) 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 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 lifetime by double(min=1.0, max=15.0, default=5.0)
val opacityHack by boolean(true) val opacityHack by boolean(true)
} }
object risingSoul : ConfigCategory(){ object risingSoul {
val enabled by featureEnable() val enabled by featureEnable()
val chance by double(min=0.001, max=1.0, default=0.02) 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 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 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 trailSize by double(min=0.25, max=1.5, default=0.75)
val opacity by double(min=0.05, max=1.0, default=0.5) 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 sizeDecay by double(min=0.5, max=1.0, default=0.97)
val opacityDecay 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 lifetime by double(min=1.0, max=15.0, default=4.0)
val trailLength by int(min=2, max=128, default=48) val trailLength by int(min=2, max=128, default=48)
val trailDensity by int(min=1, max=16, default=3) val trailDensity by int(min=1, max=16, default=3)
} }
}
object BlockConfig { val forceReloadOptions = listOf(
private val list = mutableListOf<Any>() blocks.leavesClasses,
blocks.leavesModels,
blocks.grassClasses,
blocks.grassModels,
shortGrass["saturationThreshold"]!!
)
val leafBlocks = blocks("leaves_blocks_default.cfg") override fun onChange(event: ConfigChangedEvent.PostConfigChangedEvent) {
val leafModels = models("leaves_models_default.cfg") if (hasChanged(forceReloadOptions))
val grassBlocks = blocks("grass_blocks_default.cfg") Minecraft.getMinecraft().refreshResources()
val grassModels = models("grass_models_default.cfg") else
val mycelium = blocks("mycelium_blocks_default.cfg") Minecraft.getMinecraft().renderGlobal.loadRenderers()
// val dirt = blocks("dirt_default.cfg")
val crops = blocks("crop_default.cfg")
val logBlocks = blocks("log_blocks_default.cfg")
val logModels = models("log_models_default.cfg")
val lilypad = blocks("lilypad_default.cfg")
init { BetterFoliageMod.bus.register(this) }
private fun blocks(cfgName: String) = ConfigurableBlockMatcher(BetterFoliage.logDetail, ResourceLocation(BetterFoliageMod.MOD_ID, cfgName)).apply { list.add(this) }
private fun models(cfgName: String) = ModelTextureListConfiguration(BetterFoliage.logDetail, ResourceLocation(BetterFoliageMod.MOD_ID, cfgName)).apply { list.add(this) }
@SubscribeEvent
fun onConfig(event: ModConfig.ModConfigEvent) {
list.forEach { when(it) {
is ConfigurableBlockMatcher -> it.readDefaults()
is ModelTextureListConfiguration -> it.readDefaults()
} }
} }
} }

View File

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

View File

@@ -0,0 +1,29 @@
package mods.betterfoliage.client.gui
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.Config
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiScreen
import net.minecraftforge.fml.client.IModGuiFactory
import net.minecraftforge.fml.client.config.GuiConfig
class ConfigGuiFactory : IModGuiFactory {
override fun initialize(minecraftInstance: Minecraft?) { }
override fun hasConfigGui() = true
override fun runtimeGuiCategories() = hashSetOf<IModGuiFactory.RuntimeOptionCategoryElement>()
override fun createConfigGui(parentScreen: GuiScreen?) = createBFConfigGui(parentScreen)
companion object {
@JvmStatic
fun createBFConfigGui(parentScreen: GuiScreen?) = GuiConfig(
parentScreen,
Config.rootGuiElements,
BetterFoliageMod.MOD_ID,
null,
false,
false,
BetterFoliageMod.MOD_NAME
)
}
}

View File

@@ -1,135 +1,154 @@
package mods.betterfoliage.client.integration package mods.betterfoliage.client.integration
import mods.betterfoliage.BetterFoliage import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.BlockConfig import mods.betterfoliage.client.Client
import mods.betterfoliage.client.render.AsyncLogDiscovery import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.render.LogRegistry
import mods.betterfoliage.client.render.StandardLogRegistry
import mods.betterfoliage.client.render.column.ColumnTextureInfo import mods.betterfoliage.client.render.column.ColumnTextureInfo
import mods.betterfoliage.client.render.column.SimpleColumnInfo import mods.betterfoliage.client.render.column.SimpleColumnInfo
import mods.betterfoliage.client.resource.Identifier
import mods.betterfoliage.client.texture.LeafInfo import mods.betterfoliage.client.texture.LeafInfo
import mods.betterfoliage.client.texture.defaultRegisterLeaf import mods.betterfoliage.client.texture.LeafRegistry
import mods.octarinecore.HasLogger import mods.betterfoliage.client.texture.StandardLeafKey
import mods.octarinecore.Map import mods.betterfoliage.loader.Refs
import mods.octarinecore.ResourceLocation import mods.octarinecore.client.resource.ModelRenderKey
import mods.octarinecore.String import mods.octarinecore.client.resource.ModelRenderRegistry
import mods.octarinecore.client.resource.* import mods.octarinecore.client.resource.ModelRenderRegistryBase
import mods.octarinecore.metaprog.* import mods.octarinecore.getTileEntitySafe
import mods.octarinecore.metaprog.ClassRef.Companion.boolean import mods.octarinecore.metaprog.ClassRef
import net.minecraft.block.BlockState import mods.octarinecore.metaprog.FieldRef
import mods.octarinecore.metaprog.MethodRef
import mods.octarinecore.metaprog.allAvailable
import net.minecraft.block.state.IBlockState
import net.minecraft.client.Minecraft import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.model.ModelBakery import net.minecraft.client.renderer.block.model.ModelResourceLocation
import net.minecraft.resources.IResourceManager import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockReader import net.minecraft.world.IBlockAccess
import net.minecraftforge.fml.ModList import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.client.model.IModel
import net.minecraftforge.fml.common.Loader
import net.minecraftforge.fml.common.eventhandler.EventPriority
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level import org.apache.logging.log4j.Level
import java.util.concurrent.CompletableFuture import kotlin.collections.Map
import kotlin.collections.component1 import kotlin.collections.component1
import kotlin.collections.component2 import kotlin.collections.component2
import kotlin.collections.emptyMap
import kotlin.collections.find
import kotlin.collections.forEach
import kotlin.collections.get
import kotlin.collections.listOf
import kotlin.collections.mapValues
import kotlin.collections.mutableMapOf
import kotlin.collections.set
val TextureLeaves = ClassRef<Any>("forestry.arboriculture.models.TextureLeaves") @SideOnly(Side.CLIENT)
val TextureLeaves_leafTextures = FieldRef(TextureLeaves, "leafTextures", Map)
val TextureLeaves_plain = FieldRef(TextureLeaves, "plain", ResourceLocation)
val TextureLeaves_fancy = FieldRef(TextureLeaves, "fancy", ResourceLocation)
val TextureLeaves_pollinatedPlain = FieldRef(TextureLeaves, "pollinatedPlain", ResourceLocation)
val TextureLeaves_pollinatedFancy = FieldRef(TextureLeaves, "pollinatedFancy", ResourceLocation)
val TileLeaves = ClassRef<Any>("forestry.arboriculture.tiles.TileLeaves")
val TileLeaves_getLeaveSprite = MethodRef(TileLeaves, "getLeaveSprite", ResourceLocation, boolean)
val PropertyWoodType = ClassRef<Any>("forestry.arboriculture.blocks.PropertyWoodType")
val IWoodType = ClassRef<Any>("forestry.api.arboriculture.IWoodType")
val IWoodType_barkTex = MethodRef(IWoodType, "getBarkTexture", String)
val IWoodType_heartTex = MethodRef(IWoodType, "getHeartTexture", String)
val PropertyTreeType = ClassRef<Any>("forestry.arboriculture.blocks.PropertyTreeType")
val IAlleleTreeSpecies = ClassRef<Any>("forestry.api.arboriculture.IAlleleTreeSpecies")
val ILeafSpriteProvider = ClassRef<Any>("forestry.api.arboriculture.ILeafSpriteProvider")
val TreeDefinition = ClassRef<Any>("forestry.arboriculture.genetics.TreeDefinition")
val IAlleleTreeSpecies_getLeafSpriteProvider = MethodRef(IAlleleTreeSpecies, "getLeafSpriteProvider", ILeafSpriteProvider)
val TreeDefinition_species = FieldRef(TreeDefinition, "species", IAlleleTreeSpecies)
val ILeafSpriteProvider_getSprite = MethodRef(ILeafSpriteProvider, "getSprite", ResourceLocation, boolean, boolean)
object ForestryIntegration { object ForestryIntegration {
val TextureLeaves = ClassRef("forestry.arboriculture.models.TextureLeaves")
val TeLleafTextures = FieldRef(TextureLeaves, "leafTextures", Refs.Map)
val TeLplain = FieldRef(TextureLeaves, "plain", Refs.ResourceLocation)
val TeLfancy = FieldRef(TextureLeaves, "fancy", Refs.ResourceLocation)
val TeLpollplain = FieldRef(TextureLeaves, "pollinatedPlain", Refs.ResourceLocation)
val TeLpollfancy = FieldRef(TextureLeaves, "pollinatedFancy", Refs.ResourceLocation)
val TileLeaves = ClassRef("forestry.arboriculture.tiles.TileLeaves")
val TiLgetLeaveSprite = MethodRef(TileLeaves, "getLeaveSprite", Refs.ResourceLocation, ClassRef.boolean)
val PropertyWoodType = ClassRef("forestry.arboriculture.blocks.PropertyWoodType")
val IWoodType = ClassRef("forestry.api.arboriculture.IWoodType")
val barkTex = MethodRef(IWoodType, "getBarkTexture", Refs.String)
val heartTex = MethodRef(IWoodType, "getHeartTexture", Refs.String)
val PropertyTreeType = ClassRef("forestry.arboriculture.blocks.PropertyTreeType")
val TreeDefinition = ClassRef("forestry.arboriculture.genetics.TreeDefinition")
val IAlleleTreeSpecies = ClassRef("forestry.api.arboriculture.IAlleleTreeSpecies")
val ILeafSpriteProvider = ClassRef("forestry.api.arboriculture.ILeafSpriteProvider")
val TdSpecies = FieldRef(TreeDefinition, "species", IAlleleTreeSpecies)
val getLeafSpriteProvider = MethodRef(IAlleleTreeSpecies, "getLeafSpriteProvider", ILeafSpriteProvider)
val getSprite = MethodRef(ILeafSpriteProvider, "getSprite", Refs.ResourceLocation, ClassRef.boolean, ClassRef.boolean)
init { init {
if (ModList.get().isLoaded("forestry") && allAvailable(TileLeaves_getLeaveSprite, IAlleleTreeSpecies_getLeafSpriteProvider, ILeafSpriteProvider_getSprite)) { if (Loader.isModLoaded("forestry") && allAvailable(TiLgetLeaveSprite, getLeafSpriteProvider, getSprite)) {
// Just keep it inactive for now until Forestry updates Client.log(Level.INFO, "Forestry support initialized")
LeafRegistry.addRegistry(ForestryLeafRegistry)
LogRegistry.addRegistry(ForestryLogRegistry)
} }
} }
} }
object ForestryLeafDiscovery : HasLogger, AsyncSpriteProvider<ModelBakery>, ModelRenderRegistry<LeafInfo> { object ForestryLeafRegistry : ModelRenderRegistry<LeafInfo> {
override val logger = BetterFoliage.logDetail val logger = BetterFoliageMod.logDetail
var idToValue = emptyMap<Identifier, LeafInfo>() val textureToKey = mutableMapOf<ResourceLocation, ModelRenderKey<LeafInfo>>()
var textureToValue = emptyMap<ResourceLocation, LeafInfo>()
override fun get(state: BlockState, world: IBlockReader, pos: BlockPos): LeafInfo? { override fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos): LeafInfo? {
// check variant property (used in decorative leaves) // check variant property (used in decorative leaves)
state.values.entries.find { state.properties.entries.find {
PropertyTreeType.isInstance(it.key) && TreeDefinition.isInstance(it.value) ForestryIntegration.PropertyTreeType.isInstance(it.key) && ForestryIntegration.TreeDefinition.isInstance(it.value)
} ?.let { } ?.let {
val species = it.value[TreeDefinition_species] val species = ForestryIntegration.TdSpecies.get(it.value)
val spriteProvider = species[IAlleleTreeSpecies_getLeafSpriteProvider]() val spriteProvider = ForestryIntegration.getLeafSpriteProvider.invoke(species!!)
val textureLoc = spriteProvider[ILeafSpriteProvider_getSprite](false, Minecraft.isFancyGraphicsEnabled()) val textureLoc = ForestryIntegration.getSprite.invoke(spriteProvider!!, false, Minecraft.isFancyGraphicsEnabled())
return idToValue[textureLoc] return textureToValue[textureLoc]
} }
// extract leaf texture information from TileEntity // extract leaf texture information from TileEntity
val tile = world.getTileEntity(pos) ?: return null val tile = world.getTileEntitySafe(pos) ?: return null
if (!TileLeaves.isInstance(tile)) return null if (!ForestryIntegration.TileLeaves.isInstance(tile)) return null
val textureLoc = tile[TileLeaves_getLeaveSprite](Minecraft.isFancyGraphicsEnabled()) val textureLoc = ForestryIntegration.TiLgetLeaveSprite.invoke(tile, Minecraft.isFancyGraphicsEnabled()) ?: return null
return idToValue[textureLoc] return textureToValue[textureLoc]
} }
override fun setup(manager: IResourceManager, bakeryF: CompletableFuture<ModelBakery>, atlasFuture: AtlasFuture): StitchPhases { @SubscribeEvent
val futures = mutableMapOf<Identifier, CompletableFuture<LeafInfo>>() fun handlePreStitch(event: TextureStitchEvent.Pre) {
textureToValue = emptyMap()
return StitchPhases( val allLeaves = ForestryIntegration.TeLleafTextures.getStatic() as Map<*, *>
discovery = bakeryF.thenRunAsync { allLeaves.entries.forEach {
val allLeaves = TextureLeaves_leafTextures.getStatic() logger.log(Level.DEBUG, "ForestryLeavesSupport: base leaf type ${it.key.toString()}")
allLeaves.entries.forEach { (type, leaves) ->
log("base leaf type $type")
leaves!!
listOf( listOf(
leaves[TextureLeaves_plain], leaves[TextureLeaves_pollinatedPlain], ForestryIntegration.TeLplain.get(it.value) as ResourceLocation,
leaves[TextureLeaves_fancy], leaves[TextureLeaves_pollinatedFancy] ForestryIntegration.TeLfancy.get(it.value) as ResourceLocation,
ForestryIntegration.TeLpollplain.get(it.value) as ResourceLocation,
ForestryIntegration.TeLpollfancy.get(it.value) as ResourceLocation
).forEach { textureLocation -> ).forEach { textureLocation ->
futures[textureLocation] = defaultRegisterLeaf(textureLocation, atlasFuture) val key = StandardLeafKey(logger, textureLocation.toString()).apply { onPreStitch(event.map) }
textureToKey[textureLocation] = key
} }
} }
},
cleanup = atlasFuture.runAfter {
idToValue = futures.mapValues { it.value.get() }
} }
)
@SubscribeEvent(priority = EventPriority.LOW)
fun handlePostStitch(event: TextureStitchEvent.Post) {
textureToValue = textureToKey.mapValues { (_, key) -> key.resolveSprites(event.map) }
textureToKey.clear()
} }
} }
object ForestryLogDiscovery : ModelDiscovery<ColumnTextureInfo>() { object ForestryLogRegistry : ModelRenderRegistryBase<ColumnTextureInfo>() {
override val logger = BetterFoliage.logDetail override val logger = BetterFoliageMod.logDetail
override fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo>? {
override fun processModel(state: IBlockState, modelLoc: ModelResourceLocation, model: IModel): ModelRenderKey<ColumnTextureInfo>? {
// respect class list to avoid triggering on fences, stairs, etc. // respect class list to avoid triggering on fences, stairs, etc.
if (!BlockConfig.logBlocks.matchesClass(ctx.state.block)) return null if (!Config.blocks.logClasses.matchesClass(state.block)) return null
// find wood type property // find wood type property
val woodType = ctx.state.values.entries.find { val woodType = state.properties.entries.find {
PropertyWoodType.isInstance(it.key) && IWoodType.isInstance(it.value) ForestryIntegration.PropertyWoodType.isInstance(it.key) && ForestryIntegration.IWoodType.isInstance(it.value)
} } ?: return null
if (woodType != null) {
logger.log(Level.DEBUG, "ForestryLogRegistry: block state ${ctx.state}") logger.log(Level.DEBUG, "ForestryLogRegistry: block state $state")
logger.log(Level.DEBUG, "ForestryLogRegistry: variant ${woodType.value}") logger.log(Level.DEBUG, "ForestryLogRegistry: variant ${woodType.value}")
// get texture names for wood type // get texture names for wood type
val bark = woodType.value[IWoodType_barkTex]() val bark = ForestryIntegration.barkTex.invoke(woodType.value) as String?
val heart = woodType.value[IWoodType_heartTex]() val heart = ForestryIntegration.heartTex.invoke(woodType.value) as String?
logger.log(Level.DEBUG, "ForestryLogSupport: textures [heart=$heart, bark=$bark]")
val heartSprite = atlas.sprite(heart) logger.log(Level.DEBUG, "ForestryLogSupport: textures [heart=$heart, bark=$bark]")
val barkSprite = atlas.sprite(bark) if (bark != null && heart != null) return SimpleColumnInfo.Key(logger, StandardLogRegistry.getAxis(state), listOf(heart, heart, bark))
return atlas.mapAfter {
SimpleColumnInfo(AsyncLogDiscovery.getAxis(ctx.state), heartSprite.get(), heartSprite.get(), listOf(barkSprite.get()))
}
}
return null return null
} }
} }

View File

@@ -1,49 +1,60 @@
package mods.betterfoliage.client.integration package mods.betterfoliage.client.integration
import mods.betterfoliage.BetterFoliage import mods.betterfoliage.client.Client
import mods.octarinecore.* import mods.betterfoliage.loader.Refs
import mods.octarinecore.client.render.CombinedContext import mods.octarinecore.ThreadLocalDelegate
import mods.octarinecore.client.render.BlockContext
import mods.octarinecore.common.Int3
import mods.octarinecore.metaprog.allAvailable import mods.octarinecore.metaprog.allAvailable
import mods.octarinecore.metaprog.reflectField import mods.octarinecore.metaprog.reflectField
import net.minecraft.block.BlockState import net.minecraft.block.state.IBlockState
import net.minecraft.client.Minecraft import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.model.BakedQuad import net.minecraft.client.renderer.block.model.BakedQuad
import net.minecraft.client.renderer.vertex.DefaultVertexFormats import net.minecraft.client.renderer.vertex.DefaultVertexFormats
import net.minecraft.util.Direction.UP import net.minecraft.util.EnumFacing
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockAccess
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level import org.apache.logging.log4j.Level
/** /**
* Integration for OptiFine custom block colors. * Integration for OptiFine custom block colors.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@SideOnly(Side.CLIENT)
object OptifineCustomColors { object OptifineCustomColors {
val isColorAvailable = allAvailable(CustomColors, CustomColors.getColorMultiplier) val isColorAvailable = allAvailable(
Refs.CustomColors, Refs.getColorMultiplier
)
init { init {
BetterFoliage.log(Level.INFO, "Optifine custom color support is ${if (isColorAvailable) "enabled" else "disabled" }") Client.log(Level.INFO, "Optifine custom color support is ${if (isColorAvailable) "enabled" else "disabled" }")
} }
val renderEnv by ThreadLocalDelegate { OptifineRenderEnv() } val renderEnv by ThreadLocalDelegate { OptifineRenderEnv() }
val fakeQuad = BakedQuad(IntArray(0), 1, UP, null, true, DefaultVertexFormats.BLOCK) val fakeQuad = BakedQuad(IntArray(0), 1, EnumFacing.UP, null, true, DefaultVertexFormats.BLOCK)
fun getBlockColor(ctx: CombinedContext): Int { fun getBlockColor(ctx: BlockContext): Int {
val ofColor = if (isColorAvailable && Minecraft.getInstance().gameSettings.reflectField<Boolean>("ofCustomColors") == true) { val ofColor = if (isColorAvailable && Minecraft.getMinecraft().gameSettings.reflectField<Boolean>("ofCustomColors") == true) {
renderEnv.reset(ctx.state, ctx.pos) renderEnv.reset(ctx.blockState(Int3.zero), ctx.pos)
CustomColors.getColorMultiplier.invokeStatic(fakeQuad, ctx.state, ctx.world, ctx.pos, renderEnv.wrapped) as? Int Refs.getColorMultiplier.invokeStatic(fakeQuad, ctx.blockState(Int3.zero), ctx.world!!, ctx.pos, renderEnv.wrapped) as? Int
} else null } else null
return if (ofColor == null || ofColor == -1) ctx.lightingCtx.color else ofColor return if (ofColor == null || ofColor == -1) ctx.blockData(Int3.zero).color else ofColor
} }
} }
@SideOnly(Side.CLIENT)
class OptifineRenderEnv { class OptifineRenderEnv {
val wrapped: Any = RenderEnv.element!!.getDeclaredConstructor(BlockState.element, BlockPos.element).let { val wrapped: Any = Refs.RenderEnv.element!!.getDeclaredConstructor(
Refs.IBlockState.element, Refs.BlockPos.element
).let {
it.isAccessible = true it.isAccessible = true
it.newInstance(null, null) it.newInstance(null, null)
} }
fun reset(state: BlockState, pos: BlockPos) { fun reset(state: IBlockState, pos: BlockPos) {
RenderEnv.reset.invoke(wrapped, state, pos) Refs.RenderEnv_reset.invoke(wrapped, state, pos)
} }
} }

View File

@@ -1,162 +1,147 @@
package mods.betterfoliage.client.integration package mods.betterfoliage.client.integration
import mods.betterfoliage.BetterFoliage import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.render.LogRegistry import mods.betterfoliage.client.render.LogRegistry
import mods.betterfoliage.client.render.column.ColumnTextureInfo import mods.betterfoliage.client.render.column.ColumnTextureInfo
import mods.betterfoliage.client.render.column.SimpleColumnInfo import mods.betterfoliage.client.render.column.SimpleColumnInfo
import mods.betterfoliage.client.resource.Sprite
import mods.octarinecore.client.render.CombinedContext
import mods.octarinecore.client.render.Quad import mods.octarinecore.client.render.Quad
import mods.octarinecore.client.render.lighting.QuadIconResolver import mods.octarinecore.client.render.QuadIconResolver
import mods.octarinecore.client.render.ShadingContext
import mods.octarinecore.client.render.blockContext
import mods.octarinecore.client.resource.* import mods.octarinecore.client.resource.*
import mods.octarinecore.common.rotate import mods.octarinecore.common.rotate
import mods.octarinecore.metaprog.ClassRef import mods.octarinecore.metaprog.ClassRef
import mods.octarinecore.metaprog.allAvailable import mods.octarinecore.metaprog.allAvailable
import net.minecraft.client.renderer.model.BlockModel import net.minecraft.block.state.IBlockState
import net.minecraft.util.Direction import net.minecraft.client.renderer.block.model.ModelResourceLocation
import net.minecraft.util.Direction.* import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.client.renderer.texture.TextureMap
import net.minecraft.util.EnumFacing
import net.minecraft.util.ResourceLocation import net.minecraft.util.ResourceLocation
import net.minecraftforge.fml.ModList import net.minecraftforge.client.model.IModel
import net.minecraftforge.fml.common.Loader
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level import org.apache.logging.log4j.Level
import java.util.concurrent.CompletableFuture import org.apache.logging.log4j.Logger
@SideOnly(Side.CLIENT)
object IC2RubberIntegration { object IC2RubberIntegration {
val BlockRubWood = ClassRef<Any>("ic2.core.block.BlockRubWood") val BlockRubWood = ClassRef("ic2.core.block.BlockRubWood")
init { init {
if (ModList.get().isLoaded("ic2") && allAvailable(BlockRubWood)) { if (Loader.isModLoaded("ic2") && allAvailable(BlockRubWood)) {
// keep it inactive for now until IC2 updates Client.log(Level.INFO, "IC2 rubber support initialized")
// BetterFoliage.log(Level.INFO, "IC2 rubber support initialized") LogRegistry.addRegistry(IC2LogSupport)
// LogRegistry.registries.add(IC2LogDiscovery)
// BetterFoliage.blockSprites.providers.add(IC2LogDiscovery)
} }
} }
} }
// Probably unneeded, as TechReborn went Fabric-only @SideOnly(Side.CLIENT)
/*
object TechRebornRubberIntegration { object TechRebornRubberIntegration {
val BlockRubberLog = ClassRef<Any>("techreborn.blocks.BlockRubberLog") val BlockRubberLog = ClassRef("techreborn.blocks.BlockRubberLog")
init { init {
if (ModList.get().isLoaded("techreborn") && allAvailable(BlockRubberLog)) { if (Loader.isModLoaded("techreborn") && allAvailable(BlockRubberLog)) {
BetterFoliage.log(Level.INFO, "TechReborn rubber support initialized") Client.log(Level.INFO, "TechReborn rubber support initialized")
LogRegistry.registries.add(TechRebornLogDiscovery) LogRegistry.addRegistry(TechRebornLogSupport)
BetterFoliage.blockSprites.providers.add(TechRebornLogDiscovery)
} }
} }
} }
*/
class RubberLogInfo( class RubberLogInfo(
axis: Axis?, axis: EnumFacing.Axis?,
val spotDir: Direction, val spotDir: EnumFacing,
topTexture: Sprite, topTexture: TextureAtlasSprite,
bottomTexture: Sprite, bottomTexture: TextureAtlasSprite,
val spotTexture: Sprite, val spotTexture: TextureAtlasSprite,
sideTextures: List<Sprite> sideTextures: List<TextureAtlasSprite>
) : SimpleColumnInfo(axis, topTexture, bottomTexture, sideTextures) { ) : SimpleColumnInfo(axis, topTexture, bottomTexture, sideTextures) {
override val side: QuadIconResolver = { ctx: CombinedContext, idx: Int, quad: Quad -> override val side: QuadIconResolver = { ctx: ShadingContext, idx: Int, quad: Quad ->
val worldFace = (if ((idx and 1) == 0) SOUTH else EAST).rotate(ctx.modelRotation) val worldFace = (if ((idx and 1) == 0) EnumFacing.SOUTH else EnumFacing.EAST).rotate(ctx.rotation)
if (worldFace == spotDir) spotTexture else { if (worldFace == spotDir) spotTexture else {
val sideIdx = if (this.sideTextures.size > 1) (ctx.semiRandom(1) + dirToIdx[worldFace.ordinal]) % this.sideTextures.size else 0 val sideIdx = if (this.sideTextures.size > 1) (blockContext.random(1) + dirToIdx[worldFace.ordinal]) % this.sideTextures.size else 0
this.sideTextures[sideIdx] this.sideTextures[sideIdx]
} }
} }
class Key(override val logger: Logger, val axis: EnumFacing.Axis?, val spotDir: EnumFacing, val textures: List<String>): ModelRenderKey<ColumnTextureInfo> {
override fun resolveSprites(atlas: TextureMap) = RubberLogInfo(
axis,
spotDir,
atlas[textures[0]] ?: atlas.missingSprite,
atlas[textures[1]] ?: atlas.missingSprite,
atlas[textures[2]] ?: atlas.missingSprite,
textures.drop(3).map { atlas[it] ?: atlas.missingSprite }
)
}
} }
object IC2LogDiscovery : ModelDiscovery<ColumnTextureInfo>() { object IC2LogSupport : ModelRenderRegistryBase<ColumnTextureInfo>() {
override val logger = BetterFoliage.logDetail override val logger = BetterFoliageMod.logDetail
override fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo>? { override fun processModel(state: IBlockState, modelLoc: ModelResourceLocation, model: IModel): ModelRenderKey<ColumnTextureInfo>? {
// check for proper block class, existence of ModelBlock, and "state" blockstate property // check for proper block class, existence of ModelBlock, and "state" blockstate property
if (!IC2RubberIntegration.BlockRubWood.isInstance(ctx.state.block)) return null if (!IC2RubberIntegration.BlockRubWood.isInstance(state.block)) return null
val blockLoc = ctx.models.firstOrNull() as Pair<BlockModel, ResourceLocation> ?: return null val blockLoc = model.modelBlockAndLoc.firstOrNull() ?: return null
val type = ctx.state.values.entries.find { it.key.getName() == "state" }?.value?.toString() ?: return null val type = state.properties.entries.find { it.key.getName() == "state" }?.value?.toString() ?: return null
// logs with no rubber spot // logs with no rubber spot
if (blockLoc.derivesFrom(ResourceLocation("block/cube_column"))) { if (blockLoc.derivesFrom(ResourceLocation("block/cube_column"))) {
val axis = when(type) { val axis = when(type) {
"plain_y" -> Axis.Y "plain_y" -> EnumFacing.Axis.Y
"plain_x" -> Axis.X "plain_x" -> EnumFacing.Axis.X
"plain_z" -> Axis.Z "plain_z" -> EnumFacing.Axis.Z
else -> null else -> null
} }
val textureNames = listOf("end", "side").map { blockLoc.first.resolveTextureName(it) } val textureNames = listOf("end", "end", "side").map { blockLoc.first.resolveTextureName(it) }
if (textureNames.any { it == "missingno" }) return null if (textureNames.any { it == "missingno" }) return null
log("IC2LogSupport: block state ${ctx.state.toString()}") logger.log(Level.DEBUG, "IC2LogSupport: block state ${state.toString()}")
log("IC2LogSupport: axis=$axis, end=${textureNames[0]}, side=${textureNames[1]}") logger.log(Level.DEBUG, "IC2LogSupport: axis=$axis, end=${textureNames[0]}, side=${textureNames[2]}")
val endSprite = atlas.sprite(textureNames[0]) return SimpleColumnInfo.Key(logger, axis, textureNames)
val sideSprite = atlas.sprite(textureNames[1])
return atlas.mapAfter {
SimpleColumnInfo(axis, endSprite.get(), endSprite.get(), listOf(sideSprite.get()))
}
} }
// logs with rubber spot // logs with rubber spot
val spotDir = when(type) { val spotDir = when(type) {
"dry_north", "wet_north" -> NORTH "dry_north", "wet_north" -> EnumFacing.NORTH
"dry_south", "wet_south" -> SOUTH "dry_south", "wet_south" -> EnumFacing.SOUTH
"dry_west", "wet_west" -> WEST "dry_west", "wet_west" -> EnumFacing.WEST
"dry_east", "wet_east" -> EAST "dry_east", "wet_east" -> EnumFacing.EAST
else -> null else -> null
} }
val textureNames = listOf("up", "down", "north", "south").map { blockLoc.first.resolveTextureName(it) } val textureNames = listOf("up", "down", "north", "south").map { blockLoc.first.resolveTextureName(it) }
if (textureNames.any { it == "missingno" }) return null if (textureNames.any { it == "missingno" }) return null
log("IC2LogSupport: block state ${ctx.state.toString()}") logger.log(Level.DEBUG, "IC2LogSupport: block state ${state.toString()}")
log("IC2LogSupport: spotDir=$spotDir, up=${textureNames[0]}, down=${textureNames[1]}, side=${textureNames[2]}, spot=${textureNames[3]}") logger.log(Level.DEBUG, "IC2LogSupport: spotDir=$spotDir, up=${textureNames[0]}, down=${textureNames[1]}, side=${textureNames[2]}, spot=${textureNames[3]}")
val upSprite = atlas.sprite(textureNames[0]) return if (spotDir != null) RubberLogInfo.Key(logger, EnumFacing.Axis.Y, spotDir, textureNames) else SimpleColumnInfo.Key(logger, EnumFacing.Axis.Y, textureNames)
val downSprite = atlas.sprite(textureNames[1])
val sideSprite = atlas.sprite(textureNames[2])
val spotSprite = atlas.sprite(textureNames[3])
return if (spotDir != null) atlas.mapAfter {
RubberLogInfo(Axis.Y, spotDir, upSprite.get(), downSprite.get(), spotSprite.get(), listOf(sideSprite.get()))
} else atlas.mapAfter {
SimpleColumnInfo(Axis.Y, upSprite.get(), downSprite.get(), listOf(sideSprite.get()))
}
} }
} }
/* object TechRebornLogSupport : ModelRenderRegistryBase<ColumnTextureInfo>() {
object TechRebornLogDiscovery : ModelDiscovery<ColumnTextureInfo>() { override val logger = BetterFoliageMod.logDetail
override val logger = BetterFoliage.logDetail
override fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo>? { override fun processModel(state: IBlockState, modelLoc: ModelResourceLocation, model: IModel): ModelRenderKey<ColumnTextureInfo>? {
// check for proper block class, existence of ModelBlock // check for proper block class, existence of ModelBlock
if (!TechRebornRubberIntegration.BlockRubberLog.isInstance(ctx.state.block)) return null if (!TechRebornRubberIntegration.BlockRubberLog.isInstance(state.block)) return null
val blockLoc = ctx.models.map { it as? Pair<BlockModel, ResourceLocation> }.firstOrNull() ?: return null val blockLoc = model.modelBlockAndLoc.firstOrNull() ?: return null
val hasSap = ctx.state.values.entries.find { it.key.getName() == "hassap" }?.value as? Boolean ?: return null val hasSap = state.properties.entries.find { it.key.getName() == "hassap" }?.value as? Boolean ?: return null
val sapSide = ctx.state.values.entries.find { it.key.getName() == "sapside" }?.value as? Direction ?: return null val sapSide = state.properties.entries.find { it.key.getName() == "sapside" }?.value as? EnumFacing ?: return null
log("$logName: block state ${ctx.state}") logger.log(Level.DEBUG, "$logName: block state $state")
if (hasSap) { if (hasSap) {
val textureNames = listOf("end", "side", "sapside").map { blockLoc.first.resolveTextureName(it) } val textureNames = listOf("end", "end", "sapside", "side").map { blockLoc.first.resolveTextureName(it) }
log("$logName: spotDir=$sapSide, end=${textureNames[0]}, side=${textureNames[2]}, spot=${textureNames[3]}") logger.log(Level.DEBUG, "$logName: spotDir=$sapSide, end=${textureNames[0]}, side=${textureNames[2]}, spot=${textureNames[3]}")
if (textureNames.all { it != "missingno" }) { if (textureNames.all { it != "missingno" }) return RubberLogInfo.Key(logger, EnumFacing.Axis.Y, sapSide, textureNames)
val endSprite = atlas.sprite(textureNames[0])
val sideSprite = atlas.sprite(textureNames[1])
val sapSprite = atlas.sprite(textureNames[2])
return atlas.mapAfter {
RubberLogInfo(Axis.Y, sapSide, endSprite.get(), endSprite.get(), sapSprite.get(), listOf(sideSprite.get()))
}
}
} else { } else {
val textureNames = listOf("end", "side").map { blockLoc.first.resolveTextureName(it) } val textureNames = listOf("end", "end", "side").map { blockLoc.first.resolveTextureName(it) }
log("$logName: end=${textureNames[0]}, side=${textureNames[1]}") logger.log(Level.DEBUG, "$logName: end=${textureNames[0]}, side=${textureNames[2]}")
if (textureNames.all { it != "missingno" }) { if (textureNames.all { it != "missingno" })return SimpleColumnInfo.Key(logger, EnumFacing.Axis.Y, textureNames)
val endSprite = atlas.sprite(textureNames[0])
val sideSprite = atlas.sprite(textureNames[1])
return atlas.mapAfter {
SimpleColumnInfo(Axis.Y, endSprite.get(), endSprite.get(), listOf(sideSprite.get()))
}
}
} }
return null return null
} }
} }
*/

View File

@@ -1,68 +1,81 @@
package mods.betterfoliage.client.integration package mods.betterfoliage.client.integration
import mods.betterfoliage.BetterFoliage import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.BlockConfig
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.texture.GrassRegistry import mods.betterfoliage.loader.Refs
import mods.betterfoliage.client.texture.LeafRegistry
import mods.octarinecore.*
import mods.octarinecore.client.render.CombinedContext
import mods.octarinecore.metaprog.allAvailable import mods.octarinecore.metaprog.allAvailable
import mods.octarinecore.metaprog.get import net.minecraft.block.Block
import net.minecraft.block.BlockRenderType import net.minecraft.block.BlockTallGrass
import net.minecraft.block.BlockRenderType.MODEL import net.minecraft.block.state.IBlockState
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.client.renderer.BufferBuilder import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.math.BlockPos import net.minecraft.init.Blocks
import net.minecraft.world.IEnviromentBlockReader import net.minecraft.util.EnumBlockRenderType
import net.minecraft.util.EnumBlockRenderType.MODEL
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level.INFO import org.apache.logging.log4j.Level.INFO
/** /**
* Integration for ShadersMod. * Integration for ShadersMod.
*/ */
@SideOnly(Side.CLIENT)
object ShadersModIntegration { object ShadersModIntegration {
@JvmStatic val isAvailable = allAvailable(SVertexBuilder, SVertexBuilder.pushState, SVertexBuilder.pushNum, SVertexBuilder.pop) @JvmStatic val isAvailable = allAvailable(Refs.sVertexBuilder, Refs.pushEntity_state, Refs.pushEntity_num, Refs.popEntity)
val defaultLeaves = Blocks.OAK_LEAVES.defaultState val grassDefaultBlockId = blockIdFor(Blocks.TALLGRASS.defaultState.withProperty(BlockTallGrass.TYPE, BlockTallGrass.EnumType.GRASS))
val defaultGrass = Blocks.GRASS.defaultState val leavesDefaultBlockId = blockIdFor(Blocks.LEAVES.defaultState)
fun blockIdFor(blockState: IBlockState) = Block.REGISTRY.getIDForObject(blockState.block).toLong() and 65535
// fun entityDataFor(blockState: IBlockState) =
// (Block.REGISTRY.getIDForObject(blockState.block).toLong() and 65535) //or
// ((blockState.renderType.ordinal.toLong() and 65535) shl 16) or
// (blockState.block.getMetaFromState(blockState).toLong() shl 32)
fun logEntityData(name: String, blockState: IBlockState) {
val blockId = Block.REGISTRY.getIDForObject(blockState.block).toLong() and 65535
val meta = blockState.renderType.ordinal.toLong() and 65535
val renderType = blockState.renderType.ordinal.toLong() and 65535
Client.log(INFO, "ShadersMod integration for $name")
Client.log(INFO, " blockState=$blockState")
Client.log(INFO, " blockId=$blockId, meta=$meta, type=$renderType")
}
/** /**
* Called from transformed ShadersMod code. * Called from transformed ShadersMod code.
* @see mods.betterfoliage.loader.BetterFoliageTransformer * @see mods.betterfoliage.loader.BetterFoliageTransformer
*/ */
@JvmStatic fun getBlockStateOverride(state: BlockState, world: IEnviromentBlockReader, pos: BlockPos): BlockState { @JvmStatic fun getBlockIdOverride(original: Long, blockState: IBlockState): Long {
if (LeafRegistry[state, world, pos] != null) return defaultLeaves if (Config.blocks.leavesClasses.matchesClass(blockState.block)) return Config.shaders.leavesId
if (BlockConfig.crops.matchesClass(state.block)) return defaultGrass if (Config.blocks.crops.matchesClass(blockState.block)) return Config.shaders.grassId
return state return original
} }
init { init {
BetterFoliage.log(INFO, "ShadersMod integration is ${if (isAvailable) "enabled" else "disabled" }") Client.log(INFO, "ShadersMod integration is ${if (isAvailable) "enabled" else "disabled" }")
} }
inline fun renderAs(ctx: CombinedContext, renderType: BlockRenderType, enabled: Boolean = true, func: ()->Unit) =
renderAs(ctx, ctx.state, renderType, enabled, func)
/** Quads rendered inside this block will use the given block entity data in shader programs. */ /** Quads rendered inside this block will use the given block entity data in shader programs. */
inline fun renderAs(ctx: CombinedContext, state: BlockState, renderType: BlockRenderType, enabled: Boolean = true, func: ()->Unit) { inline fun renderAs(blockId: Long, renderType: EnumBlockRenderType, renderer: BufferBuilder, enabled: Boolean = true, func: ()->Unit) {
if (isAvailable && enabled) { val blockData = blockId or (renderType.ordinal shl 16).toLong()
val buffer = ctx.renderCtx.renderBuffer if ((isAvailable && enabled)) {
val sVertexBuilder = buffer[BufferBuilder_sVertexBuilder] val vertexBuilder = Refs.sVertexBuilder.get(renderer)!!
SVertexBuilder.pushState.invoke(sVertexBuilder, ctx.state, ctx.pos, ctx.world, buffer) Refs.pushEntity_num.invoke(vertexBuilder, blockId)
func() func()
SVertexBuilder.pop.invoke(sVertexBuilder) Refs.popEntity.invoke(vertexBuilder)
} else { } else {
func() func()
} }
} }
/** Quads rendered inside this block will use the given block entity data in shader programs. */
inline fun renderAs(state: IBlockState, renderType: EnumBlockRenderType, renderer: BufferBuilder, enabled: Boolean = true, func: ()->Unit) =
renderAs(blockIdFor(state), renderType, renderer, enabled, func)
/** Quads rendered inside this block will behave as tallgrass blocks in shader programs. */ /** Quads rendered inside this block will behave as tallgrass blocks in shader programs. */
inline fun grass(ctx: CombinedContext, enabled: Boolean = true, func: ()->Unit) = inline fun grass(renderer: BufferBuilder, enabled: Boolean = true, func: ()->Unit) =
renderAs(ctx, defaultGrass, MODEL, enabled, func) renderAs(Config.shaders.grassId, MODEL, renderer, enabled, func)
/** Quads rendered inside this block will behave as leaf blocks in shader programs. */ /** Quads rendered inside this block will behave as leaf blocks in shader programs. */
inline fun leaves(ctx: CombinedContext, enabled: Boolean = true, func: ()->Unit) = inline fun leaves(renderer: BufferBuilder, enabled: Boolean = true, func: ()->Unit) =
renderAs(ctx, defaultLeaves, MODEL, enabled, func) renderAs(Config.shaders.leavesId, MODEL, renderer, enabled, func)
} }

View File

@@ -5,7 +5,7 @@ import mods.betterfoliage.client.texture.LeafParticleRegistry
import mods.betterfoliage.client.texture.LeafRegistry import mods.betterfoliage.client.texture.LeafRegistry
import mods.octarinecore.PI2 import mods.octarinecore.PI2
import mods.octarinecore.client.render.AbstractEntityFX import mods.octarinecore.client.render.AbstractEntityFX
import mods.octarinecore.client.render.lighting.HSB import mods.octarinecore.client.render.HSB
import mods.octarinecore.common.Double3 import mods.octarinecore.common.Double3
import mods.octarinecore.minmax import mods.octarinecore.minmax
import mods.octarinecore.random import mods.octarinecore.random
@@ -15,22 +15,18 @@ import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.MathHelper import net.minecraft.util.math.MathHelper
import net.minecraft.world.World import net.minecraft.world.World
import net.minecraftforge.common.MinecraftForge import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.event.TickEvent
import net.minecraftforge.event.world.WorldEvent import net.minecraftforge.event.world.WorldEvent
import net.minecraftforge.eventbus.api.SubscribeEvent import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.common.gameevent.TickEvent
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.lwjgl.opengl.GL11 import org.lwjgl.opengl.GL11
import java.lang.Math.*
import java.util.* import java.util.*
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
const val rotationFactor = PI2.toFloat() / 64.0f @SideOnly(Side.CLIENT)
class EntityFallingLeavesFX(world: World, pos: BlockPos) :
class EntityFallingLeavesFX( AbstractEntityFX(world, pos.x.toDouble() + 0.5, pos.y.toDouble(), pos.z.toDouble() + 0.5) {
world: World, pos: BlockPos
) : AbstractEntityFX(
world, pos.x.toDouble() + 0.5, pos.y.toDouble(), pos.z.toDouble() + 0.5
) {
companion object { companion object {
@JvmStatic val biomeBrightnessMultiplier = 0.5f @JvmStatic val biomeBrightnessMultiplier = 0.5f
@@ -42,40 +38,38 @@ class EntityFallingLeavesFX(
var wasCollided = false var wasCollided = false
init { init {
maxAge = MathHelper.floor(random(0.6, 1.0) * Config.fallingLeaves.lifetime * 20.0) particleMaxAge = MathHelper.floor(random(0.6, 1.0) * Config.fallingLeaves.lifetime * 20.0)
motionY = -Config.fallingLeaves.speed motionY = -Config.fallingLeaves.speed
particleScale = Config.fallingLeaves.size.toFloat() * 0.1f particleScale = Config.fallingLeaves.size.toFloat() * 0.1f
val state = world.getBlockState(pos) val state = world.getBlockState(pos)
val blockColor = Minecraft.getMinecraft().blockColors.colorMultiplier(state, world, pos, 0)
val leafInfo = LeafRegistry[state, world, pos] val leafInfo = LeafRegistry[state, world, pos]
val blockColor = Minecraft.getInstance().blockColors.getColor(state, world, pos, 0)
if (leafInfo != null) { if (leafInfo != null) {
sprite = leafInfo.particleTextures[rand.nextInt(1024)] particleTexture = leafInfo.particleTextures[rand.nextInt(1024)]
calculateParticleColor(leafInfo.averageColor, blockColor) calculateParticleColor(leafInfo.averageColor, blockColor)
} else { } else {
sprite = LeafParticleRegistry["default"][rand.nextInt(1024)] particleTexture = LeafParticleRegistry["default"][rand.nextInt(1024)]
setColor(blockColor) setColor(blockColor)
} }
} }
override val isValid: Boolean get() = (sprite != null) override val isValid: Boolean get() = (particleTexture != null)
override fun update() { override fun update() {
if (rand.nextFloat() > 0.95f) rotPositive = !rotPositive if (rand.nextFloat() > 0.95f) rotPositive = !rotPositive
if (age > maxAge - 20) particleAlpha = 0.05f * (maxAge - age) if (particleAge > particleMaxAge - 20) particleAlpha = 0.05f * (particleMaxAge - particleAge)
if (onGround || wasCollided) { if (onGround || wasCollided) {
velocity.setTo(0.0, 0.0, 0.0) velocity.setTo(0.0, 0.0, 0.0)
if (!wasCollided) { if (!wasCollided) {
age = Math.max(age, maxAge - 20) particleAge = Math.max(particleAge, particleMaxAge - 20)
wasCollided = true wasCollided = true
} }
} else { } else {
velocity.setTo(cos[particleRot], 0.0, sin[particleRot]).mul(Config.fallingLeaves.perturb) 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) .add(LeafWindTracker.current).add(0.0, -1.0, 0.0).mul(Config.fallingLeaves.speed)
particleRot = (particleRot + (if (rotPositive) 1 else -1)) and 63 particleRot = (particleRot + (if (rotPositive) 1 else -1)) and 63
particleAngle = rotationFactor * particleRot.toFloat()
} }
} }
@@ -102,6 +96,7 @@ class EntityFallingLeavesFX(
} }
} }
@SideOnly(Side.CLIENT)
object LeafWindTracker { object LeafWindTracker {
var random = Random() var random = Random()
val target = Double3.zero val target = Double3.zero
@@ -113,7 +108,7 @@ object LeafWindTracker {
} }
fun changeWind(world: World) { fun changeWind(world: World) {
nextChange = world.worldInfo.gameTime + 120 + random.nextInt(80) nextChange = world.worldInfo.worldTime + 120 + random.nextInt(80)
val direction = PI2 * random.nextDouble() val direction = PI2 * random.nextDouble()
val speed = abs(random.nextGaussian()) * Config.fallingLeaves.windStrength + val speed = abs(random.nextGaussian()) * Config.fallingLeaves.windStrength +
(if (!world.isRaining) 0.0 else abs(random.nextGaussian()) * Config.fallingLeaves.stormStrength) (if (!world.isRaining) 0.0 else abs(random.nextGaussian()) * Config.fallingLeaves.stormStrength)
@@ -122,9 +117,9 @@ object LeafWindTracker {
@SubscribeEvent @SubscribeEvent
fun handleWorldTick(event: TickEvent.ClientTickEvent) { fun handleWorldTick(event: TickEvent.ClientTickEvent) {
if (event.phase == TickEvent.Phase.START) Minecraft.getInstance().world?.let { world -> if (event.phase == TickEvent.Phase.START) Minecraft.getMinecraft().world?.let { world ->
// change target wind speed // change target wind speed
if (world.worldInfo.dayTime >= nextChange) changeWind(world) if (world.worldInfo.worldTime >= nextChange) changeWind(world)
// change current wind speed // change current wind speed
val changeRate = if (world.isRaining) 0.015 else 0.005 val changeRate = if (world.isRaining) 0.015 else 0.005
@@ -137,5 +132,5 @@ object LeafWindTracker {
} }
@SubscribeEvent @SubscribeEvent
fun handleWorldLoad(event: WorldEvent.Load) { if (event.world.isRemote) changeWind(event.world.world) } fun handleWorldLoad(event: WorldEvent.Load) { if (event.world.isRemote) changeWind(event.world) }
} }

View File

@@ -1,23 +1,22 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.resource.Identifier
import mods.octarinecore.client.render.AbstractEntityFX import mods.octarinecore.client.render.AbstractEntityFX
import mods.octarinecore.client.resource.Atlas
import mods.octarinecore.client.resource.ResourceHandler import mods.octarinecore.client.resource.ResourceHandler
import mods.octarinecore.common.Double3 import mods.octarinecore.common.Double3
import mods.octarinecore.forEachPairIndexed import mods.octarinecore.forEachPairIndexed
import net.minecraft.client.renderer.BufferBuilder import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.MathHelper import net.minecraft.util.math.MathHelper
import net.minecraft.world.World import net.minecraft.world.World
import org.apache.logging.log4j.Level.DEBUG import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level.INFO
import java.util.* import java.util.*
@SideOnly(Side.CLIENT)
class EntityRisingSoulFX(world: World, pos: BlockPos) : class EntityRisingSoulFX(world: World, pos: BlockPos) :
AbstractEntityFX(world, pos.x.toDouble() + 0.5, pos.y.toDouble() + 1.0, pos.z.toDouble() + 0.5) { AbstractEntityFX(world, pos.x.toDouble() + 0.5, pos.y.toDouble() + 1.0, pos.z.toDouble() + 0.5) {
@@ -27,14 +26,14 @@ AbstractEntityFX(world, pos.x.toDouble() + 0.5, pos.y.toDouble() + 1.0, pos.z.to
init { init {
motionY = 0.1 motionY = 0.1
particleGravity = 0.0f particleGravity = 0.0f
sprite = RisingSoulTextures.headIcons[rand.nextInt(256)] particleTexture = RisingSoulTextures.headIcons[rand.nextInt(256)]
maxAge = MathHelper.floor((0.6 + 0.4 * rand.nextDouble()) * Config.risingSoul.lifetime * 20.0) particleMaxAge = MathHelper.floor((0.6 + 0.4 * rand.nextDouble()) * Config.risingSoul.lifetime * 20.0)
} }
override val isValid: Boolean get() = true override val isValid: Boolean get() = true
override fun update() { override fun update() {
val phase = (initialPhase + age) % 64 val phase = (initialPhase + particleAge) % 64
velocity.setTo(cos[phase] * Config.risingSoul.perturb, 0.1, sin[phase] * Config.risingSoul.perturb) velocity.setTo(cos[phase] * Config.risingSoul.perturb, 0.1, sin[phase] * Config.risingSoul.perturb)
particleTrail.addFirst(currentPos.copy()) particleTrail.addFirst(currentPos.copy())
@@ -44,8 +43,8 @@ AbstractEntityFX(world, pos.x.toDouble() + 0.5, pos.y.toDouble() + 1.0, pos.z.to
} }
override fun render(worldRenderer: BufferBuilder, partialTickTime: Float) { override fun render(worldRenderer: BufferBuilder, partialTickTime: Float) {
var alpha = Config.risingSoul.opacity.toFloat() var alpha = Config.risingSoul.opacity
if (age > maxAge - 40) alpha *= (maxAge - age) / 40.0f if (particleAge > particleMaxAge - 40) alpha *= (particleMaxAge - particleAge) / 40.0f
renderParticleQuad(worldRenderer, partialTickTime, renderParticleQuad(worldRenderer, partialTickTime,
size = Config.risingSoul.headSize * 0.25, size = Config.risingSoul.headSize * 0.25,
@@ -55,19 +54,24 @@ AbstractEntityFX(world, pos.x.toDouble() + 0.5, pos.y.toDouble() + 1.0, pos.z.to
var scale = Config.risingSoul.trailSize * 0.25 var scale = Config.risingSoul.trailSize * 0.25
particleTrail.forEachPairIndexed { idx, current, previous -> particleTrail.forEachPairIndexed { idx, current, previous ->
scale *= Config.risingSoul.sizeDecay scale *= Config.risingSoul.sizeDecay
alpha *= Config.risingSoul.opacityDecay.toFloat() alpha *= Config.risingSoul.opacityDecay
if (idx % Config.risingSoul.trailDensity == 0) renderParticleQuad(worldRenderer, partialTickTime, if (idx % Config.risingSoul.trailDensity == 0) renderParticleQuad(worldRenderer, partialTickTime,
currentPos = current, currentPos = current,
prevPos = previous, prevPos = previous,
size = scale, size = scale,
alpha = alpha, alpha = alpha,
icon = RisingSoulTextures.trackIcon icon = RisingSoulTextures.trackIcon.icon!!
) )
} }
} }
} }
object RisingSoulTextures : ResourceHandler(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus, targetAtlas = Atlas.PARTICLES) { @SideOnly(Side.CLIENT)
val headIcons = spriteSet { idx -> ResourceLocation(BetterFoliageMod.MOD_ID, "rising_soul_$idx") } object RisingSoulTextures : ResourceHandler(BetterFoliageMod.MOD_ID) {
val trackIcon by sprite(Identifier(BetterFoliageMod.MOD_ID, "soul_track")) val headIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/rising_soul_%d")
val trackIcon = iconStatic(BetterFoliageMod.LEGACY_DOMAIN, "blocks/soul_track")
override fun afterPreStitch() {
Client.log(INFO, "Registered ${headIcons.num} soul particle textures")
}
} }

View File

@@ -3,10 +3,10 @@ package mods.betterfoliage.client.render
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.* import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.lighting.*
import mods.octarinecore.common.Double3 import mods.octarinecore.common.Double3
import mods.octarinecore.exchange import mods.octarinecore.exchange
import net.minecraft.util.Direction.* import net.minecraft.util.EnumFacing.*
import org.lwjgl.opengl.GL11
/** Weight of the same-side AO values on the outer edges of the 45deg chamfered column faces. */ /** Weight of the same-side AO values on the outer edges of the 45deg chamfered column faces. */
const val chamferAffinity = 0.9f const val chamferAffinity = 0.9f
@@ -25,10 +25,10 @@ fun Model.columnSide(radius: Double, yBottom: Double, yTop: Double, transform: (
verticalRectangle(x1 = 0.5 - radius, z1 = 0.5, x2 = 0.5 - halfRadius, z2 = 0.5 - halfRadius, yBottom = yBottom, yTop = yTop) 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) .clampUV(minU = 0.5 - radius)
.setAoShader( .setAoShader(
faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming.toFloat())) faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming))
) )
.setAoShader( .setAoShader(
faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming.toFloat())), faceOrientedAuto(overrideFace = SOUTH, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming)),
predicate = { v, vi -> vi == 1 || vi == 2} predicate = { v, vi -> vi == 1 || vi == 2}
) )
).forEach { transform(it.setFlatShader(FaceFlat(SOUTH))).add() } ).forEach { transform(it.setFlatShader(FaceFlat(SOUTH))).add() }
@@ -37,9 +37,9 @@ fun Model.columnSide(radius: Double, yBottom: Double, yTop: Double, transform: (
verticalRectangle(x1 = 0.5 - halfRadius, z1 = 0.5 - halfRadius, x2 = 0.5, z2 = 0.5 - radius, yBottom = yBottom, yTop = yTop) 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) .clampUV(maxU = radius - 0.5)
.setAoShader( .setAoShader(
faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming.toFloat()))) faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, chamferAffinity, Config.roundLogs.dimming)))
.setAoShader( .setAoShader(
faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming.toFloat())), faceOrientedAuto(overrideFace = EAST, corner = cornerInterpolate(Axis.Y, 0.5f, Config.roundLogs.dimming)),
predicate = { v, vi -> vi == 0 || vi == 3} predicate = { v, vi -> vi == 0 || vi == 3}
), ),

View File

@@ -1,42 +1,57 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.integration.ShadersModIntegration import mods.betterfoliage.client.integration.ShadersModIntegration
import mods.octarinecore.client.render.CombinedContext import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.RenderDecorator import mods.octarinecore.common.Int3
import mods.octarinecore.common.Rotation
import net.minecraft.block.material.Material import net.minecraft.block.material.Material
import net.minecraft.tags.BlockTags import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.util.ResourceLocation import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.world.biome.Biome import net.minecraft.util.BlockRenderLayer
import org.apache.logging.log4j.Level.DEBUG import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level.INFO
class RenderAlgae : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
class RenderAlgae : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val noise = simplexNoise() val noise = simplexNoise()
val algaeIcons = spriteSet { idx -> ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_algae_$idx") } val algaeIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_algae_%d")
val algaeModels = modelSet(64) { idx -> RenderGrass.grassTopQuads(Config.algae.heightMin, Config.algae.heightMax)(idx) } val algaeModels = modelSet(64, RenderGrass.grassTopQuads(Config.algae.heightMin, Config.algae.heightMax))
override fun isEligible(ctx: CombinedContext) = override fun afterPreStitch() {
Client.log(INFO, "Registered ${algaeIcons.num} algae textures")
}
override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.algae.enabled && Config.enabled && Config.algae.enabled &&
ctx.state(up2).material == Material.WATER && ctx.blockState(up2).material == Material.WATER &&
ctx.state(up1).material == Material.WATER && ctx.blockState(up1).material == Material.WATER &&
BlockTags.DIRT_LIKE.contains(ctx.state.block) && Config.blocks.dirt.matchesClass(ctx.block) &&
ctx.biome.category.let { it == Biome.Category.OCEAN || it == Biome.Category.BEACH || it == Biome.Category.RIVER } && ctx.biomeId in Config.algae.biomes &&
noise[ctx.pos] < Config.algae.population noise[ctx.pos] < Config.algae.population
override fun render(ctx: CombinedContext) { override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
ctx.render() val baseRender = renderWorldBlockBase(ctx, dispatcher, renderer, layer)
if (!ctx.isCutout) return if (!layer.isCutout) return baseRender
modelRenderer.updateShading(Int3.zero, allFaces)
val rand = ctx.semiRandomArray(3) val rand = ctx.semiRandomArray(3)
ShadersModIntegration.grass(ctx, Config.algae.shaderWind) {
ctx.render( ShadersModIntegration.grass(renderer, Config.algae.shaderWind) {
modelRenderer.render(
renderer,
algaeModels[rand[2]], algaeModels[rand[2]],
icon = { _, qi, _ -> algaeIcons[rand[qi and 1]] } Rotation.identity,
icon = { _, qi, _ -> algaeIcons[rand[qi and 1]]!! },
postProcess = noPost
) )
} }
return true
} }
} }

View File

@@ -1,51 +1,43 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.render.column.ColumnTextureInfo import mods.betterfoliage.client.render.column.ColumnTextureInfo
import mods.betterfoliage.client.render.column.SimpleColumnInfo import mods.betterfoliage.client.render.column.SimpleColumnInfo
import mods.betterfoliage.client.resource.Identifier
import mods.octarinecore.client.render.* import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.lighting.* import mods.octarinecore.client.resource.ModelRenderRegistryConfigurable
import mods.octarinecore.client.resource.* import mods.octarinecore.common.Int3
import mods.octarinecore.common.Rotation import mods.octarinecore.common.Rotation
import mods.octarinecore.common.config.ModelTextureList import mods.octarinecore.common.config.ModelTextureList
import mods.octarinecore.common.config.SimpleBlockMatcher import mods.octarinecore.common.config.SimpleBlockMatcher
import net.minecraft.block.BlockState import net.minecraft.block.BlockCactus
import net.minecraft.block.CactusBlock import net.minecraft.block.state.IBlockState
import net.minecraft.util.Direction.* import net.minecraft.client.renderer.BlockRendererDispatcher
import org.apache.logging.log4j.Level.DEBUG import net.minecraft.client.renderer.BufferBuilder
import java.util.concurrent.CompletableFuture import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.EnumFacing.*
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level
object AsyncCactusDiscovery : ConfigurableModelDiscovery<ColumnTextureInfo>() { object StandardCactusRegistry : ModelRenderRegistryConfigurable<ColumnTextureInfo>() {
override val logger = BetterFoliage.logDetail override val logger = BetterFoliageMod.logDetail
override val matchClasses = SimpleBlockMatcher(CactusBlock::class.java) override val matchClasses = SimpleBlockMatcher(BlockCactus::class.java)
override val modelTextures = listOf(ModelTextureList("block/cactus", "top", "bottom", "side")) override val modelTextures = listOf(ModelTextureList("block/cactus", "top", "bottom", "side"))
override fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo>? { override fun processModel(state: IBlockState, textures: List<String>) = SimpleColumnInfo.Key(logger, Axis.Y, textures)
val sprites = textures.map { atlas.sprite(Identifier(it)) } init { MinecraftForge.EVENT_BUS.register(this) }
return atlas.mapAfter {
SimpleColumnInfo(
Axis.Y,
sprites[0].get(),
sprites[1].get(),
sprites.drop(2).map { it.get() }
)
}
}
fun init() {
BetterFoliage.blockSprites.providers.add(this)
}
} }
class RenderCactus : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
class RenderCactus : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val cactusStemRadius = 0.4375 val cactusStemRadius = 0.4375
val cactusArmRotation = listOf(NORTH, SOUTH, EAST, WEST).map { Rotation.rot90[it.ordinal] } val cactusArmRotation = listOf(NORTH, SOUTH, EAST, WEST).map { Rotation.rot90[it.ordinal] }
val iconCross by sprite(Identifier(BetterFoliageMod.MOD_ID, "blocks/better_cactus")) val iconCross = iconStatic(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_cactus")
val iconArm = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_cactus_arm_$idx") } val iconArm = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_cactus_arm_%d")
val modelStem = model { val modelStem = model {
horizontalRectangle(x1 = -cactusStemRadius, x2 = cactusStemRadius, z1 = -cactusStemRadius, z2 = cactusStemRadius, y = 0.5) horizontalRectangle(x1 = -cactusStemRadius, x2 = cactusStemRadius, z1 = -cactusStemRadius, z2 = cactusStemRadius, y = 0.5)
@@ -75,30 +67,45 @@ class RenderCactus : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.b
.toCross(UP) { it.move(xzDisk(modelIdx) * Config.cactus.hOffset) }.addAll() .toCross(UP) { it.move(xzDisk(modelIdx) * Config.cactus.hOffset) }.addAll()
} }
override fun isEligible(ctx: CombinedContext): Boolean = override fun afterPreStitch() {
Client.log(Level.INFO, "Registered ${iconArm.num} cactus arm textures")
}
override fun isEligible(ctx: BlockContext): Boolean =
Config.enabled && Config.cactus.enabled && Config.enabled && Config.cactus.enabled &&
AsyncCactusDiscovery[ctx] != null Config.blocks.cactus.matchesClass(ctx.block)
override val onlyOnCutout get() = true override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
// render the whole block on the cutout layer
if (!layer.isCutout) return false
override fun render(ctx: CombinedContext) { // get AO data
val icons = AsyncCactusDiscovery[ctx]!! modelRenderer.updateShading(Int3.zero, allFaces)
val icons = StandardCactusRegistry[ctx] ?: return renderWorldBlockBase(ctx, dispatcher, renderer, null)
ctx.render( modelRenderer.render(
renderer,
modelStem.model, modelStem.model,
Rotation.identity,
icon = { ctx, qi, q -> when(qi) { icon = { ctx, qi, q -> when(qi) {
0 -> icons.bottom(ctx, qi, q); 1 -> icons.top(ctx, qi, q); else -> icons.side(ctx, qi, q) 0 -> icons.bottom(ctx, qi, q); 1 -> icons.top(ctx, qi, q); else -> icons.side(ctx, qi, q)
} } } },
postProcess = noPost
) )
ctx.render( modelRenderer.render(
modelCross[ctx.semiRandom(0)], renderer,
icon = { _, _, _ -> iconCross } modelCross[ctx.random(0)],
Rotation.identity,
icon = { _, _, _ -> iconCross.icon!!},
postProcess = noPost
) )
modelRenderer.render(
ctx.render( renderer,
modelArm[ctx.semiRandom(1)], modelArm[ctx.random(1)],
cactusArmRotation[ctx.semiRandom(2) % 4], cactusArmRotation[ctx.random(2) % 4],
icon = { _, _, _ -> iconArm[ctx.semiRandom(3)] } icon = { _, _, _ -> iconArm[ctx.random(3)]!!},
postProcess = noPost
) )
return true
} }
} }

View File

@@ -2,44 +2,35 @@ package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.texture.GrassRegistry import mods.octarinecore.client.render.AbstractBlockRenderingHandler
import mods.octarinecore.client.render.CombinedContext import mods.octarinecore.client.render.BlockContext
import mods.octarinecore.client.render.RenderDecorator import mods.octarinecore.client.render.withOffset
import mods.octarinecore.common.Int3 import mods.octarinecore.common.Int3
import mods.octarinecore.common.horizontalDirections import mods.octarinecore.common.forgeDirsHorizontal
import mods.octarinecore.common.offset import mods.octarinecore.common.offset
import net.minecraft.tags.BlockTags import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.BlockRenderLayer
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
class RenderConnectedGrass : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
override fun isEligible(ctx: CombinedContext) = class RenderConnectedGrass : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.connectedGrass.enabled && Config.enabled && Config.connectedGrass.enabled &&
BlockTags.DIRT_LIKE.contains(ctx.state.block) && Config.blocks.dirt.matchesClass(ctx.block) &&
GrassRegistry[ctx, up1] != null && Config.blocks.grassClasses.matchesClass(ctx.block(up1)) &&
(Config.connectedGrass.snowEnabled || !ctx.state(up2).isSnow) (Config.connectedGrass.snowEnabled || !ctx.blockState(up2).isSnow)
override fun render(ctx: CombinedContext) { override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
// if the block sides are not visible anyway, render normally // if the block sides are not visible anyway, render normally
if (horizontalDirections.none { ctx.shouldSideBeRendered(it) }) { if (forgeDirsHorizontal.all { ctx.blockState(it.offset).isOpaqueCube }) return renderWorldBlockBase(ctx, dispatcher, renderer, layer)
ctx.render()
} else { if (ctx.isSurroundedBy { it.isOpaqueCube } ) return false
ctx.exchange(Int3.zero, up1).exchange(up1, up2).render() return ctx.withOffset(Int3.zero, up1) {
} ctx.withOffset(up1, up2) {
} renderWorldBlockBase(ctx, dispatcher, renderer, layer)
} }
class RenderConnectedGrassLog : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) {
override fun isEligible(ctx: CombinedContext) =
Config.enabled && Config.roundLogs.enabled && Config.roundLogs.connectGrass &&
BlockTags.DIRT_LIKE.contains(ctx.state.block) &&
LogRegistry[ctx, up1] != null
override fun render(ctx: CombinedContext) {
val grassDir = horizontalDirections.find { GrassRegistry[ctx, it.offset] != null }
if (grassDir == null) {
ctx.render()
} else {
ctx.exchange(Int3.zero, grassDir.offset).render()
} }
} }
} }

View File

@@ -0,0 +1,36 @@
package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.AbstractBlockRenderingHandler
import mods.octarinecore.client.render.BlockContext
import mods.octarinecore.client.render.withOffset
import mods.octarinecore.common.Int3
import mods.octarinecore.common.offset
import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.EnumFacing.*
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
@SideOnly(Side.CLIENT)
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.matchesClass(ctx.block) &&
Config.blocks.logClasses.matchesClass(ctx.block(up1))
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
val grassDir = grassCheckDirs.find {
Config.blocks.grassClasses.matchesClass(ctx.block(it.offset))
} ?: return renderWorldBlockBase(ctx, dispatcher, renderer, layer)
return ctx.withOffset(Int3.zero, grassDir.offset) {
renderWorldBlockBase(ctx, dispatcher, renderer, layer)
}
}
}

View File

@@ -1,28 +1,30 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.BlockConfig
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.octarinecore.client.render.* import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.lighting.* import mods.octarinecore.common.Int3
import mods.octarinecore.common.allDirections import mods.octarinecore.common.forgeDirOffsets
import mods.octarinecore.common.forgeDirs
import mods.octarinecore.random import mods.octarinecore.random
import net.minecraft.block.material.Material import net.minecraft.block.material.Material
import net.minecraft.tags.BlockTags import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.util.Direction.Axis import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.Direction.UP import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.ResourceLocation import net.minecraft.util.EnumFacing.Axis
import net.minecraft.world.biome.Biome import net.minecraft.util.EnumFacing.UP
import org.apache.logging.log4j.Level.DEBUG import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level.INFO
class RenderCoral : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
class RenderCoral : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val noise = simplexNoise() val noise = simplexNoise()
val coralIcons = spriteSet { idx -> ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_coral_$idx") } val coralIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_coral_%d")
val crustIcons = spriteSet { idx -> ResourceLocation(BetterFoliageMod.MOD_ID, "blocks/better_crust_$idx") } val crustIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_crust_%d")
val coralModels = modelSet(64) { modelIdx -> val coralModels = modelSet(64) { modelIdx ->
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = 0.0, yTop = 1.0) 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) .scale(Config.coral.size).move(0.5 to UP)
@@ -38,27 +40,38 @@ class RenderCoral : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bu
} }
} }
override fun isEligible(ctx: CombinedContext) = override fun afterPreStitch() {
Client.log(INFO, "Registered ${coralIcons.num} coral textures")
Client.log(INFO, "Registered ${crustIcons.num} coral crust textures")
}
override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.coral.enabled && Config.enabled && Config.coral.enabled &&
(ctx.state(up2).material == Material.WATER || Config.coral.shallowWater) && (ctx.blockState(up2).material == Material.WATER || Config.coral.shallowWater) &&
ctx.state(up1).material == Material.WATER && ctx.blockState(up1).material == Material.WATER &&
BlockTags.SAND.contains(ctx.state.block) && Config.blocks.sand.matchesClass(ctx.block) &&
ctx.biome.category.let { it == Biome.Category.OCEAN || it == Biome.Category.BEACH } && ctx.biomeId in Config.coral.biomes &&
noise[ctx.pos] < Config.coral.population noise[ctx.pos] < Config.coral.population
override fun render(ctx: CombinedContext) { override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
val baseRender = ctx.render() val baseRender = renderWorldBlockBase(ctx, dispatcher, renderer, layer)
if (!ctx.isCutout) return if (!layer.isCutout) return baseRender
allDirections.forEachIndexed { idx, face -> modelRenderer.updateShading(Int3.zero, allFaces)
if (ctx.state(face).material == Material.WATER && ctx.semiRandom(idx) < Config.coral.chance) {
var variation = ctx.semiRandom(6) forgeDirs.forEachIndexed { idx, face ->
ctx.render( if (!ctx.blockState(forgeDirOffsets[idx]).isOpaqueCube && blockContext.random(idx) < Config.coral.chance) {
var variation = blockContext.random(6)
modelRenderer.render(
renderer,
coralModels[variation++], coralModels[variation++],
rotationFromUp[idx], rotationFromUp[idx],
icon = { _, qi, _ -> if (qi == 4) crustIcons[variation] else coralIcons[variation + (qi and 1)] } icon = { _, qi, _ -> if (qi == 4) crustIcons[variation]!! else coralIcons[variation + (qi and 1)]!!},
postProcess = noPost
) )
} }
} }
return true
} }
} }

View File

@@ -1,29 +1,27 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.integration.OptifineCustomColors import mods.betterfoliage.client.integration.OptifineCustomColors
import mods.betterfoliage.client.integration.ShadersModIntegration import mods.betterfoliage.client.integration.ShadersModIntegration
import mods.betterfoliage.client.resource.Identifier
import mods.betterfoliage.client.texture.GeneratedGrass
import mods.betterfoliage.client.texture.GrassRegistry import mods.betterfoliage.client.texture.GrassRegistry
import mods.octarinecore.client.render.CombinedContext import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.Model import mods.octarinecore.common.*
import mods.octarinecore.client.render.RenderDecorator
import mods.octarinecore.client.render.fullCube
import mods.octarinecore.client.render.lighting.cornerAo
import mods.octarinecore.client.render.lighting.cornerFlat
import mods.octarinecore.client.render.lighting.faceOrientedAuto
import mods.octarinecore.common.Double3
import mods.octarinecore.common.allDirections
import mods.octarinecore.random import mods.octarinecore.random
import net.minecraft.tags.BlockTags import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.util.Direction.* import net.minecraft.client.renderer.BufferBuilder
import org.apache.logging.log4j.Level.DEBUG import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.EnumBlockRenderType
import net.minecraft.util.EnumBlockRenderType.MODEL
import net.minecraft.util.EnumFacing.Axis
import net.minecraft.util.EnumFacing.UP
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level.INFO
class RenderGrass : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
class RenderGrass : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
companion object { companion object {
@JvmStatic fun grassTopQuads(heightMin: Double, heightMax: Double): Model.(Int)->Unit = { modelIdx -> @JvmStatic fun grassTopQuads(heightMin: Double, heightMax: Double): Model.(Int)->Unit = { modelIdx ->
@@ -38,36 +36,55 @@ class RenderGrass : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bu
val noise = simplexNoise() val noise = simplexNoise()
val normalIcons = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_grass_long_$idx") } val normalIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_grass_long_%d")
val snowedIcons = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_grass_snowed_$idx") } val snowedIcons = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_grass_snowed_%d")
val normalGenIcon by sprite { GeneratedGrass(sprite = "minecraft:blocks/tall_grass_top", isSnowed = false).register(BetterFoliage.asyncPack) } val normalGenIcon = iconStatic(Client.genGrass.generatedResource("minecraft:blocks/tallgrass", "snowed" to false))
val snowedGenIcon by sprite { GeneratedGrass(sprite = "minecraft:blocks/tall_grass_top", isSnowed = true).register(BetterFoliage.asyncPack) } val snowedGenIcon = iconStatic(Client.genGrass.generatedResource("minecraft:blocks/tallgrass", "snowed" to true))
val grassModels = modelSet(64) { idx -> grassTopQuads(Config.shortGrass.heightMin, Config.shortGrass.heightMax)(idx) } val grassModels = modelSet(64, grassTopQuads(Config.shortGrass.heightMin, Config.shortGrass.heightMax))
override fun isEligible(ctx: CombinedContext) = override fun afterPreStitch() {
Client.log(INFO, "Registered ${normalIcons.num} grass textures")
Client.log(INFO, "Registered ${snowedIcons.num} snowed grass textures")
}
override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.enabled &&
(Config.shortGrass.grassEnabled || Config.connectedGrass.enabled) && (Config.shortGrass.grassEnabled || Config.connectedGrass.enabled) &&
GrassRegistry[ctx] != null GrassRegistry[ctx] != null
override val onlyOnCutout get() = true override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
// render the whole block on the cutout layer
if (!layer.isCutout) return false
override fun render(ctx: CombinedContext) { val isConnected = ctx.block(down1).let {
val isConnected = BlockTags.DIRT_LIKE.contains(ctx.state(DOWN).block) || GrassRegistry[ctx, down1] != null Config.blocks.dirt.matchesClass(it) ||
val isSnowed = ctx.state(UP).isSnow Config.blocks.grassClasses.matchesClass(it)
}
val isSnowed = ctx.blockState(up1).isSnow
val connectedGrass = isConnected && Config.connectedGrass.enabled && (!isSnowed || Config.connectedGrass.snowEnabled) val connectedGrass = isConnected && Config.connectedGrass.enabled && (!isSnowed || Config.connectedGrass.snowEnabled)
val grass = GrassRegistry[ctx]!! val grass = GrassRegistry[ctx]
if (grass == null) {
// shouldn't happen
Client.logRenderError(ctx.blockState(Int3.zero), ctx.pos)
return renderWorldBlockBase(ctx, dispatcher, renderer, null)
}
val blockColor = OptifineCustomColors.getBlockColor(ctx) val blockColor = OptifineCustomColors.getBlockColor(ctx)
if (connectedGrass) { if (connectedGrass) {
// get full AO data
modelRenderer.updateShading(Int3.zero, allFaces)
// check occlusion // check occlusion
val isVisible = allDirections.map { ctx.shouldSideBeRendered(it) } val isHidden = forgeDirs.map { ctx.blockState(it.offset).isOpaqueCube }
// render full grass block // render full grass block
ctx.render( ShadersModIntegration.renderAs(ctx.blockState(Int3.zero), MODEL, renderer) {
modelRenderer.render(
renderer,
fullCube, fullCube,
quadFilter = { qi, _ -> isVisible[qi] }, quadFilter = { qi, _ -> !isHidden[qi] },
icon = { _, _, _ -> grass.grassTopTexture }, icon = { _, _, _ -> grass.grassTopTexture },
postProcess = { ctx, _, _, _, _ -> postProcess = { ctx, _, _, _, _ ->
rotateUV(2) rotateUV(2)
@@ -76,27 +93,39 @@ class RenderGrass : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bu
} else if (ctx.aoEnabled && grass.overrideColor == null) multiplyColor(blockColor) } else if (ctx.aoEnabled && grass.overrideColor == null) multiplyColor(blockColor)
} }
) )
}
} else { } else {
ctx.render() renderWorldBlockBase(ctx, dispatcher, renderer, null)
// get AO data only for block top
modelRenderer.updateShading(Int3.zero, topOnly)
} }
if (!Config.shortGrass.grassEnabled) return if (!Config.shortGrass.grassEnabled) return true
if (isSnowed && !Config.shortGrass.snowEnabled) return
if (ctx.offset(UP).isNormalCube) return val stateAbove = ctx.blockState(up1)
if (Config.shortGrass.population < 64 && noise[ctx.pos] >= Config.shortGrass.population) return if (!stateAbove.block.isAir(stateAbove, ctx.world!!, ctx.pos.up())) return true
if (isSnowed && !Config.shortGrass.snowEnabled) return true
if (ctx.blockState(up1).isOpaqueCube) return true
if (Config.shortGrass.population < 64 && noise[ctx.pos] >= Config.shortGrass.population) return true
// render grass quads // render grass quads
val iconset = if (isSnowed) snowedIcons else normalIcons val iconset = if (isSnowed) snowedIcons else normalIcons
val iconGen = if (isSnowed) snowedGenIcon else normalGenIcon val iconGen = if (isSnowed) snowedGenIcon else normalGenIcon
val rand = ctx.semiRandomArray(2) val rand = ctx.semiRandomArray(2)
ShadersModIntegration.grass(ctx, Config.shortGrass.shaderWind) { ShadersModIntegration.grass(renderer, Config.shortGrass.shaderWind) {
ctx.render( modelRenderer.render(
renderer,
grassModels[rand[0]], grassModels[rand[0]],
translation = ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero), Rotation.identity,
icon = { _, qi, _ -> if (Config.shortGrass.useGenerated) iconGen else iconset[rand[qi and 1]] }, ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero),
icon = { _, qi, _ -> if (Config.shortGrass.useGenerated) iconGen.icon!! else iconset[rand[qi and 1]]!! },
postProcess = { _, _, _, _, _ -> if (isSnowed) setGrey(1.0f) else multiplyColor(grass.overrideColor ?: blockColor) } postProcess = { _, _, _, _, _ -> if (isSnowed) setGrey(1.0f) else multiplyColor(grass.overrideColor ?: blockColor) }
) )
} }
return true
} }
} }

View File

@@ -1,27 +1,31 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.integration.OptifineCustomColors import mods.betterfoliage.client.integration.OptifineCustomColors
import mods.betterfoliage.client.integration.ShadersModIntegration import mods.betterfoliage.client.integration.ShadersModIntegration
import mods.betterfoliage.client.resource.Identifier
import mods.betterfoliage.client.texture.LeafRegistry import mods.betterfoliage.client.texture.LeafRegistry
import mods.octarinecore.PI2 import mods.octarinecore.PI2
import mods.octarinecore.client.render.CombinedContext import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.RenderDecorator
import mods.octarinecore.client.render.lighting.FlatOffset
import mods.octarinecore.client.render.lighting.cornerAoMaxGreen
import mods.octarinecore.client.render.lighting.edgeOrientedAuto
import mods.octarinecore.common.Double3 import mods.octarinecore.common.Double3
import mods.octarinecore.common.Int3 import mods.octarinecore.common.Int3
import mods.octarinecore.common.allDirections import mods.octarinecore.common.Rotation
import mods.octarinecore.common.vec import mods.octarinecore.common.vec
import mods.octarinecore.random import mods.octarinecore.random
import net.minecraft.util.Direction.UP import net.minecraft.block.material.Material
import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.EnumFacing.DOWN
import net.minecraft.util.EnumFacing.UP
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import java.lang.Math.cos import java.lang.Math.cos
import java.lang.Math.sin import java.lang.Math.sin
class RenderLeaves : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
class RenderLeaves : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val leavesModel = model { 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) verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -0.5 * 1.41, yTop = 0.5 * 1.41)
@@ -30,7 +34,7 @@ class RenderLeaves : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.b
.scale(Config.leaves.size) .scale(Config.leaves.size)
.toCross(UP).addAll() .toCross(UP).addAll()
} }
val snowedIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_leaves_snowed_$idx") } val snowedIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_leaves_snowed_%d")
val perturbs = vectorSet(64) { idx -> val perturbs = vectorSet(64) { idx ->
val angle = PI2 * idx / 64.0 val angle = PI2 * idx / 64.0
@@ -38,28 +42,36 @@ class RenderLeaves : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.b
UP.vec * random(-1.0, 1.0) * Config.leaves.vOffset UP.vec * random(-1.0, 1.0) * Config.leaves.vOffset
} }
override fun isEligible(ctx: CombinedContext) = override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.enabled &&
Config.leaves.enabled && Config.leaves.enabled &&
LeafRegistry[ctx] != null && LeafRegistry[ctx] != null &&
!(Config.leaves.hideInternal && allDirections.all { ctx.offset(it).isNormalCube } ) !(Config.leaves.hideInternal && ctx.isSurroundedBy { it.isFullCube || it.material == Material.LEAVES } )
override val onlyOnCutout get() = true override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
val isSnowed = ctx.blockState(up1).material.let {
override fun render(ctx: CombinedContext) { it == Material.SNOW || it == Material.CRAFTED_SNOW
val isSnowed = ctx.state(UP).isSnow }
val leafInfo = LeafRegistry[ctx]!! val leafInfo = LeafRegistry[ctx]
if (leafInfo == null) {
// shouldn't happen
Client.logRenderError(ctx.blockState(Int3.zero), ctx.pos)
return renderWorldBlockBase(ctx, dispatcher, renderer, layer)
}
val blockColor = OptifineCustomColors.getBlockColor(ctx) val blockColor = OptifineCustomColors.getBlockColor(ctx)
ctx.render(force = true) renderWorldBlockBase(ctx, dispatcher, renderer, layer)
if (!layer.isCutout) return true
ShadersModIntegration.leaves(ctx) { modelRenderer.updateShading(Int3.zero, allFaces)
ShadersModIntegration.leaves(renderer) {
val rand = ctx.semiRandomArray(2) val rand = ctx.semiRandomArray(2)
(if (Config.leaves.dense) denseLeavesRot else normalLeavesRot).forEach { rotation -> (if (Config.leaves.dense) denseLeavesRot else normalLeavesRot).forEach { rotation ->
ctx.render( modelRenderer.render(
renderer,
leavesModel.model, leavesModel.model,
rotation, rotation,
translation = ctx.blockCenter + perturbs[rand[0]], ctx.blockCenter + perturbs[rand[0]],
icon = { _, _, _ -> leafInfo.roundLeafTexture }, icon = { _, _, _ -> leafInfo.roundLeafTexture },
postProcess = { _, _, _, _, _ -> postProcess = { _, _, _, _, _ ->
rotateUV(rand[1]) rotateUV(rand[1])
@@ -67,12 +79,16 @@ class RenderLeaves : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.b
} }
) )
} }
if (isSnowed && Config.leaves.snowEnabled) ctx.render( if (isSnowed && Config.leaves.snowEnabled) modelRenderer.render(
renderer,
leavesModel.model, leavesModel.model,
translation = ctx.blockCenter + perturbs[rand[0]], Rotation.identity,
icon = { _, _, _ -> snowedIcon[rand[1]] }, ctx.blockCenter + perturbs[rand[0]],
icon = { _, _, _ -> snowedIcon[rand[1]]!! },
postProcess = whitewash postProcess = whitewash
) )
} }
return true
} }
} }

View File

@@ -1,21 +1,23 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.BlockConfig
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.integration.ShadersModIntegration import mods.betterfoliage.client.integration.ShadersModIntegration
import mods.betterfoliage.client.resource.Identifier import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.CombinedContext
import mods.octarinecore.client.render.RenderDecorator
import mods.octarinecore.client.render.lighting.FlatOffsetNoColor
import mods.octarinecore.common.Int3 import mods.octarinecore.common.Int3
import net.minecraft.util.Direction.DOWN import mods.octarinecore.common.Rotation
import net.minecraft.util.Direction.UP import net.minecraft.client.renderer.BlockRendererDispatcher
import org.apache.logging.log4j.Level.DEBUG import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.EnumFacing.DOWN
import net.minecraft.util.EnumFacing.UP
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level
class RenderLilypad : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
class RenderLilypad : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val rootModel = model { val rootModel = model {
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -1.5, yTop = -0.5) verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yBottom = -1.5, yTop = -0.5)
@@ -28,32 +30,50 @@ class RenderLilypad : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.
.setFlatShader(FlatOffsetNoColor(Int3.zero)) .setFlatShader(FlatOffsetNoColor(Int3.zero))
.toCross(UP).addAll() .toCross(UP).addAll()
} }
val rootIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_lilypad_roots_$idx") } val rootIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_lilypad_roots_%d")
val flowerIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_lilypad_flower_$idx") } val flowerIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_lilypad_flower_%d")
val perturbs = vectorSet(64) { modelIdx -> xzDisk(modelIdx) * Config.lilypad.hOffset } val perturbs = vectorSet(64) { modelIdx -> xzDisk(modelIdx) * Config.lilypad.hOffset }
override fun isEligible(ctx: CombinedContext): Boolean = override fun afterPreStitch() {
Config.enabled && Config.lilypad.enabled && Client.log(Level.INFO, "Registered ${rootIcon.num} lilypad root textures")
BlockConfig.lilypad.matchesClass(ctx.state.block) Client.log(Level.INFO, "Registered ${flowerIcon.num} lilypad flower textures")
}
override fun render(ctx: CombinedContext) { override fun isEligible(ctx: BlockContext): Boolean =
ctx.render() Config.enabled && Config.lilypad.enabled &&
Config.blocks.lilypad.matchesClass(ctx.block)
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
// render the whole block on the cutout layer
if (!layer.isCutout) return false
renderWorldBlockBase(ctx, dispatcher, renderer, null)
modelRenderer.updateShading(Int3.zero, allFaces)
val rand = ctx.semiRandomArray(5) val rand = ctx.semiRandomArray(5)
ShadersModIntegration.grass(ctx) {
ctx.render( ShadersModIntegration.grass(renderer) {
modelRenderer.render(
renderer,
rootModel.model, rootModel.model,
translation = ctx.blockCenter.add(perturbs[rand[2]]), Rotation.identity,
ctx.blockCenter.add(perturbs[rand[2]]),
forceFlat = true, forceFlat = true,
icon = { ctx, qi, q -> rootIcon[rand[qi and 1]] } icon = { ctx, qi, q -> rootIcon[rand[qi and 1]]!! },
postProcess = noPost
) )
} }
if (rand[3] < Config.lilypad.flowerChance) ctx.render( if (rand[3] < Config.lilypad.flowerChance) modelRenderer.render(
renderer,
flowerModel.model, flowerModel.model,
translation = ctx.blockCenter.add(perturbs[rand[4]]), Rotation.identity,
ctx.blockCenter.add(perturbs[rand[4]]),
forceFlat = true, forceFlat = true,
icon = { _, _, _ -> flowerIcon[rand[0]] } icon = { _, _, _ -> flowerIcon[rand[0]]!! },
postProcess = noPost
) )
return true
} }
} }

View File

@@ -1,33 +1,30 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.chunk.ChunkOverlayManager import mods.betterfoliage.client.chunk.ChunkOverlayManager
import mods.betterfoliage.client.config.BlockConfig
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.render.column.AbstractRenderColumn import mods.betterfoliage.client.render.column.AbstractRenderColumn
import mods.betterfoliage.client.render.column.ColumnRenderLayer import mods.betterfoliage.client.render.column.ColumnRenderLayer
import mods.betterfoliage.client.render.column.ColumnTextureInfo import mods.betterfoliage.client.render.column.ColumnTextureInfo
import mods.betterfoliage.client.render.column.SimpleColumnInfo import mods.betterfoliage.client.render.column.SimpleColumnInfo
import mods.betterfoliage.client.resource.Identifier import mods.octarinecore.client.render.BlockContext
import mods.octarinecore.client.render.CombinedContext
import mods.octarinecore.client.resource.* import mods.octarinecore.client.resource.*
import mods.octarinecore.common.config.ConfigurableBlockMatcher import mods.octarinecore.common.config.ConfigurableBlockMatcher
import mods.octarinecore.common.config.ModelTextureList import mods.octarinecore.common.config.ModelTextureList
import mods.octarinecore.tryDefault import mods.octarinecore.tryDefault
import net.minecraft.block.BlockState import net.minecraft.block.BlockLog
import net.minecraft.block.LogBlock import net.minecraft.block.state.IBlockState
import net.minecraft.util.Direction.Axis import net.minecraft.util.EnumFacing.Axis
import org.apache.logging.log4j.Level import net.minecraftforge.fml.relauncher.Side
import java.util.concurrent.CompletableFuture import net.minecraftforge.fml.relauncher.SideOnly
class RenderLog : AbstractRenderColumn(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { class RenderLog : AbstractRenderColumn(BetterFoliageMod.MOD_ID) {
override val renderOnCutout: Boolean get() = false override val addToCutout: Boolean get() = false
override fun isEligible(ctx: CombinedContext) = override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.roundLogs.enabled && Config.enabled && Config.roundLogs.enabled &&
LogRegistry[ctx] != null Config.blocks.logClasses.matchesClass(ctx.block)
override val overlayLayer = RoundLogOverlayLayer() override val overlayLayer = RoundLogOverlayLayer()
override val connectPerpendicular: Boolean get() = Config.roundLogs.connectPerpendicular override val connectPerpendicular: Boolean get() = Config.roundLogs.connectPerpendicular
@@ -40,37 +37,26 @@ class RenderLog : AbstractRenderColumn(BetterFoliageMod.MOD_ID, BetterFoliageMod
class RoundLogOverlayLayer : ColumnRenderLayer() { class RoundLogOverlayLayer : ColumnRenderLayer() {
override val registry: ModelRenderRegistry<ColumnTextureInfo> get() = LogRegistry override val registry: ModelRenderRegistry<ColumnTextureInfo> get() = LogRegistry
override val blockPredicate = { state: BlockState -> BlockConfig.logBlocks.matchesClass(state.block) } override val blockPredicate = { state: IBlockState -> Config.blocks.logClasses.matchesClass(state.block) }
override val surroundPredicate = { state: IBlockState -> state.isOpaqueCube && !Config.blocks.logClasses.matchesClass(state.block) }
override val connectSolids: Boolean get() = Config.roundLogs.connectSolids override val connectSolids: Boolean get() = Config.roundLogs.connectSolids
override val lenientConnect: Boolean get() = Config.roundLogs.lenientConnect override val lenientConnect: Boolean get() = Config.roundLogs.lenientConnect
override val defaultToY: Boolean get() = Config.roundLogs.defaultY override val defaultToY: Boolean get() = Config.roundLogs.defaultY
} }
@SideOnly(Side.CLIENT)
object LogRegistry : ModelRenderRegistryRoot<ColumnTextureInfo>() object LogRegistry : ModelRenderRegistryRoot<ColumnTextureInfo>()
object AsyncLogDiscovery : ConfigurableModelDiscovery<ColumnTextureInfo>() { object StandardLogRegistry : ModelRenderRegistryConfigurable<ColumnTextureInfo>() {
override val logger = BetterFoliage.logDetail override val logger = BetterFoliageMod.logDetail
override val matchClasses: ConfigurableBlockMatcher get() = BlockConfig.logBlocks override val matchClasses: ConfigurableBlockMatcher get() = Config.blocks.logClasses
override val modelTextures: List<ModelTextureList> get() = BlockConfig.logModels.modelList override val modelTextures: List<ModelTextureList> get() = Config.blocks.logModels.list
override fun processModel(state: IBlockState, textures: List<String>) = SimpleColumnInfo.Key(logger, getAxis(state), textures)
override fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture): CompletableFuture<ColumnTextureInfo> { fun getAxis(state: IBlockState): Axis? {
val axis = getAxis(state) val axis = tryDefault(null) { state.getValue(BlockLog.LOG_AXIS).toString() } ?:
logger.log(Level.DEBUG, "$logName: axis $axis") state.properties.entries.find { it.key.getName().toLowerCase() == "axis" }?.value?.toString()
val spriteList = textures.map { atlas.sprite(Identifier(it)) }
return atlas.mapAfter {
SimpleColumnInfo(
axis,
spriteList[0].get(),
spriteList[1].get(),
spriteList.drop(2).map { it.get() }
)
}
}
fun getAxis(state: BlockState): Axis? {
val axis = tryDefault(null) { state.get(LogBlock.AXIS).toString() } ?:
state.values.entries.find { it.key.getName().toLowerCase() == "axis" }?.value?.toString()
return when (axis) { return when (axis) {
"x" -> Axis.X "x" -> Axis.X
"y" -> Axis.Y "y" -> Axis.Y
@@ -78,9 +64,4 @@ object AsyncLogDiscovery : ConfigurableModelDiscovery<ColumnTextureInfo>() {
else -> null else -> null
} }
} }
fun init() {
LogRegistry.registries.add(this)
BetterFoliage.blockSprites.providers.add(this)
}
} }

View File

@@ -1,42 +1,57 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.BlockConfig
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.resource.Identifier import mods.octarinecore.client.render.AbstractBlockRenderingHandler
import mods.octarinecore.client.render.CombinedContext import mods.octarinecore.client.render.BlockContext
import mods.octarinecore.client.render.RenderDecorator import mods.octarinecore.client.render.modelRenderer
import mods.octarinecore.client.render.noPost import mods.octarinecore.client.render.noPost
import mods.octarinecore.common.Double3 import mods.octarinecore.common.Double3
import net.minecraft.util.Direction.UP import mods.octarinecore.common.Rotation
import org.apache.logging.log4j.Level.DEBUG import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.BlockRenderLayer
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level.INFO
class RenderMycelium : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
class RenderMycelium : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val myceliumIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_mycel_$idx") } val myceliumIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_mycel_%d")
val myceliumModel = modelSet(64) { idx -> RenderGrass.grassTopQuads(Config.shortGrass.heightMin, Config.shortGrass.heightMax)(idx) } val myceliumModel = modelSet(64, RenderGrass.grassTopQuads(Config.shortGrass.heightMin, Config.shortGrass.heightMax))
override fun isEligible(ctx: CombinedContext): Boolean { override fun afterPreStitch() {
if (!Config.enabled || !Config.shortGrass.myceliumEnabled) return false Client.log(INFO, "Registered ${myceliumIcon.num} mycelium textures")
return BlockConfig.mycelium.matchesClass(ctx.state.block)
} }
override fun render(ctx: CombinedContext) { override fun isEligible(ctx: BlockContext): Boolean {
ctx.render() if (!Config.enabled || !Config.shortGrass.myceliumEnabled) return false
if (!ctx.isCutout) return return Config.blocks.mycelium.matchesClass(ctx.block)
}
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
// render the whole block on the cutout layer
if (!layer.isCutout) return false
val isSnowed = ctx.blockState(up1).isSnow
renderWorldBlockBase(ctx, dispatcher, renderer, null)
if (isSnowed && !Config.shortGrass.snowEnabled) return true
if (ctx.blockState(up1).isOpaqueCube) return true
val isSnowed = ctx.state(UP).isSnow
if (isSnowed && !Config.shortGrass.snowEnabled) return
if (ctx.offset(UP).isNormalCube) return
val rand = ctx.semiRandomArray(2) val rand = ctx.semiRandomArray(2)
modelRenderer.render(
ctx.render( renderer,
myceliumModel[rand[0]], myceliumModel[rand[0]],
translation = ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero), Rotation.identity,
icon = { _, qi, _ -> myceliumIcon[rand[qi and 1]] }, ctx.blockCenter + (if (isSnowed) snowOffset else Double3.zero),
icon = { _, qi, _ -> myceliumIcon[rand[qi and 1]]!! },
postProcess = if (isSnowed) whitewash else noPost postProcess = if (isSnowed) whitewash else noPost
) )
return true
} }
} }

View File

@@ -1,22 +1,24 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.BlockConfig
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.resource.Identifier
import mods.octarinecore.client.render.* import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.lighting.* import mods.octarinecore.common.Int3
import mods.octarinecore.common.Rotation
import mods.octarinecore.random import mods.octarinecore.random
import net.minecraft.block.Blocks import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.util.Direction.Axis import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.Direction.* import net.minecraft.util.BlockRenderLayer
import org.apache.logging.log4j.Level.DEBUG import net.minecraft.util.EnumFacing.*
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level.INFO
class RenderNetherrack : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
class RenderNetherrack : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val netherrackIcon = spriteSet { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_netherrack_$idx") } val netherrackIcon = iconSet(BetterFoliageMod.LEGACY_DOMAIN, "blocks/better_netherrack_%d")
val netherrackModel = modelSet(64) { modelIdx -> val netherrackModel = modelSet(64) { modelIdx ->
verticalRectangle(x1 = -0.5, z1 = 0.5, x2 = 0.5, z2 = -0.5, yTop = -0.5, 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)) yBottom = -0.5 - random(Config.netherrack.heightMin, Config.netherrack.heightMax))
@@ -26,18 +28,32 @@ class RenderNetherrack : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageM
} }
override fun isEligible(ctx: CombinedContext) = override fun afterPreStitch() {
Config.enabled && Config.netherrack.enabled && ctx.state.block == Blocks.NETHERRACK Client.log(INFO, "Registered ${netherrackIcon.num} netherrack textures")
}
override fun render(ctx: CombinedContext) { override fun isEligible(ctx: BlockContext): Boolean {
ctx.render() if (!Config.enabled || !Config.netherrack.enabled) return false
if (!ctx.isCutout) return return Config.blocks.netherrack.matchesClass(ctx.block)
if (ctx.offset(DOWN).isNormalCube) return }
override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
val baseRender = renderWorldBlockBase(ctx, dispatcher, renderer, layer)
if (!layer.isCutout) return baseRender
if (ctx.blockState(down1).isOpaqueCube) return baseRender
modelRenderer.updateShading(Int3.zero, allFaces)
val rand = ctx.semiRandomArray(2) val rand = ctx.semiRandomArray(2)
ctx.render( modelRenderer.render(
renderer,
netherrackModel[rand[0]], netherrackModel[rand[0]],
icon = { _, qi, _ -> netherrackIcon[rand[qi and 1]] } Rotation.identity,
icon = { _, qi, _ -> netherrackIcon[rand[qi and 1]]!! },
postProcess = noPost
) )
return true
} }
} }

View File

@@ -1,28 +1,27 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.integration.ShadersModIntegration import mods.betterfoliage.client.integration.ShadersModIntegration
import mods.betterfoliage.client.resource.Identifier import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.CombinedContext import mods.octarinecore.common.Int3
import mods.octarinecore.client.render.RenderDecorator import mods.octarinecore.common.Rotation
import mods.octarinecore.client.render.lighting.FlatOffsetNoColor
import mods.octarinecore.client.resource.CenteredSprite
import mods.octarinecore.random import mods.octarinecore.random
import net.minecraft.block.material.Material import net.minecraft.block.material.Material
import net.minecraft.tags.BlockTags import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.util.Direction.UP import net.minecraft.client.renderer.BufferBuilder
import org.apache.logging.log4j.Level.DEBUG import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.EnumFacing.UP
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level
class RenderReeds : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bus) { @SideOnly(Side.CLIENT)
class RenderReeds : AbstractBlockRenderingHandler(BetterFoliageMod.MOD_ID) {
val noise = simplexNoise() val noise = simplexNoise()
val reedIcons = spriteSetTransformed( val reedIcons = iconSet(Client.genReeds.generatedResource("${BetterFoliageMod.LEGACY_DOMAIN}:blocks/better_reed_%d"))
check = { idx -> Identifier(BetterFoliageMod.MOD_ID, "blocks/better_reed_$idx")},
register = { CenteredSprite(it).register(BetterFoliage.asyncPack) }
)
val reedModels = modelSet(64) { modelIdx -> val reedModels = modelSet(64) { modelIdx ->
val height = random(Config.reed.heightMin, Config.reed.heightMax) val height = random(Config.reed.heightMin, Config.reed.heightMax)
val waterline = 0.875f val waterline = 0.875f
@@ -41,27 +40,35 @@ class RenderReeds : RenderDecorator(BetterFoliageMod.MOD_ID, BetterFoliageMod.bu
} }
} }
override fun isEligible(ctx: CombinedContext) = override fun afterPreStitch() {
Client.log(Level.INFO, "Registered ${reedIcons.num} reed textures")
}
override fun isEligible(ctx: BlockContext) =
Config.enabled && Config.reed.enabled && Config.enabled && Config.reed.enabled &&
ctx.state(up2).material == Material.AIR && ctx.blockState(up2).material == Material.AIR &&
ctx.state(UP).material == Material.WATER && ctx.blockState(up1).material == Material.WATER &&
BlockTags.DIRT_LIKE.contains(ctx.state.block) && Config.blocks.dirt.matchesClass(ctx.block) &&
ctx.biome.let { it.downfall > Config.reed.minBiomeRainfall && it.defaultTemperature >= Config.reed.minBiomeTemp } && ctx.biomeId in Config.reed.biomes &&
noise[ctx.pos] < Config.reed.population noise[ctx.pos] < Config.reed.population
override val onlyOnCutout get() = false override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
val baseRender = renderWorldBlockBase(ctx, dispatcher, renderer, layer)
if (!layer.isCutout) return baseRender
override fun render(ctx: CombinedContext) { modelRenderer.updateShading(Int3.zero, allFaces)
ctx.render()
if (!ctx.isCutout) return
val iconVar = ctx.semiRandom(1) val iconVar = ctx.random(1)
ShadersModIntegration.grass(ctx, Config.reed.shaderWind) { ShadersModIntegration.grass(renderer, Config.reed.shaderWind) {
ctx.render( modelRenderer.render(
reedModels[ctx.semiRandom(0)], renderer,
reedModels[ctx.random(0)],
Rotation.identity,
forceFlat = true, forceFlat = true,
icon = { _, _, _ -> reedIcons[iconVar] } icon = { _, _, _ -> reedIcons[iconVar]!! },
postProcess = noPost
) )
} }
return true
} }
} }

View File

@@ -2,20 +2,17 @@
package mods.betterfoliage.client.render package mods.betterfoliage.client.render
import mods.octarinecore.PI2 import mods.octarinecore.PI2
import mods.octarinecore.client.render.Model import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.lighting.PostProcessLambda
import mods.octarinecore.client.render.Quad
import mods.octarinecore.common.Double3 import mods.octarinecore.common.Double3
import mods.octarinecore.common.Int3 import mods.octarinecore.common.Int3
import mods.octarinecore.common.Rotation import mods.octarinecore.common.Rotation
import mods.octarinecore.common.times import mods.octarinecore.common.times
import net.minecraft.block.BlockState import net.minecraft.block.Block
import net.minecraft.block.material.Material import net.minecraft.block.material.Material
import net.minecraft.block.state.IBlockState
import net.minecraft.util.BlockRenderLayer import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.Direction import net.minecraft.util.EnumFacing
import net.minecraft.util.Direction.* import net.minecraft.util.EnumFacing.*
import kotlin.math.cos
import kotlin.math.sin
val up1 = Int3(1 to UP) val up1 = Int3(1 to UP)
val up2 = Int3(2 to UP) val up2 = Int3(2 to UP)
@@ -28,15 +25,15 @@ val denseLeavesRot = arrayOf(Rotation.identity, Rotation.rot90[EAST.ordinal], Ro
val whitewash: PostProcessLambda = { _, _, _, _, _ -> setGrey(1.4f) } val whitewash: PostProcessLambda = { _, _, _, _, _ -> setGrey(1.4f) }
val greywash: PostProcessLambda = { _, _, _, _, _ -> setGrey(1.0f) } val greywash: PostProcessLambda = { _, _, _, _, _ -> setGrey(1.0f) }
val BlockState.isSnow: Boolean get() = material.let { it == Material.SNOW } val IBlockState.isSnow: Boolean get() = material.let { it == Material.SNOW || it == Material.CRAFTED_SNOW }
fun Quad.toCross(rotAxis: Direction, trans: (Quad)->Quad) = fun Quad.toCross(rotAxis: EnumFacing, trans: (Quad)->Quad) =
(0..3).map { rotIdx -> (0..3).map { rotIdx ->
trans(rotate(Rotation.rot90[rotAxis.ordinal] * rotIdx).mirrorUV(rotIdx > 1, false)) trans(rotate(Rotation.rot90[rotAxis.ordinal] * rotIdx).mirrorUV(rotIdx > 1, false))
} }
fun Quad.toCross(rotAxis: Direction) = toCross(rotAxis) { it } fun Quad.toCross(rotAxis: EnumFacing) = toCross(rotAxis) { it }
fun xzDisk(modelIdx: Int) = (PI2 * modelIdx / 64.0).let { Double3(cos(it), 0.0, sin(it)) } fun xzDisk(modelIdx: Int) = (PI2 * modelIdx / 64.0).let { Double3(Math.cos(it), 0.0, Math.sin(it)) }
val rotationFromUp = arrayOf( val rotationFromUp = arrayOf(
Rotation.rot90[EAST.ordinal] * 2, Rotation.rot90[EAST.ordinal] * 2,
@@ -61,5 +58,5 @@ fun Model.mix(first: Model, second: Model, predicate: (Int)->Boolean) {
val BlockRenderLayer.isCutout: Boolean get() = (this == BlockRenderLayer.CUTOUT) || (this == BlockRenderLayer.CUTOUT_MIPPED) val BlockRenderLayer.isCutout: Boolean get() = (this == BlockRenderLayer.CUTOUT) || (this == BlockRenderLayer.CUTOUT_MIPPED)
fun BlockState.canRenderInLayer(layer: BlockRenderLayer) = this.block.canRenderInLayer(this, layer) fun IBlockState.canRenderInLayer(layer: BlockRenderLayer) = this.block.canRenderInLayer(this, layer)
fun BlockState.canRenderInCutout() = this.block.canRenderInLayer(this, BlockRenderLayer.CUTOUT) || this.block.canRenderInLayer(this, BlockRenderLayer.CUTOUT_MIPPED) fun IBlockState.canRenderInCutout() = this.block.canRenderInLayer(this, BlockRenderLayer.CUTOUT) || this.block.canRenderInLayer(this, BlockRenderLayer.CUTOUT_MIPPED)

View File

@@ -1,26 +1,33 @@
package mods.betterfoliage.client.render.column package mods.betterfoliage.client.render.column
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.client.Client import mods.betterfoliage.client.Client
import mods.betterfoliage.client.chunk.ChunkOverlayLayer
import mods.betterfoliage.client.chunk.ChunkOverlayManager import mods.betterfoliage.client.chunk.ChunkOverlayManager
import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.integration.ShadersModIntegration.renderAs import mods.betterfoliage.client.integration.ShadersModIntegration.renderAs
import mods.betterfoliage.client.render.* import mods.betterfoliage.client.render.*
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.BlockType.* import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.BlockType.*
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType.* import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType.*
import mods.octarinecore.client.render.CombinedContext import mods.octarinecore.client.render.*
import mods.octarinecore.client.render.Model import mods.octarinecore.client.resource.ModelRenderRegistry
import mods.octarinecore.client.render.RenderDecorator import mods.octarinecore.common.*
import mods.octarinecore.client.render.noPost import net.minecraft.block.state.IBlockState
import mods.octarinecore.common.Rotation import net.minecraft.client.renderer.BlockRendererDispatcher
import mods.octarinecore.common.face import net.minecraft.client.renderer.BufferBuilder
import mods.octarinecore.common.rot import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.block.BlockRenderType.MODEL import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.Direction.* import net.minecraft.util.EnumBlockRenderType
import net.minecraftforge.eventbus.api.IEventBus import net.minecraft.util.EnumBlockRenderType.MODEL
import net.minecraft.util.EnumFacing.*
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockAccess
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
@SideOnly(Side.CLIENT)
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
abstract class AbstractRenderColumn(modId: String, modBus: IEventBus) : RenderDecorator(modId, modBus) { abstract class AbstractRenderColumn(modId: String) : AbstractBlockRenderingHandler(modId) {
/** The rotations necessary to bring the models in position for the 4 quadrants */ /** The rotations necessary to bring the models in position for the 4 quadrants */
val quadrantRotations = Array(4) { Rotation.rot90[UP.ordinal] * it } val quadrantRotations = Array(4) { Rotation.rot90[UP.ordinal] * it }
@@ -91,25 +98,28 @@ abstract class AbstractRenderColumn(modId: String, modBus: IEventBus) : RenderDe
q1 == q2 || ((q1 == SQUARE || q1 == INVISIBLE) && (q2 == SQUARE || q2 == INVISIBLE)) q1 == q2 || ((q1 == SQUARE || q1 == INVISIBLE) && (q2 == SQUARE || q2 == INVISIBLE))
@Suppress("NON_EXHAUSTIVE_WHEN") @Suppress("NON_EXHAUSTIVE_WHEN")
override fun render(ctx: CombinedContext) { override fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean {
val roundLog = ChunkOverlayManager.get(overlayLayer, ctx) val roundLog = ChunkOverlayManager.get(overlayLayer, ctx.world!!, ctx.pos)
when(roundLog) { when(roundLog) {
ColumnLayerData.SkipRender -> return ColumnLayerData.SkipRender -> return true
ColumnLayerData.NormalRender -> return ctx.render() ColumnLayerData.NormalRender -> return renderWorldBlockBase(ctx, dispatcher, renderer, null)
ColumnLayerData.ResolveError, null -> { ColumnLayerData.ResolveError, null -> {
BetterFoliage.logRenderError(ctx.state, ctx.pos) Client.logRenderError(ctx.blockState(Int3.zero), ctx.pos)
return ctx.render() return renderWorldBlockBase(ctx, dispatcher, renderer, null)
} }
} }
// if log axis is not defined and "Default to vertical" config option is not set, render normally // if log axis is not defined and "Default to vertical" config option is not set, render normally
if ((roundLog as ColumnLayerData.SpecialRender).column.axis == null && !overlayLayer.defaultToY) { if ((roundLog as ColumnLayerData.SpecialRender).column.axis == null && !overlayLayer.defaultToY) {
return ctx.render() return renderWorldBlockBase(ctx, dispatcher, renderer, null)
} }
// get AO data
modelRenderer.updateShading(Int3.zero, allFaces)
val baseRotation = rotationFromUp[((roundLog.column.axis ?: Axis.Y) to AxisDirection.POSITIVE).face.ordinal] val baseRotation = rotationFromUp[((roundLog.column.axis ?: Axis.Y) to AxisDirection.POSITIVE).face.ordinal]
renderAs(ctx, MODEL) { renderAs(ctx.blockState(Int3.zero), MODEL, renderer) {
quadrantRotations.forEachIndexed { idx, quadrantRotation -> quadrantRotations.forEachIndexed { idx, quadrantRotation ->
// set rotation for the current quadrant // set rotation for the current quadrant
val rotation = baseRotation + quadrantRotation val rotation = baseRotation + quadrantRotation
@@ -131,7 +141,8 @@ abstract class AbstractRenderColumn(modId: String, modBus: IEventBus) : RenderDe
else -> null else -> null
} }
if (sideModel != null) ctx.render( if (sideModel != null) modelRenderer.render(
renderer,
sideModel, sideModel,
rotation, rotation,
icon = roundLog.column.side, icon = roundLog.column.side,
@@ -184,7 +195,8 @@ abstract class AbstractRenderColumn(modId: String, modBus: IEventBus) : RenderDe
} }
} }
if (upModel != null) ctx.render( if (upModel != null) modelRenderer.render(
renderer,
upModel, upModel,
rotation, rotation,
icon = upIcon, icon = upIcon,
@@ -194,7 +206,8 @@ abstract class AbstractRenderColumn(modId: String, modBus: IEventBus) : RenderDe
} }
} }
) )
if (downModel != null) ctx.render( if (downModel != null) modelRenderer.render(
renderer,
downModel, downModel,
rotation, rotation,
icon = downIcon, icon = downIcon,
@@ -206,5 +219,6 @@ abstract class AbstractRenderColumn(modId: String, modBus: IEventBus) : RenderDe
) )
} }
} }
return true
} }
} }

View File

@@ -2,20 +2,23 @@ package mods.betterfoliage.client.render.column
import mods.betterfoliage.client.chunk.ChunkOverlayLayer import mods.betterfoliage.client.chunk.ChunkOverlayLayer
import mods.betterfoliage.client.chunk.ChunkOverlayManager import mods.betterfoliage.client.chunk.ChunkOverlayManager
import mods.betterfoliage.client.chunk.dimType
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.BlockType.* import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.BlockType.*
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType
import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType.* import mods.betterfoliage.client.render.column.ColumnLayerData.SpecialRender.QuadrantType.*
import mods.betterfoliage.client.render.rotationFromUp import mods.betterfoliage.client.render.rotationFromUp
import mods.octarinecore.client.render.BlockCtx import mods.octarinecore.client.render.BlockContext
import mods.octarinecore.client.resource.ModelRenderRegistry import mods.octarinecore.client.resource.ModelRenderRegistry
import mods.octarinecore.common.* import mods.octarinecore.common.Int3
import net.minecraft.block.BlockState import mods.octarinecore.common.Rotation
import net.minecraft.util.Direction.Axis import mods.octarinecore.common.face
import net.minecraft.util.Direction.AxisDirection import mods.octarinecore.common.plus
import net.minecraft.block.state.IBlockState
import net.minecraft.util.EnumFacing
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.world.IEnviromentBlockReader import net.minecraft.world.IBlockAccess
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
/** Index of SOUTH-EAST quadrant. */ /** Index of SOUTH-EAST quadrant. */
const val SE = 0 const val SE = 0
@@ -29,11 +32,13 @@ const val SW = 3
/** /**
* Sealed class hierarchy for all possible render outcomes * Sealed class hierarchy for all possible render outcomes
*/ */
@SideOnly(Side.CLIENT)
sealed class ColumnLayerData { sealed class ColumnLayerData {
/** /**
* Data structure to cache texture and world neighborhood data relevant to column rendering * Data structure to cache texture and world neighborhood data relevant to column rendering
*/ */
@Suppress("ArrayInDataClass") // not used in comparisons anywhere @Suppress("ArrayInDataClass") // not used in comparisons anywhere
@SideOnly(Side.CLIENT)
data class SpecialRender( data class SpecialRender(
val column: ColumnTextureInfo, val column: ColumnTextureInfo,
val upType: BlockType, val upType: BlockType,
@@ -47,12 +52,15 @@ sealed class ColumnLayerData {
} }
/** Column block should not be rendered at all */ /** Column block should not be rendered at all */
@SideOnly(Side.CLIENT)
object SkipRender : ColumnLayerData() object SkipRender : ColumnLayerData()
/** Column block must be rendered normally */ /** Column block must be rendered normally */
@SideOnly(Side.CLIENT)
object NormalRender : ColumnLayerData() object NormalRender : ColumnLayerData()
/** Error while resolving render data, column block must be rendered normally */ /** Error while resolving render data, column block must be rendered normally */
@SideOnly(Side.CLIENT)
object ResolveError : ColumnLayerData() object ResolveError : ColumnLayerData()
} }
@@ -60,26 +68,29 @@ sealed class ColumnLayerData {
abstract class ColumnRenderLayer : ChunkOverlayLayer<ColumnLayerData> { abstract class ColumnRenderLayer : ChunkOverlayLayer<ColumnLayerData> {
abstract val registry: ModelRenderRegistry<ColumnTextureInfo> abstract val registry: ModelRenderRegistry<ColumnTextureInfo>
abstract val blockPredicate: (BlockState)->Boolean abstract val blockPredicate: (IBlockState)->Boolean
abstract val surroundPredicate: (IBlockState) -> Boolean
abstract val connectSolids: Boolean abstract val connectSolids: Boolean
abstract val lenientConnect: Boolean abstract val lenientConnect: Boolean
abstract val defaultToY: Boolean abstract val defaultToY: Boolean
val allNeighborOffsets = (-1..1).flatMap { offsetX -> (-1..1).flatMap { offsetY -> (-1..1).map { offsetZ -> Int3(offsetX, offsetY, offsetZ) }}} val allNeighborOffsets = (-1..1).flatMap { offsetX -> (-1..1).flatMap { offsetY -> (-1..1).map { offsetZ -> Int3(offsetX, offsetY, offsetZ) }}}
override fun onBlockUpdate(world: IEnviromentBlockReader, pos: BlockPos) { override fun onBlockUpdate(world: IBlockAccess, pos: BlockPos) {
allNeighborOffsets.forEach { offset -> ChunkOverlayManager.clear(world.dimType, this, pos + offset) } allNeighborOffsets.forEach { offset -> ChunkOverlayManager.clear(this, pos + offset) }
} }
override fun calculate(ctx: BlockCtx): ColumnLayerData { override fun calculate(world: IBlockAccess, pos: BlockPos) = calculate(BlockContext(world, pos))
if (allDirections.all { ctx.offset(it).isNormalCube }) return ColumnLayerData.SkipRender
fun calculate(ctx: BlockContext): ColumnLayerData {
if (ctx.isSurroundedBy(surroundPredicate)) return ColumnLayerData.SkipRender
val columnTextures = registry[ctx] ?: return ColumnLayerData.ResolveError val columnTextures = registry[ctx] ?: return ColumnLayerData.ResolveError
// if log axis is not defined and "Default to vertical" config option is not set, render normally // if log axis is not defined and "Default to vertical" config option is not set, render normally
val logAxis = columnTextures.axis ?: if (defaultToY) Axis.Y else return ColumnLayerData.NormalRender val logAxis = columnTextures.axis ?: if (defaultToY) EnumFacing.Axis.Y else return ColumnLayerData.NormalRender
// check log neighborhood // check log neighborhood
val baseRotation = rotationFromUp[(logAxis to AxisDirection.POSITIVE).face.ordinal] val baseRotation = rotationFromUp[(logAxis to EnumFacing.AxisDirection.POSITIVE).face.ordinal]
val upType = ctx.blockType(baseRotation, logAxis, Int3(0, 1, 0)) val upType = ctx.blockType(baseRotation, logAxis, Int3(0, 1, 0))
val downType = ctx.blockType(baseRotation, logAxis, Int3(0, -1, 0)) val downType = ctx.blockType(baseRotation, logAxis, Int3(0, -1, 0))
@@ -98,7 +109,7 @@ abstract class ColumnRenderLayer : ChunkOverlayLayer<ColumnLayerData> {
} }
/** Fill the array of [QuadrantType]s based on the blocks to the sides of this one. */ /** Fill the array of [QuadrantType]s based on the blocks to the sides of this one. */
fun Array<QuadrantType>.checkNeighbors(ctx: BlockCtx, rotation: Rotation, logAxis: Axis, yOff: Int): Array<QuadrantType> { fun Array<QuadrantType>.checkNeighbors(ctx: BlockContext, rotation: Rotation, logAxis: EnumFacing.Axis, yOff: Int): Array<QuadrantType> {
val blkS = ctx.blockType(rotation, logAxis, Int3(0, yOff, 1)) val blkS = ctx.blockType(rotation, logAxis, Int3(0, yOff, 1))
val blkE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 0)) val blkE = ctx.blockType(rotation, logAxis, Int3(1, yOff, 0))
val blkN = ctx.blockType(rotation, logAxis, Int3(0, yOff, -1)) val blkN = ctx.blockType(rotation, logAxis, Int3(0, yOff, -1))
@@ -165,13 +176,13 @@ abstract class ColumnRenderLayer : ChunkOverlayLayer<ColumnLayerData> {
/** /**
* Get the type of the block at the given offset in a rotated reference frame. * Get the type of the block at the given offset in a rotated reference frame.
*/ */
fun BlockCtx.blockType(rotation: Rotation, axis: Axis, offset: Int3): ColumnLayerData.SpecialRender.BlockType { fun BlockContext.blockType(rotation: Rotation, axis: EnumFacing.Axis, offset: Int3): ColumnLayerData.SpecialRender.BlockType {
val offsetRot = offset.rotate(rotation) val offsetRot = offset.rotate(rotation)
val state = state(offsetRot) val state = blockState(offsetRot)
return if (!blockPredicate(state)) { return if (!blockPredicate(state)) {
if (offset(offsetRot).isNormalCube) SOLID else NONSOLID if (state.isOpaqueCube) SOLID else NONSOLID
} else { } else {
(registry[state, world, pos + offsetRot]?.axis ?: if (Config.roundLogs.defaultY) Axis.Y else null)?.let { (registry[state, world!!, pos + offsetRot]?.axis ?: if (Config.roundLogs.defaultY) EnumFacing.Axis.Y else null)?.let {
if (it == axis) PARALLEL else PERPENDICULAR if (it == axis) PARALLEL else PERPENDICULAR
} ?: SOLID } ?: SOLID
} }

View File

@@ -1,19 +1,28 @@
package mods.betterfoliage.client.render.column package mods.betterfoliage.client.render.column
import mods.octarinecore.client.render.lighting.QuadIconResolver import mods.octarinecore.client.render.QuadIconResolver
import mods.octarinecore.client.render.blockContext
import mods.octarinecore.client.resource.ModelRenderKey
import mods.octarinecore.client.resource.get
import mods.octarinecore.common.rotate import mods.octarinecore.common.rotate
import net.minecraft.client.renderer.texture.TextureAtlasSprite import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.Direction.* import net.minecraft.client.renderer.texture.TextureMap
import net.minecraft.util.EnumFacing
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Logger
@SideOnly(Side.CLIENT)
interface ColumnTextureInfo { interface ColumnTextureInfo {
val axis: Axis? val axis: EnumFacing.Axis?
val top: QuadIconResolver val top: QuadIconResolver
val bottom: QuadIconResolver val bottom: QuadIconResolver
val side: QuadIconResolver val side: QuadIconResolver
} }
@SideOnly(Side.CLIENT)
open class SimpleColumnInfo( open class SimpleColumnInfo(
override val axis: Axis?, override val axis: EnumFacing.Axis?,
val topTexture: TextureAtlasSprite, val topTexture: TextureAtlasSprite,
val bottomTexture: TextureAtlasSprite, val bottomTexture: TextureAtlasSprite,
val sideTextures: List<TextureAtlasSprite> val sideTextures: List<TextureAtlasSprite>
@@ -25,8 +34,17 @@ open class SimpleColumnInfo(
override val top: QuadIconResolver = { _, _, _ -> topTexture } override val top: QuadIconResolver = { _, _, _ -> topTexture }
override val bottom: QuadIconResolver = { _, _, _ -> bottomTexture } override val bottom: QuadIconResolver = { _, _, _ -> bottomTexture }
override val side: QuadIconResolver = { ctx, idx, _ -> override val side: QuadIconResolver = { ctx, idx, _ ->
val worldFace = (if ((idx and 1) == 0) SOUTH else EAST).rotate(ctx.modelRotation) val worldFace = (if ((idx and 1) == 0) EnumFacing.SOUTH else EnumFacing.EAST).rotate(ctx.rotation)
val sideIdx = if (sideTextures.size > 1) (ctx.semiRandom(1) + dirToIdx[worldFace.ordinal]) % sideTextures.size else 0 val sideIdx = if (sideTextures.size > 1) (blockContext.random(1) + dirToIdx[worldFace.ordinal]) % sideTextures.size else 0
sideTextures[sideIdx] sideTextures[sideIdx]
} }
class Key(override val logger: Logger, val axis: EnumFacing.Axis?, val textures: List<String>) : ModelRenderKey<ColumnTextureInfo> {
override fun resolveSprites(atlas: TextureMap) = SimpleColumnInfo(
axis,
atlas[textures[0]] ?: atlas.missingSprite,
atlas[textures[1]] ?: atlas.missingSprite,
textures.drop(2).map { atlas[it] ?: atlas.missingSprite }
)
}
} }

View File

@@ -1,10 +0,0 @@
package mods.betterfoliage.client.resource
import net.minecraft.client.renderer.model.ModelResourceLocation
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.ResourceLocation
typealias Identifier = ResourceLocation
typealias ModelIdentifier = ModelResourceLocation
typealias Sprite = TextureAtlasSprite

View File

@@ -1,8 +1,7 @@
package mods.betterfoliage.client.texture package mods.betterfoliage.client.texture
import mods.betterfoliage.client.resource.Identifier
import mods.octarinecore.client.resource.* import mods.octarinecore.client.resource.*
import net.minecraft.resources.IResourceManager import net.minecraft.util.ResourceLocation
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
/** /**
@@ -11,13 +10,13 @@ import java.awt.image.BufferedImage
* *
* @param[domain] Resource domain of generator * @param[domain] Resource domain of generator
*/ */
data class GeneratedGrass(val sprite: Identifier, val isSnowed: Boolean, val atlas: Atlas = Atlas.BLOCKS) { class GrassGenerator(domain: String) : TextureGenerator(domain) {
constructor(sprite: String, isSnowed: Boolean) : this(Identifier(sprite), isSnowed)
fun register(pack: GeneratedBlockTexturePack) = pack.register(this, this::draw) override fun generate(params: ParameterList): BufferedImage? {
val target = targetResource(params)!!
val isSnowed = params["snowed"]?.toBoolean() ?: false
fun draw(resourceManager: IResourceManager): ByteArray { val baseTexture = resourceManager[target.second]?.loadImage() ?: return null
val baseTexture = resourceManager.loadSprite(atlas.wrap(sprite))
val result = BufferedImage(baseTexture.width, baseTexture.height, BufferedImage.TYPE_4BYTE_ABGR) val result = BufferedImage(baseTexture.width, baseTexture.height, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = result.createGraphics() val graphics = result.createGraphics()
@@ -26,7 +25,7 @@ data class GeneratedGrass(val sprite: Identifier, val isSnowed: Boolean, val atl
val frames = baseTexture.height / size val frames = baseTexture.height / size
// iterate all frames // iterate all frames
for (frame in 0 until frames) { for (frame in 0 .. frames - 1) {
val baseFrame = baseTexture.getSubimage(0, size * frame, size, size) val baseFrame = baseTexture.getSubimage(0, size * frame, size, size)
val grassFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR) val grassFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR)
@@ -40,11 +39,12 @@ data class GeneratedGrass(val sprite: Identifier, val isSnowed: Boolean, val atl
} }
// blend with white if snowed // blend with white if snowed
if (isSnowed) { if (isSnowed && target.first == ResourceType.COLOR) {
for (x in 0..result.width - 1) for (y in 0..result.height - 1) { for (x in 0..result.width - 1) for (y in 0..result.height - 1) {
result[x, y] = blendRGB(result[x, y], 16777215, 2, 3) result[x, y] = blendRGB(result[x, y], 16777215, 2, 3)
} }
} }
return result.bytes
return result
} }
} }

View File

@@ -1,18 +1,28 @@
package mods.betterfoliage.client.texture package mods.betterfoliage.client.texture
import mods.betterfoliage.BetterFoliage import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.BlockConfig import mods.betterfoliage.client.Client
import mods.betterfoliage.client.config.Config import mods.betterfoliage.client.config.Config
import mods.betterfoliage.client.resource.Identifier import mods.octarinecore.client.render.BlockContext
import mods.octarinecore.client.render.lighting.HSB import mods.octarinecore.client.render.HSB
import mods.octarinecore.client.resource.* import mods.octarinecore.client.resource.*
import mods.octarinecore.common.Int3
import mods.octarinecore.common.config.ConfigurableBlockMatcher import mods.octarinecore.common.config.ConfigurableBlockMatcher
import mods.octarinecore.common.config.IBlockMatcher
import mods.octarinecore.common.config.ModelTextureList import mods.octarinecore.common.config.ModelTextureList
import net.minecraft.block.BlockState import mods.octarinecore.findFirst
import net.minecraft.block.state.IBlockState
import net.minecraft.client.renderer.texture.TextureAtlasSprite import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.client.renderer.texture.TextureMap
import net.minecraft.util.EnumFacing
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockAccess
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level import org.apache.logging.log4j.Level
import org.apache.logging.log4j.Logger
import java.lang.Math.min import java.lang.Math.min
import java.util.concurrent.CompletableFuture
const val defaultGrassColor = 0 const val defaultGrassColor = 0
@@ -32,20 +42,19 @@ class GrassInfo(
object GrassRegistry : ModelRenderRegistryRoot<GrassInfo>() object GrassRegistry : ModelRenderRegistryRoot<GrassInfo>()
object AsyncGrassDiscovery : ConfigurableModelDiscovery<GrassInfo>() { object StandardGrassRegistry : ModelRenderRegistryConfigurable<GrassInfo>() {
override val logger = BetterFoliage.logDetail override val logger = BetterFoliageMod.logDetail
override val matchClasses: ConfigurableBlockMatcher get() = BlockConfig.grassBlocks override val matchClasses: ConfigurableBlockMatcher get() = Config.blocks.grassClasses
override val modelTextures: List<ModelTextureList> get() = BlockConfig.grassModels.modelList override val modelTextures: List<ModelTextureList> get() = Config.blocks.grassModels.list
override fun processModel(state: IBlockState, textures: List<String>) = StandardGrassKey(logger, textures[0])
}
override fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture): CompletableFuture<GrassInfo> { class StandardGrassKey(override val logger: Logger, val textureName: String) : ModelRenderKey<GrassInfo> {
val textureName = textures[0] override fun resolveSprites(atlas: TextureMap): GrassInfo {
val spriteF = atlas.sprite(Identifier(textureName)) val logName = "StandardGrassKey"
val texture = atlas[textureName] ?: atlas.missingSprite
logger.log(Level.DEBUG, "$logName: texture $textureName") logger.log(Level.DEBUG, "$logName: texture $textureName")
return atlas.mapAfter { val hsb = HSB.fromColor(texture.averageColor ?: defaultGrassColor)
val sprite = spriteF.get()
logger.log(Level.DEBUG, "$logName: block state $state")
logger.log(Level.DEBUG, "$logName: texture $textureName")
val hsb = HSB.fromColor(sprite.averageColor)
val overrideColor = if (hsb.saturation >= Config.shortGrass.saturationThreshold) { val overrideColor = if (hsb.saturation >= Config.shortGrass.saturationThreshold) {
logger.log(Level.DEBUG, "$logName: brightness ${hsb.brightness}") logger.log(Level.DEBUG, "$logName: brightness ${hsb.brightness}")
logger.log(Level.DEBUG, "$logName: saturation ${hsb.saturation} >= ${Config.shortGrass.saturationThreshold}, using texture color") logger.log(Level.DEBUG, "$logName: saturation ${hsb.saturation} >= ${Config.shortGrass.saturationThreshold}, using texture color")
@@ -54,12 +63,6 @@ object AsyncGrassDiscovery : ConfigurableModelDiscovery<GrassInfo>() {
logger.log(Level.DEBUG, "$logName: saturation ${hsb.saturation} < ${Config.shortGrass.saturationThreshold}, using block color") logger.log(Level.DEBUG, "$logName: saturation ${hsb.saturation} < ${Config.shortGrass.saturationThreshold}, using block color")
null null
} }
GrassInfo(sprite, overrideColor) return GrassInfo(texture, overrideColor)
}
}
fun init() {
GrassRegistry.registries.add(this)
BetterFoliage.blockSprites.providers.add(this)
} }
} }

View File

@@ -2,8 +2,7 @@ package mods.betterfoliage.client.texture
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.octarinecore.client.resource.* import mods.octarinecore.client.resource.*
import net.minecraft.resources.IResource import mods.octarinecore.stripStart
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation import net.minecraft.util.ResourceLocation
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
@@ -11,17 +10,22 @@ import java.awt.image.BufferedImage
* Generate round leaf textures from leaf block textures. * 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. * The base texture is tiled 2x2, then parts of it are made transparent by applying a mask to the alpha channel.
* *
* Different leaf types may have their own alpha mask. * Generator parameter _type_: Leaf type (configurable by user). Different leaf types may have their own alpha mask.
* *
* @param[domain] Resource domain of generator * @param[domain] Resource domain of generator
*/ */
data class GeneratedLeaf(val sprite: ResourceLocation, val leafType: String, val atlas: Atlas = Atlas.BLOCKS) { class LeafGenerator(domain: String) : TextureGenerator(domain) {
fun register(pack: GeneratedBlockTexturePack) = pack.register(this, this::draw) override fun generate(params: ParameterList): BufferedImage? {
val target = targetResource(params)!!
val leafType = params["type"] ?: "default"
fun draw(resourceManager: IResourceManager): ByteArray { val handDrawnLoc = target.second.stripStart("textures/").stripStart("blocks/").let {
val baseTexture = resourceManager.loadSprite(atlas.wrap(sprite)) ResourceLocation(BetterFoliageMod.DOMAIN, "${it.namespace}/textures/blocks/${it.path}")
}
resourceManager[handDrawnLoc]?.loadImage()?.let { return it }
val baseTexture = resourceManager[target.second]?.loadImage() ?: return null
val size = baseTexture.width val size = baseTexture.width
val frames = baseTexture.height / size val frames = baseTexture.height / size
@@ -32,7 +36,7 @@ data class GeneratedLeaf(val sprite: ResourceLocation, val leafType: String, val
val graphics = leafTexture.createGraphics() val graphics = leafTexture.createGraphics()
// iterate all frames // iterate all frames
for (frame in 0 until frames) { for (frame in 0 .. frames - 1) {
val baseFrame = baseTexture.getSubimage(0, size * frame, size, size) val baseFrame = baseTexture.getSubimage(0, size * frame, size, size)
val leafFrame = BufferedImage(size * 2, size * 2, BufferedImage.TYPE_4BYTE_ABGR) val leafFrame = BufferedImage(size * 2, size * 2, BufferedImage.TYPE_4BYTE_ABGR)
@@ -45,8 +49,8 @@ data class GeneratedLeaf(val sprite: ResourceLocation, val leafType: String, val
} }
// overlay alpha mask // overlay alpha mask
if (maskTexture != null) { if (target.first == ResourceType.COLOR && maskTexture != null) {
for (x in 0 until size * 2) for (y in 0 until size * 2) { for (x in 0 .. size * 2 - 1) for (y in 0 .. size * 2 - 1) {
val basePixel = leafFrame[x, y].toLong() and 0xFFFFFFFFL val basePixel = leafFrame[x, y].toLong() and 0xFFFFFFFFL
val maskPixel = maskTexture[scale(x), scale(y)].toLong() and 0xFF000000L or 0xFFFFFFL val maskPixel = maskTexture[scale(x), scale(y)].toLong() and 0xFF000000L or 0xFFFFFFL
leafFrame[x, y] = (basePixel and maskPixel).toInt() leafFrame[x, y] = (basePixel and maskPixel).toInt()
@@ -57,7 +61,7 @@ data class GeneratedLeaf(val sprite: ResourceLocation, val leafType: String, val
graphics.drawImage(leafFrame, 0, size * frame * 2, null) graphics.drawImage(leafFrame, 0, size * frame * 2, null)
} }
return leafTexture.bytes return leafTexture
} }
/** /**
@@ -67,20 +71,7 @@ data class GeneratedLeaf(val sprite: ResourceLocation, val leafType: String, val
* @param[maxSize] Preferred mask size. * @param[maxSize] Preferred mask size.
*/ */
fun getLeafMask(type: String, maxSize: Int) = getMultisizeTexture(maxSize) { size -> fun getLeafMask(type: String, maxSize: Int) = getMultisizeTexture(maxSize) { size ->
ResourceLocation(BetterFoliageMod.MOD_ID, "textures/blocks/leafmask_${size}_${type}.png") ResourceLocation(BetterFoliageMod.DOMAIN, "textures/blocks/leafmask_${size}_${type}.png")
} }
/**
* 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 = mutableListOf<Int>()
while(size > 2) { sizes.add(size); size /= 2 }
return sizes.map { resourceManager[maskPath(it)] }.filterNotNull().firstOrNull()
}
} }

View File

@@ -1,63 +1,48 @@
package mods.betterfoliage.client.texture package mods.betterfoliage.client.texture
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.BetterFoliageMod import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.resource.Identifier
import mods.betterfoliage.client.resource.Sprite
import mods.octarinecore.client.resource.* import mods.octarinecore.client.resource.*
import mods.octarinecore.stripStart import mods.octarinecore.stripStart
import mods.octarinecore.client.resource.Atlas import net.minecraft.client.renderer.texture.TextureAtlasSprite
import mods.octarinecore.common.sinkAsync import net.minecraft.util.ResourceLocation
import net.minecraft.client.particle.ParticleManager import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraft.resources.IResourceManager import net.minecraftforge.common.MinecraftForge
import java.util.concurrent.CompletableFuture import net.minecraftforge.fml.common.eventhandler.EventPriority
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
class FixedSpriteSet(val sprites: List<Sprite>) : SpriteSet { object LeafParticleRegistry {
override val num = sprites.size
override fun get(idx: Int) = sprites[idx % num]
}
object LeafParticleRegistry : AsyncSpriteProvider<ParticleManager> {
val targetAtlas = Atlas.PARTICLES
val typeMappings = TextureMatcher() val typeMappings = TextureMatcher()
val particles = hashMapOf<String, SpriteSet>() val particles = hashMapOf<String, IconSet>()
operator fun get(type: String) = particles[type] ?: particles["default"]!! operator fun get(type: String) = particles[type] ?: particles["default"]!!
override fun setup(manager: IResourceManager, particleF: CompletableFuture<ParticleManager>, atlasFuture: AtlasFuture): StitchPhases { init { MinecraftForge.EVENT_BUS.register(this) }
@SubscribeEvent(priority = EventPriority.HIGH)
fun handleLoadModelData(event: LoadModelDataEvent) {
particles.clear() particles.clear()
val futures = mutableMapOf<String, List<CompletableFuture<Sprite>>>() typeMappings.loadMappings(ResourceLocation(BetterFoliageMod.DOMAIN, "leaf_texture_mappings.cfg"))
return StitchPhases(
discovery = particleF.sinkAsync {
typeMappings.loadMappings(Identifier(BetterFoliageMod.MOD_ID, "leaf_texture_mappings.cfg"))
(typeMappings.mappings.map { it.type } + "default").distinct().forEach { leafType ->
val ids = (0 until 16).map { idx -> Identifier(BetterFoliageMod.MOD_ID, "falling_leaf_${leafType}_$idx") }
val wids = ids.map { Atlas.PARTICLES.wrap(it) }
futures[leafType] = (0 until 16).map { idx -> Identifier(BetterFoliageMod.MOD_ID, "falling_leaf_${leafType}_$idx") }
.filter { manager.hasResource(Atlas.PARTICLES.wrap(it)) }
.map { atlasFuture.sprite(it) }
}
},
cleanup = atlasFuture.runAfter {
futures.forEach { leafType, spriteFutures ->
val sprites = spriteFutures.filter { !it.isCompletedExceptionally }.map { it.get() }
if (sprites.isNotEmpty()) particles[leafType] = FixedSpriteSet(sprites)
}
if (particles["default"] == null) particles["default"] = FixedSpriteSet(listOf(atlasFuture.missing.get()!!))
}
)
} }
fun init() { @SubscribeEvent
BetterFoliage.particleSprites.providers.add(this) fun handlePreStitch(event: TextureStitchEvent.Pre) {
val allTypes = (typeMappings.mappings.map { it.type } + "default").distinct()
allTypes.forEach { leafType ->
val particleSet = IconSet("betterfoliage", "blocks/falling_leaf_${leafType}_%d").apply { onPreStitch(event.map) }
if (leafType == "default" || particleSet.num > 0) particles[leafType] = particleSet
}
}
@SubscribeEvent
fun handlePostStitch(event: TextureStitchEvent.Post) {
particles.forEach { (_, particleSet) -> particleSet.onPostStitch(event.map) }
} }
} }
class TextureMatcher { class TextureMatcher {
data class Mapping(val domain: String?, val path: String, val type: String) { data class Mapping(val domain: String?, val path: String, val type: String) {
fun matches(iconLocation: Identifier): Boolean { fun matches(iconLocation: ResourceLocation): Boolean {
return (domain == null || domain == iconLocation.namespace) && return (domain == null || domain == iconLocation.namespace) &&
iconLocation.path.stripStart("blocks/").contains(path, ignoreCase = true) iconLocation.path.stripStart("blocks/").contains(path, ignoreCase = true)
} }
@@ -65,10 +50,10 @@ class TextureMatcher {
val mappings: MutableList<Mapping> = mutableListOf() val mappings: MutableList<Mapping> = mutableListOf()
fun getType(resource: Identifier) = mappings.filter { it.matches(resource) }.map { it.type }.firstOrNull() fun getType(resource: ResourceLocation) = mappings.filter { it.matches(resource) }.map { it.type }.firstOrNull()
fun getType(iconName: String) = Identifier(iconName).let { getType(it) } fun getType(iconName: String) = ResourceLocation(iconName).let { getType(it) }
fun loadMappings(mappingLocation: Identifier) { fun loadMappings(mappingLocation: ResourceLocation) {
mappings.clear() mappings.clear()
resourceManager[mappingLocation]?.getLines()?.let { lines -> resourceManager[mappingLocation]?.getLines()?.let { lines ->
lines.filter { !it.startsWith("//") }.filter { !it.isEmpty() }.forEach { line -> lines.filter { !it.startsWith("//") }.filter { !it.isEmpty() }.forEach { line ->

View File

@@ -1,15 +1,30 @@
package mods.betterfoliage.client.texture package mods.betterfoliage.client.texture
import mods.betterfoliage.BetterFoliage import mods.betterfoliage.BetterFoliageMod
import mods.betterfoliage.client.config.BlockConfig import mods.betterfoliage.client.Client
import mods.betterfoliage.client.resource.Identifier import mods.betterfoliage.client.config.Config
import mods.octarinecore.HasLogger import mods.octarinecore.client.render.BlockContext
import mods.octarinecore.client.resource.* import mods.octarinecore.client.resource.*
import mods.octarinecore.common.Int3
import mods.octarinecore.common.config.ConfigurableBlockMatcher import mods.octarinecore.common.config.ConfigurableBlockMatcher
import mods.octarinecore.common.config.IBlockMatcher
import mods.octarinecore.common.config.ModelTextureList import mods.octarinecore.common.config.ModelTextureList
import net.minecraft.block.BlockState import mods.octarinecore.findFirst
import net.minecraft.block.state.IBlockState
import net.minecraft.client.renderer.texture.TextureAtlasSprite import net.minecraft.client.renderer.texture.TextureAtlasSprite
import java.util.concurrent.CompletableFuture import net.minecraft.client.renderer.texture.TextureMap
import net.minecraft.util.EnumFacing
import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockAccess
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.fml.common.eventhandler.EventPriority
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.relauncher.Side
import net.minecraftforge.fml.relauncher.SideOnly
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.Logger
const val defaultLeafColor = 0 const val defaultLeafColor = 0
@@ -22,35 +37,34 @@ class LeafInfo(
val leafType: String, val leafType: String,
/** Average color of the round leaf texture. */ /** Average color of the round leaf texture. */
val averageColor: Int = roundLeafTexture.averageColor val averageColor: Int = roundLeafTexture.averageColor ?: defaultLeafColor
) { ) {
/** [IconSet] of the textures to use for leaf particles emitted from this block. */ /** [IconSet] of the textures to use for leaf particles emitted from this block. */
val particleTextures: SpriteSet get() = LeafParticleRegistry[leafType] val particleTextures: IconSet get() = LeafParticleRegistry[leafType]
} }
object LeafRegistry : ModelRenderRegistryRoot<LeafInfo>() object LeafRegistry : ModelRenderRegistryRoot<LeafInfo>()
object AsyncLeafDiscovery : ConfigurableModelDiscovery<LeafInfo>() { object StandardLeafRegistry : ModelRenderRegistryConfigurable<LeafInfo>() {
override val logger = BetterFoliage.logDetail override val logger = BetterFoliageMod.logDetail
override val matchClasses: ConfigurableBlockMatcher get() = BlockConfig.leafBlocks override val matchClasses: ConfigurableBlockMatcher get() = Config.blocks.leavesClasses
override val modelTextures: List<ModelTextureList> get() = BlockConfig.leafModels.modelList override val modelTextures: List<ModelTextureList> get() = Config.blocks.leavesModels.list
override fun processModel(state: IBlockState, textures: List<String>) = StandardLeafKey(logger, textures[0])
override fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture) = defaultRegisterLeaf(Identifier(textures[0]), atlas)
fun init() {
LeafRegistry.registries.add(this)
BetterFoliage.blockSprites.providers.add(this)
}
} }
fun HasLogger.defaultRegisterLeaf(sprite: Identifier, atlas: AtlasFuture): CompletableFuture<LeafInfo> { class StandardLeafKey(override val logger: Logger, val textureName: String) : ModelRenderKey<LeafInfo> {
val leafType = LeafParticleRegistry.typeMappings.getType(sprite) ?: "default" lateinit var leafType: String
val generated = GeneratedLeaf(sprite, leafType).register(BetterFoliage.asyncPack) lateinit var generated: ResourceLocation
val roundLeaf = atlas.sprite(generated)
log(" leaf texture $sprite") override fun onPreStitch(atlas: TextureMap) {
log(" particle $leafType") val logName = "StandardLeafKey"
return atlas.mapAfter { leafType = LeafParticleRegistry.typeMappings.getType(textureName) ?: "default"
LeafInfo(roundLeaf.get(), leafType) generated = Client.genLeaves.generatedResource(textureName, "type" to leafType)
atlas.registerSprite(generated)
logger.log(Level.DEBUG, "$logName: leaf texture $textureName")
logger.log(Level.DEBUG, "$logName: particle $leafType")
} }
override fun resolveSprites(atlas: TextureMap) = LeafInfo(atlas[generated] ?: atlas.missingSprite, leafType)
} }

View File

@@ -1,13 +1,6 @@
@file:JvmName("Utils") @file:JvmName("Utils")
package mods.betterfoliage.client.texture package mods.betterfoliage.client.texture
import mods.betterfoliage.client.resource.Identifier
import mods.octarinecore.client.resource.Atlas
import mods.octarinecore.client.resource.get
import mods.octarinecore.client.resource.loadImage
import net.minecraft.resources.IResourceManager
import java.io.IOException
fun blendRGB(rgb1: Int, rgb2: Int, weight1: Int, weight2: Int): Int { 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 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 g = (((rgb1 shr 8) and 255) * weight1 + ((rgb2 shr 8) and 255) * weight2) / (weight1 + weight2)
@@ -16,5 +9,3 @@ fun blendRGB(rgb1: Int, rgb2: Int, weight1: Int, weight2: Int): Int {
val result = ((a shl 24) or (r shl 16) or (g shl 8) or b).toInt() val result = ((a shl 24) or (r shl 16) or (g shl 8) or b).toInt()
return result return result
} }
fun IResourceManager.loadSprite(id: Identifier) = this.get(id)?.loadImage() ?: throw IOException("Cannot load resource $id")

View File

@@ -0,0 +1,138 @@
package mods.betterfoliage.loader
import mods.octarinecore.metaprog.Transformer
import mods.octarinecore.metaprog.allAvailable
import net.minecraftforge.fml.relauncher.FMLLaunchHandler
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.ClassWriter.COMPUTE_FRAMES
import org.objectweb.asm.ClassWriter.COMPUTE_MAXS
import org.objectweb.asm.Opcodes.*
class BetterFoliageTransformer : Transformer() {
val isOptifinePresent = allAvailable(Refs.OptifineClassTransformer)
init {
if (FMLLaunchHandler.side().isClient) setupClient()
}
fun setupClient() {
// where: WorldClient.showBarrierParticles(), right after invoking Block.randomDisplayTick
// what: invoke BF code for every random display tick
// why: allows us to catch random display ticks, without touching block code
transformMethod(Refs.showBarrierParticles) {
find(invokeRef(Refs.randomDisplayTick))?.insertAfter {
log.info("[BetterFoliageLoader] Applying random display tick call hook")
varinsn(ALOAD, 0)
varinsn(ALOAD, 11)
varinsn(ALOAD, 7)
invokeStatic(Refs.onRandomDisplayTick)
} ?: log.warn("[BetterFoliageLoader] Failed to apply random display tick call hook!")
}
// where: BlockStateContainer$StateImplementation.getAmbientOcclusionLightValue()
// what: invoke BF code to overrule AO transparency value
// why: allows us to have light behave properly on non-solid log blocks
transformMethod(Refs.getAmbientOcclusionLightValue) {
find(FRETURN)?.insertBefore {
log.info("[BetterFoliageLoader] Applying getAmbientOcclusionLightValue() override")
varinsn(ALOAD, 0)
invokeStatic(Refs.getAmbientOcclusionLightValueOverride)
} ?: log.warn("[BetterFoliageLoader] Failed to apply getAmbientOcclusionLightValue() override!")
}
// where: BlockStateContainer$StateImplementation.useNeighborBrightness()
// what: invoke BF code to overrule _useNeighborBrightness_
// why: allows us to have light behave properly on non-solid log blocks
transformMethod(Refs.useNeighborBrightness) {
find(IRETURN)?.insertBefore {
log.info("[BetterFoliageLoader] Applying useNeighborBrightness() override")
varinsn(ALOAD, 0)
invokeStatic(Refs.useNeighborBrightnessOverride)
} ?: log.warn("[BetterFoliageLoader] Failed to apply useNeighborBrightness() override!")
}
// where: BlockStateContainer$StateImplementation.doesSideBlockRendering()
// what: invoke BF code to overrule condition
// why: allows us to make log blocks non-solid
transformMethod(Refs.doesSideBlockRendering) {
find(IRETURN)?.insertBefore {
log.info("[BetterFoliageLoader] Applying doesSideBlockRendering() override")
varinsn(ALOAD, 1)
varinsn(ALOAD, 2)
varinsn(ALOAD, 3)
invokeStatic(Refs.doesSideBlockRenderingOverride)
} ?: log.warn("[BetterFoliageLoader] Failed to apply doesSideBlockRendering() override!")
}
// where: BlockStateContainer$StateImplementation.isOpaqueCube()
// what: invoke BF code to overrule condition
// why: allows us to make log blocks non-solid
transformMethod(Refs.isOpaqueCube) {
find(IRETURN)?.insertBefore {
log.info("[BetterFoliageLoader] Applying isOpaqueCube() override")
varinsn(ALOAD, 0)
invokeStatic(Refs.isOpaqueCubeOverride)
} ?: log.warn("[BetterFoliageLoader] Failed to apply isOpaqueCube() override!")
}
// where: ModelLoader.setupModelRegistry(), right before the textures are loaded
// what: invoke handler code with ModelLoader instance
// why: allows us to iterate the unbaked models in ModelLoader in time to register textures
transformMethod(Refs.setupModelRegistry) {
find(invokeName("addAll"))?.insertAfter {
log.info("[BetterFoliageLoader] Applying ModelLoader lifecycle callback")
varinsn(ALOAD, 0)
invokeStatic(Refs.onAfterLoadModelDefinitions)
} ?: log.warn("[BetterFoliageLoader] Failed to apply ModelLoader lifecycle callback!")
}
// where: RenderChunk.rebuildChunk()
// what: replace call to BlockRendererDispatcher.renderBlock()
// why: allows us to perform additional rendering for each block
// what: invoke code to overrule result of Block.canRenderInLayer()
// why: allows us to render transparent quads for blocks which are only on the SOLID layer
transformMethod(Refs.rebuildChunk) {
applyWriterFlags(COMPUTE_FRAMES, COMPUTE_MAXS)
find(invokeRef(Refs.renderBlock))?.replace {
log.info("[BetterFoliageLoader] Applying RenderChunk block render override")
varinsn(ALOAD, if (isOptifinePresent) 22 else 20)
invokeStatic(Refs.renderWorldBlock)
}
if (isOptifinePresent) {
find(varinsn(ISTORE, 23))?.insertAfter {
log.info("[BetterFoliageLoader] Applying RenderChunk block layer override (Optifine)")
varinsn(ALOAD, 19)
varinsn(ALOAD, 18)
varinsn(ALOAD, 22)
invokeStatic(Refs.canRenderBlockInLayer)
varinsn(ISTORE, 23)
}
} else {
find(invokeRef(Refs.canRenderInLayer))?.replace {
log.info("[BetterFoliageLoader] Applying RenderChunk block layer override (non-Optifine)")
invokeStatic(Refs.canRenderBlockInLayer)
}
}
}
// where: net.minecraft.client.renderer.BlockModelRenderer$AmbientOcclusionFace
// what: make constructor public
// why: use vanilla AO calculation at will without duplicating code
transformMethod(Refs.AOF_constructor) {
log.info("[BetterFoliageLoader] Setting AmbientOcclusionFace constructor public")
makePublic()
}
// where: shadersmod.client.SVertexBuilder.pushEntity()
// what: invoke code to overrule block data
// why: allows us to change the block ID seen by shader programs
transformMethod(Refs.pushEntity_state) {
find(invokeRef(Refs.pushEntity_num))?.insertBefore {
log.info("[BetterFoliageLoader] Applying SVertexBuilder.pushEntity() block ID override")
varinsn(ALOAD, 0)
invokeStatic(Refs.getBlockIdOverride)
} ?: log.warn("[BetterFoliageLoader] Failed to apply SVertexBuilder.pushEntity() block ID override!")
}
}
}

View File

@@ -0,0 +1,117 @@
package mods.betterfoliage.loader
import mods.octarinecore.metaprog.ClassRef
import mods.octarinecore.metaprog.FieldRef
import mods.octarinecore.metaprog.MethodRef
import net.minecraftforge.fml.relauncher.FMLInjectionData
/** Singleton object holding references to foreign code elements. */
object Refs {
val mcVersion = FMLInjectionData.data()[4].toString()
// Java
val String = ClassRef("java.lang.String")
val Map = ClassRef("java.util.Map")
val List = ClassRef("java.util.List")
val Random = ClassRef("java.util.Random")
// Minecraft
val IBlockAccess = ClassRef("net.minecraft.world.IBlockAccess")
val IBlockState = ClassRef("net.minecraft.block.state.IBlockState")
val BlockStateBase = ClassRef("net.minecraft.block.state.BlockStateBase")
val BlockPos = ClassRef("net.minecraft.util.math.BlockPos")
val MutableBlockPos = ClassRef("net.minecraft.util.math.BlockPos\$MutableBlockPos")
val BlockRenderLayer = ClassRef("net.minecraft.util.BlockRenderLayer")
val EnumFacing = ClassRef("net.minecraft.util.EnumFacing")
val World = ClassRef("net.minecraft.world.World")
val WorldClient = ClassRef("net.minecraft.client.multiplayer.WorldClient")
val ChunkCache = ClassRef("net.minecraft.world.ChunkCache")
val showBarrierParticles = MethodRef(WorldClient, "showBarrierParticles", "func_184153_a", ClassRef.void, ClassRef.int, ClassRef.int, ClassRef.int, ClassRef.int, Random, ClassRef.boolean, MutableBlockPos)
val Block = ClassRef("net.minecraft.block.Block")
val StateImplementation = ClassRef("net.minecraft.block.state.BlockStateContainer\$StateImplementation")
val canRenderInLayer = MethodRef(Block, "canRenderInLayer", ClassRef.boolean, IBlockState, BlockRenderLayer)
val getAmbientOcclusionLightValue = MethodRef(StateImplementation, "getAmbientOcclusionLightValue", "func_185892_j", ClassRef.float)
val useNeighborBrightness = MethodRef(StateImplementation, "useNeighborBrightness", "func_185916_f", ClassRef.boolean)
val doesSideBlockRendering = MethodRef(StateImplementation, "doesSideBlockRendering", ClassRef.boolean, IBlockAccess, BlockPos, EnumFacing)
val isOpaqueCube = MethodRef(StateImplementation, "isOpaqueCube", "func_185914_p", ClassRef.boolean)
val randomDisplayTick = MethodRef(Block, "randomDisplayTick", "func_180655_c", ClassRef.void, IBlockState, World, BlockPos, Random)
val BlockModelRenderer = ClassRef("net.minecraft.client.renderer.BlockModelRenderer")
val AmbientOcclusionFace = ClassRef("net.minecraft.client.renderer.BlockModelRenderer\$AmbientOcclusionFace")
val ChunkCompileTaskGenerator = ClassRef("net.minecraft.client.renderer.chunk.ChunkCompileTaskGenerator")
val BufferBuilder = ClassRef("net.minecraft.client.renderer.BufferBuilder")
val AOF_constructor = MethodRef(AmbientOcclusionFace, "<init>", ClassRef.void, BlockModelRenderer)
val RenderChunk = ClassRef("net.minecraft.client.renderer.chunk.RenderChunk")
val rebuildChunk = MethodRef(RenderChunk, "rebuildChunk", "func_178581_b", ClassRef.void, ClassRef.float, ClassRef.float, ClassRef.float, ChunkCompileTaskGenerator)
val BlockRendererDispatcher = ClassRef("net.minecraft.client.renderer.BlockRendererDispatcher")
val renderBlock = MethodRef(BlockRendererDispatcher, "renderBlock", "func_175018_a", ClassRef.boolean, IBlockState, BlockPos, IBlockAccess, BufferBuilder)
val TextureAtlasSprite = ClassRef("net.minecraft.client.renderer.texture.TextureAtlasSprite")
val IRegistry = ClassRef("net.minecraft.util.registry.IRegistry")
val ModelLoader = ClassRef("net.minecraftforge.client.model.ModelLoader")
val stateModels = FieldRef(ModelLoader, "stateModels", Map)
val setupModelRegistry = MethodRef(ModelLoader, "setupModelRegistry", "func_177570_a", IRegistry)
val IModel = ClassRef("net.minecraftforge.client.model.IModel")
val ModelBlock = ClassRef("net.minecraft.client.renderer.block.model.ModelBlock")
val ResourceLocation = ClassRef("net.minecraft.util.ResourceLocation")
val ModelResourceLocation = ClassRef("net.minecraft.client.renderer.block.model.ModelResourceLocation")
val VanillaModelWrapper = ClassRef("net.minecraftforge.client.model.ModelLoader\$VanillaModelWrapper")
val model_VMW = FieldRef(VanillaModelWrapper, "model", ModelBlock)
val location_VMW = FieldRef(VanillaModelWrapper, "location", ModelBlock)
val WeightedRandomModel = ClassRef("net.minecraftforge.client.model.ModelLoader\$WeightedRandomModel")
val models_WRM = FieldRef(WeightedRandomModel, "models", List)
val MultiModel = ClassRef("net.minecraftforge.client.model.MultiModel")
val base_MM = FieldRef(MultiModel, "base", IModel)
val MultipartModel = ClassRef("net.minecraftforge.client.model.ModelLoader\$MultipartModel")
val partModels_MPM = FieldRef(MultipartModel, "partModels", List)
val BakedQuad = ClassRef("net.minecraft.client.renderer.block.model.BakedQuad")
val resetChangedState = MethodRef(ClassRef("net.minecraftforge.common.config.Configuration"), "resetChangedState", ClassRef.void)
// Better Foliage
val BetterFoliageHooks = ClassRef("mods.betterfoliage.client.Hooks")
val getAmbientOcclusionLightValueOverride = MethodRef(BetterFoliageHooks, "getAmbientOcclusionLightValueOverride", ClassRef.float, ClassRef.float, IBlockState)
val useNeighborBrightnessOverride = MethodRef(BetterFoliageHooks, "getUseNeighborBrightnessOverride", ClassRef.boolean, ClassRef.boolean, IBlockState)
val doesSideBlockRenderingOverride = MethodRef(BetterFoliageHooks, "doesSideBlockRenderingOverride", ClassRef.boolean, ClassRef.boolean, IBlockAccess, BlockPos, EnumFacing)
val isOpaqueCubeOverride = MethodRef(BetterFoliageHooks, "isOpaqueCubeOverride", ClassRef.boolean, ClassRef.boolean, IBlockState)
val onRandomDisplayTick = MethodRef(BetterFoliageHooks, "onRandomDisplayTick", ClassRef.void, World, IBlockState, BlockPos)
val onAfterLoadModelDefinitions = MethodRef(BetterFoliageHooks, "onAfterLoadModelDefinitions", ClassRef.void, ModelLoader)
val onAfterBakeModels = MethodRef(BetterFoliageHooks, "onAfterBakeModels", ClassRef.void, Map)
val renderWorldBlock = MethodRef(BetterFoliageHooks, "renderWorldBlock", ClassRef.boolean, BlockRendererDispatcher, IBlockState, BlockPos, IBlockAccess, BufferBuilder, BlockRenderLayer)
val canRenderBlockInLayer = MethodRef(BetterFoliageHooks, "canRenderBlockInLayer", ClassRef.boolean, Block, IBlockState, BlockRenderLayer)
// Optifine
val OptifineClassTransformer = ClassRef("optifine.OptiFineClassTransformer")
val OptifineChunkCache = ClassRef("net.optifine.override.ChunkCacheOF")
val CCOFChunkCache = FieldRef(OptifineChunkCache, "chunkCache", ChunkCache)
val getBlockId = MethodRef(BlockStateBase, "getBlockId", ClassRef.int);
val getMetadata = MethodRef(BlockStateBase, "getMetadata", ClassRef.int);
// Optifine
val RenderEnv = ClassRef("net.optifine.render.RenderEnv")
val RenderEnv_reset = MethodRef(RenderEnv, "reset", ClassRef.void, IBlockState, BlockPos)
val quadSprite = FieldRef(BufferBuilder, "quadSprite", TextureAtlasSprite)
// Optifine: custom colors
val CustomColors = ClassRef("net.optifine.CustomColors")
val getColorMultiplier = MethodRef(CustomColors, "getColorMultiplier", ClassRef.int, BakedQuad, IBlockState, IBlockAccess, BlockPos, RenderEnv)
// Optifine: shaders
val SVertexBuilder = ClassRef("net.optifine.shaders.SVertexBuilder")
val sVertexBuilder = FieldRef(BufferBuilder, "sVertexBuilder", SVertexBuilder)
val pushEntity_state = MethodRef(SVertexBuilder, "pushEntity", ClassRef.void, IBlockState, BlockPos, IBlockAccess, BufferBuilder)
val pushEntity_num = MethodRef(SVertexBuilder, "pushEntity", ClassRef.void, ClassRef.long)
val popEntity = MethodRef(SVertexBuilder, "popEntity", ClassRef.void)
val ShadersModIntegration = ClassRef("mods.betterfoliage.client.integration.ShadersModIntegration")
val getBlockIdOverride = MethodRef(ShadersModIntegration, "getBlockIdOverride", ClassRef.long, ClassRef.long, IBlockState)
}

View File

@@ -1,69 +0,0 @@
package mods.octarinecore
import mods.octarinecore.metaprog.ClassRef
import mods.octarinecore.metaprog.ClassRef.Companion.void
import mods.octarinecore.metaprog.FieldRef
import mods.octarinecore.metaprog.MethodRef
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.client.renderer.chunk.ChunkRenderCache
import net.minecraft.client.renderer.model.BakedQuad
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockReader
import net.minecraft.world.IEnviromentBlockReader
import java.util.*
// Java
val String = ClassRef<String>("java.lang.String")
val Map = ClassRef<Map<*, *>>("java.util.Map")
val List = ClassRef<List<*>>("java.util.List")
val Random = ClassRef<Random>("java.util.Random")
// Minecraft
val IBlockReader = ClassRef<IBlockReader>("net.minecraft.world.IBlockReader")
val IEnvironmentBlockReader = ClassRef<IEnviromentBlockReader>("net.minecraft.world.IEnviromentBlockReader")
val BlockState = ClassRef<BlockState>("net.minecraft.block.BlockState")
val BlockPos = ClassRef<BlockPos>("net.minecraft.util.math.BlockPos")
val BlockRenderLayer = ClassRef<BlockRenderLayer>("net.minecraft.util.BlockRenderLayer")
val Block = ClassRef<Block>("net.minecraft.block.Block")
val TextureAtlasSprite = ClassRef<TextureAtlasSprite>("net.minecraft.client.renderer.texture.TextureAtlasSprite")
val BufferBuilder = ClassRef<BufferBuilder>("net.minecraft.client.renderer.BufferBuilder")
val BufferBuilder_setSprite = MethodRef(BufferBuilder, "setSprite", void, TextureAtlasSprite)
val BufferBuilder_sVertexBuilder = FieldRef(BufferBuilder, "sVertexBuilder", SVertexBuilder)
val BlockRendererDispatcher = ClassRef<BlockRendererDispatcher>("net.minecraft.client.renderer.BlockRendererDispatcher")
val ChunkRenderCache = ClassRef<ChunkRenderCache>("net.minecraft.client.renderer.chunk.ChunkRenderCache")
val ResourceLocation = ClassRef<ResourceLocation>("net.minecraft.util.ResourceLocation")
val BakedQuad = ClassRef<BakedQuad>("net.minecraft.client.renderer.model.BakedQuad")
// Optifine
val OptifineClassTransformer = ClassRef<Any>("optifine.OptiFineClassTransformer")
val BlockPosM = ClassRef<Any>("net.optifine.BlockPosM")
object ChunkCacheOF : ClassRef<Any>("net.optifine.override.ChunkCacheOF") {
val chunkCache = FieldRef(this, "chunkCache", ChunkRenderCache)
}
object RenderEnv : ClassRef<Any>("net.optifine.render.RenderEnv") {
val reset = MethodRef(this, "reset", void, BlockState, BlockPos)
}
// Optifine custom colors
val IColorizer = ClassRef<Any>("net.optifine.CustomColors\$IColorizer")
object CustomColors : ClassRef<Any>("net.optifine.CustomColors") {
val getColorMultiplier = MethodRef(this, "getColorMultiplier", int, BakedQuad, BlockState, IEnvironmentBlockReader, BlockPos, RenderEnv)
}
// Optifine shaders
object SVertexBuilder : ClassRef<Any>("net.optifine.shaders.SVertexBuilder") {
val pushState = MethodRef(this, "pushEntity", void, BlockState, BlockPos, IEnvironmentBlockReader, BufferBuilder)
val pushNum = MethodRef(this, "pushEntity", void, long)
val pop = MethodRef(this, "popEntity", void)
}

View File

@@ -2,24 +2,23 @@
@file:Suppress("NOTHING_TO_INLINE") @file:Suppress("NOTHING_TO_INLINE")
package mods.octarinecore package mods.octarinecore
import mods.betterfoliage.loader.Refs
import net.minecraft.tileentity.TileEntity import net.minecraft.tileentity.TileEntity
import net.minecraft.util.ResourceLocation import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.world.* import net.minecraft.world.ChunkCache
import org.apache.logging.log4j.Level import net.minecraft.world.IBlockAccess
import org.apache.logging.log4j.Logger import net.minecraft.world.World
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import java.lang.Math.* import java.lang.Math.*
const val PI2 = 2.0 * PI const val PI2 = 2.0 * PI
/** Strip the given prefix off the start of the string, if present */ /** Strip the given prefix off the start of the string, if present */
inline fun String.stripStart(str: String, ignoreCase: Boolean = true) = if (startsWith(str, ignoreCase)) substring(str.length) else this inline fun String.stripStart(str: String) = if (startsWith(str)) substring(str.length) else this
inline fun String.stripEnd(str: String, ignoreCase: Boolean = true) = if (endsWith(str, ignoreCase)) substring(0, length - str.length) else this
/** Strip the given prefix off the start of the resource path, if present */ /** Strip the given prefix off the start of the resource path, if present */
inline fun ResourceLocation.stripStart(str: String) = ResourceLocation(namespace, path.stripStart(str)) inline fun ResourceLocation.stripStart(str: String) = ResourceLocation(namespace, path.stripStart(str))
inline fun ResourceLocation.stripEnd(str: String) = ResourceLocation(namespace, path.stripEnd(str))
/** Mutating version of _map_. Replace each element of the list with the result of the given transformation. */ /** Mutating version of _map_. Replace each element of the list with the result of the given transformation. */
inline fun <reified T> MutableList<T>.replace(transform: (T) -> T) = forEachIndexed { idx, value -> this[idx] = transform(value) } inline fun <reified T> MutableList<T>.replace(transform: (T) -> T) = forEachIndexed { idx, value -> this[idx] = transform(value) }
@@ -105,25 +104,17 @@ fun nextPowerOf2(x: Int): Int {
* Check if the Chunk containing the given [BlockPos] is loaded. * Check if the Chunk containing the given [BlockPos] is loaded.
* Works for both [World] and [ChunkCache] (vanilla and OptiFine) instances. * Works for both [World] and [ChunkCache] (vanilla and OptiFine) instances.
*/ */
//fun IWorldReader.isBlockLoaded(pos: BlockPos) = when { fun IBlockAccess.isBlockLoaded(pos: BlockPos) = when {
// this is World -> isBlockLoaded(pos, false) this is World -> isBlockLoaded(pos, false)
// this is RenderChunkCache -> isworld.isBlockLoaded(pos, false) this is ChunkCache -> world.isBlockLoaded(pos, false)
// Refs.OptifineChunkCache.isInstance(this) -> (Refs.CCOFChunkCache.get(this) as ChunkCache).world.isBlockLoaded(pos, false) Refs.OptifineChunkCache.isInstance(this) -> (Refs.CCOFChunkCache.get(this) as ChunkCache).world.isBlockLoaded(pos, false)
// else -> false else -> false
//} }
/** /**
* Get the [TileEntity] at the given position, suppressing exceptions. * Get the [TileEntity] at the given position, suppressing exceptions.
* Also returns null if the chunk is unloaded, which can happen because of multithreaded rendering. * Also returns null if the chunk is unloaded, which can happen because of multithreaded rendering.
*/ */
fun IWorldReader.getTileEntitySafe(pos: BlockPos): TileEntity? = tryDefault(null as TileEntity?) { fun IBlockAccess.getTileEntitySafe(pos: BlockPos): TileEntity? = tryDefault(null as TileEntity?) {
if (isBlockLoaded(pos)) getTileEntity(pos) else null if (isBlockLoaded(pos)) getTileEntity(pos) else null
} }
interface HasLogger {
val logger: Logger
val logName: String get() = this::class.simpleName!!
fun log(msg: String) = log(Level.DEBUG, msg)
fun log(level: Level, msg: String) = logger.log(level, "[$logName] $msg")
}

View File

@@ -1,10 +1,10 @@
package mods.octarinecore.client package mods.octarinecore.client
import net.minecraft.client.settings.KeyBinding import net.minecraft.client.settings.KeyBinding
import net.minecraftforge.client.event.InputEvent
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.eventbus.api.SubscribeEvent
import net.minecraftforge.fml.client.registry.ClientRegistry import net.minecraftforge.fml.client.registry.ClientRegistry
import net.minecraftforge.fml.common.FMLCommonHandler
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.common.gameevent.InputEvent
class KeyHandler(val modId: String, val defaultKey: Int, val lang: String, val action: (InputEvent.KeyInputEvent)->Unit) { class KeyHandler(val modId: String, val defaultKey: Int, val lang: String, val action: (InputEvent.KeyInputEvent)->Unit) {
@@ -12,7 +12,7 @@ class KeyHandler(val modId: String, val defaultKey: Int, val lang: String, val a
init { init {
ClientRegistry.registerKeyBinding(keyBinding) ClientRegistry.registerKeyBinding(keyBinding)
MinecraftForge.EVENT_BUS.register(this) FMLCommonHandler.instance().bus().register(this)
} }
@SubscribeEvent @SubscribeEvent

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
@file:JvmName("Utils") @file:JvmName("Utils")
package mods.octarinecore.client.gui package mods.octarinecore.client.gui
import net.minecraft.util.text.StringTextComponent
import net.minecraft.util.text.Style import net.minecraft.util.text.Style
import net.minecraft.util.text.TextComponentString
import net.minecraft.util.text.TextFormatting import net.minecraft.util.text.TextFormatting
import net.minecraft.util.text.TextFormatting.AQUA import net.minecraft.util.text.TextFormatting.AQUA
import net.minecraft.util.text.TextFormatting.GRAY import net.minecraft.util.text.TextFormatting.GRAY
@@ -16,7 +16,7 @@ fun stripTooltipDefaultText(tooltip: MutableList<String>) {
} }
} }
fun textComponent(msg: String, color: TextFormatting = GRAY): StringTextComponent { fun textComponent(msg: String, color: TextFormatting = GRAY): TextComponentString {
val style = Style().apply { this.color = color } val style = Style().apply { this.color = color }
return StringTextComponent(msg).apply { this.style = style } return TextComponentString(msg).apply { this.style = style }
} }

View File

@@ -0,0 +1,122 @@
@file:JvmName("RendererHolder")
package mods.octarinecore.client.render
import mods.betterfoliage.client.render.canRenderInCutout
import mods.betterfoliage.client.render.canRenderInLayer
import mods.betterfoliage.client.render.isCutout
import mods.octarinecore.ThreadLocalDelegate
import mods.octarinecore.client.resource.ResourceHandler
import mods.octarinecore.common.Double3
import mods.octarinecore.common.Int3
import mods.octarinecore.common.forgeDirOffsets
import mods.octarinecore.common.plus
import mods.octarinecore.semiRandom
import net.minecraft.block.Block
import net.minecraft.block.state.IBlockState
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.client.renderer.color.BlockColors
import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.MathHelper
import net.minecraft.world.IBlockAccess
import net.minecraft.world.biome.Biome
import kotlin.math.abs
/**
* [ThreadLocal] instance of [BlockContext] representing the block being rendered.
*/
val blockContext by ThreadLocalDelegate { BlockContext() }
/**
* [ThreadLocal] instance of [ModelRenderer].
*/
val modelRenderer by ThreadLocalDelegate { ModelRenderer() }
val blockColors = ThreadLocal<BlockColors>()
abstract class AbstractBlockRenderingHandler(modId: String) : ResourceHandler(modId) {
open val addToCutout: Boolean get() = true
// ============================
// Custom rendering
// ============================
abstract fun isEligible(ctx: BlockContext): Boolean
abstract fun render(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer): Boolean
// ============================
// Vanilla rendering wrapper
// ============================
/**
* Render the block in the current [BlockContext]
*/
fun renderWorldBlockBase(ctx: BlockContext, dispatcher: BlockRendererDispatcher, renderer: BufferBuilder, layer: BlockRenderLayer?): Boolean {
ctx.blockState(Int3.zero).let { state ->
if (layer == null ||
state.canRenderInLayer(layer) ||
(state.canRenderInCutout() && layer.isCutout)) {
return dispatcher.renderBlock(state, ctx.pos, ctx.world, renderer)
}
}
return false
}
}
data class BlockData(val state: IBlockState, val color: Int, val brightness: Int)
/**
* 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 pos: BlockPos = BlockPos.ORIGIN
) {
fun set(world: IBlockAccess, pos: BlockPos) { this.world = world; this.pos = pos; }
val block: Block get() = block(Int3.zero)
fun block(offset: Int3) = blockState(offset).block
fun blockState(offset: Int3) = (pos + offset).let { world!!.getBlockState(it) }
fun blockData(offset: Int3) = (pos + offset).let { pos ->
world!!.getBlockState(pos).let { state ->
BlockData(
state,
Minecraft.getMinecraft().blockColors.colorMultiplier(state, world!!, pos, 0),
state.block.getPackedLightmapCoords(state, world!!, pos)
)
}
}
/** Get the biome ID at the block position. */
val biomeId: Int get() = Biome.getIdForBiome(world!!.getBiome(pos))
/** Get the centerpoint of the block being rendered. */
val blockCenter: Double3 get() = Double3(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
val chunkBase: Double3 get() {
val cX = if (pos.x >= 0) pos.x / 16 else (pos.x + 1) / 16 - 1
val cY = pos.y / 16
val cZ = if (pos.z >= 0) pos.z / 16 else (pos.z + 1) / 16 - 1
return Double3(cX * 16.0, cY * 16.0, cZ * 16.0)
}
/** Is the block surrounded by other blocks that satisfy the predicate on all sides? */
fun isSurroundedBy(predicate: (IBlockState)->Boolean) = forgeDirOffsets.all { predicate(blockState(it)) }
/** Get a semi-random value based on the block coordinate and the given seed. */
fun random(seed: Int) = semiRandom(pos.x, pos.y, pos.z, seed)
/** Get an array of semi-random values based on the block coordinate. */
fun semiRandomArray(num: Int): Array<Int> = Array(num) { random(it) }
/** Get the distance of the block from the camera (player). */
val cameraDistance: Int get() {
val camera = Minecraft.getMinecraft().renderViewEntity ?: return 0
return abs(pos.x - MathHelper.floor(camera.posX)) +
abs(pos.y - MathHelper.floor(camera.posY)) +
abs(pos.z - MathHelper.floor(camera.posZ))
}
}

View File

@@ -3,16 +3,13 @@ package mods.octarinecore.client.render
import mods.octarinecore.PI2 import mods.octarinecore.PI2
import mods.octarinecore.common.Double3 import mods.octarinecore.common.Double3
import net.minecraft.client.Minecraft import net.minecraft.client.Minecraft
import net.minecraft.client.particle.IParticleRenderType
import net.minecraft.client.particle.Particle import net.minecraft.client.particle.Particle
import net.minecraft.client.particle.SpriteTexturedParticle
import net.minecraft.client.renderer.ActiveRenderInfo
import net.minecraft.client.renderer.BufferBuilder import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.client.renderer.texture.TextureAtlasSprite import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.entity.Entity import net.minecraft.entity.Entity
import net.minecraft.world.World import net.minecraft.world.World
abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) : SpriteTexturedParticle(world, x, y, z) { abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) : Particle(world, x, y, z) {
companion object { companion object {
@JvmStatic val sin = Array(64) { idx -> Math.sin(PI2 / 64.0 * idx) } @JvmStatic val sin = Array(64) { idx -> Math.sin(PI2 / 64.0 * idx) }
@@ -24,8 +21,8 @@ abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) :
val prevPos = Double3.zero val prevPos = Double3.zero
val velocity = Double3.zero val velocity = Double3.zero
override fun tick() { override fun onUpdate() {
super.tick() super.onUpdate()
currentPos.setTo(posX, posY, posZ) currentPos.setTo(posX, posY, posZ)
prevPos.setTo(prevPosX, prevPosY, prevPosZ) prevPos.setTo(prevPosX, prevPosY, prevPosZ)
velocity.setTo(motionX, motionY, motionZ) velocity.setTo(motionX, motionY, motionZ)
@@ -44,12 +41,12 @@ abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) :
abstract val isValid: Boolean abstract val isValid: Boolean
/** Add the particle to the effect renderer if it is valid. */ /** Add the particle to the effect renderer if it is valid. */
fun addIfValid() { if (isValid) Minecraft.getInstance().particles.addEffect(this) } fun addIfValid() { if (isValid) Minecraft.getMinecraft().effectRenderer.addEffect(this) }
override fun renderParticle(buffer: BufferBuilder, entity: ActiveRenderInfo, partialTicks: Float, rotX: Float, rotZ: Float, rotYZ: Float, rotXY: Float, rotXZ: Float) { override fun renderParticle(worldRenderer: BufferBuilder, entity: Entity, partialTickTime: Float, rotX: Float, rotZ: Float, rotYZ: Float, rotXY: Float, rotXZ: Float) {
billboardRot.first.setTo(rotX + rotXY, rotZ, rotYZ + rotXZ) billboardRot.first.setTo(rotX + rotXY, rotZ, rotYZ + rotXZ)
billboardRot.second.setTo(rotX - rotXY, -rotZ, rotYZ - rotXZ) billboardRot.second.setTo(rotX - rotXY, -rotZ, rotYZ - rotXZ)
render(buffer, partialTicks) render(worldRenderer, partialTickTime)
} }
/** /**
* Render a particle quad. * Render a particle quad.
@@ -70,7 +67,7 @@ abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) :
prevPos: Double3 = this.prevPos, prevPos: Double3 = this.prevPos,
size: Double = particleScale.toDouble(), size: Double = particleScale.toDouble(),
rotation: Int = 0, rotation: Int = 0,
icon: TextureAtlasSprite = sprite, icon: TextureAtlasSprite = particleTexture,
isMirrored: Boolean = false, isMirrored: Boolean = false,
alpha: Float = this.particleAlpha) { alpha: Float = this.particleAlpha) {
@@ -118,8 +115,7 @@ abstract class AbstractEntityFX(world: World, x: Double, y: Double, z: Double) :
.endVertex() .endVertex()
} }
// override fun getFXLayer() = 1 override fun getFXLayer() = 1
override fun getRenderType(): IParticleRenderType = IParticleRenderType.PARTICLE_SHEET_TRANSLUCENT
fun setColor(color: Int) { fun setColor(color: Int) {
particleBlue = (color and 255) / 256.0f particleBlue = (color and 255) / 256.0f

View File

@@ -1,71 +0,0 @@
package mods.octarinecore.client.render
import mods.octarinecore.common.*
import mods.octarinecore.semiRandom
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.Direction
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockReader
import net.minecraft.world.IEnviromentBlockReader
import net.minecraft.world.World
import net.minecraft.world.biome.Biome
import java.util.*
/**
* Represents the block being rendered. Has properties and methods to query the neighborhood of the block in
* block-relative coordinates.
*/
interface BlockCtx {
val world: IEnviromentBlockReader
val pos: BlockPos
fun offset(dir: Direction) = offset(dir.offset)
fun offset(offset: Int3): BlockCtx
val state: BlockState get() = world.getBlockState(pos)
fun state(dir: Direction) = world.getBlockState(pos + dir.offset)
fun state(offset: Int3) = world.getBlockState(pos + offset)
val biome: Biome get() = world.getBiome(pos)
val isNormalCube: Boolean get() = state.isNormalCube(world, pos)
fun shouldSideBeRendered(side: Direction) = Block.shouldSideBeRendered(state, world, pos, side)
/** Get a semi-random value based on the block coordinate and the given seed. */
fun semiRandom(seed: Int) = semiRandom(pos.x, pos.y, pos.z, seed)
/** Get an array of semi-random values based on the block coordinate. */
fun semiRandomArray(num: Int): Array<Int> = Array(num) { semiRandom(it) }
}
open class BasicBlockCtx(
override val world: IEnviromentBlockReader,
override val pos: BlockPos
) : BlockCtx {
override var state: BlockState = world.getBlockState(pos)
protected set
override fun offset(offset: Int3) = BasicBlockCtx(world, pos + offset)
fun cache() = CachedBlockCtx(world, pos)
}
open class CachedBlockCtx(world: IEnviromentBlockReader, pos: BlockPos) : BasicBlockCtx(world, pos) {
var neighbors = Array<BlockState>(6) { world.getBlockState(pos + allDirections[it].offset) }
override var biome: Biome = world.getBiome(pos)
override fun state(dir: Direction) = neighbors[dir.ordinal]
}
data class RenderCtx(
val dispatcher: BlockRendererDispatcher,
val renderBuffer: BufferBuilder,
val layer: BlockRenderLayer,
val random: Random
) {
fun render(worldBlock: BlockCtx) = dispatcher.renderBlock(worldBlock.state, worldBlock.pos, worldBlock.world, renderBuffer, random, null)
}

View File

@@ -1,101 +0,0 @@
package mods.octarinecore.client.render
import mods.betterfoliage.client.render.canRenderInCutout
import mods.betterfoliage.client.render.isCutout
import mods.octarinecore.BufferBuilder
import mods.octarinecore.BufferBuilder_setSprite
import mods.octarinecore.client.render.lighting.*
import mods.octarinecore.common.Double3
import mods.octarinecore.common.Int3
import mods.octarinecore.common.Rotation
import mods.octarinecore.common.plus
import mods.octarinecore.metaprog.get
import mods.octarinecore.metaprog.set
import net.minecraft.block.Blocks
import net.minecraft.fluid.Fluids
import net.minecraft.util.Direction
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IEnviromentBlockReader
import net.minecraft.world.LightType
import net.minecraft.world.biome.Biomes
class CombinedContext(
val blockCtx: BlockCtx, val renderCtx: RenderCtx, val lightingCtx: DefaultLightingCtx
) : BlockCtx by blockCtx, LightingCtx by lightingCtx {
var hasRendered = false
fun render(force: Boolean = false) = renderCtx.let {
if (force || state.canRenderInLayer(it.layer) || (state.canRenderInCutout() && it.layer.isCutout)) {
it.render(blockCtx)
hasRendered = true
}
Unit
}
fun exchange(moddedOffset: Int3, targetOffset: Int3) = CombinedContext(
BasicBlockCtx(OffsetEnvBlockReader(blockCtx.world, pos + moddedOffset, pos + targetOffset), pos),
renderCtx,
lightingCtx
)
val isCutout = renderCtx.layer.isCutout
/** Get the centerpoint of the block being rendered. */
val blockCenter: Double3 get() = Double3(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
/** Holds final vertex data before it goes to the [Tessellator]. */
val temp = RenderVertex()
fun render(
model: Model,
rotation: Rotation = Rotation.identity,
translation: Double3 = blockCenter,
forceFlat: Boolean = false,
quadFilter: (Int, Quad) -> Boolean = { _, _ -> true },
icon: QuadIconResolver,
postProcess: PostProcessLambda = noPost
) {
lightingCtx.modelRotation = rotation
model.quads.forEachIndexed { quadIdx, quad ->
if (quadFilter(quadIdx, quad)) {
val drawIcon = icon(this, quadIdx, quad)
if (drawIcon != null) {
// let OptiFine know the texture we're using, so it can
// transform UV coordinates to quad-relative
BufferBuilder_setSprite.invoke(renderCtx.renderBuffer, drawIcon)
quad.verts.forEachIndexed { vertIdx, vert ->
temp.init(vert).rotate(lightingCtx.modelRotation).translate(translation)
val shader = if (lightingCtx.aoEnabled && !forceFlat) vert.aoShader else vert.flatShader
shader.shade(lightingCtx, temp)
temp.postProcess(this, quadIdx, quad, vertIdx, vert)
temp.setIcon(drawIcon)
renderCtx.renderBuffer
.pos(temp.x, temp.y, temp.z)
.color(temp.red, temp.green, temp.blue, 1.0f)
.tex(temp.u, temp.v)
.lightmap(temp.brightness shr 16 and 65535, temp.brightness and 65535)
.endVertex()
}
}
}
}
hasRendered = true
}
}
val allFaces: (Direction) -> Boolean = { true }
val topOnly: (Direction) -> Boolean = { it == Direction.UP }
/** Perform no post-processing */
val noPost: PostProcessLambda = { _, _, _, _, _ -> }
object NonNullWorld : IEnviromentBlockReader {
override fun getBlockState(pos: BlockPos) = Blocks.AIR.defaultState
override fun getLightFor(type: LightType, pos: BlockPos) = 0
override fun getFluidState(pos: BlockPos) = Fluids.EMPTY.defaultState
override fun getTileEntity(pos: BlockPos) = null
override fun getBiome(pos: BlockPos) = Biomes.THE_VOID
}

View File

@@ -1,10 +1,9 @@
package mods.octarinecore.client.render package mods.octarinecore.client.render
import mods.octarinecore.client.render.lighting.*
import mods.octarinecore.common.* import mods.octarinecore.common.*
import mods.octarinecore.minmax import mods.octarinecore.minmax
import mods.octarinecore.replace import mods.octarinecore.replace
import net.minecraft.util.Direction import net.minecraft.util.EnumFacing
import java.lang.Math.max import java.lang.Math.max
import java.lang.Math.min import java.lang.Math.min
@@ -41,13 +40,13 @@ data class UV(val u: Double, val v: Double) {
* *
* @param[xyz] x, y, z coordinates * @param[xyz] x, y, z coordinates
* @param[uv] u, v coordinates * @param[uv] u, v coordinates
* @param[aoShader] [ModelLighter] instance to use with AO rendering * @param[aoShader] [Shader] instance to use with AO rendering
* @param[flatShader] [ModelLighter] instance to use with non-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), data class Vertex(val xyz: Double3 = Double3(0.0, 0.0, 0.0),
val uv: UV = UV(0.0, 0.0), val uv: UV = UV(0.0, 0.0),
val aoShader: ModelLighter = NoLighting, val aoShader: Shader = NoShader,
val flatShader: ModelLighter = NoLighting) val flatShader: Shader = NoShader)
/** /**
* Model quad * Model quad
@@ -60,7 +59,7 @@ data class Quad(val v1: Vertex, val v2: Vertex, val v3: Vertex, val v4: Vertex)
val normal: Double3 get() = (v2.xyz - v1.xyz).cross(v4.xyz - v1.xyz).normalize 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: Double3) = transformV { it.copy(xyz = it.xyz + trans) }
fun move(trans: Pair<Double, Direction>) = move(Double3(trans.second) * trans.first) fun move(trans: Pair<Double, EnumFacing>) = move(Double3(trans.second) * trans.first)
fun scale (scale: Double) = transformV { it.copy(xyz = it.xyz * scale) } 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 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 scaleUV (scale: Double) = transformV { it.copy(uv = UV(it.uv.u * scale, it.uv.v * scale)) }
@@ -79,7 +78,7 @@ data class Quad(val v1: Vertex, val v2: Vertex, val v3: Vertex, val v4: Vertex)
transformVI { vertex, idx -> transformVI { vertex, idx ->
if (!predicate(vertex, idx)) vertex else vertex.copy(flatShader = factory(this@Quad, vertex)) if (!predicate(vertex, idx)) vertex else vertex.copy(flatShader = factory(this@Quad, vertex))
} }
fun setFlatShader(shader: ModelLighter) = transformVI { vertex, idx -> vertex.copy(flatShader = shader) } fun setFlatShader(shader: Shader) = transformVI { vertex, idx -> vertex.copy(flatShader = shader) }
val flipped: Quad get() = Quad(v4, v3, v2, v1) val flipped: Quad get() = Quad(v4, v3, v2, v1)
fun cycleVertices(n: Int) = when(n % 4) { fun cycleVertices(n: Int) = when(n % 4) {
@@ -124,10 +123,10 @@ class Model() {
) )
} }
fun faceQuad(face: Direction): Quad { fun faceQuad(face: EnumFacing): Quad {
val base = face.vec * 0.5 val base = face.vec * 0.5
val top = boxFaces[face].top * 0.5 val top = faceCorners[face.ordinal].topLeft.first.vec * 0.5
val left = boxFaces[face].left * 0.5 val left = faceCorners[face.ordinal].topLeft.second.vec * 0.5
return Quad( return Quad(
Vertex(base + top + left, UV.topLeft), Vertex(base + top + left, UV.topLeft),
Vertex(base - top + left, UV.bottomLeft), Vertex(base - top + left, UV.bottomLeft),
@@ -138,7 +137,7 @@ class Model() {
} }
val fullCube = Model().apply { val fullCube = Model().apply {
allDirections.forEach { forgeDirs.forEach {
faceQuad(it) faceQuad(it)
.setAoShader(faceOrientedAuto(corner = cornerAo(it.axis), edge = null)) .setAoShader(faceOrientedAuto(corner = cornerAo(it.axis), edge = null))
.setFlatShader(faceOrientedAuto(corner = cornerFlat, edge = null)) .setFlatShader(faceOrientedAuto(corner = cornerFlat, edge = null))

View File

@@ -0,0 +1,181 @@
package mods.octarinecore.client.render
import mods.betterfoliage.loader.Refs
import mods.octarinecore.common.*
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.EnumFacing
import net.minecraft.util.EnumFacing.*
typealias QuadIconResolver = (ShadingContext, Int, Quad) -> TextureAtlasSprite?
typealias PostProcessLambda = RenderVertex.(ShadingContext, Int, Quad, Int, Vertex) -> Unit
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]
*/
fun render(
worldRenderer: BufferBuilder,
model: Model,
rot: Rotation = Rotation.identity,
trans: Double3 = blockContext.blockCenter,
forceFlat: Boolean = false,
quadFilter: (Int, Quad) -> Boolean = { _, _ -> true },
icon: QuadIconResolver,
postProcess: PostProcessLambda
) {
rotation = rot
aoEnabled = Minecraft.isAmbientOcclusionEnabled()
// make sure we have space in the buffer for our quads plus one
worldRenderer.ensureSpaceForQuads(model.quads.size + 1)
model.quads.forEachIndexed { quadIdx, quad ->
if (quadFilter(quadIdx, quad)) {
val drawIcon = icon(this, quadIdx, quad)
if (drawIcon != null) {
// let OptiFine know the texture we're using, so it can
// transform UV coordinates to quad-relative
Refs.quadSprite.set(worldRenderer, drawIcon)
quad.verts.forEachIndexed { vertIdx, vert ->
temp.init(vert).rotate(rotation).translate(trans)
val shader = if (aoEnabled && !forceFlat) vert.aoShader else vert.flatShader
shader.shade(this, temp)
temp.postProcess(this, quadIdx, quad, vertIdx, vert)
temp.setIcon(drawIcon)
worldRenderer
.pos(temp.x, temp.y, temp.z)
.color(temp.red, temp.green, temp.blue, 1.0f)
.tex(temp.u, temp.v)
.lightmap(temp.brightness shr 16 and 65535, temp.brightness and 65535)
.endVertex()
}
}
}
}
}
}
/**
* 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()
val aoFaces = Array(6) { AoFaceData(forgeDirs[it]) }
val EnumFacing.aoMultiplier: Float get() = when(this) {
UP -> 1.0f
DOWN -> 0.5f
NORTH, SOUTH -> 0.8f
EAST, WEST -> 0.6f
}
fun updateShading(offset: Int3, predicate: (EnumFacing) -> Boolean = { true }) {
forgeDirs.forEach { if (predicate(it)) aoFaces[it.ordinal].update(offset, multiplier = it.aoMultiplier) }
}
fun aoShading(face: EnumFacing, corner1: EnumFacing, corner2: EnumFacing) =
aoFaces[face.rotate(rotation).ordinal][corner1.rotate(rotation), corner2.rotate(rotation)]
fun blockData(offset: Int3) = blockContext.blockData(offset.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
val rawData = IntArray(7)
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 mirrorUV(mirrorU: Boolean, mirrorV: Boolean) {
if (mirrorU) u = -u
if (mirrorV) v = -v
}
inline fun setIcon(icon: TextureAtlasSprite): 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
}
}
fun BufferBuilder.ensureSpaceForQuads(num: Int) {
rawIntBuffer.position(bufferSize)
growBuffer(num * vertexFormat.size)
}
val allFaces: (EnumFacing) -> Boolean = { true }
val topOnly: (EnumFacing) -> Boolean = { it == UP }
/** Perform no post-processing */
val noPost: PostProcessLambda = { _, _, _, _, _ -> }

View File

@@ -0,0 +1,44 @@
package mods.octarinecore.client.render
import mods.octarinecore.common.Int3
import mods.octarinecore.common.plus
import net.minecraft.util.EnumFacing
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockAccess
/**
* 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
*/
@Suppress("NOTHING_TO_INLINE")
class OffsetBlockAccess(val original: IBlockAccess, val modded: BlockPos, val target: BlockPos) : IBlockAccess {
inline fun actualPos(pos: BlockPos?) =
if (pos != null && pos.x == modded.x && pos.y == modded.y && pos.z == modded.z) target else pos
override fun getBiome(pos: BlockPos?) = original.getBiome(actualPos(pos))
override fun getBlockState(pos: BlockPos?) = original.getBlockState(actualPos(pos))
override fun getCombinedLight(pos: BlockPos?, lightValue: Int) = original.getCombinedLight(actualPos(pos), lightValue)
override fun getStrongPower(pos: BlockPos?, direction: EnumFacing?) = original.getStrongPower(actualPos(pos), direction)
override fun getTileEntity(pos: BlockPos?) = original.getTileEntity(actualPos(pos))
override fun getWorldType() = original.worldType
override fun isAirBlock(pos: BlockPos?) = original.isAirBlock(actualPos(pos))
override fun isSideSolid(pos: BlockPos?, side: EnumFacing?, _default: Boolean) = original.isSideSolid(actualPos(pos), side, _default)
}
/**
* Temporarily replaces the [IBlockAccess] used by this [BlockContext] and the corresponding [ExtendedRenderBlocks]
* to use an [OffsetBlockAccess] while executing this lambda.
*
* @param[modded] the _modified_ location
* @param[target] the _target_ location
* @param[func] the lambda to execute
*/
inline fun <reified T> BlockContext.withOffset(modded: Int3, target: Int3, func: () -> T): T {
val original = world!!
world = OffsetBlockAccess(original, pos + modded, pos + target)
val result = func()
world = original
return result
}

View File

@@ -1,52 +0,0 @@
package mods.octarinecore.client.render
import mods.octarinecore.common.Int3
import mods.octarinecore.common.plus
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockReader
import net.minecraft.world.IEnviromentBlockReader
import net.minecraft.world.LightType
/**
* 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
*/
@Suppress("NOTHING_TO_INLINE", "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "HasPlatformType")
open class OffsetBlockReader(open val original: IBlockReader, val modded: BlockPos, val target: BlockPos) : IBlockReader {
inline fun actualPos(pos: BlockPos) = if (pos != null && pos.x == modded.x && pos.y == modded.y && pos.z == modded.z) target else pos
override fun getBlockState(pos: BlockPos) = original.getBlockState(actualPos(pos))
override fun getTileEntity(pos: BlockPos) = original.getTileEntity(actualPos(pos))
override fun getFluidState(pos: BlockPos) = original.getFluidState(actualPos(pos))
}
@Suppress("NOTHING_TO_INLINE", "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", "HasPlatformType")
class OffsetEnvBlockReader(val original: IEnviromentBlockReader, val modded: BlockPos, val target: BlockPos) : IEnviromentBlockReader by original {
inline fun actualPos(pos: BlockPos) = if (pos != null && pos.x == modded.x && pos.y == modded.y && pos.z == modded.z) target else pos
override fun getBlockState(pos: BlockPos) = original.getBlockState(actualPos(pos))
override fun getTileEntity(pos: BlockPos) = original.getTileEntity(actualPos(pos))
override fun getFluidState(pos: BlockPos) = original.getFluidState(actualPos(pos))
override fun getLightFor(type: LightType, pos: BlockPos) = original.getLightFor(type, actualPos(pos))
override fun getCombinedLight(pos: BlockPos, light: Int) = original.getCombinedLight(actualPos(pos), light)
override fun getBiome(pos: BlockPos) = original.getBiome(actualPos(pos))
}
/**
* Temporarily replaces the [IBlockReader] used by this [BlockContext] and the corresponding [ExtendedRenderBlocks]
* to use an [OffsetEnvBlockReader] while executing this lambda.
*
* @param[modded] the _modified_ location
* @param[target] the _target_ location
* @param[func] the lambda to execute
*/
//inline fun <reified T> BlockContext.withOffset(modded: Int3, target: Int3, func: () -> T): T {
// val original = reader!!
// reader = OffsetEnvBlockReader(original, pos + modded, pos + target)
// val result = func()
// reader = original
// return result
//}

View File

@@ -0,0 +1,69 @@
@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
}
fun brWeighted(br1: Int, weight1: Float, br2: Int, weight2: Float): Int {
val w1int = (weight1 * 256.0f + 0.5f).toInt()
val w2int = (weight2 * 256.0f + 0.5f).toInt()
var result = 0
brightnessComponents.forEachIndexed { idx, shift ->
val comp1 = (br1 shr shift) and 15
val comp2 = (br2 shr shift) and 15
val compWeighted = (comp1 * w1int + comp2 * w2int) / 256
result = result or ((compWeighted and 15) shl shift)
}
return result
}
data class HSB(var hue: Float, var saturation: Float, var brightness: Float) {
companion object {
fun fromColor(color: Int): HSB {
val hsbVals = Color.RGBtoHSB((color shr 16) and 255, (color shr 8) and 255, color and 255, null)
return HSB(hsbVals[0], hsbVals[1], hsbVals[2])
}
}
val asColor: Int get() = Color.HSBtoRGB(hue, saturation, brightness)
}

View File

@@ -1,45 +0,0 @@
@file:JvmName("RendererHolder")
package mods.octarinecore.client.render
import mods.betterfoliage.client.render.canRenderInCutout
import mods.betterfoliage.client.render.isCutout
import mods.octarinecore.ThreadLocalDelegate
import mods.octarinecore.client.resource.ResourceHandler
import mods.octarinecore.common.Double3
import mods.octarinecore.common.Int3
import mods.octarinecore.common.allDirOffsets
import mods.octarinecore.common.plus
import mods.octarinecore.semiRandom
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.BlockRendererDispatcher
import net.minecraft.client.renderer.BufferBuilder
import net.minecraft.client.renderer.color.BlockColors
import net.minecraft.util.BlockRenderLayer
import net.minecraft.util.Direction
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.MathHelper
import net.minecraft.world.IEnviromentBlockReader
import net.minecraft.world.biome.Biome
import net.minecraftforge.client.model.data.IModelData
import net.minecraftforge.eventbus.api.IEventBus
import java.util.*
import kotlin.math.abs
abstract class RenderDecorator(modId: String, modBus: IEventBus) : ResourceHandler(modId, modBus) {
open val renderOnCutout: Boolean get() = true
open val onlyOnCutout: Boolean get() = false
// ============================
// Custom rendering
// ============================
abstract fun isEligible(ctx: CombinedContext): Boolean
abstract fun render(ctx: CombinedContext)
}
data class BlockData(val state: BlockState, val color: Int, val brightness: Int)

View File

@@ -0,0 +1,155 @@
package mods.octarinecore.client.render
import mods.octarinecore.common.*
import net.minecraft.util.EnumFacing
const val defaultCornerDimming = 0.5f
const val defaultEdgeDimming = 0.8f
// ================================
// Shader instantiation lambdas
// ================================
fun cornerAo(fallbackAxis: EnumFacing.Axis): CornerShaderFactory = { face, dir1, dir2 ->
val fallbackDir = listOf(face, dir1, dir2).find { it.axis == fallbackAxis }!!
CornerSingleFallback(face, dir1, dir2, fallbackDir)
}
val cornerFlat = { face: EnumFacing, dir1: EnumFacing, dir2: EnumFacing -> FaceFlat(face) }
fun cornerAoTri(func: (AoData, AoData)-> AoData) = { face: EnumFacing, dir1: EnumFacing, dir2: EnumFacing ->
CornerTri(face, dir1, dir2, func)
}
val cornerAoMaxGreen = cornerAoTri { s1, s2 -> if (s1.green > s2.green) s1 else s2 }
fun cornerInterpolate(edgeAxis: EnumFacing.Axis, weight: Float, dimming: Float): CornerShaderFactory = { 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: EnumFacing, val dir1: EnumFacing, val dir2: EnumFacing, val fallbackDir: EnumFacing, 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 context.blockData(offset).let {
vertex.shade(it.brightness brMul fallbackDimming, it.color 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: EnumFacing, val dir1: EnumFacing, val dir2: EnumFacing,
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: EnumFacing, val edgeDir: EnumFacing, 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 EnumFacing.AxisDirection.POSITIVE).face)
val shadingN = context.aoShading(face, edgeDir, (edgeAxis to EnumFacing.AxisDirection.NEGATIVE).face)
if (!shadingP.valid && !shadingN.valid) context.blockData(offset).let {
return vertex.shade(it.brightness brMul fallbackDimming, it.color 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: EnumFacing, val face2: EnumFacing, val edgeDir: EnumFacing,
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) context.blockData(offset).let {
return vertex.shade(it.brightness brMul fallbackDimming, it.color 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: EnumFacing): 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: EnumFacing): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
val color = context.blockData(Int3.zero).color
vertex.shade(context.blockData(face.offset).brightness, color)
}
override fun rotate(rot: Rotation): Shader = FaceFlat(face.rotate(rot))
}
class FlatOffset(val offset: Int3): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
context.blockData(offset).let {
vertex.brightness = it.brightness
vertex.setColor(it.color)
}
}
override fun rotate(rot: Rotation): Shader = this
}
class FlatOffsetNoColor(val offset: Int3): Shader {
override fun shade(context: ShadingContext, vertex: RenderVertex) {
vertex.brightness = context.blockData(offset).brightness
vertex.red = 1.0f; vertex.green = 1.0f; vertex.blue = 1.0f
}
override fun rotate(rot: Rotation): Shader = this
}

View File

@@ -0,0 +1,192 @@
package mods.octarinecore.client.render
import mods.betterfoliage.loader.Refs
import mods.octarinecore.common.*
import mods.octarinecore.metaprog.allAvailable
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.BlockModelRenderer
import net.minecraft.util.EnumFacing
import net.minecraft.util.EnumFacing.*
import java.lang.Math.min
import java.util.*
typealias EdgeShaderFactory = (EnumFacing, EnumFacing) -> Shader
typealias CornerShaderFactory = (EnumFacing, EnumFacing, EnumFacing) -> Shader
typealias ShaderFactory = (Quad, Vertex) -> Shader
/** 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
}
fun set(brightness: Int, colorMultiplier: Float) {
this.valid = true
this.brightness = brightness
this.red = colorMultiplier
this.green = colorMultiplier
this.blue = colorMultiplier
}
companion object {
val black = AoData()
}
}
class AoFaceData(val face: EnumFacing) {
val ao = Refs.AmbientOcclusionFace.element!!.getDeclaredConstructor(Refs.BlockModelRenderer.element!!)
.newInstance(BlockModelRenderer(Minecraft.getMinecraft().blockColors))
as BlockModelRenderer.AmbientOcclusionFace
val top = faceCorners[face.ordinal].topLeft.first
val left = faceCorners[face.ordinal].topLeft.second
val topLeft = AoData()
val topRight = AoData()
val bottomLeft = AoData()
val bottomRight = AoData()
val ordered = when(face) {
DOWN -> listOf(topLeft, bottomLeft, bottomRight, topRight)
UP -> listOf(bottomRight, topRight, topLeft, bottomLeft)
NORTH -> listOf(bottomLeft, bottomRight, topRight, topLeft)
SOUTH -> listOf(topLeft, bottomLeft, bottomRight, topRight)
WEST -> listOf(bottomLeft, bottomRight, topRight, topLeft)
EAST -> listOf(topRight, topLeft, bottomLeft, bottomRight)
}
fun update(offset: Int3, useBounds: Boolean = false, multiplier: Float = 1.0f) {
val ctx = blockContext
val blockState = ctx.blockState(offset)
val quadBounds: FloatArray = FloatArray(12)
val flags = BitSet(3).apply { set(0) }
ao.updateVertexBrightness(ctx.world, blockState, ctx.pos + offset, face, quadBounds, flags)
ordered.forEachIndexed { idx, aoData -> aoData.set(ao.vertexBrightness[idx], ao.vertexColorMultiplier[idx] * multiplier) }
}
operator fun get(dir1: EnumFacing, dir2: EnumFacing): AoData {
val isTop = top == dir1 || top == dir2
val isLeft = left == dir1 || left == dir2
return if (isTop) {
if (isLeft) topLeft else topRight
} else {
if (isLeft) bottomLeft else bottomRight
}
}
}
/**
* 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 = brWeighted(shading1.brightness, weight1, shading2.brightness, 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: EnumFacing? = null,
corner: CornerShaderFactory? = null,
edge: EdgeShaderFactory? = 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<EnumFacing, EnumFacing>? = null,
corner: CornerShaderFactory) =
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: EnumFacing? = 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 EnumFacing.AxisDirection.POSITIVE).face)
val pos = vertex.xyz.dot(vec)
EdgeInterpolateFallback(face, edgeDir, pos)
})
return resolver(quad, vertex)
}

View File

@@ -1,146 +0,0 @@
package mods.octarinecore.client.render.lighting
import mods.octarinecore.client.render.*
import mods.octarinecore.common.*
import net.minecraft.util.Direction
import net.minecraft.util.Direction.*
import java.lang.Math.min
typealias EdgeShaderFactory = (Direction, Direction) -> ModelLighter
typealias CornerShaderFactory = (Direction, Direction, Direction) -> ModelLighter
typealias ShaderFactory = (Quad, Vertex) -> ModelLighter
/** Holds lighting values for block corners as calculated by vanilla Minecraft rendering. */
class CornerLightData {
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
}
fun set(brightness: Int, colorMultiplier: Float) {
this.valid = true
this.brightness = brightness
this.red = colorMultiplier
this.green = colorMultiplier
this.blue = colorMultiplier
}
companion object {
val black: CornerLightData get() = CornerLightData()
}
}
/**
* Instances of this interface are associated with [Model] vertices, and used to apply brightness and color
* values to a [RenderVertex].
*/
interface ModelLighter {
/**
* Set shading values of a [RenderVertex]
*
* @param[context] context that can be queried for lighting data in a [Model]-relative frame of reference
* @param[vertex] the [RenderVertex] to manipulate
*/
fun shade(context: LightingCtx, vertex: RenderVertex)
/**
* Return a new rotated version of this [ModelLighter]. Used during [Model] setup when rotating the model itself.
*/
fun rotate(rot: Rotation): ModelLighter
/** Set all lighting values on the [RenderVertex] to match the given [CornerLightData]. */
fun RenderVertex.shade(shading: CornerLightData) {
brightness = shading.brightness; red = shading.red; green = shading.green; blue = shading.blue
}
/** Set the lighting values on the [RenderVertex] to a weighted average of the two [CornerLightData] instances. */
fun RenderVertex.shade(shading1: CornerLightData, shading2: CornerLightData, 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 = brWeighted(shading1.brightness, weight1, shading2.brightness, weight2)
}
/**
* Set the lighting 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 [ModelLighter] 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 [ModelLighter] created by _corner_
* - if _edge_ is given, and the _vertex_ is closest to an edge midpoint, returns the [ModelLighter] created by _edge_
*
* @param[overrideFace] assume the given face instead of going by the _quad_ normal
* @param[corner] [ModelLighter] instantiation lambda for corner vertices
* @param[edge] [ModelLighter] instantiation lambda for edge midpoint vertices
*/
fun faceOrientedAuto(overrideFace: Direction? = null,
corner: CornerShaderFactory? = null,
edge: EdgeShaderFactory? = null) =
fun(quad: Quad, vertex: Vertex): ModelLighter {
val quadFace = overrideFace ?: quad.normal.nearestCardinal
val nearestCorner = nearestPosition(vertex.xyz, boxFaces[quadFace].allCorners) {
(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 ModelLighter 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 [ModelLighter] created by _corner_
*
* @param[overrideEdge] assume the given edge instead of going by the _quad_ normal
* @param[corner] ModelLighter instantiation lambda
*/
fun edgeOrientedAuto(overrideEdge: Pair<Direction, Direction>? = null,
corner: CornerShaderFactory) =
fun(quad: Quad, vertex: Vertex): ModelLighter {
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, boxFaces[nearestFace].allCorners) {
(nearestFace.vec + it.first.vec + it.second.vec) * 0.5
}.first
return corner(nearestFace, nearestCorner.first, nearestCorner.second)
}
fun faceOrientedInterpolate(overrideFace: Direction? = null) =
fun(quad: Quad, vertex: Vertex): ModelLighter {
val resolver = faceOrientedAuto(overrideFace, edge = { face, edgeDir ->
val axis = axes.find { it != face.axis && it != edgeDir.axis }!!
val vec = Double3((axis to AxisDirection.POSITIVE).face)
val pos = vertex.xyz.dot(vec)
EdgeInterpolateFallback(face, edgeDir, pos)
})
return resolver(quad, vertex)
}

View File

@@ -1,111 +0,0 @@
package mods.octarinecore.client.render.lighting
import mods.octarinecore.client.render.BlockCtx
import mods.octarinecore.common.*
import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.BlockModelRenderer
import net.minecraft.util.Direction
import net.minecraft.world.IEnviromentBlockReader
import java.util.*
val Direction.aoMultiplier: Float get() = when(this) {
Direction.UP -> 1.0f
Direction.DOWN -> 0.5f
Direction.NORTH, Direction.SOUTH -> 0.8f
Direction.EAST, Direction.WEST -> 0.6f
}
interface LightingCtx {
val modelRotation: Rotation
val blockContext: BlockCtx
val aoEnabled: Boolean
val brightness get() = brightness(Int3.zero)
val color get() = color(Int3.zero)
fun brightness(face: Direction) = brightness(face.offset)
fun color(face: Direction) = color(face.offset)
fun brightness(offset: Int3) = offset.rotate(modelRotation).let {
blockContext.state(it).getPackedLightmapCoords(blockContext.world, blockContext.pos + it)
}
fun color(offset: Int3) = blockContext.offset(offset.rotate(modelRotation)).let { Minecraft.getInstance().blockColors.getColor(it.state, it.world, it.pos, 0) }
fun lighting(face: Direction, corner1: Direction, corner2: Direction): CornerLightData
}
class DefaultLightingCtx(blockContext: BlockCtx) : LightingCtx {
override var modelRotation = Rotation.identity
override var aoEnabled = false
protected set
override var blockContext: BlockCtx = blockContext
protected set
override var brightness = brightness(Int3.zero)
protected set
override var color = color(Int3.zero)
protected set
override fun brightness(face: Direction) = brightness(face.offset)
override fun color(face: Direction) = color(face.offset)
// smooth lighting stuff
val lightingData = Array(6) { FaceLightData(allDirections[it]) }
override fun lighting(face: Direction, corner1: Direction, corner2: Direction): CornerLightData = lightingData[face.rotate(modelRotation)].let { faceData ->
if (!faceData.isValid) faceData.update(blockContext, faceData.face.aoMultiplier)
return faceData[corner1.rotate(modelRotation), corner2.rotate(modelRotation)]
}
fun reset(blockContext: BlockCtx) {
this.blockContext = blockContext
brightness = brightness(Int3.zero)
color = color(Int3.zero)
modelRotation = Rotation.identity
lightingData.forEach { it.isValid = false }
aoEnabled = Minecraft.isAmbientOcclusionEnabled()
// allDirections.forEach { lightingData[it].update(blockContext, it.aoMultiplier) }
}
}
private val vanillaAOFactory = BlockModelRenderer.AmbientOcclusionFace::class.java.let {
it.getDeclaredConstructor(BlockModelRenderer::class.java).apply { isAccessible = true }
}.let { ctor -> { ctor.newInstance(Minecraft.getInstance().blockRendererDispatcher.blockModelRenderer) } }
class FaceLightData(val face: Direction) {
val topDir = boxFaces[face].top
val leftDir = boxFaces[face].left
val topLeft = CornerLightData()
val topRight = CornerLightData()
val bottomLeft = CornerLightData()
val bottomRight = CornerLightData()
val vanillaOrdered = when(face) {
Direction.DOWN -> listOf(topLeft, bottomLeft, bottomRight, topRight)
Direction.UP -> listOf(bottomRight, topRight, topLeft, bottomLeft)
Direction.NORTH -> listOf(bottomLeft, bottomRight, topRight, topLeft)
Direction.SOUTH -> listOf(topLeft, bottomLeft, bottomRight, topRight)
Direction.WEST -> listOf(bottomLeft, bottomRight, topRight, topLeft)
Direction.EAST -> listOf(topRight, topLeft, bottomLeft, bottomRight)
}
val delegate = vanillaAOFactory()
var isValid = false
fun update(blockCtx: BlockCtx, multiplier: Float) {
val quadBounds = FloatArray(12)
val flags = BitSet(3).apply { set(0) }
delegate.updateVertexBrightness(blockCtx.world, blockCtx.state, blockCtx.pos, face, quadBounds, flags)
vanillaOrdered.forEachIndexed { idx, corner -> corner.set(delegate.vertexBrightness[idx], delegate.vertexColorMultiplier[idx] * multiplier) }
isValid = true
}
operator fun get(dir1: Direction, dir2: Direction): CornerLightData {
val isTop = topDir == dir1 || topDir == dir2
val isLeft = leftDir == dir1 || leftDir == dir2
return if (isTop) {
if (isLeft) topLeft else topRight
} else {
if (isLeft) bottomLeft else bottomRight
}
}
}

View File

@@ -1,152 +0,0 @@
package mods.octarinecore.client.render.lighting
import mods.octarinecore.common.*
import net.minecraft.util.Direction
const val defaultCornerDimming = 0.5f
const val defaultEdgeDimming = 0.8f
// ================================
// Shader instantiation lambdas
// ================================
fun cornerAo(fallbackAxis: Direction.Axis): CornerShaderFactory = { face, dir1, dir2 ->
val fallbackDir = listOf(face, dir1, dir2).find { it.axis == fallbackAxis }!!
CornerSingleFallback(face, dir1, dir2, fallbackDir)
}
val cornerFlat = { face: Direction, dir1: Direction, dir2: Direction -> FaceFlat(face) }
fun cornerAoTri(func: (CornerLightData, CornerLightData)-> CornerLightData) = { face: Direction, dir1: Direction, dir2: Direction ->
CornerTri(face, dir1, dir2, func)
}
val cornerAoMaxGreen = cornerAoTri { s1, s2 -> if (s1.green > s2.green) s1 else s2 }
fun cornerInterpolate(edgeAxis: Direction.Axis, weight: Float, dimming: Float): CornerShaderFactory = { 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 NoLighting : ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) = vertex.shade(CornerLightData.black)
override fun rotate(rot: Rotation) = this
}
class CornerSingleFallback(val face: Direction, val dir1: Direction, val dir2: Direction, val fallbackDir: Direction, val fallbackDimming: Float = defaultCornerDimming) : ModelLighter {
val offset = Int3(fallbackDir)
override fun shade(context: LightingCtx, vertex: RenderVertex) {
val shading = context.lighting(face, dir1, dir2)
if (shading.valid)
vertex.shade(shading)
else {
vertex.shade(context.brightness(offset) brMul fallbackDimming, context.color(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: CornerLightData?, v2: CornerLightData?, func: ((CornerLightData, CornerLightData)-> CornerLightData)): CornerLightData? {
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: Direction, val dir1: Direction, val dir2: Direction,
val func: ((CornerLightData, CornerLightData)-> CornerLightData)) : ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
var acc = accumulate(
context.lighting(face, dir1, dir2),
context.lighting(dir1, face, dir2),
func)
acc = accumulate(
acc,
context.lighting(dir2, face, dir1),
func)
vertex.shade(acc ?: CornerLightData.black)
}
override fun rotate(rot: Rotation) = CornerTri(face.rotate(rot), dir1.rotate(rot), dir2.rotate(rot), func)
}
class EdgeInterpolateFallback(val face: Direction, val edgeDir: Direction, val pos: Double, val fallbackDimming: Float = defaultEdgeDimming): ModelLighter {
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: LightingCtx, vertex: RenderVertex) {
val shadingP = context.lighting(face, edgeDir, (edgeAxis to Direction.AxisDirection.POSITIVE).face)
val shadingN = context.lighting(face, edgeDir, (edgeAxis to Direction.AxisDirection.NEGATIVE).face)
if (!shadingP.valid && !shadingN.valid) {
return vertex.shade(context.brightness(offset) brMul fallbackDimming, context.color(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: Direction, val face2: Direction, val edgeDir: Direction,
val weight: Float, val dimming: Float, val fallbackDimming: Float = defaultCornerDimming) : ModelLighter {
val offset = Int3(edgeDir)
override fun shade(context: LightingCtx, vertex: RenderVertex) {
var shading1 = context.lighting(face1, edgeDir, face2)
var shading2 = context.lighting(face2, edgeDir, face1)
var weight1 = weight
var weight2 = 1.0f - weight
if (!shading1.valid && !shading2.valid) {
return vertex.shade(context.brightness(offset) brMul fallbackDimming, context.color(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: Direction): ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
vertex.red = 0.0f; vertex.green = 0.0f; vertex.blue = 0.0f;
val b = IntArray(4)
boxFaces[face].allCorners.forEachIndexed { idx, corner ->
val shading = context.lighting(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: Direction): ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
vertex.shade(context.brightness(face.offset), context.color(Int3.zero))
}
override fun rotate(rot: Rotation): ModelLighter = FaceFlat(face.rotate(rot))
}
class FlatOffset(val offset: Int3): ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
vertex.brightness = context.brightness(offset)
vertex.setColor(context.color(offset))
}
override fun rotate(rot: Rotation): ModelLighter = this
}
class FlatOffsetNoColor(val offset: Int3): ModelLighter {
override fun shade(context: LightingCtx, vertex: RenderVertex) {
vertex.brightness = context.brightness(offset)
vertex.red = 1.0f; vertex.green = 1.0f; vertex.blue = 1.0f
}
override fun rotate(rot: Rotation): ModelLighter = this
}

View File

@@ -1,5 +0,0 @@
@file:JvmName("PixelFormat")
package mods.octarinecore.client.render.lighting
import java.awt.Color

View File

@@ -1,146 +0,0 @@
package mods.octarinecore.client.render.lighting
import mods.octarinecore.client.render.CombinedContext
import mods.octarinecore.client.render.Quad
import mods.octarinecore.client.render.Vertex
import mods.octarinecore.common.Double3
import mods.octarinecore.common.Rotation
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.util.Direction.*
import java.awt.Color
typealias QuadIconResolver = (CombinedContext, Int, Quad) -> TextureAtlasSprite?
typealias PostProcessLambda = RenderVertex.(CombinedContext, Int, Quad, Int, Vertex) -> Unit
@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
val rawData = IntArray(7)
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 mirrorUV(mirrorU: Boolean, mirrorV: Boolean) {
if (mirrorU) u = -u
if (mirrorV) v = -v
}
inline fun setIcon(icon: TextureAtlasSprite): 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
}
}
/** 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
}
fun brWeighted(br1: Int, weight1: Float, br2: Int, weight2: Float): Int {
val w1int = (weight1 * 256.0f + 0.5f).toInt()
val w2int = (weight2 * 256.0f + 0.5f).toInt()
var result = 0
brightnessComponents.forEachIndexed { idx, shift ->
val comp1 = (br1 shr shift) and 15
val comp2 = (br2 shr shift) and 15
val compWeighted = (comp1 * w1int + comp2 * w2int) / 256
result = result or ((compWeighted and 15) shl shift)
}
return result
}
data class HSB(var hue: Float, var saturation: Float, var brightness: Float) {
companion object {
fun fromColor(color: Int): HSB {
val hsbVals = Color.RGBtoHSB((color shr 16) and 255, (color shr 8) and 255, color and 255, null)
return HSB(hsbVals[0], hsbVals[1], hsbVals[2])
}
}
val asColor: Int get() = Color.HSBtoRGB(hue, saturation, brightness)
}

View File

@@ -1,95 +0,0 @@
package mods.octarinecore.client.resource
import mods.betterfoliage.client.resource.Identifier
import mods.betterfoliage.client.resource.Sprite
import mods.octarinecore.common.map
import net.minecraft.client.renderer.texture.AtlasTexture
import net.minecraft.client.renderer.texture.MissingTextureSprite
import net.minecraft.profiler.IProfiler
import net.minecraft.resources.IResourceManager
import java.util.*
import java.util.concurrent.CompletableFuture
/**
* Main entry point to atlas manipulation. Called from mixins that wrap [AtlasTexture.stitch] calls.
*
* 1. All registered providers receive an [AsyncSpriteProvider.setup] call. Providers can set up their
* processing chain at this point, but should not do anything yet except configuration and housekeeping.
* 2. The [CompletableFuture] of the stitch source finishes, starting the "discovery" phase. Providers
* may register sprites in the [AtlasFuture].
* 3. After all providers finish their discovery, the atlas is stitched.
* 4. The [AtlasFuture] finishes, starting the "cleanup" phase. All [AtlasFuture.runAfter] and
* [AtlasFuture.mapAfter] tasks are processed.
* 5. After all providers finish their cleanup, we return to the original code path.
*/
class AsnycSpriteProviderManager<SOURCE: Any>(val profilerSection: String) {
val providers = mutableListOf<AsyncSpriteProvider<SOURCE>>()
/**
* Needed in order to keep the actual [AtlasTexture.stitch] call in the original method, in case
* other modders want to modify it too.
*/
class StitchWrapper(val idList: Iterable<Identifier>, val onComplete: (AtlasTexture.SheetData)->Unit) {
fun complete(sheet: AtlasTexture.SheetData) = onComplete(sheet)
}
var currentAtlas: AtlasFuture? = null
var currentPhases: List<StitchPhases> = emptyList()
@Suppress("UNCHECKED_CAST")
fun prepare(sourceObj: Any, manager: IResourceManager, idList: Iterable<Identifier>, profiler: IProfiler): Set<Identifier> {
profiler.startSection(profilerSection)
val source = CompletableFuture<SOURCE>()
currentAtlas = AtlasFuture(idList)
currentPhases = providers.map { it.setup(manager, source, currentAtlas!!) }
source.complete(sourceObj as SOURCE)
currentPhases.forEach { it.discovery.get() }
return currentAtlas!!.idSet
}
fun finish(sheetData: AtlasTexture.SheetData, profiler: IProfiler): AtlasTexture.SheetData {
currentAtlas!!.complete(sheetData)
currentPhases.forEach { it.cleanup.get() }
currentAtlas = null
currentPhases = emptyList()
profiler.endSection()
return sheetData
}
}
/**
* Provides a way for [AsyncSpriteProvider]s to register sprites to receive [CompletableFuture]s.
* Tracks sprite ids that need to be stitched.
*/
class AtlasFuture(initial: Iterable<Identifier>) {
val idSet = Collections.synchronizedSet(mutableSetOf<Identifier>().apply { addAll(initial) })
protected val sheet = CompletableFuture<AtlasTexture.SheetData>()
protected val finished = CompletableFuture<Void>()
fun complete(sheetData: AtlasTexture.SheetData) {
sheet.complete(sheetData)
finished.complete(null)
}
fun sprite(id: String) = sprite(Identifier(id))
fun sprite(id: Identifier): CompletableFuture<Sprite> {
idSet.add(id)
return sheet.map { sheetData -> sheetData.sprites.find { it.name == id } ?: throw IllegalStateException("Atlas does not contain $id") }
}
val missing = sheet.map { sheetData -> sheetData.sprites.find { it.name == MissingTextureSprite.getLocation() } }
fun <T> mapAfter(supplier: ()->T): CompletableFuture<T> = finished.map{ supplier() }
fun runAfter(action: ()->Unit): CompletableFuture<Void> = finished.thenRun(action)
}
class StitchPhases(
val discovery: CompletableFuture<Void>,
val cleanup: CompletableFuture<Void>
)
interface AsyncSpriteProvider<SOURCE: Any> {
fun setup(manager: IResourceManager, source: CompletableFuture<SOURCE>, atlas: AtlasFuture): StitchPhases
}

View File

@@ -1,17 +1,13 @@
package mods.octarinecore.client.resource package mods.octarinecore.client.resource
import mods.betterfoliage.client.resource.Identifier
import mods.betterfoliage.client.texture.loadSprite
import net.minecraft.resources.IResourceManager
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.lang.Math.max import java.lang.Math.*
data class CenteredSprite(val sprite: Identifier, val atlas: Atlas = Atlas.BLOCKS, val aspectHeight: Int = 1, val aspectWidth: Int = 1) { class CenteringTextureGenerator(domain: String, val aspectWidth: Int, val aspectHeight: Int) : TextureGenerator(domain) {
fun register(pack: GeneratedBlockTexturePack) = pack.register(this, this::draw) override fun generate(params: ParameterList): BufferedImage? {
val target = targetResource(params)!!
fun draw(resourceManager: IResourceManager): ByteArray { val baseTexture = resourceManager[target.second]?.loadImage() ?: return null
val baseTexture = resourceManager.loadSprite(atlas.wrap(sprite))
val frameWidth = baseTexture.width val frameWidth = baseTexture.width
val frameHeight = baseTexture.width * aspectHeight / aspectWidth val frameHeight = baseTexture.width * aspectHeight / aspectWidth
@@ -22,7 +18,7 @@ data class CenteredSprite(val sprite: Identifier, val atlas: Atlas = Atlas.BLOCK
val graphics = resultTexture.createGraphics() val graphics = resultTexture.createGraphics()
// iterate all frames // iterate all frames
for (frame in 0 until frames) { for (frame in 0 .. frames - 1) {
val baseFrame = baseTexture.getSubimage(0, size * frame, frameWidth, frameHeight) val baseFrame = baseTexture.getSubimage(0, size * frame, frameWidth, frameHeight)
val resultFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR) val resultFrame = BufferedImage(size, size, BufferedImage.TYPE_4BYTE_ABGR)
@@ -32,6 +28,6 @@ data class CenteredSprite(val sprite: Identifier, val atlas: Atlas = Atlas.BLOCK
graphics.drawImage(resultFrame, 0, size * frame, null) graphics.drawImage(resultFrame, 0, size * frame, null)
} }
return resultTexture.bytes return resultTexture
} }
} }

View File

@@ -1,132 +0,0 @@
package mods.octarinecore.client.resource
import com.google.common.base.Joiner
import mods.betterfoliage.client.resource.ModelIdentifier
import mods.octarinecore.HasLogger
import mods.octarinecore.client.render.BlockCtx
import mods.octarinecore.common.Int3
import mods.octarinecore.common.config.IBlockMatcher
import mods.octarinecore.common.config.ModelTextureList
import mods.octarinecore.common.plus
import mods.octarinecore.findFirst
import mods.octarinecore.common.sinkAsync
import net.minecraft.block.BlockState
import net.minecraft.client.renderer.BlockModelShapes
import net.minecraft.client.renderer.model.BlockModel
import net.minecraft.client.renderer.model.IUnbakedModel
import net.minecraft.client.renderer.model.ModelBakery
import net.minecraft.client.renderer.model.VariantList
import net.minecraft.resources.IResourceManager
import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockReader
import net.minecraftforge.registries.ForgeRegistries
import java.util.concurrent.CompletableFuture
interface ModelRenderRegistry<T> {
operator fun get(ctx: BlockCtx) = get(ctx.state, ctx.world, ctx.pos)
operator fun get(ctx: BlockCtx, offset: Int3) = get(ctx.state(offset), ctx.world, ctx.pos + offset)
operator fun get(state: BlockState, world: IBlockReader, pos: BlockPos): T?
}
abstract class ModelRenderRegistryRoot<T> : ModelRenderRegistry<T> {
val registries = mutableListOf<ModelRenderRegistry<T>>()
override fun get(state: BlockState, world: IBlockReader, pos: BlockPos) = registries.findFirst { it[state, world, pos] }
}
/**
* Information about a single BlockState and all the IUnbakedModel it could render as.
*/
class ModelDiscoveryContext(
bakery: ModelBakery,
val state: BlockState,
val modelId: ModelIdentifier
) {
val models = bakery.unwrapVariants(bakery.getUnbakedModel(modelId) to modelId)
.filter { it.second != bakery.getUnbakedModel(ModelBakery.MODEL_MISSING) }
fun ModelBakery.unwrapVariants(modelAndLoc: Pair<IUnbakedModel, ResourceLocation>): List<Pair<IUnbakedModel, ResourceLocation>> = when(val model = modelAndLoc.first) {
is VariantList -> model.variantList.flatMap { variant -> unwrapVariants(getUnbakedModel(variant.modelLocation) to variant.modelLocation) }
is BlockModel -> listOf(modelAndLoc)
else -> emptyList()
}
}
abstract class ModelDiscovery<T> : HasLogger, AsyncSpriteProvider<ModelBakery>, ModelRenderRegistry<T> {
var modelData: Map<BlockState, T> = emptyMap()
protected set
override fun get(state: BlockState, world: IBlockReader, pos: BlockPos) = modelData[state]
abstract fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<T>?
override fun setup(manager: IResourceManager, bakeryF: CompletableFuture<ModelBakery>, atlas: AtlasFuture): StitchPhases {
val modelDataTemp = mutableMapOf<BlockState, CompletableFuture<T>>()
return StitchPhases(
discovery = bakeryF.sinkAsync { bakery ->
var errors = 0
bakery.iterateModels { ctx ->
try {
processModel(ctx, atlas)?.let { modelDataTemp[ctx.state] = it }
} catch (e: Exception) {
errors++
}
}
log("${modelDataTemp.size} BlockStates discovered, $errors errors")
},
cleanup = atlas.runAfter {
modelData = modelDataTemp.filterValues { !it.isCompletedExceptionally }.mapValues { it.value.get() }
val errors = modelDataTemp.values.filter { it.isCompletedExceptionally }.size
log("${modelData.size} BlockStates loaded, $errors errors")
}
)
}
fun ModelBakery.iterateModels(func: (ModelDiscoveryContext)->Unit) {
ForgeRegistries.BLOCKS.flatMap { block ->
block.stateContainer.validStates.map { state -> state to BlockModelShapes.getModelLocation(state) }
}.forEach { (state, stateModelResource) ->
func(ModelDiscoveryContext(this, state, stateModelResource))
}
}
}
abstract class ConfigurableModelDiscovery<T> : ModelDiscovery<T>() {
abstract val matchClasses: IBlockMatcher
abstract val modelTextures: List<ModelTextureList>
abstract fun processModel(state: BlockState, textures: List<String>, atlas: AtlasFuture): CompletableFuture<T>?
override fun processModel(ctx: ModelDiscoveryContext, atlas: AtlasFuture): CompletableFuture<T>? {
val matchClass = matchClasses.matchingClass(ctx.state.block) ?: return null
log("block state ${ctx.state.toString()}")
log(" class ${ctx.state.block.javaClass.name} matches ${matchClass.name}")
if (ctx.models.isEmpty()) {
log(" no models found")
return null
}
ctx.models.filter { it.first is BlockModel }.forEach { (model, location) ->
model as BlockModel
val modelMatch = modelTextures.firstOrNull { (model to location).derivesFrom(it.modelLocation) }
if (modelMatch != null) {
log(" model ${model} matches ${modelMatch.modelLocation}")
val textures = modelMatch.textureNames.map { it to model.resolveTextureName(it) }
val texMapString = Joiner.on(", ").join(textures.map { "${it.first}=${it.second}" })
log(" sprites [$texMapString]")
if (textures.all { it.second != "missingno" }) {
// found a valid model (all required textures exist)
return processModel(ctx.state, textures.map { it.second}, atlas)
}
}
}
return null
}
}

View File

@@ -0,0 +1,136 @@
package mods.octarinecore.client.resource
import com.google.common.base.Joiner
import mods.betterfoliage.client.Client
import mods.betterfoliage.loader.Refs
import mods.octarinecore.client.render.BlockContext
import mods.octarinecore.common.Int3
import mods.octarinecore.common.config.IBlockMatcher
import mods.octarinecore.common.config.ModelTextureList
import mods.octarinecore.filterValuesNotNull
import mods.octarinecore.findFirst
import net.minecraft.block.Block
import net.minecraft.block.state.IBlockState
import net.minecraft.client.renderer.block.model.ModelResourceLocation
import net.minecraft.client.renderer.block.statemap.DefaultStateMapper
import net.minecraft.client.renderer.block.statemap.IStateMapper
import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.client.renderer.texture.TextureMap
import net.minecraft.util.ResourceLocation
import net.minecraft.util.math.BlockPos
import net.minecraft.world.IBlockAccess
import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.client.model.IModel
import net.minecraftforge.client.model.ModelLoader
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.fml.common.eventhandler.Event
import net.minecraftforge.fml.common.eventhandler.EventPriority
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.Logger
class LoadModelDataEvent(val loader: ModelLoader) : Event()
interface ModelRenderRegistry<T> {
operator fun get(ctx: BlockContext) = get(ctx.blockState(Int3.zero), ctx.world!!, ctx.pos)
operator fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos): T?
}
interface ModelRenderDataExtractor<T> {
fun processModel(state: IBlockState, modelLoc: ModelResourceLocation, model: IModel): ModelRenderKey<T>?
}
interface ModelRenderKey<T> {
val logger: Logger?
fun onPreStitch(atlas: TextureMap) {}
fun resolveSprites(atlas: TextureMap): T
}
abstract class ModelRenderRegistryRoot<T> : ModelRenderRegistry<T> {
val subRegistries = mutableListOf<ModelRenderRegistry<T>>()
override fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos) = subRegistries.findFirst { it[state, world, pos] }
fun addRegistry(registry: ModelRenderRegistry<T>) {
subRegistries.add(registry)
MinecraftForge.EVENT_BUS.register(registry)
}
}
abstract class ModelRenderRegistryBase<T> : ModelRenderRegistry<T>, ModelRenderDataExtractor<T> {
open val logger: Logger? = null
open val logName: String get() = this::class.java.name
val stateToKey = mutableMapOf<IBlockState, ModelRenderKey<T>>()
var stateToValue = mapOf<IBlockState, T>()
override fun get(state: IBlockState, world: IBlockAccess, pos: BlockPos) = stateToValue[state]
@Suppress("UNCHECKED_CAST")
@SubscribeEvent
fun handleLoadModelData(event: LoadModelDataEvent) {
stateToValue = emptyMap()
val stateMappings = Block.REGISTRY.flatMap { block ->
val mapper = event.loader.blockModelShapes.blockStateMapper.blockStateMap[block] as? IStateMapper ?: DefaultStateMapper()
(mapper.putStateModelLocations(block as Block) as Map<IBlockState, ModelResourceLocation>).entries
}
val stateModels = Refs.stateModels.get(event.loader) as Map<ModelResourceLocation, IModel>
stateMappings.forEach { mapping ->
if (mapping.key.block != null) stateModels[mapping.value]?.let { model ->
try {
processModel(mapping.key, mapping.value, model)?.let { stateToKey[mapping.key] = it }
} catch (e: Exception) {
logger?.warn("Exception while trying to process model ${mapping.value}", e)
}
}
}
}
@SubscribeEvent(priority = EventPriority.LOW)
fun handlePreStitch(event: TextureStitchEvent.Pre) {
stateToKey.forEach { (_, key) -> key.onPreStitch(event.map) }
}
@SubscribeEvent(priority = EventPriority.LOW)
fun handlePostStitch(event: TextureStitchEvent.Post) {
stateToValue = stateToKey.mapValues { (_, key) -> key.resolveSprites(event.map) }
stateToKey.clear()
}
}
abstract class ModelRenderRegistryConfigurable<T> : ModelRenderRegistryBase<T>() {
abstract val matchClasses: IBlockMatcher
abstract val modelTextures: List<ModelTextureList>
override fun processModel(state: IBlockState, modelLoc: ModelResourceLocation, model: IModel): ModelRenderKey<T>? {
val matchClass = matchClasses.matchingClass(state.block) ?: return null
logger?.log(Level.DEBUG, "$logName: block state ${state.toString()}")
logger?.log(Level.DEBUG, "$logName: class ${state.block.javaClass.name} matches ${matchClass.name}")
val allModels = model.modelBlockAndLoc.distinctBy { it.second }
if (allModels.isEmpty()) {
logger?.log(Level.DEBUG, "$logName: no models found")
return null
}
allModels.forEach { blockLoc ->
val modelMatch = modelTextures.firstOrNull { blockLoc.derivesFrom(it.modelLocation) }
if (modelMatch != null) {
logger?.log(Level.DEBUG, "$logName: model ${blockLoc.second} matches ${modelMatch.modelLocation}")
val textures = modelMatch.textureNames.map { it to blockLoc.first.resolveTextureName(it) }
val texMapString = Joiner.on(", ").join(textures.map { "${it.first}=${it.second}" })
logger?.log(Level.DEBUG, "$logName: textures [$texMapString]")
if (textures.all { it.second != "missingno" }) {
// found a valid model (all required textures exist)
return processModel(state, textures.map { it.second} )
}
}
}
return null
}
abstract fun processModel(state: IBlockState, textures: List<String>) : ModelRenderKey<T>?
}

View File

@@ -1,85 +1,120 @@
package mods.octarinecore.client.resource package mods.octarinecore.client.resource
import mods.betterfoliage.client.resource.Identifier import mods.octarinecore.metaprog.reflectField
import mods.octarinecore.HasLogger import net.minecraft.client.resources.IResourcePack
import mods.octarinecore.common.completedVoid import net.minecraft.client.resources.data.IMetadataSection
import mods.octarinecore.common.map import net.minecraft.client.resources.data.IMetadataSectionSerializer
import net.minecraft.client.renderer.model.ModelBakery import net.minecraft.client.resources.data.MetadataSerializer
import net.minecraft.client.resources.ClientResourcePackInfo import net.minecraft.client.resources.data.PackMetadataSection
import net.minecraft.resources.* import net.minecraft.util.ResourceLocation
import net.minecraft.resources.ResourcePackType.CLIENT_RESOURCES import net.minecraft.util.text.TextComponentString
import net.minecraft.resources.data.IMetadataSectionSerializer import net.minecraftforge.fml.client.FMLClientHandler
import net.minecraft.util.text.StringTextComponent import java.io.InputStream
import org.apache.logging.log4j.Logger
import java.io.IOException
import java.lang.IllegalStateException
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
import java.util.function.Predicate
import java.util.function.Supplier
/** /**
* [IResourcePack] containing generated resources * [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[name] Name of the resource pack
* @param[generators] List of resource generators * @param[generators] List of resource generators
*/ */
class GeneratedBlockTexturePack(val nameSpace: String, val packName: String, override val logger: Logger) : HasLogger, IResourcePack, AsyncSpriteProvider<ModelBakery> { class GeneratorPack(val name: String, vararg val generators: GeneratorBase) : IResourcePack {
override fun getName() = packName fun inject() {
override fun getResourceNamespaces(type: ResourcePackType) = setOf(nameSpace) FMLClientHandler.instance().reflectField<MutableList<IResourcePack>>("resourcePackList")!!.add(this)
override fun <T : Any?> getMetadata(deserializer: IMetadataSectionSerializer<T>) = null
override fun getRootResourceStream(id: String) = null
override fun getAllResourceLocations(type: ResourcePackType, path: String, maxDepth: Int, filter: Predicate<String>) = emptyList<Identifier>()
override fun close() {}
protected var manager: CompletableFuture<IResourceManager>? = null
val identifiers = Collections.synchronizedMap(mutableMapOf<Any, Identifier>())
val resources = Collections.synchronizedMap(mutableMapOf<Identifier, CompletableFuture<ByteArray>>())
fun register(key: Any, func: (IResourceManager)->ByteArray): Identifier {
if (manager == null) throw IllegalStateException("Cannot register resources unless block textures are being reloaded")
identifiers[key]?.let { return it }
val id = Identifier(nameSpace, UUID.randomUUID().toString())
val resource = manager!!.map { func(it) }
identifiers[key] = id
resources[Atlas.BLOCKS.wrap(id)] = resource
log("generated resource $key -> $id")
return id
} }
override fun getResourceStream(type: ResourcePackType, id: Identifier) = override fun getPackName() = name
if (type != CLIENT_RESOURCES) null else override fun getPackImage() = null
try { resources[id]!!.get().inputStream() } override fun getResourceDomains() = HashSet(generators.map { it.domain })
catch (e: ExecutionException) { (e.cause as? IOException)?.let { throw it } } // rethrow wrapped IOException if present override fun <T : IMetadataSection?> getPackMetadata(serializer: MetadataSerializer?, sectionName: String?) =
if (sectionName == "pack") PackMetadataSection(TextComponentString("Generated resources"), 1) as? T else null
override fun resourceExists(type: ResourcePackType, id: Identifier) = override fun resourceExists(location: ResourceLocation?): Boolean =
type == CLIENT_RESOURCES && resources.containsKey(id) if (location == null) false
else generators.find {
it.domain == location.namespace && it.resourceExists(location)
} != null
override fun setup(manager: IResourceManager, bakeryF: CompletableFuture<ModelBakery>, atlas: AtlasFuture): StitchPhases { override fun getInputStream(location: ResourceLocation?): InputStream? =
this.manager = CompletableFuture.completedFuture(manager) if (location == null) null
return StitchPhases( else generators.filter {
completedVoid(), it.domain == location.namespace && it.resourceExists(location)
atlas.runAfter { }.map { it.getInputStream(location) }
this.manager = null .filterNotNull().first()
identifiers.clear()
resources.clear() // override fun <T : IMetadataSection?> getPackMetadata(p_135058_1_: IMetadataSerializer?, p_135058_2_: String?): T {
// return if (type == "pack") PackMetadataSection(ChatComponentText("Generated resources"), 1) else null
// }
}
/**
* Abstract base class for resource generators
*
* @param[domain] Resource domain of generator
*/
abstract class GeneratorBase(val domain: String) {
/** @see [IResourcePack.resourceExists] */
abstract fun resourceExists(location: ResourceLocation?): Boolean
/** @see [IResourcePack.getInputStream] */
abstract fun getInputStream(location: ResourceLocation?): InputStream?
}
/**
* Collection of named [String]-valued key-value pairs, with an extra unnamed (keyless) value.
* Meant to be encoded as a pipe-delimited list, and used as a [ResourceLocation] path
* to parametrized generated resources.
*
* @param[params] key-value pairs
* @param[value] keyless extra value
*/
class ParameterList(val params: Map<String, String>, val value: String?) {
override fun toString() =
params.entries
.sortedBy { it.key }
.fold("") { result, entry -> result + "|${entry.key}=${entry.value}"} +
(value?.let { "|$it" } ?: "")
/** Return the value of the given parameter. */
operator fun get(key: String) = params[key]
/** Check if the given parameter exists in this list. */
operator fun contains(key: String) = key in params
/** Return a new [ParameterList] with the given key-value pair appended to it. */
operator fun plus(pair: Pair<String, String>) = ParameterList(params + pair, this.value)
companion object {
/**
* Recreate the parameter list from the encoded string, i.e. the opposite of [toString].
*
* Everything before the first pipe character is dropped, so the decoding works even if
* something is prepended to the list (like _textures/blocks/_)
*/
fun fromString(input: String): ParameterList {
val params = hashMapOf<String, String>()
var value: String? = null
val slices = input.dropWhile { it != '|'}.split('|')
slices.forEach {
if (it.contains('=')) {
val keyValue = it.split('=')
if (keyValue.size == 2) params.put(keyValue[0], keyValue[1])
} else value = it
} }
) return ParameterList(params, value)
} }
val finder = object : IPackFinder {
val packInfo = ClientResourcePackInfo(
packName, true, Supplier { this@GeneratedBlockTexturePack },
StringTextComponent(packName),
StringTextComponent("Generated block textures resource pack"),
PackCompatibility.COMPATIBLE, ResourcePackInfo.Priority.TOP, true, null, true
)
override fun <T : ResourcePackInfo> addPackInfosToMap(nameToPackMap: MutableMap<String, T>, packInfoFactory: ResourcePackInfo.IFactory<T>) {
(nameToPackMap as MutableMap<String, ResourcePackInfo>).put(packName, packInfo)
}
} }
} }
abstract class ParameterBasedGenerator(domain: String) : GeneratorBase(domain) {
abstract fun resourceExists(params: ParameterList): Boolean
abstract fun getInputStream(params: ParameterList): InputStream?
override fun resourceExists(location: ResourceLocation?) =
resourceExists(ParameterList.fromString(location?.path ?: ""))
override fun getInputStream(location: ResourceLocation?) =
getInputStream(ParameterList.fromString(location?.path ?: ""))
}

View File

@@ -1,44 +1,31 @@
package mods.octarinecore.client.resource package mods.octarinecore.client.resource
import mods.betterfoliage.BetterFoliage
import mods.betterfoliage.client.resource.Identifier
import mods.betterfoliage.client.resource.Sprite
import mods.octarinecore.client.render.Model import mods.octarinecore.client.render.Model
import mods.octarinecore.common.Double3 import mods.octarinecore.common.Double3
import mods.octarinecore.common.Int3 import mods.octarinecore.common.Int3
import mods.octarinecore.common.completedVoid import net.minecraft.client.renderer.texture.TextureAtlasSprite
import mods.octarinecore.common.sink import net.minecraft.client.renderer.texture.TextureMap
import mods.octarinecore.stripEnd import net.minecraft.util.ResourceLocation
import mods.octarinecore.stripStart
import net.minecraft.resources.IResourceManager
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.MathHelper import net.minecraft.util.math.MathHelper
import net.minecraft.world.IWorld import net.minecraft.world.World
import net.minecraft.world.gen.SimplexNoiseGenerator import net.minecraft.world.gen.NoiseGeneratorSimplex
import net.minecraftforge.client.event.TextureStitchEvent import net.minecraftforge.client.event.TextureStitchEvent
import net.minecraftforge.common.MinecraftForge
import net.minecraftforge.event.world.WorldEvent import net.minecraftforge.event.world.WorldEvent
import net.minecraftforge.eventbus.api.IEventBus import net.minecraftforge.fml.client.event.ConfigChangedEvent
import net.minecraftforge.eventbus.api.SubscribeEvent import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import net.minecraftforge.fml.config.ModConfig
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
enum class Atlas(val basePath: String) {
BLOCKS("textures"),
PARTICLES("textures/particle");
fun wrap(resource: Identifier) = Identifier(resource.namespace, "$basePath/${resource.path}.png")
fun unwrap(resource: Identifier) = resource.stripStart("$basePath/").stripEnd(".png")
fun matches(event: TextureStitchEvent) = event.map.basePath == basePath
}
// ============================ // ============================
// Resource types // Resource types
// ============================ // ============================
interface IStitchListener {
fun onPreStitch(atlas: TextureMap)
fun onPostStitch(atlas: TextureMap)
}
interface IConfigChangeListener { fun onConfigChange() } interface IConfigChangeListener { fun onConfigChange() }
interface IWorldLoadListener { fun onWorldLoad(world: IWorld) } interface IWorldLoadListener { fun onWorldLoad(world: World) }
/** /**
* Base class for declarative resource handling. * Base class for declarative resource handling.
@@ -47,26 +34,24 @@ interface IWorldLoadListener { fun onWorldLoad(world: IWorld) }
* *
* @param[modId] mod ID associated with this handler (used to filter config change events) * @param[modId] mod ID associated with this handler (used to filter config change events)
*/ */
open class ResourceHandler( open class ResourceHandler(val modId: String) {
val modId: String,
val modBus: IEventBus,
val targetAtlas: Atlas = Atlas.BLOCKS
) {
val resources = mutableListOf<Any>() val resources = mutableListOf<Any>()
open fun afterPreStitch() {}
open fun afterPostStitch() {}
// ============================ // ============================
// Self-registration // Self-registration
// ============================ // ============================
init { modBus.register(this) } init { MinecraftForge.EVENT_BUS.register(this) }
// ============================ // ============================
// Resource declarations // Resource declarations
// ============================ // ============================
fun sprite(id: Identifier) = sprite { id } fun iconStatic(domain: String, path: String) = IconHolder(domain, path).apply { resources.add(this) }
fun sprite(idFunc: ()->Identifier) = AsyncSpriteDelegate(idFunc).apply { BetterFoliage.getSpriteManager(targetAtlas).providers.add(this) } fun iconStatic(location: ResourceLocation) = iconStatic(location.namespace, location.path)
fun spriteSet(idFunc: (Int)->Identifier) = AsyncSpriteSet(targetAtlas, idFunc).apply { BetterFoliage.getSpriteManager(targetAtlas).providers.add(this) } fun iconSet(domain: String, pathPattern: String) = IconSet(domain, pathPattern).apply { this@ResourceHandler.resources.add(this) }
fun spriteSetTransformed(check: (Int)->Identifier, register: (Identifier)->Identifier) = fun iconSet(location: ResourceLocation) = iconSet(location.namespace, location.path)
AsyncSpriteSet(targetAtlas, check, register).apply { BetterFoliage.getSpriteManager(targetAtlas).providers.add(this) }
fun model(init: Model.()->Unit) = ModelHolder(init).apply { resources.add(this) } 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 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 vectorSet(num: Int, init: (Int)-> Double3) = VectorSet(num, init).apply { resources.add(this) }
@@ -76,8 +61,20 @@ open class ResourceHandler(
// Event registration // Event registration
// ============================ // ============================
@SubscribeEvent @SubscribeEvent
fun handleModConfigChange(event: ModConfig.ModConfigEvent) { fun onPreStitch(event: TextureStitchEvent.Pre) {
resources.forEach { (it as? IConfigChangeListener)?.onConfigChange() } resources.forEach { (it as? IStitchListener)?.onPreStitch(event.map) }
afterPreStitch()
}
@SubscribeEvent
fun onPostStitch(event: TextureStitchEvent.Post) {
resources.forEach { (it as? IStitchListener)?.onPostStitch(event.map) }
afterPostStitch()
}
@SubscribeEvent
fun handleConfigChange(event: ConfigChangedEvent.OnConfigChangedEvent) {
if (event.modID == modId) resources.forEach { (it as? IConfigChangeListener)?.onConfigChange() }
} }
@SubscribeEvent @SubscribeEvent
@@ -88,71 +85,54 @@ open class ResourceHandler(
// ============================ // ============================
// Resource container classes // Resource container classes
// ============================ // ============================
class AsyncSpriteDelegate(val idFunc: ()->Identifier) : ReadOnlyProperty<Any, Sprite>, AsyncSpriteProvider<Any> { class IconHolder(val domain: String, val name: String) : IStitchListener {
protected lateinit var value: Sprite val iconRes = ResourceLocation(domain, name)
override fun getValue(thisRef: Any, property: KProperty<*>) = value var icon: TextureAtlasSprite? = null
override fun onPreStitch(atlas: TextureMap) { atlas.registerSprite(iconRes) }
override fun setup(manager: IResourceManager, sourceF: CompletableFuture<Any>, atlas: AtlasFuture): StitchPhases { override fun onPostStitch(atlas: TextureMap) { icon = atlas[iconRes] }
sourceF.thenRun {
val sprite = atlas.sprite(idFunc())
atlas.runAfter {
sprite.handle { sprite, error -> value = sprite ?: atlas.missing.get()!! }
}
}
return StitchPhases(completedVoid(), completedVoid())
}
}
interface SpriteSet {
val num: Int
operator fun get(idx: Int): Sprite
}
class AsyncSpriteSet(val targetAtlas: Atlas = Atlas.BLOCKS, val idFunc: (Int)->Identifier, val transform: (Identifier)->Identifier = { it }) : AsyncSpriteProvider<Any> {
var num = 0
protected set
protected var sprites: List<Sprite> = emptyList()
override fun setup(manager: IResourceManager, sourceF: CompletableFuture<Any>, atlas: AtlasFuture): StitchPhases {
var list: List<CompletableFuture<Sprite>> = emptyList()
return StitchPhases(
discovery = sourceF.sink {
list = (0 until 16).map { idFunc(it) }
.filter { manager.hasResource( targetAtlas.wrap(it)) }
.map { transform(it) }
.map { atlas.sprite(it) }
},
cleanup = atlas.runAfter {
sprites = list.filter { !it.isCompletedExceptionally }.map { it.get() }
if (sprites.isEmpty()) sprites = listOf(atlas.missing.get()!!)
num = sprites.size
}
)
}
operator fun get(idx: Int) = sprites[idx % num]
} }
class ModelHolder(val init: Model.()->Unit): IConfigChangeListener { class ModelHolder(val init: Model.()->Unit): IConfigChangeListener {
var model: Model = Model() var model: Model = Model().apply(init)
override fun onConfigChange() { model = Model().apply(init) } override fun onConfigChange() { model = Model().apply(init) }
} }
class IconSet(val domain: String, val namePattern: String) : IStitchListener {
val resources = arrayOfNulls<ResourceLocation>(16)
val icons = arrayOfNulls<TextureAtlasSprite>(16)
var num = 0
override fun onPreStitch(atlas: TextureMap) {
num = 0
(0..15).forEach { idx ->
icons[idx] = null
val locReal = ResourceLocation(domain, "textures/${namePattern.format(idx)}.png")
if (resourceManager[locReal] != null) resources[num++] = ResourceLocation(domain, namePattern.format(idx)).apply { atlas.registerSprite(this) }
}
}
override fun onPostStitch(atlas: TextureMap) {
(0 until num).forEach { idx -> icons[idx] = atlas[resources[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 { class ModelSet(val num: Int, val init: Model.(Int)->Unit): IConfigChangeListener {
val models = Array(num) { Model() } val models = Array(num) { Model().apply{ init(it) } }
override fun onConfigChange() { (0 until num).forEach { models[it] = Model().apply{ init(it) } } } override fun onConfigChange() { (0 until num).forEach { models[it] = Model().apply{ init(it) } } }
operator fun get(idx: Int) = models[idx % num] operator fun get(idx: Int) = models[idx % num]
} }
class VectorSet(val num: Int, val init: (Int)->Double3): IConfigChangeListener { class VectorSet(val num: Int, val init: (Int)->Double3): IConfigChangeListener {
val models = Array(num) { Double3.zero } val models = Array(num) { init(it) }
override fun onConfigChange() { (0 until num).forEach { models[it] = init(it) } } override fun onConfigChange() { (0 until num).forEach { models[it] = init(it) } }
operator fun get(idx: Int) = models[idx % num] operator fun get(idx: Int) = models[idx % num]
} }
class SimplexNoise : IWorldLoadListener { class SimplexNoise() : IWorldLoadListener {
var noise = SimplexNoiseGenerator(Random()) var noise = NoiseGeneratorSimplex()
override fun onWorldLoad(world: IWorld) { noise = SimplexNoiseGenerator(Random(world.worldInfo.seed)) override fun onWorldLoad(world: World) { noise = NoiseGeneratorSimplex(Random(world.worldInfo.seed))
} }
operator fun get(x: Int, z: Int) = MathHelper.floor((noise.getValue(x.toDouble(), z.toDouble()) + 1.0) * 32.0) operator fun get(x: Int, z: Int) = MathHelper.floor((noise.getValue(x.toDouble(), z.toDouble()) + 1.0) * 32.0)
operator fun get(pos: Int3) = get(pos.x, pos.z) operator fun get(pos: Int3) = get(pos.x, pos.z)

View File

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

View File

@@ -1,31 +1,31 @@
@file:JvmName("Utils") @file:JvmName("Utils")
package mods.octarinecore.client.resource package mods.octarinecore.client.resource
import mods.betterfoliage.loader.Refs
import mods.octarinecore.PI2 import mods.octarinecore.PI2
import mods.octarinecore.client.render.lighting.HSB import mods.octarinecore.client.render.HSB
import mods.octarinecore.metaprog.reflectField
import mods.octarinecore.stripStart import mods.octarinecore.stripStart
import mods.octarinecore.tryDefault import mods.octarinecore.tryDefault
import net.minecraft.client.Minecraft import net.minecraft.client.Minecraft
import net.minecraft.client.renderer.model.BlockModel import net.minecraft.client.renderer.block.model.ModelBlock
import net.minecraft.client.renderer.texture.AtlasTexture
import net.minecraft.client.renderer.texture.MissingTextureSprite
import net.minecraft.client.renderer.texture.TextureAtlasSprite import net.minecraft.client.renderer.texture.TextureAtlasSprite
import net.minecraft.resources.IResource import net.minecraft.client.renderer.texture.TextureMap
import net.minecraft.resources.IResourceManager import net.minecraft.client.resources.IResource
import net.minecraft.resources.SimpleReloadableResourceManager import net.minecraft.client.resources.IResourceManager
import net.minecraft.client.resources.SimpleReloadableResourceManager
import net.minecraft.util.ResourceLocation import net.minecraft.util.ResourceLocation
import net.minecraftforge.client.model.IModel
import java.awt.image.BufferedImage import java.awt.image.BufferedImage
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.lang.Math.* import java.lang.Math.*
import javax.imageio.ImageIO import javax.imageio.ImageIO
import kotlin.math.atan2
/** Concise getter for the Minecraft resource manager. */ /** Concise getter for the Minecraft resource manager. */
val resourceManager: SimpleReloadableResourceManager val resourceManager: SimpleReloadableResourceManager get() =
get() = Minecraft.getMinecraft().resourceManager as SimpleReloadableResourceManager
Minecraft.getInstance().resourceManager as SimpleReloadableResourceManager
/** Append a string to the [ResourceLocation]'s path. */ /** Append a string to the [ResourceLocation]'s path. */
operator fun ResourceLocation.plus(str: String) = ResourceLocation(namespace, path + str) operator fun ResourceLocation.plus(str: String) = ResourceLocation(namespace, path + str)
@@ -36,10 +36,8 @@ operator fun IResourceManager.get(domain: String, path: String): IResource? = ge
operator fun IResourceManager.get(location: ResourceLocation): IResource? = tryDefault(null) { getResource(location) } operator fun IResourceManager.get(location: ResourceLocation): IResource? = tryDefault(null) { getResource(location) }
/** Index operator to get a texture sprite. */ /** Index operator to get a texture sprite. */
operator fun AtlasTexture.get(res: ResourceLocation): TextureAtlasSprite? = getSprite(res) operator fun TextureMap.get(name: String): TextureAtlasSprite? = getTextureExtry(ResourceLocation(name).toString())
operator fun AtlasTexture.get(name: String): TextureAtlasSprite? = getSprite(ResourceLocation(name)) operator fun TextureMap.get(res: ResourceLocation): TextureAtlasSprite? = getTextureExtry(res.toString())
val missingSprite: TextureAtlasSprite get() = MissingTextureSprite.func_217790_a()
/** Load an image resource. */ /** Load an image resource. */
fun IResource.loadImage(): BufferedImage? = ImageIO.read(this.inputStream) fun IResource.loadImage(): BufferedImage? = ImageIO.read(this.inputStream)
@@ -59,23 +57,26 @@ operator fun BufferedImage.set(x: Int, y: Int, value: Int) = this.setRGB(x, y, v
/** Get an [InputStream] to an image object in PNG format. */ /** Get an [InputStream] to an image object in PNG format. */
val BufferedImage.asStream: InputStream get() = val BufferedImage.asStream: InputStream get() =
ByteArrayInputStream(ByteArrayOutputStream().let { ImageIO.write(this, "PNG", it); it.toByteArray() }) ByteArrayInputStream(ByteArrayOutputStream().let { ImageIO.write(this, "PNG", it); it.toByteArray() })
val BufferedImage.bytes: ByteArray get() =
ByteArrayOutputStream().let { ImageIO.write(this, "PNG", it); it.toByteArray() }
/** /**
* Calculate the average color of a texture. * 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), * 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. * and the result transformed back to the RGB color space.
*/ */
val TextureAtlasSprite.averageColor: Int get() { val TextureAtlasSprite.averageColor: Int? get() {
val locationNoDirs = ResourceLocation(iconName).stripStart("blocks/")
val locationWithDirs = ResourceLocation(locationNoDirs.namespace, "textures/blocks/%s.png".format(locationNoDirs.path))
val image = resourceManager[locationWithDirs]?.loadImage() ?: return null
var numOpaque = 0 var numOpaque = 0
var sumHueX = 0.0 var sumHueX = 0.0
var sumHueY = 0.0 var sumHueY = 0.0
var sumSaturation = 0.0f var sumSaturation = 0.0f
var sumBrightness = 0.0f var sumBrightness = 0.0f
for (x in 0 until width) for (x in 0..image.width - 1)
for (y in 0 until height) { for (y in 0..image.height - 1) {
val pixel = getPixelRGBA(0, x, y) val pixel = image[x, y]
val alpha = (pixel shr 24) and 255 val alpha = (pixel shr 24) and 255
val hsb = HSB.fromColor(pixel) val hsb = HSB.fromColor(pixel)
if (alpha == 255) { if (alpha == 255) {
@@ -88,7 +89,7 @@ val TextureAtlasSprite.averageColor: Int get() {
} }
// circular average - transform sum vector to polar angle // circular average - transform sum vector to polar angle
val avgHue = (atan2(sumHueY, sumHueX) / PI2 + 0.5).toFloat() val avgHue = (atan2(sumHueY.toDouble(), sumHueX.toDouble()) / PI2 + 0.5).toFloat()
return HSB(avgHue, sumSaturation / numOpaque.toFloat(), sumBrightness / numOpaque.toFloat()).asColor return HSB(avgHue, sumSaturation / numOpaque.toFloat(), sumBrightness / numOpaque.toFloat()).asColor
} }
@@ -100,9 +101,27 @@ fun textureLocation(iconName: String) = ResourceLocation(iconName).let {
else ResourceLocation(it.namespace, "textures/${it.path}") else ResourceLocation(it.namespace, "textures/${it.path}")
} }
fun Pair<BlockModel, ResourceLocation>.derivesFrom(targetLocation: ResourceLocation): Boolean { @Suppress("UNCHECKED_CAST")
val IModel.modelBlockAndLoc: List<Pair<ModelBlock, ResourceLocation>> get() {
if (Refs.VanillaModelWrapper.isInstance(this))
return listOf(Pair(Refs.model_VMW.get(this) as ModelBlock, Refs.location_VMW.get(this) as ResourceLocation))
else if (Refs.WeightedRandomModel.isInstance(this)) Refs.models_WRM.get(this)?.let {
return (it as List<IModel>).flatMap(IModel::modelBlockAndLoc)
}
else if (Refs.MultipartModel.isInstance(this)) Refs.partModels_MPM.get(this)?.let {
return (it as Map<Any, IModel>).flatMap { it.value.modelBlockAndLoc }
} else {
this::class.java.declaredFields.find { it.type.isInstance(IModel::class.java) }?.let { modelField ->
modelField.isAccessible = true
return (modelField.get(this) as IModel).modelBlockAndLoc
}
}
return listOf()
}
fun Pair<ModelBlock, ResourceLocation>.derivesFrom(targetLocation: ResourceLocation): Boolean {
if (second.stripStart("models/") == targetLocation) return true if (second.stripStart("models/") == targetLocation) return true
if (first.parent != null && first.parentLocation != null) if (first.parent != null && first.parentLocation != null)
return Pair(first.parent!!, first.parentLocation!!).derivesFrom(targetLocation) return Pair(first.parent, first.parentLocation!!).derivesFrom(targetLocation)
return false return false
} }

View File

@@ -1,15 +0,0 @@
package mods.octarinecore.common
import net.minecraft.client.Minecraft
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.function.Consumer
import java.util.function.Function
fun completedVoid() = CompletableFuture.completedFuture<Void>(null)!!
fun <T, U> CompletionStage<T>.map(func: (T)->U) = thenApply(Function(func)).toCompletableFuture()!!
fun <T, U> CompletionStage<T>.mapAsync(func: (T)->U) = thenApplyAsync(Function(func), Minecraft.getInstance()).toCompletableFuture()!!
fun <T> CompletionStage<T>.sink(func: (T)->Unit) = thenAccept(Consumer(func)).toCompletableFuture()!!
fun <T> CompletionStage<T>.sinkAsync(func: (T)->Unit) = thenAcceptAsync(Consumer(func), Minecraft.getInstance()).toCompletableFuture()!!

View File

@@ -1,11 +1,11 @@
package mods.octarinecore.common package mods.octarinecore.common
import mods.octarinecore.cross import mods.octarinecore.cross
import net.minecraft.util.Direction import net.minecraft.util.EnumFacing
import net.minecraft.util.Direction.* import net.minecraft.util.EnumFacing.*
import net.minecraft.util.Direction.Axis.* import net.minecraft.util.EnumFacing.Axis.*
import net.minecraft.util.Direction.AxisDirection.NEGATIVE import net.minecraft.util.EnumFacing.AxisDirection.NEGATIVE
import net.minecraft.util.Direction.AxisDirection.POSITIVE import net.minecraft.util.EnumFacing.AxisDirection.POSITIVE
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
// ================================ // ================================
@@ -13,19 +13,19 @@ import net.minecraft.util.math.BlockPos
// ================================ // ================================
val axes = listOf(X, Y, Z) val axes = listOf(X, Y, Z)
val axisDirs = listOf(POSITIVE, NEGATIVE) val axisDirs = listOf(POSITIVE, NEGATIVE)
val Direction.dir: AxisDirection get() = axisDirection val EnumFacing.dir: AxisDirection get() = axisDirection
val AxisDirection.sign: String get() = when(this) { POSITIVE -> "+"; NEGATIVE -> "-" } val AxisDirection.sign: String get() = when(this) { POSITIVE -> "+"; NEGATIVE -> "-" }
val allDirections = Direction.values() val forgeDirs = EnumFacing.values()
val horizontalDirections = listOf(NORTH, SOUTH, EAST, WEST) val forgeDirsHorizontal = listOf(NORTH, SOUTH, EAST, WEST)
val allDirOffsets = allDirections.map { Int3(it) } val forgeDirOffsets = forgeDirs.map { Int3(it) }
val Pair<Axis, AxisDirection>.face: Direction get() = when(this) { val Pair<Axis, AxisDirection>.face: EnumFacing get() = when(this) {
X to POSITIVE -> EAST; X to NEGATIVE -> WEST; X to POSITIVE -> EAST; X to NEGATIVE -> WEST;
Y to POSITIVE -> UP; Y to NEGATIVE -> DOWN; Y to POSITIVE -> UP; Y to NEGATIVE -> DOWN;
Z to POSITIVE -> SOUTH; else -> NORTH; Z to POSITIVE -> SOUTH; else -> NORTH;
} }
val Direction.perpendiculars: List<Direction> get() = val EnumFacing.perpendiculars: List<EnumFacing> get() =
axes.filter { it != this.axis }.cross(axisDirs).map { it.face } axes.filter { it != this.axis }.cross(axisDirs).map { it.face }
val Direction.offset: Int3 get() = allDirOffsets[ordinal] val EnumFacing.offset: Int3 get() = forgeDirOffsets[ordinal]
/** Old ForgeDirection rotation matrix yanked from 1.7.10 */ /** Old ForgeDirection rotation matrix yanked from 1.7.10 */
val ROTATION_MATRIX: Array<IntArray> get() = arrayOf( val ROTATION_MATRIX: Array<IntArray> get() = arrayOf(
@@ -40,16 +40,15 @@ val ROTATION_MATRIX: Array<IntArray> get() = arrayOf(
// ================================ // ================================
// Vectors // Vectors
// ================================ // ================================
operator fun Direction.times(scale: Double) = operator fun EnumFacing.times(scale: Double) =
Double3(directionVec.x.toDouble() * scale, directionVec.y.toDouble() * scale, directionVec.z.toDouble() * scale) Double3(directionVec.x.toDouble() * scale, directionVec.y.toDouble() * scale, directionVec.z.toDouble() * scale)
val Direction.vec: Double3 get() = Double3(directionVec.x.toDouble(), directionVec.y.toDouble(), directionVec.z.toDouble()) val EnumFacing.vec: Double3 get() = Double3(directionVec.x.toDouble(), directionVec.y.toDouble(), directionVec.z.toDouble())
operator fun BlockPos.plus(other: Int3) = BlockPos(x + other.x, y + other.y, z + other.z) operator fun BlockPos.plus(other: Int3) = BlockPos(x + other.x, y + other.y, z + other.z)
/** 3D vector of [Double]s. Offers both mutable operations, and immutable operations in operator notation. */ /** 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) { 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(x: Float, y: Float, z: Float) : this(x.toDouble(), y.toDouble(), z.toDouble())
constructor(dir: Direction) : this(dir.directionVec.x.toDouble(), dir.directionVec.y.toDouble(), dir.directionVec.z.toDouble()) constructor(dir: EnumFacing) : this(dir.directionVec.x.toDouble(), dir.directionVec.y.toDouble(), dir.directionVec.z.toDouble())
companion object { companion object {
val zero: Double3 get() = Double3(0.0, 0.0, 0.0) val zero: Double3 get() = Double3(0.0, 0.0, 0.0)
fun weight(v1: Double3, weight1: Double, v2: Double3, weight2: Double) = fun weight(v1: Double3, weight1: Double, v2: Double3, weight2: Double) =
@@ -93,13 +92,13 @@ data class Double3(var x: Double, var y: Double, var z: Double) {
infix fun cross(o: Double3) = Double3(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x) 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 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 normalize: Double3 get() = (1.0 / length).let { Double3(x * it, y * it, z * it) }
val nearestCardinal: Direction get() = nearestAngle(this, allDirections.asIterable()) { it.vec }.first val nearestCardinal: EnumFacing get() = nearestAngle(this, forgeDirs.asIterable()) { it.vec }.first
} }
/** 3D vector of [Int]s. Offers both mutable operations, and immutable operations in operator notation. */ /** 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) { data class Int3(var x: Int, var y: Int, var z: Int) {
constructor(dir: Direction) : this(dir.directionVec.x, dir.directionVec.y, dir.directionVec.z) constructor(dir: EnumFacing) : this(dir.directionVec.x, dir.directionVec.y, dir.directionVec.z)
constructor(offset: Pair<Int, Direction>) : this( constructor(offset: Pair<Int, EnumFacing>) : this(
offset.first * offset.second.directionVec.x, offset.first * offset.second.directionVec.x,
offset.first * offset.second.directionVec.y, offset.first * offset.second.directionVec.y,
offset.first * offset.second.directionVec.z offset.first * offset.second.directionVec.z
@@ -110,7 +109,7 @@ data class Int3(var x: Int, var y: Int, var z: Int) {
// immutable operations // immutable operations
operator fun plus(other: Int3) = Int3(x + other.x, y + other.y, z + other.z) operator fun plus(other: Int3) = Int3(x + other.x, y + other.y, z + other.z)
operator fun plus(other: Pair<Int, Direction>) = Int3( operator fun plus(other: Pair<Int, EnumFacing>) = Int3(
x + other.first * other.second.directionVec.x, x + other.first * other.second.directionVec.x,
y + other.first * other.second.directionVec.y, y + other.first * other.second.directionVec.y,
z + other.first * other.second.directionVec.z z + other.first * other.second.directionVec.z
@@ -146,17 +145,17 @@ data class Int3(var x: Int, var y: Int, var z: Int) {
// ================================ // ================================
// Rotation // Rotation
// ================================ // ================================
val Direction.rotations: Array<Direction> get() = val EnumFacing.rotations: Array<EnumFacing> get() =
Array(6) { idx -> Direction.values()[ROTATION_MATRIX[ordinal][idx]] } Array(6) { idx -> EnumFacing.values()[ROTATION_MATRIX[ordinal][idx]] }
fun Direction.rotate(rot: Rotation) = rot.forward[ordinal] fun EnumFacing.rotate(rot: Rotation) = rot.forward[ordinal]
fun rot(axis: Direction) = Rotation.rot90[axis.ordinal] fun rot(axis: EnumFacing) = Rotation.rot90[axis.ordinal]
/** /**
* Class representing an arbitrary rotation (or combination of rotations) around cardinal axes by 90 degrees. * Class representing an arbitrary rotation (or combination of rotations) around cardinal axes by 90 degrees.
* In effect, a permutation of [ForgeDirection]s. * In effect, a permutation of [ForgeDirection]s.
*/ */
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
class Rotation(val forward: Array<Direction>, val reverse: Array<Direction>) { class Rotation(val forward: Array<EnumFacing>, val reverse: Array<EnumFacing>) {
operator fun plus(other: Rotation) = Rotation( operator fun plus(other: Rotation) = Rotation(
Array(6) { idx -> forward[other.forward[idx].ordinal] }, Array(6) { idx -> forward[other.forward[idx].ordinal] },
Array(6) { idx -> other.reverse[reverse[idx].ordinal] } Array(6) { idx -> other.reverse[reverse[idx].ordinal] }
@@ -164,15 +163,15 @@ class Rotation(val forward: Array<Direction>, val reverse: Array<Direction>) {
operator fun unaryMinus() = Rotation(reverse, forward) operator fun unaryMinus() = Rotation(reverse, forward)
operator fun times(num: Int) = when(num % 4) { 1 -> this; 2 -> this + this; 3 -> -this; else -> identity } operator fun times(num: Int) = when(num % 4) { 1 -> this; 2 -> this + this; 3 -> -this; else -> identity }
inline fun rotatedComponent(dir: Direction, x: Int, y: Int, z: Int) = inline fun rotatedComponent(dir: EnumFacing, x: Int, y: Int, z: Int) =
when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0 } when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0 }
inline fun rotatedComponent(dir: Direction, x: Double, y: Double, z: Double) = inline fun rotatedComponent(dir: EnumFacing, 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 } when(reverse[dir.ordinal]) { EAST -> x; WEST -> -x; UP -> y; DOWN -> -y; SOUTH -> z; NORTH -> -z; else -> 0.0 }
companion object { companion object {
// Forge rotation matrix is left-hand // Forge rotation matrix is left-hand
val rot90 = Array(6) { idx -> Rotation(allDirections[idx].opposite.rotations, allDirections[idx].rotations) } val rot90 = Array(6) { idx -> Rotation(forgeDirs[idx].opposite.rotations, forgeDirs[idx].rotations) }
val identity = Rotation(allDirections, allDirections) val identity = Rotation(forgeDirs, forgeDirs)
} }
} }
@@ -180,24 +179,8 @@ class Rotation(val forward: Array<Direction>, val reverse: Array<Direction>) {
// ================================ // ================================
// Miscellaneous // Miscellaneous
// ================================ // ================================
inline operator fun <reified T> Array<T>.get(face: Direction): T = get(face.ordinal)
data class BoxFace(val top: Direction, val left: Direction) {
val allCorners = listOf(top to left, top to left.opposite, top.opposite to left, top.opposite to left.opposite)
}
val boxFaces = allDirections.map { when(it) {
DOWN -> BoxFace(SOUTH, WEST)
UP -> BoxFace(SOUTH, EAST)
NORTH -> BoxFace(WEST, UP)
SOUTH -> BoxFace(UP, WEST)
WEST -> BoxFace(SOUTH, UP)
EAST -> BoxFace(SOUTH, DOWN)
}}.toTypedArray()
/** List of all 12 box edges, represented as a [Pair] of [ForgeDirection]s */ /** List of all 12 box edges, represented as a [Pair] of [ForgeDirection]s */
val boxEdges = allDirections.flatMap { face1 -> allDirections.filter { it.axis > face1.axis }.map { face1 to it } } 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. * Get the closest object to the specified point from a list of objects.
@@ -220,3 +203,23 @@ fun <T> nearestPosition(vertex: Double3, objs: Iterable<T>, objPos: (T)-> Double
*/ */
fun <T> nearestAngle(vector: Double3, objs: Iterable<T>, objAngle: (T)-> Double3): Pair<T, Double> = fun <T> nearestAngle(vector: Double3, objs: Iterable<T>, objAngle: (T)-> Double3): Pair<T, Double> =
objs.map { it to objAngle(it).dot(vector) }.maxBy { it.second }!! objs.map { it to objAngle(it).dot(vector) }.maxBy { it.second }!!
data class FaceCorners(val topLeft: Pair<EnumFacing, EnumFacing>,
val topRight: Pair<EnumFacing, EnumFacing>,
val bottomLeft: Pair<EnumFacing, EnumFacing>,
val bottomRight: Pair<EnumFacing, EnumFacing>) {
constructor(top: EnumFacing, left: EnumFacing) :
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)
}}

View File

@@ -0,0 +1,56 @@
package mods.octarinecore.common.config
import mods.octarinecore.client.gui.NonVerboseArrayEntry
import mods.octarinecore.client.resource.get
import mods.octarinecore.client.resource.getLines
import mods.octarinecore.client.resource.resourceManager
import net.minecraftforge.common.config.Configuration
import net.minecraftforge.common.config.Property
abstract class BlackWhiteListConfigOption<VALUE>(val domain: String, val path: String) : ConfigPropertyBase() {
val blackList = mutableListOf<VALUE>()
val whiteList = mutableListOf<VALUE>()
var blacklistProperty: Property? = null
var whitelistProperty: Property? = null
override val hasChanged: Boolean
get() = blacklistProperty?.hasChanged() ?: false || whitelistProperty?.hasChanged() ?: false
override val guiProperties: List<Property> get() = listOf(whitelistProperty!!, blacklistProperty!!)
override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) {
lang = null
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.configEntryClass = NonVerboseArrayEntry::class.java
it.languageKey = "$langPrefix.$categoryName.${it.name}"
}
read()
}
abstract fun convertValue(line: String): VALUE?
override fun read() {
listOf(Pair(blackList, blacklistProperty!!), Pair(whiteList, whitelistProperty!!)).forEach {
it.first.clear()
it.second.stringList.forEach { line ->
val value = convertValue(line)
if (value != null) it.first.add(value)
}
}
}
fun readDefaults(domain: String, path: String): Pair<Array<String>, Array<String>> {
val blackList = arrayListOf<String>()
val whiteList = arrayListOf<String>()
val defaults = resourceManager[domain, path]?.getLines()
defaults?.map{ it.trim() }?.filter { !it.startsWith("//") && it.isNotEmpty() }?.forEach {
if (it.startsWith("-")) { blackList.add(it.substring(1)) }
else { whiteList.add(it) }
}
return (blackList.toTypedArray() to whiteList.toTypedArray())
}
}

View File

@@ -1,89 +1,266 @@
@file:JvmName("DelegatingConfigKt")
package mods.octarinecore.common.config package mods.octarinecore.common.config
import mods.octarinecore.metaprog.reflectDelegates import com.google.common.collect.LinkedListMultimap
import mods.betterfoliage.loader.Refs
import mods.octarinecore.metaprog.reflectField
import mods.octarinecore.metaprog.reflectFieldsOfType
import mods.octarinecore.metaprog.reflectNestedObjects import mods.octarinecore.metaprog.reflectNestedObjects
import net.minecraftforge.common.ForgeConfigSpec import net.minecraftforge.common.MinecraftForge
import kotlin.properties.ReadOnlyProperty import net.minecraftforge.common.config.ConfigElement
import net.minecraftforge.common.config.Configuration
import net.minecraftforge.common.config.Property
import net.minecraftforge.fml.client.config.GuiConfigEntries
import net.minecraftforge.fml.client.config.IConfigElement
import net.minecraftforge.fml.client.event.ConfigChangedEvent
import net.minecraftforge.fml.common.FMLCommonHandler
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
open class DelegatingConfig(val modId: String, val langPrefix: String) { // ============================
fun build() = ForgeConfigSpec.Builder().apply { ConfigBuildContext(langPrefix, emptyList(), this).addCategory(this@DelegatingConfig) }.build() // 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) {
class ConfigBuildContext(val langPrefix: String, val path: List<String>, val builder: ForgeConfigSpec.Builder) { init { MinecraftForge.EVENT_BUS.register(this) }
fun addCategory(configObj: Any) { /** The [Configuration] backing this config object. */
configObj.reflectNestedObjects.forEach { (name, category) -> var config: Configuration? = null
builder.push(name) val rootGuiElements = mutableListOf<IConfigElement>()
descend(name).addCategory(category)
builder.pop() /** Attach this config object to the given [Configuration] and update all properties. */
fun attach(config: Configuration) {
this.config = config
val subProperties = LinkedListMultimap.create<String, String>()
rootGuiElements.clear()
forEachProperty { category, name, property ->
property.lang = property.lang ?: "$category.$name"
property.attach(config, langPrefix, category, name)
property.guiProperties.forEach { guiProperty ->
property.guiClass?.let { guiProperty.setConfigEntryClass(it) }
if (category == "global") rootGuiElements.add(ConfigElement(guiProperty))
else subProperties.put(category, guiProperty.name)
} }
configObj.reflectDelegates(ConfigDelegate::class.java).forEach { (name, delegate) -> }
descend(name).apply { delegate.addToBuilder(this) } for (category in subProperties.keySet()) {
val configCategory = config.getCategory(category)
configCategory.setLanguageKey("$langPrefix.$category")
configCategory.setPropertyOrder(subProperties[category])
rootGuiElements.add(ConfigElement(configCategory))
}
save()
// hide all categories not in the config singleton
config.categoryNames.forEach {
config.getCategory(it).setShowInGui(it in subProperties.keySet())
} }
} }
fun descend(pathName: String) = ConfigBuildContext(langPrefix, path + pathName, builder) /**
} * Execute the given lambda for all config properties.
* Lambda params: (category name, property name, property instance)
open class ConfigCategory(val comment: String? = null) { */
} inline fun forEachProperty(init: (String, String, ConfigPropertyBase)->Unit) {
reflectFieldsOfType(ConfigPropertyBase::class.java).forEach { property ->
abstract class ConfigDelegate<T> : ReadOnlyProperty<Any, T> { init("global", property.first.split("$")[0], property.second as ConfigPropertyBase)
lateinit var configValue: ForgeConfigSpec.ConfigValue<T> }
var cachedValue: T? = null for (category in reflectNestedObjects) {
category.second.reflectFieldsOfType(ConfigPropertyBase::class.java).forEach { property ->
override fun getValue(thisRef: Any, property: KProperty<*>): T { init(category.first, property.first.split("$")[0], property.second as ConfigPropertyBase)
if (cachedValue == null) cachedValue = configValue.get() }
return cachedValue!! }
} }
abstract fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder): ForgeConfigSpec.ConfigValue<T> /** Save changes to the [Configuration]. */
fun addToBuilder(ctx: ConfigBuildContext) { fun save() { if (config?.hasChanged() ?: false) config!!.save() }
val langKey = ctx.langPrefix + "." + (langPrefixOverride ?: ctx.path.joinToString("."))
ctx.builder.translation(langKey) /**
configValue = getConfigValue(ctx.path.last(), ctx.builder) * Returns true if any of the given configuration elements have changed.
* Supports both categories and
*/
fun hasChanged(elements: List<ConfigPropertyBase>): 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
} }
var langPrefixOverride: String? = null /** Called when the configuration for the mod changes. */
fun lang(prefix: String) = apply { langPrefixOverride = prefix } abstract fun onChange(event: ConfigChangedEvent.PostConfigChangedEvent)
@SubscribeEvent
fun handleConfigChange(event: ConfigChangedEvent.PostConfigChangedEvent) {
if (event.modID == modId) {
// refresh values
forEachProperty { c, n, prop -> prop.read() }
// call mod-specific handler
onChange(event)
// save to file
save()
Refs.resetChangedState.invoke(config!!)
}
}
/** Extension to get the underlying delegate of a field */
operator fun Any.get(name: String) = this.reflectField<ConfigPropertyBase>("$name\$delegate")
} }
class DelegatingBooleanValue(val defaultValue: Boolean) : ConfigDelegate<Boolean>() { // ============================
override fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder) = builder.define(name, defaultValue) // Property delegates
// ============================
/** Base class for config property delegates. */
abstract class ConfigPropertyBase {
/** Language key of the property. */
var lang: String? = null
/** GUI class to use. */
var guiClass: Class<out GuiConfigEntries.IConfigEntry>? = null
/** @return true if the property has changed. */
abstract val hasChanged: Boolean
/** Attach this delegate to a Forge [Configuration]. */
abstract fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String)
/** List of [Property] instances backing this delegate. */
abstract val guiProperties: List<Property>
/** Re-read the property value from the [Configuration]. */
open fun read() {}
} }
class DelegatingIntValue( class ObsoleteConfigProperty : ConfigPropertyBase() {
val minValue: Int = 0, override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) {
val maxValue: Int = 1, target.getCategory(categoryName)?.remove(propertyName)
val defaultValue: Int = 0 }
) : ConfigDelegate<Int>() { override val guiProperties = emptyList<Property>()
override fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder) = builder.defineInRange(name, defaultValue, minValue, maxValue) override val hasChanged: Boolean get() = false
} }
class DelegatingLongValue( /** Delegate for a property backed by a single [Property] instance. */
val minValue: Long = 0, abstract class ConfigPropertyDelegate<T>() : ConfigPropertyBase() {
val maxValue: Long = 1, /** Cached value of the property. */
val defaultValue: Long = 0 var cached: T? = null
) : ConfigDelegate<Long>() { /** The [Property] backing this delegate. */
override fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder) = builder.defineInRange(name, defaultValue, minValue, maxValue) var property: Property? = null
override val guiProperties: List<Property> get() = listOf(property!!)
override val hasChanged: Boolean get() = property?.hasChanged() ?: false
/** Chained setter for the language key. */
fun lang(lang: String) = apply { this.lang = lang }
/** Read the backing [Property] instance. */
abstract fun Property.read(): T
/** Write the backing [Property] instance. */
abstract fun Property.write(value: T)
/** Get the backing [Property] instance. */
abstract fun resolve(target: Configuration, category: String, name: String): Property
/** Kotlin deleagation implementation. */
operator fun getValue(thisRef: Any, delegator: KProperty<*>): T {
if (cached != null) return cached!!
cached = property!!.read()
return cached!!
}
/** Kotlin deleagation implementation. */
operator fun setValue(thisRef: Any, delegator: KProperty<*>, value: T) {
cached = value
property!!.write(value)
}
override fun read() { cached = null }
override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) {
cached = null
property = resolve(target, categoryName, propertyName)
property!!.setLanguageKey("$langPrefix.$lang")
}
} }
class DelegatingDoubleValue( /** [Double]-typed property delegate. */
val minValue: Double = 0.0, class ConfigPropertyDouble(val min: Double, val max: Double, val default: Double) :
val maxValue: Double = 1.0, ConfigPropertyDelegate<Double>() {
val defaultValue: Double = 0.0 override fun resolve(target: Configuration, category: String, name: String) =
) : ConfigDelegate<Double>() { target.get(category, name, default, null).apply { setMinValue(min); setMaxValue(max) }
override fun getConfigValue(name: String, builder: ForgeConfigSpec.Builder) = builder.defineInRange(name, defaultValue, minValue, maxValue) override fun Property.read() = property!!.double
override fun Property.write(value: Double) = property!!.set(value)
}
/** [Float]-typed property delegate. */
class ConfigPropertyFloat(val min: Double, val max: Double, val default: Double) :
ConfigPropertyDelegate<Float>() {
override fun resolve(target: Configuration, category: String, name: String) =
target.get(category, name, default, null).apply { setMinValue(min); setMaxValue(max) }
override fun Property.read() = property!!.double.toFloat()
override fun Property.write(value: Float) = property!!.set(value.toDouble())
}
/** [Int]-typed property delegate. */
class ConfigPropertyInt(val min: Int, val max: Int, val default: Int) :
ConfigPropertyDelegate<Int>() {
override fun resolve(target: Configuration, category: String, name: String) =
target.get(category, name, default, null).apply { setMinValue(min); setMaxValue(max) }
override fun Property.read() = property!!.int
override fun Property.write(value: Int) = property!!.set(value)
}
/** [Long]-typed property delegate. Still uses [Int] internally */
class ConfigPropertyLong(val min: Int, val max: Int, val default: Int) :
ConfigPropertyDelegate<Long>() {
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!!.long
override fun Property.write(value: Long) = property!!.set(value)
}
/** [Boolean]-typed property delegate. */
class ConfigPropertyBoolean(val default: Boolean) :
ConfigPropertyDelegate<Boolean>() {
override fun resolve(target: Configuration, category: String, name: String) =
target.get(category, name, default, null)
override fun Property.read() = property!!.boolean
override fun Property.write(value: Boolean) = property!!.set(value)
}
/** [Int] array typed property delegate. */
class ConfigPropertyIntList(val defaults: ()->Array<Int>) :
ConfigPropertyDelegate<Array<Int>>() {
override fun resolve(target: Configuration, category: String, name: String) =
target.get(category, name, defaults().toIntArray(), null)
override fun Property.read() = property!!.intList.toTypedArray()
override fun Property.write(value: Array<Int>) = property!!.set(value.toIntArray())
} }
// ============================ // ============================
// Delegate factory methods // Delegate factory methods
// ============================ // ============================
fun double(min: Double = 0.0, max: Double = 1.0, default: Double) = DelegatingDoubleValue(min, max, default) fun double(min: Double = 0.0, max: Double = 1.0, default: Double) = ConfigPropertyDouble(min, max, default)
fun int(min: Int = 0, max: Int, default: Int) = DelegatingIntValue(min, max, default) fun float(min: Double = 0.0, max: Double = 1.0, default: Double) = ConfigPropertyFloat(min, max, default)
fun long(min: Long = 0, max: Long, default: Long) = DelegatingLongValue(min, max, default) fun int(min: Int = 0, max: Int, default: Int) = ConfigPropertyInt(min, max, default)
fun boolean(default: Boolean) = DelegatingBooleanValue(default) fun long(min: Int = 0, max: Int, default: Int) = ConfigPropertyLong(min, max, default)
fun intList(defaults: ()->Array<Int>) = ConfigPropertyIntList(defaults)
fun boolean(default: Boolean) = ConfigPropertyBoolean(default)

View File

@@ -1,11 +1,8 @@
package mods.octarinecore.common.config package mods.octarinecore.common.config
import mods.octarinecore.client.resource.getLines
import mods.octarinecore.client.resource.resourceManager
import mods.octarinecore.metaprog.getJavaClass import mods.octarinecore.metaprog.getJavaClass
import net.minecraft.block.Block import net.minecraft.block.Block
import net.minecraft.util.ResourceLocation import net.minecraft.util.ResourceLocation
import org.apache.logging.log4j.Logger
interface IBlockMatcher { interface IBlockMatcher {
fun matchesClass(block: Block): Boolean fun matchesClass(block: Block): Boolean
@@ -22,11 +19,8 @@ class SimpleBlockMatcher(vararg val classes: Class<*>) : IBlockMatcher {
} }
} }
class ConfigurableBlockMatcher(val logger: Logger, val location: ResourceLocation) : IBlockMatcher { class ConfigurableBlockMatcher(domain: String, path: String) : IBlockMatcher, BlackWhiteListConfigOption<Class<*>>(domain, path) {
override fun convertValue(line: String) = getJavaClass(line)
val blackList = mutableListOf<Class<*>>()
val whiteList = mutableListOf<Class<*>>()
// override fun convertValue(line: String) = getJavaClass(line)
override fun matchesClass(block: Block): Boolean { override fun matchesClass(block: Block): Boolean {
val blockClass = block.javaClass val blockClass = block.javaClass
@@ -41,34 +35,16 @@ class ConfigurableBlockMatcher(val logger: Logger, val location: ResourceLocatio
whiteList.forEach { if (it.isAssignableFrom(blockClass)) return it } whiteList.forEach { if (it.isAssignableFrom(blockClass)) return it }
return null return null
} }
fun readDefaults() {
blackList.clear()
whiteList.clear()
resourceManager.getAllResources(location).forEach { resource ->
logger.debug("Reading resource $location from pack ${resource.packName}")
resource.getLines().map{ it.trim() }.filter { !it.startsWith("//") && it.isNotEmpty() }.forEach { line ->
if (line.startsWith("-")) getJavaClass(line.substring(1))?.let { blackList.add(it) }
else getJavaClass(line)?.let { whiteList.add(it) }
}
}
}
} }
data class ModelTextureList(val modelLocation: ResourceLocation, val textureNames: List<String>) { data class ModelTextureList(val modelLocation: ResourceLocation, val textureNames: List<String>) {
constructor(vararg args: String) : this(ResourceLocation(args[0]), listOf(*args).drop(1)) constructor(vararg args: String) : this(ResourceLocation(args[0]), listOf(*args).drop(1))
} }
class ModelTextureListConfiguration(val logger: Logger, val location: ResourceLocation) { class ModelTextureListConfigOption(domain: String, path: String, val minTextures: Int) : StringListConfigOption<ModelTextureList>(domain, path) {
val modelList = mutableListOf<ModelTextureList>() override fun convertValue(line: String): ModelTextureList? {
fun readDefaults() {
resourceManager.getAllResources(location).forEach { resource ->
logger.debug("Reading resource $location from pack ${resource.packName}")
resource.getLines().map{ it.trim() }.filter { !it.startsWith("//") && it.isNotEmpty() }.forEach { line ->
val elements = line.split(",") val elements = line.split(",")
modelList.add(ModelTextureList(ResourceLocation(elements.first()), elements.drop(1))) if (elements.size < minTextures + 1) return null
} return ModelTextureList(ResourceLocation(elements.first()), elements.drop(1))
}
} }
} }

View File

@@ -0,0 +1,43 @@
package mods.octarinecore.common.config
import mods.octarinecore.client.gui.NonVerboseArrayEntry
import mods.octarinecore.client.resource.get
import mods.octarinecore.client.resource.getLines
import mods.octarinecore.client.resource.resourceManager
import net.minecraftforge.common.config.Configuration
import net.minecraftforge.common.config.Property
abstract class StringListConfigOption<VALUE>(val domain: String, val path: String) : ConfigPropertyBase() {
val list = mutableListOf<VALUE>()
lateinit var listProperty: Property
override val hasChanged: Boolean get() = listProperty.hasChanged() ?: false
override val guiProperties: List<Property> get() = listOf(listProperty)
override fun attach(target: Configuration, langPrefix: String, categoryName: String, propertyName: String) {
lang = null
val defaults = readDefaults(domain, path)
listProperty = target.get(categoryName, "${propertyName}", defaults)
listProperty.configEntryClass = NonVerboseArrayEntry::class.java
listProperty.languageKey = "$langPrefix.$categoryName.${listProperty.name}"
read()
}
abstract fun convertValue(line: String): VALUE?
override fun read() {
list.clear()
listProperty.stringList.forEach { line ->
val value = convertValue(line)
if (value != null) list.add(value)
}
}
fun readDefaults(domain: String, path: String): Array<String> {
val list = arrayListOf<String>()
val defaults = resourceManager[domain, path]?.getLines()
defaults?.map { it.trim() }?.filter { !it.startsWith("//") && it.isNotEmpty() }?.forEach { list.add(it) }
return list.toTypedArray()
}
}

View File

@@ -5,8 +5,6 @@ import java.lang.reflect.Field
import java.lang.reflect.Method import java.lang.reflect.Method
import mods.octarinecore.metaprog.Namespace.* import mods.octarinecore.metaprog.Namespace.*
import mods.octarinecore.tryDefault import mods.octarinecore.tryDefault
import kotlin.reflect.KCallable
import kotlin.reflect.KFunction
/** Get a Java class with the given name. */ /** Get a Java class with the given name. */
fun getJavaClass(name: String) = tryDefault(null) { Class.forName(name) } fun getJavaClass(name: String) = tryDefault(null) { Class.forName(name) }
@@ -45,14 +43,6 @@ fun Any.reflectFieldsOfType(vararg types: Class<*>) = this.javaClass.declaredFie
.map { field -> field.name to field.let { it.isAccessible = true; it.get(this) } } .map { field -> field.name to field.let { it.isAccessible = true; it.get(this) } }
.filterNotNull() .filterNotNull()
fun <T> Any.reflectFields(type: Class<T>) = this.javaClass.declaredFields
.filter { field -> type.isAssignableFrom(field.type) }
.map { field -> field.name to field.let { it.isAccessible = true; it.get(this) as T } }
fun <T> Any.reflectDelegates(type: Class<T>) = this.javaClass.declaredFields
.filter { field -> type.isAssignableFrom(field.type) && field.name.contains("$") }
.map { field -> field.name.split("$")[0] to field.let { it.isAccessible = true; it.get(this) } as T }
enum class Namespace { MCP, SRG } enum class Namespace { MCP, SRG }
abstract class Resolvable<T> { abstract class Resolvable<T> {
@@ -68,19 +58,19 @@ fun allAvailable(vararg codeElement: Resolvable<*>) = codeElement.all { it.eleme
* *
* @param[name] MCP name of the class * @param[name] MCP name of the class
*/ */
open class ClassRef<T: Any?>(val name: String) : Resolvable<Class<T>>() { open class ClassRef(val name: String) : Resolvable<Class<*>>() {
companion object { companion object {
val int = ClassRefPrimitive("I", Int::class.java) val int = ClassRefPrimitive("I", Int::class.java)
val long = ClassRefPrimitive("J", Long::class.java) val long = ClassRefPrimitive("J", Long::class.java)
val float = ClassRefPrimitive("F", Float::class.java) val float = ClassRefPrimitive("F", Float::class.java)
val boolean = ClassRefPrimitive("Z", Boolean::class.java) val boolean = ClassRefPrimitive("Z", Boolean::class.java)
val void = ClassRefPrimitive("V", Void::class.java) val void = ClassRefPrimitive("V", null)
} }
open fun asmDescriptor(namespace: Namespace) = "L${name.replace(".", "/")};" open fun asmDescriptor(namespace: Namespace) = "L${name.replace(".", "/")};"
override fun resolve() = getJavaClass(name) as Class<T>? override fun resolve() = getJavaClass(name)
fun isInstance(obj: Any) = element?.isInstance(obj) ?: false fun isInstance(obj: Any) = element?.isInstance(obj) ?: false
} }
@@ -91,11 +81,18 @@ open class ClassRef<T: Any?>(val name: String) : Resolvable<Class<T>>() {
* @param[name] ASM descriptor of this primitive type * @param[name] ASM descriptor of this primitive type
* @param[clazz] class of this primitive type * @param[clazz] class of this primitive type
*/ */
class ClassRefPrimitive<T>(name: String, val clazz: Class<T>?) : ClassRef<T>(name) { class ClassRefPrimitive(name: String, val clazz: Class<*>?) : ClassRef(name) {
override fun asmDescriptor(namespace: Namespace) = name override fun asmDescriptor(namespace: Namespace) = name
override fun resolve() = clazz override fun resolve() = clazz
} }
class ClassRefArray(name: String) : ClassRef(name) {
override fun asmDescriptor(namespace: Namespace) = "[" + super.asmDescriptor(namespace)
override fun resolve() = getJavaClass("[L$name;")
}
fun ClassRef.array() = ClassRefArray(name)
/** /**
* Reference to a method. * Reference to a method.
* *
@@ -105,13 +102,13 @@ class ClassRefPrimitive<T>(name: String, val clazz: Class<T>?) : ClassRef<T>(nam
* @param[returnType] reference to the return type * @param[returnType] reference to the return type
* @param[returnType] references to the argument types * @param[returnType] references to the argument types
*/ */
class MethodRef<T: Any?>(val parentClass: ClassRef<*>, class MethodRef(val parentClass: ClassRef,
val mcpName: String, val mcpName: String,
val srgName: String, val srgName: String,
val returnType: ClassRef<T>, val returnType: ClassRef,
vararg val argTypes: ClassRef<*> vararg val argTypes: ClassRef
) : Resolvable<Method>() { ) : Resolvable<Method>() {
constructor(parentClass: ClassRef<*>, mcpName: String, returnType: ClassRef<T>, vararg argTypes: ClassRef<*>) : constructor(parentClass: ClassRef, mcpName: String, returnType: ClassRef, vararg argTypes: ClassRef) :
this(parentClass, mcpName, mcpName, returnType, *argTypes) this(parentClass, mcpName, mcpName, returnType, *argTypes)
fun name(namespace: Namespace) = when(namespace) { SRG -> srgName; MCP -> mcpName } fun name(namespace: Namespace) = when(namespace) { SRG -> srgName; MCP -> mcpName }
@@ -128,10 +125,11 @@ class MethodRef<T: Any?>(val parentClass: ClassRef<*>,
} }
/** Invoke this method using reflection. */ /** Invoke this method using reflection. */
operator fun invoke(receiver: Any, vararg args: Any?) = element?.invoke(receiver, *args) as T fun invoke(receiver: Any, vararg args: Any?) = element?.invoke(receiver, *args)
/** Invoke this static method using reflection. */ /** Invoke this static method using reflection. */
fun invokeStatic(vararg args: Any) = element?.invoke(null, *args) fun invokeStatic(vararg args: Any) = element?.invoke(null, *args)
} }
/** /**
@@ -142,41 +140,30 @@ class MethodRef<T: Any?>(val parentClass: ClassRef<*>,
* @param[srgName] SRG name of the field * @param[srgName] SRG name of the field
* @param[type] reference to the field type * @param[type] reference to the field type
*/ */
class FieldRef<T>(val parentClass: ClassRef<*>, class FieldRef(val parentClass: ClassRef,
val mcpName: String, val mcpName: String,
val srgName: String, val srgName: String,
val type: ClassRef<T> val type: ClassRef?
) : Resolvable<Field>() { ) : Resolvable<Field>() {
constructor(parentClass: ClassRef<*>, mcpName: String, type: ClassRef<T>) : this(parentClass, mcpName, mcpName, type) constructor(parentClass: ClassRef, mcpName: String, type: ClassRef?) : this(parentClass, mcpName, mcpName, type)
fun name(namespace: Namespace) = when(namespace) { SRG -> srgName; MCP -> mcpName } fun name(namespace: Namespace) = when(namespace) { SRG -> srgName; MCP -> mcpName }
fun asmDescriptor(namespace: Namespace) = type.asmDescriptor(namespace) fun asmDescriptor(namespace: Namespace) = type!!.asmDescriptor(namespace)
override fun resolve(): Field? = override fun resolve(): Field? =
if (parentClass.element == null) null if (parentClass.element == null) null
else { else {
listOf(srgName, mcpName).mapNotNull { tryDefault(null) { listOf(srgName, mcpName).map { tryDefault(null) {
parentClass.element!!.getDeclaredField(it) parentClass.element!!.getDeclaredField(it)
} }.firstOrNull() }}.filterNotNull().firstOrNull()
?.apply{ isAccessible = true } ?.apply{ isAccessible = true }
} }
/** Get this field using reflection. */ /** Get this field using reflection. */
operator fun get(receiver: Any?) = element?.get(receiver) as T fun get(receiver: Any?) = element?.get(receiver)
/** Get this static field using reflection. */ /** Get this static field using reflection. */
fun getStatic() = get(null) fun getStatic() = get(null)
fun set(receiver: Any?, obj: Any?) { element?.set(receiver, obj) } fun set(receiver: Any?, obj: Any?) { element?.set(receiver, obj) }
} }
fun Any.isInstance(cls: ClassRef<*>) = cls.isInstance(this)
interface ReflectionCallable<T> {
operator fun invoke(vararg args: Any): T
}
inline operator fun <reified T> Any.get(field: FieldRef<T>) = field.get(this)
inline operator fun <reified T> Any.set(field: FieldRef<T>, value: T) = field.set(this, value)
inline operator fun <T> Any.get(methodRef: MethodRef<T>) = object : ReflectionCallable<T> {
override fun invoke(vararg args: Any) = methodRef.invoke(this@get, args)
}

View File

@@ -0,0 +1,212 @@
package mods.octarinecore.metaprog
import mods.octarinecore.metaprog.Namespace.*
import net.minecraft.launchwrapper.IClassTransformer
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin
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.*
import java.io.File
import java.io.FileOutputStream
/**
* Base class for convenient bytecode transformers.
*/
open class Transformer : IClassTransformer {
val log = LogManager.getLogger(this)
/** The list of transformers and targets. */
var methodTransformers: MutableList<Pair<MethodRef, MethodTransformContext.()->Unit>> = arrayListOf()
/** Add a transformation to perform. Call this during instance initialization.
*
* @param[method] the target method of the transformation
* @param[trans] method transformation lambda
*/
fun transformMethod(method: MethodRef, trans: MethodTransformContext.()->Unit) = methodTransformers.add(method to trans)
override fun transform(name: String?, transformedName: String?, classData: ByteArray?): ByteArray? {
if (classData == null) return null
val classNode = ClassNode().apply { val reader = ClassReader(classData); reader.accept(this, 0) }
var workDone = false
var writerFlags = 0
synchronized(this) {
methodTransformers.forEach { (targetMethod, transform) ->
if (transformedName != targetMethod.parentClass.name) return@forEach
for (method in classNode.methods) {
val namespace = Namespace.values().find {
method.name == targetMethod.name(it) && method.desc == targetMethod.asmDescriptor(it)
} ?: continue
when (namespace) {
MCP -> log.info("Found method ${targetMethod.parentClass.name}.${targetMethod.name(MCP)} ${targetMethod.asmDescriptor(MCP)}")
SRG -> log.info("Found method ${targetMethod.parentClass.name}.${targetMethod.name(namespace)} ${targetMethod.asmDescriptor(namespace)} (matching ${targetMethod.name(MCP)})")
}
// write input bytecode for debugging - definitely not in production...
//File("BF_debug").mkdir()
//FileOutputStream(File("BF_debug/$transformedName.class")).apply {
// write(classData)
// close()
//}
// transform
writerFlags = MethodTransformContext(method, namespace, writerFlags).apply(transform).writerFlags
workDone = true
}
}
}
return if (!workDone) classData else ClassWriter(writerFlags).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, var writerFlags: Int) {
fun applyWriterFlags(vararg flagValue: Int) { flagValue.forEach { writerFlags = writerFlags or it } }
fun makePublic() {
method.access = (method.access or Opcodes.ACC_PUBLIC) and (Opcodes.ACC_PRIVATE or Opcodes.ACC_PROTECTED).inv()
}
/**
* 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{
val insertBeforeNode = this@insertBefore //.let { if (it.previous is FrameNode) it.previous else it }
this.init(); list.forEach { method.instructions.insertBefore(insertBeforeNode, it) }
}
fun AbstractInsnNode.replace(init: InstructionList.()->Unit) = InstructionList(environment).apply {
insertAfter(init)
method.instructions.remove(this@replace)
}
/** Remove all isntructiuons between the given two (inclusive). */
fun Pair<AbstractInsnNode, AbstractInsnNode>.remove() {
var current: AbstractInsnNode? = first
while (current != null && current != second) {
val next = current.next
method.instructions.remove(current)
current = next
}
if (current != null) method.instructions.remove(current)
}
/**
* Replace all isntructiuons between the given two (inclusive) with the specified instruction list.
*
* @param[init] builder-style lambda to assemble instruction list
*/
fun Pair<AbstractInsnNode, AbstractInsnNode>.replace(init: InstructionList.()->Unit) {
val beforeInsn = first.previous
remove()
beforeInsn.insertAfter(init)
}
/**
* Matches variable instructions.
*
* @param[opcode] instruction opcode
* @param[idx] variable the opcode references
*/
fun varinsn(opcode: Int, idx: Int): (AbstractInsnNode)->Boolean = { insn ->
insn.opcode == opcode && insn is VarInsnNode && insn.`var` == idx
}
fun invokeName(name: String): (AbstractInsnNode)->Boolean = { insn ->
(insn as? MethodInsnNode)?.name == name
}
fun invokeRef(ref: MethodRef): (AbstractInsnNode)->Boolean = { insn ->
(insn as? MethodInsnNode)?.let {
it.name == ref.name(environment) && it.owner == ref.parentClass.name.replace(".", "/")
} ?: false
}
}
/**
* Allows builder-style declarative definition of instruction lists.
*
* @param[environment] the type of environment we are in
*/
class InstructionList(val environment: Namespace) {
fun insn(opcode: Int) = list.add(InsnNode(opcode))
/** The instruction list being assembled. */
val list: MutableList<AbstractInsnNode> = arrayListOf()
/**
* Adds a variable instruction.
*
* @param[opcode] instruction opcode
* @param[idx] variable the opcode references
*/
fun varinsn(opcode: Int, idx: Int) = list.add(VarInsnNode(opcode, idx))
/**
* Adds an INVOKESTATIC instruction.
*
* @param[target] the target method of the instruction
* @param[isInterface] true if the target method is defined by an interface
*/
fun invokeStatic(target: MethodRef, isInterface: Boolean = false) = list.add(MethodInsnNode(
Opcodes.INVOKESTATIC,
target.parentClass.name.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.replace(".", "/"),
target.name(environment),
target.asmDescriptor(environment)
))
}

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