Recipes3 min read
A verified ranked queue
Admit only real, unmodified Teho clients, lock the mods down and keep players posted while they wait.
Ranked play only works if players trust that everyone else is playing fair. On most servers that trust comes from anticheat and hope. With Teho you can do better: ask the Teho backend to sign that a player is running a real, unmodified build, lock the client's mods down to what you allow, and keep players posted while they wait, all from your plugin.
This page builds the queue end to end. When you are done:
- Only players with a valid integrity proof can join the queue.
- While they wait, a single toast tells them their position and updates in place.
- When a match starts, competitive rules switch on and the 1.8 combat feel is set.
- Controller players are matched with each other, if they agree to share their input method.
Check the client before it joins
When a player asks to queue, request an integrity proof. It comes back as a future because the client has to ask the backend first, which usually takes well under a second.
public void join(Player player) {
Optional<TehoPlayer> found = BukkitTeho.player(player);
if (!found.isPresent()) {
player.sendMessage("Ranked needs Teho Client: tehoclient.com");
return;
}
TehoPlayer teho = found.get();
teho.verify().integrity().thenAccept(proof -> Bukkit.getScheduler().runTask(plugin, () -> {
if (!proof.valid()) {
player.sendMessage("Ranked needs an unmodified Teho build (" + proof.error() + ")");
return;
}
queue.add(player.getUniqueId());
announcePositions();
}));
}The future can complete off the main thread, which is why the result hops back with runTask before touching the queue.
Keep the queue in one toast
Instead of spamming chat, give every queued player one notification with a fixed id. Sending the same id again updates the toast that is already on screen.
private void announcePositions() {
int position = 1;
for (UUID id : queue) {
int place = position++;
TehoApi.get().player(id).ifPresent(teho -> teho.notify(Notification.builder("queue")
.title("Ranked queue")
.body(place == 1 ? "You're next" : "Position " + place + " of " + queue.size())
.accent(0xF472B6)
.duration(15_000)
.build()));
}
}Lock it down when the match starts
Competitive mode turns off every Teho module except the ones you allow. Players see a banner in their mods panel saying your server did it and why, so nobody thinks a mod is broken.
private void startMatch(TehoPlayer teho) {
teho.modules().competitive("Ranked match",
"fps_display", "keystrokes", "cps_counter", "ping", "armor_status");
teho.modules().cap(SettingCap.atMost("zoom", "factor", 4));
teho.modules().combat(CombatRules.LEGACY_PVP);
teho.notify(Notification.builder("queue")
.title("Match found")
.body("Competitive rules are on for this match")
.accent(0x7CDB8A)
.build());
}Reusing the queue id turns the waiting toast into the match-found toast instead of adding a second one.
If you also want to block third-party mods, ask the client which of a deny list it has. The player is asked once whether to share it:
teho.modules().denyMods("Ranked mod policy", "freecam", "xaerominimap").thenAccept(report -> {
if (report.shared() && !report.compliant()) {
Bukkit.getScheduler().runTask(plugin, () -> kickFromQueue(teho, report.violations()));
}
});Match controller players fairly
Controller and handheld players are at a disadvantage against mouse players. Client info lets you ask for the input method, which the player can decline, and pool them separately.
teho.info().request("Fair matchmaking", ClientInfo.Field.INPUT).thenAccept(info -> {
boolean controller = info.has(ClientInfo.Field.INPUT)
&& (info.input() == ClientInfo.Input.CONTROLLER || info.input() == ClientInfo.Input.HANDHELD);
Bukkit.getScheduler().runTask(plugin, () -> pools.assign(teho.uniqueId(), controller));
});If the player declines, has(INPUT) is false and they go in the default pool. Never refuse to queue someone for declining.
Hand back their client afterwards
Rules only apply on your server and lift when the player leaves, but if they stay in your lobby after the match, clear them yourself:
teho.modules().clear();
teho.modules().combat(CombatRules.VANILLA);What players experience
From the player's side, this is the whole flow: they type /ranked, see one toast that counts down their position, get a "Match found" in the same toast, and the mods panel shows exactly which mods are off for this match and why. When they go back to the lobby, everything is back to how they had it.