Files
TeyvatGuide/src/plugins/Sqlite/modules/userAchi.ts
2026-01-15 22:33:36 +08:00

381 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 用户成就模块
* @since Beta v0.9.2
*/
import { UiafAchiStatEnum } from "@enum/uiaf.js";
import { path } from "@tauri-apps/api";
import { exists, mkdir, readDir, readTextFile, writeTextFile } from "@tauri-apps/plugin-fs";
import TGLogger from "@utils/TGLogger.js";
import { timestampToDate } from "@utils/toolFunc.js";
import TGSqlite from "../index.js";
import { AppAchievementsData, AppAchievementSeriesData } from "@/data/index.js";
/**
* 根据 completed 跟 progress 获取 status
* @since Beta v0.9.0
* @param completed - 是否完成
* @param progress - 进度
* @returns 完成状态
*/
function getUiafStatus(completed: boolean, progress: number): TGApp.Plugins.UIAF.AchiItemStatEnum {
if (!completed) return UiafAchiStatEnum.Unfinished;
if (progress === 0) return UiafAchiStatEnum.Finished;
if (progress !== 0) return UiafAchiStatEnum.RewardTaken;
return UiafAchiStatEnum.Invalid;
}
/**
* 获取最新成就版本
* @since Beta v0.6.0
* @returns 最新成就版本
*/
function getLatestAchiVersion(): string {
let maxVersion = "0";
for (const series of AppAchievementSeriesData) {
if (series.version > maxVersion) maxVersion = series.version;
}
return maxVersion;
}
/**
* 获取成就概况
* @since Beta v0.6.0
* @param uid - 存档UID
* @param series - 系列ID
* @remarks series 为可选参数
* - 当传入 series 时,统计该系列成就
* - 否则,统计全部成就
* @returns 成就概况
*/
async function getOverview(uid: number, series?: number): Promise<TGApp.App.Achievement.Overview> {
const db = await TGSqlite.getDB();
let totalAchi: Array<number> = [];
if (series === undefined) totalAchi = AppAchievementsData.map((i) => i.id);
else totalAchi = AppAchievementsData.filter((s) => s.series === series).map((i) => i.id);
const finAchi = (
await db.select<Array<TGApp.Sqlite.Achievement.TableRaw>>(
"SELECT * FROM Achievements WHERE uid = ? AND isCompleted = 1;",
[uid],
)
).filter((i) => totalAchi.includes(i.id));
return { total: totalAchi.length, fin: finAchi.length };
}
/**
* 合并成就数据
* @since Beta v0.6.0
* @param raw - 元数据
* @param uid - 存档 UID可选参数
* @param data - 数据库数据,可选参数
* @returns 渲染数据
*/
function getRenderAchi(
raw: TGApp.App.Achievement.Item,
uid?: number,
data?: TGApp.Sqlite.Achievement.TableRaw,
): TGApp.App.Achievement.RenderItem {
const emptyAchi: TGApp.Sqlite.Achievement.TableRaw = {
id: 0,
uid: uid ?? 0,
isCompleted: 0,
completedTime: "",
progress: 0,
updated: "",
};
const achiData = data ?? emptyAchi;
return {
id: raw.id,
uid: achiData.uid,
order: raw.order,
series: raw.series,
name: raw.name,
description: raw.description,
reward: raw.reward,
version: raw.version,
trigger: raw.trigger,
isCompleted: achiData.isCompleted === 1,
completedTime: achiData.completedTime,
progress: achiData.progress,
updated: achiData.updated,
};
}
/**
* 获取单个成就
* @since Beta v0.6.0
* @param uid - 存档 UID
* @param id - 成就 ID
* @returns 成就数据
*/
async function getAchi(
uid: number,
id: number,
): Promise<TGApp.Sqlite.Achievement.TableRaw | false> {
const db = await TGSqlite.getDB();
const res = await db.select<Array<TGApp.Sqlite.Achievement.TableRaw>>(
"SELECT * FROM Achievements WHERE uid = ? AND id = ?;",
[uid, id],
);
if (res.length === 0) return false;
return res[0];
}
/**
* 获取成就数据
* @since Beta v0.9.2
* @param uid - 存档 UID
* @param series - 成就系列ID
* @returns 成就数据
*/
async function getAchievements(
uid: number,
series?: number,
): Promise<Array<TGApp.App.Achievement.RenderItem>> {
const db = await TGSqlite.getDB();
const res: Array<TGApp.App.Achievement.RenderItem> = [];
const userData = await db.select<Array<TGApp.Sqlite.Achievement.TableRaw>>(
"SELECT * FROM Achievements WHERE uid = ?;",
[uid],
);
let rawData: Array<TGApp.App.Achievement.Item>;
if (series === undefined || series === -1) rawData = AppAchievementsData;
else rawData = AppAchievementsData.filter((a) => a.series === series);
for (const achi of rawData) {
const achiFind = userData.find((u) => u.id === achi.id);
const achievement = getRenderAchi(achi, uid, achiFind);
res.push(achievement);
}
res.sort(
(a, b) =>
Number(a.isCompleted) - Number(b.isCompleted) ||
b.version.localeCompare(a.version) ||
b.series - a.series ||
b.order - a.order,
);
return res;
}
/**
* 查找成就数据
* @since Beta v0.8.7
* @remarks 支持三种搜索方式:
* - 版本搜索:输入 vx.x 格式的关键词(如 v1.2),搜索对应版本的成就
* - ID搜索输入 ixxx 格式的关键词(如 i1001搜索对应ID的成就
* - 名称/描述搜索:输入任意关键词,搜索成就名称或描述中包含该关键词的成就
* @param uid - 存档 UID
* @param keyword - 关键词
* @returns 成就数据
*/
async function searchAchi(
uid: number,
keyword: string,
): Promise<Array<TGApp.App.Achievement.RenderItem>> {
if (keyword === "") return await getAchievements(uid);
const versionReg = /^v\d+(\.\d+)?$/;
const idReg = /^i\d+$/;
let rawData: Array<TGApp.App.Achievement.Item>;
const res: Array<TGApp.App.Achievement.RenderItem> = [];
if (versionReg.test(keyword)) {
const version = keyword.replace("v", "");
rawData = AppAchievementsData.filter((i) => i.version.includes(version));
} else if (idReg.test(keyword)) {
const id = parseInt(keyword.replace("i", ""));
rawData = AppAchievementsData.filter((a) => a.id === id);
} else {
rawData = AppAchievementsData.filter((a) => {
if (a.name.includes(keyword)) return true;
if (a.description.includes(keyword)) return true;
});
}
for (const data of rawData) {
const achiFind = await getAchi(uid, data.id);
let achievement: TGApp.App.Achievement.RenderItem;
if (achiFind === false) achievement = getRenderAchi(data, uid);
else achievement = getRenderAchi(data, uid, achiFind);
res.push(achievement);
}
res.sort(
(a, b) => a.isCompleted.toString().localeCompare(b.isCompleted.toString()) || a.order - b.order,
);
return res;
}
/**
* 更新成就数据
* @since Beta v0.6.0
* @param data - 成就数据
* @returns 无返回值
*/
async function updateAchi(data: TGApp.App.Achievement.RenderItem): Promise<void> {
const db = await TGSqlite.getDB();
await db.execute(
"INSERT INTO Achievements(id, uid, isCompleted, completedTime, progress, updated) \
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(id,uid) DO UPDATE \
SET isCompleted=?,completedTime=?,progress=?,updated=?;",
[
data.id,
data.uid,
data.isCompleted ? 1 : 0,
data.completedTime,
data.progress,
timestampToDate(new Date().getTime()),
data.isCompleted ? 1 : 0,
data.completedTime,
data.progress,
timestampToDate(new Date().getTime()),
],
);
}
/**
* 将数据库数据转换为UIAF数据
* @since Beta v0.6.0
* @param data - 数据库数据
* @returns UIAF数据
*/
function transDb2Uiaf(data: TGApp.Sqlite.Achievement.TableRaw): TGApp.Plugins.UIAF.Achievement {
let timestamp = 0;
if (data.isCompleted === 1) timestamp = Math.floor(new Date(data.completedTime).getTime() / 1000);
const status = getUiafStatus(data.isCompleted === 1, data.progress);
return { id: data.id, timestamp: timestamp, current: data.progress, status };
}
/**
* 获取指定Uid的UIAF数据
* @since Beta v0.6.0
* @param uid - 存档UID
* @returns UIAF数据
*/
async function getUiafData(uid: number): Promise<Array<TGApp.Plugins.UIAF.Achievement>> {
const db = await TGSqlite.getDB();
const data = await db.select<Array<TGApp.Sqlite.Achievement.TableRaw>>(
"SELECT * FROM Achievements WHERE uid=?;",
[uid],
);
const res: Array<TGApp.Plugins.UIAF.Achievement> = [];
for (const item of data) res.push(transDb2Uiaf(item));
return res;
}
/**
* 获取所有存档 uid
* @since Beta v0.6.0
* @returns 存档UID列表
*/
async function getAllUid(): Promise<Array<number>> {
const db = await TGSqlite.getDB();
type resType = Array<{ uid: number }>;
const res = await db.select<resType>("SELECT DISTINCT uid FROM Achievements;");
return res.map((i) => i.uid);
}
/**
* 备份成就数据
* @since Beta v0.6.0
* @param dir - 存档数据
* @param uid - 存档UID未指定则导出所有
* @returns 无返回值
*/
async function backupUiaf(dir: string, uid?: number): Promise<void> {
let uidList = [];
if (uid === undefined) uidList = await getAllUid();
else uidList.push(uid);
if (!(await exists(dir))) {
await TGLogger.Warn("不存在指定的成就备份目录,即将创建");
await mkdir(dir, { recursive: true });
}
for (const uidItem of uidList) {
const data = await getUiafData(uidItem);
const fileName = `UIAF_${uidItem}`;
await writeTextFile(`${dir}${path.sep()}${fileName}.json`, JSON.stringify(data, null, 2));
await TGLogger.Info(`成功备份${uidItem}的成就存档`);
}
}
/**
* 恢复成就数据
* @since Beta v0.6.0
* @param dir - 数据目录
* @returns 是否恢复成功
*/
async function restoreUiaf(dir: string): Promise<boolean> {
if (!(await exists(dir))) return false;
const filesRead = await readDir(dir);
// 校验 UIAF_xxx.json 文件
const fileRegex = /^UIAF_\d+\.json$/;
const files = filesRead.filter((item) => item.isFile && fileRegex.test(item.name));
if (files.length === 0) return false;
for (const file of files) {
try {
const uid = parseInt(file.name.replace("UIAF_", "").replace(".json", ""));
const filePath = `${dir}${path.sep()}${file.name}`;
const data: Array<TGApp.Plugins.UIAF.Achievement> = JSON.parse(await readTextFile(filePath));
await TSUserAchi.mergeUiaf(data, uid);
} catch (e) {
await TGLogger.Error(`[UIAF][RESTORE] 恢复成就数据${file.name} `);
await TGLogger.Error(`${e}`);
return false;
}
}
return true;
}
/**
* 导入Uiaf数据
* @since Beta v0.7.8
* @param data - 成就数据
* @param uid - 存档UID
* @returns 无返回值
*/
async function mergeUiaf(data: Array<TGApp.Plugins.UIAF.Achievement>, uid: number): Promise<void> {
const db = await TGSqlite.getDB();
for (const achi of data) {
const status =
achi.status === UiafAchiStatEnum.Finished || achi.status === UiafAchiStatEnum.RewardTaken
? 1
: 0;
const timeC = status === 1 ? timestampToDate(achi.timestamp * 1000) : "";
const timeN = timestampToDate(new Date().getTime());
await db.execute(
"INSERT INTO Achievements(id, uid, isCompleted, completedTime, progress, updated) \
VALUES (?,?,?,?,?,?) ON CONFLICT(id,uid) DO UPDATE SET\
isCompleted=?,completedTime=?,progress=?,updated=?;",
[achi.id, uid, status, timeC, achi.current, timeN, status, timeC, achi.current, timeN],
);
}
}
/**
* 删除指定 UID 存档的数据
* @since Beta v0.6.0
* @param uid - 存档UID
* @returns 无返回值
*/
async function delUid(uid: number): Promise<void> {
const db = await TGSqlite.getDB();
await db.execute("DELETE FROM Achievements WHERE uid=?;", [uid]);
}
/**
* 用户成就数据库操作类
* @since Beta v0.9.0
*/
const TSUserAchi = {
getLatestAchiVersion,
getOverview,
getAchievements,
getAllUid,
getUiafData,
searchAchi,
updateAchi,
mergeUiaf,
backupUiaf,
restoreUiaf,
delUid,
};
export default TSUserAchi;