/** * Copyright (C) 2013 Gundog Studios LLC. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.godsandtowers; import gnu.trove.list.array.TIntArrayList; import gnu.trove.map.hash.TIntObjectHashMap; import gnu.trove.procedure.TIntObjectProcedure; import gnu.trove.procedure.TIntProcedure; import java.net.Inet4Address; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import com.godsandtowers.core.networking.ClientInfo; import com.godsandtowers.core.networking.DeviceInfo; import com.godsandtowers.core.networking.NetworkGameInfo; public class GameMatcher implements Runnable { private static GameMatcher MATCHER; private ReentrantReadWriteLock masterLock; private ReadLock readLock; private WriteLock writeLock; private boolean running; private AtomicInteger currentID; private TIntObjectHashMap<ClientInfo> clients; private TIntObjectHashMap<NetworkGameInfo> games; private GameMatcher() { running = false; currentID = new AtomicInteger(0); masterLock = new ReentrantReadWriteLock(true); readLock = masterLock.readLock(); writeLock = masterLock.writeLock(); clients = new TIntObjectHashMap<ClientInfo>(128); games = new TIntObjectHashMap<NetworkGameInfo>(128); } public NetworkGameInfo getGame(int id) { System.out.println("getGame: " + id); readLock.lock(); NetworkGameInfo info = games.get(id); readLock.unlock(); return info; } public int prepareGame(ClientInfo clientInfo) { System.out.println("prepareGame: " + clientInfo); int id = currentID.incrementAndGet(); clientInfo.resetPing(); writeLock.lock(); clients.put(id, clientInfo); notifyMatcher(); writeLock.unlock(); return id; } private void notifyMatcher() { if (!running) { synchronized (clients) { running = true; clients.notify(); } } } public void update(int id, Inet4Address address, int port) { System.out.println("update: " + id + " - " + address.getHostAddress() + ":" + port); readLock.lock(); ClientInfo client = clients.get(id); if (client == null) { readLock.unlock(); return; } // Note, due to the update atomically changing data for this client, we do not need a writelock // we do however need to notify the matcher of the change client.update(address, port); notifyMatcher(); readLock.unlock(); } @Override public void run() { System.out.println("GameMatcher Started"); while (true) { System.out.println("Running match cycle: currentID=" + currentID + " numClients=" + clients.size()); writeLock.lock(); removeStaleClients(); matchClients(); synchronized (clients) { running = false; writeLock.unlock(); try { clients.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } private void removeStaleClients() { final TIntArrayList staleIDs = new TIntArrayList(); clients.forEachEntry(new TIntObjectProcedure<ClientInfo>() { @Override public boolean execute(int id, ClientInfo client) { if (client.checkIfStale()) staleIDs.add(id); return true; } }); if (staleIDs.size() == 0) return; staleIDs.forEach(new TIntProcedure() { @Override public boolean execute(int id) { // System.out.println("Removing stale ID: " + id); clients.remove(id); games.remove(id); return true; } }); } private void matchClients() { final TIntArrayList ids = new TIntArrayList(clients.size()); clients.forEachEntry(new TIntObjectProcedure<ClientInfo>() { @Override public boolean execute(int id, ClientInfo client) { if (client.readyForGame()) ids.add(id); return true; } }); if (ids.size() < 2) return; ids.sort(); for (int i = 0; i < ids.size(); i += 2) { int idOne = ids.get(i); int idTwo = ids.get(i + 1); ClientInfo clientOne = clients.remove(idOne); ClientInfo clientTwo = clients.remove(idTwo); if (clientOne.getAddress().equals(clientTwo.getAddress()) && clientOne.getPort() == clientTwo.getPort()) { if (clientOne.getLastPingTime() > clientTwo.getLastPingTime()) { clients.put(idOne, clientOne); } else { clients.put(idTwo, clientTwo); } continue; } boolean isHost = isHost(clientOne, clientTwo); NetworkGameInfo networkGameInfo; if (isHost) { networkGameInfo = new NetworkGameInfo(idOne, clientOne, clientTwo, clientOne.getRequestedBoard(), clientOne.getRequestedSpeed()); } else { networkGameInfo = new NetworkGameInfo(idTwo, clientTwo, clientOne, clientTwo.getRequestedBoard(), clientTwo.getRequestedSpeed()); } games.put(idOne, networkGameInfo); games.put(idTwo, networkGameInfo); System.out.println("Made game for " + idOne + " and " + idTwo + " " + networkGameInfo); } } private boolean isHost(ClientInfo clientOne, ClientInfo clientTwo) { DeviceInfo one = clientOne.getDeviceInfo(); DeviceInfo two = clientTwo.getDeviceInfo(); if (one.getNumProcessors() != two.getNumProcessors()) return one.getNumProcessors() > two.getNumProcessors(); if (one.getMaxMemory() != two.getMaxMemory()) return one.getMaxMemory() > two.getMaxMemory(); if (one.getApiLevel() != two.getApiLevel()) return one.getApiLevel() > two.getApiLevel(); if (one.getGameEngineSpeed() != two.getGameEngineSpeed()) return one.getGameEngineSpeed() > two.getGameEngineSpeed(); if (one.getMeshQuality() != two.getMeshQuality()) return one.getMeshQuality() > two.getMeshQuality(); if (one.getTextureQuality() != two.getTextureQuality()) return one.getTextureQuality() > two.getTextureQuality(); return true; } public static GameMatcher instance() { return MATCHER; } public static void init() { MATCHER = new GameMatcher(); new Thread(MATCHER).start(); } }