Deck list work
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
SVSim.Bootstrap/DesignTimeDbContextFactory.cs
Normal file
23
SVSim.Bootstrap/DesignTimeDbContextFactory.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user