Merge remote-tracking branch 'origin/main'

main
Rosalina 2023-02-27 23:08:34 -05:00
commit da92cb9382
No known key found for this signature in database
8 changed files with 243 additions and 129 deletions

View File

@ -1,3 +1,9 @@
## 0.3.0
feat: clear state when season change
feat: update `WEB_VIEW_VERSION`
## 0.2.9
feat: update `WEB_VIEW_VERSION`

View File

@ -4,7 +4,8 @@
"fmt": "deno fmt",
"fmt:check": "deno fmt --check",
"lint": "deno lint",
"run": "deno run -A"
"run": "deno run -A",
"gen": "deno run -A ./scripts/update-constant.ts"
},
"fmt": {
"files": {

View File

@ -3,8 +3,9 @@ import { assertEquals } from "../dev_deps.ts";
import { BattleListNode } from "./types.ts";
import { base64 } from "../deps.ts";
import { gameId } from "./utils.ts";
import { RankState } from "./state.ts";
const INIT_STATE = {
const INIT_STATE: RankState = {
gameId: await gameId(genId(0)),
rank: "B-",
rankPoint: 100,
@ -20,9 +21,9 @@ class TestRankTracker extends RankTracker {
}
}
function genId(id: number): string {
function genId(id: number, date = "20220101"): string {
return base64.encode(
`VsHistoryDetail-asdf:asdf:20220101T${
`VsHistoryDetail-asdf:asdf:${date}T${
id.toString().padStart(6, "0")
}_------------------------------------`,
);
@ -213,20 +214,7 @@ Deno.test("RankTracker issue #36", async () => {
});
assertEquals(await tracker.getRankStateById(genId(0)), undefined);
assertEquals(await tracker.getRankStateById(genId(1)), {
before: {
gameId: await gameId(genId(0)),
rank: "S+19",
rankPoint: 3750,
timestamp: 1640995200,
},
after: {
gameId: await gameId(genId(1)),
rank: "S+19",
rankPoint: 3750,
timestamp: 1640995201,
},
});
assertEquals(await tracker.getRankStateById(genId(1)), undefined);
assertEquals(await tracker.getRankStateById(genId(2)), {
before: {
@ -491,3 +479,50 @@ Deno.test("RankTracker", async () => {
timestamp: 1640995229,
});
});
Deno.test("RankTracker clears state when season changes", async () => {
const firstDay = new Date("2022-09-09T00:00:00+00:00").getTime() / 1000;
const firstSeason = {
...INIT_STATE,
timestamp: firstDay,
};
const tracker = new TestRankTracker(firstSeason);
assertEquals(tracker.testGet(), {
state: firstSeason,
deltaMap: new Map(),
});
const afterState = await tracker.updateState([{
xMatchMeasurement: null,
bankaraMatchChallenge: {
winCount: 3,
loseCount: 0,
maxWinCount: 3,
maxLoseCount: 3,
state: "SUCCEEDED",
isPromo: true,
isUdemaeUp: true,
udemaeAfter: "B-",
earnedUdemaePoint: 1,
},
historyDetails: {
nodes: [{
id: genId(1, "20221209"),
udemae: "B-",
judgement: "WIN",
bankaraMatch: {
earnedUdemaePoint: 8,
},
}, {
id: genId(0),
udemae: "B-",
judgement: "WIN",
bankaraMatch: {
earnedUdemaePoint: 8,
},
}],
},
}]);
assertEquals(afterState, undefined);
});

View File

@ -5,7 +5,8 @@ import {
HistoryGroups,
RankParam,
} from "./types.ts";
import { gameId, nonNullable, parseHistoryDetailId } from "./utils.ts";
import { gameId, parseHistoryDetailId } from "./utils.ts";
import { getSeason } from "./VersionData.ts";
const splusParams = () => {
const out: RankParam[] = [];
@ -79,9 +80,13 @@ export const RANK_PARAMS: RankParam[] = [{
}, ...splusParams()];
type Delta = {
beforeGameId: string;
before: {
gameId: string;
timestamp: number;
};
gameId: string;
timestamp: number;
rank?: string;
rankAfter?: string;
rankPoint: number;
isPromotion: boolean;
@ -89,8 +94,24 @@ type Delta = {
isChallengeFirst: boolean;
};
// TODO: auto rank up using rank params and delta.
function addRank(state: RankState, delta: Delta): RankState {
// delta's beforeGameId must be state's gameId
function addRank(
state: RankState | undefined,
delta: Delta,
): { before: RankState; after: RankState } | undefined {
if (!state) {
// is rank up, generate state here
if (delta.isPromotion && delta.isRankUp) {
state = getRankStateByDelta(delta);
} else {
return;
}
}
if (state.gameId !== delta.before.gameId) {
throw new Error("Invalid state");
}
const { rank, rankPoint } = state;
const {
gameId,
@ -101,6 +122,16 @@ function addRank(state: RankState, delta: Delta): RankState {
isChallengeFirst,
} = delta;
if (state.timestamp) {
const oldSeason = getSeason(new Date(state.timestamp * 1000));
if (oldSeason) {
const newSeason = getSeason(new Date(timestamp * 1000));
if (newSeason?.id !== oldSeason.id) {
return;
}
}
}
const rankIndex = RANK_PARAMS.findIndex((r) => r.rank === rank);
if (rankIndex === -1) {
@ -111,20 +142,29 @@ function addRank(state: RankState, delta: Delta): RankState {
if (isChallengeFirst) {
return {
before: state,
after: {
gameId,
timestamp,
rank,
rankPoint: rankPoint - rankParam.charge,
},
};
}
// S+50 is the highest rank
if (rankIndex === RANK_PARAMS.length - 1) {
return {
before: state,
after: {
timestamp,
gameId,
rank,
rankPoint: Math.min(rankPoint + delta.rankPoint, rankParam.pointRange[1]),
rankPoint: Math.min(
rankPoint + delta.rankPoint,
rankParam.pointRange[1],
),
},
};
}
@ -132,18 +172,24 @@ function addRank(state: RankState, delta: Delta): RankState {
const nextRankParam = RANK_PARAMS[rankIndex + 1];
return {
before: state,
after: {
gameId,
timestamp,
rank: nextRankParam.rank,
rankPoint: nextRankParam.pointRange[0],
},
};
}
return {
before: state,
after: {
gameId,
timestamp,
rank: rankAfter ?? rank,
rankPoint: rankPoint + delta.rankPoint,
},
};
}
@ -168,19 +214,39 @@ type FlattenItem = {
detail: BattleListNode;
};
function generateDeltaList(
state: RankState,
function beginPoint(
state: RankState | undefined,
flatten: FlattenItem[],
) {
): [firstItem: FlattenItem, unProcessed: FlattenItem[]] {
if (state) {
const index = flatten.findIndex((i) => i.gameId === state.gameId);
if (index === -1) {
return;
if (index !== -1) {
return [flatten[index], flatten.slice(index)];
}
}
const unProcessed = flatten.slice(index);
if (flatten.length === 0) {
throw new Error("flatten must not be empty");
}
return [flatten[0], flatten];
}
function getTimestamp(date: Date) {
return Math.floor(date.getTime() / 1000);
}
function generateDeltaList(
state: RankState | undefined,
flatten: FlattenItem[],
) {
const [firstItem, unProcessed] = beginPoint(state, flatten);
const deltaList: Delta[] = [];
let beforeGameId = state.gameId;
let before = {
gameId: firstItem.gameId,
timestamp: getTimestamp(firstItem.time),
};
for (const i of unProcessed.slice(1)) {
if (!i.detail.bankaraMatch) {
@ -188,21 +254,25 @@ function generateDeltaList(
}
let delta: Delta = {
beforeGameId,
before,
gameId: i.gameId,
timestamp: Math.floor(i.time.getTime() / 1000),
timestamp: getTimestamp(i.time),
rankPoint: 0,
isPromotion: false,
isRankUp: false,
isChallengeFirst: false,
};
beforeGameId = i.gameId;
before = {
gameId: i.gameId,
timestamp: Math.floor(i.time.getTime() / 1000),
};
if (i.bankaraMatchChallenge) {
// challenge
if (i.index === 0 && i.bankaraMatchChallenge.state !== "INPROGRESS") {
// last battle in challenge
delta = {
...delta,
rank: i.detail.udemae,
rankAfter: i.bankaraMatchChallenge.udemaeAfter ?? undefined,
rankPoint: i.bankaraMatchChallenge.earnedUdemaePoint ?? 0,
isPromotion: i.bankaraMatchChallenge.isPromo ?? false,
@ -229,19 +299,20 @@ function generateDeltaList(
deltaList.push(delta);
}
return deltaList;
return {
firstItem,
deltaList,
};
}
function getRankState(i: FlattenItem): RankState {
const rank = i.detail.udemae;
const nextRank = i.bankaraMatchChallenge?.udemaeAfter;
const earnedUdemaePoint = i.bankaraMatchChallenge?.earnedUdemaePoint;
if (!nonNullable(earnedUdemaePoint)) {
throw new TypeError("earnedUdemaePoint must be defined");
}
function getRankStateByDelta(i: Delta): RankState {
const rank = i.rank;
const nextRank = i.rankAfter;
const earnedUdemaePoint = i.rankPoint;
if (!rank || !nextRank) {
throw new Error("rank and nextRank must be defined");
}
const param = RANK_PARAMS.find((i) => i.rank === rank);
const nextParam = RANK_PARAMS.find((i) => i.rank === nextRank);
@ -253,8 +324,8 @@ function getRankState(i: FlattenItem): RankState {
earnedUdemaePoint;
return {
gameId: i.gameId,
timestamp: Math.floor(i.time.getTime() / 1000),
gameId: i.before.gameId,
timestamp: i.before.timestamp,
rank,
rankPoint: oldRankPoint,
};
@ -266,37 +337,18 @@ function getRankState(i: FlattenItem): RankState {
export class RankTracker {
// key: privous game id
protected deltaMap: Map<string, Delta> = new Map();
// key: after game id
protected stateMap: Map<string, { before: RankState; after: RankState }> =
new Map();
constructor(protected state: RankState | undefined) {}
async getRankStateById(
id: string,
): Promise<{ before: RankState; after: RankState } | undefined> {
if (!this.state) {
return;
}
const gid = await gameId(id);
let cur = this.state;
let before = cur;
// there is no delta for first game
if (cur.gameId === gid) {
return;
}
while (cur.gameId !== gid) {
const delta = this.deltaMap.get(cur.gameId);
if (!delta) {
return;
}
before = cur;
cur = addRank(cur, delta);
}
return {
before,
after: cur,
};
return this.stateMap.get(gid);
}
setState(state: RankState | undefined) {
@ -326,55 +378,22 @@ export class RankTracker {
.map((i) => i.gameId.then((gameId) => ({ ...i, gameId }))),
);
const gameIdTime = new Map<string | undefined, Date>(
flatten.map((i) => [i.gameId, i.time]),
);
let curState: RankState | undefined = this.state;
let curState: RankState | undefined;
const oldestPromotion = flatten.find((i) =>
i.bankaraMatchChallenge?.isPromo && i.bankaraMatchChallenge.isUdemaeUp
);
const { firstItem, deltaList } = generateDeltaList(curState, flatten);
/*
* There are 4 cases:
* 1. state === undefined, oldestPromotion === undefined
* 2. state === undefined, oldestPromotion !== undefined
* 3. state !== undefined, oldestPromotion === undefined
* 4. state !== undefined, oldestPromotion !== undefined
*
* In case 1, we can't track rank. So we do nothing.
* In case 2, 3, we track rank by the non-undefined state.
* In case 4, we can track by the elder game.
*/
const thisStateTime = gameIdTime.get(this.state?.gameId);
if (!thisStateTime && !oldestPromotion) {
return;
} else if (thisStateTime && !oldestPromotion) {
curState = this.state;
} else if (!thisStateTime && oldestPromotion) {
curState = getRankState(oldestPromotion);
} else if (thisStateTime && oldestPromotion) {
if (thisStateTime <= oldestPromotion.time) {
curState = this.state;
} else {
curState = getRankState(oldestPromotion);
}
}
if (!curState) {
return;
}
this.state = curState;
const deltaList = generateDeltaList(curState, flatten);
if (!deltaList) {
// history don't contain current state, so skip update
if (curState && firstItem.gameId !== curState.gameId) {
return;
}
for (const delta of deltaList) {
this.deltaMap.set(delta.beforeGameId, delta);
curState = addRank(curState, delta);
this.deltaMap.set(delta.before.gameId, delta);
const result = addRank(curState, delta);
curState = result?.after;
if (result) {
this.stateMap.set(result.after.gameId, result);
}
}
return curState;

28
src/VersionData.test.ts Normal file
View File

@ -0,0 +1,28 @@
import { getSeason, SEASONS } from "./VersionData.ts";
import { assertEquals } from "../dev_deps.ts";
Deno.test("Seasons are continuous", () => {
let curDate: Date | undefined;
for (const season of SEASONS) {
if (!curDate) {
curDate = season.end;
} else {
assertEquals(curDate, season.start);
curDate = season.end;
}
}
});
Deno.test("getSeason", () => {
const season1 = getSeason(new Date("2022-09-09T00:00:00+00:00"));
assertEquals(season1?.id, "season202209");
const season2 = getSeason(new Date("2022-12-09T00:00:00+00:00"));
assertEquals(season2?.id, "season202212");
const nonExist = getSeason(new Date("2022-06-09T00:00:00+00:00"));
assertEquals(nonExist, undefined);
});

25
src/VersionData.ts Normal file
View File

@ -0,0 +1,25 @@
export type Season = {
id: string;
name: string;
start: Date;
end: Date;
};
export const SEASONS: Season[] = [
{
id: "season202209",
name: "Drizzle Season 2022",
start: new Date("2022-09-01T00:00:00+00:00"),
end: new Date("2022-12-01T00:00:00+00:00"),
},
{
id: "season202212",
name: "Chill Season 2022",
start: new Date("2022-12-01T00:00:00+00:00"),
end: new Date("2023-03-01T00:00:00+00:00"),
},
];
export const getSeason = (date: Date): Season | undefined => {
return SEASONS.find((s) => s.start <= date && date < s.end);
};

View File

@ -2,10 +2,10 @@ import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
export const AGENT_NAME = "splatoon.catgirlin.space / s3si.ts";
export const AGENT_VERSION = "1.0.0";
export const S3SI_VERSION = "0.2.9";
export const S3SI_VERSION = "0.3.0";
export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`;
export const NSOAPP_VERSION = "2.4.0";
export const WEB_VIEW_VERSION = "2.0.0-7070f95e";
export const WEB_VIEW_VERSION = "3.0.0-2857bc50";
export const S3SI_LINK = "https://forgejo.catgirlin.space/catgirl/s3si.ts";
export const USERAGENT = `${AGENT_NAME}/(${COMBINED_VERSION}) (${S3SI_LINK})`;

View File

@ -1,7 +1,7 @@
import { RankState } from "./state.ts";
export enum Queries {
HomeQuery = "dba47124d5ec3090c97ba17db5d2f4b3",
HomeQuery = "22e2fa8294168003c21b00c333c35384",
LatestBattleHistoriesQuery = "4f5f26e64bca394b45345a65a2f383bd",
RegularBattleHistoriesQuery = "d5b795d09e67ce153e622a184b7e7dfa",
BankaraBattleHistoriesQuery = "de4754588109b77dbcb90fbe44b612ee",
@ -9,7 +9,7 @@ export enum Queries {
PrivateBattleHistoriesQuery = "1d6ed57dc8b801863126ad4f351dfb9a",
VsHistoryDetailQuery = "291295ad311b99a6288fc95a5c4cb2d2",
CoopHistoryQuery = "6ed02537e4a65bbb5e7f4f23092f6154",
CoopHistoryDetailQuery = "3cc5f826a6646b85f3ae45db51bd0707",
CoopHistoryDetailQuery = "379f0d9b78b531be53044bcac031b34b",
myOutfitCommonDataFilteringConditionQuery =
"d02ab22c9dccc440076055c8baa0fa7a",
myOutfitCommonDataEquipmentsQuery = "d29cd0c2b5e6bac90dd5b817914832f8",