feat: add udemae info
parent
5737f9da37
commit
74a0ef99ec
|
|
@ -1,4 +1,4 @@
|
|||
import { BattleExporter, VsHistoryDetail } from "../types.ts";
|
||||
import { BattleExporter, VsBattle } from "../types.ts";
|
||||
import { datetime, path } from "../deps.ts";
|
||||
import { NSOAPP_VERSION, S3SI_VERSION } from "../constant.ts";
|
||||
const FILENAME_FORMAT = "yyyyMMddHHmmss";
|
||||
|
|
@ -8,7 +8,7 @@ type FileExporterType = {
|
|||
nsoVersion: string;
|
||||
s3siVersion: string;
|
||||
exportTime: string;
|
||||
data: VsHistoryDetail;
|
||||
data: VsBattle;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -17,14 +17,14 @@ type FileExporterType = {
|
|||
* This is useful for debugging. It will write each battle detail to a file.
|
||||
* Timestamp is used as filename. Example: 2021-01-01T00:00:00.000Z.json
|
||||
*/
|
||||
export class FileExporter implements BattleExporter<VsHistoryDetail> {
|
||||
export class FileExporter implements BattleExporter<VsBattle> {
|
||||
name = "file";
|
||||
constructor(private exportPath: string) {
|
||||
}
|
||||
async exportBattle(detail: VsHistoryDetail) {
|
||||
async exportBattle(battle: VsBattle) {
|
||||
await Deno.mkdir(this.exportPath, { recursive: true });
|
||||
|
||||
const playedTime = new Date(detail.playedTime);
|
||||
const playedTime = new Date(battle.detail.playedTime);
|
||||
const filename = `${datetime.format(playedTime, FILENAME_FORMAT)}.json`;
|
||||
const filepath = path.join(this.exportPath, filename);
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ export class FileExporter implements BattleExporter<VsHistoryDetail> {
|
|||
nsoVersion: NSOAPP_VERSION,
|
||||
s3siVersion: S3SI_VERSION,
|
||||
exportTime: new Date().toISOString(),
|
||||
data: detail,
|
||||
data: battle,
|
||||
};
|
||||
|
||||
await Deno.writeTextFile(filepath, JSON.stringify(body));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
AGENT_NAME,
|
||||
S3SI_NAMESPACE,
|
||||
S3SI_VERSION,
|
||||
SPLATNET3_STATINK_MAP,
|
||||
USERAGENT,
|
||||
|
|
@ -10,31 +9,16 @@ import {
|
|||
StatInkPlayer,
|
||||
StatInkPostBody,
|
||||
StatInkStage,
|
||||
VsBattle,
|
||||
VsHistoryDetail,
|
||||
VsPlayer,
|
||||
} from "../types.ts";
|
||||
import { base64, msgpack, uuid } from "../deps.ts";
|
||||
import { base64, msgpack } from "../deps.ts";
|
||||
import { APIError } from "../APIError.ts";
|
||||
import { cache } from "../utils.ts";
|
||||
import { battleId, cache } from "../utils.ts";
|
||||
|
||||
const S3S_NAMESPACE = "b3a2dbf5-2c09-4792-b78c-00b548b70aeb";
|
||||
|
||||
/**
|
||||
* generate s3s uuid
|
||||
*
|
||||
* @param id ID from SplatNet3
|
||||
* @returns id generated from s3s
|
||||
*/
|
||||
function s3sUuid(id: string): Promise<string> {
|
||||
const fullId = base64.decode(id);
|
||||
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
|
||||
return uuid.v5.generate(S3S_NAMESPACE, tsUuid);
|
||||
}
|
||||
|
||||
function battleId(id: string): Promise<string> {
|
||||
return uuid.v5.generate(S3SI_NAMESPACE, new TextEncoder().encode(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode ID and get number after '-'
|
||||
*/
|
||||
|
|
@ -56,7 +40,7 @@ const getStage = cache(_getStage);
|
|||
*
|
||||
* This is the default exporter. It will upload each battle detail to stat.ink.
|
||||
*/
|
||||
export class StatInkExporter implements BattleExporter<VsHistoryDetail> {
|
||||
export class StatInkExporter implements BattleExporter<VsBattle> {
|
||||
name = "stat.ink";
|
||||
constructor(private statInkApiKey: string) {
|
||||
if (statInkApiKey.length !== 43) {
|
||||
|
|
@ -69,8 +53,8 @@ export class StatInkExporter implements BattleExporter<VsHistoryDetail> {
|
|||
"Authorization": `Bearer ${this.statInkApiKey}`,
|
||||
};
|
||||
}
|
||||
async exportBattle(detail: VsHistoryDetail) {
|
||||
const body = await this.mapBattle(detail);
|
||||
async exportBattle(battle: VsBattle) {
|
||||
const body = await this.mapBattle(battle);
|
||||
|
||||
const resp = await fetch("https://stat.ink/api/v3/battle", {
|
||||
method: "POST",
|
||||
|
|
@ -100,8 +84,6 @@ export class StatInkExporter implements BattleExporter<VsHistoryDetail> {
|
|||
json,
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("abort");
|
||||
}
|
||||
async notExported(list: string[]): Promise<string[]> {
|
||||
const uuid = await (await fetch("https://stat.ink/api/v3/s3s/uuid-list", {
|
||||
|
|
@ -111,7 +93,7 @@ export class StatInkExporter implements BattleExporter<VsHistoryDetail> {
|
|||
const out: string[] = [];
|
||||
|
||||
for (const id of list) {
|
||||
const s3sId = await s3sUuid(id);
|
||||
const s3sId = await battleId(id, S3S_NAMESPACE);
|
||||
const s3siId = await battleId(id);
|
||||
|
||||
if (!uuid.includes(s3sId) && !uuid.includes(s3siId)) {
|
||||
|
|
@ -181,10 +163,14 @@ export class StatInkExporter implements BattleExporter<VsHistoryDetail> {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
async mapBattle(vsDetail: VsHistoryDetail): Promise<StatInkPostBody> {
|
||||
async mapBattle(
|
||||
{ lastInChallenge, bankaraMatchChallenge, listNode, detail: vsDetail }:
|
||||
VsBattle,
|
||||
): Promise<StatInkPostBody> {
|
||||
const {
|
||||
knockout,
|
||||
vsMode: { mode },
|
||||
vsRule: { rule },
|
||||
myTeam,
|
||||
otherTeams,
|
||||
bankaraMatch,
|
||||
|
|
@ -244,7 +230,7 @@ export class StatInkExporter implements BattleExporter<VsHistoryDetail> {
|
|||
result.clout_change = festMatch.contribution;
|
||||
result.fest_power = festMatch.myFestPower ?? undefined;
|
||||
}
|
||||
if (mode === "FEST" || mode === "REGULAR") {
|
||||
if (rule === "TURF_WAR") {
|
||||
result.our_team_percent = (myTeam.result.paintRatio ?? 0) * 100;
|
||||
result.their_team_percent = (otherTeams?.[0].result.paintRatio ?? 0) *
|
||||
100;
|
||||
|
|
@ -257,17 +243,40 @@ export class StatInkExporter implements BattleExporter<VsHistoryDetail> {
|
|||
0,
|
||||
);
|
||||
}
|
||||
if (mode === "BANKARA") {
|
||||
if (!bankaraMatch) {
|
||||
throw new TypeError("bankaraMatch is null");
|
||||
}
|
||||
if (bankaraMatch) {
|
||||
result.our_team_count = myTeam.result.score ?? undefined;
|
||||
result.their_team_count = otherTeams?.[0].result.score ?? undefined;
|
||||
|
||||
result.knockout = (!knockout || knockout === "NEITHER") ? "no" : "yes";
|
||||
result.rank_exp_change = bankaraMatch.earnedUdemaePoint;
|
||||
}
|
||||
if (listNode) {
|
||||
[result.rank_before, result.rank_before_s_plus] = parseUdemae(
|
||||
listNode.udemae,
|
||||
);
|
||||
}
|
||||
if (bankaraMatchChallenge) {
|
||||
result.rank_up_battle = bankaraMatchChallenge.isPromo ? "yes" : "no";
|
||||
if (bankaraMatchChallenge.udemaeAfter) {
|
||||
[result.rank_after, result.rank_after_s_plus] = parseUdemae(
|
||||
bankaraMatchChallenge.udemaeAfter,
|
||||
);
|
||||
}
|
||||
if (lastInChallenge) {
|
||||
result.challenge_win = bankaraMatchChallenge.winCount;
|
||||
result.challenge_lose = bankaraMatchChallenge.loseCount;
|
||||
result.rank_exp_change = bankaraMatchChallenge.earnedUdemaePoint;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function parseUdemae(udemae: string): [string, number | undefined] {
|
||||
const [rank, rankNum] = udemae.split(/([0-9]+)/);
|
||||
return [
|
||||
rank.toLowerCase(),
|
||||
rankNum === undefined ? undefined : parseInt(rankNum),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
91
s3si.ts
91
s3si.ts
|
|
@ -1,11 +1,21 @@
|
|||
import { getBulletToken, getGToken, loginManually } from "./iksm.ts";
|
||||
import { flags, MultiProgressBar, Mutex } from "./deps.ts";
|
||||
import { DEFAULT_STATE, State } from "./state.ts";
|
||||
import { checkToken, getBattleDetail, getBattleList } from "./splatnet3.ts";
|
||||
import { BattleExporter, VsHistoryDetail } from "./types.ts";
|
||||
import {
|
||||
checkToken,
|
||||
getBankaraBattleHistories,
|
||||
getBattleDetail,
|
||||
getBattleList,
|
||||
} from "./splatnet3.ts";
|
||||
import {
|
||||
BattleExporter,
|
||||
HistoryGroups,
|
||||
VsBattle,
|
||||
VsHistoryDetail,
|
||||
} from "./types.ts";
|
||||
import { Cache, FileCache, MemoryCache } from "./cache.ts";
|
||||
import { StatInkExporter } from "./exporter/stat.ink.ts";
|
||||
import { readline, showError } from "./utils.ts";
|
||||
import { battleId, readline, showError } from "./utils.ts";
|
||||
import { FileExporter } from "./exporter/file.ts";
|
||||
|
||||
type Opts = {
|
||||
|
|
@ -29,6 +39,8 @@ class BattleFetcher {
|
|||
state: State;
|
||||
cache: Cache;
|
||||
lock: Record<string, Mutex | undefined> = {};
|
||||
bankaraLock = new Mutex();
|
||||
bankaraHistory?: HistoryGroups["nodes"];
|
||||
|
||||
constructor(
|
||||
{ cache = new MemoryCache(), state }: { state: State; cache?: Cache },
|
||||
|
|
@ -36,16 +48,62 @@ class BattleFetcher {
|
|||
this.state = state;
|
||||
this.cache = cache;
|
||||
}
|
||||
getLock(id: string): Mutex {
|
||||
let cur = this.lock[id];
|
||||
private async getLock(id: string): Promise<Mutex> {
|
||||
const bid = await battleId(id);
|
||||
|
||||
let cur = this.lock[bid];
|
||||
if (!cur) {
|
||||
cur = new Mutex();
|
||||
this.lock[id] = cur;
|
||||
this.lock[bid] = cur;
|
||||
}
|
||||
|
||||
return cur;
|
||||
}
|
||||
fetchBattle(id: string): Promise<VsHistoryDetail> {
|
||||
const lock = this.getLock(id);
|
||||
getBankaraHistory() {
|
||||
return this.bankaraLock.use(async () => {
|
||||
if (this.bankaraHistory) {
|
||||
return this.bankaraHistory;
|
||||
}
|
||||
|
||||
const { bankaraBattleHistories: { historyGroups } } =
|
||||
await getBankaraBattleHistories(
|
||||
this.state,
|
||||
);
|
||||
|
||||
this.bankaraHistory = historyGroups.nodes;
|
||||
|
||||
return this.bankaraHistory;
|
||||
});
|
||||
}
|
||||
async getBattleMetaById(id: string): Promise<Omit<VsBattle, "detail">> {
|
||||
const bid = await battleId(id);
|
||||
const bankaraHistory = await this.getBankaraHistory();
|
||||
const group = bankaraHistory.find((i) =>
|
||||
i.historyDetails.nodes.some((i) => i._bid === bid)
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
return {
|
||||
bankaraMatchChallenge: null,
|
||||
listNode: null,
|
||||
lastInChallenge: null,
|
||||
};
|
||||
}
|
||||
|
||||
const { bankaraMatchChallenge } = group;
|
||||
const listNode = group.historyDetails.nodes.find((i) => i._bid === bid) ??
|
||||
null;
|
||||
const idx = group.historyDetails.nodes.indexOf(listNode!);
|
||||
|
||||
return {
|
||||
bankaraMatchChallenge,
|
||||
listNode,
|
||||
lastInChallenge: (bankaraMatchChallenge?.state !== "INPROGRESS") &&
|
||||
(idx === 0),
|
||||
};
|
||||
}
|
||||
async getBattleDetail(id: string): Promise<VsHistoryDetail> {
|
||||
const lock = await this.getLock(id);
|
||||
|
||||
return lock.use(async () => {
|
||||
const cached = await this.cache.read<VsHistoryDetail>(id);
|
||||
|
|
@ -61,6 +119,17 @@ class BattleFetcher {
|
|||
return detail;
|
||||
});
|
||||
}
|
||||
async fetchBattle(id: string): Promise<VsBattle> {
|
||||
const detail = await this.getBattleDetail(id);
|
||||
const metadata = await this.getBattleMetaById(id);
|
||||
|
||||
const battle: VsBattle = {
|
||||
...metadata,
|
||||
detail,
|
||||
};
|
||||
|
||||
return battle;
|
||||
}
|
||||
}
|
||||
|
||||
type Progress = {
|
||||
|
|
@ -111,9 +180,9 @@ Options:
|
|||
await this.writeState(DEFAULT_STATE);
|
||||
}
|
||||
}
|
||||
async getExporters(): Promise<BattleExporter<VsHistoryDetail>[]> {
|
||||
async getExporters(): Promise<BattleExporter<VsBattle>[]> {
|
||||
const exporters = this.opts.exporter.split(",");
|
||||
const out: BattleExporter<VsHistoryDetail>[] = [];
|
||||
const out: BattleExporter<VsBattle>[] = [];
|
||||
|
||||
if (exporters.includes("stat.ink")) {
|
||||
if (!this.state.statInkApiKey) {
|
||||
|
|
@ -248,7 +317,7 @@ Options:
|
|||
onStep,
|
||||
}: {
|
||||
fetcher: BattleFetcher;
|
||||
exporter: BattleExporter<VsHistoryDetail>;
|
||||
exporter: BattleExporter<VsBattle>;
|
||||
battleList: string[];
|
||||
onStep?: (progress: Progress) => void;
|
||||
},
|
||||
|
|
|
|||
11
splatnet3.ts
11
splatnet3.ts
|
|
@ -10,6 +10,7 @@ import {
|
|||
RespMap,
|
||||
VarsMap,
|
||||
} from "./types.ts";
|
||||
import { battleId } from "./utils.ts";
|
||||
|
||||
async function request<Q extends Queries>(
|
||||
state: State,
|
||||
|
|
@ -121,3 +122,13 @@ export function getBattleDetail(
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function getBankaraBattleHistories(state: State) {
|
||||
const resp = await request(state, Queries.BankaraBattleHistoriesQuery);
|
||||
for (const i of resp.bankaraBattleHistories.historyGroups.nodes) {
|
||||
for (const j of i.historyDetails.nodes) {
|
||||
j._bid = await battleId(j.id);
|
||||
}
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
|
|
|||
39
types.ts
39
types.ts
|
|
@ -28,12 +28,28 @@ export type Image = {
|
|||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
export type BankaraMatchChallenge = {
|
||||
winCount: number;
|
||||
loseCount: number;
|
||||
maxWinCount: number;
|
||||
maxLoseCount: number;
|
||||
state: "FAILED" | "SUCCEEDED" | "INPROGRESS";
|
||||
isPromo: boolean;
|
||||
isUdemaeUp: boolean;
|
||||
udemaeAfter: string | null;
|
||||
earnedUdemaePoint: number;
|
||||
};
|
||||
export type BattleListNode = {
|
||||
// battle id added after fetch
|
||||
_bid: string;
|
||||
id: string;
|
||||
udemae: string;
|
||||
};
|
||||
export type HistoryGroups = {
|
||||
nodes: {
|
||||
bankaraMatchChallenge: null | BankaraMatchChallenge;
|
||||
historyDetails: {
|
||||
nodes: {
|
||||
id: string;
|
||||
}[];
|
||||
nodes: BattleListNode[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
|
@ -65,12 +81,27 @@ export type VsTeam = {
|
|||
score: null | number;
|
||||
};
|
||||
};
|
||||
export type VsRule =
|
||||
| "TURF_WAR"
|
||||
| "AREA"
|
||||
| "LOFT"
|
||||
| "GOAL"
|
||||
| "CLAM"
|
||||
| "TRI_COLOR";
|
||||
|
||||
// With challenge info
|
||||
export type VsBattle = {
|
||||
listNode: null | BattleListNode;
|
||||
bankaraMatchChallenge: null | BankaraMatchChallenge;
|
||||
lastInChallenge: null | boolean;
|
||||
detail: VsHistoryDetail;
|
||||
};
|
||||
export type VsHistoryDetail = {
|
||||
id: string;
|
||||
vsRule: {
|
||||
name: string;
|
||||
id: string;
|
||||
rule: "TURF_WAR" | "AREA" | "LOFT" | "GOAL" | "CLAM" | "TRI_COLOR";
|
||||
rule: VsRule;
|
||||
};
|
||||
vsMode: {
|
||||
id: string;
|
||||
|
|
|
|||
12
utils.ts
12
utils.ts
|
|
@ -1,5 +1,6 @@
|
|||
import { APIError } from "./APIError.ts";
|
||||
import { base64, io } from "./deps.ts";
|
||||
import { S3SI_NAMESPACE } from "./constant.ts";
|
||||
import { base64, io, uuid } from "./deps.ts";
|
||||
|
||||
const stdinLines = io.readLines(Deno.stdin);
|
||||
|
||||
|
|
@ -80,3 +81,12 @@ export async function showError(p: Promise<void>) {
|
|||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export function battleId(
|
||||
id: string,
|
||||
namespace = S3SI_NAMESPACE,
|
||||
): Promise<string> {
|
||||
const fullId = base64.decode(id);
|
||||
const tsUuid = fullId.slice(fullId.length - 52, fullId.length);
|
||||
return uuid.v5.generate(namespace, tsUuid);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue