This commit is contained in:
daydreamer-json
2026-01-22 18:48:17 +09:00
commit b9c2d4253a
39 changed files with 3346 additions and 0 deletions

76
src/cmd.ts Normal file
View File

@@ -0,0 +1,76 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import cmds from './cmds.js';
import * as TypesLogLevels from './types/LogLevels.js';
import argvUtils from './utils/argv.js';
import appConfig from './utils/config.js';
import configEmbed from './utils/configEmbed.js';
import exitUtils from './utils/exit.js';
import logger from './utils/logger.js';
if (configEmbed.VERSION_NUMBER === null) throw new Error('Embed VERSION_NUMBER is null');
function wrapHandler(handler: (argv: any) => Promise<void>) {
return async (argv: any) => {
try {
await handler(argv);
await exitUtils.exit(0);
} catch (error) {
logger.error('Error caught:', error);
await exitUtils.exit(1);
}
};
}
async function parseCommand() {
const yargsInstance = yargs(hideBin(process.argv));
await yargsInstance
.command(
['test'],
'Test command',
(yargs) => {
yargs.options({
'output-dir': {
alias: ['o'],
desc: 'Output root directory',
default: appConfig.file.outputDirPath,
normalize: true,
type: 'string',
},
});
},
wrapHandler(cmds.test),
)
.options({
'log-level': {
desc: 'Set log level (' + TypesLogLevels.LOG_LEVELS_NUM.join(', ') + ')',
default: appConfig.logger.logLevel,
type: 'number',
coerce: (arg: number): TypesLogLevels.LogLevelString => {
if (arg < TypesLogLevels.LOG_LEVELS_NUM[0] || arg > TypesLogLevels.LOG_LEVELS_NUM.slice(-1)[0]!) {
throw new Error(`Invalid log level: ${arg} (Expected: ${TypesLogLevels.LOG_LEVELS_NUM.join(', ')})`);
} else {
return TypesLogLevels.LOG_LEVELS[arg as TypesLogLevels.LogLevelNumber];
}
},
},
})
.middleware(async (argv) => {
argvUtils.setArgv(argv);
logger.level = argvUtils.getArgv()['logLevel'];
logger.trace('Process started: ' + `${configEmbed.APPLICATION_NAME} v${configEmbed.VERSION_NUMBER}`);
})
.scriptName(configEmbed.APPLICATION_NAME)
.version(String(configEmbed.VERSION_NUMBER))
.usage('$0 <command> [argument] [option]')
.help()
.alias('help', 'h')
.alias('help', '?')
.alias('version', 'V')
.demandCommand(1)
.strict()
.recommendCommands()
.parse();
}
export default parseCommand;

5
src/cmds.ts Normal file
View File

@@ -0,0 +1,5 @@
import test from './cmds/test.js';
export default {
test,
};

236
src/cmds/test.ts Normal file
View File

