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
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package net.tfminecraft.vehicleframework.vehicles.controller;

/**
* Lets a terrain-following vehicle that stands idle stop re-reading the ground every tick.
* The ground step traces rays under every wheel and teleports the vehicle onto the result, even
* when the result is the spot it already stands on. Once {@link #SETTLE_STEPS} steps in a row
* leave the vehicle exactly where it was, with the same tilt, the step is left out until the
* vehicle is found anywhere else (a push, a fall, a piston) or is no longer idle. A full step
* still runs every {@link #RECHECK_TICKS} ticks, so a block placed or removed under a vehicle
* that nothing moves is seen within that time.
*/
final class TerrainRest {
/** Still steps before the vehicle counts as settled; the tilt smoothing has all but finished by then, and the rechecks complete it. */
static final int SETTLE_STEPS = 20;
/** While settled, one tick in this many runs the full ground step. */
static final int RECHECK_TICKS = 20;
private static final double POSITION_EPSILON = 1e-4;
private static final double TILT_EPSILON = 1e-3;

private int still;
private int skipped;
private double x;
private double y;
private double z;
private boolean tilted;
private float pitch;
private float roll;

/** Whether this tick's ground step can be left out for a vehicle now standing at this position. */
boolean skip(double currentX, double currentY, double currentZ) {
if (still < SETTLE_STEPS) {
return false;
}
if (!at(currentX, currentY, currentZ)) {
reset();
return false;
}
if (skipped >= RECHECK_TICKS - 1) {
skipped = 0;
return false;
}
skipped++;
return true;
}

/**
* Records one full ground step: where the vehicle stood before it, where the step put it, whether
* the step gave it any velocity, and the tilt it asked for ({@code hasTilt} false for none).
*/
void observe(
double beforeX, double beforeY, double beforeZ,
double destX, double destY, double destZ,
boolean moving, boolean hasTilt, float pitchDeg, float rollDeg) {
boolean stayed = !moving
&& near(beforeX, destX) && near(beforeY, destY) && near(beforeZ, destZ);
boolean sameAsLast = still == 0
|| (at(destX, destY, destZ) && tilted == hasTilt
&& (!hasTilt || (Math.abs(pitch - pitchDeg) <= TILT_EPSILON
&& Math.abs(roll - rollDeg) <= TILT_EPSILON)));
if (!stayed) {
still = 0;
} else if (sameAsLast) {
still = Math.min(SETTLE_STEPS, still + 1);
} else {
still = 1;
}
if (still < SETTLE_STEPS) {
skipped = 0;
}
x = destX;
y = destY;
z = destZ;
tilted = hasTilt;
pitch = pitchDeg;
roll = rollDeg;
}

/** Forgets the settled spot: the next {@link #SETTLE_STEPS} steps all run. */
void reset() {
still = 0;
skipped = 0;
}

boolean settled() {
return still >= SETTLE_STEPS;
}

private boolean at(double px, double py, double pz) {
return near(px, x) && near(py, y) && near(pz, z);
}

private static boolean near(double a, double b) {
return Math.abs(a - b) <= POSITION_EPSILON;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package net.tfminecraft.vehicleframework.vehicles.controller;

import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
Expand All @@ -8,6 +9,7 @@
import net.tfminecraft.vehicleframework.VehicleFramework;
import net.tfminecraft.vehicleframework.bones.BoneRotator;
import net.tfminecraft.vehicleframework.bones.VectorBone;
import net.tfminecraft.vehicleframework.cache.Cache;
import net.tfminecraft.vehicleframework.enums.Animation;
import net.tfminecraft.vehicleframework.enums.Component;
import net.tfminecraft.vehicleframework.enums.Direction;
Expand Down Expand Up @@ -45,6 +47,8 @@ public class VehicleMovementController implements MovementInterface{
protected FloatController floatController = new FloatController();
protected ThrottleController throttleController = new ThrottleController();
protected RotateController rotateController = new RotateController();
/** When this state's ground step may be left out for a vehicle standing idle. */
private final TerrainRest terrainRest = new TerrainRest();



Expand Down Expand Up @@ -256,21 +260,49 @@ private void move() {
* The result of its MOVE step then depends on neither the heading bone nor the blocks around
* it, so {@link #moveParked} can apply that result directly. Checked every tick: a driver,
* throttle, a state change (water, open air) or a tow brings back the full step that tick.
* A state that follows the terrain is never parked, because its step does read the blocks
* under it; {@link TerrainRest} thins that step out instead once the vehicle has settled.
*/
boolean isParked() {
// The terrain rule first: a terrain vehicle is never parked, and its idle check runs once,
// in the ground step.
return !followsTerrain() && standsIdle();
}

/**
* Forgets a settled terrain rest, so the ground step runs every tick again until the vehicle
* has settled anew. For whatever changes the vehicle without going through that step: its
* state swapped (the rest belongs to the ground it stood on in this state), or its bone reset
* (the step's tilt smoothing has to bring it back, and a resting vehicle only steps once a
* second).
*/
public void resetTerrainRest() {
terrainRest.reset();
}

/** For tests: this controller's rest. */
TerrainRest terrainRest() {
return terrainRest;
}

/** Whether this controller's state places the vehicle on the ground it reads every step. */
private boolean followsTerrain() {
if (state.isDefault()) {
return false;
}
TerrainFollowConfig follow = state.getTerrainFollow();
return follow != null && follow.isEnabled();
}

/** {@link #isParked} without the terrain rule: nothing aboard, driving, towing or burning. */
private boolean standsIdle() {
// The full step reads the vehicle's current state, which is this controller's.
if (v.getCurrentState() != state || state.getType() != State.GROUND) {
return false;
}
if (v.isTrain() || v.isDestroyed() || v.shouldFloat()) {
return false;
}
if (!state.isDefault()) {
TerrainFollowConfig follow = state.getTerrainFollow();
if (follow != null && follow.isEnabled()) {
return false;
}
}
if (v.hasComponent(Component.HARNESS)
|| !(v.hasComponent(Component.ENGINE) || v.hasComponent(Component.GEARED_ENGINE))) {
return false;
Expand Down Expand Up @@ -346,10 +378,33 @@ private boolean applyTerrainFollow(Direction dir) {
if (v.isTrain()) {
return false;
}
// The debug view draws the probes from inside the step, so it keeps every step. The ground
// engine log does not: with it on, a resting vehicle logs one step a second, not twenty.
boolean idle = !Cache.terrainFollowDebug && standsIdle();
Location before = idle ? e.getLocation() : null;
if (idle && terrainRest.skip(before.getX(), before.getY(), before.getZ())) {
// Settled and found where the last step left it: that step would put it here again.
Vector none = new Vector();
e.setVelocity(none);
propagate(none, dir);
animateMove(dir);
return true;
}
TerrainFollowEngine.Result result = TerrainFollowEngine.step(v, vector, dir, baseController, follow);
if (result == null) {
terrainRest.reset();
return true;
}
if (idle) {
TerrainFollowMath.Tilt tilt = result.tilt;
terrainRest.observe(
before.getX(), before.getY(), before.getZ(),
result.location.getX(), result.location.getY(), result.location.getZ(),
result.velocity.lengthSquared() > 0,
tilt != null, tilt == null ? 0 : tilt.pitchDeg, tilt == null ? 0 : tilt.rollDeg);
} else {
terrainRest.reset();
}
e.teleport(result.location);
e.setVelocity(result.velocity);
if (v.hasComponent(Component.HARNESS)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,17 @@ private void swapState(State s, String reason) {
to,
reason,
next != null && next.isDefault()));
// A rest settled in the state it leaves, or in the one it returns to, is about other ground.
resetTerrainRest();
setState(s);
}

/** Every state's ground step runs each tick again; see {@link VehicleMovementController#resetTerrainRest}. */
public void resetTerrainRest() {
for (VehicleState each : states.values()) {
each.getMoveControls().resetTerrainRest();
}
}

//Currentstate getters and methods

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public void attach(ActiveVehicle target) {
if(towPoint.isOccupied()) return;
towPoint.mount(target, v);
target.getBehaviourHandler().getRotator().reset();
// Its bone is level again: a vehicle resting on a slope must go back to stepping every tick,
// or the tilt would only come back one smoothing step a second.
target.getStateHandler().resetTerrainRest();
}

public void unattach() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package net.tfminecraft.vehicleframework.vehicles.controller;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

class TerrainRestTest {
private final TerrainRest rest = new TerrainRest();

/** One full step that leaves the vehicle where it stood, level. */
private void stillStep(double x, double y, double z) {
rest.observe(x, y, z, x, y, z, false, true, 0f, 0f);
}

private void settleAt(double x, double y, double z) {
for (int i = 0; i < TerrainRest.SETTLE_STEPS; i++) {
assertFalse(rest.skip(x, y, z), "step " + i + " must still run");
stillStep(x, y, z);
}
assertTrue(rest.settled());
}

@Test
void everyStepRunsUntilEnoughOfThemInARowChangeNothing() {
for (int i = 0; i < TerrainRest.SETTLE_STEPS - 1; i++) {
stillStep(8, 64, 8);
assertFalse(rest.settled());
assertFalse(rest.skip(8, 64, 8));
}
stillStep(8, 64, 8);
assertTrue(rest.settled());
assertTrue(rest.skip(8, 64, 8));
}

@Test
void aSettledVehicleRunsOneFullStepInEveryRecheckWindow() {
settleAt(8, 64, 8);
for (int window = 0; window < 3; window++) {
int skipped = 0;
while (rest.skip(8, 64, 8)) {
skipped++;
}
assertEquals(TerrainRest.RECHECK_TICKS - 1, skipped);
stillStep(8, 64, 8);
assertTrue(rest.settled(), "an unchanged recheck keeps the vehicle settled");
}
}

@Test
void aVehicleFoundAnywhereElseRunsEveryStepAgain() {
settleAt(8, 64, 8);
assertTrue(rest.skip(8, 64, 8));
assertFalse(rest.skip(8, 63.9, 8), "it fell");
assertFalse(rest.settled());
assertFalse(rest.skip(8, 64, 8), "back on the old spot is not settled either");
}

@Test
void aStepThatMovesOrPushesTheVehicleStartsTheCountAgain() {
settleAt(8, 64, 8);
rest.observe(8, 64, 8, 8, 63.5, 8, false, true, 0f, 0f);
assertFalse(rest.settled(), "the step dropped it half a block");
settleAt(8, 63.5, 8);
rest.observe(8, 63.5, 8, 8, 63.5, 8, true, true, 0f, 0f);
assertFalse(rest.settled(), "the step gave it velocity");
}

@Test
void aChangingTiltOrALostTiltKeepsTheStepRunningUntilItHolds() {
for (int i = 0; i < TerrainRest.SETTLE_STEPS * 2; i++) {
rest.observe(8, 64, 8, 8, 64, 8, false, true, i * .5f, 0f);
assertFalse(rest.settled(), "the tilt is still turning");
}
settleAt(8, 64, 8);
rest.observe(8, 64, 8, 8, 64, 8, false, false, 0f, 0f);
assertFalse(rest.settled(), "a wheel left the ground, so the step gave no tilt");
for (int i = 0; i < TerrainRest.SETTLE_STEPS - 1; i++) {
rest.observe(8, 64, 8, 8, 64, 8, false, false, 0f, 0f);
}
assertTrue(rest.settled(), "no tilt at all also settles once it holds");
}

@Test
void aStillStepSomewhereNewCountsAsTheFirstOfItsRun() {
settleAt(8, 64, 8);
stillStep(20, 70, 20);
assertFalse(rest.settled());
for (int i = 0; i < TerrainRest.SETTLE_STEPS - 2; i++) {
stillStep(20, 70, 20);
assertFalse(rest.settled());
}
stillStep(20, 70, 20);
assertTrue(rest.settled());
}

@Test
void everyAxisAndBothTiltAnglesCount() {
double[][] elsewhere = {{8.5, 64, 8}, {8, 64.5, 8}, {8, 64, 8.5}};
for (double[] spot : elsewhere) {
settleAt(8, 64, 8);
assertFalse(rest.skip(spot[0], spot[1], spot[2]), "found off its spot on one axis");
settleAt(8, 64, 8);
rest.observe(8, 64, 8, spot[0], spot[1], spot[2], false, true, 0f, 0f);
assertFalse(rest.settled(), "the step moved it along one axis");
rest.reset();
}
settleAt(8, 64, 8);
rest.observe(8, 64, 8, 8, 64, 8, false, true, 0f, 2f);
assertFalse(rest.settled(), "the roll changed");
settleAt(8, 64, 8);
rest.observe(8, 64, 8, 8, 64, 8, false, true, 2f, 0f);
assertFalse(rest.settled(), "the pitch changed");
}

@Test
void resetForgetsTheSpot() {
settleAt(8, 64, 8);
rest.reset();
assertFalse(rest.settled());
assertFalse(rest.skip(8, 64, 8));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,30 @@ class VehicleControllersCoverageTest {
handler.tick(); assertEquals(State.GROUND, handler.getCurrentState().getType());
}

@Test void aStateSwapForgetsEveryStatesTerrainRest() {
Water f = new Water(); YamlConfiguration config = yaml(); config.createSection("floating");
StateHandler handler = new StateHandler(f.vehicle, new StateHandler(config));
f.grid.fill = p -> p.y < 64 ? Material.STONE : Material.AIR;
handler.tick(); assertEquals(State.GROUND, handler.getCurrentState().getType());
TerrainRest ground = handler.getVehicleState(State.GROUND).getMoveControls().terrainRest();
TerrainRest floating = handler.getVehicleState(State.FLOATING).getMoveControls().terrainRest();
for (TerrainRest rest : List.of(ground, floating)) {
for (int i = 0; i < TerrainRest.SETTLE_STEPS; i++) rest.observe(8, 64, 8, 8, 64, 8, false, false, 0f, 0f);
assertTrue(rest.settled());
}
// Staying in the same state leaves a rest alone.
handler.tick(); assertTrue(ground.settled()); assertTrue(floating.settled());
// Water rises over it: whatever either state had settled on is no longer the ground it stands on.
f.grid.fill = p -> p.y <= 66 ? Material.WATER : Material.AIR;
handler.tick(); assertEquals(State.FLOATING, handler.getCurrentState().getType());
assertFalse(ground.settled()); assertFalse(floating.settled());
// The same when it comes back to the very spot it rested on.
for (int i = 0; i < TerrainRest.SETTLE_STEPS; i++) ground.observe(8, 64, 8, 8, 64, 8, false, false, 0f, 0f);
f.grid.fill = p -> p.y < 64 ? Material.STONE : Material.AIR;
handler.tick(); assertEquals(State.GROUND, handler.getCurrentState().getType());
assertFalse(ground.settled()); assertFalse(ground.skip(8, 64, 8));
}

@Test void stateTicksOnlyReadTheBlocksTheirConfiguredStatesNeed() {
Water f = new Water();
f.grid.fill = p -> p.y <= 66 ? Material.WATER : Material.AIR;
Expand Down
Loading
Loading