mirror of
https://github.com/Jozufozu/Flywheel.git
synced 2025-02-08 19:24:59 +01:00
Merge branch '1.20.1/dev' into 1.21.1/dev
# Conflicts: # common/build.gradle.kts # gradle.properties # vanillinForge/src/main/java/dev/engine_room/vanillin/VanillinForge.java
This commit is contained in:
commit
1b0e327d7f
48 changed files with 2269 additions and 34 deletions
|
@ -119,6 +119,8 @@ dependencies {
|
|||
modCompileOnly("maven.modrinth:sodium:${property("sodium_version")}-fabric")
|
||||
modCompileOnly("maven.modrinth:iris:${property("iris_version")}-fabric")
|
||||
|
||||
compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:0.4.1")!!)
|
||||
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.8.1")
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ public interface FlwApiLink {
|
|||
@Nullable
|
||||
<T extends Entity> EntityVisualizer<? super T> getVisualizer(EntityType<T> type);
|
||||
|
||||
<T extends BlockEntity> void setVisualizer(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer);
|
||||
<T extends BlockEntity> void setVisualizer(BlockEntityType<T> type, @Nullable BlockEntityVisualizer<? super T> visualizer);
|
||||
|
||||
<T extends Entity> void setVisualizer(EntityType<T> type, EntityVisualizer<? super T> visualizer);
|
||||
<T extends Entity> void setVisualizer(EntityType<T> type, @Nullable EntityVisualizer<? super T> visualizer);
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public final class VisualizerRegistry {
|
|||
* @param visualizer The visualizer to set.
|
||||
* @param <T> The type of the block entity.
|
||||
*/
|
||||
public static <T extends BlockEntity> void setVisualizer(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer) {
|
||||
public static <T extends BlockEntity> void setVisualizer(BlockEntityType<T> type, @Nullable BlockEntityVisualizer<? super T> visualizer) {
|
||||
FlwApiLink.INSTANCE.setVisualizer(type, visualizer);
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ public final class VisualizerRegistry {
|
|||
* @param visualizer The visualizer to set.
|
||||
* @param <T> The type of the entity.
|
||||
*/
|
||||
public static <T extends Entity> void setVisualizer(EntityType<T> type, EntityVisualizer<? super T> visualizer) {
|
||||
public static <T extends Entity> void setVisualizer(EntityType<T> type, @Nullable EntityVisualizer<? super T> visualizer) {
|
||||
FlwApiLink.INSTANCE.setVisualizer(type, visualizer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package dev.engine_room.flywheel.backend.engine;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
|
||||
import dev.engine_room.flywheel.api.RenderContext;
|
||||
import dev.engine_room.flywheel.api.backend.Engine;
|
||||
import dev.engine_room.flywheel.api.instance.Instance;
|
||||
|
@ -90,6 +92,8 @@ public class EngineImpl implements Engine {
|
|||
@Override
|
||||
public void setupRender(RenderContext context) {
|
||||
try (var state = GlStateTracker.getRestoreState()) {
|
||||
// Process the render queue for font updates
|
||||
RenderSystem.replayQueue();
|
||||
Uniforms.update(context);
|
||||
environmentStorage.flush();
|
||||
drawManager.flush(lightStorage, environmentStorage);
|
||||
|
|
|
@ -12,3 +12,5 @@ out vec3 flw_vertexNormal;
|
|||
out float flw_distance;
|
||||
|
||||
FlwMaterial flw_material;
|
||||
|
||||
#define flw_vertexId gl_VertexID
|
||||
|
|
|
@ -2,7 +2,6 @@ package dev.engine_room.flywheel.lib.instance;
|
|||
|
||||
import dev.engine_room.flywheel.api.instance.InstanceHandle;
|
||||
import dev.engine_room.flywheel.api.instance.InstanceType;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.util.FastColor;
|
||||
|
||||
public abstract class ColoredLitInstance extends AbstractInstance implements FlatLit {
|
||||
|
@ -11,7 +10,6 @@ public abstract class ColoredLitInstance extends AbstractInstance implements Fla
|
|||
public byte blue = (byte) 0xFF;
|
||||
public byte alpha = (byte) 0xFF;
|
||||
|
||||
public int overlay = OverlayTexture.NO_OVERLAY;
|
||||
public int light = 0;
|
||||
|
||||
public ColoredLitInstance(InstanceType<? extends ColoredLitInstance> type, InstanceHandle handle) {
|
||||
|
@ -49,9 +47,12 @@ public abstract class ColoredLitInstance extends AbstractInstance implements Fla
|
|||
return this;
|
||||
}
|
||||
|
||||
public ColoredLitInstance overlay(int overlay) {
|
||||
this.overlay = overlay;
|
||||
return this;
|
||||
public ColoredLitInstance color(float red, float green, float blue, float alpha) {
|
||||
return color((byte) (red * 255f), (byte) (green * 255f), (byte) (blue * 255f), (byte) (alpha * 255f));
|
||||
}
|
||||
|
||||
public ColoredLitInstance color(float red, float green, float blue) {
|
||||
return color((byte) (red * 255f), (byte) (green * 255f), (byte) (blue * 255f));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package dev.engine_room.flywheel.lib.instance;
|
||||
|
||||
import dev.engine_room.flywheel.api.instance.InstanceHandle;
|
||||
import dev.engine_room.flywheel.api.instance.InstanceType;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
|
||||
public abstract class ColoredLitOverlayInstance extends ColoredLitInstance {
|
||||
public int overlay = OverlayTexture.NO_OVERLAY;
|
||||
|
||||
public ColoredLitOverlayInstance(InstanceType<? extends ColoredLitOverlayInstance> type, InstanceHandle handle) {
|
||||
super(type, handle);
|
||||
}
|
||||
|
||||
public ColoredLitOverlayInstance overlay(int overlay) {
|
||||
this.overlay = overlay;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import dev.engine_room.flywheel.lib.transform.Rotate;
|
|||
import net.minecraft.core.Vec3i;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public class OrientedInstance extends ColoredLitInstance implements Rotate<OrientedInstance> {
|
||||
public class OrientedInstance extends ColoredLitOverlayInstance implements Rotate<OrientedInstance> {
|
||||
public float posX;
|
||||
public float posY;
|
||||
public float posZ;
|
||||
|
|
|
@ -13,7 +13,7 @@ import dev.engine_room.flywheel.api.instance.InstanceType;
|
|||
import dev.engine_room.flywheel.lib.transform.Transform;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
public class PosedInstance extends ColoredLitInstance implements Transform<PosedInstance> {
|
||||
public class PosedInstance extends ColoredLitOverlayInstance implements Transform<PosedInstance> {
|
||||
public final Matrix4f pose = new Matrix4f();
|
||||
public final Matrix3f normal = new Matrix3f();
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import dev.engine_room.flywheel.api.instance.InstanceType;
|
|||
import dev.engine_room.flywheel.lib.transform.Affine;
|
||||
import net.minecraft.core.Direction;
|
||||
|
||||
public class TransformedInstance extends ColoredLitInstance implements Affine<TransformedInstance> {
|
||||
public class TransformedInstance extends ColoredLitOverlayInstance implements Affine<TransformedInstance> {
|
||||
public final Matrix4f pose = new Matrix4f();
|
||||
|
||||
public TransformedInstance(InstanceType<? extends TransformedInstance> type, InstanceHandle handle) {
|
||||
|
|
|
@ -33,4 +33,32 @@ public final class DataPacker {
|
|||
public static float unpackNormI8(byte b) {
|
||||
return (float) b / 127f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack a float as an unsigned, normalized short.
|
||||
*/
|
||||
public static short packNormU16(float f) {
|
||||
return (short) (int) (Mth.clamp(f, 0.0f, 1.0f) * 65535);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack an unsigned, normalized short to a float.
|
||||
*/
|
||||
public static float unpackNormU16(short s) {
|
||||
return (float) (Short.toUnsignedInt(s)) / 65535f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack a float as a signed, normalized byte.
|
||||
*/
|
||||
public static short packNormI16(float f) {
|
||||
return (short) (Mth.clamp(f, -1.0f, 1.0f) * 32767);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack a signed, normalized byte to a float.
|
||||
*/
|
||||
public static float unpackNormI16(short s) {
|
||||
return (float) s / 32767f;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ public final class FireComponent implements EntityComponent {
|
|||
}
|
||||
|
||||
private record FireMesh(TextureAtlasSprite sprite) implements QuadMesh {
|
||||
private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0, 0.5f, 0, (float) (Math.sqrt(2) * 0.5));
|
||||
private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0, 0.5f, 0, Mth.SQRT_OF_TWO * 0.5f);
|
||||
|
||||
@Override
|
||||
public int vertexCount() {
|
||||
|
|
|
@ -79,12 +79,12 @@ public class FlwApiLinkImpl implements FlwApiLink {
|
|||
}
|
||||
|
||||
@Override
|
||||
public <T extends BlockEntity> void setVisualizer(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer) {
|
||||
public <T extends BlockEntity> void setVisualizer(BlockEntityType<T> type, @Nullable BlockEntityVisualizer<? super T> visualizer) {
|
||||
VisualizerRegistryImpl.setVisualizer(type, visualizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Entity> void setVisualizer(EntityType<T> type, EntityVisualizer<? super T> visualizer) {
|
||||
public <T extends Entity> void setVisualizer(EntityType<T> type, @Nullable EntityVisualizer<? super T> visualizer) {
|
||||
VisualizerRegistryImpl.setVisualizer(type, visualizer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,11 +23,11 @@ public final class VisualizerRegistryImpl {
|
|||
return ((EntityTypeExtension<T>) type).flywheel$getVisualizer();
|
||||
}
|
||||
|
||||
public static <T extends BlockEntity> void setVisualizer(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer) {
|
||||
public static <T extends BlockEntity> void setVisualizer(BlockEntityType<T> type, @Nullable BlockEntityVisualizer<? super T> visualizer) {
|
||||
((BlockEntityTypeExtension<T>) type).flywheel$setVisualizer(visualizer);
|
||||
}
|
||||
|
||||
public static <T extends Entity> void setVisualizer(EntityType<T> type, EntityVisualizer<? super T> visualizer) {
|
||||
public static <T extends Entity> void setVisualizer(EntityType<T> type, @Nullable EntityVisualizer<? super T> visualizer) {
|
||||
((EntityTypeExtension<T>) type).flywheel$setVisualizer(visualizer);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package dev.engine_room.vanillin;
|
||||
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Matrix4fc;
|
||||
|
||||
import dev.engine_room.flywheel.api.instance.InstanceHandle;
|
||||
import dev.engine_room.flywheel.api.instance.InstanceType;
|
||||
import dev.engine_room.flywheel.lib.instance.ColoredLitInstance;
|
||||
import dev.engine_room.flywheel.lib.math.DataPacker;
|
||||
import dev.engine_room.vanillin.text.BakedGlyphExtension;
|
||||
import dev.engine_room.vanillin.text.TextUtil;
|
||||
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
|
||||
|
||||
public class GlyphInstance extends ColoredLitInstance {
|
||||
// Skew x by 1 - 0.25 * y
|
||||
// Note that columns are written as rows.
|
||||
private static final Matrix4fc ITALIC_SKEW = new Matrix4f(1, 0, 0, 0, -0.25f, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1);
|
||||
|
||||
public final Matrix4f pose = new Matrix4f();
|
||||
|
||||
public int packedUs;
|
||||
public int packedVs;
|
||||
|
||||
public GlyphInstance(InstanceType<? extends GlyphInstance> type, InstanceHandle handle) {
|
||||
super(type, handle);
|
||||
}
|
||||
|
||||
public GlyphInstance setGlyph(BakedGlyph glyph, Matrix4fc initialPose, float x, float y, boolean italic) {
|
||||
var glyphExtension = TextUtil.getBakedGlyphExtension(glyph);
|
||||
setUvs(glyphExtension);
|
||||
|
||||
float left = glyphExtension.flywheel$left();
|
||||
float up = glyphExtension.flywheel$up();
|
||||
|
||||
pose.set(initialPose);
|
||||
pose.translate(x, y, 0.0f);
|
||||
|
||||
if (italic) {
|
||||
pose.mul(ITALIC_SKEW);
|
||||
}
|
||||
|
||||
pose.translate(left, up - 3.0f, 0.0f);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public GlyphInstance setEffect(BakedGlyph glyph, Matrix4fc initialPose, float x0, float y0, float x1, float y1, float depth) {
|
||||
var glyphExtension = TextUtil.getBakedGlyphExtension(glyph);
|
||||
setUvs(glyphExtension);
|
||||
|
||||
pose.set(initialPose);
|
||||
pose.translate(x0, y0, depth);
|
||||
pose.scale(x1 - x0, y1 - y0, 1.0f);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void setUvs(BakedGlyphExtension glyphExtension) {
|
||||
float u0 = glyphExtension.flywheel$u0();
|
||||
float u1 = glyphExtension.flywheel$u1();
|
||||
float v0 = glyphExtension.flywheel$v0();
|
||||
float v1 = glyphExtension.flywheel$v1();
|
||||
|
||||
// Need to make sure at least u0/v0 don't get their sign bit extended in the cast.
|
||||
// It causes u1/v1 to be completely saturated.
|
||||
packedUs = (Short.toUnsignedInt(DataPacker.packNormU16(u1)) << 16) | Short.toUnsignedInt(DataPacker.packNormU16(u0));
|
||||
packedVs = (Short.toUnsignedInt(DataPacker.packNormU16(v1)) << 16) | Short.toUnsignedInt(DataPacker.packNormU16(v0));
|
||||
}
|
||||
}
|
|
@ -1,59 +1,75 @@
|
|||
package dev.engine_room.vanillin.visuals;
|
||||
|
||||
import static dev.engine_room.flywheel.lib.visualization.SimpleBlockEntityVisualizer.builder;
|
||||
import static dev.engine_room.flywheel.lib.visualization.SimpleEntityVisualizer.builder;
|
||||
package dev.engine_room.vanillin;
|
||||
|
||||
import dev.engine_room.vanillin.config.BlockEntityVisualizerBuilder;
|
||||
import dev.engine_room.vanillin.config.Configurator;
|
||||
import dev.engine_room.vanillin.config.EntityVisualizerBuilder;
|
||||
import dev.engine_room.vanillin.visuals.*;
|
||||
import net.minecraft.client.model.geom.ModelLayers;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
|
||||
public class VanillaVisuals {
|
||||
public static final Configurator CONFIGURATOR = new Configurator();
|
||||
|
||||
public static void init() {
|
||||
builder(BlockEntityType.CHEST)
|
||||
.factory(ChestVisual::new)
|
||||
.apply();
|
||||
.apply(true);
|
||||
builder(BlockEntityType.ENDER_CHEST)
|
||||
.factory(ChestVisual::new)
|
||||
.apply();
|
||||
.apply(true);
|
||||
builder(BlockEntityType.TRAPPED_CHEST)
|
||||
.factory(ChestVisual::new)
|
||||
.apply();
|
||||
.apply(true);
|
||||
|
||||
builder(BlockEntityType.BELL)
|
||||
.factory(BellVisual::new)
|
||||
.apply();
|
||||
.apply(true);
|
||||
|
||||
builder(BlockEntityType.SHULKER_BOX)
|
||||
.factory(ShulkerBoxVisual::new)
|
||||
.apply();
|
||||
.apply(true);
|
||||
|
||||
builder(BlockEntityType.SIGN).factory(SignVisual::new)
|
||||
.apply(false);
|
||||
|
||||
builder(EntityType.CHEST_MINECART)
|
||||
.factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.CHEST_MINECART))
|
||||
.skipVanillaRender(MinecartVisual::shouldSkipRender)
|
||||
.apply();
|
||||
.apply(true);
|
||||
builder(EntityType.COMMAND_BLOCK_MINECART)
|
||||
.factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.COMMAND_BLOCK_MINECART))
|
||||
.skipVanillaRender(MinecartVisual::shouldSkipRender)
|
||||
.apply();
|
||||
.apply(true);
|
||||
builder(EntityType.FURNACE_MINECART)
|
||||
.factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.FURNACE_MINECART))
|
||||
.skipVanillaRender(MinecartVisual::shouldSkipRender)
|
||||
.apply();
|
||||
.apply(true);
|
||||
builder(EntityType.HOPPER_MINECART)
|
||||
.factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.HOPPER_MINECART))
|
||||
.skipVanillaRender(MinecartVisual::shouldSkipRender)
|
||||
.apply();
|
||||
.apply(true);
|
||||
builder(EntityType.MINECART)
|
||||
.factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.MINECART))
|
||||
.skipVanillaRender(MinecartVisual::shouldSkipRender)
|
||||
.apply();
|
||||
.apply(true);
|
||||
builder(EntityType.SPAWNER_MINECART)
|
||||
.factory((ctx, entity, partialTick) -> new MinecartVisual<>(ctx, entity, partialTick, ModelLayers.SPAWNER_MINECART))
|
||||
.skipVanillaRender(MinecartVisual::shouldSkipRender)
|
||||
.apply();
|
||||
.apply(true);
|
||||
builder(EntityType.TNT_MINECART)
|
||||
.factory(TntMinecartVisual::new)
|
||||
.skipVanillaRender(MinecartVisual::shouldSkipRender)
|
||||
.apply();
|
||||
.apply(true);
|
||||
}
|
||||
|
||||
public static <T extends BlockEntity> BlockEntityVisualizerBuilder<T> builder(BlockEntityType<T> type) {
|
||||
return new BlockEntityVisualizerBuilder<>(CONFIGURATOR, type);
|
||||
}
|
||||
|
||||
public static <T extends Entity> EntityVisualizerBuilder<T> builder(EntityType<T> type) {
|
||||
return new EntityVisualizerBuilder<>(CONFIGURATOR, type);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,17 @@
|
|||
package dev.engine_room.vanillin;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public class Vanillin {
|
||||
public static final String ID = "vanillin";
|
||||
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(ID);
|
||||
public static final Logger CONFIG_LOGGER = LoggerFactory.getLogger(ID + "/config");
|
||||
|
||||
public static ResourceLocation rl(String path) {
|
||||
return new ResourceLocation(ID, path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package dev.engine_room.vanillin;
|
||||
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import dev.engine_room.flywheel.api.instance.InstanceType;
|
||||
import dev.engine_room.flywheel.api.layout.FloatRepr;
|
||||
import dev.engine_room.flywheel.api.layout.LayoutBuilder;
|
||||
import dev.engine_room.flywheel.lib.instance.SimpleInstanceType;
|
||||
import dev.engine_room.flywheel.lib.util.ExtraMemoryOps;
|
||||
|
||||
public class VanillinInstanceTypes {
|
||||
public static final InstanceType<GlyphInstance> GLYPH = SimpleInstanceType.builder(GlyphInstance::new)
|
||||
.layout(LayoutBuilder.create()
|
||||
.matrix("pose", FloatRepr.FLOAT, 4)
|
||||
.vector("u0u1v0v1", FloatRepr.NORMALIZED_UNSIGNED_SHORT, 4)
|
||||
.vector("color", FloatRepr.NORMALIZED_UNSIGNED_BYTE, 4)
|
||||
.vector("light", FloatRepr.UNSIGNED_SHORT, 2)
|
||||
.build())
|
||||
.writer((ptr, instance) -> {
|
||||
ExtraMemoryOps.putMatrix4f(ptr, instance.pose);
|
||||
ExtraMemoryOps.put2x16(ptr + 64, instance.packedUs);
|
||||
ExtraMemoryOps.put2x16(ptr + 68, instance.packedVs);
|
||||
MemoryUtil.memPutByte(ptr + 72, instance.red);
|
||||
MemoryUtil.memPutByte(ptr + 73, instance.green);
|
||||
MemoryUtil.memPutByte(ptr + 74, instance.blue);
|
||||
MemoryUtil.memPutByte(ptr + 75, instance.alpha);
|
||||
ExtraMemoryOps.put2x16(ptr + 76, instance.light);
|
||||
})
|
||||
.vertexShader(Vanillin.rl("instance/glyph.vert"))
|
||||
.cullShader(Vanillin.rl("instance/cull/glyph.glsl"))
|
||||
.build();
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package dev.engine_room.vanillin.config;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import dev.engine_room.flywheel.lib.visualization.SimpleBlockEntityVisualizer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
|
||||
public class BlockEntityVisualizerBuilder<T extends BlockEntity> {
|
||||
private final Configurator configurator;
|
||||
private final BlockEntityType<T> type;
|
||||
@Nullable
|
||||
private SimpleBlockEntityVisualizer.Factory<T> visualFactory;
|
||||
@Nullable
|
||||
private Predicate<T> skipVanillaRender;
|
||||
|
||||
public BlockEntityVisualizerBuilder(Configurator configurator, BlockEntityType<T> type) {
|
||||
this.configurator = configurator;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visual factory for the block entity.
|
||||
*
|
||||
* @param visualFactory The visual factory.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public BlockEntityVisualizerBuilder<T> factory(SimpleBlockEntityVisualizer.Factory<T> visualFactory) {
|
||||
this.visualFactory = visualFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a predicate to determine whether to skip rendering with the vanilla {@link BlockEntityRenderer}.
|
||||
*
|
||||
* @param skipVanillaRender The predicate.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public BlockEntityVisualizerBuilder<T> skipVanillaRender(Predicate<T> skipVanillaRender) {
|
||||
this.skipVanillaRender = skipVanillaRender;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a predicate to never skip rendering with the vanilla {@link BlockEntityRenderer}.
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public BlockEntityVisualizerBuilder<T> neverSkipVanillaRender() {
|
||||
this.skipVanillaRender = blockEntity -> false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the block entity visualizer and sets it for the block entity type.
|
||||
*
|
||||
* @return The block entity visualizer.
|
||||
*/
|
||||
public SimpleBlockEntityVisualizer<T> apply(boolean enabledByDefault) {
|
||||
Objects.requireNonNull(visualFactory, "Visual factory cannot be null!");
|
||||
if (skipVanillaRender == null) {
|
||||
skipVanillaRender = blockEntity -> true;
|
||||
}
|
||||
|
||||
SimpleBlockEntityVisualizer<T> visualizer = new SimpleBlockEntityVisualizer<>(visualFactory, skipVanillaRender);
|
||||
configurator.register(type, visualizer, enabledByDefault);
|
||||
|
||||
return visualizer;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package dev.engine_room.vanillin.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import dev.engine_room.flywheel.api.visualization.BlockEntityVisualizer;
|
||||
import dev.engine_room.flywheel.api.visualization.EntityVisualizer;
|
||||
import dev.engine_room.flywheel.api.visualization.VisualizerRegistry;
|
||||
import dev.engine_room.vanillin.Vanillin;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
|
||||
public class Configurator {
|
||||
public final Map<BlockEntityType<?>, ConfiguredBlockEntity<?>> blockEntities = new HashMap<>();
|
||||
public final Map<EntityType<?>, ConfiguredEntity<?>> entities = new HashMap<>();
|
||||
|
||||
public <T extends BlockEntity> void register(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
|
||||
blockEntities.put(type, new ConfiguredBlockEntity<>(type, visualizer, enabledByDefault));
|
||||
}
|
||||
|
||||
public <T extends Entity> void register(EntityType<T> type, EntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
|
||||
entities.put(type, new ConfiguredEntity<>(type, visualizer, enabledByDefault));
|
||||
}
|
||||
|
||||
public static abstract class ConfiguredVisual {
|
||||
private final boolean enabledByDefault;
|
||||
|
||||
protected ConfiguredVisual(boolean enabledByDefault) {
|
||||
this.enabledByDefault = enabledByDefault;
|
||||
}
|
||||
|
||||
public void set(VisualConfigValue configValue, @Nullable List<VisualOverride> overrides) {
|
||||
if (configValue == VisualConfigValue.DISABLE) {
|
||||
disable();
|
||||
} else if (configValue == VisualConfigValue.FORCE_ENABLE) {
|
||||
enable();
|
||||
maybeWarnEnabledDespiteOverrides(overrides);
|
||||
} else if (configValue == VisualConfigValue.DEFAULT) {
|
||||
if (disableAndWarnDueToOverrides(overrides)) {
|
||||
disable();
|
||||
} else {
|
||||
if (enabledByDefault) {
|
||||
enable();
|
||||
} else {
|
||||
disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean disableAndWarnDueToOverrides(@Nullable List<VisualOverride> overrides) {
|
||||
if (overrides == null || overrides.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var modIds = disablingModIds(overrides);
|
||||
|
||||
if (modIds.isEmpty()) {
|
||||
return false;
|
||||
} else {
|
||||
Vanillin.CONFIG_LOGGER.warn("Disabling {} visual due to overrides from mods: {}", configKey(), String.join(", ", modIds));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeWarnEnabledDespiteOverrides(@Nullable List<VisualOverride> overrides) {
|
||||
if (overrides == null || overrides.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var modIds = disablingModIds(overrides);
|
||||
|
||||
if (!modIds.isEmpty()) {
|
||||
Vanillin.CONFIG_LOGGER.warn("Enabling {} visual despite overrides from mods: {}", configKey(), String.join(", ", modIds));
|
||||
}
|
||||
}
|
||||
|
||||
public abstract String configKey();
|
||||
|
||||
protected abstract void enable();
|
||||
|
||||
protected abstract void disable();
|
||||
|
||||
private static List<String> disablingModIds(List<VisualOverride> overrides) {
|
||||
List<String> out = new ArrayList<>();
|
||||
|
||||
for (VisualOverride override : overrides) {
|
||||
if (override.value() == VisualOverrideValue.DISABLE) {
|
||||
out.add(override.modId());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConfiguredBlockEntity<T extends BlockEntity> extends ConfiguredVisual {
|
||||
public final BlockEntityType<T> type;
|
||||
public final BlockEntityVisualizer<? super T> visualizer;
|
||||
|
||||
private ConfiguredBlockEntity(BlockEntityType<T> type, BlockEntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
|
||||
super(enabledByDefault);
|
||||
this.type = type;
|
||||
this.visualizer = visualizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String configKey() {
|
||||
return BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(type).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enable() {
|
||||
VisualizerRegistry.setVisualizer(type, visualizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void disable() {
|
||||
VisualizerRegistry.setVisualizer(type, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConfiguredEntity<T extends Entity> extends ConfiguredVisual {
|
||||
public final EntityType<T> type;
|
||||
public final EntityVisualizer<? super T> visualizer;
|
||||
|
||||
private ConfiguredEntity(EntityType<T> type, EntityVisualizer<? super T> visualizer, boolean enabledByDefault) {
|
||||
super(enabledByDefault);
|
||||
this.type = type;
|
||||
this.visualizer = visualizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String configKey() {
|
||||
return BuiltInRegistries.ENTITY_TYPE.getKey(type).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enable() {
|
||||
VisualizerRegistry.setVisualizer(type, visualizer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void disable() {
|
||||
VisualizerRegistry.setVisualizer(type, null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package dev.engine_room.vanillin.config;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import dev.engine_room.flywheel.lib.visualization.SimpleEntityVisualizer;
|
||||
import net.minecraft.client.renderer.entity.EntityRenderer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
|
||||
/**
|
||||
* An object to configure the visualizer for an entity.
|
||||
*
|
||||
* @param <T> The type of the entity.
|
||||
*/
|
||||
public final class EntityVisualizerBuilder<T extends Entity> {
|
||||
private final Configurator configurator;
|
||||
private final EntityType<T> type;
|
||||
@Nullable
|
||||
private SimpleEntityVisualizer.Factory<T> visualFactory;
|
||||
@Nullable
|
||||
private Predicate<T> skipVanillaRender;
|
||||
|
||||
public EntityVisualizerBuilder(Configurator configurator, EntityType<T> type) {
|
||||
this.configurator = configurator;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visual factory for the entity.
|
||||
*
|
||||
* @param visualFactory The visual factory.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public EntityVisualizerBuilder<T> factory(SimpleEntityVisualizer.Factory<T> visualFactory) {
|
||||
this.visualFactory = visualFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a predicate to determine whether to skip rendering with the vanilla {@link EntityRenderer}.
|
||||
*
|
||||
* @param skipVanillaRender The predicate.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public EntityVisualizerBuilder<T> skipVanillaRender(Predicate<T> skipVanillaRender) {
|
||||
this.skipVanillaRender = skipVanillaRender;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a predicate to always skip rendering with the vanilla {@link EntityRenderer}.
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public EntityVisualizerBuilder<T> neverSkipVanillaRender() {
|
||||
this.skipVanillaRender = entity -> false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the entity visualizer and sets it for the entity type.
|
||||
*
|
||||
* @return The entity visualizer.
|
||||
*/
|
||||
public SimpleEntityVisualizer<T> apply(boolean enabledByDefault) {
|
||||
Objects.requireNonNull(visualFactory, "Visual factory cannot be null!");
|
||||
if (skipVanillaRender == null) {
|
||||
skipVanillaRender = entity -> true;
|
||||
}
|
||||
|
||||
SimpleEntityVisualizer<T> visualizer = new SimpleEntityVisualizer<>(visualFactory, skipVanillaRender);
|
||||
configurator.register(type, visualizer, enabledByDefault);
|
||||
|
||||
return visualizer;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package dev.engine_room.vanillin.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public record ModOverrides(Map<String, List<VisualOverride>> blockEntities, Map<String, List<VisualOverride>> entities) {
|
||||
public ModOverrides(List<VisualOverride> blockEntities, List<VisualOverride> entities) {
|
||||
this(sort(blockEntities), sort(entities));
|
||||
}
|
||||
|
||||
public static Map<String, List<VisualOverride>> sort(List<VisualOverride> list) {
|
||||
return list.stream().collect(Collectors.groupingBy(VisualOverride::name));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package dev.engine_room.vanillin.config;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public enum VisualConfigValue {
|
||||
@SerializedName("default")
|
||||
DEFAULT,
|
||||
@SerializedName("disable")
|
||||
DISABLE,
|
||||
@SerializedName("force_enable")
|
||||
FORCE_ENABLE,
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package dev.engine_room.vanillin.config;
|
||||
|
||||
public record VisualOverride(String name, String modId, VisualOverrideValue value) {
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package dev.engine_room.vanillin.config;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public enum VisualOverrideValue {
|
||||
DEFAULT,
|
||||
DISABLE,
|
||||
;
|
||||
|
||||
@Nullable
|
||||
public static VisualOverrideValue parse(String string) {
|
||||
if (string.equals("default")) {
|
||||
return DEFAULT;
|
||||
} else if (string.equals("disable")) {
|
||||
return DISABLE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package dev.engine_room.vanillin.mixin.text;
|
||||
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
import dev.engine_room.vanillin.text.BakedGlyphExtension;
|
||||
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
@Mixin(BakedGlyph.class)
|
||||
public class BakedGlyphMixin implements BakedGlyphExtension {
|
||||
@Shadow
|
||||
@Final
|
||||
private float u0;
|
||||
@Shadow
|
||||
@Final
|
||||
private float u1;
|
||||
@Shadow
|
||||
@Final
|
||||
private float v0;
|
||||
@Shadow
|
||||
@Final
|
||||
private float v1;
|
||||
@Shadow
|
||||
@Final
|
||||
private float left;
|
||||
@Shadow
|
||||
@Final
|
||||
private float right;
|
||||
@Shadow
|
||||
@Final
|
||||
private float up;
|
||||
@Shadow
|
||||
@Final
|
||||
private float down;
|
||||
|
||||
@Unique
|
||||
private ResourceLocation flywheel$texture;
|
||||
|
||||
@Override
|
||||
public float flywheel$u0() {
|
||||
return u0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float flywheel$u1() {
|
||||
return u1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float flywheel$v0() {
|
||||
return v0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float flywheel$v1() {
|
||||
return v1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float flywheel$left() {
|
||||
return left;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float flywheel$right() {
|
||||
return right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float flywheel$up() {
|
||||
return up;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float flywheel$down() {
|
||||
return down;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation flywheel$texture() {
|
||||
return flywheel$texture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flywheel$texture(ResourceLocation location) {
|
||||
flywheel$texture = location;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package dev.engine_room.vanillin.mixin.text;
|
||||
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
|
||||
import net.minecraft.client.gui.font.CodepointMap;
|
||||
|
||||
@Mixin(CodepointMap.class)
|
||||
public class CodePointMapMixin<T> {
|
||||
@Unique
|
||||
private final Object flywheel$lock = new Object();
|
||||
|
||||
@WrapMethod(method = "clear")
|
||||
private void flywheel$wrapClearAsSynchronized(Operation<Void> original) {
|
||||
synchronized (flywheel$lock) {
|
||||
original.call();
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "get")
|
||||
private T flywheel$wrapGetAsSynchronized(int index, Operation<T> original) {
|
||||
synchronized (flywheel$lock) {
|
||||
return original.call(index);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "put")
|
||||
private T flywheel$wrapPutAsSynchronized(int index, T value, Operation<T> original) {
|
||||
synchronized (flywheel$lock) {
|
||||
return original.call(index, value);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "computeIfAbsent")
|
||||
private T flywheel$wrapComputeIfAbsentAsSynchronized(int index, IntFunction<T> valueIfAbsentGetter, Operation<T> original) {
|
||||
synchronized (flywheel$lock) {
|
||||
return original.call(index, valueIfAbsentGetter);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "remove")
|
||||
private T flywheel$wrapRemoveAsSynchronized(int index, Operation<T> original) {
|
||||
synchronized (flywheel$lock) {
|
||||
return original.call(index);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapMethod(method = "forEach")
|
||||
private void flywheel$wrapForEachAsSynchronized(CodepointMap.Output<T> output, Operation<Void> original) {
|
||||
synchronized (flywheel$lock) {
|
||||
original.call(output);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package dev.engine_room.vanillin.mixin.text;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.font.FontSet;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
@Mixin(Font.class)
|
||||
public interface FontAccessor {
|
||||
@Accessor("filterFishyGlyphs")
|
||||
boolean flywheel$getFilterFishyGlyphs();
|
||||
|
||||
@Invoker("getFontSet")
|
||||
FontSet flywheel$getFontSet(ResourceLocation fontLocation);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package dev.engine_room.vanillin.mixin.text;
|
||||
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
|
||||
import dev.engine_room.vanillin.text.FontTextureExtension;
|
||||
import net.minecraft.client.gui.font.FontSet;
|
||||
import net.minecraft.client.gui.font.FontTexture;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.RandomSource;
|
||||
|
||||
@Mixin(FontSet.class)
|
||||
public abstract class FontSetMixin {
|
||||
// Replace serial random with thread-local random
|
||||
@Shadow
|
||||
@Final
|
||||
private static RandomSource RANDOM = RandomSource.createNewThreadLocalInstance();
|
||||
|
||||
@ModifyExpressionValue(method = "stitch", at = @At(value = "NEW", target = "net/minecraft/client/gui/font/FontTexture"))
|
||||
private FontTexture flywheel$setNameAfterCreate(FontTexture original, @Local ResourceLocation name) {
|
||||
// Forward the name to the FontTexture so we can forward the name to the BakedGlyphs it creates.
|
||||
// We need to know that to determine which Material to use when actually setting up instances.
|
||||
((FontTextureExtension) original).flywheel$setName(name);
|
||||
return original;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package dev.engine_room.vanillin.mixin.text;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
@Mixin(targets = "net.minecraft.client.gui.font.FontTexture$Node")
|
||||
public interface FontTexture$NodeAccessor {
|
||||
@Accessor("x")
|
||||
int flywheel$getX();
|
||||
|
||||
@Accessor("y")
|
||||
int flywheel$getY();
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package dev.engine_room.vanillin.mixin.text;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Coerce;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import com.llamalad7.mixinextras.sugar.Share;
|
||||
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
|
||||
import com.mojang.blaze3d.font.SheetGlyphInfo;
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
|
||||
import dev.engine_room.vanillin.text.BakedGlyphExtension;
|
||||
import dev.engine_room.vanillin.text.FontTextureExtension;
|
||||
import dev.engine_room.vanillin.text.FontTextureUpload;
|
||||
import net.minecraft.client.gui.font.FontTexture;
|
||||
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
|
||||
import net.minecraft.client.renderer.texture.AbstractTexture;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
@Mixin(FontTexture.class)
|
||||
public abstract class FontTextureMixin extends AbstractTexture implements FontTextureExtension {
|
||||
@Unique
|
||||
private final List<FontTextureUpload> flywheel$uploads = new ArrayList<>();
|
||||
@Unique
|
||||
private boolean flywheel$flushScheduled = false;
|
||||
|
||||
@Unique
|
||||
private ResourceLocation flywheel$name;
|
||||
|
||||
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/font/FontTexture;getId()I"))
|
||||
private int flywheel$skipGetId(FontTexture instance, Operation<Integer> original) {
|
||||
// getId lazily creates the texture id, which is good,
|
||||
// but it doesn't check for the render thread, which explodes.
|
||||
if (RenderSystem.isOnRenderThreadOrInit()) {
|
||||
return original.call(instance);
|
||||
}
|
||||
// We'll call getId manually in the recorded render call below.
|
||||
return 0;
|
||||
}
|
||||
|
||||
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/platform/TextureUtil;prepareImage(Lcom/mojang/blaze3d/platform/NativeImage$InternalGlFormat;III)V"))
|
||||
private void flywheel$skipPrepareImage(NativeImage.InternalGlFormat arg, int i, int j, int k, Operation<Void> original) {
|
||||
if (RenderSystem.isOnRenderThreadOrInit()) {
|
||||
original.call(arg, i, j, k);
|
||||
} else {
|
||||
RenderSystem.recordRenderCall(() -> original.call(arg, getId(), j, k));
|
||||
}
|
||||
}
|
||||
|
||||
@WrapWithCondition(method = "add", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/font/FontTexture;bind()V"))
|
||||
private boolean flywheel$onlyOnRenderThreadOrInitBindAndUpload(FontTexture instance) {
|
||||
return RenderSystem.isOnRenderThreadOrInit();
|
||||
}
|
||||
|
||||
@WrapWithCondition(method = "add", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/font/SheetGlyphInfo;upload(II)V"))
|
||||
private boolean flywheel$onlyOnRenderThreadOrInitBindAndUpload2(SheetGlyphInfo instance, int x, int y) {
|
||||
return RenderSystem.isOnRenderThreadOrInit();
|
||||
}
|
||||
|
||||
@WrapOperation(method = "add", at = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/font/FontTexture$Node;x:I", ordinal = 0))
|
||||
private int flywheel$shareNode(@Coerce Object instance, Operation<Integer> original, @Share("node") LocalRef<Object> node) {
|
||||
node.set(instance);
|
||||
return original.call(instance);
|
||||
}
|
||||
|
||||
@Inject(method = "add", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/font/SheetGlyphInfo;upload(II)V", shift = At.Shift.AFTER))
|
||||
private void flywheel$uploadOrFlush(SheetGlyphInfo glyphInfo, CallbackInfoReturnable<BakedGlyph> cir, @Share("node") LocalRef<Object> node) {
|
||||
FontTexture$NodeAccessor accessor = ((FontTexture$NodeAccessor) node.get());
|
||||
|
||||
// Shove all the uploads into a list to be processed as a batch.
|
||||
// Saves a lot of lambda allocations that would be spent binding the same texture over and over.
|
||||
flywheel$uploads.add(new FontTextureUpload(glyphInfo, accessor.flywheel$getX(), accessor.flywheel$getY()));
|
||||
|
||||
if (!flywheel$flushScheduled) {
|
||||
RenderSystem.recordRenderCall(this::flywheel$flush);
|
||||
flywheel$flushScheduled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ModifyExpressionValue(method = "add", at = @At(value = "NEW", target = "net/minecraft/client/gui/font/glyphs/BakedGlyph"))
|
||||
private BakedGlyph flywheel$setGlyphExtensionName(BakedGlyph original) {
|
||||
((BakedGlyphExtension) original).flywheel$texture(flywheel$name);
|
||||
return original;
|
||||
}
|
||||
|
||||
@Unique
|
||||
public void flywheel$flush() {
|
||||
this.bind();
|
||||
for (FontTextureUpload upload : flywheel$uploads) {
|
||||
upload.info()
|
||||
.upload(upload.x(), upload.y());
|
||||
}
|
||||
|
||||
flywheel$uploads.clear();
|
||||
|
||||
flywheel$flushScheduled = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flywheel$setName(ResourceLocation name) {
|
||||
flywheel$name = name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package dev.engine_room.vanillin.text;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public interface BakedGlyphExtension {
|
||||
float flywheel$u0();
|
||||
|
||||
float flywheel$u1();
|
||||
|
||||
float flywheel$v0();
|
||||
|
||||
float flywheel$v1();
|
||||
|
||||
float flywheel$left();
|
||||
|
||||
float flywheel$right();
|
||||
|
||||
float flywheel$up();
|
||||
|
||||
float flywheel$down();
|
||||
|
||||
ResourceLocation flywheel$texture();
|
||||
|
||||
void flywheel$texture(ResourceLocation location);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package dev.engine_room.vanillin.text;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public interface FontTextureExtension {
|
||||
void flywheel$setName(ResourceLocation name);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package dev.engine_room.vanillin.text;
|
||||
|
||||
import com.mojang.blaze3d.font.SheetGlyphInfo;
|
||||
|
||||
/**
|
||||
* For use in {@link dev.engine_room.flywheel.impl.mixin.text.FontTextureMixin}
|
||||
* to batch glyph uploads when they're created in a flywheel worker thread.
|
||||
*/
|
||||
public record FontTextureUpload(SheetGlyphInfo info, int x, int y) {
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package dev.engine_room.vanillin.text;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector2fc;
|
||||
|
||||
public record SimpleTextLayer(GlyphPattern pattern, GlyphMaterial material, GlyphColor color, Vector2fc offset, int bias) implements TextLayer {
|
||||
public static class Builder {
|
||||
private static final Vector2fc NO_OFFSET = new Vector2f();
|
||||
|
||||
@Nullable
|
||||
private GlyphPattern pattern;
|
||||
@Nullable
|
||||
private GlyphMaterial material;
|
||||
@Nullable
|
||||
private GlyphColor color;
|
||||
private Vector2fc offset = NO_OFFSET;
|
||||
private int bias = 0;
|
||||
|
||||
public Builder pattern(GlyphPattern pattern) {
|
||||
this.pattern = pattern;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder material(GlyphMaterial material) {
|
||||
this.material = material;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder color(GlyphColor color) {
|
||||
this.color = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder offset(Vector2fc offset) {
|
||||
this.offset = offset;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder offset(float offsetX, float offsetY) {
|
||||
offset = new Vector2f(offsetX, offsetY);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder bias(int bias) {
|
||||
this.bias = bias;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SimpleTextLayer build() {
|
||||
Objects.requireNonNull(pattern);
|
||||
Objects.requireNonNull(material);
|
||||
Objects.requireNonNull(color);
|
||||
|
||||
return new SimpleTextLayer(pattern, material, color, offset, bias);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package dev.engine_room.vanillin.text;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector2fc;
|
||||
|
||||
import dev.engine_room.flywheel.api.material.DepthTest;
|
||||
import dev.engine_room.flywheel.api.material.Material;
|
||||
import dev.engine_room.flywheel.api.material.Transparency;
|
||||
import dev.engine_room.flywheel.api.material.WriteMask;
|
||||
import dev.engine_room.flywheel.lib.material.CutoutShaders;
|
||||
import dev.engine_room.flywheel.lib.material.FogShaders;
|
||||
import dev.engine_room.flywheel.lib.material.SimpleMaterial;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.network.chat.TextColor;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.FastColor;
|
||||
|
||||
public interface TextLayer {
|
||||
/**
|
||||
* The pattern of individual glyphs.
|
||||
*
|
||||
* @return A GlyphPattern.
|
||||
*/
|
||||
GlyphPattern pattern();
|
||||
|
||||
/**
|
||||
* A mapping from texture ResourceLocations to Flywheel materials.
|
||||
*
|
||||
* @return A GlyphMaterial.
|
||||
*/
|
||||
GlyphMaterial material();
|
||||
|
||||
/**
|
||||
* A mapping from text styles to ARGB colors.
|
||||
*
|
||||
* @return A GlyphColor.
|
||||
*/
|
||||
GlyphColor color();
|
||||
|
||||
/**
|
||||
* The offset of text in this layer.
|
||||
*
|
||||
* @return The offset.
|
||||
*/
|
||||
Vector2fc offset();
|
||||
|
||||
/**
|
||||
* The instancer bias for this layer.
|
||||
*
|
||||
* @return The bias.
|
||||
*/
|
||||
int bias();
|
||||
|
||||
// TODO: probably just convert this to Iterable<Vector2fc>
|
||||
@FunctionalInterface
|
||||
interface GlyphPattern {
|
||||
/**
|
||||
* The pattern for a single glyph with no offset.
|
||||
*/
|
||||
GlyphPattern SINGLE = out -> out.accept(new Vector2f(0, 0));
|
||||
|
||||
/**
|
||||
* The pattern for an 8x outline as used by glowing text on signs.
|
||||
*/
|
||||
GlyphPattern OUTLINE = out -> {
|
||||
for (int x = -1; x <= 1; ++x) {
|
||||
for (int y = -1; y <= 1; ++y) {
|
||||
if (x == 0 && y == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
out.accept(new Vector2f(x, y));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an arbitrary amount of glyphs. Each accepted vector represents
|
||||
* the offset of a new glyph quad.
|
||||
*
|
||||
* @param out The consumer to accept the offset of a new glyph quad
|
||||
*/
|
||||
void addGlyphs(Consumer<Vector2fc> out);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface GlyphMaterial {
|
||||
// FIXME: account for intensity
|
||||
GlyphMaterial NORMAL = texture -> SimpleMaterial.builder()
|
||||
.cutout(CutoutShaders.ONE_TENTH)
|
||||
.texture(texture)
|
||||
.mipmap(false)
|
||||
.transparency(Transparency.TRANSLUCENT)
|
||||
.diffuse(false)
|
||||
.build();
|
||||
|
||||
GlyphMaterial SEE_THROUGH = texture -> SimpleMaterial.builder()
|
||||
.fog(FogShaders.NONE)
|
||||
.cutout(CutoutShaders.ONE_TENTH)
|
||||
.texture(texture)
|
||||
.mipmap(false)
|
||||
.depthTest(DepthTest.ALWAYS)
|
||||
.transparency(Transparency.TRANSLUCENT)
|
||||
.writeMask(WriteMask.COLOR)
|
||||
.diffuse(false)
|
||||
.build();
|
||||
|
||||
GlyphMaterial POLYGON_OFFSET = texture -> SimpleMaterial.builder()
|
||||
.cutout(CutoutShaders.ONE_TENTH)
|
||||
.texture(texture)
|
||||
.mipmap(false)
|
||||
.polygonOffset(true)
|
||||
.transparency(Transparency.TRANSLUCENT)
|
||||
.diffuse(false)
|
||||
.build();
|
||||
|
||||
static GlyphMaterial fromDisplayMode(Font.DisplayMode displayMode) {
|
||||
return switch (displayMode) {
|
||||
case NORMAL -> NORMAL;
|
||||
case SEE_THROUGH -> SEE_THROUGH;
|
||||
case POLYGON_OFFSET -> POLYGON_OFFSET;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Flywheel material for the given glyph texture.
|
||||
*
|
||||
* @param texture The texture to use.
|
||||
* @return A material.
|
||||
*/
|
||||
Material create(ResourceLocation texture);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface GlyphColor {
|
||||
/**
|
||||
* Default to the given color if no color is specified in the style.
|
||||
*
|
||||
* @param color The ARGB color to default to.
|
||||
* @return A new GlyphColor.
|
||||
*/
|
||||
static GlyphColor defaultTo(int color, float dimFactor) {
|
||||
int finalColor;
|
||||
if (dimFactor != 1.0f) {
|
||||
finalColor = FastColor.ARGB32.color(
|
||||
FastColor.ARGB32.alpha(color),
|
||||
(int) (FastColor.ARGB32.red(color) * dimFactor),
|
||||
(int) (FastColor.ARGB32.green(color) * dimFactor),
|
||||
(int) (FastColor.ARGB32.blue(color) * dimFactor)
|
||||
);
|
||||
} else {
|
||||
finalColor = color;
|
||||
}
|
||||
|
||||
return textColor -> {
|
||||
if (textColor != null) {
|
||||
int textColorArgb = textColor.getValue();
|
||||
if (dimFactor != 1.0f) {
|
||||
return FastColor.ARGB32.color(
|
||||
FastColor.ARGB32.alpha(finalColor),
|
||||
(int) (FastColor.ARGB32.red(textColorArgb) * dimFactor),
|
||||
(int) (FastColor.ARGB32.green(textColorArgb) * dimFactor),
|
||||
(int) (FastColor.ARGB32.blue(textColorArgb) * dimFactor)
|
||||
);
|
||||
} else {
|
||||
return (finalColor & 0xFF000000) | (textColorArgb & 0xFFFFFF);
|
||||
}
|
||||
}
|
||||
return finalColor;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Default to the given color if no color is specified in the style.
|
||||
*
|
||||
* @param color The ARGB color to default to.
|
||||
* @return A new GlyphColor.
|
||||
*/
|
||||
static GlyphColor defaultTo(int color) {
|
||||
return defaultTo(color, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Always use the given color, regardless of the style.
|
||||
*
|
||||
* @param color The ARGB color to use.
|
||||
* @return A new GlyphColor.
|
||||
*/
|
||||
static GlyphColor always(int color) {
|
||||
return textColor -> color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the color to be fully opaque if it's very close to having 0 alpha.
|
||||
*
|
||||
* @param color The ARGB color to adjust.
|
||||
* @return The adjusted color.
|
||||
*/
|
||||
static int adjustColor(int color) {
|
||||
if ((color & 0xFC000000) == 0) {
|
||||
return color | 0xFF000000;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a nullable text color to a color.
|
||||
*
|
||||
* @param textColor The color of the text to colorize.
|
||||
* @return The color to use, in ARGB format.
|
||||
*/
|
||||
int color(@Nullable TextColor textColor);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package dev.engine_room.vanillin.text;
|
||||
|
||||
import net.minecraft.client.gui.Font;
|
||||
|
||||
public final class TextLayers {
|
||||
public static TextLayer normal(int color, Font.DisplayMode displayMode, int bias) {
|
||||
return new SimpleTextLayer.Builder().pattern(TextLayer.GlyphPattern.SINGLE)
|
||||
.material(TextLayer.GlyphMaterial.fromDisplayMode(displayMode))
|
||||
.color(TextLayer.GlyphColor.defaultTo(TextLayer.GlyphColor.adjustColor(color)))
|
||||
.bias(bias)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static TextLayer normal(int color, Font.DisplayMode displayMode) {
|
||||
return normal(color, displayMode, 0);
|
||||
}
|
||||
|
||||
public static TextLayer dropShadow(int color, Font.DisplayMode displayMode, int bias) {
|
||||
return new SimpleTextLayer.Builder().pattern(TextLayer.GlyphPattern.SINGLE)
|
||||
.material(TextLayer.GlyphMaterial.fromDisplayMode(displayMode))
|
||||
.color(TextLayer.GlyphColor.defaultTo(TextLayer.GlyphColor.adjustColor(color), 0.25f))
|
||||
.offset(1, 1)
|
||||
.bias(bias)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static TextLayer dropShadow(int color, Font.DisplayMode displayMode) {
|
||||
return dropShadow(color, displayMode, 0);
|
||||
}
|
||||
|
||||
public static TextLayer outline(int color, int bias) {
|
||||
return new SimpleTextLayer.Builder().pattern(TextLayer.GlyphPattern.OUTLINE)
|
||||
.material(TextLayer.GlyphMaterial.NORMAL)
|
||||
.color(TextLayer.GlyphColor.always(TextLayer.GlyphColor.adjustColor(color)))
|
||||
.bias(bias)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static TextLayer outline(int color) {
|
||||
return outline(color, 0);
|
||||
}
|
||||
|
||||
private TextLayers() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package dev.engine_room.vanillin.text;
|
||||
|
||||
import dev.engine_room.vanillin.mixin.text.FontAccessor;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.font.FontSet;
|
||||
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public class TextUtil {
|
||||
public static FontSet getFontSet(Font font, ResourceLocation loc) {
|
||||
return ((FontAccessor) font).flywheel$getFontSet(loc);
|
||||
}
|
||||
|
||||
public static boolean getFilterFishyGlyphs(Font font) {
|
||||
return ((FontAccessor) font).flywheel$getFilterFishyGlyphs();
|
||||
}
|
||||
|
||||
public static BakedGlyphExtension getBakedGlyphExtension(BakedGlyph glyph) {
|
||||
return (BakedGlyphExtension) glyph;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,379 @@
|
|||
package dev.engine_room.vanillin.text;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnknownNullability;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector2fc;
|
||||
import org.joml.Vector4f;
|
||||
import org.joml.Vector4fc;
|
||||
|
||||
import com.mojang.blaze3d.font.GlyphInfo;
|
||||
|
||||
import dev.engine_room.flywheel.api.instance.InstancerProvider;
|
||||
import dev.engine_room.flywheel.api.model.Mesh;
|
||||
import dev.engine_room.flywheel.api.model.Model;
|
||||
import dev.engine_room.flywheel.api.vertex.MutableVertexList;
|
||||
import dev.engine_room.flywheel.lib.model.QuadMesh;
|
||||
import dev.engine_room.flywheel.lib.model.SingleMeshModel;
|
||||
import dev.engine_room.flywheel.lib.util.RendererReloadCache;
|
||||
import dev.engine_room.flywheel.lib.visual.util.SmartRecycler;
|
||||
import dev.engine_room.vanillin.GlyphInstance;
|
||||
import dev.engine_room.vanillin.VanillinInstanceTypes;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.font.FontSet;
|
||||
import net.minecraft.client.gui.font.glyphs.BakedGlyph;
|
||||
import net.minecraft.client.gui.font.glyphs.EmptyGlyph;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import net.minecraft.util.FormattedCharSink;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
/**
|
||||
* A visual that renders a single line of text.
|
||||
*/
|
||||
public final class TextVisual {
|
||||
private static final Font FONT = Minecraft.getInstance().font;
|
||||
|
||||
private static final RendererReloadCache<GlyphMeshKey, GlyphMesh> GLYPH_MESH_CACHE = new RendererReloadCache<>(GlyphMeshKey::into);
|
||||
private static final RendererReloadCache<GlyphModelKey, Model> GLYPH_MODEL_CACHE = new RendererReloadCache<>(GlyphModelKey::into);
|
||||
|
||||
private static final ThreadLocal<Sink> SINKS = ThreadLocal.withInitial(Sink::new);
|
||||
|
||||
private final SmartRecycler<GlyphInstanceKey, GlyphInstance> recycler;
|
||||
private final List<TextLayer> layers = new ArrayList<>();
|
||||
private final Matrix4f pose = new Matrix4f();
|
||||
|
||||
private FormattedCharSequence text = FormattedCharSequence.EMPTY;
|
||||
private int backgroundColor = 0;
|
||||
private int light;
|
||||
|
||||
public TextVisual(InstancerProvider provider) {
|
||||
recycler = new SmartRecycler<>(key -> provider.instancer(VanillinInstanceTypes.GLYPH, GLYPH_MODEL_CACHE.get(key.modelKey), key.bias)
|
||||
.createInstance());
|
||||
}
|
||||
|
||||
public void setup(FormattedCharSequence textLine, List<TextLayer> layers, Matrix4f pose, int light) {
|
||||
// TODO: probably don't store everything
|
||||
this.text = textLine;
|
||||
this.layers.clear();
|
||||
this.layers.addAll(layers);
|
||||
this.pose.set(pose);
|
||||
this.light = light;
|
||||
|
||||
setup();
|
||||
}
|
||||
|
||||
public void updateObfuscated() {
|
||||
// TODO: track obfuscated glyphs and update here
|
||||
setup();
|
||||
}
|
||||
|
||||
public void backgroundColor(int backgroundColor) {
|
||||
// TODO: don't setup the whole thing
|
||||
this.backgroundColor = backgroundColor;
|
||||
setup();
|
||||
}
|
||||
|
||||
public void updateLight(int packedLight) {
|
||||
// TODO: just iterate over instances and update light
|
||||
light = packedLight;
|
||||
setup();
|
||||
}
|
||||
|
||||
private void setup() {
|
||||
recycler.resetCount();
|
||||
|
||||
var sink = SINKS.get();
|
||||
sink.prepare(recycler, layers, pose, light);
|
||||
|
||||
text.accept(sink);
|
||||
|
||||
sink.addBackground(backgroundColor, 0, sink.x);
|
||||
sink.clear();
|
||||
|
||||
recycler.discardExtra();
|
||||
}
|
||||
|
||||
private TextVisual reset() {
|
||||
// TODO: should this be public? what should it do?
|
||||
layers.clear();
|
||||
pose.identity();
|
||||
|
||||
text = FormattedCharSequence.EMPTY;
|
||||
backgroundColor = 0;
|
||||
light = 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
recycler.delete();
|
||||
}
|
||||
|
||||
private record GlyphMeshKey(float glyphWidth, float glyphHeight, TextLayer.GlyphPattern pattern, boolean bold, float boldOffset, float shadowOffset) {
|
||||
public GlyphMesh into() {
|
||||
List<Vector2fc> out = new ArrayList<>();
|
||||
|
||||
pattern.addGlyphs(offsetc -> {
|
||||
Vector2f offset = new Vector2f(offsetc).mul(shadowOffset);
|
||||
out.add(offset);
|
||||
|
||||
if (bold) {
|
||||
out.add(new Vector2f(offset.x() + boldOffset, offset.y()));
|
||||
}
|
||||
});
|
||||
|
||||
return new GlyphMesh(glyphWidth, glyphHeight, out.toArray(Vector2fc[]::new));
|
||||
}
|
||||
}
|
||||
|
||||
private record GlyphModelKey(@Nullable GlyphMeshKey meshKey, TextLayer.GlyphMaterial material, ResourceLocation texture) {
|
||||
public Model into() {
|
||||
Mesh mesh;
|
||||
|
||||
if (meshKey != null) {
|
||||
mesh = GLYPH_MESH_CACHE.get(meshKey);
|
||||
} else {
|
||||
mesh = GlyphEffectMesh.INSTANCE;
|
||||
}
|
||||
|
||||
return new SingleMeshModel(mesh, material.create(texture));
|
||||
}
|
||||
}
|
||||
|
||||
private record GlyphInstanceKey(GlyphModelKey modelKey, int bias) {
|
||||
}
|
||||
|
||||
private static class Sink implements FormattedCharSink {
|
||||
@UnknownNullability
|
||||
private SmartRecycler<GlyphInstanceKey, GlyphInstance> recycler;
|
||||
@UnknownNullability
|
||||
private List<TextLayer> layers;
|
||||
@UnknownNullability
|
||||
private Matrix4f pose;
|
||||
private int light;
|
||||
|
||||
private float x;
|
||||
|
||||
public void prepare(SmartRecycler<GlyphInstanceKey, GlyphInstance> recycler, List<TextLayer> layers, Matrix4f pose, int light) {
|
||||
this.recycler = recycler;
|
||||
this.layers = layers;
|
||||
this.pose = pose;
|
||||
this.light = light;
|
||||
this.x = 0;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
recycler = null;
|
||||
layers = null;
|
||||
pose = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(int index, Style style, int codePoint) {
|
||||
FontSet fontSet = TextUtil.getFontSet(FONT, style.getFont());
|
||||
GlyphInfo glyphInfo = fontSet.getGlyphInfo(codePoint, TextUtil.getFilterFishyGlyphs(FONT));
|
||||
BakedGlyph glyph = style.isObfuscated() && codePoint != ' ' ? fontSet.getRandomGlyph(glyphInfo) : fontSet.getGlyph(codePoint);
|
||||
|
||||
boolean bold = style.isBold();
|
||||
float advance = glyphInfo.getAdvance(bold);
|
||||
|
||||
// Process layers in the inner loop for 2 reasons:
|
||||
// 1. So we don't have to iterate over all the text and to the same glyph lookups for each layer
|
||||
// 2. So we get the same random draw in each layer for obfuscated text
|
||||
for (TextLayer layer : layers) {
|
||||
int color = layer.color()
|
||||
.color(style.getColor());
|
||||
Vector2fc offset = layer.offset();
|
||||
|
||||
if (!(glyph instanceof EmptyGlyph)) {
|
||||
GlyphInstance instance = recycler.get(key(layer, glyphInfo, glyph, bold));
|
||||
float shadowOffset = glyphInfo.getShadowOffset();
|
||||
instance.setGlyph(glyph, pose, x + offset.x() * shadowOffset, offset.y() * shadowOffset, style.isItalic());
|
||||
instance.colorArgb(color);
|
||||
instance.light(light);
|
||||
instance.setChanged();
|
||||
}
|
||||
|
||||
// SpecialGlyphs.WHITE, which effects use, has a shadowOffset of 1, so don't modify the offset returned by the layer.
|
||||
if (style.isStrikethrough()) {
|
||||
addEffect(layer, x + offset.x() - 1.0f, offset.y() + 4.5f, x + offset.x() + advance, offset.y() + 4.5f - 1.0f, 0.01f, color);
|
||||
}
|
||||
if (style.isUnderlined()) {
|
||||
addEffect(layer, x + offset.x() - 1.0f, offset.y() + 9.0f, x + offset.x() + advance, offset.y() + 9.0f - 1.0f, 0.01f, color);
|
||||
}
|
||||
}
|
||||
|
||||
x += advance;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void addBackground(int backgroundColor, float startX, float endX) {
|
||||
if (backgroundColor != 0) {
|
||||
BakedGlyph glyph = TextUtil.getFontSet(FONT, Style.DEFAULT_FONT)
|
||||
.whiteGlyph();
|
||||
|
||||
var glyphExtension = TextUtil.getBakedGlyphExtension(glyph);
|
||||
|
||||
GlyphInstance instance = recycler.get(effectKey(glyphExtension.flywheel$texture(), TextLayer.GlyphMaterial.SEE_THROUGH, 0));
|
||||
instance.setEffect(glyph, pose, startX - 1.0f, 9.0f, endX + 1.0f, 1.0f, 0.01f);
|
||||
instance.colorArgb(backgroundColor);
|
||||
instance.light(light);
|
||||
instance.setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void addEffect(TextLayer layer, float x0, float y0, float x1, float y1, float depth, int colorArgb) {
|
||||
BakedGlyph glyph = TextUtil.getFontSet(FONT, Style.DEFAULT_FONT)
|
||||
.whiteGlyph();
|
||||
|
||||
GlyphInstance instance = recycler.get(effectKey(glyph, layer));
|
||||
instance.setEffect(glyph, pose, x0, y0, x1, y1, depth);
|
||||
instance.colorArgb(colorArgb);
|
||||
instance.light(light);
|
||||
instance.setChanged();
|
||||
}
|
||||
|
||||
private static GlyphInstanceKey key(TextLayer layer, GlyphInfo glyphInfo, BakedGlyph glyph, boolean bold) {
|
||||
var glyphExtension = TextUtil.getBakedGlyphExtension(glyph);
|
||||
float glyphWidth = glyphExtension.flywheel$right() - glyphExtension.flywheel$left();
|
||||
float glyphHeight = glyphExtension.flywheel$down() - glyphExtension.flywheel$up();
|
||||
|
||||
return key(layer, glyphWidth, glyphHeight, glyphExtension.flywheel$texture(), bold, bold ? glyphInfo.getBoldOffset() : 0, glyphInfo.getShadowOffset());
|
||||
}
|
||||
|
||||
private static GlyphInstanceKey key(TextLayer layer, float glyphWidth, float glyphHeight, ResourceLocation texture, boolean bold, float boldOffset, float shadowOffset) {
|
||||
var meshKey = new GlyphMeshKey(glyphWidth, glyphHeight, layer.pattern(), bold, boldOffset, shadowOffset);
|
||||
var modelKey = new GlyphModelKey(meshKey, layer.material(), texture);
|
||||
return new GlyphInstanceKey(modelKey, layer.bias());
|
||||
}
|
||||
|
||||
private static GlyphInstanceKey effectKey(BakedGlyph glyph, TextLayer layer) {
|
||||
var glyphExtension = TextUtil.getBakedGlyphExtension(glyph);
|
||||
return effectKey(glyphExtension.flywheel$texture(), layer.material(), layer.bias());
|
||||
}
|
||||
|
||||
private static GlyphInstanceKey effectKey(ResourceLocation texture, TextLayer.GlyphMaterial material, int bias) {
|
||||
var modelKey = new GlyphModelKey(null, material, texture);
|
||||
return new GlyphInstanceKey(modelKey, bias);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A mesh that represents a pattern of a glyph with a certain width and height. Expects to be drawn with the glyph
|
||||
* instance type.
|
||||
*
|
||||
* @param offsets Each offset will be expanded into a glyph quad.
|
||||
*/
|
||||
private record GlyphMesh(float glyphWidth, float glyphHeight, Vector2fc[] offsets, Vector4fc boundingSphere) implements QuadMesh {
|
||||
private static final float[] X = new float[] { 0, 0, 1, 1 };
|
||||
private static final float[] Y = new float[] { 0, 1, 1, 0 };
|
||||
|
||||
public GlyphMesh(float glyphWidth, float glyphHeight, Vector2fc[] offsets) {
|
||||
this(glyphWidth, glyphHeight, offsets, boundingSphere(glyphWidth, glyphHeight, offsets));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int vertexCount() {
|
||||
return 4 * offsets.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(MutableVertexList vertexList) {
|
||||
for (int i = 0; i < offsets.length; i++) {
|
||||
Vector2fc offset = offsets[i];
|
||||
var startVertex = i * 4;
|
||||
|
||||
for (int j = 0; j < 4; j++) {
|
||||
vertexList.x(startVertex + j, offset.x() + (glyphWidth * X[j]));
|
||||
vertexList.y(startVertex + j, offset.y() + (glyphHeight * Y[j]));
|
||||
vertexList.z(startVertex + j, 0);
|
||||
vertexList.r(startVertex + j, 1);
|
||||
vertexList.g(startVertex + j, 1);
|
||||
vertexList.b(startVertex + j, 1);
|
||||
vertexList.a(startVertex + j, 1);
|
||||
vertexList.overlay(startVertex + j, OverlayTexture.NO_OVERLAY);
|
||||
vertexList.normalX(startVertex + j, 0);
|
||||
vertexList.normalY(startVertex + j, 0);
|
||||
vertexList.normalZ(startVertex + j, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector4fc boundingSphere() {
|
||||
return boundingSphere;
|
||||
}
|
||||
|
||||
private static Vector4fc boundingSphere(float glyphWidth, float glyphHeight, Vector2fc[] offsets) {
|
||||
if (offsets.length == 0) {
|
||||
return new Vector4f(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
float minX = Float.POSITIVE_INFINITY;
|
||||
float minY = Float.POSITIVE_INFINITY;
|
||||
float maxX = Float.NEGATIVE_INFINITY;
|
||||
float maxY = Float.NEGATIVE_INFINITY;
|
||||
for (Vector2fc offset : offsets) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
var x = offset.x() + (glyphWidth * X[j]);
|
||||
var y = offset.y() + (glyphHeight * Y[j]);
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
|
||||
float x = (minX + maxX) / 2;
|
||||
float y = (minY + maxY) / 2;
|
||||
|
||||
float sizeX = maxX - minX;
|
||||
float sizeY = maxY - minY;
|
||||
float maxSize = Math.max(sizeX, sizeY);
|
||||
|
||||
return new Vector4f(x, y, 0, Mth.SQRT_OF_TWO * maxSize / 2);
|
||||
}
|
||||
}
|
||||
|
||||
private record GlyphEffectMesh() implements QuadMesh {
|
||||
private static final float[] X = new float[] { 0, 1, 1, 0 };
|
||||
private static final float[] Y = new float[] { 0, 0, 1, 1 };
|
||||
private static final Vector4fc BOUNDING_SPHERE = new Vector4f(0.5f, 0.5f, 0, Mth.SQRT_OF_TWO * 0.5f);
|
||||
|
||||
public static final GlyphEffectMesh INSTANCE = new GlyphEffectMesh();
|
||||
|
||||
@Override
|
||||
public int vertexCount() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(MutableVertexList vertexList) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
vertexList.x(i, X[i]);
|
||||
vertexList.y(i, Y[i]);
|
||||
vertexList.z(i, 0);
|
||||
vertexList.r(i, 1);
|
||||
vertexList.g(i, 1);
|
||||
vertexList.b(i, 1);
|
||||
vertexList.a(i, 1);
|
||||
vertexList.normalX(i, 0);
|
||||
vertexList.normalY(i, 0);
|
||||
vertexList.normalZ(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vector4fc boundingSphere() {
|
||||
return BOUNDING_SPHERE;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
package dev.engine_room.vanillin.visuals;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
import dev.engine_room.flywheel.api.instance.Instance;
|
||||
import dev.engine_room.flywheel.api.material.Material;
|
||||
import dev.engine_room.flywheel.api.visualization.VisualizationContext;
|
||||
import dev.engine_room.flywheel.lib.material.CutoutShaders;
|
||||
import dev.engine_room.flywheel.lib.material.SimpleMaterial;
|
||||
import dev.engine_room.flywheel.lib.model.part.InstanceTree;
|
||||
import dev.engine_room.flywheel.lib.model.part.ModelTree;
|
||||
import dev.engine_room.flywheel.lib.model.part.ModelTrees;
|
||||
import dev.engine_room.flywheel.lib.util.RendererReloadCache;
|
||||
import dev.engine_room.flywheel.lib.visual.AbstractBlockEntityVisual;
|
||||
import dev.engine_room.flywheel.lib.visual.SimpleDynamicVisual;
|
||||
import dev.engine_room.vanillin.text.TextLayer;
|
||||
import dev.engine_room.vanillin.text.TextLayers;
|
||||
import dev.engine_room.vanillin.text.TextVisual;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.model.geom.ModelLayers;
|
||||
import net.minecraft.client.renderer.LightTexture;
|
||||
import net.minecraft.client.renderer.Sheets;
|
||||
import net.minecraft.util.FastColor;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.item.DyeColor;
|
||||
import net.minecraft.world.level.block.SignBlock;
|
||||
import net.minecraft.world.level.block.StandingSignBlock;
|
||||
import net.minecraft.world.level.block.entity.SignBlockEntity;
|
||||
import net.minecraft.world.level.block.entity.SignText;
|
||||
import net.minecraft.world.level.block.state.properties.WoodType;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public class SignVisual extends AbstractBlockEntityVisual<SignBlockEntity> implements SimpleDynamicVisual {
|
||||
private static final Vec3 TEXT_OFFSET = new Vec3(0.0, 0.3333333432674408, 0.046666666865348816);
|
||||
private static final Font FONT = Minecraft.getInstance().font;
|
||||
|
||||
private static final RendererReloadCache<WoodType, ModelTree> SIGN_MODELS = new RendererReloadCache<>(SignVisual::createSignModel);
|
||||
|
||||
private static final Material MATERIAL = SimpleMaterial.builder()
|
||||
.cutout(CutoutShaders.ONE_TENTH)
|
||||
.texture(Sheets.SIGN_SHEET)
|
||||
.backfaceCulling(false)
|
||||
.build();
|
||||
|
||||
private final InstanceTree instances;
|
||||
private final Matrix4f initialPose;
|
||||
|
||||
// The 8 lines of text we render
|
||||
private final TextVisual[] frontTextVisuals = new TextVisual[4];
|
||||
private final TextVisual[] backTextVisuals = new TextVisual[4];
|
||||
|
||||
// Need to update these every frame, so just remember which ones are obfuscated
|
||||
// Most of the time this will be empty.
|
||||
private final List<TextVisual> obfuscated = new ArrayList<>();
|
||||
|
||||
private int packedLight = 0;
|
||||
|
||||
private SignText lastFrontText;
|
||||
private SignText lastBackText;
|
||||
|
||||
public SignVisual(VisualizationContext ctx, SignBlockEntity blockEntity, float partialTick) {
|
||||
super(ctx, blockEntity, partialTick);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
frontTextVisuals[i] = new TextVisual(ctx.instancerProvider());
|
||||
backTextVisuals[i] = new TextVisual(ctx.instancerProvider());
|
||||
}
|
||||
|
||||
var block = (SignBlock) blockState.getBlock();
|
||||
WoodType woodType = SignBlock.getWoodType(block);
|
||||
var isStanding = block instanceof StandingSignBlock;
|
||||
|
||||
instances = InstanceTree.create(ctx.instancerProvider(), SIGN_MODELS.get(woodType));
|
||||
|
||||
// Maybe use a separate model tree?
|
||||
instances.childOrThrow("stick")
|
||||
.visible(isStanding);
|
||||
|
||||
var visualPosition = getVisualPosition();
|
||||
var signModelRenderScale = getSignModelRenderScale();
|
||||
var rotation = -block.getYRotationDegrees(blockState);
|
||||
initialPose = new Matrix4f().translate(visualPosition.getX() + 0.5f, visualPosition.getY() + 0.75f * signModelRenderScale, visualPosition.getZ() + 0.5f)
|
||||
.rotateY(Mth.DEG_TO_RAD * rotation);
|
||||
|
||||
if (!isStanding) {
|
||||
initialPose.translate(0.0f, -0.3125f, -0.4375f);
|
||||
}
|
||||
|
||||
// Only apply this to the instances because text gets a separate scaling.
|
||||
Matrix4f initialModelPose = new Matrix4f(initialPose).scale(signModelRenderScale, -signModelRenderScale, -signModelRenderScale);
|
||||
instances.updateInstancesStatic(initialModelPose);
|
||||
|
||||
lastFrontText = blockEntity.getFrontText();
|
||||
lastBackText = blockEntity.getBackText();
|
||||
setupText(lastFrontText, true);
|
||||
setupText(lastBackText, false);
|
||||
}
|
||||
|
||||
private static ModelTree createSignModel(WoodType woodType) {
|
||||
return ModelTrees.of(ModelLayers.createSignModelName(woodType), Sheets.getSignMaterial(woodType), MATERIAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginFrame(Context ctx) {
|
||||
boolean doSetup = false;
|
||||
if (lastFrontText != blockEntity.getFrontText()) {
|
||||
lastFrontText = blockEntity.getFrontText();
|
||||
doSetup = true;
|
||||
}
|
||||
|
||||
if (lastBackText != blockEntity.getBackText()) {
|
||||
lastBackText = blockEntity.getBackText();
|
||||
doSetup = true;
|
||||
}
|
||||
|
||||
if (doSetup) {
|
||||
// Setup both to make it easier to track obfuscation
|
||||
obfuscated.clear();
|
||||
setupText(lastFrontText, true);
|
||||
setupText(lastBackText, false);
|
||||
} else {
|
||||
// The is visible check is relatively expensive compared to the boolean checks above,
|
||||
// so only do it when it'll actually save some work in obfuscating.
|
||||
if (isVisible(ctx.frustum())) {
|
||||
obfuscated.forEach(TextVisual::updateObfuscated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateLight(float partialTick) {
|
||||
packedLight = computePackedLight();
|
||||
instances.traverse(instance -> {
|
||||
instance.light(packedLight)
|
||||
.setChanged();
|
||||
});
|
||||
|
||||
if (!lastFrontText.hasGlowingText()) {
|
||||
for (var text : frontTextVisuals) {
|
||||
text.updateLight(packedLight);
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastBackText.hasGlowingText()) {
|
||||
for (var text : backTextVisuals) {
|
||||
text.updateLight(packedLight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void collectCrumblingInstances(Consumer<@Nullable Instance> consumer) {
|
||||
instances.traverse(consumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void _delete() {
|
||||
instances.delete();
|
||||
|
||||
for (var text : frontTextVisuals) {
|
||||
text.delete();
|
||||
}
|
||||
|
||||
for (var text : backTextVisuals) {
|
||||
text.delete();
|
||||
}
|
||||
}
|
||||
|
||||
protected float getSignModelRenderScale() {
|
||||
return 0.6666667f;
|
||||
}
|
||||
|
||||
protected float getSignTextRenderScale() {
|
||||
return 0.6666667f;
|
||||
}
|
||||
|
||||
protected Vec3 getTextOffset() {
|
||||
return TEXT_OFFSET;
|
||||
}
|
||||
|
||||
private void setupText(SignText text, boolean isFrontText) {
|
||||
FormattedCharSequence[] textLines = text.getRenderMessages(Minecraft.getInstance()
|
||||
.isTextFilteringEnabled(), component -> {
|
||||
List<FormattedCharSequence> list = FONT.split(component, blockEntity.getMaxTextLineWidth());
|
||||
return list.isEmpty() ? FormattedCharSequence.EMPTY : list.get(0);
|
||||
});
|
||||
|
||||
List<TextLayer> layers = new ArrayList<>();
|
||||
|
||||
int darkColor = getDarkColor(text);
|
||||
int textColor;
|
||||
if (text.hasGlowingText()) {
|
||||
textColor = text.getColor()
|
||||
.getTextColor();
|
||||
|
||||
layers.add(TextLayers.outline(darkColor));
|
||||
} else {
|
||||
textColor = darkColor;
|
||||
}
|
||||
|
||||
layers.add(TextLayers.normal(textColor, Font.DisplayMode.POLYGON_OFFSET, 1));
|
||||
|
||||
var textVisuals = isFrontText ? frontTextVisuals : backTextVisuals;
|
||||
|
||||
int lineHeight = blockEntity.getTextLineHeight();
|
||||
int lineDelta = 4 * lineHeight / 2;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
FormattedCharSequence textLine = textLines[i];
|
||||
float x = (float) (-FONT.width(textLine) / 2);
|
||||
float y = i * lineHeight - lineDelta;
|
||||
|
||||
var pose = new Matrix4f(initialPose);
|
||||
if (!isFrontText) {
|
||||
pose.rotateY(Mth.PI);
|
||||
}
|
||||
float scale = 0.015625f * getSignTextRenderScale();
|
||||
var textOffset = getTextOffset();
|
||||
pose.translate((float) textOffset.x, (float) textOffset.y, (float) textOffset.z);
|
||||
pose.scale(scale, -scale, scale);
|
||||
pose.translate(x, y, 0.0f);
|
||||
|
||||
|
||||
var textVisual = textVisuals[i];
|
||||
int light = text.hasGlowingText() ? LightTexture.FULL_BRIGHT : packedLight;
|
||||
textVisual.setup(textLine, layers, pose, light);
|
||||
|
||||
if (hasObfuscation(textLine)) {
|
||||
obfuscated.add(textVisual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int getDarkColor(SignText signText) {
|
||||
int colorArgb = signText.getColor()
|
||||
.getTextColor();
|
||||
if (colorArgb == DyeColor.BLACK.getTextColor() && signText.hasGlowingText()) {
|
||||
return 0xFFF0EBCC;
|
||||
}
|
||||
|
||||
int r = (int) ((double) FastColor.ARGB32.red(colorArgb) * 0.4);
|
||||
int g = (int) ((double) FastColor.ARGB32.green(colorArgb) * 0.4);
|
||||
int b = (int) ((double) FastColor.ARGB32.blue(colorArgb) * 0.4);
|
||||
return FastColor.ARGB32.color(0, r, g, b);
|
||||
}
|
||||
|
||||
private static boolean hasObfuscation(FormattedCharSequence text) {
|
||||
return text.accept((i, s, j) -> s.isObfuscated());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
#include "flywheel:util/matrix.glsl"
|
||||
|
||||
void flw_transformBoundingSphere(in FlwInstance i, inout vec3 center, inout float radius) {
|
||||
transformBoundingSphere(i.pose, center, radius);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
void flw_instanceVertex(in FlwInstance i) {
|
||||
uint vertexInGlyph = flw_vertexId % 4;
|
||||
|
||||
uint yIndex = ((vertexInGlyph + 1u) >> 1u) & 1u;
|
||||
|
||||
flw_vertexPos = i.pose * flw_vertexPos;
|
||||
|
||||
flw_vertexTexCoord.s = i.u0u1v0v1[(vertexInGlyph & 2u) >> 1u];
|
||||
flw_vertexTexCoord.t = i.u0u1v0v1[2u + yIndex];
|
||||
|
||||
flw_vertexColor *= i.color;
|
||||
|
||||
// Some drivers have a bug where uint over float division is invalid, so use an explicit cast.
|
||||
flw_vertexLight = vec2(i.light) / 256.0;
|
||||
}
|
18
common/src/vanillin/resources/vanillin.mixins.json
Normal file
18
common/src/vanillin/resources/vanillin.mixins.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"required" : true,
|
||||
"minVersion" : "0.8",
|
||||
"package" : "dev.engine_room.vanillin.mixin",
|
||||
"compatibilityLevel" : "JAVA_17",
|
||||
"refmap" : "vanillin.refmap.json",
|
||||
"client" : [
|
||||
"text.BakedGlyphMixin",
|
||||
"text.CodePointMapMixin",
|
||||
"text.FontAccessor",
|
||||
"text.FontSetMixin",
|
||||
"text.FontTexture$NodeAccessor",
|
||||
"text.FontTextureMixin"
|
||||
],
|
||||
"injectors" : {
|
||||
"defaultRequire" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package dev.engine_room.vanillin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import dev.engine_room.vanillin.config.Configurator;
|
||||
import dev.engine_room.vanillin.config.ModOverrides;
|
||||
import dev.engine_room.vanillin.config.VisualConfigValue;
|
||||
import dev.engine_room.vanillin.config.VisualOverride;
|
||||
import dev.engine_room.vanillin.config.VisualOverrideValue;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.ModContainer;
|
||||
import net.fabricmc.loader.api.metadata.CustomValue;
|
||||
import net.fabricmc.loader.api.metadata.ModMetadata;
|
||||
|
||||
public class FabricVanillinConfig {
|
||||
public static final Path PATH = FabricLoader.getInstance()
|
||||
.getConfigDir()
|
||||
.resolve("vanillin.json");
|
||||
|
||||
public static final FabricVanillinConfig INSTANCE = new FabricVanillinConfig(PATH.toFile());
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
public static final String VANILLIN_OVERRIDES = "vanillin:overrides";
|
||||
|
||||
private final File file;
|
||||
|
||||
private ModOverrides overrides;
|
||||
private Config config = new Config();
|
||||
|
||||
public FabricVanillinConfig(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public void load() {
|
||||
if (file.exists()) {
|
||||
try (FileReader reader = new FileReader(file)) {
|
||||
config = GSON.fromJson(reader, Config.class);
|
||||
} catch (Exception e) {
|
||||
Vanillin.CONFIG_LOGGER.warn("Could not load config from file '{}'", file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
overrides = modOverrides();
|
||||
}
|
||||
|
||||
public void apply(Configurator configurator) {
|
||||
var blockEntities = config.blockEntities;
|
||||
var blockEntityOverrides = this.overrides.blockEntities();
|
||||
|
||||
for (Configurator.ConfiguredVisual configured : configurator.blockEntities.values()) {
|
||||
apply(configured, blockEntities, blockEntityOverrides);
|
||||
}
|
||||
|
||||
var entities = config.entities;
|
||||
var entityOverrides = this.overrides.entities();
|
||||
for (Configurator.ConfiguredVisual configured : configurator.entities.values()) {
|
||||
apply(configured, entities, entityOverrides);
|
||||
}
|
||||
}
|
||||
|
||||
private static void apply(Configurator.ConfiguredVisual configured, Map<String, VisualConfigValue> config, Map<String, List<VisualOverride>> overrides) {
|
||||
var key = configured.configKey();
|
||||
var enabled = config.computeIfAbsent(key, $ -> VisualConfigValue.DEFAULT);
|
||||
|
||||
configured.set(enabled, overrides.get(key));
|
||||
}
|
||||
|
||||
public void save() {
|
||||
try (FileWriter writer = new FileWriter(file)) {
|
||||
GSON.toJson(config, writer);
|
||||
} catch (Exception e) {
|
||||
Vanillin.CONFIG_LOGGER.warn("Could not save config to file '{}'", file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ModOverrides modOverrides() {
|
||||
var blockEntities = new ArrayList<VisualOverride>();
|
||||
var entities = new ArrayList<VisualOverride>();
|
||||
|
||||
for (ModContainer container : FabricLoader.getInstance().getAllMods()) {
|
||||
ModMetadata meta = container.getMetadata();
|
||||
var modid = meta.getId();
|
||||
|
||||
if (meta.containsCustomValue(VANILLIN_OVERRIDES)) {
|
||||
CustomValue overridesValue = meta.getCustomValue(VANILLIN_OVERRIDES);
|
||||
|
||||
if (overridesValue.getType() != CustomValue.CvType.OBJECT) {
|
||||
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override options with an invalid value, ignoring", modid);
|
||||
continue;
|
||||
}
|
||||
|
||||
var overrides = overridesValue.getAsObject();
|
||||
|
||||
readSection(blockEntities, modid, overrides, "block_entities", "block entity");
|
||||
readSection(entities, modid, overrides, "entities", "entity");
|
||||
}
|
||||
}
|
||||
|
||||
return new ModOverrides(blockEntities, entities);
|
||||
}
|
||||
|
||||
private static void readSection(List<VisualOverride> dst, String modid, CustomValue.CvObject overrides, String sectionName, String singular) {
|
||||
if (!overrides.containsKey(sectionName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var section = overrides.get(sectionName);
|
||||
|
||||
if (section.getType() != CustomValue.CvType.OBJECT) {
|
||||
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} with an invalid value, ignoring", modid, sectionName);
|
||||
return;
|
||||
}
|
||||
|
||||
for (Map.Entry<String, CustomValue> entry : section.getAsObject()) {
|
||||
var value = entry.getValue();
|
||||
var key = entry.getKey();
|
||||
if (value.getType() != CustomValue.CvType.STRING) {
|
||||
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} '{}' with an invalid value, ignoring", modid, singular, key);
|
||||
continue;
|
||||
}
|
||||
|
||||
var valueString = value.getAsString();
|
||||
|
||||
var parsed = VisualOverrideValue.parse(valueString);
|
||||
|
||||
if (parsed == null) {
|
||||
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} '{}' with an invalid value '{}', ignoring", modid, singular, key, valueString);
|
||||
continue;
|
||||
}
|
||||
|
||||
dst.add(new VisualOverride(key, modid, parsed));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
@SerializedName("block_entities")
|
||||
public Map<String, VisualConfigValue> blockEntities;
|
||||
public Map<String, VisualConfigValue> entities;
|
||||
|
||||
public Config() {
|
||||
this(new HashMap<>(), new HashMap<>());
|
||||
}
|
||||
|
||||
public Config(Map<String, VisualConfigValue> blockEntities, Map<String, VisualConfigValue> entities) {
|
||||
this.blockEntities = blockEntities;
|
||||
this.entities = entities;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
package dev.engine_room.vanillin;
|
||||
|
||||
import dev.engine_room.vanillin.visuals.VanillaVisuals;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
|
||||
public class VanillinFabric implements ClientModInitializer {
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
VanillaVisuals.init();
|
||||
FabricVanillinConfig.INSTANCE.load();
|
||||
FabricVanillinConfig.INSTANCE.apply(VanillaVisuals.CONFIGURATOR);
|
||||
FabricVanillinConfig.INSTANCE.save();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
]
|
||||
},
|
||||
"mixins" : [
|
||||
"vanillin.mixins.json"
|
||||
],
|
||||
"depends" : {
|
||||
"minecraft" : "${minecraft_semver_version_range}",
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
package dev.engine_room.vanillin;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.electronwill.nightconfig.core.Config;
|
||||
|
||||
import dev.engine_room.vanillin.config.Configurator;
|
||||
import dev.engine_room.vanillin.config.ModOverrides;
|
||||
import dev.engine_room.vanillin.config.VisualConfigValue;
|
||||
import dev.engine_room.vanillin.config.VisualOverride;
|
||||
import dev.engine_room.vanillin.config.VisualOverrideValue;
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
import net.minecraftforge.fml.ModList;
|
||||
import net.minecraftforge.fml.ModLoadingContext;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
import net.minecraftforge.forgespi.language.IModInfo;
|
||||
|
||||
public class ForgeVanillinConfig {
|
||||
public static final ForgeVanillinConfig INSTANCE = new ForgeVanillinConfig(VanillaVisuals.CONFIGURATOR);
|
||||
|
||||
private final Configurator configurator;
|
||||
private final ForgeConfigSpec clientSpec;
|
||||
|
||||
private final ConfigSection blockEntities;
|
||||
private final ConfigSection entities;
|
||||
|
||||
private ForgeVanillinConfig(Configurator configurator) {
|
||||
this.configurator = configurator;
|
||||
var builder = new ForgeConfigSpec.Builder();
|
||||
|
||||
// Seems like we need to register all field ahead of time so this constructor must run after VanillaVisuals#init
|
||||
var blockEntities = setup(builder, configurator.blockEntities.values(), "block_entities");
|
||||
var entities = setup(builder, configurator.entities.values(), "entities");
|
||||
clientSpec = builder.build();
|
||||
|
||||
var modOverrides = modOverrides();
|
||||
|
||||
this.blockEntities = new ConfigSection(blockEntities, modOverrides.blockEntities());
|
||||
this.entities = new ConfigSection(entities, modOverrides.entities());
|
||||
}
|
||||
|
||||
public void apply() {
|
||||
blockEntities.apply(configurator.blockEntities.values());
|
||||
entities.apply(configurator.entities.values());
|
||||
}
|
||||
|
||||
public void registerSpecs(ModLoadingContext context) {
|
||||
context.registerConfig(ModConfig.Type.CLIENT, clientSpec);
|
||||
}
|
||||
|
||||
private static ModOverrides modOverrides() {
|
||||
var blockEntities = new ArrayList<VisualOverride>();
|
||||
var entities = new ArrayList<VisualOverride>();
|
||||
|
||||
ModList.get()
|
||||
.forEachModFile(file -> {
|
||||
var info = file.getModFileInfo();
|
||||
for (IModInfo mod : info.getMods()) {
|
||||
var modId = mod.getModId();
|
||||
var modProperties = mod.getModProperties()
|
||||
.get("vanillin:overrides");
|
||||
|
||||
if (modProperties == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// There's no well-defined API for custom properties like in fabric.
|
||||
// It just returns an object, but internally it's represented with nightconfig.
|
||||
if (modProperties instanceof Config config) {
|
||||
readSection(blockEntities, modId, config, "block_entities", "block entity");
|
||||
readSection(entities, modId, config, "entities", "entity");
|
||||
} else {
|
||||
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override options with an invalid value, ignoring", modId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new ModOverrides(blockEntities, entities);
|
||||
}
|
||||
|
||||
private static void readSection(List<VisualOverride> dst, String modId, Config config, String section, String singular) {
|
||||
if (!config.contains(section)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sectionObject = config.getRaw(section);
|
||||
|
||||
if (sectionObject instanceof Config sectionConfig) {
|
||||
for (var entry : sectionConfig.entrySet()) {
|
||||
var key = entry.getKey();
|
||||
var value = entry.getValue();
|
||||
|
||||
if (value instanceof String valueString) {
|
||||
var parsed = VisualOverrideValue.parse(valueString);
|
||||
|
||||
if (parsed != null) {
|
||||
dst.add(new VisualOverride(key, modId, parsed));
|
||||
} else {
|
||||
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} '{}' with an invalid value '{}', ignoring", modId, singular, key, valueString);
|
||||
}
|
||||
} else {
|
||||
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} '{}' with an invalid value, ignoring", modId, singular, key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vanillin.CONFIG_LOGGER.warn("Mod '{}' attempted to override {} with an invalid value, ignoring", modId, section);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, ForgeConfigSpec.EnumValue<VisualConfigValue>> setup(ForgeConfigSpec.Builder builder, Collection<? extends Configurator.ConfiguredVisual> configuredVisuals, String push) {
|
||||
var out = new HashMap<String, ForgeConfigSpec.EnumValue<VisualConfigValue>>();
|
||||
builder.push(push);
|
||||
|
||||
for (var configured : configuredVisuals) {
|
||||
var name = configured.configKey();
|
||||
var config = builder.defineEnum(name, VisualConfigValue.DEFAULT);
|
||||
out.put(name, config);
|
||||
}
|
||||
|
||||
builder.pop();
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
private record ConfigSection(Map<String, ForgeConfigSpec.EnumValue<VisualConfigValue>> config, Map<String, List<VisualOverride>> overrides) {
|
||||
void apply(Collection<? extends Configurator.ConfiguredVisual> values) {
|
||||
for (var configured : values) {
|
||||
var key = configured.configKey();
|
||||
var value = config.get(key);
|
||||
if (value != null) {
|
||||
configured.set(value.get(), overrides.get(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,6 +42,10 @@ loom {
|
|||
add(main, "vanillin.refmap.json")
|
||||
}
|
||||
|
||||
forge {
|
||||
mixinConfig("vanillin.mixins.json")
|
||||
}
|
||||
|
||||
runs {
|
||||
configureEach {
|
||||
property("forge.logging.markers", "")
|
||||
|
@ -65,6 +69,9 @@ dependencies {
|
|||
compileOnly(project(path = common, configuration = "vanillinClasses"))
|
||||
compileOnly(project(path = common, configuration = "vanillinResources"))
|
||||
|
||||
compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:0.4.1")!!)
|
||||
implementation(include("io.github.llamalad7:mixinextras-forge:0.4.1")!!)
|
||||
|
||||
// JiJ flywheel proper
|
||||
include(project(path = platform, configuration = "flywheelRemap"))
|
||||
runtimeOnly(project(path = platform, configuration = "flywheelDev"))
|
||||
|
|
Loading…
Reference in a new issue