package main import ( "context" "database/sql" "github.com/heroiclabs/nakama-common/runtime" ) // Interface for registering match handlers type BattleRoyaleMatch struct{} // In-memory game state type BattleRoyaleMatchState struct { presences map[string]runtime.Presence emptyTicks int } // Run on match start, initializes game state and sets tick rate func (m *BattleRoyaleMatch) MatchInit(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, params map[string]interface{}) (interface{}, int, string) { state := &BattleRoyaleMatchState{ presences: map[string]runtime.Presence{}, emptyTicks: 0, } tickRate := 1 // MatchLoop invocations per second label := "" return state, tickRate, label } // Run when a user attempts to join or rejoin a match. Responsible for deciding whether or not to let them in. func (m *BattleRoyaleMatch) MatchJoinAttempt(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presence runtime.Presence, metadata map[string]string) (interface{}, bool, string) { lobbyState, ok := state.(*BattleRoyaleMatchState) if !ok { logger.Error("State is not a valid lobby state object for MatchJoin.") return nil, false, "Failed to join match: match does not exist." } accepted := true rejectedMessage := "" return lobbyState, accepted, rejectedMessage } // Run when a user successfully joins a match, registers their presence in the game state func (m *BattleRoyaleMatch) MatchJoin(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} { lobbyState, ok := state.(*BattleRoyaleMatchState) if !ok { logger.Error("State is not a valid lobby state object for MatchJoin.") return nil } for i := 0; i < len(presences); i++ { lobbyState.presences[presences[i].GetSessionId()] = presences[i] } return lobbyState } // Run when a user successfully leaves a match, de-registers their presence in the game state func (m *BattleRoyaleMatch) MatchLeave(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, presences []runtime.Presence) interface{} { lobbyState, ok := state.(*BattleRoyaleMatchState) if !ok { logger.Error("State is not a valid lobby state object for MatchLeave.") return nil } for i := 0; i < len(presences); i++ { delete(lobbyState.presences, presences[i].GetSessionId()) } return lobbyState } // Run when a match gets an arbitrary signal from the Nakama runtime (probably from the matchmaker/match lister APIs) func (m *BattleRoyaleMatch) MatchSignal(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, data string) (interface{}, string) { lobbyState, ok := state.(*BattleRoyaleMatchState) if !ok { logger.Error("State is not a valid lobby state object for MatchSignal.") return nil, "Failed to get valid state for return signal" } returnMessage := "" return lobbyState, returnMessage } // Run when the server enters the graceful shutdown flow. Gives the match a chance to shutdown cleanly within graceSeconds. func (m *BattleRoyaleMatch) MatchTerminate(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, graceSeconds int) interface{} { lobbyState, ok := state.(*BattleRoyaleMatchState) if !ok { logger.Error("State is not a valid lobby state object for MatchTerminate.") return nil } return lobbyState } // Main game loop, executed at tickRate per second specified in MatchInit func (m *BattleRoyaleMatch) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, messages []runtime.MatchData) interface{} { lobbyState, ok := state.(*BattleRoyaleMatchState) if !ok { logger.Error("State is not a valid lobby state object for MatchLoop.") return nil } // If we have no presences in the match according to the match state, increment the empty ticks count if len(lobbyState.presences) == 0 { lobbyState.emptyTicks++ } // If the match has been empty for more than 100 ticks, end the match by returning nil if lobbyState.emptyTicks > 100 { return nil } return lobbyState } // RPC for force-creating a match for debugging/development, separate from the matchmaking process func ManualForceCreateBRMatchRPC(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { /*params := make(map[string]interface{}) if err := json.Unmarshal([]byte(payload), ¶ms); err != nil { return "", err }*/ modulename := "battle-royale" if matchId, err := nk.MatchCreate(ctx, modulename, make(map[string]interface{})); err != nil { return "", err } else { return matchId, nil } } // main function for hooking into the nakama runtime, responsible for setting up all handlers func InitModule(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, initializer runtime.Initializer) error { // Register handlers for match lifecycle if err := initializer.RegisterMatch("battle-royale", func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule) (runtime.Match, error) { return &BattleRoyaleMatch{}, nil }); err != nil { logger.Error("Unable to register match handler: %v", nil) return err } // Register RPCs if err := initializer.RegisterRpc("manual_force_create_br_match_rpc", ManualForceCreateBRMatchRPC); err != nil { logger.Error("Unable to register: %v", err) return err } return nil }