39 Commits
1.4 ... 1.7.0

Author SHA1 Message Date
HolographicHat
b76e8e3cd2 Fix 2022-05-31 11:31:00 +08:00
HolographicHat
c12d376e21 Support rel2.7.0 and bump version to 1.7 2022-05-31 11:05:33 +08:00
HolographicHat
d285b1c999 Update message 2022-05-29 22:33:34 +08:00
HolographicHat
89ab4408d6 Update 2022-05-23 12:14:24 +08:00
HolographicHat
bf01214971 Merge pull request #12 from HolographicHat/no-client
Update
2022-05-19 22:07:08 +08:00
HolographicHat
1900937a50 Update 2022-05-19 22:04:23 +08:00
HolographicHat
98a03a0910 Fix #11 2022-04-25 16:16:13 +08:00
HolographicHat
82ba6120e4 snap update 2022-04-23 16:07:13 +08:00
HolographicHat
6cb07d274a UIAF: Update field name 2022-04-21 19:40:17 +08:00
HolographicHat
4acaa516bb UIAF Support 2022-04-21 15:11:05 +08:00
HolographicHat
b5caba2f7a Merge pull request #10 from gaoyifan/master
Fix paimon.moe exporting format
2022-04-18 18:42:43 +08:00
Yifan Gao
ebb11bde9c Fix paimon.moe exporting format 2022-04-18 04:29:34 +08:00
HolographicHat
4417feab53 encode uri to avoid charset problem 2022-04-17 23:46:11 +08:00
HolographicHat
28080dbafd fix 2022-04-17 22:54:35 +08:00
HolographicHat
a8b6419157 Update README.md 2022-04-16 21:52:50 +08:00
HolographicHat
7f8296c3dc fix bug and bump version to 1.6 2022-04-16 21:07:00 +08:00
HolographicHat
37382b28e0 auto package script 2022-04-16 20:59:32 +08:00
HolographicHat
a46e49722f auto package script 2022-04-16 00:35:41 +08:00
HolographicHat
700cbbb86d update 2022-04-13 20:54:00 +08:00
HolographicHat
6a23153f70 add oversea(NA-SiliconValley) api 2022-04-13 19:34:14 +08:00
HolographicHat
75e3cd848f update 2022-04-12 20:00:12 +08:00
HolographicHat
96912d3da7 support gbk encoding 2022-04-10 14:18:01 +08:00
HolographicHat
02b034cc48 Merge remote-tracking branch 'origin/master' 2022-04-10 13:03:08 +08:00
HolographicHat
00898d11cb fix 2022-04-10 13:02:36 +08:00
HolographicHat
7cf03ad905 Update README.md 2022-04-10 02:03:08 +08:00
HolographicHat
7d9e5bc218 Update README.md 2022-04-09 20:37:27 +08:00
HolographicHat
60f0d8d23b fix appcenter log upload error 2022-04-09 19:33:52 +08:00
HolographicHat
1e15a49667 new format 2022-04-09 16:35:46 +08:00
HolographicHat
4528af7235 rollback 2022-04-09 00:30:16 +08:00
HolographicHat
9850d1dbe4 bump version to 1.5 2022-04-09 00:19:15 +08:00
HolographicHat
b1f307de83 snapgenshin format update 2022-04-08 23:56:43 +08:00
HolographicHat
bea596b906 fetch achievement data from self api server 2022-04-08 23:56:20 +08:00
HolographicHat
dd582437bc use fastest cdn to fetch data 2022-04-07 22:50:36 +08:00
HolographicHat
ea168ce96b support snapgenshin 2022-04-07 21:56:27 +08:00
HolographicHat
90ab4dafe9 speed test file 2022-04-07 21:26:09 +08:00
HolographicHat
55f1ce3d55 auto export to cocogoat 2022-04-07 21:04:49 +08:00
HolographicHat
0e51e080d4 no hard code loc 2022-04-07 20:13:42 +08:00
HolographicHat
01ab053d7d Update README.md 2022-04-06 22:18:26 +08:00
HolographicHat
41af9c7cdb better pause impl 2022-04-06 13:05:20 +08:00
26 changed files with 453 additions and 382 deletions

11
.gitignore vendored
View File

@@ -1,11 +1,6 @@
cache
cert
config.json
out.*
node_modules
.idea
app*.exe
export*.json
secret.js
generated
node_modules
config.json
package-lock.json
native.node

View File

