diff --git a/SVSimLoader/CaptureWriter.cs b/SVSimLoader/CaptureWriter.cs index 8ebf2a7..977f12f 100644 --- a/SVSimLoader/CaptureWriter.cs +++ b/SVSimLoader/CaptureWriter.cs @@ -131,6 +131,17 @@ internal static class CaptureWriter try { + // SetResponseData hands us the FULL response envelope { data_headers, data }; the + // viewer payload (user_info, user_crystal_count, user_card_list, ...) lives under the + // inner `data` key. Descend into it before extracting — same as + // ExaminationPatches.TryExtractSpecialBattleSettings does. Without this every SafeGet + // below misses and the dump contains nothing but steam_id. The inner payload has no + // top-level `data` key of its own, so this is safe if a caller ever pre-strips it. + if (loadIndexData != null && loadIndexData.IsObject && loadIndexData.Keys.Contains("data")) + { + loadIndexData = loadIndexData["data"]; + } + var dump = new Dictionary { { "steam_id", _lastSeenSteamId } @@ -153,7 +164,7 @@ internal static class CaptureWriter { var cur = new Dictionary(); Copy(crystal, "crystal", cur, "crystals"); - Copy(crystal, "rupies", cur, "rupees"); + Copy(crystal, "rupy", cur, "rupees"); Copy(crystal, "red_ether", cur, "red_ether"); if (cur.Count > 0) dump["currency"] = cur; } @@ -164,6 +175,9 @@ internal static class CaptureWriter ExtractMyPageList(loadIndexData, dump); ExtractOwnedLeaderSkins(loadIndexData, dump); ExtractClasses(loadIndexData, dump); + ExtractOwnedCards(loadIndexData, dump); + ExtractItems(loadIndexData, dump); + ExtractDecks(loadIndexData, dump); lock (_lock) { @@ -278,6 +292,134 @@ internal static class CaptureWriter if (classes.Count > 0) dump["classes"] = classes; } + private static void ExtractOwnedCards(JsonData data, Dictionary dump) + { + var list = SafeGet(data, "user_card_list"); + if (list == null || !list.IsArray) return; + var cards = new List>(); + for (int i = 0; i < list.Count; i++) + { + var entry = list[i]; + var idVal = SafeGet(entry, "card_id"); + if (idVal == null) continue; + long cardId; + if (idVal.IsInt) cardId = (int)idVal; + else if (idVal.IsLong) cardId = (long)idVal; + else continue; + + var c = new Dictionary { { "card_id", cardId } }; + + var num = SafeGet(entry, "number"); + if (num != null) + { + if (num.IsInt) c["count"] = (int)num; + else if (num.IsLong) c["count"] = (int)(long)num; + } + + var prot = SafeGet(entry, "is_protected"); + if (prot != null) + { + c["is_protected"] = + (prot.IsBoolean && (bool)prot) || + (prot.IsInt && (int)prot != 0) || + (prot.IsLong && (long)prot != 0); + } + cards.Add(c); + } + if (cards.Count > 0) dump["owned_cards"] = cards; + } + + private static void ExtractItems(JsonData data, Dictionary dump) + { + var list = SafeGet(data, "user_item_list"); + if (list == null || !list.IsArray) return; + var items = new List>(); + for (int i = 0; i < list.Count; i++) + { + var entry = list[i]; + var idVal = SafeGet(entry, "item_id"); + if (idVal == null) continue; + var item = new Dictionary(); + if (idVal.IsInt) item["item_id"] = (int)idVal; + else if (idVal.IsLong) item["item_id"] = (int)(long)idVal; + else continue; + var num = SafeGet(entry, "number"); + if (num != null) + { + if (num.IsInt) item["count"] = (int)num; + else if (num.IsLong) item["count"] = (int)(long)num; + } + items.Add(item); + } + if (items.Count > 0) dump["items"] = items; + } + + // /load/index splits decks into one container per format; the format is the KEY, not a + // per-deck field. Values mirror the wire deck_format codes (Wizard/Data.cs FormatConvertApi). + private struct DeckFormatKey + { + public string Key; + public int Format; + public DeckFormatKey(string key, int format) { Key = key; Format = format; } + } + + private static readonly DeckFormatKey[] DeckFormatKeys = + { + new DeckFormatKey("user_deck_rotation", 1), + new DeckFormatKey("user_deck_unlimited", 2), + new DeckFormatKey("user_deck_pre_rotation", 3), + new DeckFormatKey("user_deck_crossover", 4), + new DeckFormatKey("user_deck_my_rotation", 5), + }; + + private static void ExtractDecks(JsonData data, Dictionary dump) + { + var decks = new List>(); + foreach (var fmt in DeckFormatKeys) + { + var container = SafeGet(data, fmt.Key); + var deckList = SafeGet(container, "user_deck_list"); + if (deckList == null || !deckList.IsArray) continue; + + for (int i = 0; i < deckList.Count; i++) + { + var entry = deckList[i]; + // /load/index ships every deck slot, most of them empty placeholders. Skip the + // empty ones — the import drops them anyway, and it keeps the dump to the few real + // decks instead of ~100 empty slots. + var cardArr = ExtractLongArray(entry, "card_id_array"); + if (cardArr == null || cardArr.Count == 0) continue; + + var d = new Dictionary { { "deck_format", fmt.Format } }; + Copy(entry, "deck_no", d, "deck_no"); + Copy(entry, "deck_name", d, "deck_name"); + Copy(entry, "class_id", d, "class_id"); + Copy(entry, "sleeve_id", d, "sleeve_id"); + Copy(entry, "leader_skin_id", d, "leader_skin_id"); + Copy(entry, "is_random_leader_skin", d, "is_random_leader_skin"); + Copy(entry, "rotation_id", d, "my_rotation_id"); // UserDeck.rotation_id -> import my_rotation_id + d["card_id_array"] = cardArr; + decks.Add(d); + } + } + if (decks.Count > 0) dump["decks"] = decks; + } + + private static List ExtractLongArray(JsonData entry, string key) + { + var arr = SafeGet(entry, key); + if (arr == null || !arr.IsArray) return null; + var ids = new List(); + for (int i = 0; i < arr.Count; i++) + { + var v = arr[i]; + if (v == null) continue; + if (v.IsInt) ids.Add((int)v); + else if (v.IsLong) ids.Add((long)v); + } + return ids; + } + // Splice the body into the envelope as nested JSON (parseable) or escaped string // (fallback). Cannot route this through Dictionary → JsonMapper.ToJson: // a LitJson.JsonData value inside such a dict makes the reflection serializer diff --git a/SVSimLoader/Patches/ExaminationPatches.cs b/SVSimLoader/Patches/ExaminationPatches.cs index 13ce38a..0c2134a 100644 --- a/SVSimLoader/Patches/ExaminationPatches.cs +++ b/SVSimLoader/Patches/ExaminationPatches.cs @@ -23,13 +23,14 @@ public static class ExaminationPatches } if (SvSimConfig.DumpUserData && __instance.Url != null && __instance.Url.EndsWith("/load/index")) { - // The /load/index response data is the inner `data` payload by this point — the - // outer `data_headers` wrapper has already been stripped by the network task base. + // `data` is the FULL envelope { data_headers, data } (see TryExtractSpecialBattleSettings + // + NetworkTask.cs:108-110). WriteUserDataFromLoadIndex descends into the inner `data` + // key itself, so pass the envelope as-is. CaptureWriter.WriteUserDataFromLoadIndex(data); } - if (SvSimConfig.SweepLeaderSkinPools && __instance.Url != null && __instance.Url.EndsWith("/pack/info")) + if (SvSimConfig.SweepGachaExchange && __instance.Url != null && __instance.Url.EndsWith("/pack/info")) { - LeaderSkinPoolSweep.OnPackInfoResponse(data); + GachaExchangeSweep.OnPackInfoResponse(data); } if (__instance.Url != null && IsStorySectionUrl(__instance.Url) && (SvSimConfig.SweepMainStory || SvSimConfig.SweepLimitedStory || SvSimConfig.SweepEventStory)) diff --git a/SVSimLoader/Patches/GachaExchangeSweep.cs b/SVSimLoader/Patches/GachaExchangeSweep.cs new file mode 100644 index 0000000..b3180c9 --- /dev/null +++ b/SVSimLoader/Patches/GachaExchangeSweep.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; +using BepInEx; +using Cute; +using LitJson; +using UnityEngine; +using Wizard; +using Wizard.Scripts.Network.Data.TaskData.SpotCardExchange; + +namespace SVSimLoader.Patches; + +/// +/// Replaces the former LeaderSkinPoolSweep. Fires GachaPointExchangeInfoTask +/// (/pack/get_gacha_point_rewards) for every pack id in +/// Wizard.Data.Master.CardSetNameMgr.GetList() — i.e. the full client master +/// list (~279 ids), not just the 35 in-catalog packs /pack/info returns. +/// +/// Goal: capture per-pack tradeable card_id lists for off-catalog families +/// (Throwback 80xxx, Rotation Select 97/98xxx, Premium 93xxx, anniversary +/// 92xxx/95xxx) so the drawrates parser's tier-4 disambiguation can resolve +/// the residual ambiguous joins. See +/// docs/superpowers/specs/2026-05-30-gacha-exchange-sweep-design.md. +/// +/// Trigger: first /pack/info response of the session (same as the old sweep). +/// Capture path: responses ride the existing ExaminationPatches.SetResponseData +/// EnableTrafficCapture branch into traffic.ndjson — this sweep never calls +/// CaptureWriter directly. +/// +/// Misses (result_code != 1) are recorded in a persistent ledger at +/// BepInEx/svsim-captures/gacha-sweep-misses.json so re-runs across sessions +/// don't re-hit dead ids. +/// +/// Gated by SvSimConfig.SweepGachaExchange (off by default). Pacing from +/// SvSimConfig.GachaExchangeSweepPacingSeconds (default 0.5, clamped >= 0.1). +/// Smoke-test mode via SvSimConfig.SweepDryRunIds (comma-separated allowlist). +/// +internal static class GachaExchangeSweep +{ + private static bool _sweepStarted; + private static readonly object _lock = new object(); + + private const string LedgerFileName = "gacha-sweep-misses.json"; + private const string LedgerSubdir = "svsim-captures"; + + public static void OnPackInfoResponse(JsonData _) + { + lock (_lock) + { + if (_sweepStarted) return; + _sweepStarted = true; + } + + if (Plugin.Instance == null) + { + Plugin.Log.LogError("GachaExchangeSweep: Plugin.Instance is null — cannot start coroutine."); + return; + } + + var ids = BuildIdList(); + if (ids.Count == 0) + { + Plugin.Log.LogWarning("GachaExchangeSweep: BuildIdList returned 0 ids — nothing to sweep."); + return; + } + + float pacing = Mathf.Max(0.1f, SvSimConfig.GachaExchangeSweepPacingSeconds); + Plugin.Log.LogInfo($"GachaExchangeSweep: queued {ids.Count} ids (pacing={pacing}s)."); + Plugin.Instance.StartCoroutine(SweepCoroutine(ids, pacing)); + } + + /// + /// Builds the candidate id list: every numeric CardSetName.ID from + /// CardSetNameMgr, minus the persistent miss ledger, optionally + /// intersected with SweepDryRunIds. + /// + private static List BuildIdList() + { + var all = new HashSet(); + var master = Data.Master?.CardSetNameMgr; + if (master == null) + { + Plugin.Log.LogWarning("GachaExchangeSweep: Wizard.Data.Master.CardSetNameMgr is null — master data not loaded yet?"); + return new List(); + } + var list = master.GetList(); + if (list == null) + { + Plugin.Log.LogWarning("GachaExchangeSweep: CardSetNameMgr.GetList() returned null."); + return new List(); + } + foreach (var cs in list) + { + if (cs == null || string.IsNullOrEmpty(cs.ID)) continue; + if (int.TryParse(cs.ID, out int id)) all.Add(id); + } + + var misses = LoadMissLedger(); + all.ExceptWith(misses); + + var dryRun = ParseDryRunIds(SvSimConfig.SweepDryRunIds); + if (dryRun.Count > 0) + { + all.IntersectWith(dryRun); + Plugin.Log.LogInfo($"GachaExchangeSweep: SweepDryRunIds active — restricted to {all.Count} of {dryRun.Count} requested ids."); + } + + var ordered = new List(all); + ordered.Sort(); + return ordered; + } + + private static HashSet ParseDryRunIds(string raw) + { + var set = new HashSet(); + if (string.IsNullOrEmpty(raw)) return set; + foreach (var part in raw.Split(',')) + { + var token = part.Trim(); + if (token.Length == 0) continue; + if (int.TryParse(token, out int id)) set.Add(id); + else Plugin.Log.LogWarning($"GachaExchangeSweep: SweepDryRunIds token '{token}' is not an int — skipped."); + } + return set; + } + + private static IEnumerator SweepCoroutine(List ids, float pacing) + { + int ok = 0, fail = 0; + var newMisses = new HashSet(); + for (int i = 0; i < ids.Count; i++) + { + int id = ids[i]; + // odds_gacha_id and parent_gacha_id observed equal in the natural-flow capture + // (data_dumps/traffic_prod_tradeables_capture.ndjson). Pass the same value twice. + var task = new GachaPointExchangeInfoTask(); + task.SetParameter(id, id); + Plugin.Log.LogInfo($"GachaExchangeSweep: [{i + 1}/{ids.Count}] pack_id={id}"); + yield return Toolbox.NetworkManager.Connect(task, _ => { }); + if (task.isServerResultCodeOK()) + { + ok++; + } + else + { + fail++; + newMisses.Add(id); + Plugin.Log.LogWarning($"GachaExchangeSweep: pack_id={id} returned result_code={task.GetResultCode()} — recording as miss."); + } + yield return new WaitForSeconds(pacing); + } + + int totalLedger = -1; + if (newMisses.Count > 0) + { + try + { + totalLedger = SaveMissLedger(newMisses); + } + catch (Exception e) + { + Plugin.Log.LogError($"GachaExchangeSweep: SaveMissLedger failed: {e}"); + } + } + if (totalLedger >= 0) + { + Plugin.Log.LogInfo($"GachaExchangeSweep: complete. ok={ok} fail={fail}/{ids.Count}, ledger now {totalLedger} ids."); + } + else + { + Plugin.Log.LogInfo($"GachaExchangeSweep: complete. ok={ok} fail={fail}/{ids.Count}, ledger unchanged."); + } + } + + private static string LedgerPath() + { + return Path.Combine(Paths.BepInExRootPath, LedgerSubdir, LedgerFileName); + } + + private static HashSet LoadMissLedger() + { + var result = new HashSet(); + var path = LedgerPath(); + if (!File.Exists(path)) return result; + try + { + var json = File.ReadAllText(path); + if (string.IsNullOrEmpty(json)) return result; + var data = JsonMapper.ToObject(json); + if (data == null || !data.IsObject || !data.Keys.Contains("miss_ids")) return result; + var arr = data["miss_ids"]; + if (arr == null || !arr.IsArray) return result; + for (int i = 0; i < arr.Count; i++) + { + var v = arr[i]; + if (v == null) continue; + if (v.IsInt) result.Add((int)v); + else if (v.IsLong) result.Add((int)(long)v); + } + } + catch (Exception e) + { + Plugin.Log.LogError($"GachaExchangeSweep: LoadMissLedger failed (treating as empty): {e}"); + return new HashSet(); + } + return result; + } + + /// + /// Atomic union-merge save. Reads the existing ledger, unions the new + /// misses in, writes to a temp file, then atomically replaces the dest. + /// Returns the total miss-id count after merge. + /// + private static int SaveMissLedger(HashSet newMisses) + { + var path = LedgerPath(); + var dir = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); + + var merged = LoadMissLedger(); + merged.UnionWith(newMisses); + + var ordered = new List(merged); + ordered.Sort(); + + var payload = new Dictionary + { + { "miss_ids", ordered }, + { "last_updated", DateTime.UtcNow.ToString("o") } + }; + var json = JsonMapper.ToJson(payload); + + var tempPath = path + ".tmp"; + File.WriteAllText(tempPath, json, new UTF8Encoding(false)); + if (File.Exists(path)) + { + File.Replace(tempPath, path, null); + } + else + { + File.Move(tempPath, path); + } + return merged.Count; + } +} diff --git a/SVSimLoader/Patches/LeaderSkinPoolSweep.cs b/SVSimLoader/Patches/LeaderSkinPoolSweep.cs deleted file mode 100644 index 55ae4cc..0000000 --- a/SVSimLoader/Patches/LeaderSkinPoolSweep.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Cute; -using LitJson; -using UnityEngine; -using Wizard.Scripts.Network.Data.TaskData.SpotCardExchange; - -namespace SVSimLoader.Patches; - -/// -/// Fires GachaPointExchangeInfoTask (pack/get_gacha_point_rewards) for every -/// parent_gacha_id in /pack/info's pack_config_list, on the first /pack/info response -/// of the session. Responses are captured by the existing traffic hook in -/// ExaminationPatches.SetResponseData — no extra writer needed. -/// -/// Originally targeted pack/get_leader_skin_owned_status (Plan B in the leader-cards -/// design doc), but that endpoint is server-gated on an active skin-select offering -/// and returned result_code=205 across all 35 packs in the May 2026 catalog. Switched -/// to pack/get_gacha_point_rewards which exposes the same card_id → leader_skin_id -/// mapping (via reward_type=10 entries) without the gate, and additionally yields -/// emblem/sleeve associations for every tradeable card. -/// -/// Gated by SvSimConfig.SweepLeaderSkinPools (off by default). -/// Hits a prod API — keep pacing conservative. -/// -internal static class LeaderSkinPoolSweep -{ - private static bool _sweepStarted; - private static readonly object _lock = new object(); - - public static void OnPackInfoResponse(JsonData data) - { - lock (_lock) - { - if (_sweepStarted) return; - _sweepStarted = true; - } - - var ids = ExtractParentGachaIds(data); - if (ids.Count == 0) - { - Plugin.Log.LogWarning("LeaderSkinPoolSweep: /pack/info had no pack_config_list — nothing to sweep."); - return; - } - if (Plugin.Instance == null) - { - Plugin.Log.LogError("LeaderSkinPoolSweep: Plugin.Instance is null — cannot start coroutine."); - return; - } - Plugin.Log.LogInfo($"LeaderSkinPoolSweep: queued {ids.Count} packs for /pack/get_gacha_point_rewards sweep."); - Plugin.Instance.StartCoroutine(SweepCoroutine(ids)); - } - - private static List ExtractParentGachaIds(JsonData data) - { - var ids = new List(); - if (data == null || !data.IsObject) - { - Plugin.Log.LogWarning("LeaderSkinPoolSweep: /pack/info response data was null or non-object."); - return ids; - } - - // Defensive: the data parameter to SetResponseData might be either the inner `data` - // payload (matching the DumpUserData assumption) or the full envelope with data_headers - // outside. Different endpoints in this codebase seem to differ; check both shapes. - JsonData list = null; - if (data.Keys.Contains("pack_config_list")) - { - list = data["pack_config_list"]; - } - else if (data.Keys.Contains("data") && data["data"] != null && data["data"].IsObject - && data["data"].Keys.Contains("pack_config_list")) - { - list = data["data"]["pack_config_list"]; - } - - if (list == null) - { - var keys = string.Join(", ", data.Keys); - Plugin.Log.LogWarning($"LeaderSkinPoolSweep: pack_config_list not found at either data.pack_config_list or data.data.pack_config_list. Top-level keys observed: [{keys}]"); - return ids; - } - if (!list.IsArray) - { - Plugin.Log.LogWarning($"LeaderSkinPoolSweep: pack_config_list was found but is not an array (type tag mismatch)."); - return ids; - } - - for (int i = 0; i < list.Count; i++) - { - var item = list[i]; - if (item == null || !item.IsObject || !item.Keys.Contains("parent_gacha_id")) continue; - var v = item["parent_gacha_id"]; - if (v.IsInt) ids.Add((int)v); - else if (v.IsLong) ids.Add((int)(long)v); - } - return ids; - } - - private static IEnumerator SweepCoroutine(List parentGachaIds) - { - int ok = 0, fail = 0; - for (int i = 0; i < parentGachaIds.Count; i++) - { - int id = parentGachaIds[i]; - // odds_gacha_id and parent_gacha_id observed equal in the natural-flow capture - // (data_dumps/traffic_prod_tradeables_capture.ndjson). Pass the same value twice. - GachaPointExchangeInfoTask task = new GachaPointExchangeInfoTask(); - task.SetParameter(id, id); - Plugin.Log.LogInfo($"LeaderSkinPoolSweep: [{i + 1}/{parentGachaIds.Count}] parent_gacha_id={id}"); - yield return Toolbox.NetworkManager.Connect(task, _ => { }); - if (task.isServerResultCodeOK()) - { - ok++; - } - else - { - fail++; - Plugin.Log.LogWarning($"LeaderSkinPoolSweep: parent_gacha_id={id} returned result_code={task.GetResultCode()}"); - } - // 0.5s per request is conservative pacing for a prod API. 35 packs ≈ 18s total. - yield return new WaitForSeconds(0.5f); - } - Plugin.Log.LogInfo($"LeaderSkinPoolSweep: complete. ok={ok} fail={fail}/{parentGachaIds.Count}"); - } -} diff --git a/SVSimLoader/Plugin.cs b/SVSimLoader/Plugin.cs index 6d33e38..0d5a75d 100644 --- a/SVSimLoader/Plugin.cs +++ b/SVSimLoader/Plugin.cs @@ -42,9 +42,15 @@ namespace SVSimLoader SvSimConfig.ProbeEventSection = Config.Bind("Sweeps", "ProbeEventSection", false, "As ProbeLimitedSection but for /event_story/section. Fired with a 1s gap after the limited probe (if both are enabled).").Value; - SvSimConfig.SweepLeaderSkinPools = - Config.Bind("Sweeps", "SweepLeaderSkinPools", false, - "On the first /pack/info response of the session, fire /pack/get_gacha_point_rewards (GachaPointExchangeInfoTask) for every parent_gacha_id in pack_config_list. Responses are captured into traffic.ndjson via the existing traffic hook. Extracts card_id → leader_skin_id mappings via reward_type=10 entries (plus emblem/sleeve associations as a bonus). One-shot per session. WARNING: hits prod API — paced at 0.5s/request, ~18s total for 35 packs.").Value; + SvSimConfig.SweepGachaExchange = + Config.Bind("Sweeps", "SweepGachaExchange", false, + "On the first /pack/info response of the session, fire /pack/get_gacha_point_rewards (GachaPointExchangeInfoTask) for every pack id in Wizard.Data.Master.CardSetNameMgr.GetList() (~279 ids), minus ids already known to fail from the persistent miss ledger at BepInEx/svsim-captures/gacha-sweep-misses.json. Replaces SweepLeaderSkinPools (which only covered the 35 in-catalog packs from /pack/info's pack_config_list). Responses captured into traffic.ndjson via the existing traffic hook. One-shot per session. WARNING: hits prod API — at 0.5s/request default, full sweep is ~2–5min wall-clock.").Value; + SvSimConfig.GachaExchangeSweepPacingSeconds = + Config.Bind("Sweeps", "GachaExchangeSweepPacingSeconds", 0.5f, + "Seconds to wait between consecutive /pack/get_gacha_point_rewards requests during the gacha-exchange sweep. Clamped to a minimum of 0.1s. Default 0.5s mirrors the old LeaderSkinPoolSweep pacing.").Value; + SvSimConfig.SweepDryRunIds = + Config.Bind("Sweeps", "SweepDryRunIds", "", + "Optional comma-separated list of pack ids to restrict the gacha-exchange sweep to (e.g. '80008,97002,93025'). Empty = full sweep. Use this to smoke-test the sweep on a handful of known-ambiguous packs before committing the session to a full ~5min run.").Value; SvSimConfig.SweepMainStory = Config.Bind("Sweeps", "SweepMainStory", false, "On the first story-section response of the session, walk every (section, chara, chapter) in Data.StoryWorldDataManager matching StoryApiType.MainStory and emit /main_story/start + /main_story/finish (no-battle skip shape) per chapter with is_skip_enabled. Captures master `special_battle_setting` payloads (server-only data) into traffic.ndjson via the existing hook. Per-family one-shot per session. SIDE EFFECT: unfinished chapters become is_skipped=true is_finish=false (blue 'Cleared' in UI, no rewards). Use a throwaway account. WARNING: hits prod API — at 5s pacing, ~6h for the full main-story tree.").Value; diff --git a/SVSimLoader/SvSimConfig.cs b/SVSimLoader/SvSimConfig.cs index 10ce8c4..a0804af 100644 --- a/SVSimLoader/SvSimConfig.cs +++ b/SVSimLoader/SvSimConfig.cs @@ -8,7 +8,9 @@ public static class SvSimConfig public static bool EnableTrafficCapture { get; set; } public static bool EnableBattleCapture { get; set; } public static bool DumpUserData { get; set; } - public static bool SweepLeaderSkinPools { get; set; } + public static bool SweepGachaExchange { get; set; } + public static float GachaExchangeSweepPacingSeconds { get; set; } + public static string SweepDryRunIds { get; set; } public static bool SweepMainStory { get; set; } public static bool SweepLimitedStory { get; set; } public static bool SweepEventStory { get; set; }