diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 78012a56b..f283b9c2f 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -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") } diff --git a/common/src/api/java/dev/engine_room/flywheel/api/internal/FlwApiLink.java b/common/src/api/java/dev/engine_room/flywheel/api/internal/FlwApiLink.java index 53301b0aa..13e752154 100644 --- a/common/src/api/java/dev/engine_room/flywheel/api/internal/FlwApiLink.java +++ b/common/src/api/java/dev/engine_room/flywheel/api/internal/FlwApiLink.java @@ -42,7 +42,7 @@ public interface FlwApiLink { @Nullable EntityVisualizer getVisualizer(EntityType type); - void setVisualizer(BlockEntityType type, BlockEntityVisualizer visualizer); + void setVisualizer(BlockEntityType type, @Nullable BlockEntityVisualizer visualizer); - void setVisualizer(EntityType type, EntityVisualizer visualizer); + void setVisualizer(EntityType type, @Nullable EntityVisualizer visualizer); } diff --git a/common/src/api/java/dev/engine_room/flywheel/api/visualization/VisualizerRegistry.java b/common/src/api/java/dev/engine_room/flywheel/api/visualization/VisualizerRegistry.java index e9dd43370..eda95a7e7 100644 --- a/common/src/api/java/dev/engine_room/flywheel/api/visualization/VisualizerRegistry.java +++ b/common/src/api/java/dev/engine_room/flywheel/api/visualization/VisualizerRegistry.java @@ -43,7 +43,7 @@ public final class VisualizerRegistry { * @param visualizer The visualizer to set. * @param The type of the block entity. */ - public static void setVisualizer(BlockEntityType type, BlockEntityVisualizer visualizer) { + public static void setVisualizer(BlockEntityType type, @Nullable BlockEntityVisualizer visualizer) { FlwApiLink.INSTANCE.setVisualizer(type, visualizer); } @@ -53,7 +53,7 @@ public final class VisualizerRegistry { * @param visualizer The visualizer to set. * @param The type of the entity. */ - public static void setVisualizer(EntityType type, EntityVisualizer visualizer) { + public static void setVisualizer(EntityType type, @Nullable EntityVisualizer visualizer) { FlwApiLink.INSTANCE.setVisualizer(type, visualizer); } } diff --git a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java index 3a84e3a13..47b58fae5 100644 --- a/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java +++ b/common/src/backend/java/dev/engine_room/flywheel/backend/engine/EngineImpl.java @@ -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); diff --git a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert index d1e751929..019df0083 100644 --- a/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert +++ b/common/src/backend/resources/assets/flywheel/flywheel/internal/api_impl.vert @@ -12,3 +12,5 @@ out vec3 flw_vertexNormal; out float flw_distance; FlwMaterial flw_material; + +#define flw_vertexId gl_VertexID diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java index 6d2f0ed66..e8f95baf5 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitInstance.java @@ -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 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 diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java new file mode 100644 index 000000000..ab0725d9b --- /dev/null +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/ColoredLitOverlayInstance.java @@ -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 type, InstanceHandle handle) { + super(type, handle); + } + + public ColoredLitOverlayInstance overlay(int overlay) { + this.overlay = overlay; + return this; + } +} diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java index 321caf936..db59429e2 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/OrientedInstance.java @@ -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 { +public class OrientedInstance extends ColoredLitOverlayInstance implements Rotate { public float posX; public float posY; public float posZ; diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java index 43e6df47b..f00a6ac5d 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/PosedInstance.java @@ -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 { +public class PosedInstance extends ColoredLitOverlayInstance implements Transform { public final Matrix4f pose = new Matrix4f(); public final Matrix3f normal = new Matrix3f(); diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java index ffab518eb..6b93d0102 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/instance/TransformedInstance.java @@ -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 { +public class TransformedInstance extends ColoredLitOverlayInstance implements Affine { public final Matrix4f pose = new Matrix4f(); public TransformedInstance(InstanceType type, InstanceHandle handle) { diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java b/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java index 6fa5f33fe..363888f1a 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/math/DataPacker.java @@ -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; + } } diff --git a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java index 884ba3bf5..deb180e8a 100644 --- a/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java +++ b/common/src/lib/java/dev/engine_room/flywheel/lib/visual/component/FireComponent.java @@ -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() { diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/FlwApiLinkImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/FlwApiLinkImpl.java index c60df1239..6f03a7dab 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/FlwApiLinkImpl.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/FlwApiLinkImpl.java @@ -79,12 +79,12 @@ public class FlwApiLinkImpl implements FlwApiLink { } @Override - public void setVisualizer(BlockEntityType type, BlockEntityVisualizer visualizer) { + public void setVisualizer(BlockEntityType type, @Nullable BlockEntityVisualizer visualizer) { VisualizerRegistryImpl.setVisualizer(type, visualizer); } @Override - public void setVisualizer(EntityType type, EntityVisualizer visualizer) { + public void setVisualizer(EntityType type, @Nullable EntityVisualizer visualizer) { VisualizerRegistryImpl.setVisualizer(type, visualizer); } } diff --git a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizerRegistryImpl.java b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizerRegistryImpl.java index 40522c89d..43d605ed0 100644 --- a/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizerRegistryImpl.java +++ b/common/src/main/java/dev/engine_room/flywheel/impl/visualization/VisualizerRegistryImpl.java @@ -23,11 +23,11 @@ public final class VisualizerRegistryImpl { return ((EntityTypeExtension) type).flywheel$getVisualizer(); } - public static void setVisualizer(BlockEntityType type, BlockEntityVisualizer visualizer) { + public static void setVisualizer(BlockEntityType type, @Nullable BlockEntityVisualizer visualizer) { ((BlockEntityTypeExtension) type).flywheel$setVisualizer(visualizer); } - public static void setVisualizer(EntityType type, EntityVisualizer visualizer) { + public static void setVisualizer(EntityType type, @Nullable EntityVisualizer visualizer) { ((EntityTypeExtension) type).flywheel$setVisualizer(visualizer); } diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/GlyphInstance.java b/common/src/vanillin/java/dev/engine_room/vanillin/GlyphInstance.java new file mode 100644 index 000000000..1356e25a7 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/GlyphInstance.java @@ -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 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)); + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/visuals/VanillaVisuals.java b/common/src/vanillin/java/dev/engine_room/vanillin/VanillaVisuals.java similarity index 65% rename from common/src/vanillin/java/dev/engine_room/vanillin/visuals/VanillaVisuals.java rename to common/src/vanillin/java/dev/engine_room/vanillin/VanillaVisuals.java index eae31e8f2..ae2618add 100644 --- a/common/src/vanillin/java/dev/engine_room/vanillin/visuals/VanillaVisuals.java +++ b/common/src/vanillin/java/dev/engine_room/vanillin/VanillaVisuals.java @@ -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 BlockEntityVisualizerBuilder builder(BlockEntityType type) { + return new BlockEntityVisualizerBuilder<>(CONFIGURATOR, type); + } + + public static EntityVisualizerBuilder builder(EntityType type) { + return new EntityVisualizerBuilder<>(CONFIGURATOR, type); } } diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/Vanillin.java b/common/src/vanillin/java/dev/engine_room/vanillin/Vanillin.java index 2b4fd2efb..8eefa0c6f 100644 --- a/common/src/vanillin/java/dev/engine_room/vanillin/Vanillin.java +++ b/common/src/vanillin/java/dev/engine_room/vanillin/Vanillin.java @@ -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); + } } diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/VanillinInstanceTypes.java b/common/src/vanillin/java/dev/engine_room/vanillin/VanillinInstanceTypes.java new file mode 100644 index 000000000..b89fcfb48 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/VanillinInstanceTypes.java @@ -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 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(); +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/config/BlockEntityVisualizerBuilder.java b/common/src/vanillin/java/dev/engine_room/vanillin/config/BlockEntityVisualizerBuilder.java new file mode 100644 index 000000000..8c8070999 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/config/BlockEntityVisualizerBuilder.java @@ -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 { + private final Configurator configurator; + private final BlockEntityType type; + @Nullable + private SimpleBlockEntityVisualizer.Factory visualFactory; + @Nullable + private Predicate skipVanillaRender; + + public BlockEntityVisualizerBuilder(Configurator configurator, BlockEntityType 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 factory(SimpleBlockEntityVisualizer.Factory 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 skipVanillaRender(Predicate skipVanillaRender) { + this.skipVanillaRender = skipVanillaRender; + return this; + } + + /** + * Sets a predicate to never skip rendering with the vanilla {@link BlockEntityRenderer}. + * + * @return {@code this} + */ + public BlockEntityVisualizerBuilder 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 apply(boolean enabledByDefault) { + Objects.requireNonNull(visualFactory, "Visual factory cannot be null!"); + if (skipVanillaRender == null) { + skipVanillaRender = blockEntity -> true; + } + + SimpleBlockEntityVisualizer visualizer = new SimpleBlockEntityVisualizer<>(visualFactory, skipVanillaRender); + configurator.register(type, visualizer, enabledByDefault); + + return visualizer; + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/config/Configurator.java b/common/src/vanillin/java/dev/engine_room/vanillin/config/Configurator.java new file mode 100644 index 000000000..28e97af02 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/config/Configurator.java @@ -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, ConfiguredBlockEntity> blockEntities = new HashMap<>(); + public final Map, ConfiguredEntity> entities = new HashMap<>(); + + public void register(BlockEntityType type, BlockEntityVisualizer visualizer, boolean enabledByDefault) { + blockEntities.put(type, new ConfiguredBlockEntity<>(type, visualizer, enabledByDefault)); + } + + public void register(EntityType type, EntityVisualizer 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 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 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 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 disablingModIds(List overrides) { + List out = new ArrayList<>(); + + for (VisualOverride override : overrides) { + if (override.value() == VisualOverrideValue.DISABLE) { + out.add(override.modId()); + } + } + return out; + } + } + + public static class ConfiguredBlockEntity extends ConfiguredVisual { + public final BlockEntityType type; + public final BlockEntityVisualizer visualizer; + + private ConfiguredBlockEntity(BlockEntityType type, BlockEntityVisualizer 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 extends ConfiguredVisual { + public final EntityType type; + public final EntityVisualizer visualizer; + + private ConfiguredEntity(EntityType type, EntityVisualizer 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); + } + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/config/EntityVisualizerBuilder.java b/common/src/vanillin/java/dev/engine_room/vanillin/config/EntityVisualizerBuilder.java new file mode 100644 index 000000000..6e9a8aa54 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/config/EntityVisualizerBuilder.java @@ -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 The type of the entity. + */ +public final class EntityVisualizerBuilder { + private final Configurator configurator; + private final EntityType type; + @Nullable + private SimpleEntityVisualizer.Factory visualFactory; + @Nullable + private Predicate skipVanillaRender; + + public EntityVisualizerBuilder(Configurator configurator, EntityType type) { + this.configurator = configurator; + this.type = type; + } + + /** + * Sets the visual factory for the entity. + * + * @param visualFactory The visual factory. + * @return {@code this} + */ + public EntityVisualizerBuilder factory(SimpleEntityVisualizer.Factory 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 skipVanillaRender(Predicate skipVanillaRender) { + this.skipVanillaRender = skipVanillaRender; + return this; + } + + /** + * Sets a predicate to always skip rendering with the vanilla {@link EntityRenderer}. + * + * @return {@code this} + */ + public EntityVisualizerBuilder neverSkipVanillaRender() { + this.skipVanillaRender = entity -> false; + return this; + } + + /** + * Constructs the entity visualizer and sets it for the entity type. + * + * @return The entity visualizer. + */ + public SimpleEntityVisualizer apply(boolean enabledByDefault) { + Objects.requireNonNull(visualFactory, "Visual factory cannot be null!"); + if (skipVanillaRender == null) { + skipVanillaRender = entity -> true; + } + + SimpleEntityVisualizer visualizer = new SimpleEntityVisualizer<>(visualFactory, skipVanillaRender); + configurator.register(type, visualizer, enabledByDefault); + + return visualizer; + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/config/ModOverrides.java b/common/src/vanillin/java/dev/engine_room/vanillin/config/ModOverrides.java new file mode 100644 index 000000000..3bc6efa1e --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/config/ModOverrides.java @@ -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> blockEntities, Map> entities) { + public ModOverrides(List blockEntities, List entities) { + this(sort(blockEntities), sort(entities)); + } + + public static Map> sort(List list) { + return list.stream().collect(Collectors.groupingBy(VisualOverride::name)); + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/config/VisualConfigValue.java b/common/src/vanillin/java/dev/engine_room/vanillin/config/VisualConfigValue.java new file mode 100644 index 000000000..723c72b7a --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/config/VisualConfigValue.java @@ -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, +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/config/VisualOverride.java b/common/src/vanillin/java/dev/engine_room/vanillin/config/VisualOverride.java new file mode 100644 index 000000000..85438f49b --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/config/VisualOverride.java @@ -0,0 +1,4 @@ +package dev.engine_room.vanillin.config; + +public record VisualOverride(String name, String modId, VisualOverrideValue value) { +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/config/VisualOverrideValue.java b/common/src/vanillin/java/dev/engine_room/vanillin/config/VisualOverrideValue.java new file mode 100644 index 000000000..333e060d7 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/config/VisualOverrideValue.java @@ -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; + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/BakedGlyphMixin.java b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/BakedGlyphMixin.java new file mode 100644 index 000000000..90d5ab0d4 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/BakedGlyphMixin.java @@ -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; + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/CodePointMapMixin.java b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/CodePointMapMixin.java new file mode 100644 index 000000000..e73f87c7f --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/CodePointMapMixin.java @@ -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 { + @Unique + private final Object flywheel$lock = new Object(); + + @WrapMethod(method = "clear") + private void flywheel$wrapClearAsSynchronized(Operation original) { + synchronized (flywheel$lock) { + original.call(); + } + } + + @WrapMethod(method = "get") + private T flywheel$wrapGetAsSynchronized(int index, Operation original) { + synchronized (flywheel$lock) { + return original.call(index); + } + } + + @WrapMethod(method = "put") + private T flywheel$wrapPutAsSynchronized(int index, T value, Operation original) { + synchronized (flywheel$lock) { + return original.call(index, value); + } + } + + @WrapMethod(method = "computeIfAbsent") + private T flywheel$wrapComputeIfAbsentAsSynchronized(int index, IntFunction valueIfAbsentGetter, Operation original) { + synchronized (flywheel$lock) { + return original.call(index, valueIfAbsentGetter); + } + } + + @WrapMethod(method = "remove") + private T flywheel$wrapRemoveAsSynchronized(int index, Operation original) { + synchronized (flywheel$lock) { + return original.call(index); + } + } + + @WrapMethod(method = "forEach") + private void flywheel$wrapForEachAsSynchronized(CodepointMap.Output output, Operation original) { + synchronized (flywheel$lock) { + original.call(output); + } + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontAccessor.java b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontAccessor.java new file mode 100644 index 000000000..70bec9f17 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontAccessor.java @@ -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); +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontSetMixin.java b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontSetMixin.java new file mode 100644 index 000000000..9ee7ea82d --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontSetMixin.java @@ -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; + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontTexture$NodeAccessor.java b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontTexture$NodeAccessor.java new file mode 100644 index 000000000..638e753bb --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontTexture$NodeAccessor.java @@ -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(); +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontTextureMixin.java b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontTextureMixin.java new file mode 100644 index 000000000..e6e01749a --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/mixin/text/FontTextureMixin.java @@ -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 flywheel$uploads = new ArrayList<>(); + @Unique + private boolean flywheel$flushScheduled = false; + + @Unique + private ResourceLocation flywheel$name; + + @WrapOperation(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/font/FontTexture;getId()I")) + private int flywheel$skipGetId(FontTexture instance, Operation 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 = "", 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 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 original, @Share("node") LocalRef 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 cir, @Share("node") LocalRef 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; + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/text/BakedGlyphExtension.java b/common/src/vanillin/java/dev/engine_room/vanillin/text/BakedGlyphExtension.java new file mode 100644 index 000000000..ceea95bb3 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/text/BakedGlyphExtension.java @@ -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); +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/text/FontTextureExtension.java b/common/src/vanillin/java/dev/engine_room/vanillin/text/FontTextureExtension.java new file mode 100644 index 000000000..4075c2753 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/text/FontTextureExtension.java @@ -0,0 +1,7 @@ +package dev.engine_room.vanillin.text; + +import net.minecraft.resources.ResourceLocation; + +public interface FontTextureExtension { + void flywheel$setName(ResourceLocation name); +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/text/FontTextureUpload.java b/common/src/vanillin/java/dev/engine_room/vanillin/text/FontTextureUpload.java new file mode 100644 index 000000000..4af88c5db --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/text/FontTextureUpload.java @@ -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) { +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/text/SimpleTextLayer.java b/common/src/vanillin/java/dev/engine_room/vanillin/text/SimpleTextLayer.java new file mode 100644 index 000000000..ab81ba7f0 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/text/SimpleTextLayer.java @@ -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); + } + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/text/TextLayer.java b/common/src/vanillin/java/dev/engine_room/vanillin/text/TextLayer.java new file mode 100644 index 000000000..2bf0c8cc4 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/text/TextLayer.java @@ -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 + @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 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); + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/text/TextLayers.java b/common/src/vanillin/java/dev/engine_room/vanillin/text/TextLayers.java new file mode 100644 index 000000000..247dc86f9 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/text/TextLayers.java @@ -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() { + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/text/TextUtil.java b/common/src/vanillin/java/dev/engine_room/vanillin/text/TextUtil.java new file mode 100644 index 000000000..6ca2fb345 --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/text/TextUtil.java @@ -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; + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/text/TextVisual.java b/common/src/vanillin/java/dev/engine_room/vanillin/text/TextVisual.java new file mode 100644 index 000000000..df2ff0fee --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/text/TextVisual.java @@ -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 GLYPH_MESH_CACHE = new RendererReloadCache<>(GlyphMeshKey::into); + private static final RendererReloadCache GLYPH_MODEL_CACHE = new RendererReloadCache<>(GlyphModelKey::into); + + private static final ThreadLocal SINKS = ThreadLocal.withInitial(Sink::new); + + private final SmartRecycler recycler; + private final List 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 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 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 recycler; + @UnknownNullability + private List layers; + @UnknownNullability + private Matrix4f pose; + private int light; + + private float x; + + public void prepare(SmartRecycler recycler, List 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; + } + } +} diff --git a/common/src/vanillin/java/dev/engine_room/vanillin/visuals/SignVisual.java b/common/src/vanillin/java/dev/engine_room/vanillin/visuals/SignVisual.java new file mode 100644 index 000000000..c121bbe1c --- /dev/null +++ b/common/src/vanillin/java/dev/engine_room/vanillin/visuals/SignVisual.java @@ -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 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 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 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 list = FONT.split(component, blockEntity.getMaxTextLineWidth()); + return list.isEmpty() ? FormattedCharSequence.EMPTY : list.get(0); + }); + + List 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()); + } +} diff --git a/common/src/vanillin/resources/assets/vanillin/flywheel/instance/cull/glyph.glsl b/common/src/vanillin/resources/assets/vanillin/flywheel/instance/cull/glyph.glsl new file mode 100644 index 000000000..c8d9a275c --- /dev/null +++ b/common/src/vanillin/resources/assets/vanillin/flywheel/instance/cull/glyph.glsl @@ -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); +} diff --git a/common/src/vanillin/resources/assets/vanillin/flywheel/instance/glyph.vert b/common/src/vanillin/resources/assets/vanillin/flywheel/instance/glyph.vert new file mode 100644 index 000000000..6592d5e8c --- /dev/null +++ b/common/src/vanillin/resources/assets/vanillin/flywheel/instance/glyph.vert @@ -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; +} diff --git a/common/src/vanillin/resources/vanillin.mixins.json b/common/src/vanillin/resources/vanillin.mixins.json new file mode 100644 index 000000000..b7f3c9a42 --- /dev/null +++ b/common/src/vanillin/resources/vanillin.mixins.json @@ -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 + } +} diff --git a/vanillinFabric/src/main/java/dev/engine_room/vanillin/FabricVanillinConfig.java b/vanillinFabric/src/main/java/dev/engine_room/vanillin/FabricVanillinConfig.java new file mode 100644 index 000000000..0cb5ef2bc --- /dev/null +++ b/vanillinFabric/src/main/java/dev/engine_room/vanillin/FabricVanillinConfig.java @@ -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 config, Map> 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(); + var entities = new ArrayList(); + + 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 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 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 blockEntities; + public Map entities; + + public Config() { + this(new HashMap<>(), new HashMap<>()); + } + + public Config(Map blockEntities, Map entities) { + this.blockEntities = blockEntities; + this.entities = entities; + } + } +} diff --git a/vanillinFabric/src/main/java/dev/engine_room/vanillin/VanillinFabric.java b/vanillinFabric/src/main/java/dev/engine_room/vanillin/VanillinFabric.java index 8b01ad009..245614932 100644 --- a/vanillinFabric/src/main/java/dev/engine_room/vanillin/VanillinFabric.java +++ b/vanillinFabric/src/main/java/dev/engine_room/vanillin/VanillinFabric.java @@ -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(); } } diff --git a/vanillinFabric/src/main/resources/fabric.mod.json b/vanillinFabric/src/main/resources/fabric.mod.json index 3dd5c0342..38337a4ad 100644 --- a/vanillinFabric/src/main/resources/fabric.mod.json +++ b/vanillinFabric/src/main/resources/fabric.mod.json @@ -22,6 +22,7 @@ ] }, "mixins" : [ + "vanillin.mixins.json" ], "depends" : { "minecraft" : "${minecraft_semver_version_range}", diff --git a/vanillinForge/src/main/java/dev/engine_room/vanillin/ForgeVanillinConfig.java b/vanillinForge/src/main/java/dev/engine_room/vanillin/ForgeVanillinConfig.java new file mode 100644 index 000000000..5504545c2 --- /dev/null +++ b/vanillinForge/src/main/java/dev/engine_room/vanillin/ForgeVanillinConfig.java @@ -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(); + var entities = new ArrayList(); + + 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 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> setup(ForgeConfigSpec.Builder builder, Collection configuredVisuals, String push) { + var out = new HashMap>(); + 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> config, Map> overrides) { + void apply(Collection values) { + for (var configured : values) { + var key = configured.configKey(); + var value = config.get(key); + if (value != null) { + configured.set(value.get(), overrides.get(key)); + } + } + } + } +} diff --git a/vanillinNeoForge/build.gradle.kts b/vanillinNeoForge/build.gradle.kts index 8ce89883c..891fc05d6 100644 --- a/vanillinNeoForge/build.gradle.kts +++ b/vanillinNeoForge/build.gradle.kts @@ -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"))