Reveal rules
By default everyone inside a zone is hidden from everyone else. A visibility rule carves out an exception: pairs of players who see each other as themselves.
The interface
Section titled “The interface”@FunctionalInterfacepublic interface VisibilityRule { boolean reveals(Player viewer, Player target, RuleContext context);}One question, one answer: does viewer see target as themselves? Returning false leaves the
disguise in place.
The factories
Section titled “The factories”Reveal holds the ready-made rules.
| Factory | Reveals a target when |
|---|---|
Reveal.sameGroup() | Viewer and target resolve to the same group. |
Reveal.group(String) | The target belongs to that group. Revealed to everyone. |
Reveal.group(Supplier<String>) | Same, with the group read at evaluation time. |
Reveal.groupsTogether(Supplier<Collection<String>>) | Viewer and target both belong to the set of groups. |
Reveal.permission(String) | The viewer holds that permission node. |
Reveal.target(Predicate<Player>) | The target matches, whoever is looking. |
Reveal.of(BiPredicate<Player, Player>) | The viewer and target pair matches. |
Reveal.never() | Never. Everyone stays hidden. |
The two group overloads differ in when the id is read. Pass a String for a group known once and for
all; pass a Supplier when it changes during the run, such as the faction currently holding a KoTH.
groupsTogether is symmetric where group is not: group reveals its members to the whole zone,
while groupsTogether reveals the listed groups to each other and to nobody else.
Composing
Section titled “Composing”Rules combine with or, and and negate:
VisibilityRule rule = Reveal.sameGroup() .or(Reveal.permission("myserver.staff"));Passing several rules to reveal(...) is a different thing: they are tried in order, and the first
one that matches decides. Compose with or when you want a single rule, and pass a list when the
rules should be tried in sequence.
Writing your own
Section titled “Writing your own”Anything the factories do not cover is a lambda:
.reveal(Reveal.target(player -> player.getGameMode() == GameMode.CREATIVE))Prefer Reveal.target when the answer depends only on the target, and keep Reveal.of for when it
genuinely depends on both players. The difference is not cosmetic, as the next section explains.
A rule that throws is skipped, and SomaHider logs the failure once per rule class rather than on every pass.
Keeping rules cheap
Section titled “Keeping rules cheap”A rule runs for every viewer and target pair, several times a second. On a busy zone that is thousands of calls per second, on the main thread. Two mechanisms keep that affordable, and both depend on how you write your rules.
The first is grouping. When every rule in a zone can be decided from group membership alone, SomaHider
evaluates the zone once per group instead of once per viewer. Every factory above works this way
except permission and of, along with any composition of them. There is nothing to configure: it is
read off the rules themselves. A single Reveal.of(...) in the list turns it off for the whole zone,
which is why Reveal.target is worth preferring when it fits.
The second is RuleContext.once:
public interface RuleContext { String groupOf(Player player); boolean hasPermission(Player viewer, String node); <T> T once(Supplier<T> supplier);}once evaluates a supplier at most once per pass and hands the same value to every pair in that pass.
Use it whenever a rule reads something costly that depends on neither the viewer nor the target, such
as a live standings table.
private final Supplier<String> currentOwner = () -> koth.getOwner().getId();
VisibilityRule rule = (viewer, target, context) -> context.once(currentOwner).equals(context.groupOf(target));The cache is keyed on the supplier instance, so hold on to it in a field. Building a new lambda on
each call gives a new key every time and caches nothing. The Supplier factories in Reveal already
do this for you.