// import * as bootstrap from 'bootstrap'; import ky from 'ky'; import { DateTime } from 'luxon'; import * as semver from 'semver'; import math from './utils/math.js'; document.addEventListener('DOMContentLoaded', () => { main(); }); const BASE_URL = 'https://raw.githubusercontent.com/daydreamer-json/ak-endfield-api-archive/refs/heads/main/output'; const FILE_SIZE_OPTS = { decimals: 2, decimalPadding: true, useBinaryUnit: true, useBitUnit: false, unitVisible: true, unit: null, }; interface MirrorFileEntry { orig: string; mirror: string; } interface StoredData { req: any; rsp: T; updatedAt: string; } const gameTargets = [ { name: 'Official', region: 'os', dirName: '6', channel: 6 }, { name: 'Epic', region: 'os', dirName: '801', channel: 6 }, { name: 'Google Play', region: 'os', dirName: '802', channel: 6 }, { name: 'Official', region: 'cn', dirName: '1', channel: 1 }, { name: 'Bilibili', region: 'cn', dirName: '2', channel: 2 }, ]; const launcherTargets = [ { id: 'os', apps: ['EndField', 'Official'], channel: 6 }, { id: 'cn', apps: ['EndField', 'Arknights', 'Official'], channel: 1 }, ]; const apiCache = new Map>(); function fetchJson(url: string): Promise { if (!apiCache.has(url)) { const promise = ky .get(url) .json() .catch((err) => { apiCache.delete(url); throw err; }); apiCache.set(url, promise); } return apiCache.get(url) as Promise; } let mirrorFileDb: MirrorFileEntry[] = []; async function preloadData() { const promises: Promise[] = []; promises.push(fetchJson(`${BASE_URL}/mirror_file_list.json`)); for (const target of gameTargets) { promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`)); promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`)); } const resTargets = [ { region: 'os', channel: 6 }, { region: 'cn', channel: 1 }, ]; const platforms = ['Windows', 'Android', 'iOS', 'PlayStation']; for (const target of resTargets) { for (const platform of platforms) { promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/game_resources/${target.channel}/${platform}/all.json`)); } } for (const region of launcherTargets) { for (const app of region.apps) { promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`)); promises.push(fetchJson(`${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`)); } } await Promise.all(promises); } async function main() { const contentDiv = document.getElementById('content'); if (!contentDiv) return; await preloadData(); try { mirrorFileDb = await fetchJson(`${BASE_URL}/mirror_file_list.json`); } catch (e) { console.warn('Failed to fetch mirror list', e); } const tabsHtml = `
`; contentDiv.innerHTML = tabsHtml; await Promise.all([ renderOverview(document.getElementById('tab-overview')!), renderGamePackages(document.getElementById('tab-game')!), renderPatches(document.getElementById('tab-patch')!), renderResources(document.getElementById('tab-resources')!), renderLaunchers(document.getElementById('tab-launcher')!), ]); } async function renderOverview(container: HTMLElement) { const mirrorOrigSet = new Set(); for (const m of mirrorFileDb) { try { const u = new URL(m.orig); u.search = ''; mirrorOrigSet.add(u.toString()); } catch {} } const countedUrls = new Set(); let totalMirrorSize = 0; const checkAndAddSize = (url: string, size: number) => { if (!url || isNaN(size)) return; try { const u = new URL(url); u.search = ''; const cleanUrl = u.toString(); if (countedUrls.has(cleanUrl)) return; if (mirrorOrigSet.has(cleanUrl)) { totalMirrorSize += size; countedUrls.add(cleanUrl); } } catch {} }; const section = document.createElement('div'); const sectionIn = document.createElement('div'); section.className = 'card mb-3'; sectionIn.className = 'card-body'; sectionIn.innerHTML = `

Latest Game Packages

${await ( async () => { const url = `${BASE_URL}/akEndfield/launcher/game/6/all.json`; const dat = await fetchJson[]>(url); return dat.at(-1)?.rsp.version; } )()}
Latest Version (Global)

