package com.hypixel.hytale.server.npc.movement.controllers; import com.hypixel.hytale.component.ComponentAccessor; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.math.shape.Box; import com.hypixel.hytale.math.util.ChunkUtil; import com.hypixel.hytale.math.util.MathUtil; import com.hypixel.hytale.math.vector.Vector3d; import com.hypixel.hytale.math.vector.Vector3f; import com.hypixel.hytale.protocol.GameMode; import com.hypixel.hytale.protocol.MovementSettings; import com.hypixel.hytale.protocol.MovementStates; import com.hypixel.hytale.protocol.Rangef; import com.hypixel.hytale.server.core.asset.modifiers.MovementEffects; import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple; import com.hypixel.hytale.server.core.asset.type.model.config.Model; import com.hypixel.hytale.server.core.asset.type.model.config.camera.CameraSettings; import com.hypixel.hytale.server.core.entity.InteractionManager; import com.hypixel.hytale.server.core.entity.entities.player.movement.MovementManager; import com.hypixel.hytale.server.core.modules.collision.BlockCollisionData; import com.hypixel.hytale.server.core.modules.collision.CollisionModule; import com.hypixel.hytale.server.core.modules.collision.CollisionModuleConfig; import com.hypixel.hytale.server.core.modules.collision.CollisionResult; import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation; import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent; import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.modules.entity.damage.Damage; import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause; import com.hypixel.hytale.server.core.modules.entity.damage.DamageSystems; import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent; import com.hypixel.hytale.server.core.modules.interaction.InteractionModule; import com.hypixel.hytale.server.core.modules.physics.component.PhysicsValues; import com.hypixel.hytale.server.core.modules.physics.util.PhysicsMath; import com.hypixel.hytale.server.core.modules.splitvelocity.SplitVelocity; import com.hypixel.hytale.server.core.modules.splitvelocity.VelocityConfig; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.npc.NPCPlugin; import com.hypixel.hytale.server.npc.asset.builder.BuilderSupport; import com.hypixel.hytale.server.npc.entities.NPCEntity; import com.hypixel.hytale.server.npc.movement.MotionKind; import com.hypixel.hytale.server.npc.movement.NavState; import com.hypixel.hytale.server.npc.movement.Steering; import com.hypixel.hytale.server.npc.movement.controllers.builders.BuilderMotionControllerBase; import com.hypixel.hytale.server.npc.role.Role; import com.hypixel.hytale.server.npc.role.RoleDebugFlags; import com.hypixel.hytale.server.npc.util.NPCPhysicsMath; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import java.util.List; import java.util.Objects; import java.util.function.BiPredicate; import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; public abstract class MotionControllerBase implements MotionController { public static final double FORCE_SCALE = 5.0; public static final double BISECT_DIST = 0.05; public static final double FILTER_COEFFICIENT = 0.7; public static final double DOT_PRODUCT_EPSILON = 0.001; public static final double DEFAULT_BLOCK_DRAG = 0.82; protected static final HytaleLogger LOGGER = NPCPlugin.get().getLogger(); public static final boolean DEBUG_APPLIED_FORCES = false; @Nonnull protected final NPCEntity entity; protected final String type; protected final double epsilonSpeed; protected final float epsilonAngle; protected final double forceVelocityDamping; protected final double maxHorizontalSpeed; protected final double fastMotionThreshold; protected final double fastMotionThresholdRange; protected final float maxHeadRotationSpeed; protected Role role; protected double inertia; protected double knockbackScale; protected double gravity; protected boolean debugModeSteer; protected boolean debugModeMove; protected boolean debugModeCollisions; protected boolean debugModeBlockCollisions; protected boolean debugModeProbeBlockCollisions; protected boolean debugModeValidatePositions; protected boolean debugModeOverlaps; protected boolean debugModeValidateMath; protected final Vector3d position = new Vector3d(); protected final Box collisionBoundingBox = new Box(); protected final CollisionResult collisionResult = new CollisionResult(); protected final Vector3d translation = new Vector3d(); protected final Vector3d bisectValidPosition = new Vector3d(); protected final Vector3d bisectInvalidPosition = new Vector3d(); protected final Vector3d lastValidPosition = new Vector3d(); protected final Vector3d forceVelocity = new Vector3d(); protected final Vector3d appliedForce = new Vector3d(); protected boolean ignoreDamping; protected final List appliedVelocities = new ObjectArrayList(); protected boolean isObstructed; protected NavState navState; protected double throttleDuration; protected double targetDeltaSquared; protected boolean recomputePath; protected final Vector3d worldNormal = Vector3d.UP; protected final Vector3d worldAntiNormal = Vector3d.DOWN; protected final Vector3d componentSelector = new Vector3d(1.0, 0.0, 1.0); protected final Vector3d planarComponentSelector = new Vector3d(1.0, 0.0, 1.0); protected boolean enableTriggers = true; protected boolean enableBlockDamage = true; protected boolean isReceivingBlockDamage; protected boolean isAvoidingBlockDamage = true; protected boolean requiresPreciseMovement; protected boolean requiresDepthProbing; protected boolean havePreciseMovementTarget; @Nonnull protected Vector3d preciseMovementTarget = new Vector3d(); protected boolean isRelaxedMoveConstraints; protected boolean isBlendingHeading; protected double blendHeading; protected boolean haveBlendHeadingPosition; @Nonnull protected Vector3d blendHeadingPosition = new Vector3d(); protected double blendLevelAtTargetPosition = 0.5; protected boolean fastMotionKind; protected boolean idleMotionKind; protected boolean horizontalIdleKind; protected double moveSpeed; protected double previousSpeed; protected MotionKind motionKind; protected MotionKind lastMovementStateUpdatedMotionKind; protected MotionKind previousMotionKind; protected double effectHorizontalSpeedMultiplier; protected boolean cachedMovementBlocked; private float yaw; private float pitch; private float roll; private final Vector3d beforeTriggerForce = new Vector3d(); private final Vector3d beforeTriggerPosition = new Vector3d(); private boolean processTriggersHasMoved; protected MovementSettings movementSettings; public MotionControllerBase(@Nonnull BuilderSupport builderSupport, @Nonnull BuilderMotionControllerBase builder) { this.entity = builderSupport.getEntity(); this.type = builder.getType(); this.epsilonSpeed = builder.getEpsilonSpeed(); this.epsilonAngle = builder.getEpsilonAngle(); this.forceVelocityDamping = builder.getForceVelocityDamping(); this.maxHorizontalSpeed = builder.getMaxHorizontalSpeed(builderSupport); this.fastMotionThreshold = builder.getFastHorizontalThreshold(builderSupport); this.fastMotionThresholdRange = builder.getFastHorizontalThresholdRange(); this.maxHeadRotationSpeed = builder.getMaxHeadRotationSpeed(builderSupport); this.setInertia(1.0); this.setKnockbackScale(1.0); this.setGravity(10.0); } @Override public Role getRole() { return this.role; } @Override public void setRole(Role role) { this.role = role; } @Override public void setInertia(double inertia) { this.inertia = Math.max(inertia, 1.0E-4); } @Override public void setKnockbackScale(double knockbackScale) { this.knockbackScale = Math.max(0.0, knockbackScale); } @Override public void updateModelParameters(Ref ref, Model model, @Nonnull Box boundingBox, ComponentAccessor componentAccessor) { Objects.requireNonNull(boundingBox, "updateModelParameters: MotionController needs a bounding box"); this.collisionBoundingBox.assign(boundingBox); } protected void readEntityPosition(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType()); assert transformComponent != null; Vector3f bodyRotation = transformComponent.getRotation(); this.position.assign(transformComponent.getPosition()); this.yaw = bodyRotation.getY(); this.pitch = bodyRotation.getPitch(); this.roll = bodyRotation.getRoll(); this.adjustReadPosition(ref, componentAccessor); this.postReadPosition(ref, componentAccessor); } public void postReadPosition(Ref ref, ComponentAccessor componentAccessor) { } public void moveEntity(@Nonnull Ref ref, double dt, @Nonnull ComponentAccessor componentAccessor) { TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType()); assert transformComponent != null; this.adjustWritePosition(ref, dt, componentAccessor); Vector3f bodyRotation = transformComponent.getRotation(); bodyRotation.setYaw(this.yaw); bodyRotation.setPitch(this.pitch); bodyRotation.setRoll(this.roll); this.entity.moveTo(ref, this.position.x, this.position.y, this.position.z, componentAccessor); } public float getYaw() { return this.yaw; } public float getPitch() { return this.pitch; } public float getRoll() { return this.roll; } public boolean touchesWater(boolean defaultValue, @Nonnull ComponentAccessor componentAccessor) { World world = componentAccessor.getExternalData().getWorld(); ChunkStore chunkStore = world.getChunkStore(); long chunkIndex = ChunkUtil.indexChunkFromBlock(this.position.getX(), this.position.getZ()); Ref chunkRef = chunkStore.getChunkReference(chunkIndex); if (chunkRef != null && chunkRef.isValid()) { WorldChunk worldChunkComponent = chunkStore.getStore().getComponent(chunkRef, WorldChunk.getComponentType()); assert worldChunkComponent != null; int blockX = MathUtil.floor(this.position.getX()); int blockY = MathUtil.floor(this.position.getY() + this.collisionBoundingBox.min.y); int blockZ = MathUtil.floor(this.position.getZ()); int fluidId = worldChunkComponent.getFluidId(blockX, blockY, blockZ); return fluidId != 0; } else { return defaultValue; } } @Override public void updateMovementState( @Nonnull Ref ref, @Nonnull MovementStates movementStates, @Nonnull Steering steering, @Nonnull Vector3d velocity, @Nonnull ComponentAccessor componentAccessor ) { boolean lastFastMotion = movementStates.running; movementStates.climbing = false; movementStates.swimJumping = false; movementStates.inFluid = this.touchesWater(movementStates.inFluid, componentAccessor); movementStates.onGround = this.role.isOnGround(); double speed = this.waypointDistance(Vector3d.ZERO, velocity); speed = 0.7 * this.previousSpeed + 0.30000000000000004 * speed; this.previousSpeed = speed; this.fastMotionKind = this.isFastMotionKind(speed); this.idleMotionKind = steering.getTranslation().equals(Vector3d.ZERO); this.horizontalIdleKind = this.isHorizontalIdle(speed); if (this.motionKind != this.lastMovementStateUpdatedMotionKind || lastFastMotion != this.fastMotionKind || movementStates.idle != this.idleMotionKind || movementStates.horizontalIdle != this.horizontalIdleKind) { switch (this.motionKind) { case FLYING: this.updateFlyingStates(movementStates, this.idleMotionKind, this.fastMotionKind); break; case SWIMMING: this.updateSwimmingStates(movementStates, this.idleMotionKind, this.fastMotionKind, this.horizontalIdleKind); break; case SWIMMING_TURNING: this.updateSwimmingStates(movementStates, false, true, false); break; case ASCENDING: this.updateAscendingStates(ref, movementStates, this.fastMotionKind, this.horizontalIdleKind, componentAccessor); break; case MOVING: updateMovingStates(ref, movementStates, this.fastMotionKind, componentAccessor); break; case DESCENDING: NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType()); assert npcComponent != null; this.updateDescendingStates(ref, movementStates, this.fastMotionKind, npcComponent.getHoverHeight() > 0.0, componentAccessor); break; case DROPPING: this.updateDroppingStates(movementStates); break; case STANDING: default: NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType()); assert npcComponent != null; this.updateStandingStates(movementStates, this.motionKind, npcComponent.getHoverHeight() > 0.0); } } this.lastMovementStateUpdatedMotionKind = this.motionKind; } protected abstract boolean isFastMotionKind(double var1); protected void updateFlyingStates(@Nonnull MovementStates movementStates, boolean idle, boolean fastMotionKind) { movementStates.flying = true; movementStates.idle = idle; movementStates.horizontalIdle = false; movementStates.walking = !fastMotionKind; movementStates.running = fastMotionKind; movementStates.falling = false; movementStates.swimming = false; movementStates.jumping = false; } protected void updateSwimmingStates(@Nonnull MovementStates movementStates, boolean idle, boolean fastMotionKind, boolean horizontalIdleKind) { movementStates.flying = false; movementStates.idle = idle; movementStates.horizontalIdle = horizontalIdleKind; movementStates.walking = !fastMotionKind; movementStates.running = fastMotionKind; movementStates.falling = false; movementStates.swimming = true; movementStates.jumping = false; } protected static void updateMovingStates( @Nonnull Ref ref, @Nonnull MovementStates movementStates, boolean fastMotionKind, @Nonnull ComponentAccessor componentAccessor ) { NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType()); assert npcComponent != null; movementStates.flying = npcComponent.getHoverHeight() > 0.0; movementStates.idle = false; movementStates.horizontalIdle = false; movementStates.falling = false; movementStates.walking = !fastMotionKind; movementStates.running = fastMotionKind; movementStates.swimming = false; movementStates.jumping = false; } protected void updateAscendingStates( @Nonnull Ref ref, @Nonnull MovementStates movementStates, boolean fastMotionKind, boolean horizontalIdleKind, @Nonnull ComponentAccessor componentAccessor ) { updateMovingStates(ref, movementStates, fastMotionKind, componentAccessor); } protected void updateDescendingStates( @Nonnull Ref ref, @Nonnull MovementStates movementStates, boolean fastMotionKind, boolean hovering, @Nonnull ComponentAccessor componentAccessor ) { updateMovingStates(ref, movementStates, fastMotionKind, componentAccessor); } protected void updateDroppingStates(@Nonnull MovementStates movementStates) { movementStates.falling = true; } protected void updateStandingStates(@Nonnull MovementStates movementStates, @Nonnull MotionKind motionKind, boolean hovering) { movementStates.flying = hovering; movementStates.idle = true; movementStates.horizontalIdle = true; movementStates.walking = false; movementStates.running = false; movementStates.falling = false; movementStates.swimming = false; movementStates.jumping = false; } @Override public double steer( @Nonnull Ref ref, @Nonnull Role role, @Nonnull Steering bodySteering, @Nonnull Steering headSteering, double interval, @Nonnull ComponentAccessor componentAccessor ) { this.readEntityPosition(ref, componentAccessor); if (this.debugModeSteer) { double dx = this.position.x; double dz = this.position.z; double st = this.steer0(ref, role, bodySteering, headSteering, interval, componentAccessor); double t = interval - st; dx = this.position.x - dx; dz = this.position.z - dz; double l = Math.sqrt(dx * dx + dz * dz); double v = t > 0.0 ? l / t : 0.0; LOGGER.at(Level.INFO) .log( "== Steer %s = t =%.4f dt=%.4f h =%.4f l =%.4f v =%.4f motion=%s", this.getType(), interval, t, (180.0F / (float)Math.PI) * this.yaw, l, v, role.getSteeringMotionName() ); return st; } else { return this.steer0(ref, role, bodySteering, headSteering, interval, componentAccessor); } } public double steer0( @Nonnull Ref ref, @Nonnull Role role, @Nonnull Steering bodySteering, @Nonnull Steering headSteering, double interval, @Nonnull ComponentAccessor componentAccessor ) { World world = componentAccessor.getExternalData().getWorld(); NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType()); assert npcComponent != null; this.effectHorizontalSpeedMultiplier = npcComponent.getCurrentHorizontalSpeedMultiplier(ref, componentAccessor); this.setAvoidingBlockDamage(this.isAvoidingBlockDamage && !this.isReceivingBlockDamage); this.translation.assign(0.0); this.cachedMovementBlocked = this.isMovementBlocked(ref, componentAccessor); this.computeMove(ref, role, bodySteering, interval, this.translation, componentAccessor); if (this.debugModeValidateMath && !NPCPhysicsMath.isValid(this.translation)) { throw new IllegalArgumentException(String.valueOf(this.translation)); } else { if (this.translation.squaredLength() > 1000000.0) { if (this.debugModeValidateMath) { LOGGER.at(Level.WARNING) .log("NPC with role %s has abnormal high speed! (Distance=%s, MotionController=%s)", role.getRoleName(), this.translation.length(), this.type); } this.translation.assign(Vector3d.ZERO); } this.executeMove(ref, role, interval, this.translation, componentAccessor); this.postExecuteMove(); this.clearRequirePreciseMovement(); this.clearRequireDepthProbing(); this.clearBlendHeading(); this.setAvoidingBlockDamage(!this.isReceivingBlockDamage); this.setRelaxedMoveConstraints(false); float maxBodyRotation = (float)(interval * this.getCurrentMaxBodyRotationSpeed() * bodySteering.getRelativeTurnSpeed()); float maxHeadRotation = (float)(interval * this.maxHeadRotationSpeed * headSteering.getRelativeTurnSpeed() * this.effectHorizontalSpeedMultiplier); this.calculateYaw(ref, bodySteering, headSteering, maxHeadRotation, maxBodyRotation, componentAccessor); this.calculatePitch(ref, bodySteering, headSteering, maxHeadRotation, componentAccessor); this.calculateRoll(bodySteering, headSteering); this.moveEntity(ref, interval, componentAccessor); HeadRotation headRotationComponent = componentAccessor.getComponent(ref, HeadRotation.getComponentType()); assert headRotationComponent != null; Vector3f headRotation = headRotationComponent.getRotation(); headRotation.setYaw(headSteering.getYaw()); headRotation.setPitch(headSteering.getPitch()); headRotation.setRoll(headSteering.getRoll()); if (!this.forceVelocity.equals(Vector3d.ZERO) && !this.ignoreDamping) { double movementThresholdSquared = 1.0000000000000002E-10; if (this.forceVelocity.squaredLength() >= movementThresholdSquared) { this.dampForceVelocity(this.forceVelocity, this.forceVelocityDamping, interval, componentAccessor); } else { this.forceVelocity.assign(Vector3d.ZERO); } } double clientTps = 60.0; int serverTps = world.getTps(); double rate = clientTps / serverTps; boolean dampenY = this.shouldDampenAppliedVelocitiesY(); boolean useGroundResistance = this.shouldAlwaysUseGroundResistance() || this.onGround(); for (int i = 0; i < this.appliedVelocities.size(); i++) { MotionControllerBase.AppliedVelocity entry = this.appliedVelocities.get(i); float min; float max; if (useGroundResistance) { min = entry.config.getGroundResistance(); max = entry.config.getGroundResistanceMax(); } else { min = entry.config.getAirResistance(); max = entry.config.getAirResistanceMax(); } float resistance = min; if (max >= 0.0F) { resistance = switch (entry.config.getStyle()) { case Linear -> { float len = (float)entry.velocity.length(); if (len < entry.config.getThreshold()) { float mul = len / entry.config.getThreshold(); yield min * mul + max * (1.0F - mul); } else { yield min; } } case Exp -> { float len = (float)entry.velocity.squaredLength(); if (len < entry.config.getThreshold() * entry.config.getThreshold()) { float mul = len / (entry.config.getThreshold() * entry.config.getThreshold()); yield min * mul + max * (1.0F - mul); } else { yield min; } } }; } double resistanceScale = Math.pow(resistance, rate); entry.velocity.x *= resistanceScale; entry.velocity.z *= resistanceScale; if (dampenY) { entry.velocity.y *= resistanceScale; } } this.appliedVelocities.removeIf(v -> v.velocity.squaredLength() < 0.001); return interval; } } protected boolean shouldDampenAppliedVelocitiesY() { return false; } protected boolean shouldAlwaysUseGroundResistance() { return false; } protected void calculateYaw( @Nonnull Ref ref, @Nonnull Steering bodySteering, @Nonnull Steering headSteering, float maxHeadRotation, float maxBodyRotation, @Nonnull ComponentAccessor componentAccessor ) { if (bodySteering.hasYaw()) { this.yaw = bodySteering.getYaw(); } else if (NPCPhysicsMath.dotProduct(this.translation.x, 0.0, this.translation.z) > 0.001) { this.yaw = PhysicsMath.headingFromDirection(this.translation.x, this.translation.z); } boolean hasHeadSteering = headSteering.hasYaw(); if (!hasHeadSteering) { headSteering.setYaw(this.yaw); } HeadRotation headRotationComponent = componentAccessor.getComponent(ref, HeadRotation.getComponentType()); assert headRotationComponent != null; ModelComponent modelComponent = componentAccessor.getComponent(ref, ModelComponent.getComponentType()); assert modelComponent != null; Vector3f headRotation = headRotationComponent.getRotation(); float currentYaw = headRotation.getYaw(); float targetYaw = headSteering.getYaw(); float turnAngle = MathUtil.clamp(NPCPhysicsMath.turnAngle(currentYaw, targetYaw), -maxHeadRotation, maxHeadRotation); headSteering.setYaw(PhysicsMath.normalizeTurnAngle(currentYaw + turnAngle)); if (hasHeadSteering) { float yawOffset = MathUtil.wrapAngle(headSteering.getYaw() - this.yaw); CameraSettings headRotationRestrictions = modelComponent.getModel().getCamera(); float yawMin; float yawMax; if (headRotationRestrictions != null && headRotationRestrictions.getYaw() != null && headRotationRestrictions.getYaw().getAngleRange() != null) { Rangef yawRange = headRotationRestrictions.getYaw().getAngleRange(); yawMin = yawRange.min * (float) (Math.PI / 180.0); yawMax = yawRange.max * (float) (Math.PI / 180.0); } else { yawMin = (float) (-Math.PI / 4); yawMax = (float) (Math.PI / 4); } if (yawOffset > yawMax) { float initialBodyYaw = this.yaw; if (!bodySteering.hasYaw()) { this.yaw = this.blendBodyYaw(ref, yawOffset, maxBodyRotation, componentAccessor); } headSteering.setYaw(MathUtil.wrapAngle(initialBodyYaw + yawMax)); } else if (yawOffset < yawMin) { float initialBodyYaw = this.yaw; if (!bodySteering.hasYaw()) { this.yaw = this.blendBodyYaw(ref, yawOffset, maxBodyRotation, componentAccessor); } headSteering.setYaw(MathUtil.wrapAngle(initialBodyYaw + yawMin)); } } } protected float blendBodyYaw( @Nonnull Ref ref, float yawOffset, float maxBodyRotation, @Nonnull ComponentAccessor componentAccessor ) { TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType()); assert transformComponent != null; Vector3f bodyRotation = transformComponent.getRotation(); float currentBodyYaw = bodyRotation.getYaw(); float targetBodyYaw = MathUtil.wrapAngle(this.yaw + yawOffset); float bodyTurnAngle = MathUtil.clamp(NPCPhysicsMath.turnAngle(currentBodyYaw, targetBodyYaw), -maxBodyRotation, maxBodyRotation); return MathUtil.wrapAngle(this.yaw + bodyTurnAngle); } protected void calculatePitch( @Nonnull Ref ref, @Nonnull Steering bodySteering, @Nonnull Steering headSteering, float maxHeadRotation, @Nonnull ComponentAccessor componentAccessor ) { if (bodySteering.hasPitch()) { this.pitch = bodySteering.getPitch(); } else if (NPCPhysicsMath.dotProduct(this.translation.x, this.translation.y, this.translation.z) > 0.001) { this.pitch = PhysicsMath.pitchFromDirection(this.translation.x, this.translation.y, this.translation.z); } boolean hasHeadSteering = headSteering.hasPitch(); if (!hasHeadSteering) { headSteering.setPitch(this.pitch); } HeadRotation headRotationComponent = componentAccessor.getComponent(ref, HeadRotation.getComponentType()); assert headRotationComponent != null; Vector3f headRotation = headRotationComponent.getRotation(); float currentPitch = headRotation.getPitch(); float targetPitch = headSteering.getPitch(); float turnAngle = MathUtil.clamp(NPCPhysicsMath.turnAngle(currentPitch, targetPitch), -maxHeadRotation, maxHeadRotation); headSteering.setPitch(PhysicsMath.normalizeTurnAngle(currentPitch + turnAngle)); if (hasHeadSteering) { ModelComponent modelComponent = componentAccessor.getComponent(ref, ModelComponent.getComponentType()); assert modelComponent != null; float bodyPitch = this.pitch; float pitchOffset = MathUtil.wrapAngle(headSteering.getPitch() - bodyPitch); CameraSettings headRotationRestrictions = modelComponent.getModel().getCamera(); float pitchMin; float pitchMax; if (headRotationRestrictions != null && headRotationRestrictions.getPitch() != null && headRotationRestrictions.getPitch().getAngleRange() != null) { Rangef pitchRange = headRotationRestrictions.getPitch().getAngleRange(); pitchMin = pitchRange.min * (float) (Math.PI / 180.0); pitchMax = pitchRange.max * (float) (Math.PI / 180.0); } else { pitchMin = (float) (-Math.PI / 4); pitchMax = (float) (Math.PI / 4); } if (pitchOffset > pitchMax) { headSteering.setPitch(MathUtil.wrapAngle(bodyPitch + pitchMax)); } else if (pitchOffset < pitchMin) { headSteering.setPitch(MathUtil.wrapAngle(bodyPitch + pitchMin)); } } } protected void calculateRoll(@Nonnull Steering bodySteering, @Nonnull Steering headSteering) { if (bodySteering.hasRoll()) { this.roll = bodySteering.getRoll(); } if (!headSteering.hasRoll()) { headSteering.setRoll(this.roll); } } protected void dampForceVelocity( @Nonnull Vector3d forceVelocity, double forceVelocityDamping, double interval, ComponentAccessor componentAccessor ) { World world = componentAccessor.getExternalData().getWorld(); double drag = 0.0; if (this.motionKind != MotionKind.FLYING) { if (!this.onGround() && this.motionKind != MotionKind.SWIMMING && this.motionKind != MotionKind.SWIMMING_TURNING) { double horizontalSpeed = Math.sqrt(forceVelocity.x * forceVelocity.x + forceVelocity.z * forceVelocity.z); drag = convertToNewRange( horizontalSpeed, this.movementSettings.airDragMinSpeed, this.movementSettings.airDragMaxSpeed, this.movementSettings.airDragMin, this.movementSettings.airDragMax ); } else { drag = 0.82; } } double clientTps = 60.0; int serverTps = world.getTps(); double rate = 60.0 / serverTps; drag = Math.pow(drag, rate); forceVelocity.x *= drag; forceVelocity.z *= drag; float velocityEpsilon = 0.1F; if (Math.abs(forceVelocity.x) <= velocityEpsilon) { forceVelocity.x = 0.0; } if (Math.abs(forceVelocity.y) <= velocityEpsilon) { forceVelocity.y = 0.0; } if (Math.abs(forceVelocity.z) <= velocityEpsilon) { forceVelocity.z = 0.0; } } private static double convertToNewRange(double value, double oldMinRange, double oldMaxRange, double newMinRange, double newMaxRange) { if (newMinRange != newMaxRange && oldMinRange != oldMaxRange) { double newValue = (value - oldMinRange) * (newMaxRange - newMinRange) / (oldMaxRange - oldMinRange) + newMinRange; return MathUtil.clamp(newValue, Math.min(newMinRange, newMaxRange), Math.max(newMinRange, newMaxRange)); } else { return newMinRange; } } @Override public double probeMove( @Nonnull Ref ref, @Nonnull Vector3d position, @Nonnull Vector3d direction, @Nonnull ProbeMoveData probeMoveData, @Nonnull ComponentAccessor componentAccessor ) { probeMoveData.setPosition(position).setDirection(direction); return this.probeMove(ref, probeMoveData, componentAccessor); } protected void postExecuteMove() { } protected void adjustReadPosition(Ref ref, ComponentAccessor componentAccessor) { } protected void adjustWritePosition(Ref ref, double dt, @Nonnull ComponentAccessor componentAccessor) { } @Override public boolean isInProgress() { return false; } @Override public boolean isObstructed() { return this.isObstructed; } @Override public NavState getNavState() { return this.navState; } @Override public double getThrottleDuration() { return this.throttleDuration; } @Override public double getTargetDeltaSquared() { return this.targetDeltaSquared; } @Override public void setNavState(NavState navState, double throttleDuration, double targetDeltaSquared) { this.navState = navState; this.throttleDuration = throttleDuration; this.targetDeltaSquared = targetDeltaSquared; } @Override public boolean isForceRecomputePath() { return this.recomputePath; } @Override public void setForceRecomputePath(boolean recomputePath) { this.recomputePath = recomputePath; } @Override public void beforeInstructionSensorsAndActions(double physicsTickDuration) { this.recomputePath = false; } @Override public void beforeInstructionMotion(double physicsTickDuration) { this.resetNavState(); } public boolean isHorizontalIdle(double speed) { return speed == 0.0; } @Override public boolean canAct(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { return this.isAlive(ref, componentAccessor) && this.role.couldBreatheCached() && this.forceVelocity.equals(Vector3d.ZERO) && this.appliedVelocities.isEmpty(); } public boolean isMovementBlocked(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { InteractionManager interactionManager = componentAccessor.getComponent(ref, InteractionModule.get().getInteractionManagerComponent()); if (interactionManager != null) { Boolean movementBlocked = interactionManager.forEachInteraction((chain, interaction, val) -> { if (val) { return Boolean.TRUE; } else { MovementEffects movementEffects = interaction.getEffects().getMovementEffects(); return movementEffects != null ? movementEffects.isDisableAll() : Boolean.FALSE; } }, Boolean.FALSE); return movementBlocked; } else { return false; } } protected abstract double computeMove( @Nonnull Ref var1, @Nonnull Role var2, Steering var3, double var4, Vector3d var6, @Nonnull ComponentAccessor var7 ); protected abstract double executeMove( @Nonnull Ref var1, @Nonnull Role var2, double var3, Vector3d var5, @Nonnull ComponentAccessor var6 ); public double bisect( @Nonnull Vector3d validPosition, @Nonnull Vector3d invalidPosition, @Nonnull T t, @Nonnull BiPredicate validate, @Nonnull Vector3d result ) { return this.bisect(validPosition, invalidPosition, t, validate, 0.05, result); } public double bisect( @Nonnull Vector3d validPosition, @Nonnull Vector3d invalidPosition, @Nonnull T t, @Nonnull BiPredicate validate, double maxDistance, @Nonnull Vector3d result ) { double validDistance = 0.0; double invalidDistance = 1.0; this.bisectValidPosition.assign(validPosition); this.bisectInvalidPosition.assign(invalidPosition); maxDistance *= maxDistance; double validWeight = 0.1; double invalidWeight = 0.9; while (this.bisectValidPosition.distanceSquaredTo(this.bisectInvalidPosition) > maxDistance) { double distance = validWeight * validDistance + invalidWeight * invalidDistance; result.x = validWeight * this.bisectValidPosition.x + invalidWeight * this.bisectInvalidPosition.x; result.y = validWeight * this.bisectValidPosition.y + invalidWeight * this.bisectInvalidPosition.y; result.z = validWeight * this.bisectValidPosition.z + invalidWeight * this.bisectInvalidPosition.z; if (validate.test(t, result)) { validDistance = distance; this.bisectValidPosition.assign(result); } else { invalidDistance = distance; this.bisectInvalidPosition.assign(result); validWeight = 0.5; invalidWeight = 0.5; } } result.assign(this.bisectValidPosition); return validDistance; } @Nonnull @Override public Vector3d getForce() { return this.forceVelocity; } @Override public void addForce(@Nonnull Vector3d force, VelocityConfig velocityConfig) { double scale = this.knockbackScale; if (!SplitVelocity.SHOULD_MODIFY_VELOCITY && velocityConfig != null) { this.appliedVelocities.add(new MotionControllerBase.AppliedVelocity(new Vector3d(force.x * scale, force.y * scale, force.z * scale), velocityConfig)); } else { double horzMul = 0.18000000000000005 * this.movementSettings.velocityResistance; this.forceVelocity.add(force.x * scale * horzMul, force.y * scale, force.z * scale * horzMul); this.appliedForce.assign(this.forceVelocity); this.ignoreDamping = false; } } @Override public void forceVelocity(@Nonnull Vector3d velocity, @Nullable VelocityConfig velocityConfig, boolean ignoreDamping) { if (!SplitVelocity.SHOULD_MODIFY_VELOCITY && velocityConfig != null) { this.appliedVelocities.clear(); this.appliedVelocities.add(new MotionControllerBase.AppliedVelocity(velocity.clone(), velocityConfig)); } else { this.forceVelocity.assign(velocity); this.ignoreDamping = ignoreDamping; } } public void clearForce() { this.forceVelocity.assign(Vector3d.ZERO); } protected void dumpCollisionResults() { String slideString = ""; if (this.collisionResult.isSliding) { slideString = String.format("SLIDE: start/end=%f/%f", this.collisionResult.slideStart, this.collisionResult.slideEnd); } LOGGER.at(Level.INFO) .log( "CollRes: pos=%s yaw=%f count=%d %s", Vector3d.formatShortString(this.position), (180.0F / (float)Math.PI) * this.yaw, this.collisionResult.getBlockCollisionCount(), slideString ); if (this.collisionResult.getBlockCollisionCount() > 0) { for (int i = 0; i < this.collisionResult.getBlockCollisionCount(); i++) { BlockCollisionData cd = this.collisionResult.getBlockCollision(i); String materialName = cd.blockMaterial != null ? cd.blockMaterial.name() : "none"; String typeName = cd.blockType != null ? cd.blockType.getId() : "none"; String hitboxName = cd.blockType != null ? cd.blockType.getHitboxType() : "none"; String rotation; if (cd.blockType != null) { RotationTuple blockRotation = RotationTuple.get(cd.rotation); rotation = blockRotation.yaw() + " " + blockRotation.pitch(); } else { rotation = "none"; } LOGGER.at(Level.INFO) .log( " COLL: blk=%s/%s/%s start=%f norm=%s pos=%s mat=%s block=%s hitbox=%s rot=%s", cd.x, cd.y, cd.z, cd.collisionStart, Vector3d.formatShortString(cd.collisionNormal), Vector3d.formatShortString(cd.collisionPoint), materialName, typeName, hitboxName, rotation ); } } } public void setEnableTriggers(boolean enableTriggers) { this.enableTriggers = enableTriggers; } public void setEnableBlockDamage(boolean enableBlockDamage) { this.enableBlockDamage = enableBlockDamage; } @Override public boolean willReceiveBlockDamage() { return this.isReceivingBlockDamage; } @Override public void setAvoidingBlockDamage(boolean avoid) { this.isAvoidingBlockDamage = avoid; } @Override public boolean isAvoidingBlockDamage() { return this.isAvoidingBlockDamage; } public void processTriggers( @Nonnull Ref ref, @Nonnull CollisionResult collisionResult, double t, @Nonnull ComponentAccessor componentAccessor ) { this.processTriggersHasMoved = false; this.isReceivingBlockDamage = false; if (this.enableTriggers || this.enableBlockDamage) { collisionResult.pruneTriggerBlocks(t); int count = collisionResult.getTriggerBlocks().size(); if (count != 0) { if (this.enableTriggers) { this.beforeTriggerForce.assign(this.getForce()); this.beforeTriggerPosition.assign(this.position); } this.moveEntity(ref, 0.0, componentAccessor); InteractionManager interactionManagerComponent = componentAccessor.getComponent(ref, InteractionModule.get().getInteractionManagerComponent()); assert interactionManagerComponent != null; int damageToEntity = collisionResult.defaultTriggerBlocksProcessing( interactionManagerComponent, this.entity, ref, this.enableTriggers, componentAccessor ); if (this.enableBlockDamage && damageToEntity > 0) { Damage damage = new Damage(Damage.NULL_SOURCE, DamageCause.ENVIRONMENT, damageToEntity); DamageSystems.executeDamage(ref, componentAccessor, damage); this.isReceivingBlockDamage = true; } this.readEntityPosition(ref, componentAccessor); if (this.enableTriggers) { this.processTriggersHasMoved = !this.beforeTriggerForce.equals(this.getForce()) || !this.beforeTriggerPosition.equals(this.position); } } } } protected boolean isDebugMode(RoleDebugFlags mode) { return this.getRole() != null && this.getRole().getDebugSupport().getDebugFlags().contains(mode); } public boolean isProcessTriggersHasMoved() { return this.processTriggersHasMoved; } protected boolean isAlive(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { return !componentAccessor.getArchetype(ref).contains(DeathComponent.getComponentType()); } @Override public void activate() { this.debugModeSteer = this.isDebugMode(RoleDebugFlags.MotionControllerSteer); this.debugModeMove = this.isDebugMode(RoleDebugFlags.MotionControllerMove); this.debugModeCollisions = this.isDebugMode(RoleDebugFlags.Collisions); this.debugModeBlockCollisions = this.isDebugMode(RoleDebugFlags.BlockCollisions); this.debugModeProbeBlockCollisions = this.isDebugMode(RoleDebugFlags.ProbeBlockCollisions); this.debugModeValidatePositions = this.isDebugMode(RoleDebugFlags.ValidatePositions); this.debugModeOverlaps = this.isDebugMode(RoleDebugFlags.Overlaps); this.debugModeValidateMath = this.isDebugMode(RoleDebugFlags.ValidateMath); this.resetObstructedFlags(); this.resetNavState(); } public void resetNavState() { this.navState = NavState.AT_GOAL; this.throttleDuration = 0.0; this.targetDeltaSquared = 0.0; } public void resetObstructedFlags() { this.isObstructed = false; } @Override public void deactivate() { } public double getEpsilonSpeed() { return this.epsilonSpeed; } public float getEpsilonAngle() { return this.epsilonAngle; } @Nonnull @Override public Vector3d getComponentSelector() { return this.componentSelector; } @Nonnull @Override public Vector3d getPlanarComponentSelector() { return this.planarComponentSelector; } @Override public void setComponentSelector(@Nonnull Vector3d componentSelector) { this.componentSelector.assign(componentSelector); } @Override public Vector3d getWorldNormal() { return this.worldNormal; } @Override public Vector3d getWorldAntiNormal() { return this.worldAntiNormal; } @Override public double waypointDistance(@Nonnull Vector3d p, @Nonnull Vector3d q) { return Math.sqrt(this.waypointDistanceSquared(p, q)); } @Override public double waypointDistanceSquared(@Nonnull Vector3d p, @Nonnull Vector3d q) { double dx = (p.x - q.x) * this.getComponentSelector().x; double dy = (p.y - q.y) * this.getComponentSelector().y; double dz = (p.z - q.z) * this.getComponentSelector().z; return dx * dx + dy * dy + dz * dz; } @Override public double waypointDistance(@Nonnull Ref ref, @Nonnull Vector3d p, @Nonnull ComponentAccessor componentAccessor) { return Math.sqrt(this.waypointDistanceSquared(ref, p, componentAccessor)); } @Override public double waypointDistanceSquared(@Nonnull Ref ref, @Nonnull Vector3d p, @Nonnull ComponentAccessor componentAccessor) { TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType()); assert transformComponent != null; Vector3d position = transformComponent.getPosition(); double dx = (p.x - position.getX()) * this.getComponentSelector().x; double dy = (p.y - position.getY()) * this.getComponentSelector().y; double dz = (p.z - position.getZ()) * this.getComponentSelector().z; return dx * dx + dy * dy + dz * dz; } @Override public boolean isValidPosition(@Nonnull Vector3d position, @Nonnull ComponentAccessor componentAccessor) { return this.isValidPosition(position, this.collisionResult, componentAccessor); } public boolean isValidPosition( @Nonnull Vector3d position, @Nonnull CollisionResult collisionResult, @Nonnull ComponentAccessor componentAccessor ) { World world = componentAccessor.getExternalData().getWorld(); CollisionModule module = CollisionModule.get(); CollisionModuleConfig config = module.getConfig(); boolean saveDebugModeOverlaps = config.isDumpInvalidBlocks(); config.setDumpInvalidBlocks(this.debugModeOverlaps); boolean isValid = module.validatePosition( world, this.collisionBoundingBox, position, this.getInvalidOverlapMaterials(), null, (_this, collisionCode, collision, collisionConfig) -> collisionConfig.blockId != Integer.MIN_VALUE, collisionResult ) != -1; config.setDumpInvalidBlocks(saveDebugModeOverlaps); return isValid; } public int getInvalidOverlapMaterials() { return 4; } protected void saveMotionKind() { this.previousMotionKind = this.getMotionKind(); } protected boolean switchedToMotionKind(MotionKind motionKind) { return this.getMotionKind() == motionKind && this.previousMotionKind != motionKind; } public MotionKind getMotionKind() { return this.motionKind; } public void setMotionKind(MotionKind motionKind) { this.motionKind = motionKind; } @Override public double getGravity() { return this.gravity; } public void setGravity(double gravity) { this.gravity = gravity; } @Override public boolean translateToAccessiblePosition( Vector3d position, Box boundingBox, double minYValue, double maxYValue, ComponentAccessor componentAccessor ) { return true; } @Override public boolean standingOnBlockOfType(int blockSet) { return false; } @Override public void requirePreciseMovement(@Nullable Vector3d positionHint) { this.requiresPreciseMovement = true; this.havePreciseMovementTarget = positionHint != null; if (this.havePreciseMovementTarget) { this.preciseMovementTarget.assign(positionHint); } } public void clearRequirePreciseMovement() { this.requiresPreciseMovement = false; this.havePreciseMovementTarget = false; } public boolean isRequiresPreciseMovement() { return this.requiresPreciseMovement; } @Override public void requireDepthProbing() { this.requiresDepthProbing = true; } public void clearRequireDepthProbing() { this.requiresDepthProbing = false; } public boolean isRequiresDepthProbing() { return this.requiresDepthProbing; } @Override public void enableHeadingBlending(double heading, @Nullable Vector3d targetPosition, double blendLevel) { this.isBlendingHeading = true; this.blendHeading = heading; this.haveBlendHeadingPosition = targetPosition != null; if (this.haveBlendHeadingPosition) { this.blendHeadingPosition.assign(targetPosition); } this.blendLevelAtTargetPosition = blendLevel; } @Override public void enableHeadingBlending() { this.enableHeadingBlending(Double.NaN, null, 0.0); } public void clearBlendHeading() { this.isBlendingHeading = false; this.haveBlendHeadingPosition = false; } @Override public void setRelaxedMoveConstraints(boolean relax) { this.isRelaxedMoveConstraints = relax; } @Override public boolean isRelaxedMoveConstraints() { return this.isRelaxedMoveConstraints; } @Override public void updatePhysicsValues(PhysicsValues values) { this.movementSettings = MovementManager.MASTER_DEFAULT.apply(values, GameMode.Adventure); } protected static class AppliedVelocity { protected final Vector3d velocity; protected final VelocityConfig config; protected boolean canClear; public AppliedVelocity(Vector3d velocity, VelocityConfig config) { this.velocity = velocity; this.config = config; } } }