diff --git a/core/src/main/java/tc/oc/pgm/action/ActionParser.java b/core/src/main/java/tc/oc/pgm/action/ActionParser.java index d2fe636b09..641bcfafef 100644 --- a/core/src/main/java/tc/oc/pgm/action/ActionParser.java +++ b/core/src/main/java/tc/oc/pgm/action/ActionParser.java @@ -3,9 +3,14 @@ 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.List; import java.util.Map; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; @@ -37,6 +42,7 @@ import tc.oc.pgm.kits.Kit; import tc.oc.pgm.regions.BlockBoundedValidation; import tc.oc.pgm.regions.RegionParser; +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; @@ -49,6 +55,8 @@ public class ActionParser { + private static final NumberFormat DEFAULT_FORMAT = NumberFormat.getIntegerInstance(); + private final MapFactory factory; private final FeatureDefinitionContext features; private final FilterParser filters; @@ -227,7 +235,8 @@ public Kit parseKitTrigger(Element el, Class scope) throws InvalidXMLExceptio } @MethodParser("message") - public MessageAction parseChatMessage(Element el, Class scope) throws InvalidXMLException { + public > MessageAction parseChatMessage(Element el, Class scope) + throws InvalidXMLException { Component text = XMLUtils.parseFormattedText(Node.fromChildOrAttr(el, "text")); Component actionbar = XMLUtils.parseFormattedText(Node.fromChildOrAttr(el, "actionbar")); @@ -245,7 +254,36 @@ public MessageAction parseChatMessage(Element el, Class scope) throws Invalid throw new InvalidXMLException( "Expected at least one of text, title, subtitle or actionbar", el); - return new MessageAction(text, actionbar, title); + List replacements = XMLUtils.flattenElements(el, "replacements"); + if (replacements.isEmpty()) { + return new MessageAction<>(Audience.class, text, actionbar, title, null); + } + + scope = parseScope(el, scope); + + ImmutableMap.Builder> replacementMap = + ImmutableMap.builder(); + for (Element replacement : XMLUtils.flattenElements(el, "replacements")) { + replacementMap.put( + XMLUtils.parseRequiredId(replacement), parseReplacement(replacement, scope)); + } + return new MessageAction<>(scope, text, actionbar, title, replacementMap.build()); + } + + private > MessageAction.Replacement parseReplacement( + Element el, Class scope) throws InvalidXMLException { + // TODO: Support alternative replacement types (eg: player(s), team(s), or durations) + switch (el.getName()) { + case "decimal": + Formula formula = + Formula.of(Node.fromRequiredAttr(el, "value").getValue(), variables.getContext(scope)); + 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))); + default: + throw new InvalidXMLException("Unknown replacement type", el); + } } @MethodParser("sound") diff --git a/core/src/main/java/tc/oc/pgm/action/actions/MessageAction.java b/core/src/main/java/tc/oc/pgm/action/actions/MessageAction.java index e523c7962a..18aacb9b73 100644 --- a/core/src/main/java/tc/oc/pgm/action/actions/MessageAction.java +++ b/core/src/main/java/tc/oc/pgm/action/actions/MessageAction.java @@ -1,28 +1,64 @@ 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; -public class MessageAction extends AbstractAction { +public class MessageAction extends AbstractAction { + private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("\\{(.+?)}"); private final Component text; private final Component actionbar; private final Title title; + private final Map> replacements; public MessageAction( - @Nullable Component text, @Nullable Component actionbar, @Nullable Title title) { - super(Audience.class); + Class scope, + @Nullable Component text, + @Nullable Component actionbar, + @Nullable Title title, + @Nullable Map> replacements) { + super(scope); this.text = text; this.actionbar = actionbar; this.title = title; + this.replacements = replacements; } @Override - public void trigger(Audience audience) { - if (text != null) audience.sendMessage(text); - if (title != null) audience.showTitle(title); - if (actionbar != null) audience.sendActionBar(actionbar); + public void trigger(T scope) { + if (text != null) scope.sendMessage(replace(text, scope)); + if (title != null) scope.showTitle(replace(title, scope)); + if (actionbar != null) scope.sendActionBar(replace(actionbar, scope)); } + + private Component replace(Component component, T scope) { + if (component == null || replacements == null) { + return component; + } + + return component.replaceText( + TextReplacementConfig.builder() + .match(REPLACEMENT_PATTERN) + .replacement( + (match, original) -> { + Replacement r = replacements.get(match.group(1)); + return r != null ? r.apply(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 extends Function {} }