mirror of
https://github.com/HolographicHat/Yae.git
synced 2025-12-08 07:32:47 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f8296c3dc | ||
|
|
37382b28e0 | ||
|
|
a46e49722f | ||
|
|
700cbbb86d | ||
|
|
6a23153f70 | ||
|
|
75e3cd848f | ||
|
|
96912d3da7 | ||
|
|
02b034cc48 | ||
|
|
00898d11cb | ||
|
|
7cf03ad905 | ||
|
|
7d9e5bc218 | ||
|
|
60f0d8d23b | ||
|
|
1e15a49667 | ||
|
|
4528af7235 | ||
|
|
9850d1dbe4 | ||
|
|
b1f307de83 | ||
|
|
bea596b906 | ||
|
|
dd582437bc | ||
|
|
ea168ce96b | ||
|
|
90ab4dafe9 | ||
|
|
55f1ce3d55 | ||
|
|
0e51e080d4 | ||
|
|
01ab053d7d | ||
|
|
41af9c7cdb |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,7 +4,8 @@ config.json
|
||||
out.*
|
||||
node_modules
|
||||
.idea
|
||||
app*.exe
|
||||
YaeAchievement-*.7z
|
||||
YaeAchievement-*.exe
|
||||
export*.json
|
||||
secret.js
|
||||
package-lock.json
|
||||
|
||||
25
README.md
25
README.md
@@ -1,11 +1,11 @@
|
||||
# 原神成就导出工具
|
||||
|
||||
   
|
||||
    
|
||||
|
||||
- 支持导出所有成就
|
||||
- 支持官服,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)
|
||||
- 没有窗口大小、游戏语言等要求
|
||||
|
||||
## 使用说明
|
||||
**打开程序前需要关闭正在运行的原神主程序**
|
||||
@@ -13,20 +13,7 @@
|
||||

