Sort applicable regions by inheritance too.

This commit is contained in:
sk89q 2015-01-10 22:45:15 -08:00
parent e7273aaf6f
commit 3bd1c869f1
8 changed files with 339 additions and 123 deletions

View File

@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.sk89q.worldguard.domains.Association;
import com.sk89q.worldguard.protection.association.RegionAssociable;
import com.sk89q.worldguard.protection.flags.DefaultFlag;
@ -29,6 +30,7 @@
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.flags.StateFlag.State;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import com.sk89q.worldguard.protection.util.NormativeOrders;
import javax.annotation.Nullable;
import java.util.*;
@ -55,7 +57,7 @@ public class FlagValueCalculator {
/**
* Create a new instance.
*
* @param regions a list of applicable regions that <strong>must be sorted by priority descending</strong>
* @param regions a list of applicable regions that must be sorted according to {@link NormativeOrders}
* @param globalRegion an optional global region (null to not use one)
*/
public FlagValueCalculator(List<ProtectedRegion> regions, @Nullable ProtectedRegion globalRegion) {
@ -102,36 +104,9 @@ public Result getMembership(RegionAssociable subject) {
checkNotNull(subject);
int minimumPriority = Integer.MIN_VALUE;
boolean foundApplicableRegion = false;
Result result = Result.NO_REGIONS;
// Say there are two regions in one location: CHILD and PARENT (CHILD
// is a child of PARENT). If there are two overlapping regions in WG, a
// subject has to be a member of /both/ (or flags permit) in order to
// build in that location. However, inheritance is supposed
// to allow building if the subject is a member of just CHILD. That
// presents a problem.
//
// To rectify this, we keep two sets. When we iterate over the list of
// regions, there are two scenarios that we may encounter:
//
// 1) PARENT first, CHILD later:
// a) When the loop reaches PARENT, PARENT is added to needsClear.
// b) When the loop reaches CHILD, parents of CHILD (which includes
// PARENT) are removed from needsClear.
// c) needsClear is empty again.
//
// 2) CHILD first, PARENT later:
// a) When the loop reaches CHILD, CHILD's parents (i.e. PARENT) are
// added to hasCleared.
// b) When the loop reaches PARENT, since PARENT is already in
// hasCleared, it does not add PARENT to needsClear.
// c) needsClear stays empty.
//
// As long as the process ends with needsClear being empty, then
// we have satisfied all membership requirements.
Set<ProtectedRegion> needsClear = new HashSet<ProtectedRegion>();
Set<ProtectedRegion> hasCleared = new HashSet<ProtectedRegion>();
Set<ProtectedRegion> ignoredRegions = Sets.newHashSet();
for (ProtectedRegion region : getApplicable()) {
// Don't consider lower priorities below minimumPriority
@ -147,24 +122,23 @@ public Result getMembership(RegionAssociable subject) {
continue;
}
minimumPriority = getPriority(region);
foundApplicableRegion = true;
if (ignoredRegions.contains(region)) {
continue;
}
if (!hasCleared.contains(region)) {
if (!RegionGroup.MEMBERS.contains(subject.getAssociation(Arrays.asList(region)))) {
needsClear.add(region);
} else {
// Need to clear all parents
removeParents(needsClear, hasCleared, region);
}
minimumPriority = getPriority(region);
boolean member = RegionGroup.MEMBERS.contains(subject.getAssociation(Arrays.asList(region)));
if (member) {
result = Result.SUCCESS;
addParents(ignoredRegions, region);
} else {
return Result.FAIL;
}
}
if (foundApplicableRegion) {
return needsClear.isEmpty() ? Result.SUCCESS : Result.FAIL;
} else {
return Result.NO_REGIONS;
}
return result;
}
@ -243,7 +217,7 @@ public State queryState(@Nullable RegionAssociable subject, StateFlag flag) {
*/
@Nullable
public <V> V queryValue(@Nullable RegionAssociable subject, Flag<V> flag) {
Collection<V> values = queryAllValues(subject, flag);
Collection<V> values = queryAllValues(subject, flag, true);
return flag.chooseValue(values);
}
@ -264,10 +238,37 @@ public <V> V queryValue(@Nullable RegionAssociable subject, Flag<V> flag) {
* @param flag the flag
* @return a collection of values
*/
@SuppressWarnings("unchecked")
public <V> Collection<V> queryAllValues(@Nullable RegionAssociable subject, Flag<V> flag) {
return queryAllValues(subject, flag, false);
}
/**
* 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,
* from the collection will be used.
*
* <p>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.</p>
*
* @param subject an optional subject, which would be used to determine the region group to apply
* @param flag the flag
* @param acceptOne if possible, return only one value if it doesn't matter
* @return a collection of values
*/
@SuppressWarnings("unchecked")
private <V> Collection<V> queryAllValues(@Nullable RegionAssociable subject, Flag<V> flag, boolean acceptOne) {
checkNotNull(flag);
// Can't use this optimization with flags that have a conflict resolution strategy
if (acceptOne && flag.hasConflictStrategy()) {
acceptOne = false;
}
// Check to see whether we have a subject if this is BUILD
if (flag == DefaultFlag.BUILD && subject == null) {
throw new NullPointerException("The BUILD flag is handled in a special fashion and requires a non-null subject parameter");
@ -275,58 +276,33 @@ public <V> Collection<V> queryAllValues(@Nullable RegionAssociable subject, Flag
int minimumPriority = Integer.MIN_VALUE;
// Say there are two regions in one location: CHILD and PARENT (CHILD
// is a child of PARENT). If the two are overlapping regions in WG,
// both with values set, then we have a problem. Due to inheritance,
// only the CHILD's value for the flag should be used because it
// overrides its parent's value, but default behavior is to collect
// all the values into a list.
//
// To rectify this, we keep a map of consideredValues (region -> value)
// and an ignoredRegions set. When we iterate over the list of
// regions, there are two scenarios that we may encounter:
//
// 1) PARENT first, CHILD later:
// a) When the loop reaches PARENT, PARENT's value is added to
// consideredValues
// b) When the loop reaches CHILD, parents of CHILD (which includes
// PARENT) are removed from consideredValues (so we no longer
// consider those values). The CHILD's value is then added to
// consideredValues.
// c) In the end, only CHILD's value exists in consideredValues.
//
// 2) CHILD first, PARENT later:
// a) When the loop reaches CHILD, CHILD's value is added to
// consideredValues. In addition, the CHILD's parents (which
// includes PARENT) are added to ignoredRegions.
// b) When the loop reaches PARENT, since PARENT is in
// ignoredRegions, the parent is skipped over.
// c) In the end, only CHILD's value exists in consideredValues.
Map<ProtectedRegion, V> consideredValues = new HashMap<ProtectedRegion, V>();
Set<ProtectedRegion> ignoredRegions = new HashSet<ProtectedRegion>();
Set<ProtectedRegion> ignoredParents = new HashSet<ProtectedRegion>();
for (ProtectedRegion region : getApplicable()) {
// Don't consider lower priorities below minimumPriority
// (which starts at Integer.MIN_VALUE). A region that "counts"
// (has the flag set) will raise minimumPriority to its own
// priority.
if (getPriority(region) < minimumPriority) {
break;
}
if (ignoredParents.contains(region)) {
continue;
}
V value = getEffectiveFlag(region, flag, subject);
int priority = getPriority(region);
if (value != null) {
if (!ignoredRegions.contains(region)) {
minimumPriority = priority;
minimumPriority = priority;
ignoreValuesOfParents(consideredValues, ignoredRegions, region);
if (acceptOne) {
return Arrays.asList(value);
} else {
consideredValues.put(region, value);
}
}
addParents(ignoredParents, region);
// The BUILD flag (of lower priorities) can be overridden if
// this region has members... this check is here due to legacy
// reasons
@ -435,40 +411,17 @@ public <V> V getEffectiveFlag(final ProtectedRegion region, Flag<V> flag, @Nulla
return null;
}
/**
* Clear a region's parents for isFlagAllowed().
*
* @param needsClear the regions that should be cleared
* @param hasCleared the regions already cleared
* @param region the region to start from
*/
private void removeParents(Set<ProtectedRegion> needsClear, Set<ProtectedRegion> hasCleared, ProtectedRegion region) {
ProtectedRegion parent = region.getParent();
while (parent != null) {
if (!needsClear.remove(parent)) {
hasCleared.add(parent);
}
parent = parent.getParent();
}
}
/**
* Clear a region's parents for getFlag().
*
* @param needsClear The regions that should be cleared
* @param hasCleared The regions already cleared
* @param ignored The regions to ignore
* @param region The region to start from
*/
private void ignoreValuesOfParents(Map<ProtectedRegion, ?> needsClear, Set<ProtectedRegion> hasCleared, ProtectedRegion region) {
private void addParents(Set<ProtectedRegion> ignored, ProtectedRegion region) {
ProtectedRegion parent = region.getParent();
while (parent != null) {
if (needsClear.remove(parent) == null) {
hasCleared.add(parent);
}
ignored.add(parent);
parent = parent.getParent();
}
}

View File

@ -25,14 +25,10 @@
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.flags.StateFlag.State;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import com.sk89q.worldguard.protection.util.NormativeOrders;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.*;
import static com.google.common.base.Preconditions.checkNotNull;
@ -47,9 +43,10 @@ public class RegionResultSet extends AbstractRegionSet {
private Set<ProtectedRegion> regionSet;
/**
* Construct the object.
* Create a new region result set.
*
* <p>A sorted set will be created to include the collection of regions.</p>
* <p>The given list must not contain duplicates or the behavior of
* this instance will be undefined.</p>
*
* @param applicable the regions contained in this set
* @param globalRegion the global region, set aside for special handling.
@ -59,16 +56,30 @@ public RegionResultSet(List<ProtectedRegion> applicable, @Nullable ProtectedRegi
}
/**
* Construct the object.
* Create a new region result set.
*
* @param applicable the regions contained in this set
* @param globalRegion the global region, set aside for special handling.
*/
public RegionResultSet(Set<ProtectedRegion> applicable, @Nullable ProtectedRegion globalRegion) {
this(NormativeOrders.fromSet(applicable), globalRegion, true);
}
/**
* Create a new region result set.
*
* <p>The list of regions may be first sorted with
* {@link NormativeOrders}. If that is the case, {@code sorted} should be
* {@code true}. Otherwise, the list will be sorted in-place.</p>
*
* @param applicable the regions contained in this set
* @param globalRegion the global region, set aside for special handling.
* @param sorted true if the list is already sorted
* @param sorted true if the list is already sorted with {@link NormativeOrders}
*/
private RegionResultSet(List<ProtectedRegion> applicable, @Nullable ProtectedRegion globalRegion, boolean sorted) {
public RegionResultSet(List<ProtectedRegion> applicable, @Nullable ProtectedRegion globalRegion, boolean sorted) {
checkNotNull(applicable);
if (!sorted) {
Collections.sort(applicable);
NormativeOrders.sort(applicable);
}
this.applicable = applicable;
this.flagValueCalculator = new FlagValueCalculator(applicable, globalRegion);

View File

@ -61,6 +61,19 @@ public T getDefault() {
return null;
}
/**
* Whether the flag can take a list of values and choose a "best one."
*
* <p>This is the case with the {@link StateFlag} where {@code DENY}
* overrides {@code ALLOW}, but most flags just return the
* first result from a list.</p>
*
* @return whether a best value can be chosen
*/
public boolean hasConflictStrategy() {
return false;
}
@Nullable
public T chooseValue(Collection<T> values) {
if (!values.isEmpty()) {

View File

@ -53,6 +53,11 @@ public State getDefault() {
return def ? State.ALLOW : null;
}
@Override
public boolean hasConflictStrategy() {
return true;
}
@Override
@Nullable
public State chooseValue(Collection<State> values) {

View File

@ -21,6 +21,7 @@
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.collect.Sets;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldedit.Vector2D;
import com.sk89q.worldguard.LocalPlayer;
@ -317,7 +318,7 @@ public Set<ProtectedRegion> removeRegion(String id, RemovalStrategy strategy) {
public ApplicableRegionSet getApplicableRegions(Vector position) {
checkNotNull(position);
List<ProtectedRegion> regions = new ArrayList<ProtectedRegion>();
Set<ProtectedRegion> regions = Sets.newHashSet();
index.applyContaining(position, new RegionCollectionConsumer(regions, true));
return new RegionResultSet(regions, index.get("__global__"));
}
@ -332,7 +333,7 @@ public ApplicableRegionSet getApplicableRegions(Vector position) {
public ApplicableRegionSet getApplicableRegions(ProtectedRegion region) {
checkNotNull(region);
List<ProtectedRegion> regions = new ArrayList<ProtectedRegion>();
Set<ProtectedRegion> regions = Sets.newHashSet();
index.applyIntersecting(region, new RegionCollectionConsumer(regions, true));
return new RegionResultSet(regions, index.get("__global__"));
}

View File

@ -0,0 +1,141 @@
/*
* WorldGuard, a suite of tools for Minecraft
* Copyright (C) sk89q <http://www.sk89q.com>
* 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 <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldguard.protection.util;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.sk89q.worldguard.protection.ApplicableRegionSet;
import com.sk89q.worldguard.protection.FlagValueCalculator;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import javax.annotation.Nullable;
import java.util.*;
/**
* Sorts a list of regions so that higher priority regions always take
* precedence over lower priority ones, and after sorting by priority, so
* child regions always take priority over their parent regions.
*
* <p>For example, if the regions are a, aa, aaa, aab, aac, b, ba, bc, where
* aa implies that the second 'a' is a child of the first 'a', the sorted
* order must reflect the following properties (where regions on the
* left of &lt; appear before in the sorted list):</p>
*
* <ul>
* <li>[aaa, aab, aac] < aa < a</li>
* <li>[ba, bc] < b</li>
* </ul>
*
* <p>In the case of "[aaa, aab, aac]," the relative order between these
* regions is unimportant as they all share the same parent (aaa). The
* following choices would be valid sorts:</p>
*
* <ul>
* <li>aaa, aab, aac, aa, a, ba, bc, b</li>
* <li>aab, aaa, aac, aa, a, bc, ba, b</li>
* <li>bc, ba, b, aab, aaa, aac, aa, a</li>
* <li>aab, aaa, bc, aac, aa, ba, a, b</li>
* </ul>
*
* <p>These sorted lists are required for {@link FlagValueCalculator} and
* some implementations of {@link ApplicableRegionSet}.</p>
*/
public class NormativeOrders {
private static final PriorityComparator PRIORITY_COMPARATOR = new PriorityComparator();
public static void sort(List<ProtectedRegion> regions) {
sortInto(Sets.newHashSet(regions), regions);
}
public static List<ProtectedRegion> fromSet(Set<ProtectedRegion> regions) {
List<ProtectedRegion> sorted = Arrays.asList(new ProtectedRegion[regions.size()]);
sortInto(regions, sorted);
return sorted;
}
private static void sortInto(Set<ProtectedRegion> regions, List<ProtectedRegion> sorted) {
List<RegionNode> root = Lists.newArrayList();
Map<ProtectedRegion, RegionNode> nodes = Maps.newHashMap();
for (ProtectedRegion region : regions) {
addNode(nodes, root, region);
}
int index = regions.size() - 1;
for (RegionNode node : root) {
while (node != null) {
if (regions.contains(node.region)) {
sorted.set(index, node.region);
index--;
}
node = node.next;
}
}
Collections.sort(sorted, PRIORITY_COMPARATOR);
}
private static RegionNode addNode(Map<ProtectedRegion, RegionNode> nodes, List<RegionNode> root, ProtectedRegion region) {
RegionNode node = nodes.get(region);
if (node == null) {
node = new RegionNode(region);
nodes.put(region, node);
if (region.getParent() != null) {
addNode(nodes, root, region.getParent()).insertAfter(node);
} else {
root.add(node);
}
}
return node;
}
private static class RegionNode {
@Nullable private RegionNode next;
private final ProtectedRegion region;
private RegionNode(ProtectedRegion region) {
this.region = region;
}
private void insertAfter(RegionNode node) {
if (this.next == null) {
this.next = node;
} else {
node.next = this.next;
this.next = node;
}
}
}
private static class PriorityComparator implements Comparator<ProtectedRegion> {
@Override
public int compare(ProtectedRegion o1, ProtectedRegion o2) {
if (o1.getPriority() > o2.getPriority()) {
return -1;
} else if (o1.getPriority() < o2.getPriority()) {
return 1;
} else {
return 0;
}
}
}
}

View File

@ -363,6 +363,97 @@ public void testQueryValueMultipleFlags() throws Exception {
assertThat(result.queryValue(null, flag3), is((State) null));
}
@Test
public void testQueryValueFlagsWithRegionGroupsAndInheritance() throws Exception {
MockApplicableRegionSet mock = new MockApplicableRegionSet();
StateFlag flag1 = new StateFlag("test1", false);
LocalPlayer nonMember = mock.createPlayer();
LocalPlayer member = mock.createPlayer();
ProtectedRegion parent = mock.add(0);
parent.setFlag(flag1, State.DENY);
parent.setFlag(flag1.getRegionGroupFlag(), RegionGroup.NON_MEMBERS);
ProtectedRegion region = mock.add(0);
region.getMembers().addPlayer(member);
region.setParent(parent);
FlagValueCalculator result = mock.getFlagCalculator();
assertThat(result.queryValue(nonMember, flag1), is(State.DENY));
assertThat(result.queryValue(member, flag1), is((State) null));
}
@Test
public void testQueryValueFlagsWithRegionGroupsAndInheritanceAndParentMember() throws Exception {
MockApplicableRegionSet mock = new MockApplicableRegionSet();
StateFlag flag1 = new StateFlag("test1", false);
LocalPlayer nonMember = mock.createPlayer();
LocalPlayer memberOne = mock.createPlayer();
LocalPlayer memberTwo = mock.createPlayer();
ProtectedRegion parent = mock.add(0);
parent.getMembers().addPlayer(memberOne);
parent.setFlag(flag1, State.DENY);
parent.setFlag(flag1.getRegionGroupFlag(), RegionGroup.NON_MEMBERS);
ProtectedRegion region = mock.add(0);
region.getMembers().addPlayer(memberOne);
region.setParent(parent);
FlagValueCalculator result = mock.getFlagCalculator();
assertThat(result.queryValue(nonMember, flag1), is(State.DENY));
assertThat(result.queryValue(memberOne, flag1), is((State) null));
assertThat(result.queryValue(memberTwo, flag1), is(State.DENY));
}
@Test
public void testQueryValueFlagsWithRegionGroupsAndPriority() throws Exception {
MockApplicableRegionSet mock = new MockApplicableRegionSet();
StateFlag flag1 = new StateFlag("test1", false);
LocalPlayer nonMember = mock.createPlayer();
LocalPlayer member = mock.createPlayer();
ProtectedRegion lower = mock.add(-1);
lower.setFlag(flag1, State.DENY);
lower.setFlag(flag1.getRegionGroupFlag(), RegionGroup.NON_MEMBERS);
ProtectedRegion region = mock.add(0);
region.getMembers().addPlayer(member);
FlagValueCalculator result = mock.getFlagCalculator();
assertThat(result.queryValue(nonMember, flag1), is(State.DENY));
assertThat(result.queryValue(member, flag1), is(State.DENY));
}
@Test
public void testQueryValueFlagsWithRegionGroupsAndPriorityAndOveride() throws Exception {
MockApplicableRegionSet mock = new MockApplicableRegionSet();
StateFlag flag1 = new StateFlag("test1", false);
LocalPlayer nonMember = mock.createPlayer();
LocalPlayer member = mock.createPlayer();
ProtectedRegion lower = mock.add(-1);
lower.setFlag(flag1, State.DENY);
lower.setFlag(flag1.getRegionGroupFlag(), RegionGroup.NON_MEMBERS);
ProtectedRegion region = mock.add(0);
region.setFlag(flag1, State.ALLOW);
region.setFlag(flag1.getRegionGroupFlag(), RegionGroup.MEMBERS);
region.getMembers().addPlayer(member);
FlagValueCalculator result = mock.getFlagCalculator();
assertThat(result.queryValue(nonMember, flag1), is(State.DENY));
assertThat(result.queryValue(member, flag1), is(State.ALLOW));
}
@Test
public void testQueryValueStringFlag() throws Exception {
MockApplicableRegionSet mock = new MockApplicableRegionSet();

View File

@ -25,6 +25,7 @@
import com.sk89q.worldguard.protection.regions.GlobalProtectedRegion;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import com.sk89q.worldguard.protection.util.NormativeOrders;
import java.util.ArrayList;
import java.util.Collections;
@ -86,7 +87,7 @@ public ApplicableRegionSet getApplicableSet() {
}
public FlagValueCalculator getFlagCalculator() {
Collections.sort(regions);
NormativeOrders.sort(regions);
return new FlagValueCalculator(regions, global);
}