/*
* Aphelion
* Copyright (c) 2013 Joris van der Wel
*
* This file is part of Aphelion
*
* Aphelion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* Aphelion 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 Affero General Public License
* along with Aphelion. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, the following supplemental terms apply, based on section 7 of
* the GNU Affero General Public License (version 3):
* a) Preservation of all legal notices and author attributions
* b) Prohibition of misrepresentation of the origin of this material, and
* modified versions are required to be marked in reasonable ways as
* different from the original version (for example by appending a copyright notice).
*
* Linking this library statically or dynamically with other modules is making a
* combined work based on this library. Thus, the terms and conditions of the
* GNU Affero General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your
* choice, provided that you also meet, for each linked independent module,
* the terms and conditions of the license of that module. An independent
* module is a module which is not derived from or based on this library.
*/
package aphelion.client;
import aphelion.client.graphics.nifty.chat.AphelionChatControl;
import aphelion.client.graphics.Graph;
import aphelion.client.graphics.nifty.*;
import aphelion.client.net.NetworkedGame;
import aphelion.client.net.SingleGameConnection;
import aphelion.shared.resource.ResourceDB;
import aphelion.client.graphics.screen.Gauges;
import aphelion.client.graphics.screen.NiftyCameraImpl;
import aphelion.client.graphics.world.*;
import aphelion.shared.event.TickedEventLoop;
import aphelion.shared.physics.entities.ActorPublic;
import aphelion.shared.map.MapClassic;
import aphelion.shared.physics.DualRunnerEnvironment;
import aphelion.shared.swissarmyknife.Point;
import aphelion.shared.swissarmyknife.SwissArmyKnife;
import de.lessvoid.nifty.Nifty;
import de.lessvoid.nifty.controls.Controller;
import de.lessvoid.nifty.elements.Element;
import de.lessvoid.nifty.elements.render.TextRenderer;
import de.lessvoid.nifty.renderer.lwjgl.input.LwjglInputSystem;
import de.lessvoid.nifty.screen.Screen;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import org.lwjgl.opengl.Display;
/**
*
* @author Joris
*/
public class GameLoop
{
private static final Logger log = Logger.getLogger("aphelion.client");
private final ResourceDB resourceDB;
private final TickedEventLoop loop;
private boolean connectionError = false;
// Network:
private final SingleGameConnection connection;
private final NetworkedGame networkedGame;
private LocalChat localChat;
private GameEvents gameEvents;
// Input:
private final LwjglInputSystem inputSystem;
private MyKeyboard myKeyboard;
// Physics:
private final DualRunnerEnvironment physicsEnv;
private ActorPublic localActor;
// Map:
private final MapClassic mapClassic;
// Graphics:
private final StarField stars;
private final MapEntities mapEntities;
// Screen Graphics
private final Nifty nifty;
private final NiftyCameraImpl niftyCameraImpl;
private final Screen mainScreen;
private EnergyBar[] energyBars;
private Element[] energyTexts;
private Gauges gauges;
private AphelionChatControl[] chatLocals;
private Element gameMenuPopup;
private GameEventsDisplay[] gameEventsDisplays;
// Graphics statistics
private long frames;
private long lastFrameReset;
private long lastFps;
public GameLoop(@Nonnull InitializeLoop initializeLoop)
{
this.resourceDB = initializeLoop.resourceDB;
this.loop = initializeLoop.loop;
this.connection = initializeLoop.connection;
this.networkedGame = initializeLoop.networkedGame;
this.mapClassic = initializeLoop.mapClassic;
this.physicsEnv = initializeLoop.physicsEnv;
this.inputSystem = initializeLoop.inputSystem;
this.stars = initializeLoop.stars;
this.mapEntities = initializeLoop.mapEntities;
this.nifty = initializeLoop.nifty;
this.mainScreen = initializeLoop.mainScreen;
this.niftyCameraImpl = initializeLoop.niftyCameraImpl;
}
public boolean isConnectionError()
{
return connectionError;
}
/** Find all controls that begin with the given prefix by adding up numbers.
* e.x. findControls("bla", ...) looks for:
* bla (optional)
* bla0
* bla1
* bla2
* bla3
*/
private @Nonnull <T extends Controller> T[] findControls(@Nonnull String elementNamePrefix, @Nonnull Class<T> requestedControlClass, @Nonnull T[] emptyArray)
{
LinkedList<Controller> ret = new LinkedList<>();
T control = mainScreen.findControl(elementNamePrefix, requestedControlClass);
if (control != null)
{
ret.add(control);
}
int i = 0;
while (true)
{
control = mainScreen.findControl(elementNamePrefix + "-" + i, requestedControlClass);
++i;
if (control == null)
{
break;
}
ret.add(control);
}
return ret.toArray(emptyArray);
}
private @Nonnull Element[] findElements(@Nonnull String elementNamePrefix)
{
return SwissArmyKnife.findNiftyElementsByIdPrefix(mainScreen, elementNamePrefix);
}
private void lookUpNiftyElements()
{
energyBars = findControls("energy-bar", EnergyBar.class, new EnergyBar[]{});
energyTexts = findElements("energy-text");
chatLocals = findControls("chat-local", AphelionChatControl.class, new AphelionChatControl[]{});
gameMenuPopup = nifty.createPopup("gameMenuPopup");
gameEventsDisplays = findControls("game-events", GameEventsDisplay.class, new GameEventsDisplay[]{});
loop.addLoopEvent(gameEventsDisplays);
}
public void loop()
{
try
{
// This thread takes care of rendering graphics
Thread.currentThread().setPriority(Thread.MAX_PRIORITY - 1);
loop.addTickEvent(mapEntities);
loop.addLoopEvent(mapEntities);
lookUpNiftyElements();
localChat = new LocalChat(loop, networkedGame, Collections.unmodifiableList(Arrays.asList(chatLocals)));
localChat.subscribeListeners(mainScreen);
gameEvents = new GameEvents(networkedGame, Collections.unmodifiableList(Arrays.asList(gameEventsDisplays)));
gameEvents.subscribeListeners(connection);
lastFrameReset = System.nanoTime();
frames = 60;
boolean tickingPhysics = false;
while (!loop.isInterruped())
{
long begin = System.nanoTime();
Display.update();
if (Display.isCloseRequested())
{
log.log(Level.WARNING, "Close requested in game loop");
loop.interrupt();
break;
}
Graph.graphicsLoop();
if (!tickingPhysics && networkedGame.hasArenaSynced())
{
// Do not tick() on physics until ArenaSync has been received.
// The server tick count is not known until ArenaSync has been received.
// must come before keyboard input, which generates new events, therefor prepend
loop.prependTickEvent(physicsEnv);
loop.addLoopEvent(physicsEnv);
tickingPhysics = true;
}
if (localActor == null)
{
localActor = physicsEnv.getActor(networkedGame.getMyPid());
}
if (myKeyboard == null && localActor != null)
{
myKeyboard = new MyKeyboard(
inputSystem,
mainScreen,
networkedGame,
physicsEnv,
localActor,
physicsEnv.getGlobalConfigStringList("ships"),
gameMenuPopup
);
loop.addTickEvent(myKeyboard);
GameMenuController gameMenuControl = gameMenuPopup.getControl(GameMenuController.class);
if (gameMenuControl != null)
{
gameMenuControl.aphelionBind(networkedGame);
}
}
if (nifty.update())
{
log.log(Level.WARNING, "Close by nifty in game loop");
loop.interrupt();
break;
}
if (gauges != null && myKeyboard != null)
{
myKeyboard.poll();
gauges.setMultiFireGun(myKeyboard.isMultiFireGun());
}
Client.initGL();
if (Client.wasResized())
{
nifty.resolutionChanged();
}
loop.loop(); // logic
if (networkedGame.isDisconnected())
{
connectionError = true;
return;
}
ActorShip localShip = mapEntities.getLocalShip();
mapEntities.tryInitialize(physicsEnv, connection);
mapEntities.updateGraphicsFromPhysics();
if (localShip == null || localActor == null)
{
niftyCameraImpl.setDefaultCameraPosition(8192, 8192);
}
else
{
int energy = localShip.getEnergy(false);
float energyProgress = energy / (float) localShip.getMaxEnergy();
for (EnergyBar energyBar : this.energyBars)
{
energyBar.setProgress(energyProgress);
}
for (Element energyText : this.energyTexts)
{
energyText.getRenderer(TextRenderer.class).setText(energy + "");
}
if (gauges == null)
{
assert mainScreen != null;
gauges = new Gauges(mainScreen, localActor);
}
Point cameraPos = new Point();
localShip.getCameraPosition(cameraPos);
niftyCameraImpl.setDefaultCameraPosition(cameraPos);
}
nifty.render(false);
// statistics
long now = System.nanoTime();
++frames;
long frameTimeDelta = (now - begin) / 1_000_000;
Element dbg = mainScreen == null ? null : mainScreen.findElementByName("debug-info");
if (physicsEnv != null && dbg != null)
{
String text = String.format("%d (%2dms) %4d (%d %d) %3dms",
lastFps,
frameTimeDelta,
physicsEnv.getTick(),
physicsEnv.getTimewarpCount(),
physicsEnv.getResetCount(),
networkedGame.getlastRTTNano() / 1_000_000L);
if (localShip != null)
{
text += "\n("+((int) localShip.pos.x / 16)+","+((int) localShip.pos.y / 16)+")";
if (localShip.getActor() != null)
{
text += " " + localShip.getActor().getShip();
}
}
dbg.getRenderer(TextRenderer.class).setText(text);
}
Display.sync(60);
if (now - lastFrameReset > 1000000000L)
{
lastFps = frames;
frames = 0;
lastFrameReset = now;
}
}
}
finally
{
loop.removeTickEvent(physicsEnv);
loop.removeLoopEvent(physicsEnv);
loop.removeTickEvent(mapEntities);
loop.removeLoopEvent(mapEntities);
loop.removeLoopEvent(gameEventsDisplays);
loop.removeTickEvent(myKeyboard);
physicsEnv.done();
}
}
}