diff --git a/common/src/main/java/com/xtracr/realcamera/compat/CompatibilityHelper.java b/common/src/main/java/com/xtracr/realcamera/compat/CompatibilityHelper.java index 117d43dd..da0f68d6 100644 --- a/common/src/main/java/com/xtracr/realcamera/compat/CompatibilityHelper.java +++ b/common/src/main/java/com/xtracr/realcamera/compat/CompatibilityHelper.java @@ -21,6 +21,7 @@ public static void initialize(PlatformHelper platformHelper) { CompatibilityHelper.platformHelper = platformHelper; LegacyBindingMode.register(); if (isModLoaded("yes_steve_model")) YSMCompat.register(); + if (isModLoaded("dragonsurvival")) DragonSurvivalCompat.register(); if (isModLoaded("freecam")) try { Class FC_Freecam = Class.forName("net.xolt.freecam.Freecam"); Method FC_Freecam_isEnabled = FC_Freecam.getDeclaredMethod("isEnabled"); diff --git a/common/src/main/java/com/xtracr/realcamera/compat/DragonSurvivalCompat.java b/common/src/main/java/com/xtracr/realcamera/compat/DragonSurvivalCompat.java new file mode 100644 index 00000000..1360efe3 --- /dev/null +++ b/common/src/main/java/com/xtracr/realcamera/compat/DragonSurvivalCompat.java @@ -0,0 +1,346 @@ +package com.xtracr.realcamera.compat; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import com.xtracr.realcamera.RealCamera; +import com.xtracr.realcamera.config.BindTarget; +import com.xtracr.realcamera.config.DisableConfig; +import com.xtracr.realcamera.renderer.state.BuiltModelRecord; +import com.xtracr.realcamera.renderer.state.MutableVertex; +import com.xtracr.realcamera.renderer.state.VertexData; +import com.xtracr.realcamera.util.RenderTypeCache; +import net.minecraft.client.renderer.SubmitNodeCollector.CustomGeometryRenderer; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import org.joml.Matrix4fc; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.BiFunction; + +public final class DragonSurvivalCompat { + private static final String NAMESPACE = "dragonsurvival:"; + private static volatile Method isDragon; + private static volatile Field renderInFirstPerson; + private static volatile Method createUIRenderState; + private static boolean failureWarned; + + private DragonSurvivalCompat() { + } + + static void register() { + try { + Class provider = Class.forName("by.dragonsurvivalteam.dragonsurvival.common.capability.DragonStateProvider"); + isDragon = provider.getMethod("isDragon", Entity.class); + } catch (Exception | LinkageError e) { + failDragonState(e); + } + try { + Class renderer = Class.forName("by.dragonsurvivalteam.dragonsurvival.client.render.ClientDragonRenderer"); + renderInFirstPerson = renderer.getField("renderInFirstPerson"); + } catch (Exception | LinkageError e) { + failOwnership(e); + } + if (isDragon != null && renderInFirstPerson != null) { + DisableHelper.RENDER_MODEL.registerOr(DragonSurvivalCompat::ownsFirstPersonBody); + } + try { + Class renderer = Class.forName("by.dragonsurvivalteam.dragonsurvival.client.render.entity.dragon.DragonRenderer"); + createUIRenderState = renderer.getMethod("createUIRenderState", LivingEntity.class, float.class, double.class, double.class, double.class); + } catch (Exception | LinkageError e) { + failUI(e); + } + } + + private static boolean isDragon(Player player) { + Method method = isDragon; + if (method == null) return false; + try { + return (boolean) method.invoke(null, player); + } catch (Exception | LinkageError e) { + failDragonState(e); + return false; + } + } + + public static @Nullable EntityRenderState createUIRenderState(Entity entity, float partialTicks) { + Method method = createUIRenderState; + if (!(entity instanceof Player player) || method == null || !isDragon(player)) return null; + try { + Object result = method.invoke(null, player, partialTicks, 0.0, + Mth.wrapDegrees(player.yHeadRot - player.yBodyRot), player.getXRot()); + if (!(result instanceof EntityRenderState renderState)) return null; + if (renderState instanceof LivingEntityRenderState livingState) { + livingState.bodyRot = 180.0f; + livingState.yRot = 0.0f; + livingState.xRot = 0.0f; + } + return renderState; + } catch (Exception | LinkageError e) { + failUI(e); + return null; + } + } + + public static boolean applyDisableConfigs( + List records, + BindTarget target, + Set hiddenNames) { + if (!isTarget(target)) return false; + for (int i = 0; i < records.size(); i++) { + BuiltModelRecord record = records.get(i); + boolean dragonLayer = record.textureId().contains(NAMESPACE); + DisableConfig[] configs = target.filteredDisableConfigs(config -> hiddenNames.contains(config.name()) + && (record.containsTextureId(config.textureId()) + || dragonLayer && target.textureId().contains(config.textureId()))); + List primitives = new ArrayList<>(); + if (!disablesAll(configs)) for (VertexData[] primitive : record.primitives()) { + if (!disabled(primitive, configs)) primitives.add(primitive); + } + records.set(i, new BuiltModelRecord( + record.renderType(), + record.textureId(), + record.vertices(), + primitives.toArray(new VertexData[0][]))); + } + return true; + } + + public static List focusCandidates( + List records, + BindTarget target, + String textureId) { + if (!isTarget(target)) return records; + String focusTextureId = textureId.isBlank() ? target.textureId() : textureId; + if (focusTextureId.isBlank()) return records; + List exactMatches = records.stream() + .filter(record -> record.textureId().equals(focusTextureId)) + .toList(); + return exactMatches.isEmpty() + ? records.stream().filter(record -> record.containsTextureId(focusTextureId)).toList() + : exactMatches; + } + + public static @Nullable BiFunction createGeometryFilter( + BindTarget target, + Matrix4fc viewRotationMatrix) { + if (!isTarget(target) || viewRotationMatrix == null) return null; + float m02 = viewRotationMatrix.m02(); + float m12 = viewRotationMatrix.m12(); + float m22 = viewRotationMatrix.m22(); + float m32 = viewRotationMatrix.m32(); + float depth = target.disablingDepth(); + return (renderType, renderer) -> { + String textureId = RenderTypeCache.getTextureId(renderType); + if (renderType.mode() != VertexFormat.Mode.QUADS || !textureId.contains(NAMESPACE)) return renderer; + DisableConfig[] configs = target.filteredDisableConfigs(config -> + textureId.contains(config.textureId()) || target.textureId().contains(config.textureId())); + return (pose, output) -> { + FilteringVertexConsumer filtering = new FilteringVertexConsumer(output, configs, m02, m12, m22, m32, depth); + try { + renderer.render(pose, filtering); + } finally { + filtering.finish(); + } + }; + }; + } + + public static boolean ownsFirstPersonBody(Player player) { + Field field = renderInFirstPerson; + if (field == null || !isDragon(player)) return false; + try { + return Boolean.TRUE.equals(field.get(null)); + } catch (Exception | LinkageError e) { + failOwnership(e); + return false; + } + } + + private static boolean isTarget(BindTarget target) { + return isDragon != null && target != null && !target.isEmpty() && target.textureId().contains(NAMESPACE); + } + + private static boolean disabled(VertexData[] primitive, DisableConfig[] configs) { + for (VertexData vertex : primitive) { + for (DisableConfig config : configs) if (config.disable(vertex)) return true; + } + return false; + } + + private static boolean disablesAll(DisableConfig[] configs) { + for (DisableConfig config : configs) if (config.disableAll()) return true; + return false; + } + + private static synchronized void failDragonState(Throwable throwable) { + isDragon = null; + warn(throwable); + } + + private static synchronized void failOwnership(Throwable throwable) { + renderInFirstPerson = null; + warn(throwable); + } + + private static synchronized void failUI(Throwable throwable) { + createUIRenderState = null; + warn(throwable); + } + + private static void warn(Throwable throwable) { + if (failureWarned) return; + failureWarned = true; + Throwable cause = throwable instanceof InvocationTargetException && throwable.getCause() != null + ? throwable.getCause() + : throwable; + RealCamera.LOGGER.warn("Compatibility with Dragon Survival failed: [{}] {}", cause.getClass().getName(), cause.getMessage()); + } + + private static final class FilteringVertexConsumer implements VertexConsumer { + private final VertexConsumer output; + private final DisableConfig[] disableConfigs; + private final MutableVertex[] quad = { + VertexData.mutable(), VertexData.mutable(), VertexData.mutable(), VertexData.mutable() + }; + private final float m02; + private final float m12; + private final float m22; + private final float m32; + private final float depth; + private final boolean disableAll; + private int vertexCount; + + private FilteringVertexConsumer( + VertexConsumer output, + DisableConfig[] disableConfigs, + float m02, + float m12, + float m22, + float m32, + float depth) { + this.output = output; + this.disableConfigs = disableConfigs; + this.m02 = m02; + this.m12 = m12; + this.m22 = m22; + this.m32 = m32; + this.depth = depth; + this.disableAll = disablesAll(disableConfigs); + } + + @Override + public void addVertex( + float x, + float y, + float z, + int color, + float u, + float v, + int overlay, + int light, + float normalX, + float normalY, + float normalZ) { + MutableVertex vertex = quad[vertexCount++]; + vertex.x = x; + vertex.y = y; + vertex.z = z; + vertex.argb = color; + vertex.u = u; + vertex.v = v; + vertex.overlay = overlay; + vertex.light = light; + vertex.normalX = normalX; + vertex.normalY = normalY; + vertex.normalZ = normalZ; + if (vertexCount == quad.length) flushQuad(); + } + + @Override + public VertexConsumer addVertex(float x, float y, float z) { + flushTail(); + return output.addVertex(x, y, z); + } + + @Override + public VertexConsumer setColor(int red, int green, int blue, int alpha) { + flushTail(); + return output.setColor(red, green, blue, alpha); + } + + @Override + public VertexConsumer setColor(int color) { + flushTail(); + return output.setColor(color); + } + + @Override + public VertexConsumer setUv(float u, float v) { + flushTail(); + return output.setUv(u, v); + } + + @Override + public VertexConsumer setUv1(int u, int v) { + flushTail(); + return output.setUv1(u, v); + } + + @Override + public VertexConsumer setUv2(int u, int v) { + flushTail(); + return output.setUv2(u, v); + } + + @Override + public VertexConsumer setNormal(float x, float y, float z) { + flushTail(); + return output.setNormal(x, y, z); + } + + @Override + public VertexConsumer setLineWidth(float width) { + flushTail(); + return output.setLineWidth(width); + } + + private void finish() { + flushTail(); + } + + private void flushQuad() { + boolean disabled = disableAll; + boolean allTooClose = true; + for (MutableVertex vertex : quad) { + float cameraZ = Math.fma(m02, vertex.x, Math.fma(m12, vertex.y, Math.fma(m22, vertex.z, m32))); + if (cameraZ <= -depth) allTooClose = false; + if (!disabled) { + for (DisableConfig config : disableConfigs) { + if (config.disable(vertex)) { + disabled = true; + break; + } + } + } + } + if (!disabled && !allTooClose) for (MutableVertex vertex : quad) vertex.render(output); + vertexCount = 0; + } + + private void flushTail() { + for (int i = 0; i < vertexCount; i++) quad[i].render(output); + vertexCount = 0; + } + } +} diff --git a/common/src/main/java/com/xtracr/realcamera/gui/ModelAnalyser.java b/common/src/main/java/com/xtracr/realcamera/gui/ModelAnalyser.java index f963e14d..d19c8395 100644 --- a/common/src/main/java/com/xtracr/realcamera/gui/ModelAnalyser.java +++ b/common/src/main/java/com/xtracr/realcamera/gui/ModelAnalyser.java @@ -5,6 +5,7 @@ import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexFormat; import com.xtracr.realcamera.api.BindResult; +import com.xtracr.realcamera.compat.DragonSurvivalCompat; import com.xtracr.realcamera.config.BindTarget; import com.xtracr.realcamera.config.BindTarget.TargetConfig; import com.xtracr.realcamera.config.DisableConfig; @@ -17,6 +18,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.client.renderer.rendertype.RenderType; import net.minecraft.client.renderer.rendertype.RenderTypes; import net.minecraft.client.renderer.state.level.CameraRenderState; @@ -44,6 +46,7 @@ public final class ModelAnalyser { private BuiltModelRecord focusedRecord; public static void applyDisableConfigs(List modelRecords, BindTarget target, String textureId, Set hiddenNames) { + if (DragonSurvivalCompat.applyDisableConfigs(modelRecords, target, hiddenNames)) return; for (int i = 0; i < modelRecords.size(); i++) { BuiltModelRecord record = modelRecords.get(i); List primitives = new ArrayList<>(); @@ -241,7 +244,9 @@ public void drawFocusedInTextureArea(GuiGraphicsExtractor graphics, Matrix4f tex public List captureModel(Minecraft client, Entity entity, float partialTicks, PoseStack poseStack, BindTarget target) { EntityRenderDispatcher dispatcher = client.getEntityRenderDispatcher(); client.gameRenderer.getLighting().setupFor(Lighting.Entry.ENTITY_IN_UI); - dispatcher.submit(dispatcher.extractEntity(entity, partialTicks), new CameraRenderState(), 0, 0, 0, poseStack, vertexCatcher.initCollector()); + EntityRenderState renderState = DragonSurvivalCompat.createUIRenderState(entity, partialTicks); + if (renderState == null) renderState = dispatcher.extractEntity(entity, partialTicks); + dispatcher.submit(renderState, new CameraRenderState(), 0, 0, 0, poseStack, vertexCatcher.initCollector()); List records = new ArrayList<>(); vertexCatcher.forEachBuffer(buf -> computeRecord(buf, records, target)); return records; diff --git a/common/src/main/java/com/xtracr/realcamera/gui/ModelViewScreen.java b/common/src/main/java/com/xtracr/realcamera/gui/ModelViewScreen.java index eeaeba5f..a2e601b2 100644 --- a/common/src/main/java/com/xtracr/realcamera/gui/ModelViewScreen.java +++ b/common/src/main/java/com/xtracr/realcamera/gui/ModelViewScreen.java @@ -7,6 +7,7 @@ import com.mojang.serialization.DataResult; import com.xtracr.realcamera.RealCameraCore; import com.xtracr.realcamera.compat.CompatibilityHelper; +import com.xtracr.realcamera.compat.DragonSurvivalCompat; import com.xtracr.realcamera.config.*; import com.xtracr.realcamera.config.BindTarget.BindConfig; import com.xtracr.realcamera.config.BindTarget.TargetConfig; @@ -399,7 +400,8 @@ public void extractBackground(@NonNull GuiGraphicsExtractor graphics, int mouseX List modelRecords = captureRotatedEntity(analyser, target, minecraft.player); List textureRecords = modelRecords.stream().filter(record -> record.containsTextureId(textureId)).toList(); ModelAnalyser.applyDisableConfigs(modelRecords, target, textureId, hiddenNames); - computeFocusedPrimitives(analyser, modelRecords, textureRecords, mouseX, mouseY); + List focusedModelRecords = DragonSurvivalCompat.focusCandidates(modelRecords, target, textureId); + computeFocusedPrimitives(analyser, focusedModelRecords, textureRecords, mouseX, mouseY); renderCulledModels(graphics, analyser, target, modelRecords); renderFlattenedModels(graphics, analyser, textureRecords); } diff --git a/common/src/main/java/com/xtracr/realcamera/mixin/MixinDragonSurvivalCompat.java b/common/src/main/java/com/xtracr/realcamera/mixin/MixinDragonSurvivalCompat.java new file mode 100644 index 00000000..9830384e --- /dev/null +++ b/common/src/main/java/com/xtracr/realcamera/mixin/MixinDragonSurvivalCompat.java @@ -0,0 +1,17 @@ +package com.xtracr.realcamera.mixin; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.xtracr.realcamera.RealCameraCore; +import com.xtracr.realcamera.compat.CompatibilityHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; + +@Pseudo +@Mixin(targets = "by.dragonsurvivalteam.dragonsurvival.compat.Compat", remap = false) +public abstract class MixinDragonSurvivalCompat { + @ModifyReturnValue(method = "displayNeck()Z", at = @At("RETURN"), remap = false, require = 0, expect = 1) + private static boolean realcamera$displayDragonNeck(boolean original) { + return original || RealCameraCore.isActive() || CompatibilityHelper.isRenderInScreen; + } +} diff --git a/common/src/main/java/com/xtracr/realcamera/renderer/RoutingSubmitCollector.java b/common/src/main/java/com/xtracr/realcamera/renderer/RoutingSubmitCollector.java index eac3cbaf..3fd46351 100644 --- a/common/src/main/java/com/xtracr/realcamera/renderer/RoutingSubmitCollector.java +++ b/common/src/main/java/com/xtracr/realcamera/renderer/RoutingSubmitCollector.java @@ -25,14 +25,25 @@ import org.jspecify.annotations.Nullable; import java.util.List; +import java.util.function.BiFunction; public final class RoutingSubmitCollector implements SubmitNodeCollector { + private static final BiFunction IDENTITY_DECORATOR = (_, renderer) -> renderer; private final SubmitNodeCollector defaultCollector; private final SubmitNodeCollector modelCollector; + private final BiFunction customGeometryDecorator; public RoutingSubmitCollector(SubmitNodeCollector defaultCollector, SubmitNodeCollector modelCollector) { + this(defaultCollector, modelCollector, IDENTITY_DECORATOR); + } + + public RoutingSubmitCollector( + SubmitNodeCollector defaultCollector, + SubmitNodeCollector modelCollector, + BiFunction customGeometryDecorator) { this.defaultCollector = defaultCollector; this.modelCollector = modelCollector; + this.customGeometryDecorator = customGeometryDecorator; } @Override @@ -97,7 +108,7 @@ public void submitItem(@NonNull PoseStack poseStack, @NonNull ItemDisplayContext @Override public void submitCustomGeometry(@NonNull PoseStack poseStack, @NonNull RenderType renderType, @NonNull CustomGeometryRenderer customGeometryRenderer) { - modelCollector.submitCustomGeometry(poseStack, renderType, customGeometryRenderer); + modelCollector.submitCustomGeometry(poseStack, renderType, customGeometryDecorator.apply(renderType, customGeometryRenderer)); } @Override @@ -171,7 +182,7 @@ public void submitItem(@NonNull PoseStack poseStack, @NonNull ItemDisplayContext @Override public void submitCustomGeometry(@NonNull PoseStack poseStack, @NonNull RenderType renderType, @NonNull CustomGeometryRenderer customGeometryRenderer) { - modelDelegate.submitCustomGeometry(poseStack, renderType, customGeometryRenderer); + modelDelegate.submitCustomGeometry(poseStack, renderType, customGeometryDecorator.apply(renderType, customGeometryRenderer)); } @Override diff --git a/common/src/main/resources/realcamera-common.mixins.json b/common/src/main/resources/realcamera-common.mixins.json index fea92b3d..93f0a1db 100644 --- a/common/src/main/resources/realcamera-common.mixins.json +++ b/common/src/main/resources/realcamera-common.mixins.json @@ -7,6 +7,7 @@ ], "client": [ "MixinCamera", + "MixinDragonSurvivalCompat", "MixinFishingHookRenderer", "MixinGameRenderer", "MixinGui", diff --git a/neoforge/src/main/java/com/xtracr/realcamera/mixin/MixinAvatarRenderer.java b/neoforge/src/main/java/com/xtracr/realcamera/mixin/MixinAvatarRenderer.java new file mode 100644 index 00000000..472bb1fe --- /dev/null +++ b/neoforge/src/main/java/com/xtracr/realcamera/mixin/MixinAvatarRenderer.java @@ -0,0 +1,63 @@ +package com.xtracr.realcamera.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.mojang.blaze3d.vertex.PoseStack; +import com.xtracr.realcamera.RealCameraCore; +import com.xtracr.realcamera.compat.CompatibilityHelper; +import com.xtracr.realcamera.compat.DragonSurvivalCompat; +import com.xtracr.realcamera.config.ConfigFile; +import com.xtracr.realcamera.renderer.RoutingSubmitCollector; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.SubmitNodeCollector.CustomGeometryRenderer; +import net.minecraft.client.renderer.entity.player.AvatarRenderer; +import net.minecraft.client.renderer.entity.state.AvatarRenderState; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.world.entity.player.Player; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.jspecify.annotations.Nullable; + +import java.util.function.BiFunction; + +@Mixin(AvatarRenderer.class) +public abstract class MixinAvatarRenderer { + @WrapMethod(method = "submit(Lnet/minecraft/client/renderer/entity/state/AvatarRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;Lnet/minecraft/client/renderer/state/level/CameraRenderState;)V") + private void realcamera$filterDragonGeometry( + AvatarRenderState renderState, + PoseStack poseStack, + SubmitNodeCollector output, + CameraRenderState cameraRenderState, + Operation original) { + BiFunction filter = realcamera$dragonFilter(renderState, cameraRenderState); + if (filter == null) { + original.call(renderState, poseStack, output, cameraRenderState); + return; + } + + SubmitNodeCollector routing = new RoutingSubmitCollector(output, output, filter); + original.call(renderState, poseStack, routing, cameraRenderState); + } + + @Unique + private static @Nullable BiFunction realcamera$dragonFilter( + AvatarRenderState renderState, + CameraRenderState cameraRenderState) { + if (!RealCameraCore.isActive() + || ConfigFile.config().isClassic + || !ConfigFile.config().renderModel + || CompatibilityHelper.isRenderInScreen) return null; + + Minecraft client = Minecraft.getInstance(); + if (cameraRenderState != client.gameRenderer.getGameRenderState().levelRenderState.cameraRenderState) return null; + Player player = client.player; + if (player == null + || client.getCameraEntity() != player + || renderState.id != player.getId() + || !DragonSurvivalCompat.ownsFirstPersonBody(player)) return null; + + return DragonSurvivalCompat.createGeometryFilter(RealCameraCore.currentTarget(), cameraRenderState.viewRotationMatrix); + } +} diff --git a/neoforge/src/main/resources/realcamera.mixins.json b/neoforge/src/main/resources/realcamera.mixins.json index 6e60890b..a205d277 100644 --- a/neoforge/src/main/resources/realcamera.mixins.json +++ b/neoforge/src/main/resources/realcamera.mixins.json @@ -6,6 +6,7 @@ "mixins": [ ], "client": [ + "MixinAvatarRenderer" ], "server": [ ],