@@ -0,0 +1,236 @@
import path from 'node:path';
import { DateTime } from 'luxon';
import semver from 'semver';
import apiUtils from '../utils/api.js';
import argvUtils from '../utils/argv.js';
import appConfig from '../utils/config.js';
import logger from '../utils/logger.js';
async function mainCmdHandler() {
const cfg = appConfig.network.api.akEndfield;
await (async () => {
const channel = appConfig.network.api.akEndfield.channel.osWinRel;
logger.debug('apiAkEndfield.launcher.latestGame fetching ...');
console.log(
await apiUtils.apiAkEndfield.launcher.latestGame(
appConfig.network.api.akEndfield.appCode.osWinRel,
appConfig.network.api.akEndfield.launcherAppCode.osWinRel,
channel,
channel,
channel,
null,
),
);
logger.debug('apiAkEndfield.launcher.latestLauncher fetching ...');
console.log(
await apiUtils.apiAkEndfield.launcher.latestLauncher(
appConfig.network.api.akEndfield.launcherAppCode.osWinRel,
channel,
channel,
null,
null,
),
);
logger.debug('apiAkEndfield.launcher.web fetching ...');
for (const fn of [
apiUtils.apiAkEndfield.launcher.web.sidebar,
apiUtils.apiAkEndfield.launcher.web.singleEnt,
apiUtils.apiAkEndfield.launcher.web.mainBgImage,
apiUtils.apiAkEndfield.launcher.web.banner,
apiUtils.apiAkEndfield.launcher.web.announcement,
]) {
console.dir(await fn(appConfig.network.api.akEndfield.appCode.osWinRel, channel, channel, 'ja-jp'), {
depth: null,
});
}
})();
await (async () => {
const rsp = await apiUtils.apiAkEndfield.launcher.latestGame(
cfg.appCode.osWinRel,
cfg.launcherAppCode.osWinRel,
cfg.channel.osWinRel,
cfg.channel.osWinRel,
cfg.channel.osWinRel,
null,
);
const prettyRsp = {
req: {
appCode: cfg.appCode.osWinRel,
launcherAppCode: cfg.launcherAppCode.osWinRel,
channel: cfg.channel.osWinRel,
subChannel: cfg.channel.osWinRel,
launcherSubChannel: cfg.channel.osWinRel,
},
rsp,
};
const filePathBase = path.join(
argvUtils.getArgv()['outputDir'],
'akEndfield',
'launcher',
'game',
String(cfg.channel.osWinRel),
);
for (const filePath of [path.join(filePathBase, 'latest.json'), path.join(filePathBase, `v${rsp.version}.json`)]) {
if (
(await Bun.file(filePath).exists()) === false ||
JSON.stringify(await Bun.file(filePath).json()) !== JSON.stringify(prettyRsp)
) {
await Bun.write(filePath, JSON.stringify(prettyRsp, null, 2));
}
}
await (async () => {
let needWrite: boolean = true;
const tmp: ({ updatedAt: string } & typeof prettyRsp)[] = await Bun.file(
path.join(filePathBase, 'all.json'),
).json();
for (const dataStr of tmp.map((e) => JSON.stringify({ req: e.req, rsp: e.rsp }))) {
if (dataStr === JSON.stringify(prettyRsp)) {
needWrite = false;
}
}
if (needWrite) {
tmp.push({ updatedAt: DateTime.now().toISO(), ...prettyRsp });
await Bun.write(path.join(filePathBase, 'all.json'), JSON.stringify(tmp, null, 2));
}
})();
})();
await (async () => {
const versionInfoList = (
(
await Bun.file(
path.join(
argvUtils.getArgv()['outputDir'],
'akEndfield',
'launcher',
'game',
String(cfg.channel.osWinRel),
'all.json',
),
).json()
).map((e: any) => e.rsp) as Awaited<ReturnType<typeof apiUtils.apiAkEndfield.launcher.latestGame>>[]
)
.map((e) => ({
version: e.version,
versionMinor: semver.major(e.version) + '.' + semver.minor(e.version),
randStr: /_([^/]+)\/.+?$/.exec(e.pkg.file_path)![1],
}))
.sort((a, b) => semver.compare(b.version, a.version));
const filePathBase = path.join(
argvUtils.getArgv()['outputDir'],
'akEndfield',
'launcher',
'game_resources',
String(cfg.channel.osWinRel),
);
let isLatestWrote: boolean = false;
for (const versionInfoEntry of versionInfoList) {
if (!versionInfoEntry.randStr) throw new Error('version rand_str not found');
const rsp = await apiUtils.apiAkEndfield.launcher.latestGameResources(
cfg.appCode.osWinRel,
versionInfoEntry.versionMinor,
versionInfoEntry.version,
versionInfoEntry.randStr,
);
const prettyRsp = {
req: {
appCode: cfg.appCode.osWinRel,
gameVersion: versionInfoEntry.versionMinor,
version: versionInfoEntry.version,
randStr: versionInfoEntry.randStr,
},
rsp,
};
if (isLatestWrote === false) {
if (
(await Bun.file(path.join(filePathBase, 'latest.json')).exists()) === false ||
JSON.stringify(await Bun.file(path.join(filePathBase, 'latest.json')).json()) !== JSON.stringify(prettyRsp)
) {
await Bun.write(path.join(filePathBase, 'latest.json'), JSON.stringify(prettyRsp, null, 2));
}
isLatestWrote = true;
}
for (const filePath of [path.join(filePathBase, `v${versionInfoEntry.version}.json`)]) {
if (
(await Bun.file(filePath).exists()) === false ||
JSON.stringify(await Bun.file(filePath).json()) !== JSON.stringify(prettyRsp)
) {
await Bun.write(filePath, JSON.stringify(prettyRsp, null, 2));
}
}
await (async () => {
let needWrite: boolean = true;
const tmp: ({ updatedAt: string } & typeof prettyRsp)[] = await Bun.file(
path.join(filePathBase, 'all.json'),
).json();
for (const dataStr of tmp.map((e) => JSON.stringify({ req: e.req, rsp: e.rsp }))) {
if (dataStr === JSON.stringify(prettyRsp)) needWrite = false;
}
if (needWrite) {
tmp.push({ updatedAt: DateTime.now().toISO(), ...prettyRsp });
await Bun.write(path.join(filePathBase, 'all.json'), JSON.stringify(tmp, null, 2));
}
})();
}
})();
await (async () => {
const launcherTargetAppList = ['EndField', 'official'] as const;
for (const launcherTargetAppEntry of launcherTargetAppList) {
const rsp = await apiUtils.apiAkEndfield.launcher.latestLauncher(
cfg.launcherAppCode.osWinRel,
cfg.channel.osWinRel,
cfg.channel.osWinRel,
null,
launcherTargetAppEntry === 'official' ? null : launcherTargetAppEntry,
);
const prettyRsp = {
req: {
appCode: cfg.launcherAppCode.osWinRel,
channel: cfg.channel.osWinRel,
subChannel: cfg.channel.osWinRel,
targetApp: launcherTargetAppEntry === 'official' ? null : launcherTargetAppEntry,
},
rsp,
};
const filePathBase = path.join(
argvUtils.getArgv()['outputDir'],
'akEndfield',
'launcher',
'launcher',
launcherTargetAppEntry,
String(cfg.channel.osWinRel),
);
for (const filePath of [
path.join(filePathBase, 'latest.json'),
path.join(filePathBase, `v${rsp.version}.json`),
]) {
if (
(await Bun.file(filePath).exists()) === false ||
JSON.stringify(await Bun.file(filePath).json()) !== JSON.stringify(prettyRsp)
) {
await Bun.write(filePath, JSON.stringify(prettyRsp, null, 2));
}
}
await (async () => {
let needWrite: boolean = true;
const tmp: ({ updatedAt: string } & typeof prettyRsp)[] = await Bun.file(
path.join(filePathBase, 'all.json'),
).json();
for (const dataStr of tmp.map((e) => JSON.stringify({ req: e.req, rsp: e.rsp }))) {
if (dataStr === JSON.stringify(prettyRsp)) {
needWrite = false;
}
}
if (needWrite) {
tmp.push({ updatedAt: DateTime.now().toISO(), ...prettyRsp });
await Bun.write(path.join(filePathBase, 'all.json'), JSON.stringify(tmp, null, 2));
}
})();
}
})();
}
export default mainCmdHandler;

