More features
This commit is contained in:
@@ -1,30 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using DCGEngine.Database.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace DCGEngine.Database.Configuration;
|
|
||||||
|
|
||||||
public class DCGEDatabaseConfiguration
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The default name of the appsettings section where these are configured.
|
|
||||||
/// </summary>
|
|
||||||
public const string DefaultSectionName = "DCGEDatabaseConfiguration";
|
|
||||||
|
|
||||||
#region Appsettings
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Manual Configuration
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Assemblies to be searched for classes implementing <see cref="BaseEntity{TKey}"/> to be added as <see cref="DbSet{TEntity}"/>s. Should be set in code, not in appsettings.
|
|
||||||
/// </summary>
|
|
||||||
public List<Assembly> DbSetSearchAssemblies { get; set; } = new List<Assembly>();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
using DCGEngine.Database.Configuration;
|
|
||||||
using DCGEngine.Database.Interfaces;
|
|
||||||
using DCGEngine.Database.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace DCGEngine.Database;
|
|
||||||
|
|
||||||
public class DCGEDbContext : DbContext
|
|
||||||
{
|
|
||||||
private readonly DCGEDatabaseConfiguration _configuration;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
public DCGEDbContext(IOptions<DCGEDatabaseConfiguration> configuration, ILogger<DCGEDbContext> logger, DbContextOptions options) : base(options)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_configuration = configuration.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
_configuration.DbSetSearchAssemblies.ForEach(assembly =>
|
|
||||||
{
|
|
||||||
foreach (var typeInfo in assembly.DefinedTypes.Where(type => type.IsAssignableTo(typeof(IDbTrackedEntity))))
|
|
||||||
{
|
|
||||||
modelBuilder.Entity(typeInfo.AsType());
|
|
||||||
}
|
|
||||||
foreach (var typeInfo in assembly.DefinedTypes.Where(type => type.IsAssignableTo(typeof(IDataSeeder))))
|
|
||||||
{
|
|
||||||
((IDataSeeder?)Activator.CreateInstance(typeInfo.AsType()))?.Seed(modelBuilder);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity<DeckEntry>()
|
|
||||||
.OwnsMany<DeckCard>(de => de.Cards);
|
|
||||||
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Attributes\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace DCGEngine.Database.Interfaces;
|
|
||||||
|
|
||||||
public interface IDataSeeder
|
|
||||||
{
|
|
||||||
void Seed(ModelBuilder builder);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace DCGEngine.Database.Interfaces;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Indicates a class should have a <see cref="DbSet{TEntity}"/> created and be tracked by the <see cref="DbContext"/>.
|
|
||||||
/// </summary>
|
|
||||||
public interface IDbTrackedEntity
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace DCGEngine.Database.Interfaces;
|
|
||||||
|
|
||||||
public interface ITimeTrackedEntity
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The <see cref="DateTime"/> this entity was first added to the database.
|
|
||||||
/// </summary>
|
|
||||||
public DateTime DateCreated { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The <see cref="DateTime"/> this entity was last updated.
|
|
||||||
/// </summary>
|
|
||||||
public DateTime? DateUpdated { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using DCGEngine.Database.Interfaces;
|
|
||||||
|
|
||||||
namespace DCGEngine.Database.Models;
|
|
||||||
|
|
||||||
public class BaseEntity<TKey> : ITimeTrackedEntity, IDbTrackedEntity
|
|
||||||
{
|
|
||||||
[Key]
|
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.None)]
|
|
||||||
public virtual TKey Id { get; set; }
|
|
||||||
|
|
||||||
public DateTime DateCreated { get; set; } = DateTime.MinValue;
|
|
||||||
|
|
||||||
public DateTime? DateUpdated { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace DCGEngine.Database.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A card within the system.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class CardEntry : BaseEntity<long>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The name of this card used internally, to separate it from any localization key stored.
|
|
||||||
/// </summary>
|
|
||||||
[Required(AllowEmptyStrings = false)]
|
|
||||||
public virtual string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The offensive power of this card.
|
|
||||||
/// </summary>
|
|
||||||
public virtual int? Attack { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The defensive power of this card.
|
|
||||||
/// </summary>
|
|
||||||
public virtual int? Defense { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// How much of the primary resource (ie mana, energy) does this card cost to play in a match.
|
|
||||||
/// </summary>
|
|
||||||
public virtual int? PrimaryResourceCost { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace DCGEngine.Database.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A set containing 0 or more <see cref="CardEntry"/>s. All cards must belong to a set.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class CardSetEntry : BaseEntity<int>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The internal name of the set.
|
|
||||||
/// </summary>
|
|
||||||
public virtual string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The cards in the set.
|
|
||||||
/// </summary>
|
|
||||||
public virtual List<CardEntry> Cards { get; set; } = new List<CardEntry>();
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace DCGEngine.Database.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A <see cref="CardEntry"/> that is in a <see cref="DeckEntry"/> X times.
|
|
||||||
/// </summary>
|
|
||||||
[Owned]
|
|
||||||
public class DeckCard
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The card in the deck.
|
|
||||||
/// </summary>
|
|
||||||
public virtual CardEntry Card { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of the card that is in the deck.
|
|
||||||
/// </summary>
|
|
||||||
public virtual int Count { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The deck this belongs to.
|
|
||||||
/// </summary>
|
|
||||||
public virtual DeckEntry Deck { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using DCGEngine.Database.Interfaces;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace DCGEngine.Database.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A deck consisting of multiple <see cref="CardEntry"/> stored in the DB.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class DeckEntry : BaseEntity<Guid>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// How this deck is referred to internally.
|
|
||||||
/// </summary>
|
|
||||||
[Required(AllowEmptyStrings = false)]
|
|
||||||
public virtual string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The cards present in this deck.
|
|
||||||
/// </summary>
|
|
||||||
public virtual List<DeckCard> Cards { get; set; } = new List<DeckCard>();
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,24 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.Content", "SVSim.Content\SVSim.Content.csproj", "{7D990EA3-0A15-4A71-A992-7E0FC8F4F677}"
|
#
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.EmulatedEntrypoint", "SVSim.EmulatedEntrypoint\SVSim.EmulatedEntrypoint.csproj", "{B345A858-043F-404E-9D98-B5A6CA2EACA9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.EmulatedEntrypoint", "SVSim.EmulatedEntrypoint\SVSim.EmulatedEntrypoint.csproj", "{B345A858-043F-404E-9D98-B5A6CA2EACA9}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DCGEngine.Database", "DCGEngine.Database\DCGEngine.Database.csproj", "{8D5DF264-F1A7-4455-837A-EC64849E0AE3}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.Database", "SVSim.Database\SVSim.Database.csproj", "{9CE5D4F0-0D98-4E1C-942F-D692978F6103}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.Database", "SVSim.Database\SVSim.Database.csproj", "{9CE5D4F0-0D98-4E1C-942F-D692978F6103}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.UnitTests", "SVSim.UnitTests\SVSim.UnitTests.csproj", "{00E87101-F286-46F3-858E-83AB1CEBF8D1}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.UnitTests", "SVSim.UnitTests\SVSim.UnitTests.csproj", "{00E87101-F286-46F3-858E-83AB1CEBF8D1}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SVSim.CardImport", "SVSim.CardImport\SVSim.CardImport.csproj", "{666786D9-9A4D-49EA-A759-39055C57F9AA}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{7D990EA3-0A15-4A71-A992-7E0FC8F4F677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{7D990EA3-0A15-4A71-A992-7E0FC8F4F677}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{7D990EA3-0A15-4A71-A992-7E0FC8F4F677}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{7D990EA3-0A15-4A71-A992-7E0FC8F4F677}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{B345A858-043F-404E-9D98-B5A6CA2EACA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B345A858-043F-404E-9D98-B5A6CA2EACA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B345A858-043F-404E-9D98-B5A6CA2EACA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B345A858-043F-404E-9D98-B5A6CA2EACA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{B345A858-043F-404E-9D98-B5A6CA2EACA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B345A858-043F-404E-9D98-B5A6CA2EACA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B345A858-043F-404E-9D98-B5A6CA2EACA9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B345A858-043F-404E-9D98-B5A6CA2EACA9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{8D5DF264-F1A7-4455-837A-EC64849E0AE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{8D5DF264-F1A7-4455-837A-EC64849E0AE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{8D5DF264-F1A7-4455-837A-EC64849E0AE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{8D5DF264-F1A7-4455-837A-EC64849E0AE3}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{9CE5D4F0-0D98-4E1C-942F-D692978F6103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9CE5D4F0-0D98-4E1C-942F-D692978F6103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9CE5D4F0-0D98-4E1C-942F-D692978F6103}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9CE5D4F0-0D98-4E1C-942F-D692978F6103}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9CE5D4F0-0D98-4E1C-942F-D692978F6103}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9CE5D4F0-0D98-4E1C-942F-D692978F6103}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@@ -36,5 +27,9 @@ Global
|
|||||||
{00E87101-F286-46F3-858E-83AB1CEBF8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{00E87101-F286-46F3-858E-83AB1CEBF8D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{00E87101-F286-46F3-858E-83AB1CEBF8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{00E87101-F286-46F3-858E-83AB1CEBF8D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{00E87101-F286-46F3-858E-83AB1CEBF8D1}.Release|Any CPU.Build.0 = Release|Any CPU
|
{00E87101-F286-46F3-858E-83AB1CEBF8D1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{666786D9-9A4D-49EA-A759-39055C57F9AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{666786D9-9A4D-49EA-A759-39055C57F9AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{666786D9-9A4D-49EA-A759-39055C57F9AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{666786D9-9A4D-49EA-A759-39055C57F9AA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
namespace SVSim.Content;
|
|
||||||
|
|
||||||
public class Class1
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using CsvHelper;
|
using CsvHelper;
|
||||||
using CsvHelper.Configuration;
|
using CsvHelper.Configuration;
|
||||||
using DCGEngine.Database.Interfaces;
|
using SVSim.Database.Common;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
@@ -12,6 +12,17 @@ namespace SVSim.Database.DataSeeders;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BaseDataSeeder : IDataSeeder
|
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>
|
private class ClassEntryMap : ClassMap<ClassEntry>
|
||||||
{
|
{
|
||||||
public ClassEntryMap()
|
public ClassEntryMap()
|
||||||
@@ -108,99 +119,33 @@ public class BaseDataSeeder : IDataSeeder
|
|||||||
|
|
||||||
public void Seed(ModelBuilder builder)
|
public void Seed(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
List<ClassEntry> classes = new();
|
// Migrations bake the HasData rows into InsertData calls — once the migration is
|
||||||
List<LeaderSkinEntry> leaderSkins = new();
|
// generated, runtime model-creation no longer needs the CSVs. Tools that only query
|
||||||
List<EmblemEntry> emblems = new();
|
// an already-migrated DB (e.g. SVSim.CardImport) don't ship the Data folder; skip
|
||||||
List<DegreeEntry> degrees = new();
|
// gracefully so DbContext construction succeeds for them.
|
||||||
List<SleeveEntry> sleeves = new();
|
if (!File.Exists(DataPath("classes.csv")))
|
||||||
List<BattlefieldEntry> battlefields = new();
|
|
||||||
List<MyPageBackgroundEntry> myPageBackgrounds = new();
|
|
||||||
List<ClassExpEntry> classexp = new();
|
|
||||||
List<RankInfoEntry> rankinfos = new();
|
|
||||||
|
|
||||||
using (StreamReader reader = new("data/classes.csv"))
|
|
||||||
{
|
{
|
||||||
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
|
Console.Error.WriteLine($"[BaseDataSeeder] Skipping seed: Data folder not found at {DataPath("")}");
|
||||||
csv.Context.RegisterClassMap<ClassEntryMap>();
|
return;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load rest of default data
|
List<ClassEntry> classes = ReadCsv<ClassEntry, ClassEntryMap>("classes.csv");
|
||||||
using (StreamReader reader = new("data/emblems.csv"))
|
List<LeaderSkinEntry> leaderSkins = ReadCsv<LeaderSkinEntry, LeaderSkinEntryMap>("leaderskins.csv");
|
||||||
|
leaderSkins.ForEach(skin =>
|
||||||
{
|
{
|
||||||
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
|
if (skin.ClassId == 0)
|
||||||
csv.Context.RegisterClassMap<EmblemEntryMap>();
|
{
|
||||||
|
skin.ClassId = null;
|
||||||
IEnumerable<EmblemEntry> records = csv.GetRecords<EmblemEntry>();
|
}
|
||||||
emblems.AddRange(records);
|
});
|
||||||
}
|
|
||||||
using (StreamReader reader = new("data/degrees.csv"))
|
List<EmblemEntry> emblems = ReadCsv<EmblemEntry, EmblemEntryMap>("emblems.csv");
|
||||||
{
|
List<DegreeEntry> degrees = ReadCsv<DegreeEntry, DegreeEntryMap>("degrees.csv");
|
||||||
using CsvReader csv = new(reader, CultureInfo.InvariantCulture);
|
List<SleeveEntry> sleeves = ReadCsv<SleeveEntry, SleeveEntryMap>("sleeves.csv");
|
||||||
csv.Context.RegisterClassMap<DegreeEntryMap>();
|
List<BattlefieldEntry> battlefields = ReadCsv<BattlefieldEntry, BattlefieldEntryMap>("battlefields.csv");
|
||||||
|
List<MyPageBackgroundEntry> myPageBackgrounds = ReadCsv<MyPageBackgroundEntry, MyPageBackgroundEntryMap>("mypagebackgrounds.csv");
|
||||||
IEnumerable<DegreeEntry> records = csv.GetRecords<DegreeEntry>();
|
List<RankInfoEntry> rankinfos = ReadCsv<RankInfoEntry, RankInfoEntryMap>("ranks.csv");
|
||||||
degrees.AddRange(records);
|
List<ClassExpEntry> classexp = ReadCsv<ClassExpEntry, ClassExpEntryMap>("classexp.csv");
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.Entity<ClassEntry>().HasData(classes);
|
builder.Entity<ClassEntry>().HasData(classes);
|
||||||
builder.Entity<LeaderSkinEntry>().HasData(leaderSkins);
|
builder.Entity<LeaderSkinEntry>().HasData(leaderSkins);
|
||||||
@@ -212,4 +157,4 @@ public class BaseDataSeeder : IDataSeeder
|
|||||||
builder.Entity<RankInfoEntry>().HasData(rankinfos);
|
builder.Entity<RankInfoEntry>().HasData(rankinfos);
|
||||||
builder.Entity<ClassExpEntry>().HasData(classexp);
|
builder.Entity<ClassExpEntry>().HasData(classexp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Interfaces;
|
using SVSim.Database.Common;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
33897
SVSim.Database/Migrations/20240929140334_Initial.Designer.cs
generated
33897
SVSim.Database/Migrations/20240929140334_Initial.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -25,78 +25,6 @@ namespace SVSim.Database.Migrations
|
|||||||
modelBuilder.HasSequence("ShortUdidSequence")
|
modelBuilder.HasSequence("ShortUdidSequence")
|
||||||
.StartsAt(400000000L);
|
.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 =>
|
modelBuilder.Entity("DegreeEntryViewer", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("DegreesId")
|
b.Property<int>("DegreesId")
|
||||||
@@ -177,7 +105,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("BattlefieldEntry");
|
b.ToTable("Battlefields");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -311,7 +239,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("ClassEntry");
|
b.ToTable("Classes");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -380,7 +308,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("ClassExpEntry");
|
b.ToTable("ClassExpCurve");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -1298,7 +1226,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("DegreeEntry");
|
b.ToTable("Degrees");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -10396,7 +10324,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("EmblemEntry");
|
b.ToTable("Emblems");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -21516,7 +21444,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasIndex("DefaultSleeveId");
|
b.HasIndex("DefaultSleeveId");
|
||||||
|
|
||||||
b.ToTable("GameConfiguration");
|
b.ToTable("GameConfigurations");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -21551,7 +21479,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("ItemEntry");
|
b.ToTable("Items");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b =>
|
modelBuilder.Entity("SVSim.Database.Models.LeaderSkinEntry", b =>
|
||||||
@@ -21579,7 +21507,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ClassId");
|
b.HasIndex("ClassId");
|
||||||
|
|
||||||
b.ToTable("LeaderSkinEntry");
|
b.ToTable("LeaderSkins");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -24929,7 +24857,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("MyPageBackgroundEntry");
|
b.ToTable("MyPageBackgrounds");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -25081,7 +25009,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("RankInfoEntry");
|
b.ToTable("RankInfo");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
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 =>
|
modelBuilder.Entity("SVSim.Database.Models.ShadowverseCardSetEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -25718,7 +25688,56 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
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 =>
|
modelBuilder.Entity("SVSim.Database.Models.SleeveEntry", b =>
|
||||||
@@ -25734,7 +25753,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("SleeveEntry");
|
b.ToTable("Sleeves");
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
@@ -33281,7 +33300,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ShortUdid");
|
b.HasIndex("ShortUdid");
|
||||||
|
|
||||||
b.ToTable("Viewer");
|
b.ToTable("Viewers");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SleeveEntryViewer", b =>
|
modelBuilder.Entity("SleeveEntryViewer", b =>
|
||||||
@@ -33299,106 +33318,6 @@ namespace SVSim.Database.Migrations
|
|||||||
b.ToTable("SleeveEntryViewer");
|
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 =>
|
modelBuilder.Entity("DegreeEntryViewer", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("SVSim.Database.Models.DegreeEntry", null)
|
b.HasOne("SVSim.Database.Models.DegreeEntry", null)
|
||||||
@@ -33503,6 +33422,108 @@ namespace SVSim.Database.Migrations
|
|||||||
b.Navigation("Class");
|
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 =>
|
modelBuilder.Entity("SVSim.Database.Models.Viewer", b =>
|
||||||
{
|
{
|
||||||
b.OwnsMany("SVSim.Database.Models.OwnedCardEntry", "Cards", b1 =>
|
b.OwnsMany("SVSim.Database.Models.OwnedCardEntry", "Cards", b1 =>
|
||||||
@@ -33531,7 +33552,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b1.ToTable("OwnedCardEntry");
|
b1.ToTable("OwnedCardEntry");
|
||||||
|
|
||||||
b1.HasOne("DCGEngine.Database.Models.CardEntry", "Card")
|
b1.HasOne("SVSim.Database.Models.ShadowverseCardEntry", "Card")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("CardId")
|
.HasForeignKey("CardId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -33694,7 +33715,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b1.HasKey("ViewerId");
|
b1.HasKey("ViewerId");
|
||||||
|
|
||||||
b1.ToTable("Viewer");
|
b1.ToTable("Viewers");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("ViewerId");
|
.HasForeignKey("ViewerId");
|
||||||
@@ -33733,7 +33754,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b1.HasIndex("SelectedEmblemId");
|
b1.HasIndex("SelectedEmblemId");
|
||||||
|
|
||||||
b1.ToTable("Viewer");
|
b1.ToTable("Viewers");
|
||||||
|
|
||||||
b1.HasOne("SVSim.Database.Models.DegreeEntry", "SelectedDegree")
|
b1.HasOne("SVSim.Database.Models.DegreeEntry", "SelectedDegree")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
@@ -33774,7 +33795,7 @@ namespace SVSim.Database.Migrations
|
|||||||
|
|
||||||
b1.HasKey("ViewerId");
|
b1.HasKey("ViewerId");
|
||||||
|
|
||||||
b1.ToTable("Viewer");
|
b1.ToTable("Viewers");
|
||||||
|
|
||||||
b1.WithOwner()
|
b1.WithOwner()
|
||||||
.HasForeignKey("ViewerId");
|
.HasForeignKey("ViewerId");
|
||||||
@@ -33813,67 +33834,6 @@ namespace SVSim.Database.Migrations
|
|||||||
.IsRequired();
|
.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 =>
|
modelBuilder.Entity("SVSim.Database.Models.ClassEntry", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("LeaderSkins");
|
b.Navigation("LeaderSkins");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using DCGEngine.Database.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
@@ -9,7 +8,7 @@ namespace SVSim.Database.Models;
|
|||||||
[Owned]
|
[Owned]
|
||||||
public class OwnedCardEntry
|
public class OwnedCardEntry
|
||||||
{
|
{
|
||||||
public CardEntry Card { get; set; } = new ShadowverseCardEntry();
|
public ShadowverseCardEntry Card { get; set; } = new ShadowverseCardEntry();
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
public bool IsProtected { get; set; }
|
public bool IsProtected { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,32 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using SVSim.Database.Common;
|
||||||
using DCGEngine.Database.Models;
|
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
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>
|
/// <summary>
|
||||||
/// The rarity of this card.
|
/// The rarity of this card.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -24,9 +44,9 @@ public class ShadowverseCardEntry : CardEntry
|
|||||||
#region Navigation Properties
|
#region Navigation Properties
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public ClassEntry? Class { get; set; }
|
public ClassEntry? Class { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using SVSim.Database.Common;
|
||||||
using DCGEngine.Database.Models;
|
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
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 IsInRotation { get; set; }
|
||||||
public bool IsBasic { get; set; }
|
public bool IsBasic { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
using DCGEngine.Database.Models;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using SVSim.Database.Common;
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
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 int Number { get; set; }
|
||||||
public Format Format { get; set; }
|
public Format Format { get; set; }
|
||||||
public bool RandomLeaderSkin { get; set; }
|
public bool RandomLeaderSkin { get; set; }
|
||||||
@@ -19,4 +30,4 @@ public class ShadowverseDeckEntry : DeckEntry
|
|||||||
public LeaderSkinEntry LeaderSkin { get; set; } = new LeaderSkinEntry();
|
public LeaderSkinEntry LeaderSkin { get; set; } = new LeaderSkinEntry();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using DCGEngine.Database.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SVSim.Database.Enums;
|
using SVSim.Database.Enums;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using DCGEngine.Database.Models;
|
using SVSim.Database.Common;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace SVSim.Database.Models;
|
namespace SVSim.Database.Models;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using DCGEngine.Database.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ public class CardRepository : BaseRepository<ShadowverseCardEntry>, ICardReposit
|
|||||||
public async Task<List<ShadowverseCardEntry>> GetAllBasic()
|
public async Task<List<ShadowverseCardEntry>> GetAllBasic()
|
||||||
{
|
{
|
||||||
return await DbContext.Set<ShadowverseCardSetEntry>().Where(set => set.IsBasic).SelectMany(set => set.Cards)
|
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)
|
public async Task<List<ShadowverseCardSetEntry>> GetCardSets(bool onlyInRotation)
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ public class ViewerRepository : IViewerRepository
|
|||||||
|
|
||||||
public async Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId)
|
public async Task<Models.Viewer?> GetViewerBySocialConnection(SocialAccountType accountType, ulong socialId)
|
||||||
{
|
{
|
||||||
return (await _dbContext.Set<SocialAccountConnection>()
|
// SocialAccountConnection is [Owned]-by-Viewer — can't be queried as a top-level Set<T>.
|
||||||
.AsNoTracking()
|
// Look up the Viewer that has a matching owned connection instead.
|
||||||
.Include(sac => sac.Viewer)
|
return await _dbContext.Set<Models.Viewer>()
|
||||||
.FirstOrDefaultAsync(sac => sac.AccountType == accountType && sac.AccountId == socialId))
|
.AsNoTracking()
|
||||||
?.Viewer;
|
.FirstOrDefaultAsync(v => v.SocialAccountConnections.Any(sac =>
|
||||||
|
sac.AccountType == accountType && sac.AccountId == socialId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Models.Viewer?> GetViewerWithSocials(long id)
|
public async Task<Models.Viewer?> GetViewerWithSocials(long id)
|
||||||
@@ -32,10 +33,31 @@ public class ViewerRepository : IViewerRepository
|
|||||||
.FirstOrDefaultAsync(viewer => viewer.Id == id);
|
.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)
|
public async Task<Models.Viewer?> GetViewerByShortUdid(long shortUdid)
|
||||||
{
|
{
|
||||||
return await _dbContext.Set<Models.Viewer>().AsNoTracking().Include(viewer => viewer.MissionData)
|
return await _dbContext.Set<Models.Viewer>()
|
||||||
.Include(viewer => viewer.Info).FirstOrDefaultAsync(viewer => viewer.ShortUdid == shortUdid);
|
.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,
|
public async Task<Models.Viewer> RegisterViewer(string displayName, SocialAccountType socialType,
|
||||||
@@ -46,7 +68,7 @@ public class ViewerRepository : IViewerRepository
|
|||||||
DisplayName = displayName
|
DisplayName = displayName
|
||||||
};
|
};
|
||||||
GameConfiguration gameConfig = await new GlobalsRepository(_dbContext).GetGameConfiguration("default");
|
GameConfiguration gameConfig = await new GlobalsRepository(_dbContext).GetGameConfiguration("default");
|
||||||
|
|
||||||
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
viewer.SocialAccountConnections.Add(new SocialAccountConnection
|
||||||
{
|
{
|
||||||
AccountId = socialAccountIdentifier,
|
AccountId = socialAccountIdentifier,
|
||||||
@@ -61,23 +83,40 @@ public class ViewerRepository : IViewerRepository
|
|||||||
viewer.Currency.RedEther = gameConfig.DefaultEther;
|
viewer.Currency.RedEther = gameConfig.DefaultEther;
|
||||||
viewer.MissionData.TutorialState = 100; // finishes tutorial for now
|
viewer.MissionData.TutorialState = 100; // finishes tutorial for now
|
||||||
|
|
||||||
List<ClassEntry> classes = await _dbContext.Set<ClassEntry>().ToListAsync();
|
// Load classes WITH their LeaderSkins — DefaultLeaderSkin iterates the nav collection
|
||||||
viewer.Classes = classes.Select(ce => new ViewerClassData
|
// 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,
|
var skin = ce.DefaultLeaderSkin ?? ce.LeaderSkins.FirstOrDefault();
|
||||||
Exp = 0,
|
return new ViewerClassData
|
||||||
Level = 0,
|
{
|
||||||
LeaderSkin = ce.DefaultLeaderSkin
|
Class = ce,
|
||||||
|
Exp = 0,
|
||||||
|
Level = 0,
|
||||||
|
LeaderSkin = skin ?? new LeaderSkinEntry { Id = 0, Name = "<missing>", ClassId = ce.Id }
|
||||||
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
viewer.Sleeves.Add(gameConfig.DefaultSleeve);
|
if (gameConfig.DefaultSleeve is not null) viewer.Sleeves.Add(gameConfig.DefaultSleeve);
|
||||||
viewer.Degrees.Add(gameConfig.DefaultDegree);
|
if (gameConfig.DefaultDegree is not null) viewer.Degrees.Add(gameConfig.DefaultDegree);
|
||||||
viewer.Emblems.Add(gameConfig.DefaultEmblem);
|
if (gameConfig.DefaultEmblem is not null) viewer.Emblems.Add(gameConfig.DefaultEmblem);
|
||||||
viewer.MyPageBackgrounds.Add(gameConfig.DefaultMyPageBackground);
|
if (gameConfig.DefaultMyPageBackground is not null) viewer.MyPageBackgrounds.Add(gameConfig.DefaultMyPageBackground);
|
||||||
viewer.LeaderSkins.AddRange(viewer.Classes.Select(vcd => vcd.LeaderSkin));
|
|
||||||
|
// 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);
|
_dbContext.Set<Models.Viewer>().Add(viewer);
|
||||||
await _dbContext.SaveChangesAsync();
|
await _dbContext.SaveChangesAsync();
|
||||||
return viewer;
|
return viewer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,11 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\DCGEngine.Database\DCGEngine.Database.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Data\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,30 +1,108 @@
|
|||||||
using DCGEngine.Database;
|
|
||||||
using DCGEngine.Database.Configuration;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using SVSim.Database.Common;
|
||||||
|
using SVSim.Database.DataSeeders;
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
|
|
||||||
namespace SVSim.Database;
|
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)
|
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>()
|
modelBuilder.Entity<SleeveEntry>()
|
||||||
.HasMany<Viewer>(se => se.Viewers)
|
.HasMany(se => se.Viewers)
|
||||||
.WithMany(v => v.Sleeves);
|
.WithMany(v => v.Sleeves);
|
||||||
|
|
||||||
modelBuilder.HasSequence<long>("ShortUdidSequence").StartsAt(400000000);
|
modelBuilder.HasSequence<long>("ShortUdidSequence").StartsAt(400000000);
|
||||||
modelBuilder.Entity<Viewer>()
|
modelBuilder.Entity<Viewer>()
|
||||||
.Property(v => v.ShortUdid)
|
.Property(v => v.ShortUdid)
|
||||||
.UseSequence("ShortUdidSequence");
|
.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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
using System.Buffers.Text;
|
|
||||||
using System.Text;
|
|
||||||
using MessagePack;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Serialization;
|
|
||||||
using SVSim.Database.Models;
|
using SVSim.Database.Models;
|
||||||
using SVSim.Database.Repositories.Viewer;
|
using SVSim.Database.Repositories.Viewer;
|
||||||
using SVSim.EmulatedEntrypoint.Extensions;
|
using SVSim.EmulatedEntrypoint.Extensions;
|
||||||
@@ -13,64 +7,68 @@ using SVSim.EmulatedEntrypoint.Models.Dtos;
|
|||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
using SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Controllers
|
namespace SVSim.EmulatedEntrypoint.Controllers;
|
||||||
|
|
||||||
|
public class CheckController : SVSimController
|
||||||
{
|
{
|
||||||
[Route("api/[controller]")]
|
private readonly ILogger _logger;
|
||||||
[ApiController]
|
private readonly IViewerRepository _viewerRepository;
|
||||||
public class CheckController : SVSimController
|
|
||||||
|
public CheckController(ILogger<CheckController> logger, IViewerRepository viewerRepository)
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
_logger = logger;
|
||||||
private readonly IViewerRepository _viewerRepository;
|
_viewerRepository = viewerRepository;
|
||||||
|
|
||||||
public CheckController(ILogger<CheckController> logger, IViewerRepository viewerRepository)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_viewerRepository = viewerRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpPost("special_title")]
|
|
||||||
public async Task<SpecialTitleCheckResponse> SpecialTitleCheck(SpecialTitleCheckRequest request)
|
|
||||||
{
|
|
||||||
int titleId = Random.Shared.Next(8, 33);
|
|
||||||
var res = new SpecialTitleCheckResponse
|
|
||||||
{
|
|
||||||
TitleImageId = titleId,
|
|
||||||
TitleSoundId = titleId
|
|
||||||
};
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("game_start")]
|
|
||||||
public async Task<GameStartResponse> GameStart(GameStartRequest request)
|
|
||||||
{
|
|
||||||
Viewer? viewer = await _viewerRepository.GetViewerWithSocials(HttpContext.GetViewer().Id);
|
|
||||||
return new GameStartResponse()
|
|
||||||
{
|
|
||||||
IsSetTransitionPassword = true,
|
|
||||||
KorAuthorityId = default,
|
|
||||||
KorAuthorityState = default,
|
|
||||||
NowRank = new Dictionary<string, string>()
|
|
||||||
{
|
|
||||||
{ "1", "RankName_010" },
|
|
||||||
{ "2", "RankName_010" },
|
|
||||||
{ "4", "RankName_017" }
|
|
||||||
},
|
|
||||||
NowName = viewer.DisplayName,
|
|
||||||
PolicyState = default,
|
|
||||||
PolicyId = default,
|
|
||||||
NowTutorialStep = "100",
|
|
||||||
NowViewerId = viewer.Id,
|
|
||||||
TosId = default,
|
|
||||||
TosState = default,
|
|
||||||
TransitionAccountData = viewer.SocialAccountConnections.Select(sac => new TransitionAccountData
|
|
||||||
{
|
|
||||||
ConnectedViewerId = viewer.Id.ToString(),
|
|
||||||
SocialAccountId = sac.AccountId.ToString(),
|
|
||||||
SocialAccountType = ((int)sac.AccountType).ToString()
|
|
||||||
}).ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("special_title")]
|
||||||
|
public Task<SpecialTitleCheckResponse> SpecialTitleCheck(SpecialTitleCheckRequest request)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new SpecialTitleCheckResponse
|
||||||
|
{
|
||||||
|
TitleImageId = "0"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: spec lists this as anonymous (identity from SHORT_UDID), but the base controller's
|
||||||
|
// [Authorize] still applies. For now requires a Steam-linked viewer; new-user bootstrap (where
|
||||||
|
// the server creates a viewer + returns rewrite_viewer_id) is deferred until the boot flow is
|
||||||
|
// exercised end-to-end with a real client.
|
||||||
|
[HttpPost("game_start")]
|
||||||
|
public async Task<GameStartResponse> GameStart(GameStartRequest request)
|
||||||
|
{
|
||||||
|
Viewer viewer = HttpContext.GetViewer()
|
||||||
|
?? throw new InvalidOperationException("Auth handler must set viewer in context.");
|
||||||
|
Viewer fullViewer = await _viewerRepository.GetViewerWithSocials(viewer.Id) ?? viewer;
|
||||||
|
|
||||||
|
return new GameStartResponse
|
||||||
|
{
|
||||||
|
NowViewerId = fullViewer.Id,
|
||||||
|
NowName = fullViewer.DisplayName,
|
||||||
|
NowTutorialStep = fullViewer.MissionData.TutorialState.ToString(),
|
||||||
|
IsSetTransitionPassword = true,
|
||||||
|
// Stub rank map until per-format ranks are persisted (prod observed: "1"/"2"/"4"
|
||||||
|
// keys mapping to RankName_010 / RankName_017). Empty dict here may be safe but
|
||||||
|
// we don't yet know which client paths read this — match prod stub.
|
||||||
|
NowRank = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "1", "RankName_010" },
|
||||||
|
{ "2", "RankName_010" },
|
||||||
|
{ "4", "RankName_017" }
|
||||||
|
},
|
||||||
|
TransitionAccountData = fullViewer.SocialAccountConnections
|
||||||
|
.Select(sac => new TransitionAccountData
|
||||||
|
{
|
||||||
|
SocialAccountId = sac.AccountId.ToString(),
|
||||||
|
SocialAccountType = ((int)sac.AccountType).ToString(),
|
||||||
|
ConnectedViewerId = fullViewer.Id.ToString()
|
||||||
|
}).ToList(),
|
||||||
|
TosState = 1,
|
||||||
|
PolicyState = 1,
|
||||||
|
KorAuthorityState = 0,
|
||||||
|
TosId = 1,
|
||||||
|
PolicyId = 1,
|
||||||
|
KorAuthorityId = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,12 +14,29 @@ namespace SVSim.EmulatedEntrypoint.Controllers;
|
|||||||
|
|
||||||
public class LoadController : SVSimController
|
public class LoadController : SVSimController
|
||||||
{
|
{
|
||||||
|
// Per-format rank entries the wire expects (5 entries, in deck_format discriminator order).
|
||||||
|
// Hard-coded until viewer rank-state is persisted (see audit §6 #1).
|
||||||
|
private static readonly Format[] RankFormats =
|
||||||
|
{
|
||||||
|
Format.Rotation, Format.Unlimited, Format.MyRotation, Format.Avatar, Format.Crossover
|
||||||
|
};
|
||||||
|
|
||||||
|
// Until ShadowverseCardSetEntry is seeded by CardImport, hard-code a stub so the client
|
||||||
|
// doesn't crash on RotationCardSetList[1] / [Count-1] (LoadDetail.cs:184).
|
||||||
|
private static readonly List<CardSetIdentifier> StubRotationSets = new()
|
||||||
|
{
|
||||||
|
new CardSetIdentifier { SetId = 10000 },
|
||||||
|
new CardSetIdentifier { SetId = 10005 },
|
||||||
|
new CardSetIdentifier { SetId = 10010 }
|
||||||
|
};
|
||||||
|
|
||||||
private readonly IViewerRepository _viewerRepository;
|
private readonly IViewerRepository _viewerRepository;
|
||||||
private readonly ICardRepository _cardRepository;
|
private readonly ICardRepository _cardRepository;
|
||||||
private readonly ICollectionRepository _collectionRepository;
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
private readonly IGlobalsRepository _globalsRepository;
|
private readonly IGlobalsRepository _globalsRepository;
|
||||||
|
|
||||||
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository, ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository)
|
public LoadController(IViewerRepository viewerRepository, ICardRepository cardRepository,
|
||||||
|
ICollectionRepository collectionRepository, IGlobalsRepository globalsRepository)
|
||||||
{
|
{
|
||||||
_viewerRepository = viewerRepository;
|
_viewerRepository = viewerRepository;
|
||||||
_cardRepository = cardRepository;
|
_cardRepository = cardRepository;
|
||||||
@@ -30,29 +47,30 @@ public class LoadController : SVSimController
|
|||||||
[HttpPost("index")]
|
[HttpPost("index")]
|
||||||
public async Task<ActionResult<IndexResponse>> Index(IndexRequest request)
|
public async Task<ActionResult<IndexResponse>> Index(IndexRequest request)
|
||||||
{
|
{
|
||||||
Viewer? viewer = await _viewerRepository.GetViewerByShortUdid(long.Parse(User.Claims
|
var shortUdidClaim = User.Claims.FirstOrDefault(c => c.Type == ShadowverseClaimTypes.ShortUdidClaim)?.Value;
|
||||||
.FirstOrDefault(claim => claim.Type == ShadowverseClaimTypes.ShortUdidClaim).Value));
|
if (shortUdidClaim is null || !long.TryParse(shortUdidClaim, out long shortUdid))
|
||||||
|
{
|
||||||
if (viewer == null)
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
Viewer? viewer = await _viewerRepository.GetViewerByShortUdid(shortUdid);
|
||||||
|
if (viewer is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cards. Empty until CardImport lands (audit §3 — user_card_list is blocked).
|
||||||
List<ShadowverseCardEntry> allCollectibleCards = await _cardRepository.GetAll(true);
|
List<ShadowverseCardEntry> allCollectibleCards = await _cardRepository.GetAll(true);
|
||||||
List<ShadowverseCardEntry> allBasicCards = await _cardRepository.GetAllBasic();
|
List<ShadowverseCardEntry> allBasicCards = await _cardRepository.GetAllBasic();
|
||||||
List<OwnedCardEntry> ownedCards = viewer.Cards;
|
List<OwnedCardEntry> ownedCards = viewer.Cards;
|
||||||
List<OwnedCardEntry> allCardsAsOwned = allCollectibleCards.GroupJoin(ownedCards, card => card.Id,
|
List<OwnedCardEntry> allCardsAsOwned = allCollectibleCards.GroupJoin(ownedCards,
|
||||||
|
card => card.Id,
|
||||||
ownedCard => ownedCard.Card.Id,
|
ownedCard => ownedCard.Card.Id,
|
||||||
(card, foundOwnedCards) =>
|
(card, foundOwnedCards) => foundOwnedCards.DefaultIfEmpty().FirstOrDefault() ?? new OwnedCardEntry
|
||||||
{
|
{
|
||||||
OwnedCardEntry ownedCard = foundOwnedCards.DefaultIfEmpty().FirstOrDefault() ?? new OwnedCardEntry
|
Card = card,
|
||||||
{
|
Count = 0,
|
||||||
Card = card,
|
IsProtected = false
|
||||||
Count = 0,
|
|
||||||
IsProtected = false
|
|
||||||
};
|
|
||||||
|
|
||||||
return ownedCard;
|
|
||||||
}).ToList();
|
}).ToList();
|
||||||
allCardsAsOwned = allCardsAsOwned.Union(allBasicCards.Select(bc => new OwnedCardEntry
|
allCardsAsOwned = allCardsAsOwned.Union(allBasicCards.Select(bc => new OwnedCardEntry
|
||||||
{
|
{
|
||||||
@@ -60,14 +78,11 @@ public class LoadController : SVSimController
|
|||||||
Count = 3,
|
Count = 3,
|
||||||
IsProtected = true
|
IsProtected = true
|
||||||
})).ToList();
|
})).ToList();
|
||||||
|
|
||||||
List<LeaderSkinEntry> allLeaderSkins = await _collectionRepository.GetLeaderSkins();
|
|
||||||
|
|
||||||
Dictionary<string, ClassExp> classExp = new Dictionary<string, ClassExp>();
|
List<LeaderSkinEntry> allLeaderSkins = await _collectionRepository.GetLeaderSkins();
|
||||||
var classExpCurve = await _globalsRepository.GetClassExpCurve();
|
var classExpCurve = await _globalsRepository.GetClassExpCurve();
|
||||||
|
|
||||||
List<ClassExp> classExps = new List<ClassExp>();
|
List<ClassExp> classExps = new();
|
||||||
|
|
||||||
int accumulateExp = 0;
|
int accumulateExp = 0;
|
||||||
int? prevNecessaryExp = null;
|
int? prevNecessaryExp = null;
|
||||||
foreach (var entry in classExpCurve)
|
foreach (var entry in classExpCurve)
|
||||||
@@ -75,7 +90,7 @@ public class LoadController : SVSimController
|
|||||||
accumulateExp += entry.NecessaryExp;
|
accumulateExp += entry.NecessaryExp;
|
||||||
classExps.Add(new ClassExp
|
classExps.Add(new ClassExp
|
||||||
{
|
{
|
||||||
Level = entry.Id, // You need to specify the level value based on your logic
|
Level = entry.Id,
|
||||||
NecessaryExp = entry.NecessaryExp,
|
NecessaryExp = entry.NecessaryExp,
|
||||||
DiffExp = prevNecessaryExp.HasValue ? entry.NecessaryExp - prevNecessaryExp.Value : entry.NecessaryExp,
|
DiffExp = prevNecessaryExp.HasValue ? entry.NecessaryExp - prevNecessaryExp.Value : entry.NecessaryExp,
|
||||||
AccumulateExp = accumulateExp
|
AccumulateExp = accumulateExp
|
||||||
@@ -83,37 +98,53 @@ public class LoadController : SVSimController
|
|||||||
prevNecessaryExp = entry.NecessaryExp;
|
prevNecessaryExp = entry.NecessaryExp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<CardSetIdentifier> rotationSets = (await _cardRepository.GetCardSets(true))
|
||||||
|
.Select(set => new CardSetIdentifier { SetId = set.Id })
|
||||||
|
.ToList();
|
||||||
|
if (rotationSets.Count < 2)
|
||||||
|
{
|
||||||
|
rotationSets = StubRotationSets;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviceHeader = Request.Headers["DEVICE"].FirstOrDefault();
|
||||||
|
int deviceType = int.TryParse(deviceHeader, out int parsed) ? parsed : 0;
|
||||||
|
|
||||||
return new IndexResponse
|
return new IndexResponse
|
||||||
{
|
{
|
||||||
UserTutorial = new UserTutorial
|
UserTutorial = new UserTutorial
|
||||||
{
|
{
|
||||||
TutorialStep = viewer.MissionData.TutorialState
|
TutorialStep = viewer.MissionData.TutorialState
|
||||||
},
|
},
|
||||||
UserInfo = new UserInfo(int.Parse(Request.Headers["DEVICE"].FirstOrDefault()), viewer),
|
UserInfo = new UserInfo(deviceType, viewer),
|
||||||
UserCurrency = new UserCurrency(viewer),
|
UserCurrency = new UserCurrency(viewer),
|
||||||
UserItems = viewer.Items.Select(item => new UserItem(item)).ToList(),
|
UserItems = viewer.Items.Select(item => new UserItem(item)).ToList(),
|
||||||
UserRotationDecks = new UserFormatDeckInfo
|
UserRotationDecks = new UserFormatDeckInfo
|
||||||
{
|
{
|
||||||
UserDecks = viewer.Decks.Where(deck => deck.Format == Format.Rotation).Select(deck => new UserDeck(deck)).ToList()
|
UserDecks = viewer.Decks.Where(d => d.Format == Format.Rotation)
|
||||||
|
.Select(d => new UserDeck(d)).ToList()
|
||||||
},
|
},
|
||||||
UserUnlimitedDecks = new UserFormatDeckInfo
|
UserUnlimitedDecks = new UserFormatDeckInfo
|
||||||
{
|
{
|
||||||
UserDecks = viewer.Decks.Where(deck => deck.Format == Format.Unlimited).Select(deck => new UserDeck(deck)).ToList()
|
UserDecks = viewer.Decks.Where(d => d.Format == Format.Unlimited)
|
||||||
|
.Select(d => new UserDeck(d)).ToList()
|
||||||
},
|
},
|
||||||
UserMyRotationDecks = new UserFormatDeckInfo
|
UserMyRotationDecks = new UserFormatDeckInfo
|
||||||
{
|
{
|
||||||
UserDecks = viewer.Decks.Where(deck => deck.Format == Format.MyRotation).Select(deck => new UserDeck(deck)).ToList()
|
UserDecks = viewer.Decks.Where(d => d.Format == Format.MyRotation)
|
||||||
|
.Select(d => new UserDeck(d)).ToList()
|
||||||
},
|
},
|
||||||
UserCards = allCardsAsOwned.Select(card => new UserCard(card)).ToList(),
|
UserCards = allCardsAsOwned.Select(card => new UserCard(card)).ToList(),
|
||||||
UserClasses = viewer.Classes.Select(viewerClass => new UserClass(viewerClass)).ToList(),
|
UserClasses = viewer.Classes.Select(vc => new UserClass(vc)).ToList(),
|
||||||
Sleeves = viewer.Sleeves.ToDictionary(sleeve => sleeve.Id.ToString(), sleeve => new SleeveIdentifier { SleeveId = sleeve.Id }),
|
Sleeves = viewer.Sleeves.Select(s => new SleeveIdentifier { SleeveId = s.Id }).ToList(),
|
||||||
UserEmblems = viewer.Emblems.Select(emblem => new EmblemIdentifier { EmblemId = emblem.Id }).ToList(),
|
UserEmblems = viewer.Emblems.Select(e => new EmblemIdentifier { EmblemId = e.Id }).ToList(),
|
||||||
UserDegrees = viewer.Degrees.Select(degree => new DegreeIdentifier { DegreeId = degree.Id }).ToList(),
|
UserDegrees = viewer.Degrees.Select(d => new DegreeIdentifier { DegreeId = d.Id }).ToList(),
|
||||||
LeaderSkins = allLeaderSkins.ToDictionary(skin => skin.Id.ToString(), skin => new UserLeaderSkin(skin, viewer.LeaderSkins.Any(vs => vs.Id == skin.Id))),
|
LeaderSkins = allLeaderSkins
|
||||||
MyPageBackgrounds = viewer.MyPageBackgrounds.Select(mpbg => mpbg.Id).ToList(),
|
.Select(skin => new UserLeaderSkin(skin, viewer.LeaderSkins.Any(vs => vs.Id == skin.Id)))
|
||||||
|
.ToList(),
|
||||||
|
MyPageBackgrounds = viewer.MyPageBackgrounds.Select(mpbg => mpbg.Id.ToString()).ToList(),
|
||||||
LootBoxRegulations = new LootBoxRegulations(),
|
LootBoxRegulations = new LootBoxRegulations(),
|
||||||
GatheringInfo = new GatheringInfo(),
|
GatheringInfo = new GatheringInfo(),
|
||||||
IsBattlePassPeriod = false,
|
IsBattlePassPeriod = 0,
|
||||||
BattlePassLevelInfo = null,
|
BattlePassLevelInfo = null,
|
||||||
SpecialCrystalInfos = new List<SpecialCrystalInfo>(),
|
SpecialCrystalInfos = new List<SpecialCrystalInfo>(),
|
||||||
AvatarRotationInfo = null,
|
AvatarRotationInfo = null,
|
||||||
@@ -121,22 +152,35 @@ public class LoadController : SVSimController
|
|||||||
FeatureMaintenances = new List<FeatureMaintenance>(),
|
FeatureMaintenances = new List<FeatureMaintenance>(),
|
||||||
PreReleaseInfo = null,
|
PreReleaseInfo = null,
|
||||||
SpotCards = new Dictionary<string, int>(),
|
SpotCards = new Dictionary<string, int>(),
|
||||||
ReprintedCards = new Dictionary<string, long>(),
|
ReprintedCards = new List<long>(),
|
||||||
UnlimitedBanList = new Dictionary<string, int>(),
|
UnlimitedBanList = new Dictionary<string, int>(),
|
||||||
LoadingTipCardExclusions = new List<long>(),
|
LoadingTipCardExclusions = new List<long>(),
|
||||||
MaintenanceCards = new List<CardIdentifier>(),
|
MaintenanceCards = new List<long>(),
|
||||||
RedEtherOverrides = new List<RedEtherOverride>(),
|
RedEtherOverrides = new List<RedEtherOverride>(),
|
||||||
DailyLoginBonus = new DailyLoginBonus(),
|
DailyLoginBonus = new DailyLoginBonus(),
|
||||||
UserRankedMatches = new List<UserRankedMatches>(),
|
UserRankedMatches = new List<UserRankedMatches>(),
|
||||||
UserRankInfo = new Dictionary<string, UserRankInfo>(),
|
UserRankInfo = RankFormats.Select(f => new UserRankInfo
|
||||||
|
{
|
||||||
|
DeckFormat = (int)f,
|
||||||
|
Rank = 1,
|
||||||
|
BattlePoints = 0,
|
||||||
|
WinStreak = 0,
|
||||||
|
IsPromotion = 0,
|
||||||
|
IsMasterRank = 0,
|
||||||
|
IsGrandMasterRank = 0,
|
||||||
|
MasterPoints = 0
|
||||||
|
}).ToList(),
|
||||||
ArenaConfig = new ArenaConfig(),
|
ArenaConfig = new ArenaConfig(),
|
||||||
ArenaInfos = new List<ArenaInfo>(),
|
ArenaInfos = new List<ArenaInfo>(),
|
||||||
RotationSets = (await _cardRepository.GetCardSets(true)).Select(set => new CardSetIdentifier { SetId = set.Id }).ToList(),
|
RotationSets = rotationSets,
|
||||||
UserConfig = new UserConfig(),
|
UserConfig = new UserConfig(),
|
||||||
OpenBattlefieldIds = (await _globalsRepository.GetBattlefields(true)).ToDictionary(bf => bf.Id.ToString(), bf => bf.Id),
|
OpenBattlefieldIds = (await _globalsRepository.GetBattlefields(true))
|
||||||
|
.Select(bf => bf.Id.ToString()).ToList(),
|
||||||
DefaultSettings = new DefaultSettings(await _globalsRepository.GetGameConfiguration("default")),
|
DefaultSettings = new DefaultSettings(await _globalsRepository.GetGameConfiguration("default")),
|
||||||
ClassExp = classExps.ToDictionary(kv => kv.Level.ToString(), kv => kv),
|
ClassExp = classExps,
|
||||||
RankInfo = (await _globalsRepository.GetRankInfo()).Select(ri => new RankInfo(ri)).ToDictionary(ri => ri.RankId.ToString(), ri => ri)
|
RankInfo = (await _globalsRepository.GetRankInfo()).Select(ri => new RankInfo(ri)).ToList(),
|
||||||
|
DeckFormat = 1,
|
||||||
|
CardSetIdForResourceDlView = rotationSets.Last().SetId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace SVSim.EmulatedEntrypoint.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A base controller for SVSim with helpers for getting some values.
|
/// A base controller for SVSim with helpers for getting some values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("api/[controller]")]
|
[Route("[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize(AuthenticationSchemes = SteamAuthenticationConstants.SchemeName)]
|
[Authorize(AuthenticationSchemes = SteamAuthenticationConstants.SchemeName)]
|
||||||
public abstract class SVSimController : ControllerBase
|
public abstract class SVSimController : ControllerBase
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Conventions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures controllers go to the correct swagger definition.
|
|
||||||
/// </summary>
|
|
||||||
public class SwaggerDefinitionConvention : IControllerModelConvention
|
|
||||||
{
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public void Apply(ControllerModel controller)
|
|
||||||
{
|
|
||||||
var controllerNamespace = controller.ControllerType.Namespace; // eg. Controllers.V1
|
|
||||||
var swaggerDefinition = controllerNamespace.Split('.').Last().ToLower();
|
|
||||||
const string defaultRoute = "api/[controller]";
|
|
||||||
|
|
||||||
controller.ApiExplorer.GroupName = swaggerDefinition;
|
|
||||||
foreach (SelectorModel selector in controller.Selectors)
|
|
||||||
{
|
|
||||||
if (selector.AttributeRouteModel != null && selector.AttributeRouteModel.Template == defaultRoute)
|
|
||||||
{
|
|
||||||
selector.AttributeRouteModel.Template = $"api/{swaggerDefinition}/[controller]";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,5 +6,5 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos;
|
|||||||
public class EmblemIdentifier
|
public class EmblemIdentifier
|
||||||
{
|
{
|
||||||
[Key("emblem_id")]
|
[Key("emblem_id")]
|
||||||
public int EmblemId { get; set; }
|
public long EmblemId { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
|
using MessagePack;
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Requests;
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
public class IndexRequest : BaseRequest
|
public class IndexRequest : BaseRequest
|
||||||
{
|
{
|
||||||
|
[Key("carrier")]
|
||||||
public string Carrier { get; set; }
|
public string Carrier { get; set; }
|
||||||
|
|
||||||
|
[Key("card_master_hash")]
|
||||||
public string CardMasterHash { get; set; }
|
public string CardMasterHash { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,31 +2,83 @@ using MessagePack;
|
|||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wire-shape mirrors production's <c>/check/game_start</c> response. Several fields here are
|
||||||
|
/// NOT read by <c>Cute/GameStartCheckTask.Parse</c> (<c>now_viewer_id</c>, <c>now_name</c>,
|
||||||
|
/// <c>now_rank</c> — those are consumed by sibling tasks); they're included because prod sends
|
||||||
|
/// them and the boot worked when we matched prod exactly. Removing them is a regression risk
|
||||||
|
/// even though the parse-time decompile says they're unused.
|
||||||
|
/// </summary>
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public class GameStartResponse
|
public class GameStartResponse
|
||||||
{
|
{
|
||||||
|
/// <summary>The signed-in viewer's internal id. Prod always sends.</summary>
|
||||||
[Key("now_viewer_id")]
|
[Key("now_viewer_id")]
|
||||||
public long NowViewerId { get; set; }
|
public long NowViewerId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the user has set a data-transfer password. Prod sends a non-null bool;
|
||||||
|
/// <c>GameStartCheckTask.Parse</c> gates the read with <c>Keys.Contains</c>.
|
||||||
|
/// </summary>
|
||||||
[Key("is_set_transition_password")]
|
[Key("is_set_transition_password")]
|
||||||
public bool IsSetTransitionPassword { get; set; }
|
public bool IsSetTransitionPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Viewer display name. Not read by GameStartCheckTask but sent by prod.</summary>
|
||||||
[Key("now_name")]
|
[Key("now_name")]
|
||||||
public string NowName { get; set; }
|
public string NowName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-format rank-name map keyed by deck-format id ("1", "2", "4" observed in prod).
|
||||||
|
/// Stub for now until rank state is persisted; pinned to RankName_010 / RankName_017
|
||||||
|
/// (matches prod's shape).
|
||||||
|
/// </summary>
|
||||||
[Key("now_rank")]
|
[Key("now_rank")]
|
||||||
public Dictionary<string, string> NowRank { get; set; }
|
public Dictionary<string, string> NowRank { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tutorial progress — **sent as a string on the wire** ("100" = tutorial complete).
|
||||||
|
/// <c>GameStartCheckTask.Parse</c> calls <c>.ToInt()</c> so LitJson coerces.
|
||||||
|
/// </summary>
|
||||||
[Key("now_tutorial_step")]
|
[Key("now_tutorial_step")]
|
||||||
public string NowTutorialStep { get; set; }
|
public string NowTutorialStep { get; set; } = "100";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Linked social accounts. Per-entry shape in <see cref="TransitionAccountData"/>.
|
||||||
|
/// </summary>
|
||||||
[Key("transition_account_data")]
|
[Key("transition_account_data")]
|
||||||
public List<TransitionAccountData> TransitionAccountData { get; set; }
|
public List<TransitionAccountData> TransitionAccountData { get; set; } = new();
|
||||||
|
|
||||||
|
// INTENTIONALLY OMITTED: `rewrite_viewer_id` and `account_delete_reservation_status`.
|
||||||
|
// Both are presence-checked by the client via `Keys.Contains(...)` + `.ToInt()` with no
|
||||||
|
// null guard. MessagePack-CSharp writes [Key] properties unconditionally (null → Nil),
|
||||||
|
// and the System.Text.Json `WhenWritingNull` ignore only affects the plain-JSON path.
|
||||||
|
// So including these as nullable properties is a guaranteed NRE on the encrypted client
|
||||||
|
// path. We don't need them — the client tolerates their absence — so don't declare them.
|
||||||
|
// Re-add only if we have a real value to send.
|
||||||
|
|
||||||
|
// --- Agreement / consent state (all required) ---
|
||||||
|
|
||||||
|
/// <summary><c>PlayerStaticData.AgreementState</c> enum.</summary>
|
||||||
[Key("tos_state")]
|
[Key("tos_state")]
|
||||||
public int TosState { get; set; }
|
public int TosState { get; set; }
|
||||||
[Key("tos_id")]
|
|
||||||
public int TosId { get; set; }
|
/// <summary><c>PlayerStaticData.AgreementState</c> enum.</summary>
|
||||||
[Key("policy_state")]
|
[Key("policy_state")]
|
||||||
public int PolicyState { get; set; }
|
public int PolicyState { get; set; }
|
||||||
[Key("policy_id")]
|
|
||||||
public int PolicyId { get; set; }
|
/// <summary><c>PlayerStaticData.AgreementState</c> enum.</summary>
|
||||||
[Key("kor_authority_id")]
|
|
||||||
public int KorAuthorityId { get; set; }
|
|
||||||
[Key("kor_authority_state")]
|
[Key("kor_authority_state")]
|
||||||
public int KorAuthorityState { get; set; }
|
public int KorAuthorityState { get; set; }
|
||||||
}
|
|
||||||
|
/// <summary>Current Terms of Service document id.</summary>
|
||||||
|
[Key("tos_id")]
|
||||||
|
public int TosId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Current Privacy Policy document id.</summary>
|
||||||
|
[Key("policy_id")]
|
||||||
|
public int PolicyId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Current Korean authority consent document id.</summary>
|
||||||
|
[Key("kor_authority_id")]
|
||||||
|
public int KorAuthorityId { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ public class IndexResponse
|
|||||||
{
|
{
|
||||||
#region Primitive Returns
|
#region Primitive Returns
|
||||||
|
|
||||||
[Key("ts_card_rotation")]
|
|
||||||
public string TsCardRotation { get; set; } = string.Empty;
|
|
||||||
[Key("is_beginner_mission")]
|
|
||||||
public int IsBeginnerMission { get; set; }
|
|
||||||
[Key("spot_point")]
|
[Key("spot_point")]
|
||||||
public int SpotPoint { get; set; }
|
public int SpotPoint { get; set; }
|
||||||
[Key("is_available_colosseum_free_entry")]
|
[Key("is_available_colosseum_free_entry")]
|
||||||
@@ -23,29 +19,22 @@ public class IndexResponse
|
|||||||
[Key("room_recovery_status")]
|
[Key("room_recovery_status")]
|
||||||
public int RoomRecoveryStatus { get; set; }
|
public int RoomRecoveryStatus { get; set; }
|
||||||
[Key("is_battle_pass_period")]
|
[Key("is_battle_pass_period")]
|
||||||
public bool IsBattlePassPeriod { get; set; }
|
public int IsBattlePassPeriod { get; set; }
|
||||||
[Key("card_set_id_for_resource_dl_view")]
|
[Key("card_set_id_for_resource_dl_view")]
|
||||||
public int CardSetIdForResourceDlView { get; set; }
|
public int CardSetIdForResourceDlView { get; set; }
|
||||||
|
[Key("deck_format")]
|
||||||
|
public int DeckFormat { get; set; } = 1;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Basic User Data
|
#region Basic User Data
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The user's tutorial progress state.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_tutorial")]
|
[Key("user_tutorial")]
|
||||||
public UserTutorial UserTutorial { get; set; } = new UserTutorial();
|
public UserTutorial UserTutorial { get; set; } = new UserTutorial();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Basic information about the user.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_info")]
|
[Key("user_info")]
|
||||||
public UserInfo UserInfo { get; set; } = new UserInfo();
|
public UserInfo UserInfo { get; set; } = new UserInfo();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The in-game currency information for this user.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_crystal_count")]
|
[Key("user_crystal_count")]
|
||||||
public UserCurrency UserCurrency { get; set; } = new UserCurrency();
|
public UserCurrency UserCurrency { get; set; } = new UserCurrency();
|
||||||
|
|
||||||
@@ -53,234 +42,163 @@ public class IndexResponse
|
|||||||
|
|
||||||
#region Inventory Data
|
#region Inventory Data
|
||||||
|
|
||||||
/// <summary>
|
[Key("user_item_list")]
|
||||||
/// Items that the user has and how many of each.
|
public List<UserItem> UserItems { get; set; } = new();
|
||||||
/// </summary>
|
|
||||||
[Key("user_item_list")]
|
|
||||||
public List<UserItem> UserItems { get; set; } = new List<UserItem>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decks for the rotation format.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_deck_rotation")]
|
[Key("user_deck_rotation")]
|
||||||
public UserFormatDeckInfo UserRotationDecks { get; set; } = new UserFormatDeckInfo();
|
public UserFormatDeckInfo UserRotationDecks { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decks for the unlimited format.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_deck_unlimited")]
|
[Key("user_deck_unlimited")]
|
||||||
public UserFormatDeckInfo UserUnlimitedDecks { get; set; } = new UserFormatDeckInfo();
|
public UserFormatDeckInfo UserUnlimitedDecks { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decks for the unlimited format.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_deck_my_rotation")]
|
[Key("user_deck_my_rotation")]
|
||||||
public UserFormatDeckInfo UserMyRotationDecks { get; set; } = new UserFormatDeckInfo();
|
public UserFormatDeckInfo UserMyRotationDecks { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The list of cards and how many of each this user has.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_card_list")]
|
[Key("user_card_list")]
|
||||||
public List<UserCard> UserCards { get; set; } = new List<UserCard>();
|
public List<UserCard> UserCards { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The classes a user has and their stats.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_class_list")]
|
[Key("user_class_list")]
|
||||||
public List<UserClass> UserClasses { get; set; } = new List<UserClass>();
|
public List<UserClass> UserClasses { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mapping of SleeveId to a <see cref="SleeveIdentifier"/> object.
|
/// Wire is an array; parser iterates by index (LoadDetail.cs:358-360).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("user_sleeve_list")]
|
[Key("user_sleeve_list")]
|
||||||
public Dictionary<string, SleeveIdentifier> Sleeves { get; set; } = new Dictionary<string, SleeveIdentifier>();
|
public List<SleeveIdentifier> Sleeves { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The emblems available to this user.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_emblem_list")]
|
[Key("user_emblem_list")]
|
||||||
public List<EmblemIdentifier> UserEmblems { get; set; } = new List<EmblemIdentifier>();
|
public List<EmblemIdentifier> UserEmblems { get; set; } = new();
|
||||||
|
|
||||||
|
[Key("user_degree_list")]
|
||||||
|
public List<DegreeIdentifier> UserDegrees { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The degrees available to this user.
|
/// Wire is an array; parser iterates by index (LoadDetail.cs:348-356).
|
||||||
/// </summary>
|
|
||||||
[Key("degree_id")]
|
|
||||||
public List<DegreeIdentifier> UserDegrees { get; set; } = new List<DegreeIdentifier>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Leader skins available to the leader.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("user_leader_skin_list")]
|
[Key("user_leader_skin_list")]
|
||||||
public Dictionary<string, UserLeaderSkin> LeaderSkins { get; set; } = new Dictionary<string, UserLeaderSkin>();
|
public List<UserLeaderSkin> LeaderSkins { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Backgrounds for 'My Page' the user has collected.
|
/// Wire is string[]; parser calls .ToString() on each element (LoadDetail.cs:387-392).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("user_mypage_list")]
|
[Key("user_mypage_list")]
|
||||||
public List<int> MyPageBackgrounds { get; set; } = new List<int>();
|
public List<string> MyPageBackgrounds { get; set; } = new();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Advanced Player Data
|
#region Advanced Player Data
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps a deck format (as a string) to info about ranked for that format.
|
/// Wire is an array of 5 entries; parser uses deck_format as discriminator
|
||||||
|
/// (LoadDetail.cs:527-538).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("user_rank")]
|
[Key("user_rank")]
|
||||||
public Dictionary<string, UserRankInfo> UserRankInfo { get; set; } = new Dictionary<string, UserRankInfo>();
|
public List<UserRankInfo> UserRankInfo { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The number of ranked matches for each class the user has played.
|
|
||||||
/// </summary>
|
|
||||||
[Key("user_rank_match_list")]
|
[Key("user_rank_match_list")]
|
||||||
public List<UserRankedMatches> UserRankedMatches { get; set; } = new List<UserRankedMatches>();
|
public List<UserRankedMatches> UserRankedMatches { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The daily login bonuses currently going on, including if the player should receive the next reward for any.
|
|
||||||
/// </summary>
|
|
||||||
[Key("daily_login_bonus")]
|
[Key("daily_login_bonus")]
|
||||||
public DailyLoginBonus DailyLoginBonus { get; set; } = new DailyLoginBonus();
|
public DailyLoginBonus DailyLoginBonus { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// User configuration for the arena.
|
|
||||||
/// </summary>
|
|
||||||
[Key("challenge_config")]
|
[Key("challenge_config")]
|
||||||
public ArenaConfig ArenaConfig { get; set; } = new ArenaConfig();
|
public ArenaConfig ArenaConfig { get; set; } = new();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Global Data
|
#region Global Data
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cards that have had their red ether values overriden.
|
|
||||||
/// </summary>
|
|
||||||
[Key("red_ether_overwrite_list")]
|
[Key("red_ether_overwrite_list")]
|
||||||
public List<RedEtherOverride> RedEtherOverrides { get; set; } = new List<RedEtherOverride>();
|
public List<RedEtherOverride> RedEtherOverrides { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cards that are currently undergoing maintenance.
|
/// Wire is a flat number[]; parser passes it straight to SetMaintenanceCardIds
|
||||||
|
/// (LoadDetail.cs:165).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("maintenance_card_list")]
|
[Key("maintenance_card_list")]
|
||||||
public List<CardIdentifier> MaintenanceCards { get; set; } = new List<CardIdentifier>();
|
public List<long> MaintenanceCards { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The arena formats currently available.
|
|
||||||
/// </summary>
|
|
||||||
[Key("arena_info")]
|
[Key("arena_info")]
|
||||||
public List<ArenaInfo> ArenaInfos { get; set; } = new List<ArenaInfo>();
|
public List<ArenaInfo> ArenaInfos { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dictionary of rank id to information about that rank.
|
/// Wire is an array; client uses POSITIONAL logic (index >= 24 = master ranks,
|
||||||
|
/// LoadDetail.cs:417-422). Order must match repository's ordering.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("rank_info")]
|
[Key("rank_info")]
|
||||||
public Dictionary<string, RankInfo> RankInfo { get; set; } = new Dictionary<string, RankInfo>();
|
public List<RankInfo> RankInfo { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dictionary mapping a class level to information about that level.
|
/// Wire is an array; parser iterates by index (LoadDetail.cs:425-434).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("class_exp")]
|
[Key("class_exp")]
|
||||||
public Dictionary<string, ClassExp> ClassExp { get; set; } = new Dictionary<string, ClassExp>();
|
public List<ClassExp> ClassExp { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Card ids that should not show up on loading screen tips.
|
|
||||||
/// </summary>
|
|
||||||
[Key("loading_exclusion_card_list")]
|
[Key("loading_exclusion_card_list")]
|
||||||
public List<long> LoadingTipCardExclusions { get; set; } = new List<long>();
|
public List<long> LoadingTipCardExclusions { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The default settings for every user.
|
|
||||||
/// </summary>
|
|
||||||
[Key("default_setting")]
|
[Key("default_setting")]
|
||||||
public DefaultSettings DefaultSettings { get; set; } = new DefaultSettings();
|
public DefaultSettings DefaultSettings { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Any cards that are restricted in unlimited, and the maximum count that can be run of the card.
|
|
||||||
/// </summary>
|
|
||||||
[Key("unlimited_restricted_base_card_id_list")]
|
[Key("unlimited_restricted_base_card_id_list")]
|
||||||
public Dictionary<string, int> UnlimitedBanList { get; set; } = new Dictionary<string, int>();
|
public Dictionary<string, int> UnlimitedBanList { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets currently available in rotation.
|
/// Client unconditionally accesses [1] and [Count-1] (LoadDetail.cs:184) — list MUST
|
||||||
|
/// have at least 2 entries or the client crashes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("rotation_card_set_id_list")]
|
[Key("rotation_card_set_id_list")]
|
||||||
public List<CardSetIdentifier> RotationSets { get; set; } = new List<CardSetIdentifier>();
|
public List<CardSetIdentifier> RotationSets { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Allows cards out of your 'My Rotation' to still be used. TODO investigate
|
/// Wire is a flat number[]; parser iterates and reads .ToInt() (LoadDetail.cs:463-468).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("reprinted_base_card_ids")]
|
[Key("reprinted_base_card_ids")]
|
||||||
public Dictionary<string, long> ReprintedCards { get; set; } = new Dictionary<string, long>();
|
public List<long> ReprintedCards { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Something to do with destroying cards. TODO investigate
|
|
||||||
/// </summary>
|
|
||||||
[Key("spot_cards")]
|
[Key("spot_cards")]
|
||||||
public Dictionary<string, int> SpotCards { get; set; } = new Dictionary<string, int>();
|
public Dictionary<string, int> SpotCards { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Info about the next set to be released? TODO investigate
|
|
||||||
/// </summary>
|
|
||||||
[Key("pre_release_info")]
|
[Key("pre_release_info")]
|
||||||
public PreReleaseInfo? PreReleaseInfo { get; set; }
|
public PreReleaseInfo? PreReleaseInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Information for the 'My Rotation' mode.
|
|
||||||
/// </summary>
|
|
||||||
[Key("my_rotation_info")]
|
[Key("my_rotation_info")]
|
||||||
public MyRotationInfo? MyRotationInfo { get; set; }
|
public MyRotationInfo? MyRotationInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Information about some avatar mode? TODO investigate
|
|
||||||
/// </summary>
|
|
||||||
[Key("avatar_info")]
|
[Key("avatar_info")]
|
||||||
public MyRotationInfo? AvatarRotationInfo { get; set; }
|
public MyRotationInfo? AvatarRotationInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// List of features that are undergoing maintenance.
|
|
||||||
/// </summary>
|
|
||||||
[Key("feature_maintenance_list")]
|
[Key("feature_maintenance_list")]
|
||||||
public List<FeatureMaintenance> FeatureMaintenances { get; set; } = new List<FeatureMaintenance>();
|
public List<FeatureMaintenance> FeatureMaintenances { get; set; } = new();
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Special deals on crystal purchases.
|
|
||||||
/// </summary>
|
|
||||||
[Key("special_crystal_info")]
|
[Key("special_crystal_info")]
|
||||||
public List<SpecialCrystalInfo> SpecialCrystalInfos { get; set; } = new List<SpecialCrystalInfo>();
|
public List<SpecialCrystalInfo> SpecialCrystalInfos { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current battle pass levels and required points for each.
|
|
||||||
/// </summary>
|
|
||||||
[Key("battle_pass_level_info")]
|
[Key("battle_pass_level_info")]
|
||||||
public Dictionary<string, BattlePassLevel>? BattlePassLevelInfo { get; set; }
|
public Dictionary<string, BattlePassLevel>? BattlePassLevelInfo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Battlefields that can currently be picked.
|
/// Wire is string[]; parser calls .ToString() on each element (LoadDetail.cs:493-499).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("open_battle_field_id_list")]
|
[Key("open_battle_field_id_list")]
|
||||||
public Dictionary<string, int> OpenBattlefieldIds { get; set; } = new Dictionary<string, int>();
|
public List<string> OpenBattlefieldIds { get; set; } = new();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Misc Data
|
#region Misc Data
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// What loot box features are disabled for this user.
|
|
||||||
/// </summary>
|
|
||||||
[Key("loot_box_regulation")]
|
[Key("loot_box_regulation")]
|
||||||
public LootBoxRegulations LootBoxRegulations { get; set; } = new LootBoxRegulations();
|
public LootBoxRegulations LootBoxRegulations { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Something about whether the user has an invite notification.
|
|
||||||
/// </summary>
|
|
||||||
[Key("gathering_info")]
|
[Key("gathering_info")]
|
||||||
public GatheringInfo GatheringInfo { get; set; } = new GatheringInfo();
|
public GatheringInfo GatheringInfo { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// User configuration.
|
/// Spec is unclear whether this is returned at /load/index or only at /config/* endpoints
|
||||||
|
/// (load-index.md line 390). Pending live-capture confirmation; harmless extra.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("user_config")]
|
[Key("user_config")]
|
||||||
public UserConfig UserConfig { get; set; } = new UserConfig();
|
public UserConfig UserConfig { get; set; } = new();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ namespace SVSim.EmulatedEntrypoint.Models.Dtos.Responses;
|
|||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public class SpecialTitleCheckResponse
|
public class SpecialTitleCheckResponse
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Numeric string. "0"/"1" are the built-in default title screens; any other value
|
||||||
|
/// is treated as an asset-bundle id. When omitted, the client defaults to "0".
|
||||||
|
/// </summary>
|
||||||
[Key("title_image_id")]
|
[Key("title_image_id")]
|
||||||
public int TitleImageId { get; set; }
|
public string? TitleImageId { get; set; }
|
||||||
[Key("title_sound_id")]
|
}
|
||||||
public int TitleSoundId { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ public class SleeveIdentifier
|
|||||||
/// The id of the sleeve.
|
/// The id of the sleeve.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key("sleeve_id")]
|
[Key("sleeve_id")]
|
||||||
public int SleeveId { get; set; }
|
public long SleeveId { get; set; }
|
||||||
}
|
}
|
||||||
@@ -2,15 +2,35 @@ using MessagePack;
|
|||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
|
namespace SVSim.EmulatedEntrypoint.Models.Dtos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Per-link entry in <c>transition_account_data</c>. Production sends three string fields per
|
||||||
|
/// entry even though <c>GameStartCheckTask.Parse</c> only reads <c>social_account_type</c>.
|
||||||
|
/// The extra two are read by adjacent tasks (<c>GetGameDataByTransitionCode</c>,
|
||||||
|
/// <c>GetGameDataBySocialAccountTask</c>) — kept here so the wire matches prod regardless of
|
||||||
|
/// which task ends up consuming the payload.
|
||||||
|
/// </summary>
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public class TransitionAccountData
|
public class TransitionAccountData
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The social provider's account id (e.g. SteamID as a string). Sent as string on the wire.
|
||||||
|
/// </summary>
|
||||||
[Key("social_account_id")]
|
[Key("social_account_id")]
|
||||||
public string SocialAccountId { get; set; }
|
public string? SocialAccountId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>Cute/CuteNetworkDefine.ACCOUNT_TYPE</c> enum, **sent as string** on the wire even
|
||||||
|
/// though it's numeric. <c>GameStartCheckTask.Parse</c> calls <c>.ToInt()</c> on it so
|
||||||
|
/// LitJson coerces transparently — but matching prod's string form makes us safer against
|
||||||
|
/// future client paths that might compare it as a literal.
|
||||||
|
/// 1=GooglePlay, 2=GameCenter, 3=Facebook, 4=DMM, 5=Steam, 6=AppleID.
|
||||||
|
/// </summary>
|
||||||
[Key("social_account_type")]
|
[Key("social_account_type")]
|
||||||
public string SocialAccountType { get; set; }
|
public string? SocialAccountType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The viewer id this social connection is linked to. Sent as string.
|
||||||
|
/// </summary>
|
||||||
[Key("connected_viewer_id")]
|
[Key("connected_viewer_id")]
|
||||||
public string ConnectedViewerId { get; set; }
|
public string? ConnectedViewerId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ public class UserInfo
|
|||||||
public DateTime LastPlayTime { get; set; }
|
public DateTime LastPlayTime { get; set; }
|
||||||
[Key("is_received_two_pick_mission")]
|
[Key("is_received_two_pick_mission")]
|
||||||
public int HasReceivedPickTwoMission { get; set; }
|
public int HasReceivedPickTwoMission { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Birth date as yyyy-MM-dd. Parser does .ToString() on this field (LoadDetail.cs:203).
|
||||||
|
/// Format verified against live capture pending.
|
||||||
|
/// </summary>
|
||||||
[Key("birth")]
|
[Key("birth")]
|
||||||
public long Birthday { get; set; }
|
public string Birthday { get; set; } = string.Empty;
|
||||||
[Key("selected_emblem_id")]
|
[Key("selected_emblem_id")]
|
||||||
public long SelectedEmblemId { get; set; }
|
public long SelectedEmblemId { get; set; }
|
||||||
[Key("selected_degree_id")]
|
[Key("selected_degree_id")]
|
||||||
@@ -45,7 +49,7 @@ public class UserInfo
|
|||||||
this.MaxFriend = viewer.Info.MaxFriends;
|
this.MaxFriend = viewer.Info.MaxFriends;
|
||||||
this.LastPlayTime = viewer.LastLogin;
|
this.LastPlayTime = viewer.LastLogin;
|
||||||
this.HasReceivedPickTwoMission = viewer.MissionData.HasReceivedPickTwoMission ? 1 : 0;
|
this.HasReceivedPickTwoMission = viewer.MissionData.HasReceivedPickTwoMission ? 1 : 0;
|
||||||
this.Birthday = viewer.Info.BirthDate.Ticks;
|
this.Birthday = viewer.Info.BirthDate.ToString("yyyy-MM-dd");
|
||||||
this.SelectedEmblemId = viewer.Info.SelectedEmblem.Id;
|
this.SelectedEmblemId = viewer.Info.SelectedEmblem.Id;
|
||||||
this.SelectedDegreeId = viewer.Info.SelectedDegree.Id;
|
this.SelectedDegreeId = viewer.Info.SelectedDegree.Id;
|
||||||
this.MissionChangeTime = viewer.MissionData.MissionChangeTime;
|
this.MissionChangeTime = viewer.MissionData.MissionChangeTime;
|
||||||
@@ -53,4 +57,4 @@ public class UserInfo
|
|||||||
this.IsOfficial = viewer.Info.IsOfficial ? 1 : 0;
|
this.IsOfficial = viewer.Info.IsOfficial ? 1 : 0;
|
||||||
this.IsOfficialMarkDisplayed = viewer.Info.IsOfficialMarkDisplayed ? 1 : 0;
|
this.IsOfficialMarkDisplayed = viewer.Info.IsOfficialMarkDisplayed ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ public class UserLeaderSkin
|
|||||||
{
|
{
|
||||||
this.Id = leaderSkin.Id;
|
this.Id = leaderSkin.Id;
|
||||||
this.Name = leaderSkin.Name;
|
this.Name = leaderSkin.Name;
|
||||||
this.ClassId = leaderSkin.Class.Id;
|
// Class is nullable — BaseDataSeeder maps CSV class_chara_id=0 to null. Fall back to
|
||||||
|
// the FK column (also nullable) and finally 0 for class-agnostic skins.
|
||||||
|
this.ClassId = leaderSkin.Class?.Id ?? leaderSkin.ClassId ?? 0;
|
||||||
this.EmoteId = leaderSkin.EmoteId;
|
this.EmoteId = leaderSkin.EmoteId;
|
||||||
this.IsOwned = isOwned;
|
this.IsOwned = isOwned;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class UserRankInfo
|
|||||||
[Key("is_master_rank")]
|
[Key("is_master_rank")]
|
||||||
public int IsMasterRank { get; set; }
|
public int IsMasterRank { get; set; }
|
||||||
[Key("is_grand_master_rank")]
|
[Key("is_grand_master_rank")]
|
||||||
public bool IsGrandMasterRank { get; set; }
|
public int IsGrandMasterRank { get; set; }
|
||||||
[Key("master_point")]
|
[Key("master_point")]
|
||||||
public int MasterPoints { get; set; }
|
public int MasterPoints { get; set; }
|
||||||
[Key("period_grand_master_point")]
|
[Key("period_grand_master_point")]
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Reflection;
|
|
||||||
using DCGEngine.Database.Configuration;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SVSim.Database;
|
using SVSim.Database;
|
||||||
using SVSim.Database.Models;
|
|
||||||
using SVSim.Database.Repositories.Card;
|
using SVSim.Database.Repositories.Card;
|
||||||
|
using SVSim.Database.Repositories.Collectibles;
|
||||||
|
using SVSim.Database.Repositories.Deck;
|
||||||
using SVSim.Database.Repositories.Globals;
|
using SVSim.Database.Repositories.Globals;
|
||||||
using SVSim.Database.Repositories.Viewer;
|
using SVSim.Database.Repositories.Viewer;
|
||||||
using SVSim.EmulatedEntrypoint.Middlewares;
|
using SVSim.EmulatedEntrypoint.Middlewares;
|
||||||
@@ -21,7 +20,14 @@ public class Program
|
|||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers().AddJsonOptions(opt =>
|
||||||
|
{
|
||||||
|
// Production omits null/optional fields entirely; the client uses
|
||||||
|
// `Keys.Contains(name)` as a presence check and calls `.ToInt()` (etc.) on the
|
||||||
|
// value without a null guard. Emitting `"key":null` makes Contains return true and
|
||||||
|
// crashes the client. Match prod by dropping nulls during serialization.
|
||||||
|
opt.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||||
|
});
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
@@ -38,16 +44,14 @@ public class Program
|
|||||||
});
|
});
|
||||||
builder.Services.AddTransient<IViewerRepository, ViewerRepository>();
|
builder.Services.AddTransient<IViewerRepository, ViewerRepository>();
|
||||||
builder.Services.AddTransient<ICardRepository, CardRepository>();
|
builder.Services.AddTransient<ICardRepository, CardRepository>();
|
||||||
|
builder.Services.AddTransient<ICollectionRepository, CollectionRepository>();
|
||||||
builder.Services.AddTransient<IGlobalsRepository, GlobalsRepository>();
|
builder.Services.AddTransient<IGlobalsRepository, GlobalsRepository>();
|
||||||
|
builder.Services.AddTransient<IDeckRepository, DeckRepository>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
builder.Services.AddTransient<ShadowverseTranslationMiddleware>();
|
builder.Services.AddTransient<ShadowverseTranslationMiddleware>();
|
||||||
builder.Services.AddTransient<SessionidMappingMiddleware>();
|
builder.Services.AddTransient<SessionidMappingMiddleware>();
|
||||||
builder.Services.Configure<DCGEDatabaseConfiguration>(opt =>
|
|
||||||
{
|
|
||||||
opt.DbSetSearchAssemblies = new List<Assembly> { Assembly.GetAssembly(typeof(SVSimDbContext)) };
|
|
||||||
});
|
|
||||||
builder.Services.AddSingleton<ShadowverseSessionService>();
|
builder.Services.AddSingleton<ShadowverseSessionService>();
|
||||||
builder.Services.AddSingleton<SteamSessionService>();
|
builder.Services.AddSingleton<SteamSessionService>();
|
||||||
builder.Services.AddAuthentication()
|
builder.Services.AddAuthentication()
|
||||||
@@ -60,11 +64,16 @@ public class Program
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Update database
|
// Update database (skipped for non-relational providers, e.g. InMemory in tests, and
|
||||||
|
// skipped under the "Testing" environment where the test fixture has already called
|
||||||
|
// EnsureCreated against a SQLite in-memory DB — the Postgres migrations would fail there).
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<SVSimDbContext>();
|
||||||
dbContext.UpdateDatabase();
|
if (dbContext.Database.IsRelational() && !app.Environment.IsEnvironment("Testing"))
|
||||||
|
{
|
||||||
|
dbContext.UpdateDatabase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpLogging();
|
app.UseHttpLogging();
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Configuration\" />
|
<Content Include="Data\*.csv">
|
||||||
<Folder Include="Data\" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Folder Include="Utility\" />
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Globalization;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Steamworks;
|
using Steamworks;
|
||||||
|
|
||||||
namespace SVSim.EmulatedEntrypoint.Services;
|
namespace SVSim.EmulatedEntrypoint.Services;
|
||||||
|
|
||||||
public class SteamSessionService : IDisposable
|
public class SteamSessionService : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, ulong> _validatedSessionTickets;
|
private readonly ConcurrentDictionary<string, ulong> _validatedSessionTickets = new();
|
||||||
|
private readonly object _initLock = new();
|
||||||
|
private bool _steamInitialized;
|
||||||
|
|
||||||
private const int ShadowVerseAppId = 453480;
|
private const int ShadowVerseAppId = 453480;
|
||||||
|
|
||||||
public SteamSessionService()
|
|
||||||
{
|
|
||||||
_validatedSessionTickets = new ConcurrentDictionary<string, ulong>();
|
|
||||||
SteamServer.Init(ShadowVerseAppId, new SteamServerInit
|
|
||||||
{
|
|
||||||
GamePort = default,
|
|
||||||
QueryPort = default
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates if a given session ticket is valid, and matches up with the given steamid.
|
/// Validates if a given session ticket is valid, and matches up with the given steamid.
|
||||||
@@ -34,6 +24,8 @@ public class SteamSessionService : IDisposable
|
|||||||
return storedSteamId == steamId;
|
return storedSteamId == steamId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EnsureSteamInitialized();
|
||||||
|
|
||||||
List<byte> ticketBytes = new List<byte>();
|
List<byte> ticketBytes = new List<byte>();
|
||||||
for (int i = 0; i < ticket.Length; i += 2)
|
for (int i = 0; i < ticket.Length; i += 2)
|
||||||
{
|
{
|
||||||
@@ -45,12 +37,30 @@ public class SteamSessionService : IDisposable
|
|||||||
{
|
{
|
||||||
_validatedSessionTickets.TryAdd(ticket, steamId);
|
_validatedSessionTickets.TryAdd(ticket, steamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return steamCheckResults;
|
return steamCheckResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void EnsureSteamInitialized()
|
||||||
|
{
|
||||||
|
if (_steamInitialized) return;
|
||||||
|
lock (_initLock)
|
||||||
|
{
|
||||||
|
if (_steamInitialized) return;
|
||||||
|
SteamServer.Init(ShadowVerseAppId, new SteamServerInit
|
||||||
|
{
|
||||||
|
GamePort = default,
|
||||||
|
QueryPort = default
|
||||||
|
});
|
||||||
|
_steamInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
SteamServer.Shutdown();
|
if (_steamInitialized)
|
||||||
|
{
|
||||||
|
SteamServer.Shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,21 @@
|
|||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
|
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SVSim.EmulatedEntrypoint\SVSim.EmulatedEntrypoint.csproj" />
|
<ProjectReference Include="..\SVSim.EmulatedEntrypoint\SVSim.EmulatedEntrypoint.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- BaseDataSeeder reads CSVs from the runtime "Data" folder; mirror them into the test
|
||||||
|
output so HasData seeding fires when EnsureCreated builds the SQLite schema. -->
|
||||||
|
<Content Include="..\SVSim.EmulatedEntrypoint\Data\*.csv" Link="Data\%(Filename)%(Extension)">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
using SVSim.EmulatedEntrypoint.Services;
|
|
||||||
|
|
||||||
namespace SVSim.UnitTests;
|
namespace SVSim.UnitTests;
|
||||||
|
|
||||||
public class Tests
|
public class SmokeTests
|
||||||
{
|
{
|
||||||
[SetUp]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Test1()
|
public void CanLoadAssembly()
|
||||||
{
|
{
|
||||||
const string ticket =
|
Assert.That(typeof(SVSim.EmulatedEntrypoint.Program).Assembly, Is.Not.Null);
|
||||||
"140000005ee7d30c1263e214e133a10001001001e07cd866180000000100000002000000b8526bb7b8946cd27c214574f1000000b20000003200000004000000e133a1000100100168eb0600488cc2443101a8c0000000008165d4660115f06601005c7e010000000000cad61456a2b83d39595c3e3749b96b4537ebde88d048103a6f6c7b2b81ee68711378836872a11422f5bd16fad803f81122c5ae98d986b693bbbc00ac7d30a8f85af2c1a7dce57751eb2c7f21130284aa8d9ee787246c8ccc138f05936bacb1ba4baba5fa5fbf6158002cf7207ae25a6f6ee8e3fc8edbb84903d346a249179637";
|
|
||||||
using var steamService = new SteamSessionService();
|
|
||||||
bool validTicket = steamService.IsTicketValidForUser(ticket, 76561197970830305);
|
|
||||||
Assert.That(validTicket, Is.EqualTo(true));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user