Skip to content

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.

@FunctionalInterface
public 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.

Reveal holds the ready-made rules.

FactoryReveals 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.

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.

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.

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.