package com.teefun.bean.matchmaking.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.teefun.bean.matchmaking.Matchmaking;
import com.teefun.db.service.QueueService;
import com.teefun.events.event.GameAbortedEvent;
import com.teefun.events.event.GameReadyEvent;
import com.teefun.events.event.GameStartedEvent;
import com.teefun.events.event.PlayerModifiedEvent;
import com.teefun.events.event.PlayerReadyEvent;
import com.teefun.events.event.QueueCreatedEvent;
import com.teefun.events.event.QueueDeletedEvent;
import com.teefun.events.event.QueueModifiedEvent;
import com.teefun.events.event.QueueReadyTimedOutEvent;
import com.teefun.events.event.ServerFreeEvent;
import com.teefun.model.Player;
import com.teefun.model.Queue;
import com.teefun.model.QueueState;
import com.teefun.model.teeworlds.TeeworldsServer;
import com.teefun.service.teeworlds.TeeworldsServerHandler;
/**
* Default impl for {@link Matchmaking}.
*
* @author Rajh
*/
@Component
public class MatchmakingImpl implements Matchmaking {
/**
* Class logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(MatchmakingImpl.class);
/**
* Time in seconds before the ready request timeout.
*/
@Value("${queue.ready.ttl}")
private Long READY_TIMEOUT;
/**
* Teeworlds server handler.
*/
@Resource
private TeeworldsServerHandler teeworldsServerHandler;
/**
* Event bus.
*/
@Resource
private EventBus eventBus;
/**
* Queue service.
*/
@Resource
private QueueService queueService;
/**
* List of available queues.
*/
private final List<Queue> availableQueues = new CopyOnWriteArrayList<Queue>();
/**
* Load queues at startup.
*/
@PostConstruct
public void loadQueues() {
this.availableQueues.addAll(this.queueService.getQueues());
LOGGER.debug("Loaded " + this.availableQueues.size() + " queues from DB.");
}
@Override
public List<Queue> getQueues() {
return this.availableQueues;
}
@Override
public List<Queue> getQueues(final Player player) {
final List<Queue> queues = new ArrayList<Queue>();
for (final Queue queue : this.availableQueues) {
if (queue.containsPlayer(player)) {
queues.add(queue);
}
}
return queues;
}
@Override
public void joinQueue(final Player player, final Queue queue) {
LOGGER.debug(String.format("Add player '%s' to queue '%s'.", player.getName(), queue.getName()));
queue.addPlayer(player);
this.eventBus.post(new QueueModifiedEvent(queue));
}
@Override
public void quitQueue(final Player player, final Queue queue) {
if (QueueState.WAITING_PLAYERS == queue.getState()) {
LOGGER.debug(String.format("Remove player '%s' from queue '%s'.", player.getName(), queue.getName()));
queue.removePlayer(player);
}
this.eventBus.post(new QueueModifiedEvent(queue));
}
@Override
public void quitAllQueues(final Player player) {
LOGGER.debug(String.format("Remove player '%s' from all queues.", player.getName()));
for (final Queue queue : this.availableQueues) {
queue.removePlayer(player);
this.eventBus.post(new QueueModifiedEvent(queue));
}
}
@Override
public Queue getQueueByName(final String name) {
for (final Queue queue : this.availableQueues) {
if (name.equals(queue.getName())) {
return queue;
}
}
return null;
}
@Override
public Queue getQueueById(final Integer id) {
for (final Queue queue : this.availableQueues) {
if (id == queue.getId()) {
return queue;
}
}
return null;
}
@Override
public void addQueue(final Queue queue) {
if (!this.availableQueues.contains(queue)) {
this.availableQueues.add(queue);
this.eventBus.post(new QueueCreatedEvent(queue));
}
}
@Override
public void removeQueue(final Queue queue) {
if (this.availableQueues.contains(queue)) {
this.availableQueues.remove(queue);
this.eventBus.post(new QueueDeletedEvent(queue));
}
}
@Override
public boolean isInQueue(final Player player) {
for (final Queue queue : this.availableQueues) {
if (queue.containsPlayer(player)) {
return true;
}
}
return false;
}
/**
* Listen queue modified event.
*
* @param queueModifiedEvent the event
*/
@Subscribe
public void onQueueModified(final QueueModifiedEvent queueModifiedEvent) {
final Queue queue = queueModifiedEvent.getQueue();
boolean queueModified = false;
switch (queue.getState()) {
case SUSPENDED:
break;
case IN_GAME:
break;
case WAITING_PLAYERS:
if (queue.isFull()) {
LOGGER.debug(String.format("Queue '%s' is waiting for a server'.", queue.getName()));
queue.setState(QueueState.WAITING_SERVER);
queueModified = true;
}
break;
case WAITING_READY:
if (queue.isEveryoneReady()) {
this.teeworldsServerHandler.startServer(queue.getServer());
queue.setState(QueueState.IN_GAME);
LOGGER.debug(String.format("Queue '%s' has started its game.", queue.getName()));
this.eventBus.post(new GameStartedEvent(queue));
queueModified = true;
}
if (queue.hasEveryResponse() && !queue.isEveryoneReady()) {
this.cancelQueueReady(queue);
LOGGER.debug(String.format("Queue '%s' has terminated. At least one player was not ready.", queue.getName()));
this.eventBus.post(new GameAbortedEvent(queue));
queueModified = true;
}
break;
case WAITING_SERVER:
// FIXME Thread-Safe ?
if (this.teeworldsServerHandler.hasServerAvailable()) {
LOGGER.debug(String.format("Queue '%s' has found a server.", queue.getName()));
final TeeworldsServer server = this.teeworldsServerHandler.createAndBorrowServer(queue.makeConfig());
queue.setServer(server);
queue.setState(QueueState.WAITING_READY);
this.startReadyTimer(queue);
LOGGER.debug(String.format("Queue '%s' has borrowed a server.", queue.getName()));
this.eventBus.post(new GameReadyEvent(queue));
queueModified = true;
}
break;
case GAME_OVER:
if (queue.isPermanent()) {
LOGGER.debug(String.format("Queue '%s' has terminated and has been reset.", queue.getName()));
queue.reset();
queueModified = true;
} else {
LOGGER.debug(String.format("Queue '%s' has terminated.", queue.getName()));
this.removeQueue(queue);
}
break;
}
if (queueModified) {
// Care to loop here. We are firing an event inside the same event
this.eventBus.post(new QueueModifiedEvent(queue));
}
}
/**
* Cancel queue ready.
*
* @param queue the queue
*/
private void cancelQueueReady(final Queue queue) {
this.teeworldsServerHandler.freeServer(queue.getServer());
queue.setServer(null);
queue.setState(QueueState.WAITING_PLAYERS);
queue.removeLeavers();
}
/**
* When queue ready has timed out.
*
* @param queueReadyTimedOutEvent the event
*/
@Subscribe
public void onQueueReadyTimedout(final QueueReadyTimedOutEvent queueReadyTimedOutEvent) {
final Queue queue = queueReadyTimedOutEvent.getQueue();
this.cancelQueueReady(queue);
this.eventBus.post(new GameAbortedEvent(queue));
LOGGER.debug(String.format("Queue '%s' has terminated. At least one player was not ready.", queue.getName()));
this.eventBus.post(new QueueModifiedEvent(queue));
}
/**
* Start ready timer on a queue.
*
* @param queue the queue
*/
private void startReadyTimer(final Queue queue) {
final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.schedule(new Runnable() {
@Override
public void run() {
if (queue.getState() == QueueState.WAITING_READY && !queue.hasEveryResponse()) {
MatchmakingImpl.this.eventBus.post(new QueueReadyTimedOutEvent(queue));
}
}
}, this.READY_TIMEOUT, TimeUnit.SECONDS);
}
/**
* Listen to server free event.
*
* @param serverFreeEvent the event
*/
@Subscribe
public void onServerFree(final ServerFreeEvent serverFreeEvent) {
for (final Queue queue : this.availableQueues) {
if (queue.getServer() == serverFreeEvent.getServer()) {
queue.setState(QueueState.GAME_OVER);
this.eventBus.post(new QueueModifiedEvent(queue));
}
}
}
/**
* A player has been modified. Update all queue which contains it.<br/>
* FIXME : should only update the player instead of all queues
*
* @param playerModifiedEvent the event
*/
@Subscribe
public void onPlayerModified(final PlayerModifiedEvent playerModifiedEvent) {
for (final Queue queue : this.getQueues(playerModifiedEvent.getPlayer())) {
this.eventBus.post(new QueueModifiedEvent(queue));
}
}
/**
* A player has been ready. Update the queue.<br/>
*
* @param playerReadyEvent the event
*/
@Subscribe
public void onPlayerReady(final PlayerReadyEvent playerReadyEvent) {
this.eventBus.post(new QueueModifiedEvent(playerReadyEvent.getQueue()));
}
@Override
public void setPlayerReady(final Player player, final Queue queue, final Boolean isReady) {
if (queue.setPlayerReady(player, isReady)) {
LOGGER.debug("Player " + player + " is ready(" + isReady + ") on queue : " + queue);
this.eventBus.post(new PlayerReadyEvent(player, queue));
}
}
}