/*
* Copyright 2016 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.logic.players;
import org.terasology.assets.ResourceUrn;
import org.terasology.config.BindsConfig;
import org.terasology.config.Config;
import org.terasology.engine.SimpleUri;
import org.terasology.engine.Time;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.event.EventPriority;
import org.terasology.entitySystem.event.ReceiveEvent;
import org.terasology.entitySystem.systems.BaseComponentSystem;
import org.terasology.entitySystem.systems.RenderSystem;
import org.terasology.entitySystem.systems.UpdateSubscriberSystem;
import org.terasology.input.ButtonState;
import org.terasology.input.Input;
import org.terasology.input.InputSystem;
import org.terasology.input.binds.interaction.FrobButton;
import org.terasology.input.binds.inventory.UseItemButton;
import org.terasology.input.binds.movement.AutoMoveButton;
import org.terasology.input.binds.movement.CrouchButton;
import org.terasology.input.binds.movement.CrouchModeButton;
import org.terasology.input.binds.movement.ForwardsMovementAxis;
import org.terasology.input.binds.movement.ForwardsRealMovementAxis;
import org.terasology.input.binds.movement.JumpButton;
import org.terasology.input.binds.movement.RotationPitchAxis;
import org.terasology.input.binds.movement.RotationYawAxis;
import org.terasology.input.binds.movement.StrafeMovementAxis;
import org.terasology.input.binds.movement.StrafeRealMovementAxis;
import org.terasology.input.binds.movement.ToggleSpeedPermanentlyButton;
import org.terasology.input.binds.movement.ToggleSpeedTemporarilyButton;
import org.terasology.input.binds.movement.VerticalMovementAxis;
import org.terasology.input.binds.movement.VerticalRealMovementAxis;
import org.terasology.input.events.MouseXAxisEvent;
import org.terasology.input.events.MouseYAxisEvent;
import org.terasology.logic.characters.CharacterComponent;
import org.terasology.logic.characters.CharacterHeldItemComponent;
import org.terasology.logic.characters.CharacterMoveInputEvent;
import org.terasology.logic.characters.CharacterMovementComponent;
import org.terasology.logic.characters.GazeMountPointComponent;
import org.terasology.logic.characters.MovementMode;
import org.terasology.logic.characters.events.OnItemUseEvent;
import org.terasology.logic.characters.events.SetMovementModeEvent;
import org.terasology.logic.characters.interactions.InteractionUtil;
import org.terasology.logic.debug.MovementDebugCommands;
import org.terasology.logic.delay.DelayManager;
import org.terasology.logic.location.LocationComponent;
import org.terasology.logic.notifications.NotificationMessageEvent;
import org.terasology.logic.players.event.OnPlayerSpawnedEvent;
import org.terasology.math.AABB;
import org.terasology.math.Direction;
import org.terasology.math.TeraMath;
import org.terasology.math.geom.Quat4f;
import org.terasology.math.geom.Vector3f;
import org.terasology.network.ClientComponent;
import org.terasology.network.NetworkSystem;
import org.terasology.physics.engine.CharacterCollider;
import org.terasology.physics.engine.PhysicsEngine;
import org.terasology.physics.engine.SweepCallback;
import org.terasology.registry.In;
import org.terasology.rendering.AABBRenderer;
import org.terasology.rendering.BlockOverlayRenderer;
import org.terasology.rendering.cameras.Camera;
import org.terasology.rendering.cameras.PerspectiveCamera;
import org.terasology.rendering.logic.MeshComponent;
import org.terasology.world.WorldProvider;
import org.terasology.world.block.Block;
import org.terasology.world.block.BlockComponent;
import org.terasology.world.block.regions.BlockRegionComponent;
import java.util.List;
import static org.terasology.logic.characters.KinematicCharacterMover.VERTICAL_PENETRATION_LEEWAY;
// TODO: This needs a really good cleanup
// TODO: Move more input stuff to a specific input system?
// TODO: Camera should become an entity/component, so it can follow the player naturally
public class LocalPlayerSystem extends BaseComponentSystem implements UpdateSubscriberSystem, RenderSystem {
@In
NetworkSystem networkSystem;
@In
private LocalPlayer localPlayer;
@In
private WorldProvider worldProvider;
private Camera playerCamera;
@In
private MovementDebugCommands movementDebugCommands;
@In
private PhysicsEngine physics;
@In
private DelayManager delayManager;
@In
private Config config;
@In
private InputSystem inputSystem;
private BindsConfig bindsConfig;
private float bobFactor;
private float lastStepDelta;
// Input
private Vector3f relativeMovement = new Vector3f();
private boolean isAutoMove = false;
private boolean runPerDefault = true;
private boolean run = runPerDefault;
private boolean jump;
private float lookPitch;
private float lookPitchDelta;
private float lookYaw;
private float lookYawDelta;
private float crouchFraction = 0.5f;
@In
private Time time;
private BlockOverlayRenderer aabbRenderer = new AABBRenderer(AABB.createEmpty());
private int inputSequenceNumber = 1;
private AABB aabb;
public void setPlayerCamera(Camera camera) {
playerCamera = camera;
}
@Override
public void update(float delta) {
if (!localPlayer.isValid()) {
return;
}
EntityRef entity = localPlayer.getCharacterEntity();
CharacterMovementComponent characterMovementComponent = entity.getComponent(CharacterMovementComponent.class);
processInput(entity, characterMovementComponent);
updateCamera(characterMovementComponent, localPlayer.getViewPosition(), localPlayer.getViewRotation());
}
private void processInput(EntityRef entity, CharacterMovementComponent characterMovementComponent) {
lookYaw = (lookYaw - lookYawDelta) % 360;
lookYawDelta = 0f;
lookPitch = TeraMath.clamp(lookPitch + lookPitchDelta, -89, 89);
lookPitchDelta = 0f;
Vector3f relMove = new Vector3f(relativeMovement);
relMove.y = 0;
Quat4f viewRot;
switch (characterMovementComponent.mode) {
case WALKING:
viewRot = new Quat4f(TeraMath.DEG_TO_RAD * lookYaw, 0, 0);
viewRot.rotate(relMove, relMove);
break;
case CLIMBING:
// Rotation is applied in KinematicCharacterMover
relMove.y += relativeMovement.y;
break;
default:
viewRot = new Quat4f(TeraMath.DEG_TO_RAD * lookYaw, TeraMath.DEG_TO_RAD * lookPitch, 0);
viewRot.rotate(relMove, relMove);
relMove.y += relativeMovement.y;
break;
}
entity.send(new CharacterMoveInputEvent(inputSequenceNumber++, lookPitch, lookYaw, relMove, run, jump, time.getGameDeltaInMs()));
jump = false;
}
/**
* Reduces height and eyeHeight by crouchFraction and changes MovementMode.
*/
private void crouchPlayer(EntityRef entity) {
ClientComponent clientComp = entity.getComponent(ClientComponent.class);
GazeMountPointComponent gazeMountPointComponent = clientComp.character.getComponent(GazeMountPointComponent.class);
float height = clientComp.character.getComponent(CharacterMovementComponent.class).height;
float eyeHeight = gazeMountPointComponent.translate.getY();
movementDebugCommands.playerHeight(localPlayer.getClientEntity(), height * crouchFraction);
movementDebugCommands.playerEyeHeight(localPlayer.getClientEntity(), eyeHeight * crouchFraction);
clientComp.character.send(new SetMovementModeEvent(MovementMode.CROUCHING));
}
/**
* Checks if there is an impenetrable block above,
* Raises a Notification "Cannot stand up here!" if present
* If not present, increases height and eyeHeight by crouchFraction and changes MovementMode.
*/
private void standPlayer(EntityRef entity) {
ClientComponent clientComp = entity.getComponent(ClientComponent.class);
GazeMountPointComponent gazeMountPointComponent = clientComp.character.getComponent(GazeMountPointComponent.class);
float height = clientComp.character.getComponent(CharacterMovementComponent.class).height;
float eyeHeight = gazeMountPointComponent.translate.getY();
Vector3f pos = entity.getComponent(LocationComponent.class).getWorldPosition();
// Check for collision when rising
CharacterCollider collider = physics.getCharacterCollider(clientComp.character);
// height used below = (1 - crouch_fraction) * standing_height
Vector3f to = new Vector3f(pos.x, pos.y + (1 - crouchFraction) * height / crouchFraction, pos.z);
SweepCallback callback = collider.sweep(pos, to, VERTICAL_PENETRATION_LEEWAY, -1f);
if (callback.hasHit()) {
entity.send(new NotificationMessageEvent("Cannot stand up here!", entity));
return;
}
movementDebugCommands.playerHeight(localPlayer.getClientEntity(), height / crouchFraction);
movementDebugCommands.playerEyeHeight(localPlayer.getClientEntity(), eyeHeight / crouchFraction);
clientComp.character.send(new SetMovementModeEvent(MovementMode.WALKING));
}
// To check if a valid key has been assigned, either primary or secondary and return it
private Input getValidKey(List<Input> inputs) {
for(Input input: inputs) {
if(input != null) {
return input;
}
}
return null;
}
/**
* Auto move is disabled when the associated key is pressed again.
* This cancels the simulated repeated key stroke for the forward input button.
*/
private void stopAutoMove() {
List<Input> inputs = bindsConfig.getBinds(new SimpleUri("engine:forwards"));
Input forwardKey = getValidKey(inputs);
if(forwardKey != null) {
inputSystem.cancelSimulatedKeyStroke(forwardKey);
isAutoMove = false;
}
}
/**
* Append the input for moving forward to the keyboard command queue to simulate pressing of the forward key.
* For an input that repeats, the key must be in Down state before Repeat state can be applied to it.
*/
private void startAutoMove() {
isAutoMove = false;
bindsConfig = config.getInput().getBinds();
List<Input> inputs = bindsConfig.getBinds(new SimpleUri("engine:forwards"));
Input forwardKey = getValidKey(inputs);
if(forwardKey != null) {
isAutoMove = true;
inputSystem.simulateSingleKeyStroke(forwardKey);
inputSystem.simulateRepeatedKeyStroke(forwardKey);
}
}
@ReceiveEvent
public void onPlayerSpawn(OnPlayerSpawnedEvent event, EntityRef character) {
if (character.equals(localPlayer.getCharacterEntity())) {
// Change height as per PlayerSettings
Float height = config.getPlayer().getHeight();
movementDebugCommands.playerHeight(localPlayer.getClientEntity(), height);
// Change eyeHeight as per PlayerSettings
Float eyeHeight = config.getPlayer().getEyeHeight();
GazeMountPointComponent gazeMountPointComponent = character.getComponent(GazeMountPointComponent.class);
gazeMountPointComponent.translate = new Vector3f(0, eyeHeight, 0);
// Trigger updating the player camera position as soon as the local player is spawned.
// This is not done while the game is still loading, since systems are not updated.
// RenderableWorldImpl pre-generates chunks around the player camera and therefore needs
// the correct location.
lookYaw = 0f;
lookPitch = 0f;
update(0);
}
}
@ReceiveEvent(components = CharacterComponent.class)
public void onMouseX(MouseXAxisEvent event, EntityRef entity) {
lookYawDelta = event.getValue();
event.consume();
}
@ReceiveEvent(components = CharacterComponent.class)
public void onMouseY(MouseYAxisEvent event, EntityRef entity) {
lookPitchDelta = event.getValue();
event.consume();
}
@ReceiveEvent(components = {CharacterComponent.class})
public void updateRotationYaw(RotationYawAxis event, EntityRef entity) {
lookYawDelta = event.getValue();
event.consume();
}
@ReceiveEvent(components = {CharacterComponent.class})
public void updateRotationPitch(RotationPitchAxis event, EntityRef entity) {
lookPitchDelta = event.getValue();
event.consume();
}
@ReceiveEvent(components = {CharacterComponent.class, CharacterMovementComponent.class})
public void onJump(JumpButton event, EntityRef entity) {
if (event.getState() == ButtonState.DOWN) {
jump = true;
event.consume();
} else {
jump = false;
}
}
@ReceiveEvent(components = {ClientComponent.class})
public void updateForwardsMovement(ForwardsMovementAxis event, EntityRef entity) {
relativeMovement.z = event.getValue();
if(relativeMovement.z == 0f && isAutoMove) {
stopAutoMove();
}
event.consume();
}
@ReceiveEvent(components = {ClientComponent.class})
public void updateStrafeMovement(StrafeMovementAxis event, EntityRef entity) {
relativeMovement.x = event.getValue();
event.consume();
}
@ReceiveEvent(components = {ClientComponent.class})
public void updateVerticalMovement(VerticalMovementAxis event, EntityRef entity) {
relativeMovement.y = event.getValue();
event.consume();
}
@ReceiveEvent(components = {ClientComponent.class})
public void updateForwardsMovement(ForwardsRealMovementAxis event, EntityRef entity) {
relativeMovement.z = event.getValue();
event.consume();
}
@ReceiveEvent(components = {ClientComponent.class})
public void updateStrafeMovement(StrafeRealMovementAxis event, EntityRef entity) {
relativeMovement.x = event.getValue();
event.consume();
}
@ReceiveEvent(components = {ClientComponent.class})
public void updateVerticalMovement(VerticalRealMovementAxis event, EntityRef entity) {
relativeMovement.y = event.getValue();
event.consume();
}
@ReceiveEvent(components = {ClientComponent.class}, priority = EventPriority.PRIORITY_NORMAL)
public void onToggleSpeedTemporarily(ToggleSpeedTemporarilyButton event, EntityRef entity) {
boolean toggle = event.isDown();
run = runPerDefault ^ toggle;
event.consume();
}
// Crouches if button is pressed. Stands if button is released.
@ReceiveEvent(components = {ClientComponent.class}, priority = EventPriority.PRIORITY_NORMAL)
public void onCrouchTemporarily(CrouchButton event, EntityRef entity) {
ClientComponent clientComp = entity.getComponent(ClientComponent.class);
CharacterMovementComponent move = clientComp.character.getComponent(CharacterMovementComponent.class);
if (event.isDown() && move.mode == MovementMode.WALKING) {
crouchPlayer(entity);
} else if (!event.isDown() && move.mode == MovementMode.CROUCHING) {
standPlayer(entity);
}
event.consume();
}
@ReceiveEvent(components = {ClientComponent.class}, priority = EventPriority.PRIORITY_NORMAL)
public void onCrouchMode(CrouchModeButton event, EntityRef entity) {
ClientComponent clientComp = entity.getComponent(ClientComponent.class);
CharacterMovementComponent move = clientComp.character.getComponent(CharacterMovementComponent.class);
if (event.isDown()) {
if (move.mode == MovementMode.WALKING) {
crouchPlayer(entity);
} else if (move.mode == MovementMode.CROUCHING) {
standPlayer(entity);
}
}
event.consume();
}
@ReceiveEvent(components = {ClientComponent.class}, priority = EventPriority.PRIORITY_NORMAL)
public void onAutoMoveMode(AutoMoveButton event, EntityRef entity) {
if (event.isDown()) {
if (!isAutoMove) {
startAutoMove();
} else {
stopAutoMove();
}
}
event.consume();
}
@ReceiveEvent(components = {ClientComponent.class}, priority = EventPriority.PRIORITY_NORMAL)
public void onToggleSpeedPermanently(ToggleSpeedPermanentlyButton event, EntityRef entity) {
if (event.isDown()) {
runPerDefault = !runPerDefault;
run = !run;
}
event.consume();
}
@ReceiveEvent
public void onTargetChanged(PlayerTargetChangedEvent event, EntityRef entity) {
EntityRef target = event.getNewTarget();
if (target.exists()) {
LocationComponent location = target.getComponent(LocationComponent.class);
if (location != null) {
BlockComponent blockComp = target.getComponent(BlockComponent.class);
BlockRegionComponent blockRegion = target.getComponent(BlockRegionComponent.class);
if (blockComp != null || blockRegion != null) {
Vector3f blockPos = location.getWorldPosition();
Block block = worldProvider.getBlock(blockPos);
aabb = block.getBounds(blockPos);
} else {
MeshComponent mesh = target.getComponent(MeshComponent.class);
if (mesh != null && mesh.mesh != null) {
aabb = mesh.mesh.getAABB();
aabb = aabb.transform(location.getWorldRotation(), location.getWorldPosition(), location.getWorldScale());
}
}
}
} else {
aabb = null;
}
}
@Override
public void renderOverlay() {
// Display the block the player is aiming at
if (config.getRendering().isRenderPlacingBox()) {
if (aabb != null) {
aabbRenderer.setAABB(aabb);
aabbRenderer.render(2f);
}
}
}
public BlockOverlayRenderer getAABBRenderer() {
return aabbRenderer;
}
public void setAABBRenderer(BlockOverlayRenderer newAABBRender) {
aabbRenderer = newAABBRender;
}
private void updateCamera(CharacterMovementComponent charMovementComp, Vector3f position, Quat4f rotation) {
playerCamera.getPosition().set(position);
Vector3f viewDir = Direction.FORWARD.getVector3f();
rotation.rotate(viewDir, playerCamera.getViewingDirection());
float stepDelta = charMovementComp.footstepDelta - lastStepDelta;
if (stepDelta < 0) {
stepDelta += 1;
}
bobFactor += stepDelta;
lastStepDelta = charMovementComp.footstepDelta;
if (playerCamera.isBobbingAllowed()) {
if (config.getRendering().isCameraBobbing()) {
((PerspectiveCamera) playerCamera).setBobbingRotationOffsetFactor(calcBobbingOffset(0.0f, 0.01f, 2.5f));
((PerspectiveCamera) playerCamera).setBobbingVerticalOffsetFactor(calcBobbingOffset((float) java.lang.Math.PI / 4f, 0.025f, 3f));
} else {
((PerspectiveCamera) playerCamera).setBobbingRotationOffsetFactor(0.0f);
((PerspectiveCamera) playerCamera).setBobbingVerticalOffsetFactor(0.0f);
}
}
if (charMovementComp.mode == MovementMode.GHOSTING) {
playerCamera.extendFov(24);
} else {
playerCamera.resetFov();
}
}
@ReceiveEvent(components = {CharacterComponent.class})
public void onFrobButton(FrobButton event, EntityRef character) {
if (event.getState() != ButtonState.DOWN) {
return;
}
ResourceUrn activeInteractionScreenUri = InteractionUtil.getActiveInteractionScreenUri(character);
if (activeInteractionScreenUri != null) {
InteractionUtil.cancelInteractionAsClient(character);
return;
}
boolean activeRequestSent = localPlayer.activateTargetAsClient();
if (activeRequestSent) {
event.consume();
}
}
@ReceiveEvent(components = {CharacterComponent.class})
public void onUseItemButton(UseItemButton event, EntityRef entity, CharacterHeldItemComponent characterHeldItemComponent) {
if (!event.isDown()) {
return;
}
EntityRef selectedItemEntity = characterHeldItemComponent.selectedItem;
if (!selectedItemEntity.exists()) {
return;
}
boolean requestIsValid;
if (networkSystem.getMode().isAuthority()) {
// Let the ActivationRequest handler trigger the OnItemUseEvent if this is a local client
requestIsValid = true;
} else {
OnItemUseEvent onItemUseEvent = new OnItemUseEvent();
entity.send(onItemUseEvent);
requestIsValid = !onItemUseEvent.isConsumed();
}
if (requestIsValid) {
localPlayer.activateOwnedEntityAsClient(selectedItemEntity);
entity.saveComponent(characterHeldItemComponent);
event.consume();
}
}
private float calcBobbingOffset(float phaseOffset, float amplitude, float frequency) {
return (float) java.lang.Math.sin(bobFactor * frequency + phaseOffset) * amplitude;
}
@Override
public void renderOpaque() {
}
@Override
public void renderAlphaBlend() {
}
@Override
public void renderFirstPerson() {
}
@Override
public void renderShadows() {
}
}