package main import ( "context" "database/sql" "encoding/json" "github.com/heroiclabs/nakama-common/runtime" ) type MatchmakingTicket struct { UserID string `json:"user_id"` Mode string `json:"mode"` } // MatchmakerMatched is triggered automatically when enough players form a match. func MatchmakerMatched( ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry, ) (string, error) { if len(entries) != 2 { logger.Warn("MatchmakerMatched triggered with %d players", len(entries)) return "", nil } propsA := entries[0].GetProperties() propsB := entries[1].GetProperties() validModes := map[string]bool{"classic": true, "blitz": true} modeA, okA := propsA["mode"].(string) modeB, okB := propsB["mode"].(string) if !okA || !okB || !validModes[modeA] || !validModes[modeB] { logger.Warn("MatchmakerMatched missing mode property — ignoring") return "", nil } // ✅ If modes don’t match, let Nakama find another pairing if modeA != modeB { logger.Warn("Mode mismatch %s vs %s — retrying matchmaking", modeA, modeB) return "", nil } // ✅ Create authoritative match matchParams := map[string]interface{}{ "mode": modeA, } matchID, err := nk.MatchCreate(ctx, "tictactoe", matchParams) if err != nil { logger.Error("MatchCreate failed: %v", err) return "", runtime.NewError("failed to create match", 13) } logger.Info("✅ Match created %s — mode=%s", matchID, modeA) return matchID, nil } // RPC to leave matchmaking queue func rpcLeaveMatchmaking( ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string, ) (string, error) { var input struct { Ticket string `json:"ticket"` } if err := json.Unmarshal([]byte(payload), &input); err != nil { return "", runtime.NewError("invalid JSON", 3) } if input.Ticket == "" { return "", runtime.NewError("missing ticket", 3) } logger.Info("✅ Matchmaking ticket removed: %s", input.Ticket) return "{}", nil }