You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

190 lines
4.9 KiB
Go

package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/denisovdennis/autohero/internal/config"
"github.com/denisovdennis/autohero/internal/game"
"github.com/denisovdennis/autohero/internal/handler"
"github.com/denisovdennis/autohero/internal/migrate"
"github.com/denisovdennis/autohero/internal/model"
"github.com/denisovdennis/autohero/internal/router"
"github.com/denisovdennis/autohero/internal/storage"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
cfg := config.Load()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Connect to PostgreSQL.
pgPool, err := storage.NewPostgres(ctx, cfg.DB, logger)
if err != nil {
logger.Error("failed to connect to PostgreSQL", "error", err)
os.Exit(1)
}
defer pgPool.Close()
// Run database migrations.
if err := migrate.Run(ctx, pgPool, "migrations"); err != nil {
logger.Error("database migration failed", "error", err)
os.Exit(1)
}
// Connect to Redis.
redisClient, err := storage.NewRedis(ctx, cfg.Redis, logger)
if err != nil {
logger.Error("failed to connect to Redis", "error", err)
os.Exit(1)
}
defer redisClient.Close()
// Combat event channel bridges game engine to WebSocket hub.
eventCh := make(chan model.CombatEvent, 256)
// Game engine.
engine := game.NewEngine(cfg.Game.TickRate, eventCh, logger)
// WebSocket hub.
hub := handler.NewHub(logger)
// Stores (created before hub callbacks which reference them).
heroStore := storage.NewHeroStore(pgPool, logger)
logStore := storage.NewLogStore(pgPool)
// Load road graph for server-authoritative movement.
roadGraph, err := game.LoadRoadGraph(ctx, pgPool)
if err != nil {
logger.Error("failed to load road graph", "error", err)
os.Exit(1)
}
logger.Info("road graph loaded",
"towns", len(roadGraph.Towns),
"roads", len(roadGraph.Roads),
)
// Wire engine dependencies.
engine.SetSender(hub) // Hub implements game.MessageSender
engine.SetRoadGraph(roadGraph)
engine.SetHeroStore(heroStore)
// Hub callbacks: on connect, load hero and register movement; on disconnect, persist.
hub.OnConnect = func(heroID int64) {
hero, err := heroStore.GetByID(ctx, heroID)
if err != nil || hero == nil {
logger.Error("failed to load hero on ws connect", "hero_id", heroID, "error", err)
return
}
engine.RegisterHeroMovement(hero)
}
hub.OnDisconnect = func(heroID int64, remainingSameHero int) {
engine.HeroSocketDetached(heroID, remainingSameHero == 0)
}
// Bridge hub incoming client messages to engine's command channel.
go func() {
for {
select {
case <-ctx.Done():
return
case msg := <-hub.Incoming:
engine.IncomingCh() <- game.IncomingMessage{
HeroID: msg.HeroID,
Type: msg.Type,
Payload: msg.Payload,
}
}
}
}()
go hub.Run()
// Bridge: forward engine events to WebSocket hub.
go func() {
for {
select {
case <-ctx.Done():
return
case evt := <-eventCh:
hub.BroadcastEvent(evt)
}
}
}()
// Start game engine.
go func() {
if err := engine.Run(ctx); err != nil && err != context.Canceled {
logger.Error("game engine error", "error", err)
}
}()
// Record server start time for catch-up gap calculation.
serverStartedAt := time.Now()
offlineSim := game.NewOfflineSimulator(heroStore, logStore, roadGraph, logger)
go func() {
if err := offlineSim.Run(ctx); err != nil && err != context.Canceled {
logger.Error("offline simulator error", "error", err)
}
}()
// HTTP server.
r := router.New(router.Deps{
Engine: engine,
Hub: hub,
PgPool: pgPool,
BotToken: cfg.BotToken,
AdminBasicAuthUsername: cfg.Admin.BasicAuthUsername,
AdminBasicAuthPassword: cfg.Admin.BasicAuthPassword,
AdminBasicAuthRealm: cfg.Admin.BasicAuthRealm,
Logger: logger,
ServerStartedAt: serverStartedAt,
})
srv := &http.Server{
Addr: ":" + cfg.ServerPort,
Handler: r,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in background.
go func() {
logger.Info("server starting", "port", cfg.ServerPort)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("server error", "error", err)
os.Exit(1)
}
}()
// Graceful shutdown.
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit
logger.Info("shutting down", "signal", sig.String())
// Cancel game engine and event forwarding.
cancel()
// Give HTTP connections time to drain.
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
logger.Error("server shutdown error", "error", err)
}
logger.Info("server stopped")
}