/*
* 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.Graph;
import aphelion.client.graphics.nifty.*;
import aphelion.client.graphics.screen.Camera;
import aphelion.client.graphics.screen.CameraNiftyController;
import aphelion.client.graphics.screen.NiftyCameraImpl;
import aphelion.client.graphics.world.MapEntities;
import aphelion.client.graphics.world.StarField;
import aphelion.client.net.NetworkedGame;
import aphelion.client.net.SingleGameConnection;
import aphelion.client.resource.DBNiftyResourceLocation;
import aphelion.shared.event.TickEvent;
import aphelion.shared.event.TickedEventLoop;
import aphelion.shared.event.promise.AbstractPromise;
import aphelion.shared.event.promise.All;
import aphelion.shared.event.promise.PromiseException;
import aphelion.shared.event.promise.PromiseRejected;
import aphelion.shared.event.promise.PromiseResolved;
import aphelion.shared.gameconfig.LoadYamlTask;
import aphelion.shared.map.MapClassic;
import aphelion.shared.physics.DualRunnerEnvironment;
import aphelion.shared.physics.EnvironmentConf;
import aphelion.shared.resource.Asset;
import aphelion.shared.resource.DownloadAssetsTask;
import aphelion.shared.resource.ResourceDB;
import de.lessvoid.nifty.Nifty;
import de.lessvoid.nifty.nulldevice.NullSoundDevice;
import de.lessvoid.nifty.renderer.lwjgl.input.LwjglInputSystem;
import de.lessvoid.nifty.renderer.lwjgl.render.LwjglRenderDevice;
import de.lessvoid.nifty.screen.Screen;
import de.lessvoid.nifty.screen.ScreenController;
import de.lessvoid.nifty.spi.time.impl.AccurateTimeProvider;
import de.lessvoid.nifty.tools.resourceloader.ClasspathLocation;
import de.lessvoid.nifty.tools.resourceloader.NiftyResourceLoader;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import javax.swing.JOptionPane;
import org.lwjgl.opengl.Display;
import org.newdawn.slick.Color;
/**
* Download assets, load assets, etc.
* @author Joris
*/
public class InitializeLoop implements TickEvent
{
private static final Logger log = Logger.getLogger("aphelion.client");
final ResourceDB resourceDB;
final TickedEventLoop loop;
private boolean initComplete = false;
final Camera loadingCamera;
final StarField loadingStarfield;
int loadingCameraY;
// Network:
private boolean failure = false;
final SingleGameConnection connection;
final NetworkedGame networkedGame;
DownloadAssetsTask downloadAssetsTask;
// Map:
MapClassic mapClassic;
// Physics:
DualRunnerEnvironment physicsEnv;
// Input:
LwjglInputSystem inputSystem;
// Graphics:
StarField stars;
MapEntities mapEntities;
// Screen Graphics
Nifty nifty;
Screen mainScreen;
NiftyCameraImpl niftyCameraImpl;
public InitializeLoop(@Nonnull ConnectLoop connectLoop)
{
this.resourceDB = connectLoop.resourceDB;
this.loop = connectLoop.loop;
this.connection = connectLoop.connection;
this.networkedGame = connectLoop.networkedGame;
this.loadingCamera = connectLoop.loadingCamera;
this.loadingStarfield = connectLoop.loadingStarfield;
this.loadingCameraY = connectLoop.loadingCameraY;
}
private AbstractPromise downloadAssets()
{
downloadAssetsTask = new DownloadAssetsTask();
return loop.addWorkerTask(downloadAssetsTask, networkedGame.getRequiredAssets());
}
private final PromiseResolved loadAssets = new PromiseResolved()
{
@Override
public Object resolved(Object ret) throws PromiseException
{
downloadAssetsTask = null;
List<Asset> assets = (List<Asset>) ret;
for (Asset ass : assets)
{
try
{
// ass.file is now valid
// (the DownloadAssetsTask fails even if only 1 asset fails)
resourceDB.addZip(ass.file);
}
catch (IOException ex)
{
log.log(Level.SEVERE, "Received an asset file from the server that is not a zip", ex);
throw new PromiseException("Received an asset file from the server that is not a zip", ex);
}
}
return new All(loop, loadMap(), loadConfig());
}
};
private AbstractPromise loadMap()
{
return loop.addWorkerTask(new MapClassic.LoadMapTask(resourceDB, true), networkedGame.mapResource)
.then(new PromiseResolved()
{
@Override
public Object resolved(Object ret) throws PromiseException
{
log.log(Level.INFO, "Map loaded");
mapClassic = (MapClassic) ret;
// should work fine for lvl files < 2 GiB
stars = new StarField((int) mapClassic.getLevelSize(), resourceDB);
return mapClassic;
}
});
}
private AbstractPromise loadConfig()
{
return loop.addWorkerTask(new LoadYamlTask(resourceDB), Collections.unmodifiableList(networkedGame.gameConfigResources))
.then(new PromiseResolved()
{
@Override
public Object resolved(Object ret) throws PromiseException
{
log.log(Level.INFO, "Game config read");
// ret is List<LoadYamlTask.Return>
return ret;
}
});
}
private void initializePhysics(List<LoadYamlTask.Return> loadYamlResult)
{
physicsEnv = new DualRunnerEnvironment(loop, mapClassic);
mapEntities.tryInitialize(physicsEnv, connection);
for (LoadYamlTask.Return yamlResult : loadYamlResult)
{
physicsEnv.loadConfig(
physicsEnv.getTick() - physicsEnv.getConfig().HIGHEST_DELAY,
yamlResult.fileIdentifier,
yamlResult.yamlDocuments);
}
}
private void initializeNifty() throws PromiseException
{
inputSystem = new LwjglInputSystem();
try
{
inputSystem.startup();
}
catch (Exception ex)
{
throw new Error(ex);
}
nifty = new Nifty(new LwjglRenderDevice(), new NullSoundDevice(), inputSystem, new AccurateTimeProvider());
NiftyResourceLoader niftyResourceLoader = nifty.getResourceLoader();
niftyResourceLoader.removeAllResourceLocations();
// nifty first tries stuff on the class path (needed for its internal files)
niftyResourceLoader.addResourceLocation(new ClasspathLocation());
// Then try the same reference as a resource key
// (add this second so that zones can not override nifty build-ins)
niftyResourceLoader.addResourceLocation(new DBNiftyResourceLocation(resourceDB));
TriggerOnShowEffect.registerEffect(nifty);
SpriteAnimationEffect.registerEffect(nifty);
BackgroundColorSpriteEffect.registerEffect(nifty);
BackgroundColorAnimated.registerEffect(nifty);
ClockTextEffect.registerEffect(nifty);
niftyCameraImpl = new NiftyCameraImpl(resourceDB, mapEntities, mapClassic, stars);
CameraNiftyController.registerControl(nifty, niftyCameraImpl);
boolean first = true;
/*{
Logger logger = Logger.getLogger("de.lessvoid.nifty");
logger.setLevel(Level.FINER);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.FINER);
logger.addHandler(handler);
}*/
for (String res : networkedGame.niftyGuiResources)
{
if (first)
{
nifty.fromXmlWithoutStartScreen(res);
first = false;
}
else
{
nifty.addXml(res);
}
}
mainScreen = nifty.getScreen("aphelion-main");
if (mainScreen == null)
{
throw new PromiseException("Missing nifty-gui screen: aphelion-main");
}
ScreenController screenControl = mainScreen.getScreenController();
if (screenControl instanceof MainScreenController)
{
((MainScreenController) screenControl).aphelionBind(networkedGame);
}
nifty.gotoScreen("aphelion-main");
}
private final PromiseResolved assetsLoaded = new PromiseResolved()
{
@Override
public Object resolved(Object ret_) throws PromiseException
{
List ret = (List) ret_;
// argument 0 is the result of loadMap()
// argument 1 is the result of loadConfig()
initializePhysics((List<LoadYamlTask.Return>) ret.get(1));
initializeNifty();
// The server will now sync the physics data, send chats, et cetera
networkedGame.arenaLoaded(physicsEnv);
return null;
}
};
private final PromiseResolved initializeComplete = new PromiseResolved()
{
@Override
public Object resolved(Object ret) throws PromiseException
{
initComplete = true;
return null;
}
};
private final PromiseRejected initializeError = new PromiseRejected()
{
@Override
public void rejected(PromiseException error)
{
downloadAssetsTask = null;
failure = true;
log.log(Level.SEVERE, "Error while loading the arena", error);
JOptionPane.showMessageDialog(Display.getParent(),
"Error while loading the arena:\n"
+ error.getMessage()
+ "\nSee log for further details");
}
};
/** Download assets, parse them and set up the graphics, physics, input, etc.
* Returns when completed
* @return false if the loading failed and the game should quit (or go back to the menu)
*/
public boolean loop()
{
mapEntities = new MapEntities(resourceDB);
networkedGame.addActorListener(mapEntities, true);
loop.addTickEvent(mapEntities);
loop.addLoopEvent(mapEntities);
loadingCamera.setPosition(0, loadingCameraY);
// Asynchronous:
downloadAssets().then(loadAssets).then(assetsLoaded).then(initializeComplete).then(initializeError);
loop.addTickEvent(this);
try
{
while (!loop.isInterruped())
{
Display.update();
if (Display.isCloseRequested())
{
log.log(Level.WARNING, "Close requested in game loop");
loop.interrupt();
break;
}
Client.initGL();
if (Client.wasResized() && nifty != null)
{
nifty.resolutionChanged();
}
loop.loop(); // logic, call Promise callbacks
if (failure)
{
return false;
}
if (networkedGame.isDisconnected())
{
failure = true;
return false;
}
if (initComplete && networkedGame.isReady())
{
// continue on to GameLoop
return true;
}
loadingStarfield.render(loadingCamera);
if (downloadAssetsTask != null && downloadAssetsTask.getTotalFiles() > 0)
{
long totalBytes = downloadAssetsTask.getTotalBytes();
long completedBytes = downloadAssetsTask.getCompletedBytes();
double percentageComplete = (double) completedBytes / (double) totalBytes * 100.0;
double speedMiB = downloadAssetsTask.getSpeed() / 1024.0;
String line1 = String.format("Downloading: %2.1f%% (%3.1f KiB/s)",
percentageComplete,
speedMiB);
String line2 = String.format("File %d of %d; %d of %d bytes",
downloadAssetsTask.getVerifiedFiles() + 1,
downloadAssetsTask.getTotalFiles(),
completedBytes,
totalBytes);
// use the default slick font
int line1_width = Graph.g.getFont().getWidth(line1);
int line2_width = Graph.g.getFont().getWidth(line2);
int line_height = Graph.g.getFont().getLineHeight();
float y = loadingCamera.dimension.y * 0.75f;
Graph.g.setColor(Color.white);
Graph.g.drawString(line1, loadingCamera.dimensionHalf.x - line1_width / 2, y);
Graph.g.drawString(line2, loadingCamera.dimensionHalf.x - line2_width / 2, y + line_height);
}
else
{
String line = "Loading...";
int line_width = Graph.g.getFont().getWidth(line);
Graph.g.setColor(Color.white);
Graph.g.drawString(line, loadingCamera.dimensionHalf.x - line_width / 2, loadingCamera.dimension.y * 0.75f);
}
loadingCamera.setPosition(0, loadingCameraY);
Display.sync(60);
}
return false;
}
finally
{
loop.removeTickEvent(mapEntities);
loop.removeLoopEvent(mapEntities);
loop.removeTickEvent(this);
physicsEnv.done();
}
}
@Override
public void tick(long tick)
{
loadingCameraY -= 5;
}
}