|
||||
### Windows7
|
||||
系统变量添加名为```NODE_SKIP_PLATFORM_CHECK```的变量并将值设为```1```
|
||||
### 自定义代理(可选)
|
||||
配置文件内添加proxy字段,详细请参看[Axios-请求配置](https://axios-http.com/zh/docs/req_config)
|
||||
```json
|
||||
{
|
||||
"path": [],
|
||||
"offlineResource": false,
|
||||
"customCDN": "",
|
||||
"proxy": {
|
||||
"protocol": "http",
|
||||
"host": "127.0.0.1",
|
||||
"port": 7890
|
||||
}
|
||||
}
|
||||
```
|
||||
[[?]如何添加环境变量](https://www.bing.com/search?q=windows+7+环境变量)
|
||||
|
||||
## 下载地址
|
||||
[releases/latest](https://github.com/HolographicHat/genshin-achievement-export/releases/latest)
|
||||
@@ -35,7 +22,7 @@
|
||||
[issues](https://github.com/HolographicHat/genshin-achievement-export/issues)或[QQ群: 913777414](https://qm.qq.com/cgi-bin/qm/qr?k=9UGz-chQVTjZa4b82RA_A41vIcBVNpms&jump_from=webapi)
|
||||
|
||||
## 常见问题
|
||||
0. Q: 程序异常退出或被强行终止后,原神启动时报错: 无法连接网络(4201)
|
||||
0. Q: 程序异常退出或被强行终止后,程序报错:网络错误(22-1) 或 原神启动时报错: 无法连接网络(4201)
|
||||
A: 用文本编辑器打开```C:\Windows\System32\drivers\etc\hosts```,删除```127.0.0.1 dispatch**global.yuanshen.com```后保存,重启原神
|
||||
|
||||
1. Q: 原神启动时报错: 数据异常(31-4302)
|
||||
@@ -48,4 +35,4 @@
|
||||
A: 执行命令或修改hosts引发,相关代码可在./utils.js,./appcenter.js下找到
|
||||
|
||||
4. Q: 原神进门后没有自动退出,程序输出停留在“加载完成”
|
||||
A: 关闭Shadowsocks全局代理
|
||||
A: 关闭代理后重试
|
||||
|
||||
24
app.js
24
app.js
@@ -1,34 +1,35 @@
|
||||
const proxy = require("udp-proxy")
|
||||
const cp = require("child_process")
|
||||
const rs = require("./regionServer")
|
||||
const cloud = require("./secret")
|
||||
const appcenter = require("./appcenter")
|
||||
const regionServer = require("./regionServer")
|
||||
const {
|
||||
initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkCDN, checkUpdate,
|
||||
initConfig, splitPacket, upload, decodeProto, log, setupHost, KPacket, debug, checkUpdate,
|
||||
brotliCompressSync, brotliDecompressSync, checkGameIsRunning, checkPortIsUsing
|
||||
} = require("./utils")
|
||||
const { exportData } = require("./export")
|
||||
const { enablePrivilege } = require("./native")
|
||||
const { enablePrivilege, pause } = require("./native")
|
||||
|
||||
const onExit = () => {
|
||||
setupHost(true)
|
||||
console.log("按任意键退出")
|
||||
cp.execSync("pause > nul", { stdio: "inherit" })
|
||||
pause()
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
process.once("SIGHUP", () => setupHost(true))
|
||||
process.on("SIGHUP", () => setupHost(true))
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
console.log("Unhandled Rejection at: ", promise, "\n0Reason:", reason)
|
||||
})
|
||||
process.once("uncaughtException", (err, origin) => {
|
||||
process.on("uncaughtException", (err, origin) => {
|
||||
appcenter.uploadError(err, true)
|
||||
console.log(err)
|
||||
console.log(`Origin: ${origin}`)
|
||||
process.exit(1)
|
||||
})
|
||||
process.once("exit", onExit)
|
||||
process.once("SIGINT", onExit)
|
||||
process.on("exit", onExit)
|
||||
process.on("SIGINT", onExit)
|
||||
try {
|
||||
enablePrivilege()
|
||||
} catch (e) {
|
||||
@@ -37,17 +38,14 @@ const onExit = () => {
|
||||
}
|
||||
appcenter.startup()
|
||||
let conf = await initConfig()
|
||||
cloud.init(conf)
|
||||
checkPortIsUsing()
|
||||
checkGameIsRunning()
|
||||
log("检查更新")
|
||||
await checkUpdate()
|
||||
checkCDN().then(_ => debug("CDN check success.")).catch(reason => {
|
||||
console.log(reason)
|
||||
process.exit(113)
|
||||
})
|
||||
let gameProcess
|
||||
let unexpectedExit = true
|
||||
rs.create(conf,() => {
|
||||
regionServer.create(conf,() => {
|
||||
setupHost()
|
||||
gameProcess = cp.execFile(conf.executable, { cwd: conf.path },err => {
|
||||
if (err !== null && !err.killed) {
|
||||
|
||||
27
appcenter.js
27
appcenter.js
@@ -30,15 +30,17 @@ const device = (() => {
|
||||
|
||||
const upload = () => {
|
||||
if (queue.length > 0) {
|
||||
const data = JSON.stringify({ "logs": queue })
|
||||
const logs = []
|
||||
for (let i = 0; i <= queue.length; i++) {
|
||||
logs.push(queue.pop())
|
||||
}
|
||||
const data = JSON.stringify({"logs": logs})
|
||||
axios.post("https://in.appcenter.ms/logs?api-version=1.0.0", data,{
|
||||
headers: {
|
||||
"App-Secret": key,
|
||||
"Install-ID": install
|
||||
}
|
||||
}).then(_ => {
|
||||
queue.length = 0
|
||||
}).catch(_ => {})
|
||||
}).catch(_ => {}).then()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +81,19 @@ const uploadError = (err, fatal) => {
|
||||
upload()
|
||||
}
|
||||
|
||||
const uploadEvent = (name, prop) => {
|
||||
const content = {
|
||||
type: "event",
|
||||
id: crypto.randomUUID(),
|
||||
sid: session,
|
||||
name: name,
|
||||
properties: prop,
|
||||
timestamp: getTimestamp(),
|
||||
device: device
|
||||
}
|
||||
queue.push(content)
|
||||
}
|
||||
|
||||
const startup = () => {
|
||||
queue.push({
|
||||
type: "startService",
|
||||
@@ -93,9 +108,9 @@ const startup = () => {
|
||||
device: device
|
||||
})
|
||||
upload()
|
||||
setInterval(() => upload(), 10000)
|
||||
setInterval(() => upload(), 5000)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startup, upload, uploadError
|
||||
startup, upload, uploadError, uploadEvent
|
||||
}
|
||||
|
||||
107
export.js
107
export.js
@@ -1,11 +1,13 @@
|
||||
const fs = require("fs")
|
||||
const axios = require("axios")
|
||||
const readline = require("readline")
|
||||
const { randomUUID } = require("crypto")
|
||||
const { loadCache, log } = require("./utils")
|
||||
const { copyToClipboard } = require("./native")
|
||||
const { openUrl, checkSnapFastcall, 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`
|
||||
@@ -15,13 +17,9 @@ const exportToSeelie = proto => {
|
||||
|
||||
const exportToPaimon = async proto => {
|
||||
const out = { achievement: {} }
|
||||
const achTable = new Map()
|
||||
const excel = await loadCache("ExcelBinOutput/AchievementExcelConfigData.json")
|
||||
excel.forEach(({GoalId, Id}) => {
|
||||
achTable.set(Id, GoalId === undefined ? 0 : GoalId)
|
||||
})
|
||||
proto.list.filter(achievement => achievement.status === 3).forEach(({id}) => {
|
||||
const gid = achTable.get(id)
|
||||
const data = await loadCache()
|
||||
proto.list.filter(a => a.status === 3 || a.status === 2).forEach(({id}) => {
|
||||
const gid = data["a"][id]
|
||||
if (out.achievement[gid] === undefined) {
|
||||
out.achievement[gid] = {}
|
||||
}
|
||||
@@ -32,48 +30,64 @@ const exportToPaimon = async proto => {
|
||||
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,
|
||||
timestamp: finishTimestamp
|
||||
})
|
||||
})
|
||||
if (checkSnapFastcall()) {
|
||||
const json = JSON.stringify(out)
|
||||
const path = `${process.env.TMP}/YaeAchievement-export-${randomUUID()}`
|
||||
fs.writeFileSync(path, json)
|
||||
openUrl(`snapgenshin://achievement/import/file?path=\"${path}\"`)
|
||||
log("在 SnapGenshin 进行下一步操作")
|
||||
} else {
|
||||
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)
|
||||
})
|
||||
})
|
||||
const ts = Date.now()
|
||||
const json = JSON.stringify(out,null,2)
|
||||
copyToClipboard(json)
|
||||
const fp = `./export-${ts}-cocogoat.json`
|
||||
fs.writeFileSync(fp, json)
|
||||
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) {
|
||||
@@ -91,15 +105,15 @@ const exportToCsv = async proto => {
|
||||
const bl = [84517]
|
||||
proto.list.forEach(({current, finishTimestamp, id, status, require}) => {
|
||||
if (!bl.includes(id)) {
|
||||
const desc = achievementMap.get(id) === undefined ? (() => {
|
||||
const curAch = data["a"][id] === undefined ? (() => {
|
||||
console.log(`Error get id ${id} in excel`)
|
||||
return {
|
||||
goal: "未知",
|
||||
name: "未知",
|
||||
desc: "未知"
|
||||
g: "未知",
|
||||
n: "未知",
|
||||
d: "未知"
|
||||
}
|
||||
})() : achievementMap.get(id)
|
||||
outputLines.push(`${id},${getStatusText(status)},${excel.goal[desc.goal]},${desc.name},${desc.desc},${status !== 1 ? current === 0 ? require : current : current},${require},${status === 1 ? "" : getTime(finishTimestamp)}`)
|
||||
})() : data["a"][id]
|
||||
outputLines.push(`${id},${getStatusText(status)},${data["g"][curAch.g]},${curAch.n},${curAch.d},${status !== 1 ? current === 0 ? require : current : current},${require},${status === 1 ? "" : getTime(finishTimestamp)}`)
|
||||
}
|
||||
})
|
||||
const fp = `./export-${Date.now()}.csv`
|
||||
@@ -115,16 +129,19 @@ const exportData = async proto => {
|
||||
const question = (query) => new Promise(resolve => {
|
||||
rl.question(query, resolve)
|
||||
})
|
||||
const chosen = await question("导出至: \n[0] 椰羊 (https://cocogoat.work/achievement)\n[1] Paimon.moe\n[2] Seelie.me\n[3] 表格文件 (默认)\n输入一个数字(0-3): ")
|
||||
const chosen = await question("导出至: \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) {
|
||||
switch (chosen.trim()) {
|
||||
case "0":
|
||||
await exportToCocogoat(proto)
|
||||
break
|
||||
case "1":
|
||||
await exportToPaimon(proto)
|
||||
await exportToSnapGenshin(proto)
|
||||
break
|
||||
case "2":
|
||||
await exportToPaimon(proto)
|
||||
break
|
||||
case "3":
|
||||
await exportToSeelie(proto)
|
||||
break
|
||||
case "raw":
|
||||
|
||||
3
native.d.ts
vendored
3
native.d.ts
vendored
@@ -2,6 +2,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 checkSnapFastcall(): boolean
|
||||
export function enablePrivilege(): any
|
||||
export function getDeviceInfo(): any
|
||||
export function getDeviceID(): string
|
||||
export function openUrl(url: string): number
|
||||
export function pause(): any
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "node-gyp rebuild && copy .\\build\\Release\\native.node ..\\genshin-export\\"
|
||||
"build": "node-gyp rebuild && copy .\\build\\Release\\native.node ..\\genshin-export\\",
|
||||
"build-for-win7": "node-gyp rebuild --target=v14.17.0 && copy .\\build\\Release\\native.node ..\\genshin-export\\"
|
||||
},
|
||||
"gypfile": true,
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#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")
|
||||
@@ -104,7 +105,7 @@ namespace native {
|
||||
int sw = GetDeviceCaps(desktop, DESKTOPHORZRES);
|
||||
int sh = GetDeviceCaps(desktop, DESKTOPVERTRES);
|
||||
ReleaseDC(nullptr, desktop);
|
||||
DWORD buildNum = RegUtils::GetInt(env, HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"UBR");
|
||||
DWORD buildNum = RegUtils::GetInt(env, HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"UBR", 0);
|
||||
wstring locale;
|
||||
if (RegUtils::GetString(HKEY_CURRENT_USER, L"Control Panel\\International", L"LocaleName", locale) != ERROR_SUCCESS) {
|
||||
locale = L"zh-CN";
|
||||
@@ -161,12 +162,36 @@ namespace native {
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
Value pause(const CallbackInfo &info) {
|
||||
while(!_kbhit()) {
|
||||
Sleep(10);
|
||||
}
|
||||
return info.Env().Undefined();
|
||||
}
|
||||
|
||||
Value openUrl(const CallbackInfo &info) {
|
||||
Env env = info.Env();
|
||||
wstring url = StringToWString(info[0].As<Napi::String>().Utf8Value());
|
||||
HINSTANCE retcode = ShellExecute(GetConsoleWindow(), L"open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
return Napi::Number::New(env, (INT_PTR)retcode); // NOLINT(cppcoreguidelines-narrowing-conversions)
|
||||
}
|
||||
|
||||
Value checkSnapFastcall(const CallbackInfo &info) {
|
||||
Env env = info.Env();
|
||||
wstring queryResult;
|
||||
RegUtils::GetString(HKEY_CLASSES_ROOT, L"snapgenshin", L"", queryResult);
|
||||
return Napi::Boolean::New(env, wcscmp(queryResult.c_str(), L"URL:snapgenshin") == 0);
|
||||
}
|
||||
|
||||
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("checkSnapFastcall", Function::New(env, checkSnapFastcall));
|
||||
exports.Set("checkGameIsRunning", Function::New(env, checkGameIsRunning));
|
||||
exports.Set("selectGameExecutable", Function::New(env, selectGameExecutable));
|
||||
return exports;
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
|
||||
namespace RegUtils {
|
||||
|
||||
DWORD GetInt(Env env, HKEY hKey, const wstring &path, const wstring &value) {
|
||||
DWORD GetInt(Env env, HKEY hKey, const wstring &path, const wstring &value, DWORD defaultValue) {
|
||||
DWORD data = 0;
|
||||
DWORD size = sizeof(DWORD);
|
||||
LSTATUS retcode = RegGetValue(hKey, path.c_str(), value.c_str(), RRF_RT_REG_DWORD, nullptr, &data, &size);
|
||||
if (retcode != ERROR_SUCCESS) {
|
||||
Error::New(env, "RegGetValue error: " + to_string(retcode)).ThrowAsJavaScriptException();
|
||||
return 0;
|
||||
return defaultValue;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
#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);
|
||||
string GBKToUTF8(const wstring& src) {
|
||||
int len = WideCharToMultiByte(CP_UTF8, 0, src.c_str(), -1, nullptr, 0, nullptr, nullptr);
|
||||
auto *buffer = new CHAR[len];
|
||||
WideCharToMultiByte(CP_UTF8, 0, src.c_str(), -1, buffer, len, nullptr, nullptr);
|
||||
string strTemp(buffer);
|
||||
delete[] buffer;
|
||||
return result;
|
||||
return strTemp;
|
||||
}
|
||||
|
||||
wstring StringToWString(const string &src) {
|
||||
int len = MultiByteToWideChar(CP_ACP, 0, src.c_str(), -1, nullptr, 0);
|
||||
auto *buffer = new WCHAR[len];
|
||||
MultiByteToWideChar(CP_ACP, 0, src.c_str(), -1, buffer, len);
|
||||
wstring strTemp(buffer);
|
||||
delete[] buffer;
|
||||
return strTemp;
|
||||
}
|
||||
|
||||
string WStringToString(const wstring &src) {
|
||||
string result;
|
||||
int len = WideCharToMultiByte(CP_ACP, 0, src.c_str(), src.size(), nullptr, 0, nullptr, nullptr); // NOLINT(cppcoreguidelines-narrowing-conversions)
|
||||
char *buffer = new char[len + 1];
|
||||
WideCharToMultiByte(CP_ACP, 0, src.c_str(), src.size(), buffer, len, nullptr, nullptr); // NOLINT(cppcoreguidelines-narrowing-conversions)
|
||||
buffer[len] = '\0';
|
||||
result.append(buffer);
|
||||
int len = WideCharToMultiByte(CP_ACP, 0, src.c_str(), -1, nullptr, 0, nullptr, nullptr);
|
||||
auto *buffer = new CHAR[len];
|
||||
WideCharToMultiByte(CP_ACP, 0, src.c_str(), -1, buffer, len, nullptr, nullptr);
|
||||
string strTemp(buffer);
|
||||
delete[] buffer;
|
||||
return result;
|
||||
return strTemp;
|
||||
}
|
||||
|
||||
void Log(Env env, const string &msg) {
|
||||
@@ -46,7 +51,11 @@ LSTATUS OpenFile(Env env, Napi::String &result, HWND parent) {
|
||||
open.lpstrFilter = L"国服/国际服主程序 (YuanShen/GenshinImpact.exe)\0YuanShen.exe;GenshinImpact.exe\0";
|
||||
open.lStructSize = sizeof(open);
|
||||
if(GetOpenFileName(&open)) {
|
||||
result = Napi::String::New(env, WStringToString(file));
|
||||
if (GetACP() == 936) {
|
||||
result = Napi::String::New(env, GBKToUTF8(file));
|
||||
} else {
|
||||
result = Napi::String::New(env, WStringToString(file));
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
} else {
|
||||
return ERROR_ERRORS_ENCOUNTERED;
|
||||
|
||||
13
package.json
13
package.json
@@ -3,18 +3,17 @@
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"pkg": "pkg -t node16-win-x64 -C Brotli app.js --build -o app.exe",
|
||||
"pkg-for-windows7": "pkg -t node14-win-x64 -C Brotli app.js --build -o app-win7.exe"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"ini": "^2.0.0",
|
||||
"axios": "^0.26.1",
|
||||
"udp-proxy": "^1.2.0",
|
||||
"protobufjs": "^6.11.2"
|
||||
"ini": "^2.0.0",
|
||||
"protobufjs": "^6.11.2",
|
||||
"udp-proxy": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pkg": "file:C:/Users/holog/Desktop/pkg-main"
|
||||
}
|
||||
}
|
||||
|
||||
2
package/app.manifest
Normal file
2
package/app.manifest
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"><security><requestedPrivileges><requestedExecutionLevel level="requireAdministrator" uiAccess="false"/></requestedPrivileges></security></trustInfo><application xmlns="urn:schemas-microsoft-com:asm.v3"><windowsSettings><dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware><longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware></windowsSettings></application><compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"><application><supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/><supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/><supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/><supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/></application></compatibility></assembly>
|
||||
BIN
package/icon.ico
Normal file
BIN
package/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 353 KiB |
57
package/package.js
Normal file
57
package/package.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const fs = require("fs")
|
||||
const pkg = require("pkg")
|
||||
const { log } = require("../utils")
|
||||
const { version } = require("../version")
|
||||
const { execSync } = require("child_process")
|
||||
|
||||
const name = `YaeAchievement-${ version.isDev ? "DevBuild" : "Release" }-${((t = new Date()) => {
|
||||
const p = i => i.toString().padStart(2, "0")
|
||||
return `${p(t.getMonth()+1)}${p(t.getDate())}${p(t.getHours())}${p(t.getMinutes())}`
|
||||
})()}`
|
||||
|
||||
const generateAndCompileVersionInfo = () => {
|
||||
const vci = version.name.split(".")
|
||||
let content = fs.readFileSync("./version.rc", "utf-8")
|
||||
content = content.replaceAll("${BID}" , vci[2] === undefined ? "0" : vci[2])
|
||||
content = content.replaceAll("${YEAR}" , (new Date()).getFullYear())
|
||||
content = content.replaceAll("${MAJOR_VERSION}", vci[0])
|
||||
content = content.replaceAll("${MINOR_VERSION}", vci[1])
|
||||
content = content.replaceAll("${ORIGINAL_NAME}", `${name}.exe`)
|
||||
fs.writeFileSync("./tmp.rc", content, "utf-8")
|
||||
execSync(`rh.exe -open tmp.rc -save tmp.res -action compile -log NUL`)
|
||||
fs.rm("./tmp.rc", () => {})
|
||||
}
|
||||
|
||||
const generateScript = (path) => {
|
||||
let content = fs.readFileSync("./rh.script", "utf-8")
|
||||
content = content.replaceAll("${PATH}", path)
|
||||
fs.writeFileSync("./tmp.script", content, "utf-8")
|
||||
}
|
||||
|
||||
const cleanUp = () => {
|
||||
fs.rm("./rh.ini", () => {})
|
||||
fs.rm("./tmp.res", () => {})
|
||||
fs.rm("./tmp.script", () => {})
|
||||
}
|
||||
|
||||
|
||||
const c = {
|
||||
finalName: `${name}-Win7`,
|
||||
nodeVersion: "14.19.1",
|
||||
sevenZipPath: "\"C:/Program Files/7-Zip/7z.exe\""
|
||||
};
|
||||
|
||||
(async () => {
|
||||
console.log(c)
|
||||
log("Generate and compile version info")
|
||||
generateAndCompileVersionInfo()
|
||||
log("Modify executable file resources")
|
||||
const path = `C:/Users/holog/.pkg-cache/v3.3/built-v${c.nodeVersion}-win-x64`
|
||||
generateScript(path)
|
||||
execSync(`rh.exe -script tmp.script`)
|
||||
cleanUp()
|
||||
log("Build and compress package")
|
||||
await pkg.exec(`../app.js -t node${c.nodeVersion.split(".")[0]}-win-x64 -C Brotli --build -o ${c.finalName}.exe`.split(" "))
|
||||
execSync(`${c.sevenZipPath} a ${c.finalName}.7z ${c.finalName}.exe -mx=9 -myx=9 -mmt=4 -sdel -stl`)
|
||||
log("Done")
|
||||
})()
|
||||
BIN
package/rh.exe
Normal file
BIN
package/rh.exe
Normal file
Binary file not shown.
10
package/rh.script
Normal file
10
package/rh.script
Normal file
@@ -0,0 +1,10 @@
|
||||
[FILENAMES]
|
||||
Exe=${PATH}
|
||||
SaveAs=${PATH}
|
||||
Log=NUL
|
||||
[COMMANDS]
|
||||
-delete MANIFEST,,
|
||||
-delete ICONGROUP,,
|
||||
-addoverwrite icon.ico, ICONGROUP,1,0
|
||||
-addoverwrite tmp.res, VERSIONINFO,1,0
|
||||
-addoverwrite app.manifest MANIFEST,1,0
|
||||
23
package/version.rc
Normal file
23
package/version.rc
Normal file
@@ -0,0 +1,23 @@
|
||||
1 VERSIONINFO
|
||||
FILEVERSION ${MAJOR_VERSION},${MINOR_VERSION},${BID},0
|
||||
PRODUCTVERSION ${MAJOR_VERSION},${MINOR_VERSION},${BID},0
|
||||
FILEOS 0x4
|
||||
FILETYPE 0x1
|
||||
{
|
||||
BLOCK "StringFileInfo"
|
||||
{
|
||||
BLOCK "000004B0"
|
||||
{
|
||||
VALUE "ProductName", "YaeAchievement"
|
||||
VALUE "ProductVersion", "${MAJOR_VERSION}.${MINOR_VERSION}.${BID}"
|
||||
VALUE "FileDescription", "YaeAchievement"
|
||||
VALUE "FileVersion", "${MAJOR_VERSION}.${MINOR_VERSION}.${BID}"
|
||||
VALUE "OriginalFilename", "${ORIGINAL_NAME}"
|
||||
VALUE "LegalCopyright", "Copyright (c) ${YEAR} HolographicHat"
|
||||
}
|
||||
}
|
||||
BLOCK "VarFileInfo"
|
||||
{
|
||||
VALUE "Translation", 0x0000 0x04B0
|
||||
}
|
||||
}
|
||||
132
utils.js
132
utils.js
@@ -10,8 +10,8 @@ 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 encodeProto = (object, name) => protobuf.load(messages).then(r => {
|
||||
const msgType = r.lookupType(name)
|
||||
@@ -37,13 +37,23 @@ const initConfig = async () => {
|
||||
const configFileName = "./config.json"
|
||||
if (fs.existsSync(configFileName)) {
|
||||
conf = JSON.parse(fs.readFileSync(configFileName, "utf-8"))
|
||||
if (conf.offlineResource !== undefined || conf.customCDN !== undefined || conf.proxy !== undefined) {
|
||||
conf.proxy = undefined
|
||||
conf.customCDN = undefined
|
||||
conf.offlineResource = undefined
|
||||
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
|
||||
}
|
||||
} else {
|
||||
conf = {
|
||||
path: [],
|
||||
offlineResource: false,
|
||||
customCDN: ""
|
||||
oversea_api: false
|
||||
}
|
||||
let p = ""
|
||||
try {
|
||||
p = path.dirname(native.selectGameExecutable())
|
||||
} catch (e) {
|
||||
process.exit(1)
|
||||
}
|
||||
const p = path.dirname(native.selectGameExecutable())
|
||||
await checkPath(p).catch(reason => {
|
||||
console.log(reason)
|
||||
process.exit(1)
|
||||
@@ -51,10 +61,9 @@ const initConfig = async () => {
|
||||
conf.path.push(p)
|
||||
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
|
||||
}
|
||||
if (conf.proxy !== undefined) {
|
||||
axios = axios.create({
|
||||
proxy: conf.proxy
|
||||
})
|
||||
if (conf.oversea_api === undefined) {
|
||||
conf.oversea_api = false
|
||||
fs.writeFileSync(configFileName, JSON.stringify(conf, null, 2))
|
||||
}
|
||||
if (conf.path.length === 1) {
|
||||
await checkPath(conf.path[0]).catch(reason => {
|
||||
@@ -93,6 +102,7 @@ const initConfig = async () => {
|
||||
conf.executable = conf.isOversea ? `${conf.path}/GenshinImpact.exe` : `${conf.path}/YuanShen.exe`
|
||||
conf.dispatchUrl = `dispatch${conf.isOversea ? "os" : "cn"}global.yuanshen.com`
|
||||
conf.dispatchIP = (await promisify(dns.lookup).bind(dns)(conf.dispatchUrl, 4)).address
|
||||
uploadEvent("AppInitialize", { ClientVersion: version.name, GameVersion: conf.version })
|
||||
return conf
|
||||
}
|
||||
|
||||
@@ -129,77 +139,51 @@ const md5 = str => {
|
||||
return h.digest("hex")
|
||||
}
|
||||
|
||||
let cdnUrlFormat = null
|
||||
/*
|
||||
* Structure:
|
||||
* UInt16 = 0x4321: magic num
|
||||
* UInt8 = 0x2 : version
|
||||
* Char[32] : data md5
|
||||
* ... : compressed data
|
||||
*/
|
||||
|
||||
String.prototype.format = function() {
|
||||
const args = arguments;
|
||||
return this.replace(/{(\d+)}/g, (match, number) => typeof args[number] != "undefined" ? args[number] : match)
|
||||
}
|
||||
|
||||
const checkCDN = async () => {
|
||||
try {
|
||||
cdnUrlFormat = "https://cdn.jsdelivr.net/gh/{0}@master/{1}"
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
} catch (e) {}
|
||||
try {
|
||||
cdnUrlFormat = "https://raw.githubusercontent.com/{0}/master/{1}"
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
} catch (e) {}
|
||||
try {
|
||||
const s = conf === undefined ? "" : conf.customCDN.trim()
|
||||
if (s.length > 0) {
|
||||
cdnUrlFormat = s
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
cdnUrlFormat = "https://raw.fastgit.org/{0}/master/{1}"
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
} catch (e) {}
|
||||
try {
|
||||
cdnUrlFormat = "https://ghproxy.net/https://raw.githubusercontent.com/{0}/master/{1}"
|
||||
await axios.head(cdnUrlFormat.format("github/fetch", ".gitignore"))
|
||||
return
|
||||
} catch (e) {}
|
||||
throw "网络错误,请检查配置文件/网络后重试 (11-3)"
|
||||
}
|
||||
|
||||
const loadCache = async (fp, repo = "Dimbreath/GenshinData") => {
|
||||
log(`预检资源: ${cdnUrlFormat.format(repo, fp)}`)
|
||||
const loadCache = async (fp = "latest-data") => {
|
||||
log(`正在加载资源: ${fp}`)
|
||||
const startAt = Date.now()
|
||||
fs.mkdirSync("./cache", { recursive: true })
|
||||
const localPath = `./cache/${md5(fp)}`
|
||||
if (conf.offlineResource) {
|
||||
const fd = brotliDecompressSync(fs.readFileSync(localPath))
|
||||
return JSON.parse(fd.subarray(1 + fd.readUInt8()).toString())
|
||||
// remove old cache file
|
||||
fs.readdir("./cache/", (err, files) => {
|
||||
if (err === null) files.forEach(s => {
|
||||
const fn = `./cache/${s}`
|
||||
if (!fn.endsWith(".ch")) fs.rm(fn,_ => {})
|
||||
})
|
||||
})
|
||||
const localPath = `./cache/${md5(fp)}.ch`
|
||||
const headers = {
|
||||
"x-content-hash": "0".repeat(32)
|
||||
}
|
||||
const header = {}
|
||||
let fd = Buffer.alloc(0)
|
||||
if (fs.existsSync(localPath)) {
|
||||
fd = brotliDecompressSync(fs.readFileSync(localPath))
|
||||
const etagLength = fd.readUInt8()
|
||||
header["If-None-Match"] = fd.subarray(1, 1 + etagLength).toString()
|
||||
fd = fs.readFileSync(localPath)
|
||||
if (fd.readUInt16BE(0) === 0x4321 && fd.readUInt8(2) === 2) {
|
||||
headers["x-content-hash"] = fd.subarray(3, 35).toString("utf-8")
|
||||
}
|
||||
}
|
||||
const headResponse = await axios.head(cdnUrlFormat.format(repo, fp), {
|
||||
headers: header,
|
||||
validateStatus: _ => true
|
||||
})
|
||||
if (headResponse.status === 304) {
|
||||
log("%s 命中缓存", fp)
|
||||
const etagLength = fd.readUInt8()
|
||||
return JSON.parse(fd.subarray(1 + etagLength).toString())
|
||||
const resp = await cloud.get(`/${fp}`, "application/json", headers, "arraybuffer")
|
||||
if (resp.status === 304) {
|
||||
log(`完成 (Cache, ${Date.now() - startAt}ms)`)
|
||||
return JSON.parse(brotliDecompressSync(fd.subarray(35)).toString())
|
||||
} else {
|
||||
log("下载所需资源, 请稍后...")
|
||||
const response = await axios.get(cdnUrlFormat.format(repo, fp))
|
||||
const etag = response.headers.etag
|
||||
const str = JSON.stringify(response.data)
|
||||
const comp = brotliCompressSync(Buffer.concat([Buffer.of(etag.length), Buffer.from(etag), Buffer.from(str)]))
|
||||
fs.writeFileSync(localPath, comp)
|
||||
log("下载完成")
|
||||
return response.data
|
||||
const compressedData = resp.data
|
||||
const decompressedData = brotliDecompressSync(compressedData)
|
||||
const buf = Buffer.allocUnsafe(compressedData.length + 35)
|
||||
buf.writeUInt16BE(0x4321, 0)
|
||||
buf.writeUInt8(0x2, 2)
|
||||
buf.fill(md5(decompressedData), 3)
|
||||
buf.fill(compressedData, 35)
|
||||
fs.writeFileSync(localPath, buf)
|
||||
log(`完成 (Network, ${Date.now() - startAt}ms)`)
|
||||
return JSON.parse(decompressedData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +225,7 @@ let hostsContent = undefined
|
||||
|
||||
const setupHost = (restore = false) => {
|
||||
if (restore && hostsContent === undefined) return
|
||||
const path = "C:\\Windows\\System32\\drivers\\etc\\hosts"
|
||||
const path = `${process.env.windir}\\System32\\drivers\\etc\\hosts`
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.writeFileSync(path, "")
|
||||
}
|
||||
@@ -306,5 +290,5 @@ class KPacket {
|
||||
|
||||
module.exports = {
|
||||
log, encodeProto, decodeProto, initConfig, splitPacket, upload, brotliCompressSync, brotliDecompressSync,
|
||||
setupHost, loadCache, debug, checkCDN, checkUpdate, KPacket, cdnUrlFormat, checkGameIsRunning, checkPortIsUsing
|
||||
setupHost, loadCache, debug, checkUpdate, KPacket, checkGameIsRunning, checkPortIsUsing
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const version = {
|
||||
code: 5,
|
||||
name: "1.4"
|
||||
code: 7,
|
||||
name: "1.6",
|
||||
isDev: false
|
||||
}
|
||||
|
||||
module.exports = { version }
|
||||
|
||||
Reference in New Issue
Block a user