45 Commits
1.0 ... 1.4

Author SHA1 Message Date
HolographicHat
8a0c82f89f catch wmi error 2022-04-06 11:26:05 +08:00
HolographicHat
d64bf8149e Update README.md 2022-04-06 08:53:10 +08:00
HolographicHat
82e85f5ea0 v1.4 2022-04-06 00:15:42 +08:00
HolographicHat
891a19c3f7 select game by open file dialog, optimize code and message 2022-04-06 00:14:48 +08:00
HolographicHat
7b94964342 more message and check, better privilege test 2022-04-06 00:13:03 +08:00
HolographicHat
d5e290e866 modify and add some message, optimize 2022-04-06 00:11:21 +08:00
HolographicHat
f7c48472f1 native: enablePrivilege and copyToClipboard 2022-04-06 00:03:12 +08:00
HolographicHat
a6b80d3588 more error check 2022-04-05 20:13:52 +08:00
HolographicHat
18e3d3ffe3 fix #5 2022-04-05 19:11:12 +08:00
HolographicHat
a22ad8e87d Merge remote-tracking branch 'origin/master' 2022-04-05 18:53:42 +08:00
HolographicHat
35ec8b5859 native: import wmi as submodule 2022-04-05 18:53:09 +08:00
HolographicHat
35899e74f6 Update README.md 2022-04-05 02:23:54 +08:00
HolographicHat
bb4d5215f1 native: getDeviceID and getDeviceInfo 2022-04-05 02:22:01 +08:00
HolographicHat
e4e76286c9 native addon 2022-04-04 22:50:59 +08:00
HolographicHat
be5a457639 native addon 2022-04-04 17:08:47 +08:00
HolographicHat
945a94222a Update README.md 2022-04-03 23:40:26 +08:00
HolographicHat
3d03496074 Update messages 2022-04-02 22:48:51 +08:00
HolographicHat
1e6ebc76dd Update README.md 2022-04-02 22:37:15 +08:00
HolographicHat
ab47ff2b06 fix crash when hosts not exist 2022-04-01 20:31:49 +08:00
HolographicHat
5fe6e80cc8 Merge remote-tracking branch 'origin/master' 2022-03-30 14:10:23 +08:00
HolographicHat
a23d7f3cc9 more handler 2022-03-30 14:09:50 +08:00
HolographicHat
0ab6444b6f more message 2022-03-29 22:33:24 +08:00
HolographicHat
00f0aaa4a6 2.6 Achievement data 2022-03-28 18:30:08 +08:00
HolographicHat
2377dbd492 Update achievement-data.json 2022-03-28 18:26:00 +08:00
HolographicHat
0b015886ea Update README.md 2022-03-28 16:32:28 +08:00
HolographicHat
4adfc9a312 Update README.md 2022-03-27 13:04:14 +08:00
HolographicHat
b4cd69f303 v1.3 2022-03-27 12:51:41 +08:00
HolographicHat
c76ddd9e3f fix wrong field name 2022-03-27 12:47:25 +08:00
HolographicHat
d40c456494 add default arg for read registry method 2022-03-26 20:51:54 +08:00
HolographicHat
408169da4e Merge remote-tracking branch 'origin/master' 2022-03-26 17:55:57 +08:00
HolographicHat
9de8e957fd add server disconnect handler 2022-03-26 16:59:21 +08:00
HolographicHat
a287e5db43 Update README.md 2022-03-26 16:39:43 +08:00
HolographicHat
589048b912 Update README.md 2022-03-26 16:39:14 +08:00
HolographicHat
79517a37d2 try to fix copy error 2022-03-25 21:53:14 +08:00
HolographicHat
e022f04661 fix achievement data error 2022-03-25 21:33:46 +08:00
HolographicHat
d1a635be7c fix exit hook 2022-03-25 21:32:43 +08:00
HolographicHat
aa853262b7 Merge remote-tracking branch 'origin/master' 2022-03-25 19:23:18 +08:00
HolographicHat
bdf9561dfa fix crash when appcenter error 2022-03-25 19:22:34 +08:00
HolographicHat
a663fddeb0 Update README.md 2022-03-23 20:58:21 +08:00
HolographicHat
7b18bcfbd3 v1.0.1 2022-03-23 20:27:43 +08:00
HolographicHat
b03bc2add8 fix exit hook 2022-03-22 23:10:39 +08:00
HolographicHat
a3ec29eda1 Update README.md 2022-03-22 23:00:31 +08:00
HolographicHat
441ab78442 Merge remote-tracking branch 'origin/master' 2022-03-22 22:52:12 +08:00
HolographicHat
91991e1430 support node 14 on win 7 2022-03-22 22:51:48 +08:00
HolographicHat
4a43666320 Update README.md 2022-03-22 22:49:54 +08:00
23 changed files with 3724 additions and 3034 deletions

4
.gitignore vendored
View File

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

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "native/src/wmi"]
path = native/src/wmi
url = https://github.com/HolographicHat/wmi

View File

