diff --git a/package-lock.json b/package-lock.json index 5016216236..39f71ab9d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index bcb92a437f..c779778759 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/tools/set-import/importer.ts b/tools/set-import/importer.ts index ab59d11bd2..a0b9803955 100644 --- a/tools/set-import/importer.ts +++ b/tools/set-import/importer.ts @@ -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 = { [P in keyof T]?: T[P] extends (infer I)[] ? (DeepPartial)[] : DeepPartial; }; @@ -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(); - 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((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(); }); }