org.terasology.recording.EventSystemReplayImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.terasology.recording.EventSystemReplayImpl.java

Source

/*
 * Copyright 2018 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.recording;

import org.terasology.engine.paths.PathManager;
import org.terasology.entitySystem.entity.internal.EngineEntityManager;
import org.terasology.entitySystem.event.internal.EventReceiver;
import org.terasology.entitySystem.event.internal.EventSystem;
import com.esotericsoftware.reflectasm.MethodAccess;
import com.google.common.base.Objects;
import com.google.common.base.Predicates;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import org.reflections.ReflectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.engine.SimpleUri;
import org.terasology.entitySystem.Component;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.event.AbstractConsumableEvent;
import org.terasology.entitySystem.event.ConsumableEvent;
import org.terasology.entitySystem.event.Event;
import org.terasology.entitySystem.event.EventPriority;
import org.terasology.entitySystem.event.ReceiveEvent;
import org.terasology.entitySystem.event.PendingEvent;
import org.terasology.entitySystem.metadata.EventLibrary;
import org.terasology.entitySystem.metadata.EventMetadata;
import org.terasology.entitySystem.systems.ComponentSystem;
import org.terasology.monitoring.PerformanceMonitor;
import org.terasology.network.BroadcastEvent;
import org.terasology.network.Client;
import org.terasology.network.NetworkComponent;
import org.terasology.network.NetworkEvent;
import org.terasology.network.NetworkMode;
import org.terasology.network.NetworkSystem;
import org.terasology.network.OwnerEvent;
import org.terasology.network.ServerEvent;
import org.terasology.world.block.BlockComponent;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;

/**
 * Event System used during a replay. It works almost the same as EventSystemImpl, with most methods being exactly the
 * same, with the exception of 'send' and 'process'. On the 'process' method, the recorded events are loaded from a file
 * to the RecordedEventStore and then they are processed for a certain amount of time. The 'send' filters which events can
 * be sent by the engine during a replay. This is important to ensure that the recorded events are replayed correctly
 * and that the player does not interfere with the replay.
 */
public class EventSystemReplayImpl implements EventSystem {

    private static final Logger logger = LoggerFactory.getLogger(EventSystemReplayImpl.class);

    private Map<Class<? extends Event>, SetMultimap<Class<? extends Component>, EventHandlerInfo>> componentSpecificHandlers = Maps
            .newHashMap();
    private SetMultimap<Class<? extends Event>, EventSystemReplayImpl.EventHandlerInfo> generalHandlers = HashMultimap
            .create();
    private Comparator<EventHandlerInfo> priorityComparator = new EventSystemReplayImpl.EventHandlerPriorityComparator();

    // Event metadata
    private BiMap<SimpleUri, Class<? extends Event>> eventIdMap = HashBiMap.create();
    private SetMultimap<Class<? extends Event>, Class<? extends Event>> childEvents = HashMultimap.create();

    private Thread mainThread;
    private BlockingQueue<PendingEvent> pendingEvents = Queues.newLinkedBlockingQueue();
    private BlockingQueue<RecordedEvent> recordedEvents = Queues.newLinkedBlockingQueue();

    private EventLibrary eventLibrary;
    private NetworkSystem networkSystem;

    //Event replaying
    /** if the recorded events were loaded from the RecordedEventStore. */
    private boolean areRecordedEventsLoaded;
    /** When the events were loaded. Used to reproduce the events at the correct time. */
    private long replayEventsLoadTime;
    /** Necessary to do some entity id mapping from original client and replay client. */
    private EngineEntityManager entityManager;
    /** Where the RecordedEvents are deserialized */
    private RecordedEventStore recordedEventStore;
    /** Class responsible for deserializing recorded data */
    private RecordAndReplaySerializer recordAndReplaySerializer;
    /** Responsible for knowing the game name of the recording */
    private RecordAndReplayUtils recordAndReplayUtils;
    /** List of classes selected to replay */
    private List<Class<?>> selectedClassesToReplay;
    /** The current Status of Record And Replay */
    private RecordAndReplayCurrentStatus recordAndReplayCurrentStatus;
    /** The position of the last recorded event processed */
    private long lastRecordedEventIndex;