@@ -1,11 +1,15 @@
# 原神成就导出工具
<div align="center">
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/genshin-achievement-export?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/genshin-achievement-export/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
# YaeAchievement
- 支持导出所有成就
- 支持官服B服与国际服
- 没有窗口大小游戏或语言等要求
- 更快、更准
![GitHub](https://img.shields.io/badge/License-GPL--3.0-brightgreen?style=flat-square) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/HolographicHat/genshin-achievement-export?color=brightgreen&label=Release&style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/HolographicHat/genshin-achievement-export?label=Issues&style=flat-square) ![Downloads](https://img.shields.io/github/downloads/HolographicHat/genshin-achievement-export/total?color=brightgreen&label=Downloads&style=flat-square) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)
</div>
- 支持导出所有类别的成就
- 支持官服,渠道服与国际服
- 支持导出至[椰羊](https://cocogoat.work/achievement)、[SnapGenshin](https://github.com/DGP-Studio/Snap.Genshin)、[Paimon.moe](https://paimon.moe/achievement/)、[Seelie.me](https://seelie.me/achievements)和表格文件(csv)
- 没有窗口大小、游戏语言等要求
## 使用说明
**打开程序前需要关闭正在运行的原神主程序**
@@ -13,20 +17,7 @@
![alt](https://upload-bbs.mihoyo.com/upload/2022/04/06/165631158/e540a5a6d50cd5fdee19665435548e00_514247033566841954.jpg)
### Windows7
系统变量添加名为```NODE_SKIP_PLATFORM_CHECK```的变量并将值设为```1```
### 自定义代理(可选)
配置文件内添加proxy字段详细请参看[Axios-请求配置](https://axios-http.com/zh/docs/req_config)
```json
{
"path": [],
"offlineResource": false,
"customCDN": "",
"proxy": {
"protocol": "http",
"host": "127.0.0.1",
"port": 7890
}
}
```
[[?]如何添加环境变量](https://www.bing.com/search?q=windows+7+环境变量)
## 下载地址
[releases/latest](https://github.com/HolographicHat/genshin-achievement-export/releases/latest)
@@ -35,7 +26,7 @@
[issues](https://github.com/HolographicHat/genshin-achievement-export/issues)或[QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
## 常见问题
0. Q: 程序异常退出或被强行终止后,原神启动时报错: 无法连接网络(4201)
0. Q: 程序异常退出或被强行终止后,程序报错:网络错误(22-1) 或 原神启动时报错: 无法连接网络(4201)
A: 用文本编辑器打开```C:\Windows\System32\drivers\etc\hosts```,删除```127.0.0.1 dispatch**global.yuanshen.com```后保存,重启原神
1. Q: 原神启动时报错: 数据异常(31-4302)
@@ -48,4 +39,7 @@
A: 执行命令或修改hosts引发相关代码可在./utils.js,./appcenter.js下找到
4. Q: 原神进门后没有自动退出,程序输出停留在“加载完成”
A: 关闭Shadowsocks全局代理
A: 关闭代理后重试
5. Q: 网络错误,请检查网络后重试 (21-x)
A: 如果你是校园网用户使用流量上网并重试如果你是海外用户打开config.json将oversea_api的值设置为true并重试

29
app.js
View File

@@ -1,34 +1,35 @@
const proxy = require("udp-proxy")
const cp = require("child_process")
const rs = require("./regionServer")
const appcenter = require("./appcenter")
const regionServer = require("./regionServer")
const cloud = require("./generated/secret")
const {
initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkCDN, checkUpdate,
initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkUpdate,
brotliCompressSync, brotliDecompressSync, checkGameIsRunning, checkPortIsUsing
} = require("./utils")
const { exportData } = require("./export")
const { enablePrivilege } = require("./native")
const { enablePrivilege, pause } = require("./generated/native")
const onExit = () => {
setupHost(true)
console.log("按任意键退出")
cp.execSync("pause > nul", { stdio: "inherit" })
pause()
};
(async () => {
try {
process.once("SIGHUP", () => setupHost(true))
process.on("SIGHUP", () => setupHost(true))
process.on("unhandledRejection", (reason, promise) => {
console.log("Unhandled Rejection at: ", promise, "\n0Reason:", reason)
})
process.once("uncaughtException", (err, origin) => {
process.on("uncaughtException", (err, origin) => {
appcenter.uploadError(err, true)
console.log(err)
console.log(`Origin: ${origin}`)
process.exit(1)
})
process.once("exit", onExit)
process.once("SIGINT", onExit)
process.on("exit", onExit)
process.on("SIGINT", onExit)
try {
enablePrivilege()
} catch (e) {
@@ -37,17 +38,14 @@ const onExit = () => {
}
appcenter.startup()
let conf = await initConfig()
cloud.init(conf)
checkPortIsUsing()
checkGameIsRunning()
log("检查更新")
await checkUpdate()
checkCDN().then(_ => debug("CDN check success.")).catch(reason => {
console.log(reason)
process.exit(113)
})
let gameProcess
let unexpectedExit = true
rs.create(conf,() => {
regionServer.create(conf,() => {
setupHost()
gameProcess = cp.execFile(conf.executable, { cwd: conf.path },err => {
if (err !== null && !err.killed) {
@@ -125,7 +123,7 @@ const onExit = () => {
log(`请求ID: ${response.headers["x-api-requestid"]}`)
log("请联系开发者以获取帮助")
} else {
const proto = await decodeProto(data,"AllAchievement")
const proto = await decodeProto(data, "Notify1")
await exportData(proto)
}
process.exit(0)
@@ -147,9 +145,10 @@ const onExit = () => {
switch(buf.readUInt32BE(0)) {
case 325:
createMonitor()
debug("服务端握手应答")
debug("Connection established.")
break
case 404:
debug("Connection terminated.")
lastRecvTimestamp = parseInt(Date.now() / 1000) - 2333
break
default:

View File

@@ -1,7 +1,7 @@
const axios = require("axios")
const crypto = require("crypto")
const { version } = require("./version")
const { getDeviceID, getDeviceInfo } = require("./native")
const { getDeviceID, getDeviceInfo } = require("./generated/native")
const getTimestamp = (d = new Date()) => {
const p = i => i.toString().padStart(2, "0")
@@ -30,15 +30,17 @@ const device = (() => {
const upload = () => {
if (queue.length > 0) {
const data = JSON.stringify({ "logs": queue })
const logs = []
for (let i = 0; i <= queue.length; i++) {
logs.push(queue.pop())
}
const data = JSON.stringify({"logs": logs})
axios.post("https://in.appcenter.ms/logs?api-version=1.0.0", data,{
headers: {
"App-Secret": key,
"Install-ID": install
}
}).then(_ => {
queue.length = 0
}).catch(_ => {})
}).catch(_ => {}).then()
}
}
@@ -79,6 +81,19 @@ const uploadError = (err, fatal) => {
upload()
}
const uploadEvent = (name, prop) => {
const content = {
type: "event",
id: crypto.randomUUID(),
sid: session,
name: name,
properties: prop,
timestamp: getTimestamp(),
device: device
}
queue.push(content)
}
const startup = () => {
queue.push({
type: "startService",
@@ -93,9 +108,9 @@ const startup = () => {
device: device
})
upload()
setInterval(() => upload(), 10000)
setInterval(() => upload(), 5000)
}
module.exports = {
startup, upload, uploadError
startup, upload, uploadError, uploadEvent
}

131
export.js
View File

@@ -1,12 +1,14 @@
const fs = require("fs")
const axios = require("axios")
const readline = require("readline")
const { loadCache, log } = require("./utils")
const { copyToClipboard } = require("./native")
const { version } = require("./version")
const { loadCache, log, openUrl } = require("./utils")
const { checkSnapFastcall, copyToClipboard } = require("./generated/native")
const exportToSeelie = proto => {
const out = { achievements: {} }
proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => {
out.achievements[id] = { done: true }
proto.list.filter(a => a.status === 3 || a.status === 2).forEach(({id}) => {
out.achievements[id === 81222 ? 81219 : id] = { done: true }
})
const fp = `./export-${Date.now()}-seelie.json`
fs.writeFileSync(fp, JSON.stringify(out))
@@ -15,65 +17,71 @@ const exportToSeelie = proto => {
const exportToPaimon = async proto => {
const out = { achievement: {} }
const achTable = new Map()
const excel = await loadCache("ExcelBinOutput/AchievementExcelConfigData.json")
excel.forEach(({GoalId, Id}) => {
achTable.set(Id, GoalId === undefined ? 0 : GoalId)
})
proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => {
const gid = achTable.get(id)
const data = await loadCache()
proto.list.filter(a => a.status === 3 || a.status === 2).forEach(({id}) => {
const gid = data["a"][id]["g"]
if (out.achievement[gid] === undefined) {
out.achievement[gid] = {}
}
out.achievement[gid][id] = true
out.achievement[gid][id === 81222 ? 81219 : id] = true
})
const fp = `./export-${Date.now()}-paimon.json`
fs.writeFileSync(fp, JSON.stringify(out))
log(`导出为文件: ${fp}`)
}
const exportToCocogoat = async proto => {
const UIAF = proto => {
const out = {
value: {
achievements: []
}
info: {
export_app: "YaeAchievement",
export_timestamp: Date.now(),
export_app_version: version.name,
uiaf_version: "v1.0"
},
list: []
}
const achTable = new Map()
const preStageAchievementIdList = []
const excel = await loadCache("ExcelBinOutput/AchievementExcelConfigData.json")
excel.forEach(({GoalId, Id, PreStageAchievementId}) => {
if (PreStageAchievementId !== undefined) {
preStageAchievementIdList.push(PreStageAchievementId)
}
achTable.set(Id, GoalId === undefined ? 0 : GoalId)
})
const p = i => i.toString().padStart(2, "0")
const getDate = ts => {
const d = new Date(parseInt(`${ts}000`))
return `${d.getFullYear()}/${p(d.getMonth()+1)}/${p(d.getDate())}`
}
proto.list.filter(achievement => achievement.status === 3).forEach(({current, finishTimestamp, id, require}) => {
out.value.achievements.push({
proto.list.filter(a => a.status === 3 || a.status === 2).forEach(({id, finishTimestamp, current}) => {
out.list.push({
id: id,
status: current === undefined || current === 0 || preStageAchievementIdList.includes(id) ? `${require}/${require}` : `${current}/${require}`,
categoryId: achTable.get(id),
date: getDate(finishTimestamp)
timestamp: finishTimestamp,
current: current
})
})
const ts = Date.now()
const json = JSON.stringify(out,null,2)
copyToClipboard(json)
const fp = `./export-${ts}-cocogoat.json`
fs.writeFileSync(fp, json)
log(`导出内容已复制到剪贴板`)
return out
}
const exportToSnapGenshin = async proto => {
if (checkSnapFastcall()) {
const result = UIAF(proto)
const json = JSON.stringify(result)
copyToClipboard(json)
openUrl(`snapgenshin://achievement/import/uiaf`)
log("在 SnapGenshin 进行下一步操作")
} else {
log("请更新 SnapGenshin 后重试")
}
}
const exportToCocogoat = async proto => {
const result = UIAF(proto)
const response = await axios.post(`https://77.cocogoat.work/v1/memo?source=${encodeURI("全部成就")}`, result).catch(_ => {
console.log("网络错误,请检查网络后重试 (26-1)")
process.exit(261)
})
if (response.status !== 201) {
console.log(`API StatusCode 错误,请联系开发者以获取帮助 (26-2-${response.status})`)
process.exit(262)
}
const retcode = openUrl(`https://cocogoat.work/achievement?memo=${response.data.key}`)
if (retcode > 32) {
log("在浏览器内进行下一步操作")
} else {
log(`打开此链接以进行下一步操作: https://cocogoat.work/achievement?memo=${response.data.key}`)
}
}
const exportToCsv = async proto => {
const excel = await loadCache("achievement-data.json", "HolographicHat/genshin-achievement-export")
const achievementMap = new Map()
excel["achievement"].forEach(obj => {
achievementMap.set(parseInt(obj.id), obj)
})
const data = await loadCache()
const outputLines = ["ID,状态,特辑,名称,描述,当前进度,目标进度,完成时间"]
const getStatusText = i => {
switch (i) {
@@ -91,15 +99,15 @@ const exportToCsv = async proto => {
const bl = [84517]
proto.list.forEach(({current, finishTimestamp, id, status, require}) => {
if (!bl.includes(id)) {
const desc = achievementMap.get(id) === undefined ? (() => {
const curAch = data["a"][id] === undefined ? (() => {
console.log(`Error get id ${id} in excel`)
return {
goal: "未知",
name: "未知",
desc: "未知"
g: "未知",
n: "未知",
d: "未知"
}
})() : achievementMap.get(id)
outputLines.push(`${id},${getStatusText(status)},${excel.goal[desc.goal]},${desc.name},${desc.desc},${status !== 1 ? current === 0 ? require : current : current},${require},${status === 1 ? "" : getTime(finishTimestamp)}`)
})() : data["a"][id]
outputLines.push(`${id},${getStatusText(status)},${data["g"][curAch.g]},${curAch.n},${curAch.d},${status !== 1 ? current === 0 ? require : current : current},${require},${status === 1 ? "" : getTime(finishTimestamp)}`)
}
})
const fp = `./export-${Date.now()}.csv`
@@ -115,16 +123,29 @@ const exportData = async proto => {
const question = (query) => new Promise(resolve => {
rl.question(query, resolve)
})
const chosen = await question("导出至: \n[0] 椰羊 (https://cocogoat.work/achievement)\n[1] Paimon.moe\n[2] Seelie.me\n[3] 表格文件 (默认)\n输入一个数字(0-3): ")
const chosen = await question(
[
"导出至: ",
"[0] 椰羊 (https://cocogoat.work/achievement)",
"[1] SnapGenshin",
"[2] Paimon.moe",
"[3] Seelie.me",
"[4] 表格文件 (默认)",
"输入一个数字(0-4): "
].join("\n")
)
rl.close()
switch (chosen) {
switch (chosen.trim()) {
case "0":
await exportToCocogoat(proto)
break
case "1":
await exportToPaimon(proto)
await exportToSnapGenshin(proto)
break
case "2":
await exportToPaimon(proto)
break
case "3":
await exportToSeelie(proto)
break
case "raw":

7
native.d.ts vendored
View File

@@ -1,7 +0,0 @@
export function selectGameExecutable(): string
export function checkGameIsRunning(processName: string): boolean
export function whoUseThePort(port: number): object
export function copyToClipboard(value: string): any
export function enablePrivilege(): any
export function getDeviceInfo(): any
export function getDeviceID(): string

1
native/.gitignore vendored
View File

@@ -3,3 +3,4 @@ CMakeLists.txt
cmake-build-debug
node_modules
build
src/homu.cpp

View File

@@ -10,6 +10,7 @@
"src/wmi/wmi.cpp",
"src/wmi/wmi.hpp",
"src/wmi/unistd.h",
"src/VMProtectSDK.h",
"src/wmi/wmiresult.cpp",
"src/wmi/wmiresult.hpp",
"src/wmi/wmiclasses.hpp",
@@ -33,10 +34,15 @@
"AdditionalOptions": [
"-std:c++latest",
"-DUNICODE",
"-sdl"
"-sdl",
"-O2",
"-Ot",
"-Oi",
"-GL",
"-Gw"
]
}
}
}
]
}
}

View File

@@ -3,7 +3,9 @@
"version": "1.0.0",
"description": "",
"scripts": {
"build": "node-gyp rebuild && copy .\\build\\Release\\native.node ..\\genshin-export\\"
"build": "node-gyp build && copy .\\build\\Release\\native.node ..\\genshin-export\\generated",
"rebuild": "node-gyp rebuild && copy .\\build\\Release\\native.node ..\\genshin-export\\generated",
"build-for-win7": "node-gyp rebuild --target=v14.17.0 && copy .\\build\\Release\\native.node ..\\genshin-export\\generated"
},
"gypfile": true,
"devDependencies": {

View File

@@ -6,9 +6,10 @@
#include <Windows.h>
#include <TlHelp32.h>
using std::string, std::wstring, std::cout, std::to_string;
using std::string, std::wstring, std::cout, std::to_string, std::unique_ptr, std::make_unique;
using Napi::Object, Napi::Env, Napi::Function, Napi::Value, Napi::CallbackInfo, Napi::TypeError, Napi::Error;
typedef unsigned char byte;
typedef unsigned int ui;
typedef unsigned long ul;
typedef unsigned long long ull;

11
native/src/homu.h Normal file
View File

@@ -0,0 +1,11 @@
//
// Created by holog on 2022/5/20.
//
#include "define.h"
Value Initialize(const CallbackInfo &info);
#ifndef GENSHIN_EXPORT_NATIVE_HOMU_H
#define GENSHIN_EXPORT_NATIVE_HOMU_H
#endif //GENSHIN_EXPORT_NATIVE_HOMU_H

View File

@@ -1,8 +1,10 @@
//#include "homu.h"
#include "utils.h"
#include "define.h"
#include "wmi/wmi.hpp"
#include "wmi/wmiclasses.hpp"
#include "registry/registry.hpp"
#include <conio.h>
#include <iphlpapi.h>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"iphlpapi.lib")
@@ -11,7 +13,7 @@ using Wmi::Win32_ComputerSystem, Wmi::Win32_OperatingSystem, Wmi::retrieveWmi;
namespace native {
Value checkGameIsRunning(const CallbackInfo &info) {
Value CheckGameIsRunning(const CallbackInfo &info) {
Env env = info.Env();
if (info.Length() != 1 || !info[0].IsString()) {
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
@@ -33,7 +35,7 @@ namespace native {
return Napi::Boolean::New(env, isRunning);
}
Value selectGameExecutable(const CallbackInfo &info) {
Value SelectGameExecutable(const CallbackInfo &info) {
Env env = info.Env();
Napi::String path;
if (OpenFile(env, path) != ERROR_SUCCESS) {
@@ -43,7 +45,7 @@ namespace native {
return path;
}
Value whoUseThePort(const CallbackInfo &info) {
Value WhoUseThePort(const CallbackInfo &info) {
Env env = info.Env();
if (info.Length() != 1 || !info[0].IsNumber()) {
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
@@ -88,7 +90,7 @@ namespace native {
return ret;
}
Value getDeviceID(const CallbackInfo &info) {
Value GetDeviceID(const CallbackInfo &info) {
Env env = info.Env();
wstring wd;
if (RegUtils::GetString(HKEY_CURRENT_USER, L"SOFTWARE\\miHoYoSDK", L"MIHOYOSDK_DEVICE_ID", wd) != ERROR_SUCCESS) {
@@ -98,13 +100,13 @@ namespace native {
return Napi::String::New(env, id.substr(0, 8) + "-" + id.substr(8, 4) + "-" + id.substr(12, 4) + "-" + id.substr(16, 4) + "-" + id.substr(20, 12));
}
Value getDeviceInfo(const CallbackInfo &info) {
Value GetDeviceInfo(const CallbackInfo &info) {
Env env = info.Env();
HDC desktop = GetDC(nullptr);
int sw = GetDeviceCaps(desktop, DESKTOPHORZRES);
int sh = GetDeviceCaps(desktop, DESKTOPVERTRES);
ReleaseDC(nullptr, desktop);
DWORD buildNum = RegUtils::GetInt(env, HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"UBR");
DWORD buildNum = RegUtils::GetInt(env, HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"UBR", 0);
wstring locale;
if (RegUtils::GetString(HKEY_CURRENT_USER, L"Control Panel\\International", L"LocaleName", locale) != ERROR_SUCCESS) {
locale = L"zh-CN";
@@ -139,13 +141,34 @@ namespace native {
return obj;
}
Value enablePrivilege(const CallbackInfo &info) {
Value EnablePrivilege(const CallbackInfo &info) {
Env env = info.Env();
EnablePrivilege(env, L"SeDebugPrivilege");
HANDLE hToken;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) {
Error::New(env, "OpenProcessToken error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return env.Undefined();
}
TOKEN_PRIVILEGES tp;
ZeroMemory(&tp, sizeof(tp));
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!LookupPrivilegeValue(nullptr, L"SeDebugPrivilege", &tp.Privileges[0].Luid)) {
Error::New(env, "LookupPrivilegeValue error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return env.Undefined();
}
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr)) {
Error::New(env, "AdjustTokenPrivileges error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return env.Undefined();
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
Error::New(env, "The token does not have the specified privilege.").ThrowAsJavaScriptException();
return env.Undefined();
}
CloseHandle(hToken);
return env.Undefined();
}
Value copyToClipboard(const CallbackInfo &info) {
Value CopyToClipboard(const CallbackInfo &info) {
Env env = info.Env();
string text = info[0].As<Napi::String>().Utf8Value();
if (OpenClipboard(nullptr)) {
@@ -161,14 +184,64 @@ namespace native {
return env.Undefined();
}
Value Pause(const CallbackInfo &info) {
while(!_kbhit()) {
Sleep(10);
}
return info.Env().Undefined();
}
Value OpenUrl(const CallbackInfo &info) {
Env env = info.Env();
wstring url = StringToWString(info[0].As<Napi::String>().Utf8Value());
HINSTANCE retcode = ShellExecute(GetConsoleWindow(), L"open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
return Napi::Number::New(env, (INT_PTR)retcode); // NOLINT(cppcoreguidelines-narrowing-conversions)
}
Value CheckSnapFastcall(const CallbackInfo &info) {
Env env = info.Env();
wstring queryResult;
RegUtils::GetString(HKEY_CLASSES_ROOT, L"snapgenshin", L"", queryResult);
return Napi::Boolean::New(env, wcscmp(queryResult.c_str(), L"URL:snapgenshin") == 0);
}
Value GetMACAddress(const CallbackInfo &info) {
Env env = info.Env();
ULONG ulOutBufLen = sizeof(IP_ADAPTER_INFO);
auto pAdapterInfo = (IP_ADAPTER_INFO *) malloc(ulOutBufLen);
if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) != ERROR_SUCCESS) {
pAdapterInfo = (IP_ADAPTER_INFO *) malloc(ulOutBufLen);
if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) != ERROR_SUCCESS) { return env.Undefined(); }
}
PIP_ADAPTER_INFO pAdapter = pAdapterInfo;
while (pAdapter) {
if (pAdapter->Address[0] == 0x00) {
pAdapter = pAdapter->Next;
continue;
}
char *result;
ToHex((char *)pAdapter->Address, 6, &result);
auto ret = Napi::String::New(env, result);
free(result);
free(pAdapterInfo);
return ret;
}
return env.Undefined();
}
Object init(Env env, Object exports) {
exports.Set("getDeviceID", Function::New(env, getDeviceID));
exports.Set("getDeviceInfo", Function::New(env, getDeviceInfo));
exports.Set("whoUseThePort", Function::New(env, whoUseThePort));
exports.Set("copyToClipboard", Function::New(env, copyToClipboard));
exports.Set("enablePrivilege", Function::New(env, enablePrivilege));
exports.Set("checkGameIsRunning", Function::New(env, checkGameIsRunning));
exports.Set("selectGameExecutable", Function::New(env, selectGameExecutable));
exports.Set("pause", Function::New(env, Pause));
exports.Set("openUrl", Function::New(env, OpenUrl));
//exports.Set("homuInit", Function::New(env, Initialize));
exports.Set("getDeviceID", Function::New(env, GetDeviceID));
exports.Set("getMACAddress", Function::New(env, GetMACAddress));
exports.Set("getDeviceInfo", Function::New(env, GetDeviceInfo));
exports.Set("whoUseThePort", Function::New(env, WhoUseThePort));
exports.Set("copyToClipboard", Function::New(env, CopyToClipboard));
exports.Set("enablePrivilege", Function::New(env, EnablePrivilege));
exports.Set("checkSnapFastcall", Function::New(env, CheckSnapFastcall));
exports.Set("checkGameIsRunning", Function::New(env, CheckGameIsRunning));
exports.Set("selectGameExecutable", Function::New(env, SelectGameExecutable));
return exports;
}

View File

@@ -7,13 +7,12 @@
namespace RegUtils {
DWORD GetInt(Env env, HKEY hKey, const wstring &path, const wstring &value) {
DWORD GetInt(Env env, HKEY hKey, const wstring &path, const wstring &value, DWORD defaultValue) {
DWORD data = 0;
DWORD size = sizeof(DWORD);
LSTATUS retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_DWORD, nullptr, &data, &size);
if (retcode != ERROR_SUCCESS) {
Error::New(env, "RegGetValue error: " + to_string(retcode)).ThrowAsJavaScriptException();
return 0;
return defaultValue;
}
return data;
}

View File

@@ -1,26 +1,22 @@
#include "utils.h"
#include "define.h"
wstring StringToWString(const string &src) {
wstring result;
int len = MultiByteToWideChar(CP_ACP, 0, src.c_str(), src.size(), nullptr, 0); // NOLINT(cppcoreguidelines-narrowing-conversions)
auto *buffer = new WCHAR[len + 1];
MultiByteToWideChar(CP_ACP, 0, src.c_str(), src.size(), buffer, len); // NOLINT(cppcoreguidelines-narrowing-conversions)
buffer[len] = '\0';
result.append(buffer);
wstring StringToWString(const string &src, UINT codePage) {
int len = MultiByteToWideChar(codePage, 0, src.c_str(), -1, nullptr, 0);
auto *buffer = new WCHAR[len];
MultiByteToWideChar(codePage, 0, src.c_str(), -1, buffer, len);
wstring strTemp(buffer);
delete[] buffer;
return result;
return strTemp;
}
string WStringToString(const wstring &src) {
string result;
int len = WideCharToMultiByte(CP_ACP, 0, src.c_str(), src.size(), nullptr, 0, nullptr, nullptr); // NOLINT(cppcoreguidelines-narrowing-conversions)
char *buffer = new char[len + 1];
WideCharToMultiByte(CP_ACP, 0, src.c_str(), src.size(), buffer, len, nullptr, nullptr); // NOLINT(cppcoreguidelines-narrowing-conversions)
buffer[len] = '\0';
result.append(buffer);
string WStringToString(const wstring &src, UINT codePage) {
int len = WideCharToMultiByte(codePage, 0, src.c_str(), -1, nullptr, 0, nullptr, nullptr);
auto *buffer = new CHAR[len];
WideCharToMultiByte(codePage, 0, src.c_str(), -1, buffer, len, nullptr, nullptr);
string strTemp(buffer);
delete[] buffer;
return result;
return strTemp;
}
void Log(Env env, const string &msg) {
@@ -33,6 +29,18 @@ void Log(Env env, const wstring &msg) {
logFunc.Call({ Napi::String::New(env, WStringToString(msg)) });
}
char* ToHex(const char *bin, int binsz, char **result) {
char hexStr[] = "0123456789ABCDEF";
if (!(*result = (char *) malloc(binsz * 2 + 1))) return nullptr;
(*result)[binsz * 2] = 0;
if (!binsz) return nullptr;
for (int i = 0; i < binsz; i++) {
(*result)[i * 2 + 0] = hexStr[(bin[i] >> 4) & 0x0F];
(*result)[i * 2 + 1] = hexStr[(bin[i]) & 0x0F];
}
return *result;
}
LSTATUS OpenFile(Env env, Napi::String &result, HWND parent) {
OPENFILENAME open;
ZeroMemory(&open, sizeof(open));
@@ -46,35 +54,9 @@ LSTATUS OpenFile(Env env, Napi::String &result, HWND parent) {
open.lpstrFilter = L"国服/国际服主程序 (YuanShen/GenshinImpact.exe)\0YuanShen.exe;GenshinImpact.exe\0";
open.lStructSize = sizeof(open);
if(GetOpenFileName(&open)) {
result = Napi::String::New(env, WStringToString(file));
result = GetACP() == 936 ? Napi::String::New(env, WStringToString(file, CP_UTF8)) : Napi::String::New(env, WStringToString(file));
return ERROR_SUCCESS;
} else {
return ERROR_ERRORS_ENCOUNTERED;
}
}
BOOL EnablePrivilege(Env env, const wstring &name) {
HANDLE hToken;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) {
Error::New(env, "OpenProcessToken error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return FALSE;
}
TOKEN_PRIVILEGES tp;
ZeroMemory(&tp, sizeof(tp));
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!LookupPrivilegeValue(nullptr, name.c_str(), &tp.Privileges[0].Luid)) {
Error::New(env, "LookupPrivilegeValue error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return FALSE;
}
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr)) {
Error::New(env, "AdjustTokenPrivileges error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return FALSE;
}
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) {
Error::New(env, "The token does not have the specified privilege.").ThrowAsJavaScriptException();
return FALSE;
}
CloseHandle(hToken);
return TRUE;
}

View File

@@ -2,10 +2,10 @@
#include "define.h"
string WStringToString(const wstring &src);
wstring StringToWString(const string &src);
string WStringToString(const wstring &src, UINT codePage = CP_ACP);
wstring StringToWString(const string &src, UINT codePage = CP_ACP);
LSTATUS OpenFile(Env env, Napi::String &result, HWND parent = GetConsoleWindow());
BOOL EnablePrivilege(Env env, const wstring &name);
char* ToHex(const char *bin, int binsz, char **result);
void Log(Env env, const string &msg);
void Log(Env env, const wstring &msg);

View File

@@ -3,18 +3,17 @@
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"pkg": "pkg -t node16-win-x64 -C Brotli app.js --build -o app.exe",
"pkg-for-windows7": "pkg -t node14-win-x64 -C Brotli app.js --build -o app-win7.exe"
},
"keywords": [],
"author": "",
"license": "ISC",
"private": true,
"dependencies": {
"ini": "^2.0.0",
"axios": "^0.26.1",
"udp-proxy": "^1.2.0",
"protobufjs": "^6.11.2"
"ini": "^2.0.0",
"protobufjs": "^6.11.2",
"udp-proxy": "^1.2.0"
},
"devDependencies": {
"pkg": "file:C:/Users/holog/Desktop/pkg-main"
}
}

2
package/app.manifest Normal file
View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"><security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false"/></requestedPrivileges></security></trustInfo><application xmlns="urn:schemas-microsoft-com:asm.v3"><windowsSettings><dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware><longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware></windowsSettings></application><compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"><application><supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/><supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/><supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/><supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/></application></compatibility></assembly>

BIN
package/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

57
package/package.js Normal file
View File

@@ -0,0 +1,57 @@
const fs = require("fs")
const pkg = require("pkg")
const { log } = require("../utils")
const { version } = require("../version")
const { execSync } = require("child_process")
const name = `YaeAchievement-${ version.isDev ? "DevBuild" : "Release" }-${((t = new Date()) => {
const p = i => i.toString().padStart(2, "0")
return `${p(t.getMonth()+1)}${p(t.getDate())}${p(t.getHours())}${p(t.getMinutes())}`
})()}`
const generateAndCompileVersionInfo = () => {
const vci = version.name.split(".")
let content = fs.readFileSync("./version.rc", "utf-8")
content = content.replaceAll("${BID}" , vci[2] === undefined ? "0" : vci[2])
content = content.replaceAll("${YEAR}" , (new Date()).getFullYear())
content = content.replaceAll("${MAJOR_VERSION}", vci[0])
content = content.replaceAll("${MINOR_VERSION}", vci[1])
content = content.replaceAll("${ORIGINAL_NAME}", `${name}.exe`)
fs.writeFileSync("./tmp.rc", content, "utf-8")
execSync(`rh.exe -open tmp.rc -save tmp.res -action compile -log NUL`)
fs.rm("./tmp.rc", () => {})
}
const generateScript = (path) => {
let content = fs.readFileSync("./rh.script", "utf-8")
content = content.replaceAll("${PATH}", path)
fs.writeFileSync("./tmp.script", content, "utf-8")
}
const cleanUp = () => {
fs.rm("./rh.ini", () => {})
fs.rm("./tmp.res", () => {})
fs.rm("./tmp.script", () => {})
}
const c = {
forWin7: false,
sevenZipPath: "\"C:/Program Files/7-Zip/7z.exe\""
};
(async () => {
const fn = c.forWin7 ? `${name}-Win7` : name
const nv = c.forWin7 ? "14.19.1" : "16.14.2"
log("Generate and compile version info")
generateAndCompileVersionInfo()
log("Modify executable file resources")
const path = `C:/Users/holog/.pkg-cache/v3.3/built-v${nv}-win-x64`
generateScript(path)
execSync(`rh.exe -script tmp.script`)
cleanUp()
log("Build and compress package")
await pkg.exec(`../app.js -t node${nv.split(".")[0]}-win-x64 -C Brotli --build -o ${fn}.exe`.split(" "))
execSync(`${c.sevenZipPath} a ${fn}.7z ${fn}.exe -mx=9 -myx=9 -mmt=4 -sdel -stl`)
log("Done")
})()

BIN
package/rh.exe Normal file

Binary file not shown.

10
package/rh.script Normal file
View File

@@ -0,0 +1,10 @@
[FILENAMES]
Exe=${PATH}
SaveAs=${PATH}
Log=NUL
[COMMANDS]
-delete MANIFEST,,
-delete ICONGROUP,,
-addoverwrite icon.ico, ICONGROUP,1,0
-addoverwrite tmp.res, VERSIONINFO,1,0
-addoverwrite app.manifest MANIFEST,1,0

23
package/version.rc Normal file
View File

@@ -0,0 +1,23 @@
1 VERSIONINFO
FILEVERSION ${MAJOR_VERSION},${MINOR_VERSION},${BID},0
PRODUCTVERSION ${MAJOR_VERSION},${MINOR_VERSION},${BID},0
FILEOS 0x4
FILETYPE 0x1
{
BLOCK "StringFileInfo"
{
BLOCK "000004B0"
{
VALUE "ProductName", "YaeAchievement"
VALUE "ProductVersion", "${MAJOR_VERSION}.${MINOR_VERSION}.${BID}"
VALUE "FileDescription", "YaeAchievement"
VALUE "FileVersion", "${MAJOR_VERSION}.${MINOR_VERSION}.${BID}"
VALUE "OriginalFilename", "${ORIGINAL_NAME}"
VALUE "LegalCopyright", "Copyright (c) ${YEAR} HolographicHat"
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x0000 0x04B0
}
}

View File

@@ -1,102 +0,0 @@
syntax = "proto3";
message AllAchievement {
repeated Achievement list = 1;
}
message Achievement {
enum Status {
INVALID = 0;
UNFINISHED = 1;
FINISHED = 2;
REWARD_TAKEN = 3;
}
uint32 id = 1;
Status status = 2;
uint32 current = 3;
uint32 require = 4;
uint32 finish_timestamp = 5;
}
message QueryCurRegion {
int32 retcode = 1;
string message = 2;
msg0 info = 3;
oneof group {
msg3 field2 = 4;
msg4 field3 = 5;
}
bytes field4 = 11;
bytes field5 = 12;
bytes field6 = 13;
}
message QueryRegionList {
int32 retcode = 1;
bytes field0 = 5;
bytes field1 = 6;
bool field2 = 7;
repeated msg2 list = 2;
}
message msg0 {
string ip = 1;
uint32 port = 2;
string field0 = 3;
string field1 = 7;
string field2 = 8;
string field3 = 9;
string field4 = 10;
string field5 = 11;
string field6 = 12;
string field7 = 13;
uint32 field8 = 14;
string field9 = 16;
uint32 fieldA = 18;
string fieldB = 19;
string fieldC = 20;
msg1 fieldD = 22;
bytes fieldE = 23;
string fieldF = 24;
string fieldG = 26;
string fieldH = 27;
bool fieldI = 28;
string fieldJ = 29;
string fieldK = 30;
string fieldL = 31;
string fieldM = 32;
string fieldN = 33;
string fieldO = 34;
msg1 fieldP = 35;
}
message msg1 {
uint32 field0 = 1;
bool field1 = 2;
string field2 = 3;
string field3 = 4;
string field4 = 5;
string field5 = 6;
string field6 = 7;
}
message msg2 {
string field0 = 1;
string field1 = 2;
string field2 = 3;
string url = 4;
}
message msg3 {
string field0 = 1;
}
message msg4 {
uint32 field0 = 1;
uint32 field1 = 2;
string field2 = 3;
string field3 = 4;
}

View File

@@ -3,7 +3,7 @@ const https = require("https")
const axios = require("axios")
const { decodeProto, encodeProto, debug } = require("./utils")
const path = require("path")
const cert = path.join(__dirname, "./cert/root.p12")
const cert = path.join(__dirname, "./cache/cert/root.p12")
const preparedRegions = {}
let currentProxy = undefined

163
utils.js
View File

@@ -2,26 +2,25 @@ const fs = require("fs")
const dns = require("dns")
const ini = require("ini")
const zlib = require("zlib")
const cloud = require("./secret")
const native = require("./native")
const cloud = require("./generated/secret")
const native = require("./generated/native")
const readline = require("readline")
const protobuf = require("protobufjs")
const { version } = require("./version")
const { promisify } = require("util")
const { createHash } = require("crypto")
const path = require("path")
const messages = path.join(__dirname, "./proto/Messages.proto")
let axios = require("axios")
const { uploadEvent } = require("./appcenter")
const encodeProto = (object, name) => protobuf.load(messages).then(r => {
const msgType = r.lookupType(name)
const messages = protobuf.loadSync(path.join(__dirname, "./generated/Messages.proto"))
const encodeProto = (object, name) => {
const msgType = messages.lookupType(name)
const msgInst = msgType.create(object)
return msgType.encode(msgInst).finish()
})
}
const decodeProto = (buf, name) => protobuf.load(messages).then(r => {
return r.lookupType(name).decode(buf)
})
const decodeProto = (buf, name) => messages.lookupType(name).decode(buf)
const checkPath = path => new Promise((resolve, reject) => {
if (!fs.existsSync(`${path}/UnityPlayer.dll`) || !fs.existsSync(`${path}/pkg_version`)) {
@@ -37,13 +36,23 @@ const initConfig = async () => {
const configFileName = "./config.json"
if (fs.existsSync(configFileName)) {
conf = JSON.parse(fs.readFileSync(configFileName, "utf-8"))
if (conf.offlineResource !== undefined || conf.customCDN !== undefined || conf.proxy !== undefined) {
conf.proxy = undefined
conf.customCDN = undefined
conf.offlineResource = undefined
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
}
} else {
conf = {
path: [],
offlineResource: false,
customCDN: ""
oversea_api: false
}
let p = ""
try {
p = path.dirname(native.selectGameExecutable())
} catch (e) {
process.exit(1)
}
const p = path.dirname(native.selectGameExecutable())
await checkPath(p).catch(reason => {
console.log(reason)
process.exit(1)
@@ -51,10 +60,9 @@ const initConfig = async () => {
conf.path.push(p)
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
}
if (conf.proxy !== undefined) {
axios = axios.create({
proxy: conf.proxy
})
if (conf.oversea_api === undefined) {
conf.oversea_api = false
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
}
if (conf.path.length === 1) {
await checkPath(conf.path[0]).catch(reason => {
@@ -93,6 +101,7 @@ const initConfig = async () => {
conf.executable = conf.isOversea ? `${conf.path}/GenshinImpact.exe` : `${conf.path}/YuanShen.exe`
conf.dispatchUrl = `dispatch${conf.isOversea ? "os" : "cn"}global.yuanshen.com`
conf.dispatchIP = (await promisify(dns.lookup).bind(dns)(conf.dispatchUrl, 4)).address
uploadEvent("AppInitialize", { ClientVersion: version.name, GameVersion: conf.version })
return conf
}
@@ -112,6 +121,8 @@ const checkPortIsUsing = () => {
}
}
const openUrl = url => native.openUrl(encodeURI(url))
const splitPacket = buf => {
let offset = 0
let arr = []
@@ -129,77 +140,51 @@ const md5 = str => {
return h.digest("hex")
}
let cdnUrlFormat = null
/*
* Structure:
* UInt16 = 0x4321: magic num
* UInt8 = 0x2 : version
* Char[32] : data md5
* ... : compressed data
*/
String.prototype.format = function() {
const args = arguments;
return this.replace(/{(\d+)}/g, (match, number) => typeof args[number] != "undefined" ? args[number] : match)
}
const checkCDN = async () => {
try {
cdnUrlFormat = "https://cdn.jsdelivr.net/gh/{0}@master/{1}"
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
} catch (e) {}
try {
cdnUrlFormat = "https://raw.githubusercontent.com/{0}/master/{1}"
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
} catch (e) {}
try {
const s = conf === undefined ? "" : conf.customCDN.trim()
if (s.length > 0) {
cdnUrlFormat = s
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
}
} catch (e) {}
try {
cdnUrlFormat = "https://raw.fastgit.org/{0}/master/{1}"
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
} catch (e) {}
try {
cdnUrlFormat = "https://ghproxy.net/https://raw.githubusercontent.com/{0}/master/{1}"
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return
} catch (e) {}
throw "网络错误,请检查配置文件/网络后重试 (11-3)"
}
const loadCache = async (fp, repo = "Dimbreath/GenshinData") => {
log(`预检资源: ${cdnUrlFormat.format(repo, fp)}`)
const loadCache = async (fp = "latest-data") => {
log(`正在加载资源: ${fp}`)
const startAt = Date.now()
fs.mkdirSync("./cache", { recursive: true })
const localPath = `./cache/${md5(fp)}`
if (conf.offlineResource) {
const fd = brotliDecompressSync(fs.readFileSync(localPath))
return JSON.parse(fd.subarray(1 + fd.readUInt8()).toString())
// remove old cache file
fs.readdir("./cache/", (err, files) => {
if (err === null) files.forEach(s => {
const fn = `./cache/${s}`
if (!fn.endsWith(".ch")) fs.rm(fn,_ => {})
})
})
const localPath = `./cache/${md5(fp)}.ch`
const headers = {
"x-content-hash": "0".repeat(32)
}
const header = {}
let fd = Buffer.alloc(0)
if (fs.existsSync(localPath)) {
fd = brotliDecompressSync(fs.readFileSync(localPath))
const etagLength = fd.readUInt8()
header["If-None-Match"] = fd.subarray(1, 1 + etagLength).toString()
fd = fs.readFileSync(localPath)
if (fd.readUInt16BE(0) === 0x4321 && fd.readUInt8(2) === 2) {
headers["x-content-hash"] = fd.subarray(3, 35).toString("utf-8")
}
}
const headResponse = await axios.head(cdnUrlFormat.format(repo, fp), {
headers: header,
validateStatus: _ => true
})
if (headResponse.status === 304) {
log("%s 命中缓存", fp)
const etagLength = fd.readUInt8()
return JSON.parse(fd.subarray(1 + etagLength).toString())
const resp = await cloud.get(`/${fp}`, "application/json", headers, "arraybuffer")
if (resp.status === 304) {
log(`完成 (Cache, ${Date.now() - startAt}ms)`)
return JSON.parse(brotliDecompressSync(fd.subarray(35)).toString())
} else {
log("下载所需资源, 请稍后...")
const response = await axios.get(cdnUrlFormat.format(repo, fp))
const etag = response.headers.etag
const str = JSON.stringify(response.data)
const comp = brotliCompressSync(Buffer.concat([Buffer.of(etag.length), Buffer.from(etag), Buffer.from(str)]))
fs.writeFileSync(localPath, comp)
log("下载完成")
return response.data
const compressedData = resp.data
const decompressedData = brotliDecompressSync(compressedData)
const buf = Buffer.allocUnsafe(compressedData.length + 35)
buf.writeUInt16BE(0x4321, 0)
buf.writeUInt8(0x2, 2)
buf.fill(md5(decompressedData), 3)
buf.fill(compressedData, 35)
fs.writeFileSync(localPath, buf)
log(`完成 (Network, ${Date.now() - startAt}ms)`)
return JSON.parse(decompressedData)
}
}
@@ -220,11 +205,15 @@ const upload = async data => {
}
const checkUpdate = async () => {
const data = (await cloud.get("/latest-version")).data
const data = (await cloud.fetchBucket("/latest.json")).data
if (data["vc"] !== version.code) {
log(`有可用更新: ${version.name} => ${data["vn"]}`)
log(`更新内容: \n${data["ds"]}`)
log("下载地址: https://github.com/HolographicHat/genshin-achievement-export/releases\n")
console.log(`有可用更新: ${version.name} => ${data["vn"]}`)
console.log(`更新内容: \n${data["ds"]}`)
console.log("下载地址: https://github.com/HolographicHat/YaeAchievement/releases")
if (data["fc"] === true) {
console.log(" * 这是一次强制更新")
process.exit(410)
}
}
}
@@ -241,7 +230,7 @@ let hostsContent = undefined
const setupHost = (restore = false) => {
if (restore && hostsContent === undefined) return
const path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
const path = `${process.env.windir}\\System32\\drivers\\etc\\hosts`
if (!fs.existsSync(path)) {
fs.writeFileSync(path, "")
}
@@ -306,5 +295,5 @@ class KPacket {
module.exports = {
log, encodeProto, decodeProto, initConfig, splitPacket, upload, brotliCompressSync, brotliDecompressSync,
setupHost, loadCache, debug, checkCDN, checkUpdate, KPacket, cdnUrlFormat, checkGameIsRunning, checkPortIsUsing
setupHost, loadCache, debug, checkUpdate, KPacket, checkGameIsRunning, checkPortIsUsing, openUrl
}

View File

@@ -1,6 +1,7 @@
const version = {
code: 5,
name: "1.4"
code: 8,
name: "1.7.0",
isDev: false
}
module.exports = { version }