`; const tableWrapper = document.createElement('div'); tableWrapper.className = 'table-responsive'; const table = document.createElement('table'); table.className = 'table table-striped table-bordered table-sm align-middle text-nowrap'; table.innerHTML = ` Region Channel Version Packed Unpacked `; const tbody = table.querySelector('tbody')!; tableWrapper.appendChild(table); sectionIn.appendChild(tableWrapper); section.appendChild(sectionIn); container.appendChild(section); // 1. Game Packages for (const target of gameTargets) { const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`; try { const data = await fetchJson[]>(url); if (!data || data.length === 0) continue; const latest = data[data.length - 1]; if (!latest) continue; const version = latest.rsp.version; const packedSize = math.arrayTotal(latest.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))); const totalSize = parseInt(latest.rsp.pkg.total_size); const unpackedSize = totalSize - packedSize; const row = document.createElement('tr'); row.innerHTML = ` ${target.region === 'cn' ? 'China' : 'Global'} ${target.name} ${version} ${math.formatFileSize(packedSize, FILE_SIZE_OPTS)} ${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)} `; tbody.appendChild(row); for (const entry of data) { if (entry.rsp.pkg && entry.rsp.pkg.packs) { for (const pack of entry.rsp.pkg.packs) { checkAndAddSize(pack.url, parseInt(pack.package_size)); } } } } catch (e) { console.warn('Overview: Failed to fetch game data', target.name, e); } } // 2. Patches for (const target of gameTargets) { const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`; try { const data = await fetchJson[]>(url); for (const entry of data) { if (!entry.rsp.patch) continue; if (entry.rsp.patch.url) { checkAndAddSize(entry.rsp.patch.url, parseInt(entry.rsp.patch.package_size)); } if (entry.rsp.patch.patches) { for (const p of entry.rsp.patch.patches) { checkAndAddSize(p.url, parseInt(p.package_size)); } } } } catch (e) {} } // 4. Launchers for (const region of launcherTargets) { for (const app of region.apps) { try { const urlZip = `${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`; const dataZip = await fetchJson[]>(urlZip); for (const e of dataZip) { checkAndAddSize(e.rsp.zip_package_url, parseInt(e.rsp.package_size)); } } catch (e) {} try { const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`; const dataExe = await fetchJson[]>(urlExe); for (const e of dataExe) { checkAndAddSize(e.rsp.exe_url, parseInt(e.rsp.exe_size)); } } catch (e) {} } } const mirrorSection = document.createElement('div'); mirrorSection.className = 'card'; mirrorSection.innerHTML = `

Mirror Statistics

${math.formatFileSize(totalMirrorSize, { ...FILE_SIZE_OPTS, unit: 'G' })}
uploaded to mirror

`; container.appendChild(mirrorSection); } async function renderGamePackages(container: HTMLElement) { for (const target of gameTargets) { const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all.json`; try { const data = await fetchJson[]>(url); const section = document.createElement('div'); section.className = 'mb-5'; section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}

`; const accordion = document.createElement('div'); accordion.className = 'accordion'; accordion.id = `accordion-game-${target.dirName}`; // Reverse order to show latest first const list = [...data].reverse(); for (let i = 0; i < list.length; i++) { const e = list[i]; if (!e) continue; const version = e.rsp.version; const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); const packedSize = math.arrayTotal(e.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))); const unpackedSize = parseInt(e.rsp.pkg.total_size) - packedSize; let rows = ''; const fileName = (f: any) => new URL(f.url).pathname.split('/').pop() ?? ''; for (const f of e.rsp.pkg.packs) { rows += ` ${fileName(f)} ${f.md5} ${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)} ${generateDownloadLinks(f.url)} `; } const itemId = `game-${target.dirName}-${i}`; // const isExpanded = i === 0; const isExpanded = false; const item = document.createElement('div'); item.className = 'accordion-item'; item.innerHTML = `

