diff --git a/common/src/main/java/org/vivecraft/api/VivecraftAPI.java b/common/src/main/java/org/vivecraft/api/VivecraftAPI.java index d63452dfc..82f1acfa7 100644 --- a/common/src/main/java/org/vivecraft/api/VivecraftAPI.java +++ b/common/src/main/java/org/vivecraft/api/VivecraftAPI.java @@ -1,6 +1,5 @@ package org.vivecraft.api; -import com.google.common.annotations.Beta; import net.minecraft.world.entity.player.Player; import org.vivecraft.api.data.VRData; import org.vivecraft.common.api_impl.APIImpl; diff --git a/common/src/main/java/org/vivecraft/api/client/Tracker.java b/common/src/main/java/org/vivecraft/api/client/Tracker.java index bf02e26cc..9b459689c 100644 --- a/common/src/main/java/org/vivecraft/api/client/Tracker.java +++ b/common/src/main/java/org/vivecraft/api/client/Tracker.java @@ -2,6 +2,12 @@ import net.minecraft.client.player.LocalPlayer; +/** + * A tracker is an object that is run for the local player during the game tick or before rendering a frame. + * Using trackers are one of the cleanest ways to interact with Vivecraft's data; it's how Vivecraft itself does. + * Trackers should generally use {@link VivecraftClientAPI#getPreTickWorldData()}, and are only run for + * the local player if they are in VR. + */ public interface Tracker { /** @@ -19,8 +25,8 @@ public interface Tracker { /** * The ticking type for this tracker. - * If this is PER_FRAME, the tracker is called once with the local player per frame. - * If this is PER_TICK, the tracker is called once with the local player per game tick. + * If this is PER_FRAME, the tracker is called once with the local player per frame before the frame is rendered. + * If this is PER_TICK, the tracker is called once with the local player per game tick during the tick. * @return The ticking type this tracker should use. */ TrackerTickType tickType(); diff --git a/common/src/main/java/org/vivecraft/api/client/VRPoseHistory.java b/common/src/main/java/org/vivecraft/api/client/VRPoseHistory.java new file mode 100644 index 000000000..e693b0fd9 --- /dev/null +++ b/common/src/main/java/org/vivecraft/api/client/VRPoseHistory.java @@ -0,0 +1,88 @@ +package org.vivecraft.api.client; + +import net.minecraft.world.phys.Vec3; +import org.vivecraft.api.data.VRPose; + +import java.util.List; + +/** + * Represents historical VRData associated with a player. + */ +public interface VRPoseHistory { + + /** + * The maximum amount of ticks back data is held for. + * It is only guaranteed that historical data does not go beyond this number of ticks back. Functions do not + * guarantee that they will reference this many ticks, as, for example, this amount of ticks may not have gone + * by for the player this history represents. + * Passing a value larger than this number to any methods below in their maxTicksBack or ticksBack parameters + * will throw an {@link IllegalArgumentException}. + */ + int MAX_TICKS_BACK = 20; + + /** + * @return The amount of ticks worth of history being held. + */ + int ticksOfHistory(); + + /** + * Gets a raw list of {@link VRPose} instances, with index 0 representing the least recent pose known. + * @return The aforementioned list of {@link VRPose} instances. + */ + List getAllHistoricalData() throws IllegalArgumentException; + + /** + * Gets the historical data ticksBack ticks back. This will throw an IllegalStateException if the data cannot + * be retrieved due to not having enough history. + * @param ticksBack Ticks back to retrieve data. + * @return A {@link VRPose} instance from ticksBack ticks ago. + * @throws IllegalStateException If ticksBack references a tick that there is not yet data for. + * @throws IllegalArgumentException Thrown when maxTicksBack is larger than {@value #MAX_TICKS_BACK} or less than 0. + */ + VRPose getHistoricalData(int ticksBack) throws IllegalArgumentException, IllegalStateException; + + /** + * Gets the net movement between the most recent data in this instance and the oldest position that can be + * retrieved, going no farther back than maxTicksBack. + * @param maxTicksBack The maximum amount of ticks back to compare the most recent data with. + * @return The aforementioned net movement. Note that this will return zero change on all axes if only zero ticks + * can be looked back. + * @throws IllegalArgumentException Thrown when maxTicksBack is larger than {@value #MAX_TICKS_BACK} or less than 0. + */ + Vec3 netMovement(int maxTicksBack) throws IllegalArgumentException; + + /** + * Gets the average velocity in blocks/tick between the most recent data in this instance and the oldest position + * that can be retrieved, going no farther back than maxTicksBack. + * @param maxTicksBack The maximum amount of ticks back to calculate velocity with. + * @return The aforementioned average velocity on each axis. Note that this will return zero velocity on all axes + * if only zero ticks can be looked back. + * @throws IllegalArgumentException Thrown when maxTicksBack is larger than {@value #MAX_TICKS_BACK} or less than 0. + */ + Vec3 averageVelocity(int maxTicksBack) throws IllegalArgumentException; + + /** + * Gets the average speed in blocks/tick between the most recent data in this instance and the oldest position + * that can be retrieved, going no farther back than maxTicksBack. + * @param maxTicksBack The maximum amount of ticks back to calculate speed with. + * @return The aforementioned average speed on each axis. Note that this will return zero speed if only zero ticks + * can be looked back. + * @throws IllegalArgumentException Thrown when maxTicksBack is larger than {@value #MAX_TICKS_BACK} or less than 0. + */ + default double averageSpeed(int maxTicksBack) throws IllegalArgumentException { + Vec3 averageVelocity = averageVelocity(maxTicksBack); + return Math.sqrt(averageVelocity.x() * averageVelocity.x() + + averageVelocity.y() * averageVelocity.y() + + averageVelocity.z() * averageVelocity.z()); + } + + /** + * Gets the average position between the most recent data in this instance and the oldest position that can be + * retrieved, going no farther back than maxTicksBack. + * @param maxTicksBack The maximum amount of ticks back to calculate velocity with. + * @return The aforementioned average position. Note that this will return the current position if only zero ticks + * can be looked back. + * @throws IllegalArgumentException Thrown when maxTicksBack is larger than {@value #MAX_TICKS_BACK} or less than 0. + */ + Vec3 averagePosition(int maxTicksBack) throws IllegalArgumentException; +} diff --git a/common/src/main/java/org/vivecraft/api/client/VivecraftClientAPI.java b/common/src/main/java/org/vivecraft/api/client/VivecraftClientAPI.java index 68460c13b..dd9e11375 100644 --- a/common/src/main/java/org/vivecraft/api/client/VivecraftClientAPI.java +++ b/common/src/main/java/org/vivecraft/api/client/VivecraftClientAPI.java @@ -1,9 +1,11 @@ package org.vivecraft.api.client; -import com.google.common.annotations.Beta; +import net.minecraft.world.InteractionHand; import org.vivecraft.api.data.VRData; import org.vivecraft.client.api_impl.ClientAPIImpl; +import javax.annotation.Nullable; + public interface VivecraftClientAPI { static VivecraftClientAPI getInstance() { @@ -11,8 +13,15 @@ static VivecraftClientAPI getInstance() { } /** - * Gets data representing the devices as they exist in the room before the game tick. This is effectively - * the latest polling data from the VR devices. + * Adds the tracker to the list of all trackers to be run for the local player. See the documentation for + * {@link Tracker} for more information on what a tracker is. + * @param tracker Tracker to register. + */ + void addTracker(Tracker tracker); + + /** + * Gets data representing the devices as they exist in the room before the game tick. + * Note that this data is gathered BEFORE mod loaders' pre-tick events. * @return Data representing the devices in the room pre-tick. * @throws IllegalStateException Thrown when the local player isn't in VR. */ @@ -20,6 +29,7 @@ static VivecraftClientAPI getInstance() { /** * Gets data representing the devices as they exist in the room after the game tick. + * Note that this data is gathered AFTER mod loaders' post-tick events. * @return Data representing the devices in the room post-tick. * @throws IllegalStateException Thrown when the local player isn't in VR. */ @@ -28,7 +38,9 @@ static VivecraftClientAPI getInstance() { /** * Gets data representing the devices as they exist in Minecraft coordinates before the game tick. * This is the same as {@link #getPreTickRoomData()} with translation to Minecraft's coordinates as of the last - * tick. + * tick, and is the main data source used by Vivecraft. If you're unsure which {@link VRData} method to use, you + * likely want to use this one. + * Note that this data is gathered BEFORE mod loaders' pre-tick events. * @return Data representing the devices in Minecraft space pre-tick. * @throws IllegalStateException Thrown when the local player isn't in VR. */ @@ -37,7 +49,7 @@ static VivecraftClientAPI getInstance() { /** * Gets data representing the devices as they exist in Minecraft coordinates after the game tick. * This is the data sent to the server, and also used to calculate the data in {@link #getWorldRenderData()}. - * If you're unsure which {@link VRData} method to use, you likely want to use this one. + * Note that this data is gathered AFTER mod loaders' post-tick events. * @return Data representing the devices in Minecraft space post-tick. * @throws IllegalStateException Thrown when the local player isn't in VR. */ @@ -111,8 +123,50 @@ default void triggerHapticPulse(int controllerNum, float duration) { float getWorldScale(); /** - * Adds the tracker to the list of all trackers to be run for the local player. - * @param tracker Tracker to register. + * Returns the history of VR poses for the player for the HMD. Will return null if the player isn't + * in VR. + * @return The historical VR data for the player's HMD, or null if the player isn't in VR. */ - void addTracker(Tracker tracker); + @Nullable + VRPoseHistory getHistoricalVRHMDPoses(); + + /** + * Returns the history of VR poses for the player for a controller. Will return null if the player isn't + * in VR. + * @param controller The controller number to get, with 0 being the primary controller. + * @return The historical VR data for the player's controller, or null if the player isn't in VR. + */ + @Nullable + VRPoseHistory getHistoricalVRControllerPoses(int controller); + + /** + * Returns the history of VR poses for the player for a controller. Will return null if the player isn't + * in VR. + * @param hand The hand to get controller history for. + * @return The historical VR data for the player's controller, or null if the player isn't in VR. + */ + @Nullable + default VRPoseHistory getHistoricalVRControllerData(InteractionHand hand) { + return getHistoricalVRControllerPoses(hand.ordinal()); + } + + /** + * Returns the history of VR poses for the player for the primary controller. Will return null if the + * player isn't in VR. + * @return The historical VR data for the player's primary controller, or null if the player isn't in VR. + */ + @Nullable + default VRPoseHistory getHistoricalVRController0Data() { + return getHistoricalVRControllerPoses(0); + } + + /** + * Returns the history of VR poses for the player for the secondary controller. Will return null if the + * player isn't in VR. + * @return The historical VR data for the player's secondary controller, or null if the player isn't in VR. + */ + @Nullable + default VRPoseHistory getHistoricalVRController1Data() { + return getHistoricalVRControllerPoses(1); + } } diff --git a/common/src/main/java/org/vivecraft/api/data/VRData.java b/common/src/main/java/org/vivecraft/api/data/VRData.java index 75365ab7b..fe1a3d256 100644 --- a/common/src/main/java/org/vivecraft/api/data/VRData.java +++ b/common/src/main/java/org/vivecraft/api/data/VRData.java @@ -17,7 +17,7 @@ public interface VRData { /** * Gets the pose data for a given controller. * - * @param controller The controller number to get. + * @param controller The controller number to get, with 0 being the primary controller. * @return The specified controller's pose data. */ VRPose getController(int controller); diff --git a/common/src/main/java/org/vivecraft/client/api_impl/ClientAPIImpl.java b/common/src/main/java/org/vivecraft/client/api_impl/ClientAPIImpl.java index 84d8325da..5b746bf59 100644 --- a/common/src/main/java/org/vivecraft/client/api_impl/ClientAPIImpl.java +++ b/common/src/main/java/org/vivecraft/client/api_impl/ClientAPIImpl.java @@ -1,8 +1,12 @@ package org.vivecraft.client.api_impl; +import org.jetbrains.annotations.Nullable; import org.vivecraft.api.client.Tracker; +import org.vivecraft.api.client.VRPoseHistory; import org.vivecraft.api.client.VivecraftClientAPI; import org.vivecraft.api.data.VRData; +import org.vivecraft.api.data.VRPose; +import org.vivecraft.client.api_impl.data.VRPoseHistoryImpl; import org.vivecraft.client_vr.ClientDataHolderVR; import org.vivecraft.client_vr.VRState; import org.vivecraft.client_vr.provider.ControllerType; @@ -12,9 +16,25 @@ public final class ClientAPIImpl implements VivecraftClientAPI { public static final ClientAPIImpl INSTANCE = new ClientAPIImpl(); + private final VRPoseHistoryImpl hmdHistory = new VRPoseHistoryImpl(); + private VRPoseHistoryImpl c0History = new VRPoseHistoryImpl(); + private VRPoseHistoryImpl c1History = new VRPoseHistoryImpl(); + private ClientAPIImpl() { } + public void clearHistories() { + this.hmdHistory.clear(); + this.c0History.clear(); + this.c1History.clear(); + } + + public void addPosesToHistory(VRData data) { + this.hmdHistory.addPose(data.getHMD()); + this.c0History.addPose(data.getController0()); + this.c1History.addPose(data.getController1()); + } + @Override public VRData getPreTickRoomData() throws IllegalStateException { if (!isVrActive()) { @@ -108,4 +128,24 @@ public float getWorldScale() { public void addTracker(Tracker tracker) { ClientDataHolderVR.getInstance().addTracker(tracker); } + + @Nullable + @Override + public VRPoseHistory getHistoricalVRHMDPoses() { + if (!isVrActive()) { + return null; + } + return this.hmdHistory; + } + + @Nullable + @Override + public VRPoseHistory getHistoricalVRControllerPoses(int controller) { + if (controller != 0 && controller != 1) { + throw new IllegalArgumentException("Historical VR controller data only available for controllers 0 and 1."); + } else if (!isVrActive()) { + return null; + } + return controller == 0 ? this.c0History : this.c1History; + } } diff --git a/common/src/main/java/org/vivecraft/client/api_impl/data/VRPoseHistoryImpl.java b/common/src/main/java/org/vivecraft/client/api_impl/data/VRPoseHistoryImpl.java new file mode 100644 index 000000000..d338a7140 --- /dev/null +++ b/common/src/main/java/org/vivecraft/client/api_impl/data/VRPoseHistoryImpl.java @@ -0,0 +1,106 @@ +package org.vivecraft.client.api_impl.data; + +import net.minecraft.world.phys.Vec3; +import org.vivecraft.api.client.VRPoseHistory; +import org.vivecraft.api.data.VRPose; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +public class VRPoseHistoryImpl implements VRPoseHistory { + + private final LinkedList dataQueue = new LinkedList<>(); + + public VRPoseHistoryImpl() { + } + + public void addPose(VRPose pose) { + this.dataQueue.add(pose); + if (this.dataQueue.size() > VRPoseHistory.MAX_TICKS_BACK) { + this.dataQueue.removeFirst(); + } + } + + public void clear() { + this.dataQueue.clear(); + } + + @Override + public int ticksOfHistory() { + return this.dataQueue.size(); + } + + @Override + public List getAllHistoricalData() { + return new ArrayList<>(this.dataQueue); + } + + @Override + public VRPose getHistoricalData(int ticksBack) throws IllegalArgumentException, IllegalStateException { + checkTicksBack(ticksBack); + if (this.dataQueue.size() <= ticksBack) { + throw new IllegalStateException("Cannot retrieve data from " + ticksBack + " ticks ago, when there is " + + "only data for up to " + (this.dataQueue.size() - 1) + " ticks ago."); + } + return this.dataQueue.get(ticksBack); + } + + @Override + public Vec3 netMovement(int maxTicksBack) throws IllegalArgumentException { + checkTicksBack(maxTicksBack); + Vec3 current = this.dataQueue.getLast().getPos(); + Vec3 old = getOldPose(maxTicksBack).getPos(); + return current.subtract(old); + } + + @Override + public Vec3 averageVelocity(int maxTicksBack) throws IllegalArgumentException { + checkTicksBack(maxTicksBack); + Vec3 current = this.dataQueue.getLast().getPos(); + Vec3 old = getOldPose(maxTicksBack).getPos(); + return current.subtract(old).scale(1d / getNumTicksBack(maxTicksBack)); + } + + @Override + public Vec3 averagePosition(int maxTicksBack) throws IllegalArgumentException { + checkTicksBack(maxTicksBack); + int iters = getNumTicksBack(maxTicksBack); + ListIterator iterator = this.dataQueue.listIterator(this.dataQueue.size() - 1); + Vec3 avg = this.dataQueue.getLast().getPos(); + int i = iters; + while (i > 0) { + avg = avg.add(iterator.previous().getPos()); + i--; + } + return avg.scale(1d / (iters + 1)); + } + + private void checkTicksBack(int ticksBack) { + if (ticksBack < 0 || ticksBack > VRPoseHistory.MAX_TICKS_BACK) { + throw new IllegalArgumentException("Value must be between 0 and " + VRPoseHistory.MAX_TICKS_BACK + "."); + } + } + + private VRPose getOldPose(int maxTicksBack) { + if (this.dataQueue.size() <= maxTicksBack) { + return this.dataQueue.getFirst(); + } else { + return this.dataQueue.get(this.dataQueue.size() - maxTicksBack - 1); + } + } + + /** + * Converts maxTicksBack to the actual maximum number of ticks we can go back. + * @param maxTicksBack The maximum number of ticks to attempt to go back. + * @return The actual number of ticks to go back by. + */ + private int getNumTicksBack(int maxTicksBack) { + if (this.dataQueue.size() <= maxTicksBack) { + return this.dataQueue.size() - 1; + } else { + return maxTicksBack; + } + } +} diff --git a/common/src/main/java/org/vivecraft/client_vr/VRState.java b/common/src/main/java/org/vivecraft/client_vr/VRState.java index dac55383a..921b7b7ba 100644 --- a/common/src/main/java/org/vivecraft/client_vr/VRState.java +++ b/common/src/main/java/org/vivecraft/client_vr/VRState.java @@ -3,6 +3,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.network.chat.Component; import org.lwjgl.glfw.GLFW; +import org.vivecraft.client.api_impl.ClientAPIImpl; import org.vivecraft.client_vr.gameplay.VRPlayer; import org.vivecraft.client.gui.screens.ErrorScreen; import org.vivecraft.client_vr.menuworlds.MenuWorldRenderer; @@ -90,6 +91,7 @@ public static void destroyVR(boolean disableVRSetting) { ClientDataHolderVR.getInstance().vrSettings.vrEnabled = false; ClientDataHolderVR.getInstance().vrSettings.saveOptions(); } + ClientAPIImpl.INSTANCE.clearHistories(); } public static void pauseVR() { diff --git a/common/src/main/java/org/vivecraft/client_vr/gameplay/VRPlayer.java b/common/src/main/java/org/vivecraft/client_vr/gameplay/VRPlayer.java index 9942ab969..7c6735ee7 100644 --- a/common/src/main/java/org/vivecraft/client_vr/gameplay/VRPlayer.java +++ b/common/src/main/java/org/vivecraft/client_vr/gameplay/VRPlayer.java @@ -25,6 +25,7 @@ import net.minecraft.world.phys.Vec3; import org.vivecraft.api.client.Tracker; import org.vivecraft.client.VivecraftVRMod; +import org.vivecraft.client.api_impl.ClientAPIImpl; import org.vivecraft.client_vr.ClientDataHolderVR; import org.vivecraft.common.VRServerPerms; import org.vivecraft.mod_compat_vr.pehkui.PehkuiHelper; @@ -161,6 +162,8 @@ else if (this.worldScale < 0.025F) //minClip + player position indicator offset { this.dh.vrSettings.worldRotation = this.dh.vr.seatedRot; } + + ClientAPIImpl.INSTANCE.addPosesToHistory(this.vrdata_world_pre.asVRData()); } public void postTick() diff --git a/common/src/main/java/org/vivecraft/client_vr/gameplay/trackers/CameraTracker.java b/common/src/main/java/org/vivecraft/client_vr/gameplay/trackers/CameraTracker.java index 74689dab8..86212b79a 100644 --- a/common/src/main/java/org/vivecraft/client_vr/gameplay/trackers/CameraTracker.java +++ b/common/src/main/java/org/vivecraft/client_vr/gameplay/trackers/CameraTracker.java @@ -78,7 +78,7 @@ public void doProcess(LocalPlayer player) @Override public TrackerTickType tickType() { - return TrackerTickType.PER_TICK; + return TrackerTickType.PER_FRAME; } public void reset(LocalPlayer player)