Skip to content

Commit 7ed2058

Browse files
committed
feat,refactor(replacements): add a global replacement registry
Allows for global replacements to be registered on the map level: ```xml <replacements> <switch id="bombsite"> <case filter="filter-a" result="filter-a-result"/> <case filter="filter-b" result="filter-b-result"/> </switch> </replacements> ``` Where required, a global replacement requires a matching scope (in actions, the scope is inherited from the current scope): ```xml <replacements> <switch value="variable" scope="match"> ... <fallback>...</fallback> </switch> </replacements> ``` A globally defined replacement then can be referred to from within as: ```xml <!-- somewhere in <map> --> <replacements> <player id="global-replacement" var="variable" fallback="an unknown player"/> </replacements> <!-- in an action --> <message text="... {replacement}"> <replacements> <replacement id="replacement">global-replacement</replacement> </replacements> </message> ``` To support for this, replacement parsing has been forked out of ActionParser. Signed-off-by: TTtie <[email protected]>
1 parent 6278a4a commit 7ed2058

File tree

8 files changed

+303
-92
lines changed

8 files changed

+303
-92
lines changed

core/src/main/java/tc/oc/pgm/action/ActionModule.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.jdom2.Element;
99
import org.jetbrains.annotations.Nullable;
1010
import tc.oc.pgm.action.actions.ExposedAction;
11+
import tc.oc.pgm.action.replacements.ReplacementParser;
1112
import tc.oc.pgm.api.map.MapModule;
1213
import tc.oc.pgm.api.map.factory.MapFactory;
1314
import tc.oc.pgm.api.map.factory.MapModuleFactory;
@@ -58,6 +59,13 @@ public Collection<Class<? extends MapModule<?>>> getWeakDependencies() {
5859
public ActionModule parse(MapFactory factory, Logger logger, Document doc)
5960
throws InvalidXMLException {
6061
ActionParser parser = factory.getParser().getActionParser();
62+
var replacementParser = new ReplacementParser(factory, true);
63+
var features = factory.getFeatures();
64+
65+
for (var replacement : XMLUtils.flattenElements(
66+
doc.getRootElement(), Set.of("replacements"), replacementParser.replacementTypes())) {
67+
features.addFeature(replacement, replacementParser.parse(replacement, null));
68+
}
6169

6270
for (Element action :
6371
XMLUtils.flattenElements(doc.getRootElement(), Set.of("actions"), parser.actionTypes())) {

core/src/main/java/tc/oc/pgm/action/ActionParser.java

Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@
33
import static net.kyori.adventure.key.Key.key;
44
import static net.kyori.adventure.sound.Sound.sound;
55
import static net.kyori.adventure.text.Component.empty;
6-
import static net.kyori.adventure.text.Component.text;
76

87
import com.google.common.collect.ImmutableList;
98
import com.google.common.collect.ImmutableMap;
109
import java.lang.reflect.Method;
11-
import java.text.DecimalFormat;
12-
import java.text.NumberFormat;
13-
import java.util.ArrayList;
1410
import java.util.List;
15-
import java.util.Locale;
1611
import java.util.Map;
1712
import java.util.Set;
1813
import net.kyori.adventure.sound.Sound;
@@ -37,6 +32,8 @@
3732
import tc.oc.pgm.action.actions.TakePaymentAction;
3833
import tc.oc.pgm.action.actions.TeleportAction;
3934
import tc.oc.pgm.action.actions.VelocityAction;
35+
import tc.oc.pgm.action.replacements.Replacement;
36+
import tc.oc.pgm.action.replacements.ReplacementParser;
4037
import tc.oc.pgm.api.feature.FeatureValidation;
4138
import tc.oc.pgm.api.filter.Filter;
4239
import tc.oc.pgm.api.filter.Filterables;
@@ -54,12 +51,10 @@
5451
import tc.oc.pgm.shops.ShopModule;
5552
import tc.oc.pgm.shops.menu.Payable;
5653
import tc.oc.pgm.structure.StructureDefinition;
57-
import tc.oc.pgm.util.Audience;
5854
import tc.oc.pgm.util.MethodParser;
5955
import tc.oc.pgm.util.MethodParsers;
6056
import tc.oc.pgm.util.inventory.ItemMatcher;
6157
import tc.oc.pgm.util.math.Formula;
62-
import tc.oc.pgm.util.named.NameStyle;
6358
import tc.oc.pgm.util.xml.InvalidXMLException;
6459
import tc.oc.pgm.util.xml.Node;
6560
import tc.oc.pgm.util.xml.XMLFluentParser;
@@ -68,20 +63,20 @@
6863

6964
public class ActionParser {
7065

71-
private static final NumberFormat DEFAULT_FORMAT = NumberFormat.getIntegerInstance();
72-
7366
private final MapFactory factory;
7467
private final boolean legacy;
7568
private final FeatureDefinitionContext features;
7669
private final XMLFluentParser parser;
7770
private final Map<String, Method> methodParsers;
71+
private final ReplacementParser replacementParser;
7872

7973
public ActionParser(MapFactory factory) {
8074
this.factory = factory;
8175
this.legacy = !factory.getProto().isNoOlderThan(MapProtos.ACTION_REVAMP);
8276
this.features = factory.getFeatures();
8377
this.parser = factory.getParser();
8478
this.methodParsers = MethodParsers.getMethodParsersForClass(getClass());
79+
replacementParser = new ReplacementParser(factory);
8580
}
8681

8782
public <B extends Filterable<?>> Action<? super B> parseProperty(
@@ -290,82 +285,19 @@ public <T extends Filterable<?>> MessageAction<?> parseChatMessage(Element el, C
290285

291286
List<Element> replacements = XMLUtils.flattenElements(el, "replacements");
292287
if (replacements.isEmpty()) {
293-
return new MessageAction<>(Audience.class, text, actionbar, title, null);
288+
return new MessageAction<>(Filterable.class, text, actionbar, title, null);
294289
}
295290

296291
scope = parseScope(el, scope);
297292

298-
ImmutableMap.Builder<String, MessageAction.Replacement<T>> replacementMap =
299-
ImmutableMap.builder();
293+
ImmutableMap.Builder<String, Replacement> replacementMap = ImmutableMap.builder();
300294
for (Element replacement : XMLUtils.flattenElements(el, "replacements")) {
301295
replacementMap.put(
302-
XMLUtils.parseRequiredId(replacement), parseReplacement(replacement, scope));
296+
XMLUtils.parseRequiredId(replacement), replacementParser.parse(replacement, scope));
303297
}
304298
return new MessageAction<>(scope, text, actionbar, title, replacementMap.build());
305299
}
306300

307-
private <T extends Filterable<?>> MessageAction.Replacement<T> parseReplacement(
308-
Element el, Class<T> scope) throws InvalidXMLException {
309-
// TODO: Support alternative replacement types (eg: player(s), team(s), or durations)
310-
switch (el.getName().toLowerCase(Locale.ROOT)) {
311-
case "decimal": {
312-
Formula<T> formula = parser.formula(scope, el, "value").required();
313-
Node formatNode = Node.fromAttr(el, "format");
314-
NumberFormat format =
315-
formatNode != null ? new DecimalFormat(formatNode.getValue()) : DEFAULT_FORMAT;
316-
return (T filterable) -> text(format.format(formula.applyAsDouble(filterable)));
317-
}
318-
case "player": {
319-
var variable = parser.variable(el, "var").scope(MatchPlayer.class).singleExclusive();
320-
var fallback = XMLUtils.parseFormattedText(el, "fallback", empty());
321-
var nameStyle = parser.parseEnum(NameStyle.class, el, "style").optional(NameStyle.VERBOSE);
322-
323-
return (T filterable) ->
324-
variable.getHolder(filterable).map(mp -> mp.getName(nameStyle)).orElse(fallback);
325-
}
326-
case "switch": {
327-
Formula<T> formula = parser.formula(scope, el, "value").orNull();
328-
var fallback = parser.formattedText(el, "fallback").child().optional(empty());
329-
var children = el.getChildren("case");
330-
var branches = new ArrayList<CaseBranch>(children.size());
331-
332-
for (var innerEl : children) {
333-
var filter = parser.filter(innerEl, "filter").orNull();
334-
var valueRange = XMLUtils.parseNumericRange(
335-
Node.fromChildOrAttr(innerEl, "match"), Double.class, null);
336-
if (filter == null && valueRange == null) {
337-
throw new InvalidXMLException(
338-
"At least a filter or a value must be specified", innerEl);
339-
}
340-
341-
if (valueRange != null && formula == null) {
342-
throw new InvalidXMLException(
343-
"A value attribute is specified but there's no switch value to bind to", innerEl);
344-
}
345-
346-
var result = parser.formattedText(innerEl, "result").required();
347-
348-
branches.add(new CaseBranch(
349-
result,
350-
valueRange == null ? Range.all() : valueRange,
351-
filter == null ? StaticFilter.ALLOW : filter));
352-
}
353-
354-
return (T filterable) -> {
355-
var formulaResult = formula == null ? null : formula.applyAsDouble(filterable);
356-
for (var branch : branches) {
357-
if ((formula == null || branch.valueRange.contains(formulaResult))
358-
&& branch.filter.query(filterable).isAllowed()) return branch.result;
359-
}
360-
361-
return fallback;
362-
};
363-
}
364-
default:
365-
throw new InvalidXMLException("Unknown replacement type", el);
366-
}
367-
}
368-
369301
@MethodParser("sound")
370302
public SoundAction parseSoundAction(Element el, Class<?> scope) throws InvalidXMLException {
371303
SoundType soundType =
Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
package tc.oc.pgm.action.actions;
22

33
import java.util.Map;
4-
import java.util.function.Function;
54
import java.util.regex.Pattern;
65
import net.kyori.adventure.text.Component;
7-
import net.kyori.adventure.text.ComponentLike;
86
import net.kyori.adventure.text.TextReplacementConfig;
97
import net.kyori.adventure.title.Title;
108
import org.jetbrains.annotations.Nullable;
11-
import tc.oc.pgm.util.Audience;
9+
import tc.oc.pgm.action.replacements.Replacement;
10+
import tc.oc.pgm.filters.Filterable;
1211

13-
public class MessageAction<T extends Audience> extends AbstractAction<T> {
12+
public class MessageAction<T extends Filterable<?>> extends AbstractAction<T> {
1413
private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("\\{(.+?)}");
1514

1615
private final Component text;
1716
private final Component actionbar;
1817
private final Title title;
19-
private final Map<String, Replacement<T>> replacements;
18+
private final Map<String, Replacement> replacements;
2019

2120
public MessageAction(
2221
Class<T> scope,
2322
@Nullable Component text,
2423
@Nullable Component actionbar,
2524
@Nullable Title title,
26-
@Nullable Map<String, Replacement<T>> replacements) {
25+
@Nullable Map<String, Replacement> replacements) {
2726
super(scope);
2827
this.text = text;
2928
this.actionbar = actionbar;
@@ -43,22 +42,18 @@ private Component replace(Component component, T scope) {
4342
return component;
4443
}
4544

46-
return component.replaceText(
47-
TextReplacementConfig.builder()
48-
.match(REPLACEMENT_PATTERN)
49-
.replacement(
50-
(match, original) -> {
51-
Replacement<T> r = replacements.get(match.group(1));
52-
return r != null ? r.apply(scope) : original;
53-
})
54-
.build());
45+
return component.replaceText(TextReplacementConfig.builder()
46+
.match(REPLACEMENT_PATTERN)
47+
.replacement((match, original) -> {
48+
Replacement r = replacements.get(match.group(1));
49+
return r != null ? r.get(scope) : original;
50+
})
51+
.build());
5552
}
5653

5754
private Title replace(Title title, T scope) {
5855
if (replacements == null) return title;
5956
return Title.title(
6057
replace(title.title(), scope), replace(title.subtitle(), scope), title.times());
6158
}
62-
63-
public interface Replacement<T extends Audience> extends Function<T, ComponentLike> {}
6459
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package tc.oc.pgm.action.replacements;
2+
3+
import net.kyori.adventure.text.ComponentLike;
4+
import tc.oc.pgm.api.feature.FeatureDefinition;
5+
import tc.oc.pgm.filters.Filterable;
6+
7+
/** A replacement object provides replacement text in message actions. */
8+
public interface Replacement extends FeatureDefinition {
9+
/**
10+
* Tests if the replacement can be used with the passed filterable.
11+
*
12+
* @param filterable The filterable to test
13+
* @return Whether the replacement can be used with the passed filterable
14+
*/
15+
default boolean canUse(Class<? extends Filterable<?>> filterable) {
16+
return true;
17+
}
18+
19+
/**
20+
* Creates a replacement component tailored to the given filterable.
21+
*
22+
* @param filterable The filterable to use when creating the replacement component
23+
* @return The replacement component
24+
*/
25+
ComponentLike get(Filterable<?> filterable);
26+
}

0 commit comments

Comments
 (0)