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
This commit is contained in:
Kristian Knarvik 2022-07-20 18:09:09 +02:00
parent 32ec713994
commit 38717a6b91
9 changed files with 122 additions and 122 deletions

View File

@ -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(

View File

@ -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(

View File

@ -148,13 +148,15 @@ public class PaidSign {
* @param ignoreCase <p>Whether to ignore case when matching against the condition</p>
* @param ignoreColor <p>Whether to ignore color when matching against the condition</p>
*/
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));
}
/**

View File

@ -4,64 +4,14 @@ import net.knarcraft.paidsigns.utility.ColorHelper;
/**
* A condition for deciding if a paid sign matches a sign line
*
* @param stringToMatch <p>The string/regular expression the line has to match to fulfill this condition</p>
* @param executeRegex <p>Whether to execute the match string as a regular expression</p>
* @param ignoreCase <p>Whether to ignore uppercase/lowercase when comparing against this condition</p>
* @param ignoreColor <p>Whether to ignore color codes when comparing against this condition</p>
*/
public class PaidSignCondition {
final String stringToMatch;
final boolean executeRegex;
final boolean ignoreCase;
final boolean ignoreColor;
/**
* Instantiates a new paid sign condition
*
* @param stringToMatch <p>The string/regular expression the line has to match to fulfill this condition</p>
* @param executeRegex <p>Whether to execute the match string as a regular expression</p>
* @param ignoreCase <p>Whether to ignore uppercase/lowercase when comparing against this condition</p>
* @param ignoreColor <p>Whether to ignore color codes when comparing against this condition</p>
*/
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 <p>The string this condition should match</p>
*/
public String getStringToMatch() {
return this.stringToMatch;
}
/**
* Gets whether to execute the match string as RegEx
*
* @return <p>Whether to execute the match string as RegEx</p>
*/
public boolean executeRegex() {
return this.executeRegex;
}
/**
* Gets whether to ignore case when trying to match strings
*
* @return <p>Whether to ignore case when trying to match strings</p>
*/
public boolean ignoreCase() {
return this.ignoreCase;
}
/**
* Gets whether to ignore color when trying to match strings
*
* @return <p>Whether to ignore color when trying to match strings</p>
*/
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

View File

@ -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 <p>The paid sign this class keeps track of matches for</p>
* @param conditionMatches <p>The number of conditions matched for the paid sign</p>
*/
public record PaidSignConditionMatch(PaidSign paidSign,
int conditionMatches) implements Comparable<PaidSignConditionMatch> {
@Override
public int compareTo(@NotNull PaidSignConditionMatch other) {
return Integer.compare(this.conditionMatches, other.conditionMatches);
}
}

View File

@ -4,39 +4,10 @@ import java.util.UUID;
/**
* A representation of a sign placed by a player that matched a paid sign
*
* @param playerId <p>The unique id of the player that created the sign</p>
* @param cost <p>The cost the player paid for creating the sign</p>
*/
public class TrackedSign {
private final UUID playerId;
private final double cost;
/**
* Instantiates a new tracked sign
*
* @param playerId <p>The unique id of the player that created the sign</p>
* @param cost <p>The cost the player paid for creating the sign</p>
*/
public TrackedSign(UUID playerId, double cost) {
this.playerId = playerId;
this.cost = cost;
}
/**
* Gets the id of the player that created this tracked sign
*
* @return <p>The player that created this tracked sign</p>
*/
public UUID getPlayerId() {
return this.playerId;
}
/**
* Gets the cost the player paid for creating this paid sign
*
* @return <p>The cost paid for creating this sign</p>
*/
public double getCost() {
return this.cost;
}
public record TrackedSign(UUID playerId, double cost) {
}

View File

@ -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<String, PaidSign> matchingSigns = PaidSigns.getInstance().getSignManager().getAllPaidSigns();
for (PaidSign paidSign : matchingSigns.values()) {
//If a match is found, just return
if (matchSign(paidSign, lines, event)) {
return;
}
Map<String, PaidSign> allPaidSigns = PaidSigns.getInstance().getSignManager().getAllPaidSigns();
//Generate the sorted list of the number of paid sign conditions matched for every paid sign
List<PaidSignConditionMatch> 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
*
* <p>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.</p>
*
* @param allConditionMatches <p>A sorted list of all paid sign condition matches</p>
* @return <p>The best matching paid sign, or null if no paid sign matches</p>
*/
private PaidSign getMatchingSign(List<PaidSignConditionMatch> 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
*
* <p>This calculates the number of matches, allowing the most specific paid sign to be chosen</p>
*
* @param paidSign <p>The paid sign to calculate matches for</p>
* @param lines <p>The lines of a sign</p>
* @param allConditionMatches <p>The number of conditions matched</p>
*/
private void calculateConditionMatches(PaidSign paidSign, String[] lines, List<PaidSignConditionMatch> 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 <p>The paid sign to test against the sign lines</p>
* @param lines <p>The lines of a sign</p>
* @param event <p>The triggered sign change event to cancel if necessary</p>
* @return <p>True if a match was found and actions have been taken</p>
*/
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);
}
/**

View File

@ -156,7 +156,7 @@ public final class PaidSignManager {
Map<Short, PaidSignCondition> 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());

View File

@ -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(