18
src/main.ts Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env node
import childProcess from 'node:child_process';
import util from 'node:util';
import parseCommand from './cmd.js';
import exitUtils from './utils/exit.js';
async function main(): Promise<void> {
try {
process.platform === 'win32' ? await util.promisify(childProcess.exec)('chcp 65001') : undefined;
await parseCommand();
} catch (error) {
console.log(error);
exitUtils.pressAnyKeyToExit(1);
}
}
await main();

15
src/types/LogLevels.ts Normal file
View File

@@ -0,0 +1,15 @@
const LOG_LEVELS = {
0: 'trace',
1: 'debug',
2: 'info',
3: 'warn',
4: 'error',
5: 'fatal',
} as const;
const LOG_LEVELS_NUM = [0, 1, 2, 3, 4, 5] as const;
type LogLevelNumber = keyof typeof LOG_LEVELS;
type LogLevelString = (typeof LOG_LEVELS)[LogLevelNumber];
export type { LogLevelNumber, LogLevelString };
export { LOG_LEVELS, LOG_LEVELS_NUM };

View File

@@ -0,0 +1,118 @@
type LauncherLatestGame = {
action: number;
version: string; // x.y.z
request_version: string; // x.y.z or blank
pkg: {
packs: {
url: string;
md5: string;
package_size: string;
}[];
total_size: string;
file_path: string;
url: string;
md5: string;
package_size: string;
file_id: string;
sub_channel: string;
game_files_md5: string;
};
patch: unknown;
state: number;
launcher_action: number;
};
type LauncherLatestGameResources = {
resources: {
name: string;
version: string;
path: string;
}[];
configs: string; // json str
res_version: string;
patch_index_path: string;
domain: string;
};
type LauncherLatestLauncher = {
action: number;
version: string; // x.y.z
request_version: string; // x.y.z or blank
zip_package_url: string;
md5: string;
package_size: string;
total_size: string;
description: string;
};
type LauncherWebSidebar = {
data_version: string;
sidebars: {
display_type: 'DisplayType_RESERVE';
media: string;
pic: { url: string; md5: string; description: string } | null;
sidebar_labels: { content: string; jump_url: string; need_token: boolean }[];
grid_info: null;
jump_url: string;
need_token: boolean;
}[];
};
type LauncherWebSingleEnt = {
single_ent: {
version_url: string;
version_md5: string;
jump_url: string;
button_url: string;
button_md5: string;
button_hover_url: string;
button_hover_md5: string;
need_token: boolean;
};
};
type LauncherWebMainBgImage = {
data_version: string;
main_bg_image: {
url: string;
md5: string;
video_url: string;
};
};
type LauncherWebBanner = {
data_version: string;
banners: {
url: string;
md5: string;
jump_url: string;
id: string;
need_token: boolean;
}[];
};
type LauncherWebAnnouncement = {
data_version: string;
tabs: {
tabName: string;
announcements: {
content: string;
jump_url: string;
start_ts: string; // example: 1768969800000
id: string;
need_token: boolean;
}[];
tab_id: string;
}[];
};
export type {
LauncherLatestGame,
LauncherLatestGameResources,
LauncherLatestLauncher,
LauncherWebSidebar,
LauncherWebSingleEnt,
LauncherWebMainBgImage,
LauncherWebBanner,
LauncherWebAnnouncement,
};

