mirror of
https://github.com/asphyxia-core/plugins.git
synced 2026-09-22 22:27:56 +03:00
jubeat: add Qubell (L44 2016-2017) support
The existing handlers produce festo-format responses, which Qubell rejects (gametop.regist / get_pdata fail with r=7 in the game log and the card login falls back to guest). This adds a Qubell handler set and routes requests by the model date: builds older than 2018090000 (prop / Qubell / clan) use the new handlers, festo is untouched. Qubell layout follows bemaniutils' bemani/backend/jubeat/qubell.py: - shopinfo.regist / gametop.get_info: shared info block wrapped in data/info - gametop.get_pdata / regist: full profile (info, last, item lists, jbox, digdig, unlock stage list, born, ...). get_pdata creates the profile when the card is already bound on the CORE side, since Qubell then never calls gametop.regist and treats status 109 as a login failure. - gametop.get_mdata: musicdata list with play/clear/fc/ex counts, score, clear flags and ghost bars; all scores returned on partition 1. - gameend.regist: parses info counters, last settings, item lists, jbox, events, digdig/unlock state and result tunes into qubell_profile / qubell_score collections. - gameend.final, lobby.check, demodata.get_news / get_hitchart, recommend.get_recommend: minimal valid responses. Tested with jubeat Qubell L44:J:B:A:2017062001 on bemanitools: boot, card registration, login, three-song credit, save and re-login all work.
This commit is contained in:
@@ -5,6 +5,7 @@ Jubeat Plugin for Asphyxia Core
|
||||
# Supported Versions
|
||||
|
||||
- Festo
|
||||
- Qubell (tested with L44:J:B:A:2017062001; prop and clan are routed to the same handlers but untested)
|
||||
|
||||
# Versions
|
||||
|
||||
@@ -16,6 +17,10 @@ Jubeat Plugin for Asphyxia Core
|
||||
- Support hard mode score saving
|
||||
- Support Turn Run
|
||||
|
||||
- V2.1.0 (2026/09/14)
|
||||
- Add jubeat Qubell support (profile, score list, save, shop info) via version-split handlers in `qubell/`
|
||||
- Older builds (model date < 2018090000) use the Qubell handlers; festo behaviour is unchanged
|
||||
|
||||
# TODO
|
||||
|
||||
- [ ] Customized Turn Run. (Currently can't cuz Jubeat courses limit is 60, need someone to find how to patch it.)
|
||||
|
||||
+46
-12
@@ -2,6 +2,35 @@ import ShopInfo from "./routes/shopinfo";
|
||||
import {getProfile, Getinfo, loadScore, Meeting} from "./routes/gametop";
|
||||
import {saveProfile} from "./routes/gameend";
|
||||
import {Check, Entry, Refresh, Report} from "./routes/lobby";
|
||||
import * as Q from "./qubell/qubell";
|
||||
|
||||
// Version split: the original handlers target festo (2018-09 onward). Older builds
|
||||
// (prop / Qubell / clan) are answered by the Qubell handlers in ./qubell, which follow the
|
||||
// Qubell request/response layout. The model string looks like "L44:J:B:A:2017062001".
|
||||
const isPreFesto = (info: EamuseInfo): boolean => {
|
||||
const parts = (info.model || "").split(":");
|
||||
const date = parseInt(parts[4]);
|
||||
return !isNaN(date) && date < 2018090000;
|
||||
};
|
||||
|
||||
// Never let a handler exception surface as an HTTP error: log it and deny instead.
|
||||
const guard = (name: string, fn: EamusePluginRoute): EamusePluginRoute =>
|
||||
async (info, data, send) => {
|
||||
try {
|
||||
return await fn(info, data, send);
|
||||
} catch (e) {
|
||||
console.error(`[jubeat/qubell] ${name} failed: ${e && e.stack ? e.stack : e}`);
|
||||
return send.deny();
|
||||
}
|
||||
};
|
||||
|
||||
const split = (qubell: EamusePluginRoute, festo: EamusePluginRoute | boolean): EamusePluginRoute =>
|
||||
(info, data, send) => {
|
||||
if (isPreFesto(info)) return guard(`${info.module}.${info.method}`, qubell)(info, data, send);
|
||||
if (festo === true) return send.success();
|
||||
if (festo === false) return send.deny();
|
||||
return (festo as EamusePluginRoute)(info, data, send);
|
||||
};
|
||||
|
||||
export async function register() {
|
||||
if (CORE_VERSION_MAJOR <= 1 && CORE_VERSION_MINOR < 31) {
|
||||
@@ -10,22 +39,27 @@ export async function register() {
|
||||
}
|
||||
R.GameCode("L44");
|
||||
R.Contributor("yuanqiuye", "https://github.com/yuanqiuye")
|
||||
R.Route("gametop.regist",getProfile);
|
||||
R.Route("gametop.get_info", Getinfo);
|
||||
R.Route("gametop.get_pdata", getProfile);
|
||||
R.Route("gametop.get_mdata", loadScore);
|
||||
R.Route("gametop.get_meeting", Meeting);
|
||||
|
||||
R.Route("gameend.final", true);
|
||||
R.Route("gameend.regist", saveProfile);
|
||||
|
||||
R.Route("shopinfo.regist", ShopInfo);
|
||||
R.Route("lobby.check", Check);
|
||||
R.Route("gametop.regist", split(Q.gametopRegist, getProfile));
|
||||
R.Route("gametop.get_info", split(Q.gametopGetInfo, Getinfo));
|
||||
R.Route("gametop.get_pdata", split(Q.gametopGetPdata, getProfile));
|
||||
R.Route("gametop.get_mdata", split(Q.gametopGetMdata, loadScore));
|
||||
R.Route("gametop.get_meeting", split(Q.gametopGetMeeting, Meeting));
|
||||
|
||||
R.Route("gameend.final", split(Q.gameendFinal, true));
|
||||
R.Route("gameend.regist", split(Q.gameendRegist, saveProfile));
|
||||
|
||||
R.Route("shopinfo.regist", split(Q.shopinfoRegist, ShopInfo));
|
||||
R.Route("lobby.check", split(Q.lobbyCheck, Check));
|
||||
R.Route("lobby.entry", Entry);
|
||||
R.Route("lobby.refresh", Refresh);
|
||||
R.Route("lobby.report", Report);
|
||||
|
||||
|
||||
R.Route("demodata.get_news", split(Q.demodataGetNews, true));
|
||||
R.Route("demodata.get_hitchart", split(Q.demodataGetHitchart, true));
|
||||
R.Route("recommend.get_recommend", split(Q.recommendGetRecommend, true));
|
||||
|
||||
R.Route("netlog.send", true);
|
||||
R.Route("logger.report", true);
|
||||
R.Unhandled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
declare const BigInt: any; // CORE typecheck targets es2017
|
||||
// jubeat Qubell (L44 2016-2017) handlers for Asphyxia CORE.
|
||||
// Response layout follows bemaniutils' bemani/backend/jubeat/qubell.py (format_profile,
|
||||
// format_scores, unformat_profile, __get_global_info) so the game accepts it.
|
||||
// Profiles live in collection "qubell_profile", scores in "qubell_score" (separate from the
|
||||
// festo collections used by the original plugin code).
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// helpers
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
const fill = (n: number, v: number): number[] => new Array(n).fill(v);
|
||||
const s32a = (v: number[]) => K.ARRAY("s32", v);
|
||||
const fix = (v: any, n: number, def: number): number[] => {
|
||||
const a = Array.isArray(v) ? v.slice(0, n) : [];
|
||||
while (a.length < n) a.push(def);
|
||||
return a;
|
||||
};
|
||||
|
||||
const FLAG_PLAYED = 0x1, FLAG_CLEARED = 0x2, FLAG_FC = 0x4, FLAG_EX = 0x8;
|
||||
|
||||
export interface QProfile {
|
||||
collection: "qubell_profile";
|
||||
jid: number;
|
||||
name: string;
|
||||
saved: boolean;
|
||||
jubility: number; jubility_yday: number;
|
||||
tune_cnt: number; save_cnt: number; saved_cnt: number; fc_cnt: number; ex_cnt: number;
|
||||
clear_cnt: number; match_cnt: number; beat_cnt: number; mynews_cnt: number;
|
||||
bonus_tune_points: number; is_bonus_tune_played: boolean;
|
||||
last: {
|
||||
play_time: number; shopname: string; areaname: string;
|
||||
music_id: number; seq_id: number; sort: number; category: number;
|
||||
expert_option: number; dig_select: number;
|
||||
marker: number; theme: number; title: number; parts: number;
|
||||
rank_sort: number; combo_disp: number; matching: number; hazard: number; hard: number;
|
||||
emblem: number[];
|
||||
};
|
||||
music_list: number[]; theme_list: number[]; marker_list: number[];
|
||||
title_list: number[]; parts_list: number[]; emblem_list: number[]; secret_list: number[];
|
||||
theme_list_new: number[]; marker_list_new: number[]; secret_list_new: number[];
|
||||
jbox_point: number; jbox_normal_index: number; jbox_premium_index: number;
|
||||
navi_flag: number;
|
||||
born_status: number; born_year: number;
|
||||
events: { [type: string]: boolean };
|
||||
digdig: any;
|
||||
stages: { [num: string]: number };
|
||||
}
|
||||
|
||||
export interface QScore {
|
||||
collection: "qubell_score";
|
||||
musicId: number; seq: number;
|
||||
score: number; clear: number;
|
||||
playCnt: number; clearCnt: number; fcCnt: number; exCnt: number;
|
||||
bar: number[];
|
||||
}
|
||||
|
||||
const newProfile = (name: string, jid: number): QProfile => ({
|
||||
collection: "qubell_profile",
|
||||
jid, name, saved: false,
|
||||
jubility: 0, jubility_yday: 0,
|
||||
tune_cnt: 0, save_cnt: 0, saved_cnt: 0, fc_cnt: 0, ex_cnt: 0,
|
||||
clear_cnt: 0, match_cnt: 0, beat_cnt: 0, mynews_cnt: 0,
|
||||
bonus_tune_points: 0, is_bonus_tune_played: false,
|
||||
last: {
|
||||
play_time: 0, shopname: "", areaname: "",
|
||||
music_id: 0, seq_id: 0, sort: 0, category: 0, expert_option: 0, dig_select: 0,
|
||||
marker: 0, theme: 0, title: 0, parts: 0, rank_sort: 0, combo_disp: 0,
|
||||
matching: 0, hazard: 0, hard: 0, emblem: [0, 0, 0, 0, 0],
|
||||
},
|
||||
music_list: fill(64, -1), theme_list: fill(16, -1), marker_list: fill(16, -1),
|
||||
title_list: fill(160, -1), parts_list: fill(160, -1), emblem_list: fill(96, -1),
|
||||
secret_list: fill(64, -1),
|
||||
theme_list_new: fill(16, -1), marker_list_new: fill(16, -1), secret_list_new: fill(64, -1),
|
||||
jbox_point: 0, jbox_normal_index: 2, jbox_premium_index: 1,
|
||||
navi_flag: 0, born_status: 0, born_year: 0,
|
||||
events: {}, digdig: {}, stages: {},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// global "info" node (shopinfo.regist, gametop.get_info, and inside every profile)
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
const EVENTS = [5, 6, 15, 19];
|
||||
|
||||
export const qubellInfo = () => ({
|
||||
event_info: {
|
||||
event: EVENTS.map(e => K.ATTR({ type: String(e) }, { state: K.ITEM("u8", 0) })),
|
||||
},
|
||||
share_music: {},
|
||||
bonus_music: {},
|
||||
white_music_list: s32a(fill(64, -1)),
|
||||
white_marker_list: s32a(fill(16, -1)),
|
||||
white_theme_list: s32a(fill(16, -1)),
|
||||
open_music_list: s32a(fill(64, -1)),
|
||||
shareable_music_list: s32a(fill(64, -1)),
|
||||
jbox: {
|
||||
point: K.ITEM("s32", 0),
|
||||
emblem: {
|
||||
normal: { index: K.ITEM("s16", 2) },
|
||||
premium: { index: K.ITEM("s16", 1) },
|
||||
},
|
||||
},
|
||||
born: { status: K.ITEM("s8", 0), year: K.ITEM("s16", 0) },
|
||||
digdig: {
|
||||
stage_list: {
|
||||
stage: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].map(n =>
|
||||
K.ATTR({ number: String(n) }, { state: K.ITEM("u8", 1) })),
|
||||
},
|
||||
},
|
||||
collection: { rating_s: {} },
|
||||
generic_dig: { map_list: {} },
|
||||
expert_option: { is_available: K.ITEM("bool", true) },
|
||||
tsumtsum: { is_available: K.ITEM("bool", true) },
|
||||
nagatanien: { is_available: K.ITEM("bool", true) },
|
||||
all_music_matching: { is_available: K.ITEM("bool", true) },
|
||||
question_list: {},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// profile -> response
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
const formatProfile = (p: QProfile) => {
|
||||
const l = p.last;
|
||||
const emblem = fix(l.emblem, 5, 0);
|
||||
if (emblem[1] === 0) emblem[1] = 2; // default main part, as bemaniutils does
|
||||
const dd = p.digdig || {};
|
||||
const et = dd.eternal || {};
|
||||
return {
|
||||
data: {
|
||||
info: qubellInfo(),
|
||||
player: {
|
||||
server: {},
|
||||
name: K.ITEM("str", p.name),
|
||||
jid: K.ITEM("s32", p.jid),
|
||||
session_id: K.ITEM("s32", 1),
|
||||
event_flag: K.ITEM("u64", BigInt(0)),
|
||||
info: {
|
||||
jubility: K.ITEM("s16", p.jubility | 0),
|
||||
jubility_yday: K.ITEM("s16", p.jubility_yday | 0),
|
||||
tune_cnt: K.ITEM("s32", p.tune_cnt | 0),
|
||||
save_cnt: K.ITEM("s32", p.save_cnt | 0),
|
||||
saved_cnt: K.ITEM("s32", p.saved_cnt | 0),
|
||||
fc_cnt: K.ITEM("s32", p.fc_cnt | 0),
|
||||
ex_cnt: K.ITEM("s32", p.ex_cnt | 0),
|
||||
clear_cnt: K.ITEM("s32", p.clear_cnt | 0),
|
||||
match_cnt: K.ITEM("s32", p.match_cnt | 0),
|
||||
beat_cnt: K.ITEM("s32", p.beat_cnt | 0),
|
||||
mynews_cnt: K.ITEM("s32", p.mynews_cnt | 0),
|
||||
bonus_tune_points: K.ITEM("s32", p.bonus_tune_points | 0),
|
||||
is_bonus_tune_played: K.ITEM("bool", !!p.is_bonus_tune_played),
|
||||
// "inherit" true = treat as returning player (no tutorial) on the first save
|
||||
inherit: K.ITEM("bool", !p.saved),
|
||||
mtg_entry_cnt: K.ITEM("s32", 123),
|
||||
mtg_hold_cnt: K.ITEM("s32", 456),
|
||||
mtg_result: K.ITEM("u8", 10),
|
||||
},
|
||||
last: {
|
||||
play_time: K.ITEM("s64", BigInt(l.play_time | 0)),
|
||||
shopname: K.ITEM("str", l.shopname || ""),
|
||||
areaname: K.ITEM("str", l.areaname || ""),
|
||||
music_id: K.ITEM("s32", l.music_id | 0),
|
||||
seq_id: K.ITEM("s8", l.seq_id | 0),
|
||||
sort: K.ITEM("s8", l.sort | 0),
|
||||
category: K.ITEM("s8", l.category | 0),
|
||||
expert_option: K.ITEM("s8", l.expert_option | 0),
|
||||
dig_select: K.ITEM("s32", l.dig_select | 0),
|
||||
settings: {
|
||||
marker: K.ITEM("s8", l.marker | 0),
|
||||
theme: K.ITEM("s8", l.theme | 0),
|
||||
title: K.ITEM("s16", l.title | 0),
|
||||
parts: K.ITEM("s16", l.parts | 0),
|
||||
rank_sort: K.ITEM("s8", l.rank_sort | 0),
|
||||
combo_disp: K.ITEM("s8", l.combo_disp | 0),
|
||||
matching: K.ITEM("s8", l.matching | 0),
|
||||
hazard: K.ITEM("s8", l.hazard | 0),
|
||||
hard: K.ITEM("s8", l.hard | 0),
|
||||
emblem: K.ARRAY("s16", emblem),
|
||||
},
|
||||
},
|
||||
item: {
|
||||
music_list: s32a(fix(p.music_list, 64, -1)),
|
||||
secret_list: s32a(fill(64, -1)), // force-unlock everything
|
||||
theme_list: s32a(fix(p.theme_list, 16, -1)),
|
||||
marker_list: s32a(fix(p.marker_list, 16, -1)),
|
||||
title_list: s32a(fix(p.title_list, 160, -1)),
|
||||
parts_list: s32a(fix(p.parts_list, 160, -1)),
|
||||
emblem_list: s32a(fix(p.emblem_list, 96, -1)),
|
||||
new: {
|
||||
secret_list: s32a(fill(64, -1)),
|
||||
theme_list: s32a(fix(p.theme_list_new, 16, -1)),
|
||||
marker_list: s32a(fix(p.marker_list_new, 16, -1)),
|
||||
},
|
||||
},
|
||||
rivallist: K.ATTR({ count: "0" }),
|
||||
lab_edit_seq: K.ATTR({ count: "0" }),
|
||||
fc_challenge: {
|
||||
today: { music_id: K.ITEM("s32", -1), state: K.ITEM("u8", 0) },
|
||||
whim: { music_id: K.ITEM("s32", -1), state: K.ITEM("u8", 0) },
|
||||
},
|
||||
news: { checked: K.ITEM("s16", 0), checked_flag: K.ITEM("u32", 0) },
|
||||
history: K.ATTR({ count: "0" }),
|
||||
free_first_play: { is_available: K.ITEM("bool", false) },
|
||||
navi: { flag: K.ITEM("u64", BigInt(p.navi_flag | 0)) },
|
||||
event_info: {
|
||||
event: Object.keys(p.events || {}).map(t =>
|
||||
K.ATTR({ type: t }, { state: K.ITEM("u8", p.events[t] ? 2 : 0) })),
|
||||
},
|
||||
jbox: {
|
||||
point: K.ITEM("s32", p.jbox_point | 0),
|
||||
emblem: {
|
||||
normal: { index: K.ITEM("s16", p.jbox_normal_index || 2) },
|
||||
premium: { index: K.ITEM("s16", p.jbox_premium_index || 1) },
|
||||
},
|
||||
},
|
||||
digdig: {
|
||||
flag: K.ITEM("u64", BigInt(dd.flag | 0)),
|
||||
main: {
|
||||
stage: K.ATTR({ number: String(dd.stage_number || 1) }, {
|
||||
point: K.ITEM("s32", dd.point | 0),
|
||||
param: s32a(fix(dd.param, 12, 0)),
|
||||
}),
|
||||
},
|
||||
eternal: {
|
||||
ratio: K.ITEM("s32", 1),
|
||||
used_point: K.ITEM("s64", BigInt(et.used_point | 0)),
|
||||
point: K.ITEM("s64", BigInt(et.point | 0)),
|
||||
excavated_point: K.ITEM("s64", BigInt(et.excavated_point | 0)),
|
||||
cube: {
|
||||
state: K.ARRAY("s8", fix(et.state, 12, 0)),
|
||||
item: {
|
||||
kind: s32a(fix(et.item_kind, 12, 0)),
|
||||
value: s32a(fix(et.item_value, 12, 0)),
|
||||
},
|
||||
norma: {
|
||||
till_time: K.ARRAY("s64", fill(12, 0).map(() => BigInt(0))),
|
||||
kind: s32a(fix(et.norma_kind, 12, 0)),
|
||||
value: s32a(fix(et.norma_value, 12, 0)),
|
||||
param: s32a(fix(et.norma_param, 12, 0)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
unlock: {
|
||||
main: {
|
||||
stage_list: {
|
||||
stage: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].map(n =>
|
||||
K.ATTR({ number: String(n) }, { state: K.ITEM("u8", (p.stages && p.stages[String(n)]) | 0) })),
|
||||
},
|
||||
},
|
||||
},
|
||||
generic_dig: { map_list: {} },
|
||||
new_music: {},
|
||||
gift_list: {},
|
||||
born: { status: K.ITEM("s8", p.born_status | 0), year: K.ITEM("s16", p.born_year | 0) },
|
||||
question_list: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// DB access
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
const findProfile = (refid: string) =>
|
||||
DB.FindOne<QProfile>(refid, { collection: "qubell_profile" });
|
||||
|
||||
const nextJid = async (): Promise<number> => {
|
||||
// 8-digit id, retried until unused
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const jid = 10000000 + Math.floor(Math.random() * 89999999);
|
||||
const hit = await DB.FindOne<QProfile>(null, { collection: "qubell_profile", jid });
|
||||
if (!hit) return jid;
|
||||
}
|
||||
return 10000000 + Math.floor(Math.random() * 89999999);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// routes
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
export const shopinfoRegist = (info: EamuseInfo, data: any, send: EamuseSend) => {
|
||||
const locId = $(data).str("data.shop.locationid", "nowhere");
|
||||
return send.object({
|
||||
data: {
|
||||
cabid: K.ITEM("u32", 1),
|
||||
locationid: K.ITEM("str", locId || "nowhere"),
|
||||
tax_phase: K.ITEM("u8", 1),
|
||||
facility: { exist: K.ITEM("u32", 1) },
|
||||
info: qubellInfo(),
|
||||
},
|
||||
}, { compress: true });
|
||||
};
|
||||
|
||||
export const gametopGetInfo = (info: EamuseInfo, data: any, send: EamuseSend) =>
|
||||
send.object({ data: { info: qubellInfo() } }, { compress: true });
|
||||
|
||||
export const gametopGetMeeting = (info: EamuseInfo, data: any, send: EamuseSend) =>
|
||||
send.object({
|
||||
data: {
|
||||
meeting: { single: K.ATTR({ count: "0" }), tag: K.ATTR({ count: "0" }) },
|
||||
reward: { total: K.ITEM("s32", -1), point: K.ITEM("s32", -1) },
|
||||
},
|
||||
}, { compress: true });
|
||||
|
||||
export const gametopRegist = async (info: EamuseInfo, data: any, send: EamuseSend) => {
|
||||
const refid = $(data).str("data.player.refid");
|
||||
const name = $(data).str("data.player.name") || "なし";
|
||||
if (!refid) return send.deny();
|
||||
let p = await findProfile(refid);
|
||||
if (!p) {
|
||||
const np = newProfile(name, await nextJid());
|
||||
await DB.Upsert<QProfile>(refid, { collection: "qubell_profile" }, np);
|
||||
p = await findProfile(refid);
|
||||
}
|
||||
console.log(`[qubell] gametop.regist -> jid ${p.jid} name ${p.name}`);
|
||||
return send.object(formatProfile(p), { compress: true });
|
||||
};
|
||||
|
||||
export const gametopGetPdata = async (info: EamuseInfo, data: any, send: EamuseSend) => {
|
||||
const refid = $(data).str("data.player.refid");
|
||||
if (!refid) return send.deny();
|
||||
let p = await findProfile(refid);
|
||||
if (!p) {
|
||||
// The card is already bound to this game on the CORE side (it went through registration
|
||||
// once with the festo-format code), so Qubell never calls gametop.regist again and treats a
|
||||
// "no profile" status as a login failure. Create the Qubell profile here instead, reusing
|
||||
// the name from the old festo-format profile if one exists.
|
||||
const old: any = await DB.FindOne<any>(refid, { collection: "profile" });
|
||||
const name = (old && old.name) ? String(old.name) : "なし";
|
||||
await DB.Upsert<QProfile>(refid, { collection: "qubell_profile" }, newProfile(name, await nextJid()));
|
||||
p = await findProfile(refid);
|
||||
console.log(`[qubell] gametop.get_pdata: created profile on the fly, name=${name} jid=${p.jid}`);
|
||||
}
|
||||
console.log(`[qubell] gametop.get_pdata -> jid ${p.jid} name ${p.name}`);
|
||||
return send.object(formatProfile(p), { compress: true });
|
||||
};
|
||||
|
||||
export const gametopGetMdata = async (info: EamuseInfo, data: any, send: EamuseSend) => {
|
||||
const jid = $(data).number("data.player.jid");
|
||||
const mdataVer = $(data).number("data.player.mdata_ver", 1);
|
||||
if (!jid) return send.deny();
|
||||
const p = await DB.FindOne<QProfile>(null, { collection: "qubell_profile", jid });
|
||||
if (!p) return send.status(109);
|
||||
|
||||
// all scores go out in partition 1; later partitions are empty
|
||||
const scores = mdataVer === 1
|
||||
? await DB.Find<QScore>(p.__refid, { collection: "qubell_score" })
|
||||
: [];
|
||||
|
||||
const byMusic: { [id: string]: QScore[] } = {};
|
||||
for (const s of scores) (byMusic[String(s.musicId)] = byMusic[String(s.musicId)] || []).push(s);
|
||||
|
||||
const musicdata = Object.keys(byMusic).map(id => {
|
||||
const play = [0, 0, 0], clr = [0, 0, 0], fc = [0, 0, 0], ex = [0, 0, 0], pts = [0, 0, 0], flags = [0, 0, 0];
|
||||
const bars: any[] = [];
|
||||
for (const s of byMusic[id]) {
|
||||
if (s.seq < 0 || s.seq > 2) continue;
|
||||
play[s.seq] = s.playCnt | 0; clr[s.seq] = s.clearCnt | 0; fc[s.seq] = s.fcCnt | 0; ex[s.seq] = s.exCnt | 0;
|
||||
pts[s.seq] = s.score | 0;
|
||||
let f = FLAG_PLAYED;
|
||||
if (s.clearCnt > 0) f |= FLAG_CLEARED;
|
||||
if (s.fcCnt > 0) f |= FLAG_FC;
|
||||
if (s.exCnt > 0) f |= FLAG_EX;
|
||||
flags[s.seq] = f;
|
||||
if (s.bar && s.bar.length) bars.push(K.ARRAY("u8", fix(s.bar, 30, 0), { seq: String(s.seq) }));
|
||||
}
|
||||
return K.ATTR({ music_id: id }, {
|
||||
play_cnt: s32a(play), clear_cnt: s32a(clr), fc_cnt: s32a(fc), ex_cnt: s32a(ex),
|
||||
score: s32a(pts), clear: K.ARRAY("s8", flags),
|
||||
...(bars.length ? { bar: bars } : {}),
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`[qubell] gametop.get_mdata jid ${jid} ver ${mdataVer} -> ${musicdata.length} songs`);
|
||||
return send.object({
|
||||
data: { player: { jid: K.ITEM("s32", jid), mdata_list: { musicdata } } },
|
||||
}, { compress: true });
|
||||
};
|
||||
|
||||
export const gameendRegist = async (info: EamuseInfo, data: any, send: EamuseSend) => {
|
||||
const refid = $(data).str("data.player.refid");
|
||||
if (!refid) return send.deny();
|
||||
const p = await findProfile(refid);
|
||||
if (!p) return send.deny();
|
||||
const d = $(data);
|
||||
const num = (path: string, def: number) => { const v = d.number(path); return (v === undefined || v === null || isNaN(v)) ? def : v; };
|
||||
const nums = (path: string, n: number, cur: number[]) => { const v = d.numbers(path); return (v && v.length) ? fix(v, n, -1) : cur; };
|
||||
|
||||
p.saved = true;
|
||||
p.last.play_time = num("data.info.time_gameend", p.last.play_time);
|
||||
p.last.shopname = d.str("data.info.shopname") || p.last.shopname;
|
||||
p.last.areaname = d.str("data.info.areaname") || p.last.areaname;
|
||||
|
||||
const I = "data.player.info.";
|
||||
p.jubility = num(I + "jubility", p.jubility); p.jubility_yday = num(I + "jubility_yday", p.jubility_yday);
|
||||
p.tune_cnt = num(I + "tune_cnt", p.tune_cnt); p.save_cnt = num(I + "save_cnt", p.save_cnt);
|
||||
p.saved_cnt = num(I + "saved_cnt", p.saved_cnt); p.fc_cnt = num(I + "fc_cnt", p.fc_cnt);
|
||||
p.ex_cnt = num(I + "ex_cnt", p.ex_cnt); p.clear_cnt = num(I + "clear_cnt", p.clear_cnt);
|
||||
p.match_cnt = num(I + "match_cnt", p.match_cnt); p.beat_cnt = num(I + "beat_cnt", p.beat_cnt);
|
||||
p.mynews_cnt = num(I + "mynews_cnt", p.mynews_cnt);
|
||||
p.bonus_tune_points = num(I + "bonus_tune_points", p.bonus_tune_points);
|
||||
p.is_bonus_tune_played = d.bool(I + "is_bonus_tune_played");
|
||||
|
||||
const L = "data.player.last.";
|
||||
p.last.expert_option = num(L + "expert_option", p.last.expert_option);
|
||||
p.last.dig_select = num(L + "dig_select", p.last.dig_select);
|
||||
p.last.sort = num(L + "sort", p.last.sort);
|
||||
p.last.category = num(L + "category", p.last.category);
|
||||
const S = L + "settings.";
|
||||
p.last.matching = num(S + "matching", p.last.matching); p.last.hazard = num(S + "hazard", p.last.hazard);
|
||||
p.last.hard = num(S + "hard", p.last.hard); p.last.marker = num(S + "marker", p.last.marker);
|
||||
p.last.theme = num(S + "theme", p.last.theme); p.last.title = num(S + "title", p.last.title);
|
||||
p.last.parts = num(S + "parts", p.last.parts); p.last.rank_sort = num(S + "rank_sort", p.last.rank_sort);
|
||||
p.last.combo_disp = num(S + "combo_disp", p.last.combo_disp);
|
||||
p.last.emblem = fix(d.numbers(S + "emblem") || p.last.emblem, 5, 0);
|
||||
|
||||
const T = "data.player.item.";
|
||||
p.music_list = nums(T + "music_list", 64, p.music_list);
|
||||
p.theme_list = nums(T + "theme_list", 16, p.theme_list);
|
||||
p.marker_list = nums(T + "marker_list", 16, p.marker_list);
|
||||
p.title_list = nums(T + "title_list", 160, p.title_list);
|
||||
p.parts_list = nums(T + "parts_list", 160, p.parts_list);
|
||||
p.emblem_list = nums(T + "emblem_list", 96, p.emblem_list);
|
||||
p.theme_list_new = nums(T + "new.theme_list", 16, p.theme_list_new);
|
||||
p.marker_list_new = nums(T + "new.marker_list", 16, p.marker_list_new);
|
||||
|
||||
p.jbox_point = num("data.player.jbox.point", p.jbox_point);
|
||||
const jbType = num("data.player.jbox.emblem.type", 0), jbIndex = num("data.player.jbox.emblem.index", 0);
|
||||
if (jbType === 1) p.jbox_normal_index = jbIndex;
|
||||
else if (jbType === 2) p.jbox_premium_index = jbIndex;
|
||||
|
||||
p.navi_flag = num("data.player.navi.flag", p.navi_flag);
|
||||
p.born_status = num("data.player.born.status", p.born_status);
|
||||
p.born_year = num("data.player.born.year", p.born_year);
|
||||
|
||||
for (const ev of d.elements("data.player.event_info.event") || []) {
|
||||
const t = ev.attr("").type;
|
||||
if (t !== undefined) p.events[String(t)] = ev.bool("is_completed");
|
||||
}
|
||||
|
||||
const dd = p.digdig || {};
|
||||
if (d.element("data.player.digdig")) {
|
||||
dd.flag = num("data.player.digdig.flag", dd.flag | 0);
|
||||
const st = d.element("data.player.digdig.main.stage");
|
||||
if (st) {
|
||||
const sn = parseInt(d.attr("data.player.digdig.main.stage").number);
|
||||
if (!isNaN(sn)) dd.stage_number = sn;
|
||||
dd.point = num("data.player.digdig.main.stage.point", dd.point | 0);
|
||||
dd.param = fix(d.numbers("data.player.digdig.main.stage.param") || dd.param, 12, 0);
|
||||
if (d.bool("data.player.digdig.main.stage.uc_available") && !isNaN(sn)) {
|
||||
p.stages[String(sn)] = (p.stages[String(sn)] | 0) | 0x2;
|
||||
}
|
||||
}
|
||||
if (d.element("data.player.digdig.eternal")) {
|
||||
const E = "data.player.digdig.eternal.";
|
||||
dd.eternal = dd.eternal || {};
|
||||
dd.eternal.used_point = num(E + "used_point", 0);
|
||||
dd.eternal.point = num(E + "point", 0);
|
||||
dd.eternal.excavated_point = num(E + "excavated_point", 0);
|
||||
dd.eternal.state = fix(d.numbers(E + "cube.state"), 12, 0);
|
||||
dd.eternal.item_kind = fix(d.numbers(E + "cube.item.kind"), 12, 0);
|
||||
dd.eternal.item_value = fix(d.numbers(E + "cube.item.value"), 12, 0);
|
||||
dd.eternal.norma_kind = fix(d.numbers(E + "cube.norma.kind"), 12, 0);
|
||||
dd.eternal.norma_value = fix(d.numbers(E + "cube.norma.value"), 12, 0);
|
||||
dd.eternal.norma_param = fix(d.numbers(E + "cube.norma.param"), 12, 0);
|
||||
}
|
||||
}
|
||||
const ust = d.element("data.player.unlock.main.stage");
|
||||
if (ust) {
|
||||
const sn = parseInt(d.attr("data.player.unlock.main.stage").number);
|
||||
const state = num("data.player.unlock.main.stage.state", 0);
|
||||
if (!isNaN(sn)) {
|
||||
p.stages[String(sn)] = state;
|
||||
if (sn === 13 && (state & 0x18) > 0) dd.flag = (dd.flag | 0) | 0x2;
|
||||
}
|
||||
}
|
||||
p.digdig = dd;
|
||||
|
||||
// scores
|
||||
let saved = 0;
|
||||
for (const tune of d.elements("data.result.tune") || []) {
|
||||
const musicId = tune.number("music");
|
||||
const sa = tune.attr("player.score") || {};
|
||||
const seq = parseInt(sa.seq), flags = parseInt(sa.clear) | 0;
|
||||
const points = tune.number("player.score") | 0;
|
||||
const bar = tune.numbers("player.mbar");
|
||||
if (!musicId || isNaN(seq)) continue;
|
||||
p.last.music_id = musicId; p.last.seq_id = seq;
|
||||
const old = await DB.FindOne<QScore>(refid, { collection: "qubell_score", musicId, seq });
|
||||
const ns: QScore = old || { collection: "qubell_score", musicId, seq, score: 0, clear: 0, playCnt: 0, clearCnt: 0, fcCnt: 0, exCnt: 0, bar: [] };
|
||||
ns.playCnt++;
|
||||
if (flags & FLAG_CLEARED) ns.clearCnt++;
|
||||
if (flags & FLAG_FC) ns.fcCnt++;
|
||||
if (flags & FLAG_EX) ns.exCnt++;
|
||||
ns.score = Math.max(ns.score | 0, points);
|
||||
ns.clear = (ns.clear | 0) | flags;
|
||||
if (bar && bar.length) ns.bar = bar;
|
||||
await DB.Upsert<QScore>(refid, { collection: "qubell_score", musicId, seq }, {
|
||||
$set: { musicId, seq, score: ns.score, clear: ns.clear, playCnt: ns.playCnt, clearCnt: ns.clearCnt, fcCnt: ns.fcCnt, exCnt: ns.exCnt, bar: ns.bar },
|
||||
});
|
||||
saved++;
|
||||
}
|
||||
|
||||
const { __refid, _id, createdAt, updatedAt, ...plain } = p as any;
|
||||
await DB.Update<QProfile>(refid, { collection: "qubell_profile" }, { $set: plain });
|
||||
console.log(`[qubell] gameend.regist jid ${p.jid}: ${saved} tune(s) saved`);
|
||||
return send.object({
|
||||
data: { player: { session_id: K.ITEM("s32", 1), end_final_session_id: K.ITEM("s32", 1) } },
|
||||
}, { compress: true });
|
||||
};
|
||||
|
||||
export const gameendFinal = async (info: EamuseInfo, data: any, send: EamuseSend) => {
|
||||
const refid = $(data).str("data.player.refid");
|
||||
if (refid) {
|
||||
const p = await findProfile(refid);
|
||||
if (p) {
|
||||
const d = $(data);
|
||||
const jbType = d.number("data.player.jbox.emblem.type"), jbIndex = d.number("data.player.jbox.emblem.index");
|
||||
const upd: any = {};
|
||||
const pt = d.number("data.player.jbox.point"); if (pt !== undefined && !isNaN(pt)) upd.jbox_point = pt;
|
||||
if (jbType === 1) upd.jbox_normal_index = jbIndex; else if (jbType === 2) upd.jbox_premium_index = jbIndex;
|
||||
const bs = d.number("data.player.born.status"); if (bs !== undefined && !isNaN(bs)) upd.born_status = bs;
|
||||
const by = d.number("data.player.born.year"); if (by !== undefined && !isNaN(by)) upd.born_year = by;
|
||||
if (Object.keys(upd).length) await DB.Update<QProfile>(refid, { collection: "qubell_profile" }, { $set: upd });
|
||||
}
|
||||
}
|
||||
return send.success();
|
||||
};
|
||||
|
||||
export const lobbyCheck = (info: EamuseInfo, data: any, send: EamuseSend) =>
|
||||
send.object({
|
||||
data: {
|
||||
interval: K.ITEM("s16", 0),
|
||||
entry_timeout: K.ITEM("s16", 0),
|
||||
entrant_nr: K.ITEM("u32", 0, { time: "0" }),
|
||||
},
|
||||
}, { compress: true });
|
||||
|
||||
export const demodataGetNews = (info: EamuseInfo, data: any, send: EamuseSend) =>
|
||||
send.object({ data: { officialnews: K.ATTR({ count: "0" }) } }, { compress: true });
|
||||
|
||||
export const demodataGetHitchart = (info: EamuseInfo, data: any, send: EamuseSend) => {
|
||||
const now = new Date();
|
||||
const pad = (n: number) => (n < 10 ? "0" : "") + n;
|
||||
return send.object({
|
||||
data: {
|
||||
update: K.ITEM("str", `${pad(now.getDate())}/${pad(now.getMonth() + 1)}/${now.getFullYear()}`),
|
||||
hitchart_lic: K.ATTR({ count: "0" }),
|
||||
hitchart_org: K.ATTR({ count: "0" }),
|
||||
},
|
||||
}, { compress: true });
|
||||
};
|
||||
|
||||
export const recommendGetRecommend = (info: EamuseInfo, data: any, send: EamuseSend) =>
|
||||
send.object({ data: { player: { music_list: {} } } }, { compress: true });
|
||||
Reference in New Issue
Block a user