/*
* Copyright 2013 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.characters;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.engine.Time;
import org.terasology.entitySystem.entity.EntityManager;
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.RegisterMode;
import org.terasology.entitySystem.systems.RegisterSystem;
import org.terasology.entitySystem.systems.UpdateSubscriberSystem;
import org.terasology.input.binds.interaction.AttackButton;
import org.terasology.logic.characters.events.ActivationRequest;
import org.terasology.logic.characters.events.ActivationRequestDenied;
import org.terasology.logic.characters.events.AttackEvent;
import org.terasology.logic.characters.events.AttackRequest;
import org.terasology.logic.characters.events.DeathEvent;
import org.terasology.logic.characters.events.OnItemUseEvent;
import org.terasology.logic.characters.interactions.InteractionUtil;
import org.terasology.logic.common.ActivateEvent;
import org.terasology.logic.common.DisplayNameComponent;
import org.terasology.logic.health.DestroyEvent;
import org.terasology.logic.health.DoDestroyEvent;
import org.terasology.logic.health.EngineDamageTypes;
import org.terasology.logic.inventory.ItemComponent;
import org.terasology.logic.location.LocationComponent;
import org.terasology.math.geom.Vector3f;
import org.terasology.network.ClientComponent;
import org.terasology.network.NetworkSystem;
import org.terasology.physics.CollisionGroup;
import org.terasology.physics.HitResult;
import org.terasology.physics.Physics;
import org.terasology.physics.StandardCollisionGroup;
import org.terasology.registry.In;
import org.terasology.world.block.BlockComponent;
import org.terasology.world.block.regions.ActAsBlockComponent;
/**
*/
@RegisterSystem
public class CharacterSystem extends BaseComponentSystem implements UpdateSubscriberSystem {
public static final CollisionGroup[] DEFAULTPHYSICSFILTER = {StandardCollisionGroup.DEFAULT, StandardCollisionGroup.WORLD, StandardCollisionGroup.CHARACTER};
private static final Logger logger = LoggerFactory.getLogger(CharacterSystem.class);
@In
private Physics physics;
@In
private NetworkSystem networkSystem;
@In
private EntityManager entityManager;
@In
private Time time;
@ReceiveEvent(components = {CharacterComponent.class})
public void onDeath(DoDestroyEvent event, EntityRef entity) {
CharacterComponent character = entity.getComponent(CharacterComponent.class);
character.controller.send(new DeathEvent());
// TODO: Don't just destroy, ragdoll or create particle effect or something (possible allow another system to handle)
//entity.removeComponent(CharacterComponent.class);
//entity.removeComponent(CharacterMovementComponent.class);
}
@ReceiveEvent(components = {CharacterComponent.class}, netFilter = RegisterMode.CLIENT)
public void onAttackRequest(AttackButton event, EntityRef entity, CharacterHeldItemComponent characterHeldItemComponent) {
if (!event.isDown()) {
return;
}
boolean attackRequestIsValid;
if (networkSystem.getMode().isAuthority()) {
// Let the AttackRequest handler trigger the OnItemUseEvent if this is a local client
attackRequestIsValid = true;
} else {
OnItemUseEvent onItemUseEvent = new OnItemUseEvent();
entity.send(onItemUseEvent);
attackRequestIsValid = !onItemUseEvent.isConsumed();
}
if (attackRequestIsValid) {
EntityRef selectedItemEntity = characterHeldItemComponent.selectedItem;
entity.send(new AttackRequest(selectedItemEntity));
event.consume();
}
}
@ReceiveEvent(components = LocationComponent.class, netFilter = RegisterMode.AUTHORITY)
public void onAttackRequest(AttackRequest event, EntityRef character, CharacterComponent characterComponent) {
// if an item is used, make sure this entity is allowed to attack with it
if (event.getItem().exists()) {
if (!character.equals(event.getItem().getOwner())) {
return;
}
}
OnItemUseEvent onItemUseEvent = new OnItemUseEvent();
character.send(onItemUseEvent);
if (!onItemUseEvent.isConsumed()) {
EntityRef gazeEntity = GazeAuthoritySystem.getGazeEntityForCharacter(character);
LocationComponent gazeLocation = gazeEntity.getComponent(LocationComponent.class);
Vector3f direction = gazeLocation.getWorldDirection();
Vector3f originPos = gazeLocation.getWorldPosition();
HitResult result = physics.rayTrace(originPos, direction, characterComponent.interactionRange, Sets.newHashSet(character), DEFAULTPHYSICSFILTER);
if (result.isHit()) {
result.getEntity().send(new AttackEvent(character, event.getItem()));
}
}
}
@ReceiveEvent(components = {CharacterComponent.class})
public void onItemUse(OnItemUseEvent event, EntityRef entity, CharacterHeldItemComponent characterHeldItemComponent) {
long currentTime = time.getGameTimeInMs();
if (characterHeldItemComponent.nextItemUseTime > currentTime) {
// this character is not yet ready to use another item, they are still cooling down from last use
event.consume();
return;
}
EntityRef selectedItemEntity = characterHeldItemComponent.selectedItem;
characterHeldItemComponent.lastItemUsedTime = currentTime;
characterHeldItemComponent.nextItemUseTime = currentTime;
ItemComponent itemComponent = selectedItemEntity.getComponent(ItemComponent.class);
// Add the cooldown time for the next use of this item.
if (itemComponent != null) {
// Send out this event so other systems can alter the cooldown time.
AffectItemUseCooldownTimeEvent affectItemUseCooldownTimeEvent
= new AffectItemUseCooldownTimeEvent(itemComponent.cooldownTime);
entity.send(affectItemUseCooldownTimeEvent);
characterHeldItemComponent.nextItemUseTime += affectItemUseCooldownTimeEvent.getResultValue();
} else {
characterHeldItemComponent.nextItemUseTime += 200;
}
entity.saveComponent(characterHeldItemComponent);
}
@ReceiveEvent(priority = EventPriority.PRIORITY_TRIVIAL, netFilter = RegisterMode.AUTHORITY)
public void onAttackBlock(AttackEvent event, EntityRef entityRef, BlockComponent blockComponent) {
entityRef.send(new DestroyEvent(event.getInstigator(), event.getDirectCause(), EngineDamageTypes.PHYSICAL.get()));
}
@ReceiveEvent(priority = EventPriority.PRIORITY_TRIVIAL, netFilter = RegisterMode.AUTHORITY)
public void onAttackBlock(AttackEvent event, EntityRef entityRef, ActAsBlockComponent actAsBlockComponent) {
entityRef.send(new DestroyEvent(event.getInstigator(), event.getDirectCause(), EngineDamageTypes.PHYSICAL.get()));
}
@ReceiveEvent(components = {CharacterComponent.class, LocationComponent.class}, netFilter = RegisterMode.AUTHORITY)
public void onActivationRequest(ActivationRequest event, EntityRef character) {
if (isPredictionOfEventCorrect(character, event)) {
OnItemUseEvent onItemUseEvent = new OnItemUseEvent();
event.getInstigator().send(onItemUseEvent);
if (!onItemUseEvent.isConsumed()) {
if (event.getUsedOwnedEntity().exists()) {
event.getUsedOwnedEntity().send(new ActivateEvent(event));
} else {
event.getTarget().send(new ActivateEvent(event));
}
}
} else {
character.send(new ActivationRequestDenied(event.getActivationId()));
}
}
private boolean vectorsAreAboutEqual(Vector3f v1, Vector3f v2) {
Vector3f delta = new Vector3f();
delta.add(v1);
delta.sub(v2);
float epsilon = 0.0001f;
float deltaSquared = delta.lengthSquared();
return deltaSquared < epsilon;
}
private String getPlayerNameFromCharacter(EntityRef character) {
CharacterComponent characterComponent = character.getComponent(CharacterComponent.class);
if (characterComponent == null) {
return "?";
}
EntityRef controller = characterComponent.controller;
ClientComponent clientComponent = controller.getComponent(ClientComponent.class);
EntityRef clientInfo = clientComponent.clientInfo;
DisplayNameComponent displayNameComponent = clientInfo.getComponent(DisplayNameComponent.class);
if (displayNameComponent == null) {
return "?";
}
return displayNameComponent.name;
}
private boolean isPredictionOfEventCorrect(EntityRef character, ActivationRequest event) {
CharacterComponent characterComponent = character.getComponent(CharacterComponent.class);
EntityRef camera = GazeAuthoritySystem.getGazeEntityForCharacter(character);
LocationComponent location = camera.getComponent(LocationComponent.class);
Vector3f direction = location.getWorldDirection();
if (!(vectorsAreAboutEqual(event.getDirection(), direction))) {
logger.error("Direction at client {} was different than direction at server {}", event.getDirection(), direction);
}
// Assume the exact same value in case there are rounding mistakes:
direction = event.getDirection();
Vector3f originPos = location.getWorldPosition();
if (!(vectorsAreAboutEqual(event.getOrigin(), originPos))) {
String msg = "Player {} seems to have cheated: It stated that it performed an action from {} but the predicted position is {}";
logger.info(msg, getPlayerNameFromCharacter(character), event.getOrigin(), originPos);
return false;
}
if (event.isOwnedEntityUsage()) {
if (!event.getUsedOwnedEntity().exists()) {
String msg = "Denied activation attempt by {} since the used entity does not exist on the authority";
logger.info(msg, getPlayerNameFromCharacter(character));
return false;
}
if (!networkSystem.getOwnerEntity(event.getUsedOwnedEntity()).equals(networkSystem.getOwnerEntity(character))) {
String msg = "Denied activation attempt by {} since it does not own the entity at the authority";
logger.info(msg, getPlayerNameFromCharacter(character));
return false;
}
} else {
// check for cheats so that data can later be trusted:
if (event.getUsedOwnedEntity().exists()) {
String msg = "Denied activation attempt by {} since it is not properly marked as owned entity usage";
logger.info(msg, getPlayerNameFromCharacter(character));
return false;
}
}
if (event.isEventWithTarget()) {
if (!event.getTarget().exists()) {
String msg = "Denied activation attempt by {} since the target does not exist on the authority";
logger.info(msg, getPlayerNameFromCharacter(character));
return false; // can happen if target existed on client
}
HitResult result = physics.rayTrace(originPos, direction, characterComponent.interactionRange, Sets.newHashSet(character), DEFAULTPHYSICSFILTER);
if (!result.isHit()) {
String msg = "Denied activation attempt by {} since at the authority there was nothing to activate at that place";
logger.info(msg, getPlayerNameFromCharacter(character));
return false;
}
EntityRef hitEntity = result.getEntity();
if (!hitEntity.equals(event.getTarget())) {
/**
* Tip for debugging this issue: Obtain the network id of hit entity and search it in both client and
* server entity dump. When certain fields don't get replicated, then wrong entity might get hin in the
* hit test.
*/
String msg = "Denied activation attempt by {} since at the authority another entity would have been activated";
logger.info(msg, getPlayerNameFromCharacter(character));
return false;
}
if (!(vectorsAreAboutEqual(event.getHitPosition(), result.getHitPoint()))) {
String msg = "Denied activation attempt by {} since at the authority the object got hit at a differnt position";
logger.info(msg, getPlayerNameFromCharacter(character));
return false;
}
} else {
// In order to trust the data later we need to verify it even if it should be correct if no one cheats:
if (event.getTarget().exists()) {
String msg = "Denied activation attempt by {} since the event was not properly labeled as having a target";
logger.info(msg, getPlayerNameFromCharacter(character));
return false;
}
if (!(vectorsAreAboutEqual(event.getHitPosition(), originPos))) {
String msg = "Denied activation attempt by {} since the event was not properly labeled as having a hit postion";
logger.info(msg, getPlayerNameFromCharacter(character));
return false;
}
}
return true;
}
@Override
public void update(float delta) {
Iterable<EntityRef> characterEntities = entityManager.getEntitiesWith(CharacterComponent.class, LocationComponent.class);
for (EntityRef characterEntity : characterEntities) {
CharacterComponent characterComponent = characterEntity.getComponent(CharacterComponent.class);
if (characterComponent == null) {
continue; // could have changed during events below
}
LocationComponent characterLocation = characterEntity.getComponent(LocationComponent.class);
if (characterLocation == null) {
continue; // could have changed during events below
}
EntityRef target = characterComponent.authorizedInteractionTarget;
if (target.isActive()) {
LocationComponent targetLocation = target.getComponent(LocationComponent.class);
if (targetLocation == null) {
continue; // could have changed during events below
}
float maxInteractionRange = characterComponent.interactionRange;
if (isDistanceToLarge(characterLocation, targetLocation, maxInteractionRange)) {
InteractionUtil.cancelInteractionAsServer(characterEntity);
}
}
}
}
private boolean isDistanceToLarge(LocationComponent characterLocation, LocationComponent targetLocation, float maxInteractionRange) {
float maxInteractionRangeSquared = maxInteractionRange * maxInteractionRange;
Vector3f positionDelta = new Vector3f();
positionDelta.add(characterLocation.getWorldPosition());
positionDelta.sub(targetLocation.getWorldPosition());
float interactionRangeSquared = positionDelta.lengthSquared();
// add a small epsilon to have rounding mistakes be in favor of the player:
float epsilon = 0.00001f;
return interactionRangeSquared > maxInteractionRangeSquared + epsilon;
}
}