Files
a301_game_server/internal/game/game_server.go
2026-02-26 17:52:48 +09:00

463 lines
11 KiB
Go

package game
import (
"context"
"sync"
"sync/atomic"
"time"
"a301_game_server/config"
"a301_game_server/internal/ai"
"a301_game_server/internal/auth"
"a301_game_server/internal/db"
"a301_game_server/internal/db/repository"
"a301_game_server/internal/network"
"a301_game_server/internal/player"
"a301_game_server/internal/world"
"a301_game_server/pkg/logger"
"a301_game_server/pkg/mathutil"
pb "a301_game_server/proto/gen/pb"
)
const defaultZoneID uint32 = 1
// GameServer is the top-level orchestrator that connects networking with game logic.
type GameServer struct {
cfg *config.Config
world *World
sessions *player.SessionManager
dbPool *db.Pool
authSvc *auth.Service
charRepo *repository.CharacterRepo
mu sync.RWMutex
connPlayer map[uint64]*player.Player // connID -> player
playerConn map[uint64]uint64 // playerID -> connID
nextPlayerID atomic.Uint64
cancelSave context.CancelFunc
}
// NewGameServer creates the game server.
func NewGameServer(cfg *config.Config, dbPool *db.Pool) *GameServer {
gs := &GameServer{
cfg: cfg,
world: NewWorld(cfg),
sessions: player.NewSessionManager(),
dbPool: dbPool,
authSvc: auth.NewService(dbPool),
charRepo: repository.NewCharacterRepo(dbPool),
connPlayer: make(map[uint64]*player.Player),
playerConn: make(map[uint64]uint64),
}
// Create zones, portals, and mobs.
gs.setupWorld()
return gs
}
// World returns the game world.
func (gs *GameServer) World() *World { return gs.world }
// Start launches all zone game loops and periodic save.
func (gs *GameServer) Start() {
gs.world.mu.RLock()
for _, zone := range gs.world.zones {
zone.SetMessageHandler(gs)
zone.SetZoneTransferCallback(gs.handleZoneTransfer)
}
gs.world.mu.RUnlock()
gs.world.StartAll()
// Start periodic character save.
ctx, cancel := context.WithCancel(context.Background())
gs.cancelSave = cancel
go gs.periodicSave(ctx)
}
// Stop shuts down all zone game loops and saves all players.
func (gs *GameServer) Stop() {
if gs.cancelSave != nil {
gs.cancelSave()
}
// Final save of all online players.
gs.saveAllPlayers()
gs.world.StopAll()
}
// OnPacket handles incoming packets from a connection.
func (gs *GameServer) OnPacket(conn *network.Connection, pkt *network.Packet) {
switch pkt.Type {
case network.MsgLoginRequest:
gs.handleLogin(conn, pkt)
case network.MsgEnterWorldRequest:
gs.handleEnterWorld(conn, pkt)
default:
gs.mu.RLock()
p, ok := gs.connPlayer[conn.ID()]
gs.mu.RUnlock()
if !ok {
return
}
zone, err := gs.world.GetZone(p.ZoneID())
if err != nil {
return
}
zone.EnqueueMessage(PlayerMessage{PlayerID: p.EntityID(), Packet: pkt})
}
}
// OnDisconnect handles a connection closing.
func (gs *GameServer) OnDisconnect(conn *network.Connection) {
gs.mu.Lock()
p, ok := gs.connPlayer[conn.ID()]
if !ok {
gs.mu.Unlock()
return
}
delete(gs.connPlayer, conn.ID())
delete(gs.playerConn, p.EntityID())
gs.mu.Unlock()
// Save character to DB on disconnect.
if p.CharID() != 0 {
if err := gs.charRepo.Save(context.Background(), p.ToCharacterData()); err != nil {
logger.Error("failed to save player on disconnect", "playerID", p.EntityID(), "error", err)
}
}
zone, err := gs.world.GetZone(p.ZoneID())
if err == nil {
zone.EnqueueMessage(PlayerMessage{
PlayerID: p.EntityID(),
Packet: &network.Packet{Type: msgPlayerDisconnect},
})
}
logger.Info("player disconnected", "connID", conn.ID(), "playerID", p.EntityID())
}
// Internal message types.
const (
msgPlayerDisconnect uint16 = 0xFFFF
msgPlayerEnterWorld uint16 = 0xFFFE
)
// HandleZoneMessage implements ZoneMessageHandler.
func (gs *GameServer) HandleZoneMessage(zone *Zone, msg PlayerMessage) bool {
switch msg.Packet.Type {
case msgPlayerDisconnect:
zone.RemovePlayer(msg.PlayerID)
return true
case msgPlayerEnterWorld:
gs.mu.RLock()
var found *player.Player
for _, p := range gs.connPlayer {
if p.EntityID() == msg.PlayerID {
found = p
break
}
}
gs.mu.RUnlock()
if found != nil {
zone.AddPlayer(found)
}
return true
default:
return false
}
}
func (gs *GameServer) handleLogin(conn *network.Connection, pkt *network.Packet) {
req := pkt.Payload.(*pb.LoginRequest)
ctx := context.Background()
// Try login first.
accountID, err := gs.authSvc.Login(ctx, req.Username, req.Password)
if err != nil {
// Auto-register if account doesn't exist.
accountID, err = gs.authSvc.Register(ctx, req.Username, req.Password)
if err != nil {
conn.Send(network.MsgLoginResponse, &pb.LoginResponse{
Success: false,
ErrorMessage: "login failed: " + err.Error(),
})
return
}
// Create default character on first registration.
_, charErr := gs.charRepo.Create(ctx, accountID, req.Username)
if charErr != nil {
logger.Error("failed to create default character", "accountID", accountID, "error", charErr)
}
}
session := gs.sessions.Create(uint64(accountID), req.Username)
conn.Send(network.MsgLoginResponse, &pb.LoginResponse{
Success: true,
SessionToken: session.Token,
PlayerId: uint64(accountID),
})
logger.Info("player logged in", "username", req.Username, "accountID", accountID)
}
func (gs *GameServer) handleEnterWorld(conn *network.Connection, pkt *network.Packet) {
req := pkt.Payload.(*pb.EnterWorldRequest)
session := gs.sessions.Get(req.SessionToken)
if session == nil {
conn.Send(network.MsgEnterWorldResponse, &pb.EnterWorldResponse{
Success: false,
ErrorMessage: "invalid session",
})
return
}
ctx := context.Background()
// Load character from DB.
chars, err := gs.charRepo.GetByAccountID(ctx, int64(session.PlayerID))
if err != nil || len(chars) == 0 {
conn.Send(network.MsgEnterWorldResponse, &pb.EnterWorldResponse{
Success: false,
ErrorMessage: "no character found",
})
return
}
charData := chars[0] // Use first character for now.
p := player.NewPlayerFromDB(charData, conn)
// Register connection-player mapping.
gs.mu.Lock()
gs.connPlayer[conn.ID()] = p
gs.playerConn[p.EntityID()] = conn.ID()
gs.mu.Unlock()
zoneID := p.ZoneID()
zone, err := gs.world.GetZone(zoneID)
if err != nil {
// Fall back to default zone.
zoneID = defaultZoneID
p.SetZoneID(defaultZoneID)
zone, _ = gs.world.GetZone(defaultZoneID)
}
zone.EnqueueMessage(PlayerMessage{
PlayerID: p.EntityID(),
Packet: &network.Packet{Type: msgPlayerEnterWorld},
})
conn.Send(network.MsgEnterWorldResponse, &pb.EnterWorldResponse{
Success: true,
Self: p.ToProto(),
ZoneId: zoneID,
})
logger.Info("player entered world", "playerID", p.EntityID(), "charID", charData.ID, "zone", zoneID)
}
// periodicSave saves all dirty player data to DB at configured intervals.
func (gs *GameServer) periodicSave(ctx context.Context) {
ticker := time.NewTicker(gs.cfg.Database.SaveInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
gs.saveAllPlayers()
case <-ctx.Done():
return
}
}
}
func (gs *GameServer) saveAllPlayers() {
gs.mu.RLock()
var dirty []*player.Player
for _, p := range gs.connPlayer {
if p.IsDirty() && p.CharID() != 0 {
dirty = append(dirty, p)
}
}
gs.mu.RUnlock()
if len(dirty) == 0 {
return
}
chars := make([]*repository.CharacterData, 0, len(dirty))
for _, p := range dirty {
chars = append(chars, p.ToCharacterData())
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := gs.charRepo.SaveBatch(ctx, chars); err != nil {
logger.Error("periodic save failed", "count", len(chars), "error", err)
return
}
for _, p := range dirty {
p.ClearDirty()
}
logger.Debug("periodic save completed", "count", len(chars))
}
// setupWorld creates all zones, portals, and mob spawn points.
func (gs *GameServer) setupWorld() {
zone1 := gs.world.CreateZone(1) // Starting zone - plains
zone2 := gs.world.CreateZone(2) // Forest zone - medium difficulty
zone3 := gs.world.CreateZone(3) // Volcano zone - hard
// Set spawn positions.
zone1.spawnPos = mathutil.NewVec3(0, 0, 0)
zone2.spawnPos = mathutil.NewVec3(5, 0, 5)
zone3.spawnPos = mathutil.NewVec3(10, 0, 10)
// Portals: Zone 1 <-> Zone 2
zone1.AddPortal(world.ZonePortal{
SourceZoneID: 1,
TriggerPos: mathutil.NewVec3(300, 0, 150),
TriggerRadius: 5.0,
TargetZoneID: 2,
TargetPos: mathutil.NewVec3(5, 0, 5),
})
zone2.AddPortal(world.ZonePortal{
SourceZoneID: 2,
TriggerPos: mathutil.NewVec3(0, 0, 0),
TriggerRadius: 5.0,
TargetZoneID: 1,
TargetPos: mathutil.NewVec3(295, 0, 150),
})
// Portals: Zone 2 <-> Zone 3
zone2.AddPortal(world.ZonePortal{
SourceZoneID: 2,
TriggerPos: mathutil.NewVec3(300, 0, 300),
TriggerRadius: 5.0,
TargetZoneID: 3,
TargetPos: mathutil.NewVec3(10, 0, 10),
})
zone3.AddPortal(world.ZonePortal{
SourceZoneID: 3,
TriggerPos: mathutil.NewVec3(0, 0, 0),
TriggerRadius: 5.0,
TargetZoneID: 2,
TargetPos: mathutil.NewVec3(295, 0, 295),
})
// Populate zones with mobs.
gs.setupZoneMobs(zone1, []mobSpawnConfig{
{mobID: 1, count: 3, baseX: 20, baseZ: 30, spacing: 15}, // Goblins
{mobID: 2, count: 2, baseX: 80, baseZ: 80, spacing: 12}, // Wolves
})
gs.setupZoneMobs(zone2, []mobSpawnConfig{
{mobID: 2, count: 4, baseX: 50, baseZ: 50, spacing: 15}, // Wolves
{mobID: 3, count: 2, baseX: 150, baseZ: 150, spacing: 20}, // Trolls
{mobID: 4, count: 1, baseX: 200, baseZ: 50, spacing: 0}, // Fire Elemental
})
gs.setupZoneMobs(zone3, []mobSpawnConfig{
{mobID: 4, count: 3, baseX: 80, baseZ: 80, spacing: 25}, // Fire Elementals
{mobID: 5, count: 1, baseX: 200, baseZ: 200, spacing: 0}, // Dragon Whelp
})
}
type mobSpawnConfig struct {
mobID uint32
count int
baseX float32
baseZ float32
spacing float32
}
// setupZoneMobs configures mob spawn points for a zone.
func (gs *GameServer) setupZoneMobs(zone *Zone, configs []mobSpawnConfig) {
registry := ai.NewMobRegistry()
spawner := zone.Spawner()
for _, cfg := range configs {
def := registry.Get(cfg.mobID)
if def == nil {
continue
}
respawn := time.Duration(15+def.Level*3) * time.Second
for i := 0; i < cfg.count; i++ {
spawner.AddSpawnPoint(ai.SpawnPoint{
MobDef: def,
Position: mathutil.NewVec3(cfg.baseX+float32(i)*cfg.spacing, 0, cfg.baseZ+float32(i)*cfg.spacing),
RespawnDelay: respawn,
MaxCount: 1,
})
}
}
spawner.InitialSpawn()
}
// handleZoneTransfer moves a player between zones.
func (gs *GameServer) handleZoneTransfer(playerID uint64, targetZoneID uint32, targetPos mathutil.Vec3) {
gs.mu.RLock()
var p *player.Player
var connID uint64
for cid, pl := range gs.connPlayer {
if pl.EntityID() == playerID {
p = pl
connID = cid
break
}
}
gs.mu.RUnlock()
if p == nil {
return
}
_ = connID
sourceZone, err := gs.world.GetZone(p.ZoneID())
if err != nil {
return
}
targetZone, err := gs.world.GetZone(targetZoneID)
if err != nil {
logger.Warn("zone transfer target not found", "targetZone", targetZoneID)
return
}
// Remove from source zone.
sourceZone.RemovePlayer(playerID)
// Update player state.
p.SetPosition(targetPos)
p.SetZoneID(targetZoneID)
// Add to target zone via message queue.
targetZone.EnqueueMessage(PlayerMessage{
PlayerID: playerID,
Packet: &network.Packet{Type: msgPlayerEnterWorld},
})
// Notify client of zone change.
p.Connection().Send(network.MsgZoneTransferNotify, &pb.ZoneTransferNotify{
NewZoneId: targetZoneID,
Self: p.ToProto(),
})
logger.Info("zone transfer",
"playerID", playerID,
"from", sourceZone.ID(),
"to", targetZoneID,
)
}