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.
249 lines
7.4 KiB
Go
249 lines
7.4 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"
|
|
"github.com/denisovdennis/autohero/internal/tuning"
|
|
)
|
|
|
|
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)
|
|
digestStore := storage.NewOfflineDigestStore(pgPool)
|
|
questStore := storage.NewQuestStore(pgPool)
|
|
gearStore := storage.NewGearStore(pgPool)
|
|
achievementStore := storage.NewAchievementStore(pgPool)
|
|
taskStore := storage.NewDailyTaskStore(pgPool)
|
|
runtimeConfigStore := storage.NewRuntimeConfigStore(pgPool)
|
|
if err := tuning.ReloadNow(ctx, logger, runtimeConfigStore); err != nil {
|
|
logger.Error("failed to load runtime config", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
buffDebuffStore := storage.NewBuffDebuffConfigStore(pgPool)
|
|
if err := model.ReloadBuffDebuffCatalog(ctx, logger, buffDebuffStore); err != nil {
|
|
logger.Error("failed to load buff/debuff catalog", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
contentStore := storage.NewContentStore(pgPool)
|
|
enemiesFromDB, err := contentStore.LoadEnemyTemplates(ctx)
|
|
if err != nil {
|
|
logger.Error("failed to load enemy templates from db", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
model.SetEnemyTemplates(enemiesFromDB)
|
|
gearFamiliesFromDB, err := contentStore.LoadGearFamilies(ctx)
|
|
if err != nil {
|
|
logger.Error("failed to load gear templates from db", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
model.SetGearCatalog(gearFamiliesFromDB)
|
|
|
|
// 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)
|
|
engine.SetTownSessionStore(storage.NewTownSessionStore(redisClient))
|
|
engine.SetQuestStore(questStore)
|
|
engine.SetAdventureLog(func(heroID int64, line model.AdventureLogLine) {
|
|
logCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
if err := logStore.Add(logCtx, heroID, line); err != nil {
|
|
logger.Warn("failed to write adventure log", "hero_id", heroID, "error", err)
|
|
return
|
|
}
|
|
hub.SendToHero(heroID, "adventure_log_line", line)
|
|
})
|
|
engine.SetDigestStore(digestStore)
|
|
engine.SetHeroSubscriber(hub.IsHeroConnected)
|
|
|
|
// 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) {
|
|
disconnectAt := time.Now()
|
|
engine.HeroSocketDetached(heroID, remainingSameHero == 0, disconnectAt)
|
|
if remainingSameHero == 0 {
|
|
dctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
if err := heroStore.SetWsDisconnectedAt(dctx, heroID, disconnectAt); err != nil {
|
|
logger.Warn("set ws_disconnected_at", "hero_id", heroID, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bridge hub incoming client messages to engine's command channel.
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case msg := <-hub.Incoming:
|
|
if engine.IsTimePaused() {
|
|
hub.SendToHero(msg.HeroID, "error", model.ErrorPayload{
|
|
Code: "time_paused",
|
|
Message: "server time is paused",
|
|
})
|
|
continue
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Record server start time for catch-up gap calculation.
|
|
serverStartedAt := time.Now()
|
|
bootstrapSim := game.NewOfflineSimulator(heroStore, logStore, questStore, roadGraph, logger, nil, nil).
|
|
WithCombatTickRate(engine.TickRate()).
|
|
WithRewardStores(gearStore, achievementStore, taskStore).
|
|
WithDigestStore(digestStore)
|
|
bootCtx, bootCancel := context.WithTimeout(ctx, 3*time.Minute)
|
|
game.BootstrapResidentHeroes(bootCtx, engine, heroStore, bootstrapSim, 500, logger)
|
|
bootCancel()
|
|
|
|
// Start game engine (after resident heroes are registered).
|
|
go func() {
|
|
if err := engine.Run(ctx); err != nil && err != context.Canceled {
|
|
logger.Error("game engine error", "error", err)
|
|
}
|
|
}()
|
|
|
|
// HTTP server.
|
|
r := router.New(router.Deps{
|
|
Engine: engine,
|
|
Hub: hub,
|
|
PgPool: pgPool,
|
|
BotToken: cfg.BotToken,
|
|
PaymentProviderToken: cfg.PaymentProviderToken,
|
|
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")
|
|
}
|