freecol/src/net/sf/freecol/common/model/Specification.java

3322 lines
124 KiB
Java

/**
* Copyright (C) 2002-2022 The FreeCol Team
*
* This file is part of FreeCol.
*
* FreeCol is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* FreeCol 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 General Public License
* along with FreeCol. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.freecol.common.model;
import static net.sf.freecol.common.model.Constants.INFINITY;
import static net.sf.freecol.common.util.CollectionUtils.alwaysTrue;
import static net.sf.freecol.common.util.CollectionUtils.any;
import static net.sf.freecol.common.util.CollectionUtils.find;
import static net.sf.freecol.common.util.CollectionUtils.iterable;
import static net.sf.freecol.common.util.CollectionUtils.matchKey;
import static net.sf.freecol.common.util.CollectionUtils.matchKeyEquals;
import static net.sf.freecol.common.util.CollectionUtils.none;
import static net.sf.freecol.common.util.CollectionUtils.transform;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.xml.stream.XMLStreamException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.sf.freecol.common.FreeColUserMessageException;
import net.sf.freecol.common.i18n.Messages;
import net.sf.freecol.common.io.FreeColDirectories;
import net.sf.freecol.common.io.FreeColModFile;
import net.sf.freecol.common.io.FreeColRules;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import net.sf.freecol.common.model.NationOptions.Advantages;
import net.sf.freecol.common.option.AbstractOption;
import net.sf.freecol.common.option.AbstractUnitOption;
import net.sf.freecol.common.option.BooleanOption;
import net.sf.freecol.common.option.GameOptions;
import net.sf.freecol.common.option.IntegerOption;
import net.sf.freecol.common.option.MapGeneratorOptions;
import net.sf.freecol.common.option.Option;
import net.sf.freecol.common.option.OptionContainer;
import net.sf.freecol.common.option.OptionGroup;
import net.sf.freecol.common.option.PercentageOption;
import net.sf.freecol.common.option.TextOption;
import net.sf.freecol.common.option.UnitListOption;
import net.sf.freecol.common.util.Introspector;
import net.sf.freecol.common.util.LogBuilder;
/**
* This class encapsulates any parts of the "specification" for
* FreeCol that are expressed best using XML. The XML is loaded
* through the class loader from the resource named
* "specification.xml" in the same package as this class.
*/
public final class Specification implements OptionContainer {
private static final Logger logger = Logger.getLogger(Specification.class.getName());
/** Fixed class array argument used in newType(). */
private static final Class[] newTypeClasses
= new Class[] { String.class, Specification.class };
// Special reader classes for spec objects
private interface ChildReader {
void readChildren(FreeColXMLReader xr) throws XMLStreamException;
}
/**
* Modifiers have to be hooked to the specification and added to
* the special modifiers list, hence the special purpose reader.
*/
private class ModifierReader implements ChildReader {
/**
* {@inheritDoc}
*/
@Override
public void readChildren(FreeColXMLReader xr)
throws XMLStreamException {
while (xr.moreTags()) {
Modifier modifier = new Modifier(xr, Specification.this);
Specification.this.addModifier(modifier);
Specification.this.specialModifiers.add(modifier);
}
}
}
private class AbilityReader implements ChildReader {
/**
* {@inheritDoc}
*/
@Override
public void readChildren(FreeColXMLReader xr)
throws XMLStreamException {
while (xr.moreTags()) {
Ability ability = new Ability(xr, Specification.this);
Specification.this.addAbility(ability);
Specification.this.gameAbilities.add(ability);
}
}
}
/**
* Options are special as they live in the allOptionGroups
* collection, which has its own particular semantics. So they
* need their own reader.
*/
private class OptionReader implements ChildReader {
private static final String RECURSIVE_TAG = "recursive";
/**
* {@inheritDoc}
*/
@Override
public void readChildren(FreeColXMLReader xr)
throws XMLStreamException {
while (xr.moreTags()) {
readChild(xr);
}
}
private void readChild(FreeColXMLReader xr) throws XMLStreamException {
final String tag = xr.getLocalName();
boolean recursive = xr.getAttribute(RECURSIVE_TAG, true);
if (OptionGroup.TAG.equals(tag)) {
String id = xr.readId();
OptionGroup group = allOptionGroups.get(id);
if (group == null) {
group = new OptionGroup(id, Specification.this);
allOptionGroups.put(id, group);
}
group.readFromXML(xr);
Specification.this.addOptionGroup(group, recursive);
} else {
logger.warning(OptionGroup.TAG + " expected in OptionReader"
+ ", not: " + tag);
xr.nextTag();
}
}
}
/** A reader for ordinary spec object types. */
private class TypeReader<T extends FreeColSpecObjectType>
implements ChildReader {
/** The class of objects to read. */
private final Class<T> type;
/** The list to append new objects to. */
private final List<T> result;
/** Internal index to impose an ordering. */
private int index = 0;
/**
* Build a type read.
*
* @param type The class to read.
* @param listToFill A list of read types.
*/
public TypeReader(Class<T> type, List<T> listToFill) {
result = listToFill;
this.type = type;
}
/**
* {@inheritDoc}
*/
@SuppressFBWarnings(value="DLC_DUBIOUS_LIST_COLLECTION",
justification="List required externally")
@Override
public void readChildren(FreeColXMLReader xr)
throws XMLStreamException {
while (xr.moreTags()) {
final String tag = xr.getLocalName();
String id = xr.readId();
if (id == null) {
logger.warning("Null identifier, tag: " + tag);
} else if (FreeColSpecObjectType.DELETE_TAG.equals(tag)) {
FreeColSpecObjectType object = removeType(id);
if (object != null) {
result.remove(object);
} else {
logger.warning("Delete " + id + " failed");
}
} else {
T object = getAlreadyInitializedType(id, type);
if (object == null) {
object = newType(id, type);
addType(id, object);
}
// If this an existing object (with id) and the
// PRESERVE tag is present, then leave the
// attributes intact and only read the child
// elements, otherwise do a full attribute
// inclusive read. This allows mods and spec
// extensions to not have to re-specify all the
// attributes when just changing the children.
if (object.getId() != null
&& xr.getAttribute(FreeColSpecObjectType.PRESERVE_TAG, false)) {
object.readChildren(xr);
} else {
object.readFromXML(xr);
}
if (!object.isAbstractType() && !result.contains(object)) {
result.add(object);
object.setIndex(index);
index++;
}
}
}
}
}
/** Sources. Special static spec objects. */
public static class Source extends FreeColSpecObjectType {
/**
* Trivial constructor.
*
* @param id The object identifier.
*/
public Source(String id) {
super(id);
}
/**
* {@inheritDoc}
*/
@Override
public void toXML(FreeColXMLWriter xw) {
throw new RuntimeException("Can not happen: " + this);
}
/**
* {@inheritDoc}
*/
@Override
public String getXMLTagName() { return "source"; }
// Override Object
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return getId();
}
};
public static final Source AMBUSH_BONUS_SOURCE
= new Source("model.source.ambushBonus");
public static final Source AMPHIBIOUS_ATTACK_PENALTY_SOURCE
= new Source("model.source.amphibiousAttack");
public static final Source ARTILLERY_PENALTY_SOURCE
= new Source("model.source.artilleryInTheOpen");
public static final Source ATTACK_BONUS_SOURCE
= new Source("model.source.attackBonus");
public static final Source BASE_DEFENCE_SOURCE
= new Source("model.source.baseDefence");
public static final Source BASE_OFFENCE_SOURCE
= new Source("model.source.baseOffence");
public static final Source CARGO_PENALTY_SOURCE
= new Source("model.source.cargoPenalty");
public static final Source COLONY_GOODS_PARTY_SOURCE
= new Source("model.source.colonyGoodsParty");
public static final Source FORTIFICATION_BONUS_SOURCE
= new Source("model.source.fortified");
public static final Source INDIAN_RAID_BONUS_SOURCE
= new Source("model.source.artilleryAgainstRaid");
public static final Source MOVEMENT_PENALTY_SOURCE
= new Source("model.source.movementPenalty");
public static final Source SHIP_TRADE_PENALTY_SOURCE
= new Source("model.source.shipTradePenalty");
public static final Source SOL_MODIFIER_SOURCE
= new Source("model.source.solModifier");
/** All the special static sources. */
private static final Source[] sources = new Source[] {
MOVEMENT_PENALTY_SOURCE,
ARTILLERY_PENALTY_SOURCE,
ATTACK_BONUS_SOURCE,
FORTIFICATION_BONUS_SOURCE,
INDIAN_RAID_BONUS_SOURCE,
AMPHIBIOUS_ATTACK_PENALTY_SOURCE,
BASE_OFFENCE_SOURCE,
BASE_DEFENCE_SOURCE,
CARGO_PENALTY_SOURCE,
AMBUSH_BONUS_SOURCE,
COLONY_GOODS_PARTY_SOURCE,
SHIP_TRADE_PENALTY_SOURCE,
SOL_MODIFIER_SOURCE
};
// @compat 0.10.x/0.11.x
/** A map of default nation colours. */
private static final Map<String, Color> defaultColors = new HashMap<>();
static {
defaultColors.put("model.nation.dutch", new Color(0xff9d3c));
defaultColors.put("model.nation.french", new Color(0x0000ff));
defaultColors.put("model.nation.english", new Color(0xff0000));
defaultColors.put("model.nation.spanish", new Color(0xffff00));
defaultColors.put("model.nation.inca", new Color(0xf4f0c4));
defaultColors.put("model.nation.aztec", new Color(0xc4a020));
defaultColors.put("model.nation.arawak", new Color(0x6888c0));
defaultColors.put("model.nation.cherokee", new Color(0x6c3c18));
defaultColors.put("model.nation.iroquois", new Color(0x74a44c));
defaultColors.put("model.nation.sioux", new Color(0xc0ac84));
defaultColors.put("model.nation.apache", new Color(0x900000));
defaultColors.put("model.nation.tupi", new Color(0x045c04));
defaultColors.put("model.nation.dutchREF", new Color(0xcc5500));
defaultColors.put("model.nation.frenchREF", new Color(0x6050dc));
defaultColors.put("model.nation.englishREF", new Color(0xde3163));
defaultColors.put("model.nation.spanishREF", new Color(0xffdf00));
defaultColors.put("model.nation.portuguese", new Color(0x00ff00));
defaultColors.put("model.nation.swedish", new Color(0x00bfff));
defaultColors.put("model.nation.danish", new Color(0xff00bf));
defaultColors.put("model.nation.russian", new Color(0xffffff));
defaultColors.put("model.nation.portugueseREF", new Color(0xbfff00));
defaultColors.put("model.nation.swedishREF", new Color(0x367588));
defaultColors.put("model.nation.danishREF", new Color(0x91006d));
defaultColors.put("model.nation.russianREF", new Color(0xbebebe));
defaultColors.put(Nation.UNKNOWN_NATION_ID, Nation.UNKNOWN_NATION_COLOR);
}
// end @compat 0.10.x/0.11.x
public static final String TAG = "freecol-specification";
/** The difficulty levels option group is special. */
public static final String DIFFICULTY_LEVELS = "difficultyLevels";
/** Roles backward compatibility fragment. */
public static final String ROLES_COMPAT_FILE_NAME = "roles-compat.xml";
/** Unit change types backward compatibility fragment. */
public static final String UNIT_CHANGE_TYPES_COMPAT_FILE_NAME
= "unit-change-types-compat.xml";
/** The default food type. */
private static final String DEFAULT_FOOD_TYPE = "model.goods.food";
/** The default nation type, which does nothing special. */
private static final String DEFAULT_NATION_TYPE
= "model.nationType.default";
/** The default role. */
public static final String DEFAULT_ROLE_ID = "model.role.default";
/** How many game ages. */
public static final int NUMBER_OF_AGES = 3;
/** The option groups to save. */
private static final String[] coreOptionGroups = {
GameOptions.TAG, MapGeneratorOptions.TAG, DIFFICULTY_LEVELS
};
/** A map from specification object group identifier to a reader for it. */
private final Map<String, ChildReader> readerMap = new HashMap<>(20);
// Containers filled from readers in the readerMap
// readerMap("building-types")
private final List<BuildingType> buildingTypeList = new ArrayList<>();
// readerMap("disasters")
private final List<Disaster> disasters = new ArrayList<>();
// readerMap("european-nation-types")
private final List<EuropeanNationType> europeanNationTypes = new ArrayList<>();
// readerMap("events")
private final List<Event> events = new ArrayList<>();
// readerMap("founding-fathers")
private final List<FoundingFather> foundingFathers = new ArrayList<>();
// readerMap("goods-types")
private final List<GoodsType> goodsTypeList = new ArrayList<>();
// readerMap("indian-nation-types")
private final List<IndianNationType> indianNationTypes = new ArrayList<>();
// readerMap("nations")
private final List<Nation> nations = new ArrayList<>();
// readerMap("resource-types")
private final List<ResourceType> resourceTypeList = new ArrayList<>();
// readerMap("roles")
private final List<Role> roles = new ArrayList<>();
// readerMap("tile-types")
private final List<TileType> tileTypeList = new ArrayList<>();
// readerMap("tile-improvement-types")
private final List<TileImprovementType> tileImprovementTypeList = new ArrayList<>();
// readerMap("unit-change-types")
private final List<UnitChangeType> unitChangeTypeList = new ArrayList<>();
// readerMap("unit-types")
private final List<UnitType> unitTypeList = new ArrayList<>();
// readerMap("modifiers")
private final Map<String, List<Modifier>> allModifiers = new HashMap<>();
private final List<Modifier> specialModifiers = new ArrayList<>();
// readerMap("options")
private final Map<String, AbstractOption> allOptions = new HashMap<>();
private final Map<String, OptionGroup> allOptionGroups = new HashMap<>();
/* Containers derived from readerMap containers */
// Derived from readerMap container: goodsTypeList
private final List<GoodsType> storableGoodsTypeList = new ArrayList<>();
private final List<GoodsType> farmedGoodsTypeList = new ArrayList<>();
private final List<GoodsType> foodGoodsTypeList = new ArrayList<>();
private final List<GoodsType> newWorldGoodsTypeList = new ArrayList<>();
private final List<GoodsType> newWorldLuxuryGoodsTypeList = new ArrayList<>();
private final List<GoodsType> libertyGoodsTypeList = new ArrayList<>();
private final List<GoodsType> immigrationGoodsTypeList = new ArrayList<>();
private final List<GoodsType> rawBuildingGoodsTypeList = new ArrayList<>();
private final List<GoodsType> rawMaterialsForUnstorableBuildingMaterials = new ArrayList<>();
private final List<GoodsType> rawMaterialsForStorableBuildingMaterials = new ArrayList<>();
// Derived from readerMap container: nations
private final List<Nation> europeanNations = new ArrayList<>();
private final List<Nation> REFNations = new ArrayList<>();
private final List<Nation> indianNations = new ArrayList<>();
// Derived from readerMap containers: indianNationTypes europeanNationTypes
private final List<NationType> nationTypes = new ArrayList<>();
// Derived from readerMap container: europeanNationTypes
private final List<EuropeanNationType> REFNationTypes = new ArrayList<>();
// Derived from readerMap container: unitTypeList
private final ArrayList<UnitType> buildableUnitTypes = new ArrayList<>();
private final Map<GoodsType, UnitType> experts = new HashMap<>();
private final List<UnitType> unitTypesTrainedInEurope = new ArrayList<>();
private final List<UnitType> unitTypesPurchasedInEurope = new ArrayList<>();
private UnitType fastestLandUnitType = null;
private UnitType fastestNavalUnitType = null;
private final List<UnitType> defaultUnitTypes = new ArrayList<>();
// Other containers
/**
* All the FreeColSpecObjectType objects, indexed by identifier.
*
* This list include abstract types that are read from the specification files,
* but no longer contains the abstract types after reserialization. The reason
* for this is that the abstract types are needed when still applying mods
* that might reference the abstract type.
*/
private final Map<String, FreeColSpecObjectType> allTypes = new HashMap<>(256);
/** All the abilities by identifier. */
private final Map<String, List<Ability>> allAbilities = new HashMap<>(128);
private final List<Ability> gameAbilities = new ArrayList<>();
/** A cache of the military roles in decreasing order. Do not serialize. */
private List<Role> militaryRoles = null;
private boolean initialized = false;
/** The specification identifier. */
private String id;
/** The specification version. */
private String version;
/** The name of the difficulty level option group. */
private String difficultyLevel = null;
/** The turn number for the game ages for FF recruitment. */
private final int[] ages = new int[NUMBER_OF_AGES];
/**
* Creates a new Specification object.
*/
public Specification() {
logger.fine("Initializing Specification");
for (Source source : sources) addType(source.getId(), source);
readerMap.put(BUILDING_TYPES_TAG,
new TypeReader<>(BuildingType.class, buildingTypeList));
readerMap.put(DISASTERS_TAG,
new TypeReader<>(Disaster.class, disasters));
readerMap.put(EUROPEAN_NATION_TYPES_TAG,
new TypeReader<>(EuropeanNationType.class, europeanNationTypes));
readerMap.put(EVENTS_TAG,
new TypeReader<>(Event.class, events));
readerMap.put(FOUNDING_FATHERS_TAG,
new TypeReader<>(FoundingFather.class, foundingFathers));
readerMap.put(GOODS_TYPES_TAG,
new TypeReader<>(GoodsType.class, goodsTypeList));
readerMap.put(INDIAN_NATION_TYPES_TAG,
new TypeReader<>(IndianNationType.class, indianNationTypes));
readerMap.put(NATIONS_TAG,
new TypeReader<>(Nation.class, nations));
readerMap.put(RESOURCE_TYPES_TAG,
new TypeReader<>(ResourceType.class, resourceTypeList));
readerMap.put(ROLES_TAG,
new TypeReader<>(Role.class, roles));
readerMap.put(TILE_TYPES_TAG,
new TypeReader<>(TileType.class, tileTypeList));
readerMap.put(TILE_IMPROVEMENT_TYPES_TAG,
new TypeReader<>(TileImprovementType.class, tileImprovementTypeList));
// @compat 0.11.3
readerMap.put(OLD_TILEIMPROVEMENT_TYPES_TAG,
new TypeReader<>(TileImprovementType.class, tileImprovementTypeList));
// end @compat 0.11.3
readerMap.put(UNIT_CHANGE_TYPES_TAG,
new TypeReader<>(UnitChangeType.class, unitChangeTypeList));
readerMap.put(UNIT_TYPES_TAG,
new TypeReader<>(UnitType.class, unitTypeList));
readerMap.put(ABILITIES_TAG, new AbilityReader());
readerMap.put(MODIFIERS_TAG, new ModifierReader());
readerMap.put(OPTIONS_TAG, new OptionReader());
}
/**
* Creates a new Specification object by loading it from the
* given {@code FreeColXMLReader}.
*
* @param xr The {@code FreeColXMLReader} to read from.
* @exception XMLStreamException if there is a problem with the stream.
*/
public Specification(FreeColXMLReader xr) throws XMLStreamException {
this();
initialized = false;
readFromXML(xr);
prepare(null, difficultyLevel);
clean("load from stream");
initialized = true;
}
/**
* Creates a new Specification object by loading it from the
* given {@code InputStream}.
*
* @param in The {@code InputStream} to read from.
* @exception XMLStreamException if there is a problem with the stream.
*/
public Specification(InputStream in) throws XMLStreamException {
this();
initialized = false;
load(in);
prepare(null, difficultyLevel);
clean("load from InputStream");
initialized = true;
}
/**
* Load a specification or fragment from a stream.
*
* @param in The {@code InputStream} to read from.
* @exception XMLStreamException if there is a problem with the stream.
*/
private void load(InputStream in) throws XMLStreamException {
try (
FreeColXMLReader xr = new FreeColXMLReader(in);
) {
xr.nextTag();
readFromXML(xr);
}
}
/**
* Load mods into this specification.
*
* @param mods A list of {@code FreeColModFile}s for the active mods.
* @return True if any mod was loaded.
*/
public boolean loadMods(List<FreeColModFile> mods) {
initialized = false;
boolean loadedMod = false;
for (FreeColModFile mod : mods) {
InputStream sis = null;
try {
if ((sis = mod.getSpecificationInputStream()) != null) {
// Some mods are resource only
load(sis);
}
loadedMod = true;
logger.info("Loaded mod " + mod.getId());
} catch (FreeColUserMessageException e) {
throw e;
} catch (IOException|XMLStreamException ex) {
logger.log(Level.WARNING, "Read error in mod " + mod.getId(), ex);
throw new FreeColUserMessageException(
StringTemplate.template("error.mod").add("%id%", mod.getId()).add("%name%", Messages.getName("mod." + mod.getId())),
ex
);
} catch (RuntimeException rte) {
logger.log(Level.WARNING, "Parse error in mod " + mod.getId(), rte);
throw new FreeColUserMessageException(
StringTemplate.template("error.mod").add("%id%", mod.getId()).add("%name%", Messages.getName("mod." + mod.getId())),
rte
);
}
}
if (loadedMod) clean("mod loading");
initialized = true;
return loadedMod;
}
/**
* Prepare a specification with given advantages and difficulty level.
*
* @param advantages An optional {@code Advantages} setting.
* @param difficulty An optional identifier for the difficulty level.
*/
public void prepare(Advantages advantages, String difficulty) {
prepare(advantages, (difficulty == null) ? null
: getDifficultyOptionGroup(difficulty));
}
public boolean hasAbility(String key) {
List<Ability> ability = allAbilities.get(key);
return ability != null && ability.stream().anyMatch(a -> a.getValue());
}
/**
* Prepare a specification with given advantages and difficulty level.
*
* @param advantages An optional {@code Advantages} setting.
* @param difficulty An optional difficulty level {@code OptionGroup}.
*/
public void prepare(Advantages advantages, OptionGroup difficulty) {
applyFixes();
if (advantages == Advantages.NONE) {
clearEuropeanNationalAdvantages();
}
if (difficulty != null) {
setDifficultyOptionGroup(difficulty);
applyDifficultyLevel(difficulty);
}
}
/**
* Clean up the specification.
*
* Builds all the cached containers and secondary variables. This
* *must* clear any containers before building as it may be called
* multiple times in response to various specification updates.
*
* @param why A short statement of why the specification needed to
* be cleaned.
*/
public void clean(String why) {
logger.finest("Cleaning up specification following " + why + ".");
// Fix up the GoodsType derived attributes. Several GoodsType
// predicates are likely to fail until this is done.
GoodsType.setDerivedAttributes(this);
storableGoodsTypeList.clear();
farmedGoodsTypeList.clear();
foodGoodsTypeList.clear();
newWorldGoodsTypeList.clear();
newWorldLuxuryGoodsTypeList.clear();
libertyGoodsTypeList.clear();
immigrationGoodsTypeList.clear();
rawBuildingGoodsTypeList.clear();
rawMaterialsForUnstorableBuildingMaterials.clear();
rawMaterialsForStorableBuildingMaterials.clear();
for (GoodsType goodsType : goodsTypeList) {
if (goodsType.isStorable()) {
storableGoodsTypeList.add(goodsType);
}
if (goodsType.isFarmed()) {
farmedGoodsTypeList.add(goodsType);
}
if (goodsType.isFoodType()) {
foodGoodsTypeList.add(goodsType);
}
if (goodsType.isNewWorldGoodsType()) {
newWorldGoodsTypeList.add(goodsType);
if (goodsType.isNewWorldLuxuryType()) {
newWorldLuxuryGoodsTypeList.add(goodsType);
}
}
if (goodsType.isLibertyType()) {
libertyGoodsTypeList.add(goodsType);
}
if (goodsType.isImmigrationType()) {
immigrationGoodsTypeList.add(goodsType);
}
if (goodsType.isRawBuildingMaterial() && !goodsType.isFoodType()) {
rawBuildingGoodsTypeList.add(goodsType);
if (!goodsType.isRawMaterialForUnstorableBuildingMaterial()) {
rawMaterialsForStorableBuildingMaterials.add(goodsType);
}
}
if (goodsType.isRawMaterialForUnstorableBuildingMaterial() && !goodsType.isFoodType()) {
rawMaterialsForUnstorableBuildingMaterials.add(goodsType);
}
}
REFNations.clear();
europeanNations.clear();
indianNations.clear();
for (Nation nation : nations) {
if (nation.isUnknownEnemy()) continue;
if (nation.getType().isEuropean()) {
if (nation.getType().isREF()) {
REFNations.add(nation);
} else {
europeanNations.add(nation);
}
} else {
indianNations.add(nation);
}
}
nationTypes.clear();
nationTypes.addAll(indianNationTypes);
nationTypes.addAll(europeanNationTypes);
REFNationTypes.addAll(transform(europeanNationTypes, NationType::isREF));
europeanNationTypes.removeAll(REFNationTypes);
experts.clear();
unitTypesTrainedInEurope.clear();
unitTypesPurchasedInEurope.clear();
defaultUnitTypes.clear();
int bestLandValue = -1, bestNavalValue = -1;
for (UnitType unitType : unitTypeList) {
if (unitType.isDefaultUnitType()) defaultUnitTypes.add(unitType);
if (unitType.needsGoodsToBuild()
&& !unitType.hasAbility(Ability.BORN_IN_COLONY)) {
buildableUnitTypes.add(unitType);
}
if (unitType.getExpertProduction() != null) {
experts.put(unitType.getExpertProduction(), unitType);
}
if (unitType.hasPrice()) {
if (unitType.getSkill() > 0) {
unitTypesTrainedInEurope.add(unitType);
} else if (!unitType.hasSkill()) {
unitTypesPurchasedInEurope.add(unitType);
}
}
if (unitType.isNaval()) {
if (bestNavalValue < unitType.getMovement()) {
bestNavalValue = unitType.getMovement();
fastestNavalUnitType = unitType;
}
} else {
if (bestLandValue < unitType.getMovement()) {
bestLandValue = unitType.getMovement();
fastestLandUnitType = unitType;
}
}
}
// Initialize UI containers.
for (OptionGroup og : allOptionGroups.values()) {
og.generateChoices();
}
// Initialize the Turn class using GameOptions and messages.
Turn.initialize(getInteger(GameOptions.STARTING_YEAR),
getInteger(GameOptions.SEASON_YEAR),
getInteger(GameOptions.SEASONS));
boolean badAges = !hasOption(GameOptions.AGES, TextOption.class);
String agesValue = "";
if (!badAges) {
agesValue = getText(GameOptions.AGES);
String[] a = agesValue.split(",");
badAges = a.length != NUMBER_OF_AGES-1;
if (!badAges) {
try {
ages[0] = 1;
ages[1] = Turn.yearToTurn(Integer.parseInt(a[0]));
ages[2] = Turn.yearToTurn(Integer.parseInt(a[1]));
if (ages[1] < 1 || ages[2] < 1) {
badAges = true;
} else if (ages[1] > ages[2]) {
int tmp = ages[1];
ages[1] = ages[2];
ages[2] = tmp;
}
} catch (NumberFormatException nfe) {
badAges = true;
}
}
}
if (badAges) {
logger.warning("Bad ages: " + agesValue);
ages[0] = 1; // First turn
ages[1] = Turn.yearToTurn(1600);
ages[2] = Turn.yearToTurn(1700);
}
// Apply the customs on coast restriction
boolean customsOnCoast = getBoolean(GameOptions.CUSTOMS_ON_COAST);
for (Ability a : iterable(getBuildingType("model.building.customHouse")
.getAbilities(Ability.COASTAL_ONLY))) {
a.setValue(customsOnCoast);
}
StringBuilder sb = new StringBuilder(1024);
sb.append("Specification clean following ").append(why)
.append(" complete, starting year=").append(Turn.getStartingYear())
.append(", season year=").append(Turn.getSeasonYear())
.append(", ages=[").append(ages[0])
.append(',').append(ages[1])
.append(',').append(ages[2])
.append("], seasons=").append(Turn.getSeasonNumber())
.append(", difficulty=").append(difficultyLevel)
.append(", ").append(allTypes.size()).append(" Types")
.append(", ").append(allAbilities.size()).append(" Abilities")
.append(", ").append(buildingTypeList.size()).append(" BuildingTypes")
.append(", ").append(disasters.size()).append(" Disasters")
.append(", ").append(europeanNationTypes.size()).append(" EuropeanNationTypes")
.append(", ").append(events.size()).append(" Events")
.append(", ").append(foundingFathers.size()).append(" FoundingFathers")
.append(", ").append(goodsTypeList.size()).append(" GoodsTypes")
.append(", ").append(indianNationTypes.size()).append(" IndianNationTypes")
.append(", ").append(allModifiers.size()).append(" Modifiers")
.append(", ").append(nations.size()).append(" Nations")
.append(", ").append(allOptions.size()).append(" Options")
.append(", ").append(allOptionGroups.size()).append(" Option Groups")
.append(", ").append(resourceTypeList.size()).append(" ResourceTypes")
.append(", ").append(roles.size()).append(" Roles")
.append(", ").append(tileTypeList.size()).append(" TileTypes")
.append(", ").append(tileImprovementTypeList.size()).append(" TileImprovementTypes")
.append(", ").append(unitChangeTypeList.size()).append(" UnitChangeTypes")
.append(", ").append(unitTypeList.size()).append(" UnitTypes")
.append(" read.");
logger.info(sb.toString());
}
// Basic methods
/**
* Get the specification identifier.
*
* @return The specification identifier.
*/
public String getId() {
return id;
}
/**
* Get the specification version.
*
* @return The specification version.
*/
public String getVersion() {
return version;
}
/**
* Get a {@code FreeColSpecObjectType} by id.
*
* @param id The identifier to look for.
* @return The {@code FreeColSpecObjectType} found if any.
*/
public FreeColSpecObjectType getType(String id) {
return allTypes.get(id);
}
/**
* Find a {@code FreeColSpecObjectType} by id and class.
*
* @param <T> The actual return type.
* @param id The object identifier to look for.
* @param returnClass The expected {@code Class}.
* @return The {@code FreeColSpecObjectType} found if any.
*/
public <T extends FreeColSpecObjectType> T getAlreadyInitializedType(String id,
Class<T> returnClass) {
FreeColSpecObjectType ret = getType(id);
return (ret == null) ? null : returnClass.cast(ret);
}
/**
* Add a type by identifier.
*
* @param id The identifier to associate with.
* @param type The {@code FreeColSpecObjectType} to add.
*/
private void addType(String id, FreeColSpecObjectType type) {
allTypes.put(id, type);
}
/**
* Remove reference to a type.
*
* @param id The type identifier to remove.
* @return The removed {@code FreeColSpecObjectType}.
*/
private FreeColSpecObjectType removeType(String id) {
return allTypes.remove(id);
}
/**
* Registers an Ability as defined.
*
* @param ability an {@code Ability} value
*/
public void addAbility(Ability ability) {
String id = ability.getId();
addAbility(id);
allAbilities.get(id).add(ability);
}
/**
* Registers an Ability's id as defined. This is useful for
* abilities that are required rather than provided by
* FreeColSpecObjectTypes.
*
* @param id The object identifier.
*/
public void addAbility(String id) {
if (!allAbilities.containsKey(id)) {
allAbilities.put(id, new ArrayList<Ability>());
}
}
/**
* Get all the Abilities with the given identifier.
*
* @param id The object identifier to look for.
* @return A stream of {@code Ability}s.
*/
public Stream<Ability> getAbilities(String id) {
List<Ability> result = allAbilities.get(id);
return (result == null) ? Stream.<Ability>empty() : result.stream();
}
/**
* Add a modifier.
*
* @param modifier The {@code Modifier} to add.
*/
public void addModifier(Modifier modifier) {
String id = modifier.getId();
if (!allModifiers.containsKey(id)) {
allModifiers.put(id, new ArrayList<Modifier>());
}
allModifiers.get(id).add(modifier);
}
/**
* Get all the Modifiers with the given identifier.
*
* @param id The object identifier to look for.
* @return A stream of {@code Modifier}s.
*/
public Stream<Modifier> getModifiers(String id) {
List<Modifier> result = allModifiers.get(id);
return (result == null) ? Stream.<Modifier>empty() : result.stream();
}
/**
* Add a father, for test purposes.
*
* @param ff The {@code FoundingFather} to add.
*/
public void addTestFather(FoundingFather ff) {
addType(ff.getId(), ff);
foundingFathers.add(ff);
}
/**
* Disable editing of some critical option groups.
*/
public void disableEditing() {
for (String s : coreOptionGroups) {
OptionGroup og = allOptionGroups.get(s);
if (og != null) og.setEditable(false);
}
}
/**
* Compare a spec version number with the current one.
*
* @param other The other spec version number.
* @return Positive if the current version is greater than the other,
* negative if the current version is lower than the other,
* zero if they are equal.
*/
private int compareVersion(String other) {
String version = getVersion();
if (version == null) return 0;
final String rex = "\\.";
String[] sv = version.split(rex, 2);
String[] so = other.split(rex, 2);
if (sv.length == 2 && so.length == 2) {
int cmp;
try {
cmp = Integer.compare(Integer.parseInt(sv[0]),
Integer.parseInt(so[0]));
if (cmp != 0) return cmp;
return Integer.compare(Integer.parseInt(sv[1]),
Integer.parseInt(so[1]));
} catch (NumberFormatException nfe) {}
}
throw new RuntimeException("Bad version: " + other);
}
// Option routines including DifficultyLevels which are option groups
// Interface OptionContainer
/**
* {@inheritDoc}
*/
public <T extends Option> boolean hasOption(String id,
Class<T> returnClass) {
if (id == null) return false;
AbstractOption val = this.allOptions.get(id);
return (val == null) ? false
: returnClass.isAssignableFrom(val.getClass());
}
/**
* {@inheritDoc}
*/
public <T extends Option> T getOption(String id,
Class<T> returnClass) {
if (id == null) {
throw new RuntimeException("Null identifier for "
+ returnClass.getName());
} else if (!this.allOptions.containsKey(id)) {
throw new RuntimeException("Missing option: " + id);
} else {
AbstractOption op = this.allOptions.get(id);
try {
return returnClass.cast(op);
} catch (ClassCastException cce) {
throw new RuntimeException("Not a " + returnClass.getName()
+ ": " + id, cce);
}
}
}
/**
* {@inheritDoc}
*/
public OptionGroup getOptionGroup(String id) {
if (id == null) {
throw new RuntimeException("OptionGroup with null id: " + this);
} else if (!this.allOptionGroups.containsKey(id)) {
throw new RuntimeException("Missing OptionGroup: " + id);
} else {
return this.allOptionGroups.get(id);
}
}
/**
* Adds an {@code OptionGroup} to this specification.
*
* @param optionGroup The {@code OptionGroup} to add.
* @param recursive If true, add recursively to subgroups.
*/
private void addOptionGroup(OptionGroup optionGroup, boolean recursive) {
// Add the options of the group
for (Option option : optionGroup.getOptions()) {
if (option instanceof OptionGroup) {
allOptionGroups.put(option.getId(), (OptionGroup)option);
if (recursive) {
addOptionGroup((OptionGroup)option, true);
}
} else {
addAbstractOption((AbstractOption)option);
}
}
}
/**
* Adds an {@code AbstractOption} to this specification.
*
* @param abstractOption The {@code AbstractOption} to add.
*/
private void addAbstractOption(AbstractOption abstractOption) {
// Add the option
allOptions.put(abstractOption.getId(), abstractOption);
}
/**
* Merge an option group into the spec.
*
* @param group The {@code OptionGroup} to merge.
* @return The merged {@code OptionGroup} from this
* {@code Specification}.
*/
private OptionGroup mergeGroup(OptionGroup group) {
OptionGroup realGroup = allOptionGroups.get(group.getId());
if (realGroup == null || !realGroup.isEditable()) return realGroup;
for (Option o : group.getOptions()) {
if (o instanceof OptionGroup) {
mergeGroup((OptionGroup)o);
} else {
realGroup.add(o);
}
}
return realGroup;
}
/**
* Gets the difficulty levels in this specification.
*
* @return A list of difficulty levels in this specification.
*/
public List<OptionGroup> getDifficultyLevels() {
OptionGroup group = allOptionGroups.get(DIFFICULTY_LEVELS);
Stream<Option> stream = (group == null) ? Stream.<Option>empty()
: group.getOptions().stream();
return transform(stream, (Option o) -> o instanceof OptionGroup,
o -> (OptionGroup)o);
}
/**
* Get the current difficulty level.
*
* @return The difficulty level.
*/
public String getDifficultyLevel() {
return this.difficultyLevel;
}
/**
* Gets the current difficulty level options.
*
* @return The current difficulty level {@code OptionGroup}.
*/
public OptionGroup getDifficultyOptionGroup() {
return getDifficultyOptionGroup(this.difficultyLevel);
}
/**
* Gets difficulty level options by id.
*
* @param id The difficulty level identifier to look for.
* @return The corresponding difficulty level {@code OptionGroup},
* if any.
*/
public OptionGroup getDifficultyOptionGroup(String id) {
return find(getDifficultyLevels(),
matchKeyEquals(id, FreeColObject::getId));
}
/**
* Add/overwrite a difficulty option group.
*
* @param difficulty The {@code OptionGroup} to add.
*/
private void setDifficultyOptionGroup(OptionGroup difficulty) {
OptionGroup group = allOptionGroups.get(DIFFICULTY_LEVELS);
if (group != null) group.add(difficulty);
allOptionGroups.put(difficulty.getId(), difficulty);
}
/**
* Applies the difficulty level identified by the given String to
* the current specification.
*
* @param difficulty The identifier of a difficulty level to apply.
*/
public void applyDifficultyLevel(String difficulty) {
applyDifficultyLevel(getDifficultyOptionGroup(difficulty));
}
/**
* Applies the given difficulty level to the current
* specification.
*
* Public for the test suite.
*
* @param level The difficulty level {@code OptionGroup} to apply.
*/
public void applyDifficultyLevel(OptionGroup level) {
if (level == null) {
logger.warning("Null difficulty level supplied");
return;
}
logger.config("Applying difficulty level " + level.getId());
addOptionGroup(level, true);
this.difficultyLevel = level.getId();
}
public OptionGroup getGameOptions() {
return getOptionGroup(GameOptions.TAG);
}
public void setGameOptions(OptionGroup go) {
allOptionGroups.put(GameOptions.TAG, go);
addOptionGroup(go, true);
}
public OptionGroup getMapGeneratorOptions() {
return getOptionGroup(MapGeneratorOptions.TAG);
}
public void setMapGeneratorOptions(OptionGroup mgo) {
allOptionGroups.put(MapGeneratorOptions.TAG, mgo);
addOptionGroup(mgo, true);
}
/**
* Update the game and map options from the user configuration files.
*
* @return True if any option was fixed.
*/
public boolean updateGameAndMapOptions() {
boolean ret = false;
/*
String gtag = GameOptions.TAG;
File gof = FreeColDirectories
.getOptionsFile(FreeColDirectories.GAME_OPTIONS_FILE_NAME);
OptionGroup gog = (!gof.exists()) ? null
: OptionGroup.loadOptionGroup(gof, gtag, this);
if (gog != null) {
gog = mergeGroup(gog);
ret |= fixGameOptions();
} else {
gog = getOptionGroup(gtag);
}
gog.save(gof, null, true);
String mtag = MapGeneratorOptions.TAG;
File mof = FreeColDirectories
.getOptionsFile(FreeColDirectories.MAP_GENERATOR_OPTIONS_FILE_NAME);
OptionGroup mog = (!mof.exists()) ? null
: OptionGroup.loadOptionGroup(mof, mtag, this);
if (mog != null) {
mog = mergeGroup(mog);
ret |= fixMapGeneratorOptions();
} else {
mog = getOptionGroup(mtag);
}
mog.save(mof, null, true);
*/
return ret;
}
/**
* Generate the dynamic options.
*
* Only call this in the server. If clients call it the European
* prices can be desynchronized.
*/
public void generateDynamicOptions() {
logger.finest("Generating dynamic options.");
OptionGroup prices = new OptionGroup(GameOptions.GAMEOPTIONS_PRICES, this);
allOptionGroups.put(prices.getId(), prices);
for (GoodsType goodsType : goodsTypeList) {
String name = goodsType.getSuffix("model.goods.");
String base = "model.option." + name + ".";
if (goodsType.getInitialSellPrice() > 0) {
int diff = (goodsType.isNewWorldGoodsType()
|| goodsType.isNewWorldLuxuryType()) ? 3 : 0;
IntegerOption minimum
= new IntegerOption(base + "minimumPrice", this);
minimum.setValue(goodsType.getInitialSellPrice());
minimum.setMinimumValue(1);
minimum.setMaximumValue(100);
prices.add(minimum);
addAbstractOption(minimum);
IntegerOption maximum
= new IntegerOption(base + "maximumPrice", this);
maximum.setValue(goodsType.getInitialSellPrice() + diff);
maximum.setMinimumValue(1);
maximum.setMaximumValue(100);
prices.add(maximum);
addAbstractOption(maximum);
IntegerOption spread
= new IntegerOption(base + "spread", this);
spread.setValue(goodsType.getPriceDifference());
spread.setMinimumValue(1);
spread.setMaximumValue(100);
prices.add(spread);
addAbstractOption(spread);
} else if (goodsType.getPrice() < INFINITY) {
IntegerOption price
= new IntegerOption(base + "price", this);
price.setValue(goodsType.getPrice());
price.setMinimumValue(1);
price.setMaximumValue(100);
prices.add(price);
addAbstractOption(price);
}
}
getGameOptions().add(prices);
}
/**
* Merge in a new set of game options.
*
* @param newGameOptions The new game options {@code OptionGroup}
* to merge.
* @param who Where are we, client or server?
* @return True if the merge succeeded.
*/
public boolean mergeGameOptions(OptionGroup newGameOptions, String who) {
OptionGroup go = getGameOptions();
LogBuilder lb = new LogBuilder(64);
boolean ret = go.merge(newGameOptions, lb);
lb.shrink("\n"); lb.log(logger, Level.FINEST);
if (!ret) return false;
addOptionGroup(go, true); // make sure allOptions is seeing any changes
clean("merged game options (" + who + ")");
return true;
}
/**
* Merge in a new set of map options.
*
* @param newMapGeneratorOptions The new game options
* {@code OptionGroup} to merge.
* @param who Where are we, client or server?
* @return True if the merge succeeded.
*/
public boolean mergeMapGeneratorOptions(OptionGroup newMapGeneratorOptions,
String who) {
OptionGroup go = getMapGeneratorOptions();
LogBuilder lb = new LogBuilder(64);
boolean ret = go.merge(newMapGeneratorOptions, lb);
lb.shrink("\n"); lb.log(logger, Level.FINEST);
if (!ret) return false;
addOptionGroup(go, true); // make sure allOptions is seeing any changes
clean("merged map options (" + who + ")");
return true;
}
// -- Ages --
/**
* Gets the age corresponding to a given turn.
*
* @param turn The {@code Turn} to check.
* @return The age of the given turn.
*/
public int getAge(Turn turn) {
int n = turn.getNumber();
return (n < ages[0]) ? -1
: (n < ages[1]) ? 0
: (n < ages[2]) ? 1
: 2;
}
// -- Buildables --
public BuildableType getBuildableType(String id) {
return getAlreadyInitializedType(id, BuildableType.class);
}
// -- Buildings --
public List<BuildingType> getBuildingTypeList() {
return buildingTypeList;
}
/**
* Get a building type by identifier.
*
* @param id The object identifier.
* @return The {@code BuildingType} found.
*/
public BuildingType getBuildingType(String id) {
return getAlreadyInitializedType(id, BuildingType.class);
}
// -- Disasters --
public List<Disaster> getDisasters() {
return disasters;
}
/**
* Get a disaster by identifier.
*
* @param id The object identifier.
* @return The {@code Disaster} found.
*/
public Disaster getDisaster(String id) {
return getAlreadyInitializedType(id, Disaster.class);
}
// -- Events --
public List<Event> getEvents() {
return events;
}
/**
* Get an event by identifier.
*
* @param id The object identifier.
* @return The {@code Event} found.
*/
public Event getEvent(String id) {
return getAlreadyInitializedType(id, Event.class);
}
// -- Founding Fathers --
public List<FoundingFather> getFoundingFathers() {
return foundingFathers;
}
/**
* Get a founding father type by identifier.
*
* @param id The object identifier.
* @return The {@code FoundingFather} found.
*/
public FoundingFather getFoundingFather(String id) {
return getAlreadyInitializedType(id, FoundingFather.class);
}
// -- Goods --
public List<GoodsType> getGoodsTypeList() {
return new ArrayList<>(goodsTypeList);
}
public List<GoodsType> getStorableGoodsTypeList() {
return new ArrayList<>(storableGoodsTypeList);
}
public List<GoodsType> getFarmedGoodsTypeList() {
return new ArrayList<>(farmedGoodsTypeList);
}
public List<GoodsType> getNewWorldGoodsTypeList() {
return new ArrayList<>(newWorldGoodsTypeList);
}
public List<GoodsType> getNewWorldLuxuryGoodsTypeList() {
return new ArrayList<>(newWorldLuxuryGoodsTypeList);
}
public List<GoodsType> getLibertyGoodsTypeList() {
return new ArrayList<>(libertyGoodsTypeList);
}
public List<GoodsType> getImmigrationGoodsTypeList() {
return new ArrayList<>(immigrationGoodsTypeList);
}
public List<GoodsType> getFoodGoodsTypeList() {
return new ArrayList<>(foodGoodsTypeList);
}
public List<GoodsType> getRawBuildingGoodsTypeList() {
return new ArrayList<>(rawBuildingGoodsTypeList);
}
public List<GoodsType> getRawMaterialsForUnstorableBuildingMaterials() {
return new ArrayList<>(rawMaterialsForUnstorableBuildingMaterials);
}
public List<GoodsType> getRawMaterialsForStorableBuildingMaterials() {
return new ArrayList<>(rawMaterialsForStorableBuildingMaterials);
}
/**
* Get the primary food type.
*
* FIXME: The "Food" type is handled as a special case in many
* places. This routine was added to collect them into one place,
* in the hope we can one day deprecate this routine and clean up
* the special cases.
*
* @return The main food type.
*/
public GoodsType getPrimaryFoodType() {
return getGoodsType(DEFAULT_FOOD_TYPE);
}
/**
* Get the initial <em>minimum</em> price of the given goods
* type. The initial price in a particular Market may be higher.
*
* @param goodsType The {@code GoodsType} to check.
* @return The minimum price.
*/
public int getInitialPrice(GoodsType goodsType) {
String suffix = goodsType.getSuffix("model.goods.");
String minPrice = "model.option." + suffix + ".minimumPrice";
String maxPrice = "model.option." + suffix + ".maximumPrice";
return (hasOption(minPrice, IntegerOption.class)
&& hasOption(maxPrice, IntegerOption.class))
? Math.min(getInteger(minPrice), getInteger(maxPrice))
: goodsType.getInitialSellPrice();
}
/**
* Get a goods type by identifier.
*
* @param id The object identifier.
* @return The {@code GoodsType} found.
*/
public GoodsType getGoodsType(String id) {
return getAlreadyInitializedType(id, GoodsType.class);
}
// -- Improvements --
public List<TileImprovementType> getTileImprovementTypeList() {
return tileImprovementTypeList;
}
/**
* Get a tile improvement type by identifier.
*
* @param id The object identifier.
* @return The {@code TileImprovementType} found.
*/
public TileImprovementType getTileImprovementType(String id) {
return getAlreadyInitializedType(id, TileImprovementType.class);
}
// -- NationTypes --
public List<NationType> getNationTypes() {
return nationTypes;
}
public List<EuropeanNationType> getEuropeanNationTypes() {
return europeanNationTypes;
}
public List<EuropeanNationType> getVisibleEuropeanNationTypes() {
return europeanNationTypes.stream()
.filter(type -> !"model.nationType.optionOnly".equals(type.getId()))
.collect(Collectors.toList());
}
public List<EuropeanNationType> getREFNationTypes() {
return REFNationTypes;
}
public List<IndianNationType> getIndianNationTypes() {
return indianNationTypes;
}
/**
* Get a nation type by identifier.
*
* @param id The object identifier.
* @return The {@code NationType} found.
*/
public NationType getNationType(String id) {
return getAlreadyInitializedType(id, NationType.class);
}
public NationType getDefaultNationType() {
return getNationType(DEFAULT_NATION_TYPE);
}
// -- Nations --
public List<Nation> getNations() {
return nations;
}
public List<Nation> getEuropeanNations() {
return europeanNations;
}
public List<Nation> getIndianNations() {
return indianNations;
}
public List<Nation> getREFNations() {
return REFNations;
}
/**
* Get a nation by identifier.
*
* @param id The object identifier.
* @return The {@code Nation} found.
*/
public Nation getNation(String id) {
return getAlreadyInitializedType(id, Nation.class);
}
/**
* Get the special unknown enemy nation.
*
* @return The unknown enemy {@code Nation}.
*/
public Nation getUnknownEnemyNation() {
return getNation(Nation.UNKNOWN_NATION_ID);
}
/**
* Clear all European advantages. Implements the Advantages==NONE setting.
*/
public void clearEuropeanNationalAdvantages() {
for (Nation n : getEuropeanNations()) {
n.setType(getDefaultNationType());
}
}
// -- Resources --
public List<ResourceType> getResourceTypeList() {
return resourceTypeList;
}
/**
* Get a resource type by identifier.
*
* @param id The object identifier.
* @return The {@code ResourceType} found.
*/
public ResourceType getResourceType(String id) {
return getAlreadyInitializedType(id, ResourceType.class);
}
// -- Roles --
/**
* Get all the available roles.
*
* @return A list of available {@code Role}s.
*/
public List<Role> getRolesList() {
return this.roles;
}
/**
* Get all the available roles as a stream.
*
* @return A stream of available {@code Role}s.
*/
public Stream<Role> getRoles() {
return getRolesList().stream();
}
/**
* Get a role by identifier.
*
* @param id The object identifier.
* @return The {@code Role} found.
*/
public Role getRole(String id) {
return getAlreadyInitializedType(id, Role.class);
}
/**
* Get the default role.
*
* @return The default {@code Role}.
*/
public Role getDefaultRole() {
return getRole(DEFAULT_ROLE_ID);
}
/**
* Get the military roles in this specification, in decreasing order
* of effectiveness.
*
* @return An unmodifiable list of military {@code Role}s.
*/
public List<Role> getMilitaryRolesList() {
if (this.militaryRoles == null) {
this.militaryRoles = Collections.<Role>unmodifiableList(
transform(this.roles, Role::isOffensive,
Function.<Role>identity(),
Role.militaryComparator));
}
return this.militaryRoles;
}
/**
* Get the available military roles as a stream.
*
* @return A stream of military {@code Role}s.
*/
public Stream<Role> getMilitaryRoles() {
return getMilitaryRolesList().stream();
}
/**
* Gets the roles suitable for a REF unit.
*
* @param naval If true, choose roles for naval units, if not, land units.
* @return A list of {@code Role}s suitable for REF units.
*/
public List<Role> getREFRolesList(boolean naval) {
return transform(((naval) ? Stream.of(getDefaultRole())
: getMilitaryRoles()),
r -> r.requiresAbility(Ability.REF_UNIT));
}
/**
* Gets the roles suitable for a REF unit as a stream.
*
* @param naval If true, choose roles for naval units, if not, land units.
* @return A stream of {@code Role}s suitable for REF units.
*/
public Stream<Role> getREFRoles(boolean naval) {
return getREFRolesList(naval).stream();
}
/**
* Get a role with an ability.
*
* @param id The ability identifier to look for.
* @param roles An optional list of {@code Role}s to look in,
* if null all roles are used.
* @return The first {@code Role} found with the required
* ability, or null if none found.
*/
public Role getRoleWithAbility(String id, List<Role> roles) {
return find(getRoles(), r -> r.hasAbility(id));
}
/**
* Get the missionary role.
*
* @return The missionary {@code Role}.
*/
public Role getMissionaryRole() {
return getRoleWithAbility(Ability.ESTABLISH_MISSION, null);
}
/**
* Get the pioneer role.
*
* @return The pioneer {@code Role}.
*/
public Role getPioneerRole() {
return getRoleWithAbility(Ability.IMPROVE_TERRAIN, null);
}
/**
* Get the scout role.
*
* @return The scout {@code Role}.
*/
public Role getScoutRole() {
return getRoleWithAbility(Ability.SPEAK_WITH_CHIEF, null);
}
// -- Tiles --
public List<TileType> getTileTypeList() {
return tileTypeList;
}
public List<TileType> getHillsTileTypeList() {
return getTileTypeList().stream().filter(t -> t.isHills()).collect(Collectors.toList());
}
public List<TileType> getMountainsTileTypeList() {
return getTileTypeList().stream().filter(t -> t.isMountains()).collect(Collectors.toList());
}
/**
* Get a tile type by identifier.
*
* @param id The object identifier.
* @return The {@code TileType} found.
*/
public TileType getTileType(String id) {
return getAlreadyInitializedType(id, TileType.class);
}
// -- UnitChangeTypes --
/**
* Get the list of all unit change types.
*
* @return The unit change type list.
*/
public List<UnitChangeType> getUnitChangeTypeList() {
return unitChangeTypeList;
}
/**
* Get a specific type of unit change type.
*
* Suitable indexing constants are in UnitChangeType.
*
* @param id The identifier for the required change type.
* @return The {@code UnitChangeType} found, or null if none present.
*/
public UnitChangeType getUnitChangeType(String id) {
return find(unitChangeTypeList,
matchKeyEquals(id, UnitChangeType::getId));
}
/**
* Get a specific unit change for a given unit change type and
* source unit type to change.
*
* Suitable indexing constants are in UnitChangeType.
*
* @param id The identifier for the required change type.
* @param fromType The {@code UnitType} to change.
* @return A list of {@code UnitChange}s.
*/
public List<UnitTypeChange> getUnitChanges(String id, UnitType fromType) {
UnitChangeType uct = getUnitChangeType(id);
return (uct == null) ? Collections.<UnitTypeChange>emptyList()
: uct.getUnitChanges(fromType);
}
/**
* Get a specific unit change for a given unit change type, a
* source unit type to change, and a destination unit type.
*
* Suitable indexing constants are in UnitChangeType.
*
* @param id The identifier for the required change type.
* @param fromType The {@code UnitType} to change from.
* @return The {@code UnitChange} found, or null if the
* change is impossible.
*/
public UnitTypeChange getUnitChange(String id, UnitType fromType) {
return getUnitChange(id, fromType, null);
}
/**
* Get a specific unit change for a given unit change type, a
* source unit type to change, and a destination unit type.
*
* Suitable indexing constants are in UnitChangeType.
*
* @param id The identifier for the required change type.
* @param fromType The {@code UnitType} to change from.
* @param toType The {@code UnitType} to change to.
* @return The {@code UnitChange} found, or null if the
* change is impossible.
*/
public UnitTypeChange getUnitChange(String id, UnitType fromType,
UnitType toType) {
UnitChangeType uct = getUnitChangeType(id);
return (uct == null) ? null
: uct.getUnitChange(fromType, toType);
}
/**
* Gets the number of turns a unit has to train to educate a student.
* This value is only meaningful for units that can be put in a school.
*
* @param typeTeacher The teacher {@code UnitType}.
* @param typeStudent the student {@code UnitType}.
* @return The turns of training needed.
*/
public int getNeededTurnsOfTraining(UnitType typeTeacher,
UnitType typeStudent) {
UnitType learn = typeStudent.getTeachingType(typeTeacher);
if (learn == null) {
throw new RuntimeException("Can not learn: teacher=" + typeTeacher
+ " student=" + typeStudent);
}
return getUnitChange(UnitChangeType.EDUCATION, typeStudent, learn).turns;
}
// -- Units --
public List<UnitType> getUnitTypeList() {
return unitTypeList;
}
/**
* Get the most vanilla unit type for a given player.
*
* @param player The {@code Player} to find the default
* unit type for, or null indicating a normal player nation
* (i.e. non-REF European).
* @return The default unit type.
*/
public UnitType getDefaultUnitType(Player player) {
return (player == null) ? getDefaultUnitType()
: getDefaultUnitType(player.getNationType());
}
/**
* Get the most vanilla unit type for a type of nation.
*
* Provides a type to use to make a neutral comparison of the
* productivity of work locations.
*
* @param nationType The {@code NationType} to find the default
* unit type for, or null indicating a normal player nation
* (i.e. non-REF European).
* @return The free colonist unit type.
*/
public UnitType getDefaultUnitType(NationType nationType) {
final Predicate<UnitType> p = (nationType == null)
? ut -> !ut.hasAbility(Ability.BORN_IN_INDIAN_SETTLEMENT)
&& !ut.hasAbility(Ability.REF_UNIT)
: (nationType.isIndian())
? ut -> ut.hasAbility(Ability.BORN_IN_INDIAN_SETTLEMENT)
&& !ut.hasAbility(Ability.REF_UNIT)
: (nationType.isREF())
? ut -> !ut.hasAbility(Ability.BORN_IN_INDIAN_SETTLEMENT)
&& ut.hasAbility(Ability.REF_UNIT)
: ut -> !ut.hasAbility(Ability.BORN_IN_INDIAN_SETTLEMENT)
&& !ut.hasAbility(Ability.REF_UNIT);
return find(defaultUnitTypes, p, getDefaultUnitType());
}
/**
* Get the most vanilla unit type.
*
* @return The default unit type.
*/
public UnitType getDefaultUnitType() {
return getUnitType("model.unit.freeColonist"); // Drop this soon
}
/**
* Get the list of buildable unit types.
*
* @return The list of buildable unit types.
*/
public List<UnitType> getBuildableUnitTypes() {
return buildableUnitTypes;
}
/**
* Get the unit type that is the expert for producing a type of goods.
*
* @param goodsType The {@code GoodsType} to check.
* @return The expert {@code UnitType}, or null if none.
*/
public UnitType getExpertForProducing(GoodsType goodsType) {
return experts.get(goodsType);
}
/**
* Get the unit types which have any of the given abilities
*
* @param abilities The abilities for the search
* @return A list of {@code UnitType}s with the abilities.
*/
public List<UnitType> getUnitTypesWithAbility(String... abilities) {
return getTypesWithAbility(UnitType.class, abilities);
}
/**
* Get the unit types which have none of the given abilities
*
* @param abilities The abilities for the search
* @return A list of {@code UnitType}s without the abilities.
*/
public List<UnitType> getUnitTypesWithoutAbility(String... abilities) {
return getTypesWithoutAbility(UnitType.class, abilities);
}
/**
* Gets the unit types that can be trained in Europe.
*
* @param player The player that want to purchase the unit.
* @return A list of Europe-trainable {@code UnitType}s.
*/
public List<UnitType> getUnitTypesTrainedInEurope(Player player) {
return unitTypesTrainedInEurope.stream()
.filter(type -> type.isAvailableTo(player))
.collect(Collectors.toList());
}
/**
* Get the unit types that can be purchased in Europe for a given player.
*
* @param player The player that want to purchase the unit.
* @return A list of Europe-purchasable {@code UnitType}s.
*/
public List<UnitType> getUnitTypesPurchasedInEurope(Player player) {
return unitTypesPurchasedInEurope.stream()
.filter(type -> type.isAvailableTo(player))
.collect(Collectors.toList());
}
/**
* Gets the fastest land unit type in this specification.
*
* @return The fastest land unit type.
*/
public UnitType getFastestLandUnitType() {
return fastestLandUnitType;
}
/**
* Gets the fastest naval unit type in this specification.
*
* @return The fastest naval unit type.
*/
public UnitType getFastestNavalUnitType() {
return fastestNavalUnitType;
}
/**
* Gets the REF unit types.
*
* @param naval If true, choose naval units, if not, land units.
* @return A list of {@code UnitType}s allowed for the REF.
*/
public List<UnitType> getREFUnitTypes(boolean naval) {
return transform(getUnitTypesWithAbility(Ability.REF_UNIT),
matchKey(naval, UnitType::isNaval));
}
/**
* Get a unit type by identifier.
*
* @param id The object identifier.
* @return The {@code UnitType} found.
*/
public UnitType getUnitType(String id) {
return getAlreadyInitializedType(id, UnitType.class);
}
// General type retrieval
/**
* Find the {@code FreeColSpecObjectType} with the given identifier.
*
* @param <T> The actual return type.
* @param id The object identifier to look for.
* @param returnClass The expected {@code Class}.
* @return The {@code FreeColSpecObjectType} found.
*/
public <T extends FreeColSpecObjectType> T findType(String id,
Class<T> returnClass) {
T o = getAlreadyInitializedType(id, returnClass);
if (o != null) return o;
if (initialized) {
throw new RuntimeException("Undefined FCGOT: " + id);
}
// forward declaration of new type
o = newType(id, returnClass);
addType(id, o);
return o;
}
/**
* Build a new {@code FreeColSpecObjectType} with given identifier
* and class.
*
* @param <T> The actual return type.
* @param id The identifier to look for.
* @param returnClass The expected {@code Class}.
* @return The new {@code FreeColSpecObjectType} or null on error.
*/
private <T extends FreeColSpecObjectType> T newType(String id,
Class<T> returnClass) {
try {
return Introspector.instantiate(returnClass, newTypeClasses,
new Object[] { id, this });
} catch (Introspector.IntrospectorException ie) {
logger.log(Level.WARNING, "newType(" + id
+ "," + returnClass.getName() + ") failed", ie);
}
return null;
}
/**
* Get the FreeColSpecObjectTypes that provide the required ability.
*
* @param id The object identifier.
* @param value The ability value to check.
* @return A list of {@code FreeColSpecObjectType}s that
* provide the required ability.
*/
public List<FreeColSpecObjectType> getTypesProviding(String id,
boolean value) {
return transform(getAbilities(id),
a -> a.getValue() == value
&& a.getSource() instanceof FreeColSpecObjectType,
a -> (FreeColSpecObjectType)a.getSource());
}
/**
* Get all types which have any of the given abilities.
*
* @param <T> The actual return type.
* @param resultType The expected result type.
* @param abilities The abilities for the search.
* @return A list of {@code FreeColSpecObjectType}s with at
* least one of the given abilities.
*/
public <T extends FreeColSpecObjectType> List<T>
getTypesWithAbility(Class<T> resultType,
String... abilities) {
return transform(allTypes.values(),
type -> resultType.isInstance(type)
&& !type.isAbstractType()
&& any(abilities, a -> type.hasAbility(a)),
type -> resultType.cast(type));
}
/**
* Get all types which have none of the given abilities.
*
* @param <T> The actual return type.
* @param resultType The expected result type.
* @param abilities The abilities for the search
* @return A list of {@code FreeColSpecObjectType}s without the
* given abilities.
*/
public <T extends FreeColSpecObjectType> List<T>
getTypesWithoutAbility(Class<T> resultType,
String... abilities) {
return transform(allTypes.values(),
type -> resultType.isInstance(type)
&& !type.isAbstractType()
&& none(abilities, a -> type.hasAbility(a)),
type -> resultType.cast(type));
}
// Backward compatibility fixes.
// We do not pretend to fix everything, some specs are just too broken,
// or some changes too radical. But we make an effort.
/**
* Apply all the special fixes to bring older specifications up to date.
*
* @return True if a change was made.
*/
private boolean applyFixes() {
boolean ret = false;
ret |= fixDifficultyOptions();
ret |= fixGameOptions();
ret |= fixMapGeneratorOptions();
ret |= fixOrphanOptions();
// @compat 0.11.0
ret |= fixRoles();
// end @compat 0.11.0
// @compat 0.11.6
ret |= fixUnitChanges();
// end @compat 0.11.6
ret |= fixSpec();
return ret;
}
/**
* Find and remove orphan options/groups that are not part of the
* core tree of options.
*
* @return True if a fix was made.
*/
private boolean fixOrphanOptions() {
Collection<AbstractOption> allO = new HashSet<>(allOptions.values());
Collection<AbstractOption> allG = new HashSet<>(allOptionGroups.values());
for (String id : coreOptionGroups) {
dropOptions(allOptionGroups.get(id), allO);
dropOptions(allOptionGroups.get(id), allG);
}
for (AbstractOption ao : allO) {
// Drop from actual allOptions map
dropOptions(ao, allOptions.values());
if (ao instanceof OptionGroup) allOptionGroups.remove(ao.getId());
logger.warning("Dropping orphan option: " + ao);
}
for (AbstractOption ao : allG) {
allOptionGroups.remove(ao.getId());
logger.warning("Dropping orphan option group: " + ao);
}
return !allO.isEmpty() || !allG.isEmpty();
}
/**
* Drop an option and its descendents from a collection.
*
* @param o The {@code AbstractOption} to drop.
* @param all The collection of options to drop from.
*/
private void dropOptions(AbstractOption o, Collection<AbstractOption> all) {
all.remove(o);
if (o instanceof OptionGroup) {
OptionGroup og = (OptionGroup)o;
for (Option op : og.getOptions()) {
if (op instanceof AbstractOption) {
dropOptions((AbstractOption)op, all);
}
}
}
}
// @compat 0.11.0
/**
* Handle the reworking of roles that landed in 0.11.0.
*
* Deliberately not part of applyFixes(), this is called from readFromXML
* which can most accurately determine whether it is needed.
*
* @return True if a fix was made.
*/
private boolean fixRoles() {
if (!roles.isEmpty()) return false; // Trust what is there
if (compareVersion("0.85") > 0) return false;
File rolf = FreeColDirectories.getCompatibilityFile(ROLES_COMPAT_FILE_NAME);
try (InputStream fis = Files.newInputStream(rolf.toPath())) {
load(fis);
} catch (IOException|XMLStreamException e) {
logger.log(Level.WARNING, "Failed to load remedial roles.", e);
return false;
}
logger.info("Loading role backward compatibility fragment: "
+ ROLES_COMPAT_FILE_NAME + " with roles: "
+ transform(getRoles(), alwaysTrue(), Role::getId,
Collectors.joining(" ")));
return true;
}
// end @compat 0.11.0
// @compat 0.11.6
private boolean fixUnitChanges() {
if (compareVersion("0.116") > 0) return false;
// Unlike roles, we can not trust what is already there, as the changes
// are deeper. However we preserve the ENTER_COLONY change as that
// comes in from the convertUpgrade mod.
UnitChangeType enter = find(unitChangeTypeList,
matchKeyEquals(UnitChangeType.ENTER_COLONY, UnitChangeType::getId));
File uctf = FreeColDirectories.getCompatibilityFile(UNIT_CHANGE_TYPES_COMPAT_FILE_NAME);
try (InputStream fis = Files.newInputStream(uctf.toPath())) {
load(fis);
} catch (IOException|XMLStreamException e) {
logger.log(Level.WARNING, "Failed to load unit changes.", e);
return false;
}
if (enter != null) {
unitChangeTypeList.add(enter);
}
logger.info("Loading unit-changes backward compatibility fragment: "
+ UNIT_CHANGE_TYPES_COMPAT_FILE_NAME + " with changes: "
+ transform(getUnitChangeTypeList(), alwaysTrue(),
UnitChangeType::getId, Collectors.joining(" ")));
return true;
}
// end @compat 0.11.6
/**
* Backward compatibility for the Specification in general.
*
* @return True if a fix was made.
*/
private boolean fixSpec() {
boolean ret = false;
// @compat 0.10.x/0.11.x
// This section contains a bunch of things that were
// discovered and fixed post 0.11.0.
//
// We need to keep these fixes for now as they are still
// needed for games validly upgraded by 0.11.x series FreeCol.
// 0.10.x had no unknown enemy nation, just an unknown enemy
// player, and the type was poorly established.
Nation ue = findType(Nation.UNKNOWN_NATION_ID, Nation.class);
ue.setType(getNationType("model.nationType.default"));
if (!nations.contains(ue)) {
nations.add(ue);
ret = true;
}
// Nation colour moved (back) into the spec in 0.11.0, but the fixup
// code was just a wrapper, and did not modify the underlying spec.
for (Nation nation : nations) {
if (nation.getColor() == null) {
nation.setColor(defaultColors.get(nation.getId()));
ret = true;
}
}
// Coronado gained an ability in the freecol ruleset
FoundingFather coronado
= getFoundingFather("model.foundingFather.franciscoDeCoronado");
if ("freecol".equals(getId())
&& !coronado.hasAbility(Ability.SEE_ALL_COLONIES)) {
coronado.addAbility(new Ability(Ability.SEE_ALL_COLONIES,
coronado, true));
ret = true;
}
// Fix REF roles, soldier -> infantry, dragoon -> cavalry
// Older specs (<= 0.10.5 ?) had refSize directly under difficulty
// level, later moved it under the monarch group.
for (OptionGroup level : getDifficultyLevels()) {
Option monarch = level.getOption(GameOptions.DIFFICULTY_MONARCH);
Option refSize = ((OptionGroup)((monarch instanceof OptionGroup)
? monarch : level)).getOption(GameOptions.REF_FORCE);
if (!(refSize instanceof UnitListOption)) continue;
boolean changed;
for (AbstractUnit au
: ((UnitListOption)refSize).getOptionValues()) {
changed = true;
if ("DEFAULT".equals(au.getRoleId())) {
au.setRoleId(DEFAULT_ROLE_ID);
} else if ("model.role.soldier".equals(au.getRoleId())
|| "SOLDIER".equals(au.getRoleId())) {
au.setRoleId("model.role.infantry");
} else if ("model.role.dragoon".equals(au.getRoleId())
|| "DRAGOON".equals(au.getRoleId())) {
au.setRoleId("model.role.cavalry");
} else {
changed = false;
}
if (changed) ret = true;
}
}
// Fix all other UnitListOptions
List<Option> todo = new ArrayList<>(getDifficultyLevels());
while (!todo.isEmpty()) {
Option o = todo.remove(0);
if (o instanceof OptionGroup) {
List<Option> next = ((OptionGroup)o).getOptions();
todo.addAll(new ArrayList<>(next));
} else if (o instanceof UnitListOption) {
boolean changed;
for (AbstractUnit au : ((UnitListOption)o).getOptionValues()) {
String roleId = au.getRoleId();
changed = true;
if (roleId == null) {
au.setRoleId(DEFAULT_ROLE_ID);
} else if (au.getRoleId().startsWith("model.role.")) {
changed = false; // OK
} else if ("DEFAULT".equals(au.getRoleId())) {
au.setRoleId(DEFAULT_ROLE_ID);
} else if ("DRAGOON".equals(au.getRoleId())) {
au.setRoleId("model.role.dragoon");
} else if ("MISSIONARY".equals(au.getRoleId())) {
au.setRoleId("model.role.missionary");
} else if ("PIONEER".equals(au.getRoleId())) {
au.setRoleId("model.role.pioneer");
} else if ("SCOUT".equals(au.getRoleId())) {
au.setRoleId("model.role.scout");
} else if ("SOLDIER".equals(au.getRoleId())) {
au.setRoleId("model.role.soldier");
} else {
au.setRoleId(DEFAULT_ROLE_ID);
}
if (changed) ret = true;
}
}
}
// is-military was added to goods type
GoodsType goodsType = getGoodsType("model.goods.horses");
if (!goodsType.getMilitary()) {
goodsType.setMilitary();
ret = true;
}
goodsType = getGoodsType("model.goods.muskets");
if (!goodsType.getMilitary()) {
goodsType.setMilitary();
ret = true;
}
// end @compat 0.10.x/0.11.x
// @compat 0.11.0
// Bolivar changed from being an event, then to a liberty modifier,
// and now to a SoL% modifier.
FoundingFather bolivar
= getFoundingFather("model.foundingFather.simonBolivar");
boolean bolivarAdd = false;
if (!bolivar.getEvents().isEmpty()) {
bolivar.setEvents(Collections.<Event>emptyList());
bolivarAdd = true;
} else if (bolivar.hasModifier(Modifier.LIBERTY)) {
bolivar.removeModifiers(Modifier.LIBERTY);
bolivarAdd = true;
}
if (bolivarAdd) {
bolivar.addModifier(new Modifier(Modifier.SOL, 20,
Modifier.ModifierType.ADDITIVE, bolivar, 0));
ret = true;
}
// The COASTAL_ONLY attribute was added to customs house.
BuildingType customs = getBuildingType("model.building.customHouse");
if (!customs.hasAbility(Ability.COASTAL_ONLY)) {
customs.addAbility(new Ability(Ability.COASTAL_ONLY, null, false));
ret = true;
}
// end @compat 0.11.0
// @compat 0.11.3
// Added the cargo penalty modifier
if (none(getModifiers(Modifier.CARGO_PENALTY))) {
addModifier(new Modifier(Modifier.CARGO_PENALTY, -12.5f,
Modifier.ModifierType.PERCENTAGE, CARGO_PENALTY_SOURCE,
Modifier.GENERAL_COMBAT_INDEX));
ret = true;
}
// Backwards compatibility for the fixes to BR#2873.
Event event = getEvent("model.event.declareIndependence");
if (event != null) {
Limit limit = event.getLimit("model.limit.independence.coastalColonies");
if (limit != null) {
limit.setOperator(Limit.Operator.GE);
limit.getRightHandSide().setValue(1);
ret = true;
}
}
// end @compat 0.11.3
// @compat 0.11.5
// Added a modifier to hardy pioneer
UnitType hardyPioneer = getUnitType("model.unit.hardyPioneer");
if (none(hardyPioneer.getModifiers(Modifier.TILE_TYPE_CHANGE_PRODUCTION))) {
Modifier m = new Modifier(Modifier.TILE_TYPE_CHANGE_PRODUCTION,
2.0f, Modifier.ModifierType.MULTIPLICATIVE);
Scope scope = new Scope();
scope.setType("model.goods.lumber");
m.addScope(scope);
hardyPioneer.addModifier(m);
ret = true;
}
// Added modifier to Coronado
if (!coronado.hasModifier(Modifier.EXPOSED_TILES_RADIUS)) {
coronado.addModifier(new Modifier(Modifier.EXPOSED_TILES_RADIUS,
3.0f, Modifier.ModifierType.ADDITIVE, coronado, 0));
ret = true;
}
// end @compat 0.11.5
// @compat 0.11.6
// PlunderType became a FCSOT and thus gained identifiers
for (IndianNationType nt : indianNationTypes) {
for (SettlementType st : nt.getSettlementTypes()) {
for (PlunderType pt : st.getPlunderTypes()) {
if (pt.getId() == null) {
Scope scope = find(pt.getScopes(),
matchKey("model.ability.plunderNatives",
Scope::getAbilityId));
String id = "plunder." + nt.getSuffix()
+ ((st.isCapital()) ? ".capital" : "")
+ ((scope == null) ? "" : ".extra");
pt.setId(id);
ret = true;
}
}
}
}
// <upgrade> element dropped from FoundingFather, replaced by an
// ability. Only impacts Casas.
FoundingFather casas
= getFoundingFather("model.foundingFather.bartolomeDeLasCasas");
if (casas != null && !casas.hasAbility(Ability.UPGRADE_CONVERT)) {
Ability uc = new Ability(Ability.UPGRADE_CONVERT, casas, true);
addAbility(uc);
casas.addAbility(uc);
ret = true;
}
// Added mercenary-price to manOWar
UnitType mow = getUnitType("model.unit.manOWar");
if (mow != null && mow.getMercenaryPrice() < 0) {
mow.setMercenaryPrice(10000);
ret = true;
}
// Old manOWars required independenceDeclared which was moved to
// independentNation a while back, but surfaced again in old games
final String maidId = "model.ability.independenceDeclared";
if (mow != null && mow.requiresAbility(maidId)) {
mow.removeRequiredAbility(maidId);
mow.addRequiredAbility(Ability.INDEPENDENT_NATION, true);
ret = true;
}
// Added canBeSurrendered ability to all the REF units.
// REF Units are the units that can be *added* to the REF.
// The REF can capture other units, but they can all automatically
// be surrendered, whereas of the actual REF units, only the artillery
// can be handed over to the victorious independent nation.
for (UnitType ut : getUnitTypesWithAbility(Ability.REF_UNIT)) {
if (!ut.containsAbilityKey(Ability.CAN_BE_SURRENDERED)) {
ut.addAbility(new Ability(Ability.CAN_BE_SURRENDERED, null,
"model.unit.artillery".equals(ut.getId())));
ret = true;
}
}
// end @compat 0.11.6
return ret;
}
/**
* Backward compatibility code to make sure this specification
* contains a default value for every difficulty option.
*
* When a new difficulty option is added to the spec, add a
* sensible default here.
*
* @return True if an option was missing and added.
*/
private boolean fixDifficultyOptions() {
boolean ret = false;
String id;
AbstractOption op;
OptionGroup og;
UnitListOption ulo;
LogBuilder lb = new LogBuilder(64);
lb.mark();
// SAVEGAME_VERSION == 11
// SAVEGAME_VERSION == 12
// @compat 0.10.x/0.11.x
// For 0.10.6 we moved from a three level structure:
// difficultyLevels/<difficulty>/<option>
// to a four level structure
// difficultyLevels/<difficulty>/<group>/<option>
// so add the groups in, and move the options that could be present
// in anything up to 0.10.5 into their destination groups.
//
// Alas, there were weaknesses in the checkDifficulty*()
// routines that were not fixed through 0.11.x, so some 0.10.x
// games were only partially upgraded to the 0.11.x format.
//
// So keep this around until SAVEGAME_VERSION == 15.
ret |= checkDifficultyOptionGroup(GameOptions.DIFFICULTY_IMMIGRATION, lb,
GameOptions.CROSSES_INCREMENT,
GameOptions.RECRUIT_PRICE_INCREASE,
GameOptions.LOWER_CAP_INCREASE,
GameOptions.PRICE_INCREASE + ".artillery",
GameOptions.PRICE_INCREASE_PER_TYPE,
GameOptions.EXPERT_STARTING_UNITS,
GameOptions.IMMIGRANTS);
ret |= checkDifficultyOptionGroup(GameOptions.DIFFICULTY_NATIVES, lb,
GameOptions.LAND_PRICE_FACTOR,
GameOptions.NATIVE_CONVERT_PROBABILITY,
GameOptions.BURN_PROBABILITY,
GameOptions.NATIVE_DEMANDS,
GameOptions.RUMOUR_DIFFICULTY,
GameOptions.SHIP_TRADE_PENALTY,
GameOptions.BUILD_ON_NATIVE_LAND,
GameOptions.SETTLEMENT_NUMBER);
ret |= checkDifficultyOptionGroup(GameOptions.DIFFICULTY_MONARCH, lb,
GameOptions.MONARCH_MEDDLING,
GameOptions.TAX_ADJUSTMENT,
GameOptions.MERCENARY_PRICE,
GameOptions.MAXIMUM_TAX,
GameOptions.MONARCH_SUPPORT,
GameOptions.TREASURE_TRANSPORT_FEE,
GameOptions.REF_FORCE,
GameOptions.INTERVENTION_BELLS,
GameOptions.INTERVENTION_TURNS,
GameOptions.INTERVENTION_FORCE,
GameOptions.MERCENARY_FORCE);
ret |= checkDifficultyOptionGroup(GameOptions.DIFFICULTY_GOVERNMENT, lb,
GameOptions.BAD_GOVERNMENT_LIMIT,
GameOptions.VERY_BAD_GOVERNMENT_LIMIT,
GameOptions.GOOD_GOVERNMENT_LIMIT,
GameOptions.VERY_GOOD_GOVERNMENT_LIMIT);
ret |= checkDifficultyOptionGroup(GameOptions.DIFFICULTY_OTHER, lb,
GameOptions.STARTING_MONEY,
GameOptions.FOUNDING_FATHER_FACTOR,
GameOptions.ARREARS_FACTOR,
GameOptions.UNITS_THAT_USE_NO_BELLS,
GameOptions.TILE_PRODUCTION);
ret |= checkDifficultyOptionGroup(GameOptions.DIFFICULTY_CHEAT, lb,
GameOptions.LIFT_BOYCOTT_CHEAT,
GameOptions.EQUIP_SCOUT_CHEAT,
GameOptions.LAND_UNIT_CHEAT,
GameOptions.OFFENSIVE_NAVAL_UNIT_CHEAT,
GameOptions.TRANSPORT_NAVAL_UNIT_CHEAT);
id = GameOptions.REF_FORCE; // Yes, really "refSize"
ulo = checkDifficultyUnitListOption(id, GameOptions.DIFFICULTY_MONARCH, lb);
if (ulo != null) {
AbstractUnitOption regulars
= new AbstractUnitOption(id + ".regulars", this);
regulars.setValue(new AbstractUnit("model.unit.kingsRegular",
"model.role.infantry", 31));
ulo.getValue().add(regulars);
AbstractUnitOption dragoons
= new AbstractUnitOption(id + ".dragoons", this);
dragoons.setValue(new AbstractUnit("model.unit.kingsRegular",
"model.role.cavalry", 15));
ulo.getValue().add(dragoons);
AbstractUnitOption artillery
= new AbstractUnitOption(id + ".artillery", this);
artillery.setValue(new AbstractUnit("model.unit.artillery",
DEFAULT_ROLE_ID, 14));
ulo.getValue().add(artillery);
AbstractUnitOption menOfWar
= new AbstractUnitOption(id + ".menOfWar", this);
menOfWar.setValue(new AbstractUnit("model.unit.manOWar",
DEFAULT_ROLE_ID, 8));
ulo.getValue().add(menOfWar);
ret = true;
}
id = GameOptions.IMMIGRANTS;
ulo = checkDifficultyUnitListOption(id, GameOptions.DIFFICULTY_IMMIGRATION, lb);
if (ulo != null) {
AbstractUnitOption i1
= new AbstractUnitOption(id + ".1", this);
i1.setValue(new AbstractUnit("model.unit.masterCarpenter",
DEFAULT_ROLE_ID, 1));
ulo.getValue().add(i1);
ret = true;
}
ret |= checkDifficultyIntegerOption(GameOptions.INTERVENTION_BELLS,
GameOptions.DIFFICULTY_MONARCH, lb,
5000);
ret |= checkDifficultyIntegerOption(GameOptions.INTERVENTION_TURNS,
GameOptions.DIFFICULTY_MONARCH, lb,
52);
id = GameOptions.INTERVENTION_FORCE;
ulo = checkDifficultyUnitListOption(id, GameOptions.DIFFICULTY_MONARCH, lb);
if (ulo != null) {
AbstractUnitOption regulars
= new AbstractUnitOption(id + ".regulars", this);
regulars.setValue(new AbstractUnit("model.unit.colonialRegular",
"model.role.soldier", 2));
ulo.getValue().add(regulars);
AbstractUnitOption dragoons
= new AbstractUnitOption(id + ".dragoons", this);
dragoons.setValue(new AbstractUnit("model.unit.colonialRegular",
"model.role.dragoon", 2));
ulo.getValue().add(dragoons);
AbstractUnitOption artillery
= new AbstractUnitOption(id + ".artillery", this);
artillery.setValue(new AbstractUnit("model.unit.artillery",
DEFAULT_ROLE_ID, 2));
ulo.getValue().add(artillery);
AbstractUnitOption menOfWar
= new AbstractUnitOption(id + ".menOfWar", this);
menOfWar.setValue(new AbstractUnit("model.unit.manOWar",
DEFAULT_ROLE_ID, 2));
ulo.getValue().add(menOfWar);
ret = true;
}
id = GameOptions.MERCENARY_FORCE;
ulo = checkDifficultyUnitListOption(id, GameOptions.DIFFICULTY_MONARCH, lb);
if (ulo != null) {
AbstractUnitOption regulars
= new AbstractUnitOption(id + ".regulars", this);
regulars.setValue(new AbstractUnit("model.unit.veteranSoldier",
"model.role.soldier", 2));
ulo.getValue().add(regulars);
AbstractUnitOption dragoons
= new AbstractUnitOption(id + ".dragoons", this);
dragoons.setValue(new AbstractUnit("model.unit.veteranSoldier",
"model.role.dragoon", 2));
ulo.getValue().add(dragoons);
AbstractUnitOption artillery
= new AbstractUnitOption(id + ".artillery", this);
artillery.setValue(new AbstractUnit("model.unit.artillery",
DEFAULT_ROLE_ID, 2));
ulo.getValue().add(artillery);
AbstractUnitOption menOfWar
= new AbstractUnitOption(id + ".menOfWar", this);
menOfWar.setValue(new AbstractUnit("model.unit.manOWar",
DEFAULT_ROLE_ID, 2));
ulo.getValue().add(menOfWar);
ret = true;
}
ret |= checkDifficultyIntegerOption(GameOptions.GOOD_GOVERNMENT_LIMIT,
GameOptions.DIFFICULTY_GOVERNMENT, lb,
50);
ret |= checkDifficultyIntegerOption(GameOptions.VERY_GOOD_GOVERNMENT_LIMIT,
GameOptions.DIFFICULTY_GOVERNMENT, lb,
100);
ret |= checkDifficultyIntegerOption(GameOptions.LIFT_BOYCOTT_CHEAT,
GameOptions.DIFFICULTY_CHEAT, lb,
10);
ret |= checkDifficultyIntegerOption(GameOptions.EQUIP_SCOUT_CHEAT,
GameOptions.DIFFICULTY_CHEAT, lb,
10);
ret |= checkDifficultyIntegerOption(GameOptions.LAND_UNIT_CHEAT,
GameOptions.DIFFICULTY_CHEAT, lb,
10);
ret |= checkDifficultyIntegerOption(GameOptions.OFFENSIVE_NAVAL_UNIT_CHEAT,
GameOptions.DIFFICULTY_CHEAT, lb,
10);
ret |= checkDifficultyIntegerOption(GameOptions.TRANSPORT_NAVAL_UNIT_CHEAT,
GameOptions.DIFFICULTY_CHEAT, lb,
10);
// end @compat 0.10.x/0.11.x
// SAVEGAME_VERSION == 13
// @compat 0.11.0
ret |= checkDifficultyIntegerOption(GameOptions.DESTROY_SETTLEMENT_SCORE,
GameOptions.DIFFICULTY_NATIVES, lb,
-5);
ret |= checkDifficultyPercentageOption(GameOptions.BAD_RUMOUR,
GameOptions.DIFFICULTY_OTHER, lb,
23);
ret |= checkDifficultyPercentageOption(GameOptions.GOOD_RUMOUR,
GameOptions.DIFFICULTY_OTHER, lb,
48);
ret |= checkDifficultyIntegerOption(GameOptions.OFFENSIVE_LAND_UNIT_CHEAT,
GameOptions.DIFFICULTY_CHEAT, lb,
4);
ret |= checkDifficultyIntegerOption(GameOptions.EQUIP_PIONEER_CHEAT,
GameOptions.DIFFICULTY_CHEAT, lb,
10);
// end @compat 0.11.0
// @compat 0.11.3
id = GameOptions.WAR_SUPPORT_FORCE;
ulo = checkDifficultyUnitListOption(id, GameOptions.DIFFICULTY_MONARCH, lb);
if (ulo != null) {
AbstractUnitOption support = new AbstractUnitOption(id, this);
support.setValue(new AbstractUnit("model.unit.veteranSoldier",
"model.role.soldier", 4));
ulo.getValue().add(support);
ret = true;
}
ret |= checkDifficultyIntegerOption(GameOptions.WAR_SUPPORT_GOLD,
GameOptions.DIFFICULTY_MONARCH, lb,
1500);
// end @compat 0.11.3
// @compat 1.0.0
ret |= checkDifficultyIntegerOption(GameOptions.TRADE_PROFIT_MULTIPLIER_CHEAT, GameOptions.DIFFICULTY_CHEAT, lb, 10);
ret |= checkDifficultyIntegerOption(GameOptions.TRADE_PROFIT_MULTIPLIER_CHEAT_TURNS, GameOptions.DIFFICULTY_CHEAT, lb, 200);
// end @compat 1.0.0
// Ensure 2 levels of groups, and one level of leaves
for (OptionGroup level : getDifficultyLevels()) {
for (Option o : level.getOptions()) {
if (o instanceof OptionGroup) {
og = (OptionGroup)o;
for (Option o2 : og.getOptions()) {
if (o2 instanceof OptionGroup) {
lb.add("?-group ", level.getId() + "/" + og.getId()
+ "/" + o2.getId());
}
}
} else {
lb.add("? ", level.getId() + "/" + o.getId());
}
}
}
if (lb.grew("Check difficulty options:")) {
lb.log(logger, Level.INFO);
}
// SAVEGAME_VERSION == 14
return ret;
}
private boolean checkDifficultyOptionGroup(String gr, LogBuilder lb,
String... ids) {
boolean ret = false;
for (OptionGroup level : getDifficultyLevels()) {
OptionGroup og = null;
for (Option o : level.getOptions()) {
if (o.getId().equals(gr) && o instanceof OptionGroup) {
og = (OptionGroup)o;
break;
}
}
if (og == null) {
level.remove(gr);
og = new OptionGroup(gr, this);
level.add(og);
og.setGroup(level.getId());
lb.add("\n +", level.getId(), "/", gr);
ret = true;
}
for (String id : ids) {
Option op = null;
for (Option o : level.getOptions()) {
if (o.getId().equals(id) && !(o instanceof OptionGroup)) {
op = o;
break;
}
}
if (op != null) {
level.remove(op.getId());
if (op instanceof AbstractOption) {
((AbstractOption)op).setGroup(og.getId());
}
og.add(op);
lb.add("\n ~", level.getId(), "/", id, " -> ",
level.getId(), "/" + og.getId());
ret = true;
}
}
}
return ret;
}
private boolean checkDifficultyIntegerOption(String id, String gr,
LogBuilder lb,
int defaultValue) {
boolean ret = false;
for (OptionGroup level : getDifficultyLevels()) {
OptionGroup og = null;
for (Option o : level.getOptions()) {
if (o.getId().equals(gr) && o instanceof OptionGroup) {
og = (OptionGroup)o;
break;
}
}
if (og == null) continue;
Option op = null;
for (Option o : level.getOptions()) {
if (o.getId().equals(id) && !(o instanceof OptionGroup)) {
op = o;
break;
}
}
if (op != null) {
level.remove(op.getId());
if (!(op instanceof IntegerOption)) {
IntegerOption iop = new IntegerOption(id, this);
iop.setValue(defaultValue);
op = iop;
}
op.setGroup(gr);
og.add(op);
lb.add("\n ~", level.getId(), "/", id, " -> ",
level.getId(), "/" + og.getId());
} else {
op = null;
for (Option o : og.getOptions()) {
if (o.getId().equals(id)) {
op = o;
break;
}
}
if (op instanceof IntegerOption) continue;
if (op != null) og.remove(id);
IntegerOption iop = new IntegerOption(id, this);
iop.setGroup(gr);
iop.setValue(defaultValue);
og.add(iop);
lb.add("\n +", level.getId(), "/", og.getId(),
"/", id, "=", defaultValue);
}
ret = true;
}
return ret;
}
private boolean checkDifficultyPercentageOption(String id, String gr,
LogBuilder lb,
int defaultValue) {
boolean ret = false;
for (OptionGroup level : getDifficultyLevels()) {
OptionGroup og = null;
for (Option o : level.getOptions()) {
if (o.getId().equals(gr) && o instanceof OptionGroup) {
og = (OptionGroup)o;
break;
}
}
if (og == null) continue;
Option op = null;
for (Option o : level.getOptions()) {
if (o.getId().equals(id) && !(o instanceof OptionGroup)) {
op = o;
break;
}
}
if (op != null) {
level.remove(op.getId());
if (!(op instanceof PercentageOption)) {
PercentageOption pop = new PercentageOption(id, this);
pop.setValue(defaultValue);
op = pop;
}
op.setGroup(gr);
og.add(op);
lb.add("\n ~", level.getId(), "/", id, " -> ",
level.getId(), "/" + og.getId());
} else {
op = null;
for (Option o : og.getOptions()) {
if (o.getId().equals(id)) {
op = o;
break;
}
}
if (op instanceof PercentageOption) continue;
if (op != null) og.remove(id);
PercentageOption pop = new PercentageOption(id, this);
pop.setValue(defaultValue);
pop.setGroup(gr);
og.add(pop);
lb.add("\n +", level.getId(), "/", og.getId(),
"/", id, "=", defaultValue);
}
ret = true;
}
return ret;
}
private UnitListOption checkDifficultyUnitListOption(String id, String gr,
LogBuilder lb) {
UnitListOption ulo = null;
for (OptionGroup level : getDifficultyLevels()) {
OptionGroup og = null;
for (Option o : level.getOptions()) {
if (o.getId().equals(gr) && o instanceof OptionGroup) {
og = (OptionGroup)o;
break;
}
}
if (og == null) continue;
Option op = null;
for (Option o : level.getOptions()) {
if (o.getId().equals(id) && !(o instanceof OptionGroup)) {
op = o;
break;
}
}
if (op != null) {
level.remove(op.getId());
if (op instanceof AbstractOption) {
((AbstractOption)op).setGroup(og.getId());
}
og.add(op);
lb.add("\n ~", level.getId(), "/", id, " -> ",
level.getId(), "/" + og.getId());
} else {
op = null;
for (Option o : og.getOptions()) {
if (o.getId().equals(id)) {
op = o;
break;
}
}
if (op instanceof UnitListOption) continue;
if (op != null) og.remove(id);
if (ulo == null) ulo = new UnitListOption(id, this);
og.add(ulo);
lb.add("\n +", level.getId(), "/", og.getId(),
"/[", id, "]");
}
}
return ulo;
}
/**
* Backward compatibility code to make sure this specification
* contains a default value for every game option.
*
* When a new game option is added to the spec, add a sensible
* default here.
*
* @return True if an option was missing and added.
*/
private boolean fixGameOptions() {
boolean ret = false;
// SAVEGAME_VERSION == 11
// SAVEGAME_VERSION == 12
// @compat 0.10.5
ret |= checkOp(GameOptions.ENABLE_UPKEEP,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.FALSE, BooleanOption.class);
ret |= checkOp(GameOptions.NATURAL_DISASTERS,
GameOptions.GAMEOPTIONS_COLONY,
0, PercentageOption.class);
ret |= checkOp(GameOptions.GIFT_PROBABILITY,
GameOptions.GAMEOPTIONS_MAP,
5, PercentageOption.class);
ret |= checkOp(GameOptions.DEMAND_PROBABILITY,
GameOptions.GAMEOPTIONS_MAP,
10, PercentageOption.class);
ret |= checkOp(GameOptions.EMPTY_TRADERS,
GameOptions.GAMEOPTIONS_MAP,
Boolean.FALSE, BooleanOption.class);
// end @compat 0.10.5
// SAVEGAME_VERSION == 13
// @compat 0.10.7
ret |= checkOp(GameOptions.ONLY_NATURAL_IMPROVEMENTS,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.TRUE, BooleanOption.class);
ret |= checkOp(GameOptions.PEACE_PROBABILITY,
GameOptions.GAMEOPTIONS_MAP,
90, PercentageOption.class);
ret |= checkOp(GameOptions.INITIAL_IMMIGRATION,
GameOptions.GAMEOPTIONS_MAP,
15, IntegerOption.class);
ret |= checkOp(GameOptions.EUROPEAN_UNIT_IMMIGRATION_PENALTY,
GameOptions.GAMEOPTIONS_MAP,
-4, IntegerOption.class);
ret |= checkOp(GameOptions.PLAYER_IMMIGRATION_BONUS,
GameOptions.GAMEOPTIONS_MAP,
2, IntegerOption.class);
ret |= checkOp(GameOptions.FOUND_COLONY_DURING_REBELLION,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.TRUE, BooleanOption.class);
// end @compat 0.10.7
// @compat 0.11.0
ret |= checkOp(GameOptions.BELL_ACCUMULATION_CAPPED,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.FALSE, BooleanOption.class);
ret |= checkOp(GameOptions.CAPTURE_UNITS_UNDER_REPAIR,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.FALSE, BooleanOption.class);
ret |= checkOp(GameOptions.PAY_FOR_BUILDING,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.TRUE, BooleanOption.class);
ret |= checkOp(GameOptions.CLEAR_HAMMERS_ON_CONSTRUCTION_SWITCH,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.FALSE, BooleanOption.class);
ret |= checkOp(GameOptions.CUSTOMS_ON_COAST,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.FALSE, BooleanOption.class);
ret |= checkOp(GameOptions.EQUIP_EUROPEAN_RECRUITS,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.TRUE, BooleanOption.class);
// end @compat 0.11.0
// @compat 0.11.3
ret |= checkOp(GameOptions.MISSION_INFLUENCE,
GameOptions.GAMEOPTIONS_MAP,
-10, IntegerOption.class);
ret |= checkOp(GameOptions.INDEPENDENCE_TURN,
GameOptions.GAMEOPTIONS_YEARS,
468, IntegerOption.class);
ret |= checkOp(GameOptions.AGES,
GameOptions.GAMEOPTIONS_YEARS,
"1600,1700", TextOption.class);
ret |= checkOp(GameOptions.SEASONS,
GameOptions.GAMEOPTIONS_YEARS,
2, IntegerOption.class);
ret |= checkOp(GameOptions.DISEMBARK_IN_COLONY,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.FALSE, BooleanOption.class);
ret |= checkOp(GameOptions.ENHANCED_TRADE_ROUTES,
GameOptions.GAMEOPTIONS_MAP,
Boolean.FALSE, BooleanOption.class);
// end @compat 0.11.3
// @compat 0.11.6
ret |= checkOp(GameOptions.SETTLEMENT_NUMBER_OF_GOODS_TO_SELL,
GameOptions.GAMEOPTIONS_MAP,
3, IntegerOption.class);
ret |= checkOp(GameOptions.ALARM_BONUS_BUY,
GameOptions.GAMEOPTIONS_MAP,
20, PercentageOption.class);
ret |= checkOp(GameOptions.ALARM_BONUS_SELL,
GameOptions.GAMEOPTIONS_MAP,
20, PercentageOption.class);
ret |= checkOp(GameOptions.ALARM_BONUS_GIFT,
GameOptions.GAMEOPTIONS_MAP,
40, PercentageOption.class);
ret |= checkOp(GameOptions.CLAIM_ALL_TILES,
GameOptions.GAMEOPTIONS_COLONY,
Boolean.FALSE, BooleanOption.class);
// end @compat 0.11.6
// @compat 1.1.0
ret |= checkOp(GameOptions.MAP_DEFINED_STARTING_POSITIONS,
GameOptions.GAMEOPTIONS_MAP,
Boolean.TRUE, BooleanOption.class);
// end @compat 1.1.0
// SAVEGAME_VERSION == 14
return ret;
}
/**
* Backward compatibility code to make sure this specification
* contains a default value for every map option.
*
* When a new map option is added to the spec, add a sensible
* default here.
*
* @return True if an option was missing and added.
*/
private boolean fixMapGeneratorOptions() {
boolean ret = false;
// SAVEGAME_VERSION == 11
// SAVEGAME_VERSION == 12
// SAVEGAME_VERSION == 13
// SAVEGAME_VERSION == 14
return ret;
}
/**
* Check if an option exists, and if not, create and install it.
*
* FIXME: Handles failure to create a needed option badly.
*
* @param <R> The underlying type encapsulated by the option.
* @param <T> The option type.
* @param id The option identifier.
* @param gr The option group identifier.
* @param defaultValue The default value of the option.
* @param returnClass The expected class of option.
* @return True if the option did not exist and was successfully created.
*/
private <R,T extends Option<R>> boolean checkOp(String id, String gr,
R defaultValue,
Class<T> returnClass) {
if (hasOption(id, returnClass)) return false;
T op;
try {
op = Introspector.instantiate(returnClass,
new Class[] { String.class, Specification.class },
new Object[] { id, this });
} catch (Introspector.IntrospectorException ie) {
logger.warning("Failed to make " + returnClass.getName()
+ ": " + id);
return false;
}
op.setGroup(gr);
op.setValue(defaultValue);
getOptionGroup(gr).add(op);
addAbstractOption((AbstractOption)op);
return true;
}
// SerializationgameAbilities
private static final String ABILITIES_TAG = "abilities";
private static final String BUILDING_TYPES_TAG = "building-types";
private static final String DIFFICULTY_LEVEL_TAG = "difficulty-level";
private static final String DISASTERS_TAG = "disasters";
private static final String EUROPEAN_NATION_TYPES_TAG = "european-nation-types";
private static final String EVENTS_TAG = "events";
private static final String FOUNDING_FATHERS_TAG = "founding-fathers";
private static final String GOODS_TYPES_TAG = "goods-types";
private static final String INDIAN_NATION_TYPES_TAG = "indian-nation-types";
private static final String MODIFIERS_TAG = "modifiers";
private static final String NATIONS_TAG = "nations";
private static final String OPTIONS_TAG = "options";
private static final String RESOURCE_TYPES_TAG = "resource-types";
private static final String ROLES_TAG = "roles";
private static final String TILE_TYPES_TAG = "tile-types";
private static final String TILE_IMPROVEMENT_TYPES_TAG = "tile-improvement-types";
private static final String UNIT_CHANGE_TYPES_TAG = "unit-change-types";
private static final String UNIT_TYPES_TAG = "unit-types";
private static final String VERSION_TAG = "version";
// @compat 0.11.3
private static final String OLD_DIFFICULTY_LEVEL_TAG = "difficultyLevel";
private static final String OLD_TILEIMPROVEMENT_TYPES_TAG = "tileimprovement-types";
// end @compat 0.11.3
// @compat 0.11.x
private static final String OLD_EQUIPMENT_TYPES_TAG = "equipment-types";
// end @compat 0.11.x
/**
* Write an XML-representation of this object to the given stream.
*
* @param xw The {@code FreeColXMLWriter} to write to.
* @exception XMLStreamException if there are any problems writing
* to the stream.
*/
protected void toXML(FreeColXMLWriter xw) throws XMLStreamException {
xw.writeStartElement(TAG);
// Add attributes
xw.writeAttribute(FreeColObject.ID_ATTRIBUTE_TAG, getId());
if (difficultyLevel != null) {
xw.writeAttribute(DIFFICULTY_LEVEL_TAG, difficultyLevel);
}
if (version != null) {
xw.writeAttribute(VERSION_TAG, version);
}
// copy the order of section in specification.xml
writeSection(xw, ABILITIES_TAG, gameAbilities);
writeSection(xw, MODIFIERS_TAG, specialModifiers);
writeSection(xw, EVENTS_TAG, events);
writeSection(xw, DISASTERS_TAG, disasters);
writeSection(xw, GOODS_TYPES_TAG, goodsTypeList);
writeSection(xw, RESOURCE_TYPES_TAG, resourceTypeList);
writeSection(xw, TILE_TYPES_TAG, tileTypeList);
writeSection(xw, ROLES_TAG, roles);
writeSection(xw, TILE_IMPROVEMENT_TYPES_TAG, tileImprovementTypeList);
writeSection(xw, UNIT_CHANGE_TYPES_TAG, unitChangeTypeList);
writeSection(xw, UNIT_TYPES_TAG, unitTypeList);
writeSection(xw, BUILDING_TYPES_TAG, buildingTypeList);
writeSection(xw, FOUNDING_FATHERS_TAG, foundingFathers);
writeSection(xw, EUROPEAN_NATION_TYPES_TAG, europeanNationTypes);
writeSection(xw, EUROPEAN_NATION_TYPES_TAG, REFNationTypes);
writeSection(xw, INDIAN_NATION_TYPES_TAG, indianNationTypes);
writeSection(xw, NATIONS_TAG, nations);
xw.writeStartElement(OPTIONS_TAG);
for (String id : coreOptionGroups) {
OptionGroup og = allOptionGroups.get(id);
if (og != null) og.toXML(xw);
}
xw.writeEndElement();
xw.writeEndElement();
}
private <T extends FreeColObject> void writeSection(FreeColXMLWriter xw,
String section, Collection<T> items) throws XMLStreamException {
xw.writeStartElement(section);
for (T item : items) item.toXML(xw);
xw.writeEndElement();
}
/**
* Initializes this object from its XML-representation.
*
* @param xr The {@code FreeColXMLReader} to read from.
* @exception XMLStreamException if there are any problems reading
* the stream.
*/
public void readFromXML(FreeColXMLReader xr) throws XMLStreamException {
// Beware! We load mods onto an existing specification, but
// we do not want to overwrite its main attributes (id,
// difficulty, version) So only set those variables if they
// are currently null, as will be the case when we load the
// root specification.
String newId = xr.readId();
if (id == null) id = newId;
if (difficultyLevel == null) {
difficultyLevel = xr.getAttribute(DIFFICULTY_LEVEL_TAG,
(String)null);
// @compat 0.11.3
if (difficultyLevel == null) {
difficultyLevel = xr.getAttribute(OLD_DIFFICULTY_LEVEL_TAG,
(String)null);
}
// end @compat 0.11.3
}
if (version == null) {
version = xr.getAttribute(VERSION_TAG, (String)null);
}
logger.fine("Reading specification " + newId
+ " difficulty=" + difficultyLevel
+ " version=" + version);
String parentId = xr.getAttribute(FreeColSpecObjectType.EXTENDS_TAG,
(String)null);
if (parentId != null) {
try {
FreeColModFile parent = FreeColRules.getFreeColRulesFile(parentId);
try {
load(parent.getSpecificationInputStream());
} catch (RuntimeException|XMLStreamException e) {
throw new FreeColUserMessageException(
StringTemplate.template("error.mod").add("%id%", parent.getId()).add("%name%", Messages.getName("mod." + parent.getId())),
e
);
}
initialized = false;
} catch (IOException e) {
throw new XMLStreamException("Failed to open parent specification: ", e);
}
}
while (xr.moreTags()) {
String childName = xr.getLocalName();
// @compat 0.11.0
if (OLD_EQUIPMENT_TYPES_TAG.equals(childName)) {
xr.swallowTag(OLD_EQUIPMENT_TYPES_TAG);
continue;
}
// end @compat 0.11.0
ChildReader reader = readerMap.get(childName);
if (reader == null) {
logger.warning("No reader found for: " + childName);
} else {
reader.readChildren(xr);
}
}
}
/**
* {@inheritDoc}
*/
public String getXMLTagName() { return TAG; }
}