UNPKG

genshin-manager

Version:

<div align="center"> <p> <a href="https://www.npmjs.com/package/genshin-manager"><img src="https://img.shields.io/npm/v/genshin-manager.svg?maxAge=3600" alt="npm version" /></a> <a href="https://www.npmjs.com/package/genshin-manager"><img src="https:

512 lines (511 loc) 23 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AssetCacheManager = void 0; const cliProgress = __importStar(require("cli-progress")); const events_1 = __importDefault(require("events")); const fs_1 = __importDefault(require("fs")); const path = __importStar(require("path")); const promises_1 = require("stream/promises"); const AssetsNotFoundError_1 = require("../errors/AssetsNotFoundError"); const BodyNotFoundError_1 = require("../errors/BodyNotFoundError"); const TextMapFormatError_1 = require("../errors/TextMapFormatError"); const types_1 = require("../types"); const JsonParser_1 = require("../utils/JsonParser"); const ObjectKeyDecoder_1 = require("../utils/ObjectKeyDecoder"); const PromiseEventEmitter_1 = require("../utils/PromiseEventEmitter"); const ReadableStreamWrapper_1 = require("../utils/ReadableStreamWrapper"); const TextMapEmptyWritable_1 = require("../utils/TextMapEmptyWritable"); const TextMapTransform_1 = require("../utils/TextMapTransform"); /** * Class for managing cached assets * @abstract */ class AssetCacheManager extends PromiseEventEmitter_1.PromiseEventEmitter { /** * Create a AssetCacheManager * @param option Client option */ constructor(option) { super(); AssetCacheManager.option = option; AssetCacheManager.commitFilePath = path.resolve(AssetCacheManager.option.assetCacheFolderPath, 'commits.json'); AssetCacheManager.commitFilePath = path.resolve(AssetCacheManager.option.assetCacheFolderPath, 'commits.json'); AssetCacheManager.excelBinOutputFolderPath = path.resolve(AssetCacheManager.option.assetCacheFolderPath, 'ExcelBinOutput'); AssetCacheManager.textMapFolderPath = path.resolve(AssetCacheManager.option.assetCacheFolderPath, 'TextMap'); } /** * Assets game version text * @returns Assets game version text or undefined */ static get gameVersion() { const oldCommits = fs_1.default.existsSync(this.commitFilePath) ? JSON.parse(fs_1.default.readFileSync(this.commitFilePath, { encoding: 'utf8', })) : null; if (oldCommits) { const versionTexts = /OSRELWin(\d+\.\d+\.\d+)_/.exec(oldCommits[0].title); if (!versionTexts || versionTexts.length < 2) return '?.?.?'; return versionTexts[1]; } else { return undefined; } } /** * Create ExcelBinOutput Keys to cache * @returns All ExcelBinOutput Keys */ static get excelBinOutputAllKeys() { return new Set(Object.keys(types_1.ExcelBinOutputs).map((key) => key)); } /** * Add ExcelBinOutput Key from Class Prototype to AssetCacheManager * @deprecated This method is deprecated because it is used to pass data to each class * @param classPrototype Class Prototype */ static _addExcelBinOutputKeyFromClassPrototype(classPrototype) { const targetOrigin = classPrototype; const methodSource = targetOrigin.constructor.toString(); const keys = Object.keys(types_1.ExcelBinOutputs); const matches = [ ...methodSource.matchAll(new RegExp(`(?<=("|\`|'))(${keys.join('|')})(?=("|\`|'))`, 'g')), ]; matches .map((match) => match[0]) .forEach((key) => this.useExcelBinOutputKeys.add(key)); } /** * Get Json from cached excel bin output * @deprecated This method is deprecated because it is used to pass data to each class * @param key ExcelBinOutput name * @param id ID of character, etc * @returns Json */ static _getJsonFromCachedExcelBinOutput(key, id) { const excelBinOutput = this.cachedExcelBinOutput.get(key); if (!excelBinOutput) throw new AssetsNotFoundError_1.AssetsNotFoundError(key); const json = excelBinOutput.get(String(id)); if (!json) throw new AssetsNotFoundError_1.AssetsNotFoundError(key, id); return json; } /** * Get cached excel bin output by name * @deprecated This method is deprecated because it is used to pass data to each class * @param key ExcelBinOutput name * @returns Cached excel bin output */ static _getCachedExcelBinOutputByName(key) { const excelBinOutput = this.cachedExcelBinOutput.get(key); if (!excelBinOutput) throw new AssetsNotFoundError_1.AssetsNotFoundError(key); return excelBinOutput.get(); } /** * Check if cached excel bin output exists by name * @deprecated This method is deprecated because it is used to pass data to each class * @param key ExcelBinOutput name * @returns Cached excel bin output exists */ static _hasCachedExcelBinOutputByName(key) { return this.cachedExcelBinOutput.has(key); } /** * Check if cached excel bin output exists by ID * @deprecated This method is deprecated because it is used to pass data to each class * @param key ExcelBinOutput name * @param id ID of character, etc * @returns Cached excel bin output exists */ static _hasCachedExcelBinOutputById(key, id) { const excelBinOutput = this.cachedExcelBinOutput.get(key); if (!excelBinOutput) return false; const json = excelBinOutput.get(String(id)); if (!json) return false; return true; } /** * Search ID in CachedExcelBinOutput by text * @deprecated This method is deprecated because it is used to pass data to each class * @param key ExcelBinOutput name * @param text Text * @returns IDs */ static _searchIdInExcelBinOutByText(key, text) { return Object.entries(this._getCachedExcelBinOutputByName(key)) .filter(([, json]) => Object.keys(json).some((jsonKey) => { if (/TextMapHash/g.exec(jsonKey)) { const hash = json[jsonKey]; return this._cachedTextMap.get(String(hash)) === text; } })) .map(([id]) => id); } /** * Set excel bin output to cache * @param keys ExcelBinOutput names * @returns Returns true if an error occurs */ static setExcelBinOutputToCache(keys) { return __awaiter(this, void 0, void 0, function* () { this.cachedExcelBinOutput.clear(); for (const key of keys) { const filename = types_1.ExcelBinOutputs[key]; const selectedExcelBinOutputPath = path.join(this.excelBinOutputFolderPath, filename); let text = ''; if (!fs_1.default.existsSync(selectedExcelBinOutputPath)) { if (this.option.autoFixExcelBin) { if (this.option.showFetchCacheLog) { console.log(`GenshinManager: ${filename} not found. Re downloading...`); } yield this.reDownloadAllExcelBinOutput(); return true; } else { throw new AssetsNotFoundError_1.AssetsNotFoundError(key); } } const stream = fs_1.default.createReadStream(selectedExcelBinOutputPath, { highWaterMark: 1 * 1024 * 1024, }); stream.on('data', (chunk) => (text += chunk)); const setCachePromiseResult = yield new Promise((resolve, reject) => { stream.on('error', (error) => reject(error)); stream.on('end', () => { this.cachedExcelBinOutput.set(key, new JsonParser_1.JsonParser(text)); resolve(); }); }).catch((error) => __awaiter(this, void 0, void 0, function* () { if (error instanceof SyntaxError) { if (this.option.autoFixExcelBin) { if (this.option.showFetchCacheLog) { console.log(`GenshinManager: ${filename} format error. Re downloading...`); } yield this.reDownloadAllExcelBinOutput(); return true; } else { throw error; } } })); if (setCachePromiseResult) return true; } const decoder = new ObjectKeyDecoder_1.ObjectKeyDecoder(); this.cachedExcelBinOutput.forEach((v, k) => { this.cachedExcelBinOutput.set(k, new JsonParser_1.JsonParser(JSON.stringify(decoder.execute(v, k)))); }); return false; }); } /** * Change cached languages * @param language Country code * @returns Returns true if an error occurs */ static setTextMapToCache(language) { return __awaiter(this, void 0, void 0, function* () { //Since the timing of loading into the cache is the last, unnecessary cache is not loaded, and therefore clearing the cache is not necessary. const results = yield Promise.all(types_1.TextMapLanguage[language].map((fileName) => __awaiter(this, void 0, void 0, function* () { const selectedTextMapPath = path.join(this.textMapFolderPath, fileName); if (!fs_1.default.existsSync(selectedTextMapPath)) { if (this.option.autoFixTextMap) { if (this.option.showFetchCacheLog) { console.log(`GenshinManager: ${fileName} not found. Re downloading...`); } yield this.reDownloadTextMap(language); return true; } else { throw new AssetsNotFoundError_1.AssetsNotFoundError(language); } } const eventEmitter = new TextMapEmptyWritable_1.TextMapEmptyWritable(); eventEmitter.on('data', ({ key, value }) => this._cachedTextMap.set(key, value)); const pipelinePromiseResult = yield (0, promises_1.pipeline)(fs_1.default.createReadStream(selectedTextMapPath, { highWaterMark: 1 * 1024 * 1024, }), new TextMapTransform_1.TextMapTransform(language, this.textHashes), eventEmitter).catch((error) => __awaiter(this, void 0, void 0, function* () { if (error instanceof TextMapFormatError_1.TextMapFormatError) { if (this.option.autoFixTextMap) { if (this.option.showFetchCacheLog) { console.log(`GenshinManager: TextMap${language}.json format error. Re downloading...`); } yield this.reDownloadTextMap(language); return true; } else { throw error; } } })); if (pipelinePromiseResult) return true; return false; }))); return results.every((result) => result === true); }); } /** * Update cache * @example * ```ts * await Client.updateCache() * ``` */ static updateCache() { return __awaiter(this, void 0, void 0, function* () { if (this.option.showFetchCacheLog) console.log('GenshinManager: Start update cache.'); const newVersionText = yield this.checkGitUpdate(); if (!this.gameVersion) return; if (newVersionText && this.option.autoFetchLatestAssetsByCron) { this._assetEventEmitter.emit('BEGIN_UPDATE_ASSETS', newVersionText); if (this.option.showFetchCacheLog) { console.log(`GenshinManager: New Asset found. Update Assets. GameVersion: ${newVersionText}`); } yield this.fetchAssetFolder(this.excelBinOutputFolderPath, Object.values(types_1.ExcelBinOutputs)); // eslint-disable-next-line no-empty while (yield this.setExcelBinOutputToCache(this.excelBinOutputAllKeys)) { } this.createTextHashes(); const textMapFileNames = this.option.downloadLanguages .map((key) => types_1.TextMapLanguage[key]) .flat(); yield this.fetchAssetFolder(this.textMapFolderPath, textMapFileNames); this._assetEventEmitter.emit('END_UPDATE_ASSETS', newVersionText); if (this.option.showFetchCacheLog) console.log('GenshinManager: Set cache.'); } else { if (this.option.showFetchCacheLog) { console.log(`GenshinManager: No new Asset found. Set cache. GameVersion: ${this.gameVersion}`); } } this._assetEventEmitter.emit('BEGIN_UPDATE_CACHE', this.gameVersion); // eslint-disable-next-line no-empty while (yield this.setExcelBinOutputToCache(this.useExcelBinOutputKeys)) { } this.createTextHashes(); // eslint-disable-next-line no-empty while (yield this.setTextMapToCache(this.option.defaultLanguage)) { } this._assetEventEmitter.emit('END_UPDATE_CACHE', this.gameVersion); if (this.option.showFetchCacheLog) console.log('GenshinManager: Finish update cache and set cache.'); }); } /** * Check gitlab for new commits * @returns New assets version text or undefined */ static checkGitUpdate() { return __awaiter(this, void 0, void 0, function* () { ; [ this.option.assetCacheFolderPath, this.excelBinOutputFolderPath, this.textMapFolderPath, ].map((folderPath) => { if (!fs_1.default.existsSync(folderPath)) fs_1.default.mkdirSync(folderPath); }); const oldCommits = fs_1.default.existsSync(this.commitFilePath) ? JSON.parse(fs_1.default.readFileSync(this.commitFilePath, { encoding: 'utf8', })) : null; yield this.downloadJsonFile(this.GIT_REMOTE_API_URL, this.commitFilePath); const newCommits = JSON.parse(fs_1.default.readFileSync(this.commitFilePath, { encoding: 'utf8', })); this.nowCommitId = newCommits[0].id; if (oldCommits && newCommits[0].id === oldCommits[0].id) { return undefined; } else { const versionTexts = /OSRELWin(\d+\.\d+\.\d+)_/.exec(newCommits[0].title); if (!versionTexts || versionTexts.length < 2) return '?.?.?'; return versionTexts[1]; } }); } /** * Create TextHashes to cache */ static createTextHashes() { this.textHashes.clear(); this.cachedExcelBinOutput.forEach((excelBin) => { ; Object.values(excelBin.get()).forEach((obj) => { Object.values(obj).forEach((value) => { const obj = value; Object.keys(obj).forEach((key) => { if (/TextMapHash/g.exec(key)) { const hash = obj[key]; this.textHashes.add(hash); } if (key === 'paramDescList') { const hashes = obj[key]; hashes.forEach((hash) => this.textHashes.add(hash)); } }); }); Object.keys(obj).forEach((key) => { if (/TextMapHash/g.exec(key)) { const hash = obj[key]; this.textHashes.add(hash); } if (key === 'tips') { const hashes = obj[key]; hashes.forEach((hash) => this.textHashes.add(hash)); } }); }); }); } /** * Re download text map * @param language Country code */ static reDownloadTextMap(language) { return __awaiter(this, void 0, void 0, function* () { const textMapFileNames = types_1.TextMapLanguage[language]; yield this.setExcelBinOutputToCache(this.excelBinOutputAllKeys); this.createTextHashes(); yield this.fetchAssetFolder(this.textMapFolderPath, [...textMapFileNames], true); }); } /** * Re download all excel bin output */ static reDownloadAllExcelBinOutput() { return __awaiter(this, void 0, void 0, function* () { yield this.fetchAssetFolder(this.excelBinOutputFolderPath, Object.values(types_1.ExcelBinOutputs), true); }); } /** * Fetch asset folder from gitlab * @param folderPath Folder path * @param files File names * @param isRetry Is Retry */ static fetchAssetFolder(folderPath_1, files_1) { return __awaiter(this, arguments, void 0, function* (folderPath, files, isRetry = false) { if (!isRetry) { fs_1.default.rmdirSync(folderPath, { recursive: true }); fs_1.default.mkdirSync(folderPath); } const gitFolderName = path.relative(this.option.assetCacheFolderPath, folderPath); const consoleFolderName = gitFolderName.slice(0, 8); const progressBar = this.option.showFetchCacheLog ? new cliProgress.SingleBar({ hideCursor: true, format: `GenshinManager: Downloading ${consoleFolderName}...\t [{bar}] {percentage}% |ETA: {eta}s| {value}/{total} files`, }) : undefined; if (progressBar) progressBar.start(files.length, 0); yield Promise.all(files.map((fileName) => __awaiter(this, void 0, void 0, function* () { const url = [ this.GIT_REMOTE_RAW_BASE_URL, this.nowCommitId, gitFolderName, fileName, ].join('/'); const filePath = path.join(folderPath, fileName); yield this.downloadJsonFile(url, filePath); if (progressBar) progressBar.increment(); }))); if (progressBar) progressBar.stop(); }); } /** * Download json file from URL and write to downloadFilePath * @param url URL * @param downloadFilePath Download file path */ static downloadJsonFile(url, downloadFilePath) { return __awaiter(this, void 0, void 0, function* () { const res = yield fetch(url, this.option.fetchOption); if (!res.body) throw new BodyNotFoundError_1.BodyNotFoundError(url); const writeStream = fs_1.default.createWriteStream(downloadFilePath, { highWaterMark: 1 * 1024 * 1024, }); const language = path .basename(downloadFilePath) .split('.')[0]; if ('TextMap' === path.basename(path.dirname(downloadFilePath))) { yield (0, promises_1.pipeline)(new ReadableStreamWrapper_1.ReadableStreamWrapper(res.body.getReader()), new TextMapTransform_1.TextMapTransform(language, this.textHashes), writeStream); } else { yield (0, promises_1.pipeline)(new ReadableStreamWrapper_1.ReadableStreamWrapper(res.body.getReader()), writeStream); } }); } } exports.AssetCacheManager = AssetCacheManager; /** * Cached text map * @deprecated This property is deprecated because it is used to pass data to each class * @key Text hash * @value Text */ AssetCacheManager._cachedTextMap = new Map(); /** * Asset event emitter * @deprecated This property is deprecated because it is used to pass data to each class */ AssetCacheManager._assetEventEmitter = new events_1.default(); AssetCacheManager.GIT_REMOTE_API_URL = 'https://gitlab.com/api/v4/projects/53216109/repository/commits?per_page=1'; AssetCacheManager.GIT_REMOTE_RAW_BASE_URL = 'https://gitlab.com/Dimbreath/AnimeGameData/-/raw'; /** * Cached text map * @key ExcelBinOutput name * @value Cached excel bin output */ AssetCacheManager.cachedExcelBinOutput = new Map(); AssetCacheManager.textHashes = new Set(); AssetCacheManager.useExcelBinOutputKeys = new Set();