tokenize(String s) {
+ return new ArrayList<>(Arrays.asList(s.split("\\s+")));
+ }
+
+ /**
+ * A simple recursive descent parser that evaluates expressions according to the following grammar:
+ *
+ *
+ * Expression -> Term { OR Term }
+ * Term -> Factor { AND Factor }
+ * Factor -> operand operator operand
+ *
+ *
+ * A Factor is expected to be a numeric condition in the form:
+ * number operator number
+ * where operator is one of: =, <>, <=, >=, <, or >.
+ */
+ private static class Parser {
+ private final List tokens;
+ private int pos = 0;
+
+ public Parser(List tokens) {
+ this.tokens = tokens;
+ }
+
+ /**
+ * Returns true if there are more tokens to process.
+ */
+ public boolean hasNext() {
+ return pos < tokens.size();
+ }
+
+ /**
+ * Returns the next token without advancing.
+ */
+ public String peek() {
+ return tokens.get(pos);
+ }
+
+ /**
+ * Returns the next token and advances the position.
+ */
+ public String next() {
+ return tokens.get(pos++);
+ }
+
+ /**
+ * Parses an Expression:
+ * Expression -> Term { OR Term }
+ */
+ public boolean parseExpression() {
+ boolean value = parseTerm();
+ while (hasNext() && peek().equalsIgnoreCase("OR")) {
+ next(); // consume "OR"
+ boolean termValue = parseTerm();
+ value = value || termValue;
+ }
+ return value;
+ }
+
+ /**
+ * Parses a Term:
+ * Term -> Factor { AND Factor }
+ */
+ public boolean parseTerm() {
+ boolean value = parseFactor();
+ while (hasNext() && peek().equalsIgnoreCase("AND")) {
+ next(); // consume "AND"
+ boolean factorValue = parseFactor();
+ value = value && factorValue;
+ }
+ return value;
+ }
+
+ /**
+ * Parses a Factor, which is a single condition in the form:
+ * operand operator operand
+ *
+ * For example: "1234 >= 1000"
+ *
+ * @return the boolean result of the condition.
+ */
+ public boolean parseFactor() {
+ // There must be at least three tokens remaining.
+ if (pos + 2 >= tokens.size()) {
+ throw new RuntimeException("Incomplete condition");
+ }
+
+ String leftOperand = next();
+ String operator = next();
+ String rightOperand = next();
+
+ // Validate operator.
+ if (!operator.equals("=") && !operator.equals("<>") && !operator.equals("<=") && !operator.equals(">=")
+ && !operator.equals("<") && !operator.equals(">")) {
+ throw new RuntimeException("Invalid operator: " + operator);
+ }
+
+ double leftVal, rightVal;
+ try {
+ leftVal = Double.parseDouble(leftOperand);
+ rightVal = Double.parseDouble(rightOperand);
+ } catch (NumberFormatException e) {
+ // If either operand is not numeric, return false.
+ return false;
+ }
+ // Evaluate the condition.
+ switch (operator) {
+ case "=":
+ return Double.compare(leftVal, rightVal) == 0;
+ case "<>":
+ return Double.compare(leftVal, rightVal) != 0;
+ case "<=":
+ return leftVal <= rightVal;
+ case ">=":
+ return leftVal >= rightVal;
+ case "<":
+ return leftVal < rightVal;
+ case ">":
+ return leftVal > rightVal;
+ default:
+ // This case is never reached.
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/world/bentobox/challenges/database/object/requirements/OtherRequirements.java b/src/main/java/world/bentobox/challenges/database/object/requirements/OtherRequirements.java
index 5f59e7d..318b537 100644
--- a/src/main/java/world/bentobox/challenges/database/object/requirements/OtherRequirements.java
+++ b/src/main/java/world/bentobox/challenges/database/object/requirements/OtherRequirements.java
@@ -139,6 +139,20 @@ public class OtherRequirements extends Requirements
{
this.requiredIslandLevel = requiredIslandLevel;
}
+
+ /**
+ * @return the papiString
+ */
+ public String getPapiString() {
+ return papiString == null ? "" : papiString;
+ }
+
+ /**
+ * @param papiString the papiString to set
+ */
+ public void setPapiString(String papiString) {
+ this.papiString = papiString;
+ }
// ---------------------------------------------------------------------
@@ -162,6 +176,7 @@ public class OtherRequirements extends Requirements
clone.setRequiredMoney(this.requiredMoney);
clone.setTakeMoney(this.takeMoney);
clone.setRequiredIslandLevel(this.requiredIslandLevel);
+ clone.setPapiString(this.papiString);
return clone;
}
@@ -201,4 +216,12 @@ public class OtherRequirements extends Requirements
*/
@Expose
private long requiredIslandLevel;
+
+ /**
+ * Formulas that include math symbols and PAPI placeholders
+ */
+ @Expose
+ private String papiString;
+
+
}
diff --git a/src/main/java/world/bentobox/challenges/panel/admin/EditChallengePanel.java b/src/main/java/world/bentobox/challenges/panel/admin/EditChallengePanel.java
index c9264dc..0ea6e83 100644
--- a/src/main/java/world/bentobox/challenges/panel/admin/EditChallengePanel.java
+++ b/src/main/java/world/bentobox/challenges/panel/admin/EditChallengePanel.java
@@ -215,6 +215,7 @@ public class EditChallengePanel extends CommonPanel {
panelBuilder.item(12, this.createRequirementButton(RequirementButton.REQUIRED_MONEY));
panelBuilder.item(21, this.createRequirementButton(RequirementButton.REMOVE_MONEY));
+ panelBuilder.item(14, this.createRequirementButton(RequirementButton.REQUIRED_PAPI));
panelBuilder.item(23, this.createRequirementButton(RequirementButton.REQUIRED_LEVEL));
panelBuilder.item(25, this.createRequirementButton(RequirementButton.REQUIRED_PERMISSIONS));
@@ -630,7 +631,7 @@ public class EditChallengePanel extends CommonPanel {
return this.createInventoryRequirementButton(button);
}
// Buttons for Other Requirements
- case REQUIRED_EXPERIENCE, REMOVE_EXPERIENCE, REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY -> {
+ case REQUIRED_EXPERIENCE, REMOVE_EXPERIENCE, REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY, REQUIRED_PAPI -> {
return this.createOtherRequirementButton(button);
}
// Statistics
@@ -1098,6 +1099,33 @@ public class EditChallengePanel extends CommonPanel {
description.add("");
description.add(this.user.getTranslation(Constants.TIPS + "click-to-change"));
}
+ case REQUIRED_PAPI -> {
+ if (!requirements.getPapiString().isEmpty()) {
+ description
+ .add(this.user.getTranslation(reference + "value", "[formula]", requirements.getPapiString()));
+ }
+ icon = new ItemStack(
+ this.addon.getPlugin().getHooks().getHook("PlaceholderAPI").isPresent() ? Material.PAPER
+ : Material.BARRIER);
+ clickHandler = (panel, user, clickType, i) -> {
+ Consumer stringConsumer = string -> {
+ if (string != null) {
+ requirements.setPapiString(string);
+ }
+
+ // reopen panel
+ this.build();
+ };
+ ConversationUtils.createStringInput(stringConsumer, user,
+ this.user.getTranslation(Constants.CONVERSATIONS + "enter-formula"), "");
+
+ return true;
+ };
+ glow = false;
+
+ description.add("");
+ description.add(this.user.getTranslation(Constants.TIPS + "click-to-change"));
+ }
case REQUIRED_MONEY -> {
description.add(this.user.getTranslation(reference + "value", Constants.PARAMETER_NUMBER,
String.valueOf(requirements.getRequiredMoney())));
@@ -1701,7 +1729,7 @@ public class EditChallengePanel extends CommonPanel {
REQUIRED_LEVEL, REQUIRED_MONEY, REMOVE_MONEY, STATISTIC, STATISTIC_BLOCKS, STATISTIC_ITEMS,
STATISTIC_ENTITIES,
STATISTIC_AMOUNT, REMOVE_STATISTIC, REQUIRED_MATERIALTAGS, REQUIRED_ENTITYTAGS, REQUIRED_STATISTICS,
- REMOVE_STATISTICS,
+ REMOVE_STATISTICS, REQUIRED_PAPI,
}
// ---------------------------------------------------------------------
diff --git a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java
index 1f70042..fb05675 100644
--- a/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java
+++ b/src/main/java/world/bentobox/challenges/tasks/TryToComplete.java
@@ -37,6 +37,7 @@ import org.bukkit.util.BoundingBox;
import com.google.common.collect.UnmodifiableIterator;
+import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.localization.TextVariables;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island;
@@ -45,6 +46,7 @@ import world.bentobox.challenges.ChallengesAddon;
import world.bentobox.challenges.database.object.Challenge;
import world.bentobox.challenges.database.object.Challenge.ChallengeType;
import world.bentobox.challenges.database.object.ChallengeLevel;
+import world.bentobox.challenges.database.object.requirements.CheckPapi;
import world.bentobox.challenges.database.object.requirements.InventoryRequirements;
import world.bentobox.challenges.database.object.requirements.IslandRequirements;
import world.bentobox.challenges.database.object.requirements.OtherRequirements;
@@ -530,91 +532,79 @@ public class TryToComplete
}
}
- // If challenges are in sync with all island members, then punish others too.
- if (this.addon.getChallengesSettings().isStoreAsIslandData())
- {
- Island island = this.addon.getIslands().getIsland(this.world, this.user);
-
- if (island == null) {
- // hmm
- return;
- }
-
- for (UnmodifiableIterator iterator = island.getMemberSet().iterator(); iterator.hasNext()
- && removeAmount > 0;)
+ // If challenges are in sync with all island members, then punish others too.
+ if (this.addon.getChallengesSettings().isStoreAsIslandData())
{
- Player player = Bukkit.getPlayer(iterator.next());
+ Island island = this.addon.getIslands().getIsland(this.world, this.user);
- if (player == null || player == this.user.getPlayer()) {
- // cannot punish null or player who already was punished.
- continue;
+ if (island == null) {
+ // hmm
+ return;
}
- switch (Objects.requireNonNull(s.statistic()).getType()) {
- case UNTYPED -> {
- int statistic = player.getStatistic(s.statistic());
+ for (UnmodifiableIterator iterator = island.getMemberSet().iterator(); iterator.hasNext()
+ && removeAmount > 0;) {
+ Player player = Bukkit.getPlayer(iterator.next());
- if (removeAmount >= statistic)
- {
- removeAmount -= statistic;
- player.setStatistic(s.statistic(), 0);
+ if (player == null || player == this.user.getPlayer()) {
+ // cannot punish null or player who already was punished.
+ continue;
}
- else
- {
- player.setStatistic(s.statistic(), statistic - removeAmount);
- removeAmount = 0;
- }
- }
- case ITEM, BLOCK -> {
- if (s.material() == null)
- {
- // Just a sanity check. Entity cannot be null at this point of code.
- removeAmount = 0;
- }
- else
- {
- int statistic = player.getStatistic(s.statistic(), s.material());
+
+ switch (Objects.requireNonNull(s.statistic()).getType()) {
+ case UNTYPED -> {
+ int statistic = player.getStatistic(s.statistic());
if (removeAmount >= statistic)
{
removeAmount -= statistic;
- player.setStatistic(s.statistic(), s.material(), 0);
+ player.setStatistic(s.statistic(), 0);
}
else
{
- player.setStatistic(s.statistic(), s.material(),
- statistic - removeAmount);
+ player.setStatistic(s.statistic(), statistic - removeAmount);
removeAmount = 0;
}
}
- }
- case ENTITY -> {
- if (s.entity() == null)
- {
- // Just a sanity check. Entity cannot be null at this point of code.
- removeAmount = 0;
- }
- else
- {
- int statistic = player.getStatistic(s.statistic(), s.entity());
+ case ITEM, BLOCK -> {
+ if (s.material() == null) {
+ // Just a sanity check. Entity cannot be null at this point of code.
+ removeAmount = 0;
+ } else {
+ int statistic = player.getStatistic(s.statistic(), s.material());
- if (removeAmount >= statistic)
+ if (removeAmount >= statistic) {
+ removeAmount -= statistic;
+ player.setStatistic(s.statistic(), s.material(), 0);
+ } else {
+ player.setStatistic(s.statistic(), s.material(), statistic - removeAmount);
+ removeAmount = 0;
+ }
+ }
+ }
+ case ENTITY -> {
+ if (s.entity() == null)
{
- removeAmount -= statistic;
- player.setStatistic(s.statistic(), s.entity(), 0);
+ // Just a sanity check. Entity cannot be null at this point of code.
+ removeAmount = 0;
}
else
{
- player.setStatistic(s.statistic(), s.entity(),
- statistic - removeAmount);
- removeAmount = 0;
+ int statistic = player.getStatistic(s.statistic(), s.entity());
+
+ if (removeAmount >= statistic) {
+ removeAmount -= statistic;
+ player.setStatistic(s.statistic(), s.entity(), 0);
+ } else {
+ player.setStatistic(s.statistic(), s.entity(), statistic - removeAmount);
+ removeAmount = 0;
+ }
}
}
- }
+ }
}
}
}
- }
}
}
}
@@ -1426,6 +1416,15 @@ public class TryToComplete
Utils.sendMessage(this.user,
this.world, Constants.ERRORS + "island-level", TextVariables.NUMBER,
String.valueOf(requirements.getRequiredIslandLevel()));
+ } else if (this.addon.getPlugin().getHooks().getHook("PlaceholderAPI").isPresent()
+ && !requirements.getPapiString().isEmpty()
+ && !CheckPapi.evaluate(user.getPlayer(), requirements.getPapiString())) {
+ Utils.sendMessage(this.user, this.world, Constants.ERRORS + "incorrect");
+ if (!requirements.getPapiString().isEmpty()) {
+ addon.log("FYI:.Challenge failed for " + user.getName() + ". PAPI formula: "
+ + requirements.getPapiString() + " = "
+ + CheckPapi.evaluate(user.getPlayer(), requirements.getPapiString()));
+ }
}
else
{
@@ -1451,7 +1450,6 @@ public class TryToComplete
// Section: Statistic Challenge
// ---------------------------------------------------------------------
-
/**
* Checks if a statistic challenge can be completed or not
* It returns ChallengeResult.
diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml
index 7d88377..88eb994 100755
--- a/src/main/resources/locales/en-US.yml
+++ b/src/main/resources/locales/en-US.yml
@@ -415,6 +415,13 @@ challenges:
&7 money on the player's
&7 account for the challenge.
value: "&7 Current value: &e [number]"
+ required_papi:
+ name: "&f&l Required PAPI"
+ description: |-
+ &7 Checks a formula that can
+ &7 include PAPI placeholders
+ &7 and math and logical elements.
+ value: "&7 Formula: &e [formula]"
statistic:
name: "&f&l Statistic"
description: |-
@@ -1130,6 +1137,7 @@ challenges:
file-name-exist: "&c A file named '[id]' already exists. Cannot overwrite."
write-search: "&e Please enter a search value. (Type 'cancel' to exit)"
search-updated: "&a Search value updated."
+ enter-formula: "&a Enter a formula that uses PAPI number placeholders and symbols =,<>,<+,>=, AND, OR only.\n&a Example: &7 %aoneblock_my_island_lifetime_count% >= 1000 AND %Level_aoneblock_island_level% >= 100"
titles:
challenge-title: "Success!"
challenge-subtitle: "[friendlyName]"