252
src/utils/api.ts Normal file
View File

@@ -0,0 +1,252 @@
import ky from 'ky';
import semver from 'semver';
import * as TypesApiAkEndfield from '../types/api/akEndfield/api.js';
import appConfig from './config.js';
const defaultKySettings = {
headers: {
'User-Agent': appConfig.network.userAgent.minimum,
},
timeout: appConfig.network.timeout,
retry: { limit: appConfig.network.retryCount },
};
const launcherWebLang = [
'de-de',
'en-us',
'es-mx',
'fr-fr',
'id-id',
'it-it',
'ja-jp',
'ko-kr',
'pt-br',
'ru-ru',
'th-th',
'vi-vn',
'zh-cn',
'zh-tw',
] as const;
export default {
defaultKySettings,
apiAkEndfield: {
launcher: {
latestGame: async (
appCode: string,
launcherAppCode: string,
channel: number,
subChannel: number,
launcherSubChannel: number,
version: string | null,
): Promise<TypesApiAkEndfield.LauncherLatestGame> => {
if (version !== null && !semver.valid(version)) throw new Error(`Invalid version string (${version})`);
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.launcher}/game/get_latest`, {
...defaultKySettings,
searchParams: {
appcode: appCode,
launcher_appcode: launcherAppCode,
channel,
sub_channel: subChannel,
launcher_sub_channel: launcherSubChannel,
version: version ?? undefined,
},
})
.json();
return rsp as TypesApiAkEndfield.LauncherLatestGame;
},
latestGameResources: async (
appCode: string,
gameVersion: string, // example: 1.0
version: string,
randStr: string,
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherLatestGameResources> => {
if (!semver.valid(version)) throw new Error(`Invalid version string (${version})`);
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.launcher}/game/get_latest_resources`, {
...defaultKySettings,
searchParams: {
appcode: appCode,
game_version: gameVersion,
version: version,
platform,
rand_str: randStr,
},
})
.json();
return rsp as TypesApiAkEndfield.LauncherLatestGameResources;
},
latestLauncher: async (
appCode: string,
channel: number,
subChannel: number,
version: string | null,
targetApp: 'EndField' | null,
): Promise<TypesApiAkEndfield.LauncherLatestLauncher> => {
if (version !== null && !semver.valid(version)) throw new Error(`Invalid version string (${version})`);
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.launcher}/launcher/get_latest`, {
...defaultKySettings,
searchParams: {
appcode: appCode,
channel,
sub_channel: subChannel,
version: version ?? undefined,
targetApp: targetApp ?? undefined,
},
})
.json();
return rsp as TypesApiAkEndfield.LauncherLatestLauncher;
},
web: {
sidebar: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebSidebar> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_sidebar',
get_sidebar_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_sidebar_rsp as TypesApiAkEndfield.LauncherWebSidebar;
},
singleEnt: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebSingleEnt> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_single_ent',
get_single_ent_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_single_ent_rsp as TypesApiAkEndfield.LauncherWebSingleEnt;
},
mainBgImage: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebMainBgImage> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_main_bg_image',
get_main_bg_image_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_main_bg_image_rsp as TypesApiAkEndfield.LauncherWebMainBgImage;
},
banner: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebBanner> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_banner',
get_banner_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_banner_rsp as TypesApiAkEndfield.LauncherWebBanner;
},
announcement: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebAnnouncement> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_announcement',
get_announcement_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_announcement_rsp as TypesApiAkEndfield.LauncherWebAnnouncement;
},
},
},
},
};

13
src/utils/argv.ts Normal file
View File

@@ -0,0 +1,13 @@
type argvType = {
[prop: string]: any;
};
let argv: argvType | null = null;
export default {
setArgv: (argvIn: object) => {
argv = argvIn;
},
getArgv: () => {
if (argv === null) throw new Error('argv is null');
return argv;
},
};

120
src/utils/config.ts Normal file
View File

@@ -0,0 +1,120 @@
import path from 'node:path';
import deepmerge from 'deepmerge';
import YAML from 'yaml';
// import * as TypesGameEntry from '../types/GameEntry.js';
import * as TypesLogLevels from '../types/LogLevels.js';
type Freeze<T> = Readonly<{
[P in keyof T]: T[P] extends object ? Freeze<T[P]> : T[P];
}>;
type AllRequired<T> = Required<{
[P in keyof T]: T[P] extends object ? Freeze<T[P]> : T[P];
}>;
type ConfigType = AllRequired<
Freeze<{
file: {
outputDirPath: string;
};
network: {
api: {
akEndfield: {
appCode: { osWinRel: string };
launcherAppCode: { osWinRel: string };
channel: { osWinRel: number };
base: {
accountService: string;
launcher: string;
u8: string;
};
};
};
userAgent: {
// UA to hide the fact that the access is from this tool
minimum: string;
chromeWindows: string;
curl: string;
ios: string;
};
timeout: number; // Network timeout
retryCount: number; // Number of retries for access failure
};
threadCount: {
// Upper limit on the number of threads for parallel processing
network: number; // network access
};
cli: {
autoExit: boolean; // Whether to exit the tool without waiting for key input when the exit code is 0
};
logger: {
// log4js-node logger settings
logLevel: TypesLogLevels.LogLevelNumber;
useCustomLayout: boolean;
customLayoutPattern: string;
};
}>
>;
const initialConfig: ConfigType = {
file: {
outputDirPath: path.resolve('output'),
},
network: {
api: {
akEndfield: {
appCode: { osWinRel: 'YDUTE5gscDZ229CW' },
launcherAppCode: { osWinRel: 'TiaytKBUIEdoEwRT' },
channel: { osWinRel: 6 },
base: {
accountService: 'YXMuZ3J5cGhsaW5lLmNvbQ==',
launcher: 'bGF1bmNoZXIuZ3J5cGhsaW5lLmNvbS9hcGk=',
u8: 'dTguZ3J5cGhsaW5lLmNvbQ==',
},
},
},
userAgent: {
minimum: 'Mozilla/5.0',
chromeWindows:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
curl: 'curl/8.4.0',
ios: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1',
},
timeout: 20000,
retryCount: 5,
},
threadCount: { network: 16 },
cli: { autoExit: false },
logger: {
logLevel: 0,
useCustomLayout: true,
customLayoutPattern: '%[%d{hh:mm:ss.SSS} %-5.0p >%] %m',
},
};
const deobfuscator = (input: ConfigType): ConfigType => {
const newConfig = JSON.parse(JSON.stringify(input)) as any;
const api = newConfig.network.api.akEndfield;
for (const key of Object.keys(api.base) as (keyof typeof api.base)[]) {
api.base[key] = atob(api.base[key]);
}
return newConfig as ConfigType;
};
const filePath = 'config/config.yaml';
if ((await Bun.file(filePath).exists()) === false) {
await Bun.write(filePath, YAML.stringify(initialConfig));
}
const config: ConfigType = await (async () => {
const rawFileData: ConfigType = YAML.parse(await Bun.file(filePath).text()) as ConfigType;
const mergedConfig = deepmerge(initialConfig, rawFileData, {
arrayMerge: (_destinationArray, sourceArray) => sourceArray,
});
if (JSON.stringify(rawFileData) !== JSON.stringify(mergedConfig)) {
await Bun.write(filePath, YAML.stringify(mergedConfig));
}
return deobfuscator(mergedConfig);
})();
export default config;

6
src/utils/configEmbed.ts Normal file
View File

@@ -0,0 +1,6 @@
import semver from 'semver';
export default {
APPLICATION_NAME: 'ak-endfield-api-archive',
VERSION_NUMBER: semver.valid('0.1.0'),
};

50
src/utils/exit.ts Normal file
View File

@@ -0,0 +1,50 @@
import readline from 'node:readline';
import appConfig from './config.js';
async function pressAnyKeyToExit(errorCode: number): Promise<void> {
if (errorCode !== 0) console.error('An error occurred');
if (!process.stdin.isTTY || (appConfig.cli.autoExit && errorCode === 0)) {
console.log(`Exiting with code ${errorCode} ...`);
process.exit(errorCode);
}
process.stdout.write('Press any key to exit ...');
return new Promise((resolve) => {
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.once('data', () => {
process.stdin.setRawMode(false);
process.stdin.pause();
process.stdout.write(` Exiting with code ${errorCode} ...\n`);
resolve(); // Promiseを解決
process.exit(errorCode); // その後、プロセスを終了
});
});
}
async function exit(errorCode: number, str: string | null = null, printText: boolean = true): Promise<void> {
if (errorCode !== 0 && printText) console.error('An error occurred' + (str ? ': ' + str : ''));
printText ? process.stdout.write(`Exiting with code ${errorCode} ...\n`) : null;
process.exit(errorCode);
}
async function pressAnyKeyToContinue(printText: boolean = true): Promise<void> {
printText ? process.stdout.write('Press any key to continue ...') : null;
return new Promise((resolve) => {
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.once('data', () => {
process.stdin.setRawMode(false);
process.stdin.pause();
printText ? process.stdout.write(`\n`) : null;
resolve(); // Promiseを解決
});
});
}
export default {
pressAnyKeyToExit,
exit,
pressAnyKeyToContinue,
};

117
src/utils/file.ts Normal file
View File

@@ -0,0 +1,117 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import stream from 'node:stream';
import { fileURLToPath } from 'node:url';
import util from 'node:util';
import configEmbed from './configEmbed.js';
function getAppDataDir(): string {
switch (process.platform) {
case 'win32':
return path.join(
process.env['APPDATA'] || path.join(os.homedir(), 'AppData', 'Roaming'),
configEmbed.APPLICATION_NAME,
);
case 'darwin':
return path.join(os.homedir(), 'Library', 'Application Support', configEmbed.APPLICATION_NAME);
case 'linux':
return path.join(
process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'),
configEmbed.APPLICATION_NAME,
);
default:
return path.resolve('config');
}
}
/**
* Detect app is standalone or not
*/
function isAppCompiledSA(): boolean {
return Boolean(
/^file:\/\/\/\$bunfs\/root\//g.test(import.meta.url) || /^file:\/\/\/B:\/%7EBUN\/root\//g.test(import.meta.url),
);
}
function getAppRootDir(): string {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
if (isAppCompiledSA()) {
// app is standalone
return path.dirname(process.execPath);
}
// app is not standalone
let currentDir = __dirname;
while (currentDir !== path.parse(currentDir).root) {
const basename = path.basename(currentDir);
if (basename === 'dist' || basename === 'src') {
return path.dirname(currentDir);
}
currentDir = path.dirname(currentDir);
}
return __dirname; // fallback
}
async function checkFolderExists(folderPath: string): Promise<boolean> {
try {
const stats = await fs.promises.stat(folderPath);
return stats.isDirectory();
} catch (error: any) {
return false;
}
}
async function checkFileExists(filePath: string): Promise<boolean> {
try {
const stats = await fs.promises.stat(filePath);
return stats.isFile();
} catch (error: any) {
return false;
}
}
async function getFileList(dirPath: string): Promise<string[]> {
const absoluteDirPath = path.resolve(dirPath);
const filePaths: string[] = [];
async function walk(currentDir: string): Promise<void> {
const entries = await fs.promises.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
} else if (entry.isFile()) {
filePaths.push(fullPath);
}
}
}
await walk(absoluteDirPath);
return filePaths;
}
async function readFileAsArrayBuffer(filePath: string): Promise<ArrayBuffer> {
const buffer = await fs.promises.readFile(filePath);
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
}
async function copyFileWithStream(srcPath: string, destPath: string): Promise<void> {
const pipelineAsync = util.promisify(stream.pipeline);
try {
await pipelineAsync(fs.createReadStream(srcPath), fs.createWriteStream(destPath));
} catch (err) {
throw err;
}
}
export default {
getAppDataDir,
isAppCompiledSA,
getAppRootDir,
checkFolderExists,
checkFileExists,
getFileList,
readFileAsArrayBuffer,
copyFileWithStream,
};

25
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,25 @@
import log4js from 'log4js';
import * as TypesLogLevels from '../types/LogLevels.js';
import appConfig from './config.js';
log4js.configure({
appenders: {
System: {
type: 'stdout',
layout: {
type: appConfig.logger.useCustomLayout ? 'pattern' : 'colored',
pattern: appConfig.logger.useCustomLayout ? appConfig.logger.customLayoutPattern : '',
},
},
},
categories: {
default: {
appenders: ['System'],
level: TypesLogLevels.LOG_LEVELS[appConfig.logger.logLevel],
},
},
});
const logger: log4js.Logger = log4js.getLogger('System');
export default logger;

90
src/utils/math.ts Normal file
View File

@@ -0,0 +1,90 @@
export default {
arrayMax(array: Array<number>) {
return array.reduce((a, b) => Math.max(a, b));
},
arrayMin(array: Array<number>) {
return array.reduce((a, b) => Math.min(a, b));
},
arrayTotal(array: Array<number>) {
return array.reduce((acc, f) => acc + f, 0);
},
arrayAvg(array: Array<number>) {
return this.arrayTotal(array) / array.length;
},
rounder(method: 'floor' | 'ceil' | 'round', num: number, n: number) {
const pow = Math.pow(10, n);
let result: number;
switch (method) {
case 'floor':
result = Math.floor(num * pow) / pow;
break;
case 'ceil':
result = Math.ceil(num * pow) / pow;
break;
case 'round':
result = Math.round(num * pow) / pow;
break;
}
return {
orig: result,
padded: result.toFixed(n),
};
},
formatFileSize(
bytes: number,
options: {
decimals: number;
decimalPadding: boolean;
useBinaryUnit: boolean;
useBitUnit: boolean;
unitVisible: boolean;
unit: 'B' | 'K' | 'M' | 'G' | 'T' | 'P' | 'E' | 'Z' | 'Y' | null;
},
) {
const k = options.useBinaryUnit ? 1024 : 1000;
const dm = options.decimals < 0 ? 0 : options.decimals;
const baseUnits = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
const binaryUnitSuffix = options.useBitUnit ? 'ib' : 'iB';
const siUnitSuffix = options.useBitUnit ? 'b' : 'B';
const getUnitString = (i: number) => {
if (i === 0) return options.useBitUnit ? 'b' : 'B';
return baseUnits[i] + (options.useBinaryUnit ? binaryUnitSuffix : siUnitSuffix);
};
let value = bytes < 0 ? 0 : Math.floor(bytes);
if (options.useBitUnit) {
value *= 8;
}
let i: number;
if (options.unit !== null) {
i = baseUnits.indexOf(options.unit);
if (i === -1) throw new Error(`Invalid unit: ${options.unit}`);
} else {
if (value === 0) {
i = 0;
} else {
i = Math.floor(Math.log(value) / Math.log(k));
i = Math.max(0, Math.min(baseUnits.length - 1, i)); // clamp
}
}
const resultValue = value / Math.pow(k, i);
let formattedValue: string;
if (options.decimalPadding) {
formattedValue = resultValue.toFixed(dm);
} else {
formattedValue = resultValue.toFixed(dm).replace(/\.?0+$/, '');
}
return formattedValue + (options.unitVisible ? ' ' + getUnitString(i) : '');
},
};

125
src/utils/omitDeep.ts Normal file
View File

@@ -0,0 +1,125 @@
type Path = Array<string | number>;
type Paths = Path | Path[];
/**
* Removes properties from an object at specified paths.
* @param obj The target object.
* @param paths The paths of the properties to remove.
* @returns A new object with the properties removed.
*
* @example
* const user = {
* id: 1,
* name: 'John',
* profile: {
* email: 'john@example.com',
* settings: {
* theme: 'dark',
* notifications: true
* }
* },
* tags: ['user', 'premium', 'verified']
* };
*
* // Remove a single nested property
* const withoutEmail = omitDeep(user, ['profile', 'email']);
*
* // Remove multiple properties and array elements at once
* const cleanedUser = omitDeep(user, [
* ['profile', 'email'],
* ['profile', 'settings', 'notifications'],
* ['tags', 1] // Remove the second element of the array
* ]);
*/
function omitDeep<T extends object>(obj: T, paths: Paths): T {
// Robust deep copy
const deepClone = <U>(item: U): U => {
if (item === null || typeof item !== 'object') return item;
if (item instanceof Date) return new Date(item) as unknown as U;
if (item instanceof RegExp) return new RegExp(item) as unknown as U;
if (Array.isArray(item)) {
return item.map(deepClone) as unknown as U;
}
const clone = {} as U;
for (const key in item) {
if (Object.prototype.hasOwnProperty.call(item, key)) {
clone[key] = deepClone(item[key]);
}
}
return clone;
};
const result = deepClone(obj);
// Normalize paths
const pathArray: Path[] =
Array.isArray(paths) && paths.length > 0 && Array.isArray(paths[0]) ? (paths as Path[]) : [paths as Path];
// Perform deletion for each path
for (const path of pathArray) {
if (path.length === 0) continue;
// Get the parent object/array of the target property
const parentInfo = getParentAndKey(result, path);
if (!parentInfo) continue;
const { parent, lastKey } = parentInfo;
// Delete the property or array element
if (Array.isArray(parent)) {
// If it's an array
const index = Number(lastKey);
if (!isNaN(index) && index >= 0 && index < parent.length) {
parent.splice(index, 1);
}
} else if (typeof parent === 'object' && parent !== null) {
// If it's an object
delete parent[lastKey as string];
}
}
return result;
}
/**
* Gets the parent object/array and the final key for a given path.
*/
function getParentAndKey(obj: any, path: Path): { parent: any; lastKey: string | number } | null {
let current: any = obj;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
if (current === null || typeof current !== 'object') {
return null;
}
// Handle arrays
if (Array.isArray(current)) {
const index = Number(key);
if (isNaN(index) || index < 0 || index >= current.length) {
return null;
}
current = current[index];
} // Handle objects
else if (Object.prototype.hasOwnProperty.call(current, key!)) {
current = current[key!];
} else {
return null;
}
}
if (current === null || typeof current !== 'object') {
return null;
}
return {
parent: current,
lastKey: path[path.length - 1]!,
};
}
export default omitDeep;