From cea9e4307e4a7454e65f2ff7d46b469d3adc26f1 Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Mon, 24 Jul 2023 21:37:34 -0400 Subject: [PATCH] fix(plugins/community/splatoon): upgrade s3si version --- source/plugins/community/splatoon/index.mjs | 2 +- .../plugins/community/splatoon/s3si/index.ts | 368 +++++++++++++----- 2 files changed, 280 insertions(+), 90 deletions(-) diff --git a/source/plugins/community/splatoon/index.mjs b/source/plugins/community/splatoon/index.mjs index 144edcfa..3daa34eb 100644 --- a/source/plugins/community/splatoon/index.mjs +++ b/source/plugins/community/splatoon/index.mjs @@ -216,7 +216,7 @@ export default async function({login, q, imports, data, account}, {enabled = fal icon: await imports.imgb64(assets.stages[data.detail.coopStage.name]), }, date: data.detail.playedTime, - grade: data.detail.afterGrade.name, + grade: data.detail.afterGrade?.name ?? "", })), ), } diff --git a/source/plugins/community/splatoon/s3si/index.ts b/source/plugins/community/splatoon/s3si/index.ts index f2b37097..2fe6c860 100644 --- a/source/plugins/community/splatoon/s3si/index.ts +++ b/source/plugins/community/splatoon/s3si/index.ts @@ -12,9 +12,26 @@ class APIError extends Error { } } const AGENT_NAME = "s3si.ts"; -const S3SI_VERSION = "0.3.1"; -const NSOAPP_VERSION = "2.5.0"; -const WEB_VIEW_VERSION = "3.0.0-2857bc50"; +const S3SI_VERSION = "0.4.5"; +const NSOAPP_VERSION = "2.6.0"; +const WEB_VIEW_VERSION = "4.0.0-d5178440"; +var Queries; +(function(Queries) { + Queries["HomeQuery"] = "7dcc64ea27a08e70919893a0d3f70871"; + Queries["LatestBattleHistoriesQuery"] = "0d90c7576f1916469b2ae69f64292c02"; + Queries["RegularBattleHistoriesQuery"] = "3baef04b095ad8975ea679d722bc17de"; + Queries["BankaraBattleHistoriesQuery"] = "0438ea6978ae8bd77c5d1250f4f84803"; + Queries["XBattleHistoriesQuery"] = "6796e3cd5dc3ebd51864dc709d899fc5"; + Queries["EventBattleHistoriesQuery"] = "9744fcf676441873c7c8a51285b6aa4d"; + Queries["PrivateBattleHistoriesQuery"] = "8e5ae78b194264a6c230e262d069bd28"; + Queries["VsHistoryDetailQuery"] = "9ee0099fbe3d8db2a838a75cf42856dd"; + Queries["CoopHistoryQuery"] = "91b917becd2fa415890f5b47e15ffb15"; + Queries["CoopHistoryDetailQuery"] = "379f0d9b78b531be53044bcac031b34b"; + Queries["myOutfitCommonDataFilteringConditionQuery"] = "d02ab22c9dccc440076055c8baa0fa7a"; + Queries["myOutfitCommonDataEquipmentsQuery"] = "d29cd0c2b5e6bac90dd5b817914832f8"; + Queries["HistoryRecordQuery"] = "d9246baf077b2a29b5f7aac321810a77"; + Queries["ConfigureAnalyticsQuery"] = "f8ae00773cc412a50dd41a6d9a159ddd"; +})(Queries || (Queries = {})); const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts"; const USERAGENT = `${AGENT_NAME}/${S3SI_VERSION} (${S3SI_LINK})`; const DEFAULT_APP_USER_AGENT = "Mozilla/5.0 (Linux; Android 11; Pixel 5) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/94.0.4606.61 Mobile Safari/537.36"; @@ -1243,13 +1260,13 @@ const MIN_BUF_SIZE = 16; const CR = "\r".charCodeAt(0); const LF = "\n".charCodeAt(0); class BufferFullError extends Error { + partial; name; constructor(partial){ super("Buffer full"); this.partial = partial; this.name = "BufferFullError"; } - partial; } class PartialReadError extends Error { name = "PartialReadError"; @@ -1753,6 +1770,8 @@ class MultiReader { } } class LimitedReader { + reader; + limit; constructor(reader, limit){ this.reader = reader; this.limit = limit; @@ -1771,8 +1790,6 @@ class LimitedReader { this.limit -= n; return n; } - reader; - limit; } const DEFAULT_BUFFER_SIZE = 32 * 1024; async function copyN(r, dest, size) { @@ -1836,6 +1853,7 @@ function sliceLongToBytes(d, dest = Array.from({ } const decoder = new TextDecoder(); class StringWriter { + base; #chunks; #byteLength; #cache; @@ -1869,7 +1887,6 @@ class StringWriter { this.#cache = decoder.decode(buf); return this.#cache; } - base; } const mod2 = { copyN: copyN, @@ -2144,12 +2161,12 @@ function utf8DecodeTD(bytes, inputOffset, byteLength) { return sharedTextDecoder.decode(stringBytes); } class ExtData { + type; + data; constructor(type, data){ this.type = type; this.data = data; } - type; - data; } function setUint64(view, offset, value) { const high = value / 0x1_0000_0000; @@ -2336,6 +2353,13 @@ function createDataView(buffer) { return new DataView(bufferView.buffer, bufferView.byteOffset, bufferView.byteLength); } class Encoder { + extensionCodec; + context; + maxDepth; + initialBufferSize; + sortKeys; + forceFloat32; + ignoreUndefined; pos; view; bytes; @@ -2645,13 +2669,6 @@ class Encoder { setInt64(this.view, this.pos, value); this.pos += 8; } - extensionCodec; - context; - maxDepth; - initialBufferSize; - sortKeys; - forceFloat32; - ignoreUndefined; } const defaultEncodeOptions = {}; function encode1(value, options = defaultEncodeOptions) { @@ -2662,6 +2679,8 @@ function prettyByte(__byte) { return `${__byte < 0 ? "-" : ""}0x${Math.abs(__byte).toString(16).padStart(2, "0")}`; } class CachedKeyDecoder { + maxKeyLength; + maxLengthPerKey; hit; miss; caches; @@ -2717,8 +2736,6 @@ class CachedKeyDecoder { this.store(slicedCopyOfBytes, value); return value; } - maxKeyLength; - maxLengthPerKey; } var State; (function(State) { @@ -2744,6 +2761,14 @@ const DataViewIndexOutOfBoundsError = (()=>{ const MORE_DATA = new DataViewIndexOutOfBoundsError("Insufficient data"); const sharedCachedKeyDecoder = new CachedKeyDecoder(); class Decoder { + extensionCodec; + context; + maxStrLength; + maxBinLength; + maxArrayLength; + maxMapLength; + maxExtLength; + keyDecoder; totalPos; pos; view; @@ -3191,14 +3216,6 @@ class Decoder { this.pos += 8; return value; } - extensionCodec; - context; - maxStrLength; - maxBinLength; - maxArrayLength; - maxMapLength; - maxExtLength; - keyDecoder; } const defaultDecodeOptions = {}; function decode1(buffer, options = defaultDecodeOptions) { @@ -4449,7 +4466,7 @@ function globToRegExp(glob, { extended =true , globstar: globstarOption = true , else if (value == "lower") segment += "a-z"; else if (value == "print") segment += "\x20-\x7E"; else if (value == "punct") { - segment += "!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_ÔÇÿ{|}~"; + segment += "!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_‘{|}~"; } else if (value == "space") segment += "\\s\v"; else if (value == "upper") segment += "A-Z"; else if (value == "word") segment += "\\w"; @@ -5072,6 +5089,11 @@ function urlSimplify(url) { return url; } } +const battleTime = (id)=>{ + const { timestamp } = parseHistoryDetailId(id); + const dateStr = timestamp.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/, "$1-$2-$3T$4:$5:$6Z"); + return new Date(dateStr); +}; async function loginSteps({ newFetcher }, step2) { const fetch = newFetcher(); if (!step2) { @@ -5182,12 +5204,13 @@ async function getGToken({ fApi , sessionToken , env }) { } }); const uiRespJson = await uiResp.json(); - const { nickname , birthday , language , country } = uiRespJson; + const { nickname , birthday , language , country , id: userId } = uiRespJson; const getIdToken2 = async (idToken)=>{ const { f , request_id: requestId , timestamp } = await callImink({ fApi, step: 1, idToken, + userId, env }); const resp = await fetch.post({ @@ -5214,20 +5237,26 @@ async function getGToken({ fApi , sessionToken , env }) { }); const respJson = await resp.json(); const idToken2 = respJson?.result?.webApiServerCredential?.accessToken; - if (!idToken2) { + const coralUserId = respJson?.result?.user?.id?.toString(); + if (!idToken2 || !coralUserId) { throw new APIError({ response: resp, json: respJson, - message: "No idToken2 found" + message: `No idToken2 or coralUserId found. Please try again later. (${idToken2.length}, ${coralUserId.length})` }); } - return idToken2; + return [ + idToken2, + coralUserId + ]; }; - const getGToken = async (idToken)=>{ + const getGToken = async (idToken, coralUserId)=>{ const { f , request_id: requestId , timestamp } = await callImink({ step: 2, idToken, fApi, + userId, + coralUserId, env }); const resp = await fetch.post({ @@ -5261,8 +5290,8 @@ async function getGToken({ fApi , sessionToken , env }) { } return webServiceToken; }; - const idToken2 = await retry(()=>getIdToken2(idToken)); - const webServiceToken = await retry(()=>getGToken(idToken2)); + const [idToken2, coralUserId] = await retry(()=>getIdToken2(idToken)); + const webServiceToken = await retry(()=>getGToken(idToken2, coralUserId)); return { webServiceToken, nickname, @@ -5359,7 +5388,8 @@ async function getSessionToken({ fetch , sessionTokenCode , authCodeVerifier }) } return json["session_token"]; } -async function callImink({ fApi , step , idToken , env }) { +async function callImink(params) { + const { fApi , step , idToken , userId , coralUserId , env } = params; const { post } = env.newFetcher(); const resp = await post({ url: fApi, @@ -5369,7 +5399,9 @@ async function callImink({ fApi , step , idToken , env }) { }, body: JSON.stringify({ "token": idToken, - "hash_method": step + "hash_method": step, + "na_id": userId, + "coral_user_id": coralUserId }) }); return await resp.json(); @@ -5422,6 +5454,7 @@ const DEFAULT_STATE = { monitorInterval: 500 }; class FileStateBackend { + path; constructor(path){ this.path = path; } @@ -5436,7 +5469,6 @@ class FileStateBackend { await Deno.writeTextFile(swapPath, data); await Deno.rename(swapPath, this.path); } - path; } class Profile { _state; @@ -5469,29 +5501,15 @@ class Profile { } } } -var Queries; -(function(Queries) { - Queries["HomeQuery"] = "22e2fa8294168003c21b00c333c35384"; - Queries["LatestBattleHistoriesQuery"] = "4f5f26e64bca394b45345a65a2f383bd"; - Queries["RegularBattleHistoriesQuery"] = "d5b795d09e67ce153e622a184b7e7dfa"; - Queries["BankaraBattleHistoriesQuery"] = "de4754588109b77dbcb90fbe44b612ee"; - Queries["XBattleHistoriesQuery"] = "45c74fefb45a49073207229ca65f0a62"; - Queries["PrivateBattleHistoriesQuery"] = "1d6ed57dc8b801863126ad4f351dfb9a"; - Queries["VsHistoryDetailQuery"] = "291295ad311b99a6288fc95a5c4cb2d2"; - Queries["CoopHistoryQuery"] = "6ed02537e4a65bbb5e7f4f23092f6154"; - Queries["CoopHistoryDetailQuery"] = "379f0d9b78b531be53044bcac031b34b"; - Queries["myOutfitCommonDataFilteringConditionQuery"] = "d02ab22c9dccc440076055c8baa0fa7a"; - Queries["myOutfitCommonDataEquipmentsQuery"] = "d29cd0c2b5e6bac90dd5b817914832f8"; - Queries["HistoryRecordQuery"] = "32b6771f94083d8f04848109b7300af5"; - Queries["ConfigureAnalyticsQuery"] = "f8ae00773cc412a50dd41a6d9a159ddd"; -})(Queries || (Queries = {})); var BattleListType; (function(BattleListType) { BattleListType[BattleListType["Latest"] = 0] = "Latest"; BattleListType[BattleListType["Regular"] = 1] = "Regular"; BattleListType[BattleListType["Bankara"] = 2] = "Bankara"; - BattleListType[BattleListType["Private"] = 3] = "Private"; - BattleListType[BattleListType["Coop"] = 4] = "Coop"; + BattleListType[BattleListType["Event"] = 3] = "Event"; + BattleListType[BattleListType["XBattle"] = 4] = "XBattle"; + BattleListType[BattleListType["Private"] = 5] = "Private"; + BattleListType[BattleListType["Coop"] = 6] = "Coop"; })(BattleListType || (BattleListType = {})); var SummaryEnum; (function(SummaryEnum) { @@ -5596,6 +5614,8 @@ class Splatnet3 { [BattleListType.Latest]: ()=>this.request(Queries.LatestBattleHistoriesQuery).then((r)=>getIdsFromGroups(r.latestBattleHistories)), [BattleListType.Regular]: ()=>this.request(Queries.RegularBattleHistoriesQuery).then((r)=>getIdsFromGroups(r.regularBattleHistories)), [BattleListType.Bankara]: ()=>this.request(Queries.BankaraBattleHistoriesQuery).then((r)=>getIdsFromGroups(r.bankaraBattleHistories)), + [BattleListType.XBattle]: ()=>this.request(Queries.XBattleHistoriesQuery).then((r)=>getIdsFromGroups(r.xBattleHistories)), + [BattleListType.Event]: ()=>this.request(Queries.EventBattleHistoriesQuery).then((r)=>getIdsFromGroups(r.eventBattleHistories)), [BattleListType.Private]: ()=>this.request(Queries.PrivateBattleHistoriesQuery).then((r)=>getIdsFromGroups(r.privateBattleHistories)), [BattleListType.Coop]: ()=>this.request(Queries.CoopHistoryQuery).then((r)=>getIdsFromGroups(r.coopResult)) }; @@ -5614,6 +5634,24 @@ class Splatnet3 { async getBattleList(battleListType = BattleListType.Latest) { return await this.BATTLE_LIST_TYPE_MAP[battleListType](); } + async getAllBattleList() { + const ALL_TYPE = [ + BattleListType.Regular, + BattleListType.Bankara, + BattleListType.XBattle, + BattleListType.Event, + BattleListType.Private + ]; + const ids = []; + for (const type of ALL_TYPE){ + ids.push(...await this.getBattleList(type)); + } + const timeMap = new Map(ids.map((id)=>[ + id, + battleTime(id) + ])); + return ids.sort((a, b)=>timeMap.get(b).getTime() - timeMap.get(a).getTime()); + } getBattleDetail(id) { return this.request(Queries.VsHistoryDetailQuery, { vsResultId: id @@ -5689,6 +5727,7 @@ class MemoryCache { } } class FileCache { + path; constructor(path){ this.path = path; } @@ -5720,8 +5759,8 @@ class FileCache { await Deno.writeFile(swapPath, data); await Deno.rename(swapPath, path); } - path; } +const __default = JSON.parse("[\n {\n \"key\": \"ink_saver_main\",\n \"name\": {\n \"de-DE\": \"Hauptverbrauch\",\n \"en-GB\": \"Ink Saver (Main)\",\n \"en-US\": \"Ink Saver (Main)\",\n \"es-ES\": \"Tintahorro (ppal.)\",\n \"es-MX\": \"Ahorro tinta (ppal.)\",\n \"fr-CA\": \"Encrémenteur (pr.)\",\n \"fr-FR\": \"Encrémenteur (pr.)\",\n \"it-IT\": \"Eco-colore princ.\",\n \"ja-JP\": \"インク効率アップ(メイン)\",\n \"ko-KR\": \"잉크 효율 업(메인)\",\n \"nl-NL\": \"Hoofdspaarder\",\n \"ru-RU\": \"Основной баллон X\",\n \"zh-CN\": \"提升墨汁效率(主要武器)\",\n \"zh-TW\": \"提升墨汁效率(主要武器)\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"ink_saver_sub\",\n \"name\": {\n \"de-DE\": \"Sekundärverbrauch\",\n \"en-GB\": \"Ink Saver (Sub)\",\n \"en-US\": \"Ink Saver (Sub)\",\n \"es-ES\": \"Tintahorro (sec.)\",\n \"es-MX\": \"Ahorro tinta (sec.)\",\n \"fr-CA\": \"Encrémenteur (sec.)\",\n \"fr-FR\": \"Encrémenteur (sec.)\",\n \"it-IT\": \"Eco-colore second.\",\n \"ja-JP\": \"インク効率アップ(サブ)\",\n \"ko-KR\": \"잉크 효율 업(서브)\",\n \"nl-NL\": \"Subspaarder\",\n \"ru-RU\": \"Запасной баллон X\",\n \"zh-CN\": \"提升墨汁效率(次要武器)\",\n \"zh-TW\": \"提升墨汁效率(次要武器)\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"ink_recovery_up\",\n \"name\": {\n \"de-DE\": \"Regeneration +\",\n \"en-GB\": \"Ink Recovery Up\",\n \"en-US\": \"Ink Recovery Up\",\n \"es-ES\": \"Recarga rápida\",\n \"es-MX\": \"Mejor recarga tinta\",\n \"fr-CA\": \"Levée d'encre\",\n \"fr-FR\": \"Levée d'encre\",\n \"it-IT\": \"Recupero colore +\",\n \"ja-JP\": \"インク回復力アップ\",\n \"ko-KR\": \"잉크 회복력 업\",\n \"nl-NL\": \"Inktvulling\",\n \"ru-RU\": \"Быстрый баллон\",\n \"zh-CN\": \"提升墨汁回复力\",\n \"zh-TW\": \"提升墨汁回復力\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"run_speed_up\",\n \"name\": {\n \"de-DE\": \"Lauftempo +\",\n \"en-GB\": \"Run Speed Up\",\n \"en-US\": \"Run Speed Up\",\n \"es-ES\": \"Supercarrera\",\n \"es-MX\": \"Carrera acelerada\",\n \"fr-CA\": \"Course à pied\",\n \"fr-FR\": \"Course à pied\",\n \"it-IT\": \"Velocità +\",\n \"ja-JP\": \"ヒト移動速度アップ\",\n \"ko-KR\": \"인간 이동 속도 업\",\n \"nl-NL\": \"Hardloper\",\n \"ru-RU\": \"Спринтер\",\n \"zh-CN\": \"提升人类移动速度\",\n \"zh-TW\": \"提升人類移動速度\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"swim_speed_up\",\n \"name\": {\n \"de-DE\": \"Schwimmtempo +\",\n \"en-GB\": \"Swim Speed Up\",\n \"en-US\": \"Swim Speed Up\",\n \"es-ES\": \"Superbuceo\",\n \"es-MX\": \"Nado acelerado\",\n \"fr-CA\": \"Turbo-calmar\",\n \"fr-FR\": \"Turbo-calamar\",\n \"it-IT\": \"Velocità nuoto +\",\n \"ja-JP\": \"イカダッシュ速度アップ\",\n \"ko-KR\": \"징어대시 속도 업\",\n \"nl-NL\": \"Zwemdiploma\",\n \"ru-RU\": \"Плавунец\",\n \"zh-CN\": \"提升鱿鱼冲刺速度\",\n \"zh-TW\": \"提升魷魚衝刺速度\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"special_charge_up\",\n \"name\": {\n \"de-DE\": \"Spezialladezeit +\",\n \"en-GB\": \"Special Charge Up\",\n \"en-US\": \"Special Charge Up\",\n \"es-ES\": \"Recarga especial\",\n \"es-MX\": \"Recarga especial\",\n \"fr-CA\": \"Jauge spéciale +\",\n \"fr-FR\": \"Jauge spéciale +\",\n \"it-IT\": \"Ricarica speciale +\",\n \"ja-JP\": \"スペシャル増加量アップ\",\n \"ko-KR\": \"스페셜 증가량 업\",\n \"nl-NL\": \"Speciaallader\",\n \"ru-RU\": \"Особый насос\",\n \"zh-CN\": \"提升特殊武器增加量\",\n \"zh-TW\": \"提升特殊武器增加量\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"special_saver\",\n \"name\": {\n \"de-DE\": \"Spezialabzug -\",\n \"en-GB\": \"Special Saver\",\n \"en-US\": \"Special Saver\",\n \"es-ES\": \"Reducción especial\",\n \"es-MX\": \"Ahorro especial\",\n \"fr-CA\": \"Baisse spéciale -\",\n \"fr-FR\": \"Baisse spéciale -\",\n \"it-IT\": \"Riduzione speciale -\",\n \"ja-JP\": \"スペシャル減少量ダウン\",\n \"ko-KR\": \"스페셜 감소량 다운\",\n \"nl-NL\": \"Speciaalspaarder\",\n \"ru-RU\": \"Особый резерв\",\n \"zh-CN\": \"降低特殊武器减少量\",\n \"zh-TW\": \"降低特殊武器減少量\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"special_power_up\",\n \"name\": {\n \"de-DE\": \"Spezialstärke +\",\n \"en-GB\": \"Special Power Up\",\n \"en-US\": \"Special Power Up\",\n \"es-ES\": \"Superarma especial\",\n \"es-MX\": \"Mejora especial\",\n \"fr-CA\": \"Arme spéciale +\",\n \"fr-FR\": \"Arme spéciale +\",\n \"it-IT\": \"Arma speciale +\",\n \"ja-JP\": \"スペシャル性能アップ\",\n \"ko-KR\": \"스페셜 성능 업\",\n \"nl-NL\": \"Specialist\",\n \"ru-RU\": \"Особый подход\",\n \"zh-CN\": \"提升特殊武器性能\",\n \"zh-TW\": \"提升特殊武器性能\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"quick_respawn\",\n \"name\": {\n \"de-DE\": \"Schnelle Rückkehr\",\n \"en-GB\": \"Quick Respawn\",\n \"en-US\": \"Quick Respawn\",\n \"es-ES\": \"Retorno exprés\",\n \"es-MX\": \"Regeneración rápida\",\n \"fr-CA\": \"Sans temps mort\",\n \"fr-FR\": \"Sans temps morts\",\n \"it-IT\": \"Il tempo è colore\",\n \"ja-JP\": \"復活時間短縮\",\n \"ko-KR\": \"부활 시간 단축\",\n \"nl-NL\": \"Comeback\",\n \"ru-RU\": \"Феникс\",\n \"zh-CN\": \"缩短复活时间\",\n \"zh-TW\": \"縮短復活時間\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"quick_super_jump\",\n \"name\": {\n \"de-DE\": \"Supersprung +\",\n \"en-GB\": \"Quick Super Jump\",\n \"en-US\": \"Quick Super Jump\",\n \"es-ES\": \"Supersalto rápido\",\n \"es-MX\": \"Supersalto rápido\",\n \"fr-CA\": \"Aérodynamisme\",\n \"fr-FR\": \"Aérodynamisme\",\n \"it-IT\": \"Salti super e veloci\",\n \"ja-JP\": \"スーパージャンプ時間短縮\",\n \"ko-KR\": \"슈퍼 점프 시간 단축\",\n \"nl-NL\": \"Turbosprong\",\n \"ru-RU\": \"Суперпрыгун\",\n \"zh-CN\": \"缩短超级跳跃时间\",\n \"zh-TW\": \"縮短超級跳躍時間\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"sub_power_up\",\n \"name\": {\n \"de-DE\": \"Sekundärstärke +\",\n \"en-GB\": \"Sub Power Up\",\n \"en-US\": \"Sub Power Up\",\n \"es-ES\": \"Superarma secundaria\",\n \"es-MX\": \"Mejora secundaria\",\n \"fr-CA\": \"Arme secondaire +\",\n \"fr-FR\": \"Arme secondaire +\",\n \"it-IT\": \"Arma secondaria +\",\n \"ja-JP\": \"サブ性能アップ\",\n \"ko-KR\": \"서브 성능 업\",\n \"nl-NL\": \"Subtopper\",\n \"ru-RU\": \"Про-Запас\",\n \"zh-CN\": \"提升次要武器性能\",\n \"zh-TW\": \"提升次要武器性能\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"ink_resistance_up\",\n \"name\": {\n \"de-DE\": \"Tintentoleranz +\",\n \"en-GB\": \"Ink Resistance Up\",\n \"en-US\": \"Ink Resistance Up\",\n \"es-ES\": \"Impermeabilidad\",\n \"es-MX\": \"Impermeabilidad\",\n \"fr-CA\": \"Imperméabilité\",\n \"fr-FR\": \"Pieds au sec\",\n \"it-IT\": \"Scarpe impermeabili\",\n \"ja-JP\": \"相手インク影響軽減\",\n \"ko-KR\": \"상대 잉크 영향 감소\",\n \"nl-NL\": \"Inkttolerantie\",\n \"ru-RU\": \"Краскостойкость\",\n \"zh-CN\": \"减轻对手墨汁影响\",\n \"zh-TW\": \"減輕對手墨汁影響\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"sub_resistance_up\",\n \"name\": {\n \"de-DE\": \"Sekundärschutz +\",\n \"en-GB\": \"Sub Resistance Up\",\n \"en-US\": \"Sub Resistance Up\",\n \"es-ES\": \"Resistencia secundaria\",\n \"es-MX\": \"Resistencia secundaria\",\n \"fr-CA\": \"Filtre à secondaires\",\n \"fr-FR\": \"Filtre à secondaires\",\n \"it-IT\": \"Arma sec. impermeabile\",\n \"ja-JP\": \"サブ影響軽減\",\n \"ko-KR\": \"서브 영향 감소\",\n \"nl-NL\": \"Subdemper\",\n \"ru-RU\": \"Стойкость запаса\",\n \"zh-CN\": \"减轻次要武器影响\",\n \"zh-TW\": \"減輕次要武器影響\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"intensify_action\",\n \"name\": {\n \"de-DE\": \"Action +\",\n \"en-GB\": \"Intensify Action\",\n \"en-US\": \"Intensify Action\",\n \"es-ES\": \"Agilidad extra\",\n \"es-MX\": \"Agilidad extra\",\n \"fr-CA\": \"Feu de l'action\",\n \"fr-FR\": \"Feu de l'action\",\n \"it-IT\": \"Intensificazione\",\n \"ja-JP\": \"アクション強化\",\n \"ko-KR\": \"액션 강화\",\n \"nl-NL\": \"Actie-assistentie\",\n \"ru-RU\": \"Ультраудар\",\n \"zh-CN\": \"行动强化\",\n \"zh-TW\": \"行動強化\"\n },\n \"primary_only\": false\n },\n {\n \"key\": \"opening_gambit\",\n \"name\": {\n \"de-DE\": \"Startvorteil\",\n \"en-GB\": \"Opening Gambit\",\n \"en-US\": \"Opening Gambit\",\n \"es-ES\": \"Acelerón de salida\",\n \"es-MX\": \"Acelerón de salida\",\n \"fr-CA\": \"Départ toute allure\",\n \"fr-FR\": \"Chapeaux de roue\",\n \"it-IT\": \"Partenza a razzo\",\n \"ja-JP\": \"スタートダッシュ\",\n \"ko-KR\": \"스타트 대시\",\n \"nl-NL\": \"Vliegende start\",\n \"ru-RU\": \"Стартовый спурт\",\n \"zh-CN\": \"最初冲刺\",\n \"zh-TW\": \"最初衝刺\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"last_ditch_effort\",\n \"name\": {\n \"de-DE\": \"Endspurt\",\n \"en-GB\": \"Last-Ditch Effort\",\n \"en-US\": \"Last-Ditch Effort\",\n \"es-ES\": \"Sprint final\",\n \"es-MX\": \"Último recurso\",\n \"fr-CA\": \"Ultime sursaut\",\n \"fr-FR\": \"Ultime sursaut\",\n \"it-IT\": \"Splash finale\",\n \"ja-JP\": \"ラストスパート\",\n \"ko-KR\": \"라스트 스퍼트\",\n \"nl-NL\": \"Eindsprint\",\n \"ru-RU\": \"Финишный спурт\",\n \"zh-CN\": \"最后冲刺\",\n \"zh-TW\": \"最後衝刺\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"tenacity\",\n \"name\": {\n \"de-DE\": \"Zähigkeit\",\n \"en-GB\": \"Tenacity\",\n \"en-US\": \"Tenacity\",\n \"es-ES\": \"Ventaja\",\n \"es-MX\": \"Tenacidad\",\n \"fr-CA\": \"Ténacité\",\n \"fr-FR\": \"Justice\",\n \"it-IT\": \"Tenacia\",\n \"ja-JP\": \"逆境強化\",\n \"ko-KR\": \"역경 강화\",\n \"nl-NL\": \"Volharding\",\n \"ru-RU\": \"Компенсатор\",\n \"zh-CN\": \"逆境强化\",\n \"zh-TW\": \"逆境強化\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"comeback\",\n \"name\": {\n \"de-DE\": \"Rückkehr\",\n \"en-GB\": \"Comeback\",\n \"en-US\": \"Comeback\",\n \"es-ES\": \"Remontada\",\n \"es-MX\": \"Remonte\",\n \"fr-CA\": \"Retour\",\n \"fr-FR\": \"Come-back\",\n \"it-IT\": \"Gran ritorno\",\n \"ja-JP\": \"カムバック\",\n \"ko-KR\": \"컴백\",\n \"nl-NL\": \"Opfrisser\",\n \"ru-RU\": \"Ответный удар\",\n \"zh-CN\": \"回归\",\n \"zh-TW\": \"回歸\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"ninja_squid\",\n \"name\": {\n \"de-DE\": \"Tintenfisch-Ninja\",\n \"en-GB\": \"Ninja Squid\",\n \"en-US\": \"Ninja Squid\",\n \"es-ES\": \"Ninjalamar\",\n \"es-MX\": \"Ninjalamar\",\n \"fr-CA\": \"Ninjalmar\",\n \"fr-FR\": \"Ninjalamar\",\n \"it-IT\": \"Calamaro ninja\",\n \"ja-JP\": \"イカニンジャ\",\n \"ko-KR\": \"징어닌자\",\n \"nl-NL\": \"Ninja-inktvis\",\n \"ru-RU\": \"Мимикрия\",\n \"zh-CN\": \"鱿鱼忍者\",\n \"zh-TW\": \"魷魚忍者\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"haunt\",\n \"name\": {\n \"de-DE\": \"Vergeltung\",\n \"en-GB\": \"Haunt\",\n \"en-US\": \"Haunt\",\n \"es-ES\": \"Represalia\",\n \"es-MX\": \"Resentimiento\",\n \"fr-CA\": \"Vengeance\",\n \"fr-FR\": \"Revanche\",\n \"it-IT\": \"Rappresaglia\",\n \"ja-JP\": \"リベンジ\",\n \"ko-KR\": \"리벤지\",\n \"nl-NL\": \"Revanche\",\n \"ru-RU\": \"Вендетта\",\n \"zh-CN\": \"复仇\",\n \"zh-TW\": \"復仇\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"thermal_ink\",\n \"name\": {\n \"de-DE\": \"Markierfarbe\",\n \"en-GB\": \"Thermal Ink\",\n \"en-US\": \"Thermal Ink\",\n \"es-ES\": \"Señuelo\",\n \"es-MX\": \"Tinta rastreadora\",\n \"fr-CA\": \"Encre thermique\",\n \"fr-FR\": \"Encre thermique\",\n \"it-IT\": \"Inchiostro termico\",\n \"ja-JP\": \"サーマルインク\",\n \"ko-KR\": \"서멀 잉크\",\n \"nl-NL\": \"Markeerstift\",\n \"ru-RU\": \"Клеймо\",\n \"zh-CN\": \"热力墨汁\",\n \"zh-TW\": \"熱力墨汁\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"respawn_punisher\",\n \"name\": {\n \"de-DE\": \"Heimsuchung\",\n \"en-GB\": \"Respawn Punisher\",\n \"en-US\": \"Respawn Punisher\",\n \"es-ES\": \"Castigo póstumo\",\n \"es-MX\": \"Castigo póstumo\",\n \"fr-CA\": \"Retour perdant\",\n \"fr-FR\": \"Retour perdant\",\n \"it-IT\": \"Castigo\",\n \"ja-JP\": \"復活ペナルティアップ\",\n \"ko-KR\": \"부활 페널티 업\",\n \"nl-NL\": \"Repercussie\",\n \"ru-RU\": \"Кара\",\n \"zh-CN\": \"提升复活惩罚\",\n \"zh-TW\": \"提升復活懲罰\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"ability_doubler\",\n \"name\": {\n \"de-DE\": \"Effektdoppelung\",\n \"en-GB\": \"Ability Doubler\",\n \"en-US\": \"Ability Doubler\",\n \"es-ES\": \"Duplicador\",\n \"es-MX\": \"Duplicador\",\n \"fr-CA\": \"Bonus ×2\",\n \"fr-FR\": \"Bonus ×2\",\n \"it-IT\": \"Raddoppiatore\",\n \"ja-JP\": \"追加ギアパワー倍化\",\n \"ko-KR\": \"추가 기어 파워 2배\",\n \"nl-NL\": \"Verdubbelaar\",\n \"ru-RU\": \"Дупликатор\",\n \"zh-CN\": \"追加装备能力增倍\",\n \"zh-TW\": \"追加裝備能力增倍\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"stealth_jump\",\n \"name\": {\n \"de-DE\": \"Sprunginfiltration\",\n \"en-GB\": \"Stealth Jump\",\n \"en-US\": \"Stealth Jump\",\n \"es-ES\": \"Supersalto invisible\",\n \"es-MX\": \"Supersalto invisible\",\n \"fr-CA\": \"Super saut invisible\",\n \"fr-FR\": \"Réception réussie\",\n \"it-IT\": \"Salto al buio\",\n \"ja-JP\": \"ステルスジャンプ\",\n \"ko-KR\": \"스텔스 점프\",\n \"nl-NL\": \"Sluipsprong\",\n \"ru-RU\": \"Десант\",\n \"zh-CN\": \"隐身跳跃\",\n \"zh-TW\": \"隱身跳躍\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"object_shredder\",\n \"name\": {\n \"de-DE\": \"Zerstörer\",\n \"en-GB\": \"Object Shredder\",\n \"en-US\": \"Object Shredder\",\n \"es-ES\": \"Demolición\",\n \"es-MX\": \"Demolición\",\n \"fr-CA\": \"Démolition\",\n \"fr-FR\": \"Démolition\",\n \"it-IT\": \"Demolitore\",\n \"ja-JP\": \"対物攻撃力アップ\",\n \"ko-KR\": \"오브젝트 공격력 업\",\n \"nl-NL\": \"Sloper\",\n \"ru-RU\": \"Демонтажник\",\n \"zh-CN\": \"提升对物体攻击力\",\n \"zh-TW\": \"提升對物體攻擊力\"\n },\n \"primary_only\": true\n },\n {\n \"key\": \"drop_roller\",\n \"name\": {\n \"de-DE\": \"Tricklandung\",\n \"en-GB\": \"Drop Roller\",\n \"en-US\": \"Drop Roller\",\n \"es-ES\": \"Amortiguador\",\n \"es-MX\": \"Aterrizaje rodante\",\n \"fr-CA\": \"Super roulade\",\n \"fr-FR\": \"Super roulade\",\n \"it-IT\": \"Atterraggio stiloso\",\n \"ja-JP\": \"受け身術\",\n \"ko-KR\": \"낙법\",\n \"nl-NL\": \"Rolmodel\",\n \"ru-RU\": \"Акробат\",\n \"zh-CN\": \"受身术\",\n \"zh-TW\": \"受身術\"\n },\n \"primary_only\": true\n }\n]"); const COOP_POINT_MAP = { 0: -20, 1: -10, @@ -5739,6 +5778,8 @@ async function checkResponse(resp) { } } class StatInkAPI { + statInkApiKey; + env; statInk; FETCH_LOCK; cache; @@ -5751,7 +5792,7 @@ class StatInkAPI { this._salmonWeaponMap = new Map(); this.getSalmonWeapon = ()=>this._getCached(`${this.statInk}/api/v3/salmon/weapon?full=1`); this.getWeapon = ()=>this._getCached(`${this.statInk}/api/v3/weapon?full=1`); - this.getAbility = ()=>this._getCached(`${this.statInk}/api/v3/ability?full=1`); + this.getAbility = ()=>__default; this.getStage = ()=>this._getCached(`${this.statInk}/api/v3/stage`); if (statInkApiKey.length !== 43) { throw new Error("Invalid stat.ink API key"); @@ -5854,8 +5895,8 @@ class StatInkAPI { } } _getAliasName(name) { - const STAT_INK_DOT = "┬À"; - const SPLATNET_DOT = "ÔǺ"; + const STAT_INK_DOT = "·"; + const SPLATNET_DOT = "‧"; if (name.includes(STAT_INK_DOT)) { return [ name, @@ -5890,8 +5931,6 @@ class StatInkAPI { getWeapon; getAbility; getStage; - statInkApiKey; - env; } class StatInkExporter { name = "stat.ink"; @@ -5964,6 +6003,8 @@ class StatInkExporter { } } else if (vsMode === "X_MATCH") { return "xmatch"; + } else if (vsMode === "LEAGUE") { + return "event"; } throw new TypeError(`Unknown vsMode ${vsMode}`); } @@ -6028,7 +6069,7 @@ class StatInkExporter { return result; }; async mapBattle({ groupInfo , challengeProgress , bankaraMatchChallenge , listNode , detail: vsDetail , rankBeforeState , rankState }) { - const { knockout , vsRule: { rule } , myTeam , otherTeams , bankaraMatch , festMatch , playedTime } = vsDetail; + const { knockout , vsRule: { rule } , myTeam , otherTeams , bankaraMatch , leagueMatch , festMatch , playedTime } = vsDetail; const self = vsDetail.myTeam.players.find((i)=>i.isMyself); if (!self) { throw new Error("Self not found"); @@ -6124,6 +6165,10 @@ class StatInkExporter { result.rank_after_s_plus = result.rank_before_s_plus; } } + if (leagueMatch) { + result.event = leagueMatch.leagueMatchEvent?.id; + result.event_power = leagueMatch.myLeaguePower; + } if (challengeProgress) { result.challenge_win = challengeProgress.winCount; result.challenge_lose = challengeProgress.loseCount; @@ -6134,6 +6179,7 @@ class StatInkExporter { result.x_power_after = groupInfo.xMatchMeasurement.xPowerAfter; } } + result.bankara_power_after = vsDetail.bankaraMatch?.bankaraPower?.power; if (rankBeforeState && rankState) { result.rank_before_exp = rankBeforeState.rankPoint; result.rank_after_exp = rankState.rankPoint; @@ -6237,7 +6283,8 @@ class StatInkExporter { golden_quota: wave.deliverNorm, golden_appearances: wave.goldenPopCount, golden_delivered: wave.teamDeliverCount, - special_uses + special_uses, + danger_rate: null }; } async mapCoop({ gradeBefore , groupInfo , detail }) { @@ -6255,9 +6302,10 @@ class StatInkExporter { ])); const title_after = detail.afterGrade ? b64Number(detail.afterGrade.id).toString() : undefined; const title_exp_after = detail.afterGradePoint; + const maxWaves = detail.rule === "TEAM_CONTEST" ? 5 : 3; let clear_waves; if (waveResults.length > 0) { - clear_waves = waveResults.filter((i)=>i.waveNumber < 4).length - 1 + (resultWave === 0 ? 1 : 0); + clear_waves = waveResults.filter((i)=>i.waveNumber < maxWaves + 1).length - 1 + (resultWave === 0 ? 1 : 0); } else { clear_waves = 0; } @@ -6282,7 +6330,7 @@ class StatInkExporter { } } let fail_reason = null; - if (clear_waves !== 3 && waveResults.length > 0) { + if (clear_waves !== maxWaves && waveResults.length > 0) { const lastWave = waveResults[waveResults.length - 1]; if (lastWave.teamDeliverCount >= lastWave.deliverNorm) { fail_reason = "wipe_out"; @@ -6292,8 +6340,9 @@ class StatInkExporter { uuid: await gameId(detail.id), private: groupInfo?.mode === "PRIVATE_CUSTOM" ? "yes" : "no", big_run: detail.rule === "BIG_RUN" ? "yes" : "no", + eggstra_work: detail.rule === "TEAM_CONTEST" ? "yes" : "no", stage: b64Number(detail.coopStage.id).toString(), - danger_rate: dangerRate * 100, + danger_rate: detail.rule === "TEAM_CONTEST" ? null : dangerRate * 100, clear_waves, fail_reason, king_smell: smellMeter, @@ -6326,6 +6375,53 @@ class StatInkExporter { automated: "yes", start_at: startedAt }; + if (detail.rule === "TEAM_CONTEST") { + let lastWave; + for (const [wave] of result.waves.map((p, i)=>[ + p, + i + ])){ + let haz_level; + if (!lastWave) { + haz_level = 60; + } else { + const num_players = result.players.length; + const quota = lastWave.golden_quota; + const delivered = lastWave.golden_delivered; + let added_percent = 0; + if (num_players == 4) { + if (delivered >= quota * 2) { + added_percent = 60; + } else if (delivered >= quota * 1.5) { + added_percent = 30; + } + } else if (num_players == 3) { + if (delivered >= quota * 2) { + added_percent = 40; + } else if (delivered >= quota * 1.5) { + added_percent = 20; + } + } else if (num_players == 2) { + if (delivered >= quota * 2) { + added_percent = 20; + } else if (delivered >= quota * 1.5) { + added_percent = 10; + added_percent = 5; + } + } else if (num_players == 1) { + if (delivered >= quota * 2) { + added_percent = 10; + } else if (delivered >= quota * 1.5) { + added_percent = 5; + } + } + const prev_percent = lastWave.danger_rate; + haz_level = prev_percent + added_percent; + } + wave.danger_rate = haz_level; + lastWave = wave; + } + } return result; } } @@ -6347,6 +6443,7 @@ function replacer(key, value) { return typeof value === "string" ? urlSimplify(value) : undefined; } class FileExporter { + exportPath; name; constructor(exportPath){ this.exportPath = exportPath; @@ -6455,7 +6552,6 @@ class FileExporter { } return out; } - exportPath; } const SEASONS = [ { @@ -6475,6 +6571,12 @@ const SEASONS = [ name: "Fresh Season 2023", start: new Date("2023-03-01T00:00:00+00:00"), end: new Date("2023-06-01T00:00:00+00:00") + }, + { + id: "season202306", + name: "Sizzle Season 2023", + start: new Date("2023-06-01T00:00:00+00:00"), + end: new Date("2023-09-01T00:00:00+00:00") } ]; const getSeason = (date)=>{ @@ -6665,11 +6767,6 @@ function addRank(state, delta) { } }; } -const battleTime = (id)=>{ - const { timestamp } = parseHistoryDetailId(id); - const dateStr = timestamp.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/, "$1-$2-$3T$4:$5:$6Z"); - return new Date(dateStr); -}; function beginPoint(state, flatten) { if (state) { const index = flatten.findIndex((i)=>i.gameId === state.gameId); @@ -6767,6 +6864,7 @@ function getRankStateByDelta(i) { }; } class RankTracker { + state; deltaMap; stateMap; constructor(state){ @@ -6782,6 +6880,9 @@ class RankTracker { this.state = state; } async updateState(history) { + if (history.length === 0) { + return; + } const flatten = await Promise.all(history.flatMap(({ historyDetails , bankaraMatchChallenge })=>{ return historyDetails.nodes.map((j, index)=>({ id: j.id, @@ -6811,7 +6912,6 @@ class RankTracker { } return curState; } - state; } class GameFetcher { _splatnet; @@ -7018,6 +7118,7 @@ const DEFAULT_OPTS = { noProgress: false, monitor: false, withSummary: false, + listMethod: "auto", env: DEFAULT_ENV }; class StepProgress { @@ -7033,6 +7134,89 @@ class StepProgress { this.skipped = {}; } } +class BattleListFetcher { + splatnet; + listMethod; + allBattleList; + latestBattleList; + allLock; + latestLock; + constructor(listMethod, splatnet){ + this.splatnet = splatnet; + this.allLock = new Mutex(); + this.latestLock = new Mutex(); + if (listMethod === "all") { + this.listMethod = "all"; + } else if (listMethod === "latest") { + this.listMethod = "latest"; + } else { + this.listMethod = "auto"; + } + } + getAllBattleList() { + return this.allLock.use(async ()=>{ + if (!this.allBattleList) { + this.allBattleList = await this.splatnet.getAllBattleList(); + } + return this.allBattleList; + }); + } + getLatestBattleList() { + return this.latestLock.use(async ()=>{ + if (!this.latestBattleList) { + this.latestBattleList = await this.splatnet.getBattleList(); + } + return this.latestBattleList; + }); + } + async innerFetch(exporter) { + if (this.listMethod === "latest") { + return await exporter.notExported({ + type: "VsInfo", + list: await this.getLatestBattleList() + }); + } + if (this.listMethod === "all") { + return await exporter.notExported({ + type: "VsInfo", + list: await this.getAllBattleList() + }); + } + if (this.listMethod === "auto") { + const latestList = await exporter.notExported({ + type: "VsInfo", + list: await this.getLatestBattleList() + }); + if (latestList.length === 50) { + return await exporter.notExported({ + type: "VsInfo", + list: await this.getAllBattleList() + }); + } + return latestList; + } + throw new TypeError(`Unknown listMethod: ${this.listMethod}`); + } + async fetch(exporter) { + return [ + ...await this.innerFetch(exporter) + ].reverse(); + } +} +class CoopListFetcher { + splatnet; + constructor(splatnet){ + this.splatnet = splatnet; + } + async fetch(exporter) { + return [ + ...await exporter.notExported({ + type: "CoopInfo", + list: await this.splatnet.getBattleList(BattleListType.Coop) + }) + ].reverse(); + } +} function progress({ total , currentUrl , done }) { return { total, @@ -7041,6 +7225,7 @@ function progress({ total , currentUrl , done }) { }; } class App { + opts; profile; env; constructor(opts){ @@ -7051,6 +7236,13 @@ class App { env: opts.env }); this.env = opts.env; + if (opts.listMethod && ![ + "all", + "auto", + "latest" + ].includes(opts.listMethod)) { + throw new TypeError(`Unknown listMethod: ${opts.listMethod}`); + } } getSkipMode() { const mode = this.opts.skipMode; @@ -7134,8 +7326,7 @@ class App { if (skipMode.includes("vs") || exporters.length === 0) { this.env.logger.log("Skip exporting VS games."); } else { - this.env.logger.log("Fetching battle list..."); - const gameList = await splatnet.getBattleList(); + const gameListFetcher = new BattleListFetcher(this.opts.listMethod ?? "auto", splatnet); const { redraw , endBar } = this.exporterProgress("Export vs games"); const fetcher = new GameFetcher({ cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir), @@ -7147,7 +7338,7 @@ class App { type: "VsInfo", fetcher, exporter: e, - gameList, + gameListFetcher, stepProgress: stats[e.name], onStep: ()=>{ redraw(e.name, progress(stats[e.name])); @@ -7171,8 +7362,7 @@ class App { if (skipMode.includes("coop") || exporters.length === 0) { this.env.logger.log("Skip exporting coop games."); } else { - this.env.logger.log("Fetching coop battle list..."); - const coopBattleList = await splatnet.getBattleList(BattleListType.Coop); + const gameListFetcher = new CoopListFetcher(splatnet); const { redraw , endBar } = this.exporterProgress("Export coop games"); const fetcher = new GameFetcher({ cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir), @@ -7183,7 +7373,7 @@ class App { type: "CoopInfo", fetcher, exporter: e, - gameList: coopBattleList, + gameListFetcher, stepProgress: stats[e.name], onStep: ()=>{ redraw(e.name, progress(stats[e.name])); @@ -7259,14 +7449,9 @@ class App { await this.exportOnce(); } } - async exportGameList({ type , fetcher , exporter , gameList , stepProgress , onStep }) { + async exportGameList({ type , fetcher , exporter , gameListFetcher , stepProgress , onStep }) { onStep?.(); - const workQueue = [ - ...await exporter.notExported({ - type, - list: gameList - }) - ].reverse(); + const workQueue = await gameListFetcher.fetch(exporter); const step = async (id)=>{ const detail = await fetcher.fetch(type, id); const result = await exporter.exportGame(detail); @@ -7299,14 +7484,14 @@ class App { this.env.logger.log(`Skipped ${Object.entries(stats).map(([name, { skipped }])=>Object.entries(skipped).map(([reason, count])=>`${name}: ${reason} (${count})`).join(", "))}`); } } - opts; } const parseArgs = (args)=>{ const parsed = mod1.parse(args, { string: [ "profilePath", "exporter", - "skipMode" + "skipMode", + "listMethod" ], boolean: [ "help", @@ -7334,7 +7519,8 @@ const parseArgs = (args)=>{ "s", "skip-mode" ], - "withSummary": "with-summary" + "withSummary": "with-summary", + "listMethod": "list-method" } }); return parsed; @@ -7348,6 +7534,10 @@ Options: --exporter , -e Exporter list to use (default: stat.ink) Multiple exporters can be separated by commas (e.g. "stat.ink,file") + --list-method When set to "latest", the latest 50 matches will be obtained. + When set to "all", matches of all modes will be obtained with a maximum of 250 matches (5 modes x 50 matches). + When set to "auto", the latest 50 matches will be obtained. If 50 matches have not been uploaded yet, matches will be obtained from the list of all modes. + "auto" is the default setting. --no-progress, -n Disable progress bar --monitor, -m Monitor mode --skip-mode , -s Skip mode (default: null)