This commit is contained in:
André Bastos Dias 2026-06-02 14:06:27 +01:00 committed by GitHub
commit b30cb98ee9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 36 deletions

17
package-lock.json generated
View File

@ -31,7 +31,7 @@
"eslint": "^9.31.0",
"globals": "^16.0.0",
"mocha": "^11.7.1",
"smogon": "^3.0.0",
"smogon": "^4.0.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0"
},
@ -4117,10 +4117,11 @@
}
},
"node_modules/smogon": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/smogon/-/smogon-3.0.0.tgz",
"integrity": "sha512-F+Yo6yXqidabdvMqyI6rrXKejBMl0OOmgGyk5dctqeJx9fFmFUnK3bzkdUG/eaXX0S8m4hUb+WqieiHlAiQgUQ==",
"dev": true
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/smogon/-/smogon-4.0.3.tgz",
"integrity": "sha512-TxNoqKX7jv4wxhQApFkhdfNv/jP2WSWPQoGmCjkR7FpYnV+yCj5QQ4xfaim0zQ78JoX6UYa9y6StDb/VIHuocw==",
"dev": true,
"license": "MIT"
},
"node_modules/sockjs": {
"version": "0.3.24",
@ -7443,9 +7444,9 @@
"optional": true
},
"smogon": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/smogon/-/smogon-3.0.0.tgz",
"integrity": "sha512-F+Yo6yXqidabdvMqyI6rrXKejBMl0OOmgGyk5dctqeJx9fFmFUnK3bzkdUG/eaXX0S8m4hUb+WqieiHlAiQgUQ==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/smogon/-/smogon-4.0.3.tgz",
"integrity": "sha512-TxNoqKX7jv4wxhQApFkhdfNv/jP2WSWPQoGmCjkR7FpYnV+yCj5QQ4xfaim0zQ78JoX6UYa9y6StDb/VIHuocw==",
"dev": true
},
"sockjs": {

View File

@ -77,7 +77,7 @@
"eslint": "^9.31.0",
"globals": "^16.0.0",
"mocha": "^11.7.1",
"smogon": "^3.0.0",
"smogon": "^4.0.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0"
}

View File

