mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-24 03:59:50 +02:00
purge some dead code
This commit is contained in:
1 parent
878ada0d10
commit
2ee72b40ab
65 files changed
+30
-4376
No files matched your search
@@ -1,89 +0,0 @@
|
|||||||
import { assertSecret, EventManager } from "../blitzkit";
|
|
||||||
import { patientFetch } from "../blitzkit/patientFetch";
|
|
||||||
import type { Region } from "./regions";
|
|
||||||
|
|
||||||
const RETRY_ERRORS = ["REQUEST_LIMIT_EXCEEDED", "SOURCE_NOT_AVAILABLE"];
|
|
||||||
|
|
||||||
export const retryAbleBlitzFetchEvent = new EventManager();
|
|
||||||
export const blitzFetchQueueAvailableEvent = new EventManager();
|
|
||||||
|
|
||||||
type BlitzResponse<Data extends object> =
|
|
||||||
| {
|
|
||||||
status: "error";
|
|
||||||
error: { field: string; message: string; code: 402; value: null };
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
status: "ok";
|
|
||||||
data: Data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAX_CALLS_PER_SECOND = 10;
|
|
||||||
|
|
||||||
const queue: { url: string; resolve: (data: object) => void }[] = [];
|
|
||||||
let inProgress = 0;
|
|
||||||
|
|
||||||
function decrement() {
|
|
||||||
inProgress--;
|
|
||||||
blitzFetchQueueAvailableEvent.emit(undefined);
|
|
||||||
manageQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function manageQueue() {
|
|
||||||
if (queue.length > 0 && inProgress < MAX_CALLS_PER_SECOND) {
|
|
||||||
inProgress++;
|
|
||||||
|
|
||||||
const timeout = setTimeout(decrement, 1000);
|
|
||||||
const request = queue.shift()!;
|
|
||||||
const data = (await patientFetch(request.url)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.catch((error) => ({ status: "error", error }))) as BlitzResponse<
|
|
||||||
typeof request.resolve
|
|
||||||
>;
|
|
||||||
|
|
||||||
if (data.status === "ok") {
|
|
||||||
request.resolve(data.data);
|
|
||||||
} else {
|
|
||||||
if (RETRY_ERRORS.includes(data.error.message)) {
|
|
||||||
retryAbleBlitzFetchEvent.emit(undefined);
|
|
||||||
clearTimeout(timeout);
|
|
||||||
setTimeout(() => {
|
|
||||||
queue.push(request);
|
|
||||||
decrement();
|
|
||||||
}, 1000 / MAX_CALLS_PER_SECOND);
|
|
||||||
} else {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
decrement();
|
|
||||||
|
|
||||||
throw new Error(`Wargaming response error status: "${data.status}"`, {
|
|
||||||
cause: `Message: "${data.error.message}"\nURL: "${request.url}"`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FetchBlitzParams = Record<string, string | number | undefined>;
|
|
||||||
|
|
||||||
export function fetchBlitz<Data extends object>(
|
|
||||||
region: Region,
|
|
||||||
path: string,
|
|
||||||
params: FetchBlitzParams = {}
|
|
||||||
) {
|
|
||||||
return new Promise<Data>((resolve) => {
|
|
||||||
queue.push({
|
|
||||||
url: `https://api.wotblitz.${region}/wotb/${path}/?${Object.entries({
|
|
||||||
application_id: assertSecret(
|
|
||||||
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
|
|
||||||
),
|
|
||||||
...params,
|
|
||||||
})
|
|
||||||
.filter(([_, value]) => value !== undefined)
|
|
||||||
.map(([key, value]) => `${key}=${encodeURIComponent(value!)}`)
|
|
||||||
.join("&")}`,
|
|
||||||
resolve(data) {
|
|
||||||
resolve(data as Data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
manageQueue();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { assertSecret } from "../blitzkit";
|
|
||||||
import type { BlitzGlossary } from "../types";
|
|
||||||
|
|
||||||
export async function fetchGlossary(locale: string) {
|
|
||||||
const response = await fetch(
|
|
||||||
`${assertSecret(
|
|
||||||
import.meta.env.WOTB_GLOSSARY_DOMAIN
|
|
||||||
)}/${locale}${assertSecret(import.meta.env.WOTB_GLOSSARY_PATH)}`
|
|
||||||
);
|
|
||||||
return (await response.json()) as BlitzGlossary;
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { fetchBlitz } from "./fetchBlitz";
|
|
||||||
import { normalizeIds } from "./normalizeIds";
|
|
||||||
import type { Region } from "./regions";
|
|
||||||
|
|
||||||
interface IndividualAccountAchievements {
|
|
||||||
achievements: {
|
|
||||||
[achievement: string]: number;
|
|
||||||
};
|
|
||||||
max_series: {
|
|
||||||
[achievement: string]: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccountAchievements {
|
|
||||||
[id: number]: IndividualAccountAchievements;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAccountAchievements<
|
|
||||||
Ids extends number | number[],
|
|
||||||
ReturnType = Ids extends number
|
|
||||||
? IndividualAccountAchievements
|
|
||||||
: IndividualAccountAchievements[],
|
|
||||||
>(region: Region, ids: Ids) {
|
|
||||||
const object = await fetchBlitz<AccountAchievements>(
|
|
||||||
region,
|
|
||||||
"account/achievements",
|
|
||||||
{ account_id: normalizeIds(ids) }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeof ids === "number") {
|
|
||||||
return object[ids as number] as ReturnType;
|
|
||||||
} else {
|
|
||||||
return ids.map((id) => object[id]) as ReturnType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { BlitzStats } from "../statistics";
|
|
||||||
import { FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
|
|
||||||
import { normalizeIds } from "./normalizeIds";
|
|
||||||
import { Region } from "./regions";
|
|
||||||
|
|
||||||
export interface SupplementaryStats {
|
|
||||||
WN8?: number;
|
|
||||||
tier?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClanStats {
|
|
||||||
spotted: number;
|
|
||||||
max_frags_tank_id: number;
|
|
||||||
hits: number;
|
|
||||||
frags: number;
|
|
||||||
max_xp: number;
|
|
||||||
max_xp_tank_id: number;
|
|
||||||
wins: number;
|
|
||||||
losses: number;
|
|
||||||
capture_points: number;
|
|
||||||
battles: number;
|
|
||||||
damage_dealt: number;
|
|
||||||
damage_received: number;
|
|
||||||
max_frags: number;
|
|
||||||
shots: number;
|
|
||||||
frags8p: number;
|
|
||||||
xp: number;
|
|
||||||
win_and_survived: number;
|
|
||||||
survived_battles: number;
|
|
||||||
dropped_capture_points: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RatingStats {
|
|
||||||
spotted: number;
|
|
||||||
calibration_battles_left: number;
|
|
||||||
hits: number;
|
|
||||||
frags: number;
|
|
||||||
recalibration_start_time: number;
|
|
||||||
mm_rating: number;
|
|
||||||
wins: number;
|
|
||||||
losses: number;
|
|
||||||
is_recalibration: boolean;
|
|
||||||
capture_points: number;
|
|
||||||
battles: number;
|
|
||||||
damage_dealt: number;
|
|
||||||
damage_received: number;
|
|
||||||
shots: number;
|
|
||||||
frags8p: number;
|
|
||||||
xp: number;
|
|
||||||
win_and_survived: number;
|
|
||||||
survived_battles: number;
|
|
||||||
dropped_capture_points: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IndividualAccountInfo {
|
|
||||||
account_id: number;
|
|
||||||
created_at: number;
|
|
||||||
updated_at: number;
|
|
||||||
private: null;
|
|
||||||
last_battle_time: number;
|
|
||||||
nickname: string;
|
|
||||||
statistics: {
|
|
||||||
all: BlitzStats;
|
|
||||||
clan: ClanStats;
|
|
||||||
rating?: RatingStats;
|
|
||||||
frags: null | number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccountInfo {
|
|
||||||
[accountId: number]: IndividualAccountInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAccountInfo<
|
|
||||||
Ids extends number | number[],
|
|
||||||
ReturnType = Ids extends number
|
|
||||||
? IndividualAccountInfo
|
|
||||||
: IndividualAccountInfo[],
|
|
||||||
>(
|
|
||||||
region: Region,
|
|
||||||
ids: Ids,
|
|
||||||
extra: string[] = [],
|
|
||||||
params: FetchBlitzParams = {}
|
|
||||||
) {
|
|
||||||
const object = await fetchBlitz<AccountInfo>(region, "account/info", {
|
|
||||||
account_id: normalizeIds(ids),
|
|
||||||
extra: extra.length === 0 ? undefined : extra.join(","),
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof ids === "number") {
|
|
||||||
return object[ids as number] as ReturnType;
|
|
||||||
} else {
|
|
||||||
return ids.map((id) => object[id]) as ReturnType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { fetchBlitz } from "./fetchBlitz";
|
|
||||||
import { normalizeIds } from "./normalizeIds";
|
|
||||||
import type { Region } from "./regions";
|
|
||||||
|
|
||||||
type IndividualClanAccountInfo = {
|
|
||||||
account_id: number;
|
|
||||||
joined_at: number;
|
|
||||||
clan_id: number;
|
|
||||||
role: string;
|
|
||||||
account_name: string;
|
|
||||||
|
|
||||||
clan?: {
|
|
||||||
members_count: number;
|
|
||||||
name: string;
|
|
||||||
created_at: number;
|
|
||||||
tag: string;
|
|
||||||
clan_id: number;
|
|
||||||
emblem_set_id: number;
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
interface ClanAccountInfo {
|
|
||||||
[accountId: number]: IndividualClanAccountInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getClanAccountInfo<
|
|
||||||
Ids extends number | number[],
|
|
||||||
ReturnType = Ids extends number
|
|
||||||
? IndividualClanAccountInfo
|
|
||||||
: IndividualClanAccountInfo[],
|
|
||||||
>(region: Region, ids: Ids, extra: string[] = []) {
|
|
||||||
const object = await fetchBlitz<ClanAccountInfo>(
|
|
||||||
region,
|
|
||||||
"clans/accountinfo",
|
|
||||||
{
|
|
||||||
account_id: normalizeIds(ids),
|
|
||||||
extra: extra.length === 0 ? undefined : extra.join(","),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeof ids === "number") {
|
|
||||||
return object[ids as number] as ReturnType;
|
|
||||||
} else {
|
|
||||||
return ids.map((id) => object[id]) as ReturnType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { fetchBlitz } from "./fetchBlitz";
|
|
||||||
import { normalizeIds } from "./normalizeIds";
|
|
||||||
import type { Region } from "./regions";
|
|
||||||
|
|
||||||
interface IndividualClanInfo {
|
|
||||||
recruiting_options: {
|
|
||||||
vehicles_level: number;
|
|
||||||
wins_ratio: number;
|
|
||||||
average_battles_per_day: number;
|
|
||||||
battles: number;
|
|
||||||
average_damage: number;
|
|
||||||
};
|
|
||||||
members_count: number;
|
|
||||||
name: string;
|
|
||||||
creator_name: string;
|
|
||||||
clan_id: number;
|
|
||||||
created_at: number;
|
|
||||||
updated_at: number;
|
|
||||||
leader_name: string;
|
|
||||||
members_ids: number[];
|
|
||||||
recruiting_policy: string;
|
|
||||||
tag: string;
|
|
||||||
is_clan_disbanded: boolean;
|
|
||||||
old_name: string;
|
|
||||||
emblem_set_id: number;
|
|
||||||
creator_id: number;
|
|
||||||
motto: string;
|
|
||||||
renamed_at: number;
|
|
||||||
old_tag: null;
|
|
||||||
leader_id: number;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClanInfo {
|
|
||||||
[clanId: number]: IndividualClanInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getClanInfo<
|
|
||||||
Ids extends number | number[],
|
|
||||||
ReturnType = Ids extends number ? IndividualClanInfo : IndividualClanInfo[],
|
|
||||||
>(region: Region, ids: Ids) {
|
|
||||||
const object = await fetchBlitz<ClanInfo>(region, "clans/info", {
|
|
||||||
clan_id: normalizeIds(ids),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof ids === "number") {
|
|
||||||
return object[ids as number] as ReturnType;
|
|
||||||
} else {
|
|
||||||
return ids.map((id) => object[id]) as ReturnType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import type { TanksStats } from "../types";
|
|
||||||
import { type FetchBlitzParams, fetchBlitz } from "./fetchBlitz";
|
|
||||||
import type { Region } from "./regions";
|
|
||||||
|
|
||||||
export async function getTankStats(
|
|
||||||
region: Region,
|
|
||||||
id: number,
|
|
||||||
params?: FetchBlitzParams
|
|
||||||
) {
|
|
||||||
const tankStats = await fetchBlitz<TanksStats>(region, "tanks/stats", {
|
|
||||||
account_id: id,
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
|
|
||||||
return tankStats[id];
|
|
||||||
}
|
|
||||||
@@ -3,21 +3,13 @@ export * from "./clans";
|
|||||||
export * from "./classes";
|
export * from "./classes";
|
||||||
export * from "./constants";
|
export * from "./constants";
|
||||||
export * from "./equipmentPrice";
|
export * from "./equipmentPrice";
|
||||||
export * from "./fetchBlitz";
|
|
||||||
export * from "./fetchGlossary";
|
|
||||||
export * from "./generateStats";
|
export * from "./generateStats";
|
||||||
export * from "./getAccountAchievements";
|
|
||||||
export * from "./getAccountInfo";
|
|
||||||
export * from "./getClanAccountInfo";
|
|
||||||
export * from "./getClanInfo";
|
|
||||||
export * from "./getLeagueFromScore";
|
export * from "./getLeagueFromScore";
|
||||||
export * from "./getRatingInfo";
|
export * from "./getRatingInfo";
|
||||||
export * from "./getRatingLeague";
|
export * from "./getRatingLeague";
|
||||||
export * from "./getRatingNeighbors";
|
export * from "./getRatingNeighbors";
|
||||||
export * from "./getTankStats";
|
|
||||||
export * from "./idToRegion";
|
export * from "./idToRegion";
|
||||||
export * from "./isOnGoingRatingSeason";
|
export * from "./isOnGoingRatingSeason";
|
||||||
export * from "./isValidBlitzId";
|
|
||||||
export * from "./nations";
|
export * from "./nations";
|
||||||
export * from "./normalizeExtra";
|
export * from "./normalizeExtra";
|
||||||
export * from "./normalizeIds";
|
export * from "./normalizeIds";
|
||||||
@@ -27,7 +19,5 @@ export * from "./readDVPL";
|
|||||||
export * from "./regions";
|
export * from "./regions";
|
||||||
export * from "./regionSubdomainToRegion";
|
export * from "./regionSubdomainToRegion";
|
||||||
export * from "./regionToRegionSubdomain";
|
export * from "./regionToRegionSubdomain";
|
||||||
export * from "./searchClansAcrossRegions";
|
|
||||||
export * from "./searchCurrentRatingPlayers";
|
export * from "./searchCurrentRatingPlayers";
|
||||||
export * from "./searchPlayersAcrossRegions";
|
|
||||||
export * from "./toUniqueId";
|
export * from "./toUniqueId";
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { assertSecret } from "../blitzkit";
|
|
||||||
import { idToRegion } from "./idToRegion";
|
|
||||||
import { Region } from "./regions";
|
|
||||||
|
|
||||||
export async function isValidBlitzId(id: number, token?: string) {
|
|
||||||
let region: Region;
|
|
||||||
|
|
||||||
try {
|
|
||||||
region = idToRegion(id);
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = await fetch(
|
|
||||||
`https://api.wotblitz.${region}/wotb/account/info/?account_id=${id}&application_id=${assertSecret(
|
|
||||||
import.meta.env.PUBLIC_WARGAMING_APPLICATION_ID
|
|
||||||
)}${
|
|
||||||
token === undefined ? "" : `&access_token=${token}`
|
|
||||||
}&fields=account_id%2C-account_id`
|
|
||||||
).then(
|
|
||||||
(response) =>
|
|
||||||
response.json() as Promise<
|
|
||||||
| {
|
|
||||||
status: "ok";
|
|
||||||
data: Record<number, {} | null>;
|
|
||||||
}
|
|
||||||
| { status: "error" }
|
|
||||||
>
|
|
||||||
);
|
|
||||||
|
|
||||||
return json.status === "ok" && json.data[id] !== null;
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import type { ClanList } from "./clans";
|
|
||||||
import { fetchBlitz } from "./fetchBlitz";
|
|
||||||
import type { Region } from "./regions";
|
|
||||||
|
|
||||||
export async function searchClansAcrossRegions(search: string, limit = 25) {
|
|
||||||
const trimmed = search.trim();
|
|
||||||
if (trimmed.length < 3 && trimmed.length > 100) return [];
|
|
||||||
const normalizedLimit = Math.floor(limit / 3);
|
|
||||||
|
|
||||||
// TODO: remove splice once wargaming fixes the functionality of the limit felid
|
|
||||||
|
|
||||||
return (
|
|
||||||
await Promise.all([
|
|
||||||
fetchBlitz<ClanList>("com", "clans/list", {
|
|
||||||
search,
|
|
||||||
limit: normalizedLimit,
|
|
||||||
}).then((value) =>
|
|
||||||
value
|
|
||||||
.map((account) => ({
|
|
||||||
...account,
|
|
||||||
region: "com" as Region,
|
|
||||||
}))
|
|
||||||
.splice(0, normalizedLimit)
|
|
||||||
),
|
|
||||||
fetchBlitz<ClanList>("eu", "clans/list", {
|
|
||||||
search,
|
|
||||||
limit: normalizedLimit,
|
|
||||||
}).then((value) =>
|
|
||||||
value
|
|
||||||
.map((account) => ({
|
|
||||||
...account,
|
|
||||||
region: "eu" as Region,
|
|
||||||
}))
|
|
||||||
.splice(0, normalizedLimit)
|
|
||||||
),
|
|
||||||
fetchBlitz<ClanList>("asia", "clans/list", {
|
|
||||||
search,
|
|
||||||
limit: normalizedLimit,
|
|
||||||
}).then((value) =>
|
|
||||||
value
|
|
||||||
.map((account) => ({
|
|
||||||
...account,
|
|
||||||
region: "asia" as Region,
|
|
||||||
}))
|
|
||||||
.splice(0, normalizedLimit)
|
|
||||||
),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.filter(Boolean)
|
|
||||||
.flat();
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export const usernamePattern = /^[a-zA-Z0-9_]{3,24}$/;
|
|
||||||
export const usernamePatternWithoutPosition = /[a-zA-Z0-9_]{3,24}/;
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { fetchBlitz } from "../fetchBlitz";
|
|
||||||
import type { Region } from "../regions";
|
|
||||||
import { usernamePattern } from "./constants";
|
|
||||||
|
|
||||||
export interface AccountListItem {
|
|
||||||
nickname: string;
|
|
||||||
account_id: number;
|
|
||||||
}
|
|
||||||
export type AccountList = AccountListItem[];
|
|
||||||
export type AccountListWithServer = (AccountListItem & {
|
|
||||||
region: Region;
|
|
||||||
})[];
|
|
||||||
|
|
||||||
export async function searchPlayersAcrossRegions(search: string, limit = 25) {
|
|
||||||
const trimmed = search.trim();
|
|
||||||
if (trimmed.length < 3 && trimmed.length > 100) return [];
|
|
||||||
const normalizedLimit = Math.floor(limit / 3);
|
|
||||||
|
|
||||||
if (usernamePattern.test(trimmed)) {
|
|
||||||
return (
|
|
||||||
await Promise.all([
|
|
||||||
fetchBlitz<AccountList>("com", "account/list", {
|
|
||||||
search,
|
|
||||||
limit: normalizedLimit,
|
|
||||||
}).then(
|
|
||||||
(value) =>
|
|
||||||
value &&
|
|
||||||
value.map((account) => ({
|
|
||||||
...account,
|
|
||||||
region: "com" as Region,
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
fetchBlitz<AccountList>("eu", "account/list", {
|
|
||||||
search,
|
|
||||||
limit: normalizedLimit,
|
|
||||||
}).then(
|
|
||||||
(value) =>
|
|
||||||
value &&
|
|
||||||
value.map((account) => ({
|
|
||||||
...account,
|
|
||||||
region: "eu" as Region,
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
fetchBlitz<AccountList>("asia", "account/list", {
|
|
||||||
search,
|
|
||||||
limit: normalizedLimit,
|
|
||||||
}).then(
|
|
||||||
(value) =>
|
|
||||||
value &&
|
|
||||||
value.map((account) => ({
|
|
||||||
...account,
|
|
||||||
region: "asia" as Region,
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.filter(Boolean)
|
|
||||||
.flat() as AccountListWithServer;
|
|
||||||
} else {
|
|
||||||
return [] as AccountListWithServer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from "./constants";
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { assertSecret } from "./assertSecret";
|
|
||||||
|
|
||||||
export function asset(path: string) {
|
|
||||||
return `${assertSecret(import.meta.env.PUBLIC_ASSET_BASE)}/${path}`;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { emptyAllStats } from '../../blitz/constants';
|
|
||||||
import { Samples } from '../../protos';
|
|
||||||
import type { BlitzkitStats } from '../../statistics';
|
|
||||||
|
|
||||||
export const emptyAverageDefinitionsAllStats: BlitzkitStats = {
|
|
||||||
...emptyAllStats,
|
|
||||||
battle_life_time: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const averageDefinitionsAllStatsKeys = Object.keys(
|
|
||||||
emptyAverageDefinitionsAllStats,
|
|
||||||
) as (keyof BlitzkitStats)[];
|
|
||||||
|
|
||||||
export const emptySamples: Samples = {
|
|
||||||
d_1: 0,
|
|
||||||
d_7: 0,
|
|
||||||
d_30: 0,
|
|
||||||
d_60: 0,
|
|
||||||
d_90: 0,
|
|
||||||
d_120: 0,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { fetchPB } from '../../protobuf';
|
|
||||||
import { AverageDefinitions } from '../../protos';
|
|
||||||
import { asset } from '../asset';
|
|
||||||
|
|
||||||
export interface AverageDefinitionsManifest {
|
|
||||||
version: 1;
|
|
||||||
/**
|
|
||||||
* epoch in days
|
|
||||||
*/
|
|
||||||
latest: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchAverageDefinitions() {
|
|
||||||
const manifestResponse = await fetch(asset('averages/manifest.json'));
|
|
||||||
const manifestJson =
|
|
||||||
(await manifestResponse.json()) as AverageDefinitionsManifest;
|
|
||||||
return await fetchPB(
|
|
||||||
asset(`averages/${manifestJson.latest}.pb`),
|
|
||||||
AverageDefinitions,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './constants';
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { CamouflageDefinitions } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchCamouflageDefinitions() {
|
|
||||||
return fetchPB(asset('definitions/camouflages.pb'), CamouflageDefinitions);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { ConsumableDefinitions } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchConsumableDefinitions() {
|
|
||||||
return fetchPB(asset('definitions/consumables.pb'), ConsumableDefinitions);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export interface DiscoveredIdsDefinitions {
|
|
||||||
time: number;
|
|
||||||
chunks: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchDiscoveredIdsDefinitions() {
|
|
||||||
const response = await fetch(asset('ids/manifest.json'));
|
|
||||||
return response.json() as Promise<DiscoveredIdsDefinitions>;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { EquipmentDefinitions } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchEquipmentDefinitions() {
|
|
||||||
return fetchPB(asset('definitions/equipment.pb'), EquipmentDefinitions);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { Gallery } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchGallery() {
|
|
||||||
return fetchPB(asset('definitions/gallery.pb'), Gallery);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { GameDefinitions } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchGameDefinitions() {
|
|
||||||
return fetchPB(asset('definitions/game.pb'), GameDefinitions);
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { getRatingInfo } from "../blitz/getRatingInfo";
|
|
||||||
import { assertSecret } from "./assertSecret";
|
|
||||||
|
|
||||||
interface GitHubTrees {
|
|
||||||
tree: { path: string; url: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
let cachedLatestArchivedSeasonNumber: number | null = null;
|
|
||||||
|
|
||||||
export async function getArchivedLatestSeasonNumber() {
|
|
||||||
if (cachedLatestArchivedSeasonNumber !== null) {
|
|
||||||
return cachedLatestArchivedSeasonNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ratingInfo = await getRatingInfo("com");
|
|
||||||
|
|
||||||
if (ratingInfo.detail) {
|
|
||||||
const regionsURL = (
|
|
||||||
(await fetch(
|
|
||||||
`https://api.github.com/repos/${assertSecret(
|
|
||||||
import.meta.env.PUBLIC_ASSET_REPO
|
|
||||||
)}/git/trees/main`
|
|
||||||
).then((response) => response.json())) as GitHubTrees
|
|
||||||
).tree.find(({ path }) => path === "regions")!.url;
|
|
||||||
const comURL = (
|
|
||||||
(await fetch(regionsURL).then((response) =>
|
|
||||||
response.json()
|
|
||||||
)) as GitHubTrees
|
|
||||||
).tree.find(({ path }) => path === "com")!.url;
|
|
||||||
const ratingURL = (
|
|
||||||
(await fetch(comURL).then((response) => response.json())) as {
|
|
||||||
tree: [{ url: string }];
|
|
||||||
}
|
|
||||||
).tree[0].url;
|
|
||||||
const latestSeason = Math.max(
|
|
||||||
...(
|
|
||||||
(await fetch(ratingURL).then((response) => response.json())) as {
|
|
||||||
tree: { path: string }[];
|
|
||||||
}
|
|
||||||
).tree.map(({ path }) => Number(path))
|
|
||||||
);
|
|
||||||
|
|
||||||
cachedLatestArchivedSeasonNumber = latestSeason;
|
|
||||||
} else {
|
|
||||||
cachedLatestArchivedSeasonNumber = ratingInfo.current_season - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cachedLatestArchivedSeasonNumber;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import type { Region } from "../blitz/regions";
|
|
||||||
import { RatingLeaderboard } from "../protos";
|
|
||||||
import { asset } from "./asset";
|
|
||||||
|
|
||||||
const cache: Record<Region, Record<number, RatingLeaderboard>> = {
|
|
||||||
asia: {},
|
|
||||||
eu: {},
|
|
||||||
com: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getArchivedRatingLeaderboard(
|
|
||||||
region: Region,
|
|
||||||
season: number
|
|
||||||
) {
|
|
||||||
if (!cache[region][season]) {
|
|
||||||
const info = await fetch(asset(`regions/${region}/rating/${season}.pb`))
|
|
||||||
.then((response) => response.arrayBuffer())
|
|
||||||
.then((buffer) => RatingLeaderboard.decode(new Uint8Array(buffer)));
|
|
||||||
cache[region][season] = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
return cache[region][season];
|
|
||||||
}
|
|
||||||
@@ -1,50 +1,22 @@
|
|||||||
export * from "./assertSecret";
|
|
||||||
export * from "./asset";
|
|
||||||
export * from "./availableProvisions";
|
export * from "./availableProvisions";
|
||||||
export * from "./averageDefinitions";
|
|
||||||
// export * from './camouflageDefinitions';
|
|
||||||
// export * from './checkConsumableProvisionInclusivity';
|
|
||||||
export * from "./coefficient";
|
export * from "./coefficient";
|
||||||
export * from "./consumableDefinitions";
|
|
||||||
// export * from './context';
|
|
||||||
export * from "./createDefaultSkills";
|
export * from "./createDefaultSkills";
|
||||||
export * from "./degressiveStat";
|
export * from "./degressiveStat";
|
||||||
export * from "./discoveredIdDefinitions";
|
|
||||||
export * from "./emblemURL";
|
export * from "./emblemURL";
|
||||||
export * from "./equipmentDefinitions";
|
|
||||||
export * from "./eventManager";
|
export * from "./eventManager";
|
||||||
export * from "./gameDefinitions";
|
|
||||||
export * from "./getArchivedLatestSeasonNumber";
|
|
||||||
// export * from './getArchivedRatingInfo';
|
|
||||||
export * from "./getArchivedRatingLeaderboard";
|
|
||||||
// export * from './getArchivedRatingMidnightLeaderboard';
|
|
||||||
export * from "./getTimeDaysAgo";
|
export * from "./getTimeDaysAgo";
|
||||||
export * from "./isExplosive";
|
export * from "./isExplosive";
|
||||||
export * from "./mapDefinitions";
|
|
||||||
export * from "./modelDefinitions";
|
|
||||||
export * from "./normalizeBoundingBox";
|
export * from "./normalizeBoundingBox";
|
||||||
// export * from './parseBkni';
|
|
||||||
// export * from './patientFetch';
|
|
||||||
export * from "./patientFetchJSON";
|
export * from "./patientFetchJSON";
|
||||||
export * from "./progressiveStat";
|
export * from "./progressiveStat";
|
||||||
export * from "./provisionDefinitions";
|
|
||||||
export * from "./resolveDpm";
|
export * from "./resolveDpm";
|
||||||
export * from "./resolvePenetrationCoefficient";
|
export * from "./resolvePenetrationCoefficient";
|
||||||
export * from "./reviews";
|
|
||||||
export * from "./skillDefinitions";
|
|
||||||
export * from "./sum";
|
export * from "./sum";
|
||||||
export * from "./tankDefinitions";
|
|
||||||
export * from "./tankIcon";
|
|
||||||
export * from "./tankNames";
|
|
||||||
export * from "./unionBoundingBox";
|
export * from "./unionBoundingBox";
|
||||||
export * from "./uniqueGuns";
|
export * from "./uniqueGuns";
|
||||||
export * from "./unwrapBlitzkitResponse";
|
export * from "./unwrapBlitzkitResponse";
|
||||||
export * from "./votes";
|
export * from "./votes";
|
||||||
// export * from './withCORSProxy';
|
|
||||||
export * from "./createDefaultProvisions";
|
export * from "./createDefaultProvisions";
|
||||||
export * from "./gallery";
|
|
||||||
export * from "./metaSortTank";
|
export * from "./metaSortTank";
|
||||||
export * from "./popularTanks";
|
|
||||||
export * from "./romanize";
|
export * from "./romanize";
|
||||||
export * from "./sluggify.ts";
|
|
||||||
export * from "./youtubers";
|
export * from "./youtubers";
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { MapDefinitions } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchMapDefinitions() {
|
|
||||||
return fetchPB(asset('definitions/maps.pb'), MapDefinitions);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { ModelDefinitions } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchModelDefinitions() {
|
|
||||||
return fetchPB(asset('definitions/models.pb'), ModelDefinitions);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { PopularTanks } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchPopularTanks() {
|
|
||||||
return fetchPB(asset('definitions/popular-tanks.pb'), PopularTanks);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { ProvisionDefinitions } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchProvisionDefinitions() {
|
|
||||||
return fetchPB(asset('definitions/provisions.pb'), ProvisionDefinitions);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { Reviews } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchReviews() {
|
|
||||||
return fetchPB(asset('definitions/reviews.pb'), Reviews);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { fetchPB } from '../protobuf';
|
|
||||||
import { SkillDefinitions } from '../protos';
|
|
||||||
import { asset } from './asset';
|
|
||||||
|
|
||||||
export function fetchSkillDefinitions() {
|
|
||||||
return fetchPB(asset('definitions/skills.pb'), SkillDefinitions);
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { times } from "lodash-es";
|
|
||||||
import { CrewType, ShellType, TankClass, TankType } from "../../protos";
|
|
||||||
|
|
||||||
export const SHELL_NAMES: Record<ShellType, string> = {
|
|
||||||
[ShellType.SHELL_TYPE_AP]: "AP",
|
|
||||||
[ShellType.SHELL_TYPE_APCR]: "APCR",
|
|
||||||
[ShellType.SHELL_TYPE_HEAT]: "HEAT",
|
|
||||||
[ShellType.SHELL_TYPE_HE]: "HE",
|
|
||||||
};
|
|
||||||
export const CREW_MEMBER_NAMES = {
|
|
||||||
[CrewType.CREW_TYPE_COMMANDER]: "commander",
|
|
||||||
[CrewType.CREW_TYPE_DRIVER]: "driver",
|
|
||||||
[CrewType.CREW_TYPE_GUNNER]: "gunner",
|
|
||||||
[CrewType.CREW_TYPE_LOADER]: "loader",
|
|
||||||
[CrewType.CREW_TYPE_RADIOMAN]: "radioman",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use svg
|
|
||||||
*/
|
|
||||||
export const TANK_ICONS: Record<TankClass, string> = {
|
|
||||||
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/BIHSEH0.png",
|
|
||||||
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/CSNha5V.png",
|
|
||||||
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/wvf3ltm.png",
|
|
||||||
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/ECeqlZa.png",
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @deprecated use svg
|
|
||||||
*/
|
|
||||||
export const TANK_ICONS_PREMIUM: Record<TankClass, string> = {
|
|
||||||
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/TCu3EdR.png",
|
|
||||||
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/zdkpTRb.png",
|
|
||||||
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/3z7eHX6.png",
|
|
||||||
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/P3vbmyA.png",
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @deprecated use svg
|
|
||||||
*/
|
|
||||||
export const TANK_ICONS_COLLECTOR: Record<TankClass, string> = {
|
|
||||||
[TankClass.TANK_CLASS_TANK_DESTROYER]: "https://i.imgur.com/WTjeirB.png",
|
|
||||||
[TankClass.TANK_CLASS_LIGHT]: "https://i.imgur.com/EwhtKkU.png",
|
|
||||||
[TankClass.TANK_CLASS_MEDIUM]: "https://i.imgur.com/u8YDMBh.png",
|
|
||||||
[TankClass.TANK_CLASS_HEAVY]: "https://i.imgur.com/8xRf3nc.png",
|
|
||||||
};
|
|
||||||
export const TIERS = times(10, (index) => index + 1);
|
|
||||||
export const TIER_ROMAN_NUMERALS: Record<number, string> = {
|
|
||||||
1: "I",
|
|
||||||
2: "II",
|
|
||||||
3: "III",
|
|
||||||
4: "IV",
|
|
||||||
5: "V",
|
|
||||||
6: "VI",
|
|
||||||
7: "VII",
|
|
||||||
8: "VIII",
|
|
||||||
9: "IX",
|
|
||||||
10: "X",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const flags: Record<string, string> = {
|
|
||||||
ussr: "<:ussr:1218421042033197197>",
|
|
||||||
germany: "🇩🇪",
|
|
||||||
usa: "🇺🇸",
|
|
||||||
china: "🇨🇳",
|
|
||||||
uk: "🇬🇧",
|
|
||||||
france: "🇫🇷",
|
|
||||||
japan: "🇯🇵",
|
|
||||||
european: "🇪🇺",
|
|
||||||
other: "<:other:1218421572243558482>",
|
|
||||||
};
|
|
||||||
export const TREE_TYPE_ICONS: Record<TankType, Record<TankClass, string>> = {
|
|
||||||
[TankType.TANK_TYPE_RESEARCHABLE]: TANK_ICONS,
|
|
||||||
[TankType.TANK_TYPE_PREMIUM]: TANK_ICONS_PREMIUM,
|
|
||||||
[TankType.TANK_TYPE_COLLECTOR]: TANK_ICONS_COLLECTOR,
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { fetchPB } from '../../protobuf';
|
|
||||||
import { TankDefinitions } from '../../protos';
|
|
||||||
import { asset } from '../asset';
|
|
||||||
|
|
||||||
export function fetchTankDefinitions() {
|
|
||||||
return fetchPB(asset('definitions/tanks.pb'), TankDefinitions);
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from './constants';
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { asset } from "./asset";
|
|
||||||
|
|
||||||
export const BLITZKIT_TANK_ICON_SIZE = { width: 40 * 16, height: 30 * 16 };
|
|
||||||
|
|
||||||
export function tankIcon(id: number, size: "big" | "small" = "big") {
|
|
||||||
return asset(`icons/tanks/${size}/${id}.webp`);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import locales from "@blitzkit/i18n/locales.json";
|
|
||||||
import { deburr } from "lodash-es";
|
|
||||||
import { I18nString } from "../protos";
|
|
||||||
import { fetchCamouflageDefinitions } from "./camouflageDefinitions";
|
|
||||||
import { fetchTankDefinitions } from "./tankDefinitions";
|
|
||||||
|
|
||||||
export async function fetchTankNames() {
|
|
||||||
const [tankDefinitions, camouflageDefinitions] = await Promise.all([
|
|
||||||
fetchTankDefinitions(),
|
|
||||||
fetchCamouflageDefinitions(),
|
|
||||||
]);
|
|
||||||
const tankDefinitionsArray = Object.values(tankDefinitions.tanks);
|
|
||||||
|
|
||||||
return await Promise.all(
|
|
||||||
tankDefinitionsArray.map(async (tank) => {
|
|
||||||
const searchableNameDeburr: I18nString = { locales: {} };
|
|
||||||
|
|
||||||
Object.entries(tank.name!).forEach(([key, value]) => {
|
|
||||||
searchableNameDeburr.locales[key] = deburr(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: tank.id,
|
|
||||||
name: tank.name,
|
|
||||||
searchableName: tank.name,
|
|
||||||
searchableNameDeburr,
|
|
||||||
camouflages: tank.camouflages
|
|
||||||
?.map((id) =>
|
|
||||||
locales.supported.map(
|
|
||||||
({ locale }) =>
|
|
||||||
camouflageDefinitions.camouflages[id]?.name!.locales[locale],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.flat()
|
|
||||||
.filter(Boolean)
|
|
||||||
.map(deburr)
|
|
||||||
.join(" "),
|
|
||||||
treeType: tank.type,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SEARCH_KEYS = [
|
|
||||||
...locales.supported
|
|
||||||
.map(({ locale }) => [
|
|
||||||
`searchableName.locales.${locale}`,
|
|
||||||
`searchableNameDeburr.locales.${locale}`,
|
|
||||||
])
|
|
||||||
.flat(),
|
|
||||||
"camouflages",
|
|
||||||
];
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { assertSecret } from '../blitzkit/assertSecret';
|
import { secret } from "../secrets";
|
||||||
|
|
||||||
export function patreonLoginUrl() {
|
export function patreonLoginUrl() {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
response_type: 'code',
|
response_type: "code",
|
||||||
client_id: assertSecret(import.meta.env.PUBLIC_PATREON_CLIENT_ID),
|
client_id: secret("PUBLIC_PATREON_CLIENT_ID"),
|
||||||
redirect_uri: assertSecret(import.meta.env.PUBLIC_PATREON_REDIRECT_URI),
|
redirect_uri: secret("PUBLIC_PATREON_REDIRECT_URI"),
|
||||||
scope: 'identity',
|
scope: "identity",
|
||||||
});
|
});
|
||||||
|
|
||||||
return `https://www.patreon.com/oauth2/authorize?${params}`;
|
return `https://www.patreon.com/oauth2/authorize?${params}`;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export enum SecretType {
|
|||||||
Number,
|
Number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PassableSecretName = WellKnownSecret | (string & {});
|
export type PassableSecretName = WellKnownSecret;
|
||||||
|
|
||||||
export function secret(
|
export function secret(
|
||||||
name: PassableSecretName,
|
name: PassableSecretName,
|
||||||
@@ -18,10 +18,7 @@ export function secret(
|
|||||||
type: SecretType.Number,
|
type: SecretType.Number,
|
||||||
): number;
|
): number;
|
||||||
|
|
||||||
export function secret(
|
export function secret(name: PassableSecretName, type = SecretType.String) {
|
||||||
name: WellKnownSecret | (string & {}),
|
|
||||||
type = SecretType.String,
|
|
||||||
) {
|
|
||||||
if (!hasSecret(name)) {
|
if (!hasSecret(name)) {
|
||||||
throw new Error(`Secret "${name}" not defined`);
|
throw new Error(`Secret "${name}" not defined`);
|
||||||
}
|
}
|
||||||
@@ -30,7 +27,7 @@ export function secret(
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SecretType.String:
|
case SecretType.String:
|
||||||
return value;
|
return String(value);
|
||||||
|
|
||||||
case SecretType.Number:
|
case SecretType.Number:
|
||||||
return Number(value);
|
return Number(value);
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
export type WellKnownSecret =
|
export type WellKnownSecret =
|
||||||
|
// development set-up
|
||||||
|
| "TEMP"
|
||||||
|
// website
|
||||||
| "BUILD_CHUNK"
|
| "BUILD_CHUNK"
|
||||||
| "TEXTURE_CHUNKS"
|
| "TEXTURE_CHUNKS"
|
||||||
| "TEMP"
|
// wotb
|
||||||
| "WOTB_SERVER"
|
| "WOTB_SERVER"
|
||||||
| "WOTB_VERSION"
|
| "WOTB_VERSION"
|
||||||
| "PUBLIC_WOTB_CLIENT"
|
| "PUBLIC_WOTB_CLIENT"
|
||||||
| "WOTB_USMAP"
|
| "WOTB_USMAP"
|
||||||
|
// google
|
||||||
| "GOOGLE_APPLICATION_CREDENTIALS"
|
| "GOOGLE_APPLICATION_CREDENTIALS"
|
||||||
| "PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID";
|
| "PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID"
|
||||||
|
// patreon
|
||||||
|
| "PUBLIC_PATREON_CLIENT_ID"
|
||||||
|
| "PUBLIC_PATREON_REDIRECT_URI";
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { fetchTankDefinitions } from '../blitzkit';
|
|
||||||
import type { BlitzStats } from './compositeStats/constants';
|
|
||||||
|
|
||||||
export async function getTierWeights(
|
|
||||||
stats: Record<number, BlitzStats>,
|
|
||||||
order: number[] = Object.keys(stats).map(parseInt),
|
|
||||||
) {
|
|
||||||
const tankDefinitions = await fetchTankDefinitions();
|
|
||||||
const tierWeights: TierWeightsRecord = {};
|
|
||||||
|
|
||||||
order.forEach((id) => {
|
|
||||||
const entry = tankDefinitions.tanks[id];
|
|
||||||
|
|
||||||
if (!entry) return;
|
|
||||||
if (!tierWeights[entry.tier]) tierWeights[entry.tier] = 0;
|
|
||||||
|
|
||||||
tierWeights[entry.tier]! += stats[id].battles;
|
|
||||||
});
|
|
||||||
|
|
||||||
return tierWeights;
|
|
||||||
}
|
|
||||||
export type TierWeightsRecord = Partial<Record<number, number>>;
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
export * from './calculateWN8';
|
export * from "./calculateWN8";
|
||||||
export * from './compositeStats';
|
export * from "./compositeStats";
|
||||||
export * from './deltaBkrlBlitzStats';
|
export * from "./deltaBkrlBlitzStats";
|
||||||
export * from './deltaTankStats';
|
export * from "./deltaTankStats";
|
||||||
export * from './getBkniPercentile';
|
export * from "./getBkniPercentile";
|
||||||
export * from './getTierWeights';
|
export * from "./getWN8Percentile";
|
||||||
export * from './getWN8Percentile';
|
export * from "./percentiles";
|
||||||
export * from './percentiles';
|
export * from "./sumAllStats";
|
||||||
export * from './sumAllStats';
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { dvp } from "@blitzkit/closed";
|
|
||||||
import { assertSecret } from "@blitzkit/core";
|
|
||||||
import { mkdir, writeFile } from "fs/promises";
|
|
||||||
import { parse as parsePath } from "path";
|
|
||||||
import ProgressBar from "progress";
|
|
||||||
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
||||||
import { vfs } from "./buildAssets/constants";
|
|
||||||
import { writeDVPL } from "./core/blitz/writeDVPL";
|
|
||||||
|
|
||||||
const versionTextFile = await vfs.text(`Data/version.txt`);
|
|
||||||
const currentVersion = versionTextFile
|
|
||||||
.split(" ")[0]
|
|
||||||
.split(".")
|
|
||||||
.slice(0, 3)
|
|
||||||
.join(".");
|
|
||||||
|
|
||||||
console.log(`Installing patches for ${currentVersion}...`);
|
|
||||||
|
|
||||||
let patchIndex = 1;
|
|
||||||
while (true) {
|
|
||||||
const response = await fetch(
|
|
||||||
`${assertSecret(
|
|
||||||
import.meta.env.WOTB_DLC_CDN
|
|
||||||
)}/dlc/s${currentVersion}_${patchIndex}.yaml`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
console.log(`Applying patch ${patchIndex}...`);
|
|
||||||
|
|
||||||
const data = parseYaml(await response.text());
|
|
||||||
const dvpm = await fetch(
|
|
||||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11}`
|
|
||||||
).then((response) => response.arrayBuffer());
|
|
||||||
const dvpd = await fetch(
|
|
||||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${data.dx11.replace(
|
|
||||||
".dvpm",
|
|
||||||
".dvpd"
|
|
||||||
)}`
|
|
||||||
).then((response) => response.arrayBuffer());
|
|
||||||
const files = await dvp(dvpm, dvpd);
|
|
||||||
const bar = new ProgressBar(
|
|
||||||
`Patching ${files.length} files :bar`,
|
|
||||||
files.length
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const { path, data } of files) {
|
|
||||||
console.log(`Patching "${path}"...`);
|
|
||||||
|
|
||||||
const { dir } = parsePath(path);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await mkdir(`Data/${dir}`, { recursive: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to make directory "${dir}"`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDvpl = path.endsWith(".dvpl");
|
|
||||||
const buffer = Buffer.from(data);
|
|
||||||
|
|
||||||
await writeFile(
|
|
||||||
`Data/${path}${isDvpl ? "" : ".dvpl"}`,
|
|
||||||
new Uint8Array(isDvpl ? buffer : writeDVPL(buffer))
|
|
||||||
);
|
|
||||||
|
|
||||||
bar.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("dynamicContentLocalizationsDir" in data) {
|
|
||||||
console.log("Found dynamic content localizations; patching...");
|
|
||||||
|
|
||||||
const localizationsResponse = await fetch(
|
|
||||||
`${assertSecret(import.meta.env.WOTB_DLC_CDN)}/dlc/${
|
|
||||||
data.dynamicContentLocalizationsDir
|
|
||||||
}/en.yaml`
|
|
||||||
);
|
|
||||||
const newStrings = parseYaml(await localizationsResponse.text());
|
|
||||||
const oldStrings = await vfs.yaml<Record<string, string>>(
|
|
||||||
`Data/Strings/en.yaml`
|
|
||||||
);
|
|
||||||
const patchedStrings = { ...oldStrings, ...newStrings };
|
|
||||||
const patchedContent = stringifyYaml(patchedStrings);
|
|
||||||
|
|
||||||
await writeFile(
|
|
||||||
`Data/Strings/en.yaml`,
|
|
||||||
new Uint8Array(writeDVPL(Buffer.from(patchedContent)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
patchIndex++;
|
|
||||||
} else break;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import sharp from "sharp";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
|
|
||||||
export async function boosterIcons() {
|
|
||||||
console.log("Building booster icons...");
|
|
||||||
|
|
||||||
const uploader = new AssetUploader("booster icons");
|
|
||||||
const boosterFiles = await vfs
|
|
||||||
.dir("Data/Gfx/Shared/boosters")
|
|
||||||
.then((files) =>
|
|
||||||
files.filter(
|
|
||||||
(file) => !file.includes("@2x.txt") && !file.startsWith("texture0"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const image = sharp(
|
|
||||||
await vfs.file(`Data/Gfx/Shared/boosters/texture0.packed.webp`),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const file of boosterFiles) {
|
|
||||||
const name = file.match(/booster_(.+).txt/)?.[1];
|
|
||||||
|
|
||||||
if (name === undefined) continue;
|
|
||||||
|
|
||||||
const sizes = (await vfs.text(`Data/Gfx/Shared/boosters/${file}`))
|
|
||||||
.split("\n")[4]
|
|
||||||
.split(" ")
|
|
||||||
.map(Number);
|
|
||||||
const content = await image
|
|
||||||
.clone()
|
|
||||||
.extract({
|
|
||||||
left: sizes[0],
|
|
||||||
top: sizes[1],
|
|
||||||
width: sizes[2],
|
|
||||||
height: sizes[3],
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: `icons/boosters/${name}.webp`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
|
|
||||||
export async function camouflageIcons() {
|
|
||||||
console.log("Building camouflage icons...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("camo icons");
|
|
||||||
const content = await vfs.file(
|
|
||||||
`Data/Gfx/UI/Hangar/IconCamouflage.packed.webp`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await uploader.add({ content, path: "icons/camo.webp" });
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { assertSecret } from "@blitzkit/core";
|
|
||||||
import { AbstractVFS } from "../core/vfs/abstract";
|
|
||||||
import { LocalVFS } from "../core/vfs/local";
|
|
||||||
import { SteamVFS } from "../core/vfs/steam";
|
|
||||||
import { ZipVFS } from "../core/vfs/zip";
|
|
||||||
|
|
||||||
const steamPattern = /steam:(\d+)\/(\d+)/;
|
|
||||||
const zipPattern = /zip:(.+)/;
|
|
||||||
const localePattern = /local:"?(.+)"?/;
|
|
||||||
|
|
||||||
const source = Bun.argv
|
|
||||||
.find((arg) => arg.startsWith("--source="))
|
|
||||||
?.split("=")[1];
|
|
||||||
|
|
||||||
let resolved: AbstractVFS;
|
|
||||||
|
|
||||||
if (!source) {
|
|
||||||
throw new Error("No source specified");
|
|
||||||
} else if (steamPattern.test(source)) {
|
|
||||||
const [, app, depot] = source.match(steamPattern)!.map(Number);
|
|
||||||
|
|
||||||
resolved = await new SteamVFS(
|
|
||||||
assertSecret(import.meta.env.STEAM_USERNAME),
|
|
||||||
assertSecret(import.meta.env.STEAM_PASSWORD),
|
|
||||||
app,
|
|
||||||
depot,
|
|
||||||
).init();
|
|
||||||
} else if (zipPattern.test(source)) {
|
|
||||||
const [, url] = source.match(zipPattern)!;
|
|
||||||
resolved = await new ZipVFS(url).init();
|
|
||||||
} else if (localePattern.test(source)) {
|
|
||||||
const [, path] = source.match(localePattern)!;
|
|
||||||
resolved = await new LocalVFS(path).init();
|
|
||||||
} else {
|
|
||||||
throw new Error("No valid source specified");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const vfs = resolved;
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
import sharp from "sharp";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { parsePackedSpriteRect } from "../core/blitz/parsePackedSpriteRect";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
import { ConsumablesCommon, ProvisionsCommon } from "./definitions";
|
|
||||||
|
|
||||||
interface Mappings {
|
|
||||||
Header: { version: number };
|
|
||||||
StyleSheets: {
|
|
||||||
selectors: string[];
|
|
||||||
properties: { "bg-sprite": string };
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const listItemsPattern = /<items path="(.+)\.xml"\/>/g;
|
|
||||||
|
|
||||||
async function extractPackedIcon(
|
|
||||||
texture: sharp.Sharp,
|
|
||||||
sizes: number[],
|
|
||||||
context: string,
|
|
||||||
) {
|
|
||||||
const [left, top, width, height] = sizes;
|
|
||||||
const bounds = [left, top, width, height];
|
|
||||||
|
|
||||||
if (!bounds.every(Number.isInteger)) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid RIFF sprite bounds for ${context}: ${bounds.join(" ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = await texture.metadata();
|
|
||||||
|
|
||||||
if (metadata.width === undefined || metadata.height === undefined) {
|
|
||||||
throw new Error(`Failed to read image dimensions for ${context}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
left < 0 ||
|
|
||||||
top < 0 ||
|
|
||||||
width <= 0 ||
|
|
||||||
height <= 0 ||
|
|
||||||
left + width > metadata.width ||
|
|
||||||
top + height > metadata.height
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Out-of-bounds RIFF sprite for ${context}: ${bounds.join(" ")} in ${metadata.width}x${metadata.height}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await texture.clone().extract({ left, top, width, height }).toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function consumableProvisionIcons() {
|
|
||||||
console.log("Building consumable and provision icons...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("consumable and provision icons");
|
|
||||||
const styles = [
|
|
||||||
"UI/Styles/Lobby/Inventory/InventoryNormalStyles.yaml",
|
|
||||||
"UI/Styles/Lobby/Inventory/Event/InventoryBigStyles.yaml",
|
|
||||||
"UI/Styles/Lobby/Inventory/Event/InventoryNormalStyles.yaml",
|
|
||||||
"UI/Screens3/Lobby/Inventory/Equipment/EquipmentItemImage.style.yaml",
|
|
||||||
"UI/Screens/Battle/Styles/BattleEquipmentStyles.yaml",
|
|
||||||
];
|
|
||||||
const styleSheets = await Promise.all(
|
|
||||||
styles.map(async (path) =>
|
|
||||||
Object.values((await vfs.yaml<Mappings>(`Data/${path}`)).StyleSheets),
|
|
||||||
),
|
|
||||||
).then((array) => array.flat());
|
|
||||||
|
|
||||||
for (const match of (
|
|
||||||
await vfs.text(`Data/XML/item_defs/vehicles/common/consumables/list.xml`)
|
|
||||||
).matchAll(listItemsPattern)) {
|
|
||||||
const consumablesCommon = await vfs.xml<{
|
|
||||||
root: ConsumablesCommon;
|
|
||||||
}>(`Data/XML/item_defs/vehicles/common/consumables/${match[1]}.xml`);
|
|
||||||
|
|
||||||
for (const consumable of Object.values(consumablesCommon.root)) {
|
|
||||||
const styleSheet = styleSheets.find((styleSheet) =>
|
|
||||||
styleSheet.selectors.some((selector) =>
|
|
||||||
selector.includes(`${consumable.icon} `),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!styleSheet) {
|
|
||||||
console.warn(
|
|
||||||
`No style sheet found for consumable ${consumable.icon}; skipping...`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configPath = styleSheet.properties["bg-sprite"]
|
|
||||||
.replace("~res:/", "")
|
|
||||||
.replace(".psd", "")
|
|
||||||
.replace(".txt", "");
|
|
||||||
|
|
||||||
if (await vfs.resolve(`Data/${configPath}.packed.webp`)) {
|
|
||||||
const packedBuffer = await vfs.file(`Data/${configPath}.packed.webp`);
|
|
||||||
const consumablesTexture = sharp(packedBuffer);
|
|
||||||
const sizes = parsePackedSpriteRect(packedBuffer);
|
|
||||||
let content: Buffer;
|
|
||||||
|
|
||||||
if (sizes) {
|
|
||||||
content = await extractPackedIcon(
|
|
||||||
consumablesTexture,
|
|
||||||
sizes,
|
|
||||||
`consumable ${consumable.icon}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
content = await consumablesTexture.trim({ threshold: 80 }).toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: `icons/consumables/${consumable.id}.webp`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const consumablesTexture = sharp(
|
|
||||||
await vfs.file(
|
|
||||||
`${`Data/${configPath}`
|
|
||||||
.split("/")
|
|
||||||
.slice(0, -1)
|
|
||||||
.join("/")}/texture0.packed.webp`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const sizes = (await vfs.text(`Data/${configPath}.txt`))
|
|
||||||
.split("\n")[4]
|
|
||||||
.split(" ")
|
|
||||||
.map(Number);
|
|
||||||
const content = await consumablesTexture
|
|
||||||
.clone()
|
|
||||||
.extract({
|
|
||||||
left: sizes[0],
|
|
||||||
top: sizes[1],
|
|
||||||
width: sizes[2],
|
|
||||||
height: sizes[3],
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: `icons/consumables/${consumable.id}.webp`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const match of (
|
|
||||||
await vfs.text(`Data/XML/item_defs/vehicles/common/provisions/list.xml`)
|
|
||||||
).matchAll(listItemsPattern)) {
|
|
||||||
const provisionsCommon = await vfs.xml<{
|
|
||||||
root: ProvisionsCommon;
|
|
||||||
}>(`Data/XML/item_defs/vehicles/common/provisions/${match[1]}.xml`);
|
|
||||||
|
|
||||||
for (const provision of Object.values(provisionsCommon.root)) {
|
|
||||||
const styleSheet = styleSheets.find((styleSheet) =>
|
|
||||||
styleSheet.selectors.some((selector) =>
|
|
||||||
selector.includes(`${provision.icon} `),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!styleSheet) {
|
|
||||||
console.warn(
|
|
||||||
`No style sheet found for provision ${provision.icon}; skipping...`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configPath = styleSheet.properties["bg-sprite"]
|
|
||||||
.replace("~res:/", "")
|
|
||||||
.replace(".psd", "")
|
|
||||||
.replace(".txt", "");
|
|
||||||
|
|
||||||
if (await vfs.resolve(`Data/${configPath}.packed.webp`)) {
|
|
||||||
const packedBuffer = await vfs.file(`Data/${configPath}.packed.webp`);
|
|
||||||
const provisionsTexture = sharp(packedBuffer);
|
|
||||||
const sizes = parsePackedSpriteRect(packedBuffer);
|
|
||||||
let content: Buffer;
|
|
||||||
|
|
||||||
if (sizes) {
|
|
||||||
content = await extractPackedIcon(
|
|
||||||
provisionsTexture,
|
|
||||||
sizes,
|
|
||||||
`provision ${provision.icon}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
content = await provisionsTexture.trim({ threshold: 80 }).toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: `icons/provisions/${provision.id}.webp`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const provisionsTexture = sharp(
|
|
||||||
await vfs.file(
|
|
||||||
`${`Data/${configPath}`
|
|
||||||
.split("/")
|
|
||||||
.slice(0, -1)
|
|
||||||
.join("/")}/texture0.packed.webp`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const sizes = (await vfs.text(`Data/${configPath}.txt`))
|
|
||||||
.split("\n")[4]
|
|
||||||
.split(" ")
|
|
||||||
.map(Number);
|
|
||||||
const content = await provisionsTexture
|
|
||||||
.clone()
|
|
||||||
.extract({
|
|
||||||
left: sizes[0],
|
|
||||||
top: sizes[1],
|
|
||||||
width: sizes[2],
|
|
||||||
height: sizes[3],
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: `icons/provisions/${provision.id}.webp`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { assertSecret, BlitzGlossary } from "@blitzkit/core";
|
|
||||||
import sharp from "sharp";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
|
|
||||||
const ICONS = [
|
|
||||||
["currency_silver_m.packed.webp", "silver"],
|
|
||||||
["currency_premium_xl.packed.webp", "premium"],
|
|
||||||
["currency_gold_m.packed.webp", "gold"],
|
|
||||||
["currency_free-xp_xl.packed.webp", "free-xp"],
|
|
||||||
["currency_elite-xp_xl.packed.webp", "elite-xp"],
|
|
||||||
["currency_crew-xp_xl.packed.webp", "crew-xp"],
|
|
||||||
["currency_battle-xp_xl.packed.webp", "xp"],
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function currencies() {
|
|
||||||
console.log("Building currency icons...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("currency icons");
|
|
||||||
|
|
||||||
for (const [file, name] of ICONS) {
|
|
||||||
const content = await sharp(
|
|
||||||
await vfs.file(`Data/Gfx/Lobby/currency/${file}`),
|
|
||||||
)
|
|
||||||
.trim()
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
content,
|
|
||||||
path: `icons/currencies/${name}.webp`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const glossary = await fetch(assertSecret(import.meta.env.WOTB_GLOSSARY))
|
|
||||||
.then((response) => response.json() as Promise<BlitzGlossary>)
|
|
||||||
.then((glossary) =>
|
|
||||||
Object.values(glossary)
|
|
||||||
.entries()
|
|
||||||
.filter(([key]) => /^prx_season_\d+$/.test(`${key}`)),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [key, item] of glossary) {
|
|
||||||
if (item.image_url === null) throw new Error(`No image_url for ${key}`);
|
|
||||||
|
|
||||||
const imageRaw = await fetch(item.image_url).then((response) =>
|
|
||||||
response.arrayBuffer(),
|
|
||||||
);
|
|
||||||
const content = await sharp(imageRaw)
|
|
||||||
.trim({
|
|
||||||
threshold: 100,
|
|
||||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
content,
|
|
||||||
path: `icons/currencies/${key}.webp`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,80 +0,0 @@
|
|||||||
import sharp from "sharp";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
import { OptionalDevices } from "./definitions";
|
|
||||||
|
|
||||||
interface Mappings {
|
|
||||||
Header: { version: number };
|
|
||||||
|
|
||||||
StyleSheets: {
|
|
||||||
selectors: string[];
|
|
||||||
properties: { "bg-sprite": string };
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function equipmentIcons() {
|
|
||||||
console.log("Building equipment icons...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("equipment icons");
|
|
||||||
const optionalDevices = await vfs.xml<{ root: OptionalDevices }>(
|
|
||||||
`Data/XML/item_defs/vehicles/common/optional_devices.xml`,
|
|
||||||
);
|
|
||||||
const mappings = await vfs.yaml<Mappings>(
|
|
||||||
`Data/UI/Screens3/Lobby/Inventory/OptionalDevices/OptionalDevicesItemImage.style.yaml`,
|
|
||||||
);
|
|
||||||
const image = sharp(
|
|
||||||
await vfs.file(
|
|
||||||
`Data/Gfx/UI/InventoryIcons/Big/OptionalDevices/texture0.packed.webp`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const optionalDeviceKey in optionalDevices.root) {
|
|
||||||
const optionalDevice = optionalDevices.root[optionalDeviceKey];
|
|
||||||
|
|
||||||
if (optionalDeviceKey === "nextAvailableId") continue;
|
|
||||||
|
|
||||||
const mapping = mappings.StyleSheets.find((mapping) =>
|
|
||||||
mapping.selectors.includes(
|
|
||||||
`.optional_device_item.${optionalDevice.icon} #Img`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!mapping) {
|
|
||||||
console.warn(`No mapping found for ${optionalDevice.icon}; skipping...`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configPathRaw = mapping.properties["bg-sprite"];
|
|
||||||
const configPath = configPathRaw.replace("~res:/", "");
|
|
||||||
|
|
||||||
if (configPath.startsWith("Gfx/Lobby")) {
|
|
||||||
const configPathWebp = configPath.replace(".txt", "");
|
|
||||||
const image = sharp(await vfs.file(`Data/${configPathWebp}.packed.webp`));
|
|
||||||
const content = await image.trim().toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: `icons/equipment/${optionalDevice.id}.webp`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const config = await vfs.text(`Data/${configPath}`);
|
|
||||||
const sizes = config.split("\n")[4].split(" ").map(Number);
|
|
||||||
const content = await image
|
|
||||||
.clone()
|
|
||||||
.extract({
|
|
||||||
left: sizes[0],
|
|
||||||
top: sizes[1],
|
|
||||||
width: sizes[2],
|
|
||||||
height: sizes[3],
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: `icons/equipment/${optionalDevice.id}.webp`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import sharp from "sharp";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
|
|
||||||
export async function flags() {
|
|
||||||
console.log("Building flags...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("flags");
|
|
||||||
const circleFiles = await vfs
|
|
||||||
.dir(`Data/Gfx/Lobby/flags`)
|
|
||||||
.then((files) =>
|
|
||||||
files.filter(
|
|
||||||
(flag) =>
|
|
||||||
flag.startsWith("flag_profile-stat_") &&
|
|
||||||
!flag.endsWith("@2x.packed.webp"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const scratchedFiles = await vfs
|
|
||||||
.dir(`Data/Gfx/Lobby/flags`)
|
|
||||||
.then((files) =>
|
|
||||||
files.filter(
|
|
||||||
(flag) =>
|
|
||||||
flag.startsWith("flag_tutor-tank_") &&
|
|
||||||
!flag.endsWith("@2x.packed.webp"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const fadedFiles = await vfs
|
|
||||||
.dir(`Data/Gfx/Lobby/flags`)
|
|
||||||
.then((files) =>
|
|
||||||
files.filter(
|
|
||||||
(flag) =>
|
|
||||||
flag.startsWith("flag_filter_") && flag.endsWith("@2x.packed.webp"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const flag of circleFiles) {
|
|
||||||
const image = sharp(await vfs.file(`Data/Gfx/Lobby/flags/${flag}`));
|
|
||||||
const content = await image.trim({ threshold: 100 }).toBuffer();
|
|
||||||
const name = flag.match(/flag_profile-stat_(.+)\.packed\.webp/)![1];
|
|
||||||
|
|
||||||
uploader.add({
|
|
||||||
content,
|
|
||||||
path: `flags/circle/${name}.webp`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const flag of scratchedFiles) {
|
|
||||||
const content = await vfs.file(`Data/Gfx/Lobby/flags/${flag}`);
|
|
||||||
const name = flag.match(/flag_tutor-tank_(.+)\.packed\.webp/)![1];
|
|
||||||
|
|
||||||
uploader.add({
|
|
||||||
path: `flags/scratched/${name}.webp`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const flag of fadedFiles) {
|
|
||||||
const content = await sharp(await vfs.file(`Data/Gfx/Lobby/flags/${flag}`))
|
|
||||||
.trim({
|
|
||||||
threshold: 100,
|
|
||||||
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
||||||
})
|
|
||||||
.toBuffer();
|
|
||||||
const name = flag.match(/flag_filter_(.+)@2x\.packed\.webp/)![1];
|
|
||||||
|
|
||||||
uploader.add({
|
|
||||||
content,
|
|
||||||
path: `flags/fade_small/${name}.webp`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import sharp from "sharp";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
import { SquadBattleTypeStyles } from "./definitions";
|
|
||||||
|
|
||||||
export async function gameModeBanners() {
|
|
||||||
console.log("Building game mode banners...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("game mode banners");
|
|
||||||
const gameTypeSelectorStyles = await vfs.yaml<SquadBattleTypeStyles>(
|
|
||||||
`Data/UI/Screens/Lobby/Hangar/GameTypeSelector.yaml`,
|
|
||||||
);
|
|
||||||
const squadBattleTypeStyles = await vfs.yaml<SquadBattleTypeStyles>(
|
|
||||||
`Data/UI/Screens3/Lobby/Hangar/Squad/SquadBattleType.yaml`,
|
|
||||||
);
|
|
||||||
const bannerMatches: { name: string; path: string }[] = [];
|
|
||||||
|
|
||||||
for (const match of gameTypeSelectorStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
|
|
||||||
/eGameMode\.([a-zA-Z]+) -> "~res:([^"]+)"/g,
|
|
||||||
)) {
|
|
||||||
bannerMatches.push({ name: match[1], path: match[2] });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const match of squadBattleTypeStyles.Prototypes[0].components.UIDataLocalBindingsComponent.data[1][2].matchAll(
|
|
||||||
/"(\d+)" -> "battleType\/([a-zA-Z]+)"/g,
|
|
||||||
)) {
|
|
||||||
const id = Number(match[1]);
|
|
||||||
const name = match[2];
|
|
||||||
let path = bannerMatches.find(
|
|
||||||
(bannerMatch) => bannerMatch.name.toLowerCase() === name.toLowerCase(),
|
|
||||||
)?.path;
|
|
||||||
|
|
||||||
if (path === undefined) {
|
|
||||||
path = `/Gfx/UI/Hangar/GameTypes/battle-type_${name.toLowerCase()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await sharp(await vfs.file(`Data${path}.packed.webp`))
|
|
||||||
.trim({ background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: `icons/game_mode_banners/${id}.webp`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { Avatar, fetchGlossary, Gallery } from "@blitzkit/core";
|
|
||||||
import locales from "@blitzkit/i18n/locales.json";
|
|
||||||
import { extname } from "path";
|
|
||||||
import ProgressBar from "progress";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
|
|
||||||
export async function glossary() {
|
|
||||||
console.log("Building glossary...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("glossary");
|
|
||||||
const avatars: Record<string, { avatar: Avatar; url: string }> = {};
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
locales.supported.map(async (supported) => {
|
|
||||||
const glossary = await fetchGlossary(supported.blitz ?? supported.locale);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Found ${Object.keys(glossary).length} things for ${supported.locale}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const key in glossary) {
|
|
||||||
const glossaryEntry = glossary[key];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!key.startsWith("avatar") ||
|
|
||||||
key.endsWith("_part") ||
|
|
||||||
glossaryEntry.image_url === null
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key in avatars) {
|
|
||||||
avatars[key].avatar.name!.locales[supported.locale] =
|
|
||||||
glossaryEntry.title;
|
|
||||||
} else {
|
|
||||||
const extension = extname(glossaryEntry.image_url);
|
|
||||||
|
|
||||||
avatars[key] = {
|
|
||||||
url: glossaryEntry.image_url,
|
|
||||||
avatar: {
|
|
||||||
id: key,
|
|
||||||
name: { locales: { [supported.locale]: glossaryEntry.title } },
|
|
||||||
extension,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const gallery: Gallery = { avatars: [] };
|
|
||||||
const totalCount = Object.keys(avatars).length;
|
|
||||||
|
|
||||||
const bar = new ProgressBar(
|
|
||||||
`Fetching avatars of ${totalCount} things :bar`,
|
|
||||||
totalCount,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Object.entries(avatars).map(async ([key, { url, avatar }]) => {
|
|
||||||
if (!(locales.default in avatars[key].avatar.name!.locales)) {
|
|
||||||
console.warn(`Avatar ${key} has no ${locales.default} name`);
|
|
||||||
bar.tick();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gallery.avatars.push(avatar);
|
|
||||||
|
|
||||||
const content = await fetch(url)
|
|
||||||
.then((response) => response.arrayBuffer())
|
|
||||||
.then((buffer) => new Uint8Array(buffer));
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: `gallery/avatars/${key}${avatar.extension}`,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
|
|
||||||
bar.tick();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: "definitions/gallery.pb",
|
|
||||||
content: Gallery.encode(gallery).finish(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { config } from "dotenv";
|
|
||||||
import { argv } from "process";
|
|
||||||
import { boosterIcons } from "./boosterIcons";
|
|
||||||
import { camouflageIcons } from "./camouflageIcons";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
import { consumableProvisionIcons } from "./consumableProvisionIcons";
|
|
||||||
import { currencies } from "./currencies";
|
|
||||||
import { definitions } from "./definitions";
|
|
||||||
import { equipmentIcons } from "./equipmentIcons";
|
|
||||||
import { flags } from "./flags";
|
|
||||||
import { gameModeBanners } from "./gameModeBanners";
|
|
||||||
import { glossary } from "./glossary";
|
|
||||||
import { migration } from "./migration";
|
|
||||||
import { moduleIcons } from "./moduleIcons";
|
|
||||||
import { popularTanks } from "./popularTanks";
|
|
||||||
import { shellIcons } from "./shellIcons";
|
|
||||||
import { skillIcons } from "./skillIcons";
|
|
||||||
import { tankArmors } from "./tankArmors";
|
|
||||||
import { tankIcons } from "./tankIcons";
|
|
||||||
import { tankModels } from "./tankModels";
|
|
||||||
|
|
||||||
config();
|
|
||||||
|
|
||||||
const allTargets = argv.includes("--all-targets");
|
|
||||||
const targets = argv
|
|
||||||
.find((argument) => argument.startsWith("--target"))
|
|
||||||
?.split("=")[1]
|
|
||||||
.split(",");
|
|
||||||
|
|
||||||
if (!targets && !allTargets) throw new Error("No target(s) specified");
|
|
||||||
|
|
||||||
const methods = [
|
|
||||||
definitions,
|
|
||||||
tankIcons,
|
|
||||||
tankModels,
|
|
||||||
tankArmors,
|
|
||||||
gameModeBanners,
|
|
||||||
currencies,
|
|
||||||
skillIcons,
|
|
||||||
flags,
|
|
||||||
shellIcons,
|
|
||||||
moduleIcons,
|
|
||||||
equipmentIcons,
|
|
||||||
consumableProvisionIcons,
|
|
||||||
camouflageIcons,
|
|
||||||
boosterIcons,
|
|
||||||
glossary,
|
|
||||||
migration,
|
|
||||||
popularTanks,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const method of methods) {
|
|
||||||
if (allTargets || targets?.includes(method.name)) {
|
|
||||||
try {
|
|
||||||
await method();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed method ${method.name}; skipping...`);
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vfs.dispose();
|
|
||||||
process.exit(0);
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { toUniqueId } from "@blitzkit/core";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
import { VehicleDefinitionList } from "./definitions";
|
|
||||||
|
|
||||||
export async function migration() {
|
|
||||||
console.log("Building migration map...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("migration map");
|
|
||||||
const map: Record<string, number> = {};
|
|
||||||
const nations = await vfs
|
|
||||||
.dir(`Data/XML/item_defs/vehicles`)
|
|
||||||
.then((files) => files.filter((nation) => nation !== "common"));
|
|
||||||
|
|
||||||
for (const nation of nations) {
|
|
||||||
const tankList = await vfs.xml<{ root: VehicleDefinitionList }>(
|
|
||||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const tankKey in tankList.root) {
|
|
||||||
const tank = tankList.root[tankKey];
|
|
||||||
const tankId = toUniqueId(nation, tank.id);
|
|
||||||
const davaKey = `${nation}:${tankKey}`;
|
|
||||||
|
|
||||||
map[davaKey] = tankId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: "definitions/migration.json",
|
|
||||||
content: new TextEncoder().encode(JSON.stringify(map)),
|
|
||||||
});
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import sharp from "sharp";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
|
|
||||||
export async function moduleIcons() {
|
|
||||||
console.log("Building module icons...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("module icons");
|
|
||||||
const files = await vfs
|
|
||||||
.dir(`Data/Gfx/UI/ModulesTechTree`)
|
|
||||||
.then((files) =>
|
|
||||||
files.filter(
|
|
||||||
(file) =>
|
|
||||||
!file.endsWith("@2x.packed.webp") && file.startsWith("vehicle"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const nameRaw = file.match(/vehicle(.+)\.packed\.webp/)![1];
|
|
||||||
const name = nameRaw[0].toLowerCase() + nameRaw.slice(1);
|
|
||||||
const content = await sharp(
|
|
||||||
await vfs.file(`Data/Gfx/UI/ModulesTechTree/${file}`),
|
|
||||||
)
|
|
||||||
.trim()
|
|
||||||
.toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
content,
|
|
||||||
path: `icons/modules/${name}.webp`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import {
|
|
||||||
assertSecret,
|
|
||||||
fetchTankDefinitions,
|
|
||||||
PopularTanks,
|
|
||||||
} from "@blitzkit/core";
|
|
||||||
import { google } from "googleapis";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
|
|
||||||
export async function popularTanks() {
|
|
||||||
using uploader = new AssetUploader("popular tanks");
|
|
||||||
const tankDefinitions = await fetchTankDefinitions();
|
|
||||||
|
|
||||||
const auth = await google.auth.getClient({
|
|
||||||
keyFile: import.meta.env.GOOGLE_APPLICATION_CREDENTIALS,
|
|
||||||
scopes: ["https://www.googleapis.com/auth/analytics.readonly"],
|
|
||||||
});
|
|
||||||
const analytics = google.analyticsdata({ version: "v1beta", auth });
|
|
||||||
const report = await analytics.properties.runReport({
|
|
||||||
property: `properties/${assertSecret(
|
|
||||||
import.meta.env.PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID,
|
|
||||||
)}`,
|
|
||||||
requestBody: {
|
|
||||||
dateRanges: [{ startDate: "7daysAgo", endDate: "today" }],
|
|
||||||
dimensions: [{ name: "pagePath" }],
|
|
||||||
metrics: [{ name: "screenPageViews" }],
|
|
||||||
orderBys: [{ metric: { metricName: "screenPageViews" }, desc: true }],
|
|
||||||
dimensionFilter: {
|
|
||||||
filter: {
|
|
||||||
fieldName: "pagePath",
|
|
||||||
stringFilter: {
|
|
||||||
matchType: "BEGINS_WITH",
|
|
||||||
value: "/tanks/",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!report.data.rows) {
|
|
||||||
throw new Error("No rows in report");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tanks = report.data.rows
|
|
||||||
.filter(
|
|
||||||
(row) =>
|
|
||||||
row.dimensionValues &&
|
|
||||||
row.dimensionValues[0].value &&
|
|
||||||
row.dimensionValues[0].value !== "/tanks/" &&
|
|
||||||
row.metricValues &&
|
|
||||||
row.metricValues[0].value,
|
|
||||||
)
|
|
||||||
.map((row) => ({
|
|
||||||
id: Number(
|
|
||||||
row.dimensionValues![0].value!.match(
|
|
||||||
/\/tools\/tankopedia\/(\d+)\/?/,
|
|
||||||
)?.[1],
|
|
||||||
),
|
|
||||||
views: Number(row.metricValues![0].value!),
|
|
||||||
}))
|
|
||||||
.filter((row) => {
|
|
||||||
if (!(row.id in tankDefinitions.tanks)) {
|
|
||||||
console.warn("Skipping unknown popular tank", row.id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return row.id in tankDefinitions.tanks;
|
|
||||||
})
|
|
||||||
.slice(0, 8)
|
|
||||||
.map(({ id, views }) => ({ id, views }));
|
|
||||||
const popularTanks: PopularTanks = { tanks };
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
path: "definitions/popular-tanks.pb",
|
|
||||||
content: PopularTanks.encode(popularTanks).finish(),
|
|
||||||
});
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import sharp from "sharp";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
|
|
||||||
export async function shellIcons() {
|
|
||||||
console.log("Building shell icons...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("shell icons");
|
|
||||||
const image = sharp(
|
|
||||||
await vfs.file(
|
|
||||||
`Data/Gfx/Shared/tank-supply/ammunition/big/texture0.packed.webp`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const files = await vfs
|
|
||||||
.dir(`Data/Gfx/Shared/tank-supply/ammunition/big`)
|
|
||||||
.then((files) => files.filter((file) => file.endsWith("_l.txt")));
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const name = file.match(/(.+)_l\.txt/)![1];
|
|
||||||
const sizes = (
|
|
||||||
await vfs.text(`Data/Gfx/Shared/tank-supply/ammunition/big/${file}`)
|
|
||||||
)
|
|
||||||
.split("\n")[4]
|
|
||||||
.split(" ")
|
|
||||||
.map(Number);
|
|
||||||
|
|
||||||
await uploader.add({
|
|
||||||
content: await image
|
|
||||||
.clone()
|
|
||||||
.extract({
|
|
||||||
left: sizes[0],
|
|
||||||
top: sizes[1],
|
|
||||||
width: sizes[2],
|
|
||||||
height: sizes[3],
|
|
||||||
})
|
|
||||||
.toBuffer(),
|
|
||||||
path: `icons/shells/${name}.webp`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { TankClass } from "@blitzkit/core";
|
|
||||||
import sharp from "sharp";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
|
|
||||||
interface SkillIcon {
|
|
||||||
name: string;
|
|
||||||
state: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Avatar {
|
|
||||||
roles: unknown;
|
|
||||||
skillsByClasses: Record<TankClass, string>;
|
|
||||||
skills: {
|
|
||||||
[name: string]: {
|
|
||||||
userString: string;
|
|
||||||
effectDescription: string;
|
|
||||||
tipDescription: string;
|
|
||||||
icon: SkillIcon | SkillIcon[];
|
|
||||||
type: "continuous" | "trigger";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function skillIcons() {
|
|
||||||
console.log("Building skill icons...");
|
|
||||||
|
|
||||||
const avatar = await vfs.xml<{ root: Avatar }>(
|
|
||||||
`Data/XML/item_defs/tankmen/avatar.xml`,
|
|
||||||
);
|
|
||||||
const uploader = new AssetUploader("skill icons");
|
|
||||||
|
|
||||||
for (const key in avatar.root.skills) {
|
|
||||||
const skill = avatar.root.skills[key];
|
|
||||||
const icon = Array.isArray(skill.icon) ? skill.icon[0] : skill.icon;
|
|
||||||
const name = icon.name.split("/").at(-1)!.replace(/_\d$/, "");
|
|
||||||
const path = `Data${icon.name.replace("~res:", "")}.packed.webp`;
|
|
||||||
const image = sharp(await vfs.file(path)).trim();
|
|
||||||
const content = await image.toBuffer();
|
|
||||||
|
|
||||||
await uploader.add({ content, path: `icons/skills/${name}.webp` });
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { toUniqueId } from "@blitzkit/core";
|
|
||||||
import { NodeIO } from "@gltf-transform/core";
|
|
||||||
import { extractArmor } from "../core/blitz/extractArmor";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
import { VehicleDefinitionList } from "./definitions";
|
|
||||||
|
|
||||||
export async function tankArmors() {
|
|
||||||
console.log("Building tank armors...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("tank armors");
|
|
||||||
const nodeIO = new NodeIO();
|
|
||||||
const nations = await vfs
|
|
||||||
.dir(`Data/XML/item_defs/vehicles`)
|
|
||||||
.then((files) => files.filter((nation) => nation !== "common"));
|
|
||||||
|
|
||||||
for (const nation of nations) {
|
|
||||||
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
|
|
||||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const tankKey in tanks.root) {
|
|
||||||
const tank = tanks.root[tankKey];
|
|
||||||
|
|
||||||
if (tankKey.includes("tutorial_bot")) continue;
|
|
||||||
|
|
||||||
const id = toUniqueId(nation, tank.id);
|
|
||||||
const model = await extractArmor(vfs, `${nation}-${tankKey}`);
|
|
||||||
const content = await nodeIO.writeBinary(model);
|
|
||||||
|
|
||||||
await uploader.add({ path: `3d/tanks/armor/${id}.glb`, content });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { NATION_IDS } from "@blitzkit/core";
|
|
||||||
import sharp from "sharp";
|
|
||||||
import type { Vector3Tuple } from "three";
|
|
||||||
import { parsePackedSpriteRect } from "../core/blitz/parsePackedSpriteRect";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
import type { VehicleDefinitionList } from "./definitions";
|
|
||||||
|
|
||||||
export interface TankParameters {
|
|
||||||
resourcesPath: {
|
|
||||||
smallIconPath: string;
|
|
||||||
bigIconPath: string;
|
|
||||||
blitzModelPath: string;
|
|
||||||
};
|
|
||||||
collision: {
|
|
||||||
[key: string]: {
|
|
||||||
points: Vector3Tuple;
|
|
||||||
bbox: {
|
|
||||||
min: Vector3Tuple;
|
|
||||||
max: Vector3Tuple;
|
|
||||||
};
|
|
||||||
averageThickness?: Record<string, number>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
maskSlice?: {
|
|
||||||
[key: string]:
|
|
||||||
| {
|
|
||||||
enabled: boolean;
|
|
||||||
planePosition: Vector3Tuple;
|
|
||||||
planeNormal: Vector3Tuple;
|
|
||||||
planeAxis: Vector3Tuple;
|
|
||||||
frustumConeBaseRadius: number;
|
|
||||||
frustumConeAngleDegrees: number;
|
|
||||||
planeSliceMode: number;
|
|
||||||
frustumConeSliceMode: number;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function extractPackedTankIcon(packedBuffer: Buffer, context: string) {
|
|
||||||
const texture = sharp(packedBuffer);
|
|
||||||
const spriteRect = parsePackedSpriteRect(packedBuffer);
|
|
||||||
|
|
||||||
if (!spriteRect) {
|
|
||||||
throw new Error(`Missing RIFF sprite bounds for ${context}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [left, top, width, height] = spriteRect;
|
|
||||||
const bounds = [left, top, width, height];
|
|
||||||
|
|
||||||
if (!bounds.every(Number.isInteger)) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid RIFF sprite bounds for ${context}: ${bounds.join(" ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = await texture.metadata();
|
|
||||||
|
|
||||||
if (metadata.width === undefined || metadata.height === undefined) {
|
|
||||||
throw new Error(`Failed to read image dimensions for ${context}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
left < 0 ||
|
|
||||||
top < 0 ||
|
|
||||||
width <= 0 ||
|
|
||||||
height <= 0 ||
|
|
||||||
left + width > metadata.width ||
|
|
||||||
top + height > metadata.height
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Out-of-bounds RIFF sprite for ${context}: ${bounds.join(" ")} in ${metadata.width}x${metadata.height}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await texture.extract({ left, top, width, height }).toBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function tankIcons() {
|
|
||||||
console.log("Building tank icons...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("tank icons");
|
|
||||||
const nations = await vfs
|
|
||||||
.dir(`Data/XML/item_defs/vehicles`)
|
|
||||||
.then((files) => files.filter((nation) => nation !== "common"));
|
|
||||||
|
|
||||||
for (const nation of nations) {
|
|
||||||
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
|
|
||||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const tankKey in tanks.root) {
|
|
||||||
const tank = tanks.root[tankKey];
|
|
||||||
|
|
||||||
if (tankKey.includes("tutorial_bot")) continue;
|
|
||||||
|
|
||||||
const nationVehicleId = tank.id;
|
|
||||||
const id = (nationVehicleId << 8) + (NATION_IDS[nation] << 4) + 1;
|
|
||||||
|
|
||||||
const parameters = await vfs.yaml<TankParameters>(
|
|
||||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
|
||||||
);
|
|
||||||
const smallPath = `Data/${parameters.resourcesPath.smallIconPath
|
|
||||||
.replace(/~res:\//, "")
|
|
||||||
.replace(/\..+/, "")}.packed.webp`;
|
|
||||||
const bigPath = `Data/${parameters.resourcesPath.bigIconPath
|
|
||||||
.replace(/~res:\//, "")
|
|
||||||
.replace(/\..+/, "")}.packed.webp`;
|
|
||||||
const big = await extractPackedTankIcon(
|
|
||||||
await vfs.file(bigPath),
|
|
||||||
`tank ${tankKey} big`,
|
|
||||||
);
|
|
||||||
const small = await extractPackedTankIcon(
|
|
||||||
await vfs.file(smallPath),
|
|
||||||
`tank ${tankKey} small`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (big) {
|
|
||||||
await uploader.add({
|
|
||||||
content: big,
|
|
||||||
path: `icons/tanks/big/${id}.webp`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (small) {
|
|
||||||
await uploader.add({
|
|
||||||
content: small,
|
|
||||||
path: `icons/tanks/small/${id}.webp`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { toUniqueId } from "@blitzkit/core";
|
|
||||||
import { NodeIO } from "@gltf-transform/core";
|
|
||||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
|
||||||
import { extractModel } from "../core/blitz/extractModel";
|
|
||||||
import { AssetUploader } from "../core/github/assetUploader";
|
|
||||||
import { vfs } from "./constants";
|
|
||||||
import { VehicleDefinitionList } from "./definitions";
|
|
||||||
import { TankParameters } from "./tankIcons";
|
|
||||||
|
|
||||||
export async function tankModels() {
|
|
||||||
console.log("Building tank models...");
|
|
||||||
|
|
||||||
using uploader = new AssetUploader("tank models");
|
|
||||||
const nodeIO = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
|
||||||
const nations = await vfs
|
|
||||||
.dir(`Data/XML/item_defs/vehicles`)
|
|
||||||
.then((files) => files.filter((nation) => nation !== "common"));
|
|
||||||
|
|
||||||
for (const nationIndex in nations) {
|
|
||||||
const nation = nations[nationIndex];
|
|
||||||
const tanks = await vfs.xml<{ root: VehicleDefinitionList }>(
|
|
||||||
`Data/XML/item_defs/vehicles/${nation}/list.xml`,
|
|
||||||
);
|
|
||||||
const entries = Object.entries(tanks.root);
|
|
||||||
|
|
||||||
for (const [tankKey, tank] of entries) {
|
|
||||||
if (tankKey.includes("tutorial_bot")) continue;
|
|
||||||
|
|
||||||
const id = toUniqueId(nation, tank.id);
|
|
||||||
const parameters = await vfs.yaml<TankParameters>(
|
|
||||||
`Data/3d/Tanks/Parameters/${nation}/${tankKey}.yaml`,
|
|
||||||
);
|
|
||||||
const model = await extractModel(
|
|
||||||
vfs,
|
|
||||||
parameters.resourcesPath.blitzModelPath.replace(/\.sc2$/, ""),
|
|
||||||
);
|
|
||||||
const content = Buffer.from(await nodeIO.writeBinary(model));
|
|
||||||
|
|
||||||
await uploader.add({ path: `3d/tanks/models/${id}.glb`, content });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await uploader.flush();
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import { assertSecret, asset } from "@blitzkit/core";
|
|
||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
import ProgressBar from "progress";
|
|
||||||
import { compareUint8Arrays } from "./compareUint8Arrays";
|
|
||||||
|
|
||||||
export interface FileChange {
|
|
||||||
path: string;
|
|
||||||
content: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GithubChangeBlob {
|
|
||||||
sha: string;
|
|
||||||
path: string;
|
|
||||||
mode: "100644";
|
|
||||||
type: "blob";
|
|
||||||
}
|
|
||||||
|
|
||||||
enum DiffStatus {
|
|
||||||
New,
|
|
||||||
Changed,
|
|
||||||
Unchanged,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AssetUploader {
|
|
||||||
time_per_blob = 2 ** 4 * 1000;
|
|
||||||
time_between_blobs = (60 * 60 * 1000) / 5000 / 0.9;
|
|
||||||
|
|
||||||
max_tree_size = 7_000_000;
|
|
||||||
max_file_count = 128;
|
|
||||||
|
|
||||||
heuristicFormats = ["glb", "webp", "jpg", "png"];
|
|
||||||
|
|
||||||
owner: string;
|
|
||||||
repo: string;
|
|
||||||
branch: string;
|
|
||||||
|
|
||||||
changes: FileChange[] = [];
|
|
||||||
changesSize = 0;
|
|
||||||
|
|
||||||
private octokit = new Octokit({
|
|
||||||
auth: assertSecret(import.meta.env.GH_TOKEN),
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(public message: string) {
|
|
||||||
[this.owner, this.repo] = assertSecret(
|
|
||||||
import.meta.env.PUBLIC_ASSET_REPO,
|
|
||||||
).split("/");
|
|
||||||
this.branch = assertSecret(import.meta.env.PUBLIC_ASSET_BRANCH);
|
|
||||||
}
|
|
||||||
|
|
||||||
async add(change: FileChange) {
|
|
||||||
const response = await fetch(
|
|
||||||
asset(change.path),
|
|
||||||
{ headers: { "Accept-Encoding": "identity" } },
|
|
||||||
);
|
|
||||||
let diff: { change: number; status: DiffStatus };
|
|
||||||
|
|
||||||
if (response.status === 404) {
|
|
||||||
diff = { change: change.content.length, status: DiffStatus.New };
|
|
||||||
} else if (
|
|
||||||
response.headers.has("Content-Length") &&
|
|
||||||
Number(response.headers.get("Content-Length")) !== change.content.length
|
|
||||||
) {
|
|
||||||
diff = {
|
|
||||||
change:
|
|
||||||
change.content.length -
|
|
||||||
Number(response.headers.get("Content-Length")),
|
|
||||||
status: DiffStatus.Changed,
|
|
||||||
};
|
|
||||||
} else if (response.status === 200) {
|
|
||||||
if (
|
|
||||||
this.heuristicFormats.some((format) =>
|
|
||||||
change.path.endsWith(`.${format}`),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* Heuristic: if it's a large blob like a .glb, a diff = 0 might end
|
|
||||||
* up being a false positive with isDifferent = true. So, it's okay
|
|
||||||
* to assume it's unchanged.
|
|
||||||
*/
|
|
||||||
diff = { change: 0, status: DiffStatus.Unchanged };
|
|
||||||
} else {
|
|
||||||
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
||||||
|
|
||||||
if (compareUint8Arrays(buffer, change.content)) {
|
|
||||||
diff = { change: 0, status: DiffStatus.Unchanged };
|
|
||||||
} else {
|
|
||||||
diff = {
|
|
||||||
change: change.content.length - buffer.length,
|
|
||||||
status: DiffStatus.Changed,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`Unexpected status code ${response.status} for ${change.path}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diff.status === DiffStatus.Unchanged) {
|
|
||||||
// console.log(`🔵 ${change.path}`);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`${diff.status === DiffStatus.New ? "🟢" : "🟡"} (${
|
|
||||||
diff.change > 0 ? "+" : ""
|
|
||||||
}${diff.change.toLocaleString()}B) ${change.path}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// flush BEFORE going over limit
|
|
||||||
if (
|
|
||||||
this.changesSize + change.content.length > this.max_tree_size ||
|
|
||||||
this.changes.length + 1 > this.max_file_count
|
|
||||||
) {
|
|
||||||
console.log(`Flushing ${this.changes.length} changes...`);
|
|
||||||
await this.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.changes.push(change);
|
|
||||||
this.changesSize += change.content.length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async flush() {
|
|
||||||
if (this.changes.length == 0) return;
|
|
||||||
|
|
||||||
const latestCommitSha = (
|
|
||||||
await this.octokit.git.getRef({
|
|
||||||
owner: this.owner,
|
|
||||||
repo: this.repo,
|
|
||||||
ref: `heads/${this.branch}`,
|
|
||||||
})
|
|
||||||
).data.object.sha;
|
|
||||||
const treeSha = (
|
|
||||||
await this.octokit.git.getCommit({
|
|
||||||
owner: this.owner,
|
|
||||||
repo: this.repo,
|
|
||||||
commit_sha: latestCommitSha,
|
|
||||||
})
|
|
||||||
).data.tree.sha;
|
|
||||||
const blobs: GithubChangeBlob[] = [];
|
|
||||||
const bar = new ProgressBar("Blobs :bar", this.changes.length);
|
|
||||||
|
|
||||||
for (const change of this.changes) {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const start = Date.now();
|
|
||||||
var { sha } = await this.octokit.git
|
|
||||||
.createBlob({
|
|
||||||
owner: this.owner,
|
|
||||||
repo: this.repo,
|
|
||||||
encoding: "base64",
|
|
||||||
content: Buffer.from(change.content).toString("base64"),
|
|
||||||
})
|
|
||||||
.then((response) => response.data);
|
|
||||||
const end = Date.now();
|
|
||||||
|
|
||||||
console.log(` blobbed ${change.path} ${end - start}ms`);
|
|
||||||
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(
|
|
||||||
resolve,
|
|
||||||
Math.max(0, this.time_between_blobs + (start - end)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
blobs.push({ sha, path: change.path, mode: "100644", type: "blob" });
|
|
||||||
bar.tick();
|
|
||||||
|
|
||||||
break;
|
|
||||||
} catch {
|
|
||||||
console.warn(
|
|
||||||
` failed ${change.path}; retrying in ${this.time_per_blob}ms...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, this.time_per_blob),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: treeData } = await this.octokit.git.createTree({
|
|
||||||
owner: this.owner,
|
|
||||||
repo: this.repo,
|
|
||||||
base_tree: treeSha,
|
|
||||||
tree: blobs,
|
|
||||||
});
|
|
||||||
const { data: commitData } = await this.octokit.git.createCommit({
|
|
||||||
owner: this.owner,
|
|
||||||
repo: this.repo,
|
|
||||||
message: this.message,
|
|
||||||
tree: treeData.sha,
|
|
||||||
parents: [latestCommitSha],
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.octokit.git.updateRef({
|
|
||||||
owner: this.owner,
|
|
||||||
repo: this.repo,
|
|
||||||
ref: `heads/${this.branch}`,
|
|
||||||
sha: commitData.sha,
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.changes = [];
|
|
||||||
this.changesSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.dispose]() {
|
|
||||||
if (this.changes.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
`${this.changes.length} uncommitted changes. Did you forget to flush?`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export function compareUint8Arrays(a: Uint8Array, b: Uint8Array) {
|
|
||||||
if (a.length !== b.length) return false;
|
|
||||||
|
|
||||||
for (let index = 0; index < a.length; index++) {
|
|
||||||
if (a[index] !== b[index]) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { throttle } from "lodash-es";
|
|
||||||
import { FileChange } from "./assetUploader";
|
|
||||||
import { octokit } from "./octokit";
|
|
||||||
|
|
||||||
export interface GithubChangeBlob {
|
|
||||||
sha: string;
|
|
||||||
path: string;
|
|
||||||
mode: "100644";
|
|
||||||
type: "blob";
|
|
||||||
}
|
|
||||||
|
|
||||||
const TIME_PER_BLOB = 2 ** 4 * 1000;
|
|
||||||
|
|
||||||
// github rate limit: 5000 requests per hour
|
|
||||||
export const createBlob = throttle(
|
|
||||||
async (owner: string, repo: string, change: FileChange) => {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const { sha } = (
|
|
||||||
await octokit.git.createBlob({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
content: Buffer.from(change.content).toString("base64"),
|
|
||||||
encoding: "base64",
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
|
|
||||||
return {
|
|
||||||
sha,
|
|
||||||
path: change.path,
|
|
||||||
mode: "100644",
|
|
||||||
type: "blob",
|
|
||||||
} satisfies GithubChangeBlob;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`Failed blob ${change.path}; retrying in ${TIME_PER_BLOB}ms...`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, TIME_PER_BLOB));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(5000 / (60 * 60 * 1000)) * 0.9,
|
|
||||||
{ trailing: true }
|
|
||||||
);
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import {
|
|
||||||
asset,
|
|
||||||
DidsReadStream,
|
|
||||||
DiscoveredIdsDefinitions,
|
|
||||||
} from '@blitzkit/core';
|
|
||||||
import { times } from 'lodash-es';
|
|
||||||
import { decompress } from 'lz4js';
|
|
||||||
import ProgressBar from 'progress';
|
|
||||||
|
|
||||||
export async function fetchPreDiscoveredIds() {
|
|
||||||
const idChunks: number[][] = [];
|
|
||||||
const manifest = (await fetch(asset('ids/manifest.json')).then((response) =>
|
|
||||||
response.json(),
|
|
||||||
)) as DiscoveredIdsDefinitions;
|
|
||||||
const bar = new ProgressBar(
|
|
||||||
`Fetching ${manifest.count.toLocaleString()} ids in ${
|
|
||||||
manifest.chunks
|
|
||||||
} chunks :bar`,
|
|
||||||
manifest.chunks,
|
|
||||||
);
|
|
||||||
|
|
||||||
bar.render();
|
|
||||||
await Promise.all(
|
|
||||||
times(manifest.chunks, async (chunkIndex) => {
|
|
||||||
const preDiscovered = await fetch(
|
|
||||||
asset(`ids/${chunkIndex}.dids.lz4`),
|
|
||||||
).then(async (response) => {
|
|
||||||
const buffer = await response.arrayBuffer();
|
|
||||||
const decompressed = decompress(new Uint8Array(buffer)).buffer;
|
|
||||||
return new DidsReadStream(decompressed as ArrayBuffer).dids();
|
|
||||||
});
|
|
||||||
|
|
||||||
idChunks.push(preDiscovered);
|
|
||||||
bar.tick();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return idChunks.sort((a, b) => a[0] - b[0]).flat();
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { assertSecret } from "@blitzkit/core";
|
|
||||||
import { throttling } from "@octokit/plugin-throttling";
|
|
||||||
import { Octokit } from "@octokit/rest";
|
|
||||||
|
|
||||||
Octokit.plugin(throttling);
|
|
||||||
|
|
||||||
export const octokit = new Octokit({
|
|
||||||
auth: assertSecret(import.meta.env.GH_TOKEN),
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { assertSecret } from "@blitzkit/core";
|
import { secret } from "@blitzkit/core";
|
||||||
import { GameInterface } from "@blitzkit/game";
|
import { GameInterface } from "@blitzkit/game";
|
||||||
|
|
||||||
interface Bindings {
|
interface Bindings {
|
||||||
@@ -50,10 +50,10 @@ interface Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const game = new GameInterface(
|
const game = new GameInterface(
|
||||||
assertSecret(import.meta.env.PUBLIC_WOTB_CLIENT),
|
secret("PUBLIC_WOTB_CLIENT"),
|
||||||
assertSecret(import.meta.env.WOTB_USMAP),
|
secret("WOTB_USMAP"),
|
||||||
assertSecret(import.meta.env.TEMP),
|
secret("TEMP"),
|
||||||
Number(assertSecret(import.meta.env.TEXTURE_CHUNKS)),
|
Number(secret("TEXTURE_CHUNKS")),
|
||||||
);
|
);
|
||||||
|
|
||||||
game.discoveryUrls().forEach(async (url) => {
|
game.discoveryUrls().forEach(async (url) => {
|
||||||
|
|||||||
Reference in new issue
Block a user