diff --git a/core/src/main/java/tc/oc/pgm/api/map/MapTag.java b/core/src/main/java/tc/oc/pgm/api/map/MapTag.java index 1bff7ce000..a99740e2ae 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/MapTag.java +++ b/core/src/main/java/tc/oc/pgm/api/map/MapTag.java @@ -1,53 +1,51 @@ package tc.oc.pgm.api.map; -import static net.kyori.adventure.text.Component.empty; import static net.kyori.adventure.text.Component.text; import static tc.oc.pgm.util.Assert.assertNotNull; import static tc.oc.pgm.util.Assert.assertTrue; +import java.util.Collections; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.regex.Pattern; import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; /** A "#hashtag" that describes a {@link MapInfo} feature. */ public final class MapTag implements Comparable { + private static final SortedSet TAG_IDS = new TreeSet<>(); + private static final Pattern PATTERN = Pattern.compile("^[a-z0-9_-]+$"); private static final String SYMBOL = "#"; private final String id; private final Component name; - private final Component acronym; - private final boolean gamemode; + private final @Nullable Gamemode gamemode; private final boolean auxiliary; - public MapTag( - final String id, final String name, final boolean gamemode, final boolean auxiliary) { - this(id, id, name, gamemode, auxiliary); + public MapTag(String id, String name) { + this(id, name, null, true); + } + + public MapTag(String id, Gamemode gm, boolean auxiliary) { + this(id, gm.getFullName(), gm, auxiliary); } - public MapTag( - final String internalId, - final String id, - final String name, - final boolean gamemode, - final boolean auxiliary) { - assertTrue( - PATTERN.matcher(assertNotNull(id)).matches(), name + " must match " + PATTERN.pattern()); + private MapTag(String id, String name, @Nullable Gamemode gamemode, boolean auxiliary) { + assertNotNull(id); + assertTrue(PATTERN.matcher(id).matches(), id + " must match " + PATTERN.pattern()); + TAG_IDS.add(id); this.id = id; - if (gamemode) { - Gamemode gm = Gamemode.byId(internalId); - if (gm == null) { - throw new IllegalArgumentException("Gamemode id " + internalId + " not recognized"); - } - this.name = text(gm.getFullName()); - this.acronym = text(gm.getAcronym()); - } else { - this.name = text(name); - this.acronym = empty(); - } + this.name = text(name); this.gamemode = gamemode; this.auxiliary = auxiliary; } + public static Set getAllTagIds() { + return Collections.unmodifiableSortedSet(TAG_IDS); + } + /** * Get a short id for the tag. * @@ -66,26 +64,23 @@ public Component getName() { return this.name; } - /** - * Gets an acronym for the tag. - * - * @return An acronym. - */ - public Component getAcronym() { - return this.acronym; - } - /** * Get whether this tag represents a "gamemode." * - * @return If a gamemode. + * @return If the tag is for a gamemode. */ public boolean isGamemode() { + return this.gamemode != null; + } + + /** @return the gamemode if this tag represents one, null otherwise. */ + public @Nullable Gamemode getGamemode() { return this.gamemode; } /** - * Get whether this tag is an auxiliary feature. + * Get whether this tag is an auxiliary gamemode, that works as a 2nd level gamemode. Eg: blitz or + * rage are auxiliary due to wool "and blitz", or deathmatch "and rage". * * @return If an auxiliary feature. */ diff --git a/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java b/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java index ba489c86f9..594c4a844d 100644 --- a/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java +++ b/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java @@ -10,6 +10,7 @@ import org.jdom2.Document; import org.jdom2.Element; import tc.oc.pgm.api.filter.Filter; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -26,7 +27,7 @@ public class BlitzModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("blitz", "Blitz", true, true)); + ImmutableList.of(new MapTag("blitz", Gamemode.BLITZ, true)); private final BlitzConfig config; public BlitzModule(BlitzConfig config) { diff --git a/core/src/main/java/tc/oc/pgm/classes/ClassModule.java b/core/src/main/java/tc/oc/pgm/classes/ClassModule.java index fe7f4592d2..5fd7fec431 100644 --- a/core/src/main/java/tc/oc/pgm/classes/ClassModule.java +++ b/core/src/main/java/tc/oc/pgm/classes/ClassModule.java @@ -32,8 +32,7 @@ public class ClassModule implements MapModule { - private static final Collection TAGS = - ImmutableList.of(new MapTag("classes", "Classes", false, true)); + private static final Collection TAGS = ImmutableList.of(new MapTag("classes", "Classes")); final String family; final Map classes; final PlayerClass defaultClass; diff --git a/core/src/main/java/tc/oc/pgm/command/MapCommand.java b/core/src/main/java/tc/oc/pgm/command/MapCommand.java index 407da1404c..200aa07f24 100644 --- a/core/src/main/java/tc/oc/pgm/command/MapCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/MapCommand.java @@ -15,6 +15,8 @@ import cloud.commandframework.annotations.Flag; import cloud.commandframework.annotations.specifier.Greedy; import cloud.commandframework.annotations.specifier.Range; +import cloud.commandframework.annotations.suggestions.Suggestions; +import cloud.commandframework.context.CommandContext; import com.google.common.collect.ImmutableList; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -36,6 +38,7 @@ import tc.oc.pgm.rotation.MapPoolManager; import tc.oc.pgm.rotation.pools.MapPool; import tc.oc.pgm.util.Audience; +import tc.oc.pgm.util.LiquidMetal; import tc.oc.pgm.util.PrettyPaginatedComponentResults; import tc.oc.pgm.util.StringUtils; import tc.oc.pgm.util.named.MapNameStyle; @@ -52,7 +55,8 @@ public void maps( CommandSender sender, MapLibrary library, @Argument(value = "page", defaultValue = "1") @Range(min = "1") int page, - @Flag(value = "tags", aliases = "t", repeatable = true) List tags, + @Flag(value = "tags", aliases = "t", repeatable = true, suggestions = "maptags") + List tags, @Flag(value = "author", aliases = "a") String author, @Flag(value = "name", aliases = "n") String name, @Flag(value = "phase", aliases = "p") Phase phase) { @@ -115,6 +119,20 @@ public Component format(MapInfo map, int index) { }.display(audience, ImmutableList.copyOf(maps), page); } + @Suggestions("maptags") + public List suggestMapTags(CommandContext sender, String input) { + int commaIdx = input.lastIndexOf(','); + + final String prefix = input.substring(0, commaIdx == -1 ? 0 : commaIdx + 1); + final String toComplete = + input.substring(commaIdx + 1).toLowerCase(Locale.ROOT).replace("!", ""); + + return MapTag.getAllTagIds().stream() + .filter(mt -> LiquidMetal.match(mt, toComplete)) + .flatMap(tag -> Stream.of(prefix + tag, prefix + "!" + tag)) + .collect(Collectors.toList()); + } + private static boolean matchesTags( MapInfo map, Collection posTags, Collection negTags) { int matches = 0; diff --git a/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointModule.java b/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointModule.java index b11b35170b..017abfe234 100644 --- a/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointModule.java +++ b/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointModule.java @@ -11,6 +11,7 @@ import org.jdom2.Document; import org.jdom2.Element; import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -27,11 +28,9 @@ public class ControlPointModule implements MapModule { - private static final MapTag CP = - new MapTag("cp", "controlpoint", "Control the Point", true, false); - private static final MapTag KOTH = - new MapTag("koth", "controlpoint", "King of the Hill", true, false); - private static final MapTag PAYLOAD = new MapTag("payload", "Payload", true, false); + private static final MapTag CP = new MapTag("controlpoint", Gamemode.CONTROL_THE_POINT, false); + private static final MapTag KOTH = new MapTag("controlpoint", Gamemode.KING_OF_THE_HILL, false); + private static final MapTag PAYLOAD = new MapTag("payload", Gamemode.PAYLOAD, false); private final List definitions; private final Collection tags; @@ -94,7 +93,8 @@ public ControlPointModule parse(MapFactory factory, Logger logger, Document doc) for (Element kingEl : doc.getRootElement().getChildren("king")) { for (Element hillEl : XMLUtils.flattenElements(kingEl, "hills", "hill")) { - tags.add(KOTH); + // CP and KOTH are mutually exclusive, they're the same ID. + if (tags.isEmpty()) tags.add(KOTH); ControlPointDefinition definition = ControlPointParser.parseControlPoint(factory, hillEl, Type.HILL, serialNumber); factory.getFeatures().addFeature(kingEl, definition); diff --git a/core/src/main/java/tc/oc/pgm/core/CoreModule.java b/core/src/main/java/tc/oc/pgm/core/CoreModule.java index d078dbcfa3..c3516fd5f1 100644 --- a/core/src/main/java/tc/oc/pgm/core/CoreModule.java +++ b/core/src/main/java/tc/oc/pgm/core/CoreModule.java @@ -12,6 +12,7 @@ import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.map.MapTag; @@ -38,7 +39,7 @@ public class CoreModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("dtc", "core", "Destroy the Core", true, false)); + ImmutableList.of(new MapTag("core", Gamemode.DESTROY_THE_CORE, false)); protected final List coreFactories; public CoreModule(List coreFactories) { diff --git a/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java b/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java index 0910656cd2..1fc17c6032 100644 --- a/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java +++ b/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java @@ -9,6 +9,7 @@ import java.util.logging.Logger; import org.jdom2.Document; import org.jdom2.Element; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.map.MapTag; @@ -37,7 +38,7 @@ public class DestroyableModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("dtm", "monument", "Destroy the Monument", true, false)); + ImmutableList.of(new MapTag("monument", Gamemode.DESTROY_THE_MONUMENT, false)); protected final List destroyableFactories; public DestroyableModule(List destroyableFactories) { diff --git a/core/src/main/java/tc/oc/pgm/ffa/FreeForAllModule.java b/core/src/main/java/tc/oc/pgm/ffa/FreeForAllModule.java index 4f694c7677..10b67380e3 100644 --- a/core/src/main/java/tc/oc/pgm/ffa/FreeForAllModule.java +++ b/core/src/main/java/tc/oc/pgm/ffa/FreeForAllModule.java @@ -8,6 +8,7 @@ import org.jdom2.Document; import org.jdom2.Element; import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -24,7 +25,7 @@ public class FreeForAllModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("ffa", "Free for All", false, false)); + ImmutableList.of(new MapTag("ffa", Gamemode.FREE_FOR_ALL, true)); private final FreeForAllOptions options; public FreeForAllModule(FreeForAllOptions options) { diff --git a/core/src/main/java/tc/oc/pgm/flag/FlagModule.java b/core/src/main/java/tc/oc/pgm/flag/FlagModule.java index 931c1c7947..d1a2010988 100644 --- a/core/src/main/java/tc/oc/pgm/flag/FlagModule.java +++ b/core/src/main/java/tc/oc/pgm/flag/FlagModule.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.logging.Logger; import org.jdom2.Document; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -22,7 +23,7 @@ public class FlagModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("ctf", "flag", "Capture the Flag", true, false)); + ImmutableList.of(new MapTag("flag", Gamemode.CAPTURE_THE_FLAG, false)); private final ImmutableList posts; private final ImmutableList nets; private final ImmutableList flags; diff --git a/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java b/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java index 3a843490d0..b70086c145 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java @@ -2,48 +2,24 @@ import static tc.oc.pgm.util.Assert.assertNotNull; -import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; -import java.lang.ref.SoftReference; import java.util.Collection; import java.util.List; import tc.oc.pgm.api.map.MapContext; import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.map.MapModule; -import tc.oc.pgm.api.map.MapTag; -import tc.oc.pgm.ffa.FreeForAllModule; -import tc.oc.pgm.teams.TeamFactory; -import tc.oc.pgm.teams.TeamModule; public class MapContextImpl implements MapContext { - private static final MapTag TERRAIN = new MapTag("terrain", "Terrain", false, true); private final MapInfo info; private final List modules; public MapContextImpl(MapInfoImpl info, Collection> modules) { - info.context = new SoftReference<>(this); this.info = info; this.modules = ImmutableList.copyOf(assertNotNull(modules)); - for (MapModule module : this.modules) { - info.tags.addAll(module.getTags()); - - if (module instanceof TeamModule) { - info.players.clear(); - info.players.addAll( - Collections2.transform(((TeamModule) module).getTeams(), TeamFactory::getMaxPlayers)); - } - - if (module instanceof FreeForAllModule) { - info.players.clear(); - info.players.add(((FreeForAllModule) module).getOptions().maxPlayers); - } - } - - if (info.getWorld().hasTerrain()) { - info.tags.add(TERRAIN); - } + // Update the map info with stuff derived from modules, like team sizes or tags. + info.setContext(this); } public MapInfo getInfo() { diff --git a/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java b/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java index 2fa4a0c66f..574c292deb 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java @@ -4,6 +4,9 @@ import static net.kyori.adventure.text.Component.translatable; import static tc.oc.pgm.util.Assert.assertNotNull; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Iterables; import java.lang.ref.SoftReference; import java.time.LocalDate; import java.util.*; @@ -20,12 +23,17 @@ import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapContext; import tc.oc.pgm.api.map.MapInfo; +import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapSource; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.Phase; import tc.oc.pgm.api.map.WorldInfo; +import tc.oc.pgm.ffa.FreeForAllModule; import tc.oc.pgm.map.contrib.PlayerContributor; import tc.oc.pgm.map.contrib.PseudonymContributor; +import tc.oc.pgm.teams.TeamFactory; +import tc.oc.pgm.teams.TeamModule; +import tc.oc.pgm.util.StreamUtils; import tc.oc.pgm.util.StringUtils; import tc.oc.pgm.util.Version; import tc.oc.pgm.util.named.MapNameStyle; @@ -36,6 +44,9 @@ import tc.oc.pgm.util.xml.XMLUtils; public class MapInfoImpl implements MapInfo { + // See #parseWorld, this class actually is responsible for terrain tag. + private static final MapTag TERRAIN = new MapTag("terrain", "Terrain"); + private final MapSource source; private final String id; @@ -56,11 +67,13 @@ public class MapInfoImpl implements MapInfo { private final WorldInfo world; private final boolean friendlyFire; - protected final Collection tags; - protected final Collection players; - protected final Collection gamemodes; + // May be set after loading the whole context + protected Collection gamemodes; - protected SoftReference context; + // Must be set after loading the whole context + protected Collection tags = ImmutableSortedSet.of(); + protected Collection players = ImmutableList.of(); + protected SoftReference context = null; public MapInfoImpl(MapSource source, Element root) throws InvalidXMLException { this.source = source; @@ -107,8 +120,6 @@ public MapInfoImpl(MapSource source, Element root) throws InvalidXMLException { "difficulty", Difficulty.NORMAL) .ordinal(); - this.tags = new TreeSet<>(); - this.players = new ArrayList<>(); this.world = parseWorld(root); this.gamemode = XMLUtils.parseFormattedText(root, "game"); this.gamemodes = parseGamemodes(root); @@ -187,7 +198,7 @@ public int getDifficulty() { @Override public Collection getTags() { - return tags; + return Collections.unmodifiableCollection(tags); } @Override @@ -268,44 +279,37 @@ public Component getStyledName(MapNameStyle style) { } private static @NotNull List parseRules(Element root) { - List rules = new ArrayList<>(); - for (Element parent : root.getChildren("rules")) { - for (Element rule : parent.getChildren("rule")) { - rules.add(rule.getTextNormalize()); - } - } - return rules; + return XMLUtils.flattenElements(root, "rules", "rule").stream() + .map(Element::getTextNormalize) + .collect(StreamUtils.toImmutableList()); } private static @NotNull List parseGamemodes(Element root) throws InvalidXMLException { - List gamemodes = new ArrayList<>(); + ImmutableList.Builder gamemodes = ImmutableList.builder(); for (Element gamemodeEl : root.getChildren("gamemode")) { Gamemode gm = Gamemode.byId(gamemodeEl.getText()); if (gm == null) throw new InvalidXMLException("Unknown gamemode", gamemodeEl); gamemodes.add(gm); } - return gamemodes; + return gamemodes.build(); } private static @NotNull List parseContributors(Element root, String tag) throws InvalidXMLException { List contributors = new ArrayList<>(); - for (Element parent : root.getChildren(tag + "s")) { - 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); - } - - if (uuid == null) { - contributors.add(new PseudonymContributor(name, contribution)); - } else { - contributors.add(new PlayerContributor(uuid, contribution)); - } + for (Element child : XMLUtils.flattenElements(root, tag + "s", 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); } + + contributors.add( + uuid == null + ? new PseudonymContributor(name, contribution) + : new PlayerContributor(uuid, contribution)); } return contributors; } @@ -314,4 +318,43 @@ public Component getStyledName(MapNameStyle style) { final Element world = root.getChild("terrain"); return world == null ? new WorldInfoImpl() : new WorldInfoImpl(world); } + + protected void setContext(MapContextImpl context) { + // The first time context is loaded, set properties which can't be + // parsed until after modules are parsed, like team sizes or tags. + if (this.context == null) { + ImmutableSortedSet.Builder tags = ImmutableSortedSet.naturalOrder(); + ImmutableList.Builder players = ImmutableList.builder(); + + for (MapModule module : context.getModules()) { + tags.addAll(module.getTags()); + + if (module instanceof TeamModule) + players.addAll( + Iterables.transform(((TeamModule) module).getTeams(), TeamFactory::getMaxPlayers)); + + if (module instanceof FreeForAllModule) + players.add(((FreeForAllModule) module).getOptions().maxPlayers); + } + + if (world.hasTerrain()) tags.add(TERRAIN); + + this.tags = tags.build(); + this.players = players.build(); + + // 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.context = new SoftReference<>(context); + } } diff --git a/core/src/main/java/tc/oc/pgm/rage/RageModule.java b/core/src/main/java/tc/oc/pgm/rage/RageModule.java index 290f49cc9d..adc4b25072 100644 --- a/core/src/main/java/tc/oc/pgm/rage/RageModule.java +++ b/core/src/main/java/tc/oc/pgm/rage/RageModule.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.logging.Logger; import org.jdom2.Document; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -13,7 +14,7 @@ public class RageModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("rage", "Rage", true, true)); + ImmutableList.of(new MapTag("rage", Gamemode.RAGE, true)); @Override public RageMatchModule createMatchModule(Match match) { diff --git a/core/src/main/java/tc/oc/pgm/rotation/vote/book/VotingBookCreatorImpl.java b/core/src/main/java/tc/oc/pgm/rotation/vote/book/VotingBookCreatorImpl.java index de1f61ba98..21424ba2b5 100644 --- a/core/src/main/java/tc/oc/pgm/rotation/vote/book/VotingBookCreatorImpl.java +++ b/core/src/main/java/tc/oc/pgm/rotation/vote/book/VotingBookCreatorImpl.java @@ -4,14 +4,18 @@ import static net.kyori.adventure.text.event.ClickEvent.runCommand; import static net.kyori.adventure.text.event.HoverEvent.showText; +import java.util.Collection; import java.util.stream.Collectors; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.util.text.TextFormatter; public class VotingBookCreatorImpl implements VotingBookCreator { @@ -27,12 +31,34 @@ public Component getMapBookComponent(MatchPlayer viewer, MapInfo map, boolean vo voted ? NamedTextColor.DARK_GREEN : NamedTextColor.DARK_RED)); text.append(text(" ").decoration(TextDecoration.BOLD, !voted)); // Fix 1px symbol diff text.append(text(map.getName(), NamedTextColor.GOLD, TextDecoration.BOLD)); - text.hoverEvent( - showText( - text( - map.getTags().stream().map(MapTag::toString).collect(Collectors.joining(" ")), - NamedTextColor.YELLOW))); + text.hoverEvent(showText(getHover(viewer, map, voted))); text.clickEvent(runCommand("/votenext -o " + map.getName())); return text.build(); } + + public ComponentLike getHover(MatchPlayer viewer, MapInfo map, boolean voted) { + TextComponent.Builder text = text(); + + Collection gamemodes = map.getGamemodes(); + + if (map.getGamemode() != null) { + text.append(map.getGamemode().colorIfAbsent(NamedTextColor.AQUA)).appendNewline(); + } else if (!gamemodes.isEmpty()) { + boolean acronyms = gamemodes.size() > 1; + text.append( + TextFormatter.list( + gamemodes.stream() + .map(gm -> text(acronyms ? gm.getAcronym() : gm.getFullName())) + .collect(Collectors.toList()), + NamedTextColor.AQUA)) + .appendNewline(); + } + + text.append( + text( + map.getTags().stream().map(MapTag::toString).collect(Collectors.joining(" ")), + NamedTextColor.YELLOW)); + + return text; + } } diff --git a/core/src/main/java/tc/oc/pgm/score/ScoreModule.java b/core/src/main/java/tc/oc/pgm/score/ScoreModule.java index e2b8c874ee..12adeb29c7 100644 --- a/core/src/main/java/tc/oc/pgm/score/ScoreModule.java +++ b/core/src/main/java/tc/oc/pgm/score/ScoreModule.java @@ -15,6 +15,7 @@ import org.jdom2.Element; import org.jetbrains.annotations.NotNull; import tc.oc.pgm.api.filter.Filter; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.map.MapTag; @@ -36,9 +37,8 @@ import tc.oc.pgm.util.xml.XMLUtils; public class ScoreModule implements MapModule { - private static final MapTag SCORE_TAG = - new MapTag("tdm", "deathmatch", "Deathmatch", true, false); - private static final MapTag BOX_TAG = new MapTag("scorebox", "Scorebox", false, true); + private static final MapTag SCORE_TAG = new MapTag("deathmatch", Gamemode.DEATHMATCH, false); + private static final MapTag BOX_TAG = new MapTag("scorebox", "Scorebox"); public ScoreModule(@NotNull ScoreConfig config, @NotNull Set scoreBoxFactories) { assertNotNull(config, "score config"); diff --git a/core/src/main/java/tc/oc/pgm/shops/ShopModule.java b/core/src/main/java/tc/oc/pgm/shops/ShopModule.java index 7ca5ebec08..5eb26495d7 100644 --- a/core/src/main/java/tc/oc/pgm/shops/ShopModule.java +++ b/core/src/main/java/tc/oc/pgm/shops/ShopModule.java @@ -52,8 +52,7 @@ public class ShopModule implements MapModule { - private static final Collection TAGS = - ImmutableList.of(new MapTag("shops", "Shops", false, true)); + private static final Collection TAGS = ImmutableList.of(new MapTag("shops", "Shops")); private final ImmutableMap shops; private final ImmutableSet shopKeepers; diff --git a/core/src/main/java/tc/oc/pgm/teams/TeamModule.java b/core/src/main/java/tc/oc/pgm/teams/TeamModule.java index 2023fa4b3c..65982cf418 100644 --- a/core/src/main/java/tc/oc/pgm/teams/TeamModule.java +++ b/core/src/main/java/tc/oc/pgm/teams/TeamModule.java @@ -48,9 +48,7 @@ public Collection getTags() { ImmutableList.of( new MapTag( size + "team" + (size == 1 ? "" : "s"), - size + " Team" + (size == 1 ? "" : "s"), - false, - true))); + size + " Team" + (size == 1 ? "" : "s")))); } @Override diff --git a/core/src/main/java/tc/oc/pgm/timelimit/TimeLimitModule.java b/core/src/main/java/tc/oc/pgm/timelimit/TimeLimitModule.java index 0edc4cb918..3dc15e2544 100644 --- a/core/src/main/java/tc/oc/pgm/timelimit/TimeLimitModule.java +++ b/core/src/main/java/tc/oc/pgm/timelimit/TimeLimitModule.java @@ -24,7 +24,7 @@ public class TimeLimitModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("timelimit", "Timelimit", false, true)); + ImmutableList.of(new MapTag("timelimit", "Timelimit")); private final @Nullable TimeLimit timeLimit; public TimeLimitModule(@Nullable TimeLimit limit) { diff --git a/core/src/main/java/tc/oc/pgm/tnt/TNTModule.java b/core/src/main/java/tc/oc/pgm/tnt/TNTModule.java index a5d95165e6..7bf5e2c256 100644 --- a/core/src/main/java/tc/oc/pgm/tnt/TNTModule.java +++ b/core/src/main/java/tc/oc/pgm/tnt/TNTModule.java @@ -25,7 +25,7 @@ public class TNTModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("autotnt", "Instant TNT", false, true)); + ImmutableList.of(new MapTag("autotnt", "Instant TNT")); public static final int DEFAULT_DISPENSER_NUKE_LIMIT = 16; public static final float DEFAULT_DISPENSER_NUKE_MULTIPLIER = 0.25f; diff --git a/core/src/main/java/tc/oc/pgm/wool/WoolModule.java b/core/src/main/java/tc/oc/pgm/wool/WoolModule.java index 832a2976c3..df21c445bf 100644 --- a/core/src/main/java/tc/oc/pgm/wool/WoolModule.java +++ b/core/src/main/java/tc/oc/pgm/wool/WoolModule.java @@ -10,6 +10,7 @@ import org.bukkit.util.Vector; import org.jdom2.Document; import org.jdom2.Element; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.map.MapTag; @@ -34,7 +35,7 @@ public class WoolModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("ctw", "wool", "Capture the Wool", true, false)); + ImmutableList.of(new MapTag("wool", Gamemode.CAPTURE_THE_WOOL, false)); protected final Multimap woolFactories; diff --git a/core/src/main/java/tc/oc/pgm/worldborder/WorldBorderModule.java b/core/src/main/java/tc/oc/pgm/worldborder/WorldBorderModule.java index 388997e5ca..900e55363e 100644 --- a/core/src/main/java/tc/oc/pgm/worldborder/WorldBorderModule.java +++ b/core/src/main/java/tc/oc/pgm/worldborder/WorldBorderModule.java @@ -21,8 +21,7 @@ import tc.oc.pgm.util.xml.XMLUtils; public class WorldBorderModule implements MapModule { - private final Collection TAGS = - ImmutableList.of(new MapTag("border", "World Border", false, true)); + private final Collection TAGS = ImmutableList.of(new MapTag("border", "World Border")); private final List borders; public WorldBorderModule(List borders) { diff --git a/util/src/main/java/tc/oc/pgm/util/StreamUtils.java b/util/src/main/java/tc/oc/pgm/util/StreamUtils.java index dd5f15ec35..1424f930da 100644 --- a/util/src/main/java/tc/oc/pgm/util/StreamUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/StreamUtils.java @@ -1,13 +1,25 @@ package tc.oc.pgm.util; +import com.google.common.collect.ImmutableList; import java.util.Iterator; import java.util.Spliterator; import java.util.Spliterators; +import java.util.stream.Collector; import java.util.stream.Stream; import java.util.stream.StreamSupport; public class StreamUtils { + private static final Collector> TO_IMMUTABLE_LIST = + Collector.of( + ImmutableList::builder, + ImmutableList.Builder::add, + (ImmutableList.Builder a, ImmutableList.Builder b) -> { + a.addAll(b.build()); + return a; + }, + ImmutableList.Builder::build); + public static Stream of(Iterable iterable) { return of(iterable.iterator()); } @@ -16,4 +28,9 @@ public static Stream of(Iterator iterator) { return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static Collector> toImmutableList() { + return (Collector) TO_IMMUTABLE_LIST; + } }