package net.sf.colossus.server;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Document;
import javax.swing.text.StyledDocument;
import net.sf.colossus.common.Constants;
import net.sf.colossus.util.ErrorUtils;
import net.sf.colossus.util.ObjectCreationException;
import net.sf.colossus.util.StaticResourceLoader;
import net.sf.colossus.variant.AllCreatureType;
import net.sf.colossus.variant.CreatureType;
import net.sf.colossus.variant.IHintOracle;
import net.sf.colossus.variant.IOracleLegion;
import net.sf.colossus.variant.IVariantHint;
import net.sf.colossus.variant.IVariantInitializer;
import net.sf.colossus.variant.MasterBoard;
import net.sf.colossus.variant.MasterBoardTerrain;
import net.sf.colossus.variant.MasterHex;
import net.sf.colossus.variant.Variant;
import net.sf.colossus.xmlparser.CreatureLoader;
import net.sf.colossus.xmlparser.MainVarFileLoader;
import net.sf.colossus.xmlparser.StrategicMapLoader;
import net.sf.colossus.xmlparser.TerrainRecruitLoader;
import net.sf.colossus.xmlparser.TerrainRecruitLoader.NullTerrainRecruitLoader;
/**
* Class VariantSupport hold the members and functions
* required to support Variants in Colossus
*
* TODO this should probably move into the variant package sooner or later, possibly
* into the {@link Variant} class itself
*
* @author Romain Dolbeau
*/
public final class VariantSupport
{
private static final Logger LOGGER = Logger.getLogger(VariantSupport.class
.getName());
private static String varDirectory = "";
private static String varFilename = "";
private static String variantName = "";
private static String mapName = "";
private static String recruitsFileName = "";
private static String hintName = "";
private static List<String> lCreaturesName;
private static Document varREADME = null;
private static List<String> dependUpon = null;
/** whether or not there is currently a valid variant loaded.
* TODO: perhaps superfluous - check CURRENT_VARIANT for null
* instead?
*/
private static boolean loadedVariant = false;
private static Variant CURRENT_VARIANT;
private static int maxPlayers;
private static IVariantHint aihl = null;
private static Properties markerNames;
/**
* Remove all variant data, so that next variant loading attempt
* is guaranteed to load it freshly (e.g. to get XML data from
* remote server even if currently loaded was same name, but, well,
* from local files).
*/
public static void unloadVariant()
{
StaticResourceLoader.purgeImageCache();
StaticResourceLoader.purgeFileCache();
CURRENT_VARIANT = null;
loadedVariant = false;
}
private static Map<String, String> rememberCustomDirs = new HashMap<String, String>();
public static void rememberFullPathFileForVariantName(String varName,
String varFullPathFilename)
{
rememberCustomDirs.put(varName, varFullPathFilename);
}
public static String getFullPathFileForVariantName(String varName)
{
return rememberCustomDirs.get(varName);
}
/**
* Load a Colossus Variant by name.
* @param variantName The name of the variant.
* @param serverSide We're loading on a server.
* @return The loaded variant.
*/
public static Variant loadVariantByName(String variantName,
boolean serverSide)
{
// if it's a variant earlier loaded with Load Extern Variant, find out the
// directory path for it:
String fullPathFileName = getFullPathFileForVariantName(variantName);
String variantDir;
if (fullPathFileName == null)
{
// not remembered => a built-in variant
variantDir = Constants.varPath + variantName;
}
else
{
// remembered => conclude full path of directory:
File fullPathFile = new File(fullPathFileName);
variantDir = fullPathFile.getParentFile().getAbsolutePath();
}
String variantFileName = variantName + Constants.varEnd;
Variant loadedVariant = loadVariant(variantName, variantFileName,
variantDir, serverSide);
return loadedVariant;
}
/**
* Load a Colossus Variant from the specified File
* @param varFile The File to load as a Variant, probably selected
* by user in a FileSelectionDialog, with full absolute path.
* @param serverSide We're loading on a server.
* @return The loaded variant.
*/
public static Variant loadVariantByFile(File varFile, boolean serverSide)
{
String tempVarFilename = varFile.getName();
String tempVarDirectory = varFile.getParentFile().getAbsolutePath();
String tempVarName = null;
try
{
tempVarName = getVariantNameFromFilename(tempVarFilename);
}
catch (Exception e)
{
LOGGER.severe("IllegalVariantFileName - unable to conclude "
+ "variant name from filename '" + tempVarFilename + "'!");
return null;
}
// caller need to store that to options so that later a external
// variant (where re-selected in combo box) can be loaded again:
String tempFullPathFilename = varFile.getAbsolutePath();
rememberFullPathFileForVariantName(tempVarName, tempFullPathFilename);
Variant loadedVariant = loadVariant(tempVarName, tempVarFilename,
tempVarDirectory, serverSide);
return loadedVariant;
}
private static String getVariantNameFromFilename(String varFilename)
throws Exception
{
String result = null;
if (varFilename.endsWith(Constants.varEnd))
{
// We need the Variantname for loading a game with
// remote players.
result = varFilename.substring(0, varFilename.length()
- Constants.varEnd.length());
}
else
{
/* Seems the filename is not <variantname>VAR.xml
* - then we cannot conclude the name.
* TODO every loading of a variant should get the
* variant name as argument and also set the
* variantName variable from that, and saving a game
* it should store a 3rd property to hold the variant
* name (not just file and dir).
*/
throw (new Exception("IllegalVariantFilenameException"));
}
return result;
}
/**
* Try to load a Colossus Variant from the specified filename
* in the specified path. If loading fails, inform user with a message
* dialog and try to load Default variant instead. If that fails as well,
* do a system.exit after another message dialog.
*
* Synchronized to avoid concurrent threads running into it at same
* time (probably not possible so far, but if one day Public Server game
* can run with one local human and several AIs (on user's computer)
* this would become an issue.
*
* @param tempVarFilename The name of the file holding the Variant definition.
* @param tempVarDirectory The path to the directory holding the Variant.
* @param tempVariantName The actual plain name of the variant
* @param serverSide We're loading on a server.
* @return A variant object, perhaps newly created, perhaps re-used if same
* variant was used before.
*
* TODO right now variant name might sometimes be null, then we try a hack
* to retrieve the variant name from the variant file name.
*/
public static synchronized Variant loadVariant(String tempVariantName,
String tempVarFilename, String tempVarDirectory, boolean serverSide)
{
if (tempVariantName == null)
{
LOGGER.severe("variantName must not be null!");
Thread.dumpStack();
return null;
}
Variant loadedVariant = null;
try
{
loadedVariant = tryLoadVariant(tempVariantName, tempVarFilename,
tempVarDirectory, serverSide);
}
catch (VariantLoadException vle)
{
String task = vle.getMessage();
String message = "Trying to load variant '" + tempVariantName
+ "' failed " + "(task='" + task + "')."
+ "\nI will try to load variant 'Default' instead...";
String title = "Variant loading failed!";
LOGGER.warning(message);
ErrorUtils.showExceptionDialog(null, message, title, false);
try
{
loadedVariant = tryLoadVariant(Constants.defaultVarName,
Constants.defaultVARFile, Constants.defaultDirName,
serverSide);
}
catch (VariantLoadException vle2)
{
String task2 = vle2.getMessage();
String message2 = "Trying to load Variant 'Default' failed "
+ "as well (task='" + task2 + "').\nCaught exception: "
+ vle.getCause().toString()
+ "\n\nGiving up and exiting the application! ";
String title2 = "Even loading of default variant failed!";
LOGGER.severe(message2);
ErrorUtils.showExceptionDialog(null, message2, title2, true);
System.exit(1);
}
}
return loadedVariant;
}
/**
* This does the actual work for
* {@link #loadVariant(String, String, String, boolean)}
* This here is private and should be called only from the synchronized
* before-mentioned method.
*
* @param tempVariantName
* @param tempVarFilename
* @param tempVarDirectory
* @param serverSide
* @return A variant object, perhaps newly created,
* perhaps re-used if same variant was used before.
*/
private static Variant tryLoadVariant(String tempVariantName,
String tempVarFilename, String tempVarDirectory, boolean serverSide)
throws VariantLoadException
{
if (loadedVariant && varFilename.equals(tempVarFilename)
&& varDirectory.equals(tempVarDirectory))
{
LOGGER.info("Same variant " + tempVariantName
+ ", returning just same again.");
return CURRENT_VARIANT;
}
LOGGER.info("Loading variant " + tempVariantName + " freshly...");
// As long as this is static, only server may do this, not the
// local clients.
// TODO What about the remote clients? Shouldn't they do it too?
if (serverSide)
{
StaticResourceLoader.purgeImageCache();
StaticResourceLoader.purgeFileCache();
}
loadedVariant = false;
String task = "<nothing yet";
LOGGER.log(Level.FINEST, "Loading variant file " + tempVarFilename
+ ", data files in " + tempVarDirectory);
try
{
/* Can't use getVarDirectoriesList yet ! */
List<String> directories = new ArrayList<String>();
directories.add(tempVarDirectory);
directories.add(Constants.defaultDirName);
task = "Load variant file \"" + tempVarFilename + "\"";
InputStream varIS = StaticResourceLoader.getInputStream(
tempVarFilename, directories);
if (varIS == null)
{
throw new FileNotFoundException(tempVarFilename);
}
else
{
MainVarFileLoader mvfLoader = new MainVarFileLoader(varIS);
if (mvfLoader.getMaxPlayers() > 0)
{
maxPlayers = mvfLoader.getMaxPlayers();
}
else
{
maxPlayers = Constants.DEFAULT_MAX_PLAYERS;
}
if (maxPlayers > Constants.MAX_MAX_PLAYERS)
{
LOGGER.log(Level.SEVERE, "Can't use more than "
+ Constants.MAX_MAX_PLAYERS
+ " players, while variant requires " + maxPlayers);
maxPlayers = Constants.MAX_MAX_PLAYERS;
}
varDirectory = tempVarDirectory;
varFilename = tempVarFilename;
variantName = tempVariantName;
mapName = mvfLoader.getMap();
if (mapName == null)
{
mapName = Constants.defaultMAPFile;
}
LOGGER.log(Level.FINEST, "Variant using MAP " + mapName);
lCreaturesName = mvfLoader.getCre();
for (String creaturesName : lCreaturesName)
{
LOGGER.log(Level.FINEST, "Variant using CRE "
+ creaturesName);
}
recruitsFileName = mvfLoader.getTer();
if (recruitsFileName == null)
{
recruitsFileName = Constants.defaultTERFile;
}
LOGGER.log(Level.FINEST, "Variant using TER "
+ recruitsFileName);
hintName = mvfLoader.getHintName();
LOGGER.log(Level.FINEST, "Variant using hint " + hintName);
dependUpon = mvfLoader.getDepends();
LOGGER.log(Level.FINEST, "Variant depending upon "
+ dependUpon);
}
directories = new ArrayList<String>();
directories.add(tempVarDirectory);
task = "getDocument README*";
varREADME = StaticResourceLoader
.getDocument("README", directories);
/* OK, what is the proper order here ?
* We should start with HazardTerrain & HazardHexside, but those
* aren't in variant yet. They don't require anything else.
* Then must comes the CreatureType. They are only natives to
* HazardTerrain & HazardHexside, and don't need anything else.
* Then we can load the terrains & recruits ; they need the
* CreatureType.
* Finally we can load the Battlelands, they need the terrain.
*/
AllCreatureType creatureTypes = loadCreatures();
IVariantInitializer trl = loadTerrainsAndRecruits(creatureTypes);
// TODO add things as the variant package gets fleshed out
List<String> directoriesForMap = getVarDirectoriesList();
InputStream mapIS = StaticResourceLoader.getInputStream(
VariantSupport.getMapName(), directoriesForMap);
if (mapIS == null)
{
throw new FileNotFoundException(VariantSupport.getMapName());
}
StrategicMapLoader sml = new StrategicMapLoader(mapIS);
MasterBoard masterBoard = new MasterBoard(sml.getHorizSize(),
sml.getVertSize(), sml.getShow(), sml.getHexes());
// varREADME seems to be used as flag for a successfully loaded
// variant, but breaking the whole variant loading just because
// there is no readme file seems a bit overkill, thus we set
// a default in this case
if (varREADME == null)
{
varREADME = getMissingReadmeNotification();
}
CURRENT_VARIANT = new Variant(trl, creatureTypes, masterBoard,
varREADME, variantName);
loadedVariant = true;
loadHints(CURRENT_VARIANT);
task = "loadMarkerNamesProperties";
markerNames = loadMarkerNamesProperties();
}
catch (Exception e)
{
VariantLoadException vle = new VariantLoadException(task, e);
CURRENT_VARIANT = null;
throw vle;
}
return CURRENT_VARIANT;
}
/**
* A helper class to store the exception that happened during
* VariantLoading together with the task during which that happened.
*/
private static class VariantLoadException extends Exception
{
public VariantLoadException(String message, Throwable e)
{
super(message, e);
}
}
/** Call immediately after loading variant, before using creatures. */
public static AllCreatureType loadCreatures()
{
CreatureLoader creatureLoader = new CreatureLoader();
try
{
List<String> directories = VariantSupport.getVarDirectoriesList();
for (String creaturesName : VariantSupport.getCreaturesNames())
{
InputStream creIS = StaticResourceLoader.getInputStream(
creaturesName, directories);
if (creIS == null)
{
throw new FileNotFoundException(creaturesName);
}
creatureLoader.fillCreatureLoader(creIS, directories);
}
}
catch (Exception e)
{
throw new RuntimeException("Failed to load Creatures definition",
e);
}
return creatureLoader;
}
private static Document getMissingReadmeNotification()
{
StyledDocument txtdoc = new DefaultStyledDocument();
try
{
txtdoc
.insertString(
0,
"No README found -- variant is lacking a README.txt or README.html.",
null);
}
catch (BadLocationException e)
{
// really shouldn't happen with the constant offset
LOGGER
.log(
Level.WARNING,
"Failed to insert warning about missing readme into Document object",
e);
}
txtdoc
.putProperty(StaticResourceLoader.KEY_CONTENT_TYPE, "text/plain");
return txtdoc;
}
public static String getVarDirectory()
{
return varDirectory;
}
public static String getVarFilename()
{
return varFilename;
}
public static String getVariantName()
{
return variantName;
}
public static String getMapName()
{
return mapName;
}
public static List<String> getCreaturesNames()
{
return lCreaturesName;
}
public static List<String> getVarDirectoriesList()
{
List<String> directories = new ArrayList<String>();
if (!(varDirectory.equals(Constants.defaultDirName)))
{
directories.add(varDirectory);
}
Iterator<String> it = dependUpon.iterator();
while (it.hasNext())
{
directories.add(it.next());
}
directories.add(Constants.defaultDirName);
return directories;
}
public static List<String> getVarDirectoriesList(String suffixPath)
{
List<String> directories = getVarDirectoriesList();
List<String> suffixedDirs = new ArrayList<String>();
Iterator<String> it = directories.iterator();
while (it.hasNext())
{
String dir = it.next();
suffixedDirs.add(dir + StaticResourceLoader.getPathSeparator()
+ suffixPath);
}
return suffixedDirs;
}
public static List<String> getImagesDirectoriesList()
{
return getVarDirectoriesList(Constants.imagesDirName);
}
public static List<String> getBattlelandsDirectoriesList()
{
return getVarDirectoriesList(Constants.battlelandsDirName);
}
public synchronized static IVariantInitializer loadTerrainsAndRecruits(
AllCreatureType creatureTypes)
{
// remove all old stuff in the custom recruitments system
CustomRecruitBase.reset();
IVariantInitializer terrainRecruitLoader = new NullTerrainRecruitLoader(
true);
try
{
List<String> directories = getVarDirectoriesList();
InputStream terIS = StaticResourceLoader.getInputStream(
recruitsFileName, directories);
if (terIS == null)
{
throw new FileNotFoundException(recruitsFileName);
}
// TODO parsing into static fields is a side effect of this
// constructor - that's somehow not the right way...
// Clemens: started working on that.
// => partly now done via the IVariantInitializer
terrainRecruitLoader = new TerrainRecruitLoader(terIS,
creatureTypes);
}
catch (Exception e)
{
// TODO another exception anti-pattern: calling System.exit which means
// no one can escape the disappearing VM, even if they would know how
LOGGER.log(Level.SEVERE, "Recruit-per-terrain loading failed.", e);
System.exit(1);
}
return terrainRecruitLoader;
}
private static Properties loadMarkerNamesProperties()
{
Properties allNames = new Properties();
List<String> directories = getVarDirectoriesList();
/* unlike other, don't use file-level granularity ;
load all files in order, so that we get the
default mapping at the end */
ListIterator<String> it = directories.listIterator(directories.size());
boolean foundOne = false;
while (it.hasPrevious())
{
List<String> singleDirectory = new ArrayList<String>();
singleDirectory.add(it.previous());
try
{
InputStream mmfIS = StaticResourceLoader
.getInputStreamIgnoreFail(Constants.markersNameFile,
singleDirectory);
if (mmfIS != null)
{
allNames.load(mmfIS);
foundOne = true;
}
}
catch (Exception e)
{
LOGGER.log(Level.WARNING,
"Markers name loading partially failed.");
}
}
if (!foundOne)
{
LOGGER.log(Level.WARNING, "No file " + Constants.markersNameFile
+ " found anywhere in directories " + directories.toString());
}
return allNames;
}
public static Properties getMarkerNamesProperties()
{
return markerNames;
}
private synchronized static void loadHints(Variant variant)
{
aihl = null;
Object o = null;
if (hintName != null)
{
try
{
o = StaticResourceLoader.getNewObject(hintName,
getVarDirectoriesList(), new Object[] { variant });
}
catch (ObjectCreationException e)
{
// ignore here, the o == null case is covered below
}
}
if ((o != null) && (o instanceof IVariantHint))
{
aihl = (IVariantHint)o;
LOGGER.log(Level.FINEST, "Using class " + hintName
+ " to supply hints to the AIs.");
}
else
{
if (hintName.equals(Constants.defaultHINTFile))
{
LOGGER.log(Level.SEVERE, "Couldn't load default hints !");
System.exit(1);
}
else
{
LOGGER.log(Level.WARNING,
"Couldn't load hints. Trying with Default.");
hintName = Constants.defaultHINTFile;
loadHints(variant);
}
}
}
public synchronized static CreatureType getRecruitHint(
MasterBoardTerrain terrain, IOracleLegion legion,
List<CreatureType> recruits, IHintOracle oracle)
{
return getRecruitHint(terrain, legion, recruits, oracle,
Collections.singletonList(IVariantHint.AIStyle.Any));
}
public synchronized static CreatureType getRecruitHint(
MasterBoardTerrain terrain, IOracleLegion legion,
List<CreatureType> recruits, IHintOracle oracle,
List<IVariantHint.AIStyle> aiStyles)
{
assert aihl != null : "No AIHintLoader available";
return aihl
.getRecruitHint(terrain, legion, recruits, oracle, aiStyles);
}
public synchronized static List<CreatureType> getInitialSplitHint(
MasterHex hex)
{
return getInitialSplitHint(hex,
Collections.singletonList(IVariantHint.AIStyle.Any));
}
public synchronized static List<CreatureType> getInitialSplitHint(
MasterHex hex, List<IVariantHint.AIStyle> aiStyles)
{
if (aihl != null)
{
return aihl.getInitialSplitHint(hex, aiStyles);
}
return null;
}
public synchronized static int getHintedRecruitmentValueOffset(
CreatureType creature)
{
return getHintedRecruitmentValueOffset(creature,
Collections.singletonList(IVariantHint.AIStyle.Any));
}
public synchronized static int getHintedRecruitmentValueOffset(
CreatureType creature, List<IVariantHint.AIStyle> aiStyles)
{
return aihl.getHintedRecruitmentValueOffset(creature, aiStyles);
}
/** get maximum number of players in that variant */
public static int getMaxPlayers()
{
return maxPlayers;
}
/**
* Retrieves the currently loaded variant.
*
* TODO this is a helper method to introduce the Variant objects into the code,
* in the long run they should be passed around instead of being in a static
* member here.
*/
public static Variant getCurrentVariant()
{
return CURRENT_VARIANT;
}
}