Merge pull request #14 from spacemeowx2/feature-gears

Feature gears
main
imspace 2022-11-05 13:03:27 +08:00 committed by GitHub
commit 35b47d79c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 160 additions and 17 deletions

View File

@ -1,3 +1,7 @@
## 0.1.17
feat: add gears to stat.ink
## 0.1.16 ## 0.1.16
fix: RankTracker broken when token expires fix: RankTracker broken when token expires

View File

@ -6,7 +6,7 @@ import {
State, State,
StateBackend, StateBackend,
} from "./state.ts"; } from "./state.ts";
import { getBattleList, isTokenExpired } from "./splatnet3.ts"; import { getBattleList, getGearPower, isTokenExpired } from "./splatnet3.ts";
import { BattleListType, Game, GameExporter } from "./types.ts"; import { BattleListType, Game, GameExporter } from "./types.ts";
import { Cache, FileCache } from "./cache.ts"; import { Cache, FileCache } from "./cache.ts";
import { StatInkExporter } from "./exporters/stat.ink.ts"; import { StatInkExporter } from "./exporters/stat.ink.ts";
@ -65,6 +65,7 @@ export class App {
await this.fetchToken(); await this.fetchToken();
}, },
}; };
gearMap: Record<string, number> | null = null;
constructor(public opts: Opts) { constructor(public opts: Opts) {
this.stateBackend = opts.stateBackend ?? this.stateBackend = opts.stateBackend ??
@ -88,6 +89,16 @@ export class App {
await this.writeState(DEFAULT_STATE); await this.writeState(DEFAULT_STATE);
} }
} }
async getGearMap() {
if (this.gearMap) {
return this.gearMap;
}
const { gearPowers } = await getGearPower(this.state);
this.gearMap = Object.fromEntries(
gearPowers.nodes.map((i, id) => [i.name, id]),
);
return this.gearMap;
}
getSkipMode(): ("vs" | "coop")[] { getSkipMode(): ("vs" | "coop")[] {
const mode = this.opts.skipMode; const mode = this.opts.skipMode;
if (mode === "vs") { if (mode === "vs") {
@ -115,10 +126,13 @@ export class App {
}); });
} }
out.push( out.push(
new StatInkExporter( new StatInkExporter({
this.state.statInkApiKey!, statInkApiKey: this.state.statInkApiKey!,
this.opts.monitor ? "Monitoring" : "Manual", uploadMode: this.opts.monitor ? "Monitoring" : "Manual",
), nameDict: {
gearPower: await this.getGearMap(),
},
}),
); );
} }
@ -210,6 +224,8 @@ export class App {
), ),
); );
endBar();
printStats(stats); printStats(stats);
if (errors.length > 0) { if (errors.length > 0) {
throw errors[0]; throw errors[0];
@ -221,8 +237,6 @@ export class App {
...this.state, ...this.state,
rankState: finalRankState, rankState: finalRankState,
}); });
endBar();
} }
stats = initStats(); stats = initStats();
@ -268,12 +282,12 @@ export class App {
), ),
); );
endBar();
printStats(stats); printStats(stats);
if (errors.length > 0) { if (errors.length > 0) {
throw errors[0]; throw errors[0];
} }
endBar();
} }
} }
async monitor() { async monitor() {

View File

@ -1,7 +1,7 @@
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts"; import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
export const AGENT_NAME = "s3si.ts"; export const AGENT_NAME = "s3si.ts";
export const S3SI_VERSION = "0.1.16"; export const S3SI_VERSION = "0.1.17";
export const NSOAPP_VERSION = "2.3.1"; export const NSOAPP_VERSION = "2.3.1";
export const WEB_VIEW_VERSION = "1.0.0-5644e7a2"; export const WEB_VIEW_VERSION = "1.0.0-5644e7a2";
export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts"; export const S3SI_LINK = "https://github.com/spacemeowx2/s3si.ts";

View File

@ -9,6 +9,10 @@ import {
import { import {
CoopInfo, CoopInfo,
GameExporter, GameExporter,
PlayerGear,
StatInkAbility,
StatInkGear,
StatInkGears,
StatInkPlayer, StatInkPlayer,
StatInkPostBody, StatInkPostBody,
StatInkPostResponse, StatInkPostResponse,
@ -17,7 +21,7 @@ import {
VsInfo, VsInfo,
VsPlayer, VsPlayer,
} from "../types.ts"; } from "../types.ts";
import { base64, msgpack } from "../../deps.ts"; import { base64, msgpack, Mutex } from "../../deps.ts";
import { APIError } from "../APIError.ts"; import { APIError } from "../APIError.ts";
import { cache, gameId } from "../utils.ts"; import { cache, gameId } from "../utils.ts";
@ -30,13 +34,29 @@ function b64Number(id: string): number {
return parseInt(num); return parseInt(num);
} }
const FETCH_LOCK = new Mutex();
async function _getAbility(): Promise<StatInkAbility> {
const release = await FETCH_LOCK.acquire();
try {
const resp = await fetch("https://stat.ink/api/v3/ability?full=1");
const json = await resp.json();
return json;
} finally {
release();
}
}
async function _getStage(): Promise<StatInkStage> { async function _getStage(): Promise<StatInkStage> {
const resp = await fetch("https://stat.ink/api/v3/stage"); const resp = await fetch("https://stat.ink/api/v3/stage");
const json = await resp.json(); const json = await resp.json();
return json; return json;
} }
const getAbility = cache(_getAbility);
const getStage = cache(_getStage); const getStage = cache(_getStage);
export type NameDict = {
gearPower: Record<string, number | undefined>;
};
/** /**
* Exporter to stat.ink. * Exporter to stat.ink.
* *
@ -44,11 +64,23 @@ const getStage = cache(_getStage);
*/ */
export class StatInkExporter implements GameExporter { export class StatInkExporter implements GameExporter {
name = "stat.ink"; name = "stat.ink";
private statInkApiKey: string;
private uploadMode: string;
private nameDict: NameDict;
constructor(private statInkApiKey: string, private uploadMode: string) { constructor(
{ statInkApiKey, uploadMode, nameDict }: {
statInkApiKey: string;
uploadMode: string;
nameDict: NameDict;
},
) {
if (statInkApiKey.length !== 43) { if (statInkApiKey.length !== 43) {
throw new Error("Invalid stat.ink API key"); throw new Error("Invalid stat.ink API key");
} }
this.statInkApiKey = statInkApiKey;
this.uploadMode = uploadMode;
this.nameDict = nameDict;
} }
requestHeaders() { requestHeaders() {
return { return {
@ -157,7 +189,43 @@ export class StatInkExporter implements GameExporter {
return result.key; return result.key;
} }
mapPlayer(player: VsPlayer, index: number): StatInkPlayer { async mapGears(
{ headGear, clothingGear, shoesGear }: VsPlayer,
): Promise<StatInkGears> {
const amap = await getAbility();
const mapAbility = ({ name }: { name: string }): string | null => {
const abilityIdx = this.nameDict.gearPower[name];
if (!abilityIdx) {
return null;
}
const result = amap[abilityIdx];
if (!result) {
return null;
}
return result.key;
};
const mapGear = (
{ primaryGearPower, additionalGearPowers }: PlayerGear,
): StatInkGear => {
const primary = mapAbility(primaryGearPower);
if (!primary) {
throw new Error("Unknown ability: " + primaryGearPower.name);
}
return {
primary_ability: primary,
secondary_abilities: additionalGearPowers.map(mapAbility),
};
};
return {
headgear: mapGear(headGear),
clothing: mapGear(clothingGear),
shoes: mapGear(shoesGear),
};
}
mapPlayer = async (
player: VsPlayer,
index: number,
): Promise<StatInkPlayer> => {
const result: StatInkPlayer = { const result: StatInkPlayer = {
me: player.isMyself ? "yes" : "no", me: player.isMyself ? "yes" : "no",
rank_in_team: index + 1, rank_in_team: index + 1,
@ -166,6 +234,7 @@ export class StatInkExporter implements GameExporter {
splashtag_title: player.byname, splashtag_title: player.byname,
weapon: b64Number(player.weapon.id).toString(), weapon: b64Number(player.weapon.id).toString(),
inked: player.paint, inked: player.paint,
gears: await this.mapGears(player),
disconnected: player.result ? "no" : "yes", disconnected: player.result ? "no" : "yes",
}; };
if (player.result) { if (player.result) {
@ -176,7 +245,7 @@ export class StatInkExporter implements GameExporter {
result.special = player.result.special; result.special = player.result.special;
} }
return result; return result;
} };
async mapBattle( async mapBattle(
{ {
challengeProgress, challengeProgress,
@ -216,9 +285,11 @@ export class StatInkExporter implements GameExporter {
medals: vsDetail.awards.map((i) => i.name), medals: vsDetail.awards.map((i) => i.name),
our_team_players: myTeam.players.map(this.mapPlayer), our_team_players: await Promise.all(myTeam.players.map(this.mapPlayer)),
their_team_players: otherTeams.flatMap((i) => i.players).map( their_team_players: await Promise.all(
this.mapPlayer, otherTeams.flatMap((i) => i.players).map(
this.mapPlayer,
),
), ),
agent: AGENT_NAME, agent: AGENT_NAME,

View File

@ -162,3 +162,12 @@ export async function getCoopHistories(state: State) {
return resp; return resp;
} }
export async function getGearPower(state: State) {
const resp = await request(
state,
Queries.myOutfitCommonDataFilteringConditionQuery,
);
return resp;
}

View File

@ -9,6 +9,8 @@ export enum Queries {
VsHistoryDetailQuery = "2b085984f729cd51938fc069ceef784a", VsHistoryDetailQuery = "2b085984f729cd51938fc069ceef784a",
CoopHistoryQuery = "817618ce39bcf5570f52a97d73301b30", CoopHistoryQuery = "817618ce39bcf5570f52a97d73301b30",
CoopHistoryDetailQuery = "f3799a033f0a7ad4b1b396f9a3bafb1e", CoopHistoryDetailQuery = "f3799a033f0a7ad4b1b396f9a3bafb1e",
myOutfitCommonDataFilteringConditionQuery =
"d02ab22c9dccc440076055c8baa0fa7a",
} }
export type VarsMap = { export type VarsMap = {
[Queries.HomeQuery]: []; [Queries.HomeQuery]: [];
@ -23,6 +25,7 @@ export type VarsMap = {
[Queries.CoopHistoryDetailQuery]: [{ [Queries.CoopHistoryDetailQuery]: [{
coopHistoryDetailId: string; coopHistoryDetailId: string;
}]; }];
[Queries.myOutfitCommonDataFilteringConditionQuery]: [];
}; };
export type Image = { export type Image = {
@ -61,6 +64,19 @@ export type HistoryGroups<T> = {
}; };
}[]; }[];
}; };
export type PlayerGear = {
name: string;
primaryGearPower: {
name: string;
};
additionalGearPowers: {
name: string;
}[];
brand: {
name: string;
id: string;
};
};
export type VsPlayer = { export type VsPlayer = {
id: string; id: string;
nameId: string | null; nameId: string | null;
@ -81,6 +97,10 @@ export type VsPlayer = {
special: number; special: number;
} | null; } | null;
paint: number; paint: number;
headGear: PlayerGear;
clothingGear: PlayerGear;
shoesGear: PlayerGear;
}; };
export type VsTeam = { export type VsTeam = {
players: VsPlayer[]; players: VsPlayer[];
@ -223,6 +243,13 @@ export type RespMap = {
[Queries.CoopHistoryDetailQuery]: { [Queries.CoopHistoryDetailQuery]: {
coopHistoryDetail: CoopHistoryDetail; coopHistoryDetail: CoopHistoryDetail;
}; };
[Queries.myOutfitCommonDataFilteringConditionQuery]: {
gearPowers: {
nodes: {
name: string;
}[];
};
};
}; };
export type GraphQLResponse<T> = { export type GraphQLResponse<T> = {
data: T; data: T;
@ -240,6 +267,23 @@ export enum BattleListType {
Coop, Coop,
} }
export type StatInkAbility = {
key: string;
name: Record<string, string>;
primary_only: boolean;
}[];
export type StatInkGear = {
primary_ability: string;
secondary_abilities: (string | null)[];
};
export type StatInkGears = {
headgear: StatInkGear;
clothing: StatInkGear;
shoes: StatInkGear;
};
export type StatInkPlayer = { export type StatInkPlayer = {
me: "yes" | "no"; me: "yes" | "no";
rank_in_team: number; rank_in_team: number;
@ -253,6 +297,7 @@ export type StatInkPlayer = {
kill_or_assist?: number; kill_or_assist?: number;
death?: number; death?: number;
special?: number; special?: number;
gears?: StatInkGears;
disconnected: "yes" | "no"; disconnected: "yes" | "no";
}; };