Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 97 additions & 33 deletions common/src/main/java/me/collinb/dynamicview/DynamicView.java
Original file line number Diff line number Diff line change
@@ -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<CameraContext, CameraType> 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<CameraType> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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();

Check warning on line 11 in common/src/main/java/me/collinb/dynamicview/camera/CameraAnimation.java

View workflow job for this annotation

GitHub Actions / build

no comment

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) {

Check warning on line 42 in common/src/main/java/me/collinb/dynamicview/camera/CameraAnimation.java

View workflow job for this annotation

GitHub Actions / build

no @param for 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() {

Check warning on line 55 in common/src/main/java/me/collinb/dynamicview/camera/CameraAnimation.java

View workflow job for this annotation

GitHub Actions / build

no comment
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() {

Check warning on line 81 in common/src/main/java/me/collinb/dynamicview/camera/CameraAnimation.java

View workflow job for this annotation

GitHub Actions / build

no comment
return currentProgress != targetProgress;
}

public float getProgress(float partialTick) {

Check warning on line 85 in common/src/main/java/me/collinb/dynamicview/camera/CameraAnimation.java

View workflow job for this annotation

GitHub Actions / build

no comment
return Mth.lerp(partialTick, previousProgress, currentProgress);
}
}
Original file line number Diff line number Diff line change
@@ -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,

Check warning on line 9 in common/src/main/java/me/collinb/dynamicview/camera/CameraContext.java

View workflow job for this annotation

GitHub Actions / build

no comment
FLYING,

Check warning on line 10 in common/src/main/java/me/collinb/dynamicview/camera/CameraContext.java

View workflow job for this annotation

GitHub Actions / build

no comment
SWIMMING,

Check warning on line 11 in common/src/main/java/me/collinb/dynamicview/camera/CameraContext.java

View workflow job for this annotation

GitHub Actions / build

no comment
CRAWLING

Check warning on line 12 in common/src/main/java/me/collinb/dynamicview/camera/CameraContext.java

View workflow job for this annotation

GitHub Actions / build

no comment
}
27 changes: 22 additions & 5 deletions common/src/main/java/me/collinb/dynamicview/config/ModConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@

@Config(name = Constants.MOD_ID)
public class ModConfig implements ConfigData {
private static ConfigHolder<ModConfig> holder;

public static void init() {
ConfigHolder<ModConfig> holder = AutoConfig.register(ModConfig.class, GsonConfigSerializer::new);
holder = AutoConfig.register(ModConfig.class, GsonConfigSerializer::new);
}

public static ModConfig get() {
return holder.get();
}

@ConfigEntry.Gui.CollapsibleObject
Expand All @@ -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));
}
}
26 changes: 7 additions & 19 deletions common/src/main/java/me/collinb/dynamicview/mixin/CameraMixin.java
Original file line number Diff line number Diff line change
@@ -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 {

Check warning on line 12 in common/src/main/java/me/collinb/dynamicview/mixin/CameraMixin.java

View workflow job for this annotation

GitHub Actions / build

no comment

@Inject(method = "getMaxZoom", at = @At("TAIL"), cancellable = true)
private void useSmoothZooming(float pMaxZoom, CallbackInfoReturnable<Float> 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());
}
}

This file was deleted.

Loading