/** * 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.core; import gnu.trove.map.hash.TIntObjectHashMap; import gnu.trove.procedure.TIntProcedure; import gnu.trove.set.hash.TIntHashSet; import java.util.ArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import com.godsandtowers.core.networking.PlayerSnapshot; import com.godsandtowers.core.networking.RemoteNetworkManager; import com.godsandtowers.core.networking.Snapshot; import com.godsandtowers.core.networking.SpriteSnapshot; import com.godsandtowers.messaging.ApplicationMessageProcessor; import com.godsandtowers.messaging.GLMessageProcessor; import com.godsandtowers.messaging.ViewMessageProcessor; import com.godsandtowers.sprites.BuildingSphere; import com.godsandtowers.sprites.Player; import com.godsandtowers.sprites.Tower; import com.godsandtowers.util.TDWPreferences; import com.gundogstudios.gl.Sprite; import com.gundogstudios.modules.Modules; public class RemoteGameEngine implements GameEngine { private static final String TAG = "RemoteGameEngine"; private TIntObjectHashMap<SpriteSnapshot> playerOneSprites; private TIntObjectHashMap<SpriteSnapshot> playerTwoSprites; private TIntObjectHashMap<SpriteSnapshot> playerOneGridSquareSprites; private TIntObjectHashMap<SpriteSnapshot> playerTwoGridSquareSprites; private int timeSinceLastRefresh; private long lastSnapshotTimestamp; private long overSlept; private long gameStart; private boolean loading; private boolean paused; private boolean exited; private boolean started; private boolean quit; private boolean finished; private GameInfo gameInfo; private int gridSquareIDLimit; private ExecutorService executor; private RemoteNetworkManager networkManager; public RemoteGameEngine(GameInfo gameInfo) { this.gameInfo = gameInfo; timeSinceLastRefresh = 0; lastSnapshotTimestamp = 0; overSlept = 0; playerOneSprites = new TIntObjectHashMap<SpriteSnapshot>(500); playerTwoSprites = new TIntObjectHashMap<SpriteSnapshot>(500); playerOneGridSquareSprites = new TIntObjectHashMap<SpriteSnapshot>(500); playerTwoGridSquareSprites = new TIntObjectHashMap<SpriteSnapshot>(500); quit = false; gridSquareIDLimit = addGridSquares(gameInfo.getPlayer(0)); int tmp = addGridSquares(gameInfo.getPlayer(1)); if (tmp > gridSquareIDLimit) gridSquareIDLimit = tmp; networkManager = new RemoteNetworkManager(gameInfo); executor = Executors.newSingleThreadExecutor(); executor.execute(networkManager); } private int addGridSquares(Player player) { TIntObjectHashMap<SpriteSnapshot> map = getGridSquareSprites(player.getID()); int max = 0; for (Sprite sprite : player.getGrid().getAllGridSquares()) { map.put(sprite.getID(), new SpriteSnapshot(sprite)); if (sprite.getID() > max) max = sprite.getID(); } return max; } @Override public void buyCreature(int player, String name) { networkManager.buyCreature(player, name); } @Override public void buyTower(int player, String name, float x, float y) { networkManager.buyTower(player, name, x, y); } @Override public void upgradeTower(int player, float x, float y) { networkManager.upgradeTower(player, x, y); } @Override public void sellTower(int player, float x, float y) { networkManager.sellTower(player, x, y); } @Override public void buildAllTowers(int player) { networkManager.buildAllTowers(player); } @Override public void buildTower(int player, String name) { networkManager.buildTower(player, name); } @Override public void cancelBuildTowers(int player) { networkManager.cancelBuildTowers(player); } @Override public void gridTouchCommand(int player, int col, int row) { networkManager.gridTouchCommand(player, col, row); } public void executeSpecial(int player, String name) { networkManager.executeSpecial(player, name); } @Override public void autoBuyCreature(int player, String name) { networkManager.autoBuyCreature(player, name); } @Override public void maxUpgradeTower(int player, float x, float y) { networkManager.maxUpgradeTower(player, x, y); } @Override public void cancelAutoBuyCreature(int player) { networkManager.cancelAutoBuyCreature(player); } @Override public void launchUpgradeView(int player, Tower tower) { if (player == gameInfo.getLocalPlayerID()) { Modules.MESSENGER.submit(ViewMessageProcessor.ID, ViewMessageProcessor.UPGRADE_TOWER, tower); } } @Override public void processResults(int winnerID) { gameInfo.setWinner(winnerID); finished = true; quit = true; } @Override public void run() { try { gameStart = System.currentTimeMillis(); lastSnapshotTimestamp = System.currentTimeMillis(); while (!quit) { long start = System.currentTimeMillis(); started = true; if (!paused && !loading) { nextTurn(); } else { synchronized (this) { this.notifyAll(); } try { Thread.sleep(REFRESH_RATE); } catch (InterruptedException e) { Modules.LOG.error(TAG, "interrupted during sleeping: " + e.toString()); } } timeSinceLastRefresh += System.currentTimeMillis() - start; if (timeSinceLastRefresh >= REFRESH_RATE) { Modules.MESSENGER.submit(ViewMessageProcessor.ID, ViewMessageProcessor.REFRESH); timeSinceLastRefresh = 0; } } Modules.LOG.info(TAG, "thread stopping cleanly"); } catch (Exception e) { e.printStackTrace(); Modules.LOG.error(TAG, e.toString()); Modules.MESSENGER.submit(ApplicationMessageProcessor.ID, ApplicationMessageProcessor.GAME_ERROR, e); } finally { gameInfo.setLength(System.currentTimeMillis() - gameStart); Modules.LOG.info(TAG, "EXITING"); synchronized (this) { exited = true; this.notifyAll(); } if (networkManager != null) { networkManager.shutDown(); executor.shutdownNow(); } if (finished) { Modules.MESSENGER.submit(ApplicationMessageProcessor.ID, ApplicationMessageProcessor.GAME_COMPLETED, gameInfo); } } } private void nextTurn() throws InterruptedException { long start = System.currentTimeMillis(); Snapshot snapshot = networkManager.getLatestSnapshot(); if (snapshot != null && snapshot.getTimeStamp() > lastSnapshotTimestamp) { lastSnapshotTimestamp = snapshot.getTimeStamp(); processSnapshot(snapshot); } if (System.currentTimeMillis() - lastSnapshotTimestamp > DISCONNECT_TIMEOUT) { finished = true; quit = true; gameInfo.setWon(false); gameInfo.setDisconnected(true); } Modules.PROFILER.updateLogicFPS(); long totalTime = System.currentTimeMillis() - start; long sleepTime = getTickInterval() - totalTime - overSlept; start = System.currentTimeMillis(); if (sleepTime > 0) { Thread.sleep(sleepTime); } else { Modules.LOG.warn(TAG, "Processing time took: " + totalTime); } overSlept = System.currentTimeMillis() - start - sleepTime; } private void processSnapshot(Snapshot snapshot) { int currentWave = snapshot.getCurrentWave(); gameInfo.setCurrentWave(currentWave); gameInfo.setTimeUntilNextWave(snapshot.getTimeUntilNextWave()); for (PlayerSnapshot playerSnapshot : snapshot.getPlayerSnapshots()) { final int playerID = playerSnapshot.getId(); final TIntObjectHashMap<SpriteSnapshot> sprites = getSprites(playerID); final TIntObjectHashMap<SpriteSnapshot> permanentSprites = getGridSquareSprites(playerID); Player player = gameInfo.getPlayer(playerID); player.setCreatureLevel(playerSnapshot.getCreatureLevel()); player.setGold(playerSnapshot.getGold()); player.setIncome(playerSnapshot.getIncome()); player.setLife(playerSnapshot.getLife()); PlayerInfo playerInfo = gameInfo.getPlayerInfo(playerID); playerInfo.setScore(playerSnapshot.getScore()); playerInfo.setLife(playerSnapshot.getLife(), currentWave); playerInfo.setIncome(playerSnapshot.getIncome(), currentWave); playerInfo.setDefensivePower(playerSnapshot.getDefensivePower(), currentWave); playerInfo.setOffensivePower(playerSnapshot.getOffensivePower(), currentWave); ArrayList<Sprite> newSprites = new ArrayList<Sprite>(); TIntHashSet removeSpriteIDs = new TIntHashSet(sprites.keys()); for (SpriteSnapshot newSprite : playerSnapshot.getSprites()) { int id = newSprite.getID(); SpriteSnapshot oldSprite = sprites.get(id); if (oldSprite == null) { oldSprite = newSprite; sprites.put(id, newSprite); if (newSprite.getModel() != null) { if (newSprite.isTransparent()) { if (newSprite.getModel().equals(BuildingSphere.NAME)) { // TODO this is an ugly hack, fix me eventually float scale = ((float) newSprite.getLevel()) / 10f; Modules.LOG.warn(TAG, "Sphere scale is: " + scale); Modules.MESSENGER.submit(GLMessageProcessor.ID, GLMessageProcessor.ADD_TRANSPARENT_SPRITE, playerID, newSprite, scale); } else { Modules.MESSENGER.submit(GLMessageProcessor.ID, GLMessageProcessor.ADD_TRANSPARENT_SPRITE, playerID, newSprite); } } else { newSprites.add(newSprite); } } } else { oldSprite.update(newSprite); } removeSpriteIDs.remove(id); int targetID = oldSprite.getTargetID(); if (targetID != 0) { SpriteSnapshot target; if (targetID <= gridSquareIDLimit) { target = permanentSprites.get(targetID); } else { target = sprites.get(targetID); } oldSprite.setTarget(target); } } Modules.MESSENGER.submit(GLMessageProcessor.ID, GLMessageProcessor.ADD_SPRITE_BATCH, playerID, newSprites); final ArrayList<Sprite> removeBatch = new ArrayList<Sprite>(removeSpriteIDs.size()); removeSpriteIDs.forEach(new TIntProcedure() { @Override public boolean execute(int value) { SpriteSnapshot sprite = sprites.remove(value); if (sprite.isTransparent()) Modules.MESSENGER.submit(GLMessageProcessor.ID, GLMessageProcessor.REMOVE_TRANSPARENT_SPRITE, playerID, sprite); else removeBatch.add(sprite); return true; } }); Modules.MESSENGER.submit(GLMessageProcessor.ID, GLMessageProcessor.REMOVE_SPRITE_BATCH, playerID, removeBatch); } } @Override public void setAttacking(int player, boolean attacking) { gameInfo.getPlayer(player).setAttacking(attacking); } @Override public GameInfo getGameInfo() { return gameInfo; } @Override public boolean hasStarted() { return started; } @Override public boolean hasExited() { return exited; } @Override public boolean isPaused() { return paused; } @Override public boolean isLoading() { return loading; } @Override public boolean isSaveable() { return false; } @Override public void setPaused(boolean paused) { this.paused = paused; } @Override public void setLoading(boolean loading) { this.loading = loading; } @Override public void quitGame() { quit = false; executor.shutdownNow(); } private TIntObjectHashMap<SpriteSnapshot> getSprites(int playerID) { if (playerID == 0) return playerOneSprites; else return playerTwoSprites; } private TIntObjectHashMap<SpriteSnapshot> getGridSquareSprites(int playerID) { if (playerID == 0) return playerOneGridSquareSprites; else return playerTwoGridSquareSprites; } private int getTickInterval() { int speed = Modules.PREFERENCES.get(TDWPreferences.GAME_ENGINE_SPEED, FAST); switch (speed) { case SLOW: return 200; case NORMAL: return 100; case FAST: return 50; default: return 0; } } }