Unpacked Size${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}
Packed Size${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}
${rows}
FileMD5 ChecksumSizeDL
`; accordion.appendChild(item); } section.appendChild(accordion); container.appendChild(section); } catch (err) { // Ignore 404 or errors } } } async function renderPatches(container: HTMLElement) { for (const target of gameTargets) { const url = `${BASE_URL}/akEndfield/launcher/game/${target.dirName}/all_patch.json`; try { const data = await fetchJson[]>(url); if (data.length === 0) continue; const section = document.createElement('div'); section.className = 'mb-5'; section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}, ${target.name}

`; const accordion = document.createElement('div'); accordion.className = 'accordion'; accordion.id = `accordion-patch-${target.dirName}`; let itemIndex = 0; for (const e of [...data].reverse()) { if (!e.rsp.patch) continue; const version = e.rsp.version; const reqVersion = e.rsp.request_version; const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); const packedSize = math.arrayTotal(e.rsp.patch.patches.map((f: any) => parseInt(f.package_size))); const unpackedSize = parseInt(e.rsp.patch.total_size) - packedSize; let rows = ''; const fileName = (url: string) => new URL(url).pathname.split('/').pop() ?? ''; if (e.rsp.patch.url) { rows += ` ${fileName(e.rsp.patch.url)} ${e.rsp.patch.md5} ${math.formatFileSize(parseInt(e.rsp.patch.package_size), FILE_SIZE_OPTS)} ${generateDownloadLinks(e.rsp.patch.url)} `; } for (const f of e.rsp.patch.patches) { rows += ` ${fileName(f.url)} ${f.md5} ${math.formatFileSize(parseInt(f.package_size), FILE_SIZE_OPTS)} ${generateDownloadLinks(f.url)} `; } const itemId = `patch-${target.dirName}-${itemIndex}`; // const isExpanded = itemIndex === 0; const isExpanded = false; itemIndex++; const item = document.createElement('div'); item.className = 'accordion-item'; item.innerHTML = `

Unpacked Size${math.formatFileSize(unpackedSize, FILE_SIZE_OPTS)}
Packed Size${math.formatFileSize(packedSize, FILE_SIZE_OPTS)}
${rows}
FileMD5 ChecksumSizeDL
`; accordion.appendChild(item); } section.appendChild(accordion); container.appendChild(section); } catch (err) { // Ignore } } } async function renderResources(container: HTMLElement) { const platforms = ['Windows', 'Android', 'iOS', 'PlayStation']; // Filter unique channels (OS: 6, CN: 1), excluding Bilibili (2) as per archive.ts logic const targets = [ { region: 'os', channel: 6 }, { region: 'cn', channel: 1 }, ]; for (const target of targets) { const section = document.createElement('div'); section.className = 'mb-5'; section.innerHTML = `

${target.region === 'cn' ? 'China' : 'Global'}

`; const accordion = document.createElement('div'); accordion.className = 'accordion'; accordion.id = `accordion-res-${target.region}-${target.channel}`; let itemIndex = 0; for (const platform of platforms) { const url = `${BASE_URL}/akEndfield/launcher/game_resources/${target.channel}/${platform}/all.json`; try { const data = await fetchJson[]>(url); // Group by res_version const resVersionMap = new Map; versions: Set }>(); for (const e of data) { const resVer = e.rsp.res_version; if (!resVersionMap.has(resVer)) { resVersionMap.set(resVer, { rsp: e, versions: new Set() }); } resVersionMap.get(resVer)!.versions.add(e.req.version); } const resVersionSet = Array.from(resVersionMap.values()).map((d) => ({ resVersion: d.rsp.rsp.res_version, rsp: d.rsp, versions: Array.from(d.versions).sort(semver.rcompare), })); let rows = ''; for (const item of resVersionSet.reverse()) { // Newest first const dateStr = DateTime.fromISO(item.rsp.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); const initialRes = item.rsp.rsp.resources.find((e: any) => e.name === 'initial'); const mainRes = item.rsp.rsp.resources.find((e: any) => e.name === 'main'); const isKick = JSON.parse(item.rsp.rsp.configs).kick_flag === true; rows += ` ${dateStr} ${initialRes.version} ${mainRes.version} ${isKick ? '✅' : ''} ${item.versions.join(', ')} `; } const itemId = `res-${target.region}-${target.channel}-${platform}`; // const isExpanded = itemIndex === 0; const isExpanded = false; itemIndex++; const item = document.createElement('div'); item.className = 'accordion-item'; item.innerHTML = `