@ -1,6 +1,5 @@
import * as http from 'http';
import * as https from 'https';
import * as url from 'url';
import * as util from 'util';
import * as smogon from 'smogon';
@ -10,6 +9,12 @@ import { Dex, toID } from '../../sim/dex';
import { TeamValidator } from '../../sim/team-validator';
Dex.includeModData();
interface FetchOptions {
url: string;
options?: http.RequestOptions;
body?: string;
}
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends (infer I)[] ? (DeepPartial<I>)[] : DeepPartial<T[P]>;
};
@ -20,11 +25,6 @@ interface PokemonSets {
};
}
interface IncomingMessage extends NodeJS.ReadableStream {
statusCode: number;
headers: { location?: string };
}
// eg. 'gen1.json'
interface GenerationData {
[formatid: string]: FormatData;
@ -245,7 +245,7 @@ function movesetToPokemonSet(dex: ModdedDex, format: Format, pokemon: string, se
nature: set.natures[0],
teraType: set.teratypes ? set.teratypes[0] : undefined,
ivs: toStatsTable(set.ivconfigs[0], 31),
evs: toStatsTable(set.evconfigs[0]),
evs: toStatsTable(set.evconfigs[0], dex.gen <= 2 ? 252 : 0),
};
}
@ -358,7 +358,7 @@ function toPokemonSet(
const copy = { species: pokemon, ...set } as PokemonSet;
copy.ivs = fillStats(set.ivs, fill);
// The validator expects us to have at least 1 EV set to prove it is intentional
if (!set.evs && dex.gen >= 3 && format.id !== 'gen7letsgoou') set.evs = { spe: 1 };
if (!set.evs && dex.gen >= 3 && format.mod !== 'gen7letsgo') set.evs = { spe: 1 };
copy.evs = fillStats(set.evs, dex.gen <= 2 ? 252 : 0);
// The validator wants an ability even when Gen < 3
copy.ability = copy.ability || 'None';
@ -405,9 +405,9 @@ const SMOGON = {
bssseries2: 'battlestadiumsinglesseries2',
} as unknown as { [id: string]: ID };
const getAnalysis = retrying(async (u: string) => {
const getAnalysis = retrying(async (o: FetchOptions) => {
try {
return smogon.Analyses.process(await request(u));
return smogon.Analyses.process(JSON.parse(await request(o)));
} catch (err: any) {
// Don't try HTTP errors that we've already retried
if (err.message.startsWith('HTTP')) {
@ -419,16 +419,16 @@ const getAnalysis = retrying(async (u: string) => {
}, 3, 50);
async function getAnalysesByFormat(pokemon: string, gen: GenerationNum) {
const u = smogon.Analyses.url(pokemon === 'Meowstic' ? 'Meowstic-M' : pokemon, gen);
const r = smogon.Analyses.request(pokemon === 'Meowstic' ? 'Meowstic-M' : pokemon, gen);
try {
const analysesByTier = await getAnalysis(u);
const analysesByTier = await getAnalysis({ url: r.url, options: r.init, body: r.init.body });
if (!analysesByTier) {
error(`Unable to process analysis for ${pokemon} in generation ${gen}`);
return undefined;
}
const analysesByFormat = new Map<Format, smogon.Analysis[]>();
for (const [tier, analyses] of analysesByTier.entries()) {
for (const [tier, analyses] of analysesByTier.analyses.entries()) {
let t = toID(tier);
// Dumb hack, need to talk to BSS people
if (gen === 9 && t === 'battlestadiumsingles') {
@ -477,7 +477,7 @@ function importUsageBasedSets(gen: GenerationNum, format: Format, statistics: sm
level: getLevel(format),
moves: (top(stats.Moves, 4) as string[]).map(m => dex.moves.get(m).name).filter(m => m),
};
if (gen >= 2 && format.id !== 'gen7letsgoou') {
if (gen >= 2 && format.mod !== 'gen7letsgo') {
const id = top(stats.Items) as string;
set.item = dex.items.get(id).name;
if (set.item === 'nothing') set.item = undefined;
@ -487,7 +487,7 @@ function importUsageBasedSets(gen: GenerationNum, format: Format, statistics: sm
set.ability = fixedAbility(dex, pokemon, dex.abilities.get(id).name);
const { nature, evs } = fromSpread(top(stats.Spreads) as string);
set.nature = nature;
if (format.id !== 'gen7letsgoou') {
if (format.mod !== 'gen7letsgo') {
if (!evs || !Object.keys(evs).length) continue;
set.evs = evs;
}
@ -555,25 +555,34 @@ class RetryableError extends Error {
// requests makes us significantly less likely to encounter ECONNRESET errors
// on macOS (though these are still pretty frequent, Linux is recommended for running
// this tool). Retry up to 5 times with a 20ms backoff increment.
const request = retrying(throttling(fetch, 1, 50), 5, 20);
export const request = retrying(throttling(fetch, 1, 50), 5, 20);
export function fetch(r: string | FetchOptions) {
const url = typeof r === 'string' ? r : r.url;
const options = typeof r === 'string' ? undefined : r.options;
const body = typeof r === 'string' ? undefined : r.body;
const client = url.startsWith('http:') ? http : https;
export function fetch(u: string) {
const client = u.startsWith('http:') ? http : https;
return new Promise<string>((resolve, reject) => {
// @ts-expect-error Typescript bug - thinks the second argument should be RequestOptions, not a callback
const req = client.get(u, (res: IncomingMessage) => {
if (res.statusCode !== 200) {
if (res.statusCode >= 500 && res.statusCode < 600) {
return reject(new RetryableError(`HTTP ${res.statusCode}`));
} else if (res.statusCode >= 300 && res.statusCode <= 400 && res.headers.location) {
resolve(fetch(url.resolve(u, res.headers.location)));
const handleResponse = (res: http.IncomingMessage) => {
const statusCode = res.statusCode!;
if (statusCode !== 200) {
if (statusCode >= 500 && statusCode < 600) {
return reject(new RetryableError(`HTTP ${statusCode}`));
} else if (statusCode >= 300 && statusCode <= 400 && res.headers.location) {
const redirectedUrl = new URL(res.headers.location, url).toString();
resolve(fetch({ url: redirectedUrl, options, body }));
} else {
return reject(new Error(`HTTP ${res.statusCode}`));
return reject(new Error(`HTTP ${statusCode}`));
}
}
Streams.readAll(res).then(resolve, reject);
});
};
const req = options ?
client.request(url, options, handleResponse) :
client.request(url, handleResponse);
req.on('error', reject);
if (body) req.write(body);
req.end();
});
}