    public EventSystemReplayImpl(EventLibrary eventLibrary, NetworkSystem networkSystem,
            EngineEntityManager entityManager, RecordedEventStore recordedEventStore,
            RecordAndReplaySerializer recordAndReplaySerializer, RecordAndReplayUtils recordAndReplayUtils,
            List<Class<?>> selectedClassesToReplay, RecordAndReplayCurrentStatus recordAndReplayCurrentStatus) {
        this.mainThread = Thread.currentThread();
        this.eventLibrary = eventLibrary;
        this.networkSystem = networkSystem;
        this.entityManager = entityManager;
        this.recordedEventStore = recordedEventStore;
        this.recordAndReplaySerializer = recordAndReplaySerializer;
        this.recordAndReplayUtils = recordAndReplayUtils;
        this.selectedClassesToReplay = selectedClassesToReplay;
        this.recordAndReplayCurrentStatus = recordAndReplayCurrentStatus;
    }

    /**
     * Fills recordedEvents with the events in RecordedEventStore.
     */
    private void fillRecordedEvents() {
        Collection<RecordedEvent> events = recordedEventStore.getEvents();
        for (RecordedEvent event : events) {
            this.recordedEvents.offer(event);
        }
    }

    // send method of EventSystemImpl
    private void originalSend(EntityRef entity, Event event) {
        if (Thread.currentThread() != mainThread) {
            pendingEvents.offer(new PendingEvent(entity, event));
        } else {
            networkReplicate(entity, event);

            Set<EventHandlerInfo> selectedHandlersSet = selectEventHandlers(event.getClass(), entity);
            List<EventHandlerInfo> selectedHandlers = Lists.newArrayList(selectedHandlersSet);
            selectedHandlers.sort(priorityComparator);

            if (event instanceof ConsumableEvent) {
                sendConsumableEvent(entity, event, selectedHandlers);
            } else {
                sendStandardEvent(entity, event, selectedHandlers);
            }
        }
    }

    // send method of EventSystemImpl
    private void originalSend(EntityRef entity, Event event, Component component) {

        if (Thread.currentThread() != mainThread) {
            pendingEvents.offer(new PendingEvent(entity, event, component));
        } else {
            SetMultimap<Class<? extends Component>, EventSystemReplayImpl.EventHandlerInfo> handlers = componentSpecificHandlers
                    .get(event.getClass());
            if (handlers != null) {
                List<EventSystemReplayImpl.EventHandlerInfo> eventHandlers = Lists
                        .newArrayList(handlers.get(component.getClass()));
                eventHandlers.sort(priorityComparator);
                for (EventSystemReplayImpl.EventHandlerInfo eventHandler : eventHandlers) {
                    if (eventHandler.isValidFor(entity)) {
                        eventHandler.invoke(entity, event);
                    }
                }
            }
        }
    }

    /**
     * Processes recorded and pending events. If recordedEvents is not loaded, load it from RecordedEventStore.
     */
    @Override
    public void process() {
        processRecordedEvents();
        PendingEvent event = pendingEvents.poll();
        while (event != null) {
            if (event.getComponent() != null) {
                originalSend(event.getEntity(), event.getEvent(), event.getComponent());
            } else {
                originalSend(event.getEntity(), event.getEvent());
            }
            event = pendingEvents.poll();
        }
    }

    /**
     * Processes recorded events for a certain amount of time and only if the timestamp is right.
     */
    private void processRecordedEvents() {
        if (recordAndReplayCurrentStatus.getStatus() == RecordAndReplayStatus.REPLAYING
                && !this.areRecordedEventsLoaded) {
            initialiseReplayData();
        }
        //If replay is ready, process some recorded events if the time is right.
        if (recordAndReplayCurrentStatus.getStatus() == RecordAndReplayStatus.REPLAYING) {
            processRecordedEventsBatch(1);
            if (this.recordedEvents.isEmpty()) {
                if (recordAndReplayUtils.getFileCount() <= recordAndReplayUtils.getFileAmount()) { //Get next recorded events file
                    loadNextRecordedEventFile();
                } else {
                    finishReplay();
                }
            }
        }
    }

