From f96aecf53dbf6909fbed4112e36a667f72dd0774 Mon Sep 17 00:00:00 2001 From: Kirito Date: Sat, 24 Apr 2021 18:10:06 +0900 Subject: [PATCH] jubeat knit support --- jubeat@asphyxia/README.md | 18 ++ jubeat@asphyxia/handlers/common.ts | 50 ++++ jubeat@asphyxia/handlers/matching.ts | 49 ++++ jubeat@asphyxia/handlers/profile.ts | 292 +++++++++++++++++++++ jubeat@asphyxia/index.ts | 60 +++++ jubeat@asphyxia/models/profile.ts | 101 +++++++ jubeat@asphyxia/models/score.ts | 14 + jubeat@asphyxia/templates/knit/profile.pug | 89 +++++++ jubeat@asphyxia/utils.ts | 6 + 9 files changed, 679 insertions(+) create mode 100644 jubeat@asphyxia/README.md create mode 100644 jubeat@asphyxia/handlers/common.ts create mode 100644 jubeat@asphyxia/handlers/matching.ts create mode 100644 jubeat@asphyxia/handlers/profile.ts create mode 100644 jubeat@asphyxia/index.ts create mode 100644 jubeat@asphyxia/models/profile.ts create mode 100644 jubeat@asphyxia/models/score.ts create mode 100644 jubeat@asphyxia/templates/knit/profile.pug create mode 100644 jubeat@asphyxia/utils.ts diff --git a/jubeat@asphyxia/README.md b/jubeat@asphyxia/README.md new file mode 100644 index 0000000..c40587a --- /dev/null +++ b/jubeat@asphyxia/README.md @@ -0,0 +1,18 @@ +# Jubeat + +Plugin Version: **v1.0.0** + +### Supported Versions + +*** + +- knit +- knit APPEND + +### Changelogs + +*** + +#### 1.0.0 + +- Initial Release diff --git a/jubeat@asphyxia/handlers/common.ts b/jubeat@asphyxia/handlers/common.ts new file mode 100644 index 0000000..e5484a6 --- /dev/null +++ b/jubeat@asphyxia/handlers/common.ts @@ -0,0 +1,50 @@ +import {getVersion} from "../utils"; + +export const shopinfo: EPR = (info, data, send) => { + const locId = $(data.shop).content("locationid"); + const version = getVersion(info); + if (version === 0) return send.deny(); + + if (version === 3) return send.object({ + data: { + cabid: K.ITEM('u32', 1), + locationid: K.ITEM('str', locId), + is_send: K.ITEM("u8", 1) + } + }) + + return send.deny(); +} + +export const demodata = { + getNews: (_, __, send) => send.object({ data: { officialnews: K.ATTR({ count: "0" }) } }), + getData: (_, data, send) => { + const newsId = $(data).number('officialnews.newsid'); + return send.object({ + data: { + officialnews: { + data: { + newsid: K.ITEM('s16', newsId), + image: K.ITEM('u8', 0, { size: '0' }) + } + } + } + }); + }, + getHitchart: (_, __, send) => send.object({ + data: { + hitchart: { + update: K.ITEM('str', ''), + + hitchart_lic: K.ATTR({ count: "0" }), + hitchart_org: K.ATTR({ count: "0" }), + } + } + }), +}; + +export const netlog: EPR = (info, data, send) => { + const errMsg = $(data).str('msg'); + console.error(errMsg); + return send.success(); +} diff --git a/jubeat@asphyxia/handlers/matching.ts b/jubeat@asphyxia/handlers/matching.ts new file mode 100644 index 0000000..eabe516 --- /dev/null +++ b/jubeat@asphyxia/handlers/matching.ts @@ -0,0 +1,49 @@ +export const check: EPR = (info, data, send) => { + const enter = $(data).bool('data.enter'); + const time = $(data).number('data.time'); + return send.object({ + data: { + entrant_nr: K.ITEM('u32', 1, { time: String(time) }), + interval: K.ITEM('s16', 1), + entry_timeout: K.ITEM('s16', U.GetConfig("matching_entry_timeout")), + waitlist: K.ATTR({ count: "0" }) + } + }); +}; + +export const entry: EPR = (info, data, send) => { + const localMatchingNode = $(data).element("data.local_matching"); + const connectNode = $(data).element("data.connect"); + const musicNode = $(data).element('data.music'); + + const roomId = _.random(1, 999999999999999); + + // TODO Local matching support + + return send.object({ + data: { + roomid: K.ITEM('s64', BigInt(roomId), { master: "1" }), + refresh_intr: K.ITEM('s16', 3), + music: { + id: K.ITEM("u32", musicNode.number("id")), + seq: K.ITEM("u8", musicNode.number("seq")), + } + } + }); +}; + +export const refresh: EPR = (info, data, send) => { + return send.object({ + data: { + refresh_intr: K.ITEM('s16', 2), + } + }); +}; + +export const report: EPR = (info, data, send) => { + return send.object({ + data: { + refresh_intr: K.ITEM('s16', 1), + } + }); +}; diff --git a/jubeat@asphyxia/handlers/profile.ts b/jubeat@asphyxia/handlers/profile.ts new file mode 100644 index 0000000..14508f5 --- /dev/null +++ b/jubeat@asphyxia/handlers/profile.ts @@ -0,0 +1,292 @@ +import {getVersion} from "../utils"; +import Profile from '../models/profile'; +import {Score} from '../models/score'; + +export const profile: EPR = async (info, data, send) => { + let refId = $(data).str("data.player.pass.refid"); + if (!refId) return send.deny(); + + const version = getVersion(info); + if (version === 0) return send.deny(); + + const name = $(data).str("data.player.name"); + + let profile = await DB.FindOne(refId, { collection: "profile" }); + + if (!profile) { + if (!name) return send.deny(); + + const newProfile = new Profile(); + newProfile.jubeatId = _.random(1, 99999999); + newProfile.name = name; + newProfile.previous_version = version; + + await DB.Upsert(refId, { collection: "profile" }, newProfile); + + profile = newProfile; + } + + let migration = false; + if (profile.previous_version < version) { + migration = true; + profile.name = ""; + await DB.Update(refId, { collection: "profile" }, { $set: { name: "", previous_version: version } }); + } + + if (name) { + profile.name = name; + await DB.Update(refId, { collection: "profile" }, { $set: { name } }); + } + + if (version === 3) { + if (U.GetConfig("unlock_all_songs")) { + profile.knit.item = { + secretList: [-1, -1], + themeList: -1, + markerList: [-1, -1], + titleList: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] + }; + profile.knit.item_new = { + secretList: [0, 0], + themeList: 0, + markerList: [0, 0], + titleList: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }; + } + return send.pugFile('templates/knit/profile.pug', { refId, migration, ...profile }, { compress: false }); + } + + return send.deny(); +}; + +export const saveProfile: EPR = async (info, { data }, send) => { + console.log(U.toXML(data)); + const player = $(data).element("player"); + + const refId = player.str("refid"); + if (!refId) return send.deny(); + + const version = getVersion(info); + if (version === 0) return send.deny(); + + const profile = await DB.FindOne(refId, { collection: "profile" }); + + if (version === 3) { + profile.name = player.str("name"); + + profile.last.shopname = player.str("shopname", profile.last.shopname); + profile.last.areaname = player.str("areaname", profile.last.areaname); + + profile.jubility = player.number("info.jubility", profile.jubility); + profile.jubilityYday = player.number("info.jubility_yday", profile.jubilityYday); + + profile.knit.acvProg = player.number("info.acv_prog", profile.knit.acvProg); + profile.knit.acvWool = player.number("info.acv_wool", profile.knit.acvWool); + profile.knit.acvRouteProg = player.numbers("info.acv_route_prog", profile.knit.acvRouteProg); + profile.knit.acvPoint = player.number("info.acv_point", profile.knit.acvPoint); + + profile.tuneCount = player.number("info.tune_cnt", profile.tuneCount); + profile.saveCount = player.number("info.save_cnt", profile.saveCount); + profile.savedCount = player.number("info.saved_cnt", profile.savedCount); + profile.fullcomboCount = player.number("info.fc_cnt", profile.fullcomboCount); + profile.fullcomboSeqCount = player.number("info.fc_seq_cnt", profile.fullcomboSeqCount); + profile.excellentCount = player.number("info.exc_cnt", profile.excellentCount); + profile.excellentSeqCount = player.number("info.exc_seq_cnt", profile.excellentSeqCount); + profile.matchCount = player.number("info.match_cnt", profile.matchCount); + profile.beatCount = player.number('info.beat_cnt', profile.beatCount); + profile.conciergeSelectedCount = player.number('info.con_sel_cnt', profile.conciergeSelectedCount); + profile.tagCount = player.number('info.tag_cnt', profile.tagCount); + profile.mynewsCount = player.number('info.mynews_cnt', profile.mynewsCount); + + if (!U.GetConfig("unlock_all_songs")) { + profile.knit.item.secretList = player.numbers('item.secret_list', profile.knit.item.secretList); + profile.knit.item.themeList = player.number('item.theme_list', profile.knit.item.themeList); + profile.knit.item.markerList = player.numbers('item.marker_list', profile.knit.item.markerList); + profile.knit.item.titleList = player.numbers('item.title_list', profile.knit.item.titleList); + + profile.knit.item_new.secretList = player.numbers('item.secret_new', profile.knit.item_new.secretList); + profile.knit.item_new.themeList = player.number('item.theme_new', profile.knit.item_new.themeList); + profile.knit.item_new.markerList = player.numbers('item.marker_new', profile.knit.item_new.markerList); + profile.knit.item_new.titleList = player.numbers('item.title_new', profile.knit.item_new.titleList); + } + + profile.last.conciergeSuggestId = player.number('info.con_suggest_id', profile.last.conciergeSuggestId); + profile.last.playTime = BigInt(new Date().getMilliseconds()); + + // Append + const collabo = player.element("collabo"); + if (collabo) { + profile.knit.collabo.success = collabo.bool("success"); + profile.knit.collabo.completed = collabo.bool("completed"); + } + + const result = $(data).element("result"); + + if (result) { + const tunes = result.elements("tune"); + + for (const tune of tunes) { + const musicId = tune.number("music", 0); + profile.last.musicId = musicId; + profile.last.seqId = parseInt(tune.attr("player.score").seq) || 0; + profile.last.title = tune.number("title", profile.last.title); + profile.last.theme = tune.number("theme", profile.last.theme); + profile.last.marker = tune.number("marker", profile.last.marker); + profile.last.sort = tune.number("sort", profile.last.sort); + profile.last.filter = tune.number("filter", profile.last.filter); + profile.last.showRank = tune.number("combo_disp", profile.last.showRank); + profile.last.showCombo = tune.number("rank_sort", profile.last.showCombo); + profile.last.mselStat = tune.number("msel_stat", profile.last.mselStat); + + const score = tune.number('player.score'); + const seq = parseInt(tune.attr('player.score').seq); + const clear = parseInt(tune.attr('player.score').clear); + const combo = parseInt(tune.attr('player.score').combo); + const bestScore = tune.number('player.best_score'); + const bestClear = tune.number('player.best_clear'); + const playCount = tune.number('player.play_cnt'); + const clearCount = tune.number('player.clear_cnt'); + const fullcomboCount = tune.number('player.fc_cnt'); + const excellentCount = tune.number('player.exc_cnt'); + const mbar = tune.numbers('player.mbar'); + + await updateScore(refId, musicId, seq, score, clear, mbar, { + playCount, + clearCount, + fullcomboCount, + excellentCount + }); + } + } + + await DB.Update(refId, { collection: "profile" }, profile); + + return send.object({ data: { player: { session_id: K.ITEM('s32', 1) } } }); + } + + return send.deny(); +}; + +export const loadScore: EPR = async (info, data, send) => { + const jubeatId = $(data).number("data.player.jid"); + if (!jubeatId) return send.deny(); + + const profile = await DB.FindOne(null, { collection: "profile", jubeatId }); + if (!profile) return send.deny(); + + const version = getVersion(info); + if (version === 0) return send.deny(); + + const scores = await DB.Find(profile.__refid, { collection: "score" }); + const scoreData: { [musicId: number]: any } = {}; + + for (const score of scores) { + if (!scoreData[score.musicId]) { + scoreData[score.musicId] = { + playCnt: [0, 0, 0], + clearCnt: [0, 0, 0], + fcCnt: [0, 0, 0], + exCnt: [0, 0, 0], + clear: [0, 0, 0], + score: [0, 0, 0], + bar: [Array(30).fill(0), Array(30).fill(0), Array(30).fill(0)] + }; + } + + const data = scoreData[score.musicId]; + data.playCnt[score.seq] = score.playCount; + data.clearCnt[score.seq] = score.clearCount; + data.fcCnt[score.seq] = score.fullcomboCount; + data.exCnt[score.seq] = score.excellentCount; + data.clear[score.seq] = score.clearType; + data.score[score.seq] = score.score; + data.bar[score.seq] = score.bar; + } + + if (version === 3) return send.object({ + data: { + player: { + playdata: K.ATTR({ count: String(Object.keys(scoreData).length) }, { + musicdata: (() => { + const musicdata = []; + Object.entries(scoreData).forEach(([k, v]) => { + musicdata.push(K.ATTR({ music_id: String(k) }, { + play_cnt: K.ARRAY('s32', v.playCnt), + clear_cnt: K.ARRAY('s32', v.clearCnt), + fc_cnt: K.ARRAY('s32', v.fcCnt), + ex_cnt: K.ARRAY('s32', v.exCnt), + clear: K.ARRAY('s8', v.clear), + score: K.ARRAY('s32', v.score), + bar: v.bar.map((v, i) => K.ARRAY('u8', v, { seq: String(i) })) + })); + }); + return musicdata; + })() + }) + } + } + }); + + return send.deny(); +}; + +const updateScore = async (refId: string, musicId: number, seq: number, score: number, clear: number, mbar: number[], data: any) => { + let raised; + + const oldScore = await DB.FindOne(refId, { collection: "score", musicId, seq }); + + let scoreData = oldScore; + + if (!oldScore) { + scoreData = new Score(); + scoreData.musicId = musicId; + scoreData.seq = seq; + raised = true; + } else { + raised = score > oldScore.score; + score = Math.max(oldScore.score, score); + } + + scoreData.clearType = Math.max(scoreData.clearType, clear); + scoreData.playCount = data.playCount; + scoreData.clearCount = data.clearCount; + scoreData.fullcomboCount = data.fullcomboCount; + scoreData.excellentCount = data.excellentCount; + scoreData.isHardmodeClear = false; + + if (mbar && raised) { + scoreData.score = score; + scoreData.bar = mbar; + } + + await DB.Upsert(refId, { collection: "score", musicId, seq }, scoreData); +}; + +export const meeting: EPR = (info, data, send) => { + return send.object({ + data: { + meeting: { + single: K.ATTR({ count: '0' }), + tag: K.ATTR({ count: '0' }), + }, + reward: { + total: K.ITEM('s32', 0), + point: K.ITEM('s32', 0) + } + } + }); +}; + +export const getCollabo: EPR = (info, data, send) => send.object({ + data: { + collabo: { + played: { + iidx: K.ITEM("s8", 1), + popn: K.ITEM("s8", 1), + ddr: K.ITEM("s8", 1), + reflec: K.ITEM("s8", 1), + gfdm: K.ITEM("s8", 1), + } + } + } +}); diff --git a/jubeat@asphyxia/index.ts b/jubeat@asphyxia/index.ts new file mode 100644 index 0000000..194f673 --- /dev/null +++ b/jubeat@asphyxia/index.ts @@ -0,0 +1,60 @@ +import {demodata, netlog, shopinfo} from "./handlers/common"; +import {check, entry, refresh, report} from "./handlers/matching"; +import {getCollabo, loadScore, meeting, profile, saveProfile} from "./handlers/profile"; + +export function register() { + if (CORE_VERSION_MAJOR <= 1 && CORE_VERSION_MINOR < 31) { + console.error("The current version of Asphyxia Core is not supported. Requires version '1.31' or later."); + return; + } + R.GameCode("J44"); + + R.Config("unlock_all_songs", { + name: "Unlock All Songs", + desc: "Tired of unlocking songs? Have this!", + type: "boolean", + default: false + }); + + R.Config("quick_matching_end", { + name: "Quick Matching End", + desc: "Supported from clan to festo.", + type: "boolean", + default: false + }); + + R.Config("matching_entry_timeout", { + name: "Online Matching Timeout", + desc: "If online matching songs are too boring, save time! (second)", + type: "integer", + default: 30, + range: [15, 99], + }); + + R.Route("gametop.regist", profile); + R.Route("gametop.get_pdata", profile); + R.Route("gametop.get_mdata", loadScore); + R.Route("gametop.get_meeting", meeting); + R.Route("gametop.get_collabo", getCollabo); + + R.Route('gameend.regist', saveProfile); + R.Route('gameend.log', true); + R.Route('gameend.set_collabo', true); + + R.Route("shopinfo.regist", shopinfo); + R.Route("netlog.send", netlog); + R.Route("demodata.get_news", demodata.getNews); + R.Route("demodata.get_data", demodata.getData); + R.Route("demodata.get_hitchart", demodata.getHitchart); + R.Route("lobby.check", check); + R.Route("lobby.entry", entry); + R.Route("lobby.refresh", refresh); + R.Route("lobby.report", report); + + R.Unhandled((info, data, send) => { + console.log(info.module, info.method); + console.log(U.toXML(data)); + + return send.deny(); + }); +} diff --git a/jubeat@asphyxia/models/profile.ts b/jubeat@asphyxia/models/profile.ts new file mode 100644 index 0000000..f0e3925 --- /dev/null +++ b/jubeat@asphyxia/models/profile.ts @@ -0,0 +1,101 @@ +export default class Profile { + collection: "profile" = "profile"; + + jubeatId: number = _.random(1, 99999999); + name: string = "JUBEAT"; + + previous_version = 0; + + jubility: number = 0; + jubilityYday: number = 0; + tuneCount: number = 0; + saveCount: number = 0; + savedCount: number = 0; + fullcomboCount: number = 0; + fullcomboSeqCount: number = 0; + excellentCount: number = 0; + excellentSeqCount: number = 0; + matchCount: number = 0; + beatCount: number = 0; + tagCount: number = 0; + mynewsCount: number = 0; + conciergeSelectedCount: number = 0; + + last: { + shopname: string; + areaname: string; + playTime: bigint; + title: number; + theme: number; + marker: number; + showRank: number; + showCombo: number; + musicId: number; + seqId: number; + seqEditId: string; + sort: number; + filter: number; + mselStat: number; + conciergeSuggestId: number; + } = { + shopname: "NONE", + areaname: "NONE", + playTime: BigInt(0), + title: 0, + theme: 0, + marker: 0, + showRank: 1, + showCombo: 1, + musicId: 0, + seqId: 0, + seqEditId: "", + sort: 0, + filter: 0, + mselStat: 0, + conciergeSuggestId: 0 + }; + + knit: { + acvProg: number; + acvWool: number; + acvRouteProg: number[]; + acvPoint: number; + item: { + secretList: number[], + themeList: number, + markerList: number[], + titleList: number[] + }, + item_new: { + secretList: number[], + themeList: number, + markerList: number[], + titleList: number[] + }, + collabo: { + success: boolean; + completed: boolean; + } + } = { + acvProg: 0, + acvWool: 0, + acvRouteProg: [0, 0, 0, 0], + acvPoint: 0, + item: { + secretList: [0, 0], + themeList: 0, + markerList: [0, 0], + titleList: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + item_new: { + secretList: [0, 0], + themeList: 0, + markerList: [0, 0], + titleList: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + collabo: { + success: false, + completed: false + } + }; +} diff --git a/jubeat@asphyxia/models/score.ts b/jubeat@asphyxia/models/score.ts new file mode 100644 index 0000000..440c813 --- /dev/null +++ b/jubeat@asphyxia/models/score.ts @@ -0,0 +1,14 @@ +export class Score { + collection: "score" = "score"; + + musicId: number; + seq: number; + score: number = 0; + clearType: number = 0; + playCount: number = 0; + clearCount: number = 0; + fullcomboCount: number = 0; + excellentCount: number = 0; + isHardmodeClear: boolean; + bar: number[]; +} diff --git a/jubeat@asphyxia/templates/knit/profile.pug b/jubeat@asphyxia/templates/knit/profile.pug new file mode 100644 index 0000000..d56ad73 --- /dev/null +++ b/jubeat@asphyxia/templates/knit/profile.pug @@ -0,0 +1,89 @@ +gametop + data + player + name(__type="str") #{name} + jid(__type="s32") #{jubeatId} + refid(__type="str") #{refId} + session_id(__type="s32") 1 + + info + inherit(__type="bool") #{migration ? 1 : 0} + jubility(__type="s16") #{jubility} + jubility_yday(__type="s16") #{jubilityYday} + acv_prog(__type="s8") #{knit.acvProg} + acv_wool(__type="s8") #{knit.acvWool} + acv_route_prog(__type="s8" __count="4") #{knit.acvRouteProg.join(" ")} + acv_point(__type="s32") #{knit.acvPoint} + tune_cnt(__type="s32") #{tuneCount} + save_cnt(__type="s32") #{saveCount} + saved_cnt(__type="s32") #{savedCount} + fc_cnt(__type="s32") #{fullcomboCount} + ex_cnt(__type="s32") #{excellentCount} + match_cnt(__type="s32") #{matchCount} + beat_cnt(__type="s32") #{beatCount} + mynews_cnt(__type="s32") #{mynewsCount} + con_sel_cnt(__type="s32") #{conciergeSelectedCount} + tag_cnt(__type="s32") #{tagCount} + mtg_entry_cnt(__type="s32") 0 + tag_entry_cnt(__type="s32") 0 + mtg_hold_cnt(__type="s32") 0 + tag_hold_cnt(__type="s32") 0 + mtg_result(__type="u8") 0 + + last + play_time(__type="s64") #{last.playTime || 0} + shopname(__type="str") #{last.shopname} + areaname(__type="str") #{last.areaname} + title(__type="s16") #{last.title} + theme(__type="s8") #{last.theme} + marker(__type="s8") #{last.marker} + rank_sort(__type="s8") #{last.showRank} + combo_disp(__type="s8") #{last.showCombo} + music_id(__type="s32") #{last.musicId} + seq_id(__type="s8") #{last.seqId} + sort(__type="s8") #{last.sort} + filter(__type="s32") #{last.filter} + msel_stat(__type="s8") #{last.mselStat} + con_suggest_id(__type="s8") #{last.conciergeSuggestId} + + item + secret_list(__type="s32" __count="2") #{knit.item.secretList.join(" ")} + theme_list(__type="s16") #{knit.item.themeList} + marker_list(__type="s32" __count="2") #{knit.item.markerList.join(" ")} + title_list(__type="s32" __count="24") #{knit.item.titleList.join(" ")} + + new + secret_list(__type="s32" __count="2") #{knit.item_new.secretList.join(" ")} + theme_list(__type="s16") #{knit.item_new.themeList} + marker_list(__type="s32" __count="2") #{knit.item_new.markerList.join(" ")} + title_list(__type="s32" __count="24") #{knit.item_new.titleList.join(" ")} + + today_music + music_id(__type="s32") 0 + + news + checked(__type="s16") 0 + + friendlist(count="0") + + lucky_music + music_id(__type="s32") 0 + + mylist(count="0") + + group + group_id(__type="s32") 0 + + bingo + reward + total(__type="s32") 0 + point(__type="s32") 0 + + collabo + success(__type="bool") #{knit.collabo.success ? 1 : 0} + completed(__type="bool") #{knit.collabo.completed ? 1 : 0} + + history + play_hist(count="0") + + match_hist(count="0") diff --git a/jubeat@asphyxia/utils.ts b/jubeat@asphyxia/utils.ts new file mode 100644 index 0000000..3647c04 --- /dev/null +++ b/jubeat@asphyxia/utils.ts @@ -0,0 +1,6 @@ +export function getVersion({ model }: EamuseInfo) { + const dateCode = model.split(':')[4]; + + if (model.startsWith("J44")) return 3; + return 0; +}