package nl.tudelft.bw4t.server.environment;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.rmi.RemoteException;
import java.rmi.server.ServerNotActiveException;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.bind.JAXBException;
import org.apache.log4j.Logger;
import eis.eis2java.environment.AbstractEnvironment;
import eis.exceptions.ActException;
import eis.exceptions.AgentException;
import eis.exceptions.EntityException;
import eis.exceptions.ManagementException;
import eis.exceptions.NoEnvironmentException;
import eis.exceptions.PerceiveException;
import eis.exceptions.RelationException;
import eis.iilang.Action;
import eis.iilang.EnvironmentState;
import eis.iilang.Identifier;
import eis.iilang.Parameter;
import eis.iilang.Percept;
import nl.tudelft.bw4t.eis.MapParameter;
import nl.tudelft.bw4t.map.Entity;
import nl.tudelft.bw4t.map.NewMap;
import nl.tudelft.bw4t.network.BW4TClientActions;
import nl.tudelft.bw4t.scenariogui.BotConfig;
import nl.tudelft.bw4t.scenariogui.EPartnerConfig;
import nl.tudelft.bw4t.server.BW4TServer;
import nl.tudelft.bw4t.server.eis.EPartnerEntity;
import nl.tudelft.bw4t.server.eis.EntityInterface;
import nl.tudelft.bw4t.server.eis.RobotEntity;
import nl.tudelft.bw4t.server.logging.BW4TFileAppender;
import nl.tudelft.bw4t.server.logging.BotLog;
import nl.tudelft.bw4t.server.model.BW4TServerMap;
import nl.tudelft.bw4t.server.model.BW4TServerMapListerner;
import nl.tudelft.bw4t.server.model.epartners.EPartner;
import nl.tudelft.bw4t.server.model.robots.EntityFactory;
import nl.tudelft.bw4t.server.model.robots.handicap.IRobot;
import nl.tudelft.bw4t.server.repast.BW4TBuilder;
import nl.tudelft.bw4t.server.repast.MapLoader;
import nl.tudelft.bw4t.server.util.MapUtils;
import nl.tudelft.bw4t.server.view.ServerContextDisplay;
import repast.simphony.context.Context;
import repast.simphony.scenario.ScenarioLoadException;
import repast.simphony.space.continuous.NdPoint;
/**
* The central environment which runs the data model and performs actions
* received from remote environments through the server. Remote environments
* also poll percepts from this environment. Remote environments are notified of
* entity and environment events also using the server.
*
* This is a singleton. Needed because we store the map info here.
*/
public class BW4TEnvironment extends AbstractEnvironment {
public static final String VERSION = "@PROJECT_VERSION@";
private static final String ENTITY_NAME_FORMAT = "%s_%d";
private static final long serialVersionUID = -279637264069930353L;
private static BW4TEnvironment instance;
/**
* The log4j logger, logs to the console and file
*/
private static final Logger LOGGER = Logger.getLogger(BW4TEnvironment.class);
private String mapName;
/**
* start time of the first action.
*/
private static long starttime = 0;
private BW4TServer server;
private boolean mapFullyLoaded;
private Stepper stepper;
private final String scenarioLocation;
private BW4TServerMap serverMap;
private ServerContextDisplay contextDisplay;
private final boolean guiEnabled;
private final String shutdownKey;
private boolean collisionEnabled;
private boolean drawPathsEnabled;
private int nextBotSpawnIndex = 0;
private MapLoader mapLoader;
private List<PropertyChangeListener> listeners = new LinkedList<>();
/**
* A map of <agent-client> pairs. Every entity that we have can be claimed
* by a server. If that server disappears, we have to free the agents and
* entities associated with that server.
*/
private Map<String, BW4TClientActions> agentLocations = new HashMap<>();
/**
* Create a new instance of this environment
*
* @param scenarioLocation
* the location of the scenario that should be loaded in Repast
* @param mapLocation
* the location of the map file
* @throws IOException
* @throws ManagementException
* @throws ScenarioLoadException
* @throws JAXBException
*/
BW4TEnvironment(BW4TServer server2, String scenarioLocation, String mapLocation, boolean guiEnabled,
String shutdownKey, boolean collisionEnabled, boolean drawPathsEnabled)
throws IOException, ManagementException, ScenarioLoadException, JAXBException {
super();
setInstance(this);
this.server = server2;
mapName = mapLocation;
this.scenarioLocation = System.getProperty("user.dir") + "/" + scenarioLocation;
this.guiEnabled = guiEnabled;
this.shutdownKey = shutdownKey;
this.collisionEnabled = collisionEnabled;
this.drawPathsEnabled = drawPathsEnabled;
}
/**
* Subscribe to hear changes in the setup of the server.
*
* @param listener
*/
public void addChangeListener(PropertyChangeListener listener) {
if (!listeners.isEmpty()) {
System.out.println("WARNING already having listeners");
}
listeners.add(listener);
}
/**
* notify our listeners that somehting changed in our settings. Eg, gui is
* now enabled, collisions now disabled, etc. This is the easy way,
* returning always a null property change object. If necessary we may
* contain more details in the change message.
*/
private void notifyChange() {
for (PropertyChangeListener listener : listeners) {
try {
listener.propertyChange(null);
} catch (Exception e) {
LOGGER.error("callback to listener " + listener + " failed", e);
}
}
}
/**
* Notify listeners that a new entity is available, server handles correct
* distribution of entities to listeners
*
* @param entity
* the new entity
*/
@Override
public void notifyNewEntity(String entity) {
server.notifyNewEntity(entity);
}
@Override
public void notifyDeletedEntity(String entity, Collection<String> agents) {
server.notifyDeletedEntity(entity, agents);
}
@Override
public void setState(EnvironmentState newstate) throws ManagementException {
super.setState(newstate);
LOGGER.info("Environment now in state: " + newstate.name());
server.notifyStateChange(getState());
}
/**
* takes down all entities and agents. Stops repast. Leaves the server open.
* Tries to take down in any case, just reports errors and proceeds.
*/
public void removeAllEntities() throws ManagementException {
BW4TFileAppender.logFinish(System.currentTimeMillis(), "total time is ");
setState(EnvironmentState.KILLED);
LOGGER.debug("Removing all entities");
for (String entity : this.getEntities()) {
try {
this.deleteEntity(entity);
} catch (EntityException | RelationException e) {
LOGGER.error("Failure to delete entity: " + entity, e);
}
}
LOGGER.debug("Remove all (remaining) agents");
for (String agent : this.getAgents()) {
try {
this.unregisterAgent(agent);
} catch (AgentException e) {
LOGGER.error("Failure to unregister agent: " + agent, e);
}
}
mapFullyLoaded = false;
nextBotSpawnIndex = 0;
}
@Override
public void pause() throws ManagementException {
setState(EnvironmentState.PAUSED);
}
@Override
public void start() throws ManagementException {
setState(EnvironmentState.RUNNING);
}
/**
* initialize Repast with a different map. Does not reset the
* {@link BW4TServer}.
*
* @param parameters
* like the
* @throws ManagementException
*/
@Override
public void init(Map<String, Parameter> parameters) throws ManagementException {
final Parameter param = parameters.get("map");
String mapname = prepareMapParameter(param);
if (mapname == null) {
LOGGER.info("No changed parameters where found, not restarting the environment.");
return;
}
setMapName(mapname);
reset(false);
try {
while (!isMapFullyLoaded()) {
Thread.sleep(50);
}
} catch (InterruptedException e) {
LOGGER.warn("Waiting until the map is loaded interrupted", e);
}
}
/**
* Interpret the given parameter and make sure we use it properly. If the
* {@link Parameter} is a {@link MapParameter} the system stores the map
* locally and returns the new filename.
*
* @param param
* the param describing the map setting
* @return the filename to be opened for the map
*
*/
private String prepareMapParameter(Parameter param) {
String mapname = null;
if (param != null) {
if (param instanceof MapParameter) {
final MapParameter mParam = (MapParameter) param;
final NewMap map = mParam.getMap();
final NewMap servermap = getServerMap().getMap();
if (servermap == null || !servermap.equals(map)) {
mapname = map.hashCode() + ".map";
try {
NewMap.toXML(map, new FileOutputStream(getFullMapPath(mapname)));
LOGGER.info("Successfully stored the map transfered from the server at: maps/" + mapname);
} catch (FileNotFoundException | JAXBException e) {
LOGGER.error("failed to save the map received from the client", e);
mapname = null;
}
}
} else if (param instanceof Identifier) {
mapname = ((Identifier) param).getValue();
}
}
return mapname;
}
/**
* Launch server and start repast.
*
* @throws IOException
* @throws ManagementException
* @throws ScenarioLoadException
* @throws JAXBException
*/
void launchAll() throws IOException, ManagementException, ScenarioLoadException, JAXBException {
launchServer();
setState(EnvironmentState.RUNNING);
launchRepast();
}
/**
* Launch the server
*
* @throws RemoteException
* @throws ManagementException
* @throws MalformedURLException
*/
private void launchServer() throws RemoteException, ManagementException, MalformedURLException {
if (server == null) {
server = Launcher.getInstance().setupRemoteServer();
}
setState(EnvironmentState.INITIALIZING);
LOGGER.info("BW4T Server has been bound.");
}
/**
* Launches the Repast framework and GUI. Does not return until there is an
* exception or getState()==KILLED. After stopping, runner is set back to
* null.
*
* @throws IOException
* @throws ScenarioLoadException
* @throws JAXBException
*/
private void launchRepast() throws IOException, ScenarioLoadException, JAXBException {
NewMap theMap = NewMap.create(new FileInputStream(new File(getFullMapPath(this.getMapName()))));
serverMap = new BW4TServerMap(theMap);
serverMap.attachChangeListener(getMapLoader());
Launcher.getInstance().getEntityFactory().setServerMap(serverMap);
stepper = new Stepper(scenarioLocation, this);
new Thread(stepper).start();
}
/**
* Get the instance of this environment
*
* @return the instance
*/
public static BW4TEnvironment getInstance() {
if (instance == null) {
throw new IllegalStateException("BW4TEnvironment has not been initialized");
}
return instance;
}
private static void setInstance(BW4TEnvironment env) {
instance = env;
}
/**
* Get the currently active map loader, or make a default one if none are
* present.
*
* @return the map loader
*/
public BW4TServerMapListerner getMapLoader() {
if (mapLoader == null) {
mapLoader = new MapLoader();
}
return mapLoader;
}
public void setMapLoader(MapLoader loader) {
mapLoader = loader;
}
public String getMapName() {
return mapName;
}
/**
* Get the actual path for the given map name.
*
* @param name
* the name of the map
* @return the path to the actual file
*/
public static String getFullMapPath(String name) {
return System.getProperty("user.dir") + "/maps/" + name;
}
/**
* Set the map name and reset the loaded state of the map.
*
* @param mapName
*/
public void setMapName(String mapName) {
this.mapName = mapName;
this.mapFullyLoaded = false;
notifyChange();
}
public BW4TServerMap getServerMap() {
return serverMap;
}
/**
* Check whether an action is supported by this environment.
*
* @param arg0
* the action that should be checked
* @return true if there is an entity, a dropzone and sequence not yet
* complete
*/
@Override
public boolean isSupportedByEnvironment(Action arg0) {
return !getEntities().isEmpty();
}
/**
* Check whether an action is supported by an entity type, always returns
* true for now
*
* @param arg0
* the action that should be checked
* @param arg1
* the type of entity
* @return the result
*/
@Override
protected boolean isSupportedByType(Action arg0, String arg1) {
return true;
}
/**
* Check whether a state transition is valid, for now always returns true
*
* @param oldState
* the old state of the environment
* @param newState
* the new state of the environment
* @return the result
*/
@Override
public boolean isStateTransitionValid(EnvironmentState oldState, EnvironmentState newState) {
return true;
}
/**
* Helper method to allow the server to call actions received from attached
* clients
*
* @param entity
* the entity that should perform the action
* @param action
* the action that should be performed
* @return the percept received after performing the action
* @throws ActException
*/
public Percept performClientAction(String entity, Action action) throws ActException {
Long time = System.currentTimeMillis();
LOGGER.log(BotLog.BOTLOG, String.format("action %s %s", entity, action.toProlog()));
if (starttime == 0) {
starttime = time;
}
return performEntityAction(entity, action);
}
/**
* Helper method to allow the server to get all percepts for a connected
* client.
*
* This function is synchronized to ensure that multiple calls are properly
* sequenced. This is important because getAllPercepts must 'lock' the
* environment and parallel calls would cause overlapping 'locks' taken at
* different moments in time.
*
* Actually, locking the environment is done by copying the current location
* of the entity.
*
* It seems that this new function is created because
* {@link AbstractEnvironment#getAllPerceptsFromEntity(String)} is final.
*
* @param entity
* , the entity for which all percepts should be gotten
* @return all percepts for the entity
*/
public synchronized List<Percept> getAllPerceptsFrom(String entity) {
try {
if (this.isMapFullyLoaded()) {
((EntityInterface) getEntity(entity)).initializePerceptionCycle();
return getAllPerceptsFromEntity(entity);
}
} catch (PerceiveException | NoEnvironmentException e) {
LOGGER.error("failed to get percepts for entity: '" + entity + "'", e);
}
return new LinkedList<>();
}
/**
* Check if the map was fully loaded. When this is true, all entities also
* have been processed and the environment is ready to run. Note that
* because of #2016 there may be an async between Repast and this
* BW4TEnvironment, causing this flag to remain true while repast has in
* fact stopped. We detect this only when the user turns 'on' the Repast
* environment again, in {@link BW4TBuilder#build()}.
*
* @return true or false of the map is loaded
*/
public boolean isMapFullyLoaded() {
return mapFullyLoaded;
}
/**
* check that maploaded is done, so set true
*/
public void setMapFullyLoaded() {
mapFullyLoaded = true;
startGUI();
}
public final boolean isCollisionEnabled() {
return collisionEnabled;
}
/**
* Enable collision detection between bots.
*
* @param state
* True if collision detection has to be enabled.
*/
public void setCollisionEnabled(boolean state) {
if (collisionEnabled != state) {
collisionEnabled = state;
notifyChange();
}
}
public final boolean isDrawPathsEnabled() {
return drawPathsEnabled;
}
public void setDrawPathsEnabled(boolean state) {
if (drawPathsEnabled != state) {
drawPathsEnabled = state;
notifyChange();
}
}
public void setDelay(int delay) {
if (stepper == null) {
return;
}
if (delay != stepper.getDelay()) {
stepper.setDelay(delay);
notifyChange();
}
}
/**
* reset using parameters for initial situation. Does not kill the server.
* Returns after reset is complete.
*
* @param parameters
* only the map parameter is accepted
* @throws ManagementException
*/
@Override
public void reset(Map<String, Parameter> parameters) throws ManagementException {
String mapname = prepareMapParameter(parameters.get("map"));
if (mapname == null) {
setMapName("Random");
} else {
setMapName(mapname);
}
reset(true);
}
/**
* reset to initial situation. Returns after reset is complete
*
* @param resetNetwork
* Should we restart the network server?
*/
public void reset(boolean resetNetwork) throws ManagementException {
setState(EnvironmentState.INITIALIZING);
try {
listeners = new LinkedList<>();
takeDownSimulation();
if (resetNetwork && server != null) {
server.takeDown();
server = null;
}
BW4TFileAppender.resetNewFile();
launchAll();
} catch (ManagementException | IOException | ScenarioLoadException | JAXBException e) {
throw new ManagementException("Failed to reset the environment", e);
}
}
/**
* Take down the simulation: remove all entities, stop the stepper. Stop the
* {@link ServerContextDisplay}.
*
* @throws ManagementException
*/
private void takeDownSimulation() throws ManagementException {
LOGGER.info("Taking down the simulation environment");
// this should set state->KILLED which stops stepper.
removeAllEntities();
stepper.terminate();
if (contextDisplay != null) {
contextDisplay.close();
contextDisplay = null;
}
serverMap.setContext(null);
}
/**
* get the repast current context. May be null if Repast not running now.
*
* @return Repast {@link Context}.
*/
public Context<Object> getContext() {
return serverMap.getContext();
}
/**
* Set a new repast Context. May be null if Repast sstopped running. Called
* from {@link BW4TBuilder} when repast gives us context.
*
* @param c
* the new context
*/
public void startGUI() {
if (guiEnabled) {
LOGGER.info("Launching the BW4T Server Graphical User Interface.");
try {
contextDisplay = new ServerContextDisplay(getServerMap());
} catch (Exception e) {
LOGGER.error("BW4T Server started ok but failed to launch display.", e);
}
} else {
LOGGER.info("Launching the BW4T Server without a graphical user interface.");
}
}
@Override
public void freeEntity(String entity) throws RelationException, EntityException {
((EntityInterface) getEntity(entity)).disconnect();
super.freeEntity(entity);
this.deleteEntity(entity);
}
@Override
public void freePair(String agent, String entity) throws RelationException {
EntityInterface robot = (EntityInterface) getEntity(entity);
robot.disconnect();
try {
super.freePair(agent, entity);
} catch (EntityException e) {
throw new RelationException("can't free pair", e);
}
}
/**
* Stop this BW4TEnvironment completely.
*
* @param key
* the key required to stop the system
*/
public void shutdownServer(String key) {
if (key.equals(this.shutdownKey)) {
LOGGER.info("Server shutdown requested with correct key");
try {
takeDownSimulation();
this.setState(EnvironmentState.KILLED);
} catch (ManagementException e) {
LOGGER.warn("failed to notify clients that the server is going down...", e);
}
server.takeDown();
server = null;
System.exit(0);
} else {
LOGGER.warn("Server shutdown attempted with wrong key: " + key);
}
}
public String getShutdownKey() {
return this.shutdownKey;
}
public NewMap getMap() {
return serverMap.getMap();
}
public long getStarttime() {
return starttime;
}
/**
* Selects a spawn point from the list of entities in the map. Using an
* index that rotates through the number of spawns.
*
* @return the coordinates of the spawn point
*/
private Point2D getNextBotSpawnPoint() {
List<Entity> ents = getMap().getEntities();
if (nextBotSpawnIndex >= ents.size()) {
throw new IllegalStateException("Spawn failed. There are no free entities available. All " + ents.size()
+ " entities are already in use.");
}
Point2D p = ents.get(nextBotSpawnIndex++).getPosition().getPoint2D();
return p;
}
/**
* @return The list of points the humans are right now.
*/
private List<Point2D> getHumanWithoutEPartners() {
List<Point2D> points = new LinkedList<>();
for (Object robot : serverMap.getContext().getObjects(IRobot.class)) {
IRobot robotTemp = (IRobot) robot;
if (robotTemp.isHuman() && !robotTemp.isHoldingEPartner()) {
NdPoint location = robotTemp.getLocation();
points.add(new Point2D.Double(location.getX(), location.getY()));
}
}
return points;
}
/**
* Spawns a new Robot according to the given specifications and notifies the
* given client.
*
* @param bots
* list of bots to spawn
* @param client
* the client to notify
*/
public void spawnBots(List<BotConfig> bots, BW4TClientActions client) {
int skip = 0;
for (BotConfig c : bots) {
int created = 0;
String name = c.getBotName();
while (created < c.getBotAmount()) {
c.setBotName(String.format(ENTITY_NAME_FORMAT, name, created + skip + 1));
try {
if (this.getEntities().contains(c.getBotName())) {
if (!this.getAssociatedAgents(c.getBotName()).isEmpty()) {
skip++;
continue;
}
} else {
spawn(c);
}
// assign robot to client
server.notifyFreeRobot(client, c);
} catch (EntityException e) {
LOGGER.error("Failed to register new Robot in the environment.", e);
}
created++;
}
}
}
/**
* Spawns a new e-Partner according to the given specifications and notifies
* the given client.
*
* @param epartners
* list of epartners to spawn
* @param client
* the client to notify
*/
public void spawnEPartners(List<EPartnerConfig> epartners, BW4TClientActions client) {
List<Point2D> points = getHumanWithoutEPartners();
int index = 0;
for (EPartnerConfig c : epartners) {
int created = 0;
String name = c.getEpartnerName();
while (created < c.getEpartnerAmount()) {
c.setEpartnerName(String.format(ENTITY_NAME_FORMAT, name, created + 1));
try {
// if we run out of humans who don't have an e-Partner.
if (index >= points.size()) {
spawn(c, getNextBotSpawnPoint());
} else {
spawn(c, points.get(index));
}
// assign e-Partner to client
server.notifyFreeEpartner(client, c);
} catch (EntityException e) {
LOGGER.error("Failed to register new Robot in the environment.", e);
}
index++;
created++;
}
}
}
/**
* Spawns a new Robot according to the given specifications and notifies the
* given client.
*
* @param c
* the configuration to use
* @throws EntityException
* when we are unable to register Robot
*/
private void spawn(BotConfig c) throws EntityException {
// acquire spawn point first, as this may fail
Point2D p = getNextBotSpawnPoint();
EntityFactory entityFactory = Launcher.getInstance().getEntityFactory();
// create robot from request
IRobot bot = entityFactory.makeRobot(c);
// create the entity for the environment
RobotEntity be = new RobotEntity(bot);
// register the entity in the environment
this.registerEntity(c.getBotName(), be);
// Place the Robot
bot.moveTo(p.getX(), p.getY());
}
/**
* Spawns a new EPartner according to the given specifications and notifies
* the given client.
*
* @param c
* the configuration to use
* @param point
* the point the e-Partner should spawn at
* @throws EntityException
* when we are unable to register EPartner
*/
private void spawn(EPartnerConfig c, Point2D point) throws EntityException {
EntityFactory entityFactory = Launcher.getInstance().getEntityFactory();
// create robot from request
EPartner epartner = entityFactory.makeEPartner(c);
// create the entity for the environment
EPartnerEntity ee = new EPartnerEntity(epartner);
// register the entity in the environment
this.registerEntity(epartner.getName(), ee);
// Place the EPartner
epartner.moveTo(point.getX(), point.getY());
}
/**
* Get all agents associated with the server that calls us.
*
* @return
* @throws ServerNotActiveException
*/
private Set<String> getAssociatedAgents(BW4TClientActions client) throws ServerNotActiveException {
Set<String> agents = new HashSet<>();
for (String agent : agentLocations.keySet()) {
if (agentLocations.get(agent).equals(client)) {
agents.add(agent);
}
}
return agents;
}
/**
* Frees all agents associated with the client
*
* @param client
* @throws ServerNotActiveException
* @throws EntityException
* @throws RelationException
*/
public void freeClient(BW4TClientActions client)
throws ServerNotActiveException, EntityException, RelationException {
Set<String> agents = getAssociatedAgents(client);
for (String agent : agents) {
freeAgent(agent);
}
}
public void registerAgent(String agent, BW4TClientActions client) throws AgentException {
super.registerAgent(agent);
agentLocations.put(agent, client);
}
@Override
public void unregisterAgent(String agent) throws AgentException {
if (!agentLocations.containsKey(agent)) {
throw new AgentException("agent " + agent + " is not registered");
}
agentLocations.remove(agent);
super.unregisterAgent(agent);
}
@Override
public void freeAgent(String agent) throws RelationException, EntityException {
if (!agentLocations.containsKey(agent)) {
throw new RelationException("agent " + agent + " is not registered");
}
// we first handle the entities, to avoid freeAgent notifying listeners
// which is hard to override
// CHECK why do we delete the entities here, and not just free them?
Set<String> ents;
try {
ents = getAssociatedEntities(agent);
} catch (AgentException e1) {
throw new EntityException("failed to get associated entities of agent", e1);
}
for (String entity : ents) {
deleteEntity(entity);
}
super.freeAgent(agent);
}
public int getDelay() {
if (stepper == null)
return 20;
return (int) stepper.getDelay();
}
/**
* @param client
* @return Get list of all agents associated with given client
*
*/
public Set<String> getClientAgents(BW4TClientActions client) {
return MapUtils.getKeys(agentLocations, client);
}
}