Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a PAPI formula option to Other challenge types. #376

Merged
merged 1 commit into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@
<id>minecraft-repo</id>
<url>https://libraries.minecraft.net/</url>
</repository>
<!-- Placeholder API -->
<repository>
<id>placeholderapi</id>
<url>https://repo.extendedclip.com/releases/</url>
</repository>
</repositories>

<dependencies>
Expand Down Expand Up @@ -198,6 +203,13 @@
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
<!-- Placeholder API -->
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.6</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package world.bentobox.challenges.database.object.requirements;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.bukkit.entity.Player;

import me.clip.placeholderapi.PlaceholderAPI;

public class CheckPapi {

/**
* Evaluates the formula by first replacing PAPI placeholders (using the provided Player)
* and then evaluating the resulting expression. The expression is expected to be a series
* of numeric comparisons (using =, <>, <=, >=, <, >) joined by Boolean operators AND and OR.
*
* For example:
* "%aoneblock_my_island_lifetime_count% >= 1000 AND %Level_aoneblock_island_level% >= 100"
*
* If any placeholder evaluates to a non-numeric value or the formula is malformed, false is returned.
*
* @param player the Player used for placeholder replacement.
* @param formula the formula to evaluate.
* @return true if the formula evaluates to true, false otherwise.
*/
public static boolean evaluate(Player player, String formula) {
// Replace PAPI placeholders with actual values using the provided Player.
String parsedFormula = PlaceholderAPI.setPlaceholders(player, formula);

// Tokenize the parsed formula (tokens are assumed to be separated by whitespace).
List<String> tokens = tokenize(parsedFormula);
if (tokens.isEmpty()) {
return false;
}

try {
Parser parser = new Parser(tokens);
boolean result = parser.parseExpression();
// If there are leftover tokens, the expression is malformed.
if (parser.hasNext()) {
return false;
}
return result;
} catch (Exception e) {
// Any error in parsing or evaluation results in false.
return false;
}
}

/**
* Splits the given string into tokens using whitespace as the delimiter.
*
* @param s the string to tokenize.
* @return a list of tokens.
*/
private static List<String> tokenize(String s) {
return new ArrayList<>(Arrays.asList(s.split("\\s+")));
}

/**
* A simple recursive descent parser that evaluates expressions according to the following grammar:
*
* <pre>
* Expression -> Term { OR Term }
* Term -> Factor { AND Factor }
* Factor -> operand operator operand
* </pre>
*
* 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<String> tokens;
private int pos = 0;

public Parser(List<String> 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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,20 @@ public void setRequiredIslandLevel(long requiredIslandLevel)
{
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;
}


// ---------------------------------------------------------------------
Expand All @@ -162,6 +176,7 @@ public Requirements copy()
clone.setRequiredMoney(this.requiredMoney);
clone.setTakeMoney(this.takeMoney);
clone.setRequiredIslandLevel(this.requiredIslandLevel);
clone.setPapiString(this.papiString);

return clone;
}
Expand Down Expand Up @@ -201,4 +216,12 @@ public Requirements copy()
*/
@Expose
private long requiredIslandLevel;

/**
* Formulas that include math symbols and PAPI placeholders
*/
@Expose
private String papiString;


}
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ private void buildOtherRequirementsPanel(PanelBuilder panelBuilder) {
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));
Expand Down Expand Up @@ -630,7 +631,7 @@ private PanelItem createRequirementButton(RequirementButton button) {
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
Expand Down Expand Up @@ -1098,6 +1099,33 @@ private PanelItem createOtherRequirementButton(RequirementButton button) {
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<String> 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())));
Expand Down Expand Up @@ -1701,7 +1729,7 @@ public enum RequirementButton {
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,
}

// ---------------------------------------------------------------------
Expand Down
Loading