Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dancearound@asphyxia/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# DANCE aROUND

DANCE aROUND (`UDN`) plugin for Asphyxia CORE.

Drag the `dancearound@asphyxia` folder into CORE's `plugins` folder. Set the
local game path in the plugin settings to load and unlock the installed music
catalog.
127 changes: 127 additions & 0 deletions dancearound@asphyxia/handlers/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/// <reference lib="es2020.bigint" />

import { nowMs, readNumber, readString } from '../utils';
import { loadLocalMdb, MdbMusic } from '../mdb';

function mdbMusicResponse(music: MdbMusic): any {
return {
music_id: K.ITEM('s32', music.music_id),
title_name: K.ITEM('str', music.title_name),
title_yomigana: K.ITEM('str', music.title_yomigana),
artist_name: K.ITEM('str', music.artist_name),
artist_yomigana: K.ITEM('str', music.artist_yomigana),
bpm_max: K.ITEM('s32', music.bpm_max),
bpm_min: K.ITEM('s32', music.bpm_min),
distribution_date: K.ITEM('str', music.distribution_date),
release_code: K.ITEM('str', music.release_code),
volume: K.ITEM('s32', music.volume),
bg_no: K.ITEM('s32', music.bg_no),
region: K.ITEM('s32', music.region),
tags: {
tag: music.tags.map(tag => K.ITEM('str', tag)),
},
limitation_type: K.ITEM('s32', music.limitation_type),
license: K.ITEM('str', music.license),
color1: K.ITEM('str', music.color1),
color2: K.ITEM('str', music.color2),
color3: K.ITEM('str', music.color3),
has_mv: K.ITEM('bool', music.has_mv),
demo_pri: K.ITEM('s32', music.demo_pri),
fumens: {
fumen: music.fumens.map(fumen => ({
fumen_type: K.ITEM('str', fumen.fumen_type),
level: K.ITEM('s32', fumen.level),
playable: K.ITEM('s32', fumen.playable),
has_official_dance: K.ITEM('s32', fumen.has_official_dance),
backdancer_id: K.ITEM('s32', fumen.backdancer_id),
price: K.ITEM('s32', fumen.price),
limitation_type: K.ITEM('s32', fumen.limitation_type),
})),
},
video_flags: {
JP: K.ITEM('s32', music.video_flags.JP),
US: K.ITEM('s32', music.video_flags.US),
},
motion_flags: {
JP: K.ITEM('s32', music.motion_flags.JP),
US: K.ITEM('s32', music.motion_flags.US),
},
};
}

export const getCommon: EPR = async (info, data, send) => {
const catalog = U.GetConfig('unlock_all_songs') ? await loadLocalMdb() : null;
return send.object(
{
mdb: {
music: catalog ? catalog.musics.map(mdbMusicResponse) : [],
},
event: {},
},
{ status: 0 }
);
};

export const lockMultiLogin: EPR = async (info, data, send) => {
return send.object(
{
result: K.ITEM('s32', 0),
},
{ status: 0 }
);
};

export const savePcbData: EPR = async (info, data, send) => {
const reader = $(data);
await DB.Upsert(
{ collection: 'pcb' } as any,
{
$set: {
collection: 'pcb',
model: info.model,
locationId: readString(reader, 'pcbinfo.loc_id'),
region: readNumber(reader, 'pcbinfo.region'),
locationName: readString(reader, 'pcbinfo.locname'),
customer: readString(reader, 'pcbinfo.customer'),
company: readString(reader, 'pcbinfo.company'),
systemId: readString(reader, 'pcbinfo.system_id'),
hardwareId: readString(reader, 'pcbinfo.hardware_id'),
licenseId: readString(reader, 'pcbinfo.license_id'),
accountId: readString(reader, 'pcbinfo.account_id'),
boot: readNumber(reader, 'pcbinfo.boot'),
eacoinStatus: readNumber(reader, 'pcbinfo.eacoin_status'),
updateProgress: readNumber(reader, 'pcbinfo.update_progress'),
shopName: readString(reader, 'etc.network.shopname'),
matchingGroup: readString(reader, 'etc.network.matching_group'),
updatedAt: nowMs(),
},
} as any
);

return send.object(
{
next_request_interval_sec: K.ITEM('u64', BigInt(300)),
},
{ status: 0 }
);
};

export const saveLog: EPR = async (info, data, send) => {
const reader = $(data);
const items = reader.elements('logdata');
await DB.Upsert(
{ collection: 'telemetry' } as any,
{
$set: {
collection: 'telemetry',
lastModel: info.model,
lastDataKey: items.length
? readString(items[items.length - 1], 'data_key')
: '',
updatedAt: nowMs(),
},
$inc: { receivedLogBatches: 1, receivedLogItems: items.length },
} as any
);
return send.success();
};
208 changes: 208 additions & 0 deletions dancearound@asphyxia/handlers/motion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/// <reference lib="es2020.bigint" />

declare const Buffer: any;

import { MotionDocument, MotionEntry, ProfileDocument } from '../models';
import {
avatarResponse,
createProfile,
dancerGradeResponse,
makeUuid,
motionInfoResponse,
nowMs,
readAvatar,
readNumber,
readStageResult,
readString,
} from '../utils';

