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
JavaScript
;
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();