77import org .bukkit .entity .Player ;
88
99import me .clip .placeholderapi .PlaceholderAPI ;
10+ import world .bentobox .bentobox .BentoBox ;
1011
1112public class CheckPapi {
1213
1314 /**
14- * Evaluates the formula by first replacing PAPI placeholders (using the provided Player)
15- * and then evaluating the resulting expression. The expression is expected to be a series
16- * of numeric comparisons (using =, <>, <=, >=, <, >) joined by Boolean operators AND and OR.
17- *
18- * For example:
19- * "%aoneblock_my_island_lifetime_count% >= 1000 AND %Level_aoneblock_island_level% >= 100"
20- *
21- * If any placeholder evaluates to a non-numeric value or the formula is malformed, false is returned.
15+ * Evaluates the given formula by first replacing PAPI placeholders using the provided Player,
16+ * then parsing and evaluating one or more conditions.
17+ * <p>
18+ * The formula may contain conditions comparing numeric or string values.
19+ * Operands may contain spaces. The grammar for a condition is:
20+ * <pre>
21+ * leftOperand operator rightOperand
22+ * </pre>
23+ * where the leftOperand is a sequence of tokens (separated by whitespace) until a valid
24+ * operator is found, and the rightOperand is a sequence of tokens until a boolean operator
25+ * ("AND" or "OR") is encountered or the end of the formula is reached.
26+ * <p>
27+ * Supported comparison operators (case sensitive) are:
28+ * <ul>
29+ * <li>"=" or "==" for equality</li>
30+ * <li>"<>" or "!=" for inequality</li>
31+ * <li>"<=" and ">=" for less than or equal and greater than or equal</li>
32+ * <li>"<" and ">" for less than and greater than</li>
33+ * </ul>
34+ *
35+ * For strings:
36+ * <ul>
37+ * <li>"=" for case insensitive equality</li>
38+ * <li>"==" for case-sensitive equality</li>
39+ * <li>"<>" for case-insensitive inequality</li>
40+ * <li>"!=" for case sensitive inequality</li>
41+ * </ul>
42+ * Boolean connectors "AND" and "OR" (case insensitive) combine multiple conditions;
43+ * AND has higher precedence than OR.
44+ * <p>
45+ * Examples:
46+ * <pre>
47+ * "%aoneblock_my_island_lifetime_count% >= 1000 AND %aoneblock_my_island_level% >= 100"
48+ * "john smith == tasty bento AND 40 > 20"
49+ * </pre>
2250 *
23- * @param player the Player used for placeholder replacement.
24- * @param formula the formula to evaluate.
51+ * @param player the Player used for placeholder replacement
52+ * @param formula the formula to evaluate
2553 * @return true if the formula evaluates to true, false otherwise.
2654 */
2755 public static boolean evaluate (Player player , String formula ) {
28- // Replace PAPI placeholders with actual values using the provided Player .
56+ // Replace PAPI placeholders with actual values.
2957 String parsedFormula = PlaceholderAPI .setPlaceholders (player , formula );
3058
31- // Tokenize the parsed formula (tokens are assumed to be separated by whitespace) .
59+ // Tokenize the resulting formula by whitespace.
3260 List <String > tokens = tokenize (parsedFormula );
3361 if (tokens .isEmpty ()) {
3462 return false ;
@@ -37,19 +65,19 @@ public static boolean evaluate(Player player, String formula) {
3765 try {
3866 Parser parser = new Parser (tokens );
3967 boolean result = parser .parseExpression ();
40- // If there are leftover tokens, the expression is malformed.
68+ // If there are extra tokens after parsing the full expression, the formula is malformed.
4169 if (parser .hasNext ()) {
4270 return false ;
4371 }
4472 return result ;
4573 } catch (Exception e ) {
46- // Any error in parsing or evaluation results in false.
74+ // Any error in parsing or evaluating the expression results in false.
4775 return false ;
4876 }
4977 }
5078
5179 /**
52- * Splits the given string into tokens using whitespace as the delimiter.
80+ * Splits a string into tokens using whitespace as the delimiter.
5381 *
5482 * @param s the string to tokenize.
5583 * @return a list of tokens.
@@ -59,17 +87,8 @@ private static List<String> tokenize(String s) {
5987 }
6088
6189 /**
62- * A simple recursive descent parser that evaluates expressions according to the following grammar:
63- *
64- * <pre>
65- * Expression -> Term { OR Term }
66- * Term -> Factor { AND Factor }
67- * Factor -> operand operator operand
68- * </pre>
69- *
70- * A Factor is expected to be a numeric condition in the form:
71- * number operator number
72- * where operator is one of: =, <>, <=, >=, <, or >.
90+ * A simple recursive descent parser that evaluates the formula.
91+ * It supports multi-token operands for conditions.
7392 */
7493 private static class Parser {
7594 private final List <String > tokens ;
@@ -79,34 +98,27 @@ public Parser(List<String> tokens) {
7998 this .tokens = tokens ;
8099 }
81100
82- /**
83- * Returns true if there are more tokens to process.
84- */
85101 public boolean hasNext () {
86102 return pos < tokens .size ();
87103 }
88104
89- /**
90- * Returns the next token without advancing.
91- */
92105 public String peek () {
93106 return tokens .get (pos );
94107 }
95108
96- /**
97- * Returns the next token and advances the position.
98- */
99109 public String next () {
100110 return tokens .get (pos ++);
101111 }
102112
103113 /**
104114 * Parses an Expression:
105- * Expression -> Term { OR Term }
115+ * Expression -> Term { OR Term }
116+ *
117+ * @return the boolean value of the expression.
106118 */
107119 public boolean parseExpression () {
108120 boolean value = parseTerm ();
109- while (hasNext () && peek (). equalsIgnoreCase ( "OR" )) {
121+ while (hasNext () && isOr ( peek ())) {
110122 next (); // consume "OR"
111123 boolean termValue = parseTerm ();
112124 value = value || termValue ;
@@ -116,67 +128,159 @@ public boolean parseExpression() {
116128
117129 /**
118130 * Parses a Term:
119- * Term -> Factor { AND Factor }
131+ * Term -> Condition { AND Condition }
132+ *
133+ * @return the boolean value of the term.
120134 */
121135 public boolean parseTerm () {
122- boolean value = parseFactor ();
123- while (hasNext () && peek (). equalsIgnoreCase ( "AND" )) {
136+ boolean value = parseCondition ();
137+ while (hasNext () && isAnd ( peek ())) {
124138 next (); // consume "AND"
125- boolean factorValue = parseFactor ();
126- value = value && factorValue ;
139+ boolean conditionValue = parseCondition ();
140+ value = value && conditionValue ;
127141 }
128142 return value ;
129143 }
130144
131145 /**
132- * Parses a Factor, which is a single condition in the form:
133- * operand operator operand
134- *
135- * For example: "1234 >= 1000"
146+ * Parses a single condition of the form:
147+ * leftOperand operator rightOperand
148+ * <p>
149+ * The left operand is built by collecting tokens until a valid operator is found.
150+ * The right operand is built by collecting tokens until a boolean operator ("AND" or "OR")
151+ * is encountered or the end of the token list is reached.
136152 *
137153 * @return the boolean result of the condition.
138154 */
139- public boolean parseFactor () {
140- // There must be at least three tokens remaining.
141- if (pos + 2 >= tokens .size ()) {
142- throw new RuntimeException ("Incomplete condition" );
155+ public boolean parseCondition () {
156+ // Parse left operand.
157+ StringBuilder leftSB = new StringBuilder ();
158+ if (!hasNext ()) {
159+ BentoBox .getInstance ()
160+ .logError ("Challenges PAPI formula error: Expected left operand but reached end of expression" );
161+ return false ;
143162 }
144-
145- String leftOperand = next ();
163+ // Collect tokens for the left operand until an operator is encountered.
164+ while (hasNext () && !isOperator (peek ())) {
165+ if (leftSB .length () > 0 ) {
166+ leftSB .append (" " );
167+ }
168+ leftSB .append (next ());
169+ }
170+ if (!hasNext ()) {
171+ throw new RuntimeException ("Operator expected after left operand" );
172+ }
173+ // Next token should be an operator.
146174 String operator = next ();
147- String rightOperand = next ();
148-
149- // Validate operator.
150- if (!operator .equals ("=" ) && !operator .equals ("<>" ) && !operator .equals ("<=" ) && !operator .equals (">=" )
151- && !operator .equals ("<" ) && !operator .equals (">" )) {
175+ if (!isValidOperator (operator )) {
152176 throw new RuntimeException ("Invalid operator: " + operator );
153177 }
178+ // Parse right operand.
179+ StringBuilder rightSB = new StringBuilder ();
180+ while (hasNext () && !isBooleanOperator (peek ())) {
181+ if (rightSB .length () > 0 ) {
182+ rightSB .append (" " );
183+ }
184+ rightSB .append (next ());
185+ }
186+ String leftOperand = leftSB .toString ().trim ();
187+ String rightOperand = rightSB .toString ().trim ();
154188
155- double leftVal , rightVal ;
156- try {
157- leftVal = Double .parseDouble (leftOperand );
158- rightVal = Double .parseDouble (rightOperand );
159- } catch (NumberFormatException e ) {
160- // If either operand is not numeric, return false.
189+ if (rightOperand .isEmpty ()) {
161190 return false ;
162191 }
163- // Evaluate the condition.
164- switch (operator ) {
165- case "=" :
166- return Double .compare (leftVal , rightVal ) == 0 ;
167- case "<>" :
168- return Double .compare (leftVal , rightVal ) != 0 ;
169- case "<=" :
170- return leftVal <= rightVal ;
171- case ">=" :
172- return leftVal >= rightVal ;
173- case "<" :
174- return leftVal < rightVal ;
175- case ">" :
176- return leftVal > rightVal ;
177- default :
178- // This case is never reached.
179- return false ;
192+
193+ // Evaluate the condition:
194+ // If both operands can be parsed as numbers, use numeric comparison;
195+ // otherwise, perform string comparison.
196+ Double leftNum = tryParseDouble (leftOperand );
197+ Double rightNum = tryParseDouble (rightOperand );
198+ if (leftNum != null && rightNum != null ) {
199+ // Numeric comparison.
200+ switch (operator ) {
201+ case "=" :
202+ case "==" :
203+ return Double .compare (leftNum , rightNum ) == 0 ;
204+ case "<>" :
205+ case "!=" :
206+ return Double .compare (leftNum , rightNum ) != 0 ;
207+ case "<=" :
208+ return leftNum <= rightNum ;
209+ case ">=" :
210+ return leftNum >= rightNum ;
211+ case "<" :
212+ return leftNum < rightNum ;
213+ case ">" :
214+ return leftNum > rightNum ;
215+ default :
216+ BentoBox .getInstance ().logError ("Challenges PAPI formula error: Unsupported operator: " + operator );
217+ return false ;
218+ }
219+ } else {
220+ // String comparison.
221+ switch (operator ) {
222+ case "=" :
223+ return leftOperand .equalsIgnoreCase (rightOperand );
224+ case "==" :
225+ return leftOperand .equals (rightOperand );
226+ case "<>" :
227+ return !leftOperand .equalsIgnoreCase (rightOperand );
228+ case "!=" :
229+ return !leftOperand .equals (rightOperand );
230+ case "<=" :
231+ return leftOperand .compareTo (rightOperand ) <= 0 ;
232+ case ">=" :
233+ return leftOperand .compareTo (rightOperand ) >= 0 ;
234+ case "<" :
235+ return leftOperand .compareTo (rightOperand ) < 0 ;
236+ case ">" :
237+ return leftOperand .compareTo (rightOperand ) > 0 ;
238+ default :
239+ BentoBox .getInstance ().logError ("Challenges PAPI formula error: Unsupported operator: " + operator );
240+ return false ;
241+ }
242+ }
243+ }
244+
245+ /**
246+ * Checks if the given token is one of the valid comparison operators.
247+ */
248+ private boolean isValidOperator (String token ) {
249+ return token .equals ("=" ) || token .equals ("==" ) || token .equals ("<>" ) || token .equals ("!=" )
250+ || token .equals ("<=" ) || token .equals (">=" ) || token .equals ("<" ) || token .equals (">" );
251+ }
252+
253+ /**
254+ * Returns true if the token is a comparison operator.
255+ */
256+ private boolean isOperator (String token ) {
257+ return isValidOperator (token );
258+ }
259+
260+ /**
261+ * Returns true if the token is a boolean operator ("AND" or "OR").
262+ */
263+ private boolean isBooleanOperator (String token ) {
264+ return token .equalsIgnoreCase ("AND" ) || token .equalsIgnoreCase ("OR" );
265+ }
266+
267+ private boolean isAnd (String token ) {
268+ return token .equalsIgnoreCase ("AND" );
269+ }
270+
271+ private boolean isOr (String token ) {
272+ return token .equalsIgnoreCase ("OR" );
273+ }
274+
275+ /**
276+ * Tries to parse the given string as a Double.
277+ * Returns the Double if successful, or null if parsing fails.
278+ */
279+ private Double tryParseDouble (String s ) {
280+ try {
281+ return Double .parseDouble (s );
282+ } catch (NumberFormatException e ) {
283+ return null ;
180284 }
181285 }
182286 }
0 commit comments