    /**
     * Empty the RecordedEventStore and sets the RecordAndReplayStatus.
     */
    private void finishReplay() {
        recordedEventStore.popEvents();
        recordAndReplayCurrentStatus.setStatus(RecordAndReplayStatus.REPLAY_FINISHED); // stops the replay if every recorded event was already replayed
    }

    private void loadNextRecordedEventFile() {
        String recordingPath = PathManager.getInstance().getRecordingPath(recordAndReplayUtils.getGameTitle())
                .toString();
        recordAndReplaySerializer.deserializeRecordedEvents(recordingPath);
        fillRecordedEvents();
    }

    private void initialiseReplayData() {
        fillRecordedEvents();
        this.areRecordedEventsLoaded = true;
        logger.info("Loaded Recorded Events!");
        replayEventsLoadTime = System.currentTimeMillis();
    }

    /**
     * Try to process recorded events for 'maxDuration' miliseconds. Events are only processed if the time is right.
     * @param maxDuration the amount of time in which this method will try to process recorded events in one go.
     */
    private void processRecordedEventsBatch(long maxDuration) {
        long beginTime = System.currentTimeMillis();
        for (RecordedEvent re = recordedEvents.peek(); re != null; re = recordedEvents.peek()) {
            long passedTime = System.currentTimeMillis() - this.replayEventsLoadTime;
            //Waits until the time of reproduction is right or until 'maxDuration' miliseconds have already passed since this method was called
            while (passedTime < re.getTimestamp()) {
                passedTime = System.currentTimeMillis() - this.replayEventsLoadTime;
                if ((System.currentTimeMillis() - beginTime) >= maxDuration) {
                    return;
                }
            }
            recordedEvents.poll();
            EntityRef entity = getEntityRef(re);
            // Sends recorded event to be processed
            if (re.getComponent() != null) {
                originalSend(entity, re.getEvent(), re.getComponent());
            } else {
                originalSend(entity, re.getEvent());
            }
            this.lastRecordedEventIndex = re.getIndex();
            // Check if time is up.
            if ((System.currentTimeMillis() - beginTime) >= maxDuration) {
                return;
            }
        }
    }

    /**
     * Since only the EntityRef's id is saved in the RecordedEvent, it is necessary to get the real EntityRef when
     * processing a RecordedEvent.
     * @param recordedEvent The recorded event containing the ID of the entity to be gotten.
     * @return the EntityRef with the id recorded in the RecordedEvent.
     */
    private EntityRef getEntityRef(RecordedEvent recordedEvent) {
        return this.entityManager.getEntity(recordedEvent.getEntityId());
    }

    @Override
    public void registerEvent(SimpleUri uri, Class<? extends Event> eventType) {
        eventIdMap.put(uri, eventType);
        logger.debug("Registering event {}", eventType.getSimpleName());
        for (Class parent : ReflectionUtils.getAllSuperTypes(eventType, Predicates.assignableFrom(Event.class))) {
            if (!AbstractConsumableEvent.class.equals(parent) && !Event.class.equals(parent)) {
                childEvents.put(parent, eventType);
            }
        }
        if (shouldAddToLibrary(eventType)) {
            eventLibrary.register(uri, eventType);
        }
    }

    /**
     * Events are added to the event library if they have a network annotation
     *
     * @param eventType the type of the event to be checked.
     * @return Whether the event should be added to the event library
     */
    private boolean shouldAddToLibrary(Class<? extends Event> eventType) {
        return eventType.getAnnotation(ServerEvent.class) != null
                || eventType.getAnnotation(OwnerEvent.class) != null
                || eventType.getAnnotation(BroadcastEvent.class) != null;
    }

