Worked examples
Four complete patterns: safe startup, a /venus subcommand, a violation feed, and a session report.
Every example assumes compileOnly on the API artifact from the overview, and depend: [VenusLoader] in plugin.yml.
1. Safe startup
Venus loads asynchronously relative to your plugin. The async provider form cannot throw and cannot race — use it as the only entry point you call from onEnable.
public final class VenusBridge {
private static VenusApi api;
public static void init(JavaPlugin plugin) {
VenusAPIProvider.getAsync().thenAccept(loaded -> {
api = loaded;
plugin.getLogger().info("Venus " + api.version() + " hooked");
loaded.events().repository().register(UserJoinEvent.class, event ->
plugin.getLogger().info(event.user().username() + " joined"));
});
}
public static Optional<VenusApi> get() {
return Optional.ofNullable(api);
}
}2. A subcommand under /venus
Register a label, implement execute and tab completion. The dispatcher handles fallback permissions and errors; arity is your job.
VenusBridge.init(this);
VenusAPIProvider.getAsync().thenAccept(api ->
api.subcommands().register(new Subcommand("watch", false, "venus.alerts") {
@Override
public void execute(Player player, String label, String[] args) {
if (args.length != 1) {
player.sendMessage("Usage: /venus watch <player>");
return;
}
api.users().queryUserByName(args[0]).ifPresentOrElse(target -> {
api.alerts().setAlertsEnabled(target, true, false);
player.sendMessage("Now watching " + target.username());
}, () -> player.sendMessage("Venus is not tracking " + args[0]));
}
@Override
public List<String> onTabComplete(Player player, String[] args) {
if (args.length != 1) return List.of();
String prefix = args[0].toLowerCase(Locale.ROOT);
return Bukkit.getOnlinePlayers().stream()
.map(Player::getName)
.filter(name -> name.toLowerCase(Locale.ROOT).startsWith(prefix))
.toList();
}
}));3. A violation feed with filtering
Listeners run on the thread that observed the user. Keep them short; cancel events you do not want counted.
api.events().repository().register(ViolationEvent.class, event -> {
User user = event.user();
// Suppress an experimental check entirely for review players:
if (reviewRoster.contains(user.uuid()) && event.check().stableKey().startsWith("experimental.")) {
event.setCancelled(true);
return;
}
feed.submit(user.username(), event.check().name(), event.vl());
}, EventPriority.NORMAL, false);For alert text instead of raw flags, listen to DetectionAlertEvent; for the punishment branch, PunishmentEvent. The full hierarchy is on event classes.
4. Session report
History is asynchronous — chain the futures rather than blocking. This produces the same numbers a staff history view would show.
public void report(VenusApi api, User user, Consumer<String> out) {
api.history().sessions(user, 1).thenAccept(mostRecent -> {
if (mostRecent.isEmpty()) {
out.accept("No recorded sessions.");
return;
}
HistoryService.Session session = mostRecent.get(0);
out.accept("Session #" + session.ordinal()
+ " · " + session.durationMs() / 1000 + "s"
+ " · " + session.violationCount() + " flags across "
+ session.uniqueCheckCount() + " checks");
api.history().session(user, session.sessionId()).thenAccept(detail ->
detail.ifPresent(d -> d.violations().forEach(v ->
out.accept(" +" + v.offsetMs() + "ms " + v.stableKey()
+ " vl=" + String.format("%.1f", v.vl()))))));
});
}Threading rules, in one box
- Register listeners and read services from any thread.
- User events arrive on that user's Netty thread; cloud events on the cloud thread.
- Hop to the main thread yourself before touching Bukkit objects.
- Never block a Netty thread waiting on a
history()future — chain it orthenAcceptAsync(..., Bukkit.getScheduler()::runTask).