Skip to content

Commit

Permalink
feat,refactor(replacements): add a global replacement registry
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
TTtie committed Jan 4, 2025
1 parent 35f1494 commit ac17739
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 92 deletions.
8 changes: 8 additions & 0 deletions core/src/main/java/tc/oc/pgm/action/ActionModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.jdom2.Element;
import org.jetbrains.annotations.Nullable;
import tc.oc.pgm.action.actions.ExposedAction;
import tc.oc.pgm.action.replacements.ReplacementParser;
import tc.oc.pgm.api.map.MapModule;
import tc.oc.pgm.api.map.factory.MapFactory;
import tc.oc.pgm.api.map.factory.MapModuleFactory;
Expand Down Expand Up @@ -58,6 +59,13 @@ public Collection<Class<? extends MapModule<?>>> getWeakDependencies() {
public ActionModule parse(MapFactory factory, Logger logger, Document doc)
throws InvalidXMLException {
ActionParser parser = factory.getParser().getActionParser();
var replacementParser = new ReplacementParser(factory, true);
var features = factory.getFeatures();

for (var replacement : XMLUtils.flattenElements(
doc.getRootElement(), Set.of("replacements"), replacementParser.replacementTypes())) {
features.addFeature(replacement, replacementParser.parse(replacement, null));
}

for (Element action :
XMLUtils.flattenElements(doc.getRootElement(), Set.of("actions"), parser.actionTypes())) {
Expand Down
82 changes: 7 additions & 75 deletions core/src/main/java/tc/oc/pgm/action/ActionParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,11 @@
import static net.kyori.adventure.key.Key.key;
import static net.kyori.adventure.sound.Sound.sound;
import static net.kyori.adventure.text.Component.empty;
import static net.kyori.adventure.text.Component.text;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.lang.reflect.Method;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import net.kyori.adventure.sound.Sound;
Expand All @@ -37,6 +32,8 @@
import tc.oc.pgm.action.actions.TakePaymentAction;
import tc.oc.pgm.action.actions.TeleportAction;
import tc.oc.pgm.action.actions.VelocityAction;
import tc.oc.pgm.action.replacements.Replacement;
import tc.oc.pgm.action.replacements.ReplacementParser;
import tc.oc.pgm.api.feature.FeatureValidation;
import tc.oc.pgm.api.filter.Filter;
import tc.oc.pgm.api.filter.Filterables;
Expand All @@ -54,12 +51,10 @@
import tc.oc.pgm.shops.ShopModule;
import tc.oc.pgm.shops.menu.Payable;
import tc.oc.pgm.structure.StructureDefinition;
import tc.oc.pgm.util.Audience;
import tc.oc.pgm.util.MethodParser;
import tc.oc.pgm.util.MethodParsers;
import tc.oc.pgm.util.inventory.ItemMatcher;
import tc.oc.pgm.util.math.Formula;
import tc.oc.pgm.util.named.NameStyle;
import tc.oc.pgm.util.xml.InvalidXMLException;
import tc.oc.pgm.util.xml.Node;
import tc.oc.pgm.util.xml.XMLFluentParser;
Expand All @@ -68,20 +63,20 @@

public class ActionParser {

private static final NumberFormat DEFAULT_FORMAT = NumberFormat.getIntegerInstance();

private final MapFactory factory;
private final boolean legacy;
private final FeatureDefinitionContext features;
private final XMLFluentParser parser;
private final Map<String, Method> methodParsers;
private final ReplacementParser replacementParser;

public ActionParser(MapFactory factory) {
this.factory = factory;
this.legacy = !factory.getProto().isNoOlderThan(MapProtos.ACTION_REVAMP);
this.features = factory.getFeatures();
this.parser = factory.getParser();
this.methodParsers = MethodParsers.getMethodParsersForClass(getClass());
replacementParser = new ReplacementParser(factory);
}

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

List<Element> replacements = XMLUtils.flattenElements(el, "replacements");
if (replacements.isEmpty()) {
return new MessageAction<>(Audience.class, text, actionbar, title, null);
return new MessageAction<>(Filterable.class, text, actionbar, title, null);
}

scope = parseScope(el, scope);

ImmutableMap.Builder<String, MessageAction.Replacement<T>> replacementMap =
ImmutableMap.builder();
ImmutableMap.Builder<String, Replacement> replacementMap = ImmutableMap.builder();
for (Element replacement : XMLUtils.flattenElements(el, "replacements")) {
replacementMap.put(
XMLUtils.parseRequiredId(replacement), parseReplacement(replacement, scope));
XMLUtils.parseRequiredId(replacement), replacementParser.parse(replacement, scope));
}
return new MessageAction<>(scope, text, actionbar, title, replacementMap.build());
}

private <T extends Filterable<?>> MessageAction.Replacement<T> parseReplacement(
Element el, Class<T> scope) throws InvalidXMLException {
// TODO: Support alternative replacement types (eg: player(s), team(s), or durations)
switch (el.getName().toLowerCase(Locale.ROOT)) {
case "decimal": {
Formula<T> formula = parser.formula(scope, el, "value").required();
Node formatNode = Node.fromAttr(el, "format");
NumberFormat format =
formatNode != null ? new DecimalFormat(formatNode.getValue()) : DEFAULT_FORMAT;
return (T filterable) -> text(format.format(formula.applyAsDouble(filterable)));
}
case "player": {
var variable = parser.variable(el, "var").scope(MatchPlayer.class).singleExclusive();
var fallback = XMLUtils.parseFormattedText(el, "fallback", empty());
var nameStyle = parser.parseEnum(NameStyle.class, el, "style").optional(NameStyle.VERBOSE);

return (T filterable) ->
variable.getHolder(filterable).map(mp -> mp.getName(nameStyle)).orElse(fallback);
}
case "switch": {
Formula<T> formula = parser.formula(scope, el, "value").orNull();
var fallback = parser.formattedText(el, "fallback").child().optional(empty());
var children = el.getChildren("case");
var branches = new ArrayList<CaseBranch>(children.size());

for (var innerEl : children) {
var filter = parser.filter(innerEl, "filter").orNull();
var valueRange = XMLUtils.parseNumericRange(
Node.fromChildOrAttr(innerEl, "match"), Double.class, null);
if (filter == null && valueRange == null) {
throw new InvalidXMLException(
"At least a filter or a value must be specified", innerEl);
}

if (valueRange != null && formula == null) {
throw new InvalidXMLException(
"A value attribute is specified but there's no switch value to bind to", innerEl);
}

var result = parser.formattedText(innerEl, "result").required();

branches.add(new CaseBranch(
result,
valueRange == null ? Range.all() : valueRange,
filter == null ? StaticFilter.ALLOW : filter));
}

return (T filterable) -> {
var formulaResult = formula == null ? null : formula.applyAsDouble(filterable);
for (var branch : branches) {
if ((formula == null || branch.valueRange.contains(formulaResult))
&& branch.filter.query(filterable).isAllowed()) return branch.result;
}

return fallback;
};
}
default:
throw new InvalidXMLException("Unknown replacement type", el);
}
}

@MethodParser("sound")
public SoundAction parseSoundAction(Element el, Class<?> scope) throws InvalidXMLException {
SoundType soundType =
Expand Down
29 changes: 12 additions & 17 deletions core/src/main/java/tc/oc/pgm/action/actions/MessageAction.java
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
package tc.oc.pgm.action.actions;

import java.util.Map;
import java.util.function.Function;
import java.util.regex.Pattern;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.title.Title;
import org.jetbrains.annotations.Nullable;
import tc.oc.pgm.util.Audience;
import tc.oc.pgm.action.replacements.Replacement;
import tc.oc.pgm.filters.Filterable;

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

private final Component text;
private final Component actionbar;
private final Title title;
private final Map<String, Replacement<T>> replacements;
private final Map<String, Replacement> replacements;

public MessageAction(
Class<T> scope,
@Nullable Component text,
@Nullable Component actionbar,
@Nullable Title title,
@Nullable Map<String, Replacement<T>> replacements) {
@Nullable Map<String, Replacement> replacements) {
super(scope);
this.text = text;
this.actionbar = actionbar;
Expand All @@ -43,22 +42,18 @@ private Component replace(Component component, T scope) {
return component;
}

return component.replaceText(
TextReplacementConfig.builder()
.match(REPLACEMENT_PATTERN)
.replacement(
(match, original) -> {
Replacement<T> r = replacements.get(match.group(1));
return r != null ? r.apply(scope) : original;
})
.build());
return component.replaceText(TextReplacementConfig.builder()
.match(REPLACEMENT_PATTERN)
.replacement((match, original) -> {
Replacement r = replacements.get(match.group(1));
return r != null ? r.get(scope) : original;
})
.build());
}

private Title replace(Title title, T scope) {
if (replacements == null) return title;
return Title.title(
replace(title.title(), scope), replace(title.subtitle(), scope), title.times());
}

public interface Replacement<T extends Audience> extends Function<T, ComponentLike> {}
}
26 changes: 26 additions & 0 deletions core/src/main/java/tc/oc/pgm/action/replacements/Replacement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tc.oc.pgm.action.replacements;

import net.kyori.adventure.text.ComponentLike;
import tc.oc.pgm.api.feature.FeatureDefinition;
import tc.oc.pgm.filters.Filterable;

/** A replacement object provides replacement text in message actions. */
public interface Replacement extends FeatureDefinition {
/**
* Tests if the replacement can be used with the passed filterable.
*
* @param filterable The filterable to test
* @return Whether the replacement can be used with the passed filterable
*/
default boolean canUse(Class<? extends Filterable<?>> filterable) {
return true;
}

/**
* Creates a replacement component tailored to the given filterable.
*
* @param filterable The filterable to use when creating the replacement component
* @return The replacement component
*/
ComponentLike get(Filterable<?> filterable);
}
Loading

0 comments on commit ac17739

Please sign in to comment.