mirror of
https://github.com/HolographicHat/Yae.git
synced 2025-12-11 00:48:12 +08:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60f0d8d23b | ||
|
|
1e15a49667 | ||
|
|
4528af7235 | ||
|
|
9850d1dbe4 | ||
|
|
b1f307de83 | ||
|
|
bea596b906 | ||
|
|
dd582437bc | ||
|
|
ea168ce96b | ||
|
|
90ab4dafe9 | ||
|
|
55f1ce3d55 | ||
|
|
0e51e080d4 | ||
|
|
01ab053d7d | ||
|
|
41af9c7cdb | ||
|
|
8a0c82f89f | ||
|
|
d64bf8149e | ||
|
|
82e85f5ea0 | ||
|
|
891a19c3f7 | ||
|
|
7b94964342 | ||
|
|
d5e290e866 | ||
|
|
f7c48472f1 | ||
|
|
a6b80d3588 | ||
|
|
18e3d3ffe3 | ||
|
|
a22ad8e87d | ||
|
|
35ec8b5859 | ||
|
|
35899e74f6 | ||
|
|
bb4d5215f1 | ||
|
|
e4e76286c9 | ||
|
|
be5a457639 | ||
|
|
945a94222a | ||
|
|
3d03496074 | ||
|
|
1e6ebc76dd | ||
|
|
ab47ff2b06 | ||
|
|
5fe6e80cc8 | ||
|
|
a23d7f3cc9 | ||
|
|
0ab6444b6f | ||
|
|
00f0aaa4a6 | ||
|
|
2377dbd492 | ||
|
|
0b015886ea | ||
|
|
4adfc9a312 | ||
|
|
b4cd69f303 | ||
|
|
c76ddd9e3f | ||
|
|
d40c456494 | ||
|
|
408169da4e | ||
|
|
9de8e957fd | ||
|
|
a287e5db43 | ||
|
|
589048b912 | ||
|
|
79517a37d2 | ||
|
|
e022f04661 | ||
|
|
d1a635be7c | ||
|
|
aa853262b7 | ||
|
|
bdf9561dfa | ||
|
|
a663fddeb0 | ||
|
|
7b18bcfbd3 | ||
|
|
b03bc2add8 | ||
|
|
a3ec29eda1 | ||
|
|
441ab78442 | ||
|
|
91991e1430 | ||
|
|
4a43666320 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,7 +1,11 @@
|
||||
cache
|
||||
cert
|
||||
config.json
|
||||
out.*
|
||||
node_modules
|
||||
.idea
|
||||
app*.exe
|
||||
export*.json
|
||||
secret.js
|
||||
package-lock.json
|
||||
native.node
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "native/src/wmi"]
|
||||
path = native/src/wmi
|
||||
url = https://github.com/HolographicHat/wmi
|
||||
32
README.md
32
README.md
@@ -1,13 +1,18 @@
|
||||
# 原神成就导出工具
|
||||
|
||||
   
|
||||
|
||||
- 支持导出所有成就
|
||||
- 没有窗口大小游戏或语言等要求
|
||||
- 更快、更准
|
||||
- 支持官服,B服与国际服
|
||||
- 支持导出至[椰羊](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)
|
||||
- 没有窗口大小、游戏语言等要求
|
||||
|
||||
## 使用说明
|
||||
**打开程序前需要关闭正在运行的原神主程序**
|
||||
第一次打开需要先设置原神主程序所在路径,支持多个路径, 使用符号'*'分隔
|
||||

|
||||
第一次打开需要先设置原神主程序(YuanShen.exe/GenshinImpact.exe)所在路径
|
||||

