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