diff --git a/common/src/main/java/me/collinb/dynamicview/DynamicView.java b/common/src/main/java/me/collinb/dynamicview/DynamicView.java index 3512ad2..69ee339 100644 --- a/common/src/main/java/me/collinb/dynamicview/DynamicView.java +++ b/common/src/main/java/me/collinb/dynamicview/DynamicView.java @@ -1,58 +1,122 @@ package me.collinb.dynamicview; import me.collinb.dynamicview.camera.CameraAnimation; +import me.collinb.dynamicview.camera.CameraContext; import me.collinb.dynamicview.config.ModConfig; import me.collinb.dynamicview.platform.Services; -import me.shedaniel.autoconfig.AutoConfig; import net.minecraft.client.CameraType; import net.minecraft.client.Minecraft; +import net.minecraft.client.Options; + +import java.util.EnumMap; +import java.util.Iterator; public class DynamicView { - private static final ModConfig config = AutoConfig.getConfigHolder(ModConfig.class).get(); + private static final EnumMap activeContexts = new EnumMap<>(CameraContext.class); + /** Non-null while the mod controls the camera; holds the perspective to restore. */ private static CameraType previousCameraType; + /** The perspective the mod last applied, used to detect manual (F5) changes. */ + private static CameraType appliedCameraType; + + public static void init() { + Constants.LOG.debug("Hello from Common init on {}! we are currently in a {} environment!", Services.PLATFORM.getPlatformName(), Services.PLATFORM.getEnvironmentName()); + } + + public static void preTick(Minecraft mc) { + if (previousCameraType != null && mc.options.getCameraType() != appliedCameraType) { + // The user changed perspective manually; respect it and stop managing + // the camera until the active contexts change again. + relinquishCamera(); + } + CameraAnimation.INSTANCE.tick(); + } - public static void setCameraType(CameraType cameraType) { - if (getMC().options.getCameraType() != cameraType) { - previousCameraType = getMC().options.getCameraType(); - if (cameraType != null) { - getMC().options.setCameraType(cameraType); + /** + * Reports whether a context currently applies. Safe to call every tick; + * the camera is only touched when a context starts or stops. + */ + public static void setContextActive(CameraContext context, boolean active, CameraType camera) { + if (active) { + if (activeContexts.put(context, camera) != camera) { + applyContexts(); } - CameraAnimation.INSTANCE.currentDistance = 0.0f; - CameraAnimation.INSTANCE.targetDistance = 4.0f; - } - } - - public static void unsetCameraType() { - if (isCameraDynamic()) { - if (previousCameraType != getMC().options.getCameraType()) { - CameraAnimation.INSTANCE.targetDistance = 0.0f; - if (config.animationEnabled) { - CameraAnimation.INSTANCE.onAnimationComplete = () -> { - getMC().options.setCameraType(previousCameraType); - previousCameraType = null; - }; - } else { - getMC().options.setCameraType(previousCameraType); - previousCameraType = null; - } + } else if (activeContexts.remove(context) != null) { + applyContexts(); + } + } + + private static void applyContexts() { + // EnumMap iterates in declaration order, so the first entry is the + // highest-priority active context. + Iterator it = activeContexts.values().iterator(); + if (it.hasNext()) { + applyCamera(it.next()); + } else { + restoreCamera(); + } + } + + private static void applyCamera(CameraType desired) { + Options options = getMC().options; + CameraType current = options.getCameraType(); + if (previousCameraType == null) { + if (current == desired) { + // The player is already in the desired perspective; leave it alone. + return; } + previousCameraType = current; + } + if (current != desired) { + options.setCameraType(desired); + appliedCameraType = desired; + } + if (!ModConfig.get().animationEnabled || desired == CameraType.FIRST_PERSON) { + CameraAnimation.INSTANCE.reset(); + } else if (current == CameraType.FIRST_PERSON) { + CameraAnimation.INSTANCE.startEnter(); + } else { + // Already in third person, possibly mid-exit; cancel any pending restore. + CameraAnimation.INSTANCE.resumeEnter(); } } - public static void init() { - Constants.LOG.debug("Hello from Common init on {}! we are currently in a {} environment!", Services.PLATFORM.getPlatformName(), Services.PLATFORM.getEnvironmentName()); + private static void restoreCamera() { + if (previousCameraType == null) { + return; + } + Options options = getMC().options; + CameraType current = options.getCameraType(); + CameraType restoreTo = previousCameraType; + boolean animated = ModConfig.get().animationEnabled; + if (current == restoreTo) { + relinquishCamera(); + } else if (animated && restoreTo == CameraType.FIRST_PERSON && current != CameraType.FIRST_PERSON) { + // Zoom all the way in before switching back to first person. + CameraAnimation.INSTANCE.startExit(() -> { + options.setCameraType(restoreTo); + previousCameraType = null; + appliedCameraType = null; + }); + } else { + options.setCameraType(restoreTo); + previousCameraType = null; + appliedCameraType = null; + if (animated && current == CameraType.FIRST_PERSON && restoreTo != CameraType.FIRST_PERSON) { + CameraAnimation.INSTANCE.startEnter(); + } else { + CameraAnimation.INSTANCE.reset(); + } + } } - public static void preTick(Minecraft mc) { - CameraAnimation.INSTANCE.tick(); + private static void relinquishCamera() { + previousCameraType = null; + appliedCameraType = null; + CameraAnimation.INSTANCE.reset(); } public static Minecraft getMC() { return Minecraft.getInstance(); } - - public static boolean isCameraDynamic() { - return previousCameraType != null; - } } diff --git a/common/src/main/java/me/collinb/dynamicview/camera/CameraAnimation.java b/common/src/main/java/me/collinb/dynamicview/camera/CameraAnimation.java index 05455fa..9dcedd4 100644 --- a/common/src/main/java/me/collinb/dynamicview/camera/CameraAnimation.java +++ b/common/src/main/java/me/collinb/dynamicview/camera/CameraAnimation.java @@ -1,48 +1,88 @@ package me.collinb.dynamicview.camera; import me.collinb.dynamicview.config.ModConfig; -import me.shedaniel.autoconfig.AutoConfig; +import net.minecraft.util.Mth; +/** + * Animates the third-person camera distance as a fraction of the vanilla zoom, + * where 0 places the camera at the player and 1 is the full distance. + */ public final class CameraAnimation { - public static CameraAnimation INSTANCE = new CameraAnimation(); + public static final CameraAnimation INSTANCE = new CameraAnimation(); - private final ModConfig config; + private static final float SNAP_THRESHOLD = 0.01f; - public float previousDistance = 0.0f; - public float currentDistance = 0.0f; - public float targetDistance = 4.0f; + private float previousProgress = 1.0f; + private float currentProgress = 1.0f; + private float targetProgress = 1.0f; - public Runnable onAnimationComplete = null; + private Runnable onComplete; - public CameraAnimation() { - this.config = AutoConfig.getConfigHolder(ModConfig.class).get(); + private CameraAnimation() { + } + + /** Zooms out from the player to the full third-person distance. */ + public void startEnter() { + previousProgress = 0.0f; + currentProgress = 0.0f; + targetProgress = 1.0f; + onComplete = null; + } + + /** + * Re-targets the full distance from wherever the camera currently is, + * cancelling a pending exit and its completion callback. + */ + public void resumeEnter() { + targetProgress = 1.0f; + onComplete = null; + } + + /** Zooms in towards the player, then runs {@code onComplete}. */ + public void startExit(Runnable onComplete) { + targetProgress = 0.0f; + this.onComplete = onComplete; + } + + /** Stops any animation, discarding a pending completion callback. */ + public void reset() { + previousProgress = 1.0f; + currentProgress = 1.0f; + targetProgress = 1.0f; + onComplete = null; } public void tick() { + previousProgress = currentProgress; + if (currentProgress == targetProgress) { + return; + } + ModConfig config = ModConfig.get(); if (!config.animationEnabled) { - if (currentDistance != targetDistance) { - currentDistance = targetDistance; - previousDistance = currentDistance; - if (onAnimationComplete != null) { - onAnimationComplete.run(); - onAnimationComplete = null; - } - } + finish(); return; } - previousDistance = currentDistance; - - float speed = (targetDistance > currentDistance) ? config.animationEnterEasing : config.animationExitEasing; - currentDistance += (targetDistance - currentDistance) * speed; + int speed = (targetProgress > currentProgress) ? config.animationEnterSpeed : config.animationExitSpeed; + currentProgress += (targetProgress - currentProgress) * (speed / 100.0f); + if (Math.abs(currentProgress - targetProgress) < SNAP_THRESHOLD) { + finish(); + } + } - if (Math.abs(currentDistance - targetDistance) < 0.1f && onAnimationComplete != null) { - onAnimationComplete.run(); - onAnimationComplete = null; - this.currentDistance = this.targetDistance; + private void finish() { + currentProgress = targetProgress; + if (onComplete != null) { + Runnable callback = onComplete; + onComplete = null; + callback.run(); } } - public boolean isCameraAnimating() { - return this.currentDistance != this.targetDistance; + public boolean isAnimating() { + return currentProgress != targetProgress; + } + + public float getProgress(float partialTick) { + return Mth.lerp(partialTick, previousProgress, currentProgress); } } diff --git a/common/src/main/java/me/collinb/dynamicview/camera/CameraContext.java b/common/src/main/java/me/collinb/dynamicview/camera/CameraContext.java new file mode 100644 index 0000000..7e92dbf --- /dev/null +++ b/common/src/main/java/me/collinb/dynamicview/camera/CameraContext.java @@ -0,0 +1,13 @@ +package me.collinb.dynamicview.camera; + +/** + * A gameplay situation that can take control of the camera. Declaration order + * is priority order: when several contexts are active at once, the earliest + * declared one decides the perspective. + */ +public enum CameraContext { + RIDING, + FLYING, + SWIMMING, + CRAWLING +} diff --git a/common/src/main/java/me/collinb/dynamicview/config/ModConfig.java b/common/src/main/java/me/collinb/dynamicview/config/ModConfig.java index d107be7..fbcb2ae 100644 --- a/common/src/main/java/me/collinb/dynamicview/config/ModConfig.java +++ b/common/src/main/java/me/collinb/dynamicview/config/ModConfig.java @@ -12,8 +12,14 @@ @Config(name = Constants.MOD_ID) public class ModConfig implements ConfigData { + private static ConfigHolder holder; + public static void init() { - ConfigHolder holder = AutoConfig.register(ModConfig.class, GsonConfigSerializer::new); + holder = AutoConfig.register(ModConfig.class, GsonConfigSerializer::new); + } + + public static ModConfig get() { + return holder.get(); } @ConfigEntry.Gui.CollapsibleObject @@ -40,9 +46,20 @@ public static class Contexts { public boolean animationEnabled = true; - @ConfigEntry.BoundedDiscrete(min = 0, max = 10) - public float animationEnterEasing = 0.2f; + // Percentage of the remaining distance covered each tick; 100 is instant. + @ConfigEntry.BoundedDiscrete(min = 1, max = 100) + public int animationEnterSpeed = 20; + + @ConfigEntry.BoundedDiscrete(min = 1, max = 100) + public int animationExitSpeed = 60; - @ConfigEntry.BoundedDiscrete(min = 0, max = 10) - public float animationExitEasing = 0.6f; + @Override + public void validatePostLoad() { + animationEnterSpeed = clampSpeed(animationEnterSpeed); + animationExitSpeed = clampSpeed(animationExitSpeed); + } + + private static int clampSpeed(int speed) { + return Math.max(1, Math.min(100, speed)); + } } diff --git a/common/src/main/java/me/collinb/dynamicview/mixin/CameraMixin.java b/common/src/main/java/me/collinb/dynamicview/mixin/CameraMixin.java index 7d560dc..6ddf54a 100644 --- a/common/src/main/java/me/collinb/dynamicview/mixin/CameraMixin.java +++ b/common/src/main/java/me/collinb/dynamicview/mixin/CameraMixin.java @@ -1,37 +1,25 @@ package me.collinb.dynamicview.mixin; -import me.collinb.dynamicview.DynamicView; import me.collinb.dynamicview.camera.CameraAnimation; -import me.collinb.dynamicview.config.ModConfig; -import me.shedaniel.autoconfig.AutoConfig; import net.minecraft.client.Camera; -import net.minecraft.util.Mth; +import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import static me.collinb.dynamicview.DynamicView.getMC; - @Mixin(Camera.class) public abstract class CameraMixin { @Inject(method = "getMaxZoom", at = @At("TAIL"), cancellable = true) private void useSmoothZooming(float pMaxZoom, CallbackInfoReturnable cir) { - if (!CameraAnimation.INSTANCE.isCameraAnimating()) { + CameraAnimation animation = CameraAnimation.INSTANCE; + if (!animation.isAnimating()) { return; } - ModConfig config = AutoConfig.getConfigHolder(ModConfig.class).get(); - if (DynamicView.isCameraDynamic() && config.animationEnabled) { - float partial = getMC().getDeltaTracker().getGameTimeDeltaPartialTick(true); - float smoothDistance = Mth.lerp( - partial, - CameraAnimation.INSTANCE.previousDistance, - CameraAnimation.INSTANCE.currentDistance - ); - if (smoothDistance <= cir.getReturnValue()) { - cir.setReturnValue(smoothDistance); - } - } + float partialTick = Minecraft.getInstance().getDeltaTracker().getGameTimeDeltaPartialTick(true); + // Scale the vanilla (collision-clamped) distance so the animation + // respects walls and any other mod that changes the zoom distance. + cir.setReturnValue(animation.getProgress(partialTick) * cir.getReturnValue()); } } diff --git a/common/src/main/java/me/collinb/dynamicview/mixin/EntityAccessor.java b/common/src/main/java/me/collinb/dynamicview/mixin/EntityAccessor.java deleted file mode 100644 index 46f9092..0000000 --- a/common/src/main/java/me/collinb/dynamicview/mixin/EntityAccessor.java +++ /dev/null @@ -1,15 +0,0 @@ -package me.collinb.dynamicview.mixin; - -import net.minecraft.network.syncher.EntityDataAccessor; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.Pose; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; - -@Mixin(Entity.class) -public abstract class EntityAccessor { - @Shadow - @Final - protected static EntityDataAccessor DATA_POSE; -} diff --git a/common/src/main/java/me/collinb/dynamicview/mixin/LocalPlayerMixin.java b/common/src/main/java/me/collinb/dynamicview/mixin/LocalPlayerMixin.java index 7d5a989..e446f33 100644 --- a/common/src/main/java/me/collinb/dynamicview/mixin/LocalPlayerMixin.java +++ b/common/src/main/java/me/collinb/dynamicview/mixin/LocalPlayerMixin.java @@ -1,63 +1,39 @@ package me.collinb.dynamicview.mixin; import me.collinb.dynamicview.DynamicView; +import me.collinb.dynamicview.camera.CameraContext; import me.collinb.dynamicview.config.ModConfig; -import me.shedaniel.autoconfig.AutoConfig; import net.minecraft.client.player.LocalPlayer; -import net.minecraft.network.syncher.EntityDataAccessor; -import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.Pose; 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.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(LocalPlayer.class) public abstract class LocalPlayerMixin { - @Unique - private boolean dynamicView$wasSwimming = false; - - @Inject(method = "startRiding", at = @At("TAIL")) - private void startRiding(Entity p_108667_, boolean p_108668_, boolean p_435382_, CallbackInfoReturnable cir) { - ModConfig config = AutoConfig.getConfigHolder(ModConfig.class).get(); - if (config.contexts.ridingEnabled) { - DynamicView.setCameraType(config.contexts.ridingCamera); - } - } - - @Inject(method = "removeVehicle", at = @At("TAIL")) - private void removeVehicle(CallbackInfo ci) { - ModConfig config = AutoConfig.getConfigHolder(ModConfig.class).get(); - if (config.contexts.ridingEnabled) { - DynamicView.unsetCameraType(); - } - } - - @Inject(method = "onSyncedDataUpdated", at = @At("TAIL")) - private void updatePose(EntityDataAccessor pKey, CallbackInfo ci) { - ModConfig config = AutoConfig.getConfigHolder(ModConfig.class).get(); - - if (pKey.id() != EntityAccessor.DATA_POSE.id()) return; - + // Polling once per tick (instead of hooking mount/dismount and pose data + // updates) catches every way a context can start or stop: failed mount + // attempts, leaving water while still in the swimming pose, config changes + // mid-context, respawning, and so on. + @Inject(method = "tick", at = @At("TAIL")) + private void dynamicView$updateContexts(CallbackInfo ci) { LocalPlayer player = (LocalPlayer) (Object) this; - - if (player.getPose() == Pose.SWIMMING) { - if (player.isInWater() && config.contexts.swimmingEnabled) { - DynamicView.setCameraType(config.contexts.swimmingCamera); - dynamicView$wasSwimming = true; - } else if (!player.isInWater() && config.contexts.crawlingEnabled) { - DynamicView.setCameraType(config.contexts.crawlingCamera); - dynamicView$wasSwimming = true; - } - } else if (player.getPose() == Pose.FALL_FLYING && config.contexts.flyingEnabled) { - DynamicView.setCameraType(config.contexts.flyingCamera); - dynamicView$wasSwimming = true; - } else if (dynamicView$wasSwimming) { - DynamicView.unsetCameraType(); - dynamicView$wasSwimming = false; - } + ModConfig.Contexts contexts = ModConfig.get().contexts; + boolean swimmingPose = player.getPose() == Pose.SWIMMING; + + DynamicView.setContextActive(CameraContext.RIDING, + contexts.ridingEnabled && player.getVehicle() != null, + contexts.ridingCamera); + DynamicView.setContextActive(CameraContext.FLYING, + contexts.flyingEnabled && player.getPose() == Pose.FALL_FLYING, + contexts.flyingCamera); + DynamicView.setContextActive(CameraContext.SWIMMING, + contexts.swimmingEnabled && swimmingPose && player.isInWater(), + contexts.swimmingCamera); + DynamicView.setContextActive(CameraContext.CRAWLING, + contexts.crawlingEnabled && swimmingPose && !player.isInWater(), + contexts.crawlingCamera); } } diff --git a/common/src/main/resources/assets/dynamicview/lang/en_us.json b/common/src/main/resources/assets/dynamicview/lang/en_us.json index 44ded66..d447c9c 100644 --- a/common/src/main/resources/assets/dynamicview/lang/en_us.json +++ b/common/src/main/resources/assets/dynamicview/lang/en_us.json @@ -9,6 +9,6 @@ "text.autoconfig.dynamicview.option.contexts.ridingEnabled": "Riding Enabled", "text.autoconfig.dynamicview.option.contexts.ridingCamera": "Riding Camera", "text.autoconfig.dynamicview.option.animationEnabled": "Enable Zoom Animation", - "text.autoconfig.dynamicview.option.animationEnterEasing": "Animation Enter Easing", - "text.autoconfig.dynamicview.option.animationExitEasing": "Animation Exit Easing" + "text.autoconfig.dynamicview.option.animationEnterSpeed": "Enter Animation Speed (%)", + "text.autoconfig.dynamicview.option.animationExitSpeed": "Exit Animation Speed (%)" } \ No newline at end of file diff --git a/common/src/main/resources/dynamicview.mixins.json b/common/src/main/resources/dynamicview.mixins.json index 8e9f2f7..965e2d0 100644 --- a/common/src/main/resources/dynamicview.mixins.json +++ b/common/src/main/resources/dynamicview.mixins.json @@ -2,11 +2,9 @@ "required": true, "minVersion": "0.8", "package": "me.collinb.dynamicview.mixin", - "refmap": "${mod_id}.refmap.json", - "compatibilityLevel": "JAVA_18", + "compatibilityLevel": "JAVA_25", "client": [ "CameraMixin", - "EntityAccessor", "LocalPlayerMixin" ], "server": [], diff --git a/fabric/src/main/resources/dynamicview.fabric.mixins.json b/fabric/src/main/resources/dynamicview.fabric.mixins.json deleted file mode 100644 index c45d588..0000000 --- a/fabric/src/main/resources/dynamicview.fabric.mixins.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "required": true, - "minVersion": "0.8", - "package": "me.collinb.dynamicview.mixin", - "refmap": "${mod_id}.refmap.json", - "compatibilityLevel": "JAVA_25", - "mixins": [], - "client": [], - "server": [], - "injectors": { - "defaultRequire": 1 - } -} diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 4ad4f10..83e6534 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -23,16 +23,12 @@ ] }, "mixins": [ - "${mod_id}.mixins.json", - "${mod_id}.fabric.mixins.json" + "${mod_id}.mixins.json" ], "depends": { "fabricloader": ">=${fabric_loader_version}", "fabric-api": "*", "minecraft": "~${minecraft_version}", "java": ">=${java_version}" - }, - "suggests": { - "another-mod": "*" } } diff --git a/gradle.properties b/gradle.properties index 8341855..65e00c6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Important Notes: # Every field you add must be added to the root build.gradle expandProps map. # Project -version=1.1.0 +version=1.2.0 group=me.collinb java_version=25 # Common @@ -16,9 +16,9 @@ minecraft_version_range=[26.1, 26.2) neo_form_version=26.1-1 # Fabric, see https://fabricmc.net/develop/ for new versions fabric_version=0.145.1+26.1 -fabric_loader_version=0.18.6 +fabric_loader_version=0.19.3 # NeoForge, see https://projects.neoforged.net/neoforged/neoforge for new versions -neoforge_version=26.1.2.7-beta +neoforge_version=26.1.2.76 neoforge_loader_version_range=[4,) # Configs modmenu_version=18.0.0-alpha.8 diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml index cdd0a53..f2d877a 100644 --- a/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -14,8 +14,6 @@ authors = "${mod_author}" #optional description = '''${description}''' #mandatory Supports multiline text [[mixins]] config = "${mod_id}.mixins.json" -[[mixins]] -config = "${mod_id}.neoforge.mixins.json" [[dependencies."${mod_id}"]] #optional modId = "neoforge" #mandatory type = "required" #mandatory Can be one of "required", "optional", "incompatible" or "discouraged" diff --git a/neoforge/src/main/resources/dynamicview.neoforge.mixins.json b/neoforge/src/main/resources/dynamicview.neoforge.mixins.json deleted file mode 100644 index 3ff014b..0000000 --- a/neoforge/src/main/resources/dynamicview.neoforge.mixins.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "required": true, - "minVersion": "0.8", - "package": "me.collinb.dynamicview.mixin", - "compatibilityLevel": "JAVA_25", - "mixins": [], - "client": [], - "server": [], - "injectors": { - "defaultRequire": 1 - } -}