diff --git a/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java b/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java index c67358f3f1..6d46c9ece7 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java +++ b/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java @@ -1,5 +1,6 @@ package tc.oc.pgm.api.map; +import com.google.common.collect.Range; import java.time.LocalDate; import java.util.Collection; import java.util.Map; @@ -34,6 +35,14 @@ public interface MapInfo extends Comparable, Cloneable { */ Map getVariants(); + /** + * Get what servers versions should load this map, servers outside the range should ignore the + * map. + * + * @return range of the server versions that can load this map + */ + Range getServerVersion(); + /** @return the subfolder in which the world is in, or null for the parent folder */ @Nullable String getWorldFolder(); @@ -203,5 +212,7 @@ interface VariantInfo { String getMapName(); String getWorld(); + + Range getServerVersions(); } } diff --git a/core/src/main/java/tc/oc/pgm/map/MapFactoryImpl.java b/core/src/main/java/tc/oc/pgm/map/MapFactoryImpl.java index c1d8e73466..0297b0c1b7 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapFactoryImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapFactoryImpl.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -35,6 +36,7 @@ import tc.oc.pgm.regions.RegionParser; import tc.oc.pgm.util.ClassLogger; import tc.oc.pgm.util.Version; +import tc.oc.pgm.util.platform.Platform; import tc.oc.pgm.util.xml.DocumentWrapper; import tc.oc.pgm.util.xml.InvalidXMLException; import tc.oc.pgm.util.xml.Node; @@ -55,7 +57,8 @@ public class MapFactoryImpl extends ModuleGraph, MapModuleFactory variants; private final String worldFolder; + private final Range serverVersion; private final Version proto; private final Version version; private final Phase phase; @@ -88,6 +90,7 @@ public MapInfoImpl(MapSource source, Element root) throws InvalidXMLException { if (variant == null) throw new InvalidXMLException("Could not find variant definition", root); this.worldFolder = variant.getWorld(); + this.serverVersion = variant.getServerVersions(); this.name = variant.getMapName(); this.normalizedName = StringUtils.normalize(name); @@ -97,25 +100,22 @@ public MapInfoImpl(MapSource source, Element root) throws InvalidXMLException { this.proto = assertNotNull(XMLUtils.parseSemanticVersion(Node.fromRequiredAttr(root, "proto"))); this.version = assertNotNull(XMLUtils.parseSemanticVersion(Node.fromRequiredChildOrAttr(root, "version"))); - this.description = - assertNotNull( - Node.fromRequiredChildOrAttr(root, "objective", "description").getValueNormalize()); + this.description = assertNotNull( + Node.fromRequiredChildOrAttr(root, "objective", "description").getValueNormalize()); this.created = XMLUtils.parseDate(Node.fromChildOrAttr(root, "created")); this.authors = parseContributors(root, "author"); this.contributors = parseContributors(root, "contributor"); this.rules = parseRules(root); - this.difficulty = - XMLUtils.parseEnum( - Node.fromLastChildOrAttr(root, "difficulty"), Difficulty.class, Difficulty.NORMAL) - .ordinal(); + this.difficulty = XMLUtils.parseEnum( + Node.fromLastChildOrAttr(root, "difficulty"), Difficulty.class, Difficulty.NORMAL) + .ordinal(); this.world = parseWorld(root); this.gamemode = XMLUtils.parseFormattedText(Node.fromLastChildOrAttr(root, "game")); this.gamemodes = parseGamemodes(root); this.phase = XMLUtils.parseEnum(Node.fromLastChildOrAttr(root, "phase"), Phase.class, Phase.PRODUCTION); - this.friendlyFire = - XMLUtils.parseBoolean( - Node.fromLastChildOrAttr(root, "friendlyfire", "friendly-fire"), false); + this.friendlyFire = XMLUtils.parseBoolean( + Node.fromLastChildOrAttr(root, "friendlyfire", "friendly-fire"), false); } @NotNull @@ -145,6 +145,11 @@ public Map getVariants() { return variants; } + @Override + public Range getServerVersion() { + return serverVersion; + } + @Override public String getWorldFolder() { return worldFolder; @@ -349,14 +354,12 @@ protected void setContext(MapContextImpl context) { // If the map defines no game-modes manually, derive them from map tags, sorted by auxiliary // last. if (this.gamemodes.isEmpty()) { - this.gamemodes = - this.tags.stream() - .filter(MapTag::isGamemode) - .sorted( - Comparator.comparing(MapTag::isAuxiliary) - .thenComparing(Comparator.naturalOrder())) - .map(MapTag::getGamemode) - .collect(StreamUtils.toImmutableList()); + this.gamemodes = this.tags.stream() + .filter(MapTag::isGamemode) + .sorted( + Comparator.comparing(MapTag::isAuxiliary).thenComparing(Comparator.naturalOrder())) + .map(MapTag::getGamemode) + .collect(StreamUtils.toImmutableList()); } } this.context = new SoftReference<>(context); @@ -367,10 +370,13 @@ private static class VariantData implements VariantInfo { private final String mapName; private final String mapId; private final String world; + private final Range serverVersions; public VariantData(Element root, @Nullable Element variantEl) throws InvalidXMLException { String name = assertNotNull(Node.fromRequiredChildOrAttr(root, "name").getValueNormalize()); String slug = assertNotNull(root).getChildTextNormalize("slug"); + Node minVer = Node.fromAttr(root, "min-server-version"); + Node maxVer = Node.fromAttr(root, "max-server-version"); if (variantEl == null) { this.variantId = DEFAULT_VARIANT; @@ -384,9 +390,21 @@ public VariantData(Element root, @Nullable Element variantEl) throws InvalidXMLE boolean override = XMLUtils.parseBoolean(Node.fromAttr(variantEl, "override"), false); this.mapName = (override ? "" : name + ": ") + variantEl.getTextNormalize(); this.world = variantEl.getAttributeValue("world"); - if (slug != null) slug += "_" + variantId; + + String variantSlug = variantEl.getAttributeValue("slug"); + if (variantSlug != null) slug = variantSlug; + else if (slug != null) slug += "_" + variantId; + + Node minVerVariant = Node.fromAttr(variantEl, "min-server-version"); + if (minVerVariant != null) minVer = minVerVariant; + + Node maxVerVariant = Node.fromAttr(variantEl, "max-server-version"); + if (maxVerVariant != null) minVer = minVerVariant; } this.mapId = assertNotNull(slug != null ? slug : StringUtils.slugify(mapName)); + + this.serverVersions = XMLUtils.parseClosedRange( + minVer, XMLUtils.parseSemanticVersion(minVer), XMLUtils.parseSemanticVersion(maxVer)); } @Override @@ -408,5 +426,10 @@ public String getMapName() { public String getWorld() { return world; } + + @Override + public Range getServerVersions() { + return serverVersions; + } } } diff --git a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java index da75026689..032f479ab8 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java @@ -29,6 +29,7 @@ import tc.oc.pgm.api.map.includes.MapIncludeProcessor; import tc.oc.pgm.util.LiquidMetal; import tc.oc.pgm.util.StringUtils; +import tc.oc.pgm.util.platform.Platform; import tc.oc.pgm.util.usernames.UsernameResolvers; public class MapLibraryImpl implements MapLibrary { @@ -55,9 +56,8 @@ public MapInfo getMap(String idOrName) { MapInfo map = maps.get(StringUtils.slugify(idOrName)); if (map == null) { // Fuzzy match - map = - StringUtils.bestFuzzyMatch( - StringUtils.normalize(idOrName), maps.values(), MapInfo::getNormalizedName); + map = StringUtils.bestFuzzyMatch( + StringUtils.normalize(idOrName), maps.values(), MapInfo::getNormalizedName); } return map; @@ -103,16 +103,15 @@ private void logMapSuccess(int fail, int ok) { } else if (ok <= 0) { logger.info("Failed to load " + ChatColor.YELLOW + fail + ChatColor.RESET + " maps"); } else { - logger.info( - "Loaded " - + ChatColor.YELLOW - + ok - + ChatColor.RESET - + " new maps, failed to load " - + ChatColor.YELLOW - + fail - + ChatColor.RESET - + " maps"); + logger.info("Loaded " + + ChatColor.YELLOW + + ok + + ChatColor.RESET + + " new maps, failed to load " + + ChatColor.YELLOW + + fail + + ChatColor.RESET + + " maps"); } } @@ -130,72 +129,66 @@ public CompletableFuture loadNewMaps(boolean reset) { final int oldOk = reset ? 0 : maps.size(); return CompletableFuture.runAsync(UsernameResolvers::startBatch) - .thenRunAsync( - () -> { - // First ensure loadNewSources is called for all factories, this may take some time - // (eg: Git pull) - List> mapSources = - factories - .parallelStream() - .map(s -> s.loadNewSources(this::logMapError)) - .collect(Collectors.toList()); - - if (reset) { - // Doing full reset; add all known maps to be re-loaded - mapSources.add(this.maps.values().stream().map(MapInfo::getSource)); - } else { - // Not a full reset; reload failed & modified maps - mapSources.add(failed.stream()); - - mapSources.add( - this.maps.entrySet().stream() - .filter( - entry -> { - try { - return entry.getValue().getSource().checkForUpdates(); - } catch (MapMissingException e) { - logMapError(e); - this.maps.remove(entry.getKey()); - return false; - } - }) - .map(entry -> entry.getValue().getSource())); - } - - // Finally load all the maps - try (Stream stream = - mapSources.stream().flatMap(Function.identity()).parallel().unordered()) { - stream.forEach(s -> this.loadMapSafe(s, null)); - } - }) + .thenRunAsync(() -> { + // First ensure loadNewSources is called for all factories, this may take some time + // (eg: Git pull) + List> mapSources = factories.parallelStream() + .map(s -> s.loadNewSources(this::logMapError)) + .collect(Collectors.toList()); + + if (reset) { + // Doing full reset; add all known maps to be re-loaded + mapSources.add(this.maps.values().stream().map(MapInfo::getSource)); + } else { + // Not a full reset; reload failed & modified maps + mapSources.add(failed.stream()); + + mapSources.add(this.maps.entrySet().stream() + .filter(entry -> { + try { + return entry.getValue().getSource().checkForUpdates(); + } catch (MapMissingException e) { + logMapError(e); + this.maps.remove(entry.getKey()); + return false; + } + }) + .map(entry -> entry.getValue().getSource())); + } + + // Finally load all the maps + try (Stream stream = + mapSources.stream().flatMap(Function.identity()).parallel().unordered()) { + stream.forEach(s -> this.loadMapSafe(s, null)); + } + }) .thenRunAsync(() -> logMapSuccess(oldFail, oldOk)) .thenRunAsync(UsernameResolvers::endBatch); } @Override public CompletableFuture loadExistingMap(String id) { - return CompletableFuture.supplyAsync( - () -> { - final MapInfo info = maps.get(id); - if (info == null) { - throw new RuntimeException( - new MapMissingException(id, "Unable to find map from id (was it deleted?)")); - } + return CompletableFuture.supplyAsync(() -> { + final MapInfo info = maps.get(id); + if (info == null) { + throw new RuntimeException( + new MapMissingException(id, "Unable to find map from id (was it deleted?)")); + } - final MapContext context = info.getContext(); - try { - if (context != null && !info.getSource().checkForUpdates()) { - return context; - } - } catch (MapMissingException e) { - failed.remove(info.getSource()); - maps.remove(id); - throw new RuntimeException(e); - } + final MapContext context = info.getContext(); + try { + if (context != null && !info.getSource().checkForUpdates()) { + return context; + } + } catch (MapMissingException e) { + failed.remove(info.getSource()); + maps.remove(id); + throw new RuntimeException(e); + } - logger.info(ChatColor.GREEN + "XML changes detected, reloading"); - return loadMapSafe(info.getSource(), info.getId()); - }); + logger.info(ChatColor.GREEN + "XML changes detected, reloading"); + return loadMapSafe(info.getSource(), info.getId()); + }); } private MapContext loadMap(MapSource source, @Nullable String mapId) throws MapException { @@ -226,8 +219,11 @@ private MapContext loadMap(MapSource source, @Nullable String mapId) throws MapE } MapInfo info = context.getInfo(); - maps.merge( - info.getId(), info, (m1, m2) -> m2.getVersion().isOlderThan(m1.getVersion()) ? m1 : m2); + // Only if from a supported version, add it to our library + if (info.getServerVersion().contains(Platform.MINECRAFT_VERSION)) { + maps.merge( + info.getId(), info, (m1, m2) -> m2.getVersion().isOlderThan(m1.getVersion()) ? m1 : m2); + } failed.remove(source); return context; diff --git a/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java b/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java index a35a4bc3b1..2ad7c34b27 100644 --- a/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java @@ -468,6 +468,17 @@ public static > Range parseNumericRange( lowStr == null || lowStr.equals("-oo") ? null : parseNumber(node, lowStr, type, false); T upper = uppStr == null || uppStr.equals("oo") ? null : parseNumber(node, uppStr, type, false); + return parseRange(node, lower, lowerBound, upper, upperBound); + } + + public static > Range parseClosedRange( + Node node, @Nullable T lower, @Nullable T upper) throws InvalidXMLException { + return parseRange(node, lower, BoundType.CLOSED, upper, BoundType.CLOSED); + } + + public static > Range parseRange( + Node node, @Nullable T lower, BoundType lowerBound, @Nullable T upper, BoundType upperBound) + throws InvalidXMLException { if (lower != null && upper != null) { if (lower.compareTo(upper) > 0) { throw new InvalidXMLException( @@ -476,7 +487,6 @@ public static > Range parseNumericRange( } return Range.range(lower, lowerBound, upper, upperBound); - } else if (lower != null) { return Range.downTo(lower, lowerBound); } else if (upper != null) {