From 5c294a6d8b68719eaa44d45f70fc18506af24c53 Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Sat, 4 Feb 2023 17:40:41 +0100 Subject: [PATCH] Implement scoreboard filters (#1118) Signed-off-by: Pablo Herrera --- .../java/tc/oc/pgm/blitz/BlitzConfig.java | 13 +- .../tc/oc/pgm/blitz/BlitzMatchModule.java | 5 + .../java/tc/oc/pgm/blitz/BlitzModule.java | 8 +- .../pgm/controlpoint/ControlPointParser.java | 2 +- .../main/java/tc/oc/pgm/core/CoreModule.java | 2 +- .../oc/pgm/destroyable/DestroyableModule.java | 2 +- .../main/java/tc/oc/pgm/flag/FlagParser.java | 2 +- core/src/main/java/tc/oc/pgm/goals/Goal.java | 3 + .../java/tc/oc/pgm/goals/ShowOptions.java | 19 +- .../main/java/tc/oc/pgm/goals/SimpleGoal.java | 5 + .../java/tc/oc/pgm/score/ScoreConfig.java | 3 + .../tc/oc/pgm/score/ScoreMatchModule.java | 5 + .../java/tc/oc/pgm/score/ScoreModule.java | 13 +- .../tc/oc/pgm/scoreboard/RenderContext.java | 115 ++++++ .../oc/pgm/scoreboard/SidebarMatchModule.java | 379 +++--------------- .../tc/oc/pgm/scoreboard/SidebarRenderer.java | 304 ++++++++++++++ .../main/java/tc/oc/pgm/wool/WoolModule.java | 3 +- 17 files changed, 542 insertions(+), 341 deletions(-) create mode 100644 core/src/main/java/tc/oc/pgm/scoreboard/RenderContext.java create mode 100644 core/src/main/java/tc/oc/pgm/scoreboard/SidebarRenderer.java diff --git a/core/src/main/java/tc/oc/pgm/blitz/BlitzConfig.java b/core/src/main/java/tc/oc/pgm/blitz/BlitzConfig.java index bbece12f2a..a088849695 100644 --- a/core/src/main/java/tc/oc/pgm/blitz/BlitzConfig.java +++ b/core/src/main/java/tc/oc/pgm/blitz/BlitzConfig.java @@ -10,14 +10,21 @@ public class BlitzConfig { private final int lives; private final boolean broadcastLives; private final Filter filter; + private final Filter scoreboardFilter; private final Filter joinFilter; - public BlitzConfig(int lives, boolean broadcastLives, Filter filter, Filter joinFilter) { + public BlitzConfig( + int lives, + boolean broadcastLives, + Filter filter, + Filter scoreboardFilter, + Filter joinFilter) { assertTrue(lives > 0, "lives must be greater than zero"); this.lives = lives; this.broadcastLives = broadcastLives; this.filter = filter; + this.scoreboardFilter = scoreboardFilter; this.joinFilter = joinFilter; } @@ -38,6 +45,10 @@ public Filter getFilter() { return this.filter; } + public Filter getScoreboardFilter() { + return scoreboardFilter; + } + public Filter getJoinFilter() { return joinFilter; } diff --git a/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java b/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java index 55590c7451..e574fefeb7 100644 --- a/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java @@ -20,6 +20,7 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.util.Vector; +import tc.oc.pgm.api.filter.Filter; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.match.MatchModule; import tc.oc.pgm.api.match.MatchScope; @@ -55,6 +56,10 @@ public BlitzConfig getConfig() { return this.config; } + public Filter getScoreboardFilter() { + return config.getScoreboardFilter(); + } + /** Whether or not the player participated in the match and was eliminated. */ public boolean isPlayerEliminated(UUID player) { return this.eliminatedPlayers.contains(player); 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 28146f1640..ba489c86f9 100644 --- a/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java +++ b/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java @@ -17,6 +17,7 @@ import tc.oc.pgm.api.match.Match; import tc.oc.pgm.filters.FilterModule; import tc.oc.pgm.filters.matcher.StaticFilter; +import tc.oc.pgm.filters.parse.DynamicFilterValidation; import tc.oc.pgm.filters.parse.FilterParser; import tc.oc.pgm.util.xml.InvalidXMLException; import tc.oc.pgm.util.xml.Node; @@ -57,6 +58,7 @@ public BlitzModule parse(MapFactory factory, Logger logger, Document doc) int lives = Integer.MAX_VALUE; boolean broadcastLives = false; Filter filter = null; + Filter scoreboardFilter = null; Filter joinFilter = null; FilterParser filters = factory.getFilters(); @@ -66,11 +68,15 @@ public BlitzModule parse(MapFactory factory, Logger logger, Document doc) XMLUtils.parseNumberInRange( Node.fromChildOrAttr(blitzEl, "lives"), Integer.class, Range.atLeast(1), 1); filter = filters.parseProperty(blitzEl, "filter", StaticFilter.ALLOW); + scoreboardFilter = + filters.parseProperty( + blitzEl, "scoreboard-filter", StaticFilter.ALLOW, DynamicFilterValidation.PARTY); joinFilter = filters.parseProperty(blitzEl, "join-filter", StaticFilter.DENY); } if (lives != Integer.MAX_VALUE) { - return new BlitzModule(new BlitzConfig(lives, broadcastLives, filter, joinFilter)); + return new BlitzModule( + new BlitzConfig(lives, broadcastLives, filter, scoreboardFilter, joinFilter)); } return null; diff --git a/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointParser.java b/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointParser.java index 6081736a95..69cdc40035 100644 --- a/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointParser.java +++ b/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointParser.java @@ -135,7 +135,7 @@ public static ControlPointDefinition parseControlPoint( XMLUtils.parseNumber( el.getAttribute("points-growth"), Float.class, Float.POSITIVE_INFINITY); boolean showProgress = XMLUtils.parseBoolean(el.getAttribute("show-progress"), koth || pd); - ShowOptions options = ShowOptions.parse(el); + ShowOptions options = ShowOptions.parse(filterParser, el); Boolean required = XMLUtils.parseBoolean(el.getAttribute("required"), null); ControlPointDefinition.CaptureCondition captureCondition = 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 88c87165b4..d078dbcfa3 100644 --- a/core/src/main/java/tc/oc/pgm/core/CoreModule.java +++ b/core/src/main/java/tc/oc/pgm/core/CoreModule.java @@ -138,7 +138,7 @@ public CoreModule parse(MapFactory context, Logger logger, Document doc) } boolean showProgress = XMLUtils.parseBoolean(coreEl.getAttribute("show-progress"), false); - ShowOptions options = ShowOptions.parse(coreEl); + ShowOptions options = ShowOptions.parse(context.getFilters(), coreEl); Boolean required = XMLUtils.parseBoolean(coreEl.getAttribute("required"), null); ProximityMetric proximityMetric = ProximityMetric.parse( 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 a7f40a9241..0910656cd2 100644 --- a/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java +++ b/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java @@ -132,7 +132,7 @@ public DestroyableModule parse(MapFactory context, Logger logger, Document doc) XMLUtils.parseBoolean(destroyableEl.getAttribute("show-progress"), false); boolean sparks = XMLUtils.parseBoolean(destroyableEl.getAttribute("sparks"), false); boolean repairable = XMLUtils.parseBoolean(destroyableEl.getAttribute("repairable"), true); - ShowOptions options = ShowOptions.parse(destroyableEl); + ShowOptions options = ShowOptions.parse(context.getFilters(), destroyableEl); Boolean required = XMLUtils.parseBoolean(destroyableEl.getAttribute("required"), null); ProximityMetric proximityMetric = ProximityMetric.parse( diff --git a/core/src/main/java/tc/oc/pgm/flag/FlagParser.java b/core/src/main/java/tc/oc/pgm/flag/FlagParser.java index c442922aef..c575a82988 100644 --- a/core/src/main/java/tc/oc/pgm/flag/FlagParser.java +++ b/core/src/main/java/tc/oc/pgm/flag/FlagParser.java @@ -248,7 +248,7 @@ public FlagDefinition parseFlag(Element el) throws InvalidXMLException { String id = el.getAttributeValue("id"); String name = el.getAttributeValue("name"); - ShowOptions options = ShowOptions.parse(el); + ShowOptions options = ShowOptions.parse(filterParser, el); Boolean required = XMLUtils.parseBoolean(el.getAttribute("required"), null); DyeColor color = XMLUtils.parseDyeColor(el.getAttribute("color"), null); FeatureReference owner = diff --git a/core/src/main/java/tc/oc/pgm/goals/Goal.java b/core/src/main/java/tc/oc/pgm/goals/Goal.java index a881689daa..15033638fc 100644 --- a/core/src/main/java/tc/oc/pgm/goals/Goal.java +++ b/core/src/main/java/tc/oc/pgm/goals/Goal.java @@ -8,6 +8,7 @@ import org.bukkit.DyeColor; import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.feature.Feature; +import tc.oc.pgm.api.filter.Filter; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.party.Competitor; import tc.oc.pgm.api.party.Party; @@ -50,6 +51,8 @@ default boolean isCompleted(Optional competitor) { */ boolean hasShowOption(ShowOption option); + Filter getScoreboardFilter(); + boolean isRequired(); /** diff --git a/core/src/main/java/tc/oc/pgm/goals/ShowOptions.java b/core/src/main/java/tc/oc/pgm/goals/ShowOptions.java index 9b341fd27c..eaf78a053e 100644 --- a/core/src/main/java/tc/oc/pgm/goals/ShowOptions.java +++ b/core/src/main/java/tc/oc/pgm/goals/ShowOptions.java @@ -3,18 +3,24 @@ import java.util.EnumSet; import java.util.Set; import org.jdom2.Element; +import tc.oc.pgm.api.filter.Filter; +import tc.oc.pgm.filters.matcher.StaticFilter; +import tc.oc.pgm.filters.parse.DynamicFilterValidation; +import tc.oc.pgm.filters.parse.FilterParser; import tc.oc.pgm.util.xml.InvalidXMLException; import tc.oc.pgm.util.xml.XMLUtils; public class ShowOptions { private final Set options; + private final Filter scoreboardFilter; - private ShowOptions(Set options) { + private ShowOptions(Set options, Filter scoreboardFilter) { this.options = options; + this.scoreboardFilter = scoreboardFilter; } - public static ShowOptions parse(Element el) throws InvalidXMLException { + public static ShowOptions parse(FilterParser parser, Element el) throws InvalidXMLException { Set options = EnumSet.noneOf(ShowOption.class); boolean show = XMLUtils.parseBoolean(el.getAttribute("show"), true); for (ShowOption option : ShowOption.values()) { @@ -22,13 +28,20 @@ public static ShowOptions parse(Element el) throws InvalidXMLException { options.add(option); } } - return new ShowOptions(options); + Filter scoreboardFilter = + parser.parseProperty( + el, "scoreboard-filter", StaticFilter.ALLOW, DynamicFilterValidation.MATCH); + return new ShowOptions(options, scoreboardFilter); } public boolean hasOption(ShowOption option) { return options.contains(option); } + public Filter getScoreboardFilter() { + return scoreboardFilter; + } + @Override public String toString() { return options.toString(); diff --git a/core/src/main/java/tc/oc/pgm/goals/SimpleGoal.java b/core/src/main/java/tc/oc/pgm/goals/SimpleGoal.java index b891dbb37c..b112885c37 100644 --- a/core/src/main/java/tc/oc/pgm/goals/SimpleGoal.java +++ b/core/src/main/java/tc/oc/pgm/goals/SimpleGoal.java @@ -12,6 +12,7 @@ import org.bukkit.Color; import org.bukkit.DyeColor; import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.filter.Filter; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.party.Competitor; @@ -93,6 +94,10 @@ public boolean hasShowOption(ShowOption flag) { return this.definition.hasShowOption(flag); } + public Filter getScoreboardFilter() { + return this.definition.getShowOptions().getScoreboardFilter(); + } + @Override public boolean isRequired() { Boolean required = getDefinition().isRequired(); diff --git a/core/src/main/java/tc/oc/pgm/score/ScoreConfig.java b/core/src/main/java/tc/oc/pgm/score/ScoreConfig.java index 0f312e8ff2..be4929707e 100644 --- a/core/src/main/java/tc/oc/pgm/score/ScoreConfig.java +++ b/core/src/main/java/tc/oc/pgm/score/ScoreConfig.java @@ -1,9 +1,12 @@ package tc.oc.pgm.score; +import tc.oc.pgm.api.filter.Filter; + public class ScoreConfig { public int scoreLimit = -1; public int deathScore; public int killScore; public int mercyLimit; public int mercyLimitMin; + public Filter scoreboardFilter; } diff --git a/core/src/main/java/tc/oc/pgm/score/ScoreMatchModule.java b/core/src/main/java/tc/oc/pgm/score/ScoreMatchModule.java index 12e29a725c..57217f7a88 100644 --- a/core/src/main/java/tc/oc/pgm/score/ScoreMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/score/ScoreMatchModule.java @@ -27,6 +27,7 @@ import org.bukkit.inventory.PlayerInventory; import org.bukkit.util.Vector; import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.filter.Filter; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.match.MatchModule; import tc.oc.pgm.api.match.MatchScope; @@ -80,6 +81,10 @@ public boolean hasMercyRule() { return this.mercyRule != null; } + public Filter getScoreboardFilter() { + return this.config.scoreboardFilter; + } + public int getScoreLimit() { assertTrue(hasScoreLimit()); 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 5e61fcf6db..4b745bb1ca 100644 --- a/core/src/main/java/tc/oc/pgm/score/ScoreModule.java +++ b/core/src/main/java/tc/oc/pgm/score/ScoreModule.java @@ -25,6 +25,7 @@ import tc.oc.pgm.blitz.BlitzModule; import tc.oc.pgm.filters.FilterModule; import tc.oc.pgm.filters.matcher.StaticFilter; +import tc.oc.pgm.filters.parse.DynamicFilterValidation; import tc.oc.pgm.regions.RegionModule; import tc.oc.pgm.regions.RegionParser; import tc.oc.pgm.util.Version; @@ -108,8 +109,7 @@ public ScoreModule parse(MapFactory factory, Logger logger, Document doc) } // For backwards compatibility, default kill/death points to 1 if proto is old and - // tag - // is not present + // tag is not present boolean scoreKillsByDefault = proto.isOlderThan(MapProtos.DEFAULT_SCORES_TO_ZERO) && scoreEl.getChild("king") == null; config.deathScore = @@ -119,6 +119,15 @@ public ScoreModule parse(MapFactory factory, Logger logger, Document doc) XMLUtils.parseNumber( scoreEl.getChild("kills"), Integer.class, scoreKillsByDefault ? 1 : 0); + config.scoreboardFilter = + factory + .getFilters() + .parseProperty( + scoreEl, + "scoreboard-filter", + StaticFilter.ALLOW, + DynamicFilterValidation.PARTY); + for (Element scoreBoxEl : scoreEl.getChildren("box")) { int points = XMLUtils.parseNumber( diff --git a/core/src/main/java/tc/oc/pgm/scoreboard/RenderContext.java b/core/src/main/java/tc/oc/pgm/scoreboard/RenderContext.java new file mode 100644 index 0000000000..176a6a638d --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/scoreboard/RenderContext.java @@ -0,0 +1,115 @@ +package tc.oc.pgm.scoreboard; + +import static net.kyori.adventure.text.Component.empty; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import tc.oc.pgm.api.match.Match; +import tc.oc.pgm.api.party.Competitor; +import tc.oc.pgm.api.party.Party; +import tc.oc.pgm.blitz.BlitzMatchModule; +import tc.oc.pgm.goals.Goal; +import tc.oc.pgm.goals.GoalMatchModule; +import tc.oc.pgm.goals.ShowOption; +import tc.oc.pgm.score.ScoreMatchModule; +import tc.oc.pgm.wool.WoolMatchModule; + +class RenderContext { + public final @NotNull Match match; + public final @NotNull Party viewer; + public final boolean hasScores; + public final boolean isBlitz; + public final boolean isCompactWool; + public final GoalMatchModule gmm; + public final Set competitorsWithGoals; + public final List> sharedGoals; + public final boolean isSuperCompact; + + private final List rows = new ArrayList<>(SidebarRenderer.MAX_ROWS); + + private boolean addSpace = false; + + public RenderContext(@NotNull Match match, @NotNull Party viewer) { + this.match = match; + this.viewer = viewer; + this.hasScores = match.getModule(ScoreMatchModule.class) != null; + this.isBlitz = match.getModule(BlitzMatchModule.class) != null; + this.isCompactWool = isCompactWool(); + + this.gmm = match.needModule(GoalMatchModule.class); + this.competitorsWithGoals = new HashSet<>(); + this.sharedGoals = new ArrayList<>(); + + // Count the rows used for goals + for (Goal goal : gmm.getGoals()) { + if (goal.hasShowOption(ShowOption.SHOW_SIDEBAR) + && goal.getScoreboardFilter().response(match)) { + if (goal.isShared()) { + sharedGoals.add(goal); + } else { + competitorsWithGoals.addAll(gmm.getCompetitors(goal)); + } + } + } + this.isSuperCompact = isSuperCompact(); + } + + public void startSection() { + addSpace = rows.size() > 0; + } + + public void addRow(Component row) { + if (addSpace) { + this.rows.add(empty()); + addSpace = false; + } + this.rows.add(row); + } + + public List getResult() { + // Needs at least one empty row for scoreboard to show + if (rows.isEmpty()) { + rows.add(empty()); + } + return rows; + } + + public int size() { + return rows.size(); + } + + public boolean isFull() { + return rows.size() >= SidebarRenderer.MAX_ROWS; + } + + public boolean isEmpty() { + return rows.isEmpty(); + } + + // Determines if wool objectives should be given their own rows, or all shown on 1 row. + private boolean isCompactWool() { + WoolMatchModule wmm = match.getModule(WoolMatchModule.class); + return wmm != null + && !(wmm.getWools().keySet().size() * 2 - 1 + wmm.getWools().values().size() + < SidebarRenderer.MAX_ROWS); + } + + // Determines if all the map objectives can fit onto the scoreboard with empty rows in between. + private boolean isSuperCompact() { + int rowsUsed = competitorsWithGoals.size() * 2 - 1; + + if (isCompactWool()) { + WoolMatchModule wmm = match.needModule(WoolMatchModule.class); + rowsUsed += wmm.getWools().keySet().size(); + } else { + GoalMatchModule gmm = match.needModule(GoalMatchModule.class); + rowsUsed += gmm.getGoals().size(); + } + + return !(rowsUsed < SidebarRenderer.MAX_ROWS); + } +} diff --git a/core/src/main/java/tc/oc/pgm/scoreboard/SidebarMatchModule.java b/core/src/main/java/tc/oc/pgm/scoreboard/SidebarMatchModule.java index 23c2154e54..dd52b8e086 100644 --- a/core/src/main/java/tc/oc/pgm/scoreboard/SidebarMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/scoreboard/SidebarMatchModule.java @@ -1,42 +1,23 @@ package tc.oc.pgm.scoreboard; -import static net.kyori.adventure.text.Component.empty; -import static net.kyori.adventure.text.Component.space; -import static net.kyori.adventure.text.Component.text; -import static net.kyori.adventure.text.Component.translatable; - import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import fr.mrmicky.fastboard.FastBoard; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.UUID; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; -import java.util.stream.Stream; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextComponent; -import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.ChatColor; -import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.jetbrains.annotations.Nullable; -import tc.oc.pgm.api.Config; import tc.oc.pgm.api.PGM; -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.match.Match; import tc.oc.pgm.api.match.MatchModule; import tc.oc.pgm.api.match.MatchScope; @@ -44,7 +25,6 @@ import tc.oc.pgm.api.match.event.MatchVictoryChangeEvent; import tc.oc.pgm.api.match.factory.MatchModuleFactory; import tc.oc.pgm.api.module.exception.ModuleLoadException; -import tc.oc.pgm.api.party.Competitor; import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.party.event.CompetitorScoreChangeEvent; import tc.oc.pgm.api.party.event.PartyRenameEvent; @@ -57,35 +37,33 @@ import tc.oc.pgm.events.PlayerJoinMatchEvent; import tc.oc.pgm.events.PlayerLeaveMatchEvent; import tc.oc.pgm.events.PlayerPartyChangeEvent; -import tc.oc.pgm.ffa.Tribute; +import tc.oc.pgm.filters.FilterMatchModule; import tc.oc.pgm.goals.Goal; import tc.oc.pgm.goals.GoalMatchModule; -import tc.oc.pgm.goals.ProximityGoal; -import tc.oc.pgm.goals.ShowOption; import tc.oc.pgm.goals.events.GoalCompleteEvent; import tc.oc.pgm.goals.events.GoalProximityChangeEvent; import tc.oc.pgm.goals.events.GoalStatusChangeEvent; import tc.oc.pgm.goals.events.GoalTouchEvent; import tc.oc.pgm.score.ScoreMatchModule; import tc.oc.pgm.spawns.events.ParticipantSpawnEvent; -import tc.oc.pgm.teams.Team; import tc.oc.pgm.teams.events.TeamRespawnsChangeEvent; import tc.oc.pgm.util.TimeUtils; import tc.oc.pgm.util.concurrent.RateLimiter; import tc.oc.pgm.util.event.player.PlayerLocaleChangeEvent; -import tc.oc.pgm.util.named.NameStyle; -import tc.oc.pgm.util.text.TextFormatter; -import tc.oc.pgm.util.text.TextTranslations; -import tc.oc.pgm.wool.MonumentWool; -import tc.oc.pgm.wool.WoolMatchModule; @ListenerScope(MatchScope.LOADED) public class SidebarMatchModule implements MatchModule, Listener { public static class Factory implements MatchModuleFactory { + @Override + public Collection> getWeakDependencies() { + return ImmutableList.of( + GoalMatchModule.class, ScoreMatchModule.class, BlitzMatchModule.class); + } + @Override public Collection> getSoftDependencies() { - return ImmutableList.of(ScoreboardMatchModule.class); + return ImmutableList.of(FilterMatchModule.class, ScoreboardMatchModule.class); } @Override @@ -94,10 +72,6 @@ public SidebarMatchModule createMatchModule(Match match) throws ModuleLoadExcept } } - public static final int MAX_ROWS = 16; // Max rows on the scoreboard - public static final int MAX_LENGTH = 30; // Max characters per line allowed - public static final int MAX_TITLE = 32; // Max characters allowed in title - protected final Map sidebars = new HashMap<>(); protected final Map, BlinkTask> blinkingGoals = new HashMap<>(); @@ -105,45 +79,21 @@ public SidebarMatchModule createMatchModule(Match match) throws ModuleLoadExcept private final RateLimiter rateLimit = new RateLimiter(50, 1000, 40, 1000); private final Match match; + private final SidebarRenderer renderer; private final Component title; public SidebarMatchModule(Match match) { this.match = match; - this.title = renderTitle(PGM.get().getConfiguration(), match.getMap()); - } - - private boolean hasScores() { - return match.getModule(ScoreMatchModule.class) != null; - } - - private boolean isBlitz() { - return match.getModule(BlitzMatchModule.class) != null; - } - - // Determines if wool objectives should be given their own rows, or all shown on 1 row. - private boolean isCompactWool() { - WoolMatchModule wmm = match.getModule(WoolMatchModule.class); - return wmm != null - && !(wmm.getWools().keySet().size() * 2 - 1 + wmm.getWools().values().size() < MAX_ROWS); - } - - // Determines if all the map objectives can fit onto the scoreboard with empty rows in between. - private boolean isSuperCompact(Set competitorsWithGoals) { - int rowsUsed = competitorsWithGoals.size() * 2 - 1; - - if (isCompactWool()) { - WoolMatchModule wmm = match.needModule(WoolMatchModule.class); - rowsUsed += wmm.getWools().keySet().size(); - } else { - GoalMatchModule gmm = match.needModule(GoalMatchModule.class); - rowsUsed += gmm.getGoals().size(); - } - - return !(rowsUsed < MAX_ROWS); + this.renderer = new SidebarRenderer(match, this); + this.title = renderer.renderTitle(); } private void addSidebar(MatchPlayer player) { - sidebars.put(player.getId(), new FastBoard(player.getBukkit())); + FastBoard sidebar = new FastBoard(player.getBukkit()); + // Only render the title once, since it does not change during the match. + sidebar.updateTitle(renderer.renderTitle(title, player)); + + sidebars.put(player.getId(), sidebar); } @Override @@ -152,6 +102,33 @@ public void load() { addSidebar(player); } renderSidebarDebounce(); + + FilterMatchModule fmm = match.needModule(FilterMatchModule.class); + match + .needModule(GoalMatchModule.class) + .getGoals() + .forEach( + goal -> + fmm.onChange( + Match.class, + goal.getScoreboardFilter(), + (m, v) -> this.renderSidebarDebounce())); + match + .moduleOptional(ScoreMatchModule.class) + .ifPresent( + smm -> + fmm.onChange( + Party.class, + smm.getScoreboardFilter(), + (p, v) -> this.renderSidebarDebounce())); + match + .moduleOptional(BlitzMatchModule.class) + .ifPresent( + bmm -> + fmm.onChange( + Party.class, + bmm.getScoreboardFilter(), + (p, v) -> this.renderSidebarDebounce())); } @Override @@ -264,119 +241,6 @@ public void matchEnd(MatchFinishEvent event) { rateLimit.setTimeout(Long.MAX_VALUE); } - private Component renderTitle(final Config config, final MapInfo map) { - final Component header = config.getMatchHeader(); - if (header != null) { - return header.colorIfAbsent(NamedTextColor.AQUA); - } - - final Component gamemode = map.getGamemode(); - if (gamemode != null) { - return gamemode.colorIfAbsent(NamedTextColor.AQUA); - } - - final Collection gamemodes = map.getGamemodes(); - if (!gamemodes.isEmpty()) { - String suffix = gamemodes.size() <= 1 ? ".name" : ".acronym"; - List gmComponents = - gamemodes.stream() - .map(gm -> translatable("gamemode." + gm.getId() + suffix)) - .collect(Collectors.toList()); - return TextFormatter.list(gmComponents, NamedTextColor.AQUA); - } - - final List games = new LinkedList<>(); - - // First, find a primary game mode - for (final MapTag tag : map.getTags()) { - if (!tag.isGamemode() || tag.isAuxiliary()) continue; - - if (games.isEmpty()) { - games.add(tag.getName().color(NamedTextColor.AQUA)); - continue; - } - - // When there are multiple, primary game modes - games.set(0, translatable("gamemode.generic.name", NamedTextColor.AQUA)); - break; - } - - // Second, append auxiliary game modes - for (final MapTag tag : map.getTags()) { - if (!tag.isGamemode() || !tag.isAuxiliary()) continue; - - // There can only be 2 game modes - if (games.size() < 2) { - games.add(tag.getName().color(NamedTextColor.AQUA)); - } else { - break; - } - } - - // Display "Blitz: Rage" rather than "Blitz and Rage" - if (games.size() == 2 - && Stream.of("blitz", "rage") - .allMatch(id -> map.getTags().stream().anyMatch(mt -> mt.getId().equals(id)))) { - games.clear(); - games.add(translatable("gamemode.br.name").color(NamedTextColor.AQUA)); - } - - return TextFormatter.list(games, NamedTextColor.AQUA); - } - - private Component renderGoal(Goal goal, @Nullable Competitor competitor, Party viewingParty) { - final BlinkTask blinkTask = this.blinkingGoals.get(goal); - final TextComponent.Builder line = text(); - - line.append(space()); - line.append( - goal.renderSidebarStatusText(competitor, viewingParty) - .color( - blinkTask != null && blinkTask.isDark() - ? NamedTextColor.BLACK - : goal.renderSidebarStatusColor(competitor, viewingParty))); - - if (goal instanceof ProximityGoal) { - final ProximityGoal proximity = (ProximityGoal) goal; - if (proximity.shouldShowProximity(competitor, viewingParty)) { - line.append(space()); - line.append(proximity.renderProximity(competitor, viewingParty)); - } - } - - line.append(space()); - line.append( - goal.renderSidebarLabelText(competitor, viewingParty) - .color(goal.renderSidebarLabelColor(competitor, viewingParty))); - - return line.build(); - } - - private Component renderScore(Competitor competitor) { - ScoreMatchModule smm = match.needModule(ScoreMatchModule.class); - Component score = text((int) smm.getScore(competitor), NamedTextColor.WHITE); - if (!smm.hasScoreLimit()) { - return score; - } - return text() - .append(score) - .append(text("/", NamedTextColor.DARK_GRAY)) - .append(text(smm.getScoreLimit(), NamedTextColor.GRAY)) - .build(); - } - - private Component renderBlitz(Competitor competitor) { - BlitzMatchModule bmm = match.needModule(BlitzMatchModule.class); - if (competitor instanceof Team) { - return text(bmm.getRemainingPlayers(competitor), NamedTextColor.WHITE); - } else if (competitor instanceof Tribute && bmm.getConfig().getNumLives() > 1) { - final UUID id = competitor.getPlayers().iterator().next().getId(); - return text(bmm.getNumOfLives(id), NamedTextColor.WHITE); - } else { - return empty(); - } - } - private void renderSidebarDebounce() { // Debounced render if (this.renderTask == null || renderTask.isDone()) { @@ -396,161 +260,20 @@ private void renderSidebarDebounce() { } private void renderSidebar() { - final boolean hasScores = hasScores(); - final boolean isBlitz = isBlitz(); - final boolean isCompactWool = isCompactWool(); - final GoalMatchModule gmm = match.needModule(GoalMatchModule.class); - - Set competitorsWithGoals = new HashSet<>(); - List> sharedGoals = new ArrayList<>(); - - // Count the rows used for goals - for (Goal goal : gmm.getGoals()) { - if (goal.hasShowOption(ShowOption.SHOW_SIDEBAR)) { - if (goal.isShared()) { - sharedGoals.add(goal); - } else { - competitorsWithGoals.addAll(gmm.getCompetitors(goal)); - } - } - } - final boolean isSuperCompact = isSuperCompact(competitorsWithGoals); + Map> cache = new HashMap<>(); for (MatchPlayer player : this.match.getPlayers()) { - final FastBoard sidebar = this.sidebars.get(player.getId()); + FastBoard sidebar = this.sidebars.get(player.getId()); if (sidebar == null) continue; - final Player viewer = player.getBukkit(); - final Party party = player.getParty(); - final List rows = new ArrayList<>(MAX_ROWS); - - // Scores/Blitz - if (hasScores || isBlitz) { - for (Competitor competitor : match.getSortedCompetitors()) { - Component text; - if (hasScores) { - text = renderScore(competitor); - } else { - text = renderBlitz(competitor); - } - if (text != empty()) { - text = text.append(space()); - } - rows.add(text.append(competitor.getName(NameStyle.SIMPLE_COLOR))); - - // No point rendering more scores, usually seen in FFA - if (rows.size() >= MAX_ROWS) break; - } + List rows = cache.computeIfAbsent(player.getParty(), renderer::renderSidebar); - if (!competitorsWithGoals.isEmpty() || !sharedGoals.isEmpty()) { - // Blank row between scores and goals - rows.add(empty()); - } + List result = new ArrayList<>(rows.size()); + for (Component row : rows) { + result.add(renderer.renderRow(row, player)); } - boolean firstTeam = true; - - // Shared goals i.e. not grouped under a specific team - for (Goal goal : sharedGoals) { - firstTeam = false; - rows.add(this.renderGoal(goal, null, party)); - } - - // Team-specific goals - List sortedCompetitors = new ArrayList<>(match.getSortedCompetitors()); - sortedCompetitors.retainAll(competitorsWithGoals); - - if (party instanceof Competitor) { - // Bump viewing party to the top of the list - if (sortedCompetitors.remove(party)) { - sortedCompetitors.add(0, (Competitor) party); - } - } - - for (Competitor competitor : sortedCompetitors) { - // Prevent team name from showing if there isn't space for at least 1 row of its objectives - if (!(rows.size() + 2 < MAX_ROWS)) break; - - if (!(firstTeam || isSuperCompact)) { - // Add a blank row between teams - rows.add(space()); - } - firstTeam = false; - - // Add a row for the team name - rows.add(competitor.getName()); - - if (isCompactWool) { - boolean firstWool = true; - - List sortedWools = new ArrayList<>(gmm.getGoals(competitor)); - Collections.sort(sortedWools, (a, b) -> a.getName().compareToIgnoreCase(b.getName())); - - // Calculate whether having three spaces between each wool would fit on the scoreboard. - boolean horizontalCompact = - MAX_LENGTH < (3 * sortedWools.size()) + (3 * (sortedWools.size() - 1)) + 1; - TextComponent.Builder woolText = text(); - for (Goal goal : sortedWools) { - if (goal instanceof MonumentWool && goal.hasShowOption(ShowOption.SHOW_SIDEBAR)) { - MonumentWool wool = (MonumentWool) goal; - TextComponent spacer = space(); - if (!firstWool && !horizontalCompact) { - spacer = spacer.append(space()).append(space()); - } - firstWool = false; - woolText.append( - spacer - .append(wool.renderSidebarStatusText(competitor, party)) - .color(wool.renderSidebarStatusColor(competitor, party))); - } - } - // Add a row for the compact wools - rows.add(woolText.build()); - - } else { - // Not compact; add a row for each of this team's goals - for (Goal goal : gmm.getGoals()) { - if (!goal.isShared() - && goal.canComplete(competitor) - && goal.hasShowOption(ShowOption.SHOW_SIDEBAR)) { - rows.add(this.renderGoal(goal, competitor, party)); - } - } - } - } - - final Component footer = PGM.get().getConfiguration().getMatchFooter(); - if (footer != null) { - // Only shows footer if there are one or two rows available - if (rows.size() < MAX_ROWS - 2) { - rows.add(empty()); - } - rows.add(footer); - } - - // Need at least one row for the sidebar to show - if (rows.isEmpty()) { - rows.add(empty()); - } - - // Only render the title once, since it does not change during the match. - // FastBoard sets default title to ChatColor.RESET. - if (sidebar.getTitle().equals(ChatColor.RESET.toString())) { - final String titleText = TextTranslations.translateLegacy(title, viewer); - sidebar.updateTitle(titleText.substring(0, Math.min(titleText.length(), MAX_TITLE))); - } - - final int rowsSize = Math.min(rows.size(), MAX_ROWS); - final List rowsText = new ArrayList<>(); - for (int i = 0; i < rowsSize; i++) { - final String rowText = TextTranslations.translateLegacy(rows.get(i), viewer); - if (rowText.length() < MAX_LENGTH) { - rowsText.add(rowText); - } else { - rowsText.add(rowText.substring(0, MAX_LENGTH)); - } - } - sidebar.updateLines(rowsText); + sidebar.updateLines(result); } } @@ -568,7 +291,7 @@ public void stopBlinkingGoal(Goal goal) { if (task != null) task.stop(); } - private class BlinkTask implements Runnable { + protected class BlinkTask implements Runnable { private final Future task; private final Goal goal; diff --git a/core/src/main/java/tc/oc/pgm/scoreboard/SidebarRenderer.java b/core/src/main/java/tc/oc/pgm/scoreboard/SidebarRenderer.java new file mode 100644 index 0000000000..b1642c2e5e --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/scoreboard/SidebarRenderer.java @@ -0,0 +1,304 @@ +package tc.oc.pgm.scoreboard; + +import static net.kyori.adventure.text.Component.empty; +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.Component.translatable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.PGM; +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.match.Match; +import tc.oc.pgm.api.party.Competitor; +import tc.oc.pgm.api.party.Party; +import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.blitz.BlitzMatchModule; +import tc.oc.pgm.ffa.Tribute; +import tc.oc.pgm.goals.Goal; +import tc.oc.pgm.goals.ProximityGoal; +import tc.oc.pgm.goals.ShowOption; +import tc.oc.pgm.score.ScoreMatchModule; +import tc.oc.pgm.teams.Team; +import tc.oc.pgm.util.StringUtils; +import tc.oc.pgm.util.named.NameStyle; +import tc.oc.pgm.util.text.TextFormatter; +import tc.oc.pgm.util.text.TextTranslations; +import tc.oc.pgm.wool.MonumentWool; + +class SidebarRenderer { + + public static final int MAX_ROWS = 16; // Max rows on the scoreboard + public static final int MAX_LENGTH = 30; // Max characters per line allowed + public static final int MAX_TITLE = 32; // Max characters allowed in title + + private final Match match; + private final SidebarMatchModule smm; + + public SidebarRenderer(Match match, SidebarMatchModule smm) { + this.match = match; + this.smm = smm; + } + + public Component renderTitle() { + final MapInfo map = match.getMap(); + final Component header = PGM.get().getConfiguration().getMatchHeader(); + if (header != null) { + return header.colorIfAbsent(NamedTextColor.AQUA); + } + + final Component gamemode = map.getGamemode(); + if (gamemode != null) { + return gamemode.colorIfAbsent(NamedTextColor.AQUA); + } + + final Collection gamemodes = map.getGamemodes(); + if (!gamemodes.isEmpty()) { + String suffix = gamemodes.size() <= 1 ? ".name" : ".acronym"; + List gmComponents = + gamemodes.stream() + .map(gm -> translatable("gamemode." + gm.getId() + suffix)) + .collect(Collectors.toList()); + return TextFormatter.list(gmComponents, NamedTextColor.AQUA); + } + + final List games = new LinkedList<>(); + + // First, find a primary game mode + for (final MapTag tag : map.getTags()) { + if (!tag.isGamemode() || tag.isAuxiliary()) continue; + + if (games.isEmpty()) { + games.add(tag.getName().color(NamedTextColor.AQUA)); + continue; + } + + // When there are multiple, primary game modes + games.set(0, translatable("gamemode.generic.name", NamedTextColor.AQUA)); + break; + } + + // Second, append auxiliary game modes + for (final MapTag tag : map.getTags()) { + if (!tag.isGamemode() || !tag.isAuxiliary()) continue; + + // There can only be 2 game modes + if (games.size() < 2) { + games.add(tag.getName().color(NamedTextColor.AQUA)); + } else { + break; + } + } + + // Display "Blitz: Rage" rather than "Blitz and Rage" + if (games.size() == 2 + && Stream.of("blitz", "rage") + .allMatch(id -> map.getTags().stream().anyMatch(mt -> mt.getId().equals(id)))) { + games.clear(); + games.add(translatable("gamemode.br.name").color(NamedTextColor.AQUA)); + } + + return TextFormatter.list(games, NamedTextColor.AQUA); + } + + public List renderSidebar(final Party party) { + RenderContext context = new RenderContext(match, party); + + // Scores/Blitz + renderScoresOrBlitz(context); + + // Shared goals i.e. not grouped under a specific team + renderSharedGoals(context); + + // Team-specific goals + for (Competitor competitor : getSortedCompetitors(context, party)) { + // Avoid rendering team name & objectives if no objective will fit after the name + if ((context.size() + 2) >= MAX_ROWS) break; + + renderCompetitor(context, competitor); + } + + // Config-based footer, if any is defined + renderFooter(context); + + return context.getResult(); + } + + private void renderScoresOrBlitz(RenderContext context) { + if (!context.hasScores && !context.isBlitz) return; + context.startSection(); + + for (Competitor competitor : match.getSortedCompetitors()) { + Component text; + if (context.hasScores) { + text = renderScore(competitor); + } else { + text = renderBlitz(competitor); + } + if (text != null) { + + if (text != empty()) { + text = text.append(space()); + } + context.addRow(text.append(competitor.getName(NameStyle.SIMPLE_COLOR))); + + // No point rendering more scores, usually seen in FFA + if (context.isFull()) break; + } + } + } + + private void renderSharedGoals(RenderContext context) { + context.startSection(); + for (Goal goal : context.sharedGoals) { + context.addRow(this.renderGoal(goal, null, context.viewer)); + } + } + + private List getSortedCompetitors(RenderContext context, Party viewer) { + List sortedCompetitors = new ArrayList<>(match.getSortedCompetitors()); + sortedCompetitors.retainAll(context.competitorsWithGoals); + // Bump viewing party to the top of the list + if (viewer instanceof Competitor && sortedCompetitors.remove(viewer)) { + sortedCompetitors.add(0, (Competitor) viewer); + } + return sortedCompetitors; + } + + private void renderCompetitor(RenderContext context, Competitor competitor) { + if (!context.isSuperCompact) context.startSection(); + + // Add a row for the team name + context.addRow(competitor.getName()); + + if (context.isCompactWool) { + boolean firstWool = true; + + List sortedWools = new ArrayList<>(context.gmm.getGoals(competitor)); + sortedWools.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName())); + + // Calculate whether having three spaces between each wool would fit on the scoreboard. + boolean horizontalCompact = + MAX_LENGTH < (3 * sortedWools.size()) + (3 * (sortedWools.size() - 1)) + 1; + TextComponent.Builder woolText = text(); + for (Goal goal : sortedWools) { + if (goal instanceof MonumentWool && goal.hasShowOption(ShowOption.SHOW_SIDEBAR)) { + MonumentWool wool = (MonumentWool) goal; + TextComponent spacer = space(); + if (!firstWool && !horizontalCompact) { + spacer = spacer.append(space()).append(space()); + } + firstWool = false; + woolText.append( + spacer + .append(wool.renderSidebarStatusText(competitor, context.viewer)) + .color(wool.renderSidebarStatusColor(competitor, context.viewer))); + } + } + // Add a row for the compact wools + context.addRow(woolText.build()); + + } else { + // Not compact; add a row for each of this team's goals + for (Goal goal : context.gmm.getGoals()) { + if (!goal.isShared() + && goal.canComplete(competitor) + && goal.hasShowOption(ShowOption.SHOW_SIDEBAR)) { + context.addRow(this.renderGoal(goal, competitor, context.viewer)); + } + } + } + } + + private void renderFooter(RenderContext context) { + final Component footer = PGM.get().getConfiguration().getMatchFooter(); + if (footer != null) { + // Only shows footer if there are one or two rows available + if (context.size() < MAX_ROWS - 2) { + context.startSection(); + } + context.addRow(footer); + } + } + + private Component renderScore(Competitor competitor) { + ScoreMatchModule smm = match.needModule(ScoreMatchModule.class); + if (!smm.getScoreboardFilter().response(competitor)) { + return null; + } + Component score = text((int) smm.getScore(competitor), NamedTextColor.WHITE); + if (!smm.hasScoreLimit()) { + return score; + } + return text() + .append(score) + .append(text("/", NamedTextColor.DARK_GRAY)) + .append(text(smm.getScoreLimit(), NamedTextColor.GRAY)) + .build(); + } + + private Component renderBlitz(Competitor competitor) { + BlitzMatchModule bmm = match.needModule(BlitzMatchModule.class); + if (!bmm.getConfig().getScoreboardFilter().response(competitor)) { + return null; + } else if (competitor instanceof Team) { + return text(bmm.getRemainingPlayers(competitor), NamedTextColor.WHITE); + } else if (competitor instanceof Tribute && bmm.getConfig().getNumLives() > 1) { + final UUID id = competitor.getPlayers().iterator().next().getId(); + return text(bmm.getNumOfLives(id), NamedTextColor.WHITE); + } else { + return empty(); + } + } + + private Component renderGoal(Goal goal, @Nullable Competitor competitor, Party viewingParty) { + final SidebarMatchModule.BlinkTask blinkTask = smm.blinkingGoals.get(goal); + final TextComponent.Builder line = text(); + + line.append(space()); + line.append( + goal.renderSidebarStatusText(competitor, viewingParty) + .color( + blinkTask != null && blinkTask.isDark() + ? NamedTextColor.BLACK + : goal.renderSidebarStatusColor(competitor, viewingParty))); + + if (goal instanceof ProximityGoal) { + final ProximityGoal proximity = (ProximityGoal) goal; + if (proximity.shouldShowProximity(competitor, viewingParty)) { + line.append(space()); + line.append(proximity.renderProximity(competitor, viewingParty)); + } + } + + line.append(space()); + line.append( + goal.renderSidebarLabelText(competitor, viewingParty) + .color(goal.renderSidebarLabelColor(competitor, viewingParty))); + + return line.build(); + } + + @SuppressWarnings("deprecation") + public String renderTitle(Component title, MatchPlayer viewer) { + return StringUtils.substring( + TextTranslations.translateLegacy(title, viewer), 0, SidebarRenderer.MAX_TITLE); + } + + @SuppressWarnings("deprecation") + public String renderRow(Component row, MatchPlayer viewer) { + return StringUtils.substring( + TextTranslations.translateLegacy(row, viewer), 0, SidebarRenderer.MAX_LENGTH); + } +} 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 37f9a25ddb..832a2976c3 100644 --- a/core/src/main/java/tc/oc/pgm/wool/WoolModule.java +++ b/core/src/main/java/tc/oc/pgm/wool/WoolModule.java @@ -80,7 +80,6 @@ public Collection>> getSoftDependencies() { public WoolModule parse(MapFactory factory, Logger logger, Document doc) throws InvalidXMLException { Multimap woolFactories = ArrayListMultimap.create(); - TeamModule teamModule = factory.needModule(TeamModule.class); RegionParser parser = factory.getRegions(); for (Element woolEl : XMLUtils.flattenElements(doc.getRootElement(), "wools", "wool")) { @@ -95,7 +94,7 @@ public WoolModule parse(MapFactory factory, Logger logger, Document doc) } else { placement = parser.parseRequiredRegionProperty(woolEl, "monument"); } - ShowOptions options = ShowOptions.parse(woolEl); + ShowOptions options = ShowOptions.parse(factory.getFilters(), woolEl); Boolean required = XMLUtils.parseBoolean(woolEl.getAttribute("required"), null); ProximityMetric woolProximityMetric =