# A verified ranked queue Source: https://tehoclient.dev/recipes/ranked-queue SDK: 1.0.0 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. **Why a proof, not the handshake** Any client can claim to be Teho in its handshake, so the handshake is only good for UX. An integrity proof is signed by the Teho backend with Ed25519 and checked by the SDK, so it cannot be forged by a modified client. See [Verified clients](https://tehoclient.dev/modules/verify) for how it works. ### 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. ```java public void join(Player player) { Optional 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. ```java 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. ```java 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: ```java 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. ```java 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: ```java 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. - [How proofs work](https://tehoclient.dev/modules/verify) — Signing, nonces and offline verification. - [Every rule you can set](https://tehoclient.dev/modules/rules) — Module ids, categories and setting caps. - [Back it up with staff tools](https://tehoclient.dev/recipes/staff-setup) — X-Ray, ESP and anticheat alerts for moderators.