Skip to content

Commit

Permalink
Add support for version-gated maps & variants
Browse files Browse the repository at this point in the history
Signed-off-by: Pablo Herrera <[email protected]>
  • Loading branch information
Pablete1234 committed Jul 27, 2024
1 parent 726c9ff commit c7e3fc0
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 94 deletions.
11 changes: 11 additions & 0 deletions core/src/main/java/tc/oc/pgm/api/map/MapInfo.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -34,6 +35,14 @@ public interface MapInfo extends Comparable<MapInfo>, Cloneable {
*/
Map<String, VariantInfo> 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<Version> getServerVersion();

/** @return the subfolder in which the world is in, or null for the parent folder */
@Nullable
String getWorldFolder();
Expand Down Expand Up @@ -203,5 +212,7 @@ interface VariantInfo {
String getMapName();

String getWorld();

Range<Version> getServerVersions();
}
}
11 changes: 10 additions & 1 deletion core/src/main/java/tc/oc/pgm/map/MapFactoryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -55,7 +57,8 @@ public class MapFactoryImpl extends ModuleGraph<MapModule<?>, MapModuleFactory<?

public MapFactoryImpl(Logger logger, MapSource source, MapIncludeProcessor includes) {
super(Modules.MAP, Modules.MAP_DEPENDENCY_ONLY); // Don't copy, avoid N factory copies
this.logger = ClassLogger.get(assertNotNull(logger), getClass(), assertNotNull(source).getId());
this.logger =
ClassLogger.get(assertNotNull(logger), getClass(), assertNotNull(source).getId());
this.source = source;
this.includes = includes;
}
Expand All @@ -75,6 +78,12 @@ public MapContext load() throws MapException {
document = MapFilePreprocessor.getDocument(source, includes);

info = new MapInfoImpl(source, document.getRootElement());

// We're not loading this map, return a dummy map context to allow variants to load, if needed
if (!info.getServerVersion().contains(Platform.MINECRAFT_VERSION)) {
return new MapContextImpl(info, List.of());
}

try {
loadAll();
} catch (ModuleLoadException e) {
Expand Down
61 changes: 42 additions & 19 deletions core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import java.lang.ref.SoftReference;
import java.time.LocalDate;
import java.util.*;
Expand Down Expand Up @@ -56,6 +57,7 @@ public class MapInfoImpl implements MapInfo {
private final Map<String, VariantInfo> variants;

private final String worldFolder;
private final Range<Version> serverVersion;
private final Version proto;
private final Version version;
private final Phase phase;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -145,6 +145,11 @@ public Map<String, VariantInfo> getVariants() {
return variants;
}

@Override
public Range<Version> getServerVersion() {
return serverVersion;
}

@Override
public String getWorldFolder() {
return worldFolder;
Expand Down Expand Up @@ -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);
Expand All @@ -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<Version> 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;
Expand All @@ -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
Expand All @@ -408,5 +426,10 @@ public String getMapName() {
public String getWorld() {
return world;
}

@Override
public Range<Version> getServerVersions() {
return serverVersions;
}
}
}
142 changes: 69 additions & 73 deletions core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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");
}
}

Expand All @@ -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<Stream<MapSource>> 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<MapSource> 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<Stream<MapSource>> 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<MapSource> 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<MapContext> 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 {
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit c7e3fc0

Please sign in to comment.