using Microsoft.EntityFrameworkCore; using Vanguard.Server.Meta; namespace Vanguard.Server.Storage; /// /// Единый контекст игры и мета-данных: матчи, пользователи, коллекция, /// колоды, экономика, история. Одна БД — один EnsureCreated. /// public sealed class MatchDbContext : DbContext { public MatchDbContext(DbContextOptions options) : base(options) { } public DbSet Matches => Set(); public DbSet Intents => Set(); public DbSet Users => Set(); public DbSet RefreshTokens => Set(); public DbSet Collection => Set(); public DbSet Decks => Set(); public DbSet Transactions => Set(); public DbSet BoosterOpenings => Set(); public DbSet TrialDeckOwnerships => Set(); public DbSet MatchHistory => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { ConfigureMatches(modelBuilder); ConfigureMeta(modelBuilder); } private static void ConfigureMatches(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.ToTable("matches"); entity.HasKey(e => e.MatchId); entity.Property(e => e.MatchId).HasMaxLength(64); entity.Property(e => e.Mode).HasMaxLength(32); entity.Property(e => e.Player0Id).HasMaxLength(64); entity.Property(e => e.Player1Id).HasMaxLength(64); entity.Property(e => e.Deck0Json).HasColumnType("text"); entity.Property(e => e.Deck1Json).HasColumnType("text"); entity.Property(e => e.Seed).HasMaxLength(32); entity.HasIndex(e => new { e.IsFinished, e.UpdatedAt }); }); modelBuilder.Entity(entity => { entity.ToTable("match_intents"); entity.HasKey(e => e.Id); entity.Property(e => e.MatchId).HasMaxLength(64); entity.Property(e => e.IntentId).HasMaxLength(64); entity.Property(e => e.Command).HasColumnType("text"); entity.Property(e => e.Reason).HasColumnType("text"); entity.HasIndex(e => e.MatchId); }); } private static void ConfigureMeta(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.ToTable("users"); entity.HasKey(user => user.Id); entity.Property(user => user.Id).ValueGeneratedNever(); entity.Property(user => user.UserName).HasMaxLength(64); entity.Property(user => user.NormalizedUserName).HasMaxLength(64); entity.Property(user => user.PasswordHash).HasMaxLength(256); entity.Property(user => user.SelectedTrialDeckId).HasMaxLength(32); entity.HasIndex(user => user.NormalizedUserName).IsUnique(); }); modelBuilder.Entity(entity => { entity.ToTable("refresh_tokens"); entity.HasKey(token => token.Id); entity.Property(token => token.Id).ValueGeneratedNever(); entity.Property(token => token.TokenHash).HasMaxLength(64); entity.HasIndex(token => token.TokenHash).IsUnique(); entity.HasIndex(token => token.UserId); }); modelBuilder.Entity(entity => { entity.ToTable("collection"); entity.HasKey(card => new { card.UserId, card.PrintingId }); entity.Property(card => card.PrintingId).HasMaxLength(32); entity.HasIndex(card => card.UserId); }); modelBuilder.Entity(entity => { entity.ToTable("decks"); entity.HasKey(deck => deck.Id); entity.Property(deck => deck.Id).ValueGeneratedNever(); entity.Property(deck => deck.Name).HasMaxLength(64); entity.Property(deck => deck.Format).HasMaxLength(32); entity.Property(deck => deck.CardsJson).HasColumnType("text"); entity.Property(deck => deck.ShareCode).HasMaxLength(16); entity.HasIndex(deck => new { deck.UserId, deck.Name }).IsUnique(); entity.HasIndex(deck => deck.ShareCode).IsUnique(); entity.HasIndex(deck => deck.UserId); }); modelBuilder.Entity(entity => { entity.ToTable("transactions"); entity.HasKey(transaction => transaction.Id); entity.Property(transaction => transaction.Id).ValueGeneratedNever(); entity.Property(transaction => transaction.Currency).HasMaxLength(16); entity.Property(transaction => transaction.Reason).HasMaxLength(64); entity.Property(transaction => transaction.IdempotencyKey).HasMaxLength(128); entity.Property(transaction => transaction.MetadataJson).HasColumnType("text"); entity.HasIndex(transaction => new { transaction.UserId, transaction.IdempotencyKey }).IsUnique(); entity.HasIndex(transaction => new { transaction.UserId, transaction.Currency }); }); modelBuilder.Entity(entity => { entity.ToTable("booster_openings"); entity.HasKey(opening => opening.Id); entity.Property(opening => opening.Id).ValueGeneratedNever(); entity.Property(opening => opening.IdempotencyKey).HasMaxLength(128); entity.Property(opening => opening.Payment).HasMaxLength(16); entity.Property(opening => opening.CardsJson).HasColumnType("text"); entity.HasIndex(opening => new { opening.UserId, opening.IdempotencyKey }).IsUnique(); }); modelBuilder.Entity(entity => { entity.ToTable("trial_deck_ownership"); entity.HasKey(ownership => new { ownership.UserId, ownership.DeckId }); entity.Property(ownership => ownership.DeckId).HasMaxLength(32); }); modelBuilder.Entity(entity => { entity.ToTable("match_history"); entity.HasKey(history => history.Id); entity.Property(history => history.Id).ValueGeneratedNever(); entity.Property(history => history.MatchId).HasMaxLength(64); entity.Property(history => history.Mode).HasMaxLength(32); entity.Property(history => history.OpponentName).HasMaxLength(64); entity.Property(history => history.Result).HasMaxLength(16); entity.Property(history => history.FinishReason).HasMaxLength(16); entity.HasIndex(history => new { history.UserId, history.MatchId }).IsUnique(); entity.HasIndex(history => new { history.UserId, history.CreatedAt }); }); } } public sealed class MatchEntity { public string MatchId { get; set; } = string.Empty; public string Mode { get; set; } = string.Empty; public string Player0Id { get; set; } = string.Empty; public string Player1Id { get; set; } = string.Empty; public string Deck0Json { get; set; } = "[]"; public string Deck1Json { get; set; } = "[]"; public string Seed { get; set; } = "0"; public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; } public bool Started { get; set; } public long Bank0Ms { get; set; } public long Bank1Ms { get; set; } public DateTimeOffset? TurnDeadline { get; set; } public DateTimeOffset? AbsentSince0 { get; set; } public DateTimeOffset? AbsentSince1 { get; set; } public DateTimeOffset LastClockSent { get; set; } public bool IsFinished { get; set; } public int Winner { get; set; } public DateTimeOffset? FinishedAt { get; set; } public string FinishReason { get; set; } = "normal"; public long EventSeq { get; set; } } public sealed class MatchIntentEntity { public long Id { get; set; } public string MatchId { get; set; } = string.Empty; public string IntentId { get; set; } = string.Empty; public int Seat { get; set; } public string Command { get; set; } = string.Empty; public bool Accepted { get; set; } public int Code { get; set; } public string? Reason { get; set; } public long EventSeq { get; set; } }