package be.demmel.jgws;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.io.File;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URL;
import java.rmi.Naming;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.Persistence;
import javax.persistence.TypedQuery;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.JAXBIntrospector;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
import be.demmel.jgws.cfg.jaxb.Binding;
import be.demmel.jgws.cfg.jaxb.GameserverConfiguration;
import be.demmel.jgws.cfg.jaxb.Jpa;
import be.demmel.jgws.cfg.jaxb.LogBlackList;
import be.demmel.jgws.entities.MapSpawn;
import be.demmel.jgws.entities.NpcMasterData;
import be.demmel.jgws.entities.NpcSpawn;
import be.demmel.jgws.modules.Movement;
import be.demmel.jgws.network.InboundPacketHandler;
import be.demmel.jgws.network.NettyServer;
import be.demmel.jgws.packets.Packet;
import be.demmel.jgws.packets.handlers.PacketHandler;
import be.demmel.jgws.packets.handlers.tools.EntityManagerFactoryTool;
import be.demmel.jgws.packets.serialization.PacketDeserializer;
import be.demmel.jgws.packets.serialization.PacketSerializer;
import be.demmel.jgws.rmi.GameServerPortalImpl;
import be.demmel.jgws.rmi.LoginServerPlayerData;
import be.demmel.jgws.rmi.LoginServerPortal;
import be.demmel.jgws.utils.GWVector;
import be.demmel.jgws.utils.GeneralUtils;
public class GameServer {
private static final Logger LOGGER = LoggerFactory.getLogger(GameServer.class);
private static List<MapData> loadedMaps;
private static List<LoginServerPlayerData> playerDataOfAcceptedClients = new ArrayList<>();
public static final String[] WELCOME_MESSAGE = new String[] { "Welcome to:", "<--[ JGWLP:R v" + Version.getVersion() + " ]-->",
"<--[ Credits to (C#): _rusty, ACB, miracle444 & onyxphase ]-->", "<--[ Credits to (Java): iDemmel ]-->" };
private static EntityManagerFactory entityManagerFactory;
public static void addClientKeys(LoginServerPlayerData playerData) {
playerDataOfAcceptedClients.add(playerData);
}
public static LoginServerPlayerData findPlayerDataBySecurityKeys(short[] securityKeys1, short[] securityKeys2) {
outer: for (LoginServerPlayerData playerDataOfAcceptedClient : playerDataOfAcceptedClients) {
// Check the first set of securityKeys
for (int i = 0; i < playerDataOfAcceptedClient.getSecurityKey1().length; i++) {
short storedKey = playerDataOfAcceptedClient.getSecurityKey1()[i];
short searchedKey = securityKeys1[i];
if (storedKey != searchedKey) {
continue outer;
}
}
// Check the second set of securityKeys
for (int i = 0; i < playerDataOfAcceptedClient.getSecurityKey2().length; i++) {
short storedKey = playerDataOfAcceptedClient.getSecurityKey2()[i];
short searchedKey = securityKeys2[i];
if (storedKey != searchedKey) {
continue outer;
}
}
// If we reached this point it means that both sets of keys match, so return the PlayerData
return playerDataOfAcceptedClient;
}
return null;
}
public static MapData getMap(int mapId) {
for (MapData currentMap : loadedMaps) {
if (currentMap.getMapID() == mapId) {
return currentMap;
}
}
return null;
}
public static List<MapData> getLoadedMaps() {
return loadedMaps;
}
// TODO: broadcasting to other players will only happen from Map > Players, so no problem when just READING while treating packets
// When reading, the map can change, so the setter should be used by the actions when needed!!
public static void main(String... parameters) {
Thread.currentThread().setName("Startup");
LOGGER.info("The \"Java Guild Wars (Game)Server\" (version {}) is starting", Version.getVersion());
try {
GameserverConfiguration gameServerConfiguration = getGameServerConfiguration();
// Retrieve the PacketDeserializers for every outgoing (game) packet
Map<Integer, PacketDeserializer> packetDeserializers = GeneralUtils.getPacketDeserializers("be.demmel.jgws.packets.gameserver.inbound");
// Retrieve the PacketSerializers for every incoming (game) packet
Map<Class<? extends Packet>, PacketSerializer> packetSerializers = GeneralUtils.getPacketSerializers("be.demmel.jgws.packets.gameserver.outbound");
// Retrieve the PacketHandlers for every incoming (login) packet
Map<Class<? extends Packet>, PacketHandler> packetHandlers = GeneralUtils.getPacketHandlers("be.demmel.jgws.packets.handlers");
// Create the ActionQueue that processes actions
ActionQueue actionQueue = new ActionQueue();
Thread actionQueueThread = new Thread(actionQueue);
actionQueueThread.start();
Jpa jpaConfiguration = gameServerConfiguration.getJpa();
Map<String, String> properties = new HashMap<>();
properties.put("javax.persistence.jdbc.driver", jpaConfiguration.getDriver());
properties.put("javax.persistence.jdbc.url", jpaConfiguration.getUrl());
properties.put("javax.persistence.jdbc.user", jpaConfiguration.getUser());
properties.put("javax.persistence.jdbc.password", jpaConfiguration.getPassword());
entityManagerFactory = Persistence.createEntityManagerFactory("jgws", properties);
EntityManagerFactoryTool.setEntityManagerFactory(entityManagerFactory);
// build all maps for this GS
loadedMaps = loadMaps();
LOGGER.info("Registering Game Server to Login Server");
// Register this Game Server with the Login Server
String portalUri = gameServerConfiguration.getPortalUri();
URI serviceRmiUri = new URI(portalUri);
LoginServerPortal remoteObject = null;
// get the remote object from the registry
try {
remoteObject = (LoginServerPortal) Naming.lookup(serviceRmiUri.toASCIIString());
} catch (Exception e) {
// Add some understandable string
throw new Exception("RMI connection failed : ", e);
}
Binding bindingConfiguration = gameServerConfiguration.getBinding();
String bindingIp = bindingConfiguration.getIp();
int bindingPort = bindingConfiguration.getPort();
try {
InetSocketAddress hostAndPort = new InetSocketAddress(InetAddress.getByName(bindingIp), bindingPort);
GameServerPortalImpl gameServerPortal = new GameServerPortalImpl();
int portalCallbackPort = gameServerConfiguration.getPortalCallbackPort();
UnicastRemoteObject.exportObject(gameServerPortal, portalCallbackPort);
be.demmel.jgws.rmi.GameServer gameServer = new be.demmel.jgws.rmi.GameServerImpl(gameServerPortal, hostAndPort);
remoteObject.registerGameServer(gameServer);
} catch (Exception e) {
// Add some understandable string
throw new Exception("Registering this Game Server to the Login Server failed : ", e);
}
LOGGER.info("Registered Game Server to Login Server");
// TODO: SSL (with certificates) for RMI
// TODO: also add an option to disable SSL, because listening on an interface which is NOT available to the public could also work
// (intranet IP...) :)
ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
final InboundPacketHandler<GameServerSession> inboundPacketHandler = new InboundPacketHandler<GameServerSession>(channels, packetHandlers, SessionKey.SESSION_KEY, actionQueue);
LogBlackList logBlackList = gameServerConfiguration.getLogBlackList();
if(gameServerConfiguration.getLogBlackList() != null) { // don't log some inbound and outbound packets
Set<Class<? extends Packet>> inboundPacketsLogBlackList = new HashSet<Class<? extends Packet>>(), outboundPacketsLogBlackList = new HashSet<Class<? extends Packet>>();
for(String packet : logBlackList.getInbound().getPacket()) {
inboundPacketsLogBlackList.add( (Class<? extends Packet>)Class.forName(packet));
}
for(String packet : logBlackList.getOutbound().getPacket()) {
outboundPacketsLogBlackList.add( (Class<? extends Packet>)Class.forName(packet));
}
GameServerChannelInitializer pingChannelInitializer = new GameServerChannelInitializer(packetDeserializers, inboundPacketHandler, packetSerializers, SessionKey.SESSION_KEY, inboundPacketsLogBlackList, outboundPacketsLogBlackList);
Thread serverThread = new Thread(new NettyServer<GameServerSession>(new InetSocketAddress(bindingIp, bindingPort), pingChannelInitializer));
serverThread.start();
} else {
GameServerChannelInitializer pingChannelInitializer = new GameServerChannelInitializer(packetDeserializers, inboundPacketHandler, packetSerializers, SessionKey.SESSION_KEY);
Thread serverThread = new Thread(new NettyServer<GameServerSession>(new InetSocketAddress(bindingIp, bindingPort), pingChannelInitializer));
serverThread.start();
}
// Start the Movement module
Thread movementThread = new Thread(new Movement(actionQueue, channels));
movementThread.start();
// The startup process went well. Log the event and keep the main thread alive
LOGGER.info("The \"Java Guild Wars (Game)Server\" started");
for (;;) {
Thread.sleep(Long.MAX_VALUE);
}
} catch (Throwable throwable) {
LOGGER.error("Initializing the \"Java Guild Wars (Game)Server\" failed because: ", throwable);
}
}
private static List<MapData> loadMaps() throws Exception {
List<MapData> mapsData = new ArrayList<>();
// all maps
Set<be.demmel.jgws.entities.Map> maps = getAllMaps();
// result.next();
for (be.demmel.jgws.entities.Map map : maps) {
int mapId = map.getMapId();
int gameMapId = map.getGameMapId();
int gameMapFileId = map.getGameMapFileId();
// Not needed
// String gameMapName = result.getString("GameMapName");
MapData mapData = new MapData();
mapData.setMapID(mapId);
mapData.setGameMapID(gameMapId);
mapData.setGameFileID(gameMapFileId);
// FIXME: set this to false when needed
mapData.setIsOutpost(true);
// FIXME: set this to false when needed
mapData.setIsPve(true);
// FIXME: why constants??
mapData.setDistrictCountry(2);
mapData.setDistrictNumber(1);
// load the possible spawn locations for the players
Set<MapSpawn> mapSpawns = getMapSpawns(mapId, mapData.isIsOutpost(), mapData.isIsPve());
for (MapSpawn mapSpawn : mapSpawns) {
float spawnX = mapSpawn.getSpawnX();
float spawnY = mapSpawn.getSpawnY();
int planeZ = mapSpawn.getSpawnPlane();
mapData.getPossibleSpawns().add(new GWVector(spawnX, spawnY, planeZ));
}
if (mapData.getPossibleSpawns().isEmpty()) {
LOGGER.warn("No possible spawn locations for map {}", mapId);
mapData.getPossibleSpawns().add(new GWVector(0, 0, 0));
}
Set<NpcSpawn> npcSpawns = getNpcSpawns(mapId, mapData.isIsOutpost(), mapData.isIsPve());
for (NpcSpawn npcSpawn : npcSpawns) {
int npcId = npcSpawn.getNpcId();
int nameId = npcSpawn.getNameId();
float spawnY = npcSpawn.getSpawnY();
float spawnX = npcSpawn.getSpawnX();
int planeZ = npcSpawn.getSpawnPlane();
float rotation = npcSpawn.getRotation();
float speed = npcSpawn.getSpeed();
int level = npcSpawn.getLevel();
int profession = npcSpawn.getProfession();
// retrieve the NPC
NpcMasterData npcMasterData = getNpcMasterDataByNpcId(npcId);
// FIXME: what if no NPCs are found??
int npcFileId = npcMasterData.getNpcFileId();
byte[] modelHash = npcMasterData.getModelHash();
byte[] appearance = npcMasterData.getAppearance();
int scale = npcMasterData.getScale();
int professionFlags = npcMasterData.getProfessionFlags();
// String selectNpcNamesSql = "select * from npcs_names where nameID=" + nameId;
// CachedRowSet resultingNpcNames = db.executeSelect(selectNpcNamesSql);
// FIXME: wtf? there probably aren't any NPCs that why we don't get into this loop
byte[] nameHash = null;// readBlob(resultingNpc.getBlob("nameHash"));
// FIXME: what if no NPC names are found??
// resultingNpcNames.next();
NpcData npcData = new NpcData();
npcData.setAgentID(mapData.getAgentIDs().borrowId());
npcData.setLocalID(npcId);
npcData.setNpcFileID(npcFileId);
npcData.setModelHash(modelHash);
npcData.setAppearance(appearance);
// TODO: verify encoding here
npcData.setName(new String(nameHash));
npcData.setScale(scale);
npcData.setNpcFlags(professionFlags);
npcData.setPosition(new GWVector(spawnX, spawnY, planeZ));
npcData.setRotation(rotation);
npcData.setIsRotating(false);
npcData.setSpeed(speed);
npcData.setLevel(level);
npcData.setProfessionPrimary((byte) profession);
npcData.setEnergy(100);
npcData.setEnergyRegen(0.3f);
npcData.setHealth(100);
npcData.setHealthRegen(0.3f);
npcData.setDirection(new GWVector(0, 0, 0));
npcData.setMorale(100);
npcData.setVitalStatus(VitalStatus.ALIVE.getValue());
npcData.setMoveState(MovementState.MOVE_KEEP_DIR);
npcData.setMoveType(MovementType.STOP);
// add NPC data to the map
mapData.addNpc(npcData);
}
mapsData.add(mapData);
}
LOGGER.info("{} game maps were loaded", mapsData.size());
return mapsData;
}
public static Set<be.demmel.jgws.entities.Map> getAllMaps() {
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
TypedQuery<be.demmel.jgws.entities.Map> query = entityManager.createNamedQuery("findAllMaps", be.demmel.jgws.entities.Map.class);
List<be.demmel.jgws.entities.Map> foundCharacters = query.getResultList();
return new HashSet<>(foundCharacters);
} finally {
entityManager.close();
}
}
public static Set<MapSpawn> getMapSpawns(int mapId, boolean outpost, boolean pve) {
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
TypedQuery<MapSpawn> query = entityManager.createNamedQuery("findMapSpawnsByMapIdAndOutpostAndPve", MapSpawn.class);
query.setParameter("mapId", mapId);
query.setParameter("outpost", outpost);
query.setParameter("pve", pve);
List<MapSpawn> foundCharacters = query.getResultList();
return new HashSet<>(foundCharacters);
} finally {
entityManager.close();
}
}
public static Set<NpcSpawn> getNpcSpawns(int mapId, boolean outpost, boolean pve) {
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
TypedQuery<NpcSpawn> query = entityManager.createNamedQuery("findNpcSpawnsByMapIdAndOutpostAndPve", NpcSpawn.class);
query.setParameter("mapId", mapId);
query.setParameter("outpost", outpost);
query.setParameter("pve", pve);
List<NpcSpawn> foundCharacters = query.getResultList();
return new HashSet<>(foundCharacters);
} finally {
entityManager.close();
}
}
public static NpcMasterData getNpcMasterDataByNpcId(int npcId) {
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
TypedQuery<NpcMasterData> query = entityManager.createNamedQuery("findNpcMasterDataByNpcId", NpcMasterData.class);
query.setParameter("npcId", npcId);
try {
return query.getSingleResult();
} catch (NoResultException nre) {
LOGGER.debug("No NpcMasterData found using the given npcId");
return null;
}
} finally {
entityManager.close();
}
}
// TODO: generic code in the core!
private static <T> T validateAndUnmarshal(Class<T> docClass, File xmlFile, String schemaLocation) throws JAXBException, SAXException {
String packageName = docClass.getPackage().getName();
JAXBContext jc = JAXBContext.newInstance(packageName);
Unmarshaller u = jc.createUnmarshaller();
SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
URL url = GameServer.class.getResource(schemaLocation);
Schema mySchema = sf.newSchema(url);
u.setSchema(mySchema);
return (T) JAXBIntrospector.getValue(u.unmarshal(xmlFile));
}
private static GameserverConfiguration getGameServerConfiguration() throws JAXBException, SAXException {
try {
String configurationFileLocation = System.getProperty("configurationFile");
if (configurationFileLocation == null) {
LOGGER.error("The \"configurationFile\" system property must be set");
throw new RuntimeException("The \"configurationFile\" system property must be set");
}
File configurationFile = new File(configurationFileLocation);
if (!configurationFile.exists()) {
LOGGER.error("The given \"configurationFile\" system property point to a non-existing file");
throw new RuntimeException("The given \"configurationFile\" system property points to a non-existing file");
}
return validateAndUnmarshal(GameserverConfiguration.class, configurationFile, "/gameserverConfiguration.xsd");
} catch (Throwable t) {
throw t;
}
}
}