package tc.oc.pgm.modules; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.UUID; import java.util.logging.Logger; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Range; import com.google.inject.Provides; import net.md_5.bungee.api.chat.BaseComponent; import org.bukkit.Difficulty; import org.bukkit.World.Environment; import org.bukkit.WorldCreator; import org.jdom2.Document; import org.jdom2.Element; import tc.oc.api.docs.SemanticVersion; import tc.oc.api.docs.virtual.MapDoc; import tc.oc.commons.core.chat.Component; import tc.oc.pgm.features.GamemodeFeature; import tc.oc.pgm.map.Contributor; import tc.oc.pgm.map.MapId; import tc.oc.pgm.map.MapInfo; import tc.oc.pgm.map.MapModule; import tc.oc.pgm.map.MapModuleContext; import tc.oc.pgm.map.MapModuleFactory; import tc.oc.pgm.map.ProtoVersions; import tc.oc.pgm.map.inject.MapScoped; import tc.oc.pgm.terrain.WorldConfigurator; import tc.oc.pgm.module.ModuleDescription; import tc.oc.pgm.terrain.WorldConfiguratorBinder; import tc.oc.pgm.utils.XMLUtils; import tc.oc.pgm.xml.InvalidXMLException; import tc.oc.pgm.xml.Node; @ModuleDescription(name="Info") public class InfoModule implements MapModule, WorldConfigurator { private final MapInfo info; // Use a TreeSet so order is consistent private final Set<MapDoc.Gamemode> gamemodes = new TreeSet<>(Collections.singleton(MapDoc.Gamemode.mixed)); private Range<Integer> playerLimits = Range.singleton(0); public InfoModule(MapInfo info) { this.info = info; } public MapInfo getMapInfo() { return this.info; } public Set<MapDoc.Gamemode> getGamemodes() { return info.gamemodes.isEmpty() ? gamemodes : info.gamemodes; } public Range<Integer> getGlobalPlayerLimits() { return playerLimits; } @Override public void configureWorld(WorldCreator worldCreator) { worldCreator.environment(info.dimension); } @Override public void postParse(MapModuleContext context, Logger logger, Document doc) throws InvalidXMLException { for(MapModule module : context.loadedModules()) { gamemodes.addAll(module.getGamemodes(context)); } context.features() .all(GamemodeFeature.class) .flatMap(GamemodeFeature::gamemodes) .forEach(gamemodes::add); int min = 0, max = 0; for(MapModule module : context.loadedModules()) { final Range<Integer> limits = module.getPlayerLimits(); min += limits.lowerEndpoint(); max += limits.upperEndpoint(); } playerLimits = Range.closed(min, max); } public static class Factory extends MapModuleFactory<InfoModule> { @Override protected void configure() { super.configure(); new WorldConfiguratorBinder(binder()) .addBinding().to(InfoModule.class); } @Provides @MapScoped MapInfo mapInfo(InfoModule module) { return module.getMapInfo(); } @Provides @MapScoped MapId mapId(MapInfo info) { return info.id; } @Override public InfoModule parse(MapModuleContext context, Logger logger, Document doc) throws InvalidXMLException { Element root = doc.getRootElement(); String name = Node.fromRequiredChildOrAttr(root, "name").getValueNormalize(); SemanticVersion version = XMLUtils.parseSemanticVersion(Node.fromRequiredChildOrAttr(root, "version")); MapDoc.Phase phase = XMLUtils.parseEnum(Node.fromLastChildOrAttr(root, "phase"), MapDoc.Phase.class, "phase", MapDoc.Phase.PRODUCTION); MapDoc.Edition edition = XMLUtils.parseEnum(Node.fromLastChildOrAttr(root, "edition"), MapDoc.Edition.class, "edition", MapDoc.Edition.STANDARD); // Allow multiple <objective> elements, so include files can provide defaults final BaseComponent objective = XMLUtils.parseLocalizedText(Node.fromRequiredLastChildOrAttr(root, "objective")); String slug = root.getChildTextNormalize("slug"); BaseComponent game = XMLUtils.parseFormattedText(root, "game"); MapDoc.Genre genre = XMLUtils.parseEnum(Node.fromNullable(root.getChild("genre")), MapDoc.Genre.class, "genre", MapDoc.Genre.OTHER); final TreeSet<MapDoc.Gamemode> gamemodes = new TreeSet<>(); for(Element elGamemode : root.getChildren("gamemode")) { gamemodes.add(XMLUtils.parseEnum(elGamemode, MapDoc.Gamemode.class)); } List<Contributor> authors = readContributorList(root, "authors", "author"); if(game == null) { Element blitz = root.getChild("blitz"); if(blitz != null) { Element title = blitz.getChild("title"); if(title != null) { if(context.getProto().isNoOlderThan(ProtoVersions.REMOVE_BLITZ_TITLE)) { throw new InvalidXMLException("<title> inside <blitz> is no longer supported, use <map game=\"...\">", title); } game = new Component(title.getTextNormalize()); } } } List<Contributor> contributors = readContributorList(root, "contributors", "contributor"); List<String> rules = new ArrayList<String>(); for(Element parent : root.getChildren("rules")) { for(Element rule : parent.getChildren("rule")) { rules.add(rule.getTextNormalize()); } } Difficulty difficulty = XMLUtils.parseEnum(Node.fromLastChildOrAttr(root, "difficulty"), Difficulty.class, "difficulty"); Environment dimension = XMLUtils.parseEnum(Node.fromLastChildOrAttr(root, "dimension"), Environment.class, "dimension", Environment.NORMAL); boolean friendlyFire = XMLUtils.parseBoolean(Node.fromLastChildOrAttr(root, "friendly-fire", "friendlyfire"), false); return new InfoModule(new MapInfo(context.getProto(), slug, name, version, edition, phase, game, genre, ImmutableSet.copyOf(gamemodes), objective, authors, contributors, rules, difficulty, dimension, friendlyFire)); } } private static List<Contributor> readContributorList(Element root, String topLevelTag, String tag) throws InvalidXMLException { List<Contributor> contribs = new ArrayList<Contributor>(); for(Element parent : root.getChildren(topLevelTag)) { for(Element child : parent.getChildren(tag)) { String name = XMLUtils.getNormalizedNullableText(child); UUID uuid = XMLUtils.parseUuid(Node.fromAttr(child, "uuid")); String contribution = XMLUtils.getNullableAttribute(child, "contribution", "contrib"); if(name == null && uuid == null) { throw new InvalidXMLException("Contributor must have either a name or UUID", child); } contribs.add(new Contributor(uuid, name, contribution)); } } return contribs; } }