    @Override
    public void registerEventHandler(ComponentSystem handler) {
        Class handlerClass = handler.getClass();
        if (!Modifier.isPublic(handlerClass.getModifiers())) {
            logger.error("Cannot register handler {}, must be public", handler.getClass().getName());
            return;
        }

        logger.debug("Registering event handler " + handlerClass.getName());
        for (Method method : handlerClass.getMethods()) {
            ReceiveEvent receiveEventAnnotation = method.getAnnotation(ReceiveEvent.class);
            if (receiveEventAnnotation != null) {
                if (!receiveEventAnnotation.netFilter().isValidFor(networkSystem.getMode(), false)) {
                    continue;
                }
                Set<Class<? extends Component>> requiredComponents = Sets.newLinkedHashSet();
                method.setAccessible(true);
                Class<?>[] types = method.getParameterTypes();

                logger.debug("Found method: " + method.toString());
                if (!Event.class.isAssignableFrom(types[0]) || !EntityRef.class.isAssignableFrom(types[1])) {
                    logger.error("Invalid event handler method: {}", method.getName());
                    return;
                }

                requiredComponents.addAll(Arrays.asList(receiveEventAnnotation.components()));
                List<Class<? extends Component>> componentParams = Lists.newArrayList();
                for (int i = 2; i < types.length; ++i) {
                    if (!Component.class.isAssignableFrom(types[i])) {
                        logger.error("Invalid event handler method: {} - {} is not a component class",
                                method.getName(), types[i]);
                        return;
                    }
                    requiredComponents.add((Class<? extends Component>) types[i]);
                    componentParams.add((Class<? extends Component>) types[i]);
                }

                EventSystemReplayImpl.ByteCodeEventHandlerInfo handlerInfo = new EventSystemReplayImpl.ByteCodeEventHandlerInfo(
                        handler, method, receiveEventAnnotation.priority(), receiveEventAnnotation.activity(),
                        requiredComponents, componentParams);
                addEventHandler((Class<? extends Event>) types[0], handlerInfo, requiredComponents);
            }
        }
    }

    @Override
    public void unregisterEventHandler(ComponentSystem handler) {
        for (SetMultimap<Class<? extends Component>, EventSystemReplayImpl.EventHandlerInfo> eventHandlers : componentSpecificHandlers
                .values()) {
            Iterator<EventHandlerInfo> eventHandlerIterator = eventHandlers.values().iterator();
            while (eventHandlerIterator.hasNext()) {
                EventSystemReplayImpl.EventHandlerInfo eventHandler = eventHandlerIterator.next();
                if (eventHandler.getHandler().equals(handler)) {
                    eventHandlerIterator.remove();
                }
            }
        }

        Iterator<EventSystemReplayImpl.EventHandlerInfo> eventHandlerIterator = generalHandlers.values().iterator();
        while (eventHandlerIterator.hasNext()) {
            EventSystemReplayImpl.EventHandlerInfo eventHandler = eventHandlerIterator.next();
            if (eventHandler.getHandler().equals(handler)) {
                eventHandlerIterator.remove();
            }
        }
    }

    private void addEventHandler(Class<? extends Event> type, EventSystemReplayImpl.EventHandlerInfo handler,
            Collection<Class<? extends Component>> components) {
        if (components.isEmpty()) {
            generalHandlers.put(type, handler);
            for (Class<? extends Event> childType : childEvents.get(type)) {
                generalHandlers.put(childType, handler);
            }
        } else {
            for (Class<? extends Component> c : components) {
                addToComponentSpecificHandlers(type, handler, c);
                for (Class<? extends Event> childType : childEvents.get(type)) {
                    addToComponentSpecificHandlers(childType, handler, c);
                }
            }
        }
    }

