diff --git a/bst@asphyxia/README.md b/bst@asphyxia/README.md new file mode 100644 index 0000000..382da31 --- /dev/null +++ b/bst@asphyxia/README.md @@ -0,0 +1,8 @@ +# BeatStream + +Plugin Version: **v0.1.0-beta** + +Supported Versions: + +- BeatStream アニムトライヴ + - Back end ✔ \ No newline at end of file diff --git a/bst@asphyxia/handlers/bst2/common.ts b/bst@asphyxia/handlers/bst2/common.ts new file mode 100644 index 0000000..710563f --- /dev/null +++ b/bst@asphyxia/handlers/bst2/common.ts @@ -0,0 +1,246 @@ +import { Bst2EventParamsMap, getKEventControl } from "../../models/bst2/event_params" +import { Bst2AccountMap, Bst2BiscoMap, Bst2CourseMap, Bst2MusicRecordMap, Bst2PlayerMap, Bst2SurveyMap, Bst2TipsMap, Bst2UnlockingInfoMap, IBst2Account, IBst2Base, IBst2Bisco, IBst2Course, IBst2CrysisLog, IBst2Customization, IBst2Hacker, IBst2MusicRecord, IBst2Player, IBst2Survey, IBst2Tips, IBst2UnlockingInfo } from "../../models/bst2/profile" +import { Bst2CourseLogMap, Bst2StageLogMap, IBst2StageLog } from "../../models/bst2/stagelog" +import { bacK, BigIntProxy, boolme, fromMap, mapK, s16me, s32me, s8me, strme, toBigInt } from "../../utility/mapping" +import { isToday } from "../../utility/utility_functions" +import { DBM } from "../utility/db_manager" +import { readPlayerPostProcess, writePlayerPreProcess } from "./processing" + +export namespace Bst2HandlersCommon { + export const Common: EPR = async (_0, _1, send) => await send.object({ event_ctrl: { data: getKEventControl() } }) + + export const BootPcb: EPR = async (_0, _1, send) => await send.object({ sinfo: { nm: K.ITEM("str", "Asphyxia"), cl_enbl: K.ITEM("bool", 1), cl_h: K.ITEM("u8", 0), cl_m: K.ITEM("u8", 0) } }) + + export const StartPlayer: EPR = async (_, data, send) => { + let params = fromMap(Bst2EventParamsMap) + let rid = $(data).str("rid") + let account = DB.FindOne(rid, { collection: "bst.bst2.player.account" }) + if (account == null) params.playerId = -1 + params.startTime = BigInt(Date.now()) + send.object(mapK(params, Bst2EventParamsMap)) + } + + export const PlayerSucceeded: EPR = async (_, data, send) => { + let rid = $(data).str("rid") + let account: IBst2Account = await DB.FindOne(rid, { collection: "bst.bst2.player.account" }) + let result + if (account == null) { + result = { + play: false, + data: { name: "" }, + record: {}, + hacker: {}, + phantom: {} + } + } else { + let base: IBst2Base = await DB.FindOne(rid, { collection: "bst.bst2.player.base" }) + let records: IBst2MusicRecord[] = await DB.Find({ collection: "bst.bst2.playData.musicRecord#userId", userId: account.userId }) + result = { + play: true, + data: { name: base.name }, + record: {}, + hacker: {}, + phantom: {} + } + } + send.object(mapK(result, { + play: boolme(), + data: { name: strme() }, + record: {}, + hacker: {}, + phantom: {} + })) + } + + export const ReadPlayer: EPR = async (_, data, send) => { + let refid = $(data).str("rid") + let account = await DB.FindOne(refid, { collection: "bst.bst2.player.account" }) + if (account == null) return await send.deny() + + let base = await DB.FindOne(refid, { collection: "bst.bst2.player.base" }) + let survey = await DB.FindOne(refid, { collection: "bst.bst2.player.survey" }) || fromMap(Bst2SurveyMap) + let unlocking = await DB.Find(refid, { collection: "bst.bst2.player.unlockingInfo" }) + let customize = await DB.FindOne(refid, { collection: "bst.bst2.player.customization" }) + let tips = await DB.FindOne(refid, { collection: "bst.bst2.player.tips" }) || fromMap(Bst2TipsMap) + let hacker = await DB.Find(refid, { collection: "bst.bst2.player.hacker" }) + let crysis = await DB.Find(refid, { collection: "bst.bst2.player.event.crysis" }) + let bisco = await DB.FindOne(refid, { collection: "bst.bst2.player.bisco" }) || fromMap(Bst2BiscoMap) + let records = await DB.Find({ collection: "bst.bst2.playData.musicRecord#userId", userId: account.userId }) + let courses = await DB.Find({ collection: "bst.bst2.playData.course#userId", userId: account.userId }) + + account.previousStartTime = account.standardTime + account.standardTime = BigInt(Date.now()) + account.ea = true + account.intrvld = 0 + account.playCount++ + account.playCountToday++ + let eventPlayLog: { crysis?: IBst2CrysisLog[] } = {} + if (crysis.length != 0) eventPlayLog.crysis = crysis + + let player: IBst2Player = { + pdata: { + account: account, + base: base, + survey: survey, + opened: {}, + item: (unlocking.length == 0) ? {} : { info: unlocking }, + customize: customize, + tips: tips, + hacker: (hacker.length == 0) ? {} : { info: hacker }, + playLog: eventPlayLog, + bisco: { pinfo: bisco }, + record: (records.length == 0) ? {} : { rec: records }, + course: (courses.length == 0) ? {} : { record: courses } + } + } + send.object(readPlayerPostProcess(mapK(player, Bst2PlayerMap))) + } + + export const WritePlayer: EPR = async (_, data, send) => { + let player = bacK(writePlayerPreProcess(data), Bst2PlayerMap).data + let refid = player.pdata.account.refid + let userId = player.pdata.account.userId + let now = BigIntProxy(BigInt(Date.now())) + + let opm = new DBM.DBOperationManager() + + let oldAccount = await DB.FindOne(refid, { collection: "bst.bst2.player.account" }) + if (!oldAccount) { + do { + userId = Math.round(Math.random() * 99999999) + } while ((await DB.Find(null, { collection: "bst.bst2.player.account", userId: userId })).length > 0) + oldAccount = fromMap(Bst2AccountMap) + oldAccount.userId = userId + } + oldAccount.playCount++ + if (!isToday(toBigInt(oldAccount.standardTime))) { + oldAccount.dayCount++ + oldAccount.playCountToday = 1 + } else oldAccount.playCountToday++ + oldAccount.standardTime = BigIntProxy(BigInt(Date.now())) + opm.upsert(refid, { collection: "bst.bst2.player.account" }, oldAccount) + if (player.pdata.base) opm.upsert(refid, { collection: "bst.bst2.player.base" }, player.pdata.base) + if (player.pdata.item?.info?.length > 0) for (let u of player.pdata.item.info) opm.upsert(refid, { collection: "bst.bst2.player.unlockingInfo", type: u.type, id: u.id }, u) + if (player.pdata.customize) opm.upsert(refid, { collection: "bst.bst2.player.customization" }, player.pdata.customize) + if (player.pdata.tips) opm.upsert(refid, { collection: "bst.bst2.player.base" }, player.pdata.base) + if (player.pdata.hacker?.info?.length > 0) for (let h of player.pdata.hacker.info) { + h.updateTime = now + opm.upsert(refid, { collection: "bst.bst2.player.hacker", id: h.id }, h) + } + if (player.pdata.playLog?.crysis?.length > 0) for (let c of player.pdata.playLog.crysis) opm.upsert(refid, { collection: "bst.bst2.player.event.crysis", id: c.id, stageId: c.stageId }, c) + + await DBM.operate(opm) + send.object({ uid: K.ITEM("s32", 0) }) + } + + export const WriteStageLog: EPR = async (_, data, send) => { + await updateRecordFromStageLog(bacK(data, Bst2StageLogMap).data, false) + send.success() + } + + export const WriteCourseStageLog: EPR = async (_, data, send) => { + await updateRecordFromStageLog(bacK(data, Bst2StageLogMap).data, true) + send.success() + } + + async function updateRecordFromStageLog(stageLog: IBst2StageLog, isCourseStage: boolean) { + let query: Query = { collection: "bst.bst2.playData.musicRecord#userId", userId: stageLog.userId, musicId: stageLog.musicId, chart: stageLog.chart } + let oldRecord = await DB.FindOne(query) + + let time = Date.now() / 1000 + stageLog.time = time + stageLog.isCourseStage = isCourseStage + + if (oldRecord == null) { + oldRecord = fromMap(Bst2MusicRecordMap) + oldRecord.musicId = stageLog.musicId + oldRecord.chart = stageLog.chart + oldRecord.clearCount = (stageLog.medal >= 3) ? 1 : 0 + oldRecord.score = stageLog.score + oldRecord.grade = stageLog.grade + oldRecord.gaugeTimes10 = stageLog.gaugeTimes10 + oldRecord.playCount = 1 + oldRecord.medal = stageLog.medal + oldRecord.combo = stageLog.combo + oldRecord.lastPlayTime = time + oldRecord.updateTime = time + oldRecord.userId = stageLog.userId + } else { + if (stageLog.medal >= 3) oldRecord.clearCount++ + if (oldRecord.score < stageLog.score) { + oldRecord.updateTime = time + oldRecord.score = stageLog.score + } + if (oldRecord.grade < stageLog.grade) { + oldRecord.updateTime = time + oldRecord.grade = stageLog.grade + } + if (oldRecord.gaugeTimes10 < stageLog.gaugeTimes10) { + oldRecord.updateTime = time + oldRecord.gaugeTimes10 = stageLog.gaugeTimes10 + } + if (oldRecord.medal < stageLog.medal) { + oldRecord.updateTime = time + oldRecord.medal = stageLog.medal + } + if (oldRecord.combo < stageLog.combo) { + oldRecord.updateTime = time + oldRecord.combo = stageLog.combo + } + oldRecord.lastPlayTime = time + oldRecord.playCount++ + } + DBM.upsert(null, query, oldRecord) + DBM.insert(null, stageLog) + } + + export const WriteCourseLog: EPR = async (_, data, send) => { + let courseLog = bacK(data, Bst2CourseLogMap).data + let query: Query = { collection: "bst.bst2.playData.course#userId", userId: courseLog.userId, courseId: courseLog.courseId } + let oldRecord = await DB.FindOne(query) + + let time = Date.now() / 1000 + courseLog.time = time + + if (oldRecord == null) { + oldRecord = fromMap(Bst2CourseMap) + oldRecord.courseId = courseLog.courseId + oldRecord.score = courseLog.score + oldRecord.grade = courseLog.grade + oldRecord.gauge = courseLog.gauge + oldRecord.playCount = 1 + oldRecord.medal = courseLog.medal + oldRecord.combo = courseLog.combo + oldRecord.lastPlayTime = time + oldRecord.updateTime = time + oldRecord.userId = courseLog.userId + } else { + if (oldRecord.score < courseLog.score) { + oldRecord.updateTime = time + oldRecord.score = courseLog.score + } + if (oldRecord.grade < courseLog.grade) { + oldRecord.updateTime = time + oldRecord.grade = courseLog.grade + } + if (oldRecord.gauge < courseLog.gauge) { + oldRecord.updateTime = time + oldRecord.gauge = courseLog.gauge + } + if (oldRecord.medal < courseLog.medal) { + oldRecord.updateTime = time + oldRecord.medal = courseLog.medal + } + if (oldRecord.combo < courseLog.combo) { + oldRecord.updateTime = time + oldRecord.combo = courseLog.combo + } + oldRecord.lastPlayTime = time + oldRecord.playCount++ + } + DBM.upsert(null, query, oldRecord) + DBM.insert(null, courseLog) + + send.success() + } +} \ No newline at end of file diff --git a/bst@asphyxia/handlers/bst2/processing.ts b/bst@asphyxia/handlers/bst2/processing.ts new file mode 100644 index 0000000..3e08cb0 --- /dev/null +++ b/bst@asphyxia/handlers/bst2/processing.ts @@ -0,0 +1,12 @@ +import { IBst2Player } from "../../models/bst2/profile" +import { KITEM2 } from "../../utility/mapping" +import { toFullWidth, toHalfWidth } from "../../utility/utility_functions" + +export function readPlayerPostProcess(player: KITEM2): KITEM2 { + if (player.pdata.base?.name != null) player.pdata.base.name["@content"] = toFullWidth(player.pdata.base.name["@content"]) + return player +} +export function writePlayerPreProcess(player: KITEM2): KITEM2 { + if (player.pdata.base?.name != null) player.pdata.base.name["@content"] = toHalfWidth(player.pdata.base.name["@content"]) + return player +} \ No newline at end of file diff --git a/bst@asphyxia/handlers/utility/batch.ts b/bst@asphyxia/handlers/utility/batch.ts new file mode 100644 index 0000000..87596d9 --- /dev/null +++ b/bst@asphyxia/handlers/utility/batch.ts @@ -0,0 +1,21 @@ +import { IBatchResult } from "../../models/utility/batch" +import { IPluginVersion } from "../../models/utility/plugin_version" +import { isHigherVersion } from "../../utility/utility_functions" +import { DBM } from "./db_manager" + +export namespace Batch { + let registeredBatch = <{ id: string, version: string, batch: () => Promise }[]>[] + + export async function execute(version: string): Promise { + for (let b of registeredBatch) { + if ((await DB.Find({ collection: "bst.batchResult", batchId: b.id })).length == 0) if (!isHigherVersion(version, b.version)) { + await b.batch() + await DBM.insert(null, { collection: "bst.batchResult", batchId: b.id }) + } + } + } + export function register(id: string, version: string, batch: () => Promise) { + registeredBatch.push({ id: id, version: version, batch: batch }) + } + +} \ No newline at end of file diff --git a/bst@asphyxia/handlers/utility/batch_initialize.ts b/bst@asphyxia/handlers/utility/batch_initialize.ts new file mode 100644 index 0000000..fd9c86e --- /dev/null +++ b/bst@asphyxia/handlers/utility/batch_initialize.ts @@ -0,0 +1,7 @@ +import { Batch } from "./batch" +import { DBM } from "./db_manager" +import { bufferToBase64, log } from "../../utility/utility_functions" + +export function initializeBatch() { + /* Register batch here **/ +} diff --git a/bst@asphyxia/handlers/utility/common.ts b/bst@asphyxia/handlers/utility/common.ts new file mode 100644 index 0000000..4d819b1 --- /dev/null +++ b/bst@asphyxia/handlers/utility/common.ts @@ -0,0 +1,17 @@ +export namespace UtilityHandlersCommon { + export const WriteShopInfo: EPR = async (__, ___, send) => { + let result = { + sinfo: { + lid: K.ITEM("str", "ea"), + nm: K.ITEM("str", "Asphyxia shop"), + cntry: K.ITEM("str", "Japan"), + rgn: K.ITEM("str", "1"), + prf: K.ITEM("s16", 13), + cl_enbl: K.ITEM("bool", 0), + cl_h: K.ITEM("u8", 8), + cl_m: K.ITEM("u8", 0) + } + } + send.object(result) + } +} \ No newline at end of file diff --git a/bst@asphyxia/handlers/utility/db_manager.ts b/bst@asphyxia/handlers/utility/db_manager.ts new file mode 100644 index 0000000..35a26b6 --- /dev/null +++ b/bst@asphyxia/handlers/utility/db_manager.ts @@ -0,0 +1,211 @@ +import { ICollection } from "../../models/utility/definitions" +import { log } from "../../utility/utility_functions" + +export namespace DBM { + export interface IDBCollectionName extends ICollection<"dbManager.collectionName"> { + name: string + } + export interface IDBOperation { + refid?: string + query: TOperation extends "insert" ? null : Query + operation: TOperation + doc: TOperation extends "remove" ? null : T | Doc + isPublicDoc?: boolean + } + export class DBOperationManager { + public operations: IDBOperation[] = [] + + public push(...op: IDBOperation[]): void { + this.operations.push(...op) + } + public update>(refid: string | null, query: Query, data: Doc, isPublicDoc: boolean = true): void { + for (let o of this.operations) if (o.doc && DBOperationManager.isMatch(o.doc, query)) o.operation = "skip" + this.operations.push({ refid: refid, query: query, operation: "update", doc: data, isPublicDoc: isPublicDoc }) + } + public upsert>(refid: string | null, query: Query, data: Doc, isPublicDoc: boolean = true): void { + for (let o of this.operations) if (o.doc && DBOperationManager.isMatch(o.doc, query)) o.operation = "skip" + this.operations.push({ refid: refid, query: query, operation: "upsert", doc: data, isPublicDoc: isPublicDoc }) + } + public insert>(refid: string | null, data: Doc, isPublicDoc: boolean = true): void { + this.operations.push({ refid: refid, operation: "insert", query: null, doc: data, isPublicDoc: isPublicDoc }) + } + public remove>(refid: string | null, query: Query, isPublicDoc: boolean = true): void { + for (let o of this.operations) if (o.doc && DBOperationManager.isMatch(o.doc, query)) o.operation = "skip" + this.operations.push({ refid: refid, query: query, operation: "remove", doc: null, isPublicDoc: isPublicDoc }) + } + public async findOne>(refid: string | null, query: Query, isPublicDoc: boolean = true): Promise> { + for (let i = this.operations.length - 1; i >= 0; i--) { + let o = this.operations[i] + if (o.doc == null) continue + if (DBOperationManager.isMatch(o.doc, query) && ((o.refid && refid) ? (o.refid == refid) : true)) return o.doc + } + return ((refid == null) && isPublicDoc) ? await DB.FindOne(query) : await DB.FindOne(refid, query) + } + public async find>(refid: string | null, query: Query, isPublicDoc: boolean = true): Promise<(T | Doc)[]> { + let result: (T | Doc)[] = [] + for (let o of this.operations) { + if (o.doc == null) continue + if (DBOperationManager.isMatch(o.doc, query) && ((o.refid && refid) ? (o.refid == refid) : true)) result.push(o.doc) + } + return result.concat(await (((refid == null) && isPublicDoc) ? DB.Find(query) : DB.Find(refid, query))) + } + private static isMatch(entry: T | Doc, query: Query): boolean { + if (entry == null) return query == null + if (query.$where && !query.$where.apply(entry)) return false + let $orResult = null + let skipKeys = ["$where", "_id"] + for (let qk in query) { + if (skipKeys.includes(qk)) continue + switch (qk) { + case "$or": { + if ($orResult == null) $orResult = false + for (let or of query.$or) if (this.isMatch(entry, or)) $orResult = true + break + } + case "$and": { + for (let and of query.$and) if (!this.isMatch(entry, and)) return false + break + } + case "$not": { + if (this.isMatch(entry, query.$not)) return false + break + } + default: { + let value = entry[qk] + let q = query[qk] + if (value == q) continue + if ((typeof q != "object") && (typeof q != "function")) return false + if ((q.$exists != null)) if ((q.$exists && (value == null)) || (!q.$exists && (value != null))) return false + if (Array.isArray(value)) { + if (q.$elemMatch && !this.isMatch(value, q.$elemMatch)) return false + if (q.$size && (value.length != q.$size)) return false + continue + } else if ((typeof value == "number") || (typeof value == "string")) { + if (q.$lt) if (value >= q.$lt) return false + if (q.$lte) if (value > q.$lte) return false + if (q.$gt) if (value <= q.$gt) return false + if (q.$gte) if (value < q.$gte) return false + if (q.$in) if (!value.toString().includes(q.$in)) return false + if (q.$nin) if (value.toString().includes(q.$nin)) return false + if (q.$ne) if (value == q.$ne) return false + if (q.$regex) if (value.toString().match(q.$regex).length == 0) return false + continue + } else if (typeof value == "object") { + if (!this.isMatch(value, q)) return false + continue + } else if (q != null) return false + } + } + } + return ($orResult == null) || $orResult + } + } + export async function getCollectionNames(filter?: string): Promise { + let result = await DB.Find({ collection: "dbManager.collectionName" }) + if (filter != null) { + let filters = filter.split(",") + for (let i = 0; i < filter.length; i++) filters[i] = filters[i].trim() + let i = 0 + while (i < result.length) { + let removeFlag = false + for (let f of filters) if (f.startsWith("!") ? !result[i].name.includes(f) : result[i].name.includes(f)) { + result.splice(i, 1) + removeFlag = true + break + } + if (!removeFlag) i++ + } + } + + return result + } + + async function checkData>(data: T): Promise { + if (await DB.FindOne({ collection: "dbManager.collectionName", name: data.collection }) == null) { + await DB.Insert({ collection: "dbManager.collectionName", name: data.collection }) + } + } + export async function update>(refid: string | null, query: Query, data: Doc, isPublicDoc: boolean = true) { + checkData(data) + if (refid == null) return isPublicDoc ? await DB.Update(query, data) : await DB.Update(null, query, data) + else return await DB.Update(refid, query, data) + } + export async function upsert>(refid: string | null, query: Query, data: Doc, isPublicDoc: boolean = true) { + checkData(data) + if (refid == null) return isPublicDoc ? await DB.Upsert(query, data) : await DB.Upsert(null, query, data) + else return await DB.Upsert(refid, query, data) + } + export async function insert>(refid: string | null, data: Doc, isPublicDoc: boolean = true) { + checkData(data) + if (refid == null) return isPublicDoc ? await DB.Insert(data) : await DB.Insert(null, data) + else return await DB.Insert(refid, data) + } + export async function remove>(refid: string | null, query: Query, isPublicDoc: boolean = true) { + if (refid == null) return isPublicDoc ? await DB.Remove(query) : await DB.Remove(null, query) + else return await DB.Remove(refid, query) + } + + export async function operate(operations: DBOperationManager) { + let result = [] + for (let o of operations.operations) { + if (o.operation == "skip") continue + if (o.doc) delete o.doc._id + try { + switch (o.operation) { + case "insert": + result.push(await insert(o.refid, o.doc, o.isPublicDoc)) + break + case "update": + result.push(await update(o.refid, o.query, o.doc, o.isPublicDoc)) + break + case "upsert": + result.push(await upsert(o.refid, o.query, o.doc, o.isPublicDoc)) + break + case "remove": + result.push(await remove(o.refid, o.query, o.isPublicDoc)) + break + } + } catch (e) { + await log(Date.now().toLocaleString() + " Error: " + (e as Error).message) + } + } + return result + } + + export async function removeAllData(refid?: string, filter?: string) { + for (let c of await getCollectionNames(filter)) remove(refid, { collection: c.name }) + + if ((refid == null) && (filter == null)) remove(null, { collection: "dbManager.collectionName" }) + } + export async function overall(refid: string, userId: number, filter: string, operation: "delete" | "export" | "override", data?: any) { + if (refid == null) return + try { + let collections = await DBM.getCollectionNames(filter) + let traverse = async (f: (rid: string | null, query: Query>) => Promise) => { + let result = [] + for (let c of collections) { + if (c.name.includes("#userId") && (userId != null)) result.concat(...await f(null, { collection: c.name, userId: userId })) + else result.concat(...await f(refid, { collection: c.name })) + } + return result + } + switch (operation) { + case "delete": + await traverse((rid, query) => DBM.remove(rid, query)) + break + case "export": + let result = await traverse((rid, query) => DB.Find(rid, query)) + return JSON.stringify(result) + case "override": + if (!Array.isArray(data)) return "The data may not be an Asphyxia CORE savedata." + await traverse((rid, query) => DBM.remove(rid, query)) + for (let d of data) if ((typeof (d?.collection) == "string") && (!d.collection.includes(filter))) DB.Insert(d) + break + } + } catch (e) { + return e.message + } + return null + + } +} \ No newline at end of file diff --git a/bst@asphyxia/handlers/utility/initialize.ts b/bst@asphyxia/handlers/utility/initialize.ts new file mode 100644 index 0000000..fb29364 --- /dev/null +++ b/bst@asphyxia/handlers/utility/initialize.ts @@ -0,0 +1,15 @@ +import { initializeBatch } from "./batch_initialize" +import { IPluginVersion } from "../../models/utility/plugin_version" +import { isHigherVersion } from "../../utility/utility_functions" +import { Batch } from "./batch" +import { DBM } from "./db_manager" +import { version } from "../../utility/about" + +export async function initialize() { + let oldVersion = await DB.FindOne({ collection: "bst.pluginVersion" }) + if ((oldVersion == null) || isHigherVersion(oldVersion.version, version)) { + initializeBatch() + await Batch.execute(version) + await DBM.upsert(null, { collection: "bst.pluginVersion" }, { collection: "bst.pluginVersion", version: version }) + } +} \ No newline at end of file diff --git a/bst@asphyxia/handlers/utility/webui.ts b/bst@asphyxia/handlers/utility/webui.ts new file mode 100644 index 0000000..2c24cf2 --- /dev/null +++ b/bst@asphyxia/handlers/utility/webui.ts @@ -0,0 +1,12 @@ +import { IWebUIMessage, WebUIMessageType } from "../../models/utility/webui_message" +import { DBM } from "./db_manager" + +export namespace UtilityHandlersWebUI { + export function pushMessage(message: string, version: number, type: WebUIMessageType, rid?: string) { + DBM.upsert(null, { collection: "utility.webuiMessage" }, { collection: "utility.webuiMessage", message: message, type: type, refid: rid, version: version }) + } + + export const removeWebUIMessage = async () => { + await DB.Remove({ collection: "utility.webuiMessage" }) + } +} \ No newline at end of file diff --git a/bst@asphyxia/index.ts b/bst@asphyxia/index.ts new file mode 100644 index 0000000..6692759 --- /dev/null +++ b/bst@asphyxia/index.ts @@ -0,0 +1,27 @@ +import { UtilityHandlersCommon } from "./handlers/utility/common" +import { UtilityHandlersWebUI } from "./handlers/utility/webui" +import { initialize } from "./handlers/utility/initialize" +import { Bst2HandlersCommon } from "./handlers/bst2/common" + +export function register() { + R.GameCode("NBT") + + RouteBst2() + + R.Unhandled() + + initialize() +} + +function RouteBst2() { + R.Route("info2.common", Bst2HandlersCommon.Common) + R.Route("pcb2.boot", Bst2HandlersCommon.BootPcb) + R.Route("player2.start", Bst2HandlersCommon.StartPlayer) + R.Route("player2.continue", Bst2HandlersCommon.StartPlayer) + R.Route("player2.succeed", Bst2HandlersCommon.PlayerSucceeded) + R.Route("player2.read", Bst2HandlersCommon.ReadPlayer) + R.Route("player2.write", Bst2HandlersCommon.WritePlayer) + R.Route("player2.stagedata_write", Bst2HandlersCommon.WriteStageLog) + R.Route("player2.course_stage_data_write", Bst2HandlersCommon.WriteCourseStageLog) + R.Route("player2.course_data_write", Bst2HandlersCommon.WriteCourseLog) +} \ No newline at end of file diff --git a/bst@asphyxia/models/bst2/event_params.ts b/bst@asphyxia/models/bst2/event_params.ts new file mode 100644 index 0000000..4433dba --- /dev/null +++ b/bst@asphyxia/models/bst2/event_params.ts @@ -0,0 +1,47 @@ +import { BigIntProxy, boolme, KITEM2, KM, s32me, u64me } from "../../utility/mapping" + +export interface IFloorInfectionEventParams { + id: number + musicList: number + isCompleted: boolean +} +export const FloorInfectionEventParamsMap: KM = { + id: s32me("infection_id", 20), + musicList: s32me("music_list", 7), + isCompleted: boolme("is_complete", true) +} + +export interface IBst2EventParams { + playerId: number + startTime: bigint | BigIntProxy + hasRbCollaboration: boolean + hasPopnCollaboration: boolean + floorInfection: { event: IFloorInfectionEventParams } + museca: { isPlayedMuseca: boolean } +} +export const Bst2EventParamsMap: KM = { + playerId: s32me("plyid"), + startTime: u64me("start_time"), + hasRbCollaboration: boolme("reflec_collabo", true), + hasPopnCollaboration: boolme("pop_collabo", true), + floorInfection: { event: FloorInfectionEventParamsMap, $targetKey: "floor_infection" }, + museca: { isPlayedMuseca: boolme("is_play_museca", true) }, +} + +export interface IBst2EventControl { + type: number + phase: number +} +export const Bst2EventControlMap: KM = { + type: s32me(), + phase: s32me() +} + +let kEventControl: KITEM2[] +export function getKEventControl(): KITEM2[] { + if (kEventControl == null) { + kEventControl = [] + for (let i = 0; i <= 40; i++) for (let j = 0; j <= 25; j++) kEventControl.push({ type: K.ITEM("s32", i), phase: K.ITEM("s32", j) }) + } + return kEventControl +} \ No newline at end of file diff --git a/bst@asphyxia/models/bst2/profile.ts b/bst@asphyxia/models/bst2/profile.ts new file mode 100644 index 0000000..74b0b01 --- /dev/null +++ b/bst@asphyxia/models/bst2/profile.ts @@ -0,0 +1,262 @@ +import { BigIntProxy, boolme, colme, ignoreme, KM, s16me, s32me, s8me, strme, u16me, u64me, u8me } from "../../utility/mapping" +import { FixedSizeArray } from "../../utility/type" +import { ICollection } from "../utility/definitions" + +export interface IBst2Account extends ICollection<"bst.bst2.player.account"> { + userId: number + isTakeOver: number + playerId: number + continueCount: number + playCount: number + playCountToday: number + crd: number + brd: number + dayCount: number + refid: string + lobbyId: string + mode: number + version: number + pp: boolean + ps: boolean + pay: number + payedPlayCount: number + standardTime: bigint | BigIntProxy + intrvld?: number + previousStartTime?: bigint | BigIntProxy + ea?: boolean +} +export const Bst2AccountMap: KM = { + collection: colme("bst.bst2.player.account"), + userId: s32me("usrid"),// + isTakeOver: s32me("is_takeover"),// + playerId: s32me("plyid"), + continueCount: s32me("continue_cnt"), + playCount: s32me("tpc"),// + playCountToday: s32me("dpc"),// + crd: s32me(),// + brd: s32me(),// + dayCount: s32me("tdc"),// + refid: strme("rid"), + lobbyId: strme("lid", "Asphyxia"), + mode: u8me(null, 2), + version: s16me("ver"),// + pp: boolme(), + ps: boolme(), + pay: s16me(), + payedPlayCount: s16me("pay_pc"), + standardTime: u64me("st", BigInt(Date.now())),// + intrvld: s32me(),// + previousStartTime: u64me("pst"),// + ea: boolme()// +} + +export interface IBst2Base extends ICollection<"bst.bst2.player.base"> { + name: string + brnk: number + bcnum: number + lcnum: number + volt: number + gold: number + lastMusicId: number + lastChart: number + lastSort: number + lastTab: number + splv: number + preference: number + lcid: number + hat: number +} +export const Bst2BaseMap: KM = { + collection: colme("bst.bst2.player.base"), + name: strme(), + brnk: s8me(), + bcnum: s8me(), + lcnum: s8me(), + volt: s32me(), + gold: s32me(), + lastMusicId: s32me("lmid"), + lastChart: s8me("lgrd"), + lastSort: s8me("lsrt"), + lastTab: s8me("ltab"), + splv: s8me(), + preference: s8me("pref"), + lcid: s32me(), + hat: s32me() +} + +export interface IBst2Survey extends ICollection<"bst.bst2.player.survey"> { + motivate: number +} +export const Bst2SurveyMap: KM = { + collection: colme("bst.bst2.player.survey"), + motivate: s8me() +} + +export interface IBst2UnlockingInfo extends ICollection<"bst.bst2.player.unlockingInfo"> { + type: number + id: number + param: number + count: number +} +export const Bst2UnlockingInfoMap: KM = { + collection: colme("bst.bst2.player.unlockingInfo"), + type: s32me(), + id: s32me(), + param: s32me(), + count: s32me() +} + +export interface IBst2Customization extends ICollection<"bst.bst2.player.customization"> { + custom: FixedSizeArray +} +export const Bst2CustomizationMap: KM = { + collection: colme("bst.bst2.player.customization"), + custom: u16me(null, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) +} + +export interface IBst2Tips extends ICollection<"bst.bst2.player.tips"> { + lastTips: number +} +export const Bst2TipsMap: KM = { + collection: colme("bst.bst2.player.tips"), + lastTips: s32me("last_tips") +} + +export interface IBst2Hacker extends ICollection<"bst.bst2.player.hacker"> { + id: number + state0: number + state1: number + state2: number + state3: number + state4: number + updateTime: bigint | BigIntProxy +} +export const Bst2HackerMap: KM = { + collection: colme("bst.bst2.player.hacker"), + id: s32me(), + state0: s8me(), + state1: s8me(), + state2: s8me(), + state3: s8me(), + state4: s8me(), + updateTime: u64me("update_time") +} + +export interface IBst2CrysisLog extends ICollection<"bst.bst2.player.event.crysis"> { + id: number + stageId: number + step: number + gauge: number + state: number +} +export const Bst2CrysisLogMap: KM = { + collection: colme("bst.bst2.player.event.crysis"), + id: s32me(), + stageId: s32me("stage_no"), + step: s8me(), + gauge: s32me("r_gauge"), + state: s8me("r_state") +} + +export interface IBst2Bisco extends ICollection<"bst.bst2.player.bisco"> { + bnum: number + jbox: number +} +export const Bst2BiscoMap: KM = { + collection: colme("bst.bst2.player.bisco"), + bnum: s32me(), + jbox: s32me(), +} + +export interface IBst2MusicRecord extends ICollection<"bst.bst2.playData.musicRecord#userId"> { + musicId: number + chart: number + playCount: number + clearCount: number + gaugeTimes10: number + score: number + grade: number + medal: number + combo: number + userId: number + updateTime: number + lastPlayTime: number +} +export const Bst2MusicRecordMap: KM = { + collection: colme("bst.bst2.playData.musicRecord#userId"), + musicId: s32me("music_id"), + chart: s32me("note_level"), + playCount: s32me("play_count"), + clearCount: s32me("clear_count"), + gaugeTimes10: s32me("best_gauge"), + score: s32me("best_score"), + grade: s32me("best_grade"), + medal: s32me("best_medal"), + combo: ignoreme(), + userId: ignoreme(), + updateTime: ignoreme(), + lastPlayTime: ignoreme() +} + +export interface IBst2Course extends ICollection<"bst.bst2.playData.course#userId"> { + courseId: number + playCount: number + isTouched: boolean + clearType: number + gauge: number + score: number + grade: number + medal: number + combo: number + userId: number + updateTime: number + lastPlayTime: number +} +export const Bst2CourseMap: KM = { + collection: colme("bst.bst2.playData.course#userId"), + courseId: s32me("course_id"), + playCount: s32me("play"), + isTouched: boolme("is_touch"), + clearType: s32me("clear"), + gauge: s32me("gauge"), + score: s32me(), + grade: s32me(), + medal: s32me(), + combo: s32me(), + userId: ignoreme(), + updateTime: ignoreme(), + lastPlayTime: ignoreme() +} + +export interface IBst2Player { + pdata: { + account: IBst2Account + base: IBst2Base + opened: {} + survey: IBst2Survey + item: { info?: IBst2UnlockingInfo[] } + customize: IBst2Customization + tips: IBst2Tips + hacker: { info?: IBst2Hacker[] } + playLog: { crysis?: IBst2CrysisLog[] } + bisco: { pinfo: IBst2Bisco } + record: { rec?: IBst2MusicRecord[] } + course: { record?: IBst2Course[] } + } +} +export const Bst2PlayerMap: KM = { + pdata: { + account: Bst2AccountMap, + base: Bst2BaseMap, + opened: {}, + survey: Bst2SurveyMap, + item: { info: { 0: Bst2UnlockingInfoMap } }, + customize: Bst2CustomizationMap, + tips: Bst2TipsMap, + hacker: { info: { 0: Bst2HackerMap } }, + playLog: { crysis: { 0: Bst2CrysisLogMap }, $targetKey: "play_log" }, + bisco: { pinfo: Bst2BiscoMap }, + record: { rec: { 0: Bst2MusicRecordMap } }, + course: { record: { 0: Bst2CourseMap } } + } +} \ No newline at end of file diff --git a/bst@asphyxia/models/bst2/stagelog.ts b/bst@asphyxia/models/bst2/stagelog.ts new file mode 100644 index 0000000..f0d15ba --- /dev/null +++ b/bst@asphyxia/models/bst2/stagelog.ts @@ -0,0 +1,80 @@ +import { colme, ignoreme, KM, s32me, strme } from "../../utility/mapping" +import { ICollection } from "../utility/definitions" + +export interface IBst2StageLog extends ICollection<"bst.bst2.playData.stageLog#userId"> { + playerId: number + continueCount: number + stageId: number + userId: number + lobbyId: string + musicId: number + chart: number + gaugeTimes10: number + score: number + combo: number + grade: number + medal: number + fantasticCount: number + greatCount: number + fineCount: number + missCount: number + isCourseStage: boolean + time: number +} +export const Bst2StageLogMap: KM = { + collection: colme("bst.bst2.playData.stageLog#userId"), + playerId: s32me("play_id"), + continueCount: s32me("continue_count"), + stageId: s32me("stage_no"), + userId: s32me("user_id"), + lobbyId: strme("location_id"), + musicId: s32me("select_music_id"), + chart: s32me("select_grade"), + gaugeTimes10: s32me("result_clear_gauge"), + score: s32me("result_score"), + combo: s32me("result_max_combo"), + grade: s32me("result_grade"), + medal: s32me("result_medal"), + fantasticCount: s32me("result_fanta"), + greatCount: s32me("result_great"), + fineCount: s32me("result_fine"), + missCount: s32me("result_miss"), + isCourseStage: ignoreme(), + time: ignoreme(), +} + +export interface IBst2CourseLog extends ICollection<"bst.bst2.playData.courseLog#userId"> { + playerId: number + continueCount: number + userId: number + courseId: number + gauge: number + score: number + grade: number + medal: number + combo: number + fantasticCount: number + greatCount: number + fineCount: number + missCount: number + lobbyId: string + time: number +} +export const Bst2CourseLogMap: KM = { + collection: colme("bst.bst2.playData.courseLog#userId"), + playerId: s32me("play_id"), + continueCount: s32me("continue_count"), + userId: s32me("user_id"), + courseId: s32me("course_id"), + lobbyId: strme("lid"), + gauge: s32me(), + score: s32me(), + combo: s32me(), + grade: s32me(), + medal: s32me(), + fantasticCount: s32me("fanta"), + greatCount: s32me("great"), + fineCount: s32me("fine"), + missCount: s32me("miss"), + time: ignoreme() +} diff --git a/bst@asphyxia/models/utility/batch.ts b/bst@asphyxia/models/utility/batch.ts new file mode 100644 index 0000000..0147f43 --- /dev/null +++ b/bst@asphyxia/models/utility/batch.ts @@ -0,0 +1,5 @@ +import { ICollection } from "./definitions" + +export interface IBatchResult extends ICollection<"bst.batchResult"> { + batchId: string +} \ No newline at end of file diff --git a/bst@asphyxia/models/utility/definitions.d.ts b/bst@asphyxia/models/utility/definitions.d.ts new file mode 100644 index 0000000..820dde0 --- /dev/null +++ b/bst@asphyxia/models/utility/definitions.d.ts @@ -0,0 +1,3 @@ +export interface ICollection { + collection: TCollectionName +} \ No newline at end of file diff --git a/bst@asphyxia/models/utility/plugin_version.ts b/bst@asphyxia/models/utility/plugin_version.ts new file mode 100644 index 0000000..0a12446 --- /dev/null +++ b/bst@asphyxia/models/utility/plugin_version.ts @@ -0,0 +1,5 @@ +import { ICollection } from "./definitions" + +export interface IPluginVersion extends ICollection<"bst.pluginVersion"> { + version: string +} \ No newline at end of file diff --git a/bst@asphyxia/models/utility/webui_message.ts b/bst@asphyxia/models/utility/webui_message.ts new file mode 100644 index 0000000..7acc3ec --- /dev/null +++ b/bst@asphyxia/models/utility/webui_message.ts @@ -0,0 +1,14 @@ +import { ICollection } from "./definitions" + +export interface IWebUIMessage extends ICollection<"utility.webuiMessage"> { + message: string + type: WebUIMessageType + refid?: string + version: number +} + +export enum WebUIMessageType { + info = 0, + success = 1, + error = 2 +} \ No newline at end of file diff --git a/bst@asphyxia/utility/about.ts b/bst@asphyxia/utility/about.ts new file mode 100644 index 0000000..6b3b4ae --- /dev/null +++ b/bst@asphyxia/utility/about.ts @@ -0,0 +1,3 @@ +export type Game = "bst" +export const game: Game = "bst" +export const version: string = "0.1.0" diff --git a/bst@asphyxia/utility/mapping.ts b/bst@asphyxia/utility/mapping.ts new file mode 100644 index 0000000..521cdcb --- /dev/null +++ b/bst@asphyxia/utility/mapping.ts @@ -0,0 +1,480 @@ +import { ICollection } from "../models/utility/definitions" + +export type KArrayType = KNumberType | KBigIntType +export type KGroupType = KNumberGroupType | KBigIntGroupType +export type KType = KArrayType | KGroupType | "str" | "bin" | "ip4" | "bool" +export type KTypeExtended = KType | null | "kignore" +export type TypeForKItem = number | string | bigint | BigIntProxy | boolean | Buffer | number[] | bigint[] | boolean[] | BufferArray | NumberGroup +export type TypeForKObject = T extends TypeForKItem ? never : T +export type TypeForKArray = number[] | bigint[] | BufferArray + +export type KKey = keyof T & ( + T extends string ? Exclude : + T extends Buffer ? Exclude : + T extends boolean ? Exclude : + T extends number[] | bigint[] | boolean[] ? Exclude : + T extends any[] ? Exclude | number : + T extends number ? Exclude : + T extends bigint | BigIntProxy ? Exclude : + T extends BufferArray ? Exclude : + T extends NumberGroup ? Exclude> : + keyof T) + +export type KTypeConvert = + T extends string ? "str" : + T extends Buffer ? "bin" : + T extends number ? KNumberType | "ip4" | "bool" : + T extends bigint | BigIntProxy ? KBigIntType : + T extends boolean | boolean[] ? "bool" : + T extends number[] ? KNumberType : // KARRAY + T extends bigint[] ? KBigIntType : // KARRAY + T extends NumberGroup ? KNumberGroupType : + T extends NumberGroup ? KBigIntGroupType : + T extends BufferArray ? "u8" | "s8" : + never + +export type KArrayTypeConvert = + T extends Buffer ? "s8" | "u8" : + T extends number[] ? KNumberType : + T extends bigint[] ? KBigIntType : + never + +export type KTypeConvertBack = + TKType extends "str" ? string : + TKType extends "bin" ? { type: "Buffer"; data: number[] } : + TKType extends "s8" | "u8" ? [number] | number[] | { type: "Buffer"; data: number[] } : + TKType extends KNumberType ? [number] | number[] : + TKType extends KBigIntType ? [bigint] | bigint[] : + TKType extends KNumberGroupType ? number[] : + TKType extends KBigIntGroupType ? bigint[] : + unknown + +export type NumberGroup = { + "@numberGroupValue": T +} +export const NumberGroup = (ng: T) => { "@numberGroupValue": ng } +export function isNumberGroup(value: any): value is NumberGroup { + try { + return Array.isArray(BigInt(value["@numberGroupValue"])) + } catch { + return false + } +} +export type BufferArray = { + "@bufferArrayValue": Buffer +} +export const BufferArray = (ba: Buffer) => { "@bufferArrayValue": ba } +export function isBufferArray(value: any): value is BufferArray { + try { + return value["@bufferArrayValue"] instanceof Buffer + } catch { + return false + } +} +export type BigIntProxy = { + "@serializedBigInt": string +} +export const BigIntProxy = (value: bigint) => { "@serializedBigInt": value.toString() } +export function isBigIntProxy(value: any): value is BigIntProxy { + try { + return BigInt(value["@serializedBigInt"]).toString() == value["@serializedBigInt"] + } catch { + return false + } +} +export function toBigInt(value: bigint | BigIntProxy): bigint { + if (value == null) return null + if (value instanceof BigInt) return value + else if (value["@serializedBigInt"] != null) return BigInt(value["@serializedBigInt"]) + else return BigInt(0) +} + +export type KITEM2 = { [K in keyof T]?: K extends KKey ? KITEM2 : never } & +{ + ["@attr"]: KAttrMap2 + ["@content"]: + T extends string | Buffer | boolean | number[] | bigint[] ? T : + T extends number | bigint ? [T] : + T extends BufferArray ? Buffer : + T extends NumberGroup ? TGroup : + T extends BigIntProxy ? [bigint] : never +} + +export type KAttrMap2 = { [key: string]: string } & { + __type?: T extends TypeForKItem ? KTypeConvert : never + __count?: T extends TypeForKArray ? number : never +} + +export function ITEM2(ktype: KTypeConvert, value: T, attr?: KAttrMap2): KITEM2 { + // let result + // if (value instanceof NumberGroup && IsNumberGroupKType(ktype)) { + // result = K.ITEM(>ktype, value.value, attr) + // } else if (Array.isArray(value) && IsNumericKType(ktype)) { + // result = K.ARRAY(>ktype, value, attr) + // } else if (value instanceof BufferArray && IsNumericKType(ktype)) { + // result = K.ARRAY(>ktype, value.value, attr) + // } else if (typeof value != "object" && typeof value != "function") { + // result = K.ITEM(ktype, value, attr) + // } else { + // Object.assign(result, value, { ["@attr"]: attr }) + // result["@attr"].__type = ktype + // } + + // return >result + let result = >{} + result["@attr"] = Object.assign({}, attr, (!isNumberGroupKType(ktype) && isNumericKType(ktype) && Array.isArray(value)) ? { __type: ktype, __count: (value).length } : { __type: ktype }) + + if ((ktype == "bool") && (typeof value == "boolean")) { + result["@content"] = (value ? [1] : [0]) + } else if ((ktype == "bin") && value instanceof Buffer) { + result = K.ITEM("bin", value, result["@attr"]) + } else if (((ktype == "s8") || (ktype == "u8")) && isBufferArray(value)) { + result["@content"] = value["@bufferArrayValue"].toJSON() + result["@attr"].__count = value["@bufferArrayValue"].byteLength + } else if (isNumericKType(ktype) && !Array.isArray(value)) { + result["@content"] = [value] + } else if (isNumberGroupKType(ktype) && isNumberGroup(value)) { + result["@content"] = value["@numberGroupValue"] + } else if (isBigIntProxy(value)) { + result["@content"] = BigInt(value["@serializedBigInt"]) + } + else { + result["@content"] = value + } + if (isKIntType(ktype) && Array.isArray(result["@content"])) for (let i = 0; i < result["@content"].length; i++) (result["@content"])[i] = Math.trunc(result["@content"][i]) + return result +} + +export type KObjectMappingRecord = { [K in KKey]: T[K] extends TypeForKItem ? KObjectMappingElementInfer : KObjectMappingRecord } & KObjectMappingElementInfer +export interface KObjectMappingElement { + $type?: TKType, + $targetKey?: string, + $convert?: (source: T) => T + $convertBack?: (target: T) => T + $fallbackValue?: TKType extends "kignore" ? T : never + $defaultValue?: T +} +type KObjectMappingElementInfer = KObjectMappingElement extends KType ? KTypeConvert : never) | never | "kignore"> + +export type KAttrRecord = { [K in keyof T]?: T extends TypeForKItem ? KAttrMap2 : KAttrRecord } & { selfAttr?: KAttrMap2 } + +export function getCollectionMappingElement>(collectionName: TCollection extends ICollection ? TName : never): KObjectMappingElement ? TName : unknown, "kignore"> { + return ignoreme("collection", collectionName) +} + +function isKType(type: TType): boolean { + return (typeof (type) == "string") && ["s8", "u8", "s16", "u16", "s32", "u32", "time", "ip4", "float", "double", "bool", "s64", "u64", "2s8", "2u8", "2s16", "2u16", "2s32", "2u32", "2f", "2d", "3s8", "3u8", "3s16", "3u16", "3s32", "3u32", "3f", "3d", "4s8", "4u8", "4s16", "4u16", "4s32", "4u32", "4f", "4d", "2b", "3b", "4b", "vb", "2s64", "2u64", "3s64", "3u64", "4s64", "4u64", "vs8", "vu8", "vs16", "vu16", "str", "bin"].includes(type) +} +function isKIntType(type: TType): boolean { + return (typeof (type) == "string") && ["s8", "u8", "s16", "u16", "s32", "u32", "2s8", "2u8", "2s16", "2u16", "2s32", "2u32", "3s8", "3u8", "3s16", "3u16", "3s32", "3u32", "4s8", "4u8", "4s16", "4u16", "4s32", "4u32", "2b", "3b", "4b", "vb", "vs8", "vu8", "vs16", "vu16"].includes(type) +} +function isKBigIntType(type: TType): boolean { + return (typeof (type) == "string") && ["s64", "u64"].includes(type) +} +function isNumericKType(type: TType): boolean { + return (typeof (type) == "string") && ["s8", "u8", "s16", "u16", "s32", "u32", "time", "ip4", "float", "double", "bool", "s64", "u64"].includes(type) +} +function isNumberGroupKType(type: TType): boolean { + return (typeof (type) == "string") && ["2s8", "2u8", "2s16", "2u16", "2s32", "2u32", "2f", "2d", "3s8", "3u8", "3s16", "3u16", "3s32", "3u32", "3f", "3d", "4s8", "4u8", "4s16", "4u16", "4s32", "4u32", "4f", "4d", "2b", "3b", "4b", "vb", "2s64", "2u64", "3s64", "3u64", "4s64", "4u64", "vs8", "vu8", "vs16", "vu16"].includes(type) +} +function isNumericKey(k: any): boolean { + return (typeof k == "number") || (parseInt(k).toString() == k) +} +function increaseNumericKey(k: T, step: number = 1): T { + return (typeof k == "number") ? (k + step) : (typeof k == "string" && parseInt(k).toString() == k) ? (parseInt(k) + step) : k +} +function isEmptyKObject(o: object): boolean { + return (Object.keys(o).length == 0) || ((Object.keys(o).length == 1) && (o["@attr"] != null)) +} +function isKMapRecordReservedKey(key: string): boolean { + return ["$type", "$targetKey", "$convert", "$convertBack", "$fallbackValue", "$defaultValue"].includes(key) +} +function isKArray(data: KITEM2): boolean { + return (data["@attr"] != null) && (data["@attr"].__count != null) +} + +export function appendMappingElement(map: KObjectMappingRecord, element: KObjectMappingElementInfer): KObjectMappingRecord { + let result = >{} + Object.assign(result, map, element) + return result +} + +export function mapKObject(data: T, kMapRecord: KObjectMappingRecord, kAttrRecord: KAttrRecord = >{}): KITEM2 { + if (data == null) return >{} + let result: KITEM2 = (((0 in data) && data instanceof Object) ? [] : {}) + if (kAttrRecord.selfAttr != null) result["@attr"] = kAttrRecord.selfAttr + + if (data instanceof Object) { + for (let __k in data) { + let k: keyof T = __k + let mapK: keyof T = __k + let attrK: keyof T = __k + if (!(k in kMapRecord) && isNumericKey(k)) { + for (let i = parseInt(k) - 1; i >= 0; i--) if (kMapRecord[i]) { + mapK = i + break + } + } + if (!(k in kAttrRecord) && isNumericKey(k)) { + for (let i = parseInt(k) - 1; i >= 0; i--) if (kAttrRecord[i]) { + attrK = i + break + } + } + if (mapK in kMapRecord) { + let target = [keyof T]>{} + let targetMap = kMapRecord[>mapK] + let targetKey: keyof T = (targetMap.$targetKey != null) ? targetMap.$targetKey : k + let targetValue = (targetMap.$convert != null) ? >>targetMap.$convert(data[k]) : data[k] + let targetAttr = kAttrRecord[attrK] + if (targetMap.$type) { + let tt = targetMap.$type + if (tt == "kignore") continue + target["@attr"] = Object.assign({}, targetAttr, (!isNumberGroupKType(tt) && isNumericKType(tt) && Array.isArray(data[k]) && Array.isArray(targetValue)) ? { __type: tt, __count: (targetValue).length } : { __type: tt }) + + if ((tt == "bool") && (typeof targetValue == "boolean")) { + target["@content"] = (targetValue ? [1] : [0]) + } else if ((tt == "bin") && targetValue instanceof Buffer) { + target = K.ITEM("bin", targetValue, target["@attr"]) + } else if (((tt == "s8") || (tt == "u8")) && isBufferArray(targetValue)) { + target["@content"] = targetValue["@bufferArrayValue"] + } else if (isNumericKType(tt) && !Array.isArray(targetValue)) { + target["@content"] = [targetValue] + } else if (isNumberGroupKType(tt) && isNumberGroup(targetValue)) { + target["@content"] = targetValue["@numberGroupValue"] + } else if (isBufferArray(targetValue)) { + target["@content"] = targetValue["@bufferArrayValue"].toJSON() + target["@attr"].__count = targetValue["@bufferArrayValue"].byteLength + } else if (isBigIntProxy(targetValue)) { + target["@content"] = BigInt(targetValue["@serializedBigInt"]) + } else { + target["@content"] = targetValue + } + if (isKIntType(tt) && Array.isArray(target["@content"])) for (let i = 0; i < target["@content"].length; i++) (target["@content"])[i] = Math.trunc(target["@content"][i]) + } else { + target = mapKObject(targetValue, >targetMap, >targetAttr) + } + result[targetKey] = target + } + } + } else result = ITEM2(>kAttrRecord.selfAttr.$type, data, kAttrRecord.selfAttr) + + return result +} + +export type MapBackResult = { + data: T, + attr?: KAttrRecord +} +export function mapBackKObject(data: KITEM2, kMapRecord?: KObjectMappingRecord): MapBackResult { + if (kMapRecord == null) { + if (data["@content"] || data["@attr"]) return { data: data["@content"], attr: data["@attr"] } + else return { data: data } + } + let result: T = ((Array.isArray(data) || 0 in kMapRecord) ? [] : {}) + let resultAttr: KAttrRecord = { selfAttr: data["@attr"] ? data["@attr"] : null } + + for (let __k in kMapRecord) { + if (isKMapRecordReservedKey(__k)) continue + let k = __k + let preservK = __k + do { + let targetMap = kMapRecord[>preservK] + let targetKey = (targetMap.$targetKey ? targetMap.$targetKey : k) + let doOnceFlag = (isNumericKey(targetKey) && (data[targetKey] == null) && !isEmptyKObject(data)) + let targetValue = [keyof T]>(doOnceFlag ? data : data[targetKey]) + + if (targetMap.$type == "kignore") { + result[k] = targetMap.$fallbackValue + if ((targetValue != null) && (targetValue["@attr"] != null)) resultAttr[k] = [keyof T]>{ selfAttr: targetValue["@attr"] } + continue + } + + if (targetValue == null) { + if (targetMap.$convertBack != null) result[k] = targetMap.$convertBack(null) + continue + } + + if (targetValue["@attr"] != null) { + let targetAttr: KAttrMap2 = targetValue["@attr"] + let targetResult + + if (targetAttr.__type != null) { // KITEM + targetResult = targetValue["@content"] + if (isNumberGroupKType(targetAttr.__type)) { // KITEM2 + // TODO: bigint number group + targetResult = NumberGroup(targetResult) + } else if (targetAttr.__type == "bin") { // KITEM<"bin"> + targetResult = targetResult + } else if ((targetAttr.__type == "s8" || targetAttr.__type == "u8") && (targetResult?.type == "Buffer") && Array.isArray(targetResult?.data)) { // KITEM2 + targetResult = BufferArray(Buffer.from(targetResult.data)) + } else if (targetAttr.__type == "bool") { // KITEM<"bool"> + targetResult = targetResult[0] == 1 ? true : false + } else if (Array.isArray(targetResult) && (targetAttr.__count == null) && isNumericKType(targetAttr.__type)) { // KITEM + targetResult = ((targetAttr.__type == "s64") || (targetAttr.__type == "u64")) ? BigIntProxy(BigInt(targetResult[0])) : targetResult[0] + } + result[k] = (targetMap.$convertBack != null) ? targetMap.$convertBack(targetResult) : targetResult + } else { // KObject + targetResult = (targetMap.$convertBack != null) ? targetMap.$convertBack(targetValue) : targetValue; + let partial = mapBackKObject(targetResult, targetMap) + result[k] = partial.data + resultAttr[k] = partial.attr + } + } else { // KObject + let targetResult = (targetMap.$convertBack != null) ? targetMap.$convertBack(targetValue) : targetValue; + let partial = mapBackKObject(targetResult, targetMap) + result[k] = partial.data + resultAttr[k] = partial.attr + } + k = increaseNumericKey(k) + if (doOnceFlag || (isNumericKey(k) && (data[(targetMap.$targetKey ? targetMap.$targetKey : k)] == null))) break + } while (isNumericKey(k) && !(k in kMapRecord)) + } + return { data: result, attr: resultAttr } +} + +export function s8me(targetKey?: string, defaultValue?: T, convert?: (source: T) => T, convertBack?: (target: T) => T): KObjectMappingElement { + return { + $type: "s8", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} +export function u8me(targetKey?: string, defaultValue?: T, convert?: (source: T) => T, convertBack?: (target: T) => T): KObjectMappingElement { + return { + $type: "u8", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} +export function s16me(targetKey?: string, defaultValue?: T, convert?: (source: T) => T, convertBack?: (target: T) => T): KObjectMappingElement { + return { + $type: "s16", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} +export function u16me(targetKey?: string, defaultValue?: T, convert?: (source: T) => T, convertBack?: (target: T) => T): KObjectMappingElement { + return { + $type: "u16", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} +export function s32me(targetKey?: string, defaultValue?: T, convert?: (source: T) => T, convertBack?: (target: T) => T): KObjectMappingElement { + return { + $type: "s32", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} +export function u32me(targetKey?: string, defaultValue?: T, convert?: (source: T) => T, convertBack?: (target: T) => T): KObjectMappingElement { + return { + $type: "u32", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} +export function s64me(targetKey?: string, defaultValue?: bigint | BigIntProxy, convert?: (source: bigint | BigIntProxy) => bigint | BigIntProxy, convertBack?: (target: bigint | BigIntProxy) => bigint | BigIntProxy): KObjectMappingElement { + return { + $type: "s64", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} +export function u64me(targetKey?: string, defaultValue?: bigint | BigIntProxy, convert?: (source: bigint | BigIntProxy) => bigint | BigIntProxy, convertBack?: (target: bigint | BigIntProxy) => bigint | BigIntProxy): KObjectMappingElement { + return { + $type: "u64", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} + +export function boolme(targetKey?: string, defaultValue?: T, convert?: (source: T) => T, convertBack?: (target: T) => T): KObjectMappingElement { + return { + $type: "bool", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} +export function strme(targetKey?: string, defaultValue?: TName, convert?: (source: TName) => TName, convertBack?: (target: TName) => TName): KObjectMappingElement { + return { + $type: "str", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} + +export function binme(targetKey?: string, defaultValue?: Buffer, convert?: (source: Buffer) => Buffer, convertBack?: (target: Buffer) => Buffer): KObjectMappingElement { + return { + $type: "bin", + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} + +export function ignoreme(targetKey?: string, fallbackValue?: T): KObjectMappingElement { + return { + $type: "kignore", + $fallbackValue: fallbackValue + } +} +export function me(targetKey?: string, defaultValue?: T, convert?: (source: T) => T, convertBack?: (target: T) => T): KObjectMappingElement { + return { + $targetKey: targetKey, + $convert: convert, + $convertBack: convertBack, + $defaultValue: defaultValue + } +} +export const colme = getCollectionMappingElement +export const appendme = appendMappingElement +export const mapK = mapKObject +export const bacK = mapBackKObject + +export function fromMap(map: KObjectMappingRecord): T { + let result = {} + if (map.$type == "kignore") return map.$fallbackValue + if (map.$defaultValue != null) return map.$defaultValue + if (map.$type != null) { + if (isNumericKType(map.$type)) { + if (map.$type == "bool") return false + else return 0 + } else if (isKBigIntType(map.$type)) return BigInt(0) + else if (isNumberGroupKType(map.$type)) return NumberGroup([0]) + else if (map.$type == "str") return "" + + else return null + } + for (let k in map) { + if (isKMapRecordReservedKey(k)) continue + let value = fromMap(map[k]) + if (value != null) result[k] = value + } + + return result +} + +export type KM = KObjectMappingRecord \ No newline at end of file diff --git a/bst@asphyxia/utility/type.ts b/bst@asphyxia/utility/type.ts new file mode 100644 index 0000000..1140875 --- /dev/null +++ b/bst@asphyxia/utility/type.ts @@ -0,0 +1,4 @@ +export type FixedSizeArray = [T, ...T[]] & { readonly length: TSize } +export function fillArray(size: TSize, fillValue: T): FixedSizeArray { + return Array(size).fill(fillValue) +} \ No newline at end of file diff --git a/bst@asphyxia/utility/utility_functions.ts b/bst@asphyxia/utility/utility_functions.ts new file mode 100644 index 0000000..c5b6851 --- /dev/null +++ b/bst@asphyxia/utility/utility_functions.ts @@ -0,0 +1,67 @@ +export function toFullWidth(s: string): string { + let resultCharCodes: number[] = [] + for (let i = 0; i < s.length; i++) { + let cc = s.charCodeAt(i) + if ((cc >= 33) && (cc <= 126)) resultCharCodes.push(cc + 65281 - 33) + else if (cc == 32) resultCharCodes.push(12288) // Full-width space + else resultCharCodes.push(cc) + } + return String.fromCharCode(...resultCharCodes) +} +export function toHalfWidth(s: string): string { + let resultCharCodes: number[] = [] + for (let i = 0; i < s.length; i++) { + let cc = s.charCodeAt(i) + if ((cc >= 65281) && (cc <= 65374)) resultCharCodes.push(cc - 65281 + 33) + else if (cc == 12288) resultCharCodes.push(32) // Full-width space + else resultCharCodes.push(cc) + } + return String.fromCharCode(...resultCharCodes) +} +export function isToday(st: bigint): boolean { + let now = new Date() + let today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + let tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1) + return (st >= (today.valueOf())) && (st < (tomorrow.valueOf())) +} +export async function log(data: any, file?: string) { + if (file == null) file = "./log.txt" + let s = IO.Exists(file) ? await IO.ReadFile(file, "") : "" + if (typeof data == "string") s += data + "\n" + else { + let n = "" + try { + n = JSON.stringify(data) + } catch { } + s += n + "\n" + } + await IO.WriteFile(file, s) +} +export function base64ToBuffer(str: string, size?: number): Buffer { + if (size != null) { + let rem = size - Math.trunc(size / 3) * 3 + str = str.replace("=", "A").replace("=", "A").padEnd(Math.trunc(size / 3) * 4 + rem + 1, "A") + if (rem == 1) str += "==" + else if (rem == 2) str += "=" + let result = Buffer.alloc(size, str, "base64") + return result + } + else return Buffer.from(str, "base64") +} +export function bufferToBase64(buffer: Buffer, isTrimZero: boolean = true): string { + if (isTrimZero) for (let i = buffer.length - 1; i >= 0; i--) if (buffer.readInt8(i) != 0) return buffer.toString("base64", 0, i + 1) + return buffer.toString("base64") +} +export function isHigherVersion(left: string, right: string): boolean { + let splitedLeft = left.split(".") + let splitedRight = right.split(".") + + if (parseInt(splitedLeft[0]) < parseInt(splitedRight[0])) return true + else if (parseInt(splitedLeft[0]) == parseInt(splitedRight[0])) { + if (parseInt(splitedLeft[1]) < parseInt(splitedRight[1])) return true + else if (parseInt(splitedLeft[1]) == parseInt(splitedRight[1])) { + if (parseInt(splitedLeft[2]) < parseInt(splitedRight[2])) return true + } + } + return false +}