|
||||
### Windows7
|
||||
系统变量添加名为```NODE_SKIP_PLATFORM_CHECK```的变量并将值设为```1```
|
||||
|
||||
## 下载地址
|
||||
[releases/latest](https://github.com/HolographicHat/genshin-achievement-export/releases/latest)
|
||||
@@ -15,11 +20,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)
|
||||
|
||||
## FAQ
|
||||
1. Q: 为什么需要管理员权限
|
||||
## 常见问题
|
||||
0. Q: 程序异常退出或被强行终止后,程序报错:网络错误(22-1) 或 原神启动时报错: 无法连接网络(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和启动原神
|
||||
2. Q: 报毒?
|
||||
A: 执行命令或修改hosts引发,相关代码可在./utils.js,./appcenter.js下找到
|
||||
|
||||
3. Q: 报毒?
|
||||
A: 执行命令或修改hosts引发,相关代码可在./utils.js,./appcenter.js下找到
|
||||
|
||||
## TODO
|
||||
- [ ] 整个GUI
|
||||
4. Q: 原神进门后没有自动退出,程序输出停留在“加载完成”
|
||||
A: 关闭代理后重试
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
91
app.js
91
app.js
@@ -1,36 +1,62 @@
|
||||
const zlib = require("zlib")
|
||||
const proxy = require("udp-proxy")
|
||||
const cp = require("child_process")
|
||||
const rs = require("./regionServer")
|
||||
const appcenter = require("./appcenter")
|
||||
const { initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkCDN, checkUpdate } = require("./utils")
|
||||
const { exportData } = require("./export");
|
||||
const {
|
||||
initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkUpdate,
|
||||
brotliCompressSync, brotliDecompressSync, checkGameIsRunning, checkPortIsUsing
|
||||
} = require("./utils")
|
||||
const { exportData } = require("./export")
|
||||
const { enablePrivilege, pause } = require("./native")
|
||||
|
||||
const onExit = () => {
|
||||
setupHost(true)
|
||||
console.log("按任意键退出")
|
||||
pause()
|
||||
};
|
||||
|
||||
// TODO: i18n
|
||||
// TODO: send ack to avoid resend
|
||||
(async () => {
|
||||
try {
|
||||
appcenter.init()
|
||||
let conf = await initConfig()
|
||||
process.on("SIGHUP", () => setupHost(true))
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
console.log("Unhandled Rejection at: ", promise, "\n0Reason:", reason)
|
||||
})
|
||||
process.on("uncaughtException", (err, origin) => {
|
||||
appcenter.uploadError(err, true)
|
||||
console.log(err)
|
||||
console.log(`Origin: ${origin}`)
|
||||
process.exit(1)
|
||||
})
|
||||
process.on("exit", onExit)
|
||||
process.on("SIGINT", onExit)
|
||||
try {
|
||||
cp.execSync("net session", { stdio: "ignore" })
|
||||
enablePrivilege()
|
||||
} catch (e) {
|
||||
console.log("\x1b[91m请使用管理员身份运行此程序\x1b[0m")
|
||||
return
|
||||
console.log("请使用管理员身份运行此程序")
|
||||
process.exit(-1)
|
||||
}
|
||||
appcenter.startup()
|
||||
let conf = await initConfig()
|
||||
checkPortIsUsing()
|
||||
checkGameIsRunning()
|
||||
log("检查更新")
|
||||
await checkUpdate()
|
||||
checkCDN().then(_ => debug("CDN check success."))
|
||||
let gameProcess
|
||||
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,() => {
|
||||
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) => {
|
||||
let login = false
|
||||
let cache = new Map()
|
||||
@@ -54,16 +80,18 @@ const { exportData } = require("./export");
|
||||
}
|
||||
}
|
||||
let monitor;
|
||||
let stopped = false
|
||||
const createMonitor = () => {
|
||||
monitor = setInterval(async () => {
|
||||
if (login && lastRecvTimestamp + 2 < parseInt(Date.now() / 1000)) {
|
||||
if (login && lastRecvTimestamp + 2 < parseInt(Date.now() / 1000) && !stopped) {
|
||||
stopped = true
|
||||
unexpectedExit = false
|
||||
server.close()
|
||||
server.close(() => {})
|
||||
hServer.close()
|
||||
gameProcess.kill()
|
||||
clearInterval(monitor)
|
||||
setupHost(true)
|
||||
console.log("正在处理数据,请稍后...")
|
||||
log("正在处理数据,请稍后...")
|
||||
let packets = Array.from(cache.values())
|
||||
cache.clear()
|
||||
packets.sort((a, b) => a.frg - b.frg)
|
||||
@@ -85,18 +113,16 @@ const { exportData } = require("./export");
|
||||
return Buffer.concat([len, data])
|
||||
})
|
||||
const merged = Buffer.concat(packets)
|
||||
const compressed = zlib.brotliCompressSync(merged)
|
||||
const compressed = brotliCompressSync(merged)
|
||||
const response = await upload(compressed)
|
||||
const data = brotliDecompressSync(response.data)
|
||||
if (response.status !== 200) {
|
||||
log(`发生错误: ${response.data.toString()}`)
|
||||
log(`发生错误: ${data}`)
|
||||
log(`请求ID: ${response.headers["x-api-requestid"]}`)
|
||||
log("请联系开发者以获取帮助")
|
||||
} else {
|
||||
const data = zlib.brotliDecompressSync(response.data)
|
||||
const proto = await decodeProto(data,"AllAchievement")
|
||||
await exportData(proto)
|
||||
console.log("按任意键退出")
|
||||
cp.execSync("pause > nul", { stdio: "inherit" })
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
@@ -108,6 +134,8 @@ const { exportData } = require("./export");
|
||||
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, _) => {
|
||||
lastRecvTimestamp = parseInt(Date.now() / 1000)
|
||||
let buf = Buffer.from(msg)
|
||||
@@ -117,6 +145,9 @@ const { exportData } = require("./export");
|
||||
createMonitor()
|
||||
debug("服务端握手应答")
|
||||
break
|
||||
case 404:
|
||||
lastRecvTimestamp = parseInt(Date.now() / 1000) - 2333
|
||||
break
|
||||
default:
|
||||
console.log(`Unhandled: ${buf.toString("hex")}`)
|
||||
process.exit(2)
|
||||
@@ -132,7 +163,7 @@ const { exportData } = require("./export");
|
||||
})
|
||||
})
|
||||
return server
|
||||
}).then(() => console.log("加载完毕"))
|
||||
}).then(() => log("加载完毕"))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (e instanceof Error) {
|
||||
@@ -140,8 +171,6 @@ const { exportData } = require("./export");
|
||||
} else {
|
||||
appcenter.uploadError(Error(e), true)
|
||||
}
|
||||
console.log("按任意键退出")
|
||||
cp.execSync("pause > nul", { stdio: "inherit" })
|
||||
process.exit(0)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
|
||||
99
appcenter.js
99
appcenter.js
@@ -1,80 +1,52 @@
|
||||
const cp = require("child_process")
|
||||
const axios = require("axios")
|
||||
const crypto = require("crypto")
|
||||
const { version } = require("./version")
|
||||
const { pid, argv0, uptime, report } = require("node:process")
|
||||
const { getDeviceID, getDeviceInfo } = require("./native")
|
||||
|
||||
const getTimestamp = (d = new Date()) => {
|
||||
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`
|
||||
}
|
||||
|
||||
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 session = crypto.randomUUID()
|
||||
const key = "648b83bf-d439-49bd-97f4-e1e506bdfe39"
|
||||
|
||||
const install = (() => {
|
||||
const s = readRegistry("HKCU\\SOFTWARE\\miHoYoSDK", "MIHOYOSDK_DEVICE_ID")
|
||||
return `${s.substring(0, 8)}-${s.substring(8, 12)}-${s.substring(12, 16)}-${s.substring(16, 20)}-${s.substring(20, 32)}`
|
||||
const id = getDeviceID()
|
||||
return id === undefined ? crypto.randomUUID() : id
|
||||
})()
|
||||
|
||||
const device = (() => {
|
||||
const csi = cp.execSync("wmic computersystem get manufacturer,model /format:csv", {
|
||||
encoding: "utf-8"
|
||||
}).split("\n")[2].split(",").map(s => s.trim())
|
||||
const osi = cp.execSync("wmic os get currentTimeZone, version /format:csv", {
|
||||
encoding: "utf-8"
|
||||
}).split("\n")[2].split(",").map(s => s.trim())
|
||||
return {
|
||||
model: csi[2],
|
||||
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 info = getDeviceInfo()
|
||||
info.appBuild = version.code
|
||||
info.appVersion = version.name
|
||||
info.sdkName = "appcenter.wpf.netcore"
|
||||
info.sdkVersion = "4.5.0"
|
||||
info.osName = "WINDOWS"
|
||||
info.appNamespace = "default"
|
||||
return info
|
||||
})()
|
||||
|
||||
const upload = () => {
|
||||
if (queue.length > 0) {
|
||||
try {
|
||||
const data = JSON.stringify({ "logs": queue })
|
||||
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 (e) {}
|
||||
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
|
||||
}
|
||||
}).catch(_ => {}).then()
|
||||
}
|
||||
}
|
||||
|
||||
const uploadError = (err, fatal) => {
|
||||
const eid = crypto.randomUUID()
|
||||
const reportJson = report.getReport(err)
|
||||
const reportJson = process.report.getReport(err)
|
||||
const reportAttachment = {
|
||||
type: "errorAttachment",
|
||||
device: device,
|
||||
@@ -94,10 +66,10 @@ const uploadError = (err, fatal) => {
|
||||
architecture: "AMD64",
|
||||
userId: install,
|
||||
fatal: fatal,
|
||||
processId: pid,
|
||||
processName: argv0.replaceAll("\\", "/").split("/").pop(),
|
||||
processId: process.pid,
|
||||
processName: process.argv0.replaceAll("\\", "/").split("/").pop(),
|
||||
timestamp: getTimestamp(),
|
||||
appLaunchTimestamp: getTimestamp(new Date(Date.now() - uptime())),
|
||||
appLaunchTimestamp: getTimestamp(new Date(Date.now() - process.uptime())),
|
||||
exception: {
|
||||
"type": err.name,
|
||||
"message": err.message,
|
||||
@@ -109,7 +81,20 @@ const uploadError = (err, fatal) => {
|
||||
upload()
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
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",
|
||||
services: [ "Analytics","Crashes" ],
|
||||
@@ -123,9 +108,9 @@ const init = () => {
|
||||
device: device
|
||||
})
|
||||
upload()
|
||||
setInterval(() => upload(), 10000)
|
||||
setInterval(() => upload(), 5000)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init, upload, uploadError
|
||||
startup, upload, uploadError, uploadEvent
|
||||
}
|
||||
|
||||
BIN
cert/root.p12
BIN
cert/root.p12
Binary file not shown.
126
export.js
126
export.js
@@ -1,76 +1,84 @@
|
||||
const fs = require("fs")
|
||||
const util = require("util")
|
||||
const axios = require("axios")
|
||||
const readline = require("readline")
|
||||
const { spawnSync } = require("child_process")
|
||||
const { loadCache } = require("./utils")
|
||||
const { loadCache, log } = require("./utils")
|
||||
const { openUrl, copyToClipboard } = require("./native")
|
||||
|
||||
const exportToSeelie = proto => {
|
||||
const out = { achievements: {} }
|
||||
proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => {
|
||||
proto.list.filter(a => a.status === 3 || a.status === 2).forEach(({id}) => {
|
||||
out.achievements[id] = { done: true }
|
||||
})
|
||||
const fp = `./export-${Date.now()}-seelie.json`
|
||||
fs.writeFileSync(fp, JSON.stringify(out))
|
||||
console.log(`导出为文件: ${fp}`)
|
||||
log(`导出为文件: ${fp}`)
|
||||
}
|
||||
|
||||
const exportToPaimon = async proto => {
|
||||
const out = { achievements: {} }
|
||||
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)
|
||||
if (out.achievements[gid] === undefined) {
|
||||
out.achievements[gid] = {}
|
||||
const out = { achievement: {} }
|
||||
const data = await loadCache()
|
||||
proto.list.filter(a => a.status === 3 || a.status === 2).forEach(({id}) => {
|
||||
const gid = data["a"][id]
|
||||
if (out.achievement[gid] === undefined) {
|
||||
out.achievement[gid] = {}
|
||||
}
|
||||
out.achievements[gid][id] = true
|
||||
out.achievement[gid][id] = true
|
||||
})
|
||||
const fp = `./export-${Date.now()}-paimon.json`
|
||||
fs.writeFileSync(fp, JSON.stringify(out))
|
||||
console.log(`导出为文件: ${fp}`)
|
||||
log(`导出为文件: ${fp}`)
|
||||
}
|
||||
|
||||
const exportToSnapGenshin = async proto => {
|
||||
const out = []
|
||||
proto.list.filter(a => a.status === 3 || a.status === 2).forEach(({id, finishTimestamp}) => {
|
||||
out.push({
|
||||
id: id,
|
||||
ts: finishTimestamp
|
||||
})
|
||||
})
|
||||
const json = JSON.stringify(out, null, 2)
|
||||
copyToClipboard(json)
|
||||
log("导出内容已复制到剪贴板")
|
||||
}
|
||||
|
||||
const exportToCocogoat = async proto => {
|
||||
const out = {
|
||||
value: {
|
||||
achievements: []
|
||||
}
|
||||
achievements: []
|
||||
}
|
||||
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 data = await loadCache()
|
||||
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(({current, finishTimestamp, id, require}) => {
|
||||
const curAch = data["a"][id]
|
||||
out.achievements.push({
|
||||
id: id,
|
||||
status: current === undefined || current === 0 || preStageAchievementIdList.includes(id) ? `${require}/${require}` : `${current}/${require}`,
|
||||
categoryId: achTable.get(id),
|
||||
status: current === undefined || current === 0 || curAch["p"] === undefined ? `${require}/${require}` : `${current}/${require}`,
|
||||
categoryId: curAch["g"],
|
||||
date: getDate(finishTimestamp)
|
||||
})
|
||||
})
|
||||
spawnSync("clip", { input: JSON.stringify(out,null,2) })
|
||||
console.log("导出内容已复制到剪贴板")
|
||||
const response = await axios.post(`https://77.cocogoat.work/v1/memo?source=${encodeURI("全部成就")}`, out).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(`导出失败,请联系开发者以获取帮助 (26-3-${retcode})`)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -85,20 +93,23 @@ const exportToCsv = async proto => {
|
||||
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())}`
|
||||
}
|
||||
const bl = [84517]
|
||||
proto.list.forEach(({current, finishTimestamp, id, status, require}) => {
|
||||
const desc = achievementMap.get(id) === undefined ? (() => {
|
||||
console.log(`Error get id ${id} in excel`)
|
||||
return {
|
||||
goal: "未知",
|
||||
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)}`)
|
||||
if (!bl.includes(id)) {
|
||||
const curAch = data["a"][id] === undefined ? (() => {
|
||||
console.log(`Error get id ${id} in excel`)
|
||||
return {
|
||||
g: "未知",
|
||||
n: "未知",
|
||||
d: "未知"
|
||||
}
|
||||
})() : 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`
|
||||
fs.writeFileSync(fp, `\uFEFF${outputLines.join("\n")}`)
|
||||
console.log(`导出为文件: ${fp}`)
|
||||
log(`导出为文件: ${fp}`)
|
||||
}
|
||||
|
||||
const exportData = async proto => {
|
||||
@@ -106,19 +117,28 @@ const exportData = async proto => {
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
const question = util.promisify(rl.question).bind(rl)
|
||||
const chosen = await question("导出至: \n[0] 椰羊 (https://cocogoat.work/achievement)\n[1] Paimon.moe\n[2] Seelie.me\n[3] 表格文件 (默认)\n> ")
|
||||
const question = (query) => new Promise(resolve => {
|
||||
rl.question(query, resolve)
|
||||
})
|
||||
const chosen = await question("导出至: \n[0] 椰羊 (https://cocogoat.work/achievement)\n[1] SnapGenshin\n[2] Paimon.moe\n[3] Seelie.me\n[4] 表格文件 (默认)\n输入一个数字(0-4): ")
|
||||
rl.close()
|
||||
switch (chosen) {
|
||||
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":
|
||||
fs.writeFileSync(`./export-${Date.now()}-raw.json`, JSON.stringify(proto,null,2))
|
||||
log("OK")
|
||||
break
|
||||
default:
|
||||
await exportToCsv(proto)
|
||||
}
|
||||
|
||||
9
native.d.ts
vendored
Normal file
9
native.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
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
|
||||
export function openUrl(url: string): number
|
||||
export function pause(): any
|
||||
5
native/.gitignore
vendored
Normal file
5
native/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
CMakeLists.txt
|
||||
.idea
|
||||
cmake-build-debug
|
||||
node_modules
|
||||
build
|
||||
42
native/binding.gyp
Normal file
42
native/binding.gyp
Normal 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
15
native/package.json
Normal 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
14
native/src/define.h
Normal 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;
|
||||
194
native/src/main.cc
Normal file
194
native/src/main.cc
Normal file
@@ -0,0 +1,194 @@
|
||||
#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")
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Object init(Env env, Object exports) {
|
||||
exports.Set("pause", Function::New(env, pause));
|
||||
exports.Set("openUrl", Function::New(env, openUrl));
|
||||
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)
|
||||
|
||||
}
|
||||
40
native/src/registry/registry.hpp
Normal file
40
native/src/registry/registry.hpp
Normal 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
80
native/src/utils.cc
Normal 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
15
native/src/utils.h
Normal 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
1
native/src/wmi
Submodule
Submodule native/src/wmi added at aab36ea1e1
@@ -4,7 +4,8 @@
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"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": [],
|
||||
"author": "",
|
||||
|
||||
@@ -22,10 +22,24 @@ message Achievement {
|
||||
}
|
||||
|
||||
message QueryCurRegion {
|
||||
bytes field0 = 11;
|
||||
bytes field1 = 12;
|
||||
bytes field2 = 13;
|
||||
msg0 info = 3;
|
||||
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 {
|
||||
@@ -37,35 +51,36 @@ message msg0 {
|
||||
string field3 = 9;
|
||||
string field4 = 10;
|
||||
string field5 = 11;
|
||||
uint32 field6 = 14;
|
||||
string field7 = 16;
|
||||
uint32 field8 = 18;
|
||||
string field9 = 19;
|
||||
string fieldA = 20;
|
||||
bytes fieldB = 23;
|
||||
string fieldC = 24;
|
||||
string fieldD = 26;
|
||||
string fieldE = 27;
|
||||
string fieldF = 30;
|
||||
string fieldG = 31;
|
||||
string fieldH = 32;
|
||||
string fieldI = 33;
|
||||
msg1 fieldJ = 22;
|
||||
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;
|
||||
string field1 = 3;
|
||||
string field2 = 4;
|
||||
string field3 = 5;
|
||||
string field4 = 6;
|
||||
}
|
||||
|
||||
message QueryRegionList {
|
||||
bytes field0 = 5;
|
||||
bytes field1 = 6;
|
||||
bool field2 = 7;
|
||||
repeated msg2 list = 2;
|
||||
bool field1 = 2;
|
||||
string field2 = 3;
|
||||
string field3 = 4;
|
||||
string field4 = 5;
|
||||
string field5 = 6;
|
||||
string field6 = 7;
|
||||
}
|
||||
|
||||
message msg2 {
|
||||
@@ -74,3 +89,14 @@ message msg2 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,15 @@ const getModifiedRegionList = async (conf) => {
|
||||
channel_id: conf.channel,
|
||||
sub_channel_id: conf.subChannel
|
||||
}
|
||||
}).catch(_ => {
|
||||
console.log("网络错误,请检查网络后重试 (22-1)")
|
||||
process.exit(221)
|
||||
})
|
||||
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 => {
|
||||
const host = new URL(item.url).host
|
||||
if (regions.list.length === 1) {
|
||||
@@ -36,12 +43,19 @@ const getModifiedRegionInfo = async (url, uc, hs) => {
|
||||
const query = noQueryRequest ? "" : `?${splitUrl[1]}`
|
||||
const d = await axios.get(`https://${host}/query_cur_region${query}`, {
|
||||
responseType: "text"
|
||||
}).catch(_ => {
|
||||
console.log("网络错误,请检查网络后重试 (22-2)")
|
||||
process.exit(222)
|
||||
})
|
||||
if (noQueryRequest) {
|
||||
preparedRegions[host] = true
|
||||
return d.data
|
||||
} else {
|
||||
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
|
||||
if (preparedRegions[host]) {
|
||||
if (currentProxy !== undefined) {
|
||||
@@ -64,13 +78,12 @@ const agent = new https.Agent({
|
||||
|
||||
const create = async (conf, regionListLoadedCallback, regionSelectCallback) => {
|
||||
const regions = await getModifiedRegionList(conf)
|
||||
regionListLoadedCallback()
|
||||
const hServer = https.createServer({
|
||||
pfx: fs.readFileSync(cert),
|
||||
passphrase: ""
|
||||
}, async (request, response) => {
|
||||
const url = request.url
|
||||
debug("HTTP请求: %s", url)
|
||||
debug("HTTP: %s", url)
|
||||
response.writeHead(200, { "Content-Type": "text/html" })
|
||||
if (url.startsWith("/query_region_list")) {
|
||||
response.end(regions)
|
||||
@@ -81,10 +94,16 @@ const create = async (conf, regionListLoadedCallback, regionSelectCallback) => {
|
||||
const frontResponse = await axios.get(`https://${conf.dispatchIP}${url}`, {
|
||||
responseType: "arraybuffer",
|
||||
httpsAgent: agent
|
||||
}).catch(err => {
|
||||
console.log("网络错误,请检查网络后重试 (22-3)")
|
||||
console.log(err.message)
|
||||
process.exit(223)
|
||||
})
|
||||
response.end(frontResponse.data)
|
||||
}
|
||||
}).listen(443, "127.0.0.1")
|
||||
}).listen(443, "127.0.0.1", () => {
|
||||
regionListLoadedCallback()
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
230
utils.js
230
utils.js
@@ -1,22 +1,18 @@
|
||||
const fs = require("fs")
|
||||
const dns = require("dns")
|
||||
const ini = require("ini")
|
||||
const util = require("util")
|
||||
const zlib = require("zlib")
|
||||
const cloud = require("./secret")
|
||||
const native = require("./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 { uploadEvent } = require("./appcenter")
|
||||
const messages = path.join(__dirname, "./proto/Messages.proto")
|
||||
|
||||
let axios = require("axios")
|
||||
|
||||
const sleep = ms => new Promise(resolve => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
|
||||
const encodeProto = (object, name) => protobuf.load(messages).then(r => {
|
||||
const msgType = r.lookupType(name)
|
||||
const msgInst = msgType.create(object)
|
||||
@@ -27,77 +23,95 @@ const decodeProto = (buf, name) => protobuf.load(messages).then(r => {
|
||||
return r.lookupType(name).decode(buf)
|
||||
})
|
||||
|
||||
const checkPath = (path, cb) => {
|
||||
if (!fs.existsSync(`${path}/UnityPlayer.dll`) && !fs.existsSync(`${path}/pkg_version`)) {
|
||||
throw Error(`路径有误: ${path}`)
|
||||
const checkPath = path => new Promise((resolve, reject) => {
|
||||
if (!fs.existsSync(`${path}/UnityPlayer.dll`) || !fs.existsSync(`${path}/pkg_version`)) {
|
||||
reject(`路径有误 ${path}`)
|
||||
} else {
|
||||
cb(path)
|
||||
resolve(path)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let conf
|
||||
|
||||
const initConfig = async () => {
|
||||
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)) {
|
||||
conf = JSON.parse(fs.readFileSync(configFileName, "utf-8"))
|
||||
} else {
|
||||
const p = await question("原神主程序(YuanShen.exe或GenshinImpact.exe)所在路径: (支持多个路径, 使用符号'*'分隔)\n")
|
||||
conf = {
|
||||
path: [],
|
||||
offlineResource: false,
|
||||
customCDN: ""
|
||||
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))
|
||||
}
|
||||
p.split("*").forEach(s => {
|
||||
checkPath(s, () => {
|
||||
if (!conf.path.includes(s)) {
|
||||
conf.path.push(s)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
conf = {
|
||||
path: []
|
||||
}
|
||||
const p = path.dirname(native.selectGameExecutable())
|
||||
await checkPath(p).catch(reason => {
|
||||
console.log(reason)
|
||||
process.exit(1)
|
||||
})
|
||||
conf.path.push(p)
|
||||
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
|
||||
rl.close()
|
||||
}
|
||||
if (conf.proxy !== undefined) {
|
||||
axios = axios.create({
|
||||
proxy: conf.proxy
|
||||
})
|
||||
}
|
||||
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
|
||||
})
|
||||
} 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 fp = fs.existsSync(`${s}/GenshinImpact.exe`) ? `${s}\\GenshinImpact.exe` : `${s}\\YuanShen.exe`
|
||||
return `[${i}] ${fp}`
|
||||
}).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
|
||||
})
|
||||
rl.close()
|
||||
}
|
||||
rl.close()
|
||||
conf.isOversea = fs.existsSync(conf.path + "/GenshinImpact.exe")
|
||||
conf.dataDir = conf.isOversea ? conf.path + "/GenshinImpact_Data" : conf.path + "/YuanShen_Data"
|
||||
const readGameRes = (path) => fs.readFileSync(conf.dataDir + path)
|
||||
// noinspection JSUnresolvedVariable
|
||||
const genshinConf = ini.parse(fs.readFileSync(conf.path + "/config.ini", "utf-8")).General
|
||||
conf.channel = genshinConf.channel
|
||||
// noinspection JSUnresolvedVariable
|
||||
conf.subChannel = genshinConf.sub_channel
|
||||
conf.version = readGameRes("/Persistent/ChannelName").toString() + readGameRes("/Persistent/ScriptVersion").toString()
|
||||
conf.executable = conf.isOversea ? conf.path + "/GenshinImpact.exe" : conf.path + "/YuanShen.exe"
|
||||
const readGameRes = (path) => fs.readFileSync(conf.dataDir + path, "utf-8")
|
||||
const genshinConf = ini.parse(fs.readFileSync(`${conf.path}/config.ini`, "utf-8"))["General"]
|
||||
conf.channel = genshinConf["channel"]
|
||||
conf.subChannel = genshinConf["sub_channel"]
|
||||
conf.version = readGameRes("/Persistent/ChannelName") + readGameRes("/Persistent/ScriptVersion")
|
||||
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 lookup(conf.dispatchUrl, 4)).address
|
||||
conf.dispatchIP = (await promisify(dns.lookup).bind(dns)(conf.dispatchUrl, 4)).address
|
||||
uploadEvent("AppInitialize", { ClientVersion: version.name, GameVersion: conf.version })
|
||||
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 => {
|
||||
let offset = 0
|
||||
let arr = []
|
||||
@@ -115,77 +129,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 "没有可用的CDN"
|
||||
}
|
||||
|
||||
const loadCache = async (fp, repo = "Dimbreath/GenshinData") => {
|
||||
console.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) {
|
||||
console.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 {
|
||||
console.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)
|
||||
console.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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,9 +196,9 @@ const upload = async data => {
|
||||
const checkUpdate = async () => {
|
||||
const data = (await cloud.get("/latest-version")).data
|
||||
if (data["vc"] !== version.code) {
|
||||
console.log(`有可用更新: ${version.name} => ${data["vn"]}`)
|
||||
console.log(`更新内容: \n${data["ds"]}`)
|
||||
console.log("下载地址: https://github.com/HolographicHat/genshin-achievement-export/releases\n")
|
||||
log(`有可用更新: ${version.name} => ${data["vn"]}`)
|
||||
log(`更新内容: \n${data["ds"]}`)
|
||||
log("下载地址: https://github.com/HolographicHat/genshin-achievement-export/releases\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,10 +211,14 @@ const brotliCompressSync = data => zlib.brotliCompressSync(data,{
|
||||
|
||||
const brotliDecompressSync = data => zlib.brotliDecompressSync(data)
|
||||
|
||||
let hostsContent = ""
|
||||
let hostsContent = undefined
|
||||
|
||||
const setupHost = (restore = false) => {
|
||||
const path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
|
||||
if (restore && hostsContent === undefined) return
|
||||
const path = `${process.env.windir}\\System32\\drivers\\etc\\hosts`
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.writeFileSync(path, "")
|
||||
}
|
||||
fs.chmodSync(path, 0o777)
|
||||
if (restore) {
|
||||
fs.writeFileSync(path, hostsContent)
|
||||
@@ -287,6 +279,6 @@ class KPacket {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
log, sleep, encodeProto, decodeProto, initConfig, splitPacket, upload, brotliCompressSync, brotliDecompressSync,
|
||||
setupHost, loadCache, debug, checkCDN, checkUpdate, KPacket, cdnUrlFormat
|
||||
log, encodeProto, decodeProto, initConfig, splitPacket, upload, brotliCompressSync, brotliDecompressSync,
|
||||
setupHost, loadCache, debug, checkUpdate, KPacket, checkGameIsRunning, checkPortIsUsing
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const version = {
|
||||
code: 1,
|
||||
name: "1.0.0"
|
||||
code: 6,
|
||||
name: "1.5"
|
||||
}
|
||||
|
||||
module.exports = { version }
|
||||
|
||||
Reference in New Issue
Block a user