VanguardReMaster/server/src/Vanguard.Server/Program.cs

121 lines
4.9 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using Vanguard.Content;
using Vanguard.Rules.Cards;
using Vanguard.Server.Api;
using Vanguard.Server.Configuration;
using Vanguard.Server.Matches;
using Vanguard.Server.Matchmaking;
using Vanguard.Server.Profiles;
using Vanguard.Server.Realtime;
using Vanguard.Server.Storage;
using Vanguard.Server.Tickets;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<GameOptions>(builder.Configuration.GetSection("Game"));
builder.Services.Configure<ContentOptions>(builder.Configuration.GetSection("Content"));
builder.Services.Configure<StorageOptions>(builder.Configuration.GetSection("Storage"));
builder.Services.Configure<RedisOptions>(builder.Configuration.GetSection("Redis"));
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<GameOptions>>().Value);
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<ContentOptions>>().Value);
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<StorageOptions>>().Value);
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<RedisOptions>>().Value);
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton(sp =>
{
var content = sp.GetRequiredService<ContentOptions>();
var directory = ContentPaths.Resolve(content.Path);
return CatalogLoader.LoadDirectory(directory);
});
var storage = builder.Configuration.GetSection("Storage").Get<StorageOptions>() ?? new StorageOptions();
var storageProvider = storage.Provider?.Trim() ?? "None";
if (storageProvider.Equals("Postgres", StringComparison.OrdinalIgnoreCase))
{
if (string.IsNullOrWhiteSpace(storage.ConnectionString))
throw new InvalidOperationException("Storage:ConnectionString обязателен для Provider=Postgres.");
builder.Services.AddDbContextFactory<MatchDbContext>(options =>
options.UseNpgsql(storage.ConnectionString));
builder.Services.AddSingleton<IMatchStore, EfMatchStore>();
}
else if (storageProvider.Equals("Memory", StringComparison.OrdinalIgnoreCase))
{
builder.Services.AddDbContextFactory<MatchDbContext>(options =>
options.UseInMemoryDatabase("vanguard-matches"));
builder.Services.AddSingleton<IMatchStore, EfMatchStore>();
}
else if (storageProvider.Equals("None", StringComparison.OrdinalIgnoreCase))
{
builder.Services.AddSingleton<IMatchStore, NullMatchStore>();
}
else
{
throw new InvalidOperationException(
$"Неизвестный Storage:Provider «{storageProvider}». Ожидается None, Memory или Postgres.");
}
var redis = builder.Configuration.GetSection("Redis").Get<RedisOptions>() ?? new RedisOptions();
if (!string.IsNullOrWhiteSpace(redis.Configuration))
{
builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
{
var configuration = ConfigurationOptions.Parse(redis.Configuration);
configuration.AbortOnConnectFail = false;
return ConnectionMultiplexer.Connect(configuration);
});
builder.Services.AddSingleton<ITicketStore, RedisTicketStore>();
builder.Services.AddSingleton<IQueueStore, RedisQueueStore>();
}
else
{
builder.Services.AddSingleton<ITicketStore, InMemoryTicketStore>();
builder.Services.AddSingleton<IQueueStore, InMemoryQueueStore>();
}
builder.Services.AddSingleton<GameTicketService>();
builder.Services.AddSingleton<PlayerProfileStore>();
builder.Services.AddSingleton<IMatchResultSink>(sp => sp.GetRequiredService<PlayerProfileStore>());
builder.Services.AddSingleton<MatchGrainRegistry>();
builder.Services.AddSingleton<MatchRecoveryService>();
builder.Services.AddSingleton<MatchmakingService>();
builder.Services.AddHostedService<MatchTicker>();
builder.Services.AddHostedService<MatchmakingWorker>();
var app = builder.Build();
// каталог грузим сразу: сервер без карт бесполезен, лучше упасть на старте
app.Services.GetRequiredService<CardCatalog>();
if (app.Services.GetRequiredService<IMatchStore>() is EfMatchStore)
{
var factory = app.Services.GetRequiredService<IDbContextFactory<MatchDbContext>>();
await using var db = await factory.CreateDbContextAsync();
await db.Database.EnsureCreatedAsync();
}
// незавершённые матчи поднимаем до того, как сервер начнёт принимать подключения
await app.Services.GetRequiredService<MatchRecoveryService>().RecoverAsync();
await app.Services.GetRequiredService<MatchmakingService>().LoadQueuedAsync();
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30),
});
app.MapGameApi();
app.Map("/game", GameWebSocketEndpoint.HandleAsync);
app.Run();
/// <summary>Точка входа доступна интеграционным тестам через WebApplicationFactory.</summary>
public partial class Program
{
}