Files
asphyxia-core_plugins/jubeat@asphyxia/index.ts
T
barbarburrito 2b21c3d8b5 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.
2026-09-14 18:48:09 +00:00

66 lines
2.8 KiB
TypeScript

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) {
console.error("The current version of Asphyxia Core is not supported. Requires version '1.31' or later.");
return;
}
R.GameCode("L44");
R.Contributor("yuanqiuye", "https://github.com/yuanqiuye")
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();
}