From 38717a6b9190f5a5f9bcde63c3eb2d7dadd4fc06 Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Wed, 20 Jul 2022 18:09:09 +0200 Subject: [PATCH] Improves behavior when signs share conditions Makes sure that the paid sign matching the most conditions is always chosen as the matching paid sign Makes sure to always choose the most expensive paid sign if two or more paid signs with the same amount of conditions are matching. Changes some classes to records to reduce some boilerplate code --- .../paidsigns/command/EditCommand.java | 4 +- .../paidsigns/command/ListCommand.java | 4 +- .../paidsigns/container/PaidSign.java | 6 +- .../container/PaidSignCondition.java | 64 ++--------- .../container/PaidSignConditionMatch.java | 19 ++++ .../paidsigns/container/TrackedSign.java | 37 +------ .../paidsigns/listener/SignListener.java | 100 ++++++++++++++---- .../paidsigns/manager/PaidSignManager.java | 2 +- .../paidsigns/manager/TrackedSignManager.java | 8 +- 9 files changed, 122 insertions(+), 122 deletions(-) create mode 100644 src/main/java/net/knarcraft/paidsigns/container/PaidSignConditionMatch.java diff --git a/src/main/java/net/knarcraft/paidsigns/command/EditCommand.java b/src/main/java/net/knarcraft/paidsigns/command/EditCommand.java index c5a8d67..ca7005e 100644 --- a/src/main/java/net/knarcraft/paidsigns/command/EditCommand.java +++ b/src/main/java/net/knarcraft/paidsigns/command/EditCommand.java @@ -130,7 +130,7 @@ public class EditCommand extends TokenizedCommand { PaidSign updatedSign = new PaidSign(signName, cost, permission, ignoreCase, ignoreColor, matchAnyCondition); for (short line : conditions.keySet()) { PaidSignCondition condition = conditions.get(line); - updatedSign.addCondition(line, condition.getStringToMatch(), condition.executeRegex(), + updatedSign.addCondition(line, condition.stringToMatch(), condition.executeRegex(), OptionState.getFromBoolean(condition.ignoreCase()), OptionState.getFromBoolean(condition.ignoreColor())); } @@ -154,7 +154,7 @@ public class EditCommand extends TokenizedCommand { PaidSignConditionProperty property, String newValue) { PaidSignCondition condition = sign.getConditions().get(conditionIndex); String stringToMatch = property == PaidSignConditionProperty.STRING_TO_MATCH ? newValue : - condition.getStringToMatch(); + condition.stringToMatch(); boolean executeRegEx = property == PaidSignConditionProperty.EXECUTE_REG_EX ? Boolean.parseBoolean(newValue) : condition.executeRegex(); boolean ignoreCase = property == PaidSignConditionProperty.IGNORE_CASE ? OptionState.getBooleanValue( diff --git a/src/main/java/net/knarcraft/paidsigns/command/ListCommand.java b/src/main/java/net/knarcraft/paidsigns/command/ListCommand.java index 15d3361..56717f6 100644 --- a/src/main/java/net/knarcraft/paidsigns/command/ListCommand.java +++ b/src/main/java/net/knarcraft/paidsigns/command/ListCommand.java @@ -107,7 +107,7 @@ public class ListCommand extends TokenizedCommand { PaidSignCondition condition) { sender.sendMessage(StringFormatter.replacePlaceholders(Translator.getTranslatedMessage( TranslatableMessage.PAID_SIGN_CONDITION_INFO), new String[]{"{name}", "{line}", "{match}", "{regex}", - "{case}", "{color}"}, new String[]{signName, String.valueOf(signLine + 1), condition.getStringToMatch(), + "{case}", "{color}"}, new String[]{signName, String.valueOf(signLine + 1), condition.stringToMatch(), translateBoolean(condition.executeRegex()), translateBoolean(condition.ignoreCase()), translateBoolean(condition.ignoreColor())})); } @@ -124,7 +124,7 @@ public class ListCommand extends TokenizedCommand { for (short lineIndex : signConditions.keySet()) { String format = Translator.getTranslatedMessage(TranslatableMessage.PAID_SIGN_INFO_CONDITION_FORMAT); conditions.append(StringFormatter.replacePlaceholders(format, new String[]{"{line}", "{condition}"}, - new String[]{String.valueOf((lineIndex + 1)), signConditions.get(lineIndex).getStringToMatch()})); + new String[]{String.valueOf((lineIndex + 1)), signConditions.get(lineIndex).stringToMatch()})); } sender.sendMessage(replacePlaceholders(Translator.getTranslatedMessage( diff --git a/src/main/java/net/knarcraft/paidsigns/container/PaidSign.java b/src/main/java/net/knarcraft/paidsigns/container/PaidSign.java index 18a3223..e16e4d8 100644 --- a/src/main/java/net/knarcraft/paidsigns/container/PaidSign.java +++ b/src/main/java/net/knarcraft/paidsigns/container/PaidSign.java @@ -148,13 +148,15 @@ public class PaidSign { * @param ignoreCase

Whether to ignore case when matching against the condition

* @param ignoreColor

Whether to ignore color when matching against the condition

*/ - public void addCondition(short line, String stringToMatch, boolean executeRegex, OptionState ignoreCase, OptionState ignoreColor) { + public void addCondition(short line, String stringToMatch, boolean executeRegex, OptionState ignoreCase, + OptionState ignoreColor) { if (line < 0 || line > 3) { throw new IllegalArgumentException("Invalid sign line given for new paid sign condition"); } boolean ignoreCaseBoolean = OptionState.getBooleanValue(ignoreCase, this.getIgnoreCase()); boolean ignoreColorBoolean = OptionState.getBooleanValue(ignoreColor, this.getIgnoreColor()); - this.conditions.put(line, new PaidSignCondition(stringToMatch, executeRegex, ignoreCaseBoolean, ignoreColorBoolean)); + this.conditions.put(line, new PaidSignCondition(stringToMatch, executeRegex, ignoreCaseBoolean, + ignoreColorBoolean)); } /** diff --git a/src/main/java/net/knarcraft/paidsigns/container/PaidSignCondition.java b/src/main/java/net/knarcraft/paidsigns/container/PaidSignCondition.java index 7cf7df5..9ac4b7b 100644 --- a/src/main/java/net/knarcraft/paidsigns/container/PaidSignCondition.java +++ b/src/main/java/net/knarcraft/paidsigns/container/PaidSignCondition.java @@ -4,64 +4,14 @@ import net.knarcraft.paidsigns.utility.ColorHelper; /** * A condition for deciding if a paid sign matches a sign line + * + * @param stringToMatch

The string/regular expression the line has to match to fulfill this condition

+ * @param executeRegex

Whether to execute the match string as a regular expression

+ * @param ignoreCase

Whether to ignore uppercase/lowercase when comparing against this condition

+ * @param ignoreColor

Whether to ignore color codes when comparing against this condition

*/ -public class PaidSignCondition { - - final String stringToMatch; - final boolean executeRegex; - final boolean ignoreCase; - final boolean ignoreColor; - - /** - * Instantiates a new paid sign condition - * - * @param stringToMatch

The string/regular expression the line has to match to fulfill this condition

- * @param executeRegex

Whether to execute the match string as a regular expression

- * @param ignoreCase

Whether to ignore uppercase/lowercase when comparing against this condition

- * @param ignoreColor

Whether to ignore color codes when comparing against this condition

- */ - public PaidSignCondition(String stringToMatch, boolean executeRegex, boolean ignoreCase, boolean ignoreColor) { - this.stringToMatch = stringToMatch; - this.executeRegex = executeRegex; - this.ignoreCase = ignoreCase; - this.ignoreColor = ignoreColor; - } - - /** - * Gets the string this condition should match - * - * @return

The string this condition should match

- */ - public String getStringToMatch() { - return this.stringToMatch; - } - - /** - * Gets whether to execute the match string as RegEx - * - * @return

Whether to execute the match string as RegEx

- */ - public boolean executeRegex() { - return this.executeRegex; - } - - /** - * Gets whether to ignore case when trying to match strings - * - * @return

Whether to ignore case when trying to match strings

- */ - public boolean ignoreCase() { - return this.ignoreCase; - } - - /** - * Gets whether to ignore color when trying to match strings - * - * @return

Whether to ignore color when trying to match strings

- */ - public boolean ignoreColor() { - return this.ignoreColor; - } +public record PaidSignCondition(String stringToMatch, boolean executeRegex, boolean ignoreCase, + boolean ignoreColor) { /** * Tests whether the given line matches this condition diff --git a/src/main/java/net/knarcraft/paidsigns/container/PaidSignConditionMatch.java b/src/main/java/net/knarcraft/paidsigns/container/PaidSignConditionMatch.java new file mode 100644 index 0000000..8a28c0d --- /dev/null +++ b/src/main/java/net/knarcraft/paidsigns/container/PaidSignConditionMatch.java @@ -0,0 +1,19 @@ +package net.knarcraft.paidsigns.container; + +import org.jetbrains.annotations.NotNull; + +/** + * A container class for number of condition matches for a paid sign + * + * @param paidSign

The paid sign this class keeps track of matches for

+ * @param conditionMatches

The number of conditions matched for the paid sign

+ */ +public record PaidSignConditionMatch(PaidSign paidSign, + int conditionMatches) implements Comparable { + + @Override + public int compareTo(@NotNull PaidSignConditionMatch other) { + return Integer.compare(this.conditionMatches, other.conditionMatches); + } + +} diff --git a/src/main/java/net/knarcraft/paidsigns/container/TrackedSign.java b/src/main/java/net/knarcraft/paidsigns/container/TrackedSign.java index a050ea7..0b5b290 100644 --- a/src/main/java/net/knarcraft/paidsigns/container/TrackedSign.java +++ b/src/main/java/net/knarcraft/paidsigns/container/TrackedSign.java @@ -4,39 +4,10 @@ import java.util.UUID; /** * A representation of a sign placed by a player that matched a paid sign + * + * @param playerId

The unique id of the player that created the sign

+ * @param cost

The cost the player paid for creating the sign

*/ -public class TrackedSign { - - private final UUID playerId; - private final double cost; - - /** - * Instantiates a new tracked sign - * - * @param playerId

The unique id of the player that created the sign

- * @param cost

The cost the player paid for creating the sign

- */ - public TrackedSign(UUID playerId, double cost) { - this.playerId = playerId; - this.cost = cost; - } - - /** - * Gets the id of the player that created this tracked sign - * - * @return

The player that created this tracked sign

- */ - public UUID getPlayerId() { - return this.playerId; - } - - /** - * Gets the cost the player paid for creating this paid sign - * - * @return

The cost paid for creating this sign

- */ - public double getCost() { - return this.cost; - } +public record TrackedSign(UUID playerId, double cost) { } diff --git a/src/main/java/net/knarcraft/paidsigns/listener/SignListener.java b/src/main/java/net/knarcraft/paidsigns/listener/SignListener.java index d469966..a3db905 100644 --- a/src/main/java/net/knarcraft/paidsigns/listener/SignListener.java +++ b/src/main/java/net/knarcraft/paidsigns/listener/SignListener.java @@ -2,6 +2,7 @@ package net.knarcraft.paidsigns.listener; import net.knarcraft.paidsigns.PaidSigns; import net.knarcraft.paidsigns.container.PaidSign; +import net.knarcraft.paidsigns.container.PaidSignConditionMatch; import net.knarcraft.paidsigns.formatting.StringFormatter; import net.knarcraft.paidsigns.formatting.TranslatableMessage; import net.knarcraft.paidsigns.manager.EconomyManager; @@ -13,6 +14,9 @@ import org.bukkit.event.Listener; import org.bukkit.event.block.SignChangeEvent; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; /** @@ -28,36 +32,90 @@ public class SignListener implements Listener { } String[] lines = event.getLines(); - Map matchingSigns = PaidSigns.getInstance().getSignManager().getAllPaidSigns(); - for (PaidSign paidSign : matchingSigns.values()) { - //If a match is found, just return - if (matchSign(paidSign, lines, event)) { - return; - } + Map allPaidSigns = PaidSigns.getInstance().getSignManager().getAllPaidSigns(); + + //Generate the sorted list of the number of paid sign conditions matched for every paid sign + List allConditionMatches = new ArrayList<>(); + for (PaidSign paidSign : allPaidSigns.values()) { + calculateConditionMatches(paidSign, lines, allConditionMatches); + } + allConditionMatches.sort(Collections.reverseOrder()); + + //Make the player pay for the paid sign + PaidSign mostExpensive = getMatchingSign(allConditionMatches); + if (mostExpensive != null) { + performPaidSignCheck(mostExpensive, event); } } /** - * Checks if the given paid sign matches the given sign lines + * Gets the most specific paid sign match + * + *

This method chooses the most specific match, if any. If two matches have the same number of matched + * conditions, it will choose the most expensive one.

+ * + * @param allConditionMatches

A sorted list of all paid sign condition matches

+ * @return

The best matching paid sign, or null if no paid sign matches

+ */ + private PaidSign getMatchingSign(List allConditionMatches) { + PaidSign mostExpensive = null; + int bestMatch = 1; + for (PaidSignConditionMatch paidSignConditionMatch : allConditionMatches) { + int conditionMatches = paidSignConditionMatch.conditionMatches(); + /* Stop if there is no matching paid signs. Also stop if a paid sign is encountered with fewer conditions + matching */ + if (conditionMatches >= bestMatch) { + PaidSign paidSign = paidSignConditionMatch.paidSign(); + //Choose the most expensive paid sign between those with the same number of matched conditions + if (mostExpensive == null || paidSign.getCost() > mostExpensive.getCost()) { + mostExpensive = paidSign; + bestMatch = conditionMatches; + } + } else { + break; + } + } + return mostExpensive; + } + + /** + * Calculates the number of conditions matching for the given paid sign + * + *

This calculates the number of matches, allowing the most specific paid sign to be chosen

+ * + * @param paidSign

The paid sign to calculate matches for

+ * @param lines

The lines of a sign

+ * @param allConditionMatches

The number of conditions matched

+ */ + private void calculateConditionMatches(PaidSign paidSign, String[] lines, List allConditionMatches) { + if (paidSign.matches(lines)) { + if (paidSign.matchAnyCondition()) { + //For any match, it can only be assumed one lines matches + allConditionMatches.add(new PaidSignConditionMatch(paidSign, 1)); + } else { + //For a normal match, the number of conditions is equal to the number of matches + allConditionMatches.add(new PaidSignConditionMatch(paidSign, paidSign.getConditions().size())); + } + } else { + allConditionMatches.add(new PaidSignConditionMatch(paidSign, 0)); + } + } + + /** + * Performs necessary checks and performs the paid sign transaction * * @param paidSign

The paid sign to test against the sign lines

- * @param lines

The lines of a sign

* @param event

The triggered sign change event to cancel if necessary

- * @return

True if a match was found and actions have been taken

*/ - private boolean matchSign(PaidSign paidSign, String[] lines, SignChangeEvent event) { - if (paidSign.matches(lines)) { - Player player = event.getPlayer(); - String permission = paidSign.getPermission(); - //If a match is found, but the player is missing the permission, assume no plugin sign was created - if (permission != null && !permission.trim().isEmpty() && !player.hasPermission(permission)) { - return true; - } - - performPaidSignTransaction(paidSign, player, event); - return true; + private void performPaidSignCheck(PaidSign paidSign, SignChangeEvent event) { + Player player = event.getPlayer(); + String permission = paidSign.getPermission(); + //If a match is found, but the player is missing the permission, assume no plugin sign was created + if (permission != null && !permission.trim().isEmpty() && !player.hasPermission(permission)) { + return; } - return false; + + performPaidSignTransaction(paidSign, player, event); } /** diff --git a/src/main/java/net/knarcraft/paidsigns/manager/PaidSignManager.java b/src/main/java/net/knarcraft/paidsigns/manager/PaidSignManager.java index 0fe4086..164c9bb 100644 --- a/src/main/java/net/knarcraft/paidsigns/manager/PaidSignManager.java +++ b/src/main/java/net/knarcraft/paidsigns/manager/PaidSignManager.java @@ -156,7 +156,7 @@ public final class PaidSignManager { Map signConditions = sign.getConditions(); for (short lineIndex : signConditions.keySet()) { PaidSignCondition condition = signConditions.get(lineIndex); - conditionsSection.set(lineIndex + ".stringToMatch", condition.getStringToMatch()); + conditionsSection.set(lineIndex + ".stringToMatch", condition.stringToMatch()); conditionsSection.set(lineIndex + ".executeRegEx", condition.executeRegex()); conditionsSection.set(lineIndex + ".ignoreCase", condition.ignoreCase()); conditionsSection.set(lineIndex + ".ignoreColor", condition.ignoreColor()); diff --git a/src/main/java/net/knarcraft/paidsigns/manager/TrackedSignManager.java b/src/main/java/net/knarcraft/paidsigns/manager/TrackedSignManager.java index bc78b0f..0392404 100644 --- a/src/main/java/net/knarcraft/paidsigns/manager/TrackedSignManager.java +++ b/src/main/java/net/knarcraft/paidsigns/manager/TrackedSignManager.java @@ -126,8 +126,8 @@ public final class TrackedSignManager { TrackedSign sign = trackedSigns.get(signLocation); String locationString = Objects.requireNonNull(signLocation.getWorld()).getUID() + "," + signLocation.getBlockX() + "," + signLocation.getBlockY() + "," + signLocation.getBlockZ(); - signSection.set(locationString + ".cost", sign.getCost()); - signSection.set(locationString + ".playerId", sign.getPlayerId().toString()); + signSection.set(locationString + ".cost", sign.cost()); + signSection.set(locationString + ".playerId", sign.playerId().toString()); } configuration.save(signsFile); } @@ -142,8 +142,8 @@ public final class TrackedSignManager { if (!PaidSigns.getInstance().areRefundsEnabled() || !refund) { return; } - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(trackedSign.getPlayerId()); - double refundSum = trackedSign.getCost() / 100 * PaidSigns.getInstance().getRefundPercentage(); + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(trackedSign.playerId()); + double refundSum = trackedSign.cost() / 100 * PaidSigns.getInstance().getRefundPercentage(); EconomyManager.deposit(offlinePlayer, refundSum); if (offlinePlayer instanceof Player player) { player.sendMessage(String.format(StringFormatter.replacePlaceholders(