${rows}
Date Initial Main Kick Game version
`; accordion.appendChild(item); } catch (err) { // Ignore } } if (accordion.childElementCount > 0) { section.appendChild(accordion); container.appendChild(section); } } } async function renderLaunchers(container: HTMLElement) { for (const region of launcherTargets) { for (const app of region.apps) { const section = document.createElement('div'); section.className = 'mb-5'; section.innerHTML = `

${region.id.toUpperCase()} ${app}

`; const accordion = document.createElement('div'); accordion.className = 'accordion'; accordion.id = `accordion-launcher-${region.id}-${app}`; let itemIndex = 0; // Zip try { const urlZip = `${BASE_URL}/akEndfield/launcher/launcher/${app}/${region.channel}/all.json`; const dataZip = await fetchJson[]>(urlZip); let rows = ''; for (const e of [...dataZip].reverse()) { const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); const fileName = new URL(e.rsp.zip_package_url).pathname.split('/').pop() ?? ''; const unpacked = parseInt(e.rsp.total_size) - parseInt(e.rsp.package_size); rows += ` ${dateStr} ${e.rsp.version} ${fileName} ${e.rsp.md5} ${math.formatFileSize(unpacked, FILE_SIZE_OPTS)} ${math.formatFileSize(parseInt(e.rsp.package_size), FILE_SIZE_OPTS)} ${generateDownloadLinks(e.rsp.zip_package_url)} `; } const itemId = `launcher-zip-${region.id}-${app}`; // const isExpanded = itemIndex === 0; const isExpanded = false; itemIndex++; const item = document.createElement('div'); item.className = 'accordion-item'; item.innerHTML = `

${rows}
Date Version File MD5 Checksum Unpacked Packed DL
`; accordion.appendChild(item); } catch (e) {} // Exe try { const urlExe = `${BASE_URL}/akEndfield/launcher/launcherExe/${app}/${region.channel}/all.json`; const dataExe = await fetchJson[]>(urlExe); let rows = ''; for (const e of [...dataExe].reverse()) { const dateStr = DateTime.fromISO(e.updatedAt).toFormat('yyyy/MM/dd HH:mm:ss'); const fileName = new URL(e.rsp.exe_url).pathname.split('/').pop() ?? ''; rows += ` ${dateStr} ${e.rsp.version} ${fileName} ${math.formatFileSize(parseInt(e.rsp.exe_size), FILE_SIZE_OPTS)} ${generateDownloadLinks(e.rsp.exe_url)} `; } const itemId = `launcher-exe-${region.id}-${app}`; // const isExpanded = itemIndex === 0; const isExpanded = false; itemIndex++; const item = document.createElement('div'); item.className = 'accordion-item'; item.innerHTML = `

${rows}
Date Version File Size DL
`; accordion.appendChild(item); } catch (e) {} if (accordion.childElementCount > 0) { section.appendChild(accordion); container.appendChild(section); } } } } // --- Utils --- function generateDownloadLinks(url: string) { const cleanUrl = new URL(url); cleanUrl.search = ''; const mirrorEntry = mirrorFileDb.find((g) => g.orig.includes(cleanUrl.toString())); const links: string[] = []; links.push(`Orig`); if (mirrorEntry) { links.push(`Mirror`); } return links.join(' / '); }