@@ -1,13 +1,32 @@
# 原神成就导出工具 # 原神成就导出工具
![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)
- 支持导出所有成就 - 支持导出所有成就
- 支持官服B服与国际服
- 没有窗口大小游戏或语言等要求 - 没有窗口大小游戏或语言等要求
- 更快、更准 - 更快、更准
## 使用说明 ## 使用说明
**打开程序前需要关闭正在运行的原神主程序** **打开程序前需要关闭正在运行的原神主程序**
第一次打开需要先设置原神主程序所在路径,支持多个路径, 使用符号'*'分隔 第一次打开需要先设置原神主程序(YuanShen.exe/GenshinImpact.exe)所在路径
![alt](https://upload-bbs.mihoyo.com/upload/2022/03/22/165631158/a1bbf8d0604a29830c09822add53f749_8463600217231045373.png) ![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
}
}
```
## 下载地址 ## 下载地址
[releases/latest](https://github.com/HolographicHat/genshin-achievement-export/releases/latest) [releases/latest](https://github.com/HolographicHat/genshin-achievement-export/releases/latest)
@@ -15,11 +34,18 @@
## 问题反馈 ## 问题反馈
[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) [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)
## FAQ ## 常见问题
1. Q: 为什么需要管理员权限 0. Q: 程序异常退出或被强行终止后,原神启动时报错: 无法连接网络(4201)
A: 用文本编辑器打开```C:\Windows\System32\drivers\etc\hosts```,删除```127.0.0.1 dispatch**global.yuanshen.com```后保存,重启原神
1. Q: 原神启动时报错: 数据异常(31-4302)
A: 不要把软件和原神主程序放一起
2. Q: 为什么需要管理员权限
A: 临时修改Hosts和启动原神 A: 临时修改Hosts和启动原神
2. Q: 报毒?
A: 执行命令或修改hosts引发相关代码可在./utils.js,./appcenter.js下找到 3. Q: 报毒?
A: 执行命令或修改hosts引发相关代码可在./utils.js,./appcenter.js下找到
## TODO 4. Q: 原神进门后没有自动退出,程序输出停留在“加载完成”
- [ ] 整个GUI A: 关闭Shadowsocks全局代理

File diff suppressed because it is too large Load Diff

95
app.js
View File

@@ -1,36 +1,66 @@
const zlib = require("zlib")
const proxy = require("udp-proxy") const proxy = require("udp-proxy")
const cp = require("child_process") const cp = require("child_process")
const rs = require("./regionServer") const rs = require("./regionServer")
const appcenter = require("./appcenter") const appcenter = require("./appcenter")
const { initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkCDN, checkUpdate } = require("./utils") const {
const { exportData } = require("./export"); initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkCDN, checkUpdate,
brotliCompressSync, brotliDecompressSync, checkGameIsRunning, checkPortIsUsing
} = require("./utils")
const { exportData } = require("./export")
const { enablePrivilege } = require("./native")
const onExit = () => {
setupHost(true)
console.log("按任意键退出")
cp.execSync("pause > nul", { stdio: "inherit" })
};
// TODO: i18n
// TODO: send ack to avoid resend
(async () => { (async () => {
try { try {
appcenter.init() process.once("SIGHUP", () => setupHost(true))
let conf = await initConfig() process.on("unhandledRejection", (reason, promise) => {
console.log("Unhandled Rejection at: ", promise, "\n0Reason:", reason)
})
process.once("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)
try { try {
cp.execSync("net session", { stdio: "ignore" }) enablePrivilege()
} catch (e) { } catch (e) {
console.log("\x1b[91m请使用管理员身份运行此程序\x1b[0m") console.log("请使用管理员身份运行此程序")
return process.exit(-1)
} }
appcenter.startup()
let conf = await initConfig()
checkPortIsUsing()
checkGameIsRunning()
log("检查更新")
await checkUpdate() await checkUpdate()
checkCDN().then(_ => debug("CDN check success.")) checkCDN().then(_ => debug("CDN check success.")).catch(reason => {
console.log(reason)
process.exit(113)
})
let gameProcess
let unexpectedExit = true let unexpectedExit = true
const gameProcess = cp.execFile(conf.executable, { cwd: conf.path },err => {
if (err !== null && !err.killed) {
throw err
}
})
gameProcess.on("exit", () => {
if (unexpectedExit) process.exit(0)
})
rs.create(conf,() => { rs.create(conf,() => {
setupHost() setupHost()
gameProcess = cp.execFile(conf.executable, { cwd: conf.path },err => {
if (err !== null && !err.killed) {
throw err
}
})
log("启动原神")
gameProcess.on("exit", () => {
if (unexpectedExit) {
console.log("游戏进程异常退出")
process.exit(0)
}
})
},(ip, port, hServer) => { },(ip, port, hServer) => {
let login = false let login = false
let cache = new Map() let cache = new Map()
@@ -54,16 +84,18 @@ const { exportData } = require("./export");
} }
} }
let monitor; let monitor;
let stopped = false
const createMonitor = () => { const createMonitor = () => {
monitor = setInterval(async () => { monitor = setInterval(async () => {
if (login && lastRecvTimestamp + 2 < parseInt(Date.now() / 1000)) { if (login && lastRecvTimestamp + 2 < parseInt(Date.now() / 1000) && !stopped) {
stopped = true
unexpectedExit = false unexpectedExit = false
server.close() server.close(() => {})
hServer.close() hServer.close()
gameProcess.kill() gameProcess.kill()
clearInterval(monitor) clearInterval(monitor)
setupHost(true) setupHost(true)
console.log("正在处理数据,请稍后...") log("正在处理数据,请稍后...")
let packets = Array.from(cache.values()) let packets = Array.from(cache.values())
cache.clear() cache.clear()
packets.sort((a, b) => a.frg - b.frg) packets.sort((a, b) => a.frg - b.frg)
@@ -85,18 +117,16 @@ const { exportData } = require("./export");
return Buffer.concat([len, data]) return Buffer.concat([len, data])
}) })
const merged = Buffer.concat(packets) const merged = Buffer.concat(packets)
const compressed = zlib.brotliCompressSync(merged) const compressed = brotliCompressSync(merged)
const response = await upload(compressed) const response = await upload(compressed)
const data = brotliDecompressSync(response.data)
if (response.status !== 200) { if (response.status !== 200) {
log(`发生错误: ${response.data.toString()}`) log(`发生错误: ${data}`)
log(`请求ID: ${response.headers["x-api-requestid"]}`) log(`请求ID: ${response.headers["x-api-requestid"]}`)
log("请联系开发者以获取帮助") log("请联系开发者以获取帮助")
} else { } else {
const data = zlib.brotliDecompressSync(response.data)
const proto = await decodeProto(data,"AllAchievement") const proto = await decodeProto(data,"AllAchievement")
await exportData(proto) await exportData(proto)
console.log("按任意键退出")
cp.execSync("pause > nul", { stdio: "inherit" })
} }
process.exit(0) process.exit(0)
} }
@@ -108,6 +138,8 @@ const { exportData } = require("./export");
login = true login = true
} }
}) })
server.on("error", err => console.log(`Proxy error: ${err.message}` + err.message))
server.on("proxyError", err => console.log(`Proxy error: ${err.message}` + err.message))
server.on("proxyMsg", (msg, _) => { server.on("proxyMsg", (msg, _) => {
lastRecvTimestamp = parseInt(Date.now() / 1000) lastRecvTimestamp = parseInt(Date.now() / 1000)
let buf = Buffer.from(msg) let buf = Buffer.from(msg)
@@ -117,6 +149,9 @@ const { exportData } = require("./export");
createMonitor() createMonitor()
debug("服务端握手应答") debug("服务端握手应答")
break break
case 404:
lastRecvTimestamp = parseInt(Date.now() / 1000) - 2333
break
default: default:
console.log(`Unhandled: ${buf.toString("hex")}`) console.log(`Unhandled: ${buf.toString("hex")}`)
process.exit(2) process.exit(2)
@@ -132,7 +167,7 @@ const { exportData } = require("./export");
}) })
}) })
return server return server
}).then(() => console.log("加载完毕")) }).then(() => log("加载完毕"))
} catch (e) { } catch (e) {
console.log(e) console.log(e)
if (e instanceof Error) { if (e instanceof Error) {
@@ -140,8 +175,6 @@ const { exportData } = require("./export");
} else { } else {
appcenter.uploadError(Error(e), true) appcenter.uploadError(Error(e), true)
} }
console.log("按任意键退出") process.exit(1)
cp.execSync("pause > nul", { stdio: "inherit" })
process.exit(0)
} }
})() })()

View File

@@ -1,80 +1,50 @@
const cp = require("child_process")
const axios = require("axios") const axios = require("axios")
const crypto = require("crypto") const crypto = require("crypto")
const { version } = require("./version") const { version } = require("./version")
const { pid, argv0, uptime, report } = require("node:process") const { getDeviceID, getDeviceInfo } = require("./native")
const getTimestamp = (d = new Date()) => { const getTimestamp = (d = new Date()) => {
const p = i => i.toString().padStart(2, "0") const p = i => i.toString().padStart(2, "0")
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}T${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}.${p(d.getUTCMilliseconds())}Z` return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}T${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}.${p(d.getUTCMilliseconds())}Z`
} }
const readRegistry = (path, key) => {
const i = cp.execSync(`reg query "${path}" /v ${key}`, {
encoding: "utf-8"
}).split("\n")[2].split(" ").filter(s => s.length > 0).map(s => s.trim())
switch (i[1]) {
case "REG_SZ":
return i[2]
case "REG_DWORD":
return parseInt(i[2])
default:
throw "Unsupported"
}
}
const queue = [] const queue = []
const session = crypto.randomUUID() const session = crypto.randomUUID()
const key = "648b83bf-d439-49bd-97f4-e1e506bdfe39" const key = "648b83bf-d439-49bd-97f4-e1e506bdfe39"
const install = (() => { const install = (() => {
const s = readRegistry("HKCU\\SOFTWARE\\miHoYoSDK", "MIHOYOSDK_DEVICE_ID") const id = getDeviceID()
return `${s.substring(0, 8)}-${s.substring(8, 12)}-${s.substring(12, 16)}-${s.substring(16, 20)}-${s.substring(20, 32)}` return id === undefined ? crypto.randomUUID() : id
})() })()
const device = (() => { const device = (() => {
const csi = cp.execSync("wmic computersystem get manufacturer,model /format:csv", { const info = getDeviceInfo()
encoding: "utf-8" info.appBuild = version.code
}).split("\n")[2].split(",").map(s => s.trim()) info.appVersion = version.name
const osi = cp.execSync("wmic os get currentTimeZone, version /format:csv", { info.sdkName = "appcenter.wpf.netcore"
encoding: "utf-8" info.sdkVersion = "4.5.0"
}).split("\n")[2].split(",").map(s => s.trim()) info.osName = "WINDOWS"
return { info.appNamespace = "default"
model: csi[2], return info
oemName: csi[1],
timeZoneOffset: parseInt(osi[1]),
osBuild: `${osi[2]}.${readRegistry("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "UBR")}`,
osVersion: osi[2],
locale: readRegistry("HKCU\\Control Panel\\International", "LocaleName"),
carrierCountry: readRegistry("HKCU\\Control Panel\\International\\Geo", "Name"),
sdkName: "appcenter.wpf.netcore",
sdkVersion: "4.5.0",
osName: "WINDOWS",
appVersion: version.name,
appBuild: version.code,
appNamespace: "default"
}
})() })()
const upload = () => { const upload = () => {
if (queue.length > 0) { if (queue.length > 0) {
try { const data = JSON.stringify({ "logs": queue })
const data = JSON.stringify({ "logs": queue }) axios.post("https://in.appcenter.ms/logs?api-version=1.0.0", data,{
axios.post("https://in.appcenter.ms/logs?api-version=1.0.0", data,{ headers: {
headers: { "App-Secret": key,
"App-Secret": key, "Install-ID": install
"Install-ID": install }
} }).then(_ => {
}).then(_ => { queue.length = 0
queue.length = 0 }).catch(_ => {})
})
} catch (e) {}
} }
} }
const uploadError = (err, fatal) => { const uploadError = (err, fatal) => {
const eid = crypto.randomUUID() const eid = crypto.randomUUID()
const reportJson = report.getReport(err) const reportJson = process.report.getReport(err)
const reportAttachment = { const reportAttachment = {
type: "errorAttachment", type: "errorAttachment",
device: device, device: device,
@@ -94,10 +64,10 @@ const uploadError = (err, fatal) => {
architecture: "AMD64", architecture: "AMD64",
userId: install, userId: install,
fatal: fatal, fatal: fatal,
processId: pid, processId: process.pid,
processName: argv0.replaceAll("\\", "/").split("/").pop(), processName: process.argv0.replaceAll("\\", "/").split("/").pop(),
timestamp: getTimestamp(), timestamp: getTimestamp(),
appLaunchTimestamp: getTimestamp(new Date(Date.now() - uptime())), appLaunchTimestamp: getTimestamp(new Date(Date.now() - process.uptime())),
exception: { exception: {
"type": err.name, "type": err.name,
"message": err.message, "message": err.message,
@@ -109,7 +79,7 @@ const uploadError = (err, fatal) => {
upload() upload()
} }
const init = () => { const startup = () => {
queue.push({ queue.push({
type: "startService", type: "startService",
services: [ "Analytics","Crashes" ], services: [ "Analytics","Crashes" ],
@@ -127,5 +97,5 @@ const init = () => {
} }
module.exports = { module.exports = {
init, upload, uploadError startup, upload, uploadError
} }

Binary file not shown.

View File

@@ -1,8 +1,7 @@
const fs = require("fs") const fs = require("fs")
const util = require("util")
const readline = require("readline") const readline = require("readline")
const { spawnSync } = require("child_process") const { loadCache, log } = require("./utils")
const { loadCache } = require("./utils") const { copyToClipboard } = require("./native")
const exportToSeelie = proto => { const exportToSeelie = proto => {
const out = { achievements: {} } const out = { achievements: {} }
@@ -11,11 +10,11 @@ const exportToSeelie = proto => {
}) })
const fp = `./export-${Date.now()}-seelie.json` const fp = `./export-${Date.now()}-seelie.json`
fs.writeFileSync(fp, JSON.stringify(out)) fs.writeFileSync(fp, JSON.stringify(out))
console.log(`导出为文件: ${fp}`) log(`导出为文件: ${fp}`)
} }
const exportToPaimon = async proto => { const exportToPaimon = async proto => {
const out = { achievements: {} } const out = { achievement: {} }
const achTable = new Map() const achTable = new Map()
const excel = await loadCache("ExcelBinOutput/AchievementExcelConfigData.json") const excel = await loadCache("ExcelBinOutput/AchievementExcelConfigData.json")
excel.forEach(({GoalId, Id}) => { excel.forEach(({GoalId, Id}) => {
@@ -23,14 +22,14 @@ const exportToPaimon = async proto => {
}) })
proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => { proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => {
const gid = achTable.get(id) const gid = achTable.get(id)
if (out.achievements[gid] === undefined) { if (out.achievement[gid] === undefined) {
out.achievements[gid] = {} out.achievement[gid] = {}
} }
out.achievements[gid][id] = true out.achievement[gid][id] = true
}) })
const fp = `./export-${Date.now()}-paimon.json` const fp = `./export-${Date.now()}-paimon.json`
fs.writeFileSync(fp, JSON.stringify(out)) fs.writeFileSync(fp, JSON.stringify(out))
console.log(`导出为文件: ${fp}`) log(`导出为文件: ${fp}`)
} }
const exportToCocogoat = async proto => { const exportToCocogoat = async proto => {
@@ -61,8 +60,12 @@ const exportToCocogoat = async proto => {
date: getDate(finishTimestamp) date: getDate(finishTimestamp)
}) })
}) })
spawnSync("clip", { input: JSON.stringify(out,null,2) }) const ts = Date.now()
console.log("导出内容已复制到剪贴板") const json = JSON.stringify(out,null,2)
copyToClipboard(json)
const fp = `./export-${ts}-cocogoat.json`
fs.writeFileSync(fp, json)
log(`导出内容已复制到剪贴板`)
} }
const exportToCsv = async proto => { const exportToCsv = async proto => {
@@ -85,20 +88,23 @@ const exportToCsv = async proto => {
const p = i => i.toString().padStart(2, "0") const p = i => i.toString().padStart(2, "0")
return `${d.getFullYear()}/${p(d.getMonth() + 1)}/${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}` return `${d.getFullYear()}/${p(d.getMonth() + 1)}/${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
} }
const bl = [84517]
proto.list.forEach(({current, finishTimestamp, id, status, require}) => { proto.list.forEach(({current, finishTimestamp, id, status, require}) => {
const desc = achievementMap.get(id) === undefined ? (() => { if (!bl.includes(id)) {
console.log(`Error get id ${id} in excel`) const desc = achievementMap.get(id) === undefined ? (() => {
return { console.log(`Error get id ${id} in excel`)
goal: "未知", return {
name: "未知", goal: "未知",
desc: "未知" name: "未知",
} desc: "未知"
})() : 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)}`) })() : 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)}`)
}
}) })
const fp = `./export-${Date.now()}.csv` const fp = `./export-${Date.now()}.csv`
fs.writeFileSync(fp, `\uFEFF${outputLines.join("\n")}`) fs.writeFileSync(fp, `\uFEFF${outputLines.join("\n")}`)
console.log(`导出为文件: ${fp}`) log(`导出为文件: ${fp}`)
} }
const exportData = async proto => { const exportData = async proto => {
@@ -106,8 +112,10 @@ const exportData = async proto => {
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout
}) })
const question = util.promisify(rl.question).bind(rl) const question = (query) => new Promise(resolve => {
const chosen = await question("导出至: \n[0] 椰羊 (https://cocogoat.work/achievement)\n[1] Paimon.moe\n[2] Seelie.me\n[3] 表格文件 (默认)\n> ") 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): ")
rl.close() rl.close()
switch (chosen) { switch (chosen) {
case "0": case "0":
@@ -119,6 +127,10 @@ const exportData = async proto => {
case "2": case "2":
await exportToSeelie(proto) await exportToSeelie(proto)
break break
case "raw":
fs.writeFileSync(`./export-${Date.now()}-raw.json`, JSON.stringify(proto,null,2))
log("OK")
break
default: default:
await exportToCsv(proto) await exportToCsv(proto)
} }

7
native.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
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

5
native/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
CMakeLists.txt
.idea
cmake-build-debug
node_modules
build

42
native/binding.gyp Normal file
View File

@@ -0,0 +1,42 @@
{
"targets": [
{
"target_name": "native",
"sources": [
"src/main.cc",
"src/utils.h",
"src/utils.cc",
"src/define.h",
"src/wmi/wmi.cpp",
"src/wmi/wmi.hpp",
"src/wmi/unistd.h",
"src/wmi/wmiresult.cpp",
"src/wmi/wmiresult.hpp",
"src/wmi/wmiclasses.hpp",
"src/wmi/wmiexception.hpp",
"src/registry/registry.hpp"
],
"cflags!": [
"-fno-exceptions"
],
"cflags_cc!": [
"-fno-exceptions"
],
"defines": [
"NAPI_DISABLE_CPP_EXCEPTIONS"
],
"include_dirs": [
"<!(node -p \"require('node-addon-api').include_dir\")"
],
"msvs_settings": {
"VCCLCompilerTool": {
"AdditionalOptions": [
"-std:c++latest",
"-DUNICODE",
"-sdl"
]
}
}
}
]
}

15
native/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "genshin-export-native",
"version": "1.0.0",
"description": "",
"scripts": {
"build": "node-gyp rebuild && copy .\\build\\Release\\native.node ..\\genshin-export\\"
},
"gypfile": true,
"devDependencies": {
"node-gyp": "^9.0.0"
},
"dependencies": {
"node-addon-api": "^4.3.0"
}
}

14
native/src/define.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <string>
#include <iostream>
#include <napi.h>
#include <Windows.h>
#include <TlHelp32.h>
using std::string, std::wstring, std::cout, std::to_string;
using Napi::Object, Napi::Env, Napi::Function, Napi::Value, Napi::CallbackInfo, Napi::TypeError, Napi::Error;
typedef unsigned char byte;
typedef unsigned long ul;
typedef unsigned long long ull;

177
native/src/main.cc Normal file
View File

@@ -0,0 +1,177 @@
#include "utils.h"
#include "define.h"
#include "wmi/wmi.hpp"
#include "wmi/wmiclasses.hpp"
#include "registry/registry.hpp"
#include <iphlpapi.h>
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"iphlpapi.lib")
using Wmi::Win32_ComputerSystem, Wmi::Win32_OperatingSystem, Wmi::retrieveWmi;
namespace native {
Value checkGameIsRunning(const CallbackInfo &info) {
Env env = info.Env();
if (info.Length() != 1 || !info[0].IsString()) {
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Undefined();
}
wstring name = StringToWString(info[0].As<Napi::String>().Utf8Value());
bool isRunning = false;
PROCESSENTRY32 entry;
entry.dwSize = sizeof(entry);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (Process32First(snapshot, &entry) == TRUE) {
while (Process32Next(snapshot, &entry) == TRUE) {
if (wstring(entry.szExeFile) == name) {
isRunning = true;
}
}
}
CloseHandle(snapshot);
return Napi::Boolean::New(env, isRunning);
}
Value selectGameExecutable(const CallbackInfo &info) {
Env env = info.Env();
Napi::String path;
if (OpenFile(env, path) != ERROR_SUCCESS) {
Error::New(env, "Failed to open file: " + to_string(CommDlgExtendedError())).ThrowAsJavaScriptException();
return env.Undefined();
}
return path;
}
Value whoUseThePort(const CallbackInfo &info) {
Env env = info.Env();
if (info.Length() != 1 || !info[0].IsNumber()) {
TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Undefined();
}
DWORD dwSize = 0;
PMIB_TCPTABLE_OWNER_PID pTcpTable = nullptr;
GetExtendedTcpTable(pTcpTable, &dwSize, TRUE,AF_INET,TCP_TABLE_OWNER_PID_ALL,0);
pTcpTable = (PMIB_TCPTABLE_OWNER_PID)new byte[dwSize];
if(GetExtendedTcpTable(pTcpTable,&dwSize,TRUE,AF_INET,TCP_TABLE_OWNER_PID_ALL,0) != NO_ERROR) {
Error::New(env, "GetExtendedTcpTable failed").ThrowAsJavaScriptException();
return env.Undefined();
}
int port = info[0].As<Napi::Number>().Int32Value();
auto nNum = (int)pTcpTable->dwNumEntries;
DWORD pid = 0;
for(int i = 0; i < nNum; i++) {
if (htons(pTcpTable->table[i].dwLocalPort) == port) {
pid = pTcpTable->table[i].dwOwningPid;
break;
}
}
delete pTcpTable;
Value ret = env.Undefined();
if (pid != 0) {
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (hProcess == nullptr) {
Error::New(env, "OpenProcess error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return env.Undefined();
}
TCHAR fnBuf[MAX_PATH];
DWORD length = MAX_PATH;
if (QueryFullProcessImageName(hProcess, 0, fnBuf, &length) == 0) {
Error::New(env, "QueryFullProcessImageName error: " + to_string(GetLastError())).ThrowAsJavaScriptException();
return env.Undefined();
}
Object obj = Object::New(env);
obj.Set("pid", Napi::Number::New(env, pid));
obj.Set("path", Napi::String::New(env, WStringToString(fnBuf)));
ret = obj;
}
return ret;
}
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) {
return env.Undefined();
}
string id = WStringToString(wd);
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) {
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");
wstring locale;
if (RegUtils::GetString(HKEY_CURRENT_USER, L"Control Panel\\International", L"LocaleName", locale) != ERROR_SUCCESS) {
locale = L"zh-CN";
}
wstring country;
if (RegUtils::GetString(HKEY_CURRENT_USER, L"Control Panel\\International\\Geo", L"Name", country) != ERROR_SUCCESS) {
country = L"CN";
}
Object obj = Object::New(env);
obj.Set("locale", Napi::String::New(env, WStringToString(locale)));
obj.Set("screenSize", Napi::String::New(env, to_string(sw) + "x" + to_string(sh)));
obj.Set("carrierCountry", Napi::String::New(env, WStringToString(country)));
try {
auto computer = retrieveWmi<Win32_ComputerSystem>();
obj.Set("model", Napi::String::New(env, computer.Model));
obj.Set("oemName", Napi::String::New(env, computer.Manufacturer));
} catch (Wmi::WmiException &e) {
obj.Set("model", Napi::String::New(env, "Unknown"));
obj.Set("oemName", Napi::String::New(env, "Unknown"));
}
try {
auto os = retrieveWmi<Win32_OperatingSystem>();
string osv = os.Version;
obj.Set("osBuild", Napi::String::New(env, osv + "." + to_string(buildNum)));
obj.Set("osVersion", Napi::String::New(env, osv));
obj.Set("timeZoneOffset", Napi::Number::New(env, os.CurrentTimeZone));
} catch (Wmi::WmiException &e) {
obj.Set("osBuild", Napi::String::New(env, "Unknown"));
obj.Set("osVersion", Napi::String::New(env, "Unknown"));
obj.Set("timeZoneOffset", Napi::Number::New(env, 480));
}
return obj;
}
Value enablePrivilege(const CallbackInfo &info) {
Env env = info.Env();
EnablePrivilege(env, L"SeDebugPrivilege");
return env.Undefined();
}
Value copyToClipboard(const CallbackInfo &info) {
Env env = info.Env();
string text = info[0].As<Napi::String>().Utf8Value();
if (OpenClipboard(nullptr)) {
EmptyClipboard();
HGLOBAL hg = GlobalAlloc(GMEM_MOVEABLE, text.length() + 1);
if (hg != nullptr) {
memcpy(GlobalLock(hg), text.c_str(), text.length() + 1);
GlobalUnlock(hg);
SetClipboardData(CF_TEXT, hg);
}
CloseClipboard();
}
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));
return exports;
}
NODE_API_MODULE(addon, init)
}

View File

@@ -0,0 +1,40 @@
#pragma once
#ifndef REGISTRY_HPP
#define REGISTRY_HPP
#include "../define.h"
namespace RegUtils {
DWORD GetInt(Env env, HKEY hKey, const wstring &path, const wstring &value) {
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 data;
}
LSTATUS GetString(HKEY hKey, const wstring &path, const wstring &value, wstring &result) {
DWORD size = 0;
LSTATUS retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_SZ, nullptr, nullptr, &size);
if (retcode != ERROR_SUCCESS) {
return retcode;
}
wstring data;
DWORD len = size / sizeof(WCHAR);
data.resize(len);
retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_SZ, nullptr, &data[0], &size);
if (retcode != ERROR_SUCCESS) {
return retcode;
}
data.resize((len-1));
result = data;
return ERROR_SUCCESS;
}
}
#endif //REGISTRY_HPP

80
native/src/utils.cc Normal file
View File

@@ -0,0 +1,80 @@
#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);
delete[] buffer;
return result;
}
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);
delete[] buffer;
return result;
}
void Log(Env env, const string &msg) {
auto logFunc = env.Global().Get("console").As<Object>().Get("log").As<Function>();
logFunc.Call({ Napi::String::New(env, msg) });
}
void Log(Env env, const wstring &msg) {
auto logFunc = env.Global().Get("console").As<Object>().Get("log").As<Function>();
logFunc.Call({ Napi::String::New(env, WStringToString(msg)) });
}
LSTATUS OpenFile(Env env, Napi::String &result, HWND parent) {
OPENFILENAME open;
ZeroMemory(&open, sizeof(open));
WCHAR file[32768];
file[0]=L'\0';
open.Flags = OFN_FILEMUSTEXIST | OFN_NONETWORKBUTTON | OFN_PATHMUSTEXIST | OFN_EXPLORER;
open.hwndOwner = parent;
open.nMaxFile = 32768;
open.lpstrFile = file;
open.lpstrTitle = L"选择原神主程序";
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));
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;
}

15
native/src/utils.h Normal file
View File

@@ -0,0 +1,15 @@
#pragma once
#include "define.h"
string WStringToString(const wstring &src);
wstring StringToWString(const string &src);
LSTATUS OpenFile(Env env, Napi::String &result, HWND parent = GetConsoleWindow());
BOOL EnablePrivilege(Env env, const wstring &name);
void Log(Env env, const string &msg);
void Log(Env env, const wstring &msg);
#ifndef GENSHIN_EXPORT_NATIVE_UTILS_H
#define GENSHIN_EXPORT_NATIVE_UTILS_H
#endif //GENSHIN_EXPORT_NATIVE_UTILS_H

1
native/src/wmi Submodule

Submodule native/src/wmi added at aab36ea1e1

View File

@@ -4,7 +4,8 @@
"description": "", "description": "",
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"pkg": "pkg -t node16-win-x64 -C Brotli app.js --build" "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": [], "keywords": [],
"author": "", "author": "",

View File

@@ -22,10 +22,24 @@ message Achievement {
} }
message QueryCurRegion { message QueryCurRegion {
bytes field0 = 11; int32 retcode = 1;
bytes field1 = 12; string message = 2;
bytes field2 = 13; msg0 info = 3;
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 { message msg0 {
@@ -37,35 +51,36 @@ message msg0 {
string field3 = 9; string field3 = 9;
string field4 = 10; string field4 = 10;
string field5 = 11; string field5 = 11;
uint32 field6 = 14; string field6 = 12;
string field7 = 16; string field7 = 13;
uint32 field8 = 18; uint32 field8 = 14;
string field9 = 19; string field9 = 16;
string fieldA = 20; uint32 fieldA = 18;
bytes fieldB = 23; string fieldB = 19;
string fieldC = 24; string fieldC = 20;
string fieldD = 26; msg1 fieldD = 22;
string fieldE = 27; bytes fieldE = 23;
string fieldF = 30; string fieldF = 24;
string fieldG = 31; string fieldG = 26;
string fieldH = 32; string fieldH = 27;
string fieldI = 33; bool fieldI = 28;
msg1 fieldJ = 22; string fieldJ = 29;
string fieldK = 30;
string fieldL = 31;
string fieldM = 32;
string fieldN = 33;
string fieldO = 34;
msg1 fieldP = 35;
} }
message msg1 { message msg1 {
uint32 field0 = 1; uint32 field0 = 1;
string field1 = 3; bool field1 = 2;
string field2 = 4; string field2 = 3;
string field3 = 5; string field3 = 4;
string field4 = 6; string field4 = 5;
} string field5 = 6;
string field6 = 7;
message QueryRegionList {
bytes field0 = 5;
bytes field1 = 6;
bool field2 = 7;
repeated msg2 list = 2;
} }
message msg2 { message msg2 {
@@ -74,3 +89,14 @@ message msg2 {
string field2 = 3; string field2 = 3;
string url = 4; string url = 4;
} }
message msg3 {
string field0 = 1;
}
message msg4 {
uint32 field0 = 1;
uint32 field1 = 2;
string field2 = 3;
string field3 = 4;
}

View File

@@ -16,8 +16,15 @@ const getModifiedRegionList = async (conf) => {
channel_id: conf.channel, channel_id: conf.channel,
sub_channel_id: conf.subChannel sub_channel_id: conf.subChannel
} }
}).catch(_ => {
console.log("网络错误,请检查网络后重试 (22-1)")
process.exit(221)
}) })
const regions = await decodeProto(Buffer.from(d.data,"base64"),"QueryRegionList") const regions = await decodeProto(Buffer.from(d.data,"base64"),"QueryRegionList")
if (regions["retcode"] !== 0) {
console.log(`系统错误,请稍后重试 (${regions["retcode"]}-23)`)
process.exit(23)
}
regions.list = regions.list.map(item => { regions.list = regions.list.map(item => {
const host = new URL(item.url).host const host = new URL(item.url).host
if (regions.list.length === 1) { if (regions.list.length === 1) {
@@ -36,12 +43,19 @@ const getModifiedRegionInfo = async (url, uc, hs) => {
const query = noQueryRequest ? "" : `?${splitUrl[1]}` const query = noQueryRequest ? "" : `?${splitUrl[1]}`
const d = await axios.get(`https://${host}/query_cur_region${query}`, { const d = await axios.get(`https://${host}/query_cur_region${query}`, {
responseType: "text" responseType: "text"
}).catch(_ => {
console.log("网络错误,请检查网络后重试 (22-2)")
process.exit(222)
}) })
if (noQueryRequest) { if (noQueryRequest) {
preparedRegions[host] = true preparedRegions[host] = true
return d.data return d.data
} else { } else {
const region = await decodeProto(Buffer.from(d.data,"base64"),"QueryCurRegion") const region = await decodeProto(Buffer.from(d.data,"base64"),"QueryCurRegion")
if (region["retcode"] !== 0) {
console.log(`${region["message"]} (${region["retcode"]}-24)`)
process.exit(24)
}
const info = region.info const info = region.info
if (preparedRegions[host]) { if (preparedRegions[host]) {
if (currentProxy !== undefined) { if (currentProxy !== undefined) {
@@ -64,13 +78,12 @@ const agent = new https.Agent({
const create = async (conf, regionListLoadedCallback, regionSelectCallback) => { const create = async (conf, regionListLoadedCallback, regionSelectCallback) => {
const regions = await getModifiedRegionList(conf) const regions = await getModifiedRegionList(conf)
regionListLoadedCallback()
const hServer = https.createServer({ const hServer = https.createServer({
pfx: fs.readFileSync(cert), pfx: fs.readFileSync(cert),
passphrase: "" passphrase: ""
}, async (request, response) => { }, async (request, response) => {
const url = request.url const url = request.url
debug("HTTP请求: %s", url) debug("HTTP: %s", url)
response.writeHead(200, { "Content-Type": "text/html" }) response.writeHead(200, { "Content-Type": "text/html" })
if (url.startsWith("/query_region_list")) { if (url.startsWith("/query_region_list")) {
response.end(regions) response.end(regions)
@@ -81,10 +94,16 @@ const create = async (conf, regionListLoadedCallback, regionSelectCallback) => {
const frontResponse = await axios.get(`https://${conf.dispatchIP}${url}`, { const frontResponse = await axios.get(`https://${conf.dispatchIP}${url}`, {
responseType: "arraybuffer", responseType: "arraybuffer",
httpsAgent: agent httpsAgent: agent
}).catch(err => {
console.log("网络错误,请检查网络后重试 (22-3)")
console.log(err.message)
process.exit(223)
}) })
response.end(frontResponse.data) response.end(frontResponse.data)
} }
}).listen(443, "127.0.0.1") }).listen(443, "127.0.0.1", () => {
regionListLoadedCallback()
})
} }
module.exports = { module.exports = {

114
utils.js
View File

@@ -1,22 +1,18 @@
const fs = require("fs") const fs = require("fs")
const dns = require("dns") const dns = require("dns")
const ini = require("ini") const ini = require("ini")
const util = require("util")
const zlib = require("zlib") const zlib = require("zlib")
const cloud = require("./secret") const cloud = require("./secret")
const native = require("./native")
const readline = require("readline") const readline = require("readline")
const protobuf = require("protobufjs") const protobuf = require("protobufjs")
const { version } = require("./version") const { version } = require("./version")
const { promisify } = require("util")
const { createHash } = require("crypto") const { createHash } = require("crypto")
const path = require("path") const path = require("path")
const messages = path.join(__dirname, "./proto/Messages.proto") const messages = path.join(__dirname, "./proto/Messages.proto")
let axios = require("axios") let axios = require("axios")
const sleep = ms => new Promise(resolve => {
setTimeout(resolve, ms)
})
const encodeProto = (object, name) => protobuf.load(messages).then(r => { const encodeProto = (object, name) => protobuf.load(messages).then(r => {
const msgType = r.lookupType(name) const msgType = r.lookupType(name)
const msgInst = msgType.create(object) const msgInst = msgType.create(object)
@@ -27,42 +23,33 @@ const decodeProto = (buf, name) => protobuf.load(messages).then(r => {
return r.lookupType(name).decode(buf) return r.lookupType(name).decode(buf)
}) })
const checkPath = (path, cb) => { const checkPath = path => new Promise((resolve, reject) => {
if (!fs.existsSync(`${path}/UnityPlayer.dll`) && !fs.existsSync(`${path}/pkg_version`)) { if (!fs.existsSync(`${path}/UnityPlayer.dll`) || !fs.existsSync(`${path}/pkg_version`)) {
throw Error(`路径有误: ${path}`) reject(`路径有误 ${path}`)
} else { } else {
cb(path) resolve(path)
} }
} })
let conf let conf
const initConfig = async () => { const initConfig = async () => {
const configFileName = "./config.json" const configFileName = "./config.json"
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
const lookup = util.promisify(dns.lookup).bind(dns)
const question = util.promisify(rl.question).bind(rl)
if (fs.existsSync(configFileName)) { if (fs.existsSync(configFileName)) {
conf = JSON.parse(fs.readFileSync(configFileName, "utf-8")) conf = JSON.parse(fs.readFileSync(configFileName, "utf-8"))
} else { } else {
const p = await question("原神主程序(YuanShen.exe或GenshinImpact.exe)所在路径: (支持多个路径, 使用符号'*'分隔)\n")
conf = { conf = {
path: [], path: [],
offlineResource: false, offlineResource: false,
customCDN: "" customCDN: ""
} }
p.split("*").forEach(s => { const p = path.dirname(native.selectGameExecutable())
checkPath(s, () => { await checkPath(p).catch(reason => {
if (!conf.path.includes(s)) { console.log(reason)
conf.path.push(s) process.exit(1)
}
})
}) })
conf.path.push(p)
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2)) fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
rl.close()
} }
if (conf.proxy !== undefined) { if (conf.proxy !== undefined) {
axios = axios.create({ axios = axios.create({
@@ -70,34 +57,61 @@ const initConfig = async () => {
}) })
} }
if (conf.path.length === 1) { if (conf.path.length === 1) {
checkPath(conf.path[0], p => { await checkPath(conf.path[0]).catch(reason => {
console.log(reason)
process.exit(1)
}).then(p => {
conf.path = p conf.path = p
}) })
} else { } else {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
const question = (query) => new Promise(resolve => {
rl.question(query, resolve)
})
const idx = await question(`选择客户端: \n${conf.path.map((s, i) => { const idx = await question(`选择客户端: \n${conf.path.map((s, i) => {
const fp = fs.existsSync(`${s}/GenshinImpact.exe`) ? `${s}\\GenshinImpact.exe` : `${s}\\YuanShen.exe` const fp = fs.existsSync(`${s}/GenshinImpact.exe`) ? `${s}\\GenshinImpact.exe` : `${s}\\YuanShen.exe`
return `[${i}] ${fp}` return `[${i}] ${fp}`
}).join("\n")}\n> `) }).join("\n")}\n> `)
checkPath(conf.path[parseInt(idx)], p => { await checkPath(conf.path[parseInt(idx)]).catch(reason => {
console.log(reason)
process.exit(1)
}).then(p => {
conf.path = p conf.path = p
}) })
rl.close()
} }
rl.close()
conf.isOversea = fs.existsSync(conf.path + "/GenshinImpact.exe") conf.isOversea = fs.existsSync(conf.path + "/GenshinImpact.exe")
conf.dataDir = conf.isOversea ? conf.path + "/GenshinImpact_Data" : conf.path + "/YuanShen_Data" conf.dataDir = conf.isOversea ? conf.path + "/GenshinImpact_Data" : conf.path + "/YuanShen_Data"
const readGameRes = (path) => fs.readFileSync(conf.dataDir + path) const readGameRes = (path) => fs.readFileSync(conf.dataDir + path, "utf-8")
// noinspection JSUnresolvedVariable const genshinConf = ini.parse(fs.readFileSync(`${conf.path}/config.ini`, "utf-8"))["General"]
const genshinConf = ini.parse(fs.readFileSync(conf.path + "/config.ini", "utf-8")).General conf.channel = genshinConf["channel"]
conf.channel = genshinConf.channel conf.subChannel = genshinConf["sub_channel"]
// noinspection JSUnresolvedVariable conf.version = readGameRes("/Persistent/ChannelName") + readGameRes("/Persistent/ScriptVersion")
conf.subChannel = genshinConf.sub_channel conf.executable = conf.isOversea ? `${conf.path}/GenshinImpact.exe` : `${conf.path}/YuanShen.exe`
conf.version = readGameRes("/Persistent/ChannelName").toString() + readGameRes("/Persistent/ScriptVersion").toString()
conf.executable = conf.isOversea ? conf.path + "/GenshinImpact.exe" : conf.path + "/YuanShen.exe"
conf.dispatchUrl = `dispatch${conf.isOversea ? "os" : "cn"}global.yuanshen.com` conf.dispatchUrl = `dispatch${conf.isOversea ? "os" : "cn"}global.yuanshen.com`
conf.dispatchIP = (await lookup(conf.dispatchUrl, 4)).address conf.dispatchIP = (await promisify(dns.lookup).bind(dns)(conf.dispatchUrl, 4)).address
return conf return conf
} }
const checkGameIsRunning = () => {
const name = path.basename(conf.executable)
if (native.checkGameIsRunning(name)) {
console.log("原神正在运行,请关闭后重试")
process.exit(301)
}
}
const checkPortIsUsing = () => {
const portUsage = native.whoUseThePort(443)
if (portUsage !== undefined) {
console.log(`443端口被 ${path.basename(portUsage["path"])}(${portUsage["pid"]}) 占用,结束该进程后重试`)
process.exit(14)
}
}
const splitPacket = buf => { const splitPacket = buf => {
let offset = 0 let offset = 0
let arr = [] let arr = []
@@ -151,11 +165,11 @@ const checkCDN = async () => {
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore")) await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
return return
} catch (e) {} } catch (e) {}
throw "没有可用的CDN" throw "网络错误,请检查配置文件/网络后重试 (11-3)"
} }
const loadCache = async (fp, repo = "Dimbreath/GenshinData") => { const loadCache = async (fp, repo = "Dimbreath/GenshinData") => {
console.log(cdnUrlFormat.format(repo, fp)) log(`预检资源: ${cdnUrlFormat.format(repo, fp)}`)
fs.mkdirSync("./cache", { recursive: true }) fs.mkdirSync("./cache", { recursive: true })
const localPath = `./cache/${md5(fp)}` const localPath = `./cache/${md5(fp)}`
if (conf.offlineResource) { if (conf.offlineResource) {
@@ -174,17 +188,17 @@ const loadCache = async (fp, repo = "Dimbreath/GenshinData") => {
validateStatus: _ => true validateStatus: _ => true
}) })
if (headResponse.status === 304) { if (headResponse.status === 304) {
console.log("文件 %s 命中缓存", fp) log("%s 命中缓存", fp)
const etagLength = fd.readUInt8() const etagLength = fd.readUInt8()
return JSON.parse(fd.subarray(1 + etagLength).toString()) return JSON.parse(fd.subarray(1 + etagLength).toString())
} else { } else {
console.log("正在下载资源, 请稍后...") log("下载所需资源, 请稍后...")
const response = await axios.get(cdnUrlFormat.format(repo, fp)) const response = await axios.get(cdnUrlFormat.format(repo, fp))
const etag = response.headers.etag const etag = response.headers.etag
const str = JSON.stringify(response.data) const str = JSON.stringify(response.data)
const comp = brotliCompressSync(Buffer.concat([Buffer.of(etag.length), Buffer.from(etag), Buffer.from(str)])) const comp = brotliCompressSync(Buffer.concat([Buffer.of(etag.length), Buffer.from(etag), Buffer.from(str)]))
fs.writeFileSync(localPath, comp) fs.writeFileSync(localPath, comp)
console.log("完成.") log("下载完成")
return response.data return response.data
} }
} }
@@ -208,9 +222,9 @@ const upload = async data => {
const checkUpdate = async () => { const checkUpdate = async () => {
const data = (await cloud.get("/latest-version")).data const data = (await cloud.get("/latest-version")).data
if (data["vc"] !== version.code) { if (data["vc"] !== version.code) {
console.log(`有可用更新: ${version.name} => ${data["vn"]}`) log(`有可用更新: ${version.name} => ${data["vn"]}`)
console.log(`更新内容: \n${data["ds"]}`) log(`更新内容: \n${data["ds"]}`)
console.log("下载地址: https://github.com/HolographicHat/genshin-achievement-export/releases\n") log("下载地址: https://github.com/HolographicHat/genshin-achievement-export/releases\n")
} }
} }
@@ -223,10 +237,14 @@ const brotliCompressSync = data => zlib.brotliCompressSync(data,{
const brotliDecompressSync = data => zlib.brotliDecompressSync(data) const brotliDecompressSync = data => zlib.brotliDecompressSync(data)
let hostsContent = "" let hostsContent = undefined
const setupHost = (restore = false) => { const setupHost = (restore = false) => {
if (restore && hostsContent === undefined) return
const path = "C:\\Windows\\System32\\drivers\\etc\\hosts" const path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
if (!fs.existsSync(path)) {
fs.writeFileSync(path, "")
}
fs.chmodSync(path, 0o777) fs.chmodSync(path, 0o777)
if (restore) { if (restore) {
fs.writeFileSync(path, hostsContent) fs.writeFileSync(path, hostsContent)
@@ -287,6 +305,6 @@ class KPacket {
} }
module.exports = { module.exports = {
log, sleep, encodeProto, decodeProto, initConfig, splitPacket, upload, brotliCompressSync, brotliDecompressSync, log, encodeProto, decodeProto, initConfig, splitPacket, upload, brotliCompressSync, brotliDecompressSync,
setupHost, loadCache, debug, checkCDN, checkUpdate, KPacket, cdnUrlFormat setupHost, loadCache, debug, checkCDN, checkUpdate, KPacket, cdnUrlFormat, checkGameIsRunning, checkPortIsUsing
} }

View File

@@ -1,6 +1,6 @@
const version = { const version = {
code: 1, code: 5,
name: "1.0.0" name: "1.4"
} }
module.exports = { version } module.exports = { version }