/*
* 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.server;
import aphelion.server.game.Dummies;
import aphelion.shared.resource.Asset;
import aphelion.server.game.ServerGame;
import aphelion.server.http.HttpServer;
import aphelion.shared.event.*;
import aphelion.shared.event.promise.PromiseException;
import aphelion.shared.gameconfig.LoadYamlTask;
import aphelion.shared.map.MapClassic;
import aphelion.shared.map.MapClassic.LoadMapTask;
import aphelion.shared.physics.EnvironmentConf;
import aphelion.shared.physics.PhysicsEnvironment;
import aphelion.shared.physics.SimpleEnvironment;
import aphelion.shared.resource.AssetCache;
import aphelion.shared.resource.FileStorage;
import aphelion.shared.resource.ResourceDB;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.error.YAMLException;
/**
*
* @author Joris
*/
public class ServerMain implements LoopEvent, TickEvent
{
private static final Logger log = Logger.getLogger("aphelion.server");
private final ServerSocketChannel listen;
private final Map<String, Object> config;
private TickedEventLoop loop;
private AphelionServer server;
private ServerGame serverGame;
private SimpleEnvironment physicsEnv;
private Dummies dummies;
private AssetCache assetCache;
private List<Asset> assets;
private String mapResource;
private List<String> gameConfigResources;
private List<String> niftyGuiResources;
public ServerMain(ServerSocketChannel listen, Map<String, Object> config)
{
this.listen = listen;
this.config = config;
}
public void setup() throws IOException, ServerConfigException
{
int processors = Runtime.getRuntime().availableProcessors();
if (processors < 2) { processors = 2; } // minimum of two workers
loop = new TickedEventLoop(EnvironmentConf.TICK_LENGTH, processors, null);
server = new AphelionServer(listen, new File("./www"), loop);
try
{
File dir = new File((String) config.get("assets-cache-path")).getCanonicalFile();
dir.mkdirs();
FileStorage assetCacheStorage = new FileStorage(dir);
if (!assetCacheStorage.isUseable())
{
throw new ServerConfigException("assets-cache-path is not readable/writeable or not a directory: " + assetCacheStorage);
}
assetCache = new AssetCache(assetCacheStorage);
}
catch (ClassCastException | NullPointerException ex)
{
throw new ServerConfigException("Missing or invalid server config entry: assets-cache-path");
}
catch (IOException ex)
{
throw new ServerConfigException("The given assets-cache-path is not a valid directory: " + config.get("assets-cache-path"), ex);
}
try
{
// todo: multiple arenas (seperate directories with arena config?)
Map<String, Object> arena = (Map<String, Object>) config.get("arena");
List configAssets = (List) arena.get("assets");
this.assets = new ArrayList<>(configAssets.size());
for (Object c : configAssets)
{
Asset ass = new Asset(assetCache, c);
this.assets.add(ass);
}
mapResource = (String) arena.get("map");
gameConfigResources = new ArrayList<>((List<String>) arena.get("game-config"));
niftyGuiResources = new ArrayList<>((List<String>) arena.get("nifty-gui"));
}
catch (ClassCastException | NullPointerException ex)
{
throw new ServerConfigException("Invalid config for 'arena'", ex);
}
server.addHttpRouteStatic("assets", assetCache.getStorage().getDirectory());
loop.addLoopEvent(server);
ResourceDB resourceDB = new ResourceDB(loop);
for (Asset ass : this.assets)
{
try
{
ass.storeAsset(ass.configFile, true);
}
catch (AssetCache.InvalidContentException ex)
{
throw new AssertionError(ex);
}
// ass.file is now valid
resourceDB.addZip(ass.file);
}
if (!resourceDB.resourceExists(mapResource))
{
throw new ServerConfigException("Resource does not exist: " + mapResource);
}
for (String key : gameConfigResources)
{
if (!resourceDB.resourceExists(key))
{
throw new ServerConfigException("Resource does not exist: " + mapResource);
}
}
MapClassic map;
List<LoadYamlTask.Return> gameConfig;
try
{
map = new LoadMapTask(resourceDB, false).work(mapResource);
gameConfig = new LoadYamlTask(resourceDB).work(gameConfigResources);
}
catch (PromiseException ex)
{
log.log(Level.SEVERE, null, ex);
throw (IOException) ex.getCause();
}
physicsEnv = new SimpleEnvironment(true, map);
for (LoadYamlTask.Return ret : gameConfig)
{
physicsEnv.loadConfig(physicsEnv.getTick() - physicsEnv.getConfig().HIGHEST_DELAY, ret.fileIdentifier, ret.yamlDocuments);
}
gameConfig = null;
serverGame = new ServerGame(physicsEnv, loop, assets, mapResource, gameConfigResources, niftyGuiResources);
loop.addLoopEvent(serverGame);
loop.addTickEvent(serverGame);
server.setGameClientListener(serverGame);
dummies = new Dummies(10, physicsEnv, serverGame);
dummies.setup();
loop.addLoopEvent(this);
loop.addTickEvent(this);
loop.addTickEvent(dummies);
server.setup();
}
public int getHTTPListeningPort()
{
return server.getHTTPListeningPort();
}
public void run()
{
loop.run();
}
public void stop()
{
loop.interrupt();
server.stop();
log.log(Level.INFO, "ServerMain has stopped");
}
@Override
public void loop(long systemNanoTime, long sourceNanoTime)
{
if (serverGame == null)
{
server.setPingPlayerCount(-1, -1);
}
else
{
server.setPingPlayerCount(serverGame.getPlayerCount(), -1);
// todo playing
}
}
@Override
public void tick(long tick)
{
}
public static void main(String[] args) throws IOException, ServerConfigException
{
if (args.length < 1)
{
throw new IllegalArgumentException("The first argument should be the path to a yaml config file");
}
Yaml yaml = new Yaml(new SafeConstructor());
Map<String, Object> config;
String address;
int port;
try
{
config = (Map<String, Object>) yaml.load(new FileInputStream(args[0]));
}
catch (FileNotFoundException | ClassCastException | YAMLException ex)
{
// Note: YAMLException is a RunTimeException
throw new ServerConfigException("Unable to read server config", ex);
}
try
{
address = config.containsKey("bind-address") ? (String) config.get("bind-address") : "0.0.0.0";
port = config.containsKey("bind-port") ? (int) config.get("bind-port") : 80;
}
catch (ClassCastException ex)
{
throw new ServerConfigException("Invalid bind-address or bind-port", ex);
}
try (ServerSocketChannel ssChannel = HttpServer.openServerChannel(new InetSocketAddress(address, port)))
{
Deadlock.start(false, null);
ServerMain main = new ServerMain(ssChannel, config);
main.setup();
main.run();
Deadlock.stop();
}
}
}