function readMotionIds(reader: KDataReader): string[] {
const values = reader
.elements('motion_id_list.motion_id')
.map(item => readString(item, ''));
for (const dataItem of reader.elements('data_item')) {
values.push(
...dataItem
.elements('motion_id_list.motion_id')
.map(item => readString(item, ''))
);
}
return values.filter(
(value, index) => !!value && values.indexOf(value) === index
);
}

async function findProfile(refid: string): Promise<ProfileDocument> {
return (
(await DB.FindOne<ProfileDocument>(refid, { collection: 'profile' })) ||
createProfile(refid, 'PLAYER')
);
}

export const saveMotionData: EPR = async (info, data, send) => {
const reader = $(data);
const refid = readString(reader, 'userid.ref_id');
if (!refid) {
console.error('[Dance Around] save_motiondata is missing userid.ref_id');
return send.deny();
}

const motionReader = reader.element('motiondata');
const full = motionReader.buffer('motion_full', Buffer.alloc(0));
const preview = motionReader.buffer('motion_prev', Buffer.alloc(0));
const entry: MotionEntry = {
motionId: makeUuid(),
active: true,
publishSetting: readNumber(motionReader, 'publish_setting'),
playCount: 1,
totalGv: 0,
playResult: readStageResult(motionReader),
playDate: readNumber(motionReader, 'play_date', nowMs()),
locationId: readString(motionReader, 'loc_id'),
shopName: readString(motionReader, 'shopname'),
avatar: readAvatar(motionReader, 'avatar'),
motionFullBase64: full.toString('base64'),
motionPreviewBase64: preview.toString('base64'),
createdAt: nowMs(),
};

const configuredLimit = Number(U.GetConfig('motion_history_limit'));
const historyLimit = Number.isFinite(configuredLimit)
? Math.max(0, Math.min(100, Math.floor(configuredLimit)))
: 20;
if (historyLimit > 0) {
const document = (await DB.FindOne<MotionDocument>(refid, {
collection: 'motions',
})) || {
collection: 'motions' as const,
schemaVersion: 1,
entries: [],
updatedAt: 0,
};
document.entries = [entry, ...(document.entries || [])].slice(
0,
historyLimit
);
document.updatedAt = nowMs();
await DB.Upsert<MotionDocument>(
refid,
{ collection: 'motions' },
{ $set: document }
);
}

console.log(
`[Dance Around] accepted motion ${entry.motionId} (${full.length}/${preview.length} bytes)`
);
return send.object(
{
motion_id: K.ITEM('str', entry.motionId),
},
{ status: 0 }
);
};

export const getMotionInfoList: EPR = async (info, data, send) => {
const reader = $(data);
const refid = readString(reader, 'userid.ref_id');
if (!refid) {
console.error('[Dance Around] get_motioninfolist is missing userid.ref_id');
return send.deny();
}

const document = await DB.FindOne<MotionDocument>(refid, {
collection: 'motions',
});
const entries = document ? document.entries || [] : [];
return send.object(
{
data: entries.map(motionInfoResponse),
},
{ status: 0 }
);
};

export const checkPlayableMotionData: EPR = async (info, data, send) => {
const reader = $(data);
const requested = readMotionIds(reader);
const refid = readString(reader, 'ref_id');
const document = refid
? await DB.FindOne<MotionDocument>(refid, { collection: 'motions' })
: null;
const available = new Set(
(document ? document.entries || [] : []).map(entry => entry.motionId)
);
return send.object(
{
data: requested.map(motionId => ({
motion_id: K.ITEM('str', motionId),
is_playable: K.ITEM('bool', available.has(motionId)),
})),
},
{ status: 0 }
);
};

interface LocatedMotion {
refid: string;
entry: MotionEntry;
profile: ProfileDocument;
}

async function locateMotions(motionIds: string[]): Promise<LocatedMotion[]> {
if (!motionIds.length) {
return [];
}
const wanted = new Set(motionIds);
const result: LocatedMotion[] = [];
const documents = await DB.Find<MotionDocument>(null, {
collection: 'motions',
});
for (const document of documents) {
const refid = (document as any).__refid as string;
if (!refid) {
continue;
}
const profile = await findProfile(refid);
for (const entry of document.entries || []) {
if (wanted.has(entry.motionId)) {
result.push({ refid, entry, profile });
}
}
}
return result;
}

export const getMotionData: EPR = async (info, data, send) => {
const requested = readMotionIds($(data));
const located = await locateMotions(requested);
return send.object(
{
data: located.map(item => ({
info: motionInfoResponse(item.entry),
userid: {
dancer_id: K.ITEM('str', item.profile.dancerId),
},
profile: {
name: K.ITEM('str', item.profile.name),
},
privacy: {
publish_setting: K.ITEM('s32', item.profile.privacy.publishSetting),
},
dancer_grade: dancerGradeResponse(item.profile.dancerGrade),
avatar: avatarResponse(item.entry.avatar),
motiondata: {
motion_full: K.ITEM(
'bin',
Buffer.from(item.entry.motionFullBase64, 'base64')
),
motion_prev: K.ITEM(
'bin',
Buffer.from(item.entry.motionPreviewBase64, 'base64')
),
},
})),
},
{ status: 0 }
);
};
Loading