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; }
}