/******************************************************************************* * Copyright (c) 2015 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *******************************************************************************/ package jsettlers.network.synchronic.timer; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import jsettlers.network.NetworkConstants; import jsettlers.network.client.INetworkClientClock; import jsettlers.network.client.task.packets.SyncTasksPacket; import jsettlers.network.client.task.packets.TaskPacket; /** * This is a basic game timer. All synchronous actions must be based on this clock. The {@link NetworkTimer} also triggers the execution of * synchronous tasks in the network game. * * @author Andreas Eberle * */ public final class NetworkTimer extends TimerTask implements INetworkClientClock { public static final short TIME_SLICE = 50; private Comparator<SyncTasksPacket> tasksByTimeComperator = new Comparator<SyncTasksPacket>() { @Override public int compare(SyncTasksPacket o1, SyncTasksPacket o2) { return o1.getLockstepNumber() - o2.getLockstepNumber(); } }; private final Timer timer; private final Object lockstepLock = new Object(); private final List<ScheduledTimerable> timerables = new ArrayList<ScheduledTimerable>(); private final List<ScheduledTimerable> newTimerables = new LinkedList<ScheduledTimerable>(); private final List<INetworkTimerable> timerablesToBeRemoved = new LinkedList<INetworkTimerable>(); private final LinkedList<SyncTasksPacket> tasks = new LinkedList<SyncTasksPacket>(); private int time = 0; private int maxAllowedLockstep = -1; private boolean isPausing; private int pauseTime; private float speedFactor = 1.0f; private float progress = 0.0f; private boolean scheduled = false; private ITaskExecutor taskExecutor; private DataOutputStream replayLogStream; public NetworkTimer() { super(); this.timer = new Timer("NetworkTimer"); } public NetworkTimer(boolean disableLockstepWaiting) { this(); if (disableLockstepWaiting) { maxAllowedLockstep = Integer.MAX_VALUE; } } @Override public synchronized void startExecution() { if (!scheduled) { scheduled = true; timer.schedule(this, 0, TIME_SLICE); } } @Override public void stopExecution() { setPausing(true); timer.cancel(); closeReplayLogStreamIfNeeded(); } @Override public void run() { if (!isPausing) { if (pauseTime <= 0) { // this is used for synchronizing the network clients progress += speedFactor; while (progress >= 1) { executeRun(); progress--; } } else { pauseTime -= TIME_SLICE; } } } private synchronized void executeRun() { try { time += TIME_SLICE; final int lockstep = time / NetworkConstants.Client.LOCKSTEP_PERIOD; // check if the lockstep is allowed synchronized (lockstepLock) { while (lockstep > maxAllowedLockstep) { System.out.println("WAITING for lockstep!"); lockstepLock.wait(); } } SyncTasksPacket tasksPacket; synchronized (tasks) { tasksPacket = tasks.peekFirst(); } while (tasksPacket != null && tasksPacket.getLockstepNumber() <= lockstep) { assert tasksPacket.getLockstepNumber() == lockstep : "FOUND TasksPacket FOR older lockstep!"; System.out.println("Executing SyncTaskPacket(" + tasksPacket + ") in " + getLockstepText(lockstep)); try { executeTasksPacket(tasksPacket); } catch (Throwable t) { System.err.println("Error during execution of scheduled task:"); t.printStackTrace(); } synchronized (tasks) {// remove the executed tasksPacket and retrieve the next one to check it. tasks.pollFirst(); tasksPacket = tasks.peekFirst(); } } addNewTimerables(); handleRemovedTimerables(); for (ScheduledTimerable curr : timerables) { curr.checkExecution(TIME_SLICE); } } catch (Throwable t) { System.err.println("WARNING: Networking Timer catched Throwable!!!"); t.printStackTrace(); } } private void executeTasksPacket(SyncTasksPacket tasksPacket) { if (taskExecutor != null) { for (TaskPacket currTask : tasksPacket.getTasks()) { taskExecutor.executeTask(currTask); } } else { System.err.println("couldn't exeucte task, due to missing taskExecutor!"); } } private void addNewTimerables() { synchronized (newTimerables) { timerables.addAll(newTimerables); newTimerables.clear(); } } private void handleRemovedTimerables() { synchronized (timerablesToBeRemoved) { for (INetworkTimerable currToBeRemoved : timerablesToBeRemoved) { for (Iterator<ScheduledTimerable> iter = timerables.iterator(); iter.hasNext();) { if (iter.next().getTimerable() == currToBeRemoved) { iter.remove(); break; } } System.err.println("tried to remove a object from timer that's not registered!"); } timerablesToBeRemoved.clear(); } } /** * Schedules the given {@link INetworkTimerable} with given delay. The internal delay of NetworkTimer is {@value #TIME_SLICE}, but you may choose * smaller delays for the {@link INetworkTimerable}. The NetworkTimer will then call the {@link INetworkTimerable} multiple times on each internal * tick in the exact rate to ensure the given delay in the long run. * * @param timerable * {@link INetworkTimerable} to be scheduled. * @param period * delay of the given {@link INetworkTimerable}. */ @Override public void schedule(INetworkTimerable timerable, short period) { synchronized (newTimerables) { newTimerables.add(new ScheduledTimerable(timerable, period)); } } /** * removes an INetworkTimerable from the list of scheduled tasks. * * @param timerable */ @Override public void remove(INetworkTimerable timerable) { synchronized (timerablesToBeRemoved) { timerablesToBeRemoved.add(timerable); } } /** * Goes 60 * 1000 milliseconds forward as fast as possible */ @Override public synchronized void fastForward() { this.setPausing(true); final int runs = 60 * 1000 / TIME_SLICE; for (int i = 0; i < runs; i++) { executeRun(); } this.setPausing(false); } @Override public synchronized void fastForwardTo(int targetGameTime) { this.setPausing(true); System.out.println("Playing game forward to game time: " + targetGameTime); while (time < targetGameTime) { executeRun(); } } // methods for pausing @Override public void setPausing(boolean b) { this.isPausing = b; } @Override public void invertPausing() { this.isPausing = !this.isPausing; } @Override public boolean isPausing() { return isPausing; } @Override public void pauseClockFor(int timeDelta) { this.pauseTime = timeDelta; System.err.println("pausing for " + timeDelta + " ms"); } @Override public void setGameSpeed(float speedFactor) { this.speedFactor = speedFactor; } @Override public void multiplyGameSpeed(float factor) { this.speedFactor *= factor; } @Override public void setTaskExecutor(ITaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } @Override public void scheduleSyncTasksPacket(SyncTasksPacket tasksPacket) { assert maxAllowedLockstep == Integer.MAX_VALUE || maxAllowedLockstep + 1 == tasksPacket.getLockstepNumber() : "received unlock for wrong step! current max allowed: " + maxAllowedLockstep + " new: " + tasksPacket.getLockstepNumber(); if (!tasksPacket.getTasks().isEmpty()) { synchronized (tasks) { System.out.println("Scheduled SyncTasksPacket(" + tasksPacket + " for " + getLockstepText(tasksPacket.getLockstepNumber())); tasks.addLast(tasksPacket); Collections.sort(tasks, tasksByTimeComperator); saveReplayIfNeeded(tasksPacket); } } maxAllowedLockstep = Math.max(maxAllowedLockstep, tasksPacket.getLockstepNumber()); synchronized (lockstepLock) { lockstepLock.notifyAll(); } } private void saveReplayIfNeeded(SyncTasksPacket tasksPacket) { if (replayLogStream != null) { try { tasksPacket.serialize(replayLogStream); replayLogStream.flush(); } catch (IOException e) { e.printStackTrace(); } } } @Override public void setTime(int newTime) { this.time = newTime; } @Override public int getTime() { return time; } @Override public void setReplayLogStream(DataOutputStream replayFileStream) { if (this.replayLogStream != null) { throw new IllegalStateException("Replay log stream cannot be set twice!"); } if (replayFileStream != null) { replayLogStream = replayFileStream; } else { closeReplayLogStreamIfNeeded(); } } @Override public synchronized void saveRemainingTasks(DataOutputStream dos) throws IOException { for (SyncTasksPacket task : tasks) { task.serialize(dos); } dos.flush(); } private void closeReplayLogStreamIfNeeded() { if (replayLogStream != null) { try { replayLogStream.flush(); replayLogStream.close(); } catch (IOException e) { e.printStackTrace(); } finally { replayLogStream = null; } } } @Override public void loadReplayLogFromStream(DataInputStream dataInputStream) { try { while (true) { SyncTasksPacket currPacket = new SyncTasksPacket(); currPacket.deserialize(dataInputStream); scheduleSyncTasksPacket(currPacket); } } catch (IOException e1) { // something went wrong, or the stream was empty try { if (dataInputStream.read() == -1) { System.out.println("Successfully loaded jsettlers.integration.replay file."); } else { System.out.println("Error loading jsettlers.integration.replay file."); e1.printStackTrace(); } } catch (IOException e2) { System.out.println("Error loading jsettlers.integration.replay file."); e1.printStackTrace(); e2.printStackTrace(); } } } private String getLockstepText(int lockstep) { int time = lockstep * NetworkConstants.Client.LOCKSTEP_PERIOD; int hours = time / (1000 * 60 * 60); int minutes = (time / (1000 * 60)) % 60; int seconds = (time / 1000) % 60; int millis = time % 1000; return String.format("lockstep: %d (game time: %dms / %02d:%02d:%02d:%03d)", lockstep, time, hours, minutes, seconds, millis); } }