Archived
1
0
Fork 0
This repository has been archived on 2026-01-19. You can view files and clone it, but cannot push or open issues or pull requests.
Danmaku/server/main.go

247 lines
7.9 KiB
Go

package main
/*
#cgo CFLAGS: -I ${SRCDIR}/lib
#cgo LDFLAGS: -L ${SRCDIR}/lib -l shared
#include "shared.h"
#include <stdlib.h>
*/
import "C"
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"math/rand"
"slices"
"github.com/heroiclabs/nakama-common/runtime"
)
const (
MATCH_LOADING = iota
MATCH_START
SPAWN_BULLET
MATCH_END
)
// Interface for registering match handlers
type BattleRoyaleMatch struct{}
type PlayerStageState struct {
xPos float64
yPos float64
bullets []*C.Bullet
}
type PresenceState struct { // present time! hahahahahahahah!
presence runtime.Presence
stageState PlayerStageState
}
// In-memory game state
type BattleRoyaleMatchState struct {
tickRate int
currentMatchPhase int
presences map[string]*PresenceState
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) {
tickRate := 20 // MatchLoop invocations per second
state := &BattleRoyaleMatchState{
tickRate: tickRate,
presences: map[string]*PresenceState{},
emptyTicks: 0,
currentMatchPhase: MATCH_LOADING,
}
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()] = &PresenceState{
presence: presences[i],
stageState: PlayerStageState{
xPos: STAGE_WIDTH * 0.5,
yPos: STAGE_HEIGHT - STAGE_HEIGHT*0.1,
bullets: []*C.Bullet{},
},
}
}
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
}
// Test bullet spawning
if tick%20 == 0 {
for _, v := range lobbyState.presences {
vel := rand.Float64()*(STAGE_WIDTH/float64(lobbyState.tickRate)) + 1.0
bullet := C.new_bullet(
C.uint8_t(BULLET_LINEAR),
C.int64_t(tick),
C.double(STAGE_WIDTH*rand.Float64()),
C.double(STAGE_HEIGHT*rand.Float64()),
C.double(vel),
C.double(vel),
)
var x, y C.double
C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y)
bulletData := map[string]interface{}{
"class": BULLET_LINEAR,
"tick": tick,
"x": float64(x),
"y": float64(y),
"vel_x": vel,
"vel_y": vel,
}
data, err := json.Marshal(bulletData)
if err != nil {
logger.Error("Error marshalling bullet data", err)
} else {
v.stageState.bullets = append(v.stageState.bullets, bullet)
reliable := true
dispatcher.BroadcastMessage(SPAWN_BULLET, data, nil, nil, reliable)
}
}
}
// Bullet cleanup
for _, v := range lobbyState.presences {
for _, bullet := range v.stageState.bullets {
var x, y C.double
C.bullet_get_current_pos(bullet, C.int64_t(tick), &x, &y)
fmt.Printf("Bullet at (%.2f, %.2f)\n", float64(x), float64(y))
}
v.stageState.bullets = slices.DeleteFunc(v.stageState.bullets, func(b *C.Bullet) bool {
if C.bullet_beyond_kill_boundary(b, C.int64_t(tick)) {
C.destroy_bullet(b)
return true
}
return false
})
}
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), &params); 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
}