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, ) }