Updated FlagValueCalculator to treat global regions a lowest priority region.

This commit is contained in:
sk89q 2014-08-16 13:30:58 -07:00
parent 4d43ef5305
commit 7481acba8c
6 changed files with 629 additions and 852 deletions

View File

@ -26,23 +26,18 @@
import com.sk89q.worldguard.protection.flags.RegionGroup;
import com.sk89q.worldguard.protection.flags.RegionGroupFlag;
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.flags.StateFlag.*;
import com.sk89q.worldguard.protection.managers.RegionManager;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.sk89q.worldguard.protection.flags.StateFlag.*;
import static com.sk89q.worldguard.protection.flags.StateFlag.test;
/**
* Represents the effective set of flags, owners, and members for a given
@ -61,6 +56,7 @@ public class ApplicableRegionSet implements Iterable<ProtectedRegion> {
private final SortedSet<ProtectedRegion> applicable;
@Nullable
private final ProtectedRegion globalRegion;
private final FlagValueCalculator flagValueCalculator;
/**
* Construct the object.
@ -84,6 +80,7 @@ public ApplicableRegionSet(SortedSet<ProtectedRegion> applicable, @Nullable Prot
checkNotNull(applicable);
this.applicable = applicable;
this.globalRegion = globalRegion;
this.flagValueCalculator = new FlagValueCalculator(applicable, globalRegion);
}
/**
@ -94,7 +91,7 @@ public ApplicableRegionSet(SortedSet<ProtectedRegion> applicable, @Nullable Prot
*/
public boolean canBuild(LocalPlayer player) {
checkNotNull(player);
return test(calculateState(DefaultFlag.BUILD, new RegionMemberTest(player), null));
return test(flagValueCalculator.testPermission(player, DefaultFlag.BUILD));
}
/**
@ -123,7 +120,7 @@ public boolean allows(StateFlag flag) {
throw new IllegalArgumentException("Can't use build flag with allows()");
}
return test(calculateState(flag, null, null));
return test(flagValueCalculator.queryState(null, flag));
}
/**
@ -140,7 +137,8 @@ public boolean allows(StateFlag flag, @Nullable LocalPlayer player) {
if (flag == DefaultFlag.BUILD) {
throw new IllegalArgumentException("Can't use build flag with allows()");
}
return test(calculateState(flag, null, player));
return test(flagValueCalculator.queryState(player, flag));
}
/**
@ -179,206 +177,6 @@ public boolean isMemberOfAll(LocalPlayer player) {
return true;
}
/**
* Calculate the effective value of a flag based on the regions
* in this set, membership, the global region (if set), and the default
* value of a flag {@link StateFlag#getDefault()}.
*
* @param flag the flag to check
* @param membershipTest null to perform a "wilderness check" or a predicate
* returns true if a the subject is a member of the
* region passed
* @param groupPlayer a player to use for the group flag check
* @return the allow/deny state for the flag
*/
private State calculateState(StateFlag flag, @Nullable Predicate<ProtectedRegion> membershipTest, @Nullable LocalPlayer groupPlayer) {
checkNotNull(flag);
// This method works in two modes:
//
// 1) Membership mode (if membershipTest != null):
// a) Regions in this set -> Check membership + Check region flags
// a) No regions -> Use global region + default value
// 1) Flag mode:
// a) Regions in this set -> Use global region + default value
// a) No regions -> Use global region + default value
int minimumPriority = Integer.MIN_VALUE;
boolean regionsThatCountExistHere = false; // We can't do a application.isEmpty() because
// PASSTHROUGH regions have to be skipped
// (in some cases)
State state = null; // Start with NONE
// 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
// player 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 player 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>();
for (ProtectedRegion region : applicable) {
// Don't consider lower priorities below minimumPriority
// (which starts at Integer.MIN_VALUE). A region that "counts"
// (has the flag set OR has members) will raise minimumPriority
// to its own priority.
if (region.getPriority() < minimumPriority) {
break;
}
// If PASSTHROUGH is set and we are checking to see if a player
// is a member, then skip this region
if (membershipTest != null && getStateFlagIncludingParents(region, DefaultFlag.PASSTHROUGH) == State.ALLOW) {
continue;
}
// If the flag has a group set on to it, skip this region if
// the group does not match our (group) player
if (groupPlayer != null && flag.getRegionGroupFlag() != null) {
RegionGroup group = region.getFlag(flag.getRegionGroupFlag());
if (group == null) {
group = flag.getRegionGroupFlag().getDefault();
}
if (!RegionGroupFlag.isMember(region, group, groupPlayer)) {
continue;
}
}
regionsThatCountExistHere = true;
State v = getStateFlagIncludingParents(region, flag);
// DENY overrides everything
if (v == State.DENY) {
state = State.DENY;
break; // No need to process any more regions
// ALLOW means we don't care about membership
} else if (v == State.ALLOW) {
state = State.ALLOW;
minimumPriority = region.getPriority();
} else {
if (membershipTest != null) {
minimumPriority = region.getPriority();
if (!hasCleared.contains(region)) {
if (!membershipTest.apply(region)) {
needsClear.add(region);
} else {
// Need to clear all parents
clearParents(needsClear, hasCleared, region);
}
}
}
}
}
if (membershipTest != null) {
State fallback;
if (regionsThatCountExistHere) {
fallback = allowOrNone(needsClear.isEmpty());
} else {
fallback = getDefault(flag, membershipTest);
}
return combine(state, fallback);
} else {
return combine(state, getDefault(flag, null));
}
}
@Nullable
private State getDefault(StateFlag flag, @Nullable Predicate<ProtectedRegion> membershipTest) {
boolean allowed = flag.getDefault() == State.ALLOW;
// Handle defaults
if (globalRegion != null) {
State globalState = globalRegion.getFlag(flag);
// The global region has this flag set
if (globalState != null) {
// Build flag is very special
if (membershipTest != null && globalRegion.hasMembersOrOwners()) {
allowed = membershipTest.apply(globalRegion) && (globalState == State.ALLOW);
} else {
allowed = (globalState == State.ALLOW);
}
} else {
// Build flag is very special
if (membershipTest != null && globalRegion.hasMembersOrOwners()) {
allowed = membershipTest.apply(globalRegion);
}
}
}
return allowed ? State.ALLOW : 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 clearParents(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();
}
}
/**
* Get a region's state flag, checking parent regions until a value for the
* flag can be found (if one even exists).
*
* @param region the region
* @param flag the flag
* @return the value
*/
private static State getStateFlagIncludingParents(ProtectedRegion region, StateFlag flag) {
while (region != null) {
State value = region.getFlag(flag);
if (value != null) {
return value;
}
region = region.getParent();
}
return null;
}
/**
* Gets the value of a flag. Do not use this for state flags
* (use {@link #allows(StateFlag, LocalPlayer)} for that).
@ -402,82 +200,7 @@ public <T extends Flag<V>, V> V getFlag(T flag) {
*/
@Nullable
public <T extends Flag<V>, V> V getFlag(T flag, @Nullable LocalPlayer groupPlayer) {
checkNotNull(flag);
/*
if (flag instanceof StateFlag) {
throw new IllegalArgumentException("Cannot use StateFlag with getFlag()");
}
*/
int lastPriority = 0;
boolean found = false;
Map<ProtectedRegion, V> needsClear = new HashMap<ProtectedRegion, V>();
Set<ProtectedRegion> hasCleared = new HashSet<ProtectedRegion>();
for (ProtectedRegion region : applicable) {
// Ignore lower priority regions
if (found && region.getPriority() < lastPriority) {
break;
}
// Check group permissions
if (groupPlayer != null && flag.getRegionGroupFlag() != null) {
RegionGroup group = region.getFlag(flag.getRegionGroupFlag());
if (group == null) {
group = flag.getRegionGroupFlag().getDefault();
}
if (!RegionGroupFlag.isMember(region, group, groupPlayer)) {
continue;
}
}
//noinspection StatementWithEmptyBody
if (hasCleared.contains(region)) {
// Already cleared, so do nothing
} else if (region.getFlag(flag) != null) {
clearParents(needsClear, hasCleared, region);
needsClear.put(region, region.getFlag(flag));
found = true;
}
lastPriority = region.getPriority();
}
if (!needsClear.isEmpty()) {
return needsClear.values().iterator().next();
} else {
if (globalRegion != null) {
V gFlag = globalRegion.getFlag(flag);
if (gFlag != null) return gFlag;
}
return null;
}
}
/**
* Clear a region's parents for getFlag().
*
* @param needsClear The regions that should be cleared
* @param hasCleared The regions already cleared
* @param region The region to start from
*/
private void clearParents(Map<ProtectedRegion, ?> needsClear,
Set<ProtectedRegion> hasCleared, ProtectedRegion region) {
ProtectedRegion parent = region.getParent();
while (parent != null) {
if (needsClear.remove(parent) == null) {
hasCleared.add(parent);
}
parent = parent.getParent();
}
return flagValueCalculator.queryValue(groupPlayer, flag);
}
/**

View File

@ -19,6 +19,8 @@
package com.sk89q.worldguard.protection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.sk89q.worldguard.LocalPlayer;
import com.sk89q.worldguard.protection.flags.DefaultFlag;
import com.sk89q.worldguard.protection.flags.Flag;
@ -50,23 +52,37 @@
*/
public class FlagValueCalculator {
private final SortedSet<ProtectedRegion> applicable;
private final SortedSet<ProtectedRegion> regions;
@Nullable
private final ProtectedRegion globalRegion;
/**
* Create a new instance.
*
* @param applicable a list of applicable regions
* @param regions a list of applicable regions
* @param globalRegion an optional global region (null to not use one)
*/
public FlagValueCalculator(SortedSet<ProtectedRegion> applicable, @Nullable ProtectedRegion globalRegion) {
checkNotNull(applicable);
public FlagValueCalculator(SortedSet<ProtectedRegion> regions, @Nullable ProtectedRegion globalRegion) {
checkNotNull(regions);
this.applicable = applicable;
this.regions = regions;
this.globalRegion = globalRegion;
}
/**
* Returns an iterable of regions sorted by priority (descending), with
* the global region tacked on at the end if one exists.
*
* @return an iterable
*/
private Iterable<ProtectedRegion> getApplicable() {
if (globalRegion != null) {
return Iterables.concat(regions, ImmutableList.of(globalRegion));
} else {
return regions;
}
}
/**
* Return the membership status of the given player, indicating
* whether there are no (counted) regions in the list of regions,
@ -119,12 +135,12 @@ public Result getMembership(LocalPlayer player) {
Set<ProtectedRegion> needsClear = new HashSet<ProtectedRegion>();
Set<ProtectedRegion> hasCleared = new HashSet<ProtectedRegion>();
for (ProtectedRegion region : applicable) {
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 OR has members) will raise minimumPriority
// to its own priority.
if (region.getPriority() < minimumPriority) {
if (getPriority(region) < minimumPriority) {
break;
}
@ -133,7 +149,7 @@ public Result getMembership(LocalPlayer player) {
continue;
}
minimumPriority = region.getPriority();
minimumPriority = getPriority(region);
foundApplicableRegion = true;
if (!hasCleared.contains(region)) {
@ -194,76 +210,27 @@ public State testPermission(LocalPlayer player, StateFlag... flags) {
switch (getMembership(player)) {
case SUCCESS:
return StateFlag.combine(getState(player, flags), State.ALLOW);
return StateFlag.combine(queryState(player, flags), State.ALLOW);
case FAIL:
return getState(player, flags);
return queryState(player, flags);
case NO_REGIONS:
if (globalRegion != null && globalRegion.hasMembersOrOwners()) {
if (globalRegion.isMember(player)) {
return StateFlag.combine(getState(player, flags), State.ALLOW);
} else {
State value = null;
for (StateFlag flag : flags) {
value = StateFlag.combine(value,globalRegion.getFlag(flag));
if (value == State.DENY) {
break;
}
}
return value;
}
}
default:
return getStateWithFallback(player, flags);
}
}
/**
* Get the effective value for a list of state flags. The rules of
* states is observed here; that is, {@code DENY} overrides {@code ALLOW},
* and {@code ALLOW} overrides {@code NONE}. This method will check
* the global region and {@link Flag#getDefault()} (in that order) if
* a value for the flag is not set in any region.
*
* <p>This method does <strong>not</strong> properly process build
* permissions. Instead, use {@link #testPermission(LocalPlayer, StateFlag...)}
* for that purpose. This method is ideal for testing non-build related
* state flags (although a rarity), an example of which would be whether
* to play a song to players that enter an area.</p>
*
* <p>A player 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
* player is not a member, then the region would be skipped when
* querying that flag. If {@code null} is provided for the player, then
* only flags that use {@link RegionGroup#ALL},
* {@link RegionGroup#NON_MEMBERS}, etc. will apply.</p>
*
* @param player an optional player, which would be used to determine the region group to apply
* @param flags a list of flags to check
* @return a state
*/
@Nullable
public State getStateWithFallback(@Nullable LocalPlayer player, StateFlag... flags) {
State value = null;
State fallback = null;
for (StateFlag flag : flags) {
value = StateFlag.combine(value, getSingleValueWithFallback(player, flag));
if (value == State.DENY) {
if (flag.getDefault()) {
fallback = State.ALLOW;
break;
}
}
return value;
return StateFlag.combine(queryState(player, flags), fallback);
}
}
/**
* Get the effective value for a list of state flags. The rules of
* states is observed here; that is, {@code DENY} overrides {@code ALLOW},
* and {@code ALLOW} overrides {@code NONE}. This method does not check
* the global region and ignores a flag's default value.
* and {@code ALLOW} overrides {@code NONE}.
*
* <p>This method does <strong>not</strong> properly process build
* permissions. Instead, use {@link #testPermission(LocalPlayer, StateFlag...)}
@ -284,11 +251,11 @@ public State getStateWithFallback(@Nullable LocalPlayer player, StateFlag... fla
* @return a state
*/
@Nullable
public State getState(@Nullable LocalPlayer player, StateFlag... flags) {
public State queryState(@Nullable LocalPlayer player, StateFlag... flags) {
State value = null;
for (StateFlag flag : flags) {
value = StateFlag.combine(value, getSingleValue(player, flag));
value = StateFlag.combine(value, queryValue(player, flag));
if (value == State.DENY) {
break;
}
@ -302,8 +269,7 @@ public State getState(@Nullable LocalPlayer player, StateFlag... flags) {
* (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 depend
* on the flag type. This method will check the global region
* for a value as well as the flag's default value.
* on the flag type.
*
* <p>Only some flag types actually have a strategy for picking the
* "best value." For most types, the actual value that is chosen to be
@ -326,65 +292,10 @@ public State getState(@Nullable LocalPlayer player, StateFlag... flags) {
* @param player an optional player, which would be used to determine the region group to apply
* @param flag the flag
* @return a value, which could be {@code null}
* @see #getSingleValue(LocalPlayer, Flag) does not check global region, defaults
*/
@Nullable
public <V> V getSingleValueWithFallback(@Nullable LocalPlayer player, Flag<V> flag) {
checkNotNull(flag);
V value = getSingleValue(player, flag);
if (value != null) {
return value;
}
// Get the value from the global region
if (globalRegion != null) {
value = globalRegion.getFlag(flag);
}
// Still no value? Check the default value for the flag
if (value == null) {
value = flag.getDefault();
}
return flag.validateDefaultValue(value);
}
/**
* Get the effective value for a flag. 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 depend
* on the flag type. This method never checks the global region or
* the flag's default value.
*
* <p>Only some flag types actually have a strategy for picking the
* "best value." For most types, the actual value that is chosen to be
* returned is undefined (it could be any value). As of writing, the only
* type of flag that can consistently return the same 'best' value is
* {@link StateFlag}.</p>
*
* <p>This method does <strong>not</strong> properly process build
* permissions. Instead, use {@link #testPermission(LocalPlayer, StateFlag...)}
* for that purpose.</p>
*
* <p>A player 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
* player is not a member, then the region would be skipped when
* querying that flag. If {@code null} is provided for the player, then
* only flags that use {@link RegionGroup#ALL},
* {@link RegionGroup#NON_MEMBERS}, etc. will apply.</p>
*
* @param player an optional player, which would be used to determine the region group to apply
* @param flag the flag
* @return a value, which could be {@code null}
* @see #getSingleValueWithFallback(LocalPlayer, Flag) checks global regions, defaults
*/
@Nullable
public <V> V getSingleValue(@Nullable LocalPlayer player, Flag<V> flag) {
Collection<V> values = getValues(player, flag);
public <V> V queryValue(@Nullable LocalPlayer player, Flag<V> flag) {
Collection<V> values = queryAllValues(player, flag);
return flag.chooseValue(values);
}
@ -405,7 +316,7 @@ public <V> V getSingleValue(@Nullable LocalPlayer player, Flag<V> flag) {
* only flags that use {@link RegionGroup#ALL},
* {@link RegionGroup#NON_MEMBERS}, etc. will apply.</p>
*/
public <V> Collection<V> getValues(@Nullable LocalPlayer player, Flag<V> flag) {
public <V> Collection<V> queryAllValues(@Nullable LocalPlayer player, Flag<V> flag) {
checkNotNull(flag);
int minimumPriority = Integer.MIN_VALUE;
@ -441,12 +352,12 @@ public <V> Collection<V> getValues(@Nullable LocalPlayer player, Flag<V> flag) {
Map<ProtectedRegion, V> consideredValues = new HashMap<ProtectedRegion, V>();
Set<ProtectedRegion> ignoredRegions = new HashSet<ProtectedRegion>();
for (ProtectedRegion region : applicable) {
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 (region.getPriority() < minimumPriority) {
if (getPriority(region) < minimumPriority) {
break;
}
@ -454,7 +365,7 @@ public <V> Collection<V> getValues(@Nullable LocalPlayer player, Flag<V> flag) {
if (value != null) {
if (!ignoredRegions.contains(region)) {
minimumPriority = region.getPriority();
minimumPriority = getPriority(region);
ignoreValuesOfParents(consideredValues, ignoredRegions, region);
consideredValues.put(region, value);
@ -471,6 +382,21 @@ public <V> Collection<V> getValues(@Nullable LocalPlayer player, Flag<V> flag) {
return consideredValues.values();
}
/**
* Get the effective priority of a region, overriding a region's priority
* when appropriate (i.e. with the global region).
*
* @param region the region
* @return the priority
*/
public int getPriority(final ProtectedRegion region) {
if (region == globalRegion) {
return Integer.MIN_VALUE;
} else {
return region.getPriority();
}
}
/**
* Get a region's state flag, checking parent regions until a value for the
* flag can be found (if one even exists).
@ -479,7 +405,19 @@ public <V> Collection<V> getValues(@Nullable LocalPlayer player, Flag<V> flag) {
* @param flag the flag
* @return the value
*/
@SuppressWarnings("unchecked")
public <V> V getEffectiveFlag(final ProtectedRegion region, Flag<V> flag, @Nullable LocalPlayer player) {
// The global region normally does not prevent building so
// PASSTHROUGH has to be ALLOW, except when people use the global
// region as a whitelist
if (region == globalRegion && flag == DefaultFlag.PASSTHROUGH) {
if (region.hasMembersOrOwners()) {
return null;
} else {
return (V) State.ALLOW;
}
}
ProtectedRegion current = region;
while (current != null) {

View File

@ -51,24 +51,6 @@ public String getName() {
return name;
}
/**
* Suppress the value of the flag that came from the global region, reducing
* its severity (i.e. DENY -> NONE).
*
* <p>This is really only used for the {@link StateFlag}.</p>
*
* @param current the value to suppress
* @return a new value
*/
public T validateDefaultValue(T current) {
return current;
}
@Nullable
public T getDefault() {
return null;
}
@Nullable
public T chooseValue(Collection<T> values) {
if (!values.isEmpty()) {

View File

@ -38,7 +38,6 @@ public RegionGroupFlag(String name, RegionGroup def) {
this.def = def;
}
@Override
public RegionGroup getDefault() {
return def;
}

View File

@ -48,14 +48,8 @@ public StateFlag(String name, boolean def) {
this.def = def;
}
@Override
public State getDefault() {
return def ? State.ALLOW : null;
}
@Override
public State validateDefaultValue(State current) {
return denyToNone(current);
public boolean getDefault() {
return def;
}
@Override