More features

This commit is contained in:
gamer147
2026-05-23 14:18:01 -04:00
parent b2024af852
commit 6b70850b7b
59 changed files with 862 additions and 42033 deletions

View File

@@ -1,7 +1,7 @@
using System.Globalization;
using CsvHelper;
using CsvHelper.Configuration;
using DCGEngine.Database.Interfaces;
using SVSim.Database.Common;
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Models;
@@ -12,6 +12,17 @@ namespace SVSim.Database.DataSeeders;
/// </summary>
public class BaseDataSeeder : IDataSeeder
{
private static string DataPath(string fileName) =>
Path.Combine(AppContext.BaseDirectory, "Data", fileName);
private static List<T> ReadCsv<T, TMap>(string fileName) where TMap : ClassMap<T>, new()
{
using StreamReader reader = new(DataPath(fileName));
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<TMap>();
return csv.GetRecords<T>().ToList();
}
private class ClassEntryMap : ClassMap<ClassEntry>
{
public ClassEntryMap()
@@ -108,99 +119,33 @@ public class BaseDataSeeder : IDataSeeder
public void Seed(ModelBuilder builder)
{
List<ClassEntry> classes = new();
List<LeaderSkinEntry> leaderSkins = new();
List<EmblemEntry> emblems = new();
List<DegreeEntry> degrees = new();
List<SleeveEntry> sleeves = new();
List<BattlefieldEntry> battlefields = new();
List<MyPageBackgroundEntry> myPageBackgrounds = new();
List<ClassExpEntry> classexp = new();
List<RankInfoEntry> rankinfos = new();
using (StreamReader reader = new("data/classes.csv"))
// Migrations bake the HasData rows into InsertData calls — once the migration is
// generated, runtime model-creation no longer needs the CSVs. Tools that only query
// an already-migrated DB (e.g. SVSim.CardImport) don't ship the Data folder; skip
// gracefully so DbContext construction succeeds for them.
if (!File.Exists(DataPath("classes.csv")))
{
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<ClassEntryMap>();
IEnumerable<ClassEntry> records = csv.GetRecords<ClassEntry>();
classes.AddRange(records);
}
using (StreamReader reader = new("data/leaderskins.csv"))
{
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<LeaderSkinEntryMap>();
IEnumerable<LeaderSkinEntry> records = csv.GetRecords<LeaderSkinEntry>();
leaderSkins.AddRange(records);
leaderSkins.ForEach(skin =>
{
if (skin.ClassId == 0)
{
skin.ClassId = null;
}
});
Console.Error.WriteLine($"[BaseDataSeeder] Skipping seed: Data folder not found at {DataPath("")}");
return;
}
// Load rest of default data
using (StreamReader reader = new("data/emblems.csv"))
List<ClassEntry> classes = ReadCsv<ClassEntry, ClassEntryMap>("classes.csv");
List<LeaderSkinEntry> leaderSkins = ReadCsv<LeaderSkinEntry, LeaderSkinEntryMap>("leaderskins.csv");
leaderSkins.ForEach(skin =>
{
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<EmblemEntryMap>();
IEnumerable<EmblemEntry> records = csv.GetRecords<EmblemEntry>();
emblems.AddRange(records);
}
using (StreamReader reader = new("data/degrees.csv"))
{
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<DegreeEntryMap>();
IEnumerable<DegreeEntry> records = csv.GetRecords<DegreeEntry>();
degrees.AddRange(records);
}
using (StreamReader reader = new("data/sleeves.csv"))
{
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<SleeveEntryMap>();
IEnumerable<SleeveEntry> records = csv.GetRecords<SleeveEntry>();
sleeves.AddRange(records);
}
using (StreamReader reader = new("data/battlefields.csv"))
{
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<BattlefieldEntryMap>();
IEnumerable<BattlefieldEntry> records = csv.GetRecords<BattlefieldEntry>();
battlefields.AddRange(records);
}
using (StreamReader reader = new("data/mypagebackgrounds.csv"))
{
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<MyPageBackgroundEntryMap>();
IEnumerable<MyPageBackgroundEntry> records = csv.GetRecords<MyPageBackgroundEntry>();
myPageBackgrounds.AddRange(records);
}
using (StreamReader reader = new("data/ranks.csv"))
{
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<RankInfoEntryMap>();
IEnumerable<RankInfoEntry> records = csv.GetRecords<RankInfoEntry>();
rankinfos.AddRange(records);
}
using (StreamReader reader = new("data/classexp.csv"))
{
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
csv.Context.RegisterClassMap<ClassExpEntryMap>();
IEnumerable<ClassExpEntry> records = csv.GetRecords<ClassExpEntry>();
classexp.AddRange(records);
}
if (skin.ClassId == 0)
{
skin.ClassId = null;
}
});
List<EmblemEntry> emblems = ReadCsv<EmblemEntry, EmblemEntryMap>("emblems.csv");
List<DegreeEntry> degrees = ReadCsv<DegreeEntry, DegreeEntryMap>("degrees.csv");
List<SleeveEntry> sleeves = ReadCsv<SleeveEntry, SleeveEntryMap>("sleeves.csv");
List<BattlefieldEntry> battlefields = ReadCsv<BattlefieldEntry, BattlefieldEntryMap>("battlefields.csv");
List<MyPageBackgroundEntry> myPageBackgrounds = ReadCsv<MyPageBackgroundEntry, MyPageBackgroundEntryMap>("mypagebackgrounds.csv");
List<RankInfoEntry> rankinfos = ReadCsv<RankInfoEntry, RankInfoEntryMap>("ranks.csv");
List<ClassExpEntry> classexp = ReadCsv<ClassExpEntry, ClassExpEntryMap>("classexp.csv");
builder.Entity<ClassEntry>().HasData(classes);
builder.Entity<LeaderSkinEntry>().HasData(leaderSkins);
@@ -212,4 +157,4 @@ public class BaseDataSeeder : IDataSeeder
builder.Entity<RankInfoEntry>().HasData(rankinfos);
builder.Entity<ClassExpEntry>().HasData(classexp);
}
}
}

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Interfaces;
using SVSim.Database.Common;
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Models;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -25,78 +25,6 @@ namespace SVSim.Database.Migrations
modelBuilder.HasSequence("ShortUdidSequence")
.StartsAt(400000000L);
modelBuilder.Entity("DCGEngine.Database.Models.CardEntry", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint");
b.Property<int?>("Attack")
.HasColumnType("integer");
b.Property<DateTime>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DateUpdated")
.HasColumnType("timestamp with time zone");
b.Property<int?>("Defense")
.HasColumnType("integer");
b.Property<string>("Discriminator")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("PrimaryResourceCost")
.HasColumnType("integer");
b.Property<int?>("ShadowverseCardSetEntryId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ShadowverseCardSetEntryId");
b.ToTable("CardEntry");
b.HasDiscriminator().HasValue("CardEntry");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("DCGEngine.Database.Models.DeckEntry", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<DateTime>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DateUpdated")
.HasColumnType("timestamp with time zone");
b.Property<string>("Discriminator")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("character varying(21)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("DeckEntry");
b.HasDiscriminator().HasValue("DeckEntry");
b.UseTphMappingStrategy();
});
modelBuilder.Entity("DegreeEntryViewer", b =>
{
b.Property<int>("DegreesId")
@@ -177,7 +105,7 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("BattlefieldEntry");
b.ToTable("Battlefields");
b.HasData(
new
@@ -311,7 +239,7 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("ClassEntry");
b.ToTable("Classes");
b.HasData(
new
@@ -380,7 +308,7 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("ClassExpEntry");
b.ToTable("ClassExpCurve");
b.HasData(
new
@@ -1298,7 +1226,7 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("DegreeEntry");
b.ToTable("Degrees");
b.HasData(
new
@@ -10396,7 +10324,7 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("EmblemEntry");
b.ToTable("Emblems");
b.HasData(
new
@@ -21516,7 +21444,7 @@ namespace SVSim.Database.Migrations
b.HasIndex("DefaultSleeveId");
b.ToTable("GameConfiguration");
b.ToTable("GameConfigurations");
b.HasData(
new
@@ -21551,7 +21479,7 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("ItemEntry");
b.ToTable("Items");
});
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b =>
@@ -21579,7 +21507,7 @@ namespace SVSim.Database.Migrations
b.HasIndex("ClassId");
b.ToTable("LeaderSkinEntry");
b.ToTable("LeaderSkins");
b.HasData(
new
@@ -24929,7 +24857,7 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("MyPageBackgroundEntry");
b.ToTable("MyPageBackgrounds");
b.HasData(
new
@@ -25081,7 +25009,7 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("RankInfoEntry");
b.ToTable("RankInfo");
b.HasData(
new
@@ -25695,6 +25623,48 @@ namespace SVSim.Database.Migrations
});
});
modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b =>
{
b.Property<long>("Id")
.HasColumnType("bigint");
b.Property<int?>("Attack")
.HasColumnType("integer");
b.Property<int?>("ClassId")
.HasColumnType("integer");
b.Property<DateTime>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DateUpdated")
.HasColumnType("timestamp with time zone");
b.Property<int?>("Defense")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("PrimaryResourceCost")
.HasColumnType("integer");
b.Property<int>("Rarity")
.HasColumnType("integer");
b.Property<int?>("ShadowverseCardSetEntryId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ClassId");
b.HasIndex("ShadowverseCardSetEntryId");
b.ToTable("Cards");
});
modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardSetEntry", b =>
{
b.Property<int>("Id")
@@ -25718,7 +25688,56 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("ShadowverseCardSetEntry");
b.ToTable("CardSets");
});
modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<int>("ClassId")
.HasColumnType("integer");
b.Property<DateTime>("DateCreated")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DateUpdated")
.HasColumnType("timestamp with time zone");
b.Property<int>("Format")
.HasColumnType("integer");
b.Property<int>("LeaderSkinId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Number")
.HasColumnType("integer");
b.Property<bool>("RandomLeaderSkin")
.HasColumnType("boolean");
b.Property<int>("SleeveId")
.HasColumnType("integer");
b.Property<long?>("ViewerId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("ClassId");
b.HasIndex("LeaderSkinId");
b.HasIndex("SleeveId");
b.HasIndex("ViewerId");
b.ToTable("Decks");
});
modelBuilder.Entity("SVSim.Database.Models.SleeveEntry", b =>
@@ -25734,7 +25753,7 @@ namespace SVSim.Database.Migrations
b.HasKey("Id");
b.ToTable("SleeveEntry");
b.ToTable("Sleeves");
b.HasData(
new
@@ -33281,7 +33300,7 @@ namespace SVSim.Database.Migrations
b.HasIndex("ShortUdid");
b.ToTable("Viewer");
b.ToTable("Viewers");
});
modelBuilder.Entity("SleeveEntryViewer", b =>
@@ -33299,106 +33318,6 @@ namespace SVSim.Database.Migrations
b.ToTable("SleeveEntryViewer");
});
modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b =>
{
b.HasBaseType("DCGEngine.Database.Models.CardEntry");
b.Property<int?>("ClassId")
.HasColumnType("integer");
b.Property<int>("Rarity")
.HasColumnType("integer");
b.HasIndex("ClassId");
b.HasDiscriminator().HasValue("ShadowverseCardEntry");
});
modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b =>
{
b.HasBaseType("DCGEngine.Database.Models.DeckEntry");
b.Property<int>("ClassId")
.HasColumnType("integer");
b.Property<int>("Format")
.HasColumnType("integer");
b.Property<int>("LeaderSkinId")
.HasColumnType("integer");
b.Property<int>("Number")
.HasColumnType("integer");
b.Property<bool>("RandomLeaderSkin")
.HasColumnType("boolean");
b.Property<int>("SleeveId")
.HasColumnType("integer");
b.Property<long?>("ViewerId")
.HasColumnType("bigint");
b.HasIndex("ClassId");
b.HasIndex("LeaderSkinId");
b.HasIndex("SleeveId");
b.HasIndex("ViewerId");
b.HasDiscriminator().HasValue("ShadowverseDeckEntry");
});
modelBuilder.Entity("DCGEngine.Database.Models.CardEntry", b =>
{
b.HasOne("SVSim.Database.Models.ShadowverseCardSetEntry", null)
.WithMany("Cards")
.HasForeignKey("ShadowverseCardSetEntryId");
});
modelBuilder.Entity("DCGEngine.Database.Models.DeckEntry", b =>
{
b.OwnsMany("DCGEngine.Database.Models.DeckCard", "Cards", b1 =>
{
b1.Property<Guid>("DeckId")
.HasColumnType("uuid");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<long>("CardId")
.HasColumnType("bigint");
b1.Property<int>("Count")
.HasColumnType("integer");
b1.HasKey("DeckId", "Id");
b1.HasIndex("CardId");
b1.ToTable("DeckEntry_Cards");
b1.HasOne("DCGEngine.Database.Models.CardEntry", "Card")
.WithMany()
.HasForeignKey("CardId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b1.WithOwner("Deck")
.HasForeignKey("DeckId");
b1.Navigation("Card");
b1.Navigation("Deck");
});
b.Navigation("Cards");
});
modelBuilder.Entity("DegreeEntryViewer", b =>
{
b.HasOne("SVSim.Database.Models.DegreeEntry", null)
@@ -33503,6 +33422,108 @@ namespace SVSim.Database.Migrations
b.Navigation("Class");
});
modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b =>
{
b.HasOne("SVSim.Database.Models.ClassEntry", "Class")
.WithMany()
.HasForeignKey("ClassId");
b.HasOne("SVSim.Database.Models.ShadowverseCardSetEntry", null)
.WithMany("Cards")
.HasForeignKey("ShadowverseCardSetEntryId");
b.OwnsOne("SVSim.Database.Models.CardCollectionInfo", "CollectionInfo", b1 =>
{
b1.Property<long>("ShadowverseCardEntryId")
.HasColumnType("bigint");
b1.Property<int>("CraftCost")
.HasColumnType("integer");
b1.Property<int>("DustReward")
.HasColumnType("integer");
b1.HasKey("ShadowverseCardEntryId");
b1.ToTable("Cards");
b1.WithOwner()
.HasForeignKey("ShadowverseCardEntryId");
});
b.Navigation("Class");
b.Navigation("CollectionInfo");
});
modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b =>
{
b.HasOne("SVSim.Database.Models.ClassEntry", "Class")
.WithMany()
.HasForeignKey("ClassId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SVSim.Database.Models.LeaderSkinEntry", "LeaderSkin")
.WithMany()
.HasForeignKey("LeaderSkinId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SVSim.Database.Models.SleeveEntry", "Sleeve")
.WithMany()
.HasForeignKey("SleeveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SVSim.Database.Models.Viewer", null)
.WithMany("Decks")
.HasForeignKey("ViewerId");
b.OwnsMany("SVSim.Database.Models.DeckCard", "Cards", b1 =>
{
b1.Property<Guid>("ShadowverseDeckEntryId")
.HasColumnType("uuid");
b1.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property<int>("Id"));
b1.Property<long>("CardId")
.HasColumnType("bigint");
b1.Property<int>("Count")
.HasColumnType("integer");
b1.HasKey("ShadowverseDeckEntryId", "Id");
b1.HasIndex("CardId");
b1.ToTable("DeckCard");
b1.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card")
.WithMany()
.HasForeignKey("CardId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b1.WithOwner()
.HasForeignKey("ShadowverseDeckEntryId");
b1.Navigation("Card");
});
b.Navigation("Cards");
b.Navigation("Class");
b.Navigation("LeaderSkin");
b.Navigation("Sleeve");
});
modelBuilder.Entity("SVSim.Database.Models.Viewer", b =>
{
b.OwnsMany("SVSim.Database.Models.OwnedCardEntry", "Cards", b1 =>
@@ -33531,7 +33552,7 @@ namespace SVSim.Database.Migrations
b1.ToTable("OwnedCardEntry");
b1.HasOne("DCGEngine.Database.Models.CardEntry", "Card")
b1.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card")
.WithMany()
.HasForeignKey("CardId")
.OnDelete(DeleteBehavior.Cascade)
@@ -33694,7 +33715,7 @@ namespace SVSim.Database.Migrations
b1.HasKey("ViewerId");
b1.ToTable("Viewer");
b1.ToTable("Viewers");
b1.WithOwner()
.HasForeignKey("ViewerId");
@@ -33733,7 +33754,7 @@ namespace SVSim.Database.Migrations
b1.HasIndex("SelectedEmblemId");
b1.ToTable("Viewer");
b1.ToTable("Viewers");
b1.HasOne("SVSim.Database.Models.DegreeEntry", "SelectedDegree")
.WithMany()
@@ -33774,7 +33795,7 @@ namespace SVSim.Database.Migrations
b1.HasKey("ViewerId");
b1.ToTable("Viewer");
b1.ToTable("Viewers");
b1.WithOwner()
.HasForeignKey("ViewerId");
@@ -33813,67 +33834,6 @@ namespace SVSim.Database.Migrations
.IsRequired();
});
modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardEntry", b =>
{
b.HasOne("SVSim.Database.Models.ClassEntry", "Class")
.WithMany()
.HasForeignKey("ClassId");
b.OwnsOne("SVSim.Database.Models.CardCollectionInfo", "CollectionInfo", b1 =>
{
b1.Property<long>("ShadowverseCardEntryId")
.HasColumnType("bigint");
b1.Property<int>("CraftCost")
.HasColumnType("integer");
b1.Property<int>("DustReward")
.HasColumnType("integer");
b1.HasKey("ShadowverseCardEntryId");
b1.ToTable("CardEntry");
b1.WithOwner()
.HasForeignKey("ShadowverseCardEntryId");
});
b.Navigation("Class");
b.Navigation("CollectionInfo");
});
modelBuilder.Entity("SVSim.Database.Models.ShadowverseDeckEntry", b =>
{
b.HasOne("SVSim.Database.Models.ClassEntry", "Class")
.WithMany()
.HasForeignKey("ClassId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SVSim.Database.Models.LeaderSkinEntry", "LeaderSkin")
.WithMany()
.HasForeignKey("LeaderSkinId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SVSim.Database.Models.SleeveEntry", "Sleeve")
.WithMany()
.HasForeignKey("SleeveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SVSim.Database.Models.Viewer", null)
.WithMany("Decks")
.HasForeignKey("ViewerId");
b.Navigation("Class");
b.Navigation("LeaderSkin");
b.Navigation("Sleeve");
});
modelBuilder.Entity("SVSim.Database.Models.ClassEntry", b =>
{
b.Navigation("LeaderSkins");

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,5 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,4 +1,3 @@
using DCGEngine.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace SVSim.Database.Models;
@@ -9,7 +8,7 @@ namespace SVSim.Database.Models;
[Owned]
public class OwnedCardEntry
{
public CardEntry Card { get; set; } = new ShadowverseCardEntry();
public ShadowverseCardEntry Card { get; set; } = new ShadowverseCardEntry();
public int Count { get; set; }
public bool IsProtected { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,12 +1,32 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using DCGEngine.Database.Models;
using SVSim.Database.Common;
using SVSim.Database.Enums;
namespace SVSim.Database.Models;
public class ShadowverseCardEntry : CardEntry
public class ShadowverseCardEntry : BaseEntity<long>
{
/// <summary>
/// The internal name of this card (not the localized display name).
/// </summary>
[Required(AllowEmptyStrings = false)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Attack stat (atk on the wire).
/// </summary>
public int? Attack { get; set; }
/// <summary>
/// Life / defense stat (life on the wire).
/// </summary>
public int? Defense { get; set; }
/// <summary>
/// Play cost (cost on the wire).
/// </summary>
public int? PrimaryResourceCost { get; set; }
/// <summary>
/// The rarity of this card.
/// </summary>
@@ -24,9 +44,9 @@ public class ShadowverseCardEntry : CardEntry
#region Navigation Properties
/// <summary>
/// The class this card belongs to, or optionally none for neutral cards.
/// The class this card belongs to, or null for neutral cards.
/// </summary>
public ClassEntry? Class { get; set; }
#endregion
}
}

View File

@@ -1,10 +1,19 @@
using System.ComponentModel.DataAnnotations.Schema;
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;
public class ShadowverseCardSetEntry : CardSetEntry
public class ShadowverseCardSetEntry : BaseEntity<int>
{
/// <summary>
/// The internal name of the set.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The cards in the set.
/// </summary>
public List<ShadowverseCardEntry> Cards { get; set; } = new List<ShadowverseCardEntry>();
public bool IsInRotation { get; set; }
public bool IsBasic { get; set; }
}
}

View File

@@ -1,11 +1,22 @@
using DCGEngine.Database.Models;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using SVSim.Database.Common;
using SVSim.Database.Enums;
namespace SVSim.Database.Models;
public class ShadowverseDeckEntry : DeckEntry
public class ShadowverseDeckEntry : BaseEntity<Guid>
{
/// <summary>
/// Internal deck name.
/// </summary>
[Required(AllowEmptyStrings = false)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Cards in this deck.
/// </summary>
public List<DeckCard> Cards { get; set; } = new List<DeckCard>();
public int Number { get; set; }
public Format Format { get; set; }
public bool RandomLeaderSkin { get; set; }
@@ -19,4 +30,4 @@ public class ShadowverseDeckEntry : DeckEntry
public LeaderSkinEntry LeaderSkin { get; set; } = new LeaderSkinEntry();
#endregion
}
}

View File

@@ -1,4 +1,4 @@
using DCGEngine.Database.Models;
using SVSim.Database.Common;
namespace SVSim.Database.Models;

View File

@@ -1,5 +1,3 @@
using System.ComponentModel.DataAnnotations.Schema;
using DCGEngine.Database.Models;
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Enums;

View File

@@ -1,5 +1,5 @@
using System.ComponentModel.DataAnnotations.Schema;
using DCGEngine.Database.Models;
using SVSim.Database.Common;
using Microsoft.EntityFrameworkCore;
namespace SVSim.Database.Models;

View File

@@ -1,4 +1,3 @@
using DCGEngine.Database.Models;
using Microsoft.EntityFrameworkCore;
using SVSim.Database.Models;
@@ -19,7 +18,7 @@ public class CardRepository : BaseRepository<ShadowverseCardEntry>, ICardReposit
public async Task<List<ShadowverseCardEntry>> GetAllBasic()
{
return await DbContext.Set<ShadowverseCardSetEntry>().Where(set => set.IsBasic).SelectMany(set => set.Cards)
.Cast<ShadowverseCardEntry>().ToListAsync();
.ToListAsync();
}
public async Task<List<ShadowverseCardSetEntry>> GetCardSets(bool onlyInRotation)

View File

@@ -19,11 +19,12 @@ public class ViewerRepository : IViewerRepository
public async Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId)
{
return (await _dbContext.Set<SocialAccountConnection>()
.AsNoTracking()
.Include(sac => sac.Viewer)
.FirstOrDefaultAsync(sac => sac.AccountType == accountType && sac.AccountId == socialId))
?.Viewer;
// SocialAccountConnection is [Owned]-by-Viewer — can't be queried as a top-level Set<T>.
// Look up the Viewer that has a matching owned connection instead.
return await _dbContext.Set<Models.Viewer>()
.AsNoTracking()
.FirstOrDefaultAsync(v => v.SocialAccountConnections.Any(sac =>
sac.AccountType == accountType && sac.AccountId == socialId));
}
public async Task<Models.Viewer?> GetViewerWithSocials(long id)
@@ -32,10 +33,31 @@ public class ViewerRepository : IViewerRepository
.FirstOrDefaultAsync(viewer => viewer.Id == id);
}
/// <summary>
/// Loads a viewer with every navigation property needed to render the home-screen
/// (/load/index). Heavy query — only call from LoadController.Index.
/// </summary>
public async Task<Models.Viewer?> GetViewerByShortUdid(long shortUdid)
{
return await _dbContext.Set<Models.Viewer>().AsNoTracking().Include(viewer => viewer.MissionData)
.Include(viewer => viewer.Info).FirstOrDefaultAsync(viewer => viewer.ShortUdid == shortUdid);
return await _dbContext.Set<Models.Viewer>()
.AsNoTracking()
.Include(v => v.MissionData)
.Include(v => v.Info).ThenInclude(i => i.SelectedEmblem)
.Include(v => v.Info).ThenInclude(i => i.SelectedDegree)
.Include(v => v.Currency)
.Include(v => v.Classes).ThenInclude(c => c.Class).ThenInclude(c => c.LeaderSkins)
.Include(v => v.Classes).ThenInclude(c => c.LeaderSkin)
.Include(v => v.Decks).ThenInclude(d => d.Class)
.Include(v => v.Decks).ThenInclude(d => d.Sleeve)
.Include(v => v.Decks).ThenInclude(d => d.LeaderSkin)
.Include(v => v.Cards).ThenInclude(c => c.Card)
.Include(v => v.Items).ThenInclude(i => i.Item)
.Include(v => v.Sleeves)
.Include(v => v.Emblems)
.Include(v => v.Degrees)
.Include(v => v.LeaderSkins).ThenInclude(ls => ls.Class)
.Include(v => v.MyPageBackgrounds)
.FirstOrDefaultAsync(viewer => viewer.ShortUdid == shortUdid);
}
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
@@ -46,7 +68,7 @@ public class ViewerRepository : IViewerRepository
DisplayName = displayName
};
GameConfiguration gameConfig = await new GlobalsRepository(_dbContext).GetGameConfiguration("default");
viewer.SocialAccountConnections.Add(new SocialAccountConnection
{
AccountId = socialAccountIdentifier,
@@ -61,23 +83,40 @@ public class ViewerRepository : IViewerRepository
viewer.Currency.RedEther = gameConfig.DefaultEther;
viewer.MissionData.TutorialState = 100; // finishes tutorial for now
List<ClassEntry> classes = await _dbContext.Set<ClassEntry>().ToListAsync();
viewer.Classes = classes.Select(ce => new ViewerClassData
// Load classes WITH their LeaderSkins — DefaultLeaderSkin iterates the nav collection
// and would otherwise be null (audit §6 #3 latent NRE — this is the one).
List<ClassEntry> classes = await _dbContext.Set<ClassEntry>()
.Include(c => c.LeaderSkins)
.ToListAsync();
viewer.Classes = classes.Select(ce =>
{
Class = ce,
Exp = 0,
Level = 0,
LeaderSkin = ce.DefaultLeaderSkin
var skin = ce.DefaultLeaderSkin ?? ce.LeaderSkins.FirstOrDefault();
return new ViewerClassData
{
Class = ce,
Exp = 0,
Level = 0,
LeaderSkin = skin ?? new LeaderSkinEntry { Id = 0, Name = "<missing>", ClassId = ce.Id }
};
}).ToList();
viewer.Sleeves.Add(gameConfig.DefaultSleeve);
viewer.Degrees.Add(gameConfig.DefaultDegree);
viewer.Emblems.Add(gameConfig.DefaultEmblem);
viewer.MyPageBackgrounds.Add(gameConfig.DefaultMyPageBackground);
viewer.LeaderSkins.AddRange(viewer.Classes.Select(vcd => vcd.LeaderSkin));
if (gameConfig.DefaultSleeve is not null) viewer.Sleeves.Add(gameConfig.DefaultSleeve);
if (gameConfig.DefaultDegree is not null) viewer.Degrees.Add(gameConfig.DefaultDegree);
if (gameConfig.DefaultEmblem is not null) viewer.Emblems.Add(gameConfig.DefaultEmblem);
if (gameConfig.DefaultMyPageBackground is not null) viewer.MyPageBackgrounds.Add(gameConfig.DefaultMyPageBackground);
// Grant one of each class's default leader skin. Filter out the synthetic placeholders
// (Id=0) and dedupe — skins are many-to-many via SleeveEntryViewer-style join.
var grantedSkins = viewer.Classes
.Select(vcd => vcd.LeaderSkin)
.Where(s => s.Id != 0)
.DistinctBy(s => s.Id)
.ToList();
viewer.LeaderSkins.AddRange(grantedSkins);
_dbContext.Set<Models.Viewer>().Add(viewer);
await _dbContext.SaveChangesAsync();
return viewer;
}
}
}

