/*
* 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.server.AphelionServerThread;
import aphelion.client.graphics.Graph;
import aphelion.client.net.NetworkedGame;
import aphelion.client.resource.AsyncTexture;
import aphelion.server.ServerConfigException;
import aphelion.shared.event.Deadlock;
import aphelion.shared.resource.ResourceDB;
import aphelion.shared.event.TickedEventLoop;
import aphelion.shared.net.WS_CLOSE_STATUS;
import aphelion.shared.net.protobuf.GameS2C.AuthenticateResponse;
import aphelion.shared.physics.EnvironmentConf;
import aphelion.shared.resource.LocalUserStorage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.Graphics;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.error.YAMLException;
/**
*
* @author Joris
*/
public class Client
{
private static final Logger log = Logger.getLogger("aphelion.client");
public static boolean showDebug = false;
private TickedEventLoop loop;
private ResourceDB resourceDB;
private AphelionServerThread serverThread;
private ConnectLoop connectLoop;
private STATE state = STATE.NONE;
private URI serverUri;
private static enum STATE
{
NONE,
CONNECTING,
PLAYING,
QUITTING
}
/** * @param uri if null, singleplayer
* @param nickname
* @throws LWJGLException
* @throws IOException
* @throws aphelion.server.ServerConfigException
*/
public void run(@Nullable URI uri, @Nonnull final String nickname) throws LWJGLException, IOException, ServerConfigException
{
if (uri == null)
{
Yaml yaml = new Yaml(new SafeConstructor());
Map<String, Object> singlePlayerConfig;
try
{
singlePlayerConfig = (Map<String, Object>) yaml.load(new FileInputStream("./assets/singleplayer.yaml"));
}
catch (FileNotFoundException | ClassCastException | YAMLException ex)
{
// Note: YAMLException is a RunTimeException
throw new ServerConfigException("Unable to read server config", ex);
}
// ignore bind-address and bind-port
// share the assets directory
singlePlayerConfig.put("assets-cache-path", new LocalUserStorage("assets").getDirectory().getAbsolutePath());
serverThread = new AphelionServerThread(false, singlePlayerConfig);
serverThread.start();
try
{
uri = new URI("ws://127.0.0.1:" + serverThread.getHTTPListeningPort() + "/aphelion");
}
catch (URISyntaxException ex)
{
log.log(Level.SEVERE, "Malformed URI", ex);
throw new Error(ex);
}
}
this.serverUri = uri;
Display.setTitle("Aphelion");
ByteBuffer[] icons = new ByteBuffer[6];
aphelion.launcher.Main.getFrameIcons(icons);
Display.setIcon(icons);
Display.setFullscreen(false);
Display.setVSyncEnabled(false);
Display.setResizable(true);
Display.setInitialBackground(0f, 0f, 0f);
Display.setDisplayMode(new DisplayMode(1024, 768));
Display.create();
log.log(Level.INFO, "OpenGL version: {0}", GL11.glGetString(GL11.GL_VERSION));
Keyboard.create();
// use the default time source for now
// availableProcessors is including HT (for example "8" on a quad core)
int processors = Runtime.getRuntime().availableProcessors();
if (processors < 2) { processors = 2; } // minimum of two workers
loop = new TickedEventLoop(EnvironmentConf.TICK_LENGTH, processors, null);
resourceDB = new ResourceDB(loop);
loop.addLoopEvent(resourceDB);
resourceDB.addZip(new File("assets/gui.zip"));
loop.setup();
connectLoop = new ConnectLoop(serverUri, resourceDB, loop, nickname);
state = STATE.NONE;
if (!connectLoop.loop())
{
log.log(Level.SEVERE, "Connection failed");
NetworkedGame networkedGame = connectLoop.getNetworkedGame();
AuthenticateResponse.ERROR authError = networkedGame.getAuthError();
WS_CLOSE_STATUS closeStatus = networkedGame.getDisconnectCode();
breakdown();
if (authError != null)
{
String reason = networkedGame.getAuthErrorDesc();
if (reason == null) reason = "";
JOptionPane.showMessageDialog(null, "Unable to authenticate to " + uri + " (code:"+authError +")\n\n"+reason, "Aphelion", JOptionPane.ERROR_MESSAGE);
}
else if (closeStatus != null)
{
String reason = networkedGame.getDisconnectReason();
if (reason == null) reason = "";
JOptionPane.showMessageDialog(null, "Unable to connect to " + uri + " (code:"+closeStatus +")\n\n"+reason, "Aphelion", JOptionPane.ERROR_MESSAGE);
}
return;
}
loop.setClockSource(connectLoop.getSyncedClockSource());
log.log(Level.INFO, "Connected");
state = STATE.PLAYING;
NetworkedGame netGame = connectLoop.getNetworkedGame();
InitializeLoop initLoop = new InitializeLoop(connectLoop);
if (initLoop.loop())
{
GameLoop gameLoop = new GameLoop(initLoop);
initLoop = null;
gameLoop.loop();
breakdown();
if (gameLoop.isConnectionError())
{
WS_CLOSE_STATUS code = netGame.getDisconnectCode();
String reason = netGame.getDisconnectReason();
if (reason == null) reason = "";
JOptionPane.showMessageDialog(null, "Connection to the server suddenly dropped (" + uri + ") (code:"+code +")\n\n"+reason, "Aphelion", JOptionPane.ERROR_MESSAGE);
}
}
else
{
log.log(Level.SEVERE, "Initialize failed");
breakdown();
}
}
public void breakdown()
{
state = STATE.QUITTING;
if (loop != null)
{
loop.breakdown();
}
Display.destroy();
Keyboard.destroy();
if (connectLoop != null)
{
connectLoop.getConnection().stop();
}
if (serverThread != null)
{
serverThread.stopServer();
}
log.log(Level.INFO, "Breakdown completed");
}
public static void initGL()
{
int displayWidth = Display.getWidth();
int displayHeight = Display.getHeight();
glDisableAll();
GL11.glViewport(0, 0, displayWidth, displayHeight);
GL11.glMatrixMode(GL11.GL_PROJECTION); // Apply subsequent matrix operations to the projection matrix stack.
GL11.glLoadIdentity();
GL11.glOrtho(0, displayWidth, displayHeight, 0, -1, 1);
GL11.glMatrixMode(GL11.GL_TEXTURE);
GL11.glLoadIdentity();
GL11.glMatrixMode(GL11.GL_MODELVIEW);
GL11.glLoadIdentity();
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_STENCIL_BUFFER_BIT);
AsyncTexture.unbind();
// Enable alpha channels for images
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glEnable(GL11.GL_BLEND);
Graph.g.setDimensions(displayWidth, displayHeight);
Graphics.setCurrent(Graph.g);
Graph.g.setDrawMode(Graphics.MODE_NORMAL);
}
private static void glDisableAll()
{
GL11.glDisable(GL11.GL_BLEND);
GL11.glDisable(GL11.GL_COLOR_LOGIC_OP);
GL11.glDisable(GL11.GL_CULL_FACE);
GL11.glDisable(GL11.GL_DEPTH_TEST);
GL11.glDisable(GL11.GL_DITHER);
GL11.glDisable(GL11.GL_LINE_SMOOTH);
GL11.glDisable(GL11.GL_POLYGON_OFFSET_FILL);
GL11.glDisable(GL11.GL_POLYGON_OFFSET_LINE);
GL11.glDisable(GL11.GL_POLYGON_OFFSET_POINT);
GL11.glDisable(GL11.GL_POLYGON_SMOOTH);
GL11.glDisable(GL11.GL_SCISSOR_TEST);
GL11.glDisable(GL11.GL_STENCIL_TEST);
}
public static boolean wasResized()
{
boolean ret = Display.wasResized() || wasFullscreenChanged;
wasFullscreenChanged = false;
return ret;
}
private static boolean wasFullscreenChanged = false;
private static int previousDesktopWidth = 1024;
private static int previousDesktopHeight = 768;
private static int previousDesktopX = 0;
private static int previousDesktopY = 0;
public static boolean setFullScreen(boolean fullscreen)
{
if (Display.isFullscreen() == fullscreen)
{
return true;
}
wasFullscreenChanged = true;
if (fullscreen)
{
previousDesktopWidth = Display.getWidth();
previousDesktopHeight = Display.getHeight();
previousDesktopX = Display.getX();
previousDesktopY = Display.getY();
DisplayMode[] modes;
try
{
modes = Display.getAvailableDisplayModes();
}
catch (LWJGLException ex)
{
log.log(Level.SEVERE, "Unable to determine available display modes!", ex);
return false;
}
Arrays.sort(modes, new Comparator<DisplayMode>()
{
@Override
public int compare(DisplayMode o1, DisplayMode o2)
{
// more is better
int size1 = o1.getWidth() * o1.getHeight();
int size2 = o2.getWidth() * o2.getHeight();
if (o1.getBitsPerPixel() == o2.getBitsPerPixel())
{
if (size1 == size2)
{
return -Integer.compare(o1.getFrequency(), o2.getFrequency());
}
return -Integer.compare(size1, size2);
}
return -Integer.compare(o1.getBitsPerPixel(), o2.getBitsPerPixel());
}
});
for (DisplayMode mode : modes)
{
try
{
Display.setDisplayModeAndFullscreen(mode);
return true;
}
catch (LWJGLException ex)
{
log.log(Level.WARNING, "Unable to use display mode {0} ({1}). Trying next one", new Object[]{
mode,
ex.getMessage()
});
}
}
log.log(Level.SEVERE, "Exhausted display modes, none work");
}
else
{
try
{
Display.setDisplayMode(new DisplayMode(previousDesktopWidth, previousDesktopHeight));
Display.setLocation(previousDesktopX, previousDesktopY);
return true;
}
catch (LWJGLException ex)
{
log.log(Level.SEVERE, "Unable to exit fullscreen!", ex);
return false;
}
}
return false;
}
public static boolean toggleFullScreen()
{
return setFullScreen(!Display.isFullscreen());
}
public static void main(String[] args) throws Exception
{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
Deadlock.start(true, null);
Client client;
client = new Client();
try
{
String uri = null;
String nickname = null;
if (args.length >= 2)
{
uri = args[0];
nickname = args[1];
}
if (args.length == 1)
{
uri = args[0];
}
if (uri != null)
{
if (uri.equalsIgnoreCase("null") || uri.equalsIgnoreCase("singleplayer"))
{
uri = null;
}
}
if (nickname == null)
{
nickname = "Player" + (int) (Math.random() * 1000000);
}
client.run(uri == null ? null : new URI(uri), nickname);
}
catch (Throwable ex)
{
client.breakdown();
new ErrorDialog().setErrorText(ex);
Deadlock.stop();
throw ex;
}
Deadlock.stop();
}
}