Deck list work

This commit is contained in:
gamer147
2026-05-23 19:57:34 -04:00
parent 66184b3685
commit d3b2970e11
41 changed files with 70683 additions and 81 deletions

View File

@@ -0,0 +1,201 @@
{
"data_headers": {
"sid": "ac631c29b5f5d07ed5fb6712ad8623c31779553960",
"short_udid": 411054851,
"viewer_id": 906243102,
"servertime": 1779553960,
"result_code": 1
},
"data": {
"10011": {
"record_id": "21",
"id": "8",
"store_product_id": "10011",
"name": "60-crystal set",
"text": "Purchase 60 Crystals",
"price": "0.99",
"charge_crystal_num": "60",
"free_crystal_num": "0",
"purchase_limit": "999999999",
"special_shop_flag": "0",
"image_name": "thumbnail_crystal",
"start_time": "2022-10-05 15:00:00",
"end_time": "2030-03-01 14:59:59",
"remaining_time": "0",
"is_resale_product": "0",
"resale_start_date": "",
"purchase_num_current": 0
},
"30011": {
"record_id": "26",
"id": "10",
"store_product_id": "30011",
"name": "670-crystal set",
"text": "Purchase 670 Crystals",
"price": "10.99",
"charge_crystal_num": "670",
"free_crystal_num": "0",
"purchase_limit": "999999999",
"special_shop_flag": "0",
"image_name": "thumbnail_crystal",
"start_time": "2022-10-05 15:00:00",
"end_time": "2030-03-01 14:59:59",
"remaining_time": "0",
"is_resale_product": "0",
"resale_start_date": "",
"purchase_num_current": 0
},
"40000": {
"record_id": "27",
"id": "4",
"store_product_id": "40000",
"name": "1200-crystal set",
"text": "Purchase 1200 Crystals",
"price": "20.99",
"charge_crystal_num": "1200",
"free_crystal_num": "0",
"purchase_limit": "999999999",
"special_shop_flag": "0",
"image_name": "thumbnail_crystal",
"start_time": "2015-03-01 15:00:00",
"end_time": "2030-03-01 14:59:59",
"remaining_time": "0",
"is_resale_product": "0",
"resale_start_date": "",
"purchase_num_current": 0
},
"50000": {
"record_id": "28",
"id": "5",
"store_product_id": "50000",
"name": "2400-crystal set",
"text": "Purchase 2400 Crystals",
"price": "39.99",
"charge_crystal_num": "2400",
"free_crystal_num": "0",
"purchase_limit": "999999999",
"special_shop_flag": "0",
"image_name": "thumbnail_crystal",
"start_time": "2015-03-01 15:00:00",
"end_time": "2030-03-01 14:59:59",
"remaining_time": "0",
"is_resale_product": "0",
"resale_start_date": "",
"purchase_num_current": 0
},
"60000": {
"record_id": "29",
"id": "6",
"store_product_id": "60000",
"name": "5000-crystal set",
"text": "Purchase 5000 Crystals",
"price": "79.99",
"charge_crystal_num": "5000",
"free_crystal_num": "0",
"purchase_limit": "999999999",
"special_shop_flag": "0",
"image_name": "thumbnail_crystal",
"start_time": "2015-03-01 15:00:00",
"end_time": "2030-03-01 14:59:59",
"remaining_time": "0",
"is_resale_product": "0",
"resale_start_date": "",
"purchase_num_current": 0
},
"70011": {
"record_id": "24",
"id": "9",
"store_product_id": "70011",
"name": "350-crystal set",
"text": "Purchase 350 Crystals",
"price": "5.99",
"charge_crystal_num": "350",
"free_crystal_num": "0",
"purchase_limit": "999999999",
"special_shop_flag": "0",
"image_name": "thumbnail_crystal",
"start_time": "2022-10-05 15:00:00",
"end_time": "2030-03-01 14:59:59",
"remaining_time": "0",
"is_resale_product": "0",
"resale_start_date": "",
"purchase_num_current": 0
},
"80000": {
"record_id": "30",
"id": "800",
"store_product_id": "80000",
"name": "1200-crystal and Legendary set",
"text": "Purchase 1200 Crystals and Legendary set",
"price": "20.99",
"charge_crystal_num": "1200",
"free_crystal_num": "0",
"purchase_limit": "3",
"special_shop_flag": "1",
"image_name": "thumbnail_crystal_strong",
"start_time": "2018-01-01 00:00:00",
"end_time": "2019-03-19 16:15:17",
"remaining_time": "604800",
"is_resale_product": "0",
"resale_start_date": "",
"purchase_num_current": 0
},
"98900": {
"record_id": "19",
"id": "989",
"store_product_id": "98900",
"name": "[b]1-Time Deal![/b] 1000-crystal set",
"text": "Purchase 1000 Crystals",
"price": "15.99",
"charge_crystal_num": "1000",
"free_crystal_num": "0",
"purchase_limit": "1",
"special_shop_flag": "0",
"image_name": "thumbnail_crystal_strong",
"start_time": "2026-04-01 02:00:00",
"end_time": "2026-07-01 01:59:59",
"remaining_time": "0",
"is_resale_product": "1",
"resale_start_date": "2026-04-01 02:00:00",
"purchase_num_current": 0
},
"99200": {
"record_id": "3",
"id": "992",
"store_product_id": "99200",
"name": "[b]One-time Deal![/b] 800-crystal set",
"text": "Purchase 800 Crystals",
"price": "7.99",
"charge_crystal_num": "800",
"free_crystal_num": "0",
"purchase_limit": "1",
"special_shop_flag": "0",
"image_name": "thumbnail_crystal_strong",
"start_time": "2018-01-30 04:00:00",
"end_time": "2030-03-01 14:59:59",
"remaining_time": "0",
"is_resale_product": "0",
"resale_start_date": "",
"purchase_num_current": 0
},
"99400": {
"record_id": "10",
"id": "994",
"store_product_id": "99400",
"name": "[b]Special Offer![/b] 7500-crystal set (3 times per person)",
"text": "Purchase 7500 Crystals",
"price": "79.99",
"charge_crystal_num": "7500",
"free_crystal_num": "0",
"purchase_limit": "3",
"special_shop_flag": "0",
"image_name": "thumbnail_crystal_strong",
"start_time": "2017-06-01 06:00:00",
"end_time": "2030-03-01 14:59:59",
"remaining_time": "0",
"is_resale_product": "0",
"resale_start_date": "",
"purchase_num_current": 2
}
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging.Abstractions;
using SVSim.Database;
namespace SVSim.Bootstrap;
/// <summary>
/// Lets `dotnet ef migrations add` instantiate SVSimDbContext at design time. The runtime ctor
/// takes an ILogger which EF's tooling can't resolve without DI; this factory bypasses that.
/// Connection string here only needs to be valid Npgsql syntax — EF doesn't actually connect
/// during migration scaffolding.
/// </summary>
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<SVSimDbContext>
{
public SVSimDbContext CreateDbContext(string[] args)
{
var options = new DbContextOptionsBuilder<SVSimDbContext>()
.UseNpgsql("Host=localhost;Database=svsim;Username=postgres;password=postgres")
.Options;
return new SVSimDbContext(NullLogger<SVSimDbContext>.Instance, options);
}
}

View File

@@ -27,6 +27,7 @@ public class GlobalsImporter
JsonElement? loadIndex = LoadCapture(capturesDir, "load-index");
JsonElement? mypageIndex = LoadCapture(capturesDir, "mypage-index");
JsonElement? deckInfo = LoadCapture(capturesDir, "deck-info");
JsonElement? paymentItemList = LoadCapture(capturesDir, "payment-item-list");
int total = 0;
@@ -54,6 +55,7 @@ public class GlobalsImporter
total += await ImportColosseum(context, mypageIndex.Value);
total += await ImportSealed(context, mypageIndex.Value);
total += await ImportMasterPointRankingPeriod(context, mypageIndex.Value);
total += await ImportRoomTypeInSession(context, mypageIndex.Value);
}
if (deckInfo.HasValue)
@@ -62,6 +64,11 @@ public class GlobalsImporter
total += await ImportDefaultLeaderSkinSettings(context, deckInfo.Value);
}
if (paymentItemList.HasValue)
{
total += await ImportPaymentItems(context, paymentItemList.Value);
}
await context.SaveChangesAsync();
Console.WriteLine($"[GlobalsImporter] Done: {total} total rows changed.");
return total;
@@ -550,6 +557,34 @@ public class GlobalsImporter
return 1;
}
// ---------- Mypage: Room Type In Session (special deck formats) ----------
private async Task<int> ImportRoomTypeInSession(SVSimDbContext context, JsonElement mypage)
{
if (!mypage.TryGetProperty("room_type_in_session", out var rt) || rt.ValueKind != JsonValueKind.Object) return 0;
if (!rt.TryGetProperty("special_deck_format_list", out var arr) || arr.ValueKind != JsonValueKind.Array) return 0;
// Same shape semantics as Banners — the wire has no stable id, treat the capture as
// authoritative and clear-and-rewrite with a synthetic ordinal.
var existing = await context.SpecialDeckFormats.ToListAsync();
context.SpecialDeckFormats.RemoveRange(existing);
int created = 0;
int idx = 1;
foreach (var el in arr.EnumerateArray())
{
context.SpecialDeckFormats.Add(new SpecialDeckFormatEntry
{
Id = idx++,
DeckFormat = GetString(el, "deck_format"),
EndTime = ParseWireDateTime(GetString(el, "end_time"))
});
created++;
}
Console.WriteLine($"[GlobalsImporter] SpecialDeckFormats: {(existing.Count > 0 ? $"-{existing.Count}/" : "")}+{created}");
return created;
}
// ---------- Deck/info: Default Decks ----------
private async Task<int> ImportDefaultDecks(SVSimDbContext context, JsonElement deckInfo)
@@ -611,6 +646,54 @@ public class GlobalsImporter
return created + updated;
}
// ---------- Payment: Item list (Steam/PC storefront, dict-keyed by store_product_id) ----------
private async Task<int> ImportPaymentItems(SVSimDbContext context, JsonElement payment)
{
// The payment-item-list capture's `data` IS the product dict (no nested key like banner/colosseum).
// LoadCapture already unwrapped `data` for us, so iterate the dict directly.
if (payment.ValueKind != JsonValueKind.Object) return 0;
var existing = await context.PaymentItems.ToDictionaryAsync(e => e.Id);
int created = 0, updated = 0;
foreach (var kv in payment.EnumerateObject())
{
var v = kv.Value;
if (v.ValueKind != JsonValueKind.Object) continue;
int recordId = GetInt(v, "record_id");
if (recordId == 0) continue;
var entry = existing.TryGetValue(recordId, out var ex) ? ex : new PaymentItemEntry { Id = recordId };
entry.ProductId = GetInt(v, "id");
entry.StoreProductId = GetLong(v, "store_product_id");
entry.Name = GetString(v, "name");
entry.Text = GetString(v, "text");
entry.Price = ParseDecimal(GetString(v, "price"));
entry.ChargeCrystalNum = GetInt(v, "charge_crystal_num");
entry.FreeCrystalNum = GetInt(v, "free_crystal_num");
entry.PurchaseLimit = GetInt(v, "purchase_limit");
entry.SpecialShopFlag = GetInt(v, "special_shop_flag");
entry.ImageName = GetString(v, "image_name");
entry.StartTime = ParseWireDateTime(GetString(v, "start_time"));
entry.EndTime = ParseWireDateTime(GetString(v, "end_time"));
entry.RemainingTime = GetInt(v, "remaining_time");
entry.IsResaleProduct = GetInt(v, "is_resale_product");
// resale_start_date is "" when unset — store null rather than DateTime.MinValue so the
// controller can decide whether to emit "" or a real date string.
string resaleRaw = GetString(v, "resale_start_date");
entry.ResaleStartDate = string.IsNullOrWhiteSpace(resaleRaw) ? null : ParseWireDateTime(resaleRaw);
if (ex is null) { context.PaymentItems.Add(entry); created++; }
else updated++;
}
Console.WriteLine($"[GlobalsImporter] PaymentItems: +{created}/~{updated}");
return created + updated;
}
private static decimal ParseDecimal(string s) =>
decimal.TryParse(s, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out var d) ? d : 0m;
// ---------- Helpers ----------
private static void WarnOrphans(string label, int count)

View File

@@ -13,6 +13,22 @@
<Content Include="Data\prod-captures\*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<!--
Seed CSVs live in SVSim.EmulatedEntrypoint/Data — link them here so `dotnet ef migrations add`
(which uses Bootstrap as the startup project) finds the same files at AppContext.BaseDirectory.
Otherwise BaseDataSeeder.Seed short-circuits, the design-time model has no HasData rows, and
every migration diff wants to DeleteData/InsertData for all of them.
-->
<Content Include="..\SVSim.EmulatedEntrypoint\Data\*.csv" Link="Data\%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>