View File

@@ -6,16 +6,11 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DCGEngine.Database\DCGEngine.Database.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
</ItemGroup>
</Project>

View File

@@ -1,30 +1,108 @@
using DCGEngine.Database;
using DCGEngine.Database.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SVSim.Database.Common;
using SVSim.Database.DataSeeders;
using SVSim.Database.Models;
namespace SVSim.Database;
public class SVSimDbContext : DCGEDbContext
public class SVSimDbContext : DbContext
{
public SVSimDbContext(IOptions<DCGEDatabaseConfiguration> configuration, ILogger<DCGEDbContext> logger, DbContextOptions options) : base(configuration, logger, options)
private readonly ILogger<SVSimDbContext> _logger;
public SVSimDbContext(ILogger<SVSimDbContext> logger, DbContextOptions<SVSimDbContext> options) : base(options)
{
_logger = logger;
}
#region DbSets
public DbSet<Viewer> Viewers => Set<Viewer>();
public DbSet<ShadowverseCardEntry> Cards => Set<ShadowverseCardEntry>();
public DbSet<ShadowverseCardSetEntry> CardSets => Set<ShadowverseCardSetEntry>();
public DbSet<ShadowverseDeckEntry> Decks => Set<ShadowverseDeckEntry>();
public DbSet<ClassEntry> Classes => Set<ClassEntry>();
public DbSet<ClassExpEntry> ClassExpCurve => Set<ClassExpEntry>();
public DbSet<LeaderSkinEntry> LeaderSkins => Set<LeaderSkinEntry>();
public DbSet<SleeveEntry> Sleeves => Set<SleeveEntry>();
public DbSet<EmblemEntry> Emblems => Set<EmblemEntry>();
public DbSet<DegreeEntry> Degrees => Set<DegreeEntry>();
public DbSet<MyPageBackgroundEntry> MyPageBackgrounds => Set<MyPageBackgroundEntry>();
public DbSet<BattlefieldEntry> Battlefields => Set<BattlefieldEntry>();
public DbSet<RankInfoEntry> RankInfo => Set<RankInfoEntry>();
public DbSet<ItemEntry> Items => Set<ItemEntry>();
public DbSet<GameConfiguration> GameConfigurations => Set<GameConfiguration>();
#endregion
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entityEntry in ChangeTracker.Entries())
{
if (entityEntry.Entity is ITimeTrackedEntity timeTrackedEntity)
{
if (entityEntry.State is EntityState.Added && timeTrackedEntity.DateCreated == DateTime.MinValue)
{
timeTrackedEntity.DateCreated = DateTime.UtcNow;
}
if (entityEntry.State is EntityState.Modified or EntityState.Added)
{
timeTrackedEntity.DateUpdated = DateTime.UtcNow;
}
}
}
return await base.SaveChangesAsync(cancellationToken);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ShadowverseDeckEntry>()
.OwnsMany(de => de.Cards);
// For whatever reason it cannot figure out this relationship on it's own
// BaseEntity<TKey> annotates Id with [DatabaseGenerated(None)] for the integer-PK
// entities seeded via HasData. ShadowverseDeckEntry uses Guid and is created at
// runtime — without client-side generation every new deck gets Guid.Empty and the
// second deck insert collides on PK. (DDL has no column default; this only works
// because EF generates a sequential Guid before INSERT.)
modelBuilder.Entity<ShadowverseDeckEntry>()
.Property(d => d.Id)
.ValueGeneratedOnAdd();
// EF can't figure this many-to-many out on its own
modelBuilder.Entity<SleeveEntry>()
.HasMany<Viewer>(se => se.Viewers)
.HasMany(se => se.Viewers)
.WithMany(v => v.Sleeves);
modelBuilder.HasSequence<long>("ShortUdidSequence").StartsAt(400000000);
modelBuilder.Entity<Viewer>()
.Property(v => v.ShortUdid)
.UseSequence("ShortUdidSequence");
new BaseDataSeeder().Seed(modelBuilder);
new DefaultSettingsSeeder().Seed(modelBuilder);
base.OnModelCreating(modelBuilder);
}
}
public void UpdateDatabase()
{
IEnumerable<string> pendingMigrations = Database.GetPendingMigrations();
if (!pendingMigrations.Any())
{
_logger.LogDebug("No pending migrations found, continuing.");
return;
}
foreach (string migration in pendingMigrations)
{
_logger.LogInformation("Found pending migration with name {migrationName}.", migration);
}
_logger.LogInformation("Attempting to apply pending migrations...");
Database.Migrate();
_logger.LogInformation("Migrations applied.");
}
}