Namazu Crossfire ships two built-in matchmaking algorithms — a FIFO queue and a simple join-code flow — but both matching strategies are pluggable. This page walks through how the built-in algorithms actually work end-to-end, and what you get for free when you write your own (ratings-based matching, party/lobby systems, region-aware pools, and so on).
This assumes familiarity with the Crossfire Protocol Reference, in particular the four handshake flows (FIND/JOIN vs. CREATE/JOIN_CODE).
Two algorithm interfaces #
There are two Distinct extension points, corresponding to the two handshake flow pairs — they are not variants of each other, and a given implementation only serves one:
FindMatchmakingAlgorithm— servesFIND(initialize) andJOIN(resume). The built-in implementation isFIFOMatchmakingAlgorithm.JoinCodeMatchmakingAlgorithm— servesCREATE(initialize) andJOIN_CODE(resume). The built-in implementation isSimpleJoinCodeMatchmakingAlgorithm.
Both extend the common MatchmakingAlgorithm<CreateT, ResumeT> contract:
initialize(request)— called for the “first contact” request (FINDorCREATE). Must be non-blocking and return aMatchHandleimmediately.resume(request)— called for a “second contact” request (JOINorJOIN_CODE). Also non-blocking, also returns aMatchHandle.
The MatchPhase lifecycle #
Every MatchHandle tracks its own phase, independent of the connection’s ConnectionPhase:
READY → MATCHING → MATCHED → TERMINATED
TERMINATED is absorbing — every transition first checks for it and no-ops if already there, so a handle can never come back to life once the player has disconnected or left. Transitions are otherwise strict and enforced by the state record itself: startMatching() requires READY, and reporting a result requires MATCHING; calling either out of order throws ProtocolStateException rather than silently doing nothing.
What AbstractMatchHandle gives you #
AbstractMatchHandle<RequestT> (in the util module) is the base class every concrete MatchHandle should extend. It owns the MatchPhase state machine in an AtomicReference and turns each public MatchHandle method into a phase-checked dispatch to one of six abstract hooks you implement:
onMatching(state)— do the actual work of finding or creating a match. This is the only hook every algorithm must supply itself; there’s no generic implementation of “how do I match.”onResult(state, result)— called once you invokesetResult(...)from withinonMatching, completing the handshake.onLeaveMatch(state)— the player is leaving a match they were already matched into.onOpenMatch(state)/onCloseMatch(state)/onEndMatch(state)— back theOPEN/CLOSE/ENDcontrol messages. These don’t changeMatchPhasethemselves — a match can be opened and closed repeatedly whileMATCHED.
Two of these dispatches are worth studying closely, because they demonstrate the updateAndGet vs. getAndUpdate distinction called out in the protocol reference:
startMatching()usesupdateAndGet— it needs the resulting phase to decide whether to actually kick offonMatching, or just log if the handle was already terminated (e.g. the player disconnected before matching started).leaveMatch()usesgetAndUpdate— it needs the phase before the termination transition overwrote it, because the cleanup differs: aMATCHEDhandle has a real match/result to release (onLeaveMatch), while a still-MATCHINGhandle has nothing to release yet (falls through to a log-only default).
Inside onMatching, call the protected setResult(MultiMatch) once you’ve found or created the match — this is what drives the MATCHING → MATCHED transition and ultimately calls getRequest().success(this), which is what actually sends the handshake response and moves the connection into SIGNALING.
Skipping the boilerplate with StandardCancelableMatchHandle #
StandardCancelableMatchHandle<RequestT> (also in util) implements every hook except onMatching in terms of the standard MultiMatchDao operations, so most custom algorithms only need to extend it and write onMatching:
onEndMatch→DAO.endMatch(matchId)onCloseMatch→DAO.closeMatch(matchId)onOpenMatch→ re-fetches the match, thenDAO.openMatch(...)onLeaveMatch→DAO.removeProfile(matchId, Profile), and deletes the match if that was the last ProfileonResult→getRequest().success(this)
Every DAO call in these hooks (other than onOpenMatch) is dispatched via getRequest().getServer().submit(...) so it runs off the calling thread — your onMatching override should follow the same pattern rather than blocking on database access directly.
Walkthrough: FIFOMatchmakingAlgorithm #
FIFOMatchmakingAlgorithm is the reference implementation to model a new FindMatchmakingAlgorithm on:
initialize(request)returns a private innerFIFOMatchHandle, extendingStandardCancelableMatchHandle<FindHandshakeRequest>.resume(request)returns a plainStandardJoinMatchHandle— a generic, protocol-level handle (not FIFO-specific) that re-looks-up the existing match by id and verifies the requesting Profile is a member. Reconnection logic doesn’t depend on how the match was originally formed, so anyFindMatchmakingAlgorithmcan reuse this class as-is for itsresume().FIFOMatchHandle.onMatchingsubmits work to the server executor: opens a transaction, callsMultiMatchDao.findOldestAvailableMultiMatchCandidate(configuration, profileId, ""); if nothing is available it creates a brand-newOPENMultiMatch; either way it adds the requesting Profile and callssetResult(...).
A ratings-based or region-aware algorithm would follow the identical shape — swap step 3’s DAO query for your own candidate-selection logic (e.g. querying a rating Service and filtering candidates by rating window before falling back to creating a new match).
Walkthrough: SimpleJoinCodeMatchmakingAlgorithm #
The create/join-code flow differs from FIFO in a few instructive ways:
- It exposes two deployment-configurable Element attributes —
JOIN_CODE_LENGTH(default4) andMAX_ATTEMPTS(default2000) — rather than hardcoding join-code generation parameters. initialize()‘s handle creates the match via a join-code-aware DAO overload,DAO.createMultiMatch(match, parameters), whereparameters(aUniqueCodeDao.GenerationParameters) carries the code length/attempt budget along with the app config’s timeout/linger seconds.- It overrides
newHandshakeResponse()on its handle to return aCreatedHandshakeResponse(populated withmatchId,joinCode,profileId) instead of the defaultMatchedResponse. This is the pattern to copy any time your algorithm needs to hand extra data back to the client in the handshake response —MatchHandle‘s defaultnewHandshakeResponse()only returns a bareMatchedResponse. resume()(servingJOIN_CODE) usesStandardJoinCodeMatchHandle, whoseonMatchinglooks the match up by join code (DAO.getMultiMatchByJoinCode(...)) and adds the joining Profile if it isn’t already a member — unlike FIFO, where Profile-adding only happens ininitialize(), here it can also happen duringresume()since that’s when a second player actually shows up.
Registering your algorithm #
Export your implementation with @ElementServiceExport and bind it in your Element’s Guice module. To make an algorithm selectable by name from a MatchmakingApplicationConfiguration.matchmaker reference (rather than only usable as your Element’s default), bind it twice — once unqualified and once under a @Named annotation — following the exact pattern Crossfire itself uses for FIFO and simple join-code:
@ElementServiceExport(value = FindMatchmakingAlgorithm.class)
@ElementServiceExport(value = FindMatchmakingAlgorithm.class, name = "RATING_WINDOW")
public class RatingWindowMatchmakingAlgorithm implements FindMatchmakingAlgorithm { /* ... */ }
bind(FindMatchmakingAlgorithm.class).to(RatingWindowMatchmakingAlgorithm.class);
bind(FindMatchmakingAlgorithm.class)
.annotatedWith(named("RATING_WINDOW"))
.to(RatingWindowMatchmakingAlgorithm.class);
expose(FindMatchmakingAlgorithm.class);
expose(FindMatchmakingAlgorithm.class).annotatedWith(named("RATING_WINDOW"));
The unqualified binding becomes the algorithm used whenever a MatchmakingApplicationConfiguration doesn’t specify a matchmaker; the named binding is what V1HandshakeHandler.algorithmFromConfiguration() resolves when a configuration explicitly references your algorithm by its ElementServiceReference name — see Custom Elements for background on cross-Element Service resolution.

