From bfdeca09fd7bbeafd79fea77845d04b0e7f7b5f4 Mon Sep 17 00:00:00 2001 From: RoxCian <40283094+RoxCian@users.noreply.github.com> Date: Tue, 30 Mar 2021 18:55:18 +0800 Subject: [PATCH 1/3] Initial BeatStream support. --- bst@asphyxia/README.md | 8 + bst@asphyxia/handlers/bst2/common.ts | 246 +++++++++ bst@asphyxia/handlers/bst2/processing.ts | 12 + bst@asphyxia/handlers/utility/batch.ts | 21 + .../handlers/utility/batch_initialize.ts | 7 + bst@asphyxia/handlers/utility/common.ts | 17 + bst@asphyxia/handlers/utility/db_manager.ts | 211 ++++++++ bst@asphyxia/handlers/utility/initialize.ts | 15 + bst@asphyxia/handlers/utility/webui.ts | 12 + bst@asphyxia/index.ts | 27 + bst@asphyxia/models/bst2/event_params.ts | 47 ++ bst@asphyxia/models/bst2/profile.ts | 262 ++++++++++ bst@asphyxia/models/bst2/stagelog.ts | 80 +++ bst@asphyxia/models/utility/batch.ts | 5 + bst@asphyxia/models/utility/definitions.d.ts | 3 + bst@asphyxia/models/utility/plugin_version.ts | 5 + bst@asphyxia/models/utility/webui_message.ts | 14 + bst@asphyxia/utility/about.ts | 3 + bst@asphyxia/utility/mapping.ts | 480 ++++++++++++++++++ bst@asphyxia/utility/type.ts | 4 + bst@asphyxia/utility/utility_functions.ts | 67 +++ 21 files changed, 1546 insertions(+) create mode 100644 bst@asphyxia/README.md create mode 100644 bst@asphyxia/handlers/bst2/common.ts create mode 100644 bst@asphyxia/handlers/bst2/processing.ts create mode 100644 bst@asphyxia/handlers/utility/batch.ts create mode 100644 bst@asphyxia/handlers/utility/batch_initialize.ts create mode 100644 bst@asphyxia/handlers/utility/common.ts create mode 100644 bst@asphyxia/handlers/utility/db_manager.ts create mode 100644 bst@asphyxia/handlers/utility/initialize.ts create mode 100644 bst@asphyxia/handlers/utility/webui.ts create mode 100644 bst@asphyxia/index.ts create mode 100644 bst@asphyxia/models/bst2/event_params.ts create mode 100644 bst@asphyxia/models/bst2/profile.ts create mode 100644 bst@asphyxia/models/bst2/stagelog.ts create mode 100644 bst@asphyxia/models/utility/batch.ts create mode 100644 bst@asphyxia/models/utility/definitions.d.ts create mode 100644 bst@asphyxia/models/utility/plugin_version.ts create mode 100644 bst@asphyxia/models/utility/webui_message.ts create mode 100644 bst@asphyxia/utility/about.ts create mode 100644 bst@asphyxia/utility/mapping.ts create mode 100644 bst@asphyxia/utility/type.ts create mode 100644 bst@asphyxia/utility/utility_functions.ts 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 +} From c2e321e2984a8a97131771536f580a3adf99a1fb Mon Sep 17 00:00:00 2001 From: RoxCian <40283094+RoxCian@users.noreply.github.com> Date: Thu, 1 Apr 2021 12:54:54 +0800 Subject: [PATCH 2/3] Add webui for BeatStream. --- bst@asphyxia/README.md | 5 +- bst@asphyxia/handlers/bst2/common.ts | 4 +- bst@asphyxia/handlers/bst2/webui.ts | 39 + bst@asphyxia/handlers/utility/db_manager.ts | 3 +- bst@asphyxia/handlers/utility/webui.ts | 2 +- bst@asphyxia/index.ts | 9 +- bst@asphyxia/models/bst2/profile.ts | 1 + bst@asphyxia/utility/about.ts | 3 +- bst@asphyxia/webui/css/webui_util.css | 243 +++++++ bst@asphyxia/webui/js/webui_util.js | 618 ++++++++++++++++ bst@asphyxia/webui/profile_detail.pug | 750 ++++++++++++++++++++ 11 files changed, 1668 insertions(+), 9 deletions(-) create mode 100644 bst@asphyxia/handlers/bst2/webui.ts create mode 100644 bst@asphyxia/webui/css/webui_util.css create mode 100644 bst@asphyxia/webui/js/webui_util.js create mode 100644 bst@asphyxia/webui/profile_detail.pug diff --git a/bst@asphyxia/README.md b/bst@asphyxia/README.md index 382da31..c4ffc86 100644 --- a/bst@asphyxia/README.md +++ b/bst@asphyxia/README.md @@ -1,8 +1,9 @@ # BeatStream -Plugin Version: **v0.1.0-beta** +Plugin Version: **v1.0.0** Supported Versions: - BeatStream アニムトライヴ - - Back end ✔ \ No newline at end of file + - Back end ✔ + - Web UI ✔ \ No newline at end of file diff --git a/bst@asphyxia/handlers/bst2/common.ts b/bst@asphyxia/handlers/bst2/common.ts index 710563f..8039168 100644 --- a/bst@asphyxia/handlers/bst2/common.ts +++ b/bst@asphyxia/handlers/bst2/common.ts @@ -147,7 +147,7 @@ export namespace Bst2HandlersCommon { 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 + let time = Date.now() stageLog.time = time stageLog.isCourseStage = isCourseStage @@ -199,7 +199,7 @@ export namespace Bst2HandlersCommon { 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 + let time = Date.now() courseLog.time = time if (oldRecord == null) { diff --git a/bst@asphyxia/handlers/bst2/webui.ts b/bst@asphyxia/handlers/bst2/webui.ts new file mode 100644 index 0000000..85e6ffd --- /dev/null +++ b/bst@asphyxia/handlers/bst2/webui.ts @@ -0,0 +1,39 @@ +import { IBst2Base, IBst2Customization } from "../../models/bst2/profile" +import { WebUIMessageType } from "../../models/utility/webui_message" +import { DBM } from "../utility/db_manager" +import { UtilityHandlersWebUI } from "../utility/webui" + +export namespace Bst2HandlersWebUI { + export const UpdateSettings = async (data: { + refid: string + name: string + rippleNote: number + sfxNormalNote: number + sfxRippleNote: number + sfxSlashNote: number + sfxStreamNote: number + backgroundBrightness: number + judgeText: number + rippleNoteGuide: number + streamNoteGuide: number + sfxFine: number + sfxStreamNoteTail: number + }) => { + try { + let customization = await DB.FindOne(data.refid, { collection: "bst.bst2.player.customization" }) + if (customization == null) throw new Error("No profile for refid=" + data.refid) + customization.custom[0] = data.rippleNote + customization.custom[2] = data.sfxNormalNote + customization.custom[3] = data.sfxRippleNote + customization.custom[4] = data.sfxSlashNote + customization.custom[5] = data.sfxStreamNote + customization.custom[6] = data.backgroundBrightness + customization.custom[7] = (data.judgeText << 0) | (data.rippleNoteGuide << 1) | (data.streamNoteGuide << 2) | (data.sfxStreamNoteTail << 3) | (data.sfxFine << 4) + customization.custom[9] = data.judgeText + DBM.update(data.refid, { collection: "bst.bst2.player.customization" }, customization) + UtilityHandlersWebUI.pushMessage("Save BeatStream Animtribe settings succeeded!", 2, WebUIMessageType.success, data.refid) + } catch (e) { + UtilityHandlersWebUI.pushMessage("Error while save BeatStream Animtribe settings: " + e.message, 2, WebUIMessageType.error, data.refid) + } + } +} \ No newline at end of file diff --git a/bst@asphyxia/handlers/utility/db_manager.ts b/bst@asphyxia/handlers/utility/db_manager.ts index 35a26b6..b4e5a1b 100644 --- a/bst@asphyxia/handlers/utility/db_manager.ts +++ b/bst@asphyxia/handlers/utility/db_manager.ts @@ -121,6 +121,7 @@ export namespace DBM { } async function checkData>(data: T): Promise { + for (let k in data) if (k.startsWith("__")) delete data[k] if (await DB.FindOne({ collection: "dbManager.collectionName", name: data.collection }) == null) { await DB.Insert({ collection: "dbManager.collectionName", name: data.collection }) } @@ -166,7 +167,7 @@ export namespace DBM { break } } catch (e) { - await log(Date.now().toLocaleString() + " Error: " + (e as Error).message) + await log(new Date().toLocaleString() + " Error: " + (e as Error).message) } } return result diff --git a/bst@asphyxia/handlers/utility/webui.ts b/bst@asphyxia/handlers/utility/webui.ts index 2c24cf2..ba51199 100644 --- a/bst@asphyxia/handlers/utility/webui.ts +++ b/bst@asphyxia/handlers/utility/webui.ts @@ -7,6 +7,6 @@ export namespace UtilityHandlersWebUI { } export const removeWebUIMessage = async () => { - await DB.Remove({ collection: "utility.webuiMessage" }) + await DBM.remove(null, { collection: "utility.webuiMessage" }) } } \ No newline at end of file diff --git a/bst@asphyxia/index.ts b/bst@asphyxia/index.ts index 6692759..6d5d994 100644 --- a/bst@asphyxia/index.ts +++ b/bst@asphyxia/index.ts @@ -2,18 +2,21 @@ import { UtilityHandlersCommon } from "./handlers/utility/common" import { UtilityHandlersWebUI } from "./handlers/utility/webui" import { initialize } from "./handlers/utility/initialize" import { Bst2HandlersCommon } from "./handlers/bst2/common" +import { Bst2HandlersWebUI } from "./handlers/bst2/webui" export function register() { R.GameCode("NBT") - RouteBst2() + routeBst2() + + R.WebUIEvent("removeWebUIMessage", UtilityHandlersWebUI.removeWebUIMessage) R.Unhandled() initialize() } -function RouteBst2() { +function routeBst2() { R.Route("info2.common", Bst2HandlersCommon.Common) R.Route("pcb2.boot", Bst2HandlersCommon.BootPcb) R.Route("player2.start", Bst2HandlersCommon.StartPlayer) @@ -24,4 +27,6 @@ function RouteBst2() { R.Route("player2.stagedata_write", Bst2HandlersCommon.WriteStageLog) R.Route("player2.course_stage_data_write", Bst2HandlersCommon.WriteCourseStageLog) R.Route("player2.course_data_write", Bst2HandlersCommon.WriteCourseLog) + + R.WebUIEvent("bst2UpdateSettings", Bst2HandlersWebUI.UpdateSettings) } \ No newline at end of file diff --git a/bst@asphyxia/models/bst2/profile.ts b/bst@asphyxia/models/bst2/profile.ts index 74b0b01..13103cb 100644 --- a/bst@asphyxia/models/bst2/profile.ts +++ b/bst@asphyxia/models/bst2/profile.ts @@ -107,6 +107,7 @@ export const Bst2UnlockingInfoMap: KM = { } export interface IBst2Customization extends ICollection<"bst.bst2.player.customization"> { + // [rippleNote, rippleNoteColor, sfxNormalNote, sfxRippleNote, sfxSlashNote, sfxStreamNote, backgroundBrightnessTimes2, (000{sfxFine}{sfxStreamTail}{streamNoteGuide}{rippleNoteGuide}{judgeText}, ?, ?, ?, ?, ?, ?, ?, ?)] custom: FixedSizeArray } export const Bst2CustomizationMap: KM = { diff --git a/bst@asphyxia/utility/about.ts b/bst@asphyxia/utility/about.ts index 6b3b4ae..2221fdd 100644 --- a/bst@asphyxia/utility/about.ts +++ b/bst@asphyxia/utility/about.ts @@ -1,3 +1,4 @@ export type Game = "bst" export const game: Game = "bst" -export const version: string = "0.1.0" +export type PluginVersion = "1.0.0" +export const version: PluginVersion = "1.0.0" diff --git a/bst@asphyxia/webui/css/webui_util.css b/bst@asphyxia/webui/css/webui_util.css new file mode 100644 index 0000000..5bfec78 --- /dev/null +++ b/bst@asphyxia/webui/css/webui_util.css @@ -0,0 +1,243 @@ +#tab-content, .tab-content { + display: none; +} + +#tab-content.is-active, .tab-content.is-active { + display: block; +} + +tr#tab-content.is-active, tr.tab-content.is-active { + display: table-row; +} + +#tabs li.disabled a { + background-color: #c0c0c0; + border-color: #c0c0c0; + color: #7f7f7f; + cursor: default; +} + +#form-pagination ul.pagination-list { + margin: 0!important; +} +.pagination-link, .pagination-next, .pagination-previous { + border-color: transparent; + transition: .2s linear; +} +.pagination-next, .pagination-previous { + color: #209CEE; +} +.pagination-next:not([disabled]):hover, .pagination-previous:not([disabled]):hover { + color: #118fe4; +} +/* Set all link color to Asphyxia CORE blue */ +::selection { + color: white; + background-color: #209CEE; +} +a { + color: #209CEE; +} +.tabs.is-toggle li.is-active a { + background-color: #209CEE; + border-color: #209CEE; +} +.tabs li.is-active a { + color: #209CEE; + border-color: #209CEE; +} +.pagination-link.is-current { + background-color: #209CEE; + border-color: #209CEE; + cursor: default; +} +.select:not(.is-multiple):not(.is-loading):after { + border-color: #209CEE; +} +.select select:active, .select select:focus { + border-color: #209CEE; +} +.button.is-link { + background-color: #209CEE; +} +.button.is-link.is-active, .button.is-link:active, .button.is-link.is-hovered, .button.is-link:hover { + background-color: #118fe4; +} +.input:active, .input:focus { + border-color: #209CEE; +} +.table tr.is-selected { + background-color: #209CEE; +} + +#card-content.is-hidden { + display: none; +} +#card-content { + display: block; +} +.marquee-label { + display: inline-block; +} +.marquee-label-container { + overflow-x: hidden; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; +} + +/* from Bulma */ +.button.is-danger.is-light { + background-color: #feecf0; + color: #cc0f35; +} +.button.is-link.is-light { + background-color: #edf8ff; + color: #209CEE; +} +.button.is-danger.is-light.is-hovered, .button.is-danger.is-light:hover { + background-color: #fde0e6; + color: #cc0f35; +} +.button.is-link.is-light.is-hovered, .button.is-link.is-light:hover { + background-color: #e0f1fc; + color: #209CEE; +} +.tag.is-link.is-light { + background-color: #edf8ff; + color: #0D7DC6; +} +.tag.is-link.is-light:hover { + background-color: #209CEE; + color: white; +} +.tag.is-delete:hover { + background-color: #FF3860!important; + color: white; +} + +@media screen and (max-width: 768px) { + .pagination { + flex-wrap: nowrap; + justify-content: left; + } +} +.pagination-list { + flex-wrap: nowrap; + list-style: none!important; + margin-top: 0.25em!important; + margin-bottom: 0.25em!important; +} + +.content li + li { + margin-top: 0; +} + +.one-quarter#forwide, .one-third#forwide { + display: block; + min-width: 100px; +} +.one-quarter#fornarrow, .one-third#fornarrow { + display: none; + min-width: 50px; +} +@media only screen and (max-width: 1023px) { + .one-quarter#forwide { + display: none; + } + .one-quarter#fornarrow { + display: block; + } +} +@media only screen and (max-width: 700px) { + .one-third#forwide { + display: none; + } + .one-third#fornarrow { + display: block; + } +} + + +@keyframes notification-fadeout { + 0% { + opacity: 1; + display: block; + } + 80% { + opacity: 1; + display: block; + } + 99.99% { + opacity: 0; + display: block; + } + 100% { + opacity: 0; + display: none; + } +} + +.notification { + animation: notification-fadeout 8s forwards; + animation-play-state: paused; +} +.notification:hover { + animation-play-state: paused; +} +.modal { + padding-bottom: 13px; +} +@media screen and (max-width:1024px) { + .modal { + transition: padding-left .2s ease-in-out 50ms; + } +} +@media screen and (min-width:1023px) { + .modal { + padding-left: 256px; + transition: padding-left .2s ease-in-out 50ms; + } +} +.tag { + transition: linear .2s; +} +.tags .tag:not(:last-child) { + cursor: default; +} + +.modal table tr { + border: solid #dbdbdb; + border-width: 0 0 1px; +} +.modal table tbody tr:last-child { + border-bottom-width: 0; +} +.hidden-wrapper { + overflow: hidden; +} +.hidden-x-wrapper { + overflow-x: hidden; +} +.hidden-y-wrapper { + overflow-y: hidden; +} +.scrolling-wrapper { + overflow: auto; +} +.scrolling-x-wrapper { + overflow-x: auto; +} +.scrolling-y-wrapper { + overflow-y: auto; +} +a.pagination-previous { + overflow: hidden; +} +a.pagination-next { + overflow: hidden; +} +.button.checkbox, .button.checkbox .checkmark { + transition: linear .2s; +} diff --git a/bst@asphyxia/webui/js/webui_util.js b/bst@asphyxia/webui/js/webui_util.js new file mode 100644 index 0000000..33ef6f5 --- /dev/null +++ b/bst@asphyxia/webui/js/webui_util.js @@ -0,0 +1,618 @@ +function initializePaginatedContent() { + let containers = document.querySelectorAll(".paginated-container") + + for (let container of containers) { + let pageSizeInput = container.querySelector("input.page-size") + let paginations = container.querySelectorAll(".pagination") + let contents = container.querySelectorAll(".paginated-content") + let group = container.getAttribute("pagination-group") + let flags = { isFirst: true } + let refreshEllipsis = (param) => { + if (flags.isFirst) return + let maxWidth = container.offsetWidth / 2 + for (let pagination of paginations) { + let buttons = pagination.querySelector("ul.pagination-list") + if (buttons.childElementCount == 0) return + let show = (index) => buttons.querySelector("li[tab-index=\"" + index + "\"]").style.display = "block" + let hide = (index) => buttons.querySelector("li[tab-index=\"" + index + "\"]").style.display = "none" + let previousButton = pagination.querySelector("a.pagination-previous") + let nextButton = pagination.querySelector("a.pagination-next") + let leftEllipsis = buttons.querySelector("li.ellipsis-left") + let rightEllipsis = buttons.querySelector("li.ellipsis-right") + let width = buttons.firstChild.offsetWidth.toString() + leftEllipsis.style.width = width + "px" + rightEllipsis.style.width = width + "px" + let count = buttons.childElementCount - 2 + let maxButtonCount = Math.max((buttons.firstChild.offsetWidth == 0) ? 5 : Math.trunc(maxWidth / buttons.firstChild.offsetWidth), 5) + let current = (param instanceof HTMLElement) ? param : buttons.querySelector("li.is-active") + let index = parseInt((current == null) ? 0 : current.getAttribute("tab-index")) + if (index == 0) previousButton.setAttribute("disabled", "") + else previousButton.removeAttribute("disabled") + if (index == (count - 1)) nextButton.setAttribute("disabled", "") + else nextButton.removeAttribute("disabled") + if (count <= maxButtonCount) { + for (let i = 0; i < count; i++) buttons.querySelector("li[tab-index=\"" + i + "\"]").style.display = "block" + leftEllipsis.style.display = "none" + rightEllipsis.style.display = "none" + } else { + maxButtonCount = Math.trunc((maxButtonCount - 1) / 2) * 2 + 1 + let maxSurroundingButtonCount = (maxButtonCount - 5) / 2 + let maxNoEllipsisIndex = maxButtonCount - 2 - maxSurroundingButtonCount - 1 + + if (index <= maxNoEllipsisIndex) { + for (let i = 0; i <= (maxNoEllipsisIndex + maxSurroundingButtonCount); i++) show(i) + for (let i = (maxNoEllipsisIndex + maxSurroundingButtonCount) + 1; i < count - 1; i++) hide(i) + show(count - 1) + leftEllipsis.style.display = "none" + rightEllipsis.style.display = "block" + } else if (index >= (count - maxNoEllipsisIndex - 1)) { + for (let i = 1; i < (count - maxNoEllipsisIndex - maxSurroundingButtonCount - 1); i++) hide(i) + for (let i = (count - maxNoEllipsisIndex - maxSurroundingButtonCount - 1); i < count; i++) show(i) + show(0) + leftEllipsis.style.display = "block" + rightEllipsis.style.display = "none" + } else { + for (let i = 1; i < (index - maxSurroundingButtonCount); i++) hide(i) + for (let i = (index - maxSurroundingButtonCount); i <= (index + maxSurroundingButtonCount); i++) show(i) + for (let i = (index + maxSurroundingButtonCount) + 1; i < count - 1; i++) hide(i) + show(0) + show(count - 1) + leftEllipsis.style.display = "block" + rightEllipsis.style.display = "block" + } + } + } + } + let refresh = () => { + if ((pageSizeInput == null) || (parseInt(pageSizeInput.value) <= 0)) { + for (let pagination of paginations) pagination.style.display = "none" + return + } + let pageSize = parseInt(pageSizeInput.value) + let pageCount = Math.ceil(contents.length / pageSize) + if (!flags.isFirst && (flags.pageSize == pageSize) && (flags.pageCount == pageCount)) return + for (let pagination of paginations) { + let buttons = pagination.querySelector("ul.pagination-list") + buttons.innerHTML = "" + buttons.id = "tabs" + } + for (let i = 0; i < pageCount; i++) { + for (let j = i * pageSize; j < (i + 1) * pageSize; j++) { + if (contents[j] == null) break + contents[j].classList.add("tab-content") + contents[j].setAttribute("tab-group", group) + contents[j].setAttribute("tab-index", i) + if ((i == 0) && (flags.isFirst || (flags.pageCount != pageCount))) contents[j].classList.add("is-active") + if (j == ((i + 1) * pageSize - 1)) for (let td of contents[j].querySelectorAll("td")) td.style.borderBottom = "0" + } + if (pageCount > 1) for (let pagination of paginations) { + let buttons = pagination.querySelector("ul.pagination-list") + let a = document.createElement("a") + a.classList.add("pagination-link") + a.innerText = i + 1 + let li = document.createElement("li") + li.appendChild(a) + if ((i == 0) && (flags.isFirst || (flags.pageCount != pageCount))) { + li.classList.add("is-active") + a.classList.add("is-current") + } + li.setAttribute("tab-group", group) + li.setAttribute("tab-index", i) + buttons.appendChild(li) + li.addEventListener("click", () => { + refreshEllipsis(li) + }) + } + } + if (pageCount > 1) for (let pagination of paginations) { + pagination.style.display = "flex" + let buttons = pagination.querySelector("ul.pagination-list") + let leftEllipsis = document.createElement("li") + leftEllipsis.style.display = "none" + leftEllipsis.classList.add("ellipsis-left", "ignore") + leftEllipsis.innerHTML = "" + let rightEllipsis = document.createElement("li") + rightEllipsis.style.display = "none" + rightEllipsis.classList.add("ellipsis-right", "ignore") + rightEllipsis.innerHTML = "" + buttons.firstChild.after(leftEllipsis) + buttons.lastChild.before(rightEllipsis) + + let previousButton = pagination.querySelector("a.pagination-previous") + let nextButton = pagination.querySelector("a.pagination-next") + previousButton.addEventListener("click", () => { + let current = buttons.querySelector("li.is-active") + let index = parseInt(current.getAttribute("tab-index")) + if (index <= 0) return + let prev = buttons.querySelector("li[tab-index=\"" + (index - 1) + "\"]") + prev.dispatchEvent(new Event("click")) + }) + nextButton.addEventListener("click", () => { + let current = buttons.querySelector("li.is-active") + let index = parseInt(current.getAttribute("tab-index")) + if (index >= (buttons.childElementCount - 3)) return // includes left & right ellipsis + let next = buttons.querySelector("li[tab-index=\"" + (index + 1) + "\"]") + next.dispatchEvent(new Event("click")) + }) + } else for (let pagination of paginations) pagination.style.display = "none" + flags.pageCount = pageCount + flags.pageSize = pageSize + flags.isFirst = false + } + refresh() + pageSizeInput.addEventListener("change", refresh) + let o = new ResizeObserver(refreshEllipsis) + o.observe(container) + } +} + +function initializeTabs() { + let tabs = document.querySelectorAll("#tabs li") + let tabContents = document.querySelectorAll("#tab-content, .tab-content") + let updateActiveTab = (tabGroup, tabIndex) => { + for (let t of tabs) if (t && (t.getAttribute("tab-group") == tabGroup)) { + if (t.getAttribute("tab-index") != tabIndex) { + t.classList.remove("is-active") + for (let a of t.querySelectorAll("a")) a.classList.remove("is-current") + } else { + t.classList.add("is-active") + for (let a of t.querySelectorAll("a")) a.classList.add("is-current") + } + } + } + + let updateActiveContent = (tabGroup, tabIndex) => { + for (let item of tabContents) { + let group = item.getAttribute("tab-group") + let index = item.getAttribute("tab-index") + if (item && (group == tabGroup)) item.classList.remove("is-active") + if ((index == tabIndex) && (group == tabGroup)) item.classList.add("is-active") + } + } + for (let t of tabs) { + if (!t.classList.contains("disabled") && !t.classList.contains("ignore")) t.addEventListener("click", () => { + let group = t.getAttribute("tab-group") + let index = t.getAttribute("tab-index") + updateActiveTab(group, index) + updateActiveContent(group, index) + }) + } +} + +function initializeToggles() { + let toggles = document.querySelectorAll(".card-header .card-toggle") + let contents = document.querySelectorAll(".card-content") + + for (let t of toggles) { + let card = t.getAttribute("card") + if (card == null) continue + let cc = [] + for (let c of contents) if (c.getAttribute("card") == card) cc.push(c) + t.style.transition = "0.2s linear" + t.addEventListener("click", (e) => { + if (e.currentTarget.style.transform == "rotate(180deg)") { + e.currentTarget.style.transform = "" + for (let c of cc) c.classList.remove("is-hidden") + } else { + e.currentTarget.style.transform = "rotate(180deg)" + for (let c of cc) c.classList.add("is-hidden") + } + }) + } +} + +function initializeModals() { + let modaltriggers = $(".modal-trigger") + for (let t of modaltriggers) { + let m = t.querySelector(".modal") + let c = m.querySelectorAll("#close") + t.addEventListener("click", (e) => { m.style.display = "flex" }) + for (let v of c) v.addEventListener("click", (e) => { + m.style.display = "none" + e.stopPropagation() + }) + } +} + +function initializeFormSelects() { + let formSelects = document.querySelectorAll("#form-select") + for (let s of formSelects) { + let input = s.querySelector("input#form-select-input") + let select = s.querySelector("select#form-select-select") + let options = select.querySelectorAll("option") + for (let i = 0; i < options.length; i++) { + let o = options[i] + let value = (o.getAttribute("value") == null) ? i : o.getAttribute("value") + let enabled = (o.getAttribute("disabled") == null) ? true : false + if (value == input.value) select.selectedIndex = i + if (!enabled) o.style.display = "none" + } + select.addEventListener("change", () => { + for (let i = 0; i < options.length; i++) { + let o = options[i] + if (o.selected) { + input.value = (o.getAttribute("value") == null) ? i : o.getAttribute("value") + input.dispatchEvent(new Event("change")) + break + } + } + }) + } +} + +function initializeFormPaginations() { + let formPags = document.querySelectorAll("#form-pagination") + for (let p of formPags) { + let input = p.querySelector("input#form-pagination-input") + let options = p.querySelectorAll("ul.pagination-list li a.pagination-link") + for (let i = 0; i < options.length; i++) { + let o = options[i] + let value = (o.getAttribute("value") == null) ? i : o.getAttribute("value") + if (value == input.value) { + if (!o.classList.contains("is-current")) o.classList.add("is-current") + } else o.classList.remove("is-current") + o.addEventListener("click", () => { + for (let i = 0; i < options.length; i++) options[i].classList.remove("is-current") + if (!o.classList.contains("is-current")) o.classList.add("is-current") + input.value = (o.getAttribute("value") == null) ? i : o.getAttribute("value") + }) + } + } +} + +function initializeFormValidation() { + let forms = document.querySelectorAll("form#validatable") + for (let f of forms) { + let validatableFields = f.querySelectorAll(".field#validatable") + let validatableButtons = f.querySelectorAll("button#validatable") + + let getParams = (input) => { + return { + minLength: input.getAttribute("min-length"), + maxLength: input.getAttribute("max-length"), + recommendedLength: input.getAttribute("recommended-length"), + minPattern: input.getAttribute("min-pattern"), + recommendedPattern: input.getAttribute("recommended-pattern"), + isNumeric: (input.getAttribute("numeric") != null) ? true : false + } + } + let isValid = (value, params) => { + let t = value.trim() + if (params.minLength != null) if (t.length < parseInt(params.minLength)) return false + if (params.maxLength != null) if (t.length > parseInt(params.maxLength)) return false + if (params.minPattern != null) if (!(new RegExp(params.minPattern).test(t))) return false + if (params.isNumeric == true) if (parseInt(t).toString() != t) return false + return true + } + + let isFormValid = () => { + for (let field of validatableFields) for (let i of field.querySelectorAll("input#validatable")) if (!isValid(i.value, getParams(i))) return false + return true + } + + for (let field of validatableFields) { + let inputs = field.querySelectorAll("input#validatable") + let tips = field.querySelectorAll(".help") + for (let i of inputs) i.addEventListener("change", () => { + let params = getParams(i) + // inputs + if (isValid(i.value, params)) { + i.classList.remove("is-danger") + for (let t of tips) t.classList.remove("is-danger") + } else if (!i.classList.contains("is-danger")) { + i.classList.add("is-danger") + for (let t of tips) t.classList.add("is-danger") + } + // buttons + if (isFormValid()) { + for (let b of validatableButtons) b.removeAttribute("disabled") + } else { + for (let b of validatableButtons) if (b.getAttribute("disabled") == null) b.setAttribute("disabled", "") + } + }) + } + } +} + +function initializeFormCollections() { + let collections = document.querySelectorAll("#form-collection") + for (let c of collections) { + let maxLength = parseInt(c.getAttribute("max-length")) + let fallbackValue = JSON.parse(c.getAttribute("fallback")) + let input = c.querySelector("#form-collection-input") + let tags = c.querySelectorAll("#form-collection-tag") + let modButton = c.querySelector("#form-collection-modify") + let modTable = c.querySelector("table#multi-select") + let modInput = modTable.querySelector("input#multi-select-input") + let modTitle = modTable.querySelector("input#multi-select-title") + let deleteButtonClickEventListener = (tag) => () => { + let tvalue = JSON.parse(tag.getAttribute("value")) + let value = JSON.parse(input.value) + value.splice(value.indexOf(tvalue), 1) + if (fallbackValue != null) value.push(fallbackValue) + input.value = JSON.stringify(value) + modInput.value = input.value + modInput.dispatchEvent(new Event("change")) + tag.remove() + } + + for (let t of tags) { + let d = t.querySelector(".delete, .is-delete") + d.addEventListener("click", deleteButtonClickEventListener(t)) + } + modInput.value = input.value + modInput.setAttribute("max-length", maxLength) + modInput.setAttribute("fallback", JSON.stringify(fallbackValue)) + modInput.addEventListener("change", () => { + let fallbackValue = JSON.parse(c.getAttribute("fallback")) + let oldValue = JSON.parse(input.value) + let newValue = JSON.parse(modInput.value) + let tags = c.querySelectorAll("#form-collection-tag") + for (let o of oldValue) if (!newValue.includes(o) && (o != fallbackValue)) { + for (let t of tags) if (JSON.parse(t.getAttribute("value")) == o) t.remove() + } + for (let n = 0; n < newValue.length; n++) if (!oldValue.includes(newValue[n]) && (newValue[n] != fallbackValue)) { + let tag = document.createElement("div") + tag.classList.add("control") + tag.id = "form-collection-tag" + tag.setAttribute("value", newValue[n]) + tag.innerHTML = "" + JSON.parse(modTitle.value)[n] + "" + tag.querySelector("a.is-delete").addEventListener("click", deleteButtonClickEventListener(tag)) + modButton.before(tag) + } + input.value = modInput.value + }) + } +} + +function initializeMultiSelectTables() { + let tables = document.querySelectorAll("table#multi-select") + for (let table of tables) { + let valueInput = table.querySelector("input#multi-select-input") + let titleInput = table.querySelector("input#multi-select-title") + let trimValues = (values, fallback) => { + while (values.includes(fallback)) values.splice(values.indexOf(fallback), 1) + return values + } + let fillValues = (values, fallback) => { + let maxLength = (valueInput.getAttribute("max-length") == null) ? -1 : parseInt(valueInput.getAttribute("max-length")) + while (values.length < maxLength) values.push(fallback) + return values + } + let lines = table.querySelectorAll("tbody tr") + let refresh = () => { + let fallbackValue = JSON.parse(valueInput.getAttribute("fallback")) + let value = trimValues(JSON.parse(valueInput.value), fallbackValue) + let title = [] + for (let l of lines) { + let lvalue = JSON.parse(l.getAttribute("multi-select-value")) + if (value.includes(lvalue)) { + if (!l.classList.contains("is-selected")) l.classList.add("is-selected") + title[value.indexOf(lvalue)] = l.getAttribute("multi-select-title") + l.style.fontWeight = "bold" + } else { + l.classList.remove("is-selected") + l.style.fontWeight = "" + } + } + titleInput.value = JSON.stringify(title) + } + + for (let l of lines) { + l.onclick = () => { + let fallbackValue = JSON.parse(valueInput.getAttribute("fallback")) + let maxLength = (valueInput.getAttribute("max-length") == null) ? -1 : parseInt(valueInput.getAttribute("max-length")) + let value = trimValues(JSON.parse(valueInput.value), fallbackValue) + let lvalue = JSON.parse(l.getAttribute("multi-select-value")) + if (value.includes(lvalue)) value.splice(value.indexOf(lvalue), 1) + else if (maxLength >= 0) { + if (value.length < maxLength) value.push(lvalue) + else alert("Cannot add more items, items are up to " + maxLength + ".") + } else value.push(lvalue) + valueInput.value = JSON.stringify(fillValues(value, fallbackValue)) + refresh() + valueInput.dispatchEvent(new Event("change")) + } + refresh() + } + valueInput.addEventListener("change", refresh) + } +} + +function initializeFormNumerics() { + let numerics = document.querySelectorAll("#form-numeric") + for (let n of numerics) { + let add = n.querySelector("#form-numeric-add") + let sub = n.querySelector("#form-numeric-sub") + let inputs = n.querySelectorAll("#form-numeric-input") + add.addEventListener("click", (e) => { + for (let i of inputs) { + let maxValue = parseFloat(i.getAttribute("max-value")) + let step = parseFloat(i.getAttribute("step")) + + let digitCount = (i.getAttribute("digit-count") == null) ? -1 : parseInt(i.getAttribute("digit-count")) + let value = (parseFloat(i.value) * 10 + step * 10) / 10 + if (value * Math.sign(step) <= maxValue * Math.sign(step)) i.value = (digitCount >= 0) ? value.toFixed(digitCount) : value + } + e.stopPropagation() + }) + sub.addEventListener("click", (e) => { + for (let i of inputs) { + let minValue = parseFloat(i.getAttribute("min-value")) + let step = parseFloat(i.getAttribute("step")) + let digitCount = (i.getAttribute("digit-count") == null) ? -1 : parseInt(i.getAttribute("digit-count")) + let value = (parseFloat(i.value) * 10 - step * 10) / 10 + if (value * Math.sign(step) >= minValue * Math.sign(step)) i.value = (digitCount >= 0) ? value.toFixed(digitCount) : value + } + e.stopPropagation() + }) + for (let i of inputs) { + let digitCount = (i.getAttribute("digit-count") == null) ? -1 : parseInt(i.getAttribute("digit-count")) + let value = parseFloat(i.value) + i.value = (digitCount >= 0) ? value.toFixed(digitCount) : value + } + } +} + +function initializeUploader() { + let uploaders = document.querySelectorAll("div#uploader") + for (let uploader of uploaders) { + let input = uploader.querySelector("input#uploader-input") + let text = uploader.querySelector("input#uploader-text") + let placeholder = uploader.querySelector("#uploader-placeholder") + let remove = uploader.querySelector("#uploader-delete") + let reader = new FileReader() + input.addEventListener("change", () => { + if (input.files.length > 0) { + remove.style.display = "block" + placeholder.innerText = input.files[0].name + reader.readAsText(input.files[0]) + reader.onload = () => text.value = reader.result + } else { + placeholder.innerText = "" + remove.style.display = "none" + text.value = null + } + }) + remove.addEventListener("click", (e) => { + e.stopPropagation() + input.value = null + input.dispatchEvent(new Event("change")) + }) + + remove.style.display = "none" + } +} + +function checkImg() { + let imgs = document.querySelectorAll("#exist-or-not") + for (let img of imgs) { + let general = img.querySelector("img#general") + let specified = img.querySelector("img#specified") + + if (specified.width == 0) specified.style.display = "none" + else general.style.display = "none" + } +} + +function initializeMarqueeLabels() { + let marqueeContainers = document.querySelectorAll(".marquee-label-container") + for (let c of marqueeContainers) { + let marquees = c.querySelectorAll(".marquee-label") + for (let marquee of marquees) { + if (marquee.closest(".marquee-label-container") != c) continue + let refresh = () => { + let lpad = parseInt(window.getComputedStyle(c, null).getPropertyValue("padding-left")) + if (lpad == NaN) lpad = 0 + let rpad = parseInt(window.getComputedStyle(c, null).getPropertyValue("padding-right")) + if (rpad == NaN) rpad = 20 + let hpad = lpad + rpad + let speed = marquee.getAttribute("speed") + if (speed == null) speed = 1 + let stopingTime = 0.5 + let duration = (20 * (marquee.offsetWidth - c.offsetWidth + hpad)) / speed + 2 * stopingTime + if ((marquee.offsetWidth > 0) && (marquee.offsetWidth > c.offsetWidth - hpad)) { + marquee.animate([ + { transform: "translateX(0)", offset: 0 }, + { transform: "translateX(0)", easing: "cubic-bezier(0.67, 0, 0.33, 1)", offset: stopingTime / duration }, + { transform: "translateX(" + (c.offsetWidth - marquee.offsetWidth - hpad) + "px)", easing: "cubic-bezier(0.67, 0, 0.33, 1)", offset: 1 - stopingTime / duration }, + { transform: "translateX(" + (c.offsetWidth - marquee.offsetWidth - hpad) + "px)", offset: 1 } + ], { duration: (20 * (marquee.offsetWidth - c.offsetWidth) + 1000) / speed, direction: "alternate-reverse", iterations: Infinity }) + } else marquee.style.animation = "none" + } + let o = new ResizeObserver(refresh) + o.observe(c) + } + } +} + +function initializeNotificatioAnimation() { + let notifications = document.querySelectorAll(".notification.temporary") + for (let n of notifications) { + let remove = n.querySelector(".delete") + let startSubmitter = n.querySelector("form.start") + let startPath = startSubmitter.getAttribute("action") + let startRequest = new XMLHttpRequest() + startRequest.open("POST", startPath, true) + startRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded") + + let endSubmitter = n.querySelector("form.end") + let endPath = startSubmitter.getAttribute("action") + let endRequest = new XMLHttpRequest() + endRequest.open("POST", endPath, true) + endRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded") + + if (startSubmitter != null) startRequest.send() + let end = () => { + n.style.display = "none" + if (endSubmitter != null) endRequest.send() + } + + n.style.animationPlayState = "running" + remove.addEventListener("click", end) + n.addEventListener("animationend", end) + n.addEventListener("webkitAnimationEnd", end) + } +} + +function initializeCheckBoxes() { + let checks = document.querySelectorAll(".checkbox") + for (let c of checks) { + let input = c.querySelector("input[type=checkbox]") + let mark = c.querySelector(".checkmark") + let refresh = (value) => { + value = input.getAttribute("checked") + if (value == null) { + input.removeAttribute("checked") + mark.style.opacity = 0 + if (!c.classList.contains("is-light")) c.classList.add("is-light") + } else { + input.setAttribute("checked", "checked") + mark.style.opacity = 100 + c.classList.remove("is-light") + } + } + c.addEventListener("click", () => { + let value = input.getAttribute("checked") + if (value == null) input.setAttribute("checked", "checked") + else input.removeAttribute("checked") + refresh() + }) + refresh() + } +} + +function removeLoadingModal() { + let loading = document.querySelector(".loading") + setTimeout(() => (loading == null) ? null : loading.remove(), 505) + try { + let a = loading.animate([ + { offset: 0, opacity: 1 }, + { offset: 0.25, opacity: 0 }, + { offset: 1, opacity: 0 } + ], { duration: 2000 }) + a.onfinish = loading.remove + a.play() + } catch { } +} + +$(document).ready(() => { + initializeNotificatioAnimation() + initializePaginatedContent() + initializeTabs() + initializeToggles() + initializeModals() + initializeFormSelects() + initializeFormNumerics() + initializeFormPaginations() + initializeFormValidation() + initializeFormCollections() + initializeMultiSelectTables() + initializeUploader() + checkImg() + initializeMarqueeLabels() + initializeCheckBoxes() + + removeLoadingModal() +}) + diff --git a/bst@asphyxia/webui/profile_detail.pug b/bst@asphyxia/webui/profile_detail.pug new file mode 100644 index 0000000..44b8094 --- /dev/null +++ b/bst@asphyxia/webui/profile_detail.pug @@ -0,0 +1,750 @@ +//DATA// + bst2Account: DB.FindOne(refid, { collection: "bst.bst2.player.account" }) + bst2Base: DB.FindOne(refid, { collection: "bst.bst2.player.base" }) + bst2Customization: DB.FindOne(refid, { collection: "bst.bst2.player.customization" }) + bst2Course: DB.Find({ collection: "bst.bst2.playData.course#userId" }) + bst2MusicRecord: DB.Find({ collection: "bst.bst2.playData.musicRecord#userId" }) + bst2CourseLog: DB.Find({ collection: "bst.bst2.playData.courseLog#userId" }) + bst2StageLog: DB.Find({ collection: "bst.bst2.playData.stageLog#userId" }) + + webuiMessage: DB.FindOne({ collection: "utility.webuiMessage" }) + +//- Definitions +- + let defaultVersion = 2 + + let bstMusicsInfo = [{ musicId: 0, title: "SAVIOR OF SONG", version: 1, category: "ANIME", bpm: { min: 192, max: 192 }, chartsInfo: { light: "02", medium: "08", beast: "09", nightmare: "10" }, artist: "ナノ feat. MY FIRST STORY" }, { musicId: 1, title: "マネマネサイコトロピック", version: 1, category: "EXITTUNES", bpm: { min: 135, max: 210 }, chartsInfo: { light: "02", medium: "06", beast: "09⁺" }, artist: "かいりきベア" }, { musicId: 2, title: "ミスターデジャブ", version: 1, category: "EXITTUNES", bpm: { min: 230, max: 230 }, chartsInfo: { light: "01", medium: "06", beast: "10⁻" }, artist: "164 feat.MAYU" }, { musicId: 3, title: "セツナトリップ", version: 1, category: "EXITTUNES", bpm: { min: 145, max: 145 }, chartsInfo: { light: "01", medium: "06", beast: "09⁻", nightmare: "10" }, artist: "Last Note. feat. GUMI" }, { musicId: 4, title: "ラクガキスト", version: 1, category: "EXITTUNES", bpm: { min: 199, max: 199 }, chartsInfo: { light: "05", medium: "09⁻", beast: "09⁺", nightmare: "10⁺" }, artist: "cosMo@暴走P feat.GUMI" }, { musicId: 5, title: "放課後ストライド", version: 1, category: "EXITTUNES", bpm: { min: 225, max: 225 }, chartsInfo: { light: "04", medium: "07", beast: "09⁻", nightmare: "10" }, artist: "Last Note." }, { musicId: 6, title: "リズの内心革命", version: 1, category: "EXITTUNES", bpm: { min: 180, max: 180 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "じっぷす" }, { musicId: 7, title: "ロストワンの号哭", version: 1, category: "EXITTUNES", bpm: { min: 162, max: 162 }, chartsInfo: { light: "03", medium: "07", beast: "10", nightmare: "10" }, artist: "Neru" }, { musicId: 8, title: "HΨ=世界創造=EΨ", version: 1, category: "EXITTUNES", bpm: { min: 208, max: 208 }, chartsInfo: { light: "03", medium: "08", beast: "10" }, artist: "黒猫アンティーク feat.IA,GUMI" }, { musicId: 9, title: "Nyan Cat", version: 1, category: "EXITTUNES", bpm: { min: 142, max: 142 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "daniwellP feat. 桃音モモ" }, { musicId: 10, title: "天ノ弱", version: 1, category: "EXITTUNES", bpm: { min: 200, max: 205 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "164" }, { musicId: 11, title: "恋愛勇者", version: 1, category: "EXITTUNES", bpm: { min: 190, max: 190 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "Last Note." }, { musicId: 12, title: "ねこみみスイッチ", version: 1, category: "EXITTUNES", bpm: { min: 160, max: 160 }, chartsInfo: { light: "02", medium: "06", beast: "08", nightmare: "09⁻" }, artist: "daniwellP" }, { musicId: 13, title: "にゃにゃにゃんにゃんにゃーのこねこ", version: 1, category: "EXITTUNES", bpm: { min: 184, max: 184 }, chartsInfo: { light: "02", medium: "06", beast: "09⁺" }, artist: "daniwellP" }, { musicId: 14, title: "過食性:アイドル症候群", version: 1, category: "EXITTUNES", bpm: { min: 200, max: 200 }, chartsInfo: { light: "04", medium: "08", beast: "10", nightmare: "10" }, artist: "スズム" }, { musicId: 15, title: "チルノのパーフェクトさんすう教室", version: 1, category: "TOHO", bpm: { min: 175, max: 175 }, chartsInfo: { light: "02", medium: "07", beast: "09⁻", nightmare: "09⁺" }, artist: "ARM+夕野ヨシミ feat. miko" }, { musicId: 16, title: "待チ人ハ来ズ。", version: 1, category: "TOHO", bpm: { min: 160, max: 160 }, chartsInfo: { light: "03", medium: "06", beast: "08" }, artist: "豚乙女" }, { musicId: 17, title: "月に叢雲華に風", version: 1, category: "TOHO", bpm: { min: 160, max: 160 }, chartsInfo: { light: "04", medium: "07", beast: "09⁻", nightmare: "10-" }, artist: "幽閉サテライト(Arranged:Iceon) feat. senya" }, { musicId: 18, title: "物凄い勢いでけーねが物凄いうた", version: 1, category: "TOHO", bpm: { min: 180, max: 180 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "Halozy feat. ななひら" }, { musicId: 19, title: "INFINITE WORLD", version: 1, category: "TOHO", bpm: { min: 167, max: 167 }, chartsInfo: { light: "03", medium: "07", beast: "09", nightmare: "10" }, artist: "SOUND HOLIC feat. Nana Takahashi" }, { musicId: 20, title: "ウサテイ", version: 1, category: "TOHO", bpm: { min: 190, max: 190 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "あまね+ビートまりお(COOL&CREATE)" }, { musicId: 21, title: "魔理沙は大変なものを盗んでいきました", version: 1, category: "TOHO", bpm: { min: 170, max: 170 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺", nightmare: "10" }, artist: "ARM+夕野ヨシミ feat. 藤咲かりん" }, { musicId: 22, title: "色は匂へど 散りぬるを", version: 1, category: "TOHO", bpm: { min: 138, max: 138 }, chartsInfo: { light: "03", medium: "06", beast: "08" }, artist: "幽閉サテライト(Arranged:Iceon) feat. senya" }, { musicId: 23, title: "お嫁にしなさいっ!", version: 1, category: "TOHO", bpm: { min: 180, max: 180 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "ARM+夕野ヨシミ" }, { musicId: 24, title: "しゅわスパ大作戦☆", version: 1, category: "TOHO", bpm: { min: 140, max: 140 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "SOUND HOLIC feat. Nana Takahashi" }, { musicId: 25, title: "げきオコスティックファイナリアリティぷんぷんマスタースパーク", version: 1, category: "TOHO", bpm: { min: 200, max: 200 }, chartsInfo: { light: "04", medium: "08", beast: "09⁺" }, artist: "ARM+ビートまりお(COOL&CREATE)" }, { musicId: 26, title: "ケロ⑨destiny", version: 1, category: "TOHO", bpm: { min: 172, max: 172 }, chartsInfo: { light: "02", medium: "07", beast: "09⁺" }, artist: "Silver Forest feat. めらみぽっぷ" }, { musicId: 27, title: "Miracle Halloween", version: 1, category: "KDE", bpm: { min: 140, max: 140 }, chartsInfo: { light: "02", medium: "06", beast: "08" }, artist: "Sana" }, { musicId: 28, title: "ミライノトビラ", version: 1, category: "KDE", bpm: { min: 163, max: 163 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "Sana" }, { musicId: 29, title: "ちくわパフェだよ☆CKP", version: 1, category: "KDE", bpm: { min: 205, max: 205 }, chartsInfo: { light: "04", medium: "08", beast: "09⁺", nightmare: "10" }, artist: "日向美ビタースイーツ♪" }, { musicId: 30, title: "Colorful Days ~NEWラブプラス メインテーマ~", version: 1, category: "KDE", bpm: { min: 128, max: 128 }, chartsInfo: { light: "02", medium: "06", beast: "08" }, artist: "高嶺愛花&小早川凛子&姉ヶ崎寧々" }, { musicId: 31, title: "Rainbow Magic", version: 1, category: "KDE", bpm: { min: 124, max: 150 }, chartsInfo: { light: "02", medium: "06", beast: "08" }, artist: "マジックアカデミー管弦楽部×劇団レコード" }, { musicId: 32, title: "チュートリアル", version: 2, category: "KDE", bpm: { min: 100, max: 100 }, chartsInfo: { light: "01" }, artist: "-" }, { musicId: 33, title: "回レ!雪月花", version: 1, category: "ANIME", bpm: { min: 160, max: 160 }, chartsInfo: { light: "04", medium: "08", beast: "10", nightmare: "10" }, artist: "歌組雪月花" }, { musicId: 34, title: "Anicca", version: 1, category: "ANIME", bpm: { min: 175, max: 175 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "原田ひとみ" }, { musicId: 35, title: "This game", version: 1, category: "ANIME", bpm: { min: 147, max: 147 }, chartsInfo: { light: "02", medium: "04", beast: "09⁻", nightmare: "09⁺" }, artist: "鈴木このみ" }, { musicId: 36, title: "オラシオン", version: 1, category: "ANIME", bpm: { min: 166, max: 166 }, chartsInfo: { light: "01", medium: "04", beast: "08", nightmare: "09⁻" }, artist: "白(CV:茅野愛衣)" }, { musicId: 37, title: "共鳴のTrue Force", version: 1, category: "ANIME", bpm: { min: 187.5, max: 187.5 }, chartsInfo: { light: "04", medium: "07", beast: "09⁺" }, artist: "原田ひとみ" }, { musicId: 38, title: "精霊剣舞祭", version: 1, category: "ANIME", bpm: { min: 187, max: 187 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "にーそっくすす" }, { musicId: 39, title: "祝祭のエレメンタリア", version: 1, category: "ANIME", bpm: { min: 150, max: 150 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "にーそっくすす" }, { musicId: 40, title: "幻想系世界修復少女", version: 1, category: "EXITTUNES", bpm: { min: 193, max: 193 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "Last Note." }, { musicId: 41, title: "バンブーソード・ガール", version: 1, category: "EXITTUNES", bpm: { min: 208, max: 208 }, chartsInfo: { light: "04", medium: "08", beast: "09" }, artist: "cosMo@暴走P" }, { musicId: 42, title: "Idola", version: 1, category: "KDE", bpm: { min: 201, max: 201 }, chartsInfo: { light: "04", medium: "08", beast: "10" }, artist: "iconoclasm feat.GUMI" }, { musicId: 43, title: "サリシノハラ", version: 1, category: "EXITTUNES", bpm: { min: 135, max: 135 }, chartsInfo: { light: "02", medium: "06", beast: "08", nightmare: "09⁻" }, artist: "みきとP" }, { musicId: 44, title: "脱出ゲヱム", version: 1, category: "EXITTUNES", bpm: { min: 134, max: 134 }, chartsInfo: { light: "04", medium: "07", beast: "10⁻" }, artist: "ゆちゃP" }, { musicId: 45, title: "ケッペキショウ", version: 1, category: "EXITTUNES", bpm: { min: 190, max: 190 }, chartsInfo: { light: "04", medium: "08", beast: "10⁻" }, artist: "すこっぷ" }, { musicId: 46, title: "僕は空気が嫁ない", version: 1, category: "EXITTUNES", bpm: { min: 192, max: 192 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "cosMo@暴走P" }, { musicId: 47, title: "sweet little sister", version: 1, category: "TOHO", bpm: { min: 168, max: 168 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "Silver Forest feat. さゆり" }, { musicId: 48, title: "泡沫、哀のまほろば", version: 1, category: "TOHO", bpm: { min: 155, max: 155 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "幽閉サテライト(Arranged:Iceon,HiZuMi) feat. senya" }, { musicId: 49, title: "最終鬼畜妹フランドール・S", version: 1, category: "TOHO", bpm: { min: 200, max: 200 }, chartsInfo: { light: "04", medium: "07", beast: "10⁻", nightmare: "10" }, artist: "ビートまりお(COOL&CREATE)" }, { musicId: 50, title: "Help me, ERINNNNNN!!", version: 1, category: "TOHO", bpm: { min: 183, max: 183 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻", nightmare: "10-" }, artist: "ビートまりお(COOL&CREATE)" }, { musicId: 51, title: "ナイト・オブ・ナイツ", version: 1, category: "TOHO", bpm: { min: 180, max: 180 }, chartsInfo: { light: "04", medium: "08", beast: "10", nightmare: "10" }, artist: "ビートまりお(COOL&CREATE)" }, { musicId: 52, title: "†渚の小悪魔ラヴリィ~レイディオ† ", version: 1, category: "KDE", bpm: { min: 190, max: 190 }, chartsInfo: { light: "03", medium: "08", beast: "09⁺" }, artist: "夏色ビキニのPrim" }, { musicId: 53, title: "恋する☆宇宙戦争っ!!", version: 1, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "04", medium: "08", beast: "10", nightmare: "ネ申" }, artist: "Prim" }, { musicId: 54, title: "狂イ咲ケ焔ノ華", version: 1, category: "KDE", bpm: { min: 206, max: 206 }, chartsInfo: { light: "04", medium: "08", beast: "10⁻" }, artist: "覚醒ノPrim" }, { musicId: 55, title: "めうめうぺったんたん!!", version: 1, category: "KDE", bpm: { min: 185, max: 185 }, chartsInfo: { light: "04", medium: "08", beast: "10", nightmare: "10" }, artist: "日向美ビタースイーツ♪" }, { musicId: 56, title: "カタルシスの月", version: 1, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "日向美ビタースイーツ♪" }, { musicId: 57, title: "凛として咲く花の如く ~ひなビタ♪edition~", version: 1, category: "KDE", bpm: { min: 163, max: 163 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻", nightmare: "09⁺" }, artist: "日向美ビタースイーツ♪" }, { musicId: 58, title: "滅亡天使 † にこきゅっぴん", version: 1, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "日向美ビタースイーツ♪" }, { musicId: 59, title: "Bad Apple!! feat. nomico", version: 1, category: "TOHO", bpm: { min: 138, max: 138 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺", nightmare: "10-" }, artist: "Alstroemeria Records" }, { musicId: 60, title: "ビビットストリーム", version: 1, category: "KDE", bpm: { min: 184, max: 184 }, chartsInfo: { light: "03", medium: "08", beast: "09⁻", nightmare: "10-" }, artist: "DJ TOTTO" }, { musicId: 61, title: "groovin'", version: 1, category: "KDE", bpm: { min: 140, max: 140 }, chartsInfo: { light: "04", medium: "08", beast: "09⁺" }, artist: "Sota Fujimori" }, { musicId: 62, title: "センチメンタルラブ", version: 1, category: "ANIME", bpm: { min: 138, max: 138 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "みみめめMIMI" }, { musicId: 63, title: "sister’s noise", version: 1, category: "ANIME", bpm: { min: 144, max: 144 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻", nightmare: "09⁺" }, artist: "fripSide" }, { musicId: 64, title: "Daydream café", version: 1, category: "ANIME", bpm: { min: 150, max: 150 }, chartsInfo: { light: "05", medium: "08", beast: "09⁺", nightmare: "10" }, artist: "Petit Rabbit’s" }, { musicId: 65, title: "閃光の行方", version: 1, category: "ANIME", bpm: { min: 175, max: 175 }, chartsInfo: { light: "03", medium: "10", beast: "10" }, artist: "Falcom Sound Team jdk" }, { musicId: 66, title: "I'll remember you", version: 1, category: "ANIME", bpm: { min: 148, max: 148 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "Falcom Sound Team jdk" }, { musicId: 67, title: "乙女繚乱 舞い咲き誇れ", version: 1, category: "KDE", bpm: { min: 210, max: 210 }, chartsInfo: { light: "04", medium: "08", beast: "10" }, artist: "日向美ビタースイーツ♪" }, { musicId: 68, title: "Harmonia", version: 1, category: "KDE", bpm: { min: 177, max: 177 }, chartsInfo: { light: "04", medium: "08", beast: "10" }, artist: "ATSUMI UEDA" }, { musicId: 69, title: "青春セッション PARADISE", version: 1, category: "OTHER", bpm: { min: 150, max: 190 }, chartsInfo: { light: "03", medium: "07", beast: "10", nightmare: "10" }, artist: "A応P" }, { musicId: 70, title: "きょうもハレバレ", version: 1, category: "EXITTUNES", bpm: { min: 134, max: 134 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "ふわりP feat. GUMI's" }, { musicId: 71, title: "ノイジーラバーソウル", version: 1, category: "EXITTUNES", bpm: { min: 190, max: 190 }, chartsInfo: { light: "04", medium: "09⁻", beast: "10" }, artist: "Last Note." }, { musicId: 72, title: "0", version: 1, category: "EXITTUNES", bpm: { min: 168, max: 168 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "cosMo@暴走P" }, { musicId: 73, title: "いーあるふぁんくらぶ", version: 1, category: "EXITTUNES", bpm: { min: 145, max: 145 }, chartsInfo: { light: "02", medium: "04", beast: "09⁻" }, artist: "みきとP" }, { musicId: 74, title: "幸せになれる隠しコマンドがあるらしい", version: 1, category: "EXITTUNES", bpm: { min: 142, max: 142 }, chartsInfo: { light: "01", medium: "06", beast: "09⁺", nightmare: "09⁺" }, artist: "うたたP feat. 結月ゆかり" }, { musicId: 75, title: "Phantasm Brigade", version: 1, category: "TOHO", bpm: { min: 168, max: 168 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺" }, artist: "Silver Forest" }, { musicId: 76, title: "突撃!ガラスのニーソ姫!", version: 1, category: "KDE", bpm: { min: 185, max: 185 }, chartsInfo: { light: "03", medium: "08", beast: "10⁻" }, artist: "山本椛 (monotone)" }, { musicId: 77, title: "キャトられ♥恋はモ~モク", version: 1, category: "KDE", bpm: { min: 196, max: 196 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "ギュ~っとしたい♥Prim" }, { musicId: 78, title: "即席!脳直★ミュージックシステム", version: 1, category: "KDE", bpm: { min: 197, max: 197 }, chartsInfo: { light: "04", medium: "08", beast: "10" }, artist: "MOSAIC.WAV" }, { musicId: 79, title: "Synchrogazer", version: 1, category: "ANIME", bpm: { min: 159, max: 159 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻", nightmare: "10" }, artist: "水樹奈々" }, { musicId: 80, title: "家出少年と迷子少女", version: 1, category: "EXITTUNES", bpm: { min: 165, max: 165 }, chartsInfo: { light: "02", medium: "07", beast: "09⁻" }, artist: "cosMo@暴走P feat. GUMI" }, { musicId: 81, title: "白い雪のプリンセスは", version: 1, category: "EXITTUNES", bpm: { min: 200, max: 200 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻", nightmare: "09⁺" }, artist: "のぼる↑" }, { musicId: 82, title: "命のユースティティア", version: 1, category: "EXITTUNES", bpm: { min: 156, max: 156 }, chartsInfo: { light: "04", medium: "08", beast: "09⁺" }, artist: "Neru" }, { musicId: 83, title: "再教育", version: 1, category: "EXITTUNES", bpm: { min: 163, max: 163 }, chartsInfo: { light: "02", medium: "07", beast: "09⁻" }, artist: "Neru" }, { musicId: 84, title: "ドロボウナイトトリック", version: 1, category: "EXITTUNES", bpm: { min: 218, max: 218 }, chartsInfo: { light: "05", medium: "09⁻", beast: "09⁺" }, artist: "ゆちゃP" }, { musicId: 85, title: "茅蜩モラトリアム", version: 1, category: "EXITTUNES", bpm: { min: 189, max: 189 }, chartsInfo: { light: "04", medium: "09⁻", beast: "10" }, artist: "TOTAL OBJECTION feat.GUMI" }, { musicId: 86, title: "十面相", version: 1, category: "EXITTUNES", bpm: { min: 173, max: 173 }, chartsInfo: { light: "04", medium: "08", beast: "09⁺" }, artist: "YM feat. GUMI" }, { musicId: 87, title: "ハウトゥー世界征服", version: 1, category: "EXITTUNES", bpm: { min: 151, max: 151 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "Neru" }, { musicId: 88, title: "Just Be Friends", version: 1, category: "EXITTUNES", bpm: { min: 128, max: 128 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺" }, artist: "Dixie Flatline" }, { musicId: 89, title: "ハッタリだけで生きてる", version: 1, category: "EXITTUNES", bpm: { min: 145, max: 145 }, chartsInfo: { light: "04", medium: "07", beast: "09⁻" }, artist: "recog feat. デッドボールP" }, { musicId: 90, title: "ってゐ! ~えいえんてゐVer~", version: 1, category: "TOHO", bpm: { min: 314, max: 340 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "石鹸屋" }, { musicId: 91, title: "東方妖々夢 ~the maximum moving about~", version: 1, category: "TOHO", bpm: { min: 197, max: 205 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "石鹸屋" }, { musicId: 92, title: "最速最高シャッターガール", version: 1, category: "TOHO", bpm: { min: 160, max: 160 }, chartsInfo: { light: "03", medium: "08", beast: "09⁺" }, artist: "ビートまりお(COOL&CREATE)" }, { musicId: 93, title: "マスパでシュッ☆メイドウィッチまりさちゃん", version: 1, category: "TOHO", bpm: { min: 160, max: 160 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "ARM feat.桃井はるこ" }, { musicId: 94, title: "進捗どうですか?", version: 1, category: "TOHO", bpm: { min: 199, max: 199 }, chartsInfo: { light: "04", medium: "08", beast: "10", nightmare: "10" }, artist: "sumijun feat.ななひら" }, { musicId: 95, title: "トラウマ催眠少女さとり!", version: 1, category: "TOHO", bpm: { min: 210, max: 210 }, chartsInfo: { light: "03", medium: "08", beast: "10", nightmare: "10" }, artist: "DJ SHARPNEL feat.一ノ瀬月琉" }, { musicId: 96, title: "きゅうりバーにダイブ", version: 1, category: "TOHO", bpm: { min: 176, max: 176 }, chartsInfo: { light: "02", medium: "07", beast: "09⁺" }, artist: "ゆずひこ feat.めらみぽっぷ" }, { musicId: 97, title: "ジャスティス・オブ・ザ・界隈 ~ALL IS FAIR IN LOVE AND ALIMARI~", version: 1, category: "TOHO", bpm: { min: 190, max: 190 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺" }, artist: "void feat.山本椛" }, { musicId: 98, title: "EBONY & IVORY", version: 1, category: "KDE", bpm: { min: 170, max: 170 }, chartsInfo: { light: "02", medium: "08", beast: "10⁻", nightmare: "10⁺" }, artist: "OSTER project" }, { musicId: 99, title: "ツーマンライブ", version: 1, category: "KDE", bpm: { min: 174, max: 174 }, chartsInfo: { light: "02", medium: "07", beast: "09⁺", nightmare: "10-" }, artist: "東雲夏陽 meets 日向美ビタースイーツ♪" }, { musicId: 100, title: "キモチコネクト", version: 1, category: "KDE", bpm: { min: 150, max: 150 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻", nightmare: "09⁺" }, artist: "東雲心菜 meets 日向美ビタースイーツ♪" }, { musicId: 101, title: "パ→ピ→プ→Yeah!", version: 1, category: "KDE", bpm: { min: 160, max: 160 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻", nightmare: "10" }, artist: "ヒゲドライバー join. shully & Nimo" }, { musicId: 102, title: "惑星☆ロリポップ", version: 1, category: "KDE", bpm: { min: 180, max: 180 }, chartsInfo: { light: "02", medium: "07", beast: "09", nightmare: "10" }, artist: "SOUND HOLIC feat. Nana Takahashi" }, { musicId: 103, title: "爆なな☆てすとロイヤー", version: 1, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "03", medium: "08", beast: "10⁻", nightmare: "10" }, artist: "ARM feat.ななひら" }, { musicId: 104, title: "Stay Gold", version: 1, category: "OTHER", bpm: { min: 190, max: 190 }, chartsInfo: { light: "02", medium: "08", beast: "10", nightmare: "10" }, artist: "A応P" }, { musicId: 105, title: "メイビ~初恋!?ビスケット☆大作戦", version: 1, category: "KDE", bpm: { min: 182, max: 182 }, chartsInfo: { light: "02", medium: "06", beast: "09", nightmare: "10-" }, artist: "DJ TOTTO×mitsu feat.BisCo(CV:洲崎綾)" }, { musicId: 106, title: "それは花火のような恋", version: 1, category: "KDE", bpm: { min: 182, max: 182 }, chartsInfo: { light: "03", medium: "07", beast: "10", nightmare: "10" }, artist: "夏色バーニングラブ☆Prim" }, { musicId: 107, title: "NEON WORLD", version: 1, category: "TOHO", bpm: { min: 154, max: 164 }, chartsInfo: { light: "02", medium: "07", beast: "09⁺" }, artist: "SOUND HOLIC feat. Nana Takahashi" }, { musicId: 108, title: "Scarlet Moon", version: 1, category: "TOHO", bpm: { min: 180, max: 180 }, chartsInfo: { light: "02", medium: "04", beast: "09⁻" }, artist: "REDALiCE feat. Ayumi Nomiya" }, { musicId: 109, title: "Russian Caravan Rhapsody", version: 1, category: "TOHO", bpm: { min: 158, max: 158 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "Power Of Nature" }, { musicId: 110, title: "ほおずき程度には赤い頭髪", version: 1, category: "TOHO", bpm: { min: 174, max: 174 }, chartsInfo: { light: "04", medium: "07", beast: "10" }, artist: "Akhuta" }, { musicId: 111, title: "取り残された美術(Arranged:HiZuMi)", version: 1, category: "TOHO", bpm: { min: 160, max: 160 }, chartsInfo: { light: "02", medium: "06", beast: "09⁺" }, artist: "幽閉サテライト feat. senya" }, { musicId: 112, title: "Struggle", version: 1, category: "TOHO", bpm: { min: 180, max: 180 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "Masayoshi Minoshima(ALR)" }, { musicId: 113, title: "ホメ猫☆センセーション", version: 1, category: "TOHO", bpm: { min: 200, max: 200 }, chartsInfo: { light: "02", medium: "07", beast: "09⁺" }, artist: "P*Light feat. mow*2" }, { musicId: 114, title: "妖隠し -あやかしかくし-", version: 1, category: "TOHO", bpm: { min: 85, max: 85 }, chartsInfo: { light: "02", medium: "06", beast: "08" }, artist: "DJ TOTTO feat.3L" }, { musicId: 115, title: "BEAT-NEW-WORLD", version: 1, category: "TOHO", bpm: { min: 190, max: 190 }, chartsInfo: { light: "04", medium: "09⁻", beast: "10" }, artist: "ビートまりお(COOL&CREATE)" }, { musicId: 116, title: "プレインエイジア -PHQ remix-", version: 1, category: "TOHO", bpm: { min: 182, max: 182 }, chartsInfo: { light: "02", medium: "07", beast: "10⁻" }, artist: "PHQUASE" }, { musicId: 117, title: "竹取飛翔 ~ Lunatic Princess (Ryu☆Remix)", version: 1, category: "TOHO", bpm: { min: 171, max: 171 }, chartsInfo: { light: "02", medium: "06", beast: "09" }, artist: "Ryu☆" }, { musicId: 118, title: "千年ノ理", version: 1, category: "TOHO", bpm: { min: 153, max: 153 }, chartsInfo: { light: "02", medium: "07", beast: "09⁺" }, artist: "猫叉Master" }, { musicId: 119, title: "Flyers", version: 1, category: "ANIME", bpm: { min: 139, max: 139 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "BRADIO" }, { musicId: 120, title: "放課後革命", version: 1, category: "ANIME", bpm: { min: 205, max: 205 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "放課後楽園部《一宮エルナ(CV:木村珠莉)・御神楽星鎖(CV:大西沙織)・藤白おとね(CV:小澤亜李)》" }, { musicId: 121, title: "楽園ファンファーレ", version: 1, category: "ANIME", bpm: { min: 180, max: 180 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "放課後楽園部《一宮エルナ(CV:木村珠莉)・御神楽星鎖(CV:大西沙織)・藤白おとね(CV:小澤亜李)》" }, { musicId: 122, title: "FLOWER", version: 1, category: "KDE", bpm: { min: 173, max: 173 }, chartsInfo: { light: "04", medium: "09⁻", beast: "10⁺" }, artist: "DJ YOSHITAKA" }, { musicId: 123, title: "海神", version: 1, category: "KDE", bpm: { min: 159, max: 159 }, chartsInfo: { light: "04", medium: "09⁻", beast: "10⁺" }, artist: "兎々" }, { musicId: 124, title: "蛇神", version: 1, category: "KDE", bpm: { min: 170, max: 170 }, chartsInfo: { light: "04", medium: "08", beast: "10" }, artist: "Zektbach" }, { musicId: 125, title: "Ha・lle・lu・jah", version: 1, category: "KDE", bpm: { min: 150, max: 150 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "SOUND HOLIC feat. Nana Takahashi" }, { musicId: 126, title: "Element of SPADA", version: 1, category: "KDE", bpm: { min: 170, max: 170 }, chartsInfo: { light: "03", medium: "06", beast: "10" }, artist: "猫叉Master feat.霜月はるか" }, { musicId: 127, title: "ジュピターガンズノベル", version: 1, category: "KDE", bpm: { min: 156, max: 156 }, chartsInfo: { light: "04", medium: "08", beast: "10" }, artist: "工藤吉三(ベイシスケイプ)" }, { musicId: 128, title: "ヤマトなでなで♡かぐや姫", version: 1, category: "KDE", bpm: { min: 160, max: 160 }, chartsInfo: { light: "03", medium: "08", beast: "09⁺" }, artist: "ロマンチック♡Prim姫" }, { musicId: 129, title: "チョコレートスマイル", version: 1, category: "KDE", bpm: { min: 196, max: 196 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻", nightmare: "10-" }, artist: "日向美ビタースイーツ♪ & ここなつ" }, { musicId: 130, title: "さよならトリップ ~夏陽 EDM edition~", version: 1, category: "KDE", bpm: { min: 135, max: 135 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻", nightmare: "10-" }, artist: "東雲夏陽(from ここなつ)" }, { musicId: 131, title: "魔法のたまご ~心菜 ELECTRO POP edition~", version: 1, category: "KDE", bpm: { min: 142, max: 142 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺", nightmare: "10" }, artist: "東雲心菜(from ここなつ)" }, { musicId: 132, title: "怪盗BisCoの予告状!!", version: 1, category: "KDE", bpm: { min: 160, max: 160 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻", nightmare: "10" }, artist: "Qrispy Joybox" }, { musicId: 133, title: "リトライ☆ランデヴー", version: 1, category: "ANIME", bpm: { min: 175, max: 175 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "にゃ~たん(CV:村川梨衣)" }, { musicId: 134, title: "そるらる★とんちんかん", version: 1, category: "OTHER", bpm: { min: 195, max: 195 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "にゃ~たん(CV:村川梨衣)" }, { musicId: 135, title: "blue moment", version: 1, category: "ANIME", bpm: { min: 149, max: 149 }, chartsInfo: { light: "01", medium: "06", beast: "09⁻" }, artist: "ソルラルBOB" }, { musicId: 136, title: "ソルラルくれにゃ!", version: 1, category: "OTHER", bpm: { min: 185, max: 185 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "エトリオール(CV:村川梨衣、松井恵理子、花守ゆみり)" }, { musicId: 137, title: "In The Breeze", version: 1, category: "KDE", bpm: { min: 123, max: 142 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺" }, artist: "96 & Sota ft. Mayumi Morinaga" }, { musicId: 138, title: "びいすと!", version: 1, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "04", medium: "07", beast: "09", nightmare: "10" }, artist: "ビートまりおとARM" }, { musicId: 139, title: "放課後ストライド", version: 1, category: "OTHER", bpm: { min: 231, max: 231 }, chartsInfo: { light: "04", medium: "07", beast: "10" }, artist: "一宮エルナ(CV.木村珠莉)" }, { musicId: 140, title: "革新的ヒロイズム", version: 1, category: "OTHER", bpm: { min: 153, max: 153 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺" }, artist: "二宮シグレ(CV.島﨑信長)" }, { musicId: 141, title: "有頂天ビバーチェ", version: 1, category: "OTHER", bpm: { min: 133, max: 133 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻" }, artist: "八坂ひみ(CV.諏訪彩花)" }, { musicId: 142, title: "十六夜シーイング", version: 1, category: "OTHER", bpm: { min: 174, max: 174 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺" }, artist: "射水アスヒ(CV.加隈亜衣)" }, { musicId: 143, title: "我楽多イノセンス", version: 1, category: "OTHER", bpm: { min: 230, max: 230 }, chartsInfo: { light: "04", medium: "08", beast: "09⁺" }, artist: "九頭竜京摩(CV.細谷佳正)" }, { musicId: 144, title: "無気力クーデター", version: 1, category: "OTHER", bpm: { min: 200, max: 200 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "赤間遊兎(CV.花江夏樹)" }, { musicId: 145, title: "花吹雪リフレクト", version: 1, category: "OTHER", bpm: { min: 118, max: 118 }, chartsInfo: { light: "02", medium: "05", beast: "08" }, artist: "湊川貞松(CV.古川慎)" }, { musicId: 146, title: "絵空事スパイラル", version: 1, category: "OTHER", bpm: { min: 197, max: 197 }, chartsInfo: { light: "05", medium: "08", beast: "09⁺" }, artist: "御神楽星鎖(CV.大西沙織)" }, { musicId: 147, title: "赤裸々キャンディ", version: 1, category: "OTHER", bpm: { min: 193, max: 193 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "藤白おとね(CV.小澤亜李)" }, { musicId: 148, title: "不条理ルーレット", version: 1, category: "OTHER", bpm: { min: 175, max: 175 }, chartsInfo: { light: "04", medium: "07", beast: "09⁺" }, artist: "ビミィ(CV.松岡禎丞)" }, { musicId: 149, title: "激メシ!!わがにゃの晩ごはん", version: 1, category: "OTHER", bpm: { min: 170, max: 170 }, chartsInfo: { light: "02", medium: "07", beast: "09⁻" }, artist: "にゃ~たん(CV:村川梨衣)、モ~たん(CV:松井恵理子)、ピヨたん(CV:佐々木未来)" }, { musicId: 150, title: "これがにゃあの生きる道", version: 1, category: "OTHER", bpm: { min: 190, max: 190 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "にゃ~たん(CV:村川梨衣)" }, { musicId: 151, title: "ラヴ・グラビティ", version: 1, category: "OTHER", bpm: { min: 180, max: 180 }, chartsInfo: { light: "02", medium: "07", beast: "09⁻" }, artist: "モ~たん(CV:松井恵理子)" }, { musicId: 152, title: "忘却の旅路", version: 1, category: "OTHER", bpm: { min: 110, max: 110 }, chartsInfo: { light: "01", medium: "06", beast: "09⁻" }, artist: "ピヨたん(CV:佐々木未来)" }, { musicId: 153, title: "ETMファイティングクライマックス! 本気の師匠チャレンジ編", version: 1, category: "OTHER", bpm: { min: 136, max: 136 }, chartsInfo: { light: "03", medium: "06", beast: "08" }, artist: "シマたん(CV:巽悠衣子)、ドラたん(CV:内田真礼)、ウリたん(CV:花守ゆみり)" }, { musicId: 154, title: "TRY! TRY! TRY!", version: 1, category: "OTHER", bpm: { min: 162, max: 162 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "シマたん(CV:巽悠衣子)" }, { musicId: 155, title: "さてこそ桃源郷", version: 1, category: "OTHER", bpm: { min: 88, max: 140 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "ドラたん(CV:内田真礼)" }, { musicId: 156, title: "激凸!?ウリティ☆ウリリズム", version: 1, category: "OTHER", bpm: { min: 216, max: 216 }, chartsInfo: { light: "04", medium: "09⁻", beast: "10" }, artist: "ウリたん(CV:花守ゆみり)" }, { musicId: 157, title: "最強プロデュース!めざせ干支ップ☆アイドル", version: 1, category: "OTHER", bpm: { min: 70, max: 200 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "ウサたん(CV:相坂優歌)、ウマたん(CV:小澤亜李)、キーたん(CV: 戸田めぐみ)、イヌたん(CV:本多真梨子)" }, { musicId: 158, title: "がんばれ♥ ウサ☆ウサPPプロデュース!", version: 1, category: "OTHER", bpm: { min: 186, max: 186 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻" }, artist: "ウサたん(CV:相坂優歌)" }, { musicId: 159, title: "笑う門に午来る!", version: 1, category: "OTHER", bpm: { min: 172, max: 172 }, chartsInfo: { light: "02", medium: "04", beast: "08" }, artist: "ウマたん(CV:小澤亜李)" }, { musicId: 160, title: "ふたりマイウェイ!", version: 1, category: "OTHER", bpm: { min: 240, max: 240 }, chartsInfo: { light: "04", medium: "07", beast: "09⁺" }, artist: "キーたん(CV:戸田めぐみ)、イヌたん(CV:本多真梨子)" }, { musicId: 161, title: "秘湯に願いを!今夜はホット・アンド・スイート", version: 1, category: "OTHER", bpm: { min: 128, max: 128 }, chartsInfo: { light: "04", medium: "08", beast: "09⁺" }, artist: "チュウたん(CV:大原さやか)、シャアたん(CV:生天目仁美)、メイたん(CV:渕上舞)" }, { musicId: 162, title: "窮猫ハ鼠ヲモ嚙メズ", version: 1, category: "OTHER", bpm: { min: 180, max: 180 }, chartsInfo: { light: "04", medium: "09⁻", beast: "10" }, artist: "チュウたん(CV:大原さやか)" }, { musicId: 163, title: "あなたの総集編", version: 1, category: "OTHER", bpm: { min: 160, max: 160 }, chartsInfo: { light: "02", medium: "06", beast: "08" }, artist: "シャアたん(CV:生天目仁美)" }, { musicId: 164, title: "TwinklePeace", version: 1, category: "OTHER", bpm: { min: 128, max: 128 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "メイたん(CV:渕上舞)" }, { musicId: 165, title: "隅田川夏恋歌", version: 1, category: "KDE", bpm: { min: 180, max: 180 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺", nightmare: "10" }, artist: "seiya-murai feat.ALT" }, { musicId: 166, title: "Sakura Sunrise", version: 1, category: "KDE", bpm: { min: 181, max: 181 }, chartsInfo: { light: "04", medium: "08", beast: "10⁻" }, artist: "Ryu☆" }, { musicId: 167, title: "虹色遊園地", version: 1, category: "KDE", bpm: { min: 165, max: 220 }, chartsInfo: { light: "03", medium: "09⁻", beast: "10" }, artist: "Mutsuhiko Izumi VS DJ TOTTO" }, { musicId: 168, title: "ドッキン☆サマーあばんちゅーる", version: 1, category: "KDE", bpm: { min: 180, max: 180 }, chartsInfo: { light: "04", medium: "07", beast: "10⁻" }, artist: "L.E.D.-G ⁺ Qrispy Joybox ⁺ ARM feat. ななひら" }, { musicId: 169, title: "夏色DIARY BisCoの思い出(>▽<)", version: 1, category: "KDE", bpm: { min: 187, max: 187 }, chartsInfo: { light: "03", medium: "07", beast: "09" }, artist: "猫叉王子 feat.Qrispy Joybox" }, { musicId: 170, title: "エクストラ・マジック・アワー ", version: 1, category: "ANIME", bpm: { min: 180, max: 180 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻" }, artist: "AKINO with bless4" }, { musicId: 171, title: "SPLASH FREE", version: 1, category: "ANIME", bpm: { min: 128, max: 128 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "STYLE FIVE" }, { musicId: 172, title: "MURASAKI", version: 1, category: "OTHER", bpm: { min: 145, max: 145 }, chartsInfo: { light: "03", medium: "06", beast: "08" }, artist: "蒼井翔太" }, { musicId: 173, title: "はなまるぴっぴはよいこだけ", version: 1, category: "ANIME", bpm: { min: 172, max: 172 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺", nightmare: "10-" }, artist: "A応P" }, { musicId: 174, title: "neko*neko", version: 1, category: "KDE", bpm: { min: 123, max: 123 }, chartsInfo: { light: "03", medium: "07", beast: "09" }, artist: "日向美ビタースイーツ♪" }, { musicId: 175, title: "漆黒のスペシャルプリンセスサンデー", version: 1, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "日向美ビタースイーツ♪" }, { musicId: 176, title: "残酷な天使のテーゼ", version: 2, category: "ANIME", bpm: { min: 79, max: 128 }, chartsInfo: { light: "02", medium: "06", beast: "08" }, artist: "-" }, { musicId: 177, title: "紅蓮の弓矢", version: 2, category: "ANIME", bpm: { min: 180.66, max: 180.66 }, chartsInfo: { light: "01", medium: "06", beast: "09⁻", nightmare: "10-" }, artist: "Linked Horizon" }, { musicId: 178, title: "太陽曰く燃えよカオス", version: 2, category: "ANIME", bpm: { min: 143, max: 143 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻", nightmare: "10" }, artist: "後ろから這いより隊G" }, { musicId: 179, title: "Enigmatic Feeling", version: 2, category: "ANIME", bpm: { min: 142, max: 142 }, chartsInfo: { light: "02", medium: "06", beast: "09⁺" }, artist: "凛として時雨" }, { musicId: 180, title: "シュガーソングとビターステップ", version: 2, category: "ANIME", bpm: { min: 132, max: 132 }, chartsInfo: { light: "02", medium: "07", beast: "09⁺", nightmare: "10" }, artist: "UNISON SQUARE GARDEN" }, { musicId: 181, title: "COLORFUL BOX ", version: 2, category: "ANIME", bpm: { min: 161, max: 161 }, chartsInfo: { light: "03", medium: "07", beast: "09⁻" }, artist: "石田燿子" }, { musicId: 182, title: "宝箱-TREASURE BOX-", version: 2, category: "ANIME", bpm: { min: 162, max: 162 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "奥井雅美" }, { musicId: 183, title: "かくしん的☆めたまるふぉ〜ぜっ!", version: 2, category: "ANIME", bpm: { min: 133, max: 139 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺", nightmare: "10" }, artist: "土間うまる(CV:田中あいみ)" }, { musicId: 184, title: "Twinbee's Home Town Song", version: 2, category: "KDE", bpm: { min: 168, max: 168 }, chartsInfo: { light: "04", medium: "07", beast: "09⁺" }, artist: "Sota Fujimori" }, { musicId: 185, title: "トキメキストリーム", version: 2, category: "KDE", bpm: { min: 164, max: 164 }, chartsInfo: { light: "02", medium: "07", beast: "10", nightmare: "10⁺" }, artist: "Qrispy Joybox" }, { musicId: 186, title: "にゃんのパレードマーチ♪", version: 2, category: "KDE", bpm: { min: 162, max: 162 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "DJ TOTTO feat.にゃん" }, { musicId: 187, title: "アキネイション", version: 2, category: "KDE", bpm: { min: 185, max: 185 }, chartsInfo: { light: "04", medium: "08", beast: "10" }, artist: "ビートまりお(COOL&CREATE)" }, { musicId: 188, title: "秘密がーる♡乙女", version: 2, category: "KDE", bpm: { min: 180, max: 180 }, chartsInfo: { light: "03", medium: "07", beast: "09" }, artist: "MaMiKa(松下×mitsu×T.kakuta)" }, { musicId: 189, title: "ぱんだしんけん1、2、3 ~ちえ!おっしょさんにはかなわないや!~", version: 2, category: "KDE", bpm: { min: 158, max: 158 }, chartsInfo: { light: "03", medium: "08", beast: "09⁺" }, artist: "おひさまくらぶ" }, { musicId: 190, title: "蟲の棲む処", version: 2, category: "KDE", bpm: { min: 161.51, max: 161.51 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "かめりあ feat. Nana Takahashi" }, { musicId: 191, title: "激アツ☆マジヤバ☆チアガール", version: 2, category: "KDE", bpm: { min: 170, max: 170 }, chartsInfo: { light: "03", medium: "07", beast: "08" }, artist: "日向美ビタースイーツ♪" }, { musicId: 192, title: "打打打打打打打打打打", version: 2, category: "KDE", bpm: { min: 156, max: 156 }, chartsInfo: { light: "03", medium: "05", beast: "10⁻", nightmare: "10" }, artist: "ヒゲドライバー join. SELEN" }, { musicId: 193, title: "創聖のアクエリオン", version: 2, category: "ANIME", bpm: { min: 151, max: 151 }, chartsInfo: { light: "01", medium: "05", beast: "08" }, artist: "-" }, { musicId: 194, title: "RPG", version: 2, category: "ANIME", bpm: { min: 135, max: 135 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "SEKAI NO OWARI" }, { musicId: 195, title: "患部で止まってすぐ溶ける ~ 狂気の優曇華院", version: 2, category: "TOHO", bpm: { min: 200, max: 200 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻" }, artist: "ARM(IOSYS)" }, { musicId: 196, title: "Grip & Break down !!", version: 2, category: "TOHO", bpm: { min: 160, max: 160 }, chartsInfo: { light: "04", medium: "07", beast: "10⁻", nightmare: "10" }, artist: "SOUND HOLIC feat. Nana Takahashi" }, { musicId: 197, title: "究極焼肉レストラン!お燐の地獄亭!", version: 2, category: "TOHO", bpm: { min: 185, max: 185 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "ARM(IOSYS)" }, { musicId: 198, title: "楽しい夜のお茶会 - Ringo's Tea Party", version: 2, category: "TOHO", bpm: { min: 175, max: 175 }, chartsInfo: { light: "03", medium: "05", beast: "09⁻" }, artist: "ARM(IOSYS)" }, { musicId: 199, title: "幻想のサテライト", version: 2, category: "TOHO", bpm: { min: 230, max: 230 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "豚乙女" }, { musicId: 200, title: "Wheel", version: 2, category: "TOHO", bpm: { min: 136, max: 136 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "Syrufit feat.綾倉盟 / 市松椿" }, { musicId: 201, title: "Vampire Killer", version: 2, category: "KDE", bpm: { min: 128, max: 128 }, chartsInfo: { light: "03", medium: "08", beast: "10" }, artist: "浅田 靖" }, { musicId: 202, title: "GRADIUS 2012", version: 2, category: "KDE", bpm: { min: 180, max: 180 }, chartsInfo: { light: "05", medium: "09⁻", beast: "10" }, artist: "Sota Fujimori" }, { musicId: 203, title: "月風魔伝 ~ ビーストメドレー ~", version: 2, category: "KDE", bpm: { min: 170, max: 170 }, chartsInfo: { light: "04", medium: "09⁻", beast: "10" }, artist: "Akhuta" }, { musicId: 204, title: "がんばれゴエモン ~ ビーストメドレー ~", version: 2, category: "KDE", bpm: { min: 113, max: 190 }, chartsInfo: { light: "02", medium: "06", beast: "09⁻" }, artist: "96" }, { musicId: 205, title: "分けるな危険!モモモモモモーイズム", version: 2, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "04", medium: "08", beast: "10⁻" }, artist: "ARM×狐夢想 feat. 桃井はるこ" }, { musicId: 206, title: "CARNIVOROUS", version: 2, category: "KDE", bpm: { min: 170, max: 170 }, chartsInfo: { light: "05", medium: "09⁻", beast: "10" }, artist: "SOUND HOLIC feat. Nana Takahashi" }, { musicId: 207, title: "地方創生☆チクワクティクス", version: 2, category: "KDE", bpm: { min: 170, max: 170 }, chartsInfo: { light: "04", medium: "07", beast: "10" }, artist: "日向美ビタースイーツ♪" }, { musicId: 208, title: "リリーゼと炎龍レーヴァテイン", version: 2, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "05", medium: "08", beast: "10", nightmare: "10⁺" }, artist: "黒猫ダンジョン" }, { musicId: 209, title: "smooooch・∀・", version: 2, category: "KDE", bpm: { min: 177, max: 177 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "kors k" }, { musicId: 210, title: "Twinkle Wonderland", version: 2, category: "KDE", bpm: { min: 188, max: 188 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻" }, artist: "Qrispy Joybox feat.Sana " }, { musicId: 211, title: "She is my wife", version: 2, category: "KDE", bpm: { min: 85, max: 170 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "SUPER STAR 満-MITSURU-" }, { musicId: 212, title: "ほしふり", version: 2, category: "KDE", bpm: { min: 155, max: 155 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "Sana" }, { musicId: 213, title: "梅雪夜", version: 2, category: "KDE", bpm: { min: 167, max: 167 }, chartsInfo: { light: "04", medium: "07", beast: "10", nightmare: "10⁺" }, artist: "Qrispy Joybox feat.mao" }, { musicId: 214, title: "絶品☆みこみこミラクル昇天レシピ!", version: 2, category: "OTHER", bpm: { min: 204, max: 204 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "MOSAIC.WAV & miko(Alternative ending)" }, { musicId: 215, title: "GRADIUS Ⅱ ~ ビーストメドレー ~", version: 2, category: "KDE", bpm: { min: 190, max: 190 }, chartsInfo: { light: "04", medium: "08", beast: "10" }, artist: "Sota Fujimori" }, { musicId: 216, title: "キミとワタシのオンガク", version: 2, category: "KDE", bpm: { min: 160, max: 160 }, chartsInfo: { light: "02", medium: "06", beast: "10⁻" }, artist: "seiya-murai feat. ALT" }, { musicId: 217, title: "量子の海のリントヴルム", version: 2, category: "KDE", bpm: { min: 280, max: 280 }, chartsInfo: { light: "05", medium: "09", beast: "10", nightmare: "10⁺" }, artist: "黒猫ダンジョン" }, { musicId: 218, title: "混ぜるな危険", version: 2, category: "ANIME", bpm: { min: 178, max: 178 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "筋肉少女帯" }, { musicId: 219, title: "Sky High", version: 2, category: "KDE", bpm: { min: 153, max: 153 }, chartsInfo: { light: "05", medium: "09⁺", beast: "10⁺", nightmare: "ネ申" }, artist: "Cuvelia" }, { musicId: 220, title: "SPACE VILLAGE", version: 2, category: "KDE", bpm: { min: 170, max: 170 }, chartsInfo: { light: "02", medium: "06", beast: "10⁻" }, artist: "サイバー劇レコ" }, { musicId: 221, title: "全力バタンキュー", version: 2, category: "ANIME", bpm: { min: 155, max: 155 }, chartsInfo: { light: "03", medium: "06", beast: "09" }, artist: "A応P" }, { musicId: 222, title: "きゅん×きゅんばっきゅん☆LOVE", version: 2, category: "KDE", bpm: { min: 165, max: 165 }, chartsInfo: { light: "03", medium: "06", beast: "09" }, artist: "松下feat.Sota & wac" }, { musicId: 223, title: "CHERNOBOG", version: 2, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "05", medium: "09", beast: "10⁺", nightmare: "ネ申" }, artist: "漆黒のEBONY" }, { musicId: 224, title: "spring pony", version: 2, category: "KDE", bpm: { min: 150, max: 150 }, chartsInfo: { light: "02", medium: "06", beast: "09" }, artist: "S-C-U" }, { musicId: 225, title: "繚乱ヒットチャート", version: 2, category: "KDE", bpm: { min: 192, max: 192 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻" }, artist: "ギラギラメガネ団" }, { musicId: 226, title: "Spring Comes Around (In Like a Lion)", version: 2, category: "KDE", bpm: { min: 159.5, max: 159.5 }, chartsInfo: { light: "03", medium: "07", beast: "09" }, artist: "Sota Fujimori Rmx by wac" }, { musicId: 227, title: "アルストロメリア", version: 2, category: "KDE", bpm: { min: 144, max: 144 }, chartsInfo: { light: "04", medium: "08", beast: "10" }, artist: "TAG" }, { musicId: 228, title: "Sakura Reflection", version: 2, category: "KDE", bpm: { min: 181, max: 181 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "Ryu☆" }, { musicId: 229, title: "黒髪乱れし修羅となりて~凛 edition~", version: 2, category: "KDE", bpm: { min: 300, max: 300 }, chartsInfo: { light: "04", medium: "06", beast: "09⁺", nightmare: "10" }, artist: "日向美ビタースイーツ♪" }, { musicId: 230, title: "Clattanoia", version: 2, category: "ANIME", bpm: { min: 190, max: 190 }, chartsInfo: { light: "03", medium: "07", beast: "09" }, artist: "OxT" }, { musicId: 231, title: "クローバー♣かくめーしょん", version: 2, category: "ANIME", bpm: { min: 145, max: 145 }, chartsInfo: { light: "02", medium: "05", beast: "08" }, artist: "とりぷる♣ふぃーりんぐ(和久井 優/金澤まい/今村彩夏)" }, { musicId: 232, title: "Knew day", version: 2, category: "ANIME", bpm: { min: 150, max: 150 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "(K)NoW_NAME" }, { musicId: 233, title: "からくりピエロ", version: 2, category: "EXITTUNES", bpm: { min: 102, max: 102 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "40mP" }, { musicId: 234, title: "華鳥風月", version: 2, category: "TOHO", bpm: { min: 123, max: 123 }, chartsInfo: { light: "02", medium: "05", beast: "09⁻" }, artist: "幽閉サテライト" }, { musicId: 235, title: "人間が大好きなこわれた妖怪の唄", version: 2, category: "TOHO", bpm: { min: 166, max: 166 }, chartsInfo: { light: "03", medium: "06", beast: "09⁻" }, artist: "ビートまりお(COOL&CREATE)" }, { musicId: 236, title: "忌憚騒命曲 ~ Demon tale sonata", version: 2, category: "TOHO", bpm: { min: 185, max: 185 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "どぶウサギ(dBu music)" }, { musicId: 237, title: "キャプテン・ムラサのケツアンカー", version: 2, category: "TOHO", bpm: { min: 190, max: 190 }, chartsInfo: { light: "04", medium: "07", beast: "09⁺" }, artist: "ARM⁺夕野ヨシミ feat.山本椛" }, { musicId: 238, title: "Power of Battle(vs REDALiCE)", version: 2, category: "TOHO", bpm: { min: 178, max: 178 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺" }, artist: "RoughSketch vs REDALiCE feat.イザベル" }, { musicId: 239, title: "鈴瑚のお団子ジャングルジム", version: 2, category: "TOHO", bpm: { min: 190, max: 190 }, chartsInfo: { light: "03", medium: "07", beast: "09" }, artist: "ARM⁺狐夢想 feat.ななひら" }, { musicId: 240, title: "wandering to sweet", version: 2, category: "TOHO", bpm: { min: 154, max: 154 }, chartsInfo: { light: "03", medium: "05", beast: "09⁻" }, artist: "平茸⁺夕野ヨシミ feat.山本椛" }, { musicId: 241, title: "Lost wing at.0", version: 2, category: "KDE", bpm: { min: 185, max: 185 }, chartsInfo: { light: "04", medium: "07", beast: "10" }, artist: "猫叉Master⁺" }, { musicId: 242, title: "ロプノールの商隊", version: 2, category: "KDE", bpm: { min: 186, max: 186 }, chartsInfo: { light: "02", medium: "06", beast: "10" }, artist: "Akhuta" }, { musicId: 243, title: "Phlox", version: 2, category: "KDE", bpm: { min: 185, max: 185 }, chartsInfo: { light: "04", medium: "08", beast: "10⁻" }, artist: "Sota Fujimori 2nd Season" }, { musicId: 244, title: "Dynasty", version: 2, category: "KDE", bpm: { min: 189, max: 189 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "Yooh" }, { musicId: 245, title: "じゅーじゅー♥焼肉の火からフェニックス!?~再誕の†炭火焼き~", version: 2, category: "KDE", bpm: { min: 180, max: 180 }, chartsInfo: { light: "04", medium: "09⁻", beast: "10" }, artist: "かめりあ feat. ななひら" }, { musicId: 246, title: "雪女", version: 2, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "02", medium: "07", beast: "10" }, artist: "かねこちはる" }, { musicId: 247, title: "フラッター現象の顛末と単一指向性の感情論", version: 2, category: "KDE", bpm: { min: 206, max: 206 }, chartsInfo: { light: "02", medium: "06", beast: "09" }, artist: "日向美ビタースイーツ♪" }, { musicId: 248, title: "Good bye, Summer~さよならは言わない~", version: 2, category: "KDE", bpm: { min: 165, max: 165 }, chartsInfo: { light: "03", medium: "06", beast: "09" }, artist: "私立BEMANI学園軽音部 OB" }, { musicId: 249, title: "スカイダイバー", version: 2, category: "KDE", bpm: { min: 180, max: 180 }, chartsInfo: { light: "05", medium: "07", beast: "09" }, artist: "ヒゲドライバー feat.ヒゲドライVAN" }, { musicId: 250, title: "週替わりの奇跡の神話", version: 2, category: "ANIME", bpm: { min: 169, max: 169 }, chartsInfo: { light: "02", medium: "06", beast: "09" }, artist: "筋肉少女帯" }, { musicId: 251, title: "GO! GO! MANIAC", version: 2, category: "ANIME", bpm: { min: 250, max: 250 }, chartsInfo: { light: "04", medium: "08", beast: "09⁺" }, artist: "放課後ティータイム" }, { musicId: 252, title: "最高速 Fall in Love", version: 2, category: "ANIME", bpm: { min: 132, max: 175 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻" }, artist: "ミーア、パピ、セントレア、スー、メロ、ラクネラ" }, { musicId: 253, title: "ハッピーシンセサイザ", version: 2, category: "OTHER", bpm: { min: 127, max: 127 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "EasyPop" }, { musicId: 254, title: "東京テディベア", version: 2, category: "OTHER", bpm: { min: 204, max: 204 }, chartsInfo: { light: "03", medium: "07", beast: "09⁺" }, artist: "Neru" }, { musicId: 255, title: "ケンぱ!ケンぱ!拳拳ぱん打!", version: 2, category: "KDE", bpm: { min: 200, max: 200 }, chartsInfo: { light: "03", medium: "06", beast: "10⁻" }, artist: "ぱんぱんぱん打" }, { musicId: 256, title: "Lost wing at.0", version: 2, category: "KDE", bpm: { min: 185, max: 185 }, chartsInfo: { light: "04", medium: "07", beast: "10" }, artist: "猫叉Master⁺" }, { musicId: 257, title: "朧", version: 2, category: "KDE", bpm: { min: 149, max: 149 }, chartsInfo: { light: "03", medium: "07", beast: "10" }, artist: "HHH×MM×ST" }, { musicId: 258, title: "恋はどう?モロ◎波動OK☆方程式!!", version: 2, category: "KDE", bpm: { min: 128, max: 128 }, chartsInfo: { light: "05", medium: "09⁻", beast: "10" }, artist: "あべにゅうぷろじぇくと feat.佐倉紗織 produced by ave;new" }, { musicId: 259, title: "恋歌疾風!かるたクイーンいろは", version: 2, category: "KDE", bpm: { min: 120, max: 168 }, chartsInfo: { light: "04", medium: "09", beast: "10⁺" }, artist: "ねこまんまチーム!" }, { musicId: 260, title: "SAKURAスキップ", version: 2, category: "ANIME", bpm: { min: 165, max: 165 }, chartsInfo: { light: "03", medium: "06", beast: "09" }, artist: "fourfolium" }, { musicId: 261, title: "Now Loading!!!!", version: 2, category: "ANIME", bpm: { min: 193, max: 193 }, chartsInfo: { light: "03", medium: "05", beast: "09⁻" }, artist: "fourfolium" }, { musicId: 262, title: "カケラ", version: 2, category: "KDE", bpm: { min: 182, max: 182 }, chartsInfo: { light: "05", medium: "07", beast: "09⁺" }, artist: "豚乙女" }, { musicId: 263, title: "怒りと共に去りぬ!!", version: 2, category: "KDE", bpm: { min: 190, max: 190 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻" }, artist: "96 feat.すわひでお" }, { musicId: 264, title: "在るが儘に", version: 2, category: "KDE", bpm: { min: 181, max: 181 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺" }, artist: "Des-ROW・組スペシアルr" }, { musicId: 265, title: "Daisuke", version: 2, category: "KDE", bpm: { min: 157, max: 157 }, chartsInfo: { light: "04", medium: "07", beast: "10" }, artist: "Y&Co." }, { musicId: 266, title: "JOMANDA", version: 2, category: "KDE", bpm: { min: 90, max: 300 }, chartsInfo: { light: "05", medium: "09", beast: "10⁺" }, artist: "DJ YOSHITAKA" }, { musicId: 267, title: "サヨナラ・ヘヴン", version: 2, category: "KDE", bpm: { min: 111, max: 111 }, chartsInfo: { light: "02", medium: "06", beast: "09⁺" }, artist: "猫叉Master" }, { musicId: 268, title: "轟け!恋のビーンボール!!", version: 2, category: "KDE", bpm: { min: 180, max: 180 }, chartsInfo: { light: "03", medium: "07", beast: "10⁻", nightmare: "10⁺" }, artist: "ダイナミック野球兄弟 v.s. クロスファイヤーPrim" }, { musicId: 269, title: "少年リップルズ", version: 2, category: "KDE", bpm: { min: 210, max: 210 }, chartsInfo: { light: "04", medium: "07", beast: "10⁻" }, artist: "常盤ゆう" }, { musicId: 270, title: "たまゆら", version: 2, category: "KDE", bpm: { min: 180, max: 180 }, chartsInfo: { light: "04", medium: "06", beast: "09⁺" }, artist: "佐々木博史" }, { musicId: 271, title: "quaver♪", version: 2, category: "KDE", bpm: { min: 182, max: 186 }, chartsInfo: { light: "05", medium: "08", beast: "10⁺" }, artist: "Risk Junk" }, { musicId: 272, title: "SigSig", version: 2, category: "KDE", bpm: { min: 179, max: 179 }, chartsInfo: { light: "03", medium: "06", beast: "09⁺" }, artist: "kors k" }] + let courseTitle = ["RANK 01","RANK 02","RANK 03","RANK 04","RANK 05","RANK 06","RANK 07","RANK 08","RANK 09","RANK 10","RANK 11","RANK 12","RANK 13","RANK 14","RANK 15","RANK ネ申"] + let bstChartTypeDetails = [{title:"Light",color:"#2EBECC",colorInvert:"#2EBECC"},{title:"Medium",color:"#F2C40B",colorInvert:"#F2C40B"},{title:"Beast",color:"#FF3860",colorInvert:"#FF3860"},{title:"Nightmare",color:"#000000",colorInvert:"#00DDDD"},] + + let defaultPageSize = 20 + +//- Processing +- + bst2Course.sort((l, r) => r.courseId - l.courseId) + bst2StageLog.sort((l, r) => r.time - l.time) + if (bst2Account != null) { + let checkUserId = (col) => { + let removeList = [] + for (i = 0; i < col.length; i++) if (col[i].userId != bst2Account.userId) removeList.push(i) + for (let i of removeList) col.splice(i, 1) + } + checkUserId(bst2Course) + checkUserId(bst2MusicRecord) + checkUserId(bst2CourseLog) + checkUserId(bst2StageLog) + } + + let bst2MusicRecordOrganized = [] + for (let r of bst2MusicRecord) { + let o = bst2MusicRecordOrganized[r.musicId] + if (o == null) o = {} + o[r.chart] = r + bst2MusicRecordOrganized[r.musicId] = o + } + +//- Play data +- + let bst2HighestRank = -1 + for (let r of bst2Course) if ((r.courseId > bst2HighestRank) && (r.medal >= 3)) bst2HighestRank = r.courseId + function getClearTypeDetails(record) { + if (record.medal == 5) return { title: "Perfect", abbr: "P", color: "#FF3860", badge: "mdi mdi-weather-sunny mdi-48px" } + else if (record.medal == 4) return { title: "Full Combo", abbr: "FC", color: "gold", badge: "mdi mdi-cog mdi-48px" } + else if (record.medal == 3) return { title: "Clear", abbr: "C", color: "#35F2BE", badge: "mdi mdi-star-circle-outline mdi-48px"} + else if ((record.medal == 2) || (record.medal == 1)) return { title: "Failed", abbr: "F", color: "#837E8D", badge: "mdi mdi-close-circle-outline mdi-48px"} + else return null + } + function getRank(score, version) { + if ((score >= 970000) && (version == 2)) return { title: "AAA", color: "#FF3860", colorInvert: "#FF3860" } + else if (score >= 950000) return { title: "AAA", color: "black", colorInvert: "gold" } + else if (score >= 850000) return { title: "AA", color: "black", colorInvert: "gold" } + else if (score >= 700000) return { title: "A", color: "black", colorInvert: "gold" } + else if (score >= 600000) return { title: "B", color: "black", colorInvert: "#40FEAA" } + else if (score >= 500000) return { title: "C", color: "black", colorInvert: "#40FEAA" } + else return { title: "D", color: "black", colorInvert: "#837E8D" } + } + +link(rel="stylesheet", href="static/css/webui_util.css", type="text/css") +script(src = "static/js/webui_util.js") +.modal.loading.is-active + .modal-background(style = "opacity: 50%") + p(style = "position: fixed; bottom: 40px; right: 40px; color: white") + span Now Loading... + i.mdi.mdi-refresh-circle.mdi-spin +div + if (webuiMessage != null) && ((webuiMessage.refid == null) || (webuiMessage.refid == refid)) + div(class = "notification temporary" + ((webuiMessage.type == 0) ? " is-info" : (webuiMessage.type == 1) ? " is-success" : " is-danger"), style = "display: block") + .delete(style = "margin-top: 10px") + span #{webuiMessage.message} + form.start(method="post" action="/emit/removeWebUIMessage", style = "display: none") + .columns.is-mobile.is-vcentered(style = "overflow-x: auto") + .column + .column.is-narrow + p.is-size-7 Select game version + .column.is-narrow + style. + .tabs.is-toggle li a, .tabs.is-toggle li a span, .tabs.is-toggle li a span span, .tabs.is-toggle li a div, .tabs.is-toggle li a .background { + transition: .2s linear, font-weight 0s linear; + } + .tabs.is-toggle ul li.is-active[tab-group="version"][tab-index="2"] a { + background-color: black; + color: white; + font-weight: bold; + text-shadow: 0 1.5px 0 #D0D09A; + border-color: black; + } + .tabs.is-toggle ul li.is-active[tab-group="version"][tab-index="2"] a span { + transform: skewX(-12deg); + } + .tabs.is-toggle.is-toggle-rounded.is-small#tabs + ul(style = "margin: 0") + li.disabled(tab-group = "version", tab-index = "1", style = "width: 112px", title = "Coming s∞n...") + a BeatStream + li(tab-group = "version", tab-index = "2", style = "width: 112px", class = (defaultVersion == 2) ? "is-active" : "") + a + span アニムトライヴ + .card + header.card-header + p.card-header-title + span.icon + i.mdi.mdi-account-circle + | Play Data + a.card-header-icon.card-toggle(card = "play-data") + span.icon + i.mdi.mdi-chevron-down + .card-content(card = "play-data") + #tab-content(tab-group = "version", tab-index = "2", class = (defaultVersion == 2) ? "is-active" : "") + if bst2Account == null + .has-text-grey.has-text-centered + p + span.icon.is-large + i.mdi.mdi-emoticon-happy.mdi-48px + p Nothing's here yet. Let's play! + else + .tabs#tabs + ul + li.is-active(tab-group = "bst2-play-data", tab-index = "0") + a Overview + li(tab-group = "bst2-play-data", tab-index = "1") + a Scores + li(tab-group = "bst2-play-data", tab-index = "2") + a Course + li(tab-group = "bst2-play-data", tab-index = "3") + a Play History + #tab-content.is-active(tab-group = "bst2-play-data", tab-index = "0") + .field.is-horizontal + .field-label + label.label User ID + .field-body + span #{bst2Account.userId.toString().padStart(8, "0").slice(0, 4)}-#{bst2Account.userId.toString().padStart(8, "0").slice(4, 8)} + .field.is-horizontal + .field-label + label.label Name + .field-body + span #{bst2Base.name} + .field.is-horizontal + .field-label + label.label Play Count + .field-body + span #{bst2Account.playCount} + .field.is-horizontal + .field-label + label.label Beast Rank + .field-body + if bst2Base.brnk == 0 + span - + else + span #{courseTitle[bst2Base.brnk - 1]} + #tab-content(tab-group = "bst2-play-data", tab-index = "1", style = "overflow-x: auto; overflow-y: hidden") + if bst2MusicRecord.length == 0 + .has-text-grey.has-text-centered + p + span.icon.is-large + i.mdi.mdi-emoticon-happy.mdi-48px + p Nothing's here yet. Let's play! + else + .paginated-container(pagination-group = "bst2-scores") + input.page-size(type = "hidden", value = defaultPageSize) + .hidden-wrapper + .pagination + a.pagination-previous + i.mdi.mdi-chevron-left + | Previous + a.pagination-next + | Next + i.mdi.mdi-chevron-right + ul.pagination-list#tabs + .scrolling-x-wrapper.hidden-y-wrapper + table.table(style = "vertical-align: middle") + thead + tr + th ID + th(stye = "width: 250px") Title + th Scores + tbody + each r in bst2MusicRecordOrganized + if r != null + - let easiestChart = 4 + - for (let k in r) if ((parseInt(k).toString() == k) && (parseInt(k) < easiestChart)) easiestChart = parseInt(k) + - let info = bstMusicsInfo[r[easiestChart].musicId] + tr.is-vcentered.paginated-content + td(style = "white-space: nowrap; overflow: hidden; text-overflow: ellipsis; vertical-align: middle;") #{r[easiestChart].musicId} + td(style = "width: 250px; vertical-align: middle") + .marquee-label-container(style = "max-width: 250px;") + span.modal-trigger + a.marquee-label(style = "display: inline-block") #{info.title} + .modal.is-mobile + .modal-background.is-mobile#close + .modal-card.is-mobile(style = "vertical-align: middle; margin-top: 65px") + header.modal-card-head + p.modal-card-title(style = "margin-bottom: 0") Details + button.delete#close + section.modal-card-body + table.table + thead + tr + th + each i in [0, 1, 2, 3] + th(style = "width: 120px; color: " + bstChartTypeDetails[i].color) #{bstChartTypeDetails[i].title} + tbody + tr + th Music ID + td(colspan = "4") #{info.musicId} + tr + th Title + td(colspan = "4", style = "white-space: normal;") #{info.title} + tr + th Artist + td(colspan = "4", style = "white-space: normal;") #{info.artist} + tr + th BPM + td(colspan = "4") #{info.bpm.min + ((info.bpm.max == info.bpm.min) ? "" : (" - " + info.bpm.max))} + tr + th Level + each i in [0, 1, 2, 3] + - let t = bstChartTypeDetails[i] + if info.chartsInfo[t.title.toLowerCase()] != null + td(style = "color: " + ((info.chartsInfo[t.title.toLowerCase()] == "ネ申") ? "gold; text-shadow: 0 0 3px gold;" : t.color)) #{info.chartsInfo[t.title.toLowerCase()]} + else + td + tr + th State + each i in [0, 1, 2, 3] + if (r[i] != null) && (getClearTypeDetails(r[i]) != null) + - let c = r[i] + - let d = getClearTypeDetails(c) + - let t = bstChartTypeDetails[c.chartType] + td + abbr(title = d.title) #{d.abbr} + else + td + tr + th Rank + each i in [0, 1, 2, 3] + if (r[i] != null) && (getClearTypeDetails(r[i]) != null) + - let c = r[i] + - let d = getClearTypeDetails(c) + - let t = bstChartTypeDetails[c.chartType] + - let rank = getRank(c.score, 2) + td(style = "color: " + rank.color) #{rank.title} + else + td + tr + th Score + each i in [0, 1, 2, 3] + if (r[i] != null) && (getClearTypeDetails(r[i]) != null) + - let c = r[i] + - let d = getClearTypeDetails(c) + - let t = bstChartTypeDetails[c.chartType] + td #{c.score} + else + td + tr + th Combo + each i in [0, 1, 2, 3] + if (r[i] != null) && (getClearTypeDetails(r[i]) != null) + - let c = r[i] + - let d = getClearTypeDetails(c) + - let t = bstChartTypeDetails[c.chartType] + td #{c.combo} + else + td + tr + th Update + each i in [0, 1, 2, 3] + if (r[i] != null) && (getClearTypeDetails(r[i]) != null) + - let c = r[i] + - let d = getClearTypeDetails(c) + - let t = bstChartTypeDetails[c.chartType] + td #{new Date(c.updateTime).toLocaleString().split(" ")[0]} + else + td + tr + th Last Play + each i in [0, 1, 2, 3] + if (r[i] != null) && (getClearTypeDetails(r[i]) != null) + - let c = r[i] + - let d = getClearTypeDetails(c) + - let t = bstChartTypeDetails[c.chartType] + td #{new Date(c.lastPlayTime).toLocaleString().split(" ")[0]} + else + td + footer.modal-card-foot + td + .columns.is-mobile + each i in [0, 1, 2, 3] + if (r[i] != null) && (getClearTypeDetails(r[i]) != null) + - let c = r[i] + - let d = getClearTypeDetails(c) + - let t = bstChartTypeDetails[c.chart] + .column.is-one-quarter.modal-trigger(style = "vertical-align: middle;") + a.one-quarter#forwide(style = "min-width: 120px") + .heading.has-text-centered(style = "white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: " + t.color) #{t.title} + .title.has-text-centered(style = "white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: " + t.color) #{c.score} + a.one-quarter.has-text-centered#fornarrow(style = "white-space: nowrap; overflow: hidden; min-width: 60px; color: " + t.color) #{c.score} + .modal.is-mobile + .modal-background.is-mobile#close + .modal-card.is-mobile(style = "vertical-align: middle; margin-top: 65px") + header.modal-card-head + p.modal-card-title(style = "margin-bottom: 0") Details + button.delete#close + section.modal-card-body + .form + .field.is-horizontal + .field-label + label.label Music ID + .field-body + span #{c.musicId} + .field.is-horizontal + .field-label + label.label Music Title + .field-body + span #{info.title} + .field.is-horizontal + .field-label + label.label Artist + .field-body + span #{info.artist} + .field.is-horizontal + .field-label + label.label BPM + .field-body + span #{info.bpm.min + ((info.bpm.max == info.bpm.min) ? "" : (" - " + info.bpm.max))} + .field.is-horizontal + .field-label + label.label Chart + .field-body + .level.is-mobile + .level-left + .level-item + span(style = "color: " + t.color) #{t.title} + .level-item + p(style = "color: " + ((info.chartsInfo[t.title.toLowerCase()] == "ネ申") ? "gold; text-shadow: 0 0 3px gold;" : t.color)) #{info.chartsInfo[t.title.toLowerCase()]} + .field.is-horizontal + .field-label + label.label State + .field-body + span #{d.title} + .field.is-horizontal + .field-label + label.label Rank + .field-body + span(style = "color: " + getRank(c.score, 2).color) #{getRank(c.score, 2).title} + .field.is-horizontal + .field-label + label.label Score + .field-body + span #{c.score} + .field.is-horizontal + .field-label + label.label Combo + .field-body + span #{c.combo} + .field.is-horizontal + .field-label + label.label(style = "white-space: nowrap; vertical-align: middle;") Update Time + .field-body + span #{new Date(c.updateTime).toLocaleString()} + .field.is-horizontal + .field-label + label.label(style = "white-space: nowrap; vertical-align: middle;") Last Play + .field-body + span #{new Date(c.lastPlayTime).toLocaleString()} + footer.modal-card-foot + else + .column.is-one-quarter + a.one-quarter#forwide(style = "min-width: 120px") + a.one-quarter#fornarrow(style = "min-width: 120px") + .hidden-wrapper + .pagination + a.pagination-previous + i.mdi.mdi-chevron-left + | Previous + a.pagination-next + | Next + i.mdi.mdi-chevron-right + ul.pagination-list#tabs + #tab-content(tab-group = "bst2-play-data", tab-index = "2", style = "overflow-x: auto") + if bst2Course.length == 0 + .has-text-grey.has-text-centered + p + span.icon.is-large + i.mdi.mdi-emoticon-happy.mdi-48px + p Nothing's here yet. Let's play! + else + table.table + thead + tr + th Course + th State + th Score + th Combo + th Gauge + th Play Count + th Update Time + th Last Play Time + tbody + each c in bst2Course + tr + td(style = "white-space: nowrap;") #{courseTitle[c.courseId]} + td #{getClearTypeDetails(c).title} + td #{c.score} + td #{c.combo} + td #{(c.gauge / 10).toFixed(1)}% + td #{c.playCount} + td #{new Date(c.updateTime).toLocaleString()} + td #{new Date(c.lastPlayTime).toLocaleString()} + #tab-content(tab-group = "bst2-play-data", tab-index = "3", style = "overflow-x: auto") + if bst2StageLog.length == 0 + .has-text-grey.has-text-centered + p + span.icon.is-large + i.mdi.mdi-emoticon-happy.mdi-48px + p Nothing's here yet. Let's play! + else + .paginated-container(pagination-group = "bst2-play-history") + input.page-size(type = "hidden", value = defaultPageSize) + .hidden-wrapper + .pagination + a.pagination-previous + i.mdi.mdi-chevron-left + | Newer + a.pagination-next + | Older + i.mdi.mdi-chevron-right + ul.pagination-list#tabs + .scrolling-wrapper + table.table + thead + th ID + th(style = "max-width: 200px") Title + th Chart + th Level + th State + th Score + th Scene + th Time + tbody + - + let progresspattern = "" + for (i = 0; i <= 24; i++) progresspattern += "transparent " + (i * 4 + 2) + "%, #FFFFFF4F " + (i * 4 + 2) + "%, #FFFFFF4F " + (i * 4 + 3) + "%, transparent " + (i * 4 + 3) + ((i != 24) ? "%, " : "%") + each s in bst2StageLog + - let info = bstMusicsInfo[s.musicId] + - let chart = bstChartTypeDetails[s.chart] + - let level = info.chartsInfo[chart.title.toLowerCase()] + - let rank = getRank(s.score, 2) + - let clearType = getClearTypeDetails(s) + tr.paginated-content + td #{s.musicId} + td(style = "max-width: 250px") + .marquee-label-container(style = "max-width: 250px;") + span.modal-trigger + a.marquee-label #{info.title} + .modal.is-mobile(style = "x-overflow: auto;") + .modal-background.is-mobile#close + .modal-content + .box(style = "background-color: black; color: white; border-radius: 12px; min-width: 600px") + p.has-text-centered(style = "border-bottom: 3px solid grey") #{info.title} + .columns.is-mobile + .column.is-narrow + .has-text-centered(style = "width: 256px; height: 256px; background-color: lightgrey; color: darkgray; border-radius: 3%; border: 3px solid " + chart.colorInvert) + p Should be a jacket here... + .level.is-mobile + .level-left + .level-right + .level-item(style = "color: " + chart.colorInvert) #{chart.title} + .level-item.is-size-4(style = (level == "ネ申") ? "color: gold; text-shadow: 0 0 5px gold" : "") #{level} + .column + .columns.is-vcentered.is-mobile(style = "margin-bottom: 0") + .column + - + let leftProgress = (s.gaugeTimes10 >= 700) ? "70%" : ((s.gaugeTimes10 / 10) + "%") + let rightProgress = (s.gaugeTimes10 < 700) ? "0" : (((s.gaugeTimes10 - 700) / 10) + "%") + .mulprogress(style = "width: 100%; height: 16px; border-radius: 8px; overflow: hidden; position: relative;") + .leftprogress(style = "float: left; height: 100%; border-radius: 8px 0 0 8px; background-color: #2EBECC; width: " + leftProgress) + .rightprogress(style = "float: left; height: 100%; background-color: #F151F1; width: " + rightProgress) + .forepattern(style = "position: absolute; top: 0; left: 0; bottom: 0; right: 0; background: linear-gradient(315deg, " + progresspattern + ")") + .column.is-narrow + p(style = "width: 60px") #{(s.gaugeTimes10 / 10).toFixed(1)}% + div(style = "margin-bottom: 28px; border-bottom: 3px dashed grey;") + .columns.is-vcentered.is-mobile + .column.is-narrow + p Score + .column.has-text-right + p.is-size-1 #{s.score.toString().padStart(7, "0")} + .box.is-size-7(style = "margin-bottom: 0; padding: 8px; background: black; color: white; box-shadow: 0 3px 4px #FFFFFF9F, 0 0 0 2px #FFFFFF3F;") + .columns.is-vcentered.is-mobile.has-text-centered(style = "margin-bottom: 0") + - let f = (bst2Customization.custom[7] | 0b00000001) == bst2Customization.custom[7] + .column.is-one-quarter + if f + span(style = "color: #FF6FC9") F + span(style = "color: #FFC90A") a + span(style = "color: #E7FF18") n + span(style = "color: #57FD4D") t + span(style = "color: #1BFCD7") a + span(style = "color: #1CD2FF") s + span(style = "color: #7255FB") t + span(style = "color: #A835F9") i + span(style = "color: #D512E8") c + else + span(style = "color: #EF005B") サ + span(style = "color: #37FFC8") イ + span(style = "color: #FFEE5F") コ + span(style = "color: #EF005B") ー + span(style = "color: #37FFC8") ! + p #{(s.fantasticCount != null) ? s.fantasticCount.toString().padStart(4, "0") : "----"} + .column.is-one-quarter + if f + span(style = "color: #33E7AD") Great + else + span(style = "color: #37FFC8") ヨッシャー + p #{(s.greatCount != null) ? s.greatCount.toString().padStart(4, "0") : "----"} + .column.is-one-quarter + if f + span(style = "color: #FFC500") Fine + else + span(style = "color: #FFEE5F") オシイ + p #{(s.fineCount != null) ? s.fineCount.toString().padStart(4, "0") : "----"} + .column.is-one-quarter + if f + span(style = "color: #EF005B") miss + else + span(style = "color: #FF1D48; font-weight: bold; -webkit-text-stroke: 0.8px black; text-shadow: 0 0 1px #FF1D48, 0 0 1px #FF1D48, 0 0 1px #FF1D48, 0 0 1px #FF1D48") スカッ + p #{(s.missCount != null) ? s.missCount.toString().padStart(4, "0") : "----"} + .columns.is-vcentered.is-mobile + .column.is-one-quarter + .column.is-half.has-text-right + p Max Combo + .column.is-one-quarter.has-text-centered + p #{s.combo.toString().padStart(4, "0")} + .columns.is-vcentered.is-mobile + .column + .column.is-half.has-text-right(style = "position: relative") + div.has-text-centered(style = "position: absolute; transform: rotate(20deg) scale(0.7); top: 28px; right: -8px; color: " + clearType.color) + span(style = "position: relative; z-index: 1; background: white; padding: 0 8px; clip-path: polygon(0 10%, 100% 10%, 90% 47%, 100% 85%, 0 85%, 10% 47%);") #{clearType.title} + i(class = clearType.badge, style = "position: absolute; top: -22px; left: 2px; right: 2px; z-index: 0;") + p(style = "font-size: 3.8rem; color: " + rank.colorInvert) #{rank.title} + td(style = "color: " + chart.color) #{chart.title} + td(style = (level == "ネ申") ? "color: gold; text-shadow: 0 0 5px gold" : ("color: " + chart.color)) #{level} + td + abbr(title = clearType.title) #{clearType.abbr} + td #{s.score} + td #{s.stageId + 1} + td #{new Date(s.time).toLocaleString()} + .hidden-wrapper + .pagination + a.pagination-previous + i.mdi.mdi-chevron-left + | Newer + a.pagination-next + | Older + i.mdi.mdi-chevron-right + ul.pagination-list#tabs + .card + .card-header + p.card-header-title + span.icon + i.mdi.mdi-account-edit + | Player Settings + a.card-header-icon.card-toggle(card = "edit-settings") + span.icon + i.mdi.mdi-chevron-down + .card-content(card = "edit-settings") + #tab-content(tab-group = "version", tab-index = "2", class = (defaultVersion == 2) ? "is-active" : "") + if bst2Account == null + .has-text-grey.has-text-centered + p + span.icon.is-large + i.mdi.mdi-emoticon-happy.mdi-48px + p Nothing's here yet. Let's play! + else + form#validatable(method="post" action="/emit/bst2UpdateSettings") + input(type = "hidden", name = "refid", value = refid) + .field.is-horizontal#validatable + .field-label.is-normal + label.label Name + .field-body + .control + input.input#validatable(type = "text", name = "name", min-length = "1", max-length = "8", min-pattern = "^[a-zA-Z0-9\.\-_·・&!\?/\*#♯♭★@♪↓↑→←()∞◆●▼¥∀^ ]{1,8}$", value = bst2Base.name) + p.help 1~8 letter(s) and symbol(s). + .field.is-horizontal + .field-label.is-normal + label.label Ripple Note + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "rippleNote", value = bst2Customization.custom[0]) + .select + select#form-select-select + option Default + option Biggger + option Drop + option Shutter + .field.is-horizontal + .field-label.is-normal + label.label Normal Note Sound + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "sfxNormalNote", value = bst2Customization.custom[2]) + .select + select#form-select-select + option Normal Set A + option Ripple Set A + option Slash Set A + option Stream Set A + option Normal Set B + option Ripple Set B + option Slash Set B + option Stream Set B + option Normal Set C + option Ripple Set C + option Slash Set C + option Stream Set C + .field.is-horizontal + .field-label.is-normal + label.label Ripple Note Sound + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "sfxRippleNote", value = bst2Customization.custom[3]) + .select + select#form-select-select + option Normal Set A + option Ripple Set A + option Slash Set A + option Stream Set A + option Normal Set B + option Ripple Set B + option Slash Set B + option Stream Set B + option Normal Set C + option Ripple Set C + option Slash Set C + option Stream Set C + .field.is-horizontal + .field-label.is-normal + label.label Slash Note Sound + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "sfxSlashNote", value = bst2Customization.custom[4]) + .select + select#form-select-select + option Normal Set A + option Ripple Set A + option Slash Set A + option Stream Set A + option Normal Set B + option Ripple Set B + option Slash Set B + option Stream Set B + option Normal Set C + option Ripple Set C + option Slash Set C + option Stream Set C + .field.is-horizontal + .field-label.is-normal + label.label Stream Note Sound + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "sfxStreamNote", value = bst2Customization.custom[5]) + .select + select#form-select-select + option Normal Set A + option Ripple Set A + option Slash Set A + option Stream Set A + option Normal Set B + option Ripple Set B + option Slash Set B + option Stream Set B + option Normal Set C + option Ripple Set C + option Slash Set C + option Stream Set C + .field.is-horizontal + .field-label.is-normal + label.label Background Brightness + .field-body + .field.has-addons#form-numeric + .control + button.button.is-light#form-numeric-sub(type = "button") + + .control + input.input#form-numeric-input(name = "backgroundBrightness", type = "hidden", min-value = "0", max-value = "200", step = "1", value = bst2Customization.custom[6] readonly) + input.input#form-numeric-input(style = "width: 72px; text-align: center", min-value = "100.0", max-value = "0.0", step = "-0.5", digit-count = "1", value = 100 - bst2Customization.custom[6] * 0.5, readonly) + .control + button.button.is-light#form-numeric-add(type = "button") - + .field.is-horizontal + .field-label.is-normal + label.label Judge Text + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "judgeText", value = ((bst2Customization.custom[7] | 0b00000001) == bst2Customization.custom[7]) ? 1 : 0) + .select + select#form-select-select + option Bst2 // サイコー! + option Bst1 // Fantastic + .field.is-horizontal + .field-label.is-normal + label.label Ripple Note Prompt + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "rippleNoteGuide", value = ((bst2Customization.custom[7] | 0b00000010) == bst2Customization.custom[7]) ? 1 : 0) + .select + select#form-select-select + option On + option Off + .field.is-horizontal + .field-label.is-normal + label.label Stream Note Prompt + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "streamNoteGuide", value = ((bst2Customization.custom[7] | 0b00000100) == bst2Customization.custom[7]) ? 1 : 0) + .select + select#form-select-select + option On + option Off + .field.is-horizontal + .field-label.is-normal + label.label Stream Note Tail SFX + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "sfxStreamNoteTail", value = ((bst2Customization.custom[7] | 0b00001000) == bst2Customization.custom[7]) ? 1 : 0) + .select + select#form-select-select + option On + option Off + .field.is-horizontal + .field-label.is-normal + label.label Inaccurate SFX + .field-body + .control#form-select + input#form-select-input(type = "hidden", name = "sfxFine", value = ((bst2Customization.custom[7] | 0b00010000) == bst2Customization.custom[7]) ? 1 : 0) + .select + select#form-select-select + option On + option Off + .field.is-horizontal + .field-label + .field-body + button.button.is-primary#validatable(type = "submit") + span.icon + i.mdi.mdi-check + span Submit \ No newline at end of file From e64542f233e9680d8ab1d6aa44dc5e4af38e1a72 Mon Sep 17 00:00:00 2001 From: RoxCian <40283094+RoxCian@users.noreply.github.com> Date: Fri, 2 Apr 2021 02:15:48 +0800 Subject: [PATCH 3/3] Fix saving score incorrectly while the first play for BeatStream. --- bst@asphyxia/README.md | 2 +- bst@asphyxia/handlers/bst2/common.ts | 13 +++++++------ bst@asphyxia/webui/profile_detail.pug | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/bst@asphyxia/README.md b/bst@asphyxia/README.md index c4ffc86..e1e4ebc 100644 --- a/bst@asphyxia/README.md +++ b/bst@asphyxia/README.md @@ -1,6 +1,6 @@ # BeatStream -Plugin Version: **v1.0.0** +Plugin Version: **v1.0.1** Supported Versions: diff --git a/bst@asphyxia/handlers/bst2/common.ts b/bst@asphyxia/handlers/bst2/common.ts index 8039168..89d0206 100644 --- a/bst@asphyxia/handlers/bst2/common.ts +++ b/bst@asphyxia/handlers/bst2/common.ts @@ -111,12 +111,13 @@ export namespace Bst2HandlersCommon { } while ((await DB.Find(null, { collection: "bst.bst2.player.account", userId: userId })).length > 0) oldAccount = fromMap(Bst2AccountMap) oldAccount.userId = userId + } else { + oldAccount.playCount++ + if (!isToday(toBigInt(oldAccount.standardTime))) { + oldAccount.dayCount++ + oldAccount.playCountToday = 1 + } else oldAccount.playCountToday++ } - 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) @@ -130,7 +131,7 @@ export namespace Bst2HandlersCommon { 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) }) + send.object({ uid: K.ITEM("s32", oldAccount.userId) }) } export const WriteStageLog: EPR = async (_, data, send) => { diff --git a/bst@asphyxia/webui/profile_detail.pug b/bst@asphyxia/webui/profile_detail.pug index 44b8094..a87f3ff 100644 --- a/bst@asphyxia/webui/profile_detail.pug +++ b/bst@asphyxia/webui/profile_detail.pug @@ -48,9 +48,9 @@ let bst2HighestRank = -1 for (let r of bst2Course) if ((r.courseId > bst2HighestRank) && (r.medal >= 3)) bst2HighestRank = r.courseId function getClearTypeDetails(record) { - if (record.medal == 5) return { title: "Perfect", abbr: "P", color: "#FF3860", badge: "mdi mdi-weather-sunny mdi-48px" } + if (record.medal == 5) return { title: "Perfect", abbr: "P", color: "#FF3860; text-shadow: 0 0 5px #FFFFFF9F;", badge: "mdi mdi-decagram mdi-48px" } else if (record.medal == 4) return { title: "Full Combo", abbr: "FC", color: "gold", badge: "mdi mdi-cog mdi-48px" } - else if (record.medal == 3) return { title: "Clear", abbr: "C", color: "#35F2BE", badge: "mdi mdi-star-circle-outline mdi-48px"} + else if (record.medal == 3) return { title: "Clear", abbr: "C", color: "#33E7AD", badge: "mdi mdi-star-circle-outline mdi-48px"} else if ((record.medal == 2) || (record.medal == 1)) return { title: "Failed", abbr: "F", color: "#837E8D", badge: "mdi mdi-close-circle-outline mdi-48px"} else return null }