diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/ApplicableRegionSet.java b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/ApplicableRegionSet.java index 8a7a9dc22..845931d08 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/ApplicableRegionSet.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/ApplicableRegionSet.java @@ -23,6 +23,8 @@ import com.sk89q.worldguard.protection.association.RegionAssociable; import com.sk89q.worldguard.protection.flags.Flags; import com.sk89q.worldguard.protection.flags.Flag; +import com.sk89q.worldguard.protection.flags.MapFlag; +import com.sk89q.worldguard.protection.flags.RegionGroup; import com.sk89q.worldguard.protection.flags.StateFlag; import com.sk89q.worldguard.protection.flags.StateFlag.State; import com.sk89q.worldguard.protection.managers.RegionManager; @@ -119,6 +121,50 @@ public interface ApplicableRegionSet extends Iterable { @Nullable V queryValue(@Nullable RegionAssociable subject, Flag flag); + /** + * Get the effective value for a key in a {@link MapFlag}. If there are multiple values + * (for example, if there are multiple regions with the same priority + * but with different farewell messages set, there would be multiple + * completing values), then the selected (or "winning") value will be undefined. + * + *

A subject can be provided that is used to determine whether the value + * of a flag on a particular region should be used. For example, if a + * flag's region group is set to {@link RegionGroup#MEMBERS} and the given + * subject is not a member, then the region would be skipped when + * querying that flag. If {@code null} is provided for the subject, then + * only flags that use {@link RegionGroup#ALL}, + * {@link RegionGroup#NON_MEMBERS}, etc. will apply.

+ * + * @param subject an optional subject, which would be used to determine the region group to apply + * @param flag the flag of type {@link MapFlag} + * @param key the key for the map flag + * @return a value, which could be {@code null} + */ + @Nullable + V queryMapValue(@Nullable RegionAssociable subject, MapFlag flag, K key); + + /** + * Get the effective value for a key in a {@link MapFlag}. If there are multiple values + * (for example, if there are multiple regions with the same priority + * but with different farewell messages set, there would be multiple + * completing values), then the selected (or "winning") value will be undefined. + * + *

A subject can be provided that is used to determine whether the value + * of a flag on a particular region should be used. For example, if a + * flag's region group is set to {@link RegionGroup#MEMBERS} and the given + * subject is not a member, then the region would be skipped when + * querying that flag. If {@code null} is provided for the subject, then + * only flags that use {@link RegionGroup#ALL}, + * {@link RegionGroup#NON_MEMBERS}, etc. will apply.

+ * + * @param subject an optional subject, which would be used to determine the region group to apply + * @param flag the flag of type {@link MapFlag} + * @param key the key for the map flag + * @return a value, which could be {@code null} + */ + @Nullable + V queryMapValue(@Nullable RegionAssociable subject, MapFlag flag, K key, @Nullable Flag fallback); + /** * Get the effective values for a flag, returning a collection of all * values. It is up to the caller to determine which value, if any, diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/FailedLoadRegionSet.java b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/FailedLoadRegionSet.java index 452c516a4..71b99e059 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/FailedLoadRegionSet.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/FailedLoadRegionSet.java @@ -24,6 +24,7 @@ import com.sk89q.worldguard.protection.association.RegionAssociable; import com.sk89q.worldguard.protection.flags.Flags; import com.sk89q.worldguard.protection.flags.Flag; +import com.sk89q.worldguard.protection.flags.MapFlag; import com.sk89q.worldguard.protection.flags.StateFlag.State; import com.sk89q.worldguard.protection.regions.ProtectedRegion; @@ -31,6 +32,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.Map; import java.util.Set; /** @@ -65,6 +67,19 @@ public V queryValue(@Nullable RegionAssociable subject, Flag flag) { return flag.getDefault(); } + @Nullable + @Override + public V queryMapValue(@Nullable RegionAssociable subject, MapFlag flag, K key) { + return queryMapValue(subject, flag, key, null); + } + + @Nullable + @Override + public V queryMapValue(@Nullable RegionAssociable subject, MapFlag flag, K key, @Nullable Flag fallback) { + Map defaultVal = flag.getDefault(); + return defaultVal != null ? defaultVal.get(key) : fallback != null ? fallback.getDefault() : null; + } + @SuppressWarnings("unchecked") @Override public Collection queryAllValues(@Nullable RegionAssociable subject, Flag flag) { diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/FlagValueCalculator.java b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/FlagValueCalculator.java index 48c413166..b30de0cc7 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/FlagValueCalculator.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/FlagValueCalculator.java @@ -26,6 +26,7 @@ import com.sk89q.worldguard.protection.association.RegionAssociable; import com.sk89q.worldguard.protection.flags.Flags; import com.sk89q.worldguard.protection.flags.Flag; +import com.sk89q.worldguard.protection.flags.MapFlag; import com.sk89q.worldguard.protection.flags.RegionGroup; import com.sk89q.worldguard.protection.flags.StateFlag; import com.sk89q.worldguard.protection.flags.StateFlag.State; @@ -224,6 +225,110 @@ public V queryValue(@Nullable RegionAssociable subject, Flag flag) { return flag.chooseValue(values); } + /** + * Get the effective value for a key in a {@link MapFlag}. If there are multiple values + * (for example, if there are multiple regions with the same priority + * but with different farewell messages set, there would be multiple + * completing values), then the selected (or "winning") value will be undefined. + * + *

A subject can be provided that is used to determine whether the value + * of a flag on a particular region should be used. For example, if a + * flag's region group is set to {@link RegionGroup#MEMBERS} and the given + * subject is not a member, then the region would be skipped when + * querying that flag. If {@code null} is provided for the subject, then + * only flags that use {@link RegionGroup#ALL}, + * {@link RegionGroup#NON_MEMBERS}, etc. will apply.

+ * + * @param subject an optional subject, which would be used to determine the region group to apply + * @param flag the flag of type {@link MapFlag} + * @param key the key for the map flag + * @return a value, which could be {@code null} + */ + @Nullable + public V queryMapValue(@Nullable RegionAssociable subject, MapFlag flag, K key, Flag fallback) { + checkNotNull(flag); + checkNotNull(key); + + Map consideredValues = new HashMap<>(); + Map fallbackValues = new HashMap<>(); + int minimumPriority = Integer.MIN_VALUE; + Set ignoredParents = new HashSet<>(); + + for(ProtectedRegion region : getApplicable()) { + if (getPriority(region) < minimumPriority) { + break; + } + + if (ignoredParents.contains(region)) { + continue; + } + + V effectiveValue = getEffectiveMapValue(region, flag, key, subject); + + if (effectiveValue != null) { + minimumPriority = getPriority(region); + consideredValues.put(region, effectiveValue); + } else if (fallback != null) { + effectiveValue = getEffectiveFlag(region, fallback, subject); + if (effectiveValue != null) { + minimumPriority = getPriority(region); + fallbackValues.put(region, effectiveValue); + } + } + + addParents(ignoredParents, region); + } + + + if (consideredValues.isEmpty()) { + if (fallback != null && !fallbackValues.isEmpty()) { + return fallback.chooseValue(fallbackValues.values()); + } + V defaultValue = flag.getValueFlag().getDefault(); + return defaultValue != null ? defaultValue : fallback != null ? fallback.getDefault() : null; + } + + return flag.getValueFlag().chooseValue(consideredValues.values()); + } + + @Nullable + private V getEffectiveMapValue(ProtectedRegion region, MapFlag mapFlag, K key, RegionAssociable subject) { + List seen = new ArrayList<>(); + ProtectedRegion current = region; + + while (current != null) { + seen.add(current); + + Map mapValue = current.getFlag(mapFlag); + + if (mapValue != null && mapValue.containsKey(key)) { + boolean use = true; + + if (mapFlag.getRegionGroupFlag() != null) { + RegionGroup group = current.getFlag(mapFlag.getRegionGroupFlag()); + if (group == null) { + group = mapFlag.getRegionGroupFlag().getDefault(); + } + + if (group == null) { + use = false; + } else if (subject == null) { + use = group.contains(Association.NON_MEMBER); + } else if (!group.contains(subject.getAssociation(seen))) { + use = false; + } + } + + if (use) { + return mapValue.get(key); + } + } + + current = current.getParent(); + } + return null; + } + /** * Get the effective values for a flag, returning a collection of all * values. It is up to the caller to determine which value, if any, diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/PermissiveRegionSet.java b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/PermissiveRegionSet.java index 91dccc050..bf800e5b8 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/PermissiveRegionSet.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/PermissiveRegionSet.java @@ -24,6 +24,7 @@ import com.sk89q.worldguard.protection.association.RegionAssociable; import com.sk89q.worldguard.protection.flags.Flags; import com.sk89q.worldguard.protection.flags.Flag; +import com.sk89q.worldguard.protection.flags.MapFlag; import com.sk89q.worldguard.protection.flags.StateFlag.State; import com.sk89q.worldguard.protection.regions.ProtectedRegion; @@ -31,6 +32,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.Map; import java.util.Set; /** @@ -59,6 +61,19 @@ public V queryValue(@Nullable RegionAssociable subject, Flag flag) { return flag.getDefault(); } + @Nullable + @Override + public V queryMapValue(@Nullable RegionAssociable subject, MapFlag flag, K key) { + return queryMapValue(subject, flag, key, null); + } + + @Nullable + @Override + public V queryMapValue(@Nullable RegionAssociable subject, MapFlag flag, K key, @Nullable Flag fallback) { + Map defaultVal = flag.getDefault(); + return defaultVal != null ? defaultVal.get(key) : fallback != null ? fallback.getDefault() : null; + } + @SuppressWarnings("unchecked") @Override public Collection queryAllValues(@Nullable RegionAssociable subject, Flag flag) { diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/RegionResultSet.java b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/RegionResultSet.java index ae8682955..7afec7219 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/RegionResultSet.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/RegionResultSet.java @@ -22,6 +22,7 @@ import com.sk89q.worldguard.LocalPlayer; import com.sk89q.worldguard.protection.association.RegionAssociable; import com.sk89q.worldguard.protection.flags.Flag; +import com.sk89q.worldguard.protection.flags.MapFlag; import com.sk89q.worldguard.protection.flags.StateFlag; import com.sk89q.worldguard.protection.flags.StateFlag.State; import com.sk89q.worldguard.protection.regions.ProtectedRegion; @@ -108,6 +109,18 @@ public Collection queryAllValues(@Nullable RegionAssociable subject, Flag return flagValueCalculator.queryAllValues(subject, flag); } + @Override + @Nullable + public V queryMapValue(@Nullable RegionAssociable subject, MapFlag flag, K key) { + return flagValueCalculator.queryMapValue(subject, flag, key, null); + } + + @Override + @Nullable + public V queryMapValue(@Nullable RegionAssociable subject, MapFlag flag, K key, Flag fallback) { + return flagValueCalculator.queryMapValue(subject, flag, key, fallback); + } + @Override public boolean isOwnerOfAll(LocalPlayer player) { checkNotNull(player); diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/regions/RegionQuery.java b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/regions/RegionQuery.java index 59adb0c8b..03a41a0f7 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/protection/regions/RegionQuery.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/protection/regions/RegionQuery.java @@ -34,6 +34,8 @@ import com.sk89q.worldguard.protection.association.RegionAssociable; import com.sk89q.worldguard.protection.flags.Flag; import com.sk89q.worldguard.protection.flags.Flags; +import com.sk89q.worldguard.protection.flags.MapFlag; +import com.sk89q.worldguard.protection.flags.RegionGroup; import com.sk89q.worldguard.protection.flags.StateFlag; import com.sk89q.worldguard.protection.flags.StateFlag.State; import com.sk89q.worldguard.protection.managers.RegionManager; @@ -171,6 +173,56 @@ public boolean testBuild(Location location, RegionAssociable associable, StateFl queryState(location, associable, flag))); } + /** + * Returns true if the BUILD flag allows the action in the location, but it + * can be overridden by a list of other flags. The BUILD flag will not + * override the other flags, but the other flags can override BUILD. If + * neither BUILD or any of the flags permit the action, then false will + * be returned. + * + *

Use this method when checking flags that are related to build + * protection. For example, lighting fire in a region should not be + * permitted unless the player is a member of the region or the + * LIGHTER flag allows it. However, the LIGHTER flag should be able + * to allow lighting fires even if BUILD is set to DENY.

+ * + *

This method does include parameters for a {@link MapFlag}.

+ * + *

How this method works (BUILD can be overridden by other flags but + * not the other way around) is inconsistent, but it's required for + * legacy reasons.

+ * + *

This method does not check the region bypass permission. That must + * be done by the calling code.

+ * + * @param location the location + * @param associable an optional associable + * @param mapFlag the MapFlag + * @param key the key for the MapFlag + * @param fallback the fallback flag for MapFlag + * @param flag the flags + * @return true if the result was {@code ALLOW} + * @see RegionResultSet#queryValue(RegionAssociable, Flag) + */ + public boolean testBuild(Location location, RegionAssociable associable, MapFlag mapFlag, K key, + @Nullable StateFlag fallback, StateFlag... flag) { + if (mapFlag == null) + return testBuild(location, associable, flag); + + if (flag.length == 0) { + return StateFlag.test(StateFlag.combine( + StateFlag.denyToNone(queryState(location, associable, Flags.BUILD)), + queryMapValue(location, associable, mapFlag, key, fallback) + )); + } + + return StateFlag.test(StateFlag.combine( + StateFlag.denyToNone(queryState(location, associable, Flags.BUILD)), + queryMapValue(location, associable, mapFlag, key, fallback), + queryState(location, associable, flag) + )); + } + /** * Test whether the (effective) value for a list of state flags equals * {@code ALLOW}. @@ -319,6 +371,58 @@ public V queryValue(Location location, @Nullable RegionAssociable associable return getApplicableRegions(location).queryValue(associable, flag); } + /** + * Get the effective value for a key in a {@link MapFlag}. If there are multiple values + * (for example, if there are multiple regions with the same priority + * but with different farewell messages set, there would be multiple + * completing values), then the selected (or "winning") value will be undefined. + * + *

A subject can be provided that is used to determine whether the value + * of a flag on a particular region should be used. For example, if a + * flag's region group is set to {@link RegionGroup#MEMBERS} and the given + * subject is not a member, then the region would be skipped when + * querying that flag. If {@code null} is provided for the subject, then + * only flags that use {@link RegionGroup#ALL}, + * {@link RegionGroup#NON_MEMBERS}, etc. will apply.

+ * + * @param subject an optional subject, which would be used to determine the region group to apply + * @param flag the flag of type {@link MapFlag} + * @param key the key for the map flag + * @return a value, which could be {@code null} + */ + @Nullable + public V queryMapValue(Location location, @Nullable RegionAssociable subject, MapFlag flag, K key) { + return getApplicableRegions(location).queryMapValue(subject, flag, key); + } + + /** + * Get the effective value for a key in a {@link MapFlag}. If there are multiple values + * (for example, if there are multiple regions with the same priority + * but with different farewell messages set, there would be multiple + * completing values), then the selected (or "winning") value will be undefined. + * + *

A subject can be provided that is used to determine whether the value + * of a flag on a particular region should be used. For example, if a + * flag's region group is set to {@link RegionGroup#MEMBERS} and the given + * subject is not a member, then the region would be skipped when + * querying that flag. If {@code null} is provided for the subject, then + * only flags that use {@link RegionGroup#ALL}, + * {@link RegionGroup#NON_MEMBERS}, etc. will apply.

+ * + *

It's possible to provide a fallback flag for the case when the key doesn't + * exist in the {@link MapFlag}.

+ * + * @param subject an optional subject, which would be used to determine the region group to apply + * @param flag the flag of type {@link MapFlag} + * @param key the key for the map flag + * @param fallback the fallback flag + * @return a value, which could be {@code null} + */ + @Nullable + public V queryMapValue(Location location, @Nullable RegionAssociable subject, MapFlag flag, K key, Flag fallback) { + return getApplicableRegions(location).queryMapValue(subject, flag, key, fallback); + } + /** * Get the effective values for a flag, returning a collection of all * values. It is up to the caller to determine which value, if any, diff --git a/worldguard-core/src/test/java/com/sk89q/worldguard/protection/FlagValueCalculatorMapFlagTest.java b/worldguard-core/src/test/java/com/sk89q/worldguard/protection/FlagValueCalculatorMapFlagTest.java new file mode 100644 index 000000000..25dd41953 --- /dev/null +++ b/worldguard-core/src/test/java/com/sk89q/worldguard/protection/FlagValueCalculatorMapFlagTest.java @@ -0,0 +1,124 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * Copyright (C) WorldGuard team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldguard.protection; + +import com.sk89q.worldguard.protection.flags.Flags; +import com.sk89q.worldguard.protection.flags.MapFlag; +import com.sk89q.worldguard.protection.flags.StateFlag; +import com.sk89q.worldguard.protection.flags.StringFlag; +import com.sk89q.worldguard.protection.regions.ProtectedRegion; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +@SuppressWarnings({"UnusedDeclaration"}) +public class FlagValueCalculatorMapFlagTest { + @Test + public void testGetEffectiveMapFlagWithFallback() throws Exception { + MockApplicableRegionSet mock = new MockApplicableRegionSet(); + + ProtectedRegion global = mock.global(); + global.setFlag(Flags.BUILD, StateFlag.State.DENY); + MapFlag mapFlag = + new MapFlag<>("test", new StringFlag(null), new StateFlag(null, true)); + Map map = new HashMap<>(); + map.put("allow", StateFlag.State.ALLOW); + map.put("deny", StateFlag.State.DENY); + global.setFlag(mapFlag, map); + + ApplicableRegionSet applicableSet = mock.getApplicableSet(); + assertThat(applicableSet.queryMapValue(null, mapFlag, "allow", Flags.BUILD), + equalTo(StateFlag.State.ALLOW)); + assertThat(applicableSet.queryMapValue(null, mapFlag, "deny", Flags.BUILD), + equalTo(StateFlag.State.DENY)); + assertThat(applicableSet.queryMapValue(null, mapFlag, "undefined", Flags.BUILD), + equalTo(StateFlag.State.DENY)); + assertThat(applicableSet.queryMapValue(null, mapFlag, "allow", null), + equalTo(StateFlag.State.ALLOW)); + assertThat(applicableSet.queryMapValue(null, mapFlag, "deny", null), + equalTo(StateFlag.State.DENY)); + assertThat(applicableSet.queryMapValue(null, mapFlag, "undefined", null), + equalTo(StateFlag.State.ALLOW)); + } + + @Test + public void testGetEffectiveMapFlagWithSamePriority() throws Exception { + MockApplicableRegionSet mock = new MockApplicableRegionSet(); + + ProtectedRegion region1 = mock.add(0); + ProtectedRegion region2 = mock.add(0); + + MapFlag mapFlag = + new MapFlag<>("test", new StringFlag(null), new StateFlag(null, true)); + Map map1 = new HashMap<>(); + Map map2 = new HashMap<>(); + + map1.put("should-deny", StateFlag.State.ALLOW); + map2.put("should-deny", StateFlag.State.DENY); + + map1.put("should-allow", StateFlag.State.ALLOW); + + map1.put("should-allow2", StateFlag.State.ALLOW); + map2.put("should-allow2", StateFlag.State.ALLOW); + + region1.setFlag(mapFlag, map1); + region2.setFlag(mapFlag, map2); + + ApplicableRegionSet applicableSet = mock.getApplicableSet(); + assertThat(applicableSet.queryMapValue(null, mapFlag, "should-deny", null), + equalTo(StateFlag.State.DENY)); + assertThat(applicableSet.queryMapValue(null, mapFlag, "should-allow", null), + equalTo(StateFlag.State.ALLOW)); + assertThat(applicableSet.queryMapValue(null, mapFlag, "should-allow2", null), + equalTo(StateFlag.State.ALLOW)); + } + + + @Test + public void testGetEffectiveMapFlagWithInheritance() throws Exception { + MockApplicableRegionSet mock = new MockApplicableRegionSet(); + + ProtectedRegion parent = mock.add(0); + ProtectedRegion child = mock.add(0, parent); + + MapFlag mapFlag = + new MapFlag<>("test", new StringFlag(null), new StateFlag(null, true)); + Map parentMap = new HashMap<>(); + Map childMap = new HashMap<>(); + + parentMap.put("useChildValue", StateFlag.State.ALLOW); + childMap.put("useChildValue", StateFlag.State.DENY); + + parentMap.put("useParentValue", StateFlag.State.ALLOW); + + parent.setFlag(mapFlag, parentMap); + child.setFlag(mapFlag, childMap); + + ApplicableRegionSet applicableSet = mock.getApplicableSet(); + assertThat(applicableSet.queryMapValue(null, mapFlag, "useChildValue", null), + equalTo(StateFlag.State.DENY)); + assertThat(applicableSet.queryMapValue(null, mapFlag, "useParentValue", null), + equalTo(StateFlag.State.ALLOW)); + } +}