    private void addToComponentSpecificHandlers(Class<? extends Event> type,
            EventSystemReplayImpl.EventHandlerInfo handlerInfo, Class<? extends Component> c) {
        SetMultimap<Class<? extends Component>, EventSystemReplayImpl.EventHandlerInfo> componentMap = componentSpecificHandlers
                .get(type);
        if (componentMap == null) {
            componentMap = HashMultimap.create();
            componentSpecificHandlers.put(type, componentMap);
        }
        componentMap.put(c, handlerInfo);
    }

    @Override
    public <T extends Event> void registerEventReceiver(EventReceiver<T> eventReceiver, Class<T> eventClass,
            Class<? extends Component>... componentTypes) {
        registerEventReceiver(eventReceiver, eventClass, EventPriority.PRIORITY_NORMAL, componentTypes);
    }

    @Override
    public <T extends Event> void registerEventReceiver(EventReceiver<T> eventReceiver, Class<T> eventClass,
            int priority, Class<? extends Component>... componentTypes) {
        EventSystemReplayImpl.EventHandlerInfo info = new EventSystemReplayImpl.ReceiverEventHandlerInfo<>(
                eventReceiver, priority, componentTypes);
        addEventHandler(eventClass, info, Arrays.asList(componentTypes));
    }

    @Override
    public <T extends Event> void unregisterEventReceiver(EventReceiver<T> eventReceiver, Class<T> eventClass,
            Class<? extends Component>... componentTypes) {
        SetMultimap<Class<? extends Component>, EventSystemReplayImpl.EventHandlerInfo> eventHandlerMap = componentSpecificHandlers
                .get(eventClass);
        if (eventHandlerMap != null) {
            EventSystemReplayImpl.ReceiverEventHandlerInfo testReceiver = new EventSystemReplayImpl.ReceiverEventHandlerInfo<>(
                    eventReceiver, 0, componentTypes);
            for (Class<? extends Component> c : componentTypes) {
                eventHandlerMap.remove(c, testReceiver);
                for (Class<? extends Event> childType : childEvents.get(eventClass)) {
                    eventHandlerMap.remove(childType, testReceiver);
                }
            }
        }

        if (0 == componentTypes.length) {
            Iterator<EventSystemReplayImpl.EventHandlerInfo> eventHandlerIterator = generalHandlers.values()
                    .iterator();
            while (eventHandlerIterator.hasNext()) {
                EventSystemReplayImpl.EventHandlerInfo eventHandler = eventHandlerIterator.next();
                if (eventHandler.getHandler().equals(eventReceiver)) {
                    eventHandlerIterator.remove();
                }
            }
        }
    }

    /**
     * Calls the 'process' method if the replay is activated and the event is of a type selected to be replayed.
     * This way, events of the types that are recorded and replayed are ignored during a replay. This is what makes
     * the player have no control over the character during a replay.
     * @param entity the entity which the event was sent against.
     * @param event the event being sent.
     */
    @Override
    public void send(EntityRef entity, Event event) {
        if (recordAndReplayCurrentStatus.getStatus() != RecordAndReplayStatus.REPLAYING
                || !isSelectedToReplayEvent(event)) {
            originalSend(entity, event);
        }
    }

    /**
     * Calls the 'process' method if the replay is activated and the event is of a type selected to be replayed.
     * This way, events of the types that are recorded and replayed are ignored during a replay. This is what makes
     * the player have no control over the character during a replay.
     * @param entity the entity which the event was sent against.
     * @param event the event being sent.
     * @param component the component sent along with the event.
     */
    @Override
    public void send(EntityRef entity, Event event, Component component) {
        if (recordAndReplayCurrentStatus.getStatus() != RecordAndReplayStatus.REPLAYING
                || !isSelectedToReplayEvent(event)) {
            originalSend(entity, event, component);
        }
    }

    /**
     * Check if the event is selected to be replayed. If they are, they are only processed through replay and sending them
     * normally won't have any effect. Example: Since MouseWheelEvent is selected to replay, during the replay process
     * if the player moves the mouse wheel, the MouseWheelEvents generated won't be processed, but the ones on the recorded
     * events list will.
     * @param event event to be checked
     * @return if the event is selected to replay
     */
    private boolean isSelectedToReplayEvent(Event event) {
        boolean selectedToReplay = false;
        for (Class<?> supportedEventClass : this.selectedClassesToReplay) {
            if (supportedEventClass.isInstance(event)) {
                selectedToReplay = true;
                break;
            }
        }
        return selectedToReplay;

    }

    private void sendStandardEvent(EntityRef entity, Event event,
            List<EventSystemReplayImpl.EventHandlerInfo> selectedHandlers) {
        for (EventSystemReplayImpl.EventHandlerInfo handler : selectedHandlers) {
            // Check isValid at each stage in case components were removed.
            if (handler.isValidFor(entity)) {
                handler.invoke(entity, event);
            }
        }
    }

    private void sendConsumableEvent(EntityRef entity, Event event,
            List<EventSystemReplayImpl.EventHandlerInfo> selectedHandlers) {
        ConsumableEvent consumableEvent = (ConsumableEvent) event;
        for (EventSystemReplayImpl.EventHandlerInfo handler : selectedHandlers) {
            // Check isValid at each stage in case components were removed.
            if (handler.isValidFor(entity)) {
                handler.invoke(entity, event);
                if (consumableEvent.isConsumed()) {
                    return;
                }
            }
        }
    }

    private void networkReplicate(EntityRef entity, Event event) {
        EventMetadata metadata = eventLibrary.getMetadata(event);
        if (metadata != null && metadata.isNetworkEvent()) {
            logger.debug("Replicating event: {}", event);
            switch (metadata.getNetworkEventType()) {
            case BROADCAST:
                broadcastEvent(entity, event, metadata);
                break;
            case OWNER:
                sendEventToOwner(entity, event);
                break;
            case SERVER:
                sendEventToServer(entity, event);
                break;
            default:
                break;
            }
        }
    }

    private void sendEventToServer(EntityRef entity, Event event) {
        if (networkSystem.getMode() == NetworkMode.CLIENT) {
            NetworkComponent netComp = entity.getComponent(NetworkComponent.class);
            if (netComp != null) {
                networkSystem.getServer().send(event, entity);
            }
        }
    }

    private void sendEventToOwner(EntityRef entity, Event event) {
        if (networkSystem.getMode().isServer()) {
            NetworkComponent netComp = entity.getComponent(NetworkComponent.class);
            if (netComp != null) {
                Client client = networkSystem.getOwner(entity);
                if (client != null) {
                    client.send(event, entity);
                }
            }
        }
    }

    private void broadcastEvent(EntityRef entity, Event event, EventMetadata metadata) {
        if (networkSystem.getMode().isServer()) {
            NetworkComponent netComp = entity.getComponent(NetworkComponent.class);
            BlockComponent blockComp = entity.getComponent(BlockComponent.class);
            if (netComp != null || blockComp != null) {
                Client instigatorClient = null;
                if (metadata.isSkipInstigator() && event instanceof NetworkEvent) {
                    instigatorClient = networkSystem.getOwner(((NetworkEvent) event).getInstigator());
                }
                for (Client client : networkSystem.getPlayers()) {
                    if (!client.equals(instigatorClient)) {
                        client.send(event, entity);
                    }
                }
            }
        }
    }

    private Set<EventSystemReplayImpl.EventHandlerInfo> selectEventHandlers(Class<? extends Event> eventType,
            EntityRef entity) {
        Set<EventSystemReplayImpl.EventHandlerInfo> result = Sets.newHashSet();
        result.addAll(generalHandlers.get(eventType));
        SetMultimap<Class<? extends Component>, EventSystemReplayImpl.EventHandlerInfo> handlers = componentSpecificHandlers
                .get(eventType);
        if (handlers == null) {
            return result;
        }

        for (Class<? extends Component> compClass : handlers.keySet()) {
            if (entity.hasComponent(compClass)) {
                for (EventSystemReplayImpl.EventHandlerInfo eventHandler : handlers.get(compClass)) {
                    if (eventHandler.isValidFor(entity)) {
                        result.add(eventHandler);
                    }
                }
            }
        }
        return result;
    }

    public long getLastRecordedEventIndex() {
        return lastRecordedEventIndex;
    }

    private static class EventHandlerPriorityComparator
            implements Comparator<EventSystemReplayImpl.EventHandlerInfo> {

        @Override
        public int compare(EventSystemReplayImpl.EventHandlerInfo o1, EventSystemReplayImpl.EventHandlerInfo o2) {
            return o2.getPriority() - o1.getPriority();
        }
    }

    private interface EventHandlerInfo {
        boolean isValidFor(EntityRef entity);

        void invoke(EntityRef entity, Event event);

        int getPriority();

        Object getHandler();
    }

    private static class ByteCodeEventHandlerInfo implements EventSystemReplayImpl.EventHandlerInfo {
        private ComponentSystem handler;
        private String activity;
        private MethodAccess methodAccess;
        private int methodIndex;
        private ImmutableList<Class<? extends Component>> filterComponents;
        private ImmutableList<Class<? extends Component>> componentParams;
        private int priority;

        ByteCodeEventHandlerInfo(ComponentSystem handler, Method method, int priority, String activity,
                Collection<Class<? extends Component>> filterComponents,
                Collection<Class<? extends Component>> componentParams) {

            this.handler = handler;
            this.activity = activity;
            this.methodAccess = MethodAccess.get(handler.getClass());
            methodIndex = methodAccess.getIndex(method.getName(), method.getParameterTypes());
            this.filterComponents = ImmutableList.copyOf(filterComponents);
            this.componentParams = ImmutableList.copyOf(componentParams);
            this.priority = priority;
        }

        @Override
        public boolean isValidFor(EntityRef entity) {
            for (Class<? extends Component> component : filterComponents) {
                if (!entity.hasComponent(component)) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public void invoke(EntityRef entity, Event event) {
            try {
                Object[] params = new Object[2 + componentParams.size()];
                params[0] = event;
                params[1] = entity;
                for (int i = 0; i < componentParams.size(); ++i) {
                    params[i + 2] = entity.getComponent(componentParams.get(i));
                }
                if (!activity.isEmpty()) {
                    PerformanceMonitor.startActivity(activity);
                }
                try {
                    methodAccess.invoke(handler, methodIndex, params);
                } finally {
                    if (!activity.isEmpty()) {
                        PerformanceMonitor.endActivity();
                    }
                }
            } catch (Exception ex) {
                logger.error("Failed to invoke event", ex);
            }
        }

        @Override
        public int getPriority() {
            return priority;
        }

        @Override
        public ComponentSystem getHandler() {
            return handler;
        }
    }

    private static class ReceiverEventHandlerInfo<T extends Event>
            implements EventSystemReplayImpl.EventHandlerInfo {
        private EventReceiver<T> receiver;
        private Class<? extends Component>[] components;
        private int priority;

        ReceiverEventHandlerInfo(EventReceiver<T> receiver, int priority,
                Class<? extends Component>... components) {
            this.receiver = receiver;
            this.priority = priority;
            this.components = Arrays.copyOf(components, components.length);
        }

        @Override
        public boolean isValidFor(EntityRef entity) {
            for (Class<? extends Component> component : components) {
                if (!entity.hasComponent(component)) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public void invoke(EntityRef entity, Event event) {
            receiver.onEvent((T) event, entity);
        }

        @Override
        public int getPriority() {
            return priority;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }
            if (obj instanceof EventSystemReplayImpl.ReceiverEventHandlerInfo) {
                EventSystemReplayImpl.ReceiverEventHandlerInfo other = (EventSystemReplayImpl.ReceiverEventHandlerInfo) obj;
                return Objects.equal(receiver, other.receiver);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(receiver);
        }

        @Override
        public Object getHandler() {